diff options
Diffstat (limited to 'NorthstarDLL/shared')
-rw-r--r-- | NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp | 458 | ||||
-rw-r--r-- | NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp | 79 | ||||
-rw-r--r-- | NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp | 200 | ||||
-rw-r--r-- | NorthstarDLL/shared/exploit_fixes/ns_limits.cpp | 298 | ||||
-rw-r--r-- | NorthstarDLL/shared/exploit_fixes/ns_limits.h | 51 | ||||
-rw-r--r-- | NorthstarDLL/shared/keyvalues.cpp | 1316 | ||||
-rw-r--r-- | NorthstarDLL/shared/keyvalues.h | 134 | ||||
-rw-r--r-- | NorthstarDLL/shared/maxplayers.cpp | 645 | ||||
-rw-r--r-- | NorthstarDLL/shared/maxplayers.h | 7 | ||||
-rw-r--r-- | NorthstarDLL/shared/misccommands.cpp | 314 | ||||
-rw-r--r-- | NorthstarDLL/shared/misccommands.h | 3 | ||||
-rw-r--r-- | NorthstarDLL/shared/playlist.cpp | 130 | ||||
-rw-r--r-- | NorthstarDLL/shared/playlist.h | 10 |
13 files changed, 3645 insertions, 0 deletions
diff --git a/NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp b/NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp new file mode 100644 index 00000000..e4430fd4 --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp @@ -0,0 +1,458 @@ +#include "pch.h" +#include "core/convar/cvar.h" +#include "ns_limits.h" +#include "dedicated/dedicated.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "core/math/vector.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_ns_exploitfixes_log; +ConVar* Cvar_ns_should_log_all_clientcommands; + +ConVar* Cvar_sv_cheats; + +#define BLOCKED_INFO(s) \ + ( \ + [=]() -> bool \ + { \ + if (Cvar_ns_exploitfixes_log->GetBool()) \ + { \ + std::stringstream stream; \ + stream << "ExploitFixes.cpp: " << BLOCK_PREFIX << s; \ + spdlog::error(stream.str()); \ + } \ + return false; \ + }()) + +// block bad netmessages +// Servers can literally request a screenshot from any client, yeah no +// clang-format off +AUTOHOOK(CLC_Screenshot_WriteToBuffer, engine.dll + 0x22AF20, +bool, __fastcall, (void* thisptr, void* buffer)) // 48 89 5C 24 ? 57 48 83 EC 20 8B 42 10 +// clang-format on +{ + return false; +} + +// clang-format off +AUTOHOOK(CLC_Screenshot_ReadFromBuffer, engine.dll + 0x221F00, +bool, __fastcall, (void* thisptr, void* buffer)) // 48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B DA 48 8B 52 38 +// clang-format on +{ + return false; +} + +// This is unused ingame and a big client=>server=>client exploit vector +// clang-format off +AUTOHOOK(Base_CmdKeyValues_ReadFromBuffer, engine.dll + 0x220040, +bool, __fastcall, (void* thisptr, void* buffer)) // 40 55 48 81 EC ? ? ? ? 48 8D 6C 24 ? 48 89 5D 70 +// clang-format on +{ + return false; +} + +// clang-format off +AUTOHOOK(CClient_ProcessSetConVar, engine.dll + 0x75CF0, +bool, __fastcall, (void* pMsg)) // 48 8B D1 48 8B 49 18 48 8B 01 48 FF 60 10 +// clang-format on +{ + + constexpr int ENTRY_STR_LEN = 260; + struct SetConVarEntry + { + char name[ENTRY_STR_LEN]; + char val[ENTRY_STR_LEN]; + }; + + struct NET_SetConVar + { + void* vtable; + void* unk1; + void* unk2; + void* m_pMessageHandler; + SetConVarEntry* m_ConVars; // convar entry array + void* unk5; // these 2 unks are just vector capacity or whatever + void* unk6; + int m_ConVars_count; // amount of cvar entries in array (this will not be out of bounds) + }; + + auto msg = (NET_SetConVar*)pMsg; + bool bIsServerFrame = Tier0::ThreadInServerFrameThread(); + + std::string BLOCK_PREFIX = + std::string {"NET_SetConVar ("} + (bIsServerFrame ? "server" : "client") + "): Blocked dangerous/invalid msg: "; + + if (bIsServerFrame) + { + constexpr int SETCONVAR_SANITY_AMOUNT_LIMIT = 69; + if (msg->m_ConVars_count < 1 || msg->m_ConVars_count > SETCONVAR_SANITY_AMOUNT_LIMIT) + { + return BLOCKED_INFO("Invalid m_ConVars_count (" << msg->m_ConVars_count << ")"); + } + } + + for (int i = 0; i < msg->m_ConVars_count; i++) + { + auto entry = msg->m_ConVars + i; + + // Safety check for memory access + if (MemoryAddress(entry).IsMemoryReadable(sizeof(*entry))) + { + // Find null terminators + bool nameValid = false, valValid = false; + for (int i = 0; i < ENTRY_STR_LEN; i++) + { + if (!entry->name[i]) + nameValid = true; + if (!entry->val[i]) + valValid = true; + } + + if (!nameValid || !valValid) + return BLOCKED_INFO("Missing null terminators"); + + ConVar* pVar = R2::g_pCVar->FindVar(entry->name); + + if (pVar) + { + memcpy( + entry->name, + pVar->m_ConCommandBase.m_pszName, + strlen(pVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case + + int iFlags = bIsServerFrame ? FCVAR_USERINFO : FCVAR_REPLICATED; + if (!pVar->IsFlagSet(iFlags)) + return BLOCKED_INFO( + "Invalid flags (" << std::hex << "0x" << pVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name); + } + } + else + { + return BLOCKED_INFO("Unreadable memory at " << (void*)entry); // Not risking that one, they all gotta be readable + } + } + + return CClient_ProcessSetConVar(msg); +} + +// prevent invalid user CMDs +// clang-format off +AUTOHOOK(CClient_ProcessUsercmds, engine.dll + 0x1040F0, +bool, __fastcall, (void* thisptr, void* pMsg)) // 40 55 56 48 83 EC 58 +// clang-format on +{ + struct CLC_Move + { + BYTE gap0[24]; + void* m_pMessageHandler; + int m_nBackupCommands; + int m_nNewCommands; + int m_nLength; + // bf_read m_DataIn; + // bf_write m_DataOut; + }; + + auto msg = (CLC_Move*)pMsg; + + const char* BLOCK_PREFIX = "ProcessUserCmds: "; + + if (msg->m_nBackupCommands < 0) + { + return BLOCKED_INFO("Invalid m_nBackupCommands (" << msg->m_nBackupCommands << ")"); + } + + if (msg->m_nNewCommands < 0) + { + return BLOCKED_INFO("Invalid m_nNewCommands (" << msg->m_nNewCommands << ")"); + } + + if (msg->m_nLength <= 0) + return BLOCKED_INFO("Invalid message length (" << msg->m_nLength << ")"); + + return CClient_ProcessUsercmds(thisptr, pMsg); +} + +// clang-format off +AUTOHOOK(ReadUsercmd, server.dll + 0x2603F0, +void, __fastcall, (void* buf, void* pCmd_move, void* pCmd_from)) // 4C 89 44 24 ? 53 55 56 57 +// clang-format on +{ + // Let normal usercmd read happen first, it's safe + ReadUsercmd(buf, pCmd_move, pCmd_from); + + // Now let's make sure the CMD we read isnt messed up to prevent numerous exploits (including server crashing) + struct alignas(4) SV_CUserCmd + { + DWORD command_number; + DWORD tick_count; + float command_time; + Vector3 worldViewAngles; + BYTE gap18[4]; + Vector3 localViewAngles; + Vector3 attackangles; + Vector3 move; + DWORD buttons; + BYTE impulse; + short weaponselect; + DWORD meleetarget; + BYTE gap4C[24]; + char headoffset; + BYTE gap65[11]; + Vector3 cameraPos; + Vector3 cameraAngles; + BYTE gap88[4]; + int tickSomething; + DWORD dword90; + DWORD predictedServerEventAck; + DWORD dword98; + float frameTime; + }; + + auto cmd = (SV_CUserCmd*)pCmd_move; + auto fromCmd = (SV_CUserCmd*)pCmd_from; + + std::string BLOCK_PREFIX = + "ReadUsercmd (command_number delta: " + std::to_string(cmd->command_number - fromCmd->command_number) + "): "; + + // fix invalid player angles + cmd->worldViewAngles.MakeValid(); + cmd->attackangles.MakeValid(); + cmd->localViewAngles.MakeValid(); + + // Fix invalid camera angles + cmd->cameraPos.MakeValid(); + cmd->cameraAngles.MakeValid(); + + // Fix invaid movement vector + cmd->move.MakeValid(); + + if (cmd->frameTime <= 0 || cmd->tick_count == 0 || cmd->command_time <= 0) + { + BLOCKED_INFO( + "Bogus cmd timing (tick_count: " << cmd->tick_count << ", frameTime: " << cmd->frameTime + << ", commandTime : " << cmd->command_time << ")"); + goto INVALID_CMD; // No simulation of bogus-timed cmds + } + + return; + +INVALID_CMD: + + // Fix any gameplay-affecting cmd properties + // NOTE: Currently tickcount/frametime is set to 0, this ~shouldn't~ cause any problems + cmd->worldViewAngles = cmd->localViewAngles = cmd->attackangles = cmd->cameraAngles = {0, 0, 0}; + cmd->tick_count = cmd->frameTime = 0; + cmd->move = cmd->cameraPos = {0, 0, 0}; + cmd->buttons = 0; + cmd->meleetarget = 0; +} + +// ensure that GetLocalBaseClient().m_bRestrictServerCommands is set correctly, which the return value of this function controls +// this is IsValveMod in source, but we're making it IsRespawnMod now since valve didn't make this one +// clang-format off +AUTOHOOK(IsRespawnMod, engine.dll + 0x1C6360, +bool, __fastcall, (const char* pModName)) // 48 83 EC 28 48 8B 0D ? ? ? ? 48 8D 15 ? ? ? ? E8 ? ? ? ? 85 C0 74 63 +// clang-format on +{ + // somewhat temp, store the modname here, since we don't have a proper ptr in engine to it rn + int iSize = strlen(pModName); + R2::g_pModName = new char[iSize + 1]; + strcpy(R2::g_pModName, pModName); + + return (!strcmp("r2", pModName) || !strcmp("r1", pModName)) && !Tier0::CommandLine()->CheckParm("-norestrictservercommands"); +} + +// ratelimit stringcmds, and prevent remote clients from calling commands that they shouldn't +bool (*CCommand__Tokenize)(CCommand& self, const char* pCommandString, R2::cmd_source_t commandSource); + +// clang-format off +AUTOHOOK(CGameClient__ExecuteStringCommand, engine.dll + 0x1022E0, +bool, __fastcall, (R2::CBaseClient* self, uint32_t unknown, const char* pCommandString)) +// clang-format on +{ + if (Cvar_ns_should_log_all_clientcommands->GetBool()) + spdlog::info("player {} (UID: {}) sent command: \"{}\"", self->m_Name, self->m_UID, pCommandString); + + if (!g_pServerLimits->CheckStringCommandLimits(self)) + { + R2::CBaseClient__Disconnect(self, 1, "Sent too many stringcmd commands"); + return false; + } + + // verify the command we're trying to execute is FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS, if it's a concommand + char* commandBuf[1040]; // assumedly this is the size of CCommand since we don't have an actual constructor + memset(commandBuf, 0, sizeof(commandBuf)); + CCommand tempCommand = *(CCommand*)&commandBuf; + + if (!CCommand__Tokenize(tempCommand, pCommandString, R2::cmd_source_t::kCommandSrcCode) || !tempCommand.ArgC()) + return false; + + ConCommand* command = R2::g_pCVar->FindCommand(tempCommand.Arg(0)); + + // if the command doesn't exist pass it on to ExecuteStringCommand for script clientcommands and stuff + if (command && !command->IsFlagSet(FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS)) + { + // ensure FCVAR_GAMEDLL concommands without FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS can't be executed by remote clients + if (IsDedicatedServer()) + return false; + + if (strcmp(self->m_UID, R2::g_pLocalPlayerUserID)) + return false; + } + + // check for and block abusable legacy portal 2 commands + // these aren't actually concommands weirdly enough, they seem to just be hardcoded + if (!Cvar_sv_cheats->GetBool()) + { + constexpr const char* blockedCommands[] = { + "emit", // Sound-playing exploit (likely for Portal 2 coop devs testing splitscreen sound or something) + + // These both execute a command for every single entity for some reason, nice one valve + "pre_go_to_hub", + "pre_go_to_calibration", + + "end_movie", // Calls "__MovieFinished" script function, not sure exactly what this does but it certainly isn't needed + "load_recent_checkpoint" // This is the instant-respawn exploit, literally just calls RespawnPlayer() + }; + + int iCmdLength = strlen(tempCommand.Arg(0)); + + bool bIsBadCommand = false; + for (auto& blockedCommand : blockedCommands) + { + if (iCmdLength != strlen(blockedCommand)) + continue; + + for (int i = 0; tempCommand.Arg(0)[i]; i++) + if (tolower(tempCommand.Arg(0)[i]) != blockedCommand[i]) + goto NEXT_COMMAND; // break out of this loop, then go to next command + + // this is a command we need to block + return false; + NEXT_COMMAND:; + } + } + + return CGameClient__ExecuteStringCommand(self, unknown, pCommandString); +} + +// prevent clients from crashing servers through overflowing CNetworkStringTableContainer::WriteBaselines +bool bWasWritingStringTableSuccessful; + +// clang-format off +AUTOHOOK(CBaseClient__SendServerInfo, engine.dll + 0x104FB0, +void, __fastcall, (void* self)) +// clang-format on +{ + bWasWritingStringTableSuccessful = true; + CBaseClient__SendServerInfo(self); + if (!bWasWritingStringTableSuccessful) + R2::CBaseClient__Disconnect( + self, 1, "Overflowed CNetworkStringTableContainer::WriteBaselines, try restarting your client and reconnecting"); +} + +// return null when GetEntByIndex is passed an index >= 0x4000 +// this is called from exactly 1 script clientcommand that can be given an arbitrary index, and going above 0x4000 crashes +// clang-format off +AUTOHOOK(GetEntByIndex, server.dll + 0x2A8A50, +void*, __fastcall, (int i)) +// clang-format on +{ + const int MAX_ENT_IDX = 0x4000; + + if (i >= MAX_ENT_IDX) + { + spdlog::warn("GetEntByIndex {} is out of bounds (max {})", i, MAX_ENT_IDX); + return nullptr; + } + + return GetEntByIndex(i); +} +// clang-format off +AUTOHOOK(CL_CopyExistingEntity, engine.dll + 0x6F940, +bool, __fastcall, (void* a1)) +// clang-format on +{ + struct CEntityReadInfo + { + BYTE gap[40]; + int nNewEntity; + }; + + CEntityReadInfo* pReadInfo = (CEntityReadInfo*)a1; + if (pReadInfo->nNewEntity >= 0x1000 || pReadInfo->nNewEntity < 0) + { + // Value isn't sanitized in release builds for + // every game powered by the Source Engine 1 + // causing read/write outside of array bounds. + // This defect has let to the achievement of a + // full-chain RCE exploit. We hook and perform + // sanity checks for the value of m_nNewEntity + // here to prevent this behavior from happening. + return false; + } + + return CL_CopyExistingEntity(a1); +} + +ON_DLL_LOAD("engine.dll", EngineExploitFixes, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + CCommand__Tokenize = module.Offset(0x418380).As<bool (*)(CCommand&, const char*, R2::cmd_source_t)>(); + + // allow client/ui to run clientcommands despite restricting servercommands + module.Offset(0x4FB65).Patch("EB 11"); + module.Offset(0x4FBAC).Patch("EB 16"); + + // patch to set bWasWritingStringTableSuccessful in CNetworkStringTableContainer::WriteBaselines if it fails + { + MemoryAddress writeAddress(&bWasWritingStringTableSuccessful - module.Offset(0x234EDC).m_nAddress); + + MemoryAddress addr = module.Offset(0x234ED2); + addr.Patch("C7 05"); + addr.Offset(2).Patch((BYTE*)&writeAddress, sizeof(writeAddress)); + + addr.Offset(6).Patch("00 00 00 00"); + + addr.Offset(10).NOP(5); + } +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerExploitFixes, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) + + // ret at the start of CServerGameClients::ClientCommandKeyValues as it has no benefit and is forwarded to client (i.e. security issue) + // this prevents the attack vector of client=>server=>client, however server=>client also has clientside patches + module.Offset(0x153920).Patch("C3"); + + // Dumb ANTITAMPER patches (they negatively impact performance and security) + constexpr const char* ANTITAMPER_EXPORTS[] = { + "ANTITAMPER_SPOTCHECK_CODEMARKER", + "ANTITAMPER_TESTVALUE_CODEMARKER", + "ANTITAMPER_TRIGGER_CODEMARKER", + }; + + // Prevent these from actually doing anything + for (auto exportName : ANTITAMPER_EXPORTS) + { + MemoryAddress exportAddr = module.GetExport(exportName); + if (exportAddr) + { + // Just return, none of them have any args or are userpurge + exportAddr.Patch("C3"); + spdlog::info("Patched AntiTamper function export \"{}\"", exportName); + } + } + + Cvar_ns_exploitfixes_log = + new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever ExploitFixes.cpp blocks/corrects something"); + Cvar_ns_should_log_all_clientcommands = + new ConVar("ns_should_log_all_clientcommands", "0", FCVAR_NONE, "Whether to log all clientcommands"); + + Cvar_sv_cheats = R2::g_pCVar->FindVar("sv_cheats"); +} diff --git a/NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp b/NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp new file mode 100644 index 00000000..4205133a --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp @@ -0,0 +1,79 @@ +#include "pch.h" + +AUTOHOOK_INIT() + +static constexpr int LZSS_LOOKSHIFT = 4; + +struct lzss_header_t +{ + unsigned int id; + unsigned int actualSize; +}; + +// Rewrite of CLZSS::SafeUncompress to fix a vulnerability where malicious compressed payloads could cause the decompressor to try to read +// out of the bounds of the output buffer. +// clang-format off +AUTOHOOK(CLZSS__SafeDecompress, engine.dll + 0x432A10, +unsigned int, __fastcall, (void* self, const unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize)) +// clang-format on +{ + unsigned int totalBytes = 0; + int getCmdByte = 0; + int cmdByte = 0; + + lzss_header_t header = *(lzss_header_t*)pInput; + + if (!pInput || !header.actualSize || header.id != 0x53535A4C || header.actualSize > unBufSize) + return 0; + + pInput += sizeof(lzss_header_t); + + for (;;) + { + if (!getCmdByte) + cmdByte = *pInput++; + + getCmdByte = (getCmdByte + 1) & 0x07; + + if (cmdByte & 0x01) + { + int position = *pInput++ << LZSS_LOOKSHIFT; + position |= (*pInput >> LZSS_LOOKSHIFT); + position += 1; + int count = (*pInput++ & 0x0F) + 1; + if (count == 1) + break; + + // Ensure reference chunk exists entirely within our buffer + if (position > totalBytes) + return 0; + + totalBytes += count; + if (totalBytes > unBufSize) + return 0; + + unsigned char* pSource = pOutput - position; + for (int i = 0; i < count; i++) + *pOutput++ = *pSource++; + } + else + { + totalBytes++; + if (totalBytes > unBufSize) + return 0; + + *pOutput++ = *pInput++; + } + cmdByte = cmdByte >> 1; + } + + if (totalBytes != header.actualSize) + return 0; + + return totalBytes; +} + +ON_DLL_LOAD("engine.dll", ExploitFixes_LZSS, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp b/NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp new file mode 100644 index 00000000..e2510765 --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp @@ -0,0 +1,200 @@ +#include "pch.h" + +AUTOHOOK_INIT() + +INT64(__fastcall* sub_F1320)(DWORD a1, char* a2); + +// Reimplementation of an exploitable UTF decoding function in titanfall +bool __fastcall CheckUTF8Valid(INT64* a1, DWORD* a2, char* strData) +{ + DWORD v3; // eax + char* v4; // rbx + char v5; // si + char* _strData; // rdi + char* v7; // rbp + char v11; // al + DWORD v12; // er9 + DWORD v13; // ecx + DWORD v14; // edx + DWORD v15; // er8 + int v16; // eax + DWORD v17; // er9 + int v18; // eax + DWORD v19; // er9 + DWORD v20; // ecx + int v21; // eax + int v22; // er9 + DWORD v23; // edx + int v24; // eax + int v25; // er9 + DWORD v26; // er9 + DWORD v27; // er10 + DWORD v28; // ecx + DWORD v29; // edx + DWORD v30; // er8 + int v31; // eax + DWORD v32; // er10 + int v33; // eax + DWORD v34; // er10 + DWORD v35; // ecx + int v36; // eax + int v37; // er10 + DWORD v38; // edx + int v39; // eax + int v40; // er10 + DWORD v41; // er10 + INT64 v43; // r8 + INT64 v44; // rdx + INT64 v45; // rcx + INT64 v46; // rax + INT64 v47; // rax + char v48; // al + INT64 v49; // r8 + INT64 v50; // rdx + INT64 v51; // rcx + INT64 v52; // rax + INT64 v53; // rax + + v3 = a2[2]; + v4 = (char*)(a1[1] + *a2); + v5 = 0; + _strData = strData; + v7 = &v4[*((UINT16*)a2 + 2)]; + if (v3 >= 2) + { + ++v4; + --v7; + if (v3 != 2) + { + while (1) + { + if (!MemoryAddress(v4).IsMemoryReadable(1)) + return false; // INVALID + + v11 = *v4++; // crash potential + if (v11 != 92) + goto LABEL_6; + v11 = *v4++; + if (v11 == 110) + break; + switch (v11) + { + case 't': + v11 = 9; + goto LABEL_6; + case 'r': + v11 = 13; + goto LABEL_6; + case 'b': + v11 = 8; + goto LABEL_6; + case 'f': + v11 = 12; + goto LABEL_6; + } + if (v11 != 117) + goto LABEL_6; + v12 = *v4 | 0x20; + v13 = v4[1] | 0x20; + v14 = v4[2] | 0x20; + v15 = v4[3] | 0x20; + v16 = 87; + if (v12 <= 0x39) + v16 = 48; + v17 = v12 - v16; + v18 = 87; + v19 = v17 << 12; + if (v13 <= 0x39) + v18 = 48; + v20 = v13 - v18; + v21 = 87; + v22 = (v20 << 8) | v19; + if (v14 <= 0x39) + v21 = 48; + v23 = v14 - v21; + v24 = 87; + v25 = (16 * v23) | v22; + if (v15 <= 0x39) + v24 = 48; + v4 += 4; + v26 = (v15 - v24) | v25; + if (v26 - 55296 <= 0x7FF) + { + if (v26 >= 0xDC00) + return true; + if (*v4 != 92 || v4[1] != 117) + return true; + + v27 = v4[2] | 0x20; + v28 = v4[3] | 0x20; + v29 = v4[4] | 0x20; + v30 = v4[5] | 0x20; + v31 = 87; + if (v27 <= 0x39) + v31 = 48; + v32 = v27 - v31; + v33 = 87; + v34 = v32 << 12; + if (v28 <= 0x39) + v33 = 48; + v35 = v28 - v33; + v36 = 87; + v37 = (v35 << 8) | v34; + if (v29 <= 0x39) + v36 = 48; + v38 = v29 - v36; + v39 = 87; + v40 = (16 * v38) | v37; + if (v30 <= 0x39) + v39 = 48; + v4 += 6; + v41 = ((v30 - v39) | v40) - 56320; + if (v41 > 0x3FF) + return true; + v26 = v41 | ((v26 - 55296) << 10); + } + _strData += (DWORD)sub_F1320(v26, _strData); + LABEL_7: + if (v4 == v7) + goto LABEL_48; + } + v11 = 10; + LABEL_6: + v5 |= v11; + *_strData++ = v11; + goto LABEL_7; + } + } +LABEL_48: + return true; +} + +// prevent utf8 parser from crashing when provided bad data, which can be sent through user-controlled openinvites +// clang-format off +AUTOHOOK(Rson_ParseUTF8, engine.dll + 0xEF670, +bool, __fastcall, (INT64* a1, DWORD* a2, char* strData)) // 48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 41 54 41 55 41 56 41 57 48 83 EC 20 8B 1A +// clang-format on +{ + static void* targetRetAddr = CModule("engine.dll").FindPattern("84 C0 75 2C 49 8B 16"); + + // only call if we're parsing utf8 data from the network (i.e. communities), otherwise we get perf issues + void* pReturnAddress = +#ifdef _MSC_VER + _ReturnAddress() +#else + __builtin_return_address(0) +#endif + ; + + if (pReturnAddress == targetRetAddr && !CheckUTF8Valid(a1, a2, strData)) + return false; + + return Rson_ParseUTF8(a1, a2, strData); +} + +ON_DLL_LOAD("engine.dll", EngineExploitFixes_UTF8Parser, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + sub_F1320 = module.FindPattern("83 F9 7F 77 08 88 0A").As<INT64(__fastcall*)(DWORD, char*)>(); +} diff --git a/NorthstarDLL/shared/exploit_fixes/ns_limits.cpp b/NorthstarDLL/shared/exploit_fixes/ns_limits.cpp new file mode 100644 index 00000000..49f80bab --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/ns_limits.cpp @@ -0,0 +1,298 @@ +#include "pch.h" +#include "ns_limits.h" +#include "engine/hoststate.h" +#include "client/r2client.h" +#include "engine/r2engine.h" +#include "server/r2server.h" +#include "shared/maxplayers.h" +#include "core/tier0.h" +#include "core/math/vector.h" +#include "server/auth/serverauthentication.h" + +AUTOHOOK_INIT() + +ServerLimitsManager* g_pServerLimits; + +// todo: make this work on higher timescales, also possibly disable when sv_cheats is set +void ServerLimitsManager::RunFrame(double flCurrentTime, float flFrameTime) +{ + if (Cvar_sv_antispeedhack_enable->GetBool()) + { + // for each player, set their usercmd processing budget for the frame to the last frametime for the server + for (int i = 0; i < R2::GetMaxPlayers(); i++) + { + R2::CBaseClient* player = &R2::g_pClientArray[i]; + + if (m_PlayerLimitData.find(player) != m_PlayerLimitData.end()) + { + PlayerLimitData* pLimitData = &g_pServerLimits->m_PlayerLimitData[player]; + if (pLimitData->flFrameUserCmdBudget < 0.016666667 * Cvar_sv_antispeedhack_maxtickbudget->GetFloat()) + pLimitData->flFrameUserCmdBudget += + fmax(flFrameTime, 0.016666667) * g_pServerLimits->Cvar_sv_antispeedhack_budgetincreasemultiplier->GetFloat(); + } + } + } +} + +void ServerLimitsManager::AddPlayer(R2::CBaseClient* player) +{ + PlayerLimitData limitData; + limitData.flFrameUserCmdBudget = 0.016666667 * Cvar_sv_antispeedhack_maxtickbudget->GetFloat(); + + m_PlayerLimitData.insert(std::make_pair(player, limitData)); +} + +void ServerLimitsManager::RemovePlayer(R2::CBaseClient* player) +{ + if (m_PlayerLimitData.find(player) != m_PlayerLimitData.end()) + m_PlayerLimitData.erase(player); +} + +bool ServerLimitsManager::CheckStringCommandLimits(R2::CBaseClient* player) +{ + if (CVar_sv_quota_stringcmdspersecond->GetInt() != -1) + { + // note: this isn't super perfect, legit clients can trigger it in lobby if they try, mostly good enough tho imo + if (Tier0::Plat_FloatTime() - m_PlayerLimitData[player].lastClientCommandQuotaStart >= 1.0) + { + // reset quota + m_PlayerLimitData[player].lastClientCommandQuotaStart = Tier0::Plat_FloatTime(); + m_PlayerLimitData[player].numClientCommandsInQuota = 0; + } + + m_PlayerLimitData[player].numClientCommandsInQuota++; + if (m_PlayerLimitData[player].numClientCommandsInQuota > CVar_sv_quota_stringcmdspersecond->GetInt()) + { + // too many stringcmds, dc player + return false; + } + } + + return true; +} + +bool ServerLimitsManager::CheckChatLimits(R2::CBaseClient* player) +{ + if (Tier0::Plat_FloatTime() - m_PlayerLimitData[player].lastSayTextLimitStart >= 1.0) + { + m_PlayerLimitData[player].lastSayTextLimitStart = Tier0::Plat_FloatTime(); + m_PlayerLimitData[player].sayTextLimitCount = 0; + } + + if (m_PlayerLimitData[player].sayTextLimitCount >= Cvar_sv_max_chat_messages_per_sec->GetInt()) + return false; + + m_PlayerLimitData[player].sayTextLimitCount++; + return true; +} + +// clang-format off +AUTOHOOK(CNetChan__ProcessMessages, engine.dll + 0x2140A0, +char, __fastcall, (void* self, void* buf)) +// clang-format on +{ + enum eNetChanLimitMode + { + NETCHANLIMIT_WARN, + NETCHANLIMIT_KICK + }; + + double startTime = Tier0::Plat_FloatTime(); + char ret = CNetChan__ProcessMessages(self, buf); + + // check processing limits, unless we're in a level transition + if (R2::g_pHostState->m_iCurrentState == R2::HostState_t::HS_RUN && Tier0::ThreadInServerFrameThread()) + { + // player that sent the message + R2::CBaseClient* sender = *(R2::CBaseClient**)((char*)self + 368); + + // if no sender, return + // relatively certain this is fine? + if (!sender || !g_pServerLimits->m_PlayerLimitData.count(sender)) + return ret; + + // reset every second + if (startTime - g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart >= 1.0 || + g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart == -1.0) + { + g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart = startTime; + g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime = 0.0; + } + g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime += (Tier0::Plat_FloatTime() * 1000) - (startTime * 1000); + + if (g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime >= + g_pServerLimits->Cvar_net_chan_limit_msec_per_sec->GetInt()) + { + spdlog::warn( + "Client {} hit netchan processing limit with {}ms of processing time this second (max is {})", + (char*)sender + 0x16, + g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime, + g_pServerLimits->Cvar_net_chan_limit_msec_per_sec->GetInt()); + + // never kick local player + if (g_pServerLimits->Cvar_net_chan_limit_mode->GetInt() != NETCHANLIMIT_WARN && strcmp(R2::g_pLocalPlayerUserID, sender->m_UID)) + { + R2::CBaseClient__Disconnect(sender, 1, "Exceeded net channel processing limit"); + return false; + } + } + } + + return ret; +} + +// clang-format off +AUTOHOOK(ProcessConnectionlessPacket, engine.dll + 0x117800, +bool, , (void* a1, R2::netpacket_t* packet)) +// clang-format on +{ + static const ConVar* Cvar_net_data_block_enabled = R2::g_pCVar->FindVar("net_data_block_enabled"); + + // don't ratelimit datablock packets as long as datablock is enabled + if (packet->adr.type == R2::NA_IP && + (!(packet->data[4] == 'N' && Cvar_net_data_block_enabled->GetBool()) || !Cvar_net_data_block_enabled->GetBool())) + { + // bad lookup: optimise later tm + UnconnectedPlayerLimitData* sendData = nullptr; + for (UnconnectedPlayerLimitData& foundSendData : g_pServerLimits->m_UnconnectedPlayerLimitData) + { + if (!memcmp(packet->adr.ip, foundSendData.ip, 16)) + { + sendData = &foundSendData; + break; + } + } + + if (!sendData) + { + sendData = &g_pServerLimits->m_UnconnectedPlayerLimitData.emplace_back(); + memcpy(sendData->ip, packet->adr.ip, 16); + } + + if (Tier0::Plat_FloatTime() < sendData->timeoutEnd) + return false; + + if (Tier0::Plat_FloatTime() - sendData->lastQuotaStart >= 1.0) + { + sendData->lastQuotaStart = Tier0::Plat_FloatTime(); + sendData->packetCount = 0; + } + + sendData->packetCount++; + + if (sendData->packetCount >= g_pServerLimits->Cvar_sv_querylimit_per_sec->GetInt()) + { + spdlog::warn( + "Client went over connectionless ratelimit of {} per sec with packet of type {}", + g_pServerLimits->Cvar_sv_querylimit_per_sec->GetInt(), + packet->data[4]); + + // timeout for a minute + sendData->timeoutEnd = Tier0::Plat_FloatTime() + 60.0; + return false; + } + } + + return ProcessConnectionlessPacket(a1, packet); +} + +// this is weird and i'm not sure if it's correct, so not using for now +/*AUTOHOOK(CBasePlayer__PhysicsSimulate, server.dll + 0x5A6E50, bool, __fastcall, (void* self, int a2, char a3)) +{ + spdlog::info("CBasePlayer::PhysicsSimulate"); + return CBasePlayer__PhysicsSimulate(self, a2, a3); +}*/ + +struct alignas(4) SV_CUserCmd +{ + DWORD command_number; + DWORD tick_count; + float command_time; + Vector3 worldViewAngles; + BYTE gap18[4]; + Vector3 localViewAngles; + Vector3 attackangles; + Vector3 move; + DWORD buttons; + BYTE impulse; + short weaponselect; + DWORD meleetarget; + BYTE gap4C[24]; + char headoffset; + BYTE gap65[11]; + Vector3 cameraPos; + Vector3 cameraAngles; + BYTE gap88[4]; + int tickSomething; + DWORD dword90; + DWORD predictedServerEventAck; + DWORD dword98; + float frameTime; +}; + +// clang-format off +AUTOHOOK(CPlayerMove__RunCommand, server.dll + 0x5B8100, +void, __fastcall, (void* self, R2::CBasePlayer* player, SV_CUserCmd* pUserCmd, uint64_t a4)) +// clang-format on +{ + if (g_pServerLimits->Cvar_sv_antispeedhack_enable->GetBool()) + { + R2::CBaseClient* pClient = &R2::g_pClientArray[player->m_nPlayerIndex - 1]; + + if (g_pServerLimits->m_PlayerLimitData.find(pClient) != g_pServerLimits->m_PlayerLimitData.end()) + { + PlayerLimitData* pLimitData = &g_pServerLimits->m_PlayerLimitData[pClient]; + + pLimitData->flFrameUserCmdBudget = fmax(0.0, pLimitData->flFrameUserCmdBudget - pUserCmd->frameTime); + + if (pLimitData->flFrameUserCmdBudget <= 0.0) + { + spdlog::warn("player {} went over usercmd budget ({})", pClient->m_Name, pLimitData->flFrameUserCmdBudget); + return; + } + // else + // spdlog::info("{}: {}", pClient->m_Name, pLimitData->flFrameUserCmdBudget); + } + } + + CPlayerMove__RunCommand(self, player, pUserCmd, a4); +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerLimits, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + g_pServerLimits = new ServerLimitsManager; + + g_pServerLimits->CVar_sv_quota_stringcmdspersecond = new ConVar( + "sv_quota_stringcmdspersecond", + "60", + FCVAR_GAMEDLL, + "How many string commands per second clients are allowed to submit, 0 to disallow all string commands, -1 to disable"); + g_pServerLimits->Cvar_net_chan_limit_mode = + new ConVar("net_chan_limit_mode", "0", FCVAR_GAMEDLL, "The mode for netchan processing limits: 0 = warn, 1 = kick"); + g_pServerLimits->Cvar_net_chan_limit_msec_per_sec = new ConVar( + "net_chan_limit_msec_per_sec", + "100", + FCVAR_GAMEDLL, + "Netchannel processing is limited to so many milliseconds, abort connection if exceeding budget"); + g_pServerLimits->Cvar_sv_querylimit_per_sec = new ConVar("sv_querylimit_per_sec", "15", FCVAR_GAMEDLL, ""); + g_pServerLimits->Cvar_sv_max_chat_messages_per_sec = new ConVar("sv_max_chat_messages_per_sec", "5", FCVAR_GAMEDLL, ""); + g_pServerLimits->Cvar_sv_antispeedhack_enable = + new ConVar("sv_antispeedhack_enable", "0", FCVAR_NONE, "whether to enable antispeedhack protections"); + g_pServerLimits->Cvar_sv_antispeedhack_maxtickbudget = new ConVar( + "sv_antispeedhack_maxtickbudget", + "64", + FCVAR_GAMEDLL, + "Maximum number of client-issued usercmd ticks that can be replayed in packet loss conditions, 0 to allow no restrictions"); + g_pServerLimits->Cvar_sv_antispeedhack_budgetincreasemultiplier = new ConVar( + "sv_antispeedhack_budgetincreasemultiplier", + "1.2", + FCVAR_GAMEDLL, + "Increase usercmd processing budget by tickinterval * value per tick"); +} + +ON_DLL_LOAD("server.dll", ServerLimitsServer, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) +} diff --git a/NorthstarDLL/shared/exploit_fixes/ns_limits.h b/NorthstarDLL/shared/exploit_fixes/ns_limits.h new file mode 100644 index 00000000..bbc0a85f --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/ns_limits.h @@ -0,0 +1,51 @@ +#pragma once +#include "engine/r2engine.h" +#include "core/convar/convar.h" +#include <unordered_map> + +struct PlayerLimitData +{ + double lastClientCommandQuotaStart = -1.0; + int numClientCommandsInQuota = 0; + + double lastNetChanProcessingLimitStart = -1.0; + double netChanProcessingLimitTime = 0.0; + + double lastSayTextLimitStart = -1.0; + int sayTextLimitCount = 0; + + float flFrameUserCmdBudget = 0.0; +}; + +struct UnconnectedPlayerLimitData +{ + char ip[16]; + double lastQuotaStart = 0.0; + int packetCount = 0; + double timeoutEnd = -1.0; +}; + +class ServerLimitsManager +{ + public: + ConVar* CVar_sv_quota_stringcmdspersecond; + ConVar* Cvar_net_chan_limit_mode; + ConVar* Cvar_net_chan_limit_msec_per_sec; + ConVar* Cvar_sv_querylimit_per_sec; + ConVar* Cvar_sv_max_chat_messages_per_sec; + ConVar* Cvar_sv_antispeedhack_enable; + ConVar* Cvar_sv_antispeedhack_maxtickbudget; + ConVar* Cvar_sv_antispeedhack_budgetincreasemultiplier; + + std::unordered_map<R2::CBaseClient*, PlayerLimitData> m_PlayerLimitData; + std::vector<UnconnectedPlayerLimitData> m_UnconnectedPlayerLimitData; + + public: + void RunFrame(double flCurrentTime, float flFrameTime); + void AddPlayer(R2::CBaseClient* player); + void RemovePlayer(R2::CBaseClient* player); + bool CheckStringCommandLimits(R2::CBaseClient* player); + bool CheckChatLimits(R2::CBaseClient* player); +}; + +extern ServerLimitsManager* g_pServerLimits; diff --git a/NorthstarDLL/shared/keyvalues.cpp b/NorthstarDLL/shared/keyvalues.cpp new file mode 100644 index 00000000..fe7d6299 --- /dev/null +++ b/NorthstarDLL/shared/keyvalues.cpp @@ -0,0 +1,1316 @@ +#include "pch.h" +#include "keyvalues.h" +#include <winnt.h> + +// implementation of the ConVar class +// heavily based on https://github.com/Mauler125/r5sdk/blob/master/r5dev/vpc/keyvalues.cpp + +typedef int HKeySymbol; +#define INVALID_KEY_SYMBOL (-1) + +#define MAKE_3_BYTES_FROM_1_AND_2(x1, x2) ((((uint16_t)x2) << 8) | (uint8_t)(x1)) +#define SPLIT_3_BYTES_INTO_1_AND_2(x1, x2, x3) \ + do \ + { \ + x1 = (uint8_t)(x3); \ + x2 = (uint16_t)((x3) >> 8); \ + } while (0) + +struct CKeyValuesSystem +{ + public: + struct __VTable + { + char pad0[8 * 3]; // 2 methods + HKeySymbol (*GetSymbolForString)(CKeyValuesSystem* self, const char* name, bool bCreate); + const char* (*GetStringForSymbol)(CKeyValuesSystem* self, HKeySymbol symbol); + char pad1[8 * 5]; + HKeySymbol (*GetSymbolForStringCaseSensitive)( + CKeyValuesSystem* self, HKeySymbol& hCaseInsensitiveSymbol, const char* name, bool bCreate); + }; + + const __VTable* m_pVtable; +}; + +int (*V_UTF8ToUnicode)(const char* pUTF8, wchar_t* pwchDest, int cubDestSizeInBytes); +int (*V_UnicodeToUTF8)(const wchar_t* pUnicode, char* pUTF8, int cubDestSizeInBytes); +CKeyValuesSystem* (*KeyValuesSystem)(); + +KeyValues::KeyValues() {} // default constructor for copying and such + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName) +{ + Init(); + SetName(pszSetName); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// *pszFirstValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSsetName, const char* pszFirstKey, const char* pszFirstValue) +{ + Init(); + SetName(pszSsetName); + SetString(pszFirstKey, pszFirstValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// *pwszFirstValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, const wchar_t* pwszFirstValue) +{ + Init(); + SetName(pszSetName); + SetWString(pszFirstKey, pwszFirstValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// iFirstValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue) +{ + Init(); + SetName(pszSetName); + SetInt(pszFirstKey, iFirstValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// *pszFirstValue - +// *pszSecondKey - +// *pszSecondValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues( + const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue, const char* pszSecondKey, const char* pszSecondValue) +{ + Init(); + SetName(pszSetName); + SetString(pszFirstKey, pszFirstValue); + SetString(pszSecondKey, pszSecondValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// iFirstValue - +// *pszSecondKey - +// iSecondValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue, const char* pszSecondKey, int iSecondValue) +{ + Init(); + SetName(pszSetName); + SetInt(pszFirstKey, iFirstValue); + SetInt(pszSecondKey, iSecondValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +KeyValues::~KeyValues(void) +{ + RemoveEverything(); +} + +//----------------------------------------------------------------------------- +// Purpose: Initialize member variables +//----------------------------------------------------------------------------- +void KeyValues::Init(void) +{ + m_iKeyName = 0; + m_iKeyNameCaseSensitive1 = 0; + m_iKeyNameCaseSensitive2 = 0; + m_iDataType = TYPE_NONE; + + m_pSub = nullptr; + m_pPeer = nullptr; + m_pChain = nullptr; + + m_sValue = nullptr; + m_wsValue = nullptr; + m_pValue = nullptr; + + m_bHasEscapeSequences = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Clear out all subkeys, and the current value +//----------------------------------------------------------------------------- +void KeyValues::Clear(void) +{ + delete m_pSub; + m_pSub = nullptr; + m_iDataType = TYPE_NONE; +} + +//----------------------------------------------------------------------------- +// for backwards compat - we used to need this to force the free to run from the same DLL +// as the alloc +//----------------------------------------------------------------------------- +void KeyValues::DeleteThis(void) +{ + delete this; +} + +//----------------------------------------------------------------------------- +// Purpose: remove everything +//----------------------------------------------------------------------------- +void KeyValues::RemoveEverything(void) +{ + KeyValues* dat; + KeyValues* datNext = nullptr; + for (dat = m_pSub; dat != nullptr; dat = datNext) + { + datNext = dat->m_pPeer; + dat->m_pPeer = nullptr; + delete dat; + } + + for (dat = m_pPeer; dat && dat != this; dat = datNext) + { + datNext = dat->m_pPeer; + dat->m_pPeer = nullptr; + delete dat; + } + + delete[] m_sValue; + m_sValue = nullptr; + delete[] m_wsValue; + m_wsValue = nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Find a keyValue, create it if it is not found. +// Set bCreate to true to create the key if it doesn't already exist +// (which ensures a valid pointer will be returned) +// Input : *pszKeyName - +// bCreate - +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::FindKey(const char* pszKeyName, bool bCreate) +{ + assert_msg(this, "Member function called on NULL KeyValues"); + + if (!pszKeyName || !*pszKeyName) + return this; + + const char* pSubStr = strchr(pszKeyName, '/'); + + HKeySymbol iSearchStr = KeyValuesSystem()->m_pVtable->GetSymbolForString(KeyValuesSystem(), pszKeyName, bCreate); + if (iSearchStr == INVALID_KEY_SYMBOL) + { + // not found, couldn't possibly be in key value list + return nullptr; + } + + KeyValues* pLastKVs = nullptr; + KeyValues* pCurrentKVs; + // find the searchStr in the current peer list + for (pCurrentKVs = m_pSub; pCurrentKVs != NULL; pCurrentKVs = pCurrentKVs->m_pPeer) + { + pLastKVs = pCurrentKVs; // record the last item looked at (for if we need to append to the end of the list) + + // symbol compare + if (pLastKVs->m_iKeyName == (uint32_t)iSearchStr) + break; + } + + if (!pCurrentKVs && m_pChain) + pCurrentKVs = m_pChain->FindKey(pszKeyName, false); + + // make sure a key was found + if (!pCurrentKVs) + { + if (bCreate) + { + // we need to create a new key + pCurrentKVs = new KeyValues(pszKeyName); + // Assert(dat != NULL); + + // insert new key at end of list + if (pLastKVs) + pLastKVs->m_pPeer = pCurrentKVs; + else + m_pSub = pCurrentKVs; + + pCurrentKVs->m_pPeer = NULL; + + // a key graduates to be a submsg as soon as it's m_pSub is set + // this should be the only place m_pSub is set + m_iDataType = TYPE_NONE; + } + else + { + return NULL; + } + } + + // if we've still got a subStr we need to keep looking deeper in the tree + if (pSubStr) + { + // recursively chain down through the paths in the string + return pCurrentKVs->FindKey(pSubStr + 1, bCreate); + } + + return pCurrentKVs; +} + +//----------------------------------------------------------------------------- +// Purpose: Locate last child. Returns NULL if we have no children +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::FindLastSubKey(void) const +{ + // No children? + if (m_pSub == nullptr) + return nullptr; + + // Scan for the last one + KeyValues* pLastChild = m_pSub; + while (pLastChild->m_pPeer) + pLastChild = pLastChild->m_pPeer; + return pLastChild; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a subkey. Make sure the subkey isn't a child of some other keyvalues +// Input : *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::AddSubKey(KeyValues* pSubkey) +{ + // Make sure the subkey isn't a child of some other keyvalues + assert(pSubkey != nullptr); + assert(pSubkey->m_pPeer == nullptr); + + // add into subkey list + if (m_pSub == nullptr) + { + m_pSub = pSubkey; + } + else + { + KeyValues* pTempDat = m_pSub; + while (pTempDat->GetNextKey() != nullptr) + { + pTempDat = pTempDat->GetNextKey(); + } + + pTempDat->SetNextKey(pSubkey); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Remove a subkey from the list +// Input : *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::RemoveSubKey(KeyValues* pSubKey) +{ + if (!pSubKey) + return; + + // check the list pointer + if (m_pSub == pSubKey) + { + m_pSub = pSubKey->m_pPeer; + } + else + { + // look through the list + KeyValues* kv = m_pSub; + while (kv->m_pPeer) + { + if (kv->m_pPeer == pSubKey) + { + kv->m_pPeer = pSubKey->m_pPeer; + break; + } + + kv = kv->m_pPeer; + } + } + + pSubKey->m_pPeer = nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Insert a subkey at index +// Input : nIndex - +// *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::InsertSubKey(int nIndex, KeyValues* pSubKey) +{ + // Sub key must be valid and not part of another chain + assert(pSubKey && pSubKey->m_pPeer == nullptr); + + if (nIndex == 0) + { + pSubKey->m_pPeer = m_pSub; + m_pSub = pSubKey; + return; + } + else + { + int nCurrentIndex = 0; + for (KeyValues* pIter = GetFirstSubKey(); pIter != nullptr; pIter = pIter->GetNextKey()) + { + ++nCurrentIndex; + if (nCurrentIndex == nIndex) + { + pSubKey->m_pPeer = pIter->m_pPeer; + pIter->m_pPeer = pSubKey; + return; + } + } + // Index is out of range if we get here + assert(0); + return; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Checks if key contains a subkey +// Input : *pSubKey - +// Output : true if contains, false otherwise +//----------------------------------------------------------------------------- +bool KeyValues::ContainsSubKey(KeyValues* pSubKey) +{ + for (KeyValues* pIter = GetFirstSubKey(); pIter != nullptr; pIter = pIter->GetNextKey()) + { + if (pSubKey == pIter) + { + return true; + } + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Swaps existing subkey with another +// Input : *pExistingSubkey - +// *pNewSubKey - +//----------------------------------------------------------------------------- +void KeyValues::SwapSubKey(KeyValues* pExistingSubkey, KeyValues* pNewSubKey) +{ + assert(pExistingSubkey != nullptr && pNewSubKey != nullptr); + + // Make sure the new sub key isn't a child of some other keyvalues + assert(pNewSubKey->m_pPeer == nullptr); + + // Check the list pointer + if (m_pSub == pExistingSubkey) + { + pNewSubKey->m_pPeer = pExistingSubkey->m_pPeer; + pExistingSubkey->m_pPeer = nullptr; + m_pSub = pNewSubKey; + } + else + { + // Look through the list + KeyValues* kv = m_pSub; + while (kv->m_pPeer) + { + if (kv->m_pPeer == pExistingSubkey) + { + pNewSubKey->m_pPeer = pExistingSubkey->m_pPeer; + pExistingSubkey->m_pPeer = nullptr; + kv->m_pPeer = pNewSubKey; + break; + } + + kv = kv->m_pPeer; + } + // Existing sub key should always be found, otherwise it's a bug in the calling code. + assert(kv->m_pPeer != nullptr); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Elides subkey +// Input : *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::ElideSubKey(KeyValues* pSubKey) +{ + // This pointer's "next" pointer needs to be fixed up when we elide the key + KeyValues** ppPointerToFix = &m_pSub; + for (KeyValues* pKeyIter = m_pSub; pKeyIter != nullptr; ppPointerToFix = &pKeyIter->m_pPeer, pKeyIter = pKeyIter->GetNextKey()) + { + if (pKeyIter == pSubKey) + { + if (pSubKey->m_pSub == nullptr) + { + // No children, simply remove the key + *ppPointerToFix = pSubKey->m_pPeer; + delete pSubKey; + } + else + { + *ppPointerToFix = pSubKey->m_pSub; + // Attach the remainder of this chain to the last child of pSubKey + KeyValues* pChildIter = pSubKey->m_pSub; + while (pChildIter->m_pPeer != nullptr) + { + pChildIter = pChildIter->m_pPeer; + } + // Now points to the last child of pSubKey + pChildIter->m_pPeer = pSubKey->m_pPeer; + // Detach the node to be elided + pSubKey->m_pSub = nullptr; + pSubKey->m_pPeer = nullptr; + delete pSubKey; + } + return; + } + } + // Key not found; that's caller error. + assert(0); +} + +//----------------------------------------------------------------------------- +// Purpose: Check if a keyName has no value assigned to it. +// Input : *pszKeyName - +// Output : true on success, false otherwise +//----------------------------------------------------------------------------- +bool KeyValues::IsEmpty(const char* pszKeyName) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (!pKey) + return true; + + if (pKey->m_iDataType == TYPE_NONE && pKey->m_pSub == nullptr) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the first true sub key +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetFirstTrueSubKey(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pSub : nullptr; + while (pRet && pRet->m_iDataType != TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the next true sub key +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetNextTrueSubKey(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pPeer : nullptr; + while (pRet && pRet->m_iDataType != TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the first value +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetFirstValue(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pSub : nullptr; + while (pRet && pRet->m_iDataType == TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the next value +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetNextValue(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pPeer : nullptr; + while (pRet && pRet->m_iDataType == TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: Return the first subkey in the list +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetFirstSubKey() const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + return this ? m_pSub : nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Return the next subkey +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetNextKey() const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + return this ? m_pPeer : nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the name of the current key section +// Output : const char* +//----------------------------------------------------------------------------- +const char* KeyValues::GetName(void) const +{ + return KeyValuesSystem()->m_pVtable->GetStringForSymbol( + KeyValuesSystem(), MAKE_3_BYTES_FROM_1_AND_2(m_iKeyNameCaseSensitive1, m_iKeyNameCaseSensitive2)); +} + +//----------------------------------------------------------------------------- +// Purpose: Get the integer value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// nDefaultValue - +// Output : int +//----------------------------------------------------------------------------- +int KeyValues::GetInt(const char* pszKeyName, int iDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_STRING: + return atoi(pKey->m_sValue); + case TYPE_WSTRING: + return _wtoi(pKey->m_wsValue); + case TYPE_FLOAT: + return static_cast<int>(pKey->m_flValue); + case TYPE_UINT64: + // can't convert, since it would lose data + assert(0); + return 0; + case TYPE_INT: + case TYPE_PTR: + default: + return pKey->m_iValue; + }; + } + return iDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the integer value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// nDefaultValue - +// Output : uint64_t +//----------------------------------------------------------------------------- +uint64_t KeyValues::GetUint64(const char* pszKeyName, uint64_t nDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_STRING: + { + uint64_t uiResult = 0ull; + sscanf(pKey->m_sValue, "%lld", &uiResult); + return uiResult; + } + case TYPE_WSTRING: + { + uint64_t uiResult = 0ull; + swscanf(pKey->m_wsValue, L"%lld", &uiResult); + return uiResult; + } + case TYPE_FLOAT: + return static_cast<int>(pKey->m_flValue); + case TYPE_UINT64: + return *reinterpret_cast<uint64_t*>(pKey->m_sValue); + case TYPE_PTR: + return static_cast<uint64_t>(reinterpret_cast<uintptr_t>(pKey->m_pValue)); + case TYPE_INT: + default: + return pKey->m_iValue; + }; + } + return nDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the pointer value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// pDefaultValue - +// Output : void* +//----------------------------------------------------------------------------- +void* KeyValues::GetPtr(const char* pszKeyName, void* pDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_PTR: + return pKey->m_pValue; + + case TYPE_WSTRING: + case TYPE_STRING: + case TYPE_FLOAT: + case TYPE_INT: + case TYPE_UINT64: + default: + return nullptr; + }; + } + return pDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the float value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// flDefaultValue - +// Output : float +//----------------------------------------------------------------------------- +float KeyValues::GetFloat(const char* pszKeyName, float flDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_STRING: + return static_cast<float>(atof(pKey->m_sValue)); + case TYPE_WSTRING: + return static_cast<float>(_wtof(pKey->m_wsValue)); // no wtof + case TYPE_FLOAT: + return pKey->m_flValue; + case TYPE_INT: + return static_cast<float>(pKey->m_iValue); + case TYPE_UINT64: + return static_cast<float>((*(reinterpret_cast<uint64_t*>(pKey->m_sValue)))); + case TYPE_PTR: + default: + return 0.0f; + }; + } + return flDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the string pointer of a keyName. Default value is returned +// if the keyName can't be found. +// // Input : *pszKeyName - +// pszDefaultValue - +// Output : const char* +//----------------------------------------------------------------------------- +const char* KeyValues::GetString(const char* pszKeyName, const char* pszDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + // convert the data to string form then return it + char buf[64]; + switch (pKey->m_iDataType) + { + case TYPE_FLOAT: + snprintf(buf, sizeof(buf), "%f", pKey->m_flValue); + SetString(pszKeyName, buf); + break; + case TYPE_PTR: + snprintf(buf, sizeof(buf), "%lld", reinterpret_cast<uint64_t>(pKey->m_pValue)); + SetString(pszKeyName, buf); + break; + case TYPE_INT: + snprintf(buf, sizeof(buf), "%d", pKey->m_iValue); + SetString(pszKeyName, buf); + break; + case TYPE_UINT64: + snprintf(buf, sizeof(buf), "%lld", *(reinterpret_cast<uint64_t*>(pKey->m_sValue))); + SetString(pszKeyName, buf); + break; + case TYPE_COLOR: + snprintf(buf, sizeof(buf), "%d %d %d %d", pKey->m_Color[0], pKey->m_Color[1], pKey->m_Color[2], pKey->m_Color[3]); + SetString(pszKeyName, buf); + break; + + case TYPE_WSTRING: + { + // convert the string to char *, set it for future use, and return it + char wideBuf[512]; + int result = V_UnicodeToUTF8(pKey->m_wsValue, wideBuf, 512); + if (result) + { + // note: this will copy wideBuf + SetString(pszKeyName, wideBuf); + } + else + { + return pszDefaultValue; + } + break; + } + case TYPE_STRING: + break; + default: + return pszDefaultValue; + }; + + return pKey->m_sValue; + } + return pszDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the wide string pointer of a keyName. Default value is returned +// if the keyName can't be found. +// // Input : *pszKeyName - +// pwszDefaultValue - +// Output : const wchar_t* +//----------------------------------------------------------------------------- +const wchar_t* KeyValues::GetWString(const char* pszKeyName, const wchar_t* pwszDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + wchar_t wbuf[64]; + switch (pKey->m_iDataType) + { + case TYPE_FLOAT: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%f", pKey->m_flValue); + SetWString(pszKeyName, wbuf); + break; + case TYPE_PTR: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%lld", static_cast<int64_t>(reinterpret_cast<size_t>(pKey->m_pValue))); + SetWString(pszKeyName, wbuf); + break; + case TYPE_INT: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%d", pKey->m_iValue); + SetWString(pszKeyName, wbuf); + break; + case TYPE_UINT64: + { + swprintf(wbuf, ARRAYSIZE(wbuf), L"%lld", *(reinterpret_cast<uint64_t*>(pKey->m_sValue))); + SetWString(pszKeyName, wbuf); + } + break; + case TYPE_COLOR: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%d %d %d %d", pKey->m_Color[0], pKey->m_Color[1], pKey->m_Color[2], pKey->m_Color[3]); + SetWString(pszKeyName, wbuf); + break; + + case TYPE_WSTRING: + break; + case TYPE_STRING: + { + size_t bufSize = strlen(pKey->m_sValue) + 1; + wchar_t* pWBuf = new wchar_t[bufSize]; + int result = V_UTF8ToUnicode(pKey->m_sValue, pWBuf, static_cast<int>(bufSize * sizeof(wchar_t))); + if (result >= 0) // may be a zero length string + { + SetWString(pszKeyName, pWBuf); + delete[] pWBuf; + } + else + { + delete[] pWBuf; + return pwszDefaultValue; + } + + break; + } + default: + return pwszDefaultValue; + }; + + return reinterpret_cast<const wchar_t*>(pKey->m_wsValue); + } + return pwszDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Gets a color +// Input : *pszKeyName - +// &defaultColor - +// Output : Color +//----------------------------------------------------------------------------- +Color KeyValues::GetColor(const char* pszKeyName, const Color& defaultColor) +{ + Color color = defaultColor; + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + if (pKey->m_iDataType == TYPE_COLOR) + { + color[0] = pKey->m_Color[0]; + color[1] = pKey->m_Color[1]; + color[2] = pKey->m_Color[2]; + color[3] = pKey->m_Color[3]; + } + else if (pKey->m_iDataType == TYPE_FLOAT) + { + color[0] = static_cast<unsigned char>(pKey->m_flValue); + } + else if (pKey->m_iDataType == TYPE_INT) + { + color[0] = static_cast<unsigned char>(pKey->m_iValue); + } + else if (pKey->m_iDataType == TYPE_STRING) + { + // parse the colors out of the string + float a = 0, b = 0, c = 0, d = 0; + sscanf(pKey->m_sValue, "%f %f %f %f", &a, &b, &c, &d); + color[0] = static_cast<unsigned char>(a); + color[1] = static_cast<unsigned char>(b); + color[2] = static_cast<unsigned char>(c); + color[3] = static_cast<unsigned char>(d); + } + } + return color; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the data type of the value stored in a keyName +// Input : *pszKeyName - +//----------------------------------------------------------------------------- +KeyValuesTypes_t KeyValues::GetDataType(const char* pszKeyName) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + return static_cast<KeyValuesTypes_t>(pKey->m_iDataType); + + return TYPE_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the data type of the value stored in this keyName +//----------------------------------------------------------------------------- +KeyValuesTypes_t KeyValues::GetDataType(void) const +{ + return static_cast<KeyValuesTypes_t>(m_iDataType); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the integer value of a keyName. +// Input : *pszKeyName - +// iValue - +//----------------------------------------------------------------------------- +void KeyValues::SetInt(const char* pszKeyName, int iValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + if (pKey) + { + pKey->m_iValue = iValue; + pKey->m_iDataType = TYPE_INT; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the integer value of a keyName. +//----------------------------------------------------------------------------- +void KeyValues::SetUint64(const char* pszKeyName, uint64_t nValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + + if (pKey) + { + // delete the old value + delete[] pKey->m_sValue; + // make sure we're not storing the WSTRING - as we're converting over to STRING + delete[] pKey->m_wsValue; + pKey->m_wsValue = nullptr; + + pKey->m_sValue = new char[sizeof(uint64_t)]; + *(reinterpret_cast<uint64_t*>(pKey->m_sValue)) = nValue; + pKey->m_iDataType = TYPE_UINT64; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the float value of a keyName. +// Input : *pszKeyName - +// flValue - +//----------------------------------------------------------------------------- +void KeyValues::SetFloat(const char* pszKeyName, float flValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + if (pKey) + { + pKey->m_flValue = flValue; + pKey->m_iDataType = TYPE_FLOAT; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the name value of a keyName. +// Input : *pszSetName - +//----------------------------------------------------------------------------- +void KeyValues::SetName(const char* pszSetName) +{ + HKeySymbol hCaseSensitiveKeyName = INVALID_KEY_SYMBOL, hCaseInsensitiveKeyName = INVALID_KEY_SYMBOL; + hCaseSensitiveKeyName = + KeyValuesSystem()->m_pVtable->GetSymbolForStringCaseSensitive(KeyValuesSystem(), hCaseInsensitiveKeyName, pszSetName, false); + + m_iKeyName = hCaseInsensitiveKeyName; + SPLIT_3_BYTES_INTO_1_AND_2(m_iKeyNameCaseSensitive1, m_iKeyNameCaseSensitive2, hCaseSensitiveKeyName); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the pointer value of a keyName. +// Input : *pszKeyName - +// *pValue - +//----------------------------------------------------------------------------- +void KeyValues::SetPtr(const char* pszKeyName, void* pValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + + if (pKey) + { + pKey->m_pValue = pValue; + pKey->m_iDataType = TYPE_PTR; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the string value (internal) +// Input : *pszValue - +//----------------------------------------------------------------------------- +void KeyValues::SetStringValue(char const* pszValue) +{ + // delete the old value + delete[] m_sValue; + // make sure we're not storing the WSTRING - as we're converting over to STRING + delete[] m_wsValue; + m_wsValue = nullptr; + + if (!pszValue) + { + // ensure a valid value + pszValue = ""; + } + + // allocate memory for the new value and copy it in + size_t len = strlen(pszValue); + m_sValue = new char[len + 1]; + memcpy(m_sValue, pszValue, len + 1); + + m_iDataType = TYPE_STRING; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets this key's peer to the KeyValues passed in +// Input : *pDat - +//----------------------------------------------------------------------------- +void KeyValues::SetNextKey(KeyValues* pDat) +{ + m_pPeer = pDat; +} + +//----------------------------------------------------------------------------- +// Purpose: Set the string value of a keyName. +// Input : *pszKeyName - +// *pszValue - +//----------------------------------------------------------------------------- +void KeyValues::SetString(const char* pszKeyName, const char* pszValue) +{ + if (KeyValues* pKey = FindKey(pszKeyName, true)) + { + pKey->SetStringValue(pszValue); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the string value of a keyName. +// Input : *pszKeyName - +// *pwszValue - +//----------------------------------------------------------------------------- +void KeyValues::SetWString(const char* pszKeyName, const wchar_t* pwszValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + if (pKey) + { + // delete the old value + delete[] pKey->m_wsValue; + // make sure we're not storing the STRING - as we're converting over to WSTRING + delete[] pKey->m_sValue; + pKey->m_sValue = nullptr; + + if (!pwszValue) + { + // ensure a valid value + pwszValue = L""; + } + + // allocate memory for the new value and copy it in + size_t len = wcslen(pwszValue); + pKey->m_wsValue = new wchar_t[len + 1]; + memcpy(pKey->m_wsValue, pwszValue, (len + 1) * sizeof(wchar_t)); + + pKey->m_iDataType = TYPE_WSTRING; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets a color +// Input : *pszKeyName - +// color - +//----------------------------------------------------------------------------- +void KeyValues::SetColor(const char* pszKeyName, Color color) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + + if (pKey) + { + pKey->m_iDataType = TYPE_COLOR; + pKey->m_Color[0] = color[0]; + pKey->m_Color[1] = color[1]; + pKey->m_Color[2] = color[2]; + pKey->m_Color[3] = color[3]; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &src - +//----------------------------------------------------------------------------- +void KeyValues::RecursiveCopyKeyValues(KeyValues& src) +{ + // garymcthack - need to check this code for possible buffer overruns. + + m_iKeyName = src.m_iKeyName; + m_iKeyNameCaseSensitive1 = src.m_iKeyNameCaseSensitive1; + m_iKeyNameCaseSensitive2 = src.m_iKeyNameCaseSensitive2; + + if (!src.m_pSub) + { + m_iDataType = src.m_iDataType; + char buf[256]; + switch (src.m_iDataType) + { + case TYPE_NONE: + break; + case TYPE_STRING: + if (src.m_sValue) + { + size_t len = strlen(src.m_sValue) + 1; + m_sValue = new char[len]; + strncpy(m_sValue, src.m_sValue, len); + } + break; + case TYPE_INT: + { + m_iValue = src.m_iValue; + snprintf(buf, sizeof(buf), "%d", m_iValue); + size_t len = strlen(buf) + 1; + m_sValue = new char[len]; + strncpy(m_sValue, buf, len); + } + break; + case TYPE_FLOAT: + { + m_flValue = src.m_flValue; + snprintf(buf, sizeof(buf), "%f", m_flValue); + size_t len = strlen(buf) + 1; + m_sValue = new char[len]; + strncpy(m_sValue, buf, len); + } + break; + case TYPE_PTR: + { + m_pValue = src.m_pValue; + } + break; + case TYPE_UINT64: + { + m_sValue = new char[sizeof(uint64_t)]; + memcpy(m_sValue, src.m_sValue, sizeof(uint64_t)); + } + break; + case TYPE_COLOR: + { + m_Color[0] = src.m_Color[0]; + m_Color[1] = src.m_Color[1]; + m_Color[2] = src.m_Color[2]; + m_Color[3] = src.m_Color[3]; + } + break; + + default: + { + // do nothing . .what the heck is this? + assert(0); + } + break; + } + } + + // Handle the immediate child + if (src.m_pSub) + { + m_pSub = new KeyValues; + + m_pSub->Init(); + m_pSub->SetName(nullptr); + + m_pSub->RecursiveCopyKeyValues(*src.m_pSub); + } + + // Handle the immediate peer + if (src.m_pPeer) + { + m_pPeer = new KeyValues; + + m_pPeer->Init(); + m_pPeer->SetName(nullptr); + + m_pPeer->RecursiveCopyKeyValues(*src.m_pPeer); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Make a new copy of all subkeys, add them all to the passed-in keyvalues +// Input : *pParent - +//----------------------------------------------------------------------------- +void KeyValues::CopySubkeys(KeyValues* pParent) const +{ + // recursively copy subkeys + // Also maintain ordering.... + KeyValues* pPrev = nullptr; + for (KeyValues* pSub = m_pSub; pSub != nullptr; pSub = pSub->m_pPeer) + { + // take a copy of the subkey + KeyValues* pKey = pSub->MakeCopy(); + + // add into subkey list + if (pPrev) + { + pPrev->m_pPeer = pKey; + } + else + { + pParent->m_pSub = pKey; + } + pKey->m_pPeer = nullptr; + pPrev = pKey; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Makes a copy of the whole key-value pair set +// Output : KeyValues* +//----------------------------------------------------------------------------- +KeyValues* KeyValues::MakeCopy(void) const +{ + KeyValues* pNewKeyValue = new KeyValues; + + pNewKeyValue->Init(); + pNewKeyValue->SetName(GetName()); + + // copy data + pNewKeyValue->m_iDataType = m_iDataType; + switch (m_iDataType) + { + case TYPE_STRING: + { + if (m_sValue) + { + size_t len = strlen(m_sValue); + assert(!pNewKeyValue->m_sValue); + pNewKeyValue->m_sValue = new char[len + 1]; + memcpy(pNewKeyValue->m_sValue, m_sValue, len + 1); + } + } + break; + case TYPE_WSTRING: + { + if (m_wsValue) + { + size_t len = wcslen(m_wsValue); + pNewKeyValue->m_wsValue = new wchar_t[len + 1]; + memcpy(pNewKeyValue->m_wsValue, m_wsValue, len + 1 * sizeof(wchar_t)); + } + } + break; + + case TYPE_INT: + pNewKeyValue->m_iValue = m_iValue; + break; + + case TYPE_FLOAT: + pNewKeyValue->m_flValue = m_flValue; + break; + + case TYPE_PTR: + pNewKeyValue->m_pValue = m_pValue; + break; + + case TYPE_COLOR: + pNewKeyValue->m_Color[0] = m_Color[0]; + pNewKeyValue->m_Color[1] = m_Color[1]; + pNewKeyValue->m_Color[2] = m_Color[2]; + pNewKeyValue->m_Color[3] = m_Color[3]; + break; + + case TYPE_UINT64: + pNewKeyValue->m_sValue = new char[sizeof(uint64_t)]; + memcpy(pNewKeyValue->m_sValue, m_sValue, sizeof(uint64_t)); + break; + }; + + // recursively copy subkeys + CopySubkeys(pNewKeyValue); + return pNewKeyValue; +} + +ON_DLL_LOAD("vstdlib.dll", KeyValues, (CModule module)) +{ + V_UTF8ToUnicode = module.GetExport("V_UTF8ToUnicode").As<int (*)(const char*, wchar_t*, int)>(); + V_UnicodeToUTF8 = module.GetExport("V_UnicodeToUTF8").As<int (*)(const wchar_t*, char*, int)>(); + KeyValuesSystem = module.GetExport("KeyValuesSystem").As<CKeyValuesSystem* (*)()>(); +} + +AUTOHOOK_INIT() + +// clang-format off +AUTOHOOK(KeyValues__LoadFromBuffer, engine.dll + 0x426C30, +char, __fastcall, (KeyValues* self, const char* pResourceName, const char* pBuffer, void* pFileSystem, void* a5, void* a6, int a7)) +// clang-format on +{ + static void* pSavedFilesystemPtr = nullptr; + + // this is just to allow playlists to get a valid pFileSystem ptr for kv building, other functions that call this particular overload of + // LoadFromBuffer seem to get called on network stuff exclusively not exactly sure what the address wanted here is, so just taking it + // from a function call that always happens before playlists is loaded + + // note: would be better if we could serialize this to disk for playlists, as this method breaks saving playlists in demos + if (pFileSystem != nullptr) + pSavedFilesystemPtr = pFileSystem; + if (!pFileSystem && !strcmp(pResourceName, "playlists")) + pFileSystem = pSavedFilesystemPtr; + + return KeyValues__LoadFromBuffer(self, pResourceName, pBuffer, pFileSystem, a5, a6, a7); +} + +ON_DLL_LOAD("engine.dll", EngineKeyValues, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/shared/keyvalues.h b/NorthstarDLL/shared/keyvalues.h new file mode 100644 index 00000000..64ca0cc7 --- /dev/null +++ b/NorthstarDLL/shared/keyvalues.h @@ -0,0 +1,134 @@ +#pragma once +#include "core/math/color.h" + +enum KeyValuesTypes_t : char +{ + TYPE_NONE = 0x0, + TYPE_STRING = 0x1, + TYPE_INT = 0x2, + TYPE_FLOAT = 0x3, + TYPE_PTR = 0x4, + TYPE_WSTRING = 0x5, + TYPE_COLOR = 0x6, + TYPE_UINT64 = 0x7, + TYPE_COMPILED_INT_BYTE = 0x8, + TYPE_COMPILED_INT_0 = 0x9, + TYPE_COMPILED_INT_1 = 0xA, + TYPE_NUMTYPES = 0xB, +}; + +enum MergeKeyValuesOp_t +{ + MERGE_KV_ALL, + MERGE_KV_UPDATE, // update values are copied into storage, adding new keys to storage or updating existing ones + MERGE_KV_DELETE, // update values specify keys that get deleted from storage + MERGE_KV_BORROW, // update values only update existing keys in storage, keys in update that do not exist in storage are discarded +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple recursive data access class +// Used in vgui for message parameters and resource files +// Destructor deletes all child KeyValues nodes +// Data is stored in key (string names) - (string/int/float)value pairs called nodes. +// +// About KeyValues Text File Format: + +// It has 3 control characters '{', '}' and '"'. Names and values may be quoted or +// not. The quote '"' character must not be used within name or values, only for +// quoting whole tokens. You may use escape sequences wile parsing and add within a +// quoted token a \" to add quotes within your name or token. When using Escape +// Sequence the parser must now that by setting KeyValues::UsesEscapeSequences( true ), +// which it's off by default. Non-quoted tokens ends with a whitespace, '{', '}' and '"'. +// So you may use '{' and '}' within quoted tokens, but not for non-quoted tokens. +// An open bracket '{' after a key name indicates a list of subkeys which is finished +// with a closing bracket '}'. Subkeys use the same definitions recursively. +// Whitespaces are space, return, newline and tabulator. Allowed Escape sequences +// are \n, \t, \\, \n and \". The number character '#' is used for macro purposes +// (eg #include), don't use it as first character in key names. +//----------------------------------------------------------------------------- +class KeyValues +{ + private: + KeyValues(); // for internal use only + + public: + // Constructors/destructors + KeyValues(const char* pszSetName); + KeyValues(const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue); + KeyValues(const char* pszSetName, const char* pszFirstKey, const wchar_t* pwszFirstValue); + KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue); + KeyValues( + const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue, const char* pszSecondKey, const char* pszSecondValue); + KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue, const char* pszSecondKey, int iSecondValue); + ~KeyValues(void); + + void Init(void); + void Clear(void); + void DeleteThis(void); + void RemoveEverything(); + + KeyValues* FindKey(const char* pKeyName, bool bCreate = false); + KeyValues* FindLastSubKey(void) const; + + void AddSubKey(KeyValues* pSubkey); + void RemoveSubKey(KeyValues* pSubKey); + void InsertSubKey(int nIndex, KeyValues* pSubKey); + bool ContainsSubKey(KeyValues* pSubKey); + void SwapSubKey(KeyValues* pExistingSubkey, KeyValues* pNewSubKey); + void ElideSubKey(KeyValues* pSubKey); + + // Data access + bool IsEmpty(const char* pszKeyName); + KeyValues* GetFirstTrueSubKey(void) const; + KeyValues* GetNextTrueSubKey(void) const; + KeyValues* GetFirstValue(void) const; + KeyValues* GetNextValue(void) const; + KeyValues* GetFirstSubKey() const; + KeyValues* GetNextKey() const; + const char* GetName(void) const; + int GetInt(const char* pszKeyName, int iDefaultValue); + uint64_t GetUint64(const char* pszKeyName, uint64_t nDefaultValue); + void* GetPtr(const char* pszKeyName, void* pDefaultValue); + float GetFloat(const char* pszKeyName, float flDefaultValue); + const char* GetString(const char* pszKeyName = nullptr, const char* pszDefaultValue = ""); + const wchar_t* GetWString(const char* pszKeyName = nullptr, const wchar_t* pwszDefaultValue = L""); + Color GetColor(const char* pszKeyName, const Color& defaultColor); + KeyValuesTypes_t GetDataType(const char* pszKeyName); + KeyValuesTypes_t GetDataType(void) const; + + // Key writing + void SetInt(const char* pszKeyName, int iValue); + void SetUint64(const char* pszKeyName, uint64_t nValue); + void SetPtr(const char* pszKeyName, void* pValue); + void SetNextKey(KeyValues* pDat); + void SetName(const char* pszName); + void SetString(const char* pszKeyName, const char* pszValue); + void SetWString(const char* pszKeyName, const wchar_t* pwszValue); + void SetStringValue(char const* pszValue); + void SetColor(const char* pszKeyName, Color color); + void SetFloat(const char* pszKeyName, float flValue); + + void RecursiveCopyKeyValues(KeyValues& src); + void CopySubkeys(KeyValues* pParent) const; + KeyValues* MakeCopy(void) const; + + public: + uint32_t m_iKeyName : 24; // 0x0000 + uint32_t m_iKeyNameCaseSensitive1 : 8; // 0x0003 + char* m_sValue; // 0x0008 + wchar_t* m_wsValue; // 0x0010 + union // 0x0018 + { + int m_iValue; + float m_flValue; + void* m_pValue; + unsigned char m_Color[4]; + }; + char m_szShortName[8]; // 0x0020 + char m_iDataType; // 0x0028 + char m_bHasEscapeSequences; // 0x0029 + uint16_t m_iKeyNameCaseSensitive2; // 0x002A + KeyValues* m_pPeer; // 0x0030 + KeyValues* m_pSub; // 0x0038 + KeyValues* m_pChain; // 0x0040 +}; diff --git a/NorthstarDLL/shared/maxplayers.cpp b/NorthstarDLL/shared/maxplayers.cpp new file mode 100644 index 00000000..ebb44341 --- /dev/null +++ b/NorthstarDLL/shared/maxplayers.cpp @@ -0,0 +1,645 @@ +#include "pch.h" +#include "core/tier0.h" +#include "maxplayers.h" + +AUTOHOOK_INIT() + +// never set this to anything below 32 +#define NEW_MAX_PLAYERS 64 +// dg note: the theoretical limit is actually 100, 76 works without entity issues, and 64 works without clientside prediction issues. + +#define PAD_NUMBER(number, boundary) (((number) + ((boundary)-1)) / (boundary)) * (boundary) + +// this is horrible +constexpr int PlayerResource_Name_Start = 0; // Start of modded allocated space. +constexpr int PlayerResource_Name_Size = ((NEW_MAX_PLAYERS + 1) * 8); // const char* m_szName[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Ping_Start = PlayerResource_Name_Start + PlayerResource_Name_Size; +constexpr int PlayerResource_Ping_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iPing[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Team_Start = PlayerResource_Ping_Start + PlayerResource_Ping_Size; +constexpr int PlayerResource_Team_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iTeam[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_PRHealth_Start = PlayerResource_Team_Start + PlayerResource_Team_Size; +constexpr int PlayerResource_PRHealth_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iPRHealth[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Connected_Start = PlayerResource_PRHealth_Start + PlayerResource_PRHealth_Size; +constexpr int PlayerResource_Connected_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool) m_bConnected[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Alive_Start = PlayerResource_Connected_Start + PlayerResource_Connected_Size; +constexpr int PlayerResource_Alive_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool) m_bAlive[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_BoolStats_Start = PlayerResource_Alive_Start + PlayerResource_Alive_Size; +constexpr int PlayerResource_BoolStats_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool idk) m_boolStats[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_KillStats_Start = PlayerResource_BoolStats_Start + PlayerResource_BoolStats_Size; +constexpr int PlayerResource_KillStats_Length = PAD_NUMBER((NEW_MAX_PLAYERS + 1) * 6, 4); +constexpr int PlayerResource_KillStats_Size = (PlayerResource_KillStats_Length * 6); // int m_killStats[MAX_PLAYERS + 1][6]; + +constexpr int PlayerResource_ScoreStats_Start = PlayerResource_KillStats_Start + PlayerResource_KillStats_Size; +constexpr int PlayerResource_ScoreStats_Length = PAD_NUMBER((NEW_MAX_PLAYERS + 1) * 5, 4); +constexpr int PlayerResource_ScoreStats_Size = (PlayerResource_ScoreStats_Length * 4); // int m_scoreStats[MAX_PLAYERS + 1][5]; + +// must be the usage of the last field to account for any possible paddings +constexpr int PlayerResource_TotalSize = PlayerResource_ScoreStats_Start + PlayerResource_ScoreStats_Size; + +constexpr int Team_PlayerArray_AddedLength = NEW_MAX_PLAYERS - 32; +constexpr int Team_PlayerArray_AddedSize = PAD_NUMBER(Team_PlayerArray_AddedLength * 8, 4); +constexpr int Team_AddedSize = Team_PlayerArray_AddedSize; + +bool MaxPlayersIncreaseEnabled() +{ + static bool bMaxPlayersIncreaseEnabled = Tier0::CommandLine()->CheckParm("-experimentalmaxplayersincrease"); + return bMaxPlayersIncreaseEnabled; +} + +// should we use R2 for this? not sure +namespace R2 // use R2 namespace for game funcs +{ + int GetMaxPlayers() + { + if (MaxPlayersIncreaseEnabled()) + return NEW_MAX_PLAYERS; + + return 32; + } +} // namespace R2 + +template <class T> void ChangeOffset(MemoryAddress addr, unsigned int offset) +{ + addr.Patch((BYTE*)&offset, sizeof(T)); +} + +// clang-format off +AUTOHOOK(StringTables_CreateStringTable, engine.dll + 0x22E220, +void*,, (void* thisptr, const char* name, int maxentries, int userdatafixedsize, int userdatanetworkbits, int flags)) +// clang-format on +{ + // Change the amount of entries to account for a bigger player amount + if (!strcmp(name, "userinfo")) + { + int maxPlayersPowerOf2 = 1; + while (maxPlayersPowerOf2 < NEW_MAX_PLAYERS) + maxPlayersPowerOf2 <<= 1; + + maxentries = maxPlayersPowerOf2; + } + + return StringTables_CreateStringTable(thisptr, name, maxentries, userdatafixedsize, userdatanetworkbits, flags); +} + +ON_DLL_LOAD("engine.dll", MaxPlayersOverride_Engine, (CModule module)) +{ + if (!MaxPlayersIncreaseEnabled()) + return; + + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + // patch GetPlayerLimits to ignore the boundary limit + module.Offset(0x116458).Patch("0xEB"); // jle => jmp + + // patch ED_Alloc to change nFirstIndex + ChangeOffset<int>(module.Offset(0x18F46C + 1), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1) + + // patch CGameServer::SpawnServer to change GetMaxClients inline + ChangeOffset<int>(module.Offset(0x119543 + 2), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1) + + // patch CGameServer::SpawnServer to change for loop + ChangeOffset<unsigned char>(module.Offset(0x11957F + 2), NEW_MAX_PLAYERS); // original: 32 + + // patch CGameServer::SpawnServer to change for loop (there are two) + ChangeOffset<unsigned char>(module.Offset(0x119586 + 2), NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1) + + // patch max players somewhere in CClientState + ChangeOffset<unsigned char>(module.Offset(0x1A162C + 2), NEW_MAX_PLAYERS - 1); // original: 31 (32 - 1) + + // patch max players in userinfo stringtable creation + /*{ + int maxPlayersPowerOf2 = 1; + while (maxPlayersPowerOf2 < NEW_MAX_PLAYERS) + maxPlayersPowerOf2 <<= 1; + ChangeOffset<unsigned char>((char*)baseAddress + 0x114B79 + 3, maxPlayersPowerOf2); // original: 32 + }*/ + // this is not supposed to work at all but it does on 64 players (how) + // proper fix below + + // patch max players in userinfo stringtable creation loop + ChangeOffset<unsigned char>(module.Offset(0x114C48 + 2), NEW_MAX_PLAYERS); // original: 32 + + // do not load prebaked SendTable message list + module.Offset(0x75859).Patch("EB"); // jnz -> jmp +} + +typedef void (*RunUserCmds_Type)(bool a1, float a2); +RunUserCmds_Type RunUserCmds_Original; + +HMODULE serverBase = 0; +auto RandomIntZeroMax = (__int64(__fastcall*)())0; + +// lazy rebuild +// clang-format off +AUTOHOOK(RunUserCmds, server.dll + 0x483D10, +void,, (bool a1, float a2)) +// clang-format on +{ + unsigned char v3; // bl + int v5; // er14 + int i; // edi + __int64 v7; // rax + DWORD* v8; // rbx + int v9; // edi + __int64* v10; // rsi + __int64 v11; // rax + int v12; // er12 + __int64 v13; // rdi + int v14; // ebx + int v15; // eax + __int64 v16; // r8 + int v17; // edx + char v18; // r15 + char v19; // bp + int v20; // esi + __int64* v21; // rdi + __int64 v22; // rcx + bool v23; // al + __int64 v24; // rax + __int64 v25[NEW_MAX_PLAYERS]; // [rsp+20h] [rbp-138h] BYREF + + uintptr_t base = (__int64)serverBase; + auto g_pGlobals = *(__int64*)(base + 0xBFBE08); + __int64 globals = g_pGlobals; + + auto g_pEngineServer = *(__int64*)(base + 0xBFBD98); + + auto qword_1814D9648 = *(__int64*)(base + 0x14D9648); + auto qword_1814DA408 = *(__int64*)(base + 0x14DA408); + auto qword_1812107E8 = *(__int64*)(base + 0x12107E8); + auto qword_1812105A8 = *(__int64*)(base + 0x12105A8); + + auto UTIL_PlayerByIndex = (__int64(__fastcall*)(int index))(base + 0x26AA10); + auto sub_180485590 = (void(__fastcall*)(__int64))(base + 0x485590); + auto sub_18058CD80 = (void(__fastcall*)(__int64))(base + 0x58CD80); + auto sub_1805A6D90 = (void(__fastcall*)(__int64))(base + 0x5A6D90); + auto sub_1805A6E50 = (bool(__fastcall*)(__int64, int, char))(base + 0x5A6E50); + auto sub_1805A6C20 = (void(__fastcall*)(__int64))(base + 0x5A6C20); + + v3 = *(unsigned char*)(g_pGlobals + 73); + if (*(DWORD*)(qword_1814D9648 + 92) && + ((*(unsigned __int8(__fastcall**)(__int64))(*(__int64*)g_pEngineServer + 32))(g_pEngineServer) || + !*(DWORD*)(qword_1814DA408 + 92)) && + v3) + { + globals = g_pGlobals; + v5 = 1; + for (i = 1; i <= *(DWORD*)(g_pGlobals + 52); ++i) + { + v7 = UTIL_PlayerByIndex(i); + v8 = (DWORD*)v7; + if (v7) + { + *(__int64*)(base + 0x1210420) = v7; + *(float*)(g_pGlobals + 16) = a2; + if (!a1) + sub_18058CD80(v7); + sub_1805A6D90((__int64)v8); + } + globals = g_pGlobals; + } + memset(v25, 0, sizeof(v25)); + v9 = 0; + if (*(int*)(globals + 52) > 0) + { + v10 = v25; + do + { + v11 = UTIL_PlayerByIndex(++v9); + globals = g_pGlobals; + *v10++ = v11; + } while (v9 < *(DWORD*)(globals + 52)); + } + v12 = *(DWORD*)(qword_1812107E8 + 92); + if (*(DWORD*)(qword_1812105A8 + 92)) + { + v13 = *(DWORD*)(globals + 52) - 1; + if (v13 >= 1) + { + v14 = *(DWORD*)(globals + 52); + do + { + v15 = RandomIntZeroMax(); + v16 = v25[v13--]; + v17 = v15 % v14--; + v25[v13 + 1] = v25[v17]; + v25[v17] = v16; + } while (v13 >= 1); + globals = g_pGlobals; + } + } + v18 = 1; + do + { + v19 = 0; + v20 = 0; + if (*(int*)(globals + 52) > 0) + { + v21 = v25; + do + { + v22 = *v21; + if (*v21) + { + *(__int64*)(base + 0x1210420) = *v21; + *(float*)(globals + 16) = a2; + v23 = sub_1805A6E50(v22, v12, v18); + globals = g_pGlobals; + if (v23) + v19 = 1; + else + *v21 = 0; + } + ++v20; + ++v21; + } while (v20 < *(DWORD*)(globals + 52)); + } + v18 = 0; + } while (v19); + if (*(int*)(globals + 52) >= 1) + { + do + { + v24 = UTIL_PlayerByIndex(v5); + if (v24) + { + *(__int64*)(base + 0x1210420) = v24; + *(float*)(g_pGlobals + 16) = a2; + sub_1805A6C20(v24); + } + ++v5; + } while (v5 <= *(DWORD*)(g_pGlobals + 52)); + } + sub_180485590(*(__int64*)(base + 0xB7B2D8)); + } +} + +// clang-format off +AUTOHOOK(SendPropArray2, server.dll + 0x12B130, +__int64, __fastcall, (__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn, unsigned char unk1)) +// clang-format on +{ + // Change the amount of elements to account for a bigger player amount + if (!strcmp(name, "\"player_array\"")) + elements = NEW_MAX_PLAYERS; + + return SendPropArray2(recvProp, elements, flags, name, proxyFn, unk1); +} + +ON_DLL_LOAD("server.dll", MaxPlayersOverride_Server, (CModule module)) +{ + if (!MaxPlayersIncreaseEnabled()) + return; + + AUTOHOOK_DISPATCH_MODULE(server.dll) + + // get required data + serverBase = (HMODULE)module.m_nAddress; + RandomIntZeroMax = (decltype(RandomIntZeroMax))(GetProcAddress(GetModuleHandleA("vstdlib.dll"), "RandomIntZeroMax")); + + // patch max players amount + ChangeOffset<unsigned char>(module.Offset(0x9A44D + 3), NEW_MAX_PLAYERS); // 0x20 (32) => 0x80 (128) + + // patch SpawnGlobalNonRewinding to change forced edict index + ChangeOffset<unsigned char>(module.Offset(0x2BC403 + 2), NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1) + + constexpr int CPlayerResource_OriginalSize = 4776; + constexpr int CPlayerResource_AddedSize = PlayerResource_TotalSize; + constexpr int CPlayerResource_ModifiedSize = CPlayerResource_OriginalSize + CPlayerResource_AddedSize; + + // CPlayerResource class allocation function - allocate a bigger amount to fit all new max player data + ChangeOffset<unsigned int>(module.Offset(0x5C560A + 1), CPlayerResource_ModifiedSize); + + // DT_PlayerResource::m_iPing SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C5059 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C50A8 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C50E2 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iPing DataMap + ChangeOffset<unsigned int>(module.Offset(0xB94598), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset<unsigned short>(module.Offset(0xB9459C), NEW_MAX_PLAYERS + 1); + ChangeOffset<unsigned short>(module.Offset(0xB945C0), PlayerResource_Ping_Size); + + // DT_PlayerResource::m_iTeam SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C5110 + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C519C + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C517E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iTeam DataMap + ChangeOffset<unsigned int>(module.Offset(0xB94600), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset<unsigned short>(module.Offset(0xB94604), NEW_MAX_PLAYERS + 1); + ChangeOffset<unsigned short>(module.Offset(0xB94628), PlayerResource_Team_Size); + + // DT_PlayerResource::m_iPRHealth SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C51C0 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C5204 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C523E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iPRHealth DataMap + ChangeOffset<unsigned int>(module.Offset(0xB94668), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset<unsigned short>(module.Offset(0xB9466C), NEW_MAX_PLAYERS + 1); + ChangeOffset<unsigned short>(module.Offset(0xB94690), PlayerResource_PRHealth_Size); + + // DT_PlayerResource::m_bConnected SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C526C + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C52B4 + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C52EE + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bConnected DataMap + ChangeOffset<unsigned int>(module.Offset(0xB946D0), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned short>(module.Offset(0xB946D4), NEW_MAX_PLAYERS + 1); + ChangeOffset<unsigned short>(module.Offset(0xB946F8), PlayerResource_Connected_Size); + + // DT_PlayerResource::m_bAlive SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C5321 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C5364 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C539E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bAlive DataMap + ChangeOffset<unsigned int>(module.Offset(0xB94738), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset<unsigned short>(module.Offset(0xB9473C), NEW_MAX_PLAYERS + 1); + ChangeOffset<unsigned short>(module.Offset(0xB94760), PlayerResource_Alive_Size); + + // DT_PlayerResource::m_boolStats SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C53CC + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C5414 + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C544E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_boolStats DataMap + ChangeOffset<unsigned int>(module.Offset(0xB947A0), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset<unsigned short>(module.Offset(0xB947A4), NEW_MAX_PLAYERS + 1); + ChangeOffset<unsigned short>(module.Offset(0xB947C8), PlayerResource_BoolStats_Size); + + // DT_PlayerResource::m_killStats SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C547C + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C54E2 + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C54FE + 4), PlayerResource_KillStats_Length); + + // DT_PlayerResource::m_killStats DataMap + ChangeOffset<unsigned int>(module.Offset(0xB94808), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset<unsigned short>(module.Offset(0xB9480C), PlayerResource_KillStats_Length); + ChangeOffset<unsigned short>(module.Offset(0xB94830), PlayerResource_KillStats_Size); + + // DT_PlayerResource::m_scoreStats SendProp + ChangeOffset<unsigned int>(module.Offset(0x5C5528 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C5576 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C5584 + 4), PlayerResource_ScoreStats_Length); + + // DT_PlayerResource::m_scoreStats DataMap + ChangeOffset<unsigned int>(module.Offset(0xB94870), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset<unsigned short>(module.Offset(0xB94874), PlayerResource_ScoreStats_Length); + ChangeOffset<unsigned short>(module.Offset(0xB94898), PlayerResource_ScoreStats_Size); + + // CPlayerResource::UpdatePlayerData - m_bConnected + ChangeOffset<unsigned int>(module.Offset(0x5C66EE + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C672E + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // CPlayerResource::UpdatePlayerData - m_iPing + ChangeOffset<unsigned int>(module.Offset(0x5C6394 + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C63DB + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + + // CPlayerResource::UpdatePlayerData - m_iTeam + ChangeOffset<unsigned int>(module.Offset(0x5C63FD + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C6442 + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + + // CPlayerResource::UpdatePlayerData - m_iPRHealth + ChangeOffset<unsigned int>(module.Offset(0x5C645B + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C64A0 + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + + // CPlayerResource::UpdatePlayerData - m_bConnected + ChangeOffset<unsigned int>(module.Offset(0x5C64AA + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C64F0 + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // CPlayerResource::UpdatePlayerData - m_bAlive + ChangeOffset<unsigned int>(module.Offset(0x5C650A + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C654F + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + + // CPlayerResource::UpdatePlayerData - m_boolStats + ChangeOffset<unsigned int>(module.Offset(0x5C6557 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C65A5 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + + // CPlayerResource::UpdatePlayerData - m_scoreStats + ChangeOffset<unsigned int>(module.Offset(0x5C65C2 + 3), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C65E3 + 4), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + + // CPlayerResource::UpdatePlayerData - m_killStats + ChangeOffset<unsigned int>(module.Offset(0x5C6654 + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x5C665B + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + + *module.Offset(0x14E7390).As<DWORD*>() = 0; + auto DT_PlayerResource_Construct = module.Offset(0x5C4FE0).As<__int64(__fastcall*)()>(); + DT_PlayerResource_Construct(); + + constexpr int CTeam_OriginalSize = 3336; + constexpr int CTeam_AddedSize = Team_AddedSize; + constexpr int CTeam_ModifiedSize = CTeam_OriginalSize + CTeam_AddedSize; + + // CTeam class allocation function - allocate a bigger amount to fit all new team player data + ChangeOffset<unsigned int>(module.Offset(0x23924A + 1), CTeam_ModifiedSize); + + // CTeam::CTeam - increase memset length to clean newly allocated data + ChangeOffset<unsigned int>(module.Offset(0x2395AE + 2), 256 + CTeam_AddedSize); + + *module.Offset(0xC945A0).As<DWORD*>() = 0; + auto DT_Team_Construct = module.Offset(0x238F50).As<__int64(__fastcall*)()>(); + DT_Team_Construct(); +} + +// clang-format off +AUTOHOOK(RecvPropArray2, client.dll + 0x1CEDA0, +__int64, __fastcall, (__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn)) +// clang-format on +{ + // Change the amount of elements to account for a bigger player amount + if (!strcmp(name, "\"player_array\"")) + elements = NEW_MAX_PLAYERS; + + return RecvPropArray2(recvProp, elements, flags, name, proxyFn); +} + +ON_DLL_LOAD("client.dll", MaxPlayersOverride_Client, (CModule module)) +{ + if (!MaxPlayersIncreaseEnabled()) + return; + + AUTOHOOK_DISPATCH_MODULE(client.dll) + + constexpr int C_PlayerResource_OriginalSize = 5768; + constexpr int C_PlayerResource_AddedSize = PlayerResource_TotalSize; + constexpr int C_PlayerResource_ModifiedSize = C_PlayerResource_OriginalSize + C_PlayerResource_AddedSize; + + // C_PlayerResource class allocation function - allocate a bigger amount to fit all new max player data + ChangeOffset<unsigned int>(module.Offset(0x164C41 + 1), C_PlayerResource_ModifiedSize); + + // C_PlayerResource::C_PlayerResource - change loop end value + ChangeOffset<unsigned char>(module.Offset(0x1640C4 + 2), NEW_MAX_PLAYERS - 32); + + // C_PlayerResource::C_PlayerResource - change m_szName address + ChangeOffset<unsigned int>( + module.Offset(0x1640D0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); // appended to the end of the class + + // C_PlayerResource::C_PlayerResource - change m_szName address + ChangeOffset<unsigned int>( + module.Offset(0x1640D0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); // appended to the end of the class + + // C_PlayerResource::C_PlayerResource - increase memset length to clean newly allocated data + ChangeOffset<unsigned int>(module.Offset(0x1640D0 + 3), 2244 + C_PlayerResource_AddedSize); + + // C_PlayerResource::UpdatePlayerName - change m_szName address + ChangeOffset<unsigned int>(module.Offset(0x16431F + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName - change m_szName address 1 + ChangeOffset<unsigned int>(module.Offset(0x1645B1 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName - change m_szName address 2 + ChangeOffset<unsigned int>(module.Offset(0x1645C0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName - change m_szName address 3 + ChangeOffset<unsigned int>(module.Offset(0x1645DD + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName address 1 + ChangeOffset<unsigned int>(module.Offset(0x164B71 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName address 2 + ChangeOffset<unsigned int>(module.Offset(0x164B9B + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 1 + ChangeOffset<unsigned int>(module.Offset(0x164641 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 2 + ChangeOffset<unsigned int>(module.Offset(0x164650 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 3 + ChangeOffset<unsigned int>(module.Offset(0x16466D + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 1 + ChangeOffset<unsigned int>(module.Offset(0x164BA3 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 2 + ChangeOffset<unsigned int>(module.Offset(0x164BCE + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 3 + ChangeOffset<unsigned int>(module.Offset(0x164BE7 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::m_szName + ChangeOffset<unsigned int>(module.Offset(0xc350f8), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + ChangeOffset<unsigned short>(module.Offset(0xc350f8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource size + ChangeOffset<unsigned int>(module.Offset(0x163415 + 6), C_PlayerResource_ModifiedSize); + + // DT_PlayerResource::m_iPing RecvProp + ChangeOffset<unsigned int>(module.Offset(0x163492 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset<unsigned int>(module.Offset(0x1634D6 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset<unsigned int>(module.Offset(0x163515 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_iPing + ChangeOffset<unsigned int>(module.Offset(0xc35170), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset<unsigned short>(module.Offset(0xc35170 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iTeam RecvProp + ChangeOffset<unsigned int>(module.Offset(0x163549 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset<unsigned int>(module.Offset(0x1635C8 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset<unsigned int>(module.Offset(0x1635AD + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_iTeam + ChangeOffset<unsigned int>(module.Offset(0xc351e8), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset<unsigned short>(module.Offset(0xc351e8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iPRHealth RecvProp + ChangeOffset<unsigned int>(module.Offset(0x1635F9 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset<unsigned int>(module.Offset(0x163625 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset<unsigned int>(module.Offset(0x163675 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_iPRHealth + ChangeOffset<unsigned int>(module.Offset(0xc35260), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset<unsigned short>(module.Offset(0xc35260 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bConnected RecvProp + ChangeOffset<unsigned int>(module.Offset(0x1636A9 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned int>(module.Offset(0x1636D5 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned int>(module.Offset(0x163725 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_bConnected + ChangeOffset<unsigned int>(module.Offset(0xc352d8), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset<unsigned short>(module.Offset(0xc352d8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bAlive RecvProp + ChangeOffset<unsigned int>(module.Offset(0x163759 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset<unsigned int>(module.Offset(0x163785 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset<unsigned int>(module.Offset(0x1637D5 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_bAlive + ChangeOffset<unsigned int>(module.Offset(0xc35350), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset<unsigned short>(module.Offset(0xc35350 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_boolStats RecvProp + ChangeOffset<unsigned int>(module.Offset(0x163809 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x163835 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x163885 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_boolStats + ChangeOffset<unsigned int>(module.Offset(0xc353c8), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset<unsigned short>(module.Offset(0xc353c8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_killStats RecvProp + ChangeOffset<unsigned int>(module.Offset(0x1638B3 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x1638E5 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x163935 + 5), PlayerResource_KillStats_Length); + + // C_PlayerResource::m_killStats + ChangeOffset<unsigned int>(module.Offset(0xc35440), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset<unsigned short>(module.Offset(0xc35440 + 4), PlayerResource_KillStats_Length); + + // DT_PlayerResource::m_scoreStats RecvProp + ChangeOffset<unsigned int>(module.Offset(0x163969 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x163995 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset<unsigned int>(module.Offset(0x1639E5 + 5), PlayerResource_ScoreStats_Length); + + // C_PlayerResource::m_scoreStats + ChangeOffset<unsigned int>(module.Offset(0xc354b8), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset<unsigned short>(module.Offset(0xc354b8 + 4), PlayerResource_ScoreStats_Length); + + // C_PlayerResource::GetPlayerName - change m_bConnected address + ChangeOffset<unsigned int>(module.Offset(0x164599 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_bConnected address + ChangeOffset<unsigned int>(module.Offset(0x164629 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_bConnected address + ChangeOffset<unsigned int>(module.Offset(0x164B13 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // Some other get name func (that seems to be unused) - change m_bConnected address + ChangeOffset<unsigned int>(module.Offset(0x164860 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // Some other get name func 2 (that seems to be unused too) - change m_bConnected address + ChangeOffset<unsigned int>(module.Offset(0x164834 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + *module.Offset(0xC35068).As<DWORD*>() = 0; + auto DT_PlayerResource_Construct = module.Offset(0x163400).As<__int64(__fastcall*)()>(); + DT_PlayerResource_Construct(); + + constexpr int C_Team_OriginalSize = 3200; + constexpr int C_Team_AddedSize = Team_AddedSize; + constexpr int C_Team_ModifiedSize = C_Team_OriginalSize + C_Team_AddedSize; + + // C_Team class allocation function - allocate a bigger amount to fit all new team player data + ChangeOffset<unsigned int>(module.Offset(0x182321 + 1), C_Team_ModifiedSize); + + // C_Team::C_Team - increase memset length to clean newly allocated data + ChangeOffset<unsigned int>(module.Offset(0x1804A2 + 2), 256 + C_Team_AddedSize); + + // DT_Team size + ChangeOffset<unsigned int>(module.Offset(0xC3AA0C), C_Team_ModifiedSize); + + *module.Offset(0xC3AFF8).As<DWORD*>() = 0; + auto DT_Team_Construct = module.Offset(0x17F950).As<__int64(__fastcall*)()>(); + DT_Team_Construct(); +} diff --git a/NorthstarDLL/shared/maxplayers.h b/NorthstarDLL/shared/maxplayers.h new file mode 100644 index 00000000..b251f6a6 --- /dev/null +++ b/NorthstarDLL/shared/maxplayers.h @@ -0,0 +1,7 @@ +#pragma once + +// should we use R2 for this? not sure +namespace R2 // use R2 namespace for game funcs +{ + int GetMaxPlayers(); +} // namespace R2 diff --git a/NorthstarDLL/shared/misccommands.cpp b/NorthstarDLL/shared/misccommands.cpp new file mode 100644 index 00000000..ad8b2a32 --- /dev/null +++ b/NorthstarDLL/shared/misccommands.cpp @@ -0,0 +1,314 @@ +#include "pch.h" +#include "misccommands.h" +#include "core/convar/concommand.h" +#include "shared/playlist.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "core/tier0.h" +#include "engine/hoststate.h" +#include "masterserver/masterserver.h" +#include "mods/modmanager.h" +#include "server/auth/serverauthentication.h" +#include "squirrel/squirrel.h" + +void ConCommand_force_newgame(const CCommand& arg) +{ + if (arg.ArgC() < 2) + return; + + R2::g_pHostState->m_iNextState = R2::HostState_t::HS_NEW_GAME; + strncpy(R2::g_pHostState->m_levelName, arg.Arg(1), sizeof(R2::g_pHostState->m_levelName)); +} + +void ConCommand_ns_start_reauth_and_leave_to_lobby(const CCommand& arg) +{ + // hack for special case where we're on a local server, so we erase our own newly created auth data on disconnect + g_pMasterServerManager->m_bNewgameAfterSelfAuth = true; + g_pMasterServerManager->AuthenticateWithOwnServer(R2::g_pLocalPlayerUserID, g_pMasterServerManager->m_sOwnClientAuthToken); +} + +void ConCommand_ns_end_reauth_and_leave_to_lobby(const CCommand& arg) +{ + if (g_pServerAuthentication->m_RemoteAuthenticationData.size()) + R2::g_pCVar->FindVar("serverfilter")->SetValue(g_pServerAuthentication->m_RemoteAuthenticationData.begin()->first.c_str()); + + // weird way of checking, but check if client script vm is initialised, mainly just to allow players to cancel this + if (g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM) + { + g_pServerAuthentication->m_bNeedLocalAuthForNewgame = true; + + // this won't set playlist correctly on remote clients, don't think they can set playlist until they've left which sorta + // fucks things should maybe set this in HostState_NewGame? + R2::SetCurrentPlaylist("tdm"); + strcpy(R2::g_pHostState->m_levelName, "mp_lobby"); + R2::g_pHostState->m_iNextState = R2::HostState_t::HS_NEW_GAME; + } +} + +void AddMiscConCommands() +{ + RegisterConCommand( + "force_newgame", + ConCommand_force_newgame, + "forces a map load through directly setting g_pHostState->m_iNextState to HS_NEW_GAME", + FCVAR_NONE); + + RegisterConCommand( + "ns_start_reauth_and_leave_to_lobby", + ConCommand_ns_start_reauth_and_leave_to_lobby, + "called by the server, used to reauth and return the player to lobby when leaving a game", + FCVAR_SERVER_CAN_EXECUTE); + + // this is a concommand because we make a deferred call to it from another thread + RegisterConCommand("ns_end_reauth_and_leave_to_lobby", ConCommand_ns_end_reauth_and_leave_to_lobby, "", FCVAR_NONE); +} + +// fixes up various cvar flags to have more sane values +void FixupCvarFlags() +{ + if (Tier0::CommandLine()->CheckParm("-allowdevcvars")) + { + // strip hidden and devonly cvar flags + int iNumCvarsAltered = 0; + for (auto& pair : R2::g_pCVar->DumpToMap()) + { + // strip flags + int flags = pair.second->GetFlags(); + if (flags & FCVAR_DEVELOPMENTONLY) + { + flags &= ~FCVAR_DEVELOPMENTONLY; + iNumCvarsAltered++; + } + + if (flags & FCVAR_HIDDEN) + { + flags &= ~FCVAR_HIDDEN; + iNumCvarsAltered++; + } + + pair.second->m_nFlags = flags; + } + + spdlog::info("Removed {} hidden/devonly cvar flags", iNumCvarsAltered); + } + + // make all engine client commands FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS + // these are usually checked through CGameClient::IsEngineClientCommand, but we get more control over this if we just do it through + // cvar flags + const char** ppEngineClientCommands = CModule("engine.dll").Offset(0x7C5EF0).As<const char**>(); + + int i = 0; + do + { + ConCommandBase* pCommand = R2::g_pCVar->FindCommandBase(ppEngineClientCommands[i]); + if (pCommand) // not all the commands in this array actually exist in respawn source + pCommand->m_nFlags |= FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS; + } while (ppEngineClientCommands[++i]); + + // array of cvars and the flags we want to add to them + const std::vector<std::tuple<const char*, uint32_t>> CVAR_FIXUP_ADD_FLAGS = { + // system commands (i.e. necessary for proper functionality) + // servers need to be able to disconnect + {"disconnect", FCVAR_SERVER_CAN_EXECUTE}, + + // cheat commands + {"give", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"give_server", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"givecurrentammo", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"takecurrentammo", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + + {"switchclass", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"set", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"_setClassVarServer", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + + {"ent_create", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"ent_throw", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"ent_setname", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"ent_teleport", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"ent_remove", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"ent_remove_all", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"ent_fire", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + + {"particle_create", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"particle_recreate", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"particle_kill", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + + {"test_setteam", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"melee_lunge_ent", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + + // fcvars that should be cheats + {"net_ignoreAllSnapshots", FCVAR_CHEAT}, + {"highlight_draw", FCVAR_CHEAT}, + // these should potentially be replicated rather than cheat, like sv_footsteps is + // however they're defined on client, so can't make replicated atm sadly + {"cl_footstep_event_max_dist", FCVAR_CHEAT}, + {"cl_footstep_event_max_dist_titan", FCVAR_CHEAT}, + }; + + // array of cvars and the flags we want to remove from them + const std::vector<std::tuple<const char*, uint32_t>> CVAR_FIXUP_REMOVE_FLAGS = { + // unsure how this command works, not even sure it's used on retail servers, deffo shouldn't be used on northstar + {"migrateme", FCVAR_SERVER_CAN_EXECUTE | FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"recheck", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, // we don't need this on northstar servers, it's for communities + + // unsure how these work exactly (rpt system likely somewhat stripped?), removing anyway since they won't be used + {"rpt_client_enable", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + {"rpt_password", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, + + // these are devonly by default but should be modifyable + // NOTE: not all of these may actually do anything or work properly in practice + // network settings + {"cl_updaterate_mp", FCVAR_DEVELOPMENTONLY}, + {"cl_updaterate_sp", FCVAR_DEVELOPMENTONLY}, + {"clock_bias_sp", FCVAR_DEVELOPMENTONLY}, + {"clock_bias_mp", FCVAR_DEVELOPMENTONLY}, + {"cl_interpolate", FCVAR_DEVELOPMENTONLY}, // super duper ultra fucks anims if changed + {"cl_interpolateSoAllAnimsLoop", FCVAR_DEVELOPMENTONLY}, + {"cl_cmdrate", FCVAR_DEVELOPMENTONLY}, + {"cl_cmdbackup", FCVAR_DEVELOPMENTONLY}, + {"rate", FCVAR_DEVELOPMENTONLY}, + {"net_minroutable", FCVAR_DEVELOPMENTONLY}, + {"net_maxroutable", FCVAR_DEVELOPMENTONLY}, + {"net_lerpFields", FCVAR_DEVELOPMENTONLY}, + {"net_ignoreAllSnapshots", FCVAR_DEVELOPMENTONLY}, + {"net_chokeloop", FCVAR_DEVELOPMENTONLY}, + {"sv_unlag", FCVAR_DEVELOPMENTONLY}, + {"sv_maxunlag", FCVAR_DEVELOPMENTONLY}, + {"sv_lagpushticks", FCVAR_DEVELOPMENTONLY}, + {"sv_instancebaselines", FCVAR_DEVELOPMENTONLY}, + {"sv_voiceEcho", FCVAR_DEVELOPMENTONLY}, + {"net_compresspackets", FCVAR_DEVELOPMENTONLY}, + {"net_compresspackets_minsize", FCVAR_DEVELOPMENTONLY}, + {"net_verifyEncryption", FCVAR_DEVELOPMENTONLY}, // unsure if functional in retail + + // gameplay settings + {"vel_samples", FCVAR_DEVELOPMENTONLY}, + {"vel_sampleFrequency", FCVAR_DEVELOPMENTONLY}, + {"sv_friction", FCVAR_DEVELOPMENTONLY}, + {"sv_stopspeed", FCVAR_DEVELOPMENTONLY}, + {"sv_airaccelerate", FCVAR_DEVELOPMENTONLY}, + {"sv_forceGrapplesToFail", FCVAR_DEVELOPMENTONLY}, + {"sv_maxvelocity", FCVAR_DEVELOPMENTONLY}, + {"sv_footsteps", FCVAR_DEVELOPMENTONLY}, + // these 2 are flagged as CHEAT above, could be made REPLICATED later potentially + {"cl_footstep_event_max_dist", FCVAR_DEVELOPMENTONLY}, + {"cl_footstep_event_max_dist_titan", FCVAR_DEVELOPMENTONLY}, + {"sv_balanceTeams", FCVAR_DEVELOPMENTONLY}, + {"rodeo_enable", FCVAR_DEVELOPMENTONLY}, + {"sv_forceRodeoToFail", FCVAR_DEVELOPMENTONLY}, + {"player_find_rodeo_target_per_cmd", FCVAR_DEVELOPMENTONLY}, // todo test before merge + {"hud_takesshots", FCVAR_DEVELOPMENTONLY}, // very likely does not work but would be cool if it did + + {"cam_collision", FCVAR_DEVELOPMENTONLY}, + {"cam_idealdelta", FCVAR_DEVELOPMENTONLY}, + {"cam_ideallag", FCVAR_DEVELOPMENTONLY}, + + // graphics/visual settings + {"mat_colorcorrection", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoRadius", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoDepthMax", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoBlurSharpness", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoIntensity", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoBias", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoDistanceLerp", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoBlurRadius", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoExponent", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoDepthFadePctDefault", FCVAR_DEVELOPMENTONLY}, + {"r_drawscreenspaceparticles", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_fadeout_time", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_fadein_time", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_transition_time", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_mintransition_time", FCVAR_DEVELOPMENTONLY}, + // these 2 could be FCVAR_CHEAT, i guess? + {"cl_draw_player_model", FCVAR_DEVELOPMENTONLY}, + {"cl_always_draw_3p_player", FCVAR_DEVELOPMENTONLY}, + {"idcolor_neutral", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally_cb1", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally_cb2", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally_cb3", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy_cb1", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy_cb2", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy_cb3", FCVAR_DEVELOPMENTONLY}, + {"playerListPartyColorR", FCVAR_DEVELOPMENTONLY}, + {"playerListPartyColorG", FCVAR_DEVELOPMENTONLY}, + {"playerListPartyColorB", FCVAR_DEVELOPMENTONLY}, + {"playerListUseFriendColor", FCVAR_DEVELOPMENTONLY}, + {"fx_impact_neutral", FCVAR_DEVELOPMENTONLY}, + {"fx_impact_ally", FCVAR_DEVELOPMENTONLY}, + {"fx_impact_enemy", FCVAR_DEVELOPMENTONLY}, + {"hitch_alert_color", FCVAR_DEVELOPMENTONLY}, + {"particles_cull_all", FCVAR_DEVELOPMENTONLY}, + {"particles_cull_dlights", FCVAR_DEVELOPMENTONLY}, + {"map_settings_override", FCVAR_DEVELOPMENTONLY}, + {"highlight_draw", FCVAR_DEVELOPMENTONLY}, + + // sys/engine settings + {"sleep_when_meeting_framerate", FCVAR_DEVELOPMENTONLY}, + {"sleep_when_meeting_framerate_headroom_ms", FCVAR_DEVELOPMENTONLY}, + {"not_focus_sleep", FCVAR_DEVELOPMENTONLY}, + {"sp_not_focus_pause", FCVAR_DEVELOPMENTONLY}, + {"joy_requireFocus", FCVAR_DEVELOPMENTONLY}, + + {"host_thread_mode", FCVAR_DEVELOPMENTONLY}, + {"phys_enable_simd_optimizations", FCVAR_DEVELOPMENTONLY}, + {"phys_enable_experimental_optimizations", FCVAR_DEVELOPMENTONLY}, + + {"community_frame_run", FCVAR_DEVELOPMENTONLY}, + {"sv_single_core_dedi", FCVAR_DEVELOPMENTONLY}, + {"sv_stressbots", FCVAR_DEVELOPMENTONLY}, + + {"fatal_script_errors", FCVAR_DEVELOPMENTONLY}, + {"fatal_script_errors_client", FCVAR_DEVELOPMENTONLY}, + {"fatal_script_errors_server", FCVAR_DEVELOPMENTONLY}, + {"script_error_on_midgame_load", FCVAR_DEVELOPMENTONLY}, // idk what this is + + {"ai_ainRebuildOnMapStart", FCVAR_DEVELOPMENTONLY}, + + {"save_enable", FCVAR_DEVELOPMENTONLY}, + + // cheat commands + {"switchclass", FCVAR_DEVELOPMENTONLY}, + {"set", FCVAR_DEVELOPMENTONLY}, + {"_setClassVarServer", FCVAR_DEVELOPMENTONLY}, + + // reparse commands + {"aisettings_reparse", FCVAR_DEVELOPMENTONLY}, + {"aisettings_reparse_client", FCVAR_DEVELOPMENTONLY}, + {"damagedefs_reparse", FCVAR_DEVELOPMENTONLY}, + {"damagedefs_reparse_client", FCVAR_DEVELOPMENTONLY}, + {"playerSettings_reparse", FCVAR_DEVELOPMENTONLY}, + {"_playerSettings_reparse_Server", FCVAR_DEVELOPMENTONLY}, + + }; + + const std::vector<std::tuple<const char*, const char*>> CVAR_FIXUP_DEFAULT_VALUES = { + {"sv_stressbots", "0"}, // not currently used but this is probably a bad default if we get bots working + {"cl_pred_optimize", "0"} // fixes issues with animation prediction in thirdperson + }; + + for (auto& fixup : CVAR_FIXUP_ADD_FLAGS) + { + ConCommandBase* command = R2::g_pCVar->FindCommandBase(std::get<0>(fixup)); + if (command) + command->m_nFlags |= std::get<1>(fixup); + } + + for (auto& fixup : CVAR_FIXUP_REMOVE_FLAGS) + { + ConCommandBase* command = R2::g_pCVar->FindCommandBase(std::get<0>(fixup)); + if (command) + command->m_nFlags &= ~std::get<1>(fixup); + } + + for (auto& fixup : CVAR_FIXUP_DEFAULT_VALUES) + { + ConVar* cvar = R2::g_pCVar->FindVar(std::get<0>(fixup)); + if (cvar && !strcmp(cvar->GetString(), cvar->m_pszDefaultValue)) + { + cvar->SetValue(std::get<1>(fixup)); + cvar->m_pszDefaultValue = std::get<1>(fixup); + } + } +} diff --git a/NorthstarDLL/shared/misccommands.h b/NorthstarDLL/shared/misccommands.h new file mode 100644 index 00000000..07a07fb3 --- /dev/null +++ b/NorthstarDLL/shared/misccommands.h @@ -0,0 +1,3 @@ +#pragma once +void AddMiscConCommands(); +void FixupCvarFlags(); diff --git a/NorthstarDLL/shared/playlist.cpp b/NorthstarDLL/shared/playlist.cpp new file mode 100644 index 00000000..2fb856b3 --- /dev/null +++ b/NorthstarDLL/shared/playlist.cpp @@ -0,0 +1,130 @@ +#include "pch.h" +#include "playlist.h" +#include "core/convar/concommand.h" +#include "core/convar/convar.h" +#include "squirrel/squirrel.h" +#include "engine/hoststate.h" +#include "server/serverpresence.h" + +AUTOHOOK_INIT() + +// use the R2 namespace for game funcs +namespace R2 +{ + const char* (*GetCurrentPlaylistName)(); + void (*SetCurrentPlaylist)(const char* pPlaylistName); + void (*SetPlaylistVarOverride)(const char* pVarName, const char* pValue); + const char* (*GetCurrentPlaylistVar)(const char* pVarName, bool bUseOverrides); +} // namespace R2 + +ConVar* Cvar_ns_use_clc_SetPlaylistVarOverride; + +// clang-format off +AUTOHOOK(clc_SetPlaylistVarOverride__Process, engine.dll + 0x222180, +char, __fastcall, (void* a1, void* a2)) +// clang-format on +{ + // the private_match playlist on mp_lobby is the only situation where there should be any legitimate sending of this netmessage + if (!Cvar_ns_use_clc_SetPlaylistVarOverride->GetBool() || strcmp(R2::GetCurrentPlaylistName(), "private_match") || + strcmp(R2::g_pHostState->m_levelName, "mp_lobby")) + return 1; + + return clc_SetPlaylistVarOverride__Process(a1, a2); +} + +// clang-format off +AUTOHOOK(SetCurrentPlaylist, engine.dll + 0x18EB20, +bool, __fastcall, (const char* pPlaylistName)) +// clang-format on +{ + bool bSuccess = SetCurrentPlaylist(pPlaylistName); + + if (bSuccess) + { + spdlog::info("Set playlist to {}", R2::GetCurrentPlaylistName()); + g_pServerPresence->SetPlaylist(R2::GetCurrentPlaylistName()); + } + + return bSuccess; +} + +// clang-format off +AUTOHOOK(SetPlaylistVarOverride, engine.dll + 0x18ED00, +void, __fastcall, (const char* pVarName, const char* pValue)) +// clang-format on +{ + if (strlen(pValue) >= 64) + return; + + SetPlaylistVarOverride(pVarName, pValue); +} + +// clang-format off +AUTOHOOK(GetCurrentPlaylistVar, engine.dll + 0x18C680, +const char*, __fastcall, (const char* pVarName, bool bUseOverrides)) +// clang-format on +{ + if (!bUseOverrides && !strcmp(pVarName, "max_players")) + bUseOverrides = true; + + return GetCurrentPlaylistVar(pVarName, bUseOverrides); +} + +// clang-format off +AUTOHOOK(GetCurrentGamemodeMaxPlayers, engine.dll + 0x18C430, +int, __fastcall, ()) +// clang-format on +{ + const char* pMaxPlayers = R2::GetCurrentPlaylistVar("max_players", 0); + if (!pMaxPlayers) + return GetCurrentGamemodeMaxPlayers(); + + int iMaxPlayers = atoi(pMaxPlayers); + return iMaxPlayers; +} + +void ConCommand_playlist(const CCommand& args) +{ + if (args.ArgC() < 2) + return; + + R2::SetCurrentPlaylist(args.Arg(1)); +} + +void ConCommand_setplaylistvaroverride(const CCommand& args) +{ + if (args.ArgC() < 3) + return; + + for (int i = 1; i < args.ArgC(); i += 2) + R2::SetPlaylistVarOverride(args.Arg(i), args.Arg(i + 1)); +} + +ON_DLL_LOAD_RELIESON("engine.dll", PlaylistHooks, (ConCommand, ConVar), (CModule module)) +{ + AUTOHOOK_DISPATCH() + + R2::GetCurrentPlaylistName = module.Offset(0x18C640).As<const char* (*)()>(); + R2::SetCurrentPlaylist = module.Offset(0x18EB20).As<void (*)(const char*)>(); + R2::SetPlaylistVarOverride = module.Offset(0x18ED00).As<void (*)(const char*, const char*)>(); + R2::GetCurrentPlaylistVar = module.Offset(0x18C680).As<const char* (*)(const char*, bool)>(); + + // playlist is the name of the command on respawn servers, but we already use setplaylist so can't get rid of it + RegisterConCommand("playlist", ConCommand_playlist, "Sets the current playlist", FCVAR_NONE); + RegisterConCommand("setplaylist", ConCommand_playlist, "Sets the current playlist", FCVAR_NONE); + RegisterConCommand("setplaylistvaroverrides", ConCommand_setplaylistvaroverride, "sets a playlist var override", FCVAR_NONE); + + // note: clc_SetPlaylistVarOverride is pretty insecure, since it allows for entirely arbitrary playlist var overrides to be sent to the + // server, this is somewhat restricted on custom servers to prevent it being done outside of private matches, but ideally it should be + // disabled altogether, since the custom menus won't use it anyway this should only really be accepted if you want vanilla client + // compatibility + Cvar_ns_use_clc_SetPlaylistVarOverride = new ConVar( + "ns_use_clc_SetPlaylistVarOverride", "0", FCVAR_GAMEDLL, "Whether the server should accept clc_SetPlaylistVarOverride messages"); + + // patch to prevent clc_SetPlaylistVarOverride from being able to crash servers if we reach max overrides due to a call to Error (why is + // this possible respawn, wtf) todo: add a warning for this + module.Offset(0x18ED8D).Patch("C3"); + + // patch to allow setplaylistvaroverride to be called before map init on dedicated and private match launched through the game + module.Offset(0x18ED17).NOP(6); +} diff --git a/NorthstarDLL/shared/playlist.h b/NorthstarDLL/shared/playlist.h new file mode 100644 index 00000000..c77b37d9 --- /dev/null +++ b/NorthstarDLL/shared/playlist.h @@ -0,0 +1,10 @@ +#pragma once + +// use the R2 namespace for game funcs +namespace R2 +{ + extern const char* (*GetCurrentPlaylistName)(); + extern void (*SetCurrentPlaylist)(const char* pPlaylistName); + extern void (*SetPlaylistVarOverride)(const char* pVarName, const char* pValue); + extern const char* (*GetCurrentPlaylistVar)(const char* pVarName, bool bUseOverrides); +} // namespace R2 |