diff options
author | Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> | 2023-12-27 00:32:01 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-27 01:32:01 +0100 |
commit | f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287 (patch) | |
tree | 90f2c6a4885dbd181799e2325cf33588697674e1 /primedev/server | |
parent | bb8ed59f6891b1196c5f5bbe7346cd171c8215fa (diff) | |
download | NorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.tar.gz NorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.zip |
Folder restructuring from primedev (#624)v1.21.2-rc3v1.21.2
Copies of over the primedev folder structure for easier cherry-picking of further changes
Co-authored-by: F1F7Y <filip.bartos07@proton.me>
Diffstat (limited to 'primedev/server')
-rw-r--r-- | primedev/server/alltalk.cpp | 28 | ||||
-rw-r--r-- | primedev/server/auth/bansystem.cpp | 224 | ||||
-rw-r--r-- | primedev/server/auth/bansystem.h | 19 | ||||
-rw-r--r-- | primedev/server/auth/serverauthentication.cpp | 380 | ||||
-rw-r--r-- | primedev/server/auth/serverauthentication.h | 58 | ||||
-rw-r--r-- | primedev/server/buildainfile.cpp | 395 | ||||
-rw-r--r-- | primedev/server/r2server.cpp | 10 | ||||
-rw-r--r-- | primedev/server/r2server.h | 106 | ||||
-rw-r--r-- | primedev/server/serverchathooks.cpp | 174 | ||||
-rw-r--r-- | primedev/server/serverchathooks.h | 24 | ||||
-rw-r--r-- | primedev/server/servernethooks.cpp | 218 | ||||
-rw-r--r-- | primedev/server/serverpresence.cpp | 228 | ||||
-rw-r--r-- | primedev/server/serverpresence.h | 94 |
13 files changed, 1958 insertions, 0 deletions
diff --git a/primedev/server/alltalk.cpp b/primedev/server/alltalk.cpp new file mode 100644 index 00000000..74119309 --- /dev/null +++ b/primedev/server/alltalk.cpp @@ -0,0 +1,28 @@ +#include "core/convar/convar.h" +#include "engine/r2engine.h" + +size_t __fastcall ShouldAllowAlltalk() +{ + // this needs to return a 64 bit integer where 0 = true and 1 = false + static ConVar* Cvar_sv_alltalk = g_pCVar->FindVar("sv_alltalk"); + if (Cvar_sv_alltalk->GetBool()) + return 0; + + // lobby should default to alltalk, otherwise don't allow it + return strcmp(g_pGlobals->m_pMapName, "mp_lobby"); +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerAllTalk, ConVar, (CModule module)) +{ + // replace strcmp function called in CClient::ProcessVoiceData with our own code that calls ShouldAllowAllTalk + CMemoryAddress base = module.Offset(0x1085FA); + + base.Patch("48 B8"); // mov rax, 64 bit int + // (uint8_t*)&ShouldAllowAlltalk doesn't work for some reason? need to make it a uint64 first + uint64_t pShouldAllowAllTalk = reinterpret_cast<uint64_t>(ShouldAllowAlltalk); + base.Offset(0x2).Patch((uint8_t*)&pShouldAllowAllTalk, 8); + base.Offset(0xA).Patch("FF D0"); // call rax + + // nop until compare (test eax, eax) + base.Offset(0xC).NOP(0x7); +} diff --git a/primedev/server/auth/bansystem.cpp b/primedev/server/auth/bansystem.cpp new file mode 100644 index 00000000..a45cde93 --- /dev/null +++ b/primedev/server/auth/bansystem.cpp @@ -0,0 +1,224 @@ +#include "bansystem.h" +#include "serverauthentication.h" +#include "core/convar/concommand.h" +#include "server/r2server.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "config/profile.h" + +#include <filesystem> + +const char* BANLIST_PATH_SUFFIX = "/banlist.txt"; +const char BANLIST_COMMENT_CHAR = '#'; + +ServerBanSystem* g_pBanSystem; + +void ServerBanSystem::OpenBanlist() +{ + std::ifstream banlistStream(GetNorthstarPrefix() + "/banlist.txt"); + + if (!banlistStream.fail()) + { + std::string line; + while (std::getline(banlistStream, line)) + { + // ignore line if first char is # or line is empty + if (line == "" || line.front() == BANLIST_COMMENT_CHAR) + continue; + + // remove tabs which shouldnt be there but maybe someone did the funny + line.erase(std::remove(line.begin(), line.end(), '\t'), line.end()); + // remove spaces to allow for spaces before uids + line.erase(std::remove(line.begin(), line.end(), ' '), line.end()); + + // check if line is empty to allow for newlines in the file + if (line == "") + continue; + + // for inline comments like: 123123123 #banned for unfunny + std::string uid = line.substr(0, line.find(BANLIST_COMMENT_CHAR)); + + m_vBannedUids.push_back(strtoull(uid.c_str(), nullptr, 10)); + } + + banlistStream.close(); + } + + // open write stream for banlist // dont do this to allow for all time access + // m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); +} + +void ServerBanSystem::ReloadBanlist() +{ + std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); + + if (!fsBanlist.fail()) + { + std::string line; + // since we wanna use this as the reload func we need to clear the list + m_vBannedUids.clear(); + while (std::getline(fsBanlist, line)) + m_vBannedUids.push_back(strtoull(line.c_str(), nullptr, 10)); + + fsBanlist.close(); + } +} + +void ServerBanSystem::ClearBanlist() +{ + m_vBannedUids.clear(); + + // reopen the file, don't provide std::ofstream::app so it clears on open + m_sBanlistStream.close(); + m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); + m_sBanlistStream.close(); +} + +void ServerBanSystem::BanUID(uint64_t uid) +{ + // checking if last char is \n to make sure uids arent getting fucked + std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); + std::string content((std::istreambuf_iterator<char>(fsBanlist)), (std::istreambuf_iterator<char>())); + fsBanlist.close(); + + m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); + if (content.back() != '\n') + m_sBanlistStream << std::endl; + + m_vBannedUids.push_back(uid); + m_sBanlistStream << std::to_string(uid) << std::endl; + m_sBanlistStream.close(); + spdlog::info("{} was banned", uid); +} + +void ServerBanSystem::UnbanUID(uint64_t uid) +{ + auto findResult = std::find(m_vBannedUids.begin(), m_vBannedUids.end(), uid); + if (findResult == m_vBannedUids.end()) + return; + + m_vBannedUids.erase(findResult); + + std::vector<std::string> banlistText; + std::ifstream fs_readBanlist(GetNorthstarPrefix() + "/banlist.txt"); + + if (!fs_readBanlist.fail()) + { + std::string line; + while (std::getline(fs_readBanlist, line)) + { + // support for comments and newlines added in https://github.com/R2Northstar/NorthstarLauncher/pull/227 + + std::string modLine = line; // copy the line into a free var that we can fuck with, line will be the original + + // remove tabs which shouldnt be there but maybe someone did the funny + modLine.erase(std::remove(modLine.begin(), modLine.end(), '\t'), modLine.end()); + // remove spaces to allow for spaces before uids + modLine.erase(std::remove(modLine.begin(), modLine.end(), ' '), modLine.end()); + + // ignore line if first char is # or empty line, just add it + if (line.front() == BANLIST_COMMENT_CHAR || modLine == "") + { + banlistText.push_back(line); + continue; + } + + // for inline comments like: 123123123 #banned for unfunny + std::string lineUid = line.substr(0, line.find(BANLIST_COMMENT_CHAR)); + // have to erase spaces or else inline comments will fuck up the uid finding + lineUid.erase(std::remove(lineUid.begin(), lineUid.end(), '\t'), lineUid.end()); + lineUid.erase(std::remove(lineUid.begin(), lineUid.end(), ' '), lineUid.end()); + + // if the uid in the line is the uid we wanna unban + if (std::to_string(uid) == lineUid) + { + // comment the uid out + line.insert(0, "# "); + + // add a comment with unban date + // not necessary but i feel like this makes it better + std::time_t t = std::time(0); + std::tm* now = std::localtime(&t); + + std::ostringstream unbanComment; + + //{y}/{m}/{d} {h}:{m} + unbanComment << " # unban date: "; + unbanComment << now->tm_year + 1900 << "-"; // this lib is so fucking awful + unbanComment << std::setw(2) << std::setfill('0') << now->tm_mon + 1 << "-"; + unbanComment << std::setw(2) << std::setfill('0') << now->tm_mday << " "; + unbanComment << std::setw(2) << std::setfill('0') << now->tm_hour << ":"; + unbanComment << std::setw(2) << std::setfill('0') << now->tm_min; + + line.append(unbanComment.str()); + } + + banlistText.push_back(line); + } + + fs_readBanlist.close(); + } + + // open write stream for banlist // without append so we clear the file + if (m_sBanlistStream.is_open()) + m_sBanlistStream.close(); + m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); + + for (std::string updatedLine : banlistText) + m_sBanlistStream << updatedLine << std::endl; + + m_sBanlistStream.close(); + spdlog::info("{} was unbanned", uid); +} + +bool ServerBanSystem::IsUIDAllowed(uint64_t uid) +{ + uint64_t localPlayerUserID = strtoull(g_pLocalPlayerUserID, nullptr, 10); + if (localPlayerUserID == uid) + return true; + + ReloadBanlist(); // Reload to have up to date list on join + return std::find(m_vBannedUids.begin(), m_vBannedUids.end(), uid) == m_vBannedUids.end(); +} + +void ConCommand_ban(const CCommand& args) +{ + if (args.ArgC() < 2) + return; + + for (int i = 0; i < g_pGlobals->m_nMaxClients; i++) + { + CBaseClient* player = &g_pClientArray[i]; + + if (!strcmp(player->m_Name, args.Arg(1)) || !strcmp(player->m_UID, args.Arg(1))) + { + g_pBanSystem->BanUID(strtoull(player->m_UID, nullptr, 10)); + CBaseClient__Disconnect(player, 1, "Banned from server"); + break; + } + } +} + +void ConCommand_unban(const CCommand& args) +{ + if (args.ArgC() < 2) + return; + + // assumedly the player being unbanned here wasn't already connected, so don't need to iterate over players or anything + g_pBanSystem->UnbanUID(strtoull(args.Arg(1), nullptr, 10)); +} + +void ConCommand_clearbanlist(const CCommand& args) +{ + g_pBanSystem->ClearBanlist(); +} + +ON_DLL_LOAD_RELIESON("engine.dll", BanSystem, ConCommand, (CModule module)) +{ + g_pBanSystem = new ServerBanSystem; + g_pBanSystem->OpenBanlist(); + + RegisterConCommand("ban", ConCommand_ban, "bans a given player by uid or name", FCVAR_GAMEDLL); + RegisterConCommand("unban", ConCommand_unban, "unbans a given player by uid", FCVAR_GAMEDLL); + RegisterConCommand("clearbanlist", ConCommand_clearbanlist, "clears all uids on the banlist", FCVAR_GAMEDLL); +} diff --git a/primedev/server/auth/bansystem.h b/primedev/server/auth/bansystem.h new file mode 100644 index 00000000..d6ac5a4f --- /dev/null +++ b/primedev/server/auth/bansystem.h @@ -0,0 +1,19 @@ +#pragma once +#include <fstream> + +class ServerBanSystem +{ +private: + std::ofstream m_sBanlistStream; + std::vector<uint64_t> m_vBannedUids; + +public: + void OpenBanlist(); + void ReloadBanlist(); + void ClearBanlist(); + void BanUID(uint64_t uid); + void UnbanUID(uint64_t uid); + bool IsUIDAllowed(uint64_t uid); +}; + +extern ServerBanSystem* g_pBanSystem; diff --git a/primedev/server/auth/serverauthentication.cpp b/primedev/server/auth/serverauthentication.cpp new file mode 100644 index 00000000..0d46426f --- /dev/null +++ b/primedev/server/auth/serverauthentication.cpp @@ -0,0 +1,380 @@ +#include "serverauthentication.h" +#include "shared/exploit_fixes/ns_limits.h" +#include "core/convar/cvar.h" +#include "core/convar/convar.h" +#include "masterserver/masterserver.h" +#include "server/serverpresence.h" +#include "engine/hoststate.h" +#include "bansystem.h" +#include "core/convar/concommand.h" +#include "dedicated/dedicated.h" +#include "config/profile.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "server/r2server.h" + +#include <fstream> +#include <filesystem> +#include <string> +#include <thread> + +AUTOHOOK_INIT() + +// global vars +ServerAuthenticationManager* g_pServerAuthentication; +CBaseServer__RejectConnectionType CBaseServer__RejectConnection; + +void ServerAuthenticationManager::AddRemotePlayer(std::string token, uint64_t uid, std::string username, std::string pdata) +{ + std::string uidS = std::to_string(uid); + + RemoteAuthData newAuthData {}; + strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), uidS.c_str(), uidS.length()); + strncpy_s(newAuthData.username, sizeof(newAuthData.username), username.c_str(), username.length()); + newAuthData.pdata = new char[pdata.length()]; + newAuthData.pdataSize = pdata.length(); + memcpy(newAuthData.pdata, pdata.c_str(), newAuthData.pdataSize); + + std::lock_guard<std::mutex> guard(m_AuthDataMutex); + m_RemoteAuthenticationData[token] = newAuthData; +} + +void ServerAuthenticationManager::AddPlayer(CBaseClient* pPlayer, const char* pToken) +{ + PlayerAuthenticationData additionalData; + + auto remoteAuthData = m_RemoteAuthenticationData.find(pToken); + if (remoteAuthData != m_RemoteAuthenticationData.end()) + additionalData.pdataSize = remoteAuthData->second.pdataSize; + else + additionalData.pdataSize = PERSISTENCE_MAX_SIZE; + + additionalData.usingLocalPdata = pPlayer->m_iPersistenceReady == ePersistenceReady::READY_INSECURE; + + m_PlayerAuthenticationData.insert(std::make_pair(pPlayer, additionalData)); +} + +void ServerAuthenticationManager::RemovePlayer(CBaseClient* pPlayer) +{ + if (m_PlayerAuthenticationData.count(pPlayer)) + m_PlayerAuthenticationData.erase(pPlayer); +} + +bool ServerAuthenticationManager::VerifyPlayerName(const char* pAuthToken, const char* pName, char pOutVerifiedName[64]) +{ + std::lock_guard<std::mutex> guard(m_AuthDataMutex); + + // always use name from masterserver if available + // use of strncpy_s here should verify that this is always nullterminated within valid buffer size + auto authData = m_RemoteAuthenticationData.find(pAuthToken); + if (authData != m_RemoteAuthenticationData.end() && *authData->second.username) + strncpy_s(pOutVerifiedName, 64, authData->second.username, 63); + else + strncpy_s(pOutVerifiedName, 64, pName, 63); + + // now, check that whatever name we have is actually valid + // first, make sure it's >1 char + if (!*pOutVerifiedName) + return false; + + // next, make sure it's within a valid range of ascii characters + for (int i = 0; pOutVerifiedName[i]; i++) + { + if (pOutVerifiedName[i] < 32 || pOutVerifiedName[i] > 126) + return false; + } + + return true; +} + +bool ServerAuthenticationManager::IsDuplicateAccount(CBaseClient* pPlayer, const char* pPlayerUid) +{ + if (m_bAllowDuplicateAccounts) + return false; + + bool bHasUidPlayer = false; + for (int i = 0; i < g_pGlobals->m_nMaxClients; i++) + if (&g_pClientArray[i] != pPlayer && !strcmp(pPlayerUid, g_pClientArray[i].m_UID)) + return true; + + return false; +} + +bool ServerAuthenticationManager::CheckAuthentication(CBaseClient* pPlayer, uint64_t iUid, char* pAuthToken) +{ + std::string sUid = std::to_string(iUid); + + // check whether this player's authentication is valid but don't actually write anything to the player, we'll do that later + // if we don't need auth this is valid + if (Cvar_ns_auth_allow_insecure->GetBool()) + return true; + + // local server that doesn't need auth (probably sp) and local player + if (m_bStartingLocalSPGame && !strcmp(sUid.c_str(), g_pLocalPlayerUserID)) + return true; + + // don't allow duplicate accounts + if (IsDuplicateAccount(pPlayer, sUid.c_str())) + return false; + + std::lock_guard<std::mutex> guard(m_AuthDataMutex); + auto authData = m_RemoteAuthenticationData.find(pAuthToken); + if (authData != m_RemoteAuthenticationData.end() && !strcmp(sUid.c_str(), authData->second.uid)) + return true; + + return false; +} + +void ServerAuthenticationManager::AuthenticatePlayer(CBaseClient* pPlayer, uint64_t iUid, char* pAuthToken) +{ + // for bot players, generate a new uid + if (pPlayer->m_bFakePlayer) + iUid = 0; // is this a good way of doing things :clueless: + + std::string sUid = std::to_string(iUid); + + // copy uuid + strcpy(pPlayer->m_UID, sUid.c_str()); + + std::lock_guard<std::mutex> guard(m_AuthDataMutex); + auto authData = m_RemoteAuthenticationData.find(pAuthToken); + if (authData != m_RemoteAuthenticationData.end()) + { + // if we're resetting let script handle the reset with InitPersistentData() on connect + if (!m_bForceResetLocalPlayerPersistence || strcmp(sUid.c_str(), g_pLocalPlayerUserID)) + { + // copy pdata into buffer + memcpy(pPlayer->m_PersistenceBuffer, authData->second.pdata, authData->second.pdataSize); + } + + // set persistent data as ready + pPlayer->m_iPersistenceReady = ePersistenceReady::READY_REMOTE; + } + // we probably allow insecure at this point, but make sure not to write anyway if not insecure + else if (Cvar_ns_auth_allow_insecure->GetBool() || pPlayer->m_bFakePlayer) + { + // set persistent data as ready + // note: actual placeholder persistent data is populated in script with InitPersistentData() + pPlayer->m_iPersistenceReady = ePersistenceReady::READY_INSECURE; + } +} + +bool ServerAuthenticationManager::RemovePlayerAuthData(CBaseClient* pPlayer) +{ + if (!Cvar_ns_erase_auth_info->GetBool()) // keep auth data forever + return false; + + // hack for special case where we're on a local server, so we erase our own newly created auth data on disconnect + if (m_bNeedLocalAuthForNewgame && !strcmp(pPlayer->m_UID, g_pLocalPlayerUserID)) + return false; + + // we don't have our auth token at this point, so lookup authdata by uid + for (auto& auth : m_RemoteAuthenticationData) + { + if (!strcmp(pPlayer->m_UID, auth.second.uid)) + { + // pretty sure this is fine, since we don't iterate after the erase + // i think if we iterated after it'd be undefined behaviour tho + std::lock_guard<std::mutex> guard(m_AuthDataMutex); + + delete[] auth.second.pdata; + m_RemoteAuthenticationData.erase(auth.first); + return true; + } + } + + return false; +} + +void ServerAuthenticationManager::WritePersistentData(CBaseClient* pPlayer) +{ + if (pPlayer->m_iPersistenceReady == ePersistenceReady::READY_REMOTE) + { + g_pMasterServerManager->WritePlayerPersistentData( + pPlayer->m_UID, (const char*)pPlayer->m_PersistenceBuffer, m_PlayerAuthenticationData[pPlayer].pdataSize); + } + else if (Cvar_ns_auth_allow_insecure_write->GetBool()) + { + // todo: write pdata to disk here + } +} + +// auth hooks + +// store these in vars so we can use them in CBaseClient::Connect +// this is fine because ptrs won't decay by the time we use this, just don't use it outside of calls from cbaseclient::connectclient +char* pNextPlayerToken; +uint64_t iNextPlayerUid; + +// clang-format off +AUTOHOOK(CBaseServer__ConnectClient, engine.dll + 0x114430, +void*,, ( + void* self, + void* addr, + void* a3, + uint32_t a4, + uint32_t a5, + int32_t a6, + void* a7, + char* playerName, + char* serverFilter, + void* a10, + char a11, + void* a12, + char a13, + char a14, + int64_t uid, + uint32_t a16, + uint32_t a17)) +// clang-format on +{ + // auth tokens are sent with serverfilter, can't be accessed from player struct to my knowledge, so have to do this here + pNextPlayerToken = serverFilter; + iNextPlayerUid = uid; + + return CBaseServer__ConnectClient(self, addr, a3, a4, a5, a6, a7, playerName, serverFilter, a10, a11, a12, a13, a14, uid, a16, a17); +} + +ConVar* Cvar_ns_allowuserclantags; + +// clang-format off +AUTOHOOK(CBaseClient__Connect, engine.dll + 0x101740, +bool,, (CBaseClient* self, char* pName, void* pNetChannel, char bFakePlayer, void* a5, char pDisconnectReason[256], void* a7)) +// clang-format on +{ + const char* pAuthenticationFailure = nullptr; + char pVerifiedName[64]; + + if (!bFakePlayer) + { + if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, pName, pVerifiedName)) + pAuthenticationFailure = "Invalid Name."; + else if (!g_pBanSystem->IsUIDAllowed(iNextPlayerUid)) + pAuthenticationFailure = "Banned From server."; + else if (!g_pServerAuthentication->CheckAuthentication(self, iNextPlayerUid, pNextPlayerToken)) + pAuthenticationFailure = "Authentication Failed."; + } + else // need to copy name for bots still + strncpy_s(pVerifiedName, pName, 63); + + if (pAuthenticationFailure) + { + spdlog::info("{}'s (uid {}) connection was rejected: \"{}\"", pName, iNextPlayerUid, pAuthenticationFailure); + + strncpy_s(pDisconnectReason, 256, pAuthenticationFailure, 255); + return false; + } + + // try to actually connect the player + if (!CBaseClient__Connect(self, pVerifiedName, pNetChannel, bFakePlayer, a5, pDisconnectReason, a7)) + return false; + + // we already know this player's authentication data is legit, actually write it to them now + g_pServerAuthentication->AuthenticatePlayer(self, iNextPlayerUid, pNextPlayerToken); + + g_pServerAuthentication->AddPlayer(self, pNextPlayerToken); + g_pServerLimits->AddPlayer(self); + + return true; +} + +// clang-format off +AUTOHOOK(CBaseClient__ActivatePlayer, engine.dll + 0x100F80, +void,, (CBaseClient* self)) +// clang-format on +{ + // if we're authed, write our persistent data + // RemovePlayerAuthData returns true if it removed successfully, i.e. on first call only, and we only want to write on >= second call + // (since this func is called on map loads) + if (self->m_iPersistenceReady >= ePersistenceReady::READY && !g_pServerAuthentication->RemovePlayerAuthData(self)) + { + g_pServerAuthentication->m_bForceResetLocalPlayerPersistence = false; + g_pServerAuthentication->WritePersistentData(self); + g_pServerPresence->SetPlayerCount(g_pServerAuthentication->m_PlayerAuthenticationData.size()); + } + + CBaseClient__ActivatePlayer(self); +} + +// clang-format off +AUTOHOOK(_CBaseClient__Disconnect, engine.dll + 0x1012C0, +void,, (CBaseClient* self, uint32_t unknownButAlways1, const char* pReason, ...)) +// clang-format on +{ + // have to manually format message because can't pass varargs to original func + char buf[1024]; + + va_list va; + va_start(va, pReason); + vsprintf(buf, pReason, va); + va_end(va); + + // this reason is used while connecting to a local server, hacky, but just ignore it + if (strcmp(pReason, "Connection closing")) + { + spdlog::info("Player {} disconnected: \"{}\"", self->m_Name, buf); + + // dcing, write persistent data + if (g_pServerAuthentication->m_PlayerAuthenticationData[self].needPersistenceWriteOnLeave) + g_pServerAuthentication->WritePersistentData(self); + + memset(self->m_PersistenceBuffer, 0, g_pServerAuthentication->m_PlayerAuthenticationData[self].pdataSize); + g_pServerAuthentication->RemovePlayerAuthData(self); // won't do anything 99% of the time, but just in case + + g_pServerAuthentication->RemovePlayer(self); + g_pServerLimits->RemovePlayer(self); + } + + g_pServerPresence->SetPlayerCount(g_pServerAuthentication->m_PlayerAuthenticationData.size()); + + _CBaseClient__Disconnect(self, unknownButAlways1, buf); +} + +void ConCommand_ns_resetpersistence(const CCommand& args) +{ + if (*g_pServerState == server_state_t::ss_active) + { + spdlog::error("ns_resetpersistence must be entered from the main menu"); + return; + } + + spdlog::info("resetting persistence on next lobby load..."); + g_pServerAuthentication->m_bForceResetLocalPlayerPersistence = true; +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerAuthentication, (ConCommand, ConVar), (CModule module)) +{ + AUTOHOOK_DISPATCH() + + g_pServerAuthentication = new ServerAuthenticationManager; + + g_pServerAuthentication->Cvar_ns_erase_auth_info = + new ConVar("ns_erase_auth_info", "1", FCVAR_GAMEDLL, "Whether auth info should be erased from this server on disconnect or crash"); + g_pServerAuthentication->Cvar_ns_auth_allow_insecure = + new ConVar("ns_auth_allow_insecure", "0", FCVAR_GAMEDLL, "Whether this server will allow unauthenicated players to connect"); + g_pServerAuthentication->Cvar_ns_auth_allow_insecure_write = new ConVar( + "ns_auth_allow_insecure_write", + "0", + FCVAR_GAMEDLL, + "Whether the pdata of unauthenticated clients will be written to disk when changed"); + + RegisterConCommand( + "ns_resetpersistence", ConCommand_ns_resetpersistence, "resets your pdata when you next enter the lobby", FCVAR_NONE); + + // patch to disable kicking based on incorrect serverfilter in connectclient, since we repurpose it for use as an auth token + module.Offset(0x114655).Patch("EB"); + + // patch to disable fairfight marking players as cheaters and kicking them + module.Offset(0x101012).Patch("E9 90 00"); + + CBaseServer__RejectConnection = module.Offset(0x1182E0).RCast<CBaseServer__RejectConnectionType>(); + + if (CommandLine()->CheckParm("-allowdupeaccounts")) + { + // patch to allow same of multiple account + module.Offset(0x114510).Patch("EB"); + + g_pServerAuthentication->m_bAllowDuplicateAccounts = true; + } +} diff --git a/primedev/server/auth/serverauthentication.h b/primedev/server/auth/serverauthentication.h new file mode 100644 index 00000000..996d20e1 --- /dev/null +++ b/primedev/server/auth/serverauthentication.h @@ -0,0 +1,58 @@ +#pragma once +#include "core/convar/convar.h" +#include "engine/r2engine.h" +#include <unordered_map> +#include <string> + +struct RemoteAuthData +{ + char uid[33]; + char username[64]; + + // pdata + char* pdata; + size_t pdataSize; +}; + +struct PlayerAuthenticationData +{ + bool usingLocalPdata; + size_t pdataSize; + bool needPersistenceWriteOnLeave = true; +}; + +typedef int64_t (*CBaseServer__RejectConnectionType)(void* a1, unsigned int a2, void* a3, const char* a4, ...); +extern CBaseServer__RejectConnectionType CBaseServer__RejectConnection; + +class ServerAuthenticationManager +{ +public: + ConVar* Cvar_ns_erase_auth_info; + ConVar* Cvar_ns_auth_allow_insecure; + ConVar* Cvar_ns_auth_allow_insecure_write; + + std::mutex m_AuthDataMutex; + std::unordered_map<std::string, RemoteAuthData> m_RemoteAuthenticationData; + std::unordered_map<CBaseClient*, PlayerAuthenticationData> m_PlayerAuthenticationData; + + bool m_bAllowDuplicateAccounts = false; + bool m_bNeedLocalAuthForNewgame = false; + bool m_bForceResetLocalPlayerPersistence = false; + bool m_bStartingLocalSPGame = false; + +public: + void AddRemotePlayer(std::string token, uint64_t uid, std::string username, std::string pdata); + + void AddPlayer(CBaseClient* pPlayer, const char* pAuthToken); + void RemovePlayer(CBaseClient* pPlayer); + + bool VerifyPlayerName(const char* pAuthToken, const char* pName, char pOutVerifiedName[64]); + bool IsDuplicateAccount(CBaseClient* pPlayer, const char* pUid); + bool CheckAuthentication(CBaseClient* pPlayer, uint64_t iUid, char* pAuthToken); + + void AuthenticatePlayer(CBaseClient* pPlayer, uint64_t iUid, char* pAuthToken); + bool RemovePlayerAuthData(CBaseClient* pPlayer); + void WritePersistentData(CBaseClient* pPlayer); +}; + +extern ServerAuthenticationManager* g_pServerAuthentication; diff --git a/primedev/server/buildainfile.cpp b/primedev/server/buildainfile.cpp new file mode 100644 index 00000000..a7f59961 --- /dev/null +++ b/primedev/server/buildainfile.cpp @@ -0,0 +1,395 @@ +#include "core/convar/convar.h" +#include "engine/hoststate.h" +#include "engine/r2engine.h" + +#include <fstream> +#include <filesystem> + +AUTOHOOK_INIT() + +const int AINET_VERSION_NUMBER = 57; +const int AINET_SCRIPT_VERSION_NUMBER = 21; +const int PLACEHOLDER_CRC = 0; +const int MAX_HULLS = 5; + +#pragma pack(push, 1) +struct CAI_NodeLink +{ + short srcId; + short destId; + bool hulls[MAX_HULLS]; + char unk0; + char unk1; // maps => unk0 on disk + char unk2[5]; + int64_t flags; +}; +#pragma pack(pop) + +#pragma pack(push, 1) +struct CAI_NodeLinkDisk +{ + short srcId; + short destId; + char unk0; + bool hulls[MAX_HULLS]; +}; +#pragma pack(pop) + +#pragma pack(push, 1) +struct CAI_Node +{ + int index; // not present on disk + float x; + float y; + float z; + float hulls[MAX_HULLS]; + float yaw; + + int unk0; // always 2 in buildainfile, maps directly to unk0 in disk struct + int unk1; // maps directly to unk1 in disk struct + int unk2[MAX_HULLS]; // maps directly to unk2 in disk struct, despite being ints rather than shorts + + // view server.dll+393672 for context and death wish + char unk3[MAX_HULLS]; // hell on earth, should map to unk3 on disk + char pad[3]; // aligns next bytes + float unk4[MAX_HULLS]; // i have no fucking clue, calculated using some kind of demon hell function float magic + + CAI_NodeLink** links; + char unk5[16]; + int linkcount; + int unk11; // bad name lmao + short unk6; // should match up to unk4 on disk + char unk7[16]; // padding until next bit + short unk8; // should match up to unk5 on disk + char unk9[8]; // padding until next bit + char unk10[8]; // should match up to unk6 on disk +}; +#pragma pack(pop) + +// the way CAI_Nodes are represented in on-disk ain files +#pragma pack(push, 1) +struct CAI_NodeDisk +{ + float x; + float y; + float z; + float yaw; + float hulls[MAX_HULLS]; + + char unk0; + int unk1; + short unk2[MAX_HULLS]; + char unk3[MAX_HULLS]; + short unk4; + short unk5; + char unk6[8]; +}; // total size of 68 bytes +#pragma pack(pop) + +#pragma pack(push, 1) +struct UnkNodeStruct0 +{ + int index; + char unk0; + char unk1; // maps to unk1 on disk + char pad0[2]; // padding to +8 + + float x; + float y; + float z; + + char pad5[4]; + int* unk2; // maps to unk5 on disk; + char pad1[16]; // pad to +48 + int unkcount0; // maps to unkcount0 on disk + + char pad2[4]; // pad to +56 + int* unk3; + char pad3[16]; // pad to +80 + int unkcount1; + + char pad4[132]; + char unk5; +}; +#pragma pack(pop) + +int* pUnkStruct0Count; +UnkNodeStruct0*** pppUnkNodeStruct0s; + +#pragma pack(push, 1) +struct UnkLinkStruct1 +{ + short unk0; + short unk1; + int unk2; + char unk3; + char unk4; + char unk5; +}; +#pragma pack(pop) + +int* pUnkLinkStruct1Count; +UnkLinkStruct1*** pppUnkStruct1s; + +#pragma pack(push, 1) +struct CAI_ScriptNode +{ + float x; + float y; + float z; + uint64_t scriptdata; +}; +#pragma pack(pop) + +#pragma pack(push, 1) +struct CAI_Network +{ + // +0 + char unk0[8]; + // +8 + int linkcount; // this is uninitialised and never set on ain build, fun! + // +12 + char unk1[124]; + // +136 + int zonecount; + // +140 + char unk2[16]; + // +156 + int unk5; // unk8 on disk + // +160 + char unk6[4]; + // +164 + int hintcount; + // +168 + short hints[2000]; // these probably aren't actually hints, but there's 1 of them per hint so idk + // +4168 + int scriptnodecount; + // +4172 + CAI_ScriptNode scriptnodes[4000]; + // +84172 + int nodecount; + // +84176 + CAI_Node** nodes; +}; +#pragma pack(pop) + +ConVar* Cvar_ns_ai_dumpAINfileFromLoad; + +void DumpAINInfo(CAI_Network* aiNetwork) +{ + fs::path writePath(fmt::format("{}/maps/graphs", g_pModName)); + writePath /= g_pGlobals->m_pMapName; + writePath += ".ain"; + + // dump from memory + spdlog::info("writing ain file {}", writePath.string()); + spdlog::info(""); + spdlog::info(""); + spdlog::info(""); + spdlog::info(""); + spdlog::info(""); + + std::ofstream writeStream(writePath, std::ofstream::binary); + spdlog::info("writing ainet version: {}", AINET_VERSION_NUMBER); + writeStream.write((char*)&AINET_VERSION_NUMBER, sizeof(int)); + + int mapVersion = g_pGlobals->m_nMapVersion; + spdlog::info("writing map version: {}", mapVersion); + writeStream.write((char*)&mapVersion, sizeof(int)); + spdlog::info("writing placeholder crc: {}", PLACEHOLDER_CRC); + writeStream.write((char*)&PLACEHOLDER_CRC, sizeof(int)); + + int calculatedLinkcount = 0; + + // path nodes + spdlog::info("writing nodecount: {}", aiNetwork->nodecount); + writeStream.write((char*)&aiNetwork->nodecount, sizeof(int)); + + for (int i = 0; i < aiNetwork->nodecount; i++) + { + // construct on-disk node struct + CAI_NodeDisk diskNode; + diskNode.x = aiNetwork->nodes[i]->x; + diskNode.y = aiNetwork->nodes[i]->y; + diskNode.z = aiNetwork->nodes[i]->z; + diskNode.yaw = aiNetwork->nodes[i]->yaw; + memcpy(diskNode.hulls, aiNetwork->nodes[i]->hulls, sizeof(diskNode.hulls)); + diskNode.unk0 = (char)aiNetwork->nodes[i]->unk0; + diskNode.unk1 = aiNetwork->nodes[i]->unk1; + + for (int j = 0; j < MAX_HULLS; j++) + { + diskNode.unk2[j] = (short)aiNetwork->nodes[i]->unk2[j]; + spdlog::info((short)aiNetwork->nodes[i]->unk2[j]); + } + + memcpy(diskNode.unk3, aiNetwork->nodes[i]->unk3, sizeof(diskNode.unk3)); + diskNode.unk4 = aiNetwork->nodes[i]->unk6; + diskNode.unk5 = + -1; // aiNetwork->nodes[i]->unk8; // this field is wrong, however, it's always -1 in vanilla navmeshes anyway, so no biggie + memcpy(diskNode.unk6, aiNetwork->nodes[i]->unk10, sizeof(diskNode.unk6)); + + spdlog::info("writing node {} from {} to {:x}", aiNetwork->nodes[i]->index, (void*)aiNetwork->nodes[i], writeStream.tellp()); + writeStream.write((char*)&diskNode, sizeof(CAI_NodeDisk)); + + calculatedLinkcount += aiNetwork->nodes[i]->linkcount; + } + + // links + spdlog::info("linkcount: {}", aiNetwork->linkcount); + spdlog::info("calculated total linkcount: {}", calculatedLinkcount); + + calculatedLinkcount /= 2; + if (Cvar_ns_ai_dumpAINfileFromLoad->GetBool()) + { + if (aiNetwork->linkcount == calculatedLinkcount) + spdlog::info("caculated linkcount is normal!"); + else + spdlog::warn("calculated linkcount has weird value! this is expected on build!"); + } + + spdlog::info("writing linkcount: {}", calculatedLinkcount); + writeStream.write((char*)&calculatedLinkcount, sizeof(int)); + + for (int i = 0; i < aiNetwork->nodecount; i++) + { + for (int j = 0; j < aiNetwork->nodes[i]->linkcount; j++) + { + // skip links that don't originate from current node + if (aiNetwork->nodes[i]->links[j]->srcId != aiNetwork->nodes[i]->index) + continue; + + CAI_NodeLinkDisk diskLink; + diskLink.srcId = aiNetwork->nodes[i]->links[j]->srcId; + diskLink.destId = aiNetwork->nodes[i]->links[j]->destId; + diskLink.unk0 = aiNetwork->nodes[i]->links[j]->unk1; + memcpy(diskLink.hulls, aiNetwork->nodes[i]->links[j]->hulls, sizeof(diskLink.hulls)); + + spdlog::info("writing link {} => {} to {:x}", diskLink.srcId, diskLink.destId, writeStream.tellp()); + writeStream.write((char*)&diskLink, sizeof(CAI_NodeLinkDisk)); + } + } + + // don't know what this is, it's likely a block from tf1 that got deprecated? should just be 1 int per node + spdlog::info("writing {:x} bytes for unknown block at {:x}", aiNetwork->nodecount * sizeof(uint32_t), writeStream.tellp()); + uint32_t* unkNodeBlock = new uint32_t[aiNetwork->nodecount]; + memset(unkNodeBlock, 0, aiNetwork->nodecount * sizeof(uint32_t)); + writeStream.write((char*)unkNodeBlock, aiNetwork->nodecount * sizeof(uint32_t)); + delete[] unkNodeBlock; + + // TODO: this is traverse nodes i think? these aren't used in tf2 ains so we can get away with just writing count=0 and skipping + // but ideally should actually dump these + spdlog::info("writing {} traversal nodes at {:x}...", 0, writeStream.tellp()); + short traverseNodeCount = 0; + writeStream.write((char*)&traverseNodeCount, sizeof(short)); + // only write count since count=0 means we don't have to actually do anything here + + // TODO: ideally these should be actually dumped, but they're always 0 in tf2 from what i can tell + spdlog::info("writing {} bytes for unknown hull block at {:x}", MAX_HULLS * 8, writeStream.tellp()); + char* unkHullBlock = new char[MAX_HULLS * 8]; + memset(unkHullBlock, 0, MAX_HULLS * 8); + writeStream.write(unkHullBlock, MAX_HULLS * 8); + delete[] unkHullBlock; + + // unknown struct that's seemingly node-related + spdlog::info("writing {} unknown node structs at {:x}", *pUnkStruct0Count, writeStream.tellp()); + writeStream.write((char*)pUnkStruct0Count, sizeof(*pUnkStruct0Count)); + for (int i = 0; i < *pUnkStruct0Count; i++) + { + spdlog::info("writing unknown node struct {} at {:x}", i, writeStream.tellp()); + UnkNodeStruct0* nodeStruct = (*pppUnkNodeStruct0s)[i]; + + writeStream.write((char*)&nodeStruct->index, sizeof(nodeStruct->index)); + writeStream.write((char*)&nodeStruct->unk1, sizeof(nodeStruct->unk1)); + + writeStream.write((char*)&nodeStruct->x, sizeof(nodeStruct->x)); + writeStream.write((char*)&nodeStruct->y, sizeof(nodeStruct->y)); + writeStream.write((char*)&nodeStruct->z, sizeof(nodeStruct->z)); + + writeStream.write((char*)&nodeStruct->unkcount0, sizeof(nodeStruct->unkcount0)); + for (int j = 0; j < nodeStruct->unkcount0; j++) + { + short unk2Short = (short)nodeStruct->unk2[j]; + writeStream.write((char*)&unk2Short, sizeof(unk2Short)); + } + + writeStream.write((char*)&nodeStruct->unkcount1, sizeof(nodeStruct->unkcount1)); + for (int j = 0; j < nodeStruct->unkcount1; j++) + { + short unk3Short = (short)nodeStruct->unk3[j]; + writeStream.write((char*)&unk3Short, sizeof(unk3Short)); + } + + writeStream.write((char*)&nodeStruct->unk5, sizeof(nodeStruct->unk5)); + } + + // unknown struct that's seemingly link-related + spdlog::info("writing {} unknown link structs at {:x}", *pUnkLinkStruct1Count, writeStream.tellp()); + writeStream.write((char*)pUnkLinkStruct1Count, sizeof(*pUnkLinkStruct1Count)); + for (int i = 0; i < *pUnkLinkStruct1Count; i++) + { + // disk and memory structs are literally identical here so just directly write + spdlog::info("writing unknown link struct {} at {:x}", i, writeStream.tellp()); + writeStream.write((char*)(*pppUnkStruct1s)[i], sizeof(*(*pppUnkStruct1s)[i])); + } + + // some weird int idk what this is used for + writeStream.write((char*)&aiNetwork->unk5, sizeof(aiNetwork->unk5)); + + // tf2-exclusive stuff past this point, i.e. ain v57 only + spdlog::info("writing {} script nodes at {:x}", aiNetwork->scriptnodecount, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->scriptnodecount, sizeof(aiNetwork->scriptnodecount)); + for (int i = 0; i < aiNetwork->scriptnodecount; i++) + { + // disk and memory structs are literally identical here so just directly write + spdlog::info("writing script node {} at {:x}", i, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->scriptnodes[i], sizeof(aiNetwork->scriptnodes[i])); + } + + spdlog::info("writing {} hints at {:x}", aiNetwork->hintcount, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->hintcount, sizeof(aiNetwork->hintcount)); + for (int i = 0; i < aiNetwork->hintcount; i++) + { + spdlog::info("writing hint data {} at {:x}", i, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->hints[i], sizeof(aiNetwork->hints[i])); + } + + writeStream.close(); +} + +// clang-format off +AUTOHOOK(CAI_NetworkBuilder__Build, server.dll + 0x385E20, +void, __fastcall, (void* builder, CAI_Network* aiNetwork, void* unknown)) +// clang-format on +{ + CAI_NetworkBuilder__Build(builder, aiNetwork, unknown); + + DumpAINInfo(aiNetwork); +} + +// clang-format off +AUTOHOOK(LoadAINFile, server.dll + 0x3933A0, +void, __fastcall, (void* aimanager, void* buf, const char* filename)) +// clang-format on +{ + LoadAINFile(aimanager, buf, filename); + + if (Cvar_ns_ai_dumpAINfileFromLoad->GetBool()) + { + spdlog::info("running DumpAINInfo for loaded file {}", filename); + DumpAINInfo(*(CAI_Network**)((char*)aimanager + 2536)); + } +} + +ON_DLL_LOAD("server.dll", BuildAINFile, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + Cvar_ns_ai_dumpAINfileFromLoad = new ConVar( + "ns_ai_dumpAINfileFromLoad", "0", FCVAR_NONE, "For debugging: whether we should dump ain data for ains loaded from disk"); + + pUnkStruct0Count = module.Offset(0x1063BF8).RCast<int*>(); + pppUnkNodeStruct0s = module.Offset(0x1063BE0).RCast<UnkNodeStruct0***>(); + pUnkLinkStruct1Count = module.Offset(0x1063AA8).RCast<int*>(); + pppUnkStruct1s = module.Offset(0x1063A90).RCast<UnkLinkStruct1***>(); +} diff --git a/primedev/server/r2server.cpp b/primedev/server/r2server.cpp new file mode 100644 index 00000000..c52f396e --- /dev/null +++ b/primedev/server/r2server.cpp @@ -0,0 +1,10 @@ +#include "r2server.h" + +CBaseEntity* (*Server_GetEntityByIndex)(int index); +CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex); + +ON_DLL_LOAD("server.dll", R2GameServer, (CModule module)) +{ + Server_GetEntityByIndex = module.Offset(0xFB820).RCast<CBaseEntity* (*)(int)>(); + UTIL_PlayerByIndex = module.Offset(0x26AA10).RCast<CBasePlayer*(__fastcall*)(int)>(); +} diff --git a/primedev/server/r2server.h b/primedev/server/r2server.h new file mode 100644 index 00000000..c40cdc1f --- /dev/null +++ b/primedev/server/r2server.h @@ -0,0 +1,106 @@ +#pragma once + +#include "core/math/vector.h" + +// server entity stuff +class CBaseEntity; +extern CBaseEntity* (*Server_GetEntityByIndex)(int index); + +// clang-format off +OFFSET_STRUCT(CBasePlayer) +{ + FIELD(0x58, uint32_t m_nPlayerIndex) + + FIELD(0x23E8, bool m_grappleActive) + FIELD(0x1D08, uint32_t m_platformUserId) + FIELD(0x1D10, int32_t m_classModsActive) + FIELD(0x1D8C, int32_t m_posClassModsActive) + FIELD(0x1DCC, bool m_passives) + FIELD(0x4948, int32_t m_selectedOffhand) + FIELD(0x1358, int32_t m_selectedOffhandPendingHybridAction) + FIELD(0x1E88, int32_t m_playerFlags) + FIELD(0x26A8, int32_t m_lastUCmdSimulationTicks) + FIELD(0x26AC, float m_lastUCmdSimulationRemainderTime) + FIELD(0x1F04, int32_t m_remoteTurret) + FIELD(0x414, int32_t m_hGroundEntity) + FIELD(0x13B8, int32_t m_titanSoul) + FIELD(0x2054, int32_t m_petTitan) + FIELD(0x4D4, int32_t m_iHealth) + FIELD(0x4D0, int32_t m_iMaxHealth) + FIELD(0x4F1, int32_t m_lifeState) + FIELD(0x50C, float m_flMaxspeed) + FIELD(0x298, int32_t m_fFlags) + FIELD(0x1F64, int32_t m_iObserverMode) + FIELD(0x1F6C, int32_t m_hObserverTarget) + FIELD(0x2098, int32_t m_hViewModel) + FIELD(0x27E4, int32_t m_ubEFNointerpParity) + FIELD(0x1FA4, int32_t m_activeBurnCardIndex) + FIELD(0x1B68, int32_t m_hColorCorrectionCtrl) + FIELD(0x19E0, int32_t m_PlayerFog__m_hCtrl) + FIELD(0x26BC, bool m_bShouldDrawPlayerWhileUsingViewEntity) + FIELD(0x2848, char m_title[32]) + FIELD(0x2964, bool m_useCredit) + FIELD(0x1F40, float m_damageImpulseNoDecelEndTime) + FIELD(0x1E8C, bool m_hasMic) + FIELD(0x1E8D, bool m_inPartyChat) + FIELD(0x1E90, float m_playerMoveSpeedScale) + FIELD(0x1F58, float m_flDeathTime) + FIELD(0x25A8, bool m_iSpawnParity) + FIELD(0x102284, Vector3 m_upDir) + FIELD(0x259C, float m_lastDodgeTime) + FIELD(0x22E0, bool m_wallHanging) + FIELD(0x22EC, int32_t m_traversalType) + FIELD(0x22F0, int32_t m_traversalState) + FIELD(0x2328, Vector3 m_traversalRefPos) + FIELD(0x231C, Vector3 m_traversalForwardDir) + FIELD(0x2354, float m_traversalYawDelta) + FIELD(0x2358, int32_t m_traversalYawPoseParameter) + FIELD(0x2050, int32_t m_grappleHook) + FIELD(0x27C0, int32_t m_autoSprintForced) + FIELD(0x27C4, bool m_fIsSprinting) + FIELD(0x27CC, float m_sprintStartedTime) + FIELD(0x27D0, float m_sprintStartedFrac) + FIELD(0x27D4, float m_sprintEndedTime) + FIELD(0x27D8, float m_sprintEndedFrac) + FIELD(0x27DC, float m_stickySprintStartTime) + FIELD(0x2998, float m_smartAmmoPreviousHighestLockOnMeFractionValue) + FIELD(0x23FC, int32_t m_activeZipline) + FIELD(0x2400, bool m_ziplineReverse) + FIELD(0x2410, int32_t m_ziplineState) + FIELD(0x2250, int32_t m_duckState) + FIELD(0x2254, Vector3 m_StandHullMin) + FIELD(0x2260, Vector3 m_StandHullMax) + FIELD(0x226C, Vector3 m_DuckHullMin) + FIELD(0x2278, Vector3 m_DuckHullMax) + FIELD(0x205C, int32_t m_xp) + FIELD(0x2060, int32_t m_generation) + FIELD(0x2064, int32_t m_rank) + FIELD(0x2068, int32_t m_serverForceIncreasePlayerListGenerationParity) + FIELD(0x206C, bool m_isPlayingRanked) + FIELD(0x2070, float m_skill_mu) + FIELD(0x1E80, int32_t m_titanSoulBeingRodeoed) + FIELD(0x1E84, int32_t m_entitySyncingWithMe) + FIELD(0x2078, float m_nextTitanRespawnAvailable) + FIELD(0x1C90, bool m_hasBadReputation) + FIELD(0x1C91, char m_communityName[64]) + FIELD(0x1CD1, char m_communityClanTag[16]) + FIELD(0x1CE1, char m_factionName[16]) + FIELD(0x1CF1, char m_hardwareIcon[16]) + FIELD(0x1D01, bool m_happyHourActive) + FIELD(0x1EF4, int32_t m_gestureAutoKillBitfield) + FIELD(0x2EA8, int32_t m_pilotClassIndex) + FIELD(0x100490, Vector3 m_vecAbsOrigin) + FIELD(0x25BE, bool m_isPerformingBoostAction) + FIELD(0x240C, bool m_ziplineValid3pWeaponLayerAnim) + FIELD(0x345C, int32_t m_playerScriptNetDataGlobal) + FIELD(0x1598, int32_t m_bZooming) + FIELD(0x1599, bool m_zoomToggleOn) + FIELD(0x159C, float m_zoomBaseFrac) + FIELD(0x15A0, float m_zoomBaseTime) + FIELD(0x15A4, float m_zoomFullStartTime) + FIELD(0xA04, int32_t m_camoIndex) + FIELD(0xA08, int32_t m_decalIndex) +}; +// clang-format on + +extern CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex); diff --git a/primedev/server/serverchathooks.cpp b/primedev/server/serverchathooks.cpp new file mode 100644 index 00000000..d3ac4776 --- /dev/null +++ b/primedev/server/serverchathooks.cpp @@ -0,0 +1,174 @@ +#include "serverchathooks.h" +#include "shared/exploit_fixes/ns_limits.h" +#include "squirrel/squirrel.h" +#include "server/r2server.h" +#include "util/utils.h" + +#include <rapidjson/document.h> +#include <rapidjson/stringbuffer.h> +#include <rapidjson/writer.h> + +AUTOHOOK_INIT() + +class CServerGameDLL; + +class CRecipientFilter +{ + char unknown[58]; +}; + +CServerGameDLL* g_pServerGameDLL; + +void(__fastcall* CServerGameDLL__OnReceivedSayTextMessage)( + CServerGameDLL* self, unsigned int senderPlayerId, const char* text, int channelId); + +void(__fastcall* CRecipientFilter__Construct)(CRecipientFilter* self); +void(__fastcall* CRecipientFilter__Destruct)(CRecipientFilter* self); +void(__fastcall* CRecipientFilter__AddAllPlayers)(CRecipientFilter* self); +void(__fastcall* CRecipientFilter__AddRecipient)(CRecipientFilter* self, const CBasePlayer* player); +void(__fastcall* CRecipientFilter__MakeReliable)(CRecipientFilter* self); + +void(__fastcall* UserMessageBegin)(CRecipientFilter* filter, const char* messagename); +void(__fastcall* MessageEnd)(); +void(__fastcall* MessageWriteByte)(int iValue); +void(__fastcall* MessageWriteString)(const char* sz); +void(__fastcall* MessageWriteBool)(bool bValue); + +bool bShouldCallSayTextHook = false; +// clang-format off +AUTOHOOK(_CServerGameDLL__OnReceivedSayTextMessage, server.dll + 0x1595C0, +void, __fastcall, (CServerGameDLL* self, unsigned int senderPlayerId, const char* text, bool isTeam)) +// clang-format on +{ + RemoveAsciiControlSequences(const_cast<char*>(text), true); + + // MiniHook doesn't allow calling the base function outside of anywhere but the hook function. + // To allow bypassing the hook, isSkippingHook can be set. + if (bShouldCallSayTextHook) + { + bShouldCallSayTextHook = false; + _CServerGameDLL__OnReceivedSayTextMessage(self, senderPlayerId, text, isTeam); + return; + } + + // check chat ratelimits + if (!g_pServerLimits->CheckChatLimits(&g_pClientArray[senderPlayerId - 1])) + return; + + SQRESULT result = g_pSquirrel<ScriptContext::SERVER>->Call( + "CServerGameDLL_ProcessMessageStartThread", static_cast<int>(senderPlayerId) - 1, text, isTeam); + + if (result == SQRESULT_ERROR) + _CServerGameDLL__OnReceivedSayTextMessage(self, senderPlayerId, text, isTeam); +} + +void ChatSendMessage(unsigned int playerIndex, const char* text, bool isTeam) +{ + bShouldCallSayTextHook = true; + CServerGameDLL__OnReceivedSayTextMessage( + g_pServerGameDLL, + // Ensure the first bit isn't set, since this indicates a custom message + (playerIndex + 1) & CUSTOM_MESSAGE_INDEX_MASK, + text, + isTeam); +} + +void ChatBroadcastMessage(int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType) +{ + CBasePlayer* toPlayer = NULL; + if (toPlayerIndex >= 0) + { + toPlayer = UTIL_PlayerByIndex(toPlayerIndex + 1); + if (toPlayer == NULL) + return; + } + + // Build a new string where the first byte is the message type + char sendText[256]; + sendText[0] = (char)messageType; + strncpy_s(sendText + 1, 255, text, 254); + + // Anonymous custom messages use playerId=0, non-anonymous ones use a player ID with the first bit set + unsigned int fromPlayerId = fromPlayerIndex < 0 ? 0 : ((fromPlayerIndex + 1) | CUSTOM_MESSAGE_INDEX_BIT); + + CRecipientFilter filter; + CRecipientFilter__Construct(&filter); + if (toPlayer == NULL) + { + CRecipientFilter__AddAllPlayers(&filter); + } + else + { + CRecipientFilter__AddRecipient(&filter, toPlayer); + } + CRecipientFilter__MakeReliable(&filter); + + UserMessageBegin(&filter, "SayText"); + MessageWriteByte(fromPlayerId); + MessageWriteString(sendText); + MessageWriteBool(isTeam); + MessageWriteBool(isDead); + MessageEnd(); + + CRecipientFilter__Destruct(&filter); +} + +ADD_SQFUNC("void", NSSendMessage, "int playerIndex, string text, bool isTeam", "", ScriptContext::SERVER) +{ + int playerIndex = g_pSquirrel<ScriptContext::SERVER>->getinteger(sqvm, 1); + const char* text = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 2); + bool isTeam = g_pSquirrel<ScriptContext::SERVER>->getbool(sqvm, 3); + + ChatSendMessage(playerIndex, text, isTeam); + + return SQRESULT_NULL; +} + +ADD_SQFUNC( + "void", + NSBroadcastMessage, + "int fromPlayerIndex, int toPlayerIndex, string text, bool isTeam, bool isDead, int messageType", + "", + ScriptContext::SERVER) +{ + int fromPlayerIndex = g_pSquirrel<ScriptContext::SERVER>->getinteger(sqvm, 1); + int toPlayerIndex = g_pSquirrel<ScriptContext::SERVER>->getinteger(sqvm, 2); + const char* text = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 3); + bool isTeam = g_pSquirrel<ScriptContext::SERVER>->getbool(sqvm, 4); + bool isDead = g_pSquirrel<ScriptContext::SERVER>->getbool(sqvm, 5); + int messageType = g_pSquirrel<ScriptContext::SERVER>->getinteger(sqvm, 6); + + if (messageType < 1) + { + g_pSquirrel<ScriptContext::SERVER>->raiseerror(sqvm, fmt::format("Invalid message type {}", messageType).c_str()); + return SQRESULT_ERROR; + } + + ChatBroadcastMessage(fromPlayerIndex, toPlayerIndex, text, isTeam, isDead, (CustomMessageType)messageType); + + return SQRESULT_NULL; +} + +ON_DLL_LOAD("engine.dll", EngineServerChatHooks, (CModule module)) +{ + g_pServerGameDLL = module.Offset(0x13F0AA98).RCast<CServerGameDLL*>(); +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerChatHooks, ServerSquirrel, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) + + CServerGameDLL__OnReceivedSayTextMessage = + module.Offset(0x1595C0).RCast<void(__fastcall*)(CServerGameDLL*, unsigned int, const char*, int)>(); + CRecipientFilter__Construct = module.Offset(0x1E9440).RCast<void(__fastcall*)(CRecipientFilter*)>(); + CRecipientFilter__Destruct = module.Offset(0x1E9700).RCast<void(__fastcall*)(CRecipientFilter*)>(); + CRecipientFilter__AddAllPlayers = module.Offset(0x1E9940).RCast<void(__fastcall*)(CRecipientFilter*)>(); + CRecipientFilter__AddRecipient = module.Offset(0x1E9B30).RCast<void(__fastcall*)(CRecipientFilter*, const CBasePlayer*)>(); + CRecipientFilter__MakeReliable = module.Offset(0x1EA4E0).RCast<void(__fastcall*)(CRecipientFilter*)>(); + + UserMessageBegin = module.Offset(0x15C520).RCast<void(__fastcall*)(CRecipientFilter*, const char*)>(); + MessageEnd = module.Offset(0x158880).RCast<void(__fastcall*)()>(); + MessageWriteByte = module.Offset(0x158A90).RCast<void(__fastcall*)(int)>(); + MessageWriteString = module.Offset(0x158D00).RCast<void(__fastcall*)(const char*)>(); + MessageWriteBool = module.Offset(0x158A00).RCast<void(__fastcall*)(bool)>(); +} diff --git a/primedev/server/serverchathooks.h b/primedev/server/serverchathooks.h new file mode 100644 index 00000000..d033e769 --- /dev/null +++ b/primedev/server/serverchathooks.h @@ -0,0 +1,24 @@ +#pragma once +#include <rapidjson/document.h> +#include <rapidjson/stringbuffer.h> + +enum class CustomMessageType : char +{ + Chat = 1, + Whisper = 2 +}; + +constexpr unsigned char CUSTOM_MESSAGE_INDEX_BIT = 0b10000000; +constexpr unsigned char CUSTOM_MESSAGE_INDEX_MASK = (unsigned char)~CUSTOM_MESSAGE_INDEX_BIT; + +// Send a vanilla chat message as if it was from the player. +void ChatSendMessage(unsigned int playerIndex, const char* text, bool isteam); + +// Send a custom message. +// fromPlayerIndex: set to -1 for a [SERVER] message, or another value to send from a specific player +// toPlayerIndex: set to -1 to send to all players, or another value to send to a single player +// isTeam: display a [TEAM] badge +// isDead: display a [DEAD] badge +// messageType: send a specific message type +void ChatBroadcastMessage( + int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType); diff --git a/primedev/server/servernethooks.cpp b/primedev/server/servernethooks.cpp new file mode 100644 index 00000000..148b735f --- /dev/null +++ b/primedev/server/servernethooks.cpp @@ -0,0 +1,218 @@ +#include "core/convar/convar.h" +#include "engine/r2engine.h" +#include "shared/exploit_fixes/ns_limits.h" +#include "masterserver/masterserver.h" + +#include <string> +#include <thread> +#include <bcrypt.h> + +AUTOHOOK_INIT() + +static ConVar* Cvar_net_debug_atlas_packet; +static ConVar* Cvar_net_debug_atlas_packet_insecure; + +static BCRYPT_ALG_HANDLE HMACSHA256; +constexpr size_t HMACSHA256_LEN = 256 / 8; + +static bool InitHMACSHA256() +{ + NTSTATUS status; + DWORD hashLength = 0; + ULONG hashLengthSz = 0; + + if ((status = BCryptOpenAlgorithmProvider(&HMACSHA256, BCRYPT_SHA256_ALGORITHM, NULL, BCRYPT_ALG_HANDLE_HMAC_FLAG))) + { + spdlog::error("failed to initialize HMAC-SHA256: BCryptOpenAlgorithmProvider: error 0x{:08X}", (ULONG)status); + return false; + } + + if ((status = BCryptGetProperty(HMACSHA256, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength, sizeof(hashLength), &hashLengthSz, 0))) + { + spdlog::error("failed to initialize HMAC-SHA256: BCryptGetProperty(BCRYPT_HASH_LENGTH): error 0x{:08X}", (ULONG)status); + return false; + } + + if (hashLength != HMACSHA256_LEN) + { + spdlog::error("failed to initialize HMAC-SHA256: BCryptGetProperty(BCRYPT_HASH_LENGTH): unexpected value {}", hashLength); + return false; + } + + return true; +} + +// compare the HMAC-SHA256(data, key) against sig (note: all strings are treated as raw binary data) +static bool VerifyHMACSHA256(std::string key, std::string sig, std::string data) +{ + uint8_t invalid = 1; + char hash[HMACSHA256_LEN]; + + NTSTATUS status; + BCRYPT_HASH_HANDLE h = NULL; + + if ((status = BCryptCreateHash(HMACSHA256, &h, NULL, 0, (PUCHAR)key.c_str(), (ULONG)key.length(), 0))) + { + spdlog::error("failed to verify HMAC-SHA256: BCryptCreateHash: error 0x{:08X}", (ULONG)status); + goto cleanup; + } + + if ((status = BCryptHashData(h, (PUCHAR)data.c_str(), (ULONG)data.length(), 0))) + { + spdlog::error("failed to verify HMAC-SHA256: BCryptHashData: error 0x{:08X}", (ULONG)status); + goto cleanup; + } + + if ((status = BCryptFinishHash(h, (PUCHAR)&hash, (ULONG)sizeof(hash), 0))) + { + spdlog::error("failed to verify HMAC-SHA256: BCryptFinishHash: error 0x{:08X}", (ULONG)status); + goto cleanup; + } + + // constant-time compare + if (sig.length() == sizeof(hash)) + { + invalid = 0; + for (size_t i = 0; i < sizeof(hash); i++) + invalid |= (uint8_t)(sig[i]) ^ (uint8_t)(hash[i]); + } + +cleanup: + if (h) + BCryptDestroyHash(h); + return !invalid; +} + +// v1 HMACSHA256-signed masterserver request (HMAC-SHA256(JSONData, MasterServerToken) + JSONData) +static void ProcessAtlasConnectionlessPacketSigreq1(netpacket_t* packet, bool dbg, std::string pType, std::string pData) +{ + if (pData.length() < HMACSHA256_LEN) + { + if (dbg) + spdlog::warn("ignoring Atlas connectionless packet (size={} type={}): invalid: too short for signature", packet->size, pType); + return; + } + + std::string pSig; // is binary data, not actually an ASCII string + pSig = pData.substr(0, HMACSHA256_LEN); + pData = pData.substr(HMACSHA256_LEN); + + if (!g_pMasterServerManager || !g_pMasterServerManager->m_sOwnServerAuthToken[0]) + { + if (dbg) + spdlog::warn( + "ignoring Atlas connectionless packet (size={} type={}): invalid (data={}): no masterserver token yet", + packet->size, + pType, + pData); + return; + } + + if (!VerifyHMACSHA256(std::string(g_pMasterServerManager->m_sOwnServerAuthToken), pSig, pData)) + { + if (!Cvar_net_debug_atlas_packet_insecure->GetBool()) + { + if (dbg) + spdlog::warn( + "ignoring Atlas connectionless packet (size={} type={}): invalid: invalid signature (key={})", + packet->size, + pType, + std::string(g_pMasterServerManager->m_sOwnServerAuthToken)); + return; + } + spdlog::warn( + "processing Atlas connectionless packet (size={} type={}) with invalid signature due to net_debug_atlas_packet_insecure", + packet->size, + pType); + } + + if (dbg) + spdlog::info("got Atlas connectionless packet (size={} type={} data={})", packet->size, pType, pData); + + std::thread t(&MasterServerManager::ProcessConnectionlessPacketSigreq1, g_pMasterServerManager, pData); + t.detach(); + + return; +} + +static void ProcessAtlasConnectionlessPacket(netpacket_t* packet) +{ + bool dbg = Cvar_net_debug_atlas_packet->GetBool(); + + // extract kind, null-terminated type, data + std::string pType, pData; + for (int i = 5; i < packet->size; i++) + { + if (packet->data[i] == '\x00') + { + pType.assign((char*)(&packet->data[5]), (size_t)(i - 5)); + if (i + 1 < packet->size) + pData.assign((char*)(&packet->data[i + 1]), (size_t)(packet->size - i - 1)); + break; + } + } + + // note: all Atlas connectionless packets should be idempotent so multiple attempts can be made to mitigate packet loss + // note: all long-running Atlas connectionless packet handlers should be started in a new thread (with copies of the data) to avoid + // blocking networking + + // v1 HMACSHA256-signed masterserver request + if (pType == "sigreq1") + { + ProcessAtlasConnectionlessPacketSigreq1(packet, dbg, pType, pData); + return; + } + + if (dbg) + spdlog::warn("ignoring Atlas connectionless packet (size={} type={}): unknown type", packet->size, pType); + return; +} + +AUTOHOOK(ProcessConnectionlessPacket, engine.dll + 0x117800, bool, , (void* a1, netpacket_t* packet)) +{ + // packet->data consists of 0xFFFFFFFF (int32 -1) to indicate packets aren't split, followed by a header consisting of a single + // character, which is used to uniquely identify the packet kind. Most kinds follow this with a null-terminated string payload + // then an arbitrary amoount of data. + + // T (no rate limits since we authenticate packets before doing anything expensive) + if (4 < packet->size && packet->data[4] == 'T') + { + ProcessAtlasConnectionlessPacket(packet); + return false; + } + + // check rate limits for the original unconnected packets + if (!g_pServerLimits->CheckConnectionlessPacketLimits(packet)) + return false; + + // A, H, I, N + return ProcessConnectionlessPacket(a1, packet); +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerNetHooks, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + if (!InitHMACSHA256()) + throw std::runtime_error("failed to initialize bcrypt"); + + if (!VerifyHMACSHA256( + "test", + "\x88\xcd\x21\x08\xb5\x34\x7d\x97\x3c\xf3\x9c\xdf\x90\x53\xd7\xdd\x42\x70\x48\x76\xd8\xc9\xa9\xbd\x8e\x2d\x16\x82\x59\xd3\xdd" + "\xf7", + "test")) + throw std::runtime_error("bcrypt HMAC-SHA256 is broken"); + + Cvar_net_debug_atlas_packet = new ConVar( + "net_debug_atlas_packet", + "0", + FCVAR_NONE, + "Whether to log detailed debugging information for Atlas connectionless packets (warning: this allows unlimited amounts of " + "arbitrary data to be logged)"); + + Cvar_net_debug_atlas_packet_insecure = new ConVar( + "net_debug_atlas_packet_insecure", + "0", + FCVAR_NONE, + "Whether to disable signature verification for Atlas connectionless packets (DANGEROUS: this allows anyone to impersonate Atlas)"); +} diff --git a/primedev/server/serverpresence.cpp b/primedev/server/serverpresence.cpp new file mode 100644 index 00000000..159b9f30 --- /dev/null +++ b/primedev/server/serverpresence.cpp @@ -0,0 +1,228 @@ +#include "serverpresence.h" +#include "shared/playlist.h" +#include "core/tier0.h" +#include "core/convar/convar.h" + +#include <regex> + +ServerPresenceManager* g_pServerPresence; + +ConVar* Cvar_hostname; + +// Convert a hex digit char to integer. +inline int hctod(char c) +{ + if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + else + { + return c - '0'; + } +} + +// This function interprets all 4-hexadecimal-digit unicode codepoint characters like \u4E2D to UTF-8 encoding. +std::string UnescapeUnicode(const std::string& str) +{ + std::string result; + + std::regex r("\\\\u([a-f\\d]{4})", std::regex::icase); + auto matches_begin = std::sregex_iterator(str.begin(), str.end(), r); + auto matches_end = std::sregex_iterator(); + std::smatch last_match; + + for (std::sregex_iterator i = matches_begin; i != matches_end; ++i) + { + last_match = *i; + result.append(last_match.prefix()); + unsigned int cp = 0; + for (int i = 2; i <= 5; ++i) + { + cp *= 16; + cp += hctod(last_match.str()[i]); + } + if (cp <= 0x7F) + { + result.push_back(cp); + } + else if (cp <= 0x7FF) + { + result.push_back((cp >> 6) | 0b11000000 & (~(1 << 5))); + result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); + } + else if (cp <= 0xFFFF) + { + result.push_back((cp >> 12) | 0b11100000 & (~(1 << 4))); + result.push_back((cp >> 6) & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); + result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); + } + } + + if (!last_match.ready()) + return str; + else + result.append(last_match.suffix()); + + return result; +} + +void ServerPresenceManager::CreateConVars() +{ + // clang-format off + // register convars + Cvar_ns_server_presence_update_rate = new ConVar( + "ns_server_presence_update_rate", "5000", FCVAR_GAMEDLL, "How often we update our server's presence on server lists in ms"); + + Cvar_ns_server_name = new ConVar("ns_server_name", "Unnamed Northstar Server", FCVAR_GAMEDLL, "This server's name", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { + g_pServerPresence->SetName(UnescapeUnicode(g_pServerPresence->Cvar_ns_server_name->GetString())); + + // update engine hostname cvar + Cvar_hostname->SetValue(g_pServerPresence->Cvar_ns_server_name->GetString()); + }); + + Cvar_ns_server_desc = new ConVar("ns_server_desc", "Default server description", FCVAR_GAMEDLL, "This server's description", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { + g_pServerPresence->SetDescription(UnescapeUnicode(g_pServerPresence->Cvar_ns_server_desc->GetString())); + }); + + Cvar_ns_server_password = new ConVar("ns_server_password", "", FCVAR_GAMEDLL, "This server's password", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { + g_pServerPresence->SetPassword(g_pServerPresence->Cvar_ns_server_password->GetString()); + }); + + Cvar_ns_report_server_to_masterserver = new ConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver"); + Cvar_ns_report_sp_server_to_masterserver = new ConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver, when started in singleplayer"); + // clang-format on +} + +void ServerPresenceManager::AddPresenceReporter(ServerPresenceReporter* reporter) +{ + m_vPresenceReporters.push_back(reporter); +} + +void ServerPresenceManager::CreatePresence() +{ + // reset presence fields that rely on runtime server state + // these being: port, map/playlist name, and playercount/maxplayers + m_ServerPresence.m_iPort = 0; + + m_ServerPresence.m_iPlayerCount = 0; // this should actually be 0 at this point, so shouldn't need updating later + m_ServerPresence.m_iMaxPlayers = 0; + + memset(m_ServerPresence.m_MapName, 0, sizeof(m_ServerPresence.m_MapName)); + memset(m_ServerPresence.m_PlaylistName, 0, sizeof(m_ServerPresence.m_PlaylistName)); + m_ServerPresence.m_bIsSingleplayerServer = false; + + m_bHasPresence = true; + m_bFirstPresenceUpdate = true; + + // code that's calling this should set up the reset fields at this point +} + +void ServerPresenceManager::DestroyPresence() +{ + m_bHasPresence = false; + + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->DestroyPresence(&m_ServerPresence); +} + +void ServerPresenceManager::RunFrame(double flCurrentTime) +{ + if (!m_bHasPresence || !Cvar_ns_report_server_to_masterserver->GetBool()) // don't run until we actually have server presence + return; + + // don't run if we're sp and don't want to report sp + if (m_ServerPresence.m_bIsSingleplayerServer && !Cvar_ns_report_sp_server_to_masterserver->GetBool()) + return; + + // Call RunFrame() so that reporters can, for example, handle std::future results as soon as they arrive. + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->RunFrame(flCurrentTime, &m_ServerPresence); + + // run on a specified delay + if ((flCurrentTime - m_flLastPresenceUpdate) * 1000 < Cvar_ns_server_presence_update_rate->GetFloat()) + return; + + // is this the first frame we're updating this presence? + if (m_bFirstPresenceUpdate) + { + // let reporters setup/clear any state + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->CreatePresence(&m_ServerPresence); + + m_bFirstPresenceUpdate = false; + } + + m_flLastPresenceUpdate = flCurrentTime; + + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->ReportPresence(&m_ServerPresence); +} + +void ServerPresenceManager::SetPort(const int iPort) +{ + // update port + m_ServerPresence.m_iPort = iPort; +} + +void ServerPresenceManager::SetName(const std::string sServerNameUnicode) +{ + // update name + m_ServerPresence.m_sServerName = sServerNameUnicode; +} + +void ServerPresenceManager::SetDescription(const std::string sServerDescUnicode) +{ + // update desc + m_ServerPresence.m_sServerDesc = sServerDescUnicode; +} + +void ServerPresenceManager::SetPassword(const char* pPassword) +{ + // update password + strncpy_s(m_ServerPresence.m_Password, sizeof(m_ServerPresence.m_Password), pPassword, sizeof(m_ServerPresence.m_Password) - 1); +} + +void ServerPresenceManager::SetMap(const char* pMapName, bool isInitialising) +{ + // if the server is initialising (i.e. this is first map) on sp, set the server to sp + if (isInitialising) + m_ServerPresence.m_bIsSingleplayerServer = !strncmp(pMapName, "sp_", 3); + + // update map + strncpy_s(m_ServerPresence.m_MapName, sizeof(m_ServerPresence.m_MapName), pMapName, sizeof(m_ServerPresence.m_MapName) - 1); +} + +void ServerPresenceManager::SetPlaylist(const char* pPlaylistName) +{ + // update playlist + strncpy_s( + m_ServerPresence.m_PlaylistName, + sizeof(m_ServerPresence.m_PlaylistName), + pPlaylistName, + sizeof(m_ServerPresence.m_PlaylistName) - 1); + + // update maxplayers + const char* pMaxPlayers = R2::GetCurrentPlaylistVar("max_players", true); + + // can be null in some situations, so default 6 + if (pMaxPlayers) + m_ServerPresence.m_iMaxPlayers = std::stoi(pMaxPlayers); + else + m_ServerPresence.m_iMaxPlayers = 6; +} + +void ServerPresenceManager::SetPlayerCount(const int iPlayerCount) +{ + m_ServerPresence.m_iPlayerCount = iPlayerCount; +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerPresence, ConVar, (CModule module)) +{ + g_pServerPresence->CreateConVars(); + Cvar_hostname = module.Offset(0x1315BAE8).Deref().RCast<ConVar*>(); +} diff --git a/primedev/server/serverpresence.h b/primedev/server/serverpresence.h new file mode 100644 index 00000000..c644cc37 --- /dev/null +++ b/primedev/server/serverpresence.h @@ -0,0 +1,94 @@ +#pragma once +#include "core/convar/convar.h" + +struct ServerPresence +{ +public: + int m_iPort; + + std::string m_sServerId; + + std::string m_sServerName; + std::string m_sServerDesc; + char m_Password[256]; // probably bigger than will ever be used in practice, lol + + char m_MapName[32]; + char m_PlaylistName[64]; + bool m_bIsSingleplayerServer; // whether the server started in sp + + int m_iPlayerCount; + int m_iMaxPlayers; + + ServerPresence() + { + memset(this, 0, sizeof(this)); + } + + ServerPresence(const ServerPresence* obj) + { + m_iPort = obj->m_iPort; + + m_sServerId = obj->m_sServerId; + + m_sServerName = obj->m_sServerName; + m_sServerDesc = obj->m_sServerDesc; + memcpy(m_Password, obj->m_Password, sizeof(m_Password)); + + memcpy(m_MapName, obj->m_MapName, sizeof(m_MapName)); + memcpy(m_PlaylistName, obj->m_PlaylistName, sizeof(m_PlaylistName)); + + m_iPlayerCount = obj->m_iPlayerCount; + m_iMaxPlayers = obj->m_iMaxPlayers; + } +}; + +class ServerPresenceReporter +{ +public: + virtual void CreatePresence(const ServerPresence* pServerPresence) {} + virtual void ReportPresence(const ServerPresence* pServerPresence) {} + virtual void DestroyPresence(const ServerPresence* pServerPresence) {} + virtual void RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) {} +}; + +class ServerPresenceManager +{ +private: + ServerPresence m_ServerPresence; + + bool m_bHasPresence = false; + bool m_bFirstPresenceUpdate = false; + + std::vector<ServerPresenceReporter*> m_vPresenceReporters; + + double m_flLastPresenceUpdate = 0; + ConVar* Cvar_ns_server_presence_update_rate; + + ConVar* Cvar_ns_server_name; + ConVar* Cvar_ns_server_desc; + ConVar* Cvar_ns_server_password; + + ConVar* Cvar_ns_report_server_to_masterserver; + ConVar* Cvar_ns_report_sp_server_to_masterserver; + +public: + void AddPresenceReporter(ServerPresenceReporter* reporter); + + void CreateConVars(); + + void CreatePresence(); + void DestroyPresence(); + void RunFrame(double flCurrentTime); + + void SetPort(const int iPort); + + void SetName(const std::string sServerNameUnicode); + void SetDescription(const std::string sServerDescUnicode); + void SetPassword(const char* pPassword); + + void SetMap(const char* pMapName, bool isInitialising = false); + void SetPlaylist(const char* pPlaylistName); + void SetPlayerCount(const int iPlayerCount); +}; + +extern ServerPresenceManager* g_pServerPresence; |