text: #include "pch.h"
  #include "masterserver.h"
  #include "concommand.h"
  #include "gameutils.h"
  #include "hookutils.h"
  #include "libcurl/include/curl/curl.h"
  #include "serverauthentication.h"
  #include "gameutils.h"
  #include "rapidjson/document.h"
  #include "rapidjson/stringbuffer.h"
  #include "rapidjson/writer.h"
  #include "rapidjson/error/en.h"
  #include "modmanager.h"
  #include "misccommands.h"
  
  // NOTE for anyone reading this: we used to use httplib for requests here, but it had issues, so we're moving to curl now for masterserver requests
  // so httplib is used exclusively for server stuff now
  
  ConVar* Cvar_ns_masterserver_hostname;
  ConVar* Cvar_ns_report_server_to_masterserver;
  ConVar* Cvar_ns_report_sp_server_to_masterserver;
  
  //ConVar* Cvar_ns_auth_player_name;
  
  ConVar* Cvar_ns_server_name;
  ConVar* Cvar_ns_server_desc;
  ConVar* Cvar_ns_server_password;
  
  MasterServerManager* g_MasterServerManager;
  
  typedef void(*CHostState__State_NewGameType)(CHostState* hostState);
  CHostState__State_NewGameType CHostState__State_NewGame;
  
  typedef void(*CHostState__State_ChangeLevelMPType)(CHostState* hostState);
  CHostState__State_ChangeLevelMPType CHostState__State_ChangeLevelMP;
  
  typedef void(*CHostState__State_ChangeLevelSPType)(CHostState* hostState);
  CHostState__State_ChangeLevelSPType CHostState__State_ChangeLevelSP;
  
  typedef void(*CHostState__State_GameShutdownType)(CHostState* hostState);
  CHostState__State_GameShutdownType CHostState__State_GameShutdown;
  
  RemoteServerInfo::RemoteServerInfo(const char* newId, const char* newName, const char* newDescription, const char* newMap, const char* newPlaylist, int newPlayerCount, int newMaxPlayers, bool newRequiresPassword)
  {
  	// passworded servers don't have public ips
  	requiresPassword = newRequiresPassword;
  
  	strncpy((char*)id, newId, sizeof(id));
  	id[sizeof(id) - 1] = 0;
  	strncpy((char*)name, newName, sizeof(name));
  	name[sizeof(name) - 1] = 0;
  
  	description = std::string(newDescription);
  
  	strncpy((char*)map, newMap, sizeof(map));
  	map[sizeof(map) - 1] = 0;
  	strncpy((char*)playlist, newPlaylist, sizeof(playlist));
  	playlist[sizeof(playlist) - 1] = 0;
  
  	playerCount = newPlayerCount;
  	maxPlayers = newMaxPlayers;
  }
  
  void MasterServerManager::ClearServerList()
  {
  	// this doesn't really do anything lol, probably isn't threadsafe
  	m_requestingServerList = true;
  
  	m_remoteServers.clear();
  
  	m_requestingServerList = false;
  }
  
  size_t CurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp)
  {
  	((std::string*)userp)->append((char*)contents, size * nmemb);
  	return size * nmemb;
  }
  
  void MasterServerManager::AuthenticateOriginWithMasterServer(char* uid, char* originToken, char* playerName)
  {
  	
  
  	if (m_bOriginAuthWithMasterServerInProgress)
  		return;
  
  	// do this here so it's instantly set
  	m_bOriginAuthWithMasterServerInProgress = true;
  	std::string nameStr(playerName);
  	std::string uidStr(uid);
  	std::string tokenStr(originToken);
  	spdlog::info("username: {}", nameStr);
  	std::thread requestThread([this, uidStr,tokenStr ,nameStr]()
  		{
  			spdlog::info("Trying to authenticate with northstar masterserver for user {}", uidStr);
  
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/origin_auth?id={}&token={}&playerName={}", Cvar_ns_masterserver_hostname->m_pszString, uidStr, tokenStr , nameStr).c_str());
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  			{
  				m_successfullyConnected = true;
  
  				rapidjson::Document originAuthInfo;
  				originAuthInfo.Parse(readBuffer.c_str());
  
  				if (originAuthInfo.HasParseError())
  				{
  					spdlog::error("Failed reading origin auth info response: encountered parse error \{}\"", rapidjson::GetParseError_En(originAuthInfo.GetParseError()));
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!originAuthInfo.IsObject() || !originAuthInfo.HasMember("success"))
  				{
  					spdlog::error("Failed reading origin auth info response: malformed response object {}", readBuffer);
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (originAuthInfo["success"].IsTrue() && originAuthInfo.HasMember("token") && originAuthInfo["token"].IsString())
  				{
  					strncpy(m_ownClientAuthToken, originAuthInfo["token"].GetString(), sizeof(m_ownClientAuthToken));
  					m_ownClientAuthToken[sizeof(m_ownClientAuthToken) - 1] = 0;
  					spdlog::info("Northstar origin authentication completed successfully!");
  				}
  				else
  					spdlog::error("Northstar origin authentication failed");
  			}
  			else
  			{
  				spdlog::error("Failed performing northstar origin auth: error {}", curl_easy_strerror(result));
  				m_successfullyConnected = false;
  			}
  
  			// we goto this instead of returning so we always hit this
  		REQUEST_END_CLEANUP:
  			m_bOriginAuthWithMasterServerInProgress = false;
  			m_bOriginAuthWithMasterServerDone = true;
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::RequestServerList()
  {
  	// do this here so it's instantly set on call for scripts
  	m_scriptRequestingServerList = true;
  
  	std::thread requestThread([this]()
  		{
  			// make sure we never have 2 threads writing at once
  			// i sure do hope this is actually threadsafe
  			while (m_requestingServerList)
  				Sleep(100);
  
  			m_requestingServerList = true;
  			m_scriptRequestingServerList = true;
  
  			spdlog::info("Requesting server list from {}", Cvar_ns_masterserver_hostname->m_pszString);
  
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/servers", Cvar_ns_masterserver_hostname->m_pszString).c_str());
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  			{
  				m_successfullyConnected = true;
  
  				rapidjson::Document serverInfoJson;
  				serverInfoJson.Parse(readBuffer.c_str());
  
  				if (serverInfoJson.HasParseError())
  				{
  					spdlog::error("Failed reading masterserver response: encountered parse error \"{}\"", rapidjson::GetParseError_En(serverInfoJson.GetParseError()));
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (serverInfoJson.IsObject() && serverInfoJson.HasMember("error"))
  				{
  					spdlog::error("Failed reading masterserver response: got fastify error response");
  					spdlog::error(readBuffer);
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!serverInfoJson.IsArray())
  				{
  					spdlog::error("Failed reading masterserver response: root object is not an array");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				rapidjson::GenericArray<false, rapidjson::Value> serverArray = serverInfoJson.GetArray();
  
  				spdlog::info("Got {} servers", serverArray.Size());
  
  				for (auto& serverObj : serverArray)
  				{
  					if (!serverObj.IsObject())
  					{
  						spdlog::error("Failed reading masterserver response: member of server array is not an object");
  						goto REQUEST_END_CLEANUP;
  					}
  
  					// todo: verify json props are fine before adding to m_remoteServers
  					if (!serverObj.HasMember("id") || !serverObj["id"].IsString()
  						|| !serverObj.HasMember("name") || !serverObj["name"].IsString()
  						|| !serverObj.HasMember("description") || !serverObj["description"].IsString()
  						|| !serverObj.HasMember("map") || !serverObj["map"].IsString()
  						|| !serverObj.HasMember("playlist") || !serverObj["playlist"].IsString()
  						|| !serverObj.HasMember("playerCount") || !serverObj["playerCount"].IsNumber()
  						|| !serverObj.HasMember("maxPlayers") || !serverObj["maxPlayers"].IsNumber()
  						|| !serverObj.HasMember("hasPassword") || !serverObj["hasPassword"].IsBool()
  						|| !serverObj.HasMember("modInfo") || !serverObj["modInfo"].HasMember("Mods") || !serverObj["modInfo"]["Mods"].IsArray())
  					{
  						spdlog::error("Failed reading masterserver response: malformed server object");
  						continue;
  					};
  
  					const char* id = serverObj["id"].GetString();
  
  					RemoteServerInfo* newServer = nullptr;
  
  					bool createNewServerInfo = true;
  					for (RemoteServerInfo& server : m_remoteServers)
  					{
  						// if server already exists, update info rather than adding to it
  						if (!strncmp((const char*)server.id, id, 32))
  						{
  							server = RemoteServerInfo(id, serverObj["name"].GetString(), serverObj["description"].GetString(), serverObj["map"].GetString(), serverObj["playlist"].GetString(), serverObj["playerCount"].GetInt(), serverObj["maxPlayers"].GetInt(), serverObj["hasPassword"].IsTrue());
  							newServer = &server;
  							createNewServerInfo = false;
  							break;
  						}
  					}
  
  					// server didn't exist
  					if (createNewServerInfo)
  						newServer = &m_remoteServers.emplace_back(id, serverObj["name"].GetString(), serverObj["description"].GetString(), serverObj["map"].GetString(), serverObj["playlist"].GetString(), serverObj["playerCount"].GetInt(), serverObj["maxPlayers"].GetInt(), serverObj["hasPassword"].IsTrue());
  
  					newServer->requiredMods.clear();
  					for (auto& requiredMod : serverObj["modInfo"]["Mods"].GetArray())
  					{
  						RemoteModInfo modInfo;
  
  						if (!requiredMod.HasMember("RequiredOnClient") || !requiredMod["RequiredOnClient"].IsTrue())
  							continue;
  
  						if (!requiredMod.HasMember("Name") || !requiredMod["Name"].IsString())
  							continue;
  						modInfo.Name = requiredMod["Name"].GetString();
  
  						if (!requiredMod.HasMember("Version") || !requiredMod["Version"].IsString())
  							continue;
  						modInfo.Version = requiredMod["Version"].GetString();
  
  						newServer->requiredMods.push_back(modInfo);
  					}
  
  					spdlog::info("Server {} on map {} with playlist {} has {}/{} players", serverObj["name"].GetString(), serverObj["map"].GetString(), serverObj["playlist"].GetString(), serverObj["playerCount"].GetInt(), serverObj["maxPlayers"].GetInt());
  				}
  
  				std::sort(m_remoteServers.begin(), m_remoteServers.end(), [](RemoteServerInfo& a, RemoteServerInfo& b) 
  				{
  					return a.playerCount > b.playerCount;
  				});
  			}
  			else
  			{
  				spdlog::error("Failed requesting servers: error {}", curl_easy_strerror(result));
  				m_successfullyConnected = false;
  			}
  
  			// we goto this instead of returning so we always hit this
  		REQUEST_END_CLEANUP:
  			m_requestingServerList = false;
  			m_scriptRequestingServerList = false;
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::RequestMainMenuPromos()
  {
  	m_bHasMainMenuPromoData = false;
  
  	std::thread requestThread([this]()
  		{
  			while (m_bOriginAuthWithMasterServerInProgress || !m_bOriginAuthWithMasterServerDone)
  				Sleep(500);
  
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/mainmenupromos", Cvar_ns_masterserver_hostname->m_pszString).c_str());
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  			{
  				m_successfullyConnected = true;
  
  				rapidjson::Document mainMenuPromoJson;
  				mainMenuPromoJson.Parse(readBuffer.c_str());
  
  				if (mainMenuPromoJson.HasParseError())
  				{
  					spdlog::error("Failed reading masterserver main menu promos response: encountered parse error \"{}\"", rapidjson::GetParseError_En(mainMenuPromoJson.GetParseError()));
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!mainMenuPromoJson.IsObject())
  				{
  					spdlog::error("Failed reading masterserver main menu promos response: root object is not an object");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (mainMenuPromoJson.HasMember("error"))
  				{
  					spdlog::error("Failed reading masterserver response: got fastify error response");
  					spdlog::error(readBuffer);
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!mainMenuPromoJson.HasMember("newInfo") || !mainMenuPromoJson["newInfo"].IsObject() ||
  					!mainMenuPromoJson["newInfo"].HasMember("Title1") || !mainMenuPromoJson["newInfo"]["Title1"].IsString() ||
  					!mainMenuPromoJson["newInfo"].HasMember("Title2") || !mainMenuPromoJson["newInfo"]["Title2"].IsString() ||
  					!mainMenuPromoJson["newInfo"].HasMember("Title3") || !mainMenuPromoJson["newInfo"]["Title3"].IsString() ||
  
  					!mainMenuPromoJson.HasMember("largeButton") || !mainMenuPromoJson["largeButton"].IsObject() ||
  					!mainMenuPromoJson["largeButton"].HasMember("Title") || !mainMenuPromoJson["largeButton"]["Title"].IsString() ||
  					!mainMenuPromoJson["largeButton"].HasMember("Text") || !mainMenuPromoJson["largeButton"]["Text"].IsString() ||
  					!mainMenuPromoJson["largeButton"].HasMember("Url") || !mainMenuPromoJson["largeButton"]["Url"].IsString() ||
  					!mainMenuPromoJson["largeButton"].HasMember("ImageIndex") || !mainMenuPromoJson["largeButton"]["ImageIndex"].IsNumber() ||
  
  					!mainMenuPromoJson.HasMember("smallButton1") || !mainMenuPromoJson["smallButton1"].IsObject() ||
  					!mainMenuPromoJson["smallButton1"].HasMember("Title") || !mainMenuPromoJson["smallButton1"]["Title"].IsString() ||
  					!mainMenuPromoJson["smallButton1"].HasMember("Url") || !mainMenuPromoJson["smallButton1"]["Url"].IsString() ||
  					!mainMenuPromoJson["smallButton1"].HasMember("ImageIndex") || !mainMenuPromoJson["smallButton1"]["ImageIndex"].IsNumber() ||
  
  					!mainMenuPromoJson.HasMember("smallButton2") || !mainMenuPromoJson["smallButton2"].IsObject() ||
  					!mainMenuPromoJson["smallButton2"].HasMember("Title") || !mainMenuPromoJson["smallButton2"]["Title"].IsString() ||
  					!mainMenuPromoJson["smallButton2"].HasMember("Url") || !mainMenuPromoJson["smallButton2"]["Url"].IsString() ||
  					!mainMenuPromoJson["smallButton2"].HasMember("ImageIndex") || !mainMenuPromoJson["smallButton2"]["ImageIndex"].IsNumber())
  				{
  					spdlog::error("Failed reading masterserver main menu promos response: malformed json object");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				m_MainMenuPromoData.newInfoTitle1 = mainMenuPromoJson["newInfo"]["Title1"].GetString();
  				m_MainMenuPromoData.newInfoTitle2 = mainMenuPromoJson["newInfo"]["Title2"].GetString();
  				m_MainMenuPromoData.newInfoTitle3 = mainMenuPromoJson["newInfo"]["Title3"].GetString();
  
  				m_MainMenuPromoData.largeButtonTitle = mainMenuPromoJson["largeButton"]["Title"].GetString();
  				m_MainMenuPromoData.largeButtonText = mainMenuPromoJson["largeButton"]["Text"].GetString();
  				m_MainMenuPromoData.largeButtonUrl = mainMenuPromoJson["largeButton"]["Url"].GetString();
  				m_MainMenuPromoData.largeButtonImageIndex = mainMenuPromoJson["largeButton"]["ImageIndex"].GetInt();
  
  				m_MainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString();
  				m_MainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString();
  				m_MainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt();
  
  				m_MainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString();
  				m_MainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString();
  				m_MainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt();
  
  				m_bHasMainMenuPromoData = true;
  			}
  			else
  			{
  				spdlog::error("Failed requesting main menu promos: error {}", curl_easy_strerror(result));
  				m_successfullyConnected = false;
  			}
  
  		REQUEST_END_CLEANUP:
  			// nothing lol
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::AuthenticateWithOwnServer(char* uid, char* playerToken)
  {
  	// dont wait, just stop if we're trying to do 2 auth requests at once
  	if (m_authenticatingWithGameServer)
  		return;
  
  	m_authenticatingWithGameServer = true;
  	m_scriptAuthenticatingWithGameServer = true;
  	m_successfullyAuthenticatedWithGameServer = false;
  	
  	std::string uidStr(uid);
  	std::string tokenStr(playerToken);
  
  	std::thread requestThread([this, uidStr, tokenStr]()
  		{
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/auth_with_self?id={}&playerToken={}", Cvar_ns_masterserver_hostname->m_pszString, uidStr, tokenStr).c_str());
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  			{
  				m_successfullyConnected = true;
  
  				rapidjson::Document authInfoJson;
  				authInfoJson.Parse(readBuffer.c_str());
  
  				if (authInfoJson.HasParseError())
  				{
  					spdlog::error("Failed reading masterserver authentication response: encountered parse error \"{}\"", rapidjson::GetParseError_En(authInfoJson.GetParseError()));
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!authInfoJson.IsObject())
  				{
  					spdlog::error("Failed reading masterserver authentication response: root object is not an object");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (authInfoJson.HasMember("error"))
  				{
  					spdlog::error("Failed reading masterserver response: got fastify error response");
  					spdlog::error(readBuffer);
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!authInfoJson["success"].IsTrue())
  				{
  					spdlog::error("Authentication with masterserver failed: \"success\" is not true");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!authInfoJson.HasMember("success") || !authInfoJson.HasMember("id") || !authInfoJson["id"].IsString() || !authInfoJson.HasMember("authToken") || !authInfoJson["authToken"].IsString() || !authInfoJson.HasMember("persistentData") || !authInfoJson["persistentData"].IsArray())
  				{
  					spdlog::error("Failed reading masterserver authentication response: malformed json object");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				AuthData newAuthData;
  				strncpy(newAuthData.uid, authInfoJson["id"].GetString(), sizeof(newAuthData.uid));
  				newAuthData.uid[sizeof(newAuthData.uid) - 1] = 0;
  
  				newAuthData.pdataSize = authInfoJson["persistentData"].GetArray().Size();
  				newAuthData.pdata = new char[newAuthData.pdataSize];
  				//memcpy(newAuthData.pdata, authInfoJson["persistentData"].GetString(), newAuthData.pdataSize);
  
  				int i = 0;
  				// note: persistentData is a uint8array because i had problems getting strings to behave, it sucks but it's just how it be unfortunately
  				// potentially refactor later
  				for (auto& byte : authInfoJson["persistentData"].GetArray())
  				{
  					if (!byte.IsUint() || byte.GetUint() > 255)
  					{
  						spdlog::error("Failed reading masterserver authentication response: malformed json object");
  						goto REQUEST_END_CLEANUP;
  					}
  
  					newAuthData.pdata[i++] = (char)byte.GetUint();
  				}
  
  				std::lock_guard<std::mutex> guard(g_ServerAuthenticationManager->m_authDataMutex);
  				g_ServerAuthenticationManager->m_authData.clear();
  				g_ServerAuthenticationManager->m_authData.insert(std::make_pair(authInfoJson["authToken"].GetString(), newAuthData));
  
  				m_successfullyAuthenticatedWithGameServer = true;
  			}
  			else
  			{
  				spdlog::error("Failed authenticating with own server: error {}", curl_easy_strerror(result));
  				m_successfullyConnected = false;
  				m_successfullyAuthenticatedWithGameServer = false;
  				m_scriptAuthenticatingWithGameServer = false;
  			}
  
  		REQUEST_END_CLEANUP:
  			m_authenticatingWithGameServer = false;
  			m_scriptAuthenticatingWithGameServer = false;
  
  			if (m_bNewgameAfterSelfAuth)
  			{
  				// pretty sure this is threadsafe?
  				Cbuf_AddText(Cbuf_GetCurrentPlayer(), "ns_end_reauth_and_leave_to_lobby", cmd_source_t::kCommandSrcCode);
  				m_bNewgameAfterSelfAuth = false;
  			}
  
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::AuthenticateWithServer(char* uid, char* playerToken, char* serverId, char* password)
  {
  	// dont wait, just stop if we're trying to do 2 auth requests at once
  	if (m_authenticatingWithGameServer)
  		return;
  
  	m_authenticatingWithGameServer = true;
  	m_scriptAuthenticatingWithGameServer = true;
  	m_successfullyAuthenticatedWithGameServer = false;
  
  	std::string uidStr(uid);
  	std::string tokenStr(playerToken);
  	std::string serverIdStr(serverId);
  	std::string passwordStr(password);
  
  	std::thread requestThread([this, uidStr, tokenStr, serverIdStr, passwordStr]()
  		{
  			// esnure that any persistence saving is done, so we know masterserver has newest
  			while (m_savingPersistentData)
  				Sleep(100);
  
  			spdlog::info("Attempting authentication with server of id \"{}\"", serverIdStr);
  
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			{
  				char* escapedPassword = curl_easy_escape(curl, passwordStr.c_str(), passwordStr.length());
  
  				curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/auth_with_server?id={}&playerToken={}&server={}&password={}", Cvar_ns_masterserver_hostname->m_pszString, uidStr, tokenStr, serverIdStr, escapedPassword).c_str());
  			
  				curl_free(escapedPassword);
  			}
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  			{
  				m_successfullyConnected = true;
  
  				rapidjson::Document connectionInfoJson;
  				connectionInfoJson.Parse(readBuffer.c_str());
  
  				if (connectionInfoJson.HasParseError())
  				{
  					spdlog::error("Failed reading masterserver authentication response: encountered parse error \"{}\"", rapidjson::GetParseError_En(connectionInfoJson.GetParseError()));
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!connectionInfoJson.IsObject())
  				{
  					spdlog::error("Failed reading masterserver authentication response: root object is not an object");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (connectionInfoJson.HasMember("error"))
  				{
  					spdlog::error("Failed reading masterserver response: got fastify error response");
  					spdlog::error(readBuffer);
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!connectionInfoJson["success"].IsTrue())
  				{
  					spdlog::error("Authentication with masterserver failed: \"success\" is not true");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				if (!connectionInfoJson.HasMember("success") || !connectionInfoJson.HasMember("ip") || !connectionInfoJson["ip"].IsString() || !connectionInfoJson.HasMember("port") || !connectionInfoJson["port"].IsNumber() || !connectionInfoJson.HasMember("authToken") || !connectionInfoJson["authToken"].IsString())
  				{
  					spdlog::error("Failed reading masterserver authentication response: malformed json object");
  					goto REQUEST_END_CLEANUP;
  				}
  
  				m_pendingConnectionInfo.ip.S_un.S_addr = inet_addr(connectionInfoJson["ip"].GetString());
  				m_pendingConnectionInfo.port = connectionInfoJson["port"].GetInt();
  
  				strncpy(m_pendingConnectionInfo.authToken, connectionInfoJson["authToken"].GetString(), 31);
  				m_pendingConnectionInfo.authToken[31] = 0;
  
  				m_hasPendingConnectionInfo = true;
  				m_successfullyAuthenticatedWithGameServer = true;
  			}
  			else
  			{
  				spdlog::error("Failed authenticating with server: error {}", curl_easy_strerror(result));
  				m_successfullyConnected = false;
  				m_successfullyAuthenticatedWithGameServer = false;
  				m_scriptAuthenticatingWithGameServer = false;
  			}
  
  		REQUEST_END_CLEANUP:
  			m_authenticatingWithGameServer = false;
  			m_scriptAuthenticatingWithGameServer = false;
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::AddSelfToServerList(int port, int authPort, char* name, char* description, char* map, char* playlist, int maxPlayers, char* password)
  {
  	if (!Cvar_ns_report_server_to_masterserver->m_nValue)
  		return;
  
  	if (!Cvar_ns_report_sp_server_to_masterserver->m_nValue && !strncmp(map, "sp_", 3))
  	{
  		m_bRequireClientAuth = false;
  		return;
  	}
  
  	m_bRequireClientAuth = true;
  
  	std::string strName(name);
  	std::string strDescription(description);
  	std::string strMap(map);
  	std::string strPlaylist(playlist);
  	std::string strPassword(password);
  
  	std::thread requestThread([this, port, authPort, strName, strDescription, strMap, strPlaylist, maxPlayers, strPassword]
  		{
  			m_ownServerId[0] = 0;
  			m_ownServerAuthToken[0] = 0;
  
  			// build modinfo obj
  			rapidjson::Document modinfoDoc;
  			modinfoDoc.SetObject();
  			modinfoDoc.AddMember("Mods", rapidjson::Value(rapidjson::kArrayType), modinfoDoc.GetAllocator());
  
  			int currentModIndex = 0;
  			for (Mod& mod : g_ModManager->m_loadedMods)
  			{
  				if (!mod.Enabled || (!mod.RequiredOnClient && !mod.Pdiff.size()))
  					continue;
  
  				modinfoDoc["Mods"].PushBack(rapidjson::Value(rapidjson::kObjectType), modinfoDoc.GetAllocator());
  				modinfoDoc["Mods"][currentModIndex].AddMember("Name", rapidjson::StringRef(&mod.Name[0]), modinfoDoc.GetAllocator());
  				modinfoDoc["Mods"][currentModIndex].AddMember("Version", rapidjson::StringRef(&mod.Version[0]), modinfoDoc.GetAllocator());
  				modinfoDoc["Mods"][currentModIndex].AddMember("RequiredOnClient", mod.RequiredOnClient, modinfoDoc.GetAllocator());
  				modinfoDoc["Mods"][currentModIndex].AddMember("Pdiff", rapidjson::StringRef(&mod.Pdiff[0]), modinfoDoc.GetAllocator());
  
  				currentModIndex++;
  			}
  
  			rapidjson::StringBuffer buffer;
  			buffer.Clear();
  			rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  			modinfoDoc.Accept(writer);
  			const char* modInfoString = buffer.GetString();
  
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_POST, 1L);
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			curl_mime* mime = curl_mime_init(curl);
  			curl_mimepart* part = curl_mime_addpart(mime);
  
  			curl_mime_data(part, modInfoString, buffer.GetSize());
  			curl_mime_name(part, "modinfo");
  			curl_mime_filename(part, "modinfo.json");
  			curl_mime_type(part, "application/json");
  
  			curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
  
  			// format every paramter because computers hate me
  			{
  				char* nameEscaped = curl_easy_escape(curl, strName.c_str(), strName.length());
  				char* descEscaped = curl_easy_escape(curl, strDescription.c_str(), strDescription.length());
  				char* mapEscaped = curl_easy_escape(curl, strMap.c_str(), strMap.length());
  				char* playlistEscaped = curl_easy_escape(curl, strPlaylist.c_str(), strPlaylist.length());
  				char* passwordEscaped = curl_easy_escape(curl, strPassword.c_str(), strPassword.length());
  
  				curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/server/add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", Cvar_ns_masterserver_hostname->m_pszString, port, authPort, nameEscaped, descEscaped, mapEscaped, playlistEscaped, maxPlayers, passwordEscaped).c_str());
  
  				curl_free(nameEscaped);
  				curl_free(descEscaped);
  				curl_free(mapEscaped);
  				curl_free(playlistEscaped);
  				curl_free(passwordEscaped);
  			}
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  			{
  				m_successfullyConnected = 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);
  					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(m_ownServerId, serverAddedJson["id"].GetString(), sizeof(m_ownServerId));
  				m_ownServerId[sizeof(m_ownServerId) - 1] = 0;
  
  				strncpy(m_ownServerAuthToken, serverAddedJson["serverAuthToken"].GetString(), sizeof(m_ownServerAuthToken));
  				m_ownServerAuthToken[sizeof(m_ownServerAuthToken) - 1] = 0;
  
  				// heartbeat thread
  				// ideally this should actually be done in main thread, rather than on it's own thread, so it'd stop if server freezes
  				std::thread heartbeatThread([this] {
  					Sleep(5000);
  
  					do
  					{
  						CURL* curl = curl_easy_init();
  
  						std::string readBuffer;
  						curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
  						curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/server/heartbeat?id={}&playerCount={}", Cvar_ns_masterserver_hostname->m_pszString, m_ownServerId, g_ServerAuthenticationManager->m_additionalPlayerData.size()).c_str());
  						curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  						curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  						curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  						
  						CURLcode result = curl_easy_perform(curl);
  						if (result != CURLcode::CURLE_OK)
  							spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result));
  
  						curl_easy_cleanup(curl);
  						Sleep(10000);
  					} while (*m_ownServerId);
  				});
  
  				heartbeatThread.detach();
  			}
  			else
  			{
  				spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result));
  				m_successfullyConnected = false;
  			}
  
  		REQUEST_END_CLEANUP:
  			curl_easy_cleanup(curl);
  			curl_mime_free(mime);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::UpdateServerMapAndPlaylist(char* map, char* playlist, int maxPlayers)
  {
  	// dont call this if we don't have a server id
  	if (!*m_ownServerId)
  		return;
  
  	std::string strMap(map);
  	std::string strPlaylist(playlist);
  
  	std::thread requestThread([this, strMap, strPlaylist, maxPlayers]
  		{
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			// escape params
  			{
  				char* mapEscaped = curl_easy_escape(curl, strMap.c_str(), strMap.length());
  				char* playlistEscaped = curl_easy_escape(curl, strPlaylist.c_str(), strPlaylist.length());
  
  				curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/server/update_values?id={}&map={}&playlist={}&maxPlayers={}", Cvar_ns_masterserver_hostname->m_pszString, m_ownServerId, mapEscaped, playlistEscaped, maxPlayers).c_str());
  				
  				curl_free(mapEscaped);
  				curl_free(playlistEscaped);
  			}
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  				m_successfullyConnected = true;
  			else
  				m_successfullyConnected = false;
  
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::UpdateServerPlayerCount(int playerCount)
  {
  	// dont call this if we don't have a server id
  	if (!*m_ownServerId)
  		return;
  
  	std::thread requestThread([this, playerCount] 
  		{
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  			curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/server/update_values?id={}&playerCount={}", Cvar_ns_masterserver_hostname->m_pszString, m_ownServerId, playerCount).c_str());
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  				m_successfullyConnected = true;
  			else
  				m_successfullyConnected = false;
  
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::WritePlayerPersistentData(char* playerId, char* pdata, size_t pdataSize)
  {
  	// still call this if we don't have a server id, since lobbies that aren't port forwarded need to be able to call it
  	m_savingPersistentData = true;
  	if (!pdataSize)
  	{
  		spdlog::warn("attempted to write pdata of size 0!");
  		return;
  	}
  
  	std::string strPlayerId(playerId);
  	std::string strPdata(pdata, pdataSize);
  
  	std::thread requestThread([this, strPlayerId, strPdata, pdataSize]
  		{
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/accounts/write_persistence?id={}&serverId={}", Cvar_ns_masterserver_hostname->m_pszString, strPlayerId, m_ownServerId).c_str());
  			curl_easy_setopt(curl, CURLOPT_POST, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
  			curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
  
  			curl_mime* mime = curl_mime_init(curl);
  			curl_mimepart* part = curl_mime_addpart(mime);
  
  			curl_mime_data(part, strPdata.c_str(), pdataSize);
  			curl_mime_name(part, "pdata");
  			curl_mime_filename(part, "file.pdata");
  			curl_mime_type(part, "application/octet-stream");
  
  			curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  				m_successfullyConnected = true;
  			else
  				m_successfullyConnected = false;
  
  			curl_easy_cleanup(curl);
  
  			m_savingPersistentData = false;
  		});
  
  	requestThread.detach();
  }
  
  void MasterServerManager::RemoveSelfFromServerList()
  {
  	// dont call this if we don't have a server id
  	if (!*m_ownServerId || !Cvar_ns_report_server_to_masterserver->m_nValue)
  		return;
  
  	std::thread requestThread([this] {
  			CURL* curl = curl_easy_init();
  
  			std::string readBuffer;
  			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
  			curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
  			curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  			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->m_pszString, m_ownServerId).c_str());
  
  			CURLcode result = curl_easy_perform(curl);
  
  			if (result == CURLcode::CURLE_OK)
  				m_successfullyConnected = true;
  			else
  				m_successfullyConnected = false;
  
  			curl_easy_cleanup(curl);
  		});
  
  	requestThread.detach();
  }
  
  void ConCommand_ns_fetchservers(const CCommand& args)
  {
  	g_MasterServerManager->RequestServerList();
  }
  
  void CHostState__State_NewGameHook(CHostState* hostState)
  {
  	Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec autoexec_ns_server", cmd_source_t::kCommandSrcCode);
  	Cbuf_Execute();
  
  	// need to do this to ensure we don't go to private match
  	if (g_ServerAuthenticationManager->m_bNeedLocalAuthForNewgame)
  		SetCurrentPlaylist("tdm");
  
  	CHostState__State_NewGame(hostState);
  
  	int maxPlayers = 6;
  	char* maxPlayersVar = GetCurrentPlaylistVar("max_players", false);
  	if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this
  		maxPlayers = std::stoi(maxPlayersVar);
  
  	g_MasterServerManager->AddSelfToServerList(Cvar_hostport->m_nValue, Cvar_ns_player_auth_port->m_nValue, Cvar_ns_server_name->m_pszString, Cvar_ns_server_desc->m_pszString, hostState->m_levelName, (char*)GetCurrentPlaylistName(), maxPlayers, Cvar_ns_server_password->m_pszString);
  	g_ServerAuthenticationManager->StartPlayerAuthServer();
  	g_ServerAuthenticationManager->m_bNeedLocalAuthForNewgame = false;
  }
  
  void CHostState__State_ChangeLevelMPHook(CHostState* hostState)
  {
  	int maxPlayers = 6;
  	char* maxPlayersVar = GetCurrentPlaylistVar("max_players", false);
  	if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this
  		maxPlayers = std::stoi(maxPlayersVar);
  
  	g_MasterServerManager->UpdateServerMapAndPlaylist(hostState->m_levelName, (char*)GetCurrentPlaylistName(), maxPlayers);
  	CHostState__State_ChangeLevelMP(hostState);
  }
  
  void CHostState__State_ChangeLevelSPHook(CHostState* hostState)
  {
  	int maxPlayers = 6;
  	char* maxPlayersVar = GetCurrentPlaylistVar("max_players", false);
  	if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this
  		maxPlayers = std::stoi(maxPlayersVar);
  
  	g_MasterServerManager->UpdateServerMapAndPlaylist(hostState->m_levelName, (char*)GetCurrentPlaylistName(), maxPlayers);
  	CHostState__State_ChangeLevelSP(hostState);
  }
  
  void CHostState__State_GameShutdownHook(CHostState* hostState)
  {
  	g_MasterServerManager->RemoveSelfFromServerList();
  	g_ServerAuthenticationManager->StopPlayerAuthServer();
  
  	CHostState__State_GameShutdown(hostState);
  }
  
  MasterServerManager::MasterServerManager()
  {
  	curl_global_init(CURL_GLOBAL_DEFAULT);
  }
  
  void InitialiseSharedMasterServer(HMODULE baseAddress)
  {
  	Cvar_ns_masterserver_hostname = RegisterConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, "");
  	// unfortunately lib doesn't let us specify a port and still have https work
  	//Cvar_ns_masterserver_port = RegisterConVar("ns_masterserver_port", "8080", FCVAR_NONE, "");
  
  	Cvar_ns_server_name = RegisterConVar("ns_server_name", "Unnamed Northstar Server", FCVAR_GAMEDLL, "");
  	Cvar_ns_server_desc = RegisterConVar("ns_server_desc", "Default server description", FCVAR_GAMEDLL, "");
  	Cvar_ns_server_password = RegisterConVar("ns_server_password", "", FCVAR_GAMEDLL, "");
  	Cvar_ns_report_server_to_masterserver = RegisterConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, "");
  	Cvar_ns_report_sp_server_to_masterserver = RegisterConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, "");
  	g_MasterServerManager = new MasterServerManager;
  
  	RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "", FCVAR_CLIENTDLL);
  
  	HookEnabler hook;
  	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x16E7D0, CHostState__State_NewGameHook, reinterpret_cast<LPVOID*>(&CHostState__State_NewGame));
  	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x16E5D0, CHostState__State_ChangeLevelMPHook, reinterpret_cast<LPVOID*>(&CHostState__State_ChangeLevelMP));
  	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x16E520, CHostState__State_ChangeLevelSPHook, reinterpret_cast<LPVOID*>(&CHostState__State_ChangeLevelSP));
  	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x16E640, CHostState__State_GameShutdownHook, reinterpret_cast<LPVOID*>(&CHostState__State_GameShutdown));
  }