aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NorthstarDLL/masterserver.cpp680
-rw-r--r--NorthstarDLL/masterserver.h61
-rw-r--r--NorthstarDLL/serverpresence.cpp4
-rw-r--r--NorthstarDLL/serverpresence.h1
4 files changed, 477 insertions, 269 deletions
diff --git a/NorthstarDLL/masterserver.cpp b/NorthstarDLL/masterserver.cpp
index b8e00bdc..fc747e5d 100644
--- a/NorthstarDLL/masterserver.cpp
+++ b/NorthstarDLL/masterserver.cpp
@@ -17,6 +17,8 @@
#include <cstring>
#include <regex>
+using namespace std::chrono_literals;
+
MasterServerManager* g_pMasterServerManager;
ConVar* Cvar_ns_masterserver_hostname;
@@ -52,6 +54,9 @@ void SetCommonHttpClientOptions(CURL* curl)
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl, CURLOPT_VERBOSE, Cvar_ns_curl_log_enable->GetBool());
curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent);
+ // Timeout since the MS has fucky async functions without await, making curl hang due to a successful connection but no response for ~90
+ // seconds.
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
// curl_easy_setopt(curl, CURLOPT_STDERR, stdout);
if (Tier0::CommandLine()->FindParm("-msinsecure")) // TODO: this check doesn't seem to work
{
@@ -750,327 +755,464 @@ void MasterServerManager::WritePlayerPersistentData(const char* playerId, const
requestThread.detach();
}
-class MasterServerPresenceReporter : public ServerPresenceReporter
+void ConCommand_ns_fetchservers(const CCommand& args)
+{
+ g_pMasterServerManager->RequestServerList();
+}
+
+MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {}
+
+ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module))
{
- const int MAX_REGISTRATION_ATTEMPTS = 5;
+ g_pMasterServerManager = new MasterServerManager;
- bool m_bShouldTryRegisterServer;
- bool m_bHasAddServerRequest;
- int m_nNumRegistrationAttempts;
+ Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, "");
+ Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console");
+
+ RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL);
- void CreatePresence(const ServerPresence* pServerPresence) override
+ MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter;
+ g_pServerPresence->AddPresenceReporter(presenceReporter);
+}
+
+void MasterServerPresenceReporter::CreatePresence(const ServerPresence* pServerPresence)
+{
+ m_nNumRegistrationAttempts = 0;
+}
+
+void MasterServerPresenceReporter::ReportPresence(const ServerPresence* pServerPresence)
+{
+ // make a copy of presence for multithreading purposes
+ ServerPresence threadedPresence(pServerPresence);
+
+ if (!*g_pMasterServerManager->m_sOwnServerId)
{
- m_bShouldTryRegisterServer = true;
- m_bHasAddServerRequest = false;
- m_nNumRegistrationAttempts = 0;
+ // Don't try if we've reached the max registration attempts.
+ // In the future, we should probably allow servers to re-authenticate after a while if the MS was down.
+ if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS)
+ {
+ return;
+ }
+
+ // Make sure to wait til the cooldown is over for DUPLICATE_SERVER failures.
+ if (Tier0::Plat_FloatTime() < m_fNextAddServerAttemptTime)
+ {
+ return;
+ }
+
+ // If we're not running any InternalAddServer() attempt in the background.
+ if (!addServerFuture.valid())
+ {
+ // Launch an attempt to add the local server to the master server.
+ InternalAddServer(pServerPresence);
+ }
+ }
+ else
+ {
+ // If we're not running any InternalUpdateServer() attempt in the background.
+ if (!updateServerFuture.valid())
+ {
+ // Launch an attempt to update the local server on the master server.
+ InternalUpdateServer(pServerPresence);
+ }
+ }
+}
+
+void MasterServerPresenceReporter::DestroyPresence(const ServerPresence* pServerPresence)
+{
+ // Don't call this if we don't have a server id.
+ if (!*g_pMasterServerManager->m_sOwnServerId)
+ {
+ return;
}
- void ReportPresence(const ServerPresence* pServerPresence) override
+ // Not bothering with better thread safety in this case since DestroyPresence() is called when the game is shutting down.
+ *g_pMasterServerManager->m_sOwnServerId = 0;
+
+ std::thread requestThread(
+ [this]
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), g_pMasterServerManager->m_sOwnServerId)
+ .c_str());
+
+ CURLcode result = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerPresenceReporter::RunFrame(double flCurrentTime, const ServerPresence* pServerPresence)
+{
+ // Check if we're already running an InternalAddServer() call in the background.
+ // If so, grab the result if it's ready.
+ if (addServerFuture.valid())
{
- // make a copy of presence for multithreading purposes
- ServerPresence threadedPresence(pServerPresence);
+ std::future_status status = addServerFuture.wait_for(0ms);
+ if (status != std::future_status::ready)
+ {
+ // Still running, no need to do anything.
+ return;
+ }
+
+ // Check the result.
+ auto resultData = addServerFuture.get();
+
+ g_pMasterServerManager->m_bSuccessfullyConnected = resultData.result != MasterServerReportPresenceResult::FailedNoConnect;
+
+ switch (resultData.result)
+ {
+ case MasterServerReportPresenceResult::Success:
+ // Copy over the server id and auth token granted by the MS.
+ strncpy_s(
+ g_pMasterServerManager->m_sOwnServerId,
+ sizeof(g_pMasterServerManager->m_sOwnServerId),
+ resultData.id.value().c_str(),
+ sizeof(g_pMasterServerManager->m_sOwnServerId) - 1);
+ strncpy_s(
+ g_pMasterServerManager->m_sOwnServerAuthToken,
+ sizeof(g_pMasterServerManager->m_sOwnServerAuthToken),
+ resultData.serverAuthToken.value().c_str(),
+ sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1);
+ break;
+ case MasterServerReportPresenceResult::FailedNoRetry:
+ case MasterServerReportPresenceResult::FailedNoConnect:
+ // If we failed to connect to the master server, or failed with no retry, stop trying.
+ m_nNumRegistrationAttempts = MAX_REGISTRATION_ATTEMPTS;
+ break;
+ case MasterServerReportPresenceResult::Failed:
+ ++m_nNumRegistrationAttempts;
+ break;
+ case MasterServerReportPresenceResult::FailedDuplicateServer:
+ ++m_nNumRegistrationAttempts;
+ // Wait at least twenty seconds until we re-attempt to add the server.
+ m_fNextAddServerAttemptTime = Tier0::Plat_FloatTime() + 20.0f;
+ break;
+ }
- if (!*g_pMasterServerManager->m_sOwnServerId)
+ if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS)
{
- if (m_bShouldTryRegisterServer && !m_bHasAddServerRequest)
+ spdlog::error("Reached max ms server registration attempts.");
+ }
+ }
+ else if (updateServerFuture.valid())
+ {
+ // Check if the InternalUpdateServer() call completed.
+ std::future_status status = updateServerFuture.wait_for(0ms);
+ if (status != std::future_status::ready)
+ {
+ // Still running, no need to do anything.
+ return;
+ }
+
+ auto resultData = updateServerFuture.get();
+ if (resultData.result == MasterServerReportPresenceResult::Success)
+ {
+ if (resultData.id)
{
- // add server
- std::thread addServerThread(
- [this, threadedPresence]
- {
- g_pMasterServerManager->m_sOwnServerId[0] = 0;
- g_pMasterServerManager->m_sOwnServerAuthToken[0] = 0;
+ strncpy_s(
+ g_pMasterServerManager->m_sOwnServerId,
+ sizeof(g_pMasterServerManager->m_sOwnServerId),
+ resultData.id.value().c_str(),
+ sizeof(g_pMasterServerManager->m_sOwnServerId) - 1);
+ }
- CURL* curl = curl_easy_init();
- SetCommonHttpClientOptions(curl);
+ if (resultData.serverAuthToken)
+ {
+ strncpy_s(
+ g_pMasterServerManager->m_sOwnServerAuthToken,
+ sizeof(g_pMasterServerManager->m_sOwnServerAuthToken),
+ resultData.serverAuthToken.value().c_str(),
+ sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1);
+ }
+ }
+ }
+}
- std::string readBuffer;
- curl_easy_setopt(curl, CURLOPT_POST, 1L);
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+void MasterServerPresenceReporter::InternalAddServer(const ServerPresence* pServerPresence)
+{
+ const ServerPresence threadedPresence(pServerPresence);
+ // Never call this with an ongoing InternalAddServer() call.
+ assert(!addServerFuture.valid());
- curl_mime* mime = curl_mime_init(curl);
- curl_mimepart* part = curl_mime_addpart(mime);
+ g_pMasterServerManager->m_sOwnServerId[0] = 0;
+ g_pMasterServerManager->m_sOwnServerAuthToken[0] = 0;
- curl_mime_data(
- part, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->m_sOwnModInfoJson.size());
- curl_mime_name(part, "modinfo");
- curl_mime_filename(part, "modinfo.json");
- curl_mime_type(part, "application/json");
+ std::string modInfo = g_pMasterServerManager->m_sOwnModInfoJson;
+ std::string hostname = Cvar_ns_masterserver_hostname->GetString();
- curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
+ spdlog::info("Attempting to register the local server to the master server.");
- // format every paramter because computers hate me
- {
- char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL);
- char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL);
- char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL);
- char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL);
- char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL);
-
- curl_easy_setopt(
- curl,
- CURLOPT_URL,
- fmt::format(
- "{}/server/"
- "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}",
- Cvar_ns_masterserver_hostname->GetString(),
- threadedPresence.m_iPort,
- threadedPresence.m_iAuthPort,
- nameEscaped,
- descEscaped,
- mapEscaped,
- playlistEscaped,
- threadedPresence.m_iMaxPlayers,
- passwordEscaped)
- .c_str());
-
- curl_free(nameEscaped);
- curl_free(descEscaped);
- curl_free(mapEscaped);
- curl_free(playlistEscaped);
- curl_free(passwordEscaped);
- }
+ addServerFuture = std::async(
+ std::launch::async,
+ [threadedPresence, modInfo, hostname]
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
- CURLcode result = curl_easy_perform(curl);
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
- if (result == CURLcode::CURLE_OK)
- {
- g_pMasterServerManager->m_bSuccessfullyConnected = true;
-
- rapidjson_document serverAddedJson;
- serverAddedJson.Parse(readBuffer.c_str());
-
- if (serverAddedJson.HasParseError())
- {
- spdlog::error(
- "Failed reading masterserver authentication response: encountered parse error \"{}\"",
- rapidjson::GetParseError_En(serverAddedJson.GetParseError()));
- goto REQUEST_END_CLEANUP;
- }
-
- if (!serverAddedJson.IsObject())
- {
- spdlog::error("Failed reading masterserver authentication response: root object is not an object");
- goto REQUEST_END_CLEANUP;
- }
-
- if (serverAddedJson.HasMember("error"))
- {
- spdlog::error("Failed reading masterserver response: got fastify error response");
- spdlog::error(readBuffer);
-
- if (serverAddedJson["error"].HasMember("enum") &&
- !strcmp(serverAddedJson["error"]["enum"].GetString(), "DUPLICATE_SERVER"))
- {
- if (++m_nNumRegistrationAttempts == MAX_REGISTRATION_ATTEMPTS)
- m_bShouldTryRegisterServer = false;
-
- goto REQUEST_END_CLEANUP_RETRY;
- }
-
- goto REQUEST_END_CLEANUP;
- }
-
- if (!serverAddedJson["success"].IsTrue())
- {
- spdlog::error("Adding server to masterserver failed: \"success\" is not true");
- goto REQUEST_END_CLEANUP;
- }
-
- if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() ||
- !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString())
- {
- spdlog::error("Failed reading masterserver response: malformed json object");
- goto REQUEST_END_CLEANUP;
- }
-
- strncpy_s(
- g_pMasterServerManager->m_sOwnServerId,
- sizeof(g_pMasterServerManager->m_sOwnServerId),
- serverAddedJson["id"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerId) - 1);
-
- strncpy_s(
- g_pMasterServerManager->m_sOwnServerAuthToken,
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken),
- serverAddedJson["serverAuthToken"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1);
- }
- else
- {
- spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result));
- g_pMasterServerManager->m_bSuccessfullyConnected = false;
- }
+ curl_mime* mime = curl_mime_init(curl);
+ curl_mimepart* part = curl_mime_addpart(mime);
+
+ // Lambda to quickly cleanup resources and return a value.
+ auto ReturnCleanup =
+ [curl, mime](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "")
+ {
+ curl_easy_cleanup(curl);
+ curl_mime_free(mime);
+
+ MasterServerPresenceReporter::ReportPresenceResultData data;
+ data.result = result;
+ data.id = id;
+ data.serverAuthToken = serverAuthToken;
+
+ return data;
+ };
+
+ curl_mime_data(part, modInfo.c_str(), modInfo.size());
+ curl_mime_name(part, "modinfo");
+ curl_mime_filename(part, "modinfo.json");
+ curl_mime_type(part, "application/json");
- REQUEST_END_CLEANUP:
- m_bShouldTryRegisterServer = false;
+ curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
- REQUEST_END_CLEANUP_RETRY:
- m_bHasAddServerRequest = false;
+ // format every paramter because computers hate me
+ {
+ char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL);
+ char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL);
+ char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL);
+ char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL);
+ char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL);
- curl_easy_cleanup(curl);
- curl_mime_free(mime);
- });
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/server/"
+ "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}",
+ hostname.c_str(),
+ threadedPresence.m_iPort,
+ threadedPresence.m_iAuthPort,
+ nameEscaped,
+ descEscaped,
+ mapEscaped,
+ playlistEscaped,
+ threadedPresence.m_iMaxPlayers,
+ passwordEscaped)
+ .c_str());
- m_bHasAddServerRequest = true;
- addServerThread.detach();
+ curl_free(nameEscaped);
+ curl_free(descEscaped);
+ curl_free(mapEscaped);
+ curl_free(playlistEscaped);
+ curl_free(passwordEscaped);
}
- }
- else
- {
- // update server
- std::thread updateServerThread(
- [threadedPresence]
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ rapidjson_document serverAddedJson;
+ serverAddedJson.Parse(readBuffer.c_str());
+
+ // If we could not parse the JSON or it isn't an object, assume the MS is either wrong or we're completely out of date.
+ // No retry.
+ if (serverAddedJson.HasParseError())
{
- CURL* curl = curl_easy_init();
- SetCommonHttpClientOptions(curl);
+ spdlog::error(
+ "Failed reading masterserver authentication response: encountered parse error \"{}\"",
+ rapidjson::GetParseError_En(serverAddedJson.GetParseError()));
+ return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry);
+ }
- std::string readBuffer;
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
- curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L);
+ if (!serverAddedJson.IsObject())
+ {
+ spdlog::error("Failed reading masterserver authentication response: root object is not an object");
+ return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry);
+ }
- // send all registration info so we have all necessary info to reregister our server if masterserver goes down,
- // without a restart this isn't threadsafe :terror:
+ if (serverAddedJson.HasMember("error"))
+ {
+ spdlog::error("Failed reading masterserver response: got fastify error response");
+ spdlog::error(readBuffer);
+
+ // If this is DUPLICATE_SERVER, we'll retry adding the server every 20 seconds.
+ // The master server will only update its internal server list and clean up dead servers on certain events.
+ // And then again, only if a player requests the server list after the cooldown (1 second by default), or a server is
+ // added/updated/removed. In any case this needs to be fixed in the master server rewrite.
+ if (serverAddedJson["error"].HasMember("enum") &&
+ strcmp(serverAddedJson["error"]["enum"].GetString(), "DUPLICATE_SERVER") == 0)
{
- char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL);
- char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL);
- char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL);
- char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL);
- char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL);
-
- curl_easy_setopt(
- curl,
- CURLOPT_URL,
- fmt::format(
- "{}/server/"
- "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&"
- "maxPlayers={}&password={}",
- Cvar_ns_masterserver_hostname->GetString(),
- g_pMasterServerManager->m_sOwnServerId,
- threadedPresence.m_iPort,
- threadedPresence.m_iAuthPort,
- nameEscaped,
- descEscaped,
- mapEscaped,
- playlistEscaped,
- threadedPresence.m_iPlayerCount,
- threadedPresence.m_iMaxPlayers,
- passwordEscaped)
- .c_str());
-
- curl_free(nameEscaped);
- curl_free(descEscaped);
- curl_free(mapEscaped);
- curl_free(playlistEscaped);
- curl_free(passwordEscaped);
+ spdlog::error("Cooling down while the master server cleans the dead server entry, if any.");
+ return ReturnCleanup(MasterServerReportPresenceResult::FailedDuplicateServer);
}
- curl_mime* mime = curl_mime_init(curl);
- curl_mimepart* part = curl_mime_addpart(mime);
+ // Retry until we reach max retries.
+ return ReturnCleanup(MasterServerReportPresenceResult::Failed);
+ }
- curl_mime_data(
- part, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->m_sOwnModInfoJson.size());
- curl_mime_name(part, "modinfo");
- curl_mime_filename(part, "modinfo.json");
- curl_mime_type(part, "application/json");
+ if (!serverAddedJson["success"].IsTrue())
+ {
+ spdlog::error("Adding server to masterserver failed: \"success\" is not true");
+ return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry);
+ }
- curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
+ if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() ||
+ !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString())
+ {
+ spdlog::error("Failed reading masterserver response: malformed json object");
+ return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry);
+ }
- CURLcode result = curl_easy_perform(curl);
+ spdlog::info("Successfully registered the local server to the master server.");
+ return ReturnCleanup(
+ MasterServerReportPresenceResult::Success,
+ serverAddedJson["id"].GetString(),
+ serverAddedJson["serverAuthToken"].GetString());
+ }
+ else
+ {
+ spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result));
+ return ReturnCleanup(MasterServerReportPresenceResult::FailedNoConnect);
+ }
+ });
+}
- if (result == CURLcode::CURLE_OK)
- {
- rapidjson_document serverAddedJson;
- serverAddedJson.Parse(readBuffer.c_str());
+void MasterServerPresenceReporter::InternalUpdateServer(const ServerPresence* pServerPresence)
+{
+ const ServerPresence threadedPresence(pServerPresence);
- if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject())
- {
- if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString())
- {
- strncpy_s(
- g_pMasterServerManager->m_sOwnServerId,
- sizeof(g_pMasterServerManager->m_sOwnServerId),
- serverAddedJson["id"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerId) - 1);
- }
-
- if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString())
- {
-
- strncpy_s(
- g_pMasterServerManager->m_sOwnServerAuthToken,
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken),
- serverAddedJson["serverAuthToken"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1);
- }
- }
- }
- else
- spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result));
+ // Never call this with an ongoing InternalUpdateServer() call.
+ assert(!updateServerFuture.valid());
- curl_easy_cleanup(curl);
- });
- updateServerThread.detach();
- }
- }
+ const std::string serverId = g_pMasterServerManager->m_sOwnServerId;
+ const std::string hostname = Cvar_ns_masterserver_hostname->GetString();
+ const std::string modinfo = g_pMasterServerManager->m_sOwnModInfoJson;
- void DestroyPresence(const ServerPresence* pServerPresence) override
- {
- // dont call this if we don't have a server id
- if (!*g_pMasterServerManager->m_sOwnServerId)
- return;
+ updateServerFuture = std::async(
+ std::launch::async,
+ [threadedPresence, serverId, hostname, modinfo]
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
- std::thread requestThread(
- [this]
+ // Lambda to quickly cleanup resources and return a value.
+ auto ReturnCleanup = [curl](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "")
{
- CURL* curl = curl_easy_init();
- SetCommonHttpClientOptions(curl);
+ curl_easy_cleanup(curl);
+
+ MasterServerPresenceReporter::ReportPresenceResultData data;
+ data.result = result;
+
+ if (id != nullptr)
+ {
+ data.id = id;
+ }
+
+ if (serverAuthToken != nullptr)
+ {
+ data.serverAuthToken = serverAuthToken;
+ }
+
+ return data;
+ };
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+ curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L);
+
+ // send all registration info so we have all necessary info to reregister our server if masterserver goes down,
+ // without a restart this isn't threadsafe :terror:
+ {
+ char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL);
+ char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL);
+ char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL);
+ char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL);
+ char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL);
- std::string readBuffer;
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
curl_easy_setopt(
curl,
CURLOPT_URL,
fmt::format(
- "{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), g_pMasterServerManager->m_sOwnServerId)
+ "{}/server/"
+ "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&"
+ "maxPlayers={}&password={}",
+ hostname.c_str(),
+ serverId.c_str(),
+ threadedPresence.m_iPort,
+ threadedPresence.m_iAuthPort,
+ nameEscaped,
+ descEscaped,
+ mapEscaped,
+ playlistEscaped,
+ threadedPresence.m_iPlayerCount,
+ threadedPresence.m_iMaxPlayers,
+ passwordEscaped)
.c_str());
- *g_pMasterServerManager->m_sOwnServerId = 0;
- CURLcode result = curl_easy_perform(curl);
+ curl_free(nameEscaped);
+ curl_free(descEscaped);
+ curl_free(mapEscaped);
+ curl_free(playlistEscaped);
+ curl_free(passwordEscaped);
+ }
- if (result == CURLcode::CURLE_OK)
- g_pMasterServerManager->m_bSuccessfullyConnected = true;
- else
- g_pMasterServerManager->m_bSuccessfullyConnected = false;
+ curl_mime* mime = curl_mime_init(curl);
+ curl_mimepart* part = curl_mime_addpart(mime);
- curl_easy_cleanup(curl);
- });
+ curl_mime_data(part, modinfo.c_str(), modinfo.size());
+ curl_mime_name(part, "modinfo");
+ curl_mime_filename(part, "modinfo.json");
+ curl_mime_type(part, "application/json");
- requestThread.detach();
- }
-};
+ curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
-void ConCommand_ns_fetchservers(const CCommand& args)
-{
- g_pMasterServerManager->RequestServerList();
-}
+ CURLcode result = curl_easy_perform(curl);
-MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {}
+ if (result == CURLcode::CURLE_OK)
+ {
+ rapidjson_document serverAddedJson;
+ serverAddedJson.Parse(readBuffer.c_str());
-ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module))
-{
- g_pMasterServerManager = new MasterServerManager;
+ const char* updatedId = nullptr;
+ const char* updatedAuthToken = nullptr;
- Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, "");
- Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console");
+ if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject())
+ {
+ if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString())
+ {
+ updatedId = serverAddedJson["id"].GetString();
+ }
- RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL);
+ if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString())
+ {
+ updatedAuthToken = serverAddedJson["serverAuthToken"].GetString();
+ }
+ }
- MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter;
- g_pServerPresence->AddPresenceReporter(presenceReporter);
+ return ReturnCleanup(MasterServerReportPresenceResult::Success, updatedId, updatedAuthToken);
+ }
+ else
+ {
+ spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result));
+ return ReturnCleanup(MasterServerReportPresenceResult::Failed);
+ }
+ });
}
diff --git a/NorthstarDLL/masterserver.h b/NorthstarDLL/masterserver.h
index 5a4e2a91..0bbf7c76 100644
--- a/NorthstarDLL/masterserver.h
+++ b/NorthstarDLL/masterserver.h
@@ -1,9 +1,11 @@
#pragma once
+
#include "convar.h"
#include "serverpresence.h"
#include <winsock2.h>
#include <string>
#include <cstring>
+#include <future>
extern ConVar* Cvar_ns_masterserver_hostname;
extern ConVar* Cvar_ns_curl_log_enable;
@@ -124,3 +126,62 @@ class MasterServerManager
extern MasterServerManager* g_pMasterServerManager;
extern ConVar* Cvar_ns_masterserver_hostname;
+
+/** Result returned in the std::future of a MasterServerPresenceReporter::ReportPresence() call. */
+enum class MasterServerReportPresenceResult
+{
+ // Adding this server to the MS was successful.
+ Success,
+ // We failed to add this server to the MS and should retry.
+ Failed,
+ // We failed to add this server to the MS and shouldn't retry.
+ FailedNoRetry,
+ // We failed to even reach the MS.
+ FailedNoConnect,
+ // We failed to add the server because an existing server with the same ip:port exists.
+ FailedDuplicateServer,
+};
+
+class MasterServerPresenceReporter : public ServerPresenceReporter
+{
+ public:
+ /** Full data returned in the std::future of a MasterServerPresenceReporter::ReportPresence() call. */
+ struct ReportPresenceResultData
+ {
+ MasterServerReportPresenceResult result;
+
+ std::optional<std::string> id;
+ std::optional<std::string> serverAuthToken;
+ };
+
+ const int MAX_REGISTRATION_ATTEMPTS = 5;
+
+ // Called to initialise the master server presence reporter's state.
+ void CreatePresence(const ServerPresence* pServerPresence) override;
+
+ // Run on an internal to either add the server to the MS or update it.
+ void ReportPresence(const ServerPresence* pServerPresence) override;
+
+ // Called when we need to remove the server from the master server.
+ void DestroyPresence(const ServerPresence* pServerPresence) override;
+
+ // Called every frame.
+ void RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) override;
+
+ protected:
+ // Contains the async logic to add the server to the MS.
+ void InternalAddServer(const ServerPresence* pServerPresence);
+
+ // Contains the async logic to update the server on the MS.
+ void InternalUpdateServer(const ServerPresence* pServerPresence);
+
+ // The future used for InternalAddServer() calls.
+ std::future<ReportPresenceResultData> addServerFuture;
+
+ // The future used for InternalAddServer() calls.
+ std::future<ReportPresenceResultData> updateServerFuture;
+
+ int m_nNumRegistrationAttempts;
+
+ double m_fNextAddServerAttemptTime;
+};
diff --git a/NorthstarDLL/serverpresence.cpp b/NorthstarDLL/serverpresence.cpp
index 9b7b0f1a..fb8cf624 100644
--- a/NorthstarDLL/serverpresence.cpp
+++ b/NorthstarDLL/serverpresence.cpp
@@ -141,6 +141,10 @@ void ServerPresenceManager::RunFrame(double flCurrentTime)
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;
diff --git a/NorthstarDLL/serverpresence.h b/NorthstarDLL/serverpresence.h
index f27c9393..97b4654c 100644
--- a/NorthstarDLL/serverpresence.h
+++ b/NorthstarDLL/serverpresence.h
@@ -45,6 +45,7 @@ class ServerPresenceReporter
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