aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/server
diff options
context:
space:
mode:
Diffstat (limited to 'NorthstarDLL/server')
-rw-r--r--NorthstarDLL/server/auth/bansystem.cpp222
-rw-r--r--NorthstarDLL/server/auth/bansystem.h19
-rw-r--r--NorthstarDLL/server/auth/serverauthentication.cpp462
-rw-r--r--NorthstarDLL/server/auth/serverauthentication.h59
-rw-r--r--NorthstarDLL/server/buildainfile.cpp400
-rw-r--r--NorthstarDLL/server/r2server.cpp17
-rw-r--r--NorthstarDLL/server/r2server.h28
-rw-r--r--NorthstarDLL/server/serverchathooks.cpp172
-rw-r--r--NorthstarDLL/server/serverchathooks.h25
-rw-r--r--NorthstarDLL/server/serverpresence.cpp237
-rw-r--r--NorthstarDLL/server/serverpresence.h92
11 files changed, 1733 insertions, 0 deletions
diff --git a/NorthstarDLL/server/auth/bansystem.cpp b/NorthstarDLL/server/auth/bansystem.cpp
new file mode 100644
index 00000000..56719ed9
--- /dev/null
+++ b/NorthstarDLL/server/auth/bansystem.cpp
@@ -0,0 +1,222 @@
+#pragma once
+#include "pch.h"
+#include "bansystem.h"
+#include "serverauthentication.h"
+#include "shared/maxplayers.h"
+#include "core/convar/concommand.h"
+#include "server/r2server.h"
+#include "engine/r2engine.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)
+{
+ 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 < R2::GetMaxPlayers(); i++)
+ {
+ R2::CBaseClient* player = &R2::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));
+ R2::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/NorthstarDLL/server/auth/bansystem.h b/NorthstarDLL/server/auth/bansystem.h
new file mode 100644
index 00000000..6f180126
--- /dev/null
+++ b/NorthstarDLL/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/NorthstarDLL/server/auth/serverauthentication.cpp b/NorthstarDLL/server/auth/serverauthentication.cpp
new file mode 100644
index 00000000..ac1d5f17
--- /dev/null
+++ b/NorthstarDLL/server/auth/serverauthentication.cpp
@@ -0,0 +1,462 @@
+#include "pch.h"
+#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 "shared/maxplayers.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 "httplib.h"
+
+#include <fstream>
+#include <filesystem>
+#include <thread>
+
+AUTOHOOK_INIT()
+
+const char* AUTHSERVER_VERIFY_STRING = "I am a northstar server!";
+
+// global vars
+ServerAuthenticationManager* g_pServerAuthentication;
+CBaseServer__RejectConnectionType CBaseServer__RejectConnection;
+
+void ServerAuthenticationManager::StartPlayerAuthServer()
+{
+ if (m_bRunningPlayerAuthThread)
+ {
+ spdlog::warn("ServerAuthenticationManager::StartPlayerAuthServer was called while m_bRunningPlayerAuthThread is true");
+ return;
+ }
+
+ g_pServerPresence->SetAuthPort(Cvar_ns_player_auth_port->GetInt()); // set auth port for presence
+ m_bRunningPlayerAuthThread = true;
+
+ // listen is a blocking call so thread this
+ std::thread serverThread(
+ [this]
+ {
+ // this is just a super basic way to verify that servers have ports open, masterserver will try to read this before ensuring
+ // server is legit
+ m_PlayerAuthServer.Get(
+ "/verify",
+ [](const httplib::Request& request, httplib::Response& response)
+ { response.set_content(AUTHSERVER_VERIFY_STRING, "text/plain"); });
+
+ m_PlayerAuthServer.Post(
+ "/authenticate_incoming_player",
+ [this](const httplib::Request& request, httplib::Response& response)
+ {
+ if (!request.has_param("id") || !request.has_param("authToken") || request.body.size() >= R2::PERSISTENCE_MAX_SIZE ||
+ !request.has_param("serverAuthToken") ||
+ strcmp(g_pMasterServerManager->m_sOwnServerAuthToken, request.get_param_value("serverAuthToken").c_str()))
+ {
+ response.set_content("{\"success\":false}", "application/json");
+ return;
+ }
+
+ RemoteAuthData newAuthData {};
+ strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), request.get_param_value("id").c_str(), sizeof(newAuthData.uid) - 1);
+ strncpy_s(
+ newAuthData.username,
+ sizeof(newAuthData.username),
+ request.get_param_value("username").c_str(),
+ sizeof(newAuthData.username) - 1);
+
+ newAuthData.pdataSize = request.body.size();
+ newAuthData.pdata = new char[newAuthData.pdataSize];
+ memcpy(newAuthData.pdata, request.body.c_str(), newAuthData.pdataSize);
+
+ std::lock_guard<std::mutex> guard(m_AuthDataMutex);
+ m_RemoteAuthenticationData.insert(std::make_pair(request.get_param_value("authToken"), newAuthData));
+
+ response.set_content("{\"success\":true}", "application/json");
+ });
+
+ m_PlayerAuthServer.listen("0.0.0.0", Cvar_ns_player_auth_port->GetInt());
+ });
+
+ serverThread.detach();
+}
+
+void ServerAuthenticationManager::StopPlayerAuthServer()
+{
+ if (!m_bRunningPlayerAuthThread)
+ {
+ spdlog::warn("ServerAuthenticationManager::StopPlayerAuthServer was called while m_bRunningPlayerAuthThread is false");
+ return;
+ }
+
+ m_bRunningPlayerAuthThread = false;
+ m_PlayerAuthServer.stop();
+}
+
+void ServerAuthenticationManager::AddPlayer(R2::CBaseClient* player, const char* pToken)
+{
+ PlayerAuthenticationData additionalData;
+ additionalData.pdataSize = m_RemoteAuthenticationData[pToken].pdataSize;
+ additionalData.usingLocalPdata = player->m_iPersistenceReady == R2::ePersistenceReady::READY_INSECURE;
+
+ m_PlayerAuthenticationData.insert(std::make_pair(player, additionalData));
+}
+
+void ServerAuthenticationManager::RemovePlayer(R2::CBaseClient* player)
+{
+ if (m_PlayerAuthenticationData.count(player))
+ m_PlayerAuthenticationData.erase(player);
+}
+
+bool checkIsPlayerNameValid(const char* name)
+{
+ int len = strlen(name);
+ // Restricts name to max 63 characters
+ if (len >= 64)
+ return false;
+ for (int i = 0; i < len; i++)
+ {
+ // Restricts the characters in the name to a restricted range in ASCII
+ if (static_cast<int>(name[i]) < 32 || static_cast<int>(name[i]) > 126)
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool ServerAuthenticationManager::VerifyPlayerName(const char* authToken, const char* name)
+{
+ std::lock_guard<std::mutex> guard(m_AuthDataMutex);
+
+ if (CVar_ns_auth_allow_insecure->GetInt())
+ {
+ spdlog::info("Allowing player with name '{}' because ns_auth_allow_insecure is enabled", name);
+ return true;
+ }
+
+ if (!checkIsPlayerNameValid(name))
+ {
+ spdlog::info("Rejecting player with name '{}' because the name contains forbidden characters", name);
+ return false;
+ }
+ // TODO: We should really have a better way of doing this for singleplayer
+ // Best way of doing this would be to check if server is actually in singleplayer mode, or just running a SP map in multiplayer
+ // Currently there's not an easy way of checking this, so we just disable this check if mapname starts with `sp_`
+ // This means that player names are not checked on singleplayer
+ if ((m_RemoteAuthenticationData.empty() || m_RemoteAuthenticationData.count(std::string(authToken)) == 0) &&
+ strncmp(R2::g_pHostState->m_levelName, "sp_", 3) != 0)
+ {
+ spdlog::info("Rejecting player with name '{}' because authToken '{}' was not found", name, authToken);
+ return false;
+ }
+
+ const RemoteAuthData& authData = m_RemoteAuthenticationData[authToken];
+
+ if (*authData.username && strncmp(name, authData.username, 64) != 0)
+ {
+ spdlog::info("Rejecting player with name '{}' because name does not match expected name '{}'", name, authData.username);
+ return false;
+ }
+
+ return true;
+}
+
+bool ServerAuthenticationManager::CheckDuplicateAccounts(R2::CBaseClient* player)
+{
+ if (m_bAllowDuplicateAccounts)
+ return true;
+
+ bool bHasUidPlayer = false;
+ for (int i = 0; i < R2::GetMaxPlayers(); i++)
+ if (&R2::g_pClientArray[i] != player && !strcmp(R2::g_pClientArray[i].m_UID, player->m_UID))
+ return false;
+
+ return true;
+}
+
+bool ServerAuthenticationManager::AuthenticatePlayer(R2::CBaseClient* player, uint64_t uid, char* authToken)
+{
+ std::string strUid = std::to_string(uid);
+ std::lock_guard<std::mutex> guard(m_AuthDataMutex);
+
+ if (!strncmp(R2::g_pHostState->m_levelName, "sp_", 3))
+ return true;
+
+ // copy uuid
+ strcpy(player->m_UID, strUid.c_str());
+
+ bool authFail = true;
+ if (!m_RemoteAuthenticationData.empty() && m_RemoteAuthenticationData.count(std::string(authToken)))
+ {
+ if (!CheckDuplicateAccounts(player))
+ return false;
+
+ // use stored auth data
+ RemoteAuthData authData = m_RemoteAuthenticationData[authToken];
+
+ if (!strcmp(strUid.c_str(), authData.uid)) // connecting client's uid is the same as auth's uid
+ {
+ // if we're resetting let script handle the reset
+ if (!m_bForceResetLocalPlayerPersistence || strcmp(authData.uid, R2::g_pLocalPlayerUserID))
+ {
+ // copy pdata into buffer
+ memcpy(player->m_PersistenceBuffer, authData.pdata, authData.pdataSize);
+ }
+
+ // set persistent data as ready
+ player->m_iPersistenceReady = R2::ePersistenceReady::READY_REMOTE;
+ authFail = false;
+ }
+ }
+
+ if (authFail)
+ {
+ if (CVar_ns_auth_allow_insecure->GetBool())
+ {
+ // set persistent data as ready
+ // note: actual placeholder persistent data is populated in script with InitPersistentData()
+ player->m_iPersistenceReady = R2::ePersistenceReady::READY_INSECURE;
+ return true;
+ }
+ else
+ return false;
+ }
+
+ return true; // auth successful, client stays on
+}
+
+bool ServerAuthenticationManager::RemovePlayerAuthData(R2::CBaseClient* player)
+{
+ 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(player->m_UID, R2::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(player->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(R2::CBaseClient* player)
+{
+ if (player->m_iPersistenceReady == R2::ePersistenceReady::READY_REMOTE)
+ {
+ g_pMasterServerManager->WritePlayerPersistentData(
+ player->m_UID, (const char*)player->m_PersistenceBuffer, m_PlayerAuthenticationData[player].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;
+
+ spdlog::info(
+ "CBaseServer__ClientConnect attempted connection with uid {}, playerName '{}', serverFilter '{}'", uid, playerName, serverFilter);
+
+ if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, playerName))
+ {
+ CBaseServer__RejectConnection(self, *((int*)self + 3), addr, "Invalid Name.\n");
+ return nullptr;
+ }
+ if (!g_pBanSystem->IsUIDAllowed(uid))
+ {
+ CBaseServer__RejectConnection(self, *((int*)self + 3), addr, "Banned From server.\n");
+ return nullptr;
+ }
+
+ return CBaseServer__ConnectClient(self, addr, a3, a4, a5, a6, a7, playerName, serverFilter, a10, a11, a12, a13, a14, uid, a16, a17);
+}
+
+// clang-format off
+AUTOHOOK(CBaseClient__Connect, engine.dll + 0x101740,
+bool,, (R2::CBaseClient* self, char* name, void* netchan_ptr_arg, char b_fake_player_arg, void* a5, char* Buffer, void* a7))
+// clang-format on
+{
+ // try to auth player, dc if it fails
+ // we connect regardless of auth, because returning bad from this function can fuck client state p bad
+ bool ret = CBaseClient__Connect(self, name, netchan_ptr_arg, b_fake_player_arg, a5, Buffer, a7);
+ if (!ret)
+ return ret;
+
+ if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, name))
+ {
+ R2::CBaseClient__Disconnect(self, 1, "Invalid Name.\n");
+ return false;
+ }
+ if (!g_pBanSystem->IsUIDAllowed(iNextPlayerUid))
+ {
+ R2::CBaseClient__Disconnect(self, 1, "Banned From server.\n");
+ return false;
+ }
+ if (!g_pServerAuthentication->AuthenticatePlayer(self, iNextPlayerUid, pNextPlayerToken))
+ {
+ R2::CBaseClient__Disconnect(self, 1, "Authentication Failed.\n");
+ return false;
+ }
+
+ g_pServerAuthentication->AddPlayer(self, pNextPlayerToken);
+ g_pServerLimits->AddPlayer(self);
+
+ return ret;
+}
+
+// clang-format off
+AUTOHOOK(CBaseClient__ActivatePlayer, engine.dll + 0x100F80,
+void,, (R2::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 >= R2::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,, (R2::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);
+ g_pServerAuthentication->RemovePlayerAuthData(self); // won't do anything 99% of the time, but just in case
+
+ g_pServerAuthentication->RemovePlayer(self);
+ g_pServerLimits->RemovePlayer(self);
+ }
+
+ else if (g_pServerAuthentication->m_RemoteAuthenticationData.count(self->m_Name))
+ {
+ 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 (*R2::g_pServerState == R2::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_player_auth_port = new ConVar("ns_player_auth_port", "8081", FCVAR_GAMEDLL, "");
+ 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).As<CBaseServer__RejectConnectionType>();
+
+ if (Tier0::CommandLine()->CheckParm("-allowdupeaccounts"))
+ {
+ // patch to allow same of multiple account
+ module.Offset(0x114510).Patch("EB");
+
+ g_pServerAuthentication->m_bAllowDuplicateAccounts = true;
+ }
+}
diff --git a/NorthstarDLL/server/auth/serverauthentication.h b/NorthstarDLL/server/auth/serverauthentication.h
new file mode 100644
index 00000000..4ffbbfd6
--- /dev/null
+++ b/NorthstarDLL/server/auth/serverauthentication.h
@@ -0,0 +1,59 @@
+#pragma once
+#include "core/convar/convar.h"
+#include "httplib.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
+{
+ private:
+ httplib::Server m_PlayerAuthServer;
+
+ public:
+ ConVar* Cvar_ns_player_auth_port;
+ 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<R2::CBaseClient*, PlayerAuthenticationData> m_PlayerAuthenticationData;
+ bool m_bAllowDuplicateAccounts = false;
+ bool m_bRunningPlayerAuthThread = false;
+ bool m_bNeedLocalAuthForNewgame = false;
+ bool m_bForceResetLocalPlayerPersistence = false;
+
+ public:
+ void StartPlayerAuthServer();
+ void StopPlayerAuthServer();
+ void AddPlayer(R2::CBaseClient* player, const char* pToken);
+ void RemovePlayer(R2::CBaseClient* player);
+ bool CheckDuplicateAccounts(R2::CBaseClient* player);
+ bool AuthenticatePlayer(R2::CBaseClient* player, uint64_t uid, char* authToken);
+ bool VerifyPlayerName(const char* authToken, const char* name);
+ bool RemovePlayerAuthData(R2::CBaseClient* player);
+ void WritePersistentData(R2::CBaseClient* player);
+};
+
+extern ServerAuthenticationManager* g_pServerAuthentication;
diff --git a/NorthstarDLL/server/buildainfile.cpp b/NorthstarDLL/server/buildainfile.cpp
new file mode 100644
index 00000000..f6dc4717
--- /dev/null
+++ b/NorthstarDLL/server/buildainfile.cpp
@@ -0,0 +1,400 @@
+#include "pch.h"
+#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)
+
+char** pUnkServerMapversionGlobal;
+
+ConVar* Cvar_ns_ai_dumpAINfileFromLoad;
+
+void DumpAINInfo(CAI_Network* aiNetwork)
+{
+ fs::path writePath(fmt::format("{}/maps/graphs", R2::g_pModName));
+ writePath /= R2::g_pHostState->m_levelName;
+ 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));
+
+ // could probably be cleaner but whatever
+ int mapVersion = *(int*)(*pUnkServerMapversionGlobal + 104);
+ spdlog::info("writing map version: {}", mapVersion); // temp
+ 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).As<int*>();
+ pppUnkNodeStruct0s = module.Offset(0x1063BE0).As<UnkNodeStruct0***>();
+ pUnkLinkStruct1Count = module.Offset(0x1063AA8).As<int*>();
+ pppUnkStruct1s = module.Offset(0x1063A90).As<UnkLinkStruct1***>();
+ pUnkServerMapversionGlobal = module.Offset(0xBFBE08).As<char**>();
+}
diff --git a/NorthstarDLL/server/r2server.cpp b/NorthstarDLL/server/r2server.cpp
new file mode 100644
index 00000000..50cfa239
--- /dev/null
+++ b/NorthstarDLL/server/r2server.cpp
@@ -0,0 +1,17 @@
+#include "pch.h"
+#include "r2server.h"
+
+using namespace R2;
+
+// use the R2 namespace for game funcs
+namespace R2
+{
+ CBaseEntity* (*Server_GetEntityByIndex)(int index);
+ CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex);
+} // namespace R2
+
+ON_DLL_LOAD("server.dll", R2GameServer, (CModule module))
+{
+ Server_GetEntityByIndex = module.Offset(0xFB820).As<CBaseEntity* (*)(int)>();
+ UTIL_PlayerByIndex = module.Offset(0x26AA10).As<CBasePlayer*(__fastcall*)(int)>();
+}
diff --git a/NorthstarDLL/server/r2server.h b/NorthstarDLL/server/r2server.h
new file mode 100644
index 00000000..313284be
--- /dev/null
+++ b/NorthstarDLL/server/r2server.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "core/math/vector.h"
+
+// use the R2 namespace for game funcs
+namespace R2
+{
+ // server entity stuff
+ class CBaseEntity;
+ extern CBaseEntity* (*Server_GetEntityByIndex)(int index);
+
+ // clang-format off
+ OFFSET_STRUCT(CBasePlayer)
+ {
+ STRUCT_SIZE(0x1D02);
+ FIELD(0x58, uint32_t m_nPlayerIndex)
+
+ 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)
+ };
+ // clang-format on
+
+ extern CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex);
+} // namespace R2
diff --git a/NorthstarDLL/server/serverchathooks.cpp b/NorthstarDLL/server/serverchathooks.cpp
new file mode 100644
index 00000000..57c2c31a
--- /dev/null
+++ b/NorthstarDLL/server/serverchathooks.cpp
@@ -0,0 +1,172 @@
+#include "pch.h"
+#include "serverchathooks.h"
+#include "shared/exploit_fixes/ns_limits.h"
+#include "squirrel/squirrel.h"
+#include "server/r2server.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 R2::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
+{
+ // 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(&R2::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)
+{
+ R2::CBasePlayer* toPlayer = NULL;
+ if (toPlayerIndex >= 0)
+ {
+ toPlayer = R2::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).As<CServerGameDLL*>();
+}
+
+ON_DLL_LOAD_RELIESON("server.dll", ServerChatHooks, ServerSquirrel, (CModule module))
+{
+ AUTOHOOK_DISPATCH_MODULE(server.dll)
+
+ CServerGameDLL__OnReceivedSayTextMessage =
+ module.Offset(0x1595C0).As<void(__fastcall*)(CServerGameDLL*, unsigned int, const char*, int)>();
+ CRecipientFilter__Construct = module.Offset(0x1E9440).As<void(__fastcall*)(CRecipientFilter*)>();
+ CRecipientFilter__Destruct = module.Offset(0x1E9700).As<void(__fastcall*)(CRecipientFilter*)>();
+ CRecipientFilter__AddAllPlayers = module.Offset(0x1E9940).As<void(__fastcall*)(CRecipientFilter*)>();
+ CRecipientFilter__AddRecipient = module.Offset(0x1E9B30).As<void(__fastcall*)(CRecipientFilter*, const R2::CBasePlayer*)>();
+ CRecipientFilter__MakeReliable = module.Offset(0x1EA4E0).As<void(__fastcall*)(CRecipientFilter*)>();
+
+ UserMessageBegin = module.Offset(0x15C520).As<void(__fastcall*)(CRecipientFilter*, const char*)>();
+ MessageEnd = module.Offset(0x158880).As<void(__fastcall*)()>();
+ MessageWriteByte = module.Offset(0x158A90).As<void(__fastcall*)(int)>();
+ MessageWriteString = module.Offset(0x158D00).As<void(__fastcall*)(const char*)>();
+ MessageWriteBool = module.Offset(0x158A00).As<void(__fastcall*)(bool)>();
+}
diff --git a/NorthstarDLL/server/serverchathooks.h b/NorthstarDLL/server/serverchathooks.h
new file mode 100644
index 00000000..1d8a806a
--- /dev/null
+++ b/NorthstarDLL/server/serverchathooks.h
@@ -0,0 +1,25 @@
+#pragma once
+#include "pch.h"
+#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 = ~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/NorthstarDLL/server/serverpresence.cpp b/NorthstarDLL/server/serverpresence.cpp
new file mode 100644
index 00000000..bda5e7fe
--- /dev/null
+++ b/NorthstarDLL/server/serverpresence.cpp
@@ -0,0 +1,237 @@
+#include "pch.h"
+#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;
+}
+
+ServerPresenceManager::ServerPresenceManager()
+{
+ // 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 description", 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 name", 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/auth port, map/playlist name, and playercount/maxplayers
+ m_ServerPresence.m_iPort = 0;
+ m_ServerPresence.m_iAuthPort = 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::SetAuthPort(const int iAuthPort)
+{
+ // update authport
+ m_ServerPresence.m_iAuthPort = iAuthPort;
+}
+
+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 = new ServerPresenceManager;
+
+ Cvar_hostname = module.Offset(0x1315BAE8).Deref().As<ConVar*>();
+}
diff --git a/NorthstarDLL/server/serverpresence.h b/NorthstarDLL/server/serverpresence.h
new file mode 100644
index 00000000..125b2e68
--- /dev/null
+++ b/NorthstarDLL/server/serverpresence.h
@@ -0,0 +1,92 @@
+#pragma once
+#include "core/convar/convar.h"
+
+struct ServerPresence
+{
+ int m_iPort;
+ int m_iAuthPort;
+
+ 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_iAuthPort = obj->m_iAuthPort;
+
+ 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:
+ ServerPresenceManager();
+
+ void AddPresenceReporter(ServerPresenceReporter* reporter);
+
+ void CreatePresence();
+ void DestroyPresence();
+ void RunFrame(double flCurrentTime);
+
+ void SetPort(const int iPort);
+ void SetAuthPort(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;