1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "serverbrowser.h"
4
5#include "serverbrowser_http.h"
6#include "serverbrowser_ping_cache.h"
7
8#include <base/dbg.h>
9#include <base/hash_ctxt.h>
10#include <base/log.h>
11#include <base/mem.h>
12#include <base/secure.h>
13#include <base/str.h>
14#include <base/time.h>
15
16#include <engine/console.h>
17#include <engine/engine.h>
18#include <engine/favorites.h>
19#include <engine/friends.h>
20#include <engine/http.h>
21#include <engine/shared/config.h>
22#include <engine/shared/json.h>
23#include <engine/shared/masterserver.h>
24#include <engine/shared/network.h>
25#include <engine/shared/packer.h>
26#include <engine/shared/protocol.h>
27#include <engine/shared/serverinfo.h>
28#include <engine/storage.h>
29
30#include <algorithm>
31#include <map>
32#include <set>
33#include <vector>
34
35class CSortWrap
36{
37 typedef bool (CServerBrowser::*SortFunc)(int, int) const;
38 SortFunc m_pfnSort;
39 CServerBrowser *m_pThis;
40
41public:
42 CSortWrap(CServerBrowser *pServer, SortFunc Func) :
43 m_pfnSort(Func), m_pThis(pServer) {}
44 bool operator()(int a, int b) { return (g_Config.m_BrSortOrder ? (m_pThis->*m_pfnSort)(b, a) : (m_pThis->*m_pfnSort)(a, b)); }
45};
46
47static bool MatchesPart(const char *a, const char *b)
48{
49 return str_utf8_find_nocase(haystack: a, needle: b) != nullptr;
50}
51
52static bool MatchesExactly(const char *a, const char *b)
53{
54 return str_comp(a, b: &b[1]) == 0;
55}
56
57static NETADDR CommunityAddressKey(const NETADDR &Addr)
58{
59 NETADDR AddressKey = Addr;
60 AddressKey.type &= ~NETTYPE_TW7;
61 return AddressKey;
62}
63
64CServerBrowser::CServerBrowser() :
65 m_CommunityCache(this),
66 m_CountriesFilter(&m_CommunityCache),
67 m_TypesFilter(&m_CommunityCache)
68{
69 m_NeedResort = false;
70 m_Sorthash = 0;
71
72 m_ServerlistType = 0;
73 m_BroadcastTime = 0;
74 secure_random_fill(bytes: m_aTokenSeed, length: sizeof(m_aTokenSeed));
75
76 CleanUp();
77}
78
79CServerBrowser::~CServerBrowser()
80{
81 json_value_free(m_pDDNetInfo);
82
83 delete m_pHttp;
84 m_pHttp = nullptr;
85 delete m_pPingCache;
86 m_pPingCache = nullptr;
87}
88
89void CServerBrowser::SetBaseInfo(class CNetClient *pClient, const char *pNetVersion)
90{
91 m_pNetClient = pClient;
92 str_copy(dst&: m_aNetVersion, src: pNetVersion);
93 m_pConsole = Kernel()->RequestInterface<IConsole>();
94 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
95 m_pEngine = Kernel()->RequestInterface<IEngine>();
96 m_pFavorites = Kernel()->RequestInterface<IFavorites>();
97 m_pFriends = Kernel()->RequestInterface<IFriends>();
98 m_pStorage = Kernel()->RequestInterface<IStorage>();
99 m_pHttpClient = Kernel()->RequestInterface<IHttp>();
100 m_pPingCache = CreateServerBrowserPingCache(pConsole: m_pConsole, pStorage: m_pStorage);
101
102 RegisterCommands();
103}
104
105void CServerBrowser::OnInit()
106{
107 m_pHttp = CreateServerBrowserHttp(pEngine: m_pEngine, pStorage: m_pStorage, pHttp: m_pHttpClient, pPreviousBestUrl: g_Config.m_BrCachedBestServerinfoUrl);
108}
109
110void CServerBrowser::RegisterCommands()
111{
112 m_pConfigManager->RegisterCallback(pfnFunc: CServerBrowser::ConfigSaveCallback, pUserData: this);
113 m_pConsole->Register(pName: "add_favorite_community", pParams: "s[community_id]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_AddFavoriteCommunity, pUser: this, pHelp: "Add a community as a favorite");
114 m_pConsole->Register(pName: "remove_favorite_community", pParams: "s[community_id]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RemoveFavoriteCommunity, pUser: this, pHelp: "Remove a community from the favorites");
115 m_pConsole->Register(pName: "add_excluded_community", pParams: "s[community_id]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_AddExcludedCommunity, pUser: this, pHelp: "Add a community to the exclusion filter");
116 m_pConsole->Register(pName: "remove_excluded_community", pParams: "s[community_id]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RemoveExcludedCommunity, pUser: this, pHelp: "Remove a community from the exclusion filter");
117 m_pConsole->Register(pName: "add_excluded_country", pParams: "s[community_id] s[country_code]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_AddExcludedCountry, pUser: this, pHelp: "Add a country to the exclusion filter for a specific community (ISO 3166-1 numeric)");
118 m_pConsole->Register(pName: "remove_excluded_country", pParams: "s[community_id] s[country_code]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RemoveExcludedCountry, pUser: this, pHelp: "Remove a country from the exclusion filter for a specific community (ISO 3166-1 numeric)");
119 m_pConsole->Register(pName: "add_excluded_type", pParams: "s[community_id] s[type]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_AddExcludedType, pUser: this, pHelp: "Add a type to the exclusion filter for a specific community");
120 m_pConsole->Register(pName: "remove_excluded_type", pParams: "s[community_id] s[type]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RemoveExcludedType, pUser: this, pHelp: "Remove a type from the exclusion filter for a specific community");
121 m_pConsole->Register(pName: "leak_ip_address_to_all_servers", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_LeakIpAddress, pUser: this, pHelp: "Leaks your IP address to all servers by pinging each of them, also acquiring the latency in the process");
122}
123
124void CServerBrowser::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData)
125{
126 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
127 pThis->FavoriteCommunitiesFilter().Save(pConfigManager);
128 pThis->CommunitiesFilter().Save(pConfigManager);
129 pThis->CountriesFilter().Save(pConfigManager);
130 pThis->TypesFilter().Save(pConfigManager);
131}
132
133void CServerBrowser::Con_AddFavoriteCommunity(IConsole::IResult *pResult, void *pUserData)
134{
135 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
136 const char *pCommunityId = pResult->GetString(Index: 0);
137 if(!pThis->ValidateCommunityId(pCommunityId))
138 return;
139 pThis->FavoriteCommunitiesFilter().Add(pCommunityId);
140}
141
142void CServerBrowser::Con_RemoveFavoriteCommunity(IConsole::IResult *pResult, void *pUserData)
143{
144 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
145 const char *pCommunityId = pResult->GetString(Index: 0);
146 if(!pThis->ValidateCommunityId(pCommunityId))
147 return;
148 pThis->FavoriteCommunitiesFilter().Remove(pCommunityId);
149}
150
151void CServerBrowser::Con_AddExcludedCommunity(IConsole::IResult *pResult, void *pUserData)
152{
153 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
154 const char *pCommunityId = pResult->GetString(Index: 0);
155 if(!pThis->ValidateCommunityId(pCommunityId))
156 return;
157 pThis->CommunitiesFilter().Add(pCommunityId);
158}
159
160void CServerBrowser::Con_RemoveExcludedCommunity(IConsole::IResult *pResult, void *pUserData)
161{
162 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
163 const char *pCommunityId = pResult->GetString(Index: 0);
164 if(!pThis->ValidateCommunityId(pCommunityId))
165 return;
166 pThis->CommunitiesFilter().Remove(pCommunityId);
167}
168
169void CServerBrowser::Con_AddExcludedCountry(IConsole::IResult *pResult, void *pUserData)
170{
171 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
172 const char *pCommunityId = pResult->GetString(Index: 0);
173 const char *pCountryName = pResult->GetString(Index: 1);
174 if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName))
175 return;
176 pThis->CountriesFilter().Add(pCommunityId, pCountryName);
177}
178
179void CServerBrowser::Con_RemoveExcludedCountry(IConsole::IResult *pResult, void *pUserData)
180{
181 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
182 const char *pCommunityId = pResult->GetString(Index: 0);
183 const char *pCountryName = pResult->GetString(Index: 1);
184 if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName))
185 return;
186 pThis->CountriesFilter().Remove(pCommunityId, pCountryName);
187}
188
189void CServerBrowser::Con_AddExcludedType(IConsole::IResult *pResult, void *pUserData)
190{
191 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
192 const char *pCommunityId = pResult->GetString(Index: 0);
193 const char *pTypeName = pResult->GetString(Index: 1);
194 if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName))
195 return;
196 pThis->TypesFilter().Add(pCommunityId, pTypeName);
197}
198
199void CServerBrowser::Con_RemoveExcludedType(IConsole::IResult *pResult, void *pUserData)
200{
201 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
202 const char *pCommunityId = pResult->GetString(Index: 0);
203 const char *pTypeName = pResult->GetString(Index: 1);
204 if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName))
205 return;
206 pThis->TypesFilter().Remove(pCommunityId, pTypeName);
207}
208
209void CServerBrowser::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData)
210{
211 CServerBrowser *pThis = static_cast<CServerBrowser *>(pUserData);
212
213 // We only consider the first address of every server.
214
215 std::vector<int> vSortedServers;
216 // Sort servers by IP address, ignoring port.
217 class CAddrComparer
218 {
219 public:
220 CServerBrowser *m_pThis;
221 bool operator()(int i, int j) const
222 {
223 NETADDR Addr1 = m_pThis->m_vpServerlist[i]->m_Info.m_aAddresses[0];
224 NETADDR Addr2 = m_pThis->m_vpServerlist[j]->m_Info.m_aAddresses[0];
225 Addr1.port = 0;
226 Addr2.port = 0;
227 return net_addr_comp(a: &Addr1, b: &Addr2) < 0;
228 }
229 };
230 vSortedServers.reserve(n: pThis->m_vpServerlist.size());
231 for(int i = 0; i < (int)pThis->m_vpServerlist.size(); i++)
232 {
233 vSortedServers.push_back(x: i);
234 }
235 std::sort(first: vSortedServers.begin(), last: vSortedServers.end(), comp: CAddrComparer{.m_pThis: pThis});
236
237 // Group the servers into those with same IP address (but differing
238 // port).
239 NETADDR Addr;
240 int Start = -1;
241 for(int i = 0; i <= (int)vSortedServers.size(); i++)
242 {
243 NETADDR NextAddr;
244 if(i < (int)vSortedServers.size())
245 {
246 NextAddr = pThis->m_vpServerlist[vSortedServers[i]]->m_Info.m_aAddresses[0];
247 NextAddr.port = 0;
248 }
249 bool New = Start == -1 || i == (int)vSortedServers.size() || net_addr_comp(a: &Addr, b: &NextAddr) != 0;
250 if(Start != -1 && New)
251 {
252 int Chosen = Start + secure_rand_below(below: i - Start);
253 CServerEntry *pChosen = pThis->m_vpServerlist[vSortedServers[Chosen]];
254 pChosen->m_RequestIgnoreInfo = true;
255 pThis->QueueRequest(pEntry: pChosen);
256 char aAddr[NETADDR_MAXSTRSIZE];
257 net_addr_str(addr: &pChosen->m_Info.m_aAddresses[0], string: aAddr, max_length: sizeof(aAddr), add_port: true);
258 dbg_msg(sys: "serverbrowser", fmt: "queuing ping request for %s", aAddr);
259 }
260 if(i < (int)vSortedServers.size() && New)
261 {
262 Start = i;
263 Addr = NextAddr;
264 }
265 }
266}
267
268static bool ValidIdentifier(const char *pId, size_t MaxLength)
269{
270 if(pId[0] == '\0' || (size_t)str_length(str: pId) >= MaxLength)
271 {
272 return false;
273 }
274
275 for(int i = 0; pId[i] != '\0'; ++i)
276 {
277 if(pId[i] == '"' || pId[i] == '/' || pId[i] == '\\')
278 {
279 return false;
280 }
281 }
282 return true;
283}
284
285static bool ValidateIdentifier(const char *pId, size_t MaxLength, const char *pContext, IConsole *pConsole)
286{
287 if(!ValidIdentifier(pId, MaxLength))
288 {
289 char aError[32 + IConsole::CMDLINE_LENGTH];
290 str_format(buffer: aError, buffer_size: sizeof(aError), format: "%s '%s' is not valid", pContext, pId);
291 pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "serverbrowser", pStr: aError);
292 return false;
293 }
294 return true;
295}
296
297bool CServerBrowser::ValidateCommunityId(const char *pCommunityId) const
298{
299 return ValidateIdentifier(pId: pCommunityId, MaxLength: CServerInfo::MAX_COMMUNITY_ID_LENGTH, pContext: "Community ID", pConsole: m_pConsole);
300}
301
302bool CServerBrowser::ValidateCountryName(const char *pCountryName) const
303{
304 return ValidateIdentifier(pId: pCountryName, MaxLength: CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH, pContext: "Country name", pConsole: m_pConsole);
305}
306
307bool CServerBrowser::ValidateTypeName(const char *pTypeName) const
308{
309 return ValidateIdentifier(pId: pTypeName, MaxLength: CServerInfo::MAX_COMMUNITY_TYPE_LENGTH, pContext: "Type name", pConsole: m_pConsole);
310}
311
312int CServerBrowser::Players(const CServerInfo &Item) const
313{
314 return g_Config.m_BrFilterSpectators ? Item.m_NumPlayers : Item.m_NumClients;
315}
316
317int CServerBrowser::Max(const CServerInfo &Item) const
318{
319 return g_Config.m_BrFilterSpectators ? Item.m_MaxPlayers : Item.m_MaxClients;
320}
321
322const CServerInfo *CServerBrowser::SortedGet(int Index) const
323{
324 if(Index < 0 || Index >= (int)m_vSortedServerlist.size())
325 return nullptr;
326 return &m_vpServerlist[m_vSortedServerlist[Index]]->m_Info;
327}
328
329const CServerInfo *CServerBrowser::Get(int Index) const
330{
331 if(Index < 0 || Index >= (int)m_vpServerlist.size())
332 return nullptr;
333 return &m_vpServerlist[Index]->m_Info;
334}
335
336int CServerBrowser::GenerateToken(const NETADDR &Addr) const
337{
338 SHA256_CTX Sha256;
339 sha256_init(ctxt: &Sha256);
340 sha256_update(ctxt: &Sha256, data: m_aTokenSeed, data_len: sizeof(m_aTokenSeed));
341 sha256_update(ctxt: &Sha256, data: (unsigned char *)&Addr, data_len: sizeof(Addr));
342 SHA256_DIGEST Digest = sha256_finish(ctxt: &Sha256);
343 return (Digest.data[0] << 16) | (Digest.data[1] << 8) | Digest.data[2];
344}
345
346int CServerBrowser::GetBasicToken(int Token)
347{
348 return Token & 0xff;
349}
350
351int CServerBrowser::GetExtraToken(int Token)
352{
353 return Token >> 8;
354}
355
356bool CServerBrowser::SortCompareName(int Index1, int Index2) const
357{
358 CServerEntry *pIndex1 = m_vpServerlist[Index1];
359 CServerEntry *pIndex2 = m_vpServerlist[Index2];
360 // make sure empty entries are listed last
361 return (pIndex1->m_GotInfo && pIndex2->m_GotInfo) || (!pIndex1->m_GotInfo && !pIndex2->m_GotInfo) ? str_comp(a: pIndex1->m_Info.m_aName, b: pIndex2->m_Info.m_aName) < 0 :
362 pIndex1->m_GotInfo != 0;
363}
364
365bool CServerBrowser::SortCompareMap(int Index1, int Index2) const
366{
367 CServerEntry *pIndex1 = m_vpServerlist[Index1];
368 CServerEntry *pIndex2 = m_vpServerlist[Index2];
369 return str_comp(a: pIndex1->m_Info.m_aMap, b: pIndex2->m_Info.m_aMap) < 0;
370}
371
372bool CServerBrowser::SortComparePing(int Index1, int Index2) const
373{
374 CServerEntry *pIndex1 = m_vpServerlist[Index1];
375 CServerEntry *pIndex2 = m_vpServerlist[Index2];
376 return pIndex1->m_Info.m_Latency < pIndex2->m_Info.m_Latency;
377}
378
379bool CServerBrowser::SortCompareGametype(int Index1, int Index2) const
380{
381 CServerEntry *pIndex1 = m_vpServerlist[Index1];
382 CServerEntry *pIndex2 = m_vpServerlist[Index2];
383 return str_comp(a: pIndex1->m_Info.m_aGameType, b: pIndex2->m_Info.m_aGameType) < 0;
384}
385
386bool CServerBrowser::SortCompareNumPlayers(int Index1, int Index2) const
387{
388 CServerEntry *pIndex1 = m_vpServerlist[Index1];
389 CServerEntry *pIndex2 = m_vpServerlist[Index2];
390 return pIndex1->m_Info.m_NumFilteredPlayers > pIndex2->m_Info.m_NumFilteredPlayers;
391}
392
393bool CServerBrowser::SortCompareNumClients(int Index1, int Index2) const
394{
395 CServerEntry *pIndex1 = m_vpServerlist[Index1];
396 CServerEntry *pIndex2 = m_vpServerlist[Index2];
397 return pIndex1->m_Info.m_NumClients > pIndex2->m_Info.m_NumClients;
398}
399
400bool CServerBrowser::SortCompareNumFriends(int Index1, int Index2) const
401{
402 CServerEntry *pIndex1 = m_vpServerlist[Index1];
403 CServerEntry *pIndex2 = m_vpServerlist[Index2];
404
405 if(pIndex1->m_Info.m_FriendNum == pIndex2->m_Info.m_FriendNum)
406 return pIndex1->m_Info.m_NumFilteredPlayers > pIndex2->m_Info.m_NumFilteredPlayers;
407 else
408 return pIndex1->m_Info.m_FriendNum > pIndex2->m_Info.m_FriendNum;
409}
410
411bool CServerBrowser::SortCompareNumPlayersAndPing(int Index1, int Index2) const
412{
413 CServerEntry *pIndex1 = m_vpServerlist[Index1];
414 CServerEntry *pIndex2 = m_vpServerlist[Index2];
415
416 if(pIndex1->m_Info.m_NumFilteredPlayers == pIndex2->m_Info.m_NumFilteredPlayers)
417 return pIndex1->m_Info.m_Latency > pIndex2->m_Info.m_Latency;
418 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)
419 return pIndex1->m_Info.m_NumFilteredPlayers < pIndex2->m_Info.m_NumFilteredPlayers;
420 else
421 return pIndex1->m_Info.m_Latency > pIndex2->m_Info.m_Latency;
422}
423
424void CServerBrowser::Filter()
425{
426 m_NumSortedPlayers = 0;
427
428 m_vSortedServerlist.clear();
429 m_vSortedServerlist.reserve(n: m_vpServerlist.size());
430
431 for(auto &Community : m_vCommunities)
432 {
433 Community.m_NumPlayers = 0;
434 }
435
436 // filter the servers
437 for(int ServerIndex = 0; ServerIndex < (int)m_vpServerlist.size(); ServerIndex++)
438 {
439 CServerInfo &Info = m_vpServerlist[ServerIndex]->m_Info;
440 bool Filtered = false;
441
442 if(g_Config.m_BrFilterEmpty && Info.m_NumFilteredPlayers == 0)
443 Filtered = true;
444 else if(g_Config.m_BrFilterFull && Players(Item: Info) == Max(Item: Info))
445 Filtered = true;
446 else if(g_Config.m_BrFilterPw && Info.m_Flags & SERVER_FLAG_PASSWORD)
447 Filtered = true;
448 else if(g_Config.m_BrFilterServerAddress[0] && !str_find_nocase(haystack: Info.m_aAddress, needle: g_Config.m_BrFilterServerAddress))
449 Filtered = true;
450 else if(g_Config.m_BrFilterGametypeStrict && g_Config.m_BrFilterGametype[0] && str_comp_nocase(a: Info.m_aGameType, b: g_Config.m_BrFilterGametype))
451 Filtered = true;
452 else if(!g_Config.m_BrFilterGametypeStrict && g_Config.m_BrFilterGametype[0] && !str_utf8_find_nocase(haystack: Info.m_aGameType, needle: g_Config.m_BrFilterGametype))
453 Filtered = true;
454 else if(g_Config.m_BrFilterUnfinishedMap && Info.m_HasRank == CServerInfo::RANK_RANKED)
455 Filtered = true;
456 else if(g_Config.m_BrFilterLogin && Info.m_RequiresLogin)
457 Filtered = true;
458 else
459 {
460 if(!Communities().empty())
461 {
462 if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES)
463 {
464 Filtered = CommunitiesFilter().Filtered(pCommunityId: Info.m_aCommunityId);
465 }
466 if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES ||
467 (m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5))
468 {
469 Filtered = Filtered || CountriesFilter().Filtered(pCountryName: Info.m_aCommunityCountry);
470 Filtered = Filtered || TypesFilter().Filtered(pTypeName: Info.m_aCommunityType);
471 }
472 }
473
474 if(!Filtered && g_Config.m_BrFilterCountry)
475 {
476 Filtered = true;
477 // match against player country
478 for(int p = 0; p < minimum(a: Info.m_NumClients, b: (int)MAX_CLIENTS); p++)
479 {
480 if(Info.m_aClients[p].m_Country == g_Config.m_BrFilterCountryIndex)
481 {
482 Filtered = false;
483 break;
484 }
485 }
486 }
487
488 if(!Filtered && g_Config.m_BrFilterString[0] != '\0')
489 {
490 Info.m_QuickSearchHit = 0;
491
492 const char *pStr = g_Config.m_BrFilterString;
493 char aFilterStr[sizeof(g_Config.m_BrFilterString)];
494 char aFilterStrTrimmed[sizeof(g_Config.m_BrFilterString)];
495 while((pStr = str_next_token(str: pStr, delim: IServerBrowser::SEARCH_EXCLUDE_TOKEN, buffer: aFilterStr, buffer_size: sizeof(aFilterStr))))
496 {
497 str_copy(dst&: aFilterStrTrimmed, src: str_utf8_skip_whitespaces(str: aFilterStr));
498 str_utf8_trim_right(param: aFilterStrTrimmed);
499
500 if(aFilterStrTrimmed[0] == '\0')
501 {
502 continue;
503 }
504 auto MatchesFn = MatchesPart;
505 const int FilterLen = str_length(str: aFilterStrTrimmed);
506 if(aFilterStrTrimmed[0] == '"' && aFilterStrTrimmed[FilterLen - 1] == '"')
507 {
508 aFilterStrTrimmed[FilterLen - 1] = '\0';
509 MatchesFn = MatchesExactly;
510 }
511
512 // match against server name
513 if(MatchesFn(Info.m_aName, aFilterStrTrimmed))
514 {
515 Info.m_QuickSearchHit |= IServerBrowser::QUICK_SERVERNAME;
516 }
517
518 // match against players
519 for(int p = 0; p < minimum(a: Info.m_NumClients, b: (int)MAX_CLIENTS); p++)
520 {
521 if(MatchesFn(Info.m_aClients[p].m_aName, aFilterStrTrimmed) ||
522 MatchesFn(Info.m_aClients[p].m_aClan, aFilterStrTrimmed))
523 {
524 if(g_Config.m_BrFilterConnectingPlayers &&
525 str_comp(a: Info.m_aClients[p].m_aName, b: "(connecting)") == 0 &&
526 Info.m_aClients[p].m_aClan[0] == '\0')
527 {
528 continue;
529 }
530 Info.m_QuickSearchHit |= IServerBrowser::QUICK_PLAYER;
531 break;
532 }
533 }
534
535 // match against map
536 if(MatchesFn(Info.m_aMap, aFilterStrTrimmed))
537 {
538 Info.m_QuickSearchHit |= IServerBrowser::QUICK_MAPNAME;
539 }
540 }
541
542 if(!Info.m_QuickSearchHit)
543 Filtered = true;
544 }
545
546 if(!Filtered && g_Config.m_BrExcludeString[0] != '\0')
547 {
548 const char *pStr = g_Config.m_BrExcludeString;
549 char aExcludeStr[sizeof(g_Config.m_BrExcludeString)];
550 char aExcludeStrTrimmed[sizeof(g_Config.m_BrExcludeString)];
551 while((pStr = str_next_token(str: pStr, delim: IServerBrowser::SEARCH_EXCLUDE_TOKEN, buffer: aExcludeStr, buffer_size: sizeof(aExcludeStr))))
552 {
553 str_copy(dst&: aExcludeStrTrimmed, src: str_utf8_skip_whitespaces(str: aExcludeStr));
554 str_utf8_trim_right(param: aExcludeStrTrimmed);
555
556 if(aExcludeStrTrimmed[0] == '\0')
557 {
558 continue;
559 }
560 auto MatchesFn = MatchesPart;
561 const int FilterLen = str_length(str: aExcludeStrTrimmed);
562 if(aExcludeStrTrimmed[0] == '"' && aExcludeStrTrimmed[FilterLen - 1] == '"')
563 {
564 aExcludeStrTrimmed[FilterLen - 1] = '\0';
565 MatchesFn = MatchesExactly;
566 }
567
568 // match against server name
569 if(MatchesFn(Info.m_aName, aExcludeStrTrimmed))
570 {
571 Filtered = true;
572 break;
573 }
574
575 // match against map
576 if(MatchesFn(Info.m_aMap, aExcludeStrTrimmed))
577 {
578 Filtered = true;
579 break;
580 }
581
582 // match against gametype
583 if(MatchesFn(Info.m_aGameType, aExcludeStrTrimmed))
584 {
585 Filtered = true;
586 break;
587 }
588 }
589 }
590 }
591
592 UpdateServerFriends(pInfo: &Info);
593
594 if(!Filtered)
595 {
596 if(!g_Config.m_BrFilterFriends || Info.m_FriendState != IFriends::FRIEND_NO)
597 {
598 m_NumSortedPlayers += Info.m_NumFilteredPlayers;
599 m_vSortedServerlist.push_back(x: ServerIndex);
600 }
601 }
602
603 if(Info.m_NumClients > 0)
604 {
605 auto Community = std::find_if(first: m_vCommunities.begin(), last: m_vCommunities.end(), pred: [Info](const auto &Elem) {
606 return str_comp(Elem.Id(), Info.m_aCommunityId) == 0;
607 });
608 if(Community != m_vCommunities.end())
609 {
610 Community->m_NumPlayers += Info.m_NumClients;
611 }
612 }
613 }
614
615 std::stable_sort(first: m_vCommunities.begin(), last: m_vCommunities.end(), comp: [](const CCommunity &Lhs, const CCommunity &Rhs) {
616 return Lhs.NumPlayers() > Rhs.NumPlayers();
617 });
618}
619
620int CServerBrowser::SortHash() const
621{
622 int i = g_Config.m_BrSort & 0xff;
623 i |= g_Config.m_BrFilterEmpty << 4;
624 i |= g_Config.m_BrFilterFull << 5;
625 i |= g_Config.m_BrFilterSpectators << 6;
626 i |= g_Config.m_BrFilterFriends << 7;
627 i |= g_Config.m_BrFilterPw << 8;
628 i |= g_Config.m_BrSortOrder << 9;
629 i |= g_Config.m_BrFilterGametypeStrict << 12;
630 i |= g_Config.m_BrFilterUnfinishedMap << 13;
631 i |= g_Config.m_BrFilterCountry << 14;
632 i |= g_Config.m_BrFilterConnectingPlayers << 15;
633 i |= g_Config.m_BrFilterLogin << 16;
634 return i;
635}
636
637void CServerBrowser::Sort()
638{
639 // update number of filtered players
640 for(CServerEntry *pEntry : m_vpServerlist)
641 {
642 UpdateServerFilteredPlayers(pInfo: &pEntry->m_Info);
643 }
644
645 // create filtered list
646 Filter();
647
648 // sort
649 if(g_Config.m_BrSortOrder == 2 && (g_Config.m_BrSort == IServerBrowser::SORT_NUMPLAYERS || g_Config.m_BrSort == IServerBrowser::SORT_PING))
650 std::stable_sort(first: m_vSortedServerlist.begin(), last: m_vSortedServerlist.end(), comp: CSortWrap(this, &CServerBrowser::SortCompareNumPlayersAndPing));
651 else if(g_Config.m_BrSort == IServerBrowser::SORT_NAME)
652 std::stable_sort(first: m_vSortedServerlist.begin(), last: m_vSortedServerlist.end(), comp: CSortWrap(this, &CServerBrowser::SortCompareName));
653 else if(g_Config.m_BrSort == IServerBrowser::SORT_PING)
654 std::stable_sort(first: m_vSortedServerlist.begin(), last: m_vSortedServerlist.end(), comp: CSortWrap(this, &CServerBrowser::SortComparePing));
655 else if(g_Config.m_BrSort == IServerBrowser::SORT_MAP)
656 std::stable_sort(first: m_vSortedServerlist.begin(), last: m_vSortedServerlist.end(), comp: CSortWrap(this, &CServerBrowser::SortCompareMap));
657 else if(g_Config.m_BrSort == IServerBrowser::SORT_NUMFRIENDS)
658 std::stable_sort(first: m_vSortedServerlist.begin(), last: m_vSortedServerlist.end(), comp: CSortWrap(this, &CServerBrowser::SortCompareNumFriends));
659 else if(g_Config.m_BrSort == IServerBrowser::SORT_NUMPLAYERS)
660 std::stable_sort(first: m_vSortedServerlist.begin(), last: m_vSortedServerlist.end(), comp: CSortWrap(this, &CServerBrowser::SortCompareNumPlayers));
661 else if(g_Config.m_BrSort == IServerBrowser::SORT_GAMETYPE)
662 std::stable_sort(first: m_vSortedServerlist.begin(), last: m_vSortedServerlist.end(), comp: CSortWrap(this, &CServerBrowser::SortCompareGametype));
663
664 m_Sorthash = SortHash();
665}
666
667void CServerBrowser::RemoveRequest(CServerEntry *pEntry)
668{
669 if(pEntry->m_pPrevReq || pEntry->m_pNextReq || m_pFirstReqServer == pEntry)
670 {
671 if(pEntry->m_pPrevReq)
672 pEntry->m_pPrevReq->m_pNextReq = pEntry->m_pNextReq;
673 else
674 m_pFirstReqServer = pEntry->m_pNextReq;
675
676 if(pEntry->m_pNextReq)
677 pEntry->m_pNextReq->m_pPrevReq = pEntry->m_pPrevReq;
678 else
679 m_pLastReqServer = pEntry->m_pPrevReq;
680
681 pEntry->m_pPrevReq = nullptr;
682 pEntry->m_pNextReq = nullptr;
683 m_NumRequests--;
684 }
685}
686
687CServerBrowser::CServerEntry *CServerBrowser::Find(const NETADDR &Addr)
688{
689 auto Entry = m_ByAddr.find(x: Addr);
690 if(Entry == m_ByAddr.end())
691 {
692 return nullptr;
693 }
694 return m_vpServerlist[Entry->second];
695}
696
697void CServerBrowser::QueueRequest(CServerEntry *pEntry)
698{
699 // add it to the list of servers that we should request info from
700 pEntry->m_pPrevReq = m_pLastReqServer;
701 if(m_pLastReqServer)
702 m_pLastReqServer->m_pNextReq = pEntry;
703 else
704 m_pFirstReqServer = pEntry;
705 m_pLastReqServer = pEntry;
706 pEntry->m_pNextReq = nullptr;
707 m_NumRequests++;
708}
709
710static void ServerBrowserFormatAddresses(char *pBuffer, int BufferSize, NETADDR *pAddrs, int NumAddrs)
711{
712 pBuffer[0] = '\0';
713 for(int i = 0; i < NumAddrs; i++)
714 {
715 if(i != 0)
716 {
717 str_append(dst: pBuffer, src: ",", dst_size: BufferSize);
718 }
719 if(pAddrs[i].type & NETTYPE_TW7)
720 {
721 str_append(dst: pBuffer, src: "tw-0.7+udp://", dst_size: BufferSize);
722 }
723 char aIpAddr[NETADDR_MAXSTRSIZE];
724 net_addr_str(addr: &pAddrs[i], string: aIpAddr, max_length: sizeof(aIpAddr), add_port: true);
725 str_append(dst: pBuffer, src: aIpAddr, dst_size: BufferSize);
726 }
727}
728
729void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) const
730{
731 const CServerInfo TmpInfo = pEntry->m_Info;
732 pEntry->m_Info = Info;
733 pEntry->m_Info.m_Favorite = TmpInfo.m_Favorite;
734 pEntry->m_Info.m_FavoriteAllowPing = TmpInfo.m_FavoriteAllowPing;
735 pEntry->m_Info.m_ServerIndex = TmpInfo.m_ServerIndex;
736 mem_copy(dest: pEntry->m_Info.m_aAddresses, source: TmpInfo.m_aAddresses, size: sizeof(pEntry->m_Info.m_aAddresses));
737 pEntry->m_Info.m_NumAddresses = TmpInfo.m_NumAddresses;
738 ServerBrowserFormatAddresses(pBuffer: pEntry->m_Info.m_aAddress, BufferSize: sizeof(pEntry->m_Info.m_aAddress), pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
739 str_copy(dst&: pEntry->m_Info.m_aCommunityId, src: TmpInfo.m_aCommunityId);
740 str_copy(dst&: pEntry->m_Info.m_aCommunityCountry, src: TmpInfo.m_aCommunityCountry);
741 str_copy(dst&: pEntry->m_Info.m_aCommunityType, src: TmpInfo.m_aCommunityType);
742 UpdateServerRank(pInfo: &pEntry->m_Info);
743 pEntry->m_Info.m_GametypeColor = CServerInfo::GametypeColor(pGametype: pEntry->m_Info.m_aGameType);
744
745 if(pEntry->m_Info.m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_UNSPECIFIED)
746 {
747 if(str_find_nocase(haystack: pEntry->m_Info.m_aGameType, needle: "race") || str_find_nocase(haystack: pEntry->m_Info.m_aGameType, needle: "fastcap"))
748 {
749 pEntry->m_Info.m_ClientScoreKind = CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT;
750 }
751 else
752 {
753 pEntry->m_Info.m_ClientScoreKind = CServerInfo::CLIENT_SCORE_KIND_POINTS;
754 }
755 }
756
757 class CPlayerScoreNameLess
758 {
759 const int m_ScoreKind;
760
761 public:
762 CPlayerScoreNameLess(int ClientScoreKind) :
763 m_ScoreKind(ClientScoreKind)
764 {
765 }
766
767 bool operator()(const CServerInfo::CClient &Client0, const CServerInfo::CClient &Client1) const
768 {
769 // Sort players before non players
770 if(Client0.m_Player && !Client1.m_Player)
771 return true;
772 if(!Client0.m_Player && Client1.m_Player)
773 return false;
774
775 int Score0 = Client0.m_Score;
776 int Score1 = Client1.m_Score;
777
778 if(m_ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME || m_ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT)
779 {
780 // Sort unfinished (-9999) and still connecting players (-1) after others
781 if(Score0 < 0 && Score1 >= 0)
782 return false;
783 if(Score0 >= 0 && Score1 < 0)
784 return true;
785 }
786
787 if(Score0 != Score1)
788 {
789 // Handle the sign change introduced with CLIENT_SCORE_KIND_TIME
790 if(m_ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME)
791 return Score0 < Score1;
792 else
793 return Score0 > Score1;
794 }
795
796 return str_comp_nocase(a: Client0.m_aName, b: Client1.m_aName) < 0;
797 }
798 };
799
800 std::sort(first: pEntry->m_Info.m_aClients, last: pEntry->m_Info.m_aClients + Info.m_NumReceivedClients, comp: CPlayerScoreNameLess(pEntry->m_Info.m_ClientScoreKind));
801
802 pEntry->m_GotInfo = 1;
803}
804
805void CServerBrowser::SetLatency(NETADDR Addr, int Latency)
806{
807 m_pPingCache->CachePing(Addr, Ping: Latency);
808
809 Addr.port = 0;
810 for(CServerEntry *pEntry : m_vpServerlist)
811 {
812 if(!pEntry->m_GotInfo)
813 {
814 continue;
815 }
816 bool Found = false;
817 for(int i = 0; i < pEntry->m_Info.m_NumAddresses; i++)
818 {
819 NETADDR Other = pEntry->m_Info.m_aAddresses[i];
820 Other.port = 0;
821 if(Addr == Other)
822 {
823 Found = true;
824 break;
825 }
826 }
827 if(!Found)
828 {
829 continue;
830 }
831 int Ping = m_pPingCache->GetPing(pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
832 if(Ping == -1)
833 {
834 continue;
835 }
836 pEntry->m_Info.m_Latency = Ping;
837 pEntry->m_Info.m_LatencyIsEstimated = false;
838 }
839}
840
841CServerBrowser::CServerEntry *CServerBrowser::Add(const NETADDR *pAddrs, int NumAddrs)
842{
843 // create new pEntry
844 CServerEntry *pEntry = m_ServerlistHeap.Allocate<CServerEntry>();
845 *pEntry = {};
846
847 // set the info
848 mem_copy(dest: pEntry->m_Info.m_aAddresses, source: pAddrs, size: NumAddrs * sizeof(pAddrs[0]));
849 pEntry->m_Info.m_NumAddresses = NumAddrs;
850
851 pEntry->m_Info.m_Latency = 999;
852 pEntry->m_Info.m_HasRank = CServerInfo::RANK_UNAVAILABLE;
853 ServerBrowserFormatAddresses(pBuffer: pEntry->m_Info.m_aAddress, BufferSize: sizeof(pEntry->m_Info.m_aAddress), pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
854 UpdateServerCommunity(pInfo: &pEntry->m_Info);
855 str_copy(dst: pEntry->m_Info.m_aName, src: pEntry->m_Info.m_aAddress, dst_size: sizeof(pEntry->m_Info.m_aName));
856
857 // check if it's a favorite
858 pEntry->m_Info.m_Favorite = m_pFavorites->IsFavorite(pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
859 pEntry->m_Info.m_FavoriteAllowPing = m_pFavorites->IsPingAllowed(pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
860
861 int ServerIndex = m_vpServerlist.size();
862 for(int i = 0; i < NumAddrs; i++)
863 {
864 m_ByAddr[pAddrs[i]] = ServerIndex;
865 }
866
867 // add to list
868 pEntry->m_Info.m_ServerIndex = ServerIndex;
869 if(m_vpServerlist.capacity() == 0)
870 {
871 m_vpServerlist.reserve(n: 128);
872 }
873 m_vpServerlist.push_back(x: pEntry);
874
875 return pEntry;
876}
877
878CServerBrowser::CServerEntry *CServerBrowser::ReplaceEntry(CServerEntry *pEntry, const NETADDR *pAddrs, int NumAddrs)
879{
880 for(int i = 0; i < pEntry->m_Info.m_NumAddresses; i++)
881 {
882 m_ByAddr.erase(x: pEntry->m_Info.m_aAddresses[i]);
883 }
884
885 // set the info
886 mem_copy(dest: pEntry->m_Info.m_aAddresses, source: pAddrs, size: NumAddrs * sizeof(pAddrs[0]));
887 pEntry->m_Info.m_NumAddresses = NumAddrs;
888
889 pEntry->m_Info.m_Latency = 999;
890 pEntry->m_Info.m_HasRank = CServerInfo::RANK_UNAVAILABLE;
891 ServerBrowserFormatAddresses(pBuffer: pEntry->m_Info.m_aAddress, BufferSize: sizeof(pEntry->m_Info.m_aAddress), pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
892 UpdateServerCommunity(pInfo: &pEntry->m_Info);
893 str_copy(dst: pEntry->m_Info.m_aName, src: pEntry->m_Info.m_aAddress, dst_size: sizeof(pEntry->m_Info.m_aName));
894
895 pEntry->m_Info.m_Favorite = m_pFavorites->IsFavorite(pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
896 pEntry->m_Info.m_FavoriteAllowPing = m_pFavorites->IsPingAllowed(pAddrs: pEntry->m_Info.m_aAddresses, NumAddrs: pEntry->m_Info.m_NumAddresses);
897
898 for(int i = 0; i < NumAddrs; i++)
899 {
900 m_ByAddr[pAddrs[i]] = pEntry->m_Info.m_ServerIndex;
901 }
902
903 return pEntry;
904}
905
906void CServerBrowser::OnServerInfoUpdate(const NETADDR &Addr, int Token, const CServerInfo *pInfo)
907{
908 int BasicToken = Token;
909 int ExtraToken = 0;
910 if(pInfo->m_Type == SERVERINFO_EXTENDED)
911 {
912 BasicToken = Token & 0xff;
913 ExtraToken = Token >> 8;
914 }
915
916 CServerEntry *pEntry = Find(Addr);
917
918 if(m_ServerlistType == IServerBrowser::TYPE_LAN)
919 {
920 if(!pEntry)
921 {
922 NETADDR LookupAddr = Addr;
923 if(Addr.type & NETTYPE_TW7)
924 {
925 // don't add 0.7 server if 0.6 server with the same IP and port already exists
926 LookupAddr.type &= ~NETTYPE_TW7;
927 pEntry = Find(Addr: LookupAddr);
928 if(pEntry)
929 return;
930 }
931 else
932 {
933 // replace 0.7 bridge server with 0.6
934 LookupAddr.type |= NETTYPE_TW7;
935 pEntry = Find(Addr: LookupAddr);
936 if(pEntry)
937 pEntry = ReplaceEntry(pEntry, pAddrs: &Addr, NumAddrs: 1);
938 }
939 }
940
941 NETADDR Broadcast = NETADDR_ZEROED;
942 Broadcast.type = (m_pNetClient->NetType() & ~(NETTYPE_WEBSOCKET_IPV4 | NETTYPE_WEBSOCKET_IPV6)) | NETTYPE_LINK_BROADCAST;
943 int TokenBC = GenerateToken(Addr: Broadcast);
944 bool Drop = false;
945 Drop = Drop || BasicToken != GetBasicToken(Token: TokenBC);
946 Drop = Drop || (pInfo->m_Type == SERVERINFO_EXTENDED && ExtraToken != GetExtraToken(Token: TokenBC));
947 if(Drop)
948 {
949 return;
950 }
951
952 if(!pEntry)
953 pEntry = Add(pAddrs: &Addr, NumAddrs: 1);
954 }
955 else
956 {
957 if(!pEntry)
958 {
959 return;
960 }
961 int TokenAddr = GenerateToken(Addr);
962 bool Drop = false;
963 Drop = Drop || BasicToken != GetBasicToken(Token: TokenAddr);
964 Drop = Drop || (pInfo->m_Type == SERVERINFO_EXTENDED && ExtraToken != GetExtraToken(Token: TokenAddr));
965 if(Drop)
966 {
967 return;
968 }
969 }
970
971 if(m_ServerlistType == IServerBrowser::TYPE_LAN)
972 {
973 SetInfo(pEntry, Info: *pInfo);
974 pEntry->m_Info.m_Latency = minimum(a: static_cast<int>((time_get() - m_BroadcastTime) * 1000 / time_freq()), b: 999);
975 }
976 else if(pEntry->m_RequestTime > 0)
977 {
978 if(!pEntry->m_RequestIgnoreInfo)
979 {
980 SetInfo(pEntry, Info: *pInfo);
981 }
982
983 int Latency = minimum(a: static_cast<int>((time_get() - pEntry->m_RequestTime) * 1000 / time_freq()), b: 999);
984 if(!pEntry->m_RequestIgnoreInfo)
985 {
986 pEntry->m_Info.m_Latency = Latency;
987 }
988 else
989 {
990 char aAddr[NETADDR_MAXSTRSIZE];
991 net_addr_str(addr: &Addr, string: aAddr, max_length: sizeof(aAddr), add_port: true);
992 dbg_msg(sys: "serverbrowser", fmt: "received ping response from %s", aAddr);
993 SetLatency(Addr, Latency);
994 }
995 pEntry->m_RequestTime = -1; // Request has been answered
996 }
997 RemoveRequest(pEntry);
998 RequestResort();
999}
1000
1001void CServerBrowser::Refresh(int Type, bool Force)
1002{
1003 bool ServerListTypeChanged = Force || m_ServerlistType != Type;
1004 int OldServerListType = m_ServerlistType;
1005 m_ServerlistType = Type;
1006 secure_random_fill(bytes: m_aTokenSeed, length: sizeof(m_aTokenSeed));
1007
1008 if(Type == IServerBrowser::TYPE_LAN || (ServerListTypeChanged && OldServerListType == IServerBrowser::TYPE_LAN))
1009 CleanUp();
1010
1011 if(Type == IServerBrowser::TYPE_LAN)
1012 {
1013 unsigned char aBuffer[sizeof(SERVERBROWSE_GETINFO) + 1];
1014 CNetChunk Packet;
1015
1016 /* do the broadcast version */
1017 mem_zero(block: &Packet, size: sizeof(Packet));
1018 Packet.m_Address.type = (m_pNetClient->NetType() & ~(NETTYPE_WEBSOCKET_IPV4 | NETTYPE_WEBSOCKET_IPV6)) | NETTYPE_LINK_BROADCAST;
1019 Packet.m_Flags = NETSENDFLAG_CONNLESS | NETSENDFLAG_EXTENDED;
1020 Packet.m_DataSize = sizeof(aBuffer);
1021 Packet.m_pData = aBuffer;
1022
1023 int Token = GenerateToken(Addr: Packet.m_Address);
1024 mem_copy(dest: aBuffer, source: SERVERBROWSE_GETINFO, size: sizeof(SERVERBROWSE_GETINFO));
1025 aBuffer[sizeof(SERVERBROWSE_GETINFO)] = GetBasicToken(Token);
1026
1027 Packet.m_aExtraData[0] = GetExtraToken(Token) >> 8;
1028 Packet.m_aExtraData[1] = GetExtraToken(Token) & 0xff;
1029
1030 m_BroadcastTime = time_get();
1031
1032 CPacker Packer;
1033 Packer.Reset();
1034 Packer.AddRaw(pData: SERVERBROWSE_GETINFO, Size: sizeof(SERVERBROWSE_GETINFO));
1035 Packer.AddInt(i: Token);
1036
1037 CNetChunk Packet7;
1038 mem_zero(block: &Packet7, size: sizeof(Packet7));
1039 Packet7.m_Address.type = (m_pNetClient->NetType() & ~(NETTYPE_WEBSOCKET_IPV4 | NETTYPE_WEBSOCKET_IPV6)) | NETTYPE_TW7 | NETTYPE_LINK_BROADCAST;
1040 Packet7.m_Flags = NETSENDFLAG_CONNLESS;
1041 Packet7.m_DataSize = Packer.Size();
1042 Packet7.m_pData = Packer.Data();
1043
1044 for(int Port = LAN_PORT_BEGIN; Port <= LAN_PORT_END; Port++)
1045 {
1046 Packet.m_Address.port = Port;
1047 m_pNetClient->Send(pChunk: &Packet);
1048
1049 Packet7.m_Address.port = Port;
1050 m_pNetClient->Send(pChunk: &Packet7);
1051 }
1052
1053 if(g_Config.m_Debug)
1054 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "serverbrowser", pStr: "broadcasting for servers");
1055 }
1056 else
1057 {
1058 m_pHttp->Refresh();
1059 m_pPingCache->Load();
1060 m_RefreshingHttp = true;
1061
1062 if(ServerListTypeChanged && m_pHttp->NumServers() > 0)
1063 {
1064 CleanUp();
1065 UpdateFromHttp();
1066 Sort();
1067 }
1068 }
1069}
1070
1071void CServerBrowser::RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int *pBasicToken, int *pToken, bool RandomToken) const
1072{
1073 if(g_Config.m_Debug)
1074 {
1075 char aAddrStr[NETADDR_MAXSTRSIZE];
1076 net_addr_str(addr: &Addr, string: aAddrStr, max_length: sizeof(aAddrStr), add_port: true);
1077 char aBuf[256];
1078 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "requesting server info from %s", aAddrStr);
1079 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "serverbrowser", pStr: aBuf);
1080 }
1081
1082 int Token = GenerateToken(Addr);
1083 if(RandomToken)
1084 {
1085 int AvoidBasicToken = GetBasicToken(Token);
1086 do
1087 {
1088 secure_random_fill(bytes: &Token, length: sizeof(Token));
1089 Token &= 0xffffff;
1090 } while(GetBasicToken(Token) == AvoidBasicToken);
1091 }
1092 if(pToken)
1093 {
1094 *pToken = Token;
1095 }
1096 if(pBasicToken)
1097 {
1098 *pBasicToken = GetBasicToken(Token);
1099 }
1100
1101 if(Addr.type & NETTYPE_TW7)
1102 {
1103 CPacker Packer;
1104 Packer.Reset();
1105 Packer.AddRaw(pData: SERVERBROWSE_GETINFO, Size: sizeof(SERVERBROWSE_GETINFO));
1106 Packer.AddInt(i: Token);
1107
1108 CNetChunk Packet;
1109 Packet.m_ClientId = -1;
1110 Packet.m_Address = Addr;
1111 Packet.m_Flags = NETSENDFLAG_CONNLESS;
1112 Packet.m_DataSize = Packer.Size();
1113 Packet.m_pData = Packer.Data();
1114 mem_zero(block: &Packet.m_aExtraData, size: sizeof(Packet.m_aExtraData));
1115
1116 m_pNetClient->Send(pChunk: &Packet);
1117 }
1118 else
1119 {
1120 unsigned char aBuffer[sizeof(SERVERBROWSE_GETINFO) + 1];
1121 mem_copy(dest: aBuffer, source: SERVERBROWSE_GETINFO, size: sizeof(SERVERBROWSE_GETINFO));
1122 aBuffer[sizeof(SERVERBROWSE_GETINFO)] = GetBasicToken(Token);
1123
1124 CNetChunk Packet;
1125 Packet.m_ClientId = -1;
1126 Packet.m_Address = Addr;
1127 Packet.m_Flags = NETSENDFLAG_CONNLESS | NETSENDFLAG_EXTENDED;
1128 Packet.m_DataSize = sizeof(aBuffer);
1129 Packet.m_pData = aBuffer;
1130 mem_zero(block: &Packet.m_aExtraData, size: sizeof(Packet.m_aExtraData));
1131 Packet.m_aExtraData[0] = GetExtraToken(Token) >> 8;
1132 Packet.m_aExtraData[1] = GetExtraToken(Token) & 0xff;
1133
1134 m_pNetClient->Send(pChunk: &Packet);
1135 }
1136
1137 if(pEntry)
1138 pEntry->m_RequestTime = time_get();
1139}
1140
1141void CServerBrowser::RequestCurrentServer(const NETADDR &Addr) const
1142{
1143 RequestImpl(Addr, pEntry: nullptr, pBasicToken: nullptr, pToken: nullptr, RandomToken: false);
1144}
1145
1146void CServerBrowser::RequestCurrentServerWithRandomToken(const NETADDR &Addr, int *pBasicToken, int *pToken) const
1147{
1148 RequestImpl(Addr, pEntry: nullptr, pBasicToken, pToken, RandomToken: true);
1149}
1150
1151void CServerBrowser::SetCurrentServerPing(const NETADDR &Addr, int Ping)
1152{
1153 SetLatency(Addr, Latency: minimum(a: Ping, b: 999));
1154}
1155
1156void CServerBrowser::UpdateFromHttp()
1157{
1158 int OwnLocation;
1159 if(str_comp(a: g_Config.m_BrLocation, b: "auto") == 0)
1160 {
1161 OwnLocation = m_OwnLocation;
1162 }
1163 else
1164 {
1165 if(CServerInfo::ParseLocation(pResult: &OwnLocation, pString: g_Config.m_BrLocation))
1166 {
1167 char aBuf[64];
1168 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "cannot parse br_location: '%s'", g_Config.m_BrLocation);
1169 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "serverbrowser", pStr: aBuf);
1170 }
1171 }
1172
1173 int NumServers = m_pHttp->NumServers();
1174 m_vpServerlist.reserve(n: NumServers);
1175 std::function<bool(const NETADDR *, int)> Want = [](const NETADDR *pAddrs, int NumAddrs) { return true; };
1176 if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES)
1177 {
1178 Want = [this](const NETADDR *pAddrs, int NumAddrs) -> bool {
1179 return m_pFavorites->IsFavorite(pAddrs, NumAddrs) != TRISTATE::NONE;
1180 };
1181 }
1182 else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5)
1183 {
1184 const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1;
1185 std::vector<const CCommunity *> vpFavoriteCommunities = FavoriteCommunities();
1186 dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid community index");
1187 const CCommunity *pWantedCommunity = vpFavoriteCommunities[CommunityIndex];
1188 const bool IsNoneCommunity = str_comp(a: pWantedCommunity->Id(), b: COMMUNITY_NONE) == 0;
1189 Want = [this, pWantedCommunity, IsNoneCommunity](const NETADDR *pAddrs, int NumAddrs) -> bool {
1190 for(int AddressIndex = 0; AddressIndex < NumAddrs; AddressIndex++)
1191 {
1192 const auto CommunityServer = m_CommunityServersByAddr.find(x: CommunityAddressKey(Addr: pAddrs[AddressIndex]));
1193 if(CommunityServer != m_CommunityServersByAddr.end())
1194 {
1195 if(IsNoneCommunity)
1196 {
1197 // Servers with community "none" are not present in m_CommunityServersByAddr, so we ignore
1198 // any server that is found in this map to determine only the servers without community.
1199 return false;
1200 }
1201 else if(str_comp(a: CommunityServer->second.CommunityId(), b: pWantedCommunity->Id()) == 0)
1202 {
1203 return true;
1204 }
1205 }
1206 }
1207 return IsNoneCommunity;
1208 };
1209 }
1210
1211 for(int i = 0; i < NumServers; i++)
1212 {
1213 CServerInfo Info = m_pHttp->Server(Index: i);
1214 if(!Want(Info.m_aAddresses, Info.m_NumAddresses))
1215 {
1216 continue;
1217 }
1218 int Ping = m_pPingCache->GetPing(pAddrs: Info.m_aAddresses, NumAddrs: Info.m_NumAddresses);
1219 Info.m_LatencyIsEstimated = Ping == -1;
1220 if(Info.m_LatencyIsEstimated)
1221 {
1222 Info.m_Latency = CServerInfo::EstimateLatency(Loc1: OwnLocation, Loc2: Info.m_Location);
1223 }
1224 else
1225 {
1226 Info.m_Latency = Ping;
1227 }
1228 CServerEntry *pEntry = Add(pAddrs: Info.m_aAddresses, NumAddrs: Info.m_NumAddresses);
1229 SetInfo(pEntry, Info);
1230 pEntry->m_RequestIgnoreInfo = true;
1231 }
1232
1233 if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES)
1234 {
1235 const IFavorites::CEntry *pFavorites;
1236 int NumFavorites;
1237 m_pFavorites->AllEntries(ppEntries: &pFavorites, pNumEntries: &NumFavorites);
1238 for(int i = 0; i < NumFavorites; i++)
1239 {
1240 bool Found = false;
1241 for(int j = 0; j < pFavorites[i].m_NumAddrs; j++)
1242 {
1243 if(Find(Addr: pFavorites[i].m_aAddrs[j]))
1244 {
1245 Found = true;
1246 break;
1247 }
1248 }
1249 if(Found)
1250 {
1251 continue;
1252 }
1253 // (Also add favorites we're not allowed to ping.)
1254 CServerEntry *pEntry = Add(pAddrs: pFavorites[i].m_aAddrs, NumAddrs: pFavorites[i].m_NumAddrs);
1255 if(pFavorites[i].m_AllowPing)
1256 {
1257 QueueRequest(pEntry);
1258 }
1259 }
1260 }
1261
1262 RequestResort();
1263}
1264
1265void CServerBrowser::CleanUp()
1266{
1267 // clear out everything
1268 m_vSortedServerlist.clear();
1269 m_vpServerlist.clear();
1270 m_ServerlistHeap.Reset();
1271 m_NumSortedPlayers = 0;
1272 m_ByAddr.clear();
1273 m_pFirstReqServer = nullptr;
1274 m_pLastReqServer = nullptr;
1275 m_NumRequests = 0;
1276 m_CurrentMaxRequests = g_Config.m_BrMaxRequests;
1277}
1278
1279void CServerBrowser::Update()
1280{
1281 int64_t Timeout = time_freq();
1282 int64_t Now = time_get();
1283
1284 const char *pHttpBestUrl;
1285 if(!m_pHttp->GetBestUrl(pBestUrl: &pHttpBestUrl) && pHttpBestUrl != m_pHttpPrevBestUrl)
1286 {
1287 str_copy(dst&: g_Config.m_BrCachedBestServerinfoUrl, src: pHttpBestUrl);
1288 m_pHttpPrevBestUrl = pHttpBestUrl;
1289 }
1290
1291 m_pHttp->Update();
1292
1293 if(m_ServerlistType != TYPE_LAN && m_RefreshingHttp && !m_pHttp->IsRefreshing())
1294 {
1295 m_RefreshingHttp = false;
1296 CleanUp();
1297 UpdateFromHttp();
1298 // TODO: move this somewhere else
1299 Sort();
1300 return;
1301 }
1302
1303 {
1304 CServerEntry *pEntry = m_pFirstReqServer;
1305 int Count = 0;
1306 while(true)
1307 {
1308 if(!pEntry) // no more entries
1309 break;
1310 if(pEntry->m_RequestTime && pEntry->m_RequestTime + Timeout < Now)
1311 {
1312 pEntry = pEntry->m_pNextReq;
1313 continue;
1314 }
1315 // no more than 10 concurrent requests
1316 if(Count == m_CurrentMaxRequests)
1317 break;
1318
1319 if(pEntry->m_RequestTime == 0)
1320 {
1321 RequestImpl(Addr: pEntry->m_Info.m_aAddresses[0], pEntry, pBasicToken: nullptr, pToken: nullptr, RandomToken: false);
1322 }
1323
1324 Count++;
1325 pEntry = pEntry->m_pNextReq;
1326 }
1327
1328 if(m_pFirstReqServer && Count == 0 && m_CurrentMaxRequests > 1) //NO More current Server Requests
1329 {
1330 //reset old ones
1331 pEntry = m_pFirstReqServer;
1332 while(true)
1333 {
1334 if(!pEntry) // no more entries
1335 break;
1336 pEntry->m_RequestTime = 0;
1337 pEntry = pEntry->m_pNextReq;
1338 }
1339
1340 //update max-requests
1341 m_CurrentMaxRequests = m_CurrentMaxRequests / 2;
1342 if(m_CurrentMaxRequests < 1)
1343 m_CurrentMaxRequests = 1;
1344 }
1345 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
1346 {
1347 pEntry = m_pFirstReqServer;
1348 while(true)
1349 {
1350 if(!pEntry) // no more entries
1351 break;
1352 CServerEntry *pNext = pEntry->m_pNextReq;
1353 RemoveRequest(pEntry); //release request
1354 pEntry = pNext;
1355 }
1356 }
1357 }
1358
1359 // check if we need to resort
1360 if(m_Sorthash != SortHash() || m_NeedResort)
1361 {
1362 for(CServerEntry *pEntry : m_vpServerlist)
1363 {
1364 CServerInfo *pInfo = &pEntry->m_Info;
1365 pInfo->m_Favorite = m_pFavorites->IsFavorite(pAddrs: pInfo->m_aAddresses, NumAddrs: pInfo->m_NumAddresses);
1366 pInfo->m_FavoriteAllowPing = m_pFavorites->IsPingAllowed(pAddrs: pInfo->m_aAddresses, NumAddrs: pInfo->m_NumAddresses);
1367 }
1368 Sort();
1369 m_NeedResort = false;
1370 }
1371}
1372
1373const json_value *CServerBrowser::LoadDDNetInfo()
1374{
1375 LoadDDNetInfoJson();
1376 LoadDDNetLocation();
1377 LoadDDNetServers();
1378 for(CServerEntry *pEntry : m_vpServerlist)
1379 {
1380 UpdateServerCommunity(pInfo: &pEntry->m_Info);
1381 UpdateServerRank(pInfo: &pEntry->m_Info);
1382 }
1383 ValidateServerlistType();
1384 RequestResort();
1385 return m_pDDNetInfo;
1386}
1387
1388void CServerBrowser::LoadDDNetInfoJson()
1389{
1390 void *pBuf;
1391 unsigned Length;
1392 if(!m_pStorage->ReadFile(pFilename: DDNET_INFO_FILE, Type: IStorage::TYPE_SAVE, ppResult: &pBuf, pResultLen: &Length))
1393 {
1394 // Keep old info if available
1395 return;
1396 }
1397
1398 m_DDNetInfoSha256 = sha256(message: pBuf, message_len: Length);
1399
1400 json_value_free(m_pDDNetInfo);
1401 json_settings JsonSettings{};
1402 char aError[256];
1403 m_pDDNetInfo = json_parse_ex(settings: &JsonSettings, json: static_cast<json_char *>(pBuf), length: Length, error: aError);
1404 free(ptr: pBuf);
1405
1406 if(m_pDDNetInfo == nullptr)
1407 {
1408 log_error("serverbrowser", "invalid info json: '%s'", aError);
1409 }
1410 else if(m_pDDNetInfo->type != json_object)
1411 {
1412 log_error("serverbrowser", "invalid info root");
1413 json_value_free(m_pDDNetInfo);
1414 m_pDDNetInfo = nullptr;
1415 }
1416}
1417
1418void CServerBrowser::LoadDDNetLocation()
1419{
1420 m_OwnLocation = CServerInfo::LOC_UNKNOWN;
1421 if(m_pDDNetInfo)
1422 {
1423 const json_value &Location = (*m_pDDNetInfo)["location"];
1424 if(Location.type != json_string || CServerInfo::ParseLocation(pResult: &m_OwnLocation, pString: Location))
1425 {
1426 log_error("serverbrowser", "invalid location");
1427 }
1428 }
1429}
1430
1431bool CServerBrowser::ParseCommunityServers(CCommunity *pCommunity, const json_value &Servers)
1432{
1433 for(unsigned ServerIndex = 0; ServerIndex < Servers.u.array.length; ++ServerIndex)
1434 {
1435 // pServer - { name, flagId, servers }
1436 const json_value &Server = Servers[ServerIndex];
1437 if(Server.type != json_object)
1438 {
1439 log_error("serverbrowser", "invalid server (ServerIndex=%u)", ServerIndex);
1440 return false;
1441 }
1442
1443 const json_value &Name = Server["name"];
1444 const json_value &FlagId = Server["flagId"];
1445 const json_value &Types = Server["servers"];
1446 if(Name.type != json_string || FlagId.type != json_integer || Types.type != json_object)
1447 {
1448 log_error("serverbrowser", "invalid server attribute (ServerIndex=%u)", ServerIndex);
1449 return false;
1450 }
1451 if(Types.u.object.length == 0)
1452 continue;
1453
1454 pCommunity->m_vCountries.emplace_back(args: Name.u.string.ptr, args: FlagId.u.integer);
1455 CCommunityCountry *pCountry = &pCommunity->m_vCountries.back();
1456
1457 for(unsigned TypeIndex = 0; TypeIndex < Types.u.object.length; ++TypeIndex)
1458 {
1459 const json_value &Addresses = *Types.u.object.values[TypeIndex].value;
1460 if(Addresses.type != json_array)
1461 {
1462 log_error("serverbrowser", "invalid addresses (ServerIndex=%u, TypeIndex=%u)", ServerIndex, TypeIndex);
1463 return false;
1464 }
1465 if(Addresses.u.array.length == 0)
1466 continue;
1467
1468 const char *pTypeName = Types.u.object.values[TypeIndex].name;
1469
1470 // add type if it doesn't exist already
1471 const auto CommunityType = std::find_if(first: pCommunity->m_vTypes.begin(), last: pCommunity->m_vTypes.end(), pred: [pTypeName](const auto &Elem) {
1472 return str_comp(Elem.Name(), pTypeName) == 0;
1473 });
1474 if(CommunityType == pCommunity->m_vTypes.end())
1475 {
1476 pCommunity->m_vTypes.emplace_back(args&: pTypeName);
1477 }
1478
1479 // add addresses
1480 for(unsigned AddressIndex = 0; AddressIndex < Addresses.u.array.length; ++AddressIndex)
1481 {
1482 const json_value &Address = Addresses[AddressIndex];
1483 if(Address.type != json_string)
1484 {
1485 log_error("serverbrowser", "invalid address (ServerIndex=%u, TypeIndex=%u, AddressIndex=%u)", ServerIndex, TypeIndex, AddressIndex);
1486 return false;
1487 }
1488 NETADDR NetAddr;
1489 if(net_addr_from_str(addr: &NetAddr, string: Address.u.string.ptr))
1490 {
1491 log_error("serverbrowser", "invalid address (ServerIndex=%u, TypeIndex=%u, AddressIndex=%u)", ServerIndex, TypeIndex, AddressIndex);
1492 continue;
1493 }
1494 pCountry->m_vServers.emplace_back(args&: NetAddr, args&: pTypeName);
1495 }
1496 }
1497 }
1498 return true;
1499}
1500
1501bool CServerBrowser::ParseCommunityFinishes(CCommunity *pCommunity, const json_value &Finishes)
1502{
1503 for(unsigned FinishIndex = 0; FinishIndex < Finishes.u.array.length; ++FinishIndex)
1504 {
1505 const json_value &Finish = Finishes[FinishIndex];
1506 if(Finish.type != json_string)
1507 {
1508 log_error("serverbrowser", "invalid rank (FinishIndex=%u)", FinishIndex);
1509 return false;
1510 }
1511 pCommunity->m_FinishedMaps.emplace(args: (const char *)Finish);
1512 }
1513 return true;
1514}
1515
1516void CServerBrowser::LoadDDNetServers()
1517{
1518 // Parse communities
1519 m_vCommunities.clear();
1520 m_CommunityServersByAddr.clear();
1521
1522 if(!m_pDDNetInfo)
1523 {
1524 return;
1525 }
1526
1527 const json_value &Communities = (*m_pDDNetInfo)["communities"];
1528 if(Communities.type != json_array)
1529 {
1530 return;
1531 }
1532
1533 for(unsigned CommunityIndex = 0; CommunityIndex < Communities.u.array.length; ++CommunityIndex)
1534 {
1535 const json_value &Community = Communities[CommunityIndex];
1536 if(Community.type != json_object)
1537 {
1538 log_error("serverbrowser", "invalid community (CommunityIndex=%d)", (int)CommunityIndex);
1539 continue;
1540 }
1541 const json_value &Id = Community["id"];
1542 if(Id.type != json_string)
1543 {
1544 log_error("serverbrowser", "invalid community id (CommunityIndex=%d)", (int)CommunityIndex);
1545 continue;
1546 }
1547 const json_value &Icon = Community["icon"];
1548 const json_value &IconSha256 = Icon["sha256"];
1549 const json_value &IconUrl = Icon["url"];
1550 const json_value &Name = Community["name"];
1551 const json_value HasFinishes = Community["has_finishes"];
1552 const json_value *pFinishes = &Community["finishes"];
1553 const json_value *pServers = &Community["servers"];
1554 // We accidentally set finishes/servers to be part of icon in
1555 // the past, so support that, too. Can be removed once we make
1556 // a breaking change to the whole thing, necessitating a new
1557 // endpoint.
1558 if(pFinishes->type == json_none)
1559 {
1560 pServers = &Icon["finishes"];
1561 }
1562 if(pServers->type == json_none)
1563 {
1564 pServers = &Icon["servers"];
1565 }
1566 // Backward compatibility.
1567 if(pFinishes->type == json_none)
1568 {
1569 if(str_comp(a: Id, b: COMMUNITY_DDNET) == 0)
1570 {
1571 pFinishes = &(*m_pDDNetInfo)["maps"];
1572 }
1573 }
1574 if(pServers->type == json_none)
1575 {
1576 if(str_comp(a: Id, b: COMMUNITY_DDNET) == 0)
1577 {
1578 pServers = &(*m_pDDNetInfo)["servers"];
1579 }
1580 else if(str_comp(a: Id, b: "kog") == 0)
1581 {
1582 pServers = &(*m_pDDNetInfo)["servers-kog"];
1583 }
1584 }
1585 if(false ||
1586 Icon.type != json_object ||
1587 IconSha256.type != json_string ||
1588 IconUrl.type != json_string ||
1589 Name.type != json_string ||
1590 HasFinishes.type != json_boolean ||
1591 (pFinishes->type != json_array && pFinishes->type != json_none) ||
1592 pServers->type != json_array)
1593 {
1594 log_error("serverbrowser", "invalid community attribute (CommunityId=%s)", (const char *)Id);
1595 continue;
1596 }
1597 SHA256_DIGEST ParsedIconSha256;
1598 if(sha256_from_str(out: &ParsedIconSha256, str: IconSha256) != 0)
1599 {
1600 log_error("serverbrowser", "invalid community icon sha256 (CommunityId=%s)", (const char *)Id);
1601 continue;
1602 }
1603 CCommunity NewCommunity(Id, Name, ParsedIconSha256, IconUrl);
1604 if(!ParseCommunityServers(pCommunity: &NewCommunity, Servers: *pServers))
1605 {
1606 log_error("serverbrowser", "invalid community servers (CommunityId=%s)", NewCommunity.Id());
1607 continue;
1608 }
1609 NewCommunity.m_HasFinishes = HasFinishes;
1610 if(NewCommunity.m_HasFinishes && pFinishes->type == json_array && !ParseCommunityFinishes(pCommunity: &NewCommunity, Finishes: *pFinishes))
1611 {
1612 log_error("serverbrowser", "invalid community finishes (CommunityId=%s)", NewCommunity.Id());
1613 continue;
1614 }
1615
1616 for(const auto &Country : NewCommunity.Countries())
1617 {
1618 for(const auto &Server : Country.Servers())
1619 {
1620 m_CommunityServersByAddr.emplace(args: CommunityAddressKey(Addr: Server.Address()), args: CCommunityServer(NewCommunity.Id(), Country.Name(), Server.TypeName()));
1621 }
1622 }
1623 m_vCommunities.push_back(x: std::move(NewCommunity));
1624 }
1625
1626 // Add default none community
1627 {
1628 CCommunity NoneCommunity(COMMUNITY_NONE, "None", std::nullopt, "");
1629 NoneCommunity.m_vCountries.emplace_back(args: COMMUNITY_COUNTRY_NONE, args: -1);
1630 NoneCommunity.m_vTypes.emplace_back(args: COMMUNITY_TYPE_NONE);
1631 m_vCommunities.push_back(x: std::move(NoneCommunity));
1632 }
1633
1634 // Remove unknown elements from exclude lists
1635 CleanFilters();
1636}
1637
1638void CServerBrowser::UpdateServerFilteredPlayers(CServerInfo *pInfo) const
1639{
1640 pInfo->m_NumFilteredPlayers = g_Config.m_BrFilterSpectators ? pInfo->m_NumPlayers : pInfo->m_NumClients;
1641 if(g_Config.m_BrFilterConnectingPlayers)
1642 {
1643 for(const auto &Client : pInfo->m_aClients)
1644 {
1645 if((!g_Config.m_BrFilterSpectators || Client.m_Player) && str_comp(a: Client.m_aName, b: "(connecting)") == 0 && Client.m_aClan[0] == '\0')
1646 pInfo->m_NumFilteredPlayers--;
1647 }
1648 }
1649}
1650
1651void CServerBrowser::UpdateServerFriends(CServerInfo *pInfo) const
1652{
1653 pInfo->m_FriendState = IFriends::FRIEND_NO;
1654 pInfo->m_FriendNum = 0;
1655 for(int ClientIndex = 0; ClientIndex < minimum(a: pInfo->m_NumReceivedClients, b: (int)MAX_CLIENTS); ClientIndex++)
1656 {
1657 pInfo->m_aClients[ClientIndex].m_FriendState = m_pFriends->GetFriendState(pName: pInfo->m_aClients[ClientIndex].m_aName, pClan: pInfo->m_aClients[ClientIndex].m_aClan);
1658 pInfo->m_FriendState = maximum(a: pInfo->m_FriendState, b: pInfo->m_aClients[ClientIndex].m_FriendState);
1659 if(pInfo->m_aClients[ClientIndex].m_FriendState != IFriends::FRIEND_NO)
1660 pInfo->m_FriendNum++;
1661 }
1662}
1663
1664void CServerBrowser::UpdateServerCommunity(CServerInfo *pInfo) const
1665{
1666 for(int AddressIndex = 0; AddressIndex < pInfo->m_NumAddresses; AddressIndex++)
1667 {
1668 const auto Community = m_CommunityServersByAddr.find(x: CommunityAddressKey(Addr: pInfo->m_aAddresses[AddressIndex]));
1669 if(Community != m_CommunityServersByAddr.end())
1670 {
1671 str_copy(dst&: pInfo->m_aCommunityId, src: Community->second.CommunityId());
1672 str_copy(dst&: pInfo->m_aCommunityCountry, src: Community->second.CountryName());
1673 str_copy(dst&: pInfo->m_aCommunityType, src: Community->second.TypeName());
1674 return;
1675 }
1676 }
1677 str_copy(dst&: pInfo->m_aCommunityId, src: COMMUNITY_NONE);
1678 str_copy(dst&: pInfo->m_aCommunityCountry, src: COMMUNITY_COUNTRY_NONE);
1679 str_copy(dst&: pInfo->m_aCommunityType, src: COMMUNITY_TYPE_NONE);
1680}
1681
1682void CServerBrowser::UpdateServerRank(CServerInfo *pInfo) const
1683{
1684 const CCommunity *pCommunity = Community(pCommunityId: pInfo->m_aCommunityId);
1685 pInfo->m_HasRank = pCommunity == nullptr ? CServerInfo::RANK_UNAVAILABLE : pCommunity->HasRank(pMap: pInfo->m_aMap);
1686}
1687
1688void CServerBrowser::ValidateServerlistType()
1689{
1690 if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 &&
1691 m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5)
1692 {
1693 const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1;
1694 if(CommunityIndex >= FavoriteCommunities().size())
1695 {
1696 // Reset to internet type if there is no favorite community for the current browser type,
1697 // in case communities have been removed.
1698 m_ServerlistType = IServerBrowser::TYPE_INTERNET;
1699 }
1700 }
1701}
1702
1703const char *CServerBrowser::GetTutorialServer()
1704{
1705 const CCommunity *pCommunity = Community(pCommunityId: COMMUNITY_DDNET);
1706 if(pCommunity == nullptr)
1707 return nullptr;
1708
1709 const char *pBestAddr = nullptr;
1710 int BestLatency = std::numeric_limits<int>::max();
1711 for(const auto &Country : pCommunity->Countries())
1712 {
1713 for(const auto &Server : Country.Servers())
1714 {
1715 if(str_comp(a: Server.TypeName(), b: "Tutorial") != 0)
1716 continue;
1717 const CServerEntry *pEntry = Find(Addr: Server.Address());
1718 if(!pEntry)
1719 continue;
1720 if(pEntry->m_Info.m_NumPlayers > pEntry->m_Info.m_MaxPlayers - 10)
1721 continue;
1722 if(pEntry->m_Info.m_Latency >= BestLatency)
1723 continue;
1724 BestLatency = pEntry->m_Info.m_Latency;
1725 pBestAddr = pEntry->m_Info.m_aAddress;
1726 }
1727 }
1728 return pBestAddr;
1729}
1730
1731bool CServerBrowser::IsRefreshing() const
1732{
1733 return m_pFirstReqServer != nullptr;
1734}
1735
1736bool CServerBrowser::IsGettingServerlist() const
1737{
1738 return m_pHttp->IsRefreshing();
1739}
1740
1741bool CServerBrowser::IsServerlistError() const
1742{
1743 return m_pHttp->IsError();
1744}
1745
1746int CServerBrowser::LoadingProgression() const
1747{
1748 if(m_vpServerlist.empty())
1749 return 0;
1750
1751 int Servers = m_vpServerlist.size();
1752 int Loaded = m_vpServerlist.size() - m_NumRequests;
1753 return 100.0f * Loaded / Servers;
1754}
1755
1756bool CCommunity::HasCountry(const char *pCountryName) const
1757{
1758 return std::find_if(first: Countries().begin(), last: Countries().end(), pred: [pCountryName](const auto &Elem) {
1759 return str_comp(Elem.Name(), pCountryName) == 0;
1760 }) != Countries().end();
1761}
1762
1763bool CCommunity::HasType(const char *pTypeName) const
1764{
1765 return std::find_if(first: Types().begin(), last: Types().end(), pred: [pTypeName](const auto &Elem) {
1766 return str_comp(Elem.Name(), pTypeName) == 0;
1767 }) != Types().end();
1768}
1769
1770CServerInfo::ERankState CCommunity::HasRank(const char *pMap) const
1771{
1772 if(!HasRanks())
1773 return CServerInfo::RANK_UNAVAILABLE;
1774 const CCommunityMap Needle(pMap);
1775 return !m_FinishedMaps.contains(x: Needle) ? CServerInfo::RANK_UNRANKED : CServerInfo::RANK_RANKED;
1776}
1777
1778const std::vector<CCommunity> &CServerBrowser::Communities() const
1779{
1780 return m_vCommunities;
1781}
1782
1783const CCommunity *CServerBrowser::Community(const char *pCommunityId) const
1784{
1785 const auto Community = std::find_if(first: Communities().begin(), last: Communities().end(), pred: [pCommunityId](const auto &Elem) {
1786 return str_comp(Elem.Id(), pCommunityId) == 0;
1787 });
1788 return Community == Communities().end() ? nullptr : &(*Community);
1789}
1790
1791std::vector<const CCommunity *> CServerBrowser::SelectedCommunities() const
1792{
1793 std::vector<const CCommunity *> vpSelected;
1794 for(const auto &Community : Communities())
1795 {
1796 if(!CommunitiesFilter().Filtered(pCommunityId: Community.Id()))
1797 {
1798 vpSelected.push_back(x: &Community);
1799 }
1800 }
1801 return vpSelected;
1802}
1803
1804std::vector<const CCommunity *> CServerBrowser::FavoriteCommunities() const
1805{
1806 // This is done differently than SelectedCommunities because the favorite
1807 // communities should be returned in the order specified by the user.
1808 std::vector<const CCommunity *> vpFavorites;
1809 for(const auto &CommunityId : FavoriteCommunitiesFilter().Entries())
1810 {
1811 const CCommunity *pCommunity = Community(pCommunityId: CommunityId.Id());
1812 if(pCommunity)
1813 {
1814 vpFavorites.push_back(x: pCommunity);
1815 }
1816 }
1817 return vpFavorites;
1818}
1819
1820std::vector<const CCommunity *> CServerBrowser::CurrentCommunities() const
1821{
1822 if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES)
1823 {
1824 return SelectedCommunities();
1825 }
1826 else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5)
1827 {
1828 const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1;
1829 std::vector<const CCommunity *> vpFavoriteCommunities = FavoriteCommunities();
1830 dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid favorite community serverbrowser type");
1831 return {vpFavoriteCommunities[CommunityIndex]};
1832 }
1833 else
1834 {
1835 return {};
1836 }
1837}
1838
1839unsigned CServerBrowser::CurrentCommunitiesHash() const
1840{
1841 std::vector<const CCommunity *> vpCommunities = CurrentCommunities();
1842 unsigned Hash = 5381;
1843 for(const CCommunity *pCommunity : CurrentCommunities())
1844 {
1845 Hash = (Hash << 5) + Hash + str_quickhash(str: pCommunity->Id());
1846 }
1847 return Hash;
1848}
1849
1850void CCommunityCache::Update(bool Force)
1851{
1852 const unsigned CommunitiesHash = m_pServerBrowser->CurrentCommunitiesHash();
1853 const bool TypeChanged = m_LastType != m_pServerBrowser->GetCurrentType();
1854 const bool CurrentCommunitiesChanged = m_LastType == m_pServerBrowser->GetCurrentType() && m_SelectedCommunitiesHash != CommunitiesHash;
1855 if(CurrentCommunitiesChanged && m_pServerBrowser->GetCurrentType() >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_pServerBrowser->GetCurrentType() <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5)
1856 {
1857 // Favorite community was changed while its type is active,
1858 // refresh to get correct serverlist for updated community.
1859 m_pServerBrowser->Refresh(Type: m_pServerBrowser->GetCurrentType(), Force: true);
1860 }
1861
1862 if(!Force && m_InfoSha256 == m_pServerBrowser->DDNetInfoSha256() &&
1863 !CurrentCommunitiesChanged && !TypeChanged)
1864 {
1865 return;
1866 }
1867
1868 m_InfoSha256 = m_pServerBrowser->DDNetInfoSha256();
1869 m_LastType = m_pServerBrowser->GetCurrentType();
1870 m_SelectedCommunitiesHash = CommunitiesHash;
1871 m_vpSelectedCommunities = m_pServerBrowser->CurrentCommunities();
1872
1873 m_vpSelectableCountries.clear();
1874 m_vpSelectableTypes.clear();
1875 for(const CCommunity *pCommunity : m_vpSelectedCommunities)
1876 {
1877 for(const auto &Country : pCommunity->Countries())
1878 {
1879 const auto ExistingCountry = std::find_if(first: m_vpSelectableCountries.begin(), last: m_vpSelectableCountries.end(), pred: [&](const CCommunityCountry *pOther) {
1880 return str_comp(a: Country.Name(), b: pOther->Name()) == 0 && Country.FlagId() == pOther->FlagId();
1881 });
1882 if(ExistingCountry == m_vpSelectableCountries.end())
1883 {
1884 m_vpSelectableCountries.push_back(x: &Country);
1885 }
1886 }
1887 for(const auto &Type : pCommunity->Types())
1888 {
1889 const auto ExistingType = std::find_if(first: m_vpSelectableTypes.begin(), last: m_vpSelectableTypes.end(), pred: [&](const CCommunityType *pOther) {
1890 return str_comp(a: Type.Name(), b: pOther->Name()) == 0;
1891 });
1892 if(ExistingType == m_vpSelectableTypes.end())
1893 {
1894 m_vpSelectableTypes.push_back(x: &Type);
1895 }
1896 }
1897 }
1898
1899 m_AnyRanksAvailable = std::any_of(first: m_vpSelectedCommunities.begin(), last: m_vpSelectedCommunities.end(), pred: [](const CCommunity *pCommunity) {
1900 return pCommunity->HasRanks();
1901 });
1902
1903 // Country/type filters not shown if there are no countries and types, or if only the none-community is selected
1904 m_CountryTypesFilterAvailable = (!m_vpSelectableCountries.empty() || !m_vpSelectableTypes.empty()) &&
1905 (m_vpSelectedCommunities.size() != 1 || str_comp(a: m_vpSelectedCommunities[0]->Id(), b: IServerBrowser::COMMUNITY_NONE) != 0);
1906
1907 if(m_pServerBrowser->GetCurrentType() >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_pServerBrowser->GetCurrentType() <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5)
1908 {
1909 const size_t CommunityIndex = m_pServerBrowser->GetCurrentType() - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1;
1910 std::vector<const CCommunity *> vpFavoriteCommunities = m_pServerBrowser->FavoriteCommunities();
1911 dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid favorite community serverbrowser type");
1912 m_pCountryTypeFilterKey = vpFavoriteCommunities[CommunityIndex]->Id();
1913 }
1914 else
1915 {
1916 m_pCountryTypeFilterKey = IServerBrowser::COMMUNITY_ALL;
1917 }
1918
1919 m_pServerBrowser->CleanFilters();
1920}
1921
1922void CFavoriteCommunityFilterList::Add(const char *pCommunityId)
1923{
1924 // Remove community if it's already a favorite, so it will be added again at
1925 // the end of the list, to allow setting the entire list easier with binds.
1926 Remove(pCommunityId);
1927
1928 // Ensure maximum number of favorite communities, by removing the least-recently
1929 // added community from the beginning, when the maximum number of favorite
1930 // communities has been reached.
1931 constexpr size_t MaxFavoriteCommunities = IServerBrowser::TYPE_FAVORITE_COMMUNITY_5 - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 + 1;
1932 if(m_vEntries.size() >= MaxFavoriteCommunities)
1933 {
1934 dbg_assert(m_vEntries.size() == MaxFavoriteCommunities, "Maximum number of communities can never be exceeded");
1935 m_vEntries.erase(position: m_vEntries.begin());
1936 }
1937 m_vEntries.emplace_back(args&: pCommunityId);
1938}
1939
1940void CFavoriteCommunityFilterList::Remove(const char *pCommunityId)
1941{
1942 auto FoundCommunity = std::find(first: m_vEntries.begin(), last: m_vEntries.end(), val: CCommunityId(pCommunityId));
1943 if(FoundCommunity != m_vEntries.end())
1944 {
1945 m_vEntries.erase(position: FoundCommunity);
1946 }
1947}
1948
1949void CFavoriteCommunityFilterList::Clear()
1950{
1951 m_vEntries.clear();
1952}
1953
1954bool CFavoriteCommunityFilterList::Filtered(const char *pCommunityId) const
1955{
1956 return std::find(first: m_vEntries.begin(), last: m_vEntries.end(), val: CCommunityId(pCommunityId)) != m_vEntries.end();
1957}
1958
1959bool CFavoriteCommunityFilterList::Empty() const
1960{
1961 return m_vEntries.empty();
1962}
1963
1964void CFavoriteCommunityFilterList::Clean(const std::vector<CCommunity> &vAllowedCommunities)
1965{
1966 auto It = std::remove_if(first: m_vEntries.begin(), last: m_vEntries.end(), pred: [&](const auto &Community) {
1967 return std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) {
1968 return str_comp(Community.Id(), AllowedCommunity.Id()) == 0;
1969 }) == vAllowedCommunities.end();
1970 });
1971 m_vEntries.erase(first: It, last: m_vEntries.end());
1972}
1973
1974void CFavoriteCommunityFilterList::Save(IConfigManager *pConfigManager) const
1975{
1976 char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH];
1977 for(const auto &FavoriteCommunity : m_vEntries)
1978 {
1979 str_copy(dst&: aBuf, src: "add_favorite_community \"");
1980 str_append(dst&: aBuf, src: FavoriteCommunity.Id());
1981 str_append(dst&: aBuf, src: "\"");
1982 pConfigManager->WriteLine(pLine: aBuf);
1983 }
1984}
1985
1986const std::vector<CCommunityId> &CFavoriteCommunityFilterList::Entries() const
1987{
1988 return m_vEntries;
1989}
1990
1991template<typename TNamedElement, typename TElementName>
1992static bool IsSubsetEquals(const std::vector<const TNamedElement *> &vpLeft, const std::set<TElementName> &Right)
1993{
1994 return vpLeft.size() <= Right.size() && std::all_of(vpLeft.begin(), vpLeft.end(), [&](const TNamedElement *pElem) {
1995 return Right.contains(TElementName(pElem->Name()));
1996 });
1997}
1998
1999void CExcludedCommunityFilterList::Add(const char *pCommunityId)
2000{
2001 m_Entries.emplace(args&: pCommunityId);
2002}
2003
2004void CExcludedCommunityFilterList::Remove(const char *pCommunityId)
2005{
2006 m_Entries.erase(x: CCommunityId(pCommunityId));
2007}
2008
2009void CExcludedCommunityFilterList::Clear()
2010{
2011 m_Entries.clear();
2012}
2013
2014bool CExcludedCommunityFilterList::Filtered(const char *pCommunityId) const
2015{
2016 return std::find(first: m_Entries.begin(), last: m_Entries.end(), val: CCommunityId(pCommunityId)) != m_Entries.end();
2017}
2018
2019bool CExcludedCommunityFilterList::Empty() const
2020{
2021 return m_Entries.empty();
2022}
2023
2024void CExcludedCommunityFilterList::Clean(const std::vector<CCommunity> &vAllowedCommunities)
2025{
2026 for(auto It = m_Entries.begin(); It != m_Entries.end();)
2027 {
2028 const bool Found = std::find_if(first: vAllowedCommunities.begin(), last: vAllowedCommunities.end(), pred: [&](const CCommunity &AllowedCommunity) {
2029 return str_comp(a: It->Id(), b: AllowedCommunity.Id()) == 0;
2030 }) != vAllowedCommunities.end();
2031 if(Found)
2032 {
2033 ++It;
2034 }
2035 else
2036 {
2037 It = m_Entries.erase(position: It);
2038 }
2039 }
2040 // Prevent filter that would exclude all allowed communities
2041 if(m_Entries.size() == vAllowedCommunities.size())
2042 {
2043 m_Entries.clear();
2044 }
2045}
2046
2047void CExcludedCommunityFilterList::Save(IConfigManager *pConfigManager) const
2048{
2049 char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH];
2050 for(const auto &ExcludedCommunity : m_Entries)
2051 {
2052 str_copy(dst&: aBuf, src: "add_excluded_community \"");
2053 str_append(dst&: aBuf, src: ExcludedCommunity.Id());
2054 str_append(dst&: aBuf, src: "\"");
2055 pConfigManager->WriteLine(pLine: aBuf);
2056 }
2057}
2058
2059void CExcludedCommunityCountryFilterList::Add(const char *pCountryName)
2060{
2061 // Handle special case that all selectable entries are currently filtered,
2062 // where adding more entries to the exclusion list would have no effect.
2063 auto CommunityEntry = m_Entries.find(x: m_pCommunityCache->CountryTypeFilterKey());
2064 if(CommunityEntry != m_Entries.end() && IsSubsetEquals(vpLeft: m_pCommunityCache->SelectableCountries(), Right: CommunityEntry->second))
2065 {
2066 for(const CCommunityCountry *pSelectableCountry : m_pCommunityCache->SelectableCountries())
2067 {
2068 CommunityEntry->second.erase(x: pSelectableCountry->Name());
2069 }
2070 }
2071
2072 Add(pCommunityId: m_pCommunityCache->CountryTypeFilterKey(), pCountryName);
2073}
2074
2075void CExcludedCommunityCountryFilterList::Add(const char *pCommunityId, const char *pCountryName)
2076{
2077 CCommunityId CommunityId(pCommunityId);
2078 if(!m_Entries.contains(x: CommunityId))
2079 {
2080 m_Entries[CommunityId] = {};
2081 }
2082 m_Entries[CommunityId].emplace(args&: pCountryName);
2083}
2084
2085void CExcludedCommunityCountryFilterList::Remove(const char *pCountryName)
2086{
2087 Remove(pCommunityId: m_pCommunityCache->CountryTypeFilterKey(), pCountryName);
2088}
2089
2090void CExcludedCommunityCountryFilterList::Remove(const char *pCommunityId, const char *pCountryName)
2091{
2092 auto CommunityEntry = m_Entries.find(x: CCommunityId(pCommunityId));
2093 if(CommunityEntry != m_Entries.end())
2094 {
2095 CommunityEntry->second.erase(x: pCountryName);
2096 }
2097}
2098
2099void CExcludedCommunityCountryFilterList::Clear()
2100{
2101 auto CommunityEntry = m_Entries.find(x: m_pCommunityCache->CountryTypeFilterKey());
2102 if(CommunityEntry != m_Entries.end())
2103 {
2104 CommunityEntry->second.clear();
2105 }
2106}
2107
2108bool CExcludedCommunityCountryFilterList::Filtered(const char *pCountryName) const
2109{
2110 auto CommunityEntry = m_Entries.find(x: CCommunityId(m_pCommunityCache->CountryTypeFilterKey()));
2111 if(CommunityEntry == m_Entries.end())
2112 return false;
2113
2114 const auto &CountryEntries = CommunityEntry->second;
2115 return !IsSubsetEquals(vpLeft: m_pCommunityCache->SelectableCountries(), Right: CountryEntries) && CountryEntries.contains(x: CCommunityCountryName(pCountryName));
2116}
2117
2118bool CExcludedCommunityCountryFilterList::Empty() const
2119{
2120 auto CommunityEntry = m_Entries.find(x: CCommunityId(m_pCommunityCache->CountryTypeFilterKey()));
2121 return CommunityEntry == m_Entries.end() ||
2122 CommunityEntry->second.empty() ||
2123 IsSubsetEquals(vpLeft: m_pCommunityCache->SelectableCountries(), Right: CommunityEntry->second);
2124}
2125
2126void CExcludedCommunityCountryFilterList::Clean(const std::vector<CCommunity> &vAllowedCommunities)
2127{
2128 for(auto It = m_Entries.begin(); It != m_Entries.end();)
2129 {
2130 const bool AllEntry = str_comp(a: It->first.Id(), b: IServerBrowser::COMMUNITY_ALL) == 0;
2131 const bool Found = AllEntry || std::find_if(first: vAllowedCommunities.begin(), last: vAllowedCommunities.end(), pred: [&](const CCommunity &AllowedCommunity) {
2132 return str_comp(a: It->first.Id(), b: AllowedCommunity.Id()) == 0;
2133 }) != vAllowedCommunities.end();
2134 if(Found)
2135 {
2136 ++It;
2137 }
2138 else
2139 {
2140 It = m_Entries.erase(position: It);
2141 }
2142 }
2143
2144 for(const CCommunity &AllowedCommunity : vAllowedCommunities)
2145 {
2146 auto CommunityEntry = m_Entries.find(x: CCommunityId(AllowedCommunity.Id()));
2147 if(CommunityEntry != m_Entries.end())
2148 {
2149 auto &CountryEntries = CommunityEntry->second;
2150 for(auto It = CountryEntries.begin(); It != CountryEntries.end();)
2151 {
2152 if(AllowedCommunity.HasCountry(pCountryName: It->Name()))
2153 {
2154 ++It;
2155 }
2156 else
2157 {
2158 It = CountryEntries.erase(position: It);
2159 }
2160 }
2161 // Prevent filter that would exclude all allowed countries
2162 if(CountryEntries.size() == AllowedCommunity.Countries().size())
2163 {
2164 CountryEntries.clear();
2165 }
2166 }
2167 }
2168
2169 auto AllCommunityEntry = m_Entries.find(x: CCommunityId(IServerBrowser::COMMUNITY_ALL));
2170 if(AllCommunityEntry != m_Entries.end())
2171 {
2172 auto &CountryEntries = AllCommunityEntry->second;
2173 for(auto It = CountryEntries.begin(); It != CountryEntries.end();)
2174 {
2175 if(std::any_of(first: vAllowedCommunities.begin(), last: vAllowedCommunities.end(), pred: [&](const auto &Community) { return Community.HasCountry(It->Name()); }))
2176 {
2177 ++It;
2178 }
2179 else
2180 {
2181 It = CountryEntries.erase(position: It);
2182 }
2183 }
2184 // Prevent filter that would exclude all allowed countries
2185 std::set<CCommunityCountryName> UniqueCountries;
2186 for(const CCommunity &AllowedCommunity : vAllowedCommunities)
2187 {
2188 for(const CCommunityCountry &Country : AllowedCommunity.Countries())
2189 {
2190 UniqueCountries.emplace(args: Country.Name());
2191 }
2192 }
2193 if(CountryEntries.size() == UniqueCountries.size())
2194 {
2195 CountryEntries.clear();
2196 }
2197 }
2198}
2199
2200void CExcludedCommunityCountryFilterList::Save(IConfigManager *pConfigManager) const
2201{
2202 char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH];
2203 for(const auto &[Community, Countries] : m_Entries)
2204 {
2205 for(const auto &Country : Countries)
2206 {
2207 str_copy(dst&: aBuf, src: "add_excluded_country \"");
2208 str_append(dst&: aBuf, src: Community.Id());
2209 str_append(dst&: aBuf, src: "\" \"");
2210 str_append(dst&: aBuf, src: Country.Name());
2211 str_append(dst&: aBuf, src: "\"");
2212 pConfigManager->WriteLine(pLine: aBuf);
2213 }
2214 }
2215}
2216
2217void CExcludedCommunityTypeFilterList::Add(const char *pTypeName)
2218{
2219 // Handle special case that all selectable entries are currently filtered,
2220 // where adding more entries to the exclusion list would have no effect.
2221 auto CommunityEntry = m_Entries.find(x: m_pCommunityCache->CountryTypeFilterKey());
2222 if(CommunityEntry != m_Entries.end() && IsSubsetEquals(vpLeft: m_pCommunityCache->SelectableTypes(), Right: CommunityEntry->second))
2223 {
2224 for(const CCommunityType *pSelectableType : m_pCommunityCache->SelectableTypes())
2225 {
2226 CommunityEntry->second.erase(x: pSelectableType->Name());
2227 }
2228 }
2229
2230 Add(pCommunityId: m_pCommunityCache->CountryTypeFilterKey(), pTypeName);
2231}
2232
2233void CExcludedCommunityTypeFilterList::Add(const char *pCommunityId, const char *pTypeName)
2234{
2235 CCommunityId CommunityId(pCommunityId);
2236 if(!m_Entries.contains(x: CommunityId))
2237 {
2238 m_Entries[CommunityId] = {};
2239 }
2240 m_Entries[CommunityId].emplace(args&: pTypeName);
2241}
2242
2243void CExcludedCommunityTypeFilterList::Remove(const char *pTypeName)
2244{
2245 Remove(pCommunityId: m_pCommunityCache->CountryTypeFilterKey(), pTypeName);
2246}
2247
2248void CExcludedCommunityTypeFilterList::Remove(const char *pCommunityId, const char *pTypeName)
2249{
2250 auto CommunityEntry = m_Entries.find(x: CCommunityId(pCommunityId));
2251 if(CommunityEntry != m_Entries.end())
2252 {
2253 CommunityEntry->second.erase(x: pTypeName);
2254 }
2255}
2256
2257void CExcludedCommunityTypeFilterList::Clear()
2258{
2259 auto CommunityEntry = m_Entries.find(x: m_pCommunityCache->CountryTypeFilterKey());
2260 if(CommunityEntry != m_Entries.end())
2261 {
2262 CommunityEntry->second.clear();
2263 }
2264}
2265
2266bool CExcludedCommunityTypeFilterList::Filtered(const char *pTypeName) const
2267{
2268 auto CommunityEntry = m_Entries.find(x: CCommunityId(m_pCommunityCache->CountryTypeFilterKey()));
2269 if(CommunityEntry == m_Entries.end())
2270 return false;
2271
2272 const auto &TypeEntries = CommunityEntry->second;
2273 return !IsSubsetEquals(vpLeft: m_pCommunityCache->SelectableTypes(), Right: TypeEntries) && TypeEntries.contains(x: CCommunityTypeName(pTypeName));
2274}
2275
2276bool CExcludedCommunityTypeFilterList::Empty() const
2277{
2278 auto CommunityEntry = m_Entries.find(x: CCommunityId(m_pCommunityCache->CountryTypeFilterKey()));
2279 return CommunityEntry == m_Entries.end() ||
2280 CommunityEntry->second.empty() ||
2281 IsSubsetEquals(vpLeft: m_pCommunityCache->SelectableTypes(), Right: CommunityEntry->second);
2282}
2283
2284void CExcludedCommunityTypeFilterList::Clean(const std::vector<CCommunity> &vAllowedCommunities)
2285{
2286 for(auto It = m_Entries.begin(); It != m_Entries.end();)
2287 {
2288 const bool AllEntry = str_comp(a: It->first.Id(), b: IServerBrowser::COMMUNITY_ALL) == 0;
2289 const bool Found = AllEntry || std::find_if(first: vAllowedCommunities.begin(), last: vAllowedCommunities.end(), pred: [&](const CCommunity &AllowedCommunity) {
2290 return str_comp(a: It->first.Id(), b: AllowedCommunity.Id()) == 0;
2291 }) != vAllowedCommunities.end();
2292 if(Found)
2293 {
2294 ++It;
2295 }
2296 else
2297 {
2298 It = m_Entries.erase(position: It);
2299 }
2300 }
2301
2302 for(const CCommunity &AllowedCommunity : vAllowedCommunities)
2303 {
2304 auto CommunityEntry = m_Entries.find(x: CCommunityId(AllowedCommunity.Id()));
2305 if(CommunityEntry != m_Entries.end())
2306 {
2307 auto &TypeEntries = CommunityEntry->second;
2308 for(auto It = TypeEntries.begin(); It != TypeEntries.end();)
2309 {
2310 if(AllowedCommunity.HasType(pTypeName: It->Name()))
2311 {
2312 ++It;
2313 }
2314 else
2315 {
2316 It = TypeEntries.erase(position: It);
2317 }
2318 }
2319 // Prevent filter that would exclude all allowed types
2320 if(TypeEntries.size() == AllowedCommunity.Types().size())
2321 {
2322 TypeEntries.clear();
2323 }
2324 }
2325 }
2326
2327 auto AllCommunityEntry = m_Entries.find(x: CCommunityId(IServerBrowser::COMMUNITY_ALL));
2328 if(AllCommunityEntry != m_Entries.end())
2329 {
2330 auto &TypeEntries = AllCommunityEntry->second;
2331 for(auto It = TypeEntries.begin(); It != TypeEntries.end();)
2332 {
2333 if(std::any_of(first: vAllowedCommunities.begin(), last: vAllowedCommunities.end(), pred: [&](const auto &Community) { return Community.HasType(It->Name()); }))
2334 {
2335 ++It;
2336 }
2337 else
2338 {
2339 It = TypeEntries.erase(position: It);
2340 }
2341 }
2342 // Prevent filter that would exclude all allowed types
2343 std::unordered_set<CCommunityCountryName> UniqueTypes;
2344 for(const CCommunity &AllowedCommunity : vAllowedCommunities)
2345 {
2346 for(const CCommunityType &Type : AllowedCommunity.Types())
2347 {
2348 UniqueTypes.emplace(args: Type.Name());
2349 }
2350 }
2351 if(TypeEntries.size() == UniqueTypes.size())
2352 {
2353 TypeEntries.clear();
2354 }
2355 }
2356}
2357
2358void CExcludedCommunityTypeFilterList::Save(IConfigManager *pConfigManager) const
2359{
2360 char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_TYPE_LENGTH];
2361 for(const auto &[Community, Types] : m_Entries)
2362 {
2363 for(const auto &Type : Types)
2364 {
2365 str_copy(dst&: aBuf, src: "add_excluded_type \"");
2366 str_append(dst&: aBuf, src: Community.Id());
2367 str_append(dst&: aBuf, src: "\" \"");
2368 str_append(dst&: aBuf, src: Type.Name());
2369 str_append(dst&: aBuf, src: "\"");
2370 pConfigManager->WriteLine(pLine: aBuf);
2371 }
2372 }
2373}
2374
2375void CServerBrowser::CleanFilters()
2376{
2377 // Keep filters if we failed to load any communities
2378 if(Communities().empty())
2379 return;
2380 FavoriteCommunitiesFilter().Clean(vAllowedCommunities: Communities());
2381 CommunitiesFilter().Clean(vAllowedCommunities: Communities());
2382 CountriesFilter().Clean(vAllowedCommunities: Communities());
2383 TypesFilter().Clean(vAllowedCommunities: Communities());
2384}
2385
2386bool CServerBrowser::IsRegistered(const NETADDR &Addr)
2387{
2388 const int NumServers = m_pHttp->NumServers();
2389 for(int i = 0; i < NumServers; i++)
2390 {
2391 const CServerInfo &Info = m_pHttp->Server(Index: i);
2392 for(int j = 0; j < Info.m_NumAddresses; j++)
2393 {
2394 if(net_addr_comp(a: &Info.m_aAddresses[j], b: &Addr) == 0)
2395 {
2396 return true;
2397 }
2398 }
2399 }
2400 return false;
2401}
2402
2403int CServerInfo::EstimateLatency(int Loc1, int Loc2)
2404{
2405 if(Loc1 == LOC_UNKNOWN || Loc2 == LOC_UNKNOWN)
2406 {
2407 return 999;
2408 }
2409 if(Loc1 != Loc2)
2410 {
2411 return 199;
2412 }
2413 return 99;
2414}
2415
2416ColorRGBA CServerInfo::GametypeColor(const char *pGametype)
2417{
2418 ColorHSLA HslaColor;
2419 if(str_comp(a: pGametype, b: "DM") == 0 || str_comp(a: pGametype, b: "TDM") == 0 || str_comp(a: pGametype, b: "CTF") == 0 || str_comp(a: pGametype, b: "LMS") == 0 || str_comp(a: pGametype, b: "LTS") == 0)
2420 HslaColor = ColorHSLA(0.33f, 1.0f, 0.75f);
2421 else if(str_find_nocase(haystack: pGametype, needle: "catch"))
2422 HslaColor = ColorHSLA(0.17f, 1.0f, 0.75f);
2423 else if(str_find_nocase(haystack: pGametype, needle: "dm") || str_find_nocase(haystack: pGametype, needle: "tdm") || str_find_nocase(haystack: pGametype, needle: "ctf") || str_find_nocase(haystack: pGametype, needle: "lms") || str_find_nocase(haystack: pGametype, needle: "lts"))
2424 {
2425 if(pGametype[0] == 'i' || pGametype[0] == 'g')
2426 HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f);
2427 else
2428 HslaColor = ColorHSLA(0.40f, 1.0f, 0.75f);
2429 }
2430 else if(str_find_nocase(haystack: pGametype, needle: "s-ddracex"))
2431 HslaColor = ColorHSLA(1.0f, 1.0f, 0.7f);
2432 else if(str_find_nocase(haystack: pGametype, needle: "f-ddrace") || str_find_nocase(haystack: pGametype, needle: "freeze"))
2433 HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f);
2434 else if(str_find_nocase(haystack: pGametype, needle: "fng"))
2435 HslaColor = ColorHSLA(0.83f, 1.0f, 0.75f);
2436 else if(str_find_nocase(haystack: pGametype, needle: "gores"))
2437 HslaColor = ColorHSLA(0.525f, 1.0f, 0.75f);
2438 else if(str_find_nocase(haystack: pGametype, needle: "BW"))
2439 HslaColor = ColorHSLA(0.05f, 1.0f, 0.75f);
2440 else if(str_find_nocase(haystack: pGametype, needle: "ddracenet") || str_find_nocase(haystack: pGametype, needle: "ddnet") || str_find_nocase(haystack: pGametype, needle: "0xf"))
2441 HslaColor = ColorHSLA(0.58f, 1.0f, 0.75f);
2442 else if(str_find_nocase(haystack: pGametype, needle: "ddrace") || str_find_nocase(haystack: pGametype, needle: "mkrace"))
2443 HslaColor = ColorHSLA(0.75f, 1.0f, 0.75f);
2444 else if(str_find_nocase(haystack: pGametype, needle: "race") || str_find_nocase(haystack: pGametype, needle: "fastcap"))
2445 HslaColor = ColorHSLA(0.46f, 1.0f, 0.75f);
2446 else
2447 HslaColor = ColorHSLA(1.0f, 1.0f, 1.0f);
2448 return color_cast<ColorRGBA>(hsl: HslaColor);
2449}
2450
2451bool CServerInfo::ParseLocation(int *pResult, const char *pString)
2452{
2453 *pResult = LOC_UNKNOWN;
2454 int Length = str_length(str: pString);
2455 if(Length < 2)
2456 {
2457 return true;
2458 }
2459 // ISO continent code. Allow antarctica, but treat it as unknown.
2460 static const char s_apLocations[NUM_LOCS][6] = {
2461 "an", // LOC_UNKNOWN
2462 "af", // LOC_AFRICA
2463 "as", // LOC_ASIA
2464 "oc", // LOC_AUSTRALIA
2465 "eu", // LOC_EUROPE
2466 "na", // LOC_NORTH_AMERICA
2467 "sa", // LOC_SOUTH_AMERICA
2468 "as:cn", // LOC_CHINA
2469 };
2470 for(int i = std::size(s_apLocations) - 1; i >= 0; i--)
2471 {
2472 if(str_startswith(str: pString, prefix: s_apLocations[i]))
2473 {
2474 *pResult = i;
2475 return false;
2476 }
2477 }
2478 return true;
2479}
2480