aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/shared
diff options
context:
space:
mode:
Diffstat (limited to 'NorthstarDLL/shared')
-rw-r--r--NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp458
-rw-r--r--NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp79
-rw-r--r--NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp200
-rw-r--r--NorthstarDLL/shared/exploit_fixes/ns_limits.cpp298
-rw-r--r--NorthstarDLL/shared/exploit_fixes/ns_limits.h51
-rw-r--r--NorthstarDLL/shared/keyvalues.cpp1316
-rw-r--r--NorthstarDLL/shared/keyvalues.h134
-rw-r--r--NorthstarDLL/shared/maxplayers.cpp645
-rw-r--r--NorthstarDLL/shared/maxplayers.h7
-rw-r--r--NorthstarDLL/shared/misccommands.cpp314
-rw-r--r--NorthstarDLL/shared/misccommands.h3
-rw-r--r--NorthstarDLL/shared/playlist.cpp130
-rw-r--r--NorthstarDLL/shared/playlist.h10
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