/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include "serverbrowser.h" #include "serverbrowser_http.h" #include "serverbrowser_ping_cache.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static constexpr const char *COMMUNITY_COUNTRY_NONE = "none"; static constexpr const char *COMMUNITY_TYPE_NONE = "None"; class CSortWrap { typedef bool (CServerBrowser::*SortFunc)(int, int) const; SortFunc m_pfnSort; CServerBrowser *m_pThis; public: CSortWrap(CServerBrowser *pServer, SortFunc Func) : m_pfnSort(Func), m_pThis(pServer) {} bool operator()(int a, int b) { return (g_Config.m_BrSortOrder ? (m_pThis->*m_pfnSort)(b, a) : (m_pThis->*m_pfnSort)(a, b)); } }; bool matchesPart(const char *a, const char *b) { return str_utf8_find_nocase(a, b) != nullptr; } bool matchesExactly(const char *a, const char *b) { return str_comp(a, &b[1]) == 0; } CServerBrowser::CServerBrowser() : m_CountriesFilter([this]() { return CurrentCommunities(); }), m_TypesFilter([this]() { return CurrentCommunities(); }) { m_ppServerlist = nullptr; m_pSortedServerlist = nullptr; m_NeedResort = false; m_Sorthash = 0; m_NumSortedServersCapacity = 0; m_NumServerCapacity = 0; m_ServerlistType = 0; m_BroadcastTime = 0; secure_random_fill(m_aTokenSeed, sizeof(m_aTokenSeed)); CleanUp(); } CServerBrowser::~CServerBrowser() { free(m_ppServerlist); free(m_pSortedServerlist); json_value_free(m_pDDNetInfo); delete m_pHttp; m_pHttp = nullptr; delete m_pPingCache; m_pPingCache = nullptr; } void CServerBrowser::SetBaseInfo(class CNetClient *pClient, const char *pNetVersion) { m_pNetClient = pClient; str_copy(m_aNetVersion, pNetVersion); m_pConsole = Kernel()->RequestInterface(); m_pConfigManager = Kernel()->RequestInterface(); m_pEngine = Kernel()->RequestInterface(); m_pFavorites = Kernel()->RequestInterface(); m_pFriends = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); m_pHttpClient = Kernel()->RequestInterface(); m_pPingCache = CreateServerBrowserPingCache(m_pConsole, m_pStorage); RegisterCommands(); } void CServerBrowser::OnInit() { m_pHttp = CreateServerBrowserHttp(m_pEngine, m_pConsole, m_pStorage, m_pHttpClient, g_Config.m_BrCachedBestServerinfoUrl); } void CServerBrowser::RegisterCommands() { m_pConfigManager->RegisterCallback(CServerBrowser::ConfigSaveCallback, this); m_pConsole->Register("add_favorite_community", "s[community_id]", CFGFLAG_CLIENT, Con_AddFavoriteCommunity, this, "Add a community as a favorite"); m_pConsole->Register("remove_favorite_community", "s[community_id]", CFGFLAG_CLIENT, Con_RemoveFavoriteCommunity, this, "Remove a community from the favorites"); m_pConsole->Register("add_excluded_community", "s[community_id]", CFGFLAG_CLIENT, Con_AddExcludedCommunity, this, "Add a community to the exclusion filter"); m_pConsole->Register("remove_excluded_community", "s[community_id]", CFGFLAG_CLIENT, Con_RemoveExcludedCommunity, this, "Remove a community from the exclusion filter"); m_pConsole->Register("add_excluded_country", "s[community_id] s[country_code]", CFGFLAG_CLIENT, Con_AddExcludedCountry, this, "Add a country to the exclusion filter for a specific community"); m_pConsole->Register("remove_excluded_country", "s[community_id] s[country_code]", CFGFLAG_CLIENT, Con_RemoveExcludedCountry, this, "Remove a country from the exclusion filter for a specific community"); m_pConsole->Register("add_excluded_type", "s[community_id] s[type]", CFGFLAG_CLIENT, Con_AddExcludedType, this, "Add a type to the exclusion filter for a specific community"); m_pConsole->Register("remove_excluded_type", "s[community_id] s[type]", CFGFLAG_CLIENT, Con_RemoveExcludedType, this, "Remove a type from the exclusion filter for a specific community"); m_pConsole->Register("leak_ip_address_to_all_servers", "", CFGFLAG_CLIENT, Con_LeakIpAddress, this, "Leaks your IP address to all servers by pinging each of them, also acquiring the latency in the process"); } void CServerBrowser::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); pThis->FavoriteCommunitiesFilter().Save(pConfigManager); pThis->CommunitiesFilter().Save(pConfigManager); pThis->CountriesFilter().Save(pConfigManager); pThis->TypesFilter().Save(pConfigManager); } void CServerBrowser::Con_AddFavoriteCommunity(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); if(!pThis->ValidateCommunityId(pCommunityId)) return; pThis->FavoriteCommunitiesFilter().Add(pCommunityId); } void CServerBrowser::Con_RemoveFavoriteCommunity(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); if(!pThis->ValidateCommunityId(pCommunityId)) return; pThis->FavoriteCommunitiesFilter().Remove(pCommunityId); } void CServerBrowser::Con_AddExcludedCommunity(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); if(!pThis->ValidateCommunityId(pCommunityId)) return; pThis->CommunitiesFilter().Add(pCommunityId); } void CServerBrowser::Con_RemoveExcludedCommunity(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); if(!pThis->ValidateCommunityId(pCommunityId)) return; pThis->CommunitiesFilter().Remove(pCommunityId); } void CServerBrowser::Con_AddExcludedCountry(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); const char *pCountryName = pResult->GetString(1); if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName)) return; pThis->CountriesFilter().Add(pCommunityId, pCountryName); } void CServerBrowser::Con_RemoveExcludedCountry(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); const char *pCountryName = pResult->GetString(1); if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName)) return; pThis->CountriesFilter().Remove(pCommunityId, pCountryName); } void CServerBrowser::Con_AddExcludedType(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); const char *pTypeName = pResult->GetString(1); if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName)) return; pThis->TypesFilter().Add(pCommunityId, pTypeName); } void CServerBrowser::Con_RemoveExcludedType(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); const char *pCommunityId = pResult->GetString(0); const char *pTypeName = pResult->GetString(1); if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName)) return; pThis->TypesFilter().Remove(pCommunityId, pTypeName); } void CServerBrowser::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData) { CServerBrowser *pThis = static_cast(pUserData); // We only consider the first address of every server. std::vector vSortedServers; // Sort servers by IP address, ignoring port. class CAddrComparer { public: CServerBrowser *m_pThis; bool operator()(int i, int j) const { NETADDR Addr1 = m_pThis->m_ppServerlist[i]->m_Info.m_aAddresses[0]; NETADDR Addr2 = m_pThis->m_ppServerlist[j]->m_Info.m_aAddresses[0]; Addr1.port = 0; Addr2.port = 0; return net_addr_comp(&Addr1, &Addr2) < 0; } }; vSortedServers.reserve(pThis->m_NumServers); for(int i = 0; i < pThis->m_NumServers; i++) { vSortedServers.push_back(i); } std::sort(vSortedServers.begin(), vSortedServers.end(), CAddrComparer{pThis}); // Group the servers into those with same IP address (but differing // port). NETADDR Addr; int Start = -1; for(int i = 0; i <= (int)vSortedServers.size(); i++) { NETADDR NextAddr; if(i < (int)vSortedServers.size()) { NextAddr = pThis->m_ppServerlist[vSortedServers[i]]->m_Info.m_aAddresses[0]; NextAddr.port = 0; } bool New = Start == -1 || i == (int)vSortedServers.size() || net_addr_comp(&Addr, &NextAddr) != 0; if(Start != -1 && New) { int Chosen = Start + secure_rand_below(i - Start); CServerEntry *pChosen = pThis->m_ppServerlist[vSortedServers[Chosen]]; pChosen->m_RequestIgnoreInfo = true; pThis->QueueRequest(pChosen); char aAddr[NETADDR_MAXSTRSIZE]; net_addr_str(&pChosen->m_Info.m_aAddresses[0], aAddr, sizeof(aAddr), true); dbg_msg("serverbrowser", "queuing ping request for %s", aAddr); } if(i < (int)vSortedServers.size() && New) { Start = i; Addr = NextAddr; } } } static bool ValidIdentifier(const char *pId, size_t MaxLength) { if(pId[0] == '\0' || (size_t)str_length(pId) >= MaxLength) { return false; } for(int i = 0; pId[i] != '\0'; ++i) { if(pId[i] == '"' || pId[i] == '/' || pId[i] == '\\') { return false; } } return true; } static bool ValidateIdentifier(const char *pId, size_t MaxLength, const char *pContext, IConsole *pConsole) { if(!ValidIdentifier(pId, MaxLength)) { char aError[32 + IConsole::CMDLINE_LENGTH]; str_format(aError, sizeof(aError), "%s '%s' is not valid", pContext, pId); pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowser", aError); return false; } return true; } bool CServerBrowser::ValidateCommunityId(const char *pCommunityId) const { return ValidateIdentifier(pCommunityId, CServerInfo::MAX_COMMUNITY_ID_LENGTH, "Community ID", m_pConsole); } bool CServerBrowser::ValidateCountryName(const char *pCountryName) const { return ValidateIdentifier(pCountryName, CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH, "Country name", m_pConsole); } bool CServerBrowser::ValidateTypeName(const char *pTypeName) const { return ValidateIdentifier(pTypeName, CServerInfo::MAX_COMMUNITY_TYPE_LENGTH, "Type name", m_pConsole); } int CServerBrowser::Players(const CServerInfo &Item) const { return g_Config.m_BrFilterSpectators ? Item.m_NumPlayers : Item.m_NumClients; } int CServerBrowser::Max(const CServerInfo &Item) const { return g_Config.m_BrFilterSpectators ? Item.m_MaxPlayers : Item.m_MaxClients; } const CServerInfo *CServerBrowser::SortedGet(int Index) const { if(Index < 0 || Index >= m_NumSortedServers) return nullptr; return &m_ppServerlist[m_pSortedServerlist[Index]]->m_Info; } int CServerBrowser::GenerateToken(const NETADDR &Addr) const { SHA256_CTX Sha256; sha256_init(&Sha256); sha256_update(&Sha256, m_aTokenSeed, sizeof(m_aTokenSeed)); sha256_update(&Sha256, (unsigned char *)&Addr, sizeof(Addr)); SHA256_DIGEST Digest = sha256_finish(&Sha256); return (Digest.data[0] << 16) | (Digest.data[1] << 8) | Digest.data[2]; } int CServerBrowser::GetBasicToken(int Token) { return Token & 0xff; } int CServerBrowser::GetExtraToken(int Token) { return Token >> 8; } bool CServerBrowser::SortCompareName(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; CServerEntry *pIndex2 = m_ppServerlist[Index2]; // make sure empty entries are listed last return (pIndex1->m_GotInfo && pIndex2->m_GotInfo) || (!pIndex1->m_GotInfo && !pIndex2->m_GotInfo) ? str_comp(pIndex1->m_Info.m_aName, pIndex2->m_Info.m_aName) < 0 : pIndex1->m_GotInfo != 0; } bool CServerBrowser::SortCompareMap(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; CServerEntry *pIndex2 = m_ppServerlist[Index2]; return str_comp(pIndex1->m_Info.m_aMap, pIndex2->m_Info.m_aMap) < 0; } bool CServerBrowser::SortComparePing(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; CServerEntry *pIndex2 = m_ppServerlist[Index2]; return pIndex1->m_Info.m_Latency < pIndex2->m_Info.m_Latency; } bool CServerBrowser::SortCompareGametype(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; CServerEntry *pIndex2 = m_ppServerlist[Index2]; return str_comp(pIndex1->m_Info.m_aGameType, pIndex2->m_Info.m_aGameType) < 0; } bool CServerBrowser::SortCompareNumPlayers(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; CServerEntry *pIndex2 = m_ppServerlist[Index2]; return pIndex1->m_Info.m_NumFilteredPlayers > pIndex2->m_Info.m_NumFilteredPlayers; } bool CServerBrowser::SortCompareNumClients(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; CServerEntry *pIndex2 = m_ppServerlist[Index2]; return pIndex1->m_Info.m_NumClients > pIndex2->m_Info.m_NumClients; } bool CServerBrowser::SortCompareNumPlayersAndPing(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; CServerEntry *pIndex2 = m_ppServerlist[Index2]; if(pIndex1->m_Info.m_NumFilteredPlayers == pIndex2->m_Info.m_NumFilteredPlayers) return pIndex1->m_Info.m_Latency > pIndex2->m_Info.m_Latency; else if(pIndex1->m_Info.m_NumFilteredPlayers == 0 || pIndex2->m_Info.m_NumFilteredPlayers == 0 || pIndex1->m_Info.m_Latency / 100 == pIndex2->m_Info.m_Latency / 100) return pIndex1->m_Info.m_NumFilteredPlayers < pIndex2->m_Info.m_NumFilteredPlayers; else return pIndex1->m_Info.m_Latency > pIndex2->m_Info.m_Latency; } void CServerBrowser::Filter() { m_NumSortedServers = 0; m_NumSortedPlayers = 0; // allocate the sorted list if(m_NumSortedServersCapacity < m_NumServers) { free(m_pSortedServerlist); m_NumSortedServersCapacity = m_NumServers; m_pSortedServerlist = (int *)calloc(m_NumSortedServersCapacity, sizeof(int)); } // filter the servers for(int i = 0; i < m_NumServers; i++) { CServerInfo &Info = m_ppServerlist[i]->m_Info; bool Filtered = false; if(g_Config.m_BrFilterEmpty && Info.m_NumFilteredPlayers == 0) Filtered = true; else if(g_Config.m_BrFilterFull && Players(Info) == Max(Info)) Filtered = true; else if(g_Config.m_BrFilterPw && Info.m_Flags & SERVER_FLAG_PASSWORD) Filtered = true; else if(g_Config.m_BrFilterServerAddress[0] && !str_find_nocase(Info.m_aAddress, g_Config.m_BrFilterServerAddress)) Filtered = true; else if(g_Config.m_BrFilterGametypeStrict && g_Config.m_BrFilterGametype[0] && str_comp_nocase(Info.m_aGameType, g_Config.m_BrFilterGametype)) Filtered = true; else if(!g_Config.m_BrFilterGametypeStrict && g_Config.m_BrFilterGametype[0] && !str_utf8_find_nocase(Info.m_aGameType, g_Config.m_BrFilterGametype)) Filtered = true; else if(g_Config.m_BrFilterUnfinishedMap && Info.m_HasRank == CServerInfo::RANK_RANKED) Filtered = true; else { if(!Communities().empty()) { if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { Filtered = CommunitiesFilter().Filtered(Info.m_aCommunityId); } if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES || (m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_3)) { Filtered = Filtered || CountriesFilter().Filtered(Info.m_aCommunityCountry); Filtered = Filtered || TypesFilter().Filtered(Info.m_aCommunityType); } } if(!Filtered && g_Config.m_BrFilterCountry) { Filtered = true; // match against player country for(int p = 0; p < minimum(Info.m_NumClients, (int)MAX_CLIENTS); p++) { if(Info.m_aClients[p].m_Country == g_Config.m_BrFilterCountryIndex) { Filtered = false; break; } } } if(!Filtered && g_Config.m_BrFilterString[0] != '\0') { Info.m_QuickSearchHit = 0; const char *pStr = g_Config.m_BrFilterString; char aFilterStr[sizeof(g_Config.m_BrFilterString)]; while((pStr = str_next_token(pStr, IServerBrowser::SEARCH_EXCLUDE_TOKEN, aFilterStr, sizeof(aFilterStr)))) { if(aFilterStr[0] == '\0') { continue; } auto MatchesFn = matchesPart; const int FilterLen = str_length(aFilterStr); if(aFilterStr[0] == '"' && aFilterStr[FilterLen - 1] == '"') { aFilterStr[FilterLen - 1] = '\0'; MatchesFn = matchesExactly; } // match against server name if(MatchesFn(Info.m_aName, aFilterStr)) { Info.m_QuickSearchHit |= IServerBrowser::QUICK_SERVERNAME; } // match against players for(int p = 0; p < minimum(Info.m_NumClients, (int)MAX_CLIENTS); p++) { if(MatchesFn(Info.m_aClients[p].m_aName, aFilterStr) || MatchesFn(Info.m_aClients[p].m_aClan, aFilterStr)) { if(g_Config.m_BrFilterConnectingPlayers && str_comp(Info.m_aClients[p].m_aName, "(connecting)") == 0 && Info.m_aClients[p].m_aClan[0] == '\0') { continue; } Info.m_QuickSearchHit |= IServerBrowser::QUICK_PLAYER; break; } } // match against map if(MatchesFn(Info.m_aMap, aFilterStr)) { Info.m_QuickSearchHit |= IServerBrowser::QUICK_MAPNAME; } } if(!Info.m_QuickSearchHit) Filtered = true; } if(!Filtered && g_Config.m_BrExcludeString[0] != '\0') { const char *pStr = g_Config.m_BrExcludeString; char aExcludeStr[sizeof(g_Config.m_BrExcludeString)]; while((pStr = str_next_token(pStr, IServerBrowser::SEARCH_EXCLUDE_TOKEN, aExcludeStr, sizeof(aExcludeStr)))) { if(aExcludeStr[0] == '\0') { continue; } auto MatchesFn = matchesPart; const int FilterLen = str_length(aExcludeStr); if(aExcludeStr[0] == '"' && aExcludeStr[FilterLen - 1] == '"') { aExcludeStr[FilterLen - 1] = '\0'; MatchesFn = matchesExactly; } // match against server name if(MatchesFn(Info.m_aName, aExcludeStr)) { Filtered = true; break; } // match against map if(MatchesFn(Info.m_aMap, aExcludeStr)) { Filtered = true; break; } // match against gametype if(MatchesFn(Info.m_aGameType, aExcludeStr)) { Filtered = true; break; } } } } if(!Filtered) { UpdateServerFriends(&Info); if(!g_Config.m_BrFilterFriends || Info.m_FriendState != IFriends::FRIEND_NO) { m_NumSortedPlayers += Info.m_NumFilteredPlayers; m_pSortedServerlist[m_NumSortedServers++] = i; } } } } int CServerBrowser::SortHash() const { int i = g_Config.m_BrSort & 0xff; i |= g_Config.m_BrFilterEmpty << 4; i |= g_Config.m_BrFilterFull << 5; i |= g_Config.m_BrFilterSpectators << 6; i |= g_Config.m_BrFilterFriends << 7; i |= g_Config.m_BrFilterPw << 8; i |= g_Config.m_BrSortOrder << 9; i |= g_Config.m_BrFilterGametypeStrict << 12; i |= g_Config.m_BrFilterUnfinishedMap << 13; i |= g_Config.m_BrFilterCountry << 14; i |= g_Config.m_BrFilterConnectingPlayers << 15; return i; } void CServerBrowser::Sort() { // update number of filtered players for(int i = 0; i < m_NumServers; i++) { UpdateServerFilteredPlayers(&m_ppServerlist[i]->m_Info); } // create filtered list Filter(); // sort if(g_Config.m_BrSortOrder == 2 && (g_Config.m_BrSort == IServerBrowser::SORT_NUMPLAYERS || g_Config.m_BrSort == IServerBrowser::SORT_PING)) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareNumPlayersAndPing)); else if(g_Config.m_BrSort == IServerBrowser::SORT_NAME) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareName)); else if(g_Config.m_BrSort == IServerBrowser::SORT_PING) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortComparePing)); else if(g_Config.m_BrSort == IServerBrowser::SORT_MAP) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareMap)); else if(g_Config.m_BrSort == IServerBrowser::SORT_NUMPLAYERS) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareNumPlayers)); else if(g_Config.m_BrSort == IServerBrowser::SORT_GAMETYPE) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareGametype)); m_Sorthash = SortHash(); } void CServerBrowser::RemoveRequest(CServerEntry *pEntry) { if(pEntry->m_pPrevReq || pEntry->m_pNextReq || m_pFirstReqServer == pEntry) { if(pEntry->m_pPrevReq) pEntry->m_pPrevReq->m_pNextReq = pEntry->m_pNextReq; else m_pFirstReqServer = pEntry->m_pNextReq; if(pEntry->m_pNextReq) pEntry->m_pNextReq->m_pPrevReq = pEntry->m_pPrevReq; else m_pLastReqServer = pEntry->m_pPrevReq; pEntry->m_pPrevReq = nullptr; pEntry->m_pNextReq = nullptr; m_NumRequests--; } } CServerBrowser::CServerEntry *CServerBrowser::Find(const NETADDR &Addr) { auto Entry = m_ByAddr.find(Addr); if(Entry == m_ByAddr.end()) { return nullptr; } return m_ppServerlist[Entry->second]; } void CServerBrowser::QueueRequest(CServerEntry *pEntry) { // add it to the list of servers that we should request info from pEntry->m_pPrevReq = m_pLastReqServer; if(m_pLastReqServer) m_pLastReqServer->m_pNextReq = pEntry; else m_pFirstReqServer = pEntry; m_pLastReqServer = pEntry; pEntry->m_pNextReq = nullptr; m_NumRequests++; } void ServerBrowserFormatAddresses(char *pBuffer, int BufferSize, NETADDR *pAddrs, int NumAddrs) { for(int i = 0; i < NumAddrs; i++) { if(i != 0) { if(BufferSize <= 1) { return; } pBuffer[0] = ','; pBuffer[1] = '\0'; pBuffer += 1; BufferSize -= 1; } if(BufferSize <= 1) { return; } net_addr_str(&pAddrs[i], pBuffer, BufferSize, true); int Length = str_length(pBuffer); pBuffer += Length; BufferSize -= Length; } } void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) const { const CServerInfo TmpInfo = pEntry->m_Info; pEntry->m_Info = Info; pEntry->m_Info.m_Favorite = TmpInfo.m_Favorite; pEntry->m_Info.m_FavoriteAllowPing = TmpInfo.m_FavoriteAllowPing; mem_copy(pEntry->m_Info.m_aAddresses, TmpInfo.m_aAddresses, sizeof(pEntry->m_Info.m_aAddresses)); pEntry->m_Info.m_NumAddresses = TmpInfo.m_NumAddresses; ServerBrowserFormatAddresses(pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aAddress), pEntry->m_Info.m_aAddresses, pEntry->m_Info.m_NumAddresses); str_copy(pEntry->m_Info.m_aCommunityId, TmpInfo.m_aCommunityId); str_copy(pEntry->m_Info.m_aCommunityCountry, TmpInfo.m_aCommunityCountry); str_copy(pEntry->m_Info.m_aCommunityType, TmpInfo.m_aCommunityType); UpdateServerRank(&pEntry->m_Info); if(pEntry->m_Info.m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_UNSPECIFIED) { if((str_find_nocase(pEntry->m_Info.m_aGameType, "race") || str_find_nocase(pEntry->m_Info.m_aGameType, "fastcap")) && g_Config.m_ClDDRaceScoreBoard) { pEntry->m_Info.m_ClientScoreKind = CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT; } else { pEntry->m_Info.m_ClientScoreKind = CServerInfo::CLIENT_SCORE_KIND_POINTS; } } class CPlayerScoreNameLess { const int ScoreKind; public: CPlayerScoreNameLess(int ClientScoreKind) : ScoreKind(ClientScoreKind) { } bool operator()(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1) const { // Sort players before non players if(p0.m_Player && !p1.m_Player) return true; if(!p0.m_Player && p1.m_Player) return false; int Score0 = p0.m_Score; int Score1 = p1.m_Score; if(ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME || ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT) { // Sort unfinished (-9999) and still connecting players (-1) after others if(Score0 < 0 && Score1 >= 0) return false; if(Score0 >= 0 && Score1 < 0) return true; } if(Score0 != Score1) { // Handle the sign change introduced with CLIENT_SCORE_KIND_TIME if(ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME) return Score0 < Score1; else return Score0 > Score1; } return str_comp_nocase(p0.m_aName, p1.m_aName) < 0; } }; std::sort(pEntry->m_Info.m_aClients, pEntry->m_Info.m_aClients + Info.m_NumReceivedClients, CPlayerScoreNameLess(pEntry->m_Info.m_ClientScoreKind)); pEntry->m_GotInfo = 1; } void CServerBrowser::SetLatency(NETADDR Addr, int Latency) { m_pPingCache->CachePing(Addr, Latency); Addr.port = 0; for(int i = 0; i < m_NumServers; i++) { if(!m_ppServerlist[i]->m_GotInfo) { continue; } bool Found = false; for(int j = 0; j < m_ppServerlist[i]->m_Info.m_NumAddresses; j++) { NETADDR Other = m_ppServerlist[i]->m_Info.m_aAddresses[j]; Other.port = 0; if(Addr == Other) { Found = true; break; } } if(!Found) { continue; } int Ping = m_pPingCache->GetPing(m_ppServerlist[i]->m_Info.m_aAddresses, m_ppServerlist[i]->m_Info.m_NumAddresses); if(Ping == -1) { continue; } m_ppServerlist[i]->m_Info.m_Latency = Ping; m_ppServerlist[i]->m_Info.m_LatencyIsEstimated = false; } } CServerBrowser::CServerEntry *CServerBrowser::Add(const NETADDR *pAddrs, int NumAddrs) { // create new pEntry CServerEntry *pEntry = m_ServerlistHeap.Allocate(); mem_zero(pEntry, sizeof(CServerEntry)); // set the info mem_copy(pEntry->m_Info.m_aAddresses, pAddrs, NumAddrs * sizeof(pAddrs[0])); pEntry->m_Info.m_NumAddresses = NumAddrs; pEntry->m_Info.m_Latency = 999; pEntry->m_Info.m_HasRank = CServerInfo::RANK_UNAVAILABLE; ServerBrowserFormatAddresses(pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aAddress), pEntry->m_Info.m_aAddresses, pEntry->m_Info.m_NumAddresses); UpdateServerCommunity(&pEntry->m_Info); str_copy(pEntry->m_Info.m_aName, pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aName)); // check if it's a favorite pEntry->m_Info.m_Favorite = m_pFavorites->IsFavorite(pEntry->m_Info.m_aAddresses, pEntry->m_Info.m_NumAddresses); pEntry->m_Info.m_FavoriteAllowPing = m_pFavorites->IsPingAllowed(pEntry->m_Info.m_aAddresses, pEntry->m_Info.m_NumAddresses); for(int i = 0; i < NumAddrs; i++) { m_ByAddr[pAddrs[i]] = m_NumServers; } if(m_NumServers == m_NumServerCapacity) { CServerEntry **ppNewlist; m_NumServerCapacity += 100; ppNewlist = (CServerEntry **)calloc(m_NumServerCapacity, sizeof(CServerEntry *)); // NOLINT(bugprone-sizeof-expression) if(m_NumServers > 0) mem_copy(ppNewlist, m_ppServerlist, m_NumServers * sizeof(CServerEntry *)); // NOLINT(bugprone-sizeof-expression) free(m_ppServerlist); m_ppServerlist = ppNewlist; } // add to list m_ppServerlist[m_NumServers] = pEntry; pEntry->m_Info.m_ServerIndex = m_NumServers; m_NumServers++; return pEntry; } void CServerBrowser::OnServerInfoUpdate(const NETADDR &Addr, int Token, const CServerInfo *pInfo) { int BasicToken = Token; int ExtraToken = 0; if(pInfo->m_Type == SERVERINFO_EXTENDED) { BasicToken = Token & 0xff; ExtraToken = Token >> 8; } CServerEntry *pEntry = Find(Addr); if(m_ServerlistType == IServerBrowser::TYPE_LAN) { NETADDR Broadcast; mem_zero(&Broadcast, sizeof(Broadcast)); Broadcast.type = m_pNetClient->NetType() | NETTYPE_LINK_BROADCAST; int TokenBC = GenerateToken(Broadcast); bool Drop = false; Drop = Drop || BasicToken != GetBasicToken(TokenBC); Drop = Drop || (pInfo->m_Type == SERVERINFO_EXTENDED && ExtraToken != GetExtraToken(TokenBC)); if(Drop) { return; } if(!pEntry) pEntry = Add(&Addr, 1); } else { if(!pEntry) { return; } int TokenAddr = GenerateToken(Addr); bool Drop = false; Drop = Drop || BasicToken != GetBasicToken(TokenAddr); Drop = Drop || (pInfo->m_Type == SERVERINFO_EXTENDED && ExtraToken != GetExtraToken(TokenAddr)); if(Drop) { return; } } if(m_ServerlistType == IServerBrowser::TYPE_LAN) { SetInfo(pEntry, *pInfo); pEntry->m_Info.m_Latency = minimum(static_cast((time_get() - m_BroadcastTime) * 1000 / time_freq()), 999); } else if(pEntry->m_RequestTime > 0) { if(!pEntry->m_RequestIgnoreInfo) { SetInfo(pEntry, *pInfo); } int Latency = minimum(static_cast((time_get() - pEntry->m_RequestTime) * 1000 / time_freq()), 999); if(!pEntry->m_RequestIgnoreInfo) { pEntry->m_Info.m_Latency = Latency; } else { char aAddr[NETADDR_MAXSTRSIZE]; net_addr_str(&Addr, aAddr, sizeof(aAddr), true); dbg_msg("serverbrowser", "received ping response from %s", aAddr); SetLatency(Addr, Latency); } pEntry->m_RequestTime = -1; // Request has been answered } RemoveRequest(pEntry); RequestResort(); } void CServerBrowser::Refresh(int Type, bool Force) { bool ServerListTypeChanged = Force || m_ServerlistType != Type; int OldServerListType = m_ServerlistType; m_ServerlistType = Type; secure_random_fill(m_aTokenSeed, sizeof(m_aTokenSeed)); if(Type == IServerBrowser::TYPE_LAN || (ServerListTypeChanged && OldServerListType == IServerBrowser::TYPE_LAN)) CleanUp(); if(Type == IServerBrowser::TYPE_LAN) { unsigned char aBuffer[sizeof(SERVERBROWSE_GETINFO) + 1]; CNetChunk Packet; /* do the broadcast version */ Packet.m_ClientID = -1; mem_zero(&Packet, sizeof(Packet)); Packet.m_Address.type = m_pNetClient->NetType() | NETTYPE_LINK_BROADCAST; Packet.m_Flags = NETSENDFLAG_CONNLESS | NETSENDFLAG_EXTENDED; Packet.m_DataSize = sizeof(aBuffer); Packet.m_pData = aBuffer; mem_zero(&Packet.m_aExtraData, sizeof(Packet.m_aExtraData)); int Token = GenerateToken(Packet.m_Address); mem_copy(aBuffer, SERVERBROWSE_GETINFO, sizeof(SERVERBROWSE_GETINFO)); aBuffer[sizeof(SERVERBROWSE_GETINFO)] = GetBasicToken(Token); Packet.m_aExtraData[0] = GetExtraToken(Token) >> 8; Packet.m_aExtraData[1] = GetExtraToken(Token) & 0xff; m_BroadcastTime = time_get(); for(int i = 8303; i <= 8310; i++) { Packet.m_Address.port = i; m_pNetClient->Send(&Packet); } if(g_Config.m_Debug) m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "serverbrowser", "broadcasting for servers"); } else { m_pHttp->Refresh(); m_pPingCache->Load(); m_RefreshingHttp = true; if(ServerListTypeChanged && m_pHttp->NumServers() > 0) { CleanUp(); UpdateFromHttp(); Sort(); } } } void CServerBrowser::RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int *pBasicToken, int *pToken, bool RandomToken) const { if(g_Config.m_Debug) { char aAddrStr[NETADDR_MAXSTRSIZE]; net_addr_str(&Addr, aAddrStr, sizeof(aAddrStr), true); char aBuf[256]; str_format(aBuf, sizeof(aBuf), "requesting server info from %s", aAddrStr); m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "serverbrowser", aBuf); } int Token = GenerateToken(Addr); if(RandomToken) { int AvoidBasicToken = GetBasicToken(Token); do { secure_random_fill(&Token, sizeof(Token)); Token &= 0xffffff; } while(GetBasicToken(Token) == AvoidBasicToken); } if(pToken) { *pToken = Token; } if(pBasicToken) { *pBasicToken = GetBasicToken(Token); } unsigned char aBuffer[sizeof(SERVERBROWSE_GETINFO) + 1]; mem_copy(aBuffer, SERVERBROWSE_GETINFO, sizeof(SERVERBROWSE_GETINFO)); aBuffer[sizeof(SERVERBROWSE_GETINFO)] = GetBasicToken(Token); CNetChunk Packet; Packet.m_ClientID = -1; Packet.m_Address = Addr; Packet.m_Flags = NETSENDFLAG_CONNLESS | NETSENDFLAG_EXTENDED; Packet.m_DataSize = sizeof(aBuffer); Packet.m_pData = aBuffer; mem_zero(&Packet.m_aExtraData, sizeof(Packet.m_aExtraData)); Packet.m_aExtraData[0] = GetExtraToken(Token) >> 8; Packet.m_aExtraData[1] = GetExtraToken(Token) & 0xff; m_pNetClient->Send(&Packet); if(pEntry) pEntry->m_RequestTime = time_get(); } void CServerBrowser::RequestCurrentServer(const NETADDR &Addr) const { RequestImpl(Addr, nullptr, nullptr, nullptr, false); } void CServerBrowser::RequestCurrentServerWithRandomToken(const NETADDR &Addr, int *pBasicToken, int *pToken) const { RequestImpl(Addr, nullptr, pBasicToken, pToken, true); } void CServerBrowser::SetCurrentServerPing(const NETADDR &Addr, int Ping) { SetLatency(Addr, minimum(Ping, 999)); } void CServerBrowser::UpdateFromHttp() { int OwnLocation; if(str_comp(g_Config.m_BrLocation, "auto") == 0) { OwnLocation = m_OwnLocation; } else { if(CServerInfo::ParseLocation(&OwnLocation, g_Config.m_BrLocation)) { char aBuf[64]; str_format(aBuf, sizeof(aBuf), "cannot parse br_location: '%s'", g_Config.m_BrLocation); m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowser", aBuf); } } int NumServers = m_pHttp->NumServers(); int NumLegacyServers = m_pHttp->NumLegacyServers(); std::function Want = [](const NETADDR *pAddrs, int NumAddrs) { return true; }; if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { Want = [this](const NETADDR *pAddrs, int NumAddrs) -> bool { return m_pFavorites->IsFavorite(pAddrs, NumAddrs) != TRISTATE::NONE; }; } else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_3) { const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; std::vector vpFavoriteCommunities = FavoriteCommunities(); dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid community index"); const CCommunity *pWantedCommunity = vpFavoriteCommunities[CommunityIndex]; const bool IsNoneCommunity = str_comp(pWantedCommunity->Id(), COMMUNITY_NONE) == 0; Want = [this, pWantedCommunity, IsNoneCommunity](const NETADDR *pAddrs, int NumAddrs) -> bool { for(int AddressIndex = 0; AddressIndex < NumAddrs; AddressIndex++) { const auto CommunityServer = m_CommunityServersByAddr.find(pAddrs[AddressIndex]); if(CommunityServer != m_CommunityServersByAddr.end()) { if(IsNoneCommunity) { // Servers with community "none" are not present in m_CommunityServersByAddr, so we ignore // any server that is found in this map to determine only the servers without community. return false; } else if(str_comp(CommunityServer->second.CommunityId(), pWantedCommunity->Id()) == 0) { return true; } } } return IsNoneCommunity; }; } for(int i = 0; i < NumServers; i++) { CServerInfo Info = m_pHttp->Server(i); if(!Want(Info.m_aAddresses, Info.m_NumAddresses)) { continue; } int Ping = m_pPingCache->GetPing(Info.m_aAddresses, Info.m_NumAddresses); Info.m_LatencyIsEstimated = Ping == -1; if(Info.m_LatencyIsEstimated) { Info.m_Latency = CServerInfo::EstimateLatency(OwnLocation, Info.m_Location); } else { Info.m_Latency = Ping; } CServerEntry *pEntry = Add(Info.m_aAddresses, Info.m_NumAddresses); SetInfo(pEntry, Info); pEntry->m_RequestIgnoreInfo = true; } for(int i = 0; i < NumLegacyServers; i++) { NETADDR Addr = m_pHttp->LegacyServer(i); if(!Want(&Addr, 1)) { continue; } QueueRequest(Add(&Addr, 1)); } if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { const IFavorites::CEntry *pFavorites; int NumFavorites; m_pFavorites->AllEntries(&pFavorites, &NumFavorites); for(int i = 0; i < NumFavorites; i++) { bool Found = false; for(int j = 0; j < pFavorites[i].m_NumAddrs; j++) { if(Find(pFavorites[i].m_aAddrs[j])) { Found = true; break; } } if(Found) { continue; } // (Also add favorites we're not allowed to ping.) CServerEntry *pEntry = Add(pFavorites[i].m_aAddrs, pFavorites[i].m_NumAddrs); if(pFavorites->m_AllowPing) { QueueRequest(pEntry); } } } RequestResort(); } void CServerBrowser::CleanUp() { // clear out everything m_ServerlistHeap.Reset(); m_NumServers = 0; m_NumSortedServers = 0; m_NumSortedPlayers = 0; m_ByAddr.clear(); m_pFirstReqServer = nullptr; m_pLastReqServer = nullptr; m_NumRequests = 0; m_CurrentMaxRequests = g_Config.m_BrMaxRequests; } void CServerBrowser::Update() { int64_t Timeout = time_freq(); int64_t Now = time_get(); const char *pHttpBestUrl; if(!m_pHttp->GetBestUrl(&pHttpBestUrl) && pHttpBestUrl != m_pHttpPrevBestUrl) { str_copy(g_Config.m_BrCachedBestServerinfoUrl, pHttpBestUrl); m_pHttpPrevBestUrl = pHttpBestUrl; } m_pHttp->Update(); if(m_ServerlistType != TYPE_LAN && m_RefreshingHttp && !m_pHttp->IsRefreshing()) { m_RefreshingHttp = false; CleanUp(); UpdateFromHttp(); // TODO: move this somewhere else Sort(); return; } CServerEntry *pEntry = m_pFirstReqServer; int Count = 0; while(true) { if(!pEntry) // no more entries break; if(pEntry->m_RequestTime && pEntry->m_RequestTime + Timeout < Now) { pEntry = pEntry->m_pNextReq; continue; } // no more than 10 concurrent requests if(Count == m_CurrentMaxRequests) break; if(pEntry->m_RequestTime == 0) { RequestImpl(pEntry->m_Info.m_aAddresses[0], pEntry, nullptr, nullptr, false); } Count++; pEntry = pEntry->m_pNextReq; } if(m_pFirstReqServer && Count == 0 && m_CurrentMaxRequests > 1) //NO More current Server Requests { //reset old ones pEntry = m_pFirstReqServer; while(true) { if(!pEntry) // no more entries break; pEntry->m_RequestTime = 0; pEntry = pEntry->m_pNextReq; } //update max-requests m_CurrentMaxRequests = m_CurrentMaxRequests / 2; if(m_CurrentMaxRequests < 1) m_CurrentMaxRequests = 1; } else if(Count == 0 && m_CurrentMaxRequests == 1) //we reached the limit, just release all left requests. IF a server sends us a packet, a new request will be added automatically, so we can delete all { pEntry = m_pFirstReqServer; while(true) { if(!pEntry) // no more entries break; CServerEntry *pNext = pEntry->m_pNextReq; RemoveRequest(pEntry); //release request pEntry = pNext; } } // check if we need to resort if(m_Sorthash != SortHash() || m_NeedResort) { for(int i = 0; i < m_NumServers; i++) { CServerInfo *pInfo = &m_ppServerlist[i]->m_Info; pInfo->m_Favorite = m_pFavorites->IsFavorite(pInfo->m_aAddresses, pInfo->m_NumAddresses); pInfo->m_FavoriteAllowPing = m_pFavorites->IsPingAllowed(pInfo->m_aAddresses, pInfo->m_NumAddresses); } Sort(); m_NeedResort = false; } } const json_value *CServerBrowser::LoadDDNetInfo() { LoadDDNetInfoJson(); LoadDDNetLocation(); LoadDDNetServers(); for(int i = 0; i < m_NumServers; i++) { UpdateServerCommunity(&m_ppServerlist[i]->m_Info); UpdateServerRank(&m_ppServerlist[i]->m_Info); } return m_pDDNetInfo; } void CServerBrowser::LoadDDNetInfoJson() { void *pBuf; unsigned Length; if(!m_pStorage->ReadFile(DDNET_INFO_FILE, IStorage::TYPE_SAVE, &pBuf, &Length)) { m_DDNetInfoSha256 = SHA256_ZEROED; return; } m_DDNetInfoSha256 = sha256(pBuf, Length); json_value_free(m_pDDNetInfo); json_settings JsonSettings{}; char aError[256]; m_pDDNetInfo = json_parse_ex(&JsonSettings, static_cast(pBuf), Length, aError); free(pBuf); if(m_pDDNetInfo == nullptr) { log_error("serverbrowser", "invalid info json: '%s'", aError); } else if(m_pDDNetInfo->type != json_object) { log_error("serverbrowser", "invalid info root"); json_value_free(m_pDDNetInfo); m_pDDNetInfo = nullptr; } } void CServerBrowser::LoadDDNetLocation() { m_OwnLocation = CServerInfo::LOC_UNKNOWN; if(m_pDDNetInfo) { const json_value &Location = (*m_pDDNetInfo)["location"]; if(Location.type != json_string || CServerInfo::ParseLocation(&m_OwnLocation, Location)) { log_error("serverbrowser", "invalid location"); } } } bool CServerBrowser::ParseCommunityServers(CCommunity *pCommunity, const json_value &Servers) { for(unsigned ServerIndex = 0; ServerIndex < Servers.u.array.length; ++ServerIndex) { // pServer - { name, flagId, servers } const json_value &Server = Servers[ServerIndex]; if(Server.type != json_object) { log_error("serverbrowser", "invalid server (ServerIndex=%u)", ServerIndex); return false; } const json_value &Name = Server["name"]; const json_value &FlagId = Server["flagId"]; const json_value &Types = Server["servers"]; if(Name.type != json_string || FlagId.type != json_integer || Types.type != json_object) { log_error("serverbrowser", "invalid server attribute (ServerIndex=%u)", ServerIndex); return false; } if(Types.u.object.length == 0) continue; pCommunity->m_vCountries.emplace_back(Name.u.string.ptr, FlagId.u.integer); CCommunityCountry *pCountry = &pCommunity->m_vCountries.back(); for(unsigned TypeIndex = 0; TypeIndex < Types.u.object.length; ++TypeIndex) { const json_value &Addresses = *Types.u.object.values[TypeIndex].value; if(Addresses.type != json_array) { log_error("serverbrowser", "invalid addresses (ServerIndex=%u, TypeIndex=%u)", ServerIndex, TypeIndex); return false; } if(Addresses.u.array.length == 0) continue; const char *pTypeName = Types.u.object.values[TypeIndex].name; // add type if it doesn't exist already const auto CommunityType = std::find_if(pCommunity->m_vTypes.begin(), pCommunity->m_vTypes.end(), [pTypeName](const auto &Elem) { return str_comp(Elem.Name(), pTypeName) == 0; }); if(CommunityType == pCommunity->m_vTypes.end()) { pCommunity->m_vTypes.emplace_back(pTypeName); } // add addresses for(unsigned AddressIndex = 0; AddressIndex < Addresses.u.array.length; ++AddressIndex) { const json_value &Address = Addresses[AddressIndex]; if(Address.type != json_string) { log_error("serverbrowser", "invalid address (ServerIndex=%u, TypeIndex=%u, AddressIndex=%u)", ServerIndex, TypeIndex, AddressIndex); return false; } NETADDR NetAddr; if(net_addr_from_str(&NetAddr, Address.u.string.ptr)) { log_error("serverbrowser", "invalid address (ServerIndex=%u, TypeIndex=%u, AddressIndex=%u)", ServerIndex, TypeIndex, AddressIndex); continue; } pCountry->m_vServers.emplace_back(NetAddr, pTypeName); } } } return true; } bool CServerBrowser::ParseCommunityFinishes(CCommunity *pCommunity, const json_value &Finishes) { for(unsigned FinishIndex = 0; FinishIndex < Finishes.u.array.length; ++FinishIndex) { const json_value &Finish = Finishes[FinishIndex]; if(Finish.type != json_string) { log_error("serverbrowser", "invalid rank (FinishIndex=%u)", FinishIndex); return false; } pCommunity->m_FinishedMaps.emplace((const char *)Finish); } return true; } void CServerBrowser::LoadDDNetServers() { // Parse communities m_vCommunities.clear(); m_CommunityServersByAddr.clear(); if(!m_pDDNetInfo) { return; } const json_value &Communities = (*m_pDDNetInfo)["communities"]; if(Communities.type != json_array) { return; } for(unsigned CommunityIndex = 0; CommunityIndex < Communities.u.array.length; ++CommunityIndex) { const json_value &Community = Communities[CommunityIndex]; if(Community.type != json_object) { log_error("serverbrowser", "invalid community (CommunityIndex=%d)", (int)CommunityIndex); continue; } const json_value &Id = Community["id"]; if(Id.type != json_string) { log_error("serverbrowser", "invalid community id (CommunityIndex=%d)", (int)CommunityIndex); continue; } const json_value &Icon = Community["icon"]; const json_value &IconSha256 = Icon["sha256"]; const json_value &IconUrl = Icon["url"]; const json_value &Name = Community["name"]; const json_value HasFinishes = Community["has_finishes"]; const json_value *pFinishes = &Icon["finishes"]; const json_value *pServers = &Icon["servers"]; if(pFinishes->type == json_none) { if(str_comp(Id, COMMUNITY_DDNET) == 0) { pFinishes = &(*m_pDDNetInfo)["maps"]; } } // Backward compatibility. if(pServers->type == json_none) { if(str_comp(Id, COMMUNITY_DDNET) == 0) { pServers = &(*m_pDDNetInfo)["servers"]; } else if(str_comp(Id, "kog") == 0) { pServers = &(*m_pDDNetInfo)["servers-kog"]; } } if(false || Icon.type != json_object || IconSha256.type != json_string || IconUrl.type != json_string || Name.type != json_string || HasFinishes.type != json_boolean || (pFinishes->type != json_array && pFinishes->type != json_none) || pServers->type != json_array) { log_error("serverbrowser", "invalid community attribute (CommunityId=%s)", (const char *)Id); continue; } SHA256_DIGEST ParsedIconSha256; if(sha256_from_str(&ParsedIconSha256, IconSha256) != 0) { log_error("serverbrowser", "invalid community icon sha256 (CommunityId=%s)", (const char *)Id); continue; } CCommunity NewCommunity(Id, Name, ParsedIconSha256, IconUrl); if(!ParseCommunityServers(&NewCommunity, *pServers)) { log_error("serverbrowser", "invalid community servers (CommunityId=%s)", NewCommunity.Id()); continue; } NewCommunity.m_HasFinishes = HasFinishes; if(NewCommunity.m_HasFinishes && pFinishes->type == json_array && !ParseCommunityFinishes(&NewCommunity, *pFinishes)) { log_error("serverbrowser", "invalid community finishes (CommunityId=%s)", NewCommunity.Id()); continue; } for(const auto &Country : NewCommunity.Countries()) { for(const auto &Server : Country.Servers()) { m_CommunityServersByAddr.emplace(Server.Address(), CCommunityServer(NewCommunity.Id(), Country.Name(), Server.TypeName())); } } m_vCommunities.push_back(std::move(NewCommunity)); } // Add default none community { CCommunity NoneCommunity(COMMUNITY_NONE, "None", SHA256_ZEROED, ""); NoneCommunity.m_vCountries.emplace_back(COMMUNITY_COUNTRY_NONE, -1); NoneCommunity.m_vTypes.emplace_back(COMMUNITY_TYPE_NONE); m_vCommunities.push_back(std::move(NoneCommunity)); } // Remove unknown elements from exclude lists CleanFilters(); } void CServerBrowser::UpdateServerFilteredPlayers(CServerInfo *pInfo) const { pInfo->m_NumFilteredPlayers = g_Config.m_BrFilterSpectators ? pInfo->m_NumPlayers : pInfo->m_NumClients; if(g_Config.m_BrFilterConnectingPlayers) { for(const auto &Client : pInfo->m_aClients) { if((!g_Config.m_BrFilterSpectators || Client.m_Player) && str_comp(Client.m_aName, "(connecting)") == 0 && Client.m_aClan[0] == '\0') pInfo->m_NumFilteredPlayers--; } } } void CServerBrowser::UpdateServerFriends(CServerInfo *pInfo) const { pInfo->m_FriendState = IFriends::FRIEND_NO; pInfo->m_FriendNum = 0; for(int ClientIndex = 0; ClientIndex < minimum(pInfo->m_NumReceivedClients, (int)MAX_CLIENTS); ClientIndex++) { pInfo->m_aClients[ClientIndex].m_FriendState = m_pFriends->GetFriendState(pInfo->m_aClients[ClientIndex].m_aName, pInfo->m_aClients[ClientIndex].m_aClan); pInfo->m_FriendState = maximum(pInfo->m_FriendState, pInfo->m_aClients[ClientIndex].m_FriendState); if(pInfo->m_aClients[ClientIndex].m_FriendState != IFriends::FRIEND_NO) pInfo->m_FriendNum++; } } void CServerBrowser::UpdateServerCommunity(CServerInfo *pInfo) const { for(int AddressIndex = 0; AddressIndex < pInfo->m_NumAddresses; AddressIndex++) { const auto Community = m_CommunityServersByAddr.find(pInfo->m_aAddresses[AddressIndex]); if(Community != m_CommunityServersByAddr.end()) { str_copy(pInfo->m_aCommunityId, Community->second.CommunityId()); str_copy(pInfo->m_aCommunityCountry, Community->second.CountryName()); str_copy(pInfo->m_aCommunityType, Community->second.TypeName()); return; } } str_copy(pInfo->m_aCommunityId, COMMUNITY_NONE); str_copy(pInfo->m_aCommunityCountry, COMMUNITY_COUNTRY_NONE); str_copy(pInfo->m_aCommunityType, COMMUNITY_TYPE_NONE); } void CServerBrowser::UpdateServerRank(CServerInfo *pInfo) const { const CCommunity *pCommunity = Community(pInfo->m_aCommunityId); pInfo->m_HasRank = pCommunity == nullptr ? CServerInfo::RANK_UNAVAILABLE : pCommunity->HasRank(pInfo->m_aMap); } const char *CServerBrowser::GetTutorialServer() { const CCommunity *pCommunity = Community(COMMUNITY_DDNET); if(pCommunity == nullptr) return nullptr; const char *pBestAddr = nullptr; int BestLatency = std::numeric_limits::max(); for(const auto &Country : pCommunity->Countries()) { for(const auto &Server : Country.Servers()) { if(str_comp(Server.TypeName(), "Tutorial") != 0) continue; const CServerEntry *pEntry = Find(Server.Address()); if(!pEntry) continue; if(pEntry->m_Info.m_NumPlayers > pEntry->m_Info.m_MaxPlayers - 10) continue; if(pEntry->m_Info.m_Latency >= BestLatency) continue; BestLatency = pEntry->m_Info.m_Latency; pBestAddr = pEntry->m_Info.m_aAddress; } } return pBestAddr; } bool CServerBrowser::IsRefreshing() const { return m_pFirstReqServer != nullptr; } bool CServerBrowser::IsGettingServerlist() const { return m_pHttp->IsRefreshing(); } int CServerBrowser::LoadingProgression() const { if(m_NumServers == 0) return 0; int Servers = m_NumServers; int Loaded = m_NumServers - m_NumRequests; return 100.0f * Loaded / Servers; } bool CCommunity::HasCountry(const char *pCountryName) const { return std::find_if(Countries().begin(), Countries().end(), [pCountryName](const auto &Elem) { return str_comp(Elem.Name(), pCountryName) == 0; }) != Countries().end(); } bool CCommunity::HasType(const char *pTypeName) const { return std::find_if(Types().begin(), Types().end(), [pTypeName](const auto &Elem) { return str_comp(Elem.Name(), pTypeName) == 0; }) != Types().end(); } CServerInfo::ERankState CCommunity::HasRank(const char *pMap) const { if(!HasRanks()) return CServerInfo::RANK_UNAVAILABLE; const CCommunityMap Needle(pMap); return m_FinishedMaps.count(Needle) == 0 ? CServerInfo::RANK_UNRANKED : CServerInfo::RANK_RANKED; } const std::vector &CServerBrowser::Communities() const { return m_vCommunities; } const CCommunity *CServerBrowser::Community(const char *pCommunityId) const { const auto Community = std::find_if(Communities().begin(), Communities().end(), [pCommunityId](const auto &Elem) { return str_comp(Elem.Id(), pCommunityId) == 0; }); return Community == Communities().end() ? nullptr : &(*Community); } std::vector CServerBrowser::SelectedCommunities() const { std::vector vpSelected; for(const auto &Community : Communities()) { if(!CommunitiesFilter().Filtered(Community.Id())) { vpSelected.push_back(&Community); } } return vpSelected; } std::vector CServerBrowser::FavoriteCommunities() const { // This is done differently than SelectedCommunities because the favorite // communities should be returned in the order specified by the user. std::vector vpFavorites; for(const auto &CommunityId : FavoriteCommunitiesFilter().Entries()) { const CCommunity *pCommunity = Community(CommunityId.Id()); if(pCommunity) { vpFavorites.push_back(pCommunity); } } return vpFavorites; } std::vector CServerBrowser::CurrentCommunities() const { if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { return SelectedCommunities(); } else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_3) { const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; std::vector vpFavoriteCommunities = FavoriteCommunities(); dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid favorite community serverbrowser type"); return {vpFavoriteCommunities[CommunityIndex]}; } else { return {}; } } unsigned CServerBrowser::CurrentCommunitiesHash() const { std::vector vpCommunities = CurrentCommunities(); unsigned Hash = 5381; for(const CCommunity *pCommunity : CurrentCommunities()) { Hash = (Hash << 5) + Hash + str_quickhash(pCommunity->Id()); } return Hash; } void CFavoriteCommunityFilterList::Add(const char *pCommunityId) { // Remove community if it's already a favorite, so it will be added again at // the end of the list, to allow setting the entire list easier with binds. Remove(pCommunityId); // Ensure maximum number of favorite communities, by removing the least-recently // added community from the beginning, when the maximum number of favorite // communities has been reached. constexpr size_t MaxFavoriteCommunities = 3; if(m_vEntries.size() >= MaxFavoriteCommunities) { dbg_assert(m_vEntries.size() == MaxFavoriteCommunities, "Maximum number of communities can never be exceeded"); m_vEntries.erase(m_vEntries.begin()); } m_vEntries.emplace_back(pCommunityId); } void CFavoriteCommunityFilterList::Remove(const char *pCommunityId) { auto FoundCommunity = std::find(m_vEntries.begin(), m_vEntries.end(), CCommunityId(pCommunityId)); if(FoundCommunity != m_vEntries.end()) { m_vEntries.erase(FoundCommunity); } } void CFavoriteCommunityFilterList::Clear() { m_vEntries.clear(); } bool CFavoriteCommunityFilterList::Filtered(const char *pCommunityId) const { return std::find(m_vEntries.begin(), m_vEntries.end(), CCommunityId(pCommunityId)) != m_vEntries.end(); } bool CFavoriteCommunityFilterList::Empty() const { return m_vEntries.empty(); } void CFavoriteCommunityFilterList::Clean(const std::vector &vAllowedCommunities) { auto It = std::remove_if(m_vEntries.begin(), m_vEntries.end(), [&](const auto &Community) { return std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { return str_comp(Community.Id(), AllowedCommunity.Id()) == 0; }) == vAllowedCommunities.end(); }); m_vEntries.erase(It, m_vEntries.end()); } void CFavoriteCommunityFilterList::Save(IConfigManager *pConfigManager) const { char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH]; for(const auto &FavoriteCommunity : m_vEntries) { str_copy(aBuf, "add_favorite_community \""); str_append(aBuf, FavoriteCommunity.Id()); str_append(aBuf, "\""); pConfigManager->WriteLine(aBuf); } } const std::vector &CFavoriteCommunityFilterList::Entries() const { return m_vEntries; } void CExcludedCommunityFilterList::Add(const char *pCommunityId) { m_Entries.emplace(pCommunityId); } void CExcludedCommunityFilterList::Remove(const char *pCommunityId) { m_Entries.erase(CCommunityId(pCommunityId)); } void CExcludedCommunityFilterList::Clear() { m_Entries.clear(); } bool CExcludedCommunityFilterList::Filtered(const char *pCommunityId) const { return std::find(m_Entries.begin(), m_Entries.end(), CCommunityId(pCommunityId)) != m_Entries.end(); } bool CExcludedCommunityFilterList::Empty() const { return m_Entries.empty(); } void CExcludedCommunityFilterList::Clean(const std::vector &vAllowedCommunities) { for(auto It = m_Entries.begin(); It != m_Entries.end();) { const bool Found = std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { return str_comp(It->Id(), AllowedCommunity.Id()) == 0; }) != vAllowedCommunities.end(); if(Found) { ++It; } else { It = m_Entries.erase(It); } } // Prevent filter that would exclude all allowed communities if(m_Entries.size() == vAllowedCommunities.size()) { m_Entries.clear(); } } void CExcludedCommunityFilterList::Save(IConfigManager *pConfigManager) const { char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH]; for(const auto &ExcludedCommunity : m_Entries) { str_copy(aBuf, "add_excluded_community \""); str_append(aBuf, ExcludedCommunity.Id()); str_append(aBuf, "\""); pConfigManager->WriteLine(aBuf); } } void CExcludedCommunityCountryFilterList::Add(const char *pCountryName) { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { if(pCommunity->HasCountry(pCountryName)) { Add(pCommunity->Id(), pCountryName); } } } void CExcludedCommunityCountryFilterList::Add(const char *pCommunityId, const char *pCountryName) { CCommunityId CommunityId(pCommunityId); if(m_Entries.find(CommunityId) == m_Entries.end()) { m_Entries[CommunityId] = {}; } m_Entries[CommunityId].emplace(pCountryName); } void CExcludedCommunityCountryFilterList::Remove(const char *pCountryName) { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { Remove(pCommunity->Id(), pCountryName); } } void CExcludedCommunityCountryFilterList::Remove(const char *pCommunityId, const char *pCountryName) { auto CommunityEntry = m_Entries.find(CCommunityId(pCommunityId)); if(CommunityEntry != m_Entries.end()) { CommunityEntry->second.erase(pCountryName); } } void CExcludedCommunityCountryFilterList::Clear() { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { auto CommunityEntry = m_Entries.find(pCommunity->Id()); if(CommunityEntry != m_Entries.end()) { CommunityEntry->second.clear(); } } } bool CExcludedCommunityCountryFilterList::Filtered(const char *pCountryName) const { const auto Communities = m_CurrentCommunitiesGetter(); return std::none_of(Communities.begin(), Communities.end(), [&](const CCommunity *pCommunity) { if(!pCommunity->HasCountry(pCountryName)) return false; auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); if(CommunityEntry == m_Entries.end()) return true; const auto &CountryEntries = CommunityEntry->second; if(CountryEntries.find(CCommunityCountryName(pCountryName)) == CountryEntries.end()) return true; return false; }); } bool CExcludedCommunityCountryFilterList::Empty() const { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); return CommunityEntry == m_Entries.end() || CommunityEntry->second.empty(); } return false; } void CExcludedCommunityCountryFilterList::Clean(const std::vector &vAllowedCommunities) { for(auto It = m_Entries.begin(); It != m_Entries.end();) { const bool Found = std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { return str_comp(It->first.Id(), AllowedCommunity.Id()) == 0; }) != vAllowedCommunities.end(); if(Found) { ++It; } else { It = m_Entries.erase(It); } } for(const CCommunity &AllowedCommunity : vAllowedCommunities) { auto CommunityEntry = m_Entries.find(CCommunityId(AllowedCommunity.Id())); if(CommunityEntry != m_Entries.end()) { auto &CountryEntries = CommunityEntry->second; for(auto It = CountryEntries.begin(); It != CountryEntries.end();) { if(AllowedCommunity.HasCountry(It->Name())) { ++It; } else { It = CountryEntries.erase(It); } } // Prevent filter that would exclude all allowed countries if(CountryEntries.size() == AllowedCommunity.Countries().size()) { CountryEntries.clear(); } } } } void CExcludedCommunityCountryFilterList::Save(IConfigManager *pConfigManager) const { char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH]; for(const auto &[Community, Countries] : m_Entries) { for(const auto &Country : Countries) { str_copy(aBuf, "add_excluded_country \""); str_append(aBuf, Community.Id()); str_append(aBuf, "\" \""); str_append(aBuf, Country.Name()); str_append(aBuf, "\""); pConfigManager->WriteLine(aBuf); } } } void CExcludedCommunityTypeFilterList::Add(const char *pTypeName) { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { if(pCommunity->HasType(pTypeName)) { Add(pCommunity->Id(), pTypeName); } } } void CExcludedCommunityTypeFilterList::Add(const char *pCommunityId, const char *pTypeName) { CCommunityId CommunityId(pCommunityId); if(m_Entries.find(CommunityId) == m_Entries.end()) { m_Entries[CommunityId] = {}; } m_Entries[CommunityId].emplace(pTypeName); } void CExcludedCommunityTypeFilterList::Remove(const char *pTypeName) { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { Remove(pCommunity->Id(), pTypeName); } } void CExcludedCommunityTypeFilterList::Remove(const char *pCommunityId, const char *pTypeName) { auto CommunityEntry = m_Entries.find(CCommunityId(pCommunityId)); if(CommunityEntry != m_Entries.end()) { CommunityEntry->second.erase(pTypeName); } } void CExcludedCommunityTypeFilterList::Clear() { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { auto CommunityEntry = m_Entries.find(pCommunity->Id()); if(CommunityEntry != m_Entries.end()) { CommunityEntry->second.clear(); } } } bool CExcludedCommunityTypeFilterList::Filtered(const char *pTypeName) const { const auto Communities = m_CurrentCommunitiesGetter(); return std::none_of(Communities.begin(), Communities.end(), [&](const CCommunity *pCommunity) { if(!pCommunity->HasType(pTypeName)) return false; auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); if(CommunityEntry == m_Entries.end()) return true; const auto &TypeEntries = CommunityEntry->second; return TypeEntries.find(CCommunityTypeName(pTypeName)) == TypeEntries.end(); }); } bool CExcludedCommunityTypeFilterList::Empty() const { for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); return CommunityEntry == m_Entries.end() || CommunityEntry->second.empty(); } return false; } void CExcludedCommunityTypeFilterList::Clean(const std::vector &vAllowedCommunities) { for(auto It = m_Entries.begin(); It != m_Entries.end();) { const bool Found = std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { return str_comp(It->first.Id(), AllowedCommunity.Id()) == 0; }) != vAllowedCommunities.end(); if(Found) { ++It; } else { It = m_Entries.erase(It); } } for(const CCommunity &AllowedCommunity : vAllowedCommunities) { auto CommunityEntry = m_Entries.find(CCommunityId(AllowedCommunity.Id())); if(CommunityEntry != m_Entries.end()) { auto &TypeEntries = CommunityEntry->second; for(auto It = TypeEntries.begin(); It != TypeEntries.end();) { if(AllowedCommunity.HasType(It->Name())) { ++It; } else { It = TypeEntries.erase(It); } } // Prevent filter that would exclude all allowed countries if(TypeEntries.size() == AllowedCommunity.Types().size()) { TypeEntries.clear(); } } } } void CExcludedCommunityTypeFilterList::Save(IConfigManager *pConfigManager) const { char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_TYPE_LENGTH]; for(const auto &[Community, Types] : m_Entries) { for(const auto &Type : Types) { str_copy(aBuf, "add_excluded_type \""); str_append(aBuf, Community.Id()); str_append(aBuf, "\" \""); str_append(aBuf, Type.Name()); str_append(aBuf, "\""); pConfigManager->WriteLine(aBuf); } } } void CServerBrowser::CleanFilters() { // Keep filters if we failed to load any communities if(Communities().empty()) return; FavoriteCommunitiesFilter().Clean(Communities()); CommunitiesFilter().Clean(Communities()); CountriesFilter().Clean(Communities()); TypesFilter().Clean(Communities()); } bool CServerBrowser::IsRegistered(const NETADDR &Addr) { const int NumServers = m_pHttp->NumServers(); for(int i = 0; i < NumServers; i++) { const CServerInfo Info = m_pHttp->Server(i); for(int j = 0; j < Info.m_NumAddresses; j++) { if(net_addr_comp(&Info.m_aAddresses[j], &Addr) == 0) { return true; } } } const int NumLegacyServers = m_pHttp->NumLegacyServers(); for(int i = 0; i < NumLegacyServers; i++) { if(net_addr_comp(&m_pHttp->LegacyServer(i), &Addr) == 0) { return true; } } return false; } int CServerInfo::EstimateLatency(int Loc1, int Loc2) { if(Loc1 == LOC_UNKNOWN || Loc2 == LOC_UNKNOWN) { return 999; } if(Loc1 != Loc2) { return 199; } return 99; } bool CServerInfo::ParseLocation(int *pResult, const char *pString) { *pResult = LOC_UNKNOWN; int Length = str_length(pString); if(Length < 2) { return true; } // ISO continent code. Allow antarctica, but treat it as unknown. static const char s_apLocations[NUM_LOCS][6] = { "an", // LOC_UNKNOWN "af", // LOC_AFRICA "as", // LOC_ASIA "oc", // LOC_AUSTRALIA "eu", // LOC_EUROPE "na", // LOC_NORTH_AMERICA "sa", // LOC_SOUTH_AMERICA "as:cn", // LOC_CHINA }; for(int i = std::size(s_apLocations) - 1; i >= 0; i--) { if(str_startswith(pString, s_apLocations[i])) { *pResult = i; return false; } } return true; } void CServerInfo::InfoToString(char *pBuffer, int BufferSize) const { str_format( pBuffer, BufferSize, "%s\n" "Address: ddnet://%s\n" "My IGN: %s\n", m_aName, m_aAddress, g_Config.m_PlayerName); }