diff --git a/CMakeLists.txt b/CMakeLists.txt index 82e3686a5..01bcfdd84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1508,6 +1508,7 @@ set_src(ENGINE_INTERFACE GLOB src/engine server.h serverbrowser.h sound.h + sqlite.h steam.h storage.h textrender.h @@ -1571,7 +1572,8 @@ set_src(ENGINE_SHARED GLOB src/engine/shared protocol_ex_msgs.h ringbuffer.cpp ringbuffer.h - serverbrowser.cpp + serverinfo.cpp + serverinfo.h snapshot.cpp snapshot.h storage.cpp @@ -1645,6 +1647,7 @@ set(DEPS ${DEP_JSON} ${DEP_MD5} ${ZLIB_DEP}) # Libraries set(LIBS ${CRYPTO_LIBRARIES} + ${SQLite3_LIBRARIES} ${WEBSOCKETS_LIBRARIES} ${ZLIB_LIBRARIES} ${PLATFORM_LIBS} @@ -1729,8 +1732,13 @@ if(CLIENT) notifications.h serverbrowser.cpp serverbrowser.h + serverbrowser_http.cpp + serverbrowser_http.h + serverbrowser_ping_cache.cpp + serverbrowser_ping_cache.h sound.cpp sound.h + sqlite.cpp steam.cpp text.cpp updater.cpp @@ -2079,7 +2087,6 @@ endif() set(LIBS_SERVER ${LIBS} ${MYSQL_LIBRARIES} - ${SQLite3_LIBRARIES} ${TARGET_ANTIBOT} ${MINIUPNPC_LIBRARIES} # Add pthreads (on non-Windows) at the end, so that other libraries can depend @@ -2205,6 +2212,9 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST) netaddr.cpp packer.cpp prng.cpp + secure_random.cpp + serverbrowser.cpp + serverinfo.cpp sorted_array.cpp str.cpp strip_path_and_extension.cpp @@ -2218,6 +2228,15 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST) set(TESTS_EXTRA src/engine/client/blocklist_driver.cpp src/engine/client/blocklist_driver.h + src/engine/client/http.cpp + src/engine/client/http.h + src/engine/client/serverbrowser.cpp + src/engine/client/serverbrowser.h + src/engine/client/serverbrowser_http.cpp + src/engine/client/serverbrowser_http.h + src/engine/client/serverbrowser_ping_cache.cpp + src/engine/client/serverbrowser_ping_cache.h + src/engine/client/sqlite.cpp src/engine/server/name_ban.cpp src/engine/server/name_ban.h src/game/server/teehistorian.cpp @@ -2232,8 +2251,8 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST) $ ${DEPS} ) - target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${GTEST_LIBRARIES}) - target_include_directories(${TARGET_TESTRUNNER} PRIVATE ${GTEST_INCLUDE_DIRS}) + target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${CURL_LIBRARIES} ${GTEST_LIBRARIES}) + target_include_directories(${TARGET_TESTRUNNER} PRIVATE ${CURL_INCLUDE_DIRS} ${GTEST_INCLUDE_DIRS}) list(APPEND TARGETS_OWN ${TARGET_TESTRUNNER}) list(APPEND TARGETS_LINK ${TARGET_TESTRUNNER}) diff --git a/src/base/system.c b/src/base/system.c index 22cfb398a..d91ed5903 100644 --- a/src/base/system.c +++ b/src/base/system.c @@ -2205,6 +2205,19 @@ int fs_makedir(const char *path) #endif } +int fs_removedir(const char *path) +{ +#if defined(CONF_FAMILY_WINDOWS) + if(_rmdir(path) == 0) + return 0; + return -1; +#else + if(rmdir(path) == 0) + return 0; + return -1; +#endif +} + int fs_is_dir(const char *path) { #if defined(CONF_FAMILY_WINDOWS) @@ -2283,9 +2296,11 @@ int fs_parent_dir(char *path) int fs_remove(const char *filename) { - if(remove(filename) != 0) - return 1; - return 0; +#if defined(CONF_FAMILY_WINDOWS) + return _unlink(filename) != 0; +#else + return unlink(filename) != 0; +#endif } int fs_rename(const char *oldname, const char *newname) @@ -3608,6 +3623,34 @@ int secure_rand(void) return (int)(i % RAND_MAX); } +// From https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2. +static unsigned int find_next_power_of_two_minus_one(unsigned int n) +{ + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 4; + n |= n >> 16; + return n; +} + +int secure_rand_below(int below) +{ + unsigned int mask = find_next_power_of_two_minus_one(below); + dbg_assert(below > 0, "below must be positive"); + while(1) + { + unsigned int n; + secure_random_fill(&n, sizeof(n)); + n &= mask; + if((int)n < below) + { + return n; + } + } +} + #if defined(__cplusplus) } #endif diff --git a/src/base/system.h b/src/base/system.h index dd354e181..46798ee64 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -1638,6 +1638,21 @@ int fs_listdir_info(const char *dir, FS_LISTDIR_INFO_CALLBACK cb, int type, void */ int fs_makedir(const char *path); +/* + Function: fs_removedir + Removes a directory + + Parameters: + path - Directory to remove + + Returns: + Returns 0 on success. Negative value on failure. + + Remarks: + Cannot remove a non-empty directory. +*/ +int fs_removedir(const char *path); + /* Function: fs_makedir_rec_for Recursively create directories for a file @@ -1724,6 +1739,7 @@ int fs_parent_dir(char *path); Remarks: - The strings are treated as zero-terminated strings. + - Returns an error if the path specifies a directory name. */ int fs_remove(const char *filename); @@ -2193,6 +2209,16 @@ void secure_random_fill(void *bytes, unsigned length); */ int secure_rand(void); +/* + Function: secure_rand_below + Returns a random nonnegative integer below the given number, + with a uniform distribution. + + Parameters: + below - Upper limit (exclusive) of integers to return. +*/ +int secure_rand_below(int below); + #ifdef __cplusplus } #endif diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 073fcd49c..9a1240525 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -336,6 +335,12 @@ CClient::CClient() : m_Points = -1; m_CurrentServerInfoRequestTime = -1; + m_CurrentServerPingInfoType = -1; + m_CurrentServerPingBasicToken = -1; + m_CurrentServerPingToken = -1; + mem_zero(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid)); + m_CurrentServerCurrentPingTime = -1; + m_CurrentServerNextPingTime = -1; m_CurrentInput[0] = 0; m_CurrentInput[1] = 0; @@ -656,6 +661,7 @@ void CClient::EnterGame() OnEnterGame(); ServerInfoRequest(); // fresh one for timeout protection + m_CurrentServerNextPingTime = time_get() + time_freq() / 2; m_aTimeoutCodeSent[0] = false; m_aTimeoutCodeSent[1] = false; } @@ -739,6 +745,7 @@ void CClient::Connect(const char *pAddress, const char *pPassword) else str_copy(m_Password, pPassword, sizeof(m_Password)); + m_CanReceiveServerCapabilities = true; // Deregister Rcon commands from last connected server, might not have called // DisconnectWithReason if the server was shut down m_RconAuthed[0] = 0; @@ -776,13 +783,18 @@ void CClient::DisconnectWithReason(const char *pReason) // m_RconAuthed[0] = 0; - m_CanReceiveServerCapabilities = true; m_ServerSentCapabilities = false; m_UseTempRconCommands = 0; m_pConsole->DeregisterTempAll(); m_NetClient[CLIENT_MAIN].Disconnect(pReason); SetState(IClient::STATE_OFFLINE); m_pMap->Unload(); + m_CurrentServerPingInfoType = -1; + m_CurrentServerPingBasicToken = -1; + m_CurrentServerPingToken = -1; + mem_zero(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid)); + m_CurrentServerCurrentPingTime = -1; + m_CurrentServerNextPingTime = -1; // disable all downloads m_MapdownloadChunk = 0; @@ -1251,105 +1263,8 @@ const char *CClient::LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedS return pError; } -bool CClient::PlayerScoreNameLess(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1) -{ - 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(Score0 == -9999) - Score0 = INT_MIN; - if(Score1 == -9999) - Score1 = INT_MIN; - - if(Score0 > Score1) - return true; - if(Score0 < Score1) - return false; - return str_comp_nocase(p0.m_aName, p1.m_aName) < 0; -} - void CClient::ProcessConnlessPacket(CNetChunk *pPacket) { - //server count from master server - if(pPacket->m_DataSize == (int)sizeof(SERVERBROWSE_COUNT) + 2 && mem_comp(pPacket->m_pData, SERVERBROWSE_COUNT, sizeof(SERVERBROWSE_COUNT)) == 0) - { - unsigned char *pP = (unsigned char *)pPacket->m_pData; - pP += sizeof(SERVERBROWSE_COUNT); - int ServerCount = ((*pP) << 8) | *(pP + 1); - int ServerID = -1; - for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; i++) - { - if(!m_pMasterServer->IsValid(i)) - continue; - NETADDR tmp = m_pMasterServer->GetAddr(i); - if(net_addr_comp(&pPacket->m_Address, &tmp) == 0) - { - ServerID = i; - break; - } - } - if(ServerCount > -1 && ServerID != -1) - { - m_pMasterServer->SetCount(ServerID, ServerCount); - if(g_Config.m_Debug) - dbg_msg("mastercount", "server %d got %d servers", ServerID, ServerCount); - } - } - // server list from master server - if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_LIST) && - mem_comp(pPacket->m_pData, SERVERBROWSE_LIST, sizeof(SERVERBROWSE_LIST)) == 0) - { - // check for valid master server address - bool Valid = false; - for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; ++i) - { - if(m_pMasterServer->IsValid(i)) - { - NETADDR Addr = m_pMasterServer->GetAddr(i); - if(net_addr_comp(&pPacket->m_Address, &Addr) == 0) - { - Valid = true; - break; - } - } - } - if(!Valid) - return; - - int Size = pPacket->m_DataSize - sizeof(SERVERBROWSE_LIST); - int Num = Size / sizeof(CMastersrvAddr); - CMastersrvAddr *pAddrs = (CMastersrvAddr *)((char *)pPacket->m_pData + sizeof(SERVERBROWSE_LIST)); - for(int i = 0; i < Num; i++) - { - NETADDR Addr; - - static unsigned char s_IPV4Mapping[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF}; - - // copy address - if(!mem_comp(s_IPV4Mapping, pAddrs[i].m_aIp, sizeof(s_IPV4Mapping))) - { - mem_zero(&Addr, sizeof(Addr)); - Addr.type = NETTYPE_IPV4; - Addr.ip[0] = pAddrs[i].m_aIp[12]; - Addr.ip[1] = pAddrs[i].m_aIp[13]; - Addr.ip[2] = pAddrs[i].m_aIp[14]; - Addr.ip[3] = pAddrs[i].m_aIp[15]; - } - else - { - Addr.type = NETTYPE_IPV6; - mem_copy(Addr.ip, pAddrs[i].m_aIp, sizeof(Addr.ip)); - } - Addr.port = (pAddrs[i].m_aPort[0] << 8) | pAddrs[i].m_aPort[1]; - - m_ServerBrowser.Set(Addr, IServerBrowser::SET_MASTER_ADD, -1, 0x0); - } - } - // server info if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_INFO)) { @@ -1530,19 +1445,9 @@ void CClient::ProcessServerInfo(int RawType, NETADDR *pFrom, const void *pData, if(!Up.Error() || IgnoreError) { - std::sort(Info.m_aClients, Info.m_aClients + Info.m_NumReceivedClients, PlayerScoreNameLess); - if(!DuplicatedPacket && (!pEntry || !pEntry->m_GotInfo || SavedType >= pEntry->m_Info.m_Type)) { m_ServerBrowser.Set(*pFrom, IServerBrowser::SET_TOKEN, Token, &Info); - pEntry = m_ServerBrowser.Find(*pFrom); - - if(SavedType == SERVERINFO_VANILLA && Is64Player(&Info) && pEntry) - { - pEntry->m_Request64Legacy = true; - // Force a quick update. - m_ServerBrowser.RequestImpl64(pEntry->m_Addr, pEntry); - } } // Player info is irrelevant for the client (while connected), @@ -1561,6 +1466,30 @@ void CClient::ProcessServerInfo(int RawType, NETADDR *pFrom, const void *pData, m_CurrentServerInfo.m_NetAddr = m_ServerAddress; m_CurrentServerInfoRequestTime = -1; } + + bool ValidPong = false; + if(!m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && SavedType >= m_CurrentServerPingInfoType) + { + if(RawType == SERVERINFO_VANILLA) + { + ValidPong = Token == m_CurrentServerPingBasicToken; + } + else if(RawType == SERVERINFO_EXTENDED) + { + ValidPong = Token == m_CurrentServerPingToken; + } + } + if(ValidPong) + { + int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq(); + m_ServerBrowser.SetCurrentServerPing(m_ServerAddress, LatencyMs); + m_CurrentServerPingInfoType = SavedType; + m_CurrentServerCurrentPingTime = -1; + + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "got pong from current server, latency=%dms", LatencyMs); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf); + } } } @@ -1609,6 +1538,7 @@ static CServerCapabilities GetServerCapabilities(int Version, int Flags) } Result.m_ChatTimeoutCode = DDNet; Result.m_AnyPlayerFlag = DDNet; + Result.m_PingEx = false; if(Version >= 1) { Result.m_ChatTimeoutCode = Flags & SERVERCAPFLAG_CHATTIMEOUTCODE; @@ -1617,6 +1547,10 @@ static CServerCapabilities GetServerCapabilities(int Version, int Flags) { Result.m_AnyPlayerFlag = Flags & SERVERCAPFLAG_ANYPLAYERFLAG; } + if(Version >= 3) + { + Result.m_PingEx = Flags & SERVERCAPFLAG_PINGEX; + } return Result; } @@ -1814,6 +1748,35 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket) CMsgPacker Msg(NETMSG_PING_REPLY, true); SendMsg(&Msg, 0); } + else if(Msg == NETMSG_PINGEX) + { + CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID)); + if(Unpacker.Error()) + { + return; + } + CMsgPacker Msg(NETMSG_PONGEX, true); + Msg.AddRaw(pID, sizeof(*pID)); + SendMsg(&Msg, MSGFLAG_FLUSH); + } + else if(Msg == NETMSG_PONGEX) + { + CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID)); + if(Unpacker.Error()) + { + return; + } + if(m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && *pID == m_CurrentServerPingUuid) + { + int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq(); + m_ServerBrowser.SetCurrentServerPing(m_ServerAddress, LatencyMs); + m_CurrentServerCurrentPingTime = -1; + + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "got pong from current server, latency=%dms", LatencyMs); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf); + } + } else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_ADD) { if(!g_Config.m_ClDummy) @@ -2842,9 +2805,36 @@ void CClient::Update() m_CurrentServerInfoRequestTime >= 0 && time_get() > m_CurrentServerInfoRequestTime) { - m_ServerBrowser.RequestCurrentServer(m_ServerAddress); + m_ServerBrowser.RequestCurrentServer(m_ServerAddress, nullptr, nullptr); m_CurrentServerInfoRequestTime = time_get() + time_freq() * 2; } + + // periodically ping server + if(State() == IClient::STATE_ONLINE && + m_CurrentServerNextPingTime >= 0 && + time_get() > m_CurrentServerNextPingTime) + { + int64 Now = time_get(); + int64 Freq = time_freq(); + + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "pinging current server%s", !m_ServerCapabilities.m_PingEx ? ", using fallback via server info" : ""); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); + + m_CurrentServerPingUuid = RandomUuid(); + if(!m_ServerCapabilities.m_PingEx) + { + m_ServerBrowser.RequestCurrentServer(m_ServerAddress, &m_CurrentServerPingBasicToken, &m_CurrentServerPingToken); + } + else + { + CMsgPacker Msg(NETMSG_PINGEX, true); + Msg.AddRaw(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid)); + SendMsg(&Msg, MSGFLAG_FLUSH); + } + m_CurrentServerCurrentPingTime = Now; + m_CurrentServerNextPingTime = Now + 600 * Freq; // ping every 10 minutes + } } m_LastDummy = (bool)g_Config.m_ClDummy; @@ -2930,9 +2920,6 @@ void CClient::Update() } } - // update the maser server registry - MasterServer()->Update(); - // update the server browser m_ServerBrowser.Update(m_ResortServerBrowser); m_ResortServerBrowser = false; @@ -2983,7 +2970,6 @@ void CClient::InitInterfaces() m_pGameClient = Kernel()->RequestInterface(); m_pInput = Kernel()->RequestInterface(); m_pMap = Kernel()->RequestInterface(); - m_pMasterServer = Kernel()->RequestInterface(); m_pConfigManager = Kernel()->RequestInterface(); m_pConfig = m_pConfigManager->Values(); #if defined(CONF_AUTOUPDATE) @@ -3099,9 +3085,6 @@ void CClient::Run() // init the input Input()->Init(); - // start refreshing addresses while we load - MasterServer()->RefreshAddresses(m_NetClient[CLIENT_MAIN].NetType()); - // init the editor m_pEditor->Init(); @@ -3654,8 +3637,18 @@ void CClient::Con_AddFavorite(IConsole::IResult *pResult, void *pUserData) { CClient *pSelf = (CClient *)pUserData; NETADDR Addr; - if(net_addr_from_str(&Addr, pResult->GetString(0)) == 0) - pSelf->m_ServerBrowser.AddFavorite(Addr); + if(net_addr_from_str(&Addr, pResult->GetString(0)) != 0) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "invalid address '%s'", pResult->GetString(0)); + pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf); + return; + } + pSelf->m_ServerBrowser.AddFavorite(Addr); + if(pResult->NumArguments() > 1 && str_find(pResult->GetString(1), "allow_ping")) + { + pSelf->m_ServerBrowser.FavoriteAllowPing(Addr, true); + } } void CClient::Con_RemoveFavorite(IConsole::IResult *pResult, void *pUserData) @@ -4201,7 +4194,7 @@ void CClient::RegisterCommands() m_pConsole->Register("record", "?r[file]", CFGFLAG_CLIENT, Con_Record, this, "Record to the file"); m_pConsole->Register("stoprecord", "", CFGFLAG_CLIENT, Con_StopRecord, this, "Stop recording"); m_pConsole->Register("add_demomarker", "", CFGFLAG_CLIENT, Con_AddDemoMarker, this, "Add demo timeline marker"); - m_pConsole->Register("add_favorite", "r[host|ip]", CFGFLAG_CLIENT, Con_AddFavorite, this, "Add a server as a favorite"); + m_pConsole->Register("add_favorite", "s[host|ip] ?s['allow_ping']", CFGFLAG_CLIENT, Con_AddFavorite, this, "Add a server as a favorite"); m_pConsole->Register("remove_favorite", "r[host|ip]", CFGFLAG_CLIENT, Con_RemoveFavorite, this, "Remove a server from favorites"); m_pConsole->Register("demo_slice_start", "", CFGFLAG_CLIENT, Con_DemoSliceBegin, this, ""); m_pConsole->Register("demo_slice_end", "", CFGFLAG_CLIENT, Con_DemoSliceEnd, this, ""); @@ -4307,7 +4300,7 @@ int main(int argc, const char **argv) // ignore_convention pClient->RegisterInterfaces(); // create the components - IEngine *pEngine = CreateEngine("DDNet", Silent, 1); + IEngine *pEngine = CreateEngine("DDNet", Silent, 2); IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT); IStorage *pStorage = CreateStorage("Teeworlds", IStorage::STORAGETYPE_CLIENT, argc, argv); // ignore_convention IConfigManager *pConfigManager = CreateConfigManager(); @@ -4315,7 +4308,6 @@ int main(int argc, const char **argv) // ignore_convention IEngineInput *pEngineInput = CreateEngineInput(); IEngineTextRender *pEngineTextRender = CreateEngineTextRender(); IEngineMap *pEngineMap = CreateEngineMap(); - IEngineMasterServer *pEngineMasterServer = CreateEngineMasterServer(); IDiscord *pDiscord = CreateDiscord(); ISteam *pSteam = CreateSteam(); @@ -4344,9 +4336,6 @@ int main(int argc, const char **argv) // ignore_convention RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineMap); // IEngineMap RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineMap), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineMasterServer); // IEngineMasterServer - RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineMasterServer), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateEditor(), false); RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateGameClient(), false); RegisterFail = RegisterFail || !pKernel->RegisterInterface(pStorage); @@ -4365,8 +4354,6 @@ int main(int argc, const char **argv) // ignore_convention pEngine->Init(); pConfigManager->Init(); pConsole->Init(); - pEngineMasterServer->Init(); - pEngineMasterServer->Load(); // register all console commands pClient->RegisterCommands(); diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 3b110a3b2..630248b14 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -80,6 +80,7 @@ class CServerCapabilities public: bool m_ChatTimeoutCode; bool m_AnyPlayerFlag; + bool m_PingEx; }; class CClient : public IClient, public CDemoPlayer::IListener @@ -99,7 +100,6 @@ class CClient : public IClient, public CDemoPlayer::IListener IUpdater *m_pUpdater; IDiscord *m_pDiscord; ISteam *m_pSteam; - IEngineMasterServer *m_pMasterServer; enum { @@ -243,6 +243,13 @@ class CClient : public IClient, public CDemoPlayer::IListener class CServerInfo m_CurrentServerInfo; int64 m_CurrentServerInfoRequestTime; // >= 0 should request, == -1 got info + int m_CurrentServerPingInfoType; + int m_CurrentServerPingBasicToken; + int m_CurrentServerPingToken; + CUuid m_CurrentServerPingUuid; + int64 m_CurrentServerCurrentPingTime; // >= 0 request running + int64 m_CurrentServerNextPingTime; // >= 0 should request + // version info struct CVersionInfo { @@ -276,7 +283,6 @@ public: IEngineInput *Input() { return m_pInput; } IEngineSound *Sound() { return m_pSound; } IGameClient *GameClient() { return m_pGameClient; } - IEngineMasterServer *MasterServer() { return m_pMasterServer; } IConfigManager *ConfigManager() { return m_pConfigManager; } CConfig *Config() { return m_pConfig; } IStorage *Storage() { return m_pStorage; } @@ -361,8 +367,6 @@ public: const char *LoadMap(const char *pName, const char *pFilename, SHA256_DIGEST *pWantedSha256, unsigned WantedCrc); const char *LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedSha256, int WantedCrc); - static bool PlayerScoreNameLess(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1); - void ProcessConnlessPacket(CNetChunk *pPacket); void ProcessServerInfo(int Type, NETADDR *pFrom, const void *pData, int DataSize); void ProcessServerPacket(CNetChunk *pPacket); diff --git a/src/engine/client/http.cpp b/src/engine/client/http.cpp index e9631da31..315cbb4d1 100644 --- a/src/engine/client/http.cpp +++ b/src/engine/client/http.cpp @@ -186,6 +186,22 @@ int CRequest::ProgressCallback(void *pUser, double DlTotal, double DlCurr, doubl return pTask->m_Abort ? -1 : 0; } +CHead::CHead(const char *pUrl, CTimeout Timeout) : + CRequest(pUrl, Timeout) +{ +} + +CHead::~CHead() +{ +} + +bool CHead::AfterInit(void *pCurl) +{ + CURL *pHandle = pCurl; + curl_easy_setopt(pHandle, CURLOPT_NOBODY, 1L); + return true; +} + CGet::CGet(const char *pUrl, CTimeout Timeout) : CRequest(pUrl, Timeout), m_BufferSize(0), diff --git a/src/engine/client/http.h b/src/engine/client/http.h index a29514499..93ac29e20 100644 --- a/src/engine/client/http.h +++ b/src/engine/client/http.h @@ -66,6 +66,16 @@ public: void Abort() { m_Abort = true; } }; +class CHead : public CRequest +{ + virtual size_t OnData(char *pData, size_t DataSize) { return DataSize; } + virtual bool AfterInit(void *pCurl); + +public: + CHead(const char *pUrl, CTimeout Timeout); + ~CHead(); +}; + class CGet : public CRequest { virtual size_t OnData(char *pData, size_t DataSize); diff --git a/src/engine/client/serverbrowser.cpp b/src/engine/client/serverbrowser.cpp index 6c8a73c01..4ea656351 100644 --- a/src/engine/client/serverbrowser.cpp +++ b/src/engine/client/serverbrowser.cpp @@ -1,6 +1,12 @@ /* (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 // sort TODO: remove this +#include "serverbrowser.h" + +#include "serverbrowser_http.h" +#include "serverbrowser_ping_cache.h" + +#include +#include #include #include @@ -11,18 +17,18 @@ #include #include #include +#include #include #include +#include #include -#include #include #include #include -#include "serverbrowser.h" class SortWrap { typedef bool (CServerBrowser::*SortFunc)(int, int) const; @@ -37,7 +43,6 @@ public: CServerBrowser::CServerBrowser() { - m_pMasterServer = 0; m_ppServerlist = 0; m_pSortedServerlist = 0; @@ -49,8 +54,6 @@ CServerBrowser::CServerBrowser() m_pLastReqServer = 0; m_NumRequests = 0; - m_NeedRefresh = 0; - m_NumSortedServers = 0; m_NumSortedServersCapacity = 0; m_NumServers = 0; @@ -80,18 +83,89 @@ CServerBrowser::~CServerBrowser() if(m_pDDNetInfo) 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, sizeof(m_aNetVersion)); - m_pMasterServer = Kernel()->RequestInterface(); m_pConsole = Kernel()->RequestInterface(); + m_pEngine = Kernel()->RequestInterface(); m_pFriends = Kernel()->RequestInterface(); + m_pStorage = Kernel()->RequestInterface(); IConfigManager *pConfigManager = Kernel()->RequestInterface(); if(pConfigManager) pConfigManager->RegisterCallback(ConfigSaveCallback, this); + m_pPingCache = CreateServerBrowserPingCache(m_pConsole, m_pStorage); + + RegisterCommands(); +} + +void CServerBrowser::RegisterCommands() +{ + 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::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = (CServerBrowser *)pUserData; + + std::vector aSortedServers; + // Sort servers by IP address, ignoring port. + class CAddrComparer + { + public: + CServerBrowser *m_pThis; + bool operator()(int i, int j) + { + NETADDR Addr1 = m_pThis->m_ppServerlist[i]->m_Addr; + NETADDR Addr2 = m_pThis->m_ppServerlist[j]->m_Addr; + Addr1.port = 0; + Addr2.port = 0; + return net_addr_comp(&Addr1, &Addr2) < 0; + } + }; + aSortedServers.reserve(pThis->m_NumServers); + for(int i = 0; i < pThis->m_NumServers; i++) + { + aSortedServers.push_back(i); + } + std::sort(aSortedServers.begin(), aSortedServers.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)aSortedServers.size(); i++) + { + NETADDR NextAddr; + if(i < (int)aSortedServers.size()) + { + NextAddr = pThis->m_ppServerlist[aSortedServers[i]]->m_Addr; + NextAddr.port = 0; + } + bool New = Start == -1 || i == (int)aSortedServers.size() || net_addr_comp(&Addr, &NextAddr) != 0; + if(Start != -1 && New) + { + int Chosen = Start + secure_rand_below(i - Start); + CServerEntry *pChosen = pThis->m_ppServerlist[aSortedServers[Chosen]]; + pChosen->m_RequestIgnoreInfo = true; + pThis->QueueRequest(pChosen); + char aAddr[NETADDR_MAXSTRSIZE]; + net_addr_str(&pChosen->m_Addr, aAddr, sizeof(aAddr), true); + dbg_msg("serverbrowse/dbg", "queuing ping request for %s", aAddr); + } + if(i < (int)aSortedServers.size() && New) + { + Start = i; + Addr = NextAddr; + } + } } const CServerInfo *CServerBrowser::SortedGet(int Index) const @@ -426,29 +500,58 @@ void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) pEntry->m_Info.m_Favorite = Fav; pEntry->m_Info.m_Official = Off; pEntry->m_Info.m_NetAddr = pEntry->m_Addr; + net_addr_str(&pEntry->m_Info.m_NetAddr, pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aAddress), 1); - // all these are just for nice compatibility - if(pEntry->m_Info.m_aGameType[0] == '0' && pEntry->m_Info.m_aGameType[1] == 0) - str_copy(pEntry->m_Info.m_aGameType, "DM", sizeof(pEntry->m_Info.m_aGameType)); - else if(pEntry->m_Info.m_aGameType[0] == '1' && pEntry->m_Info.m_aGameType[1] == 0) - str_copy(pEntry->m_Info.m_aGameType, "TDM", sizeof(pEntry->m_Info.m_aGameType)); - else if(pEntry->m_Info.m_aGameType[0] == '2' && pEntry->m_Info.m_aGameType[1] == 0) - str_copy(pEntry->m_Info.m_aGameType, "CTF", sizeof(pEntry->m_Info.m_aGameType)); - - /*if(!request) + class CPlayerScoreNameLess { - pEntry->m_Info.latency = (time_get()-pEntry->request_time)*1000/time_freq(); - RemoveRequest(pEntry); - }*/ + public: + bool operator()(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1) + { + 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(Score0 == -9999) + Score0 = INT_MIN; + if(Score1 == -9999) + Score1 = INT_MIN; + + if(Score0 > Score1) + return true; + if(Score0 < Score1) + return false; + 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_GotInfo = 1; } +void CServerBrowser::SetLatency(NETADDR Addr, int Latency) +{ + Addr.port = 0; + for(CServerEntry *pEntry = m_aServerlistIp[Addr.ip[0]]; pEntry; pEntry = pEntry->m_pNextIp) + { + NETADDR Other = pEntry->m_Addr; + Other.port = 0; + if(net_addr_comp(&Addr, &Other) == 0) + { + pEntry->m_Info.m_Latency = Latency; + pEntry->m_Info.m_LatencyIsEstimated = false; + } + } + m_pPingCache->CachePing(Addr, Latency); +} + CServerBrowser::CServerEntry *CServerBrowser::Add(const NETADDR &Addr) { int Hash = Addr.ip[0]; CServerEntry *pEntry = 0; - int i; // create new pEntry pEntry = (CServerEntry *)m_ServerlistHeap.Allocate(sizeof(CServerEntry)); @@ -464,14 +567,7 @@ CServerBrowser::CServerEntry *CServerBrowser::Add(const NETADDR &Addr) str_copy(pEntry->m_Info.m_aName, pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aName)); // check if it's a favorite - for(i = 0; i < m_NumFavoriteServers; i++) - { - if(net_addr_comp(&Addr, &m_aFavoriteServers[i]) == 0) - { - pEntry->m_Info.m_Favorite = true; - break; - } - } + pEntry->m_Info.m_Favorite = IsFavorite(Addr); // check if it's an official server for(auto &Network : m_aNetworks) @@ -520,7 +616,6 @@ void CServerBrowser::Set(const NETADDR &Addr, int Type, int Token, const CServer { if(m_ServerlistType != IServerBrowser::TYPE_INTERNET) return; - m_LastPacketTick = 0; if(!Find(Addr)) { pEntry = Add(Addr); @@ -560,6 +655,18 @@ void CServerBrowser::Set(const NETADDR &Addr, int Type, int Token, const CServer QueueRequest(pEntry); } } + else if(Type == IServerBrowser::SET_HTTPINFO) + { + if(!pEntry) + { + pEntry = Add(Addr); + } + if(pEntry) + { + SetInfo(pEntry, *pInfo); + pEntry->m_RequestIgnoreInfo = true; + } + } else if(Type == IServerBrowser::SET_TOKEN) { int BasicToken = Token; @@ -605,13 +712,47 @@ void CServerBrowser::Set(const NETADDR &Addr, int Type, int Token, const CServer } } - SetInfo(pEntry, *pInfo); 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); + if(pInfo->m_Type == SERVERINFO_VANILLA && Is64Player(pInfo)) + { + pEntry->m_Request64Legacy = true; + // Force a quick update. + RequestImpl64(pEntry->m_Addr, pEntry); + } + } else if(pEntry->m_RequestTime > 0) { - pEntry->m_Info.m_Latency = minimum(static_cast((time_get() - pEntry->m_RequestTime) * 1000 / time_freq()), 999); + 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("serverbrowse/dbg", "received ping response from %s", aAddr); + SetLatency(Addr, Latency); + } pEntry->m_RequestTime = -1; // Request has been answered + + if(!pEntry->m_RequestIgnoreInfo) + { + if(pInfo->m_Type == SERVERINFO_VANILLA && Is64Player(pInfo)) + { + pEntry->m_Request64Legacy = true; + // Force a quick update. + RequestImpl64(pEntry->m_Addr, pEntry); + } + } } RemoveRequest(pEntry); } @@ -668,78 +809,15 @@ void CServerBrowser::Refresh(int Type) if(g_Config.m_Debug) m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client_srvbrowse", "broadcasting for servers"); } - else if(Type == IServerBrowser::TYPE_INTERNET) - m_NeedRefresh = 1; - else if(Type == IServerBrowser::TYPE_FAVORITES) + else if(Type == IServerBrowser::TYPE_FAVORITES || Type == IServerBrowser::TYPE_INTERNET || Type == IServerBrowser::TYPE_DDNET || Type == IServerBrowser::TYPE_KOG) { - for(int i = 0; i < m_NumFavoriteServers; i++) - Set(m_aFavoriteServers[i], IServerBrowser::SET_FAV_ADD, -1, 0); - } - else if(Type == IServerBrowser::TYPE_DDNET) - { - // remove unknown elements of exclude list - CountryFilterClean(NETWORK_DDNET); - TypeFilterClean(NETWORK_DDNET); - - int MaxServers = 0; - for(int i = 0; i < m_aNetworks[NETWORK_DDNET].m_NumCountries; i++) - { - CNetworkCountry *pCntr = &m_aNetworks[NETWORK_DDNET].m_aCountries[i]; - MaxServers = maximum(MaxServers, pCntr->m_NumServers); - } - - for(int g = 0; g < MaxServers; g++) - { - for(int i = 0; i < m_aNetworks[NETWORK_DDNET].m_NumCountries; i++) - { - CNetworkCountry *pCntr = &m_aNetworks[NETWORK_DDNET].m_aCountries[i]; - - // check for filter - if(DDNetFiltered(g_Config.m_BrFilterExcludeCountries, pCntr->m_aName)) - continue; - - if(g >= pCntr->m_NumServers) - continue; - - if(!DDNetFiltered(g_Config.m_BrFilterExcludeTypes, pCntr->m_aTypes[g])) - Set(pCntr->m_aServers[g], IServerBrowser::SET_DDNET_ADD, -1, 0); - } - } - } - else if(Type == IServerBrowser::TYPE_KOG) - { - // remove unknown elements of exclude list - CountryFilterClean(NETWORK_KOG); - TypeFilterClean(NETWORK_KOG); - - int MaxServers = 0; - for(int i = 0; i < m_aNetworks[NETWORK_KOG].m_NumCountries; i++) - { - CNetworkCountry *pCntr = &m_aNetworks[NETWORK_KOG].m_aCountries[i]; - MaxServers = maximum(MaxServers, pCntr->m_NumServers); - } - - for(int g = 0; g < MaxServers; g++) - { - for(int i = 0; i < m_aNetworks[NETWORK_KOG].m_NumCountries; i++) - { - CNetworkCountry *pCntr = &m_aNetworks[NETWORK_KOG].m_aCountries[i]; - - // check for filter - if(DDNetFiltered(g_Config.m_BrFilterExcludeCountriesKoG, pCntr->m_aName)) - continue; - - if(g >= pCntr->m_NumServers) - continue; - - if(!DDNetFiltered(g_Config.m_BrFilterExcludeTypesKoG, pCntr->m_aTypes[g])) - Set(pCntr->m_aServers[g], IServerBrowser::SET_KOG_ADD, -1, 0); - } - } + m_pHttp->Refresh(); + m_pPingCache->Load(); + m_RefreshingHttp = true; } } -void CServerBrowser::RequestImpl(const NETADDR &Addr, CServerEntry *pEntry) const +void CServerBrowser::RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int *pBasicToken, int *pToken) const { unsigned char Buffer[sizeof(SERVERBROWSE_GETINFO) + 1]; CNetChunk Packet; @@ -754,6 +832,14 @@ void CServerBrowser::RequestImpl(const NETADDR &Addr, CServerEntry *pEntry) cons } int Token = GenerateToken(Addr); + if(pToken) + { + *pToken = Token; + } + if(pBasicToken) + { + *pBasicToken = GetBasicToken(Token); + } mem_copy(Buffer, SERVERBROWSE_GETINFO, sizeof(SERVERBROWSE_GETINFO)); Buffer[sizeof(SERVERBROWSE_GETINFO)] = GetBasicToken(Token); @@ -802,124 +888,289 @@ void CServerBrowser::RequestImpl64(const NETADDR &Addr, CServerEntry *pEntry) co pEntry->m_RequestTime = time_get(); } -void CServerBrowser::RequestCurrentServer(const NETADDR &Addr) const +void CServerBrowser::RequestCurrentServer(const NETADDR &Addr, int *pBasicToken, int *pToken) const { - RequestImpl(Addr, 0); + RequestImpl(Addr, nullptr, pBasicToken, pToken); +} + +void CServerBrowser::SetCurrentServerPing(const NETADDR &Addr, int Ping) +{ + SetLatency(Addr, std::min(Ping, 999)); +} + +void ServerBrowserFillEstimatedLatency(int OwnLocation, const IServerBrowserPingCache::CEntry *pEntries, int NumEntries, int *pIndex, NETADDR Addr, CServerInfo *pInfo) +{ + Addr.port = 0; + while(*pIndex < NumEntries && net_addr_comp(&pEntries[*pIndex].m_Addr, &Addr) < 0) + { + *pIndex += 1; + } + if(*pIndex >= NumEntries || net_addr_comp(&pEntries[*pIndex].m_Addr, &Addr) != 0) + { + pInfo->m_LatencyIsEstimated = true; + pInfo->m_Latency = CServerInfo::EstimateLatency(OwnLocation, pInfo->m_Location); + return; + } + pInfo->m_LatencyIsEstimated = false; + pInfo->m_Latency = pEntries[*pIndex].m_Ping; +} + +void CServerBrowser::UpdateFromHttp() +{ + const IServerBrowserPingCache::CEntry *pPingEntries; + int NumPingEntries; + m_pPingCache->GetPingCache(&pPingEntries, &NumPingEntries); + 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, "serverbrowse", aBuf); + } + } + + int NumServers = m_pHttp->NumServers(); + int NumLegacyServers = m_pHttp->NumLegacyServers(); + if(m_ServerlistType != IServerBrowser::TYPE_INTERNET) + { + class CWantedAddr + { + public: + NETADDR m_Addr; + bool m_FallbackToPing; + bool m_Got; + }; + std::vector aWantedAddresses; + int LegacySetType; + if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES) + { + for(int i = 0; i < m_NumFavoriteServers; i++) + { + aWantedAddresses.push_back(CWantedAddr{m_aFavoriteServers[i], m_aFavoriteServersAllowPing[i], false}); + } + LegacySetType = IServerBrowser::SET_FAV_ADD; + } + else + { + int Network; + char *pExcludeCountries; + char *pExcludeTypes; + switch(m_ServerlistType) + { + case IServerBrowser::TYPE_DDNET: + Network = NETWORK_DDNET; + LegacySetType = IServerBrowser::SET_DDNET_ADD; + pExcludeCountries = g_Config.m_BrFilterExcludeCountries; + pExcludeTypes = g_Config.m_BrFilterExcludeTypes; + break; + case IServerBrowser::TYPE_KOG: + Network = NETWORK_KOG; + LegacySetType = IServerBrowser::SET_KOG_ADD; + pExcludeCountries = g_Config.m_BrFilterExcludeCountriesKoG; + pExcludeTypes = g_Config.m_BrFilterExcludeTypesKoG; + break; + default: + dbg_assert(0, "invalid network"); + return; + } + // remove unknown elements of exclude list + CountryFilterClean(Network); + TypeFilterClean(Network); + + int MaxServers = 0; + for(int i = 0; i < m_aNetworks[Network].m_NumCountries; i++) + { + CNetworkCountry *pCntr = &m_aNetworks[Network].m_aCountries[i]; + MaxServers = maximum(MaxServers, pCntr->m_NumServers); + } + + for(int g = 0; g < MaxServers; g++) + { + for(int i = 0; i < m_aNetworks[Network].m_NumCountries; i++) + { + CNetworkCountry *pCntr = &m_aNetworks[Network].m_aCountries[i]; + + // check for filter + if(DDNetFiltered(pExcludeCountries, pCntr->m_aName)) + continue; + + if(g >= pCntr->m_NumServers) + continue; + + if(DDNetFiltered(pExcludeTypes, pCntr->m_aTypes[g])) + continue; + aWantedAddresses.push_back(CWantedAddr{pCntr->m_aServers[g], false, false}); + } + } + } + std::vector aSortedServers; + std::vector aSortedLegacyServers; + aSortedServers.reserve(NumServers); + for(int i = 0; i < NumServers; i++) + { + aSortedServers.push_back(i); + } + aSortedLegacyServers.reserve(NumLegacyServers); + for(int i = 0; i < NumLegacyServers; i++) + { + aSortedLegacyServers.push_back(i); + } + + class CWantedAddrComparer + { + public: + bool operator()(const CWantedAddr &a, const CWantedAddr &b) + { + return net_addr_comp(&a.m_Addr, &b.m_Addr) < 0; + } + }; + class CAddrComparer + { + public: + IServerBrowserHttp *m_pHttp; + bool operator()(int i, int j) + { + return net_addr_comp(&m_pHttp->ServerAddress(i), &m_pHttp->ServerAddress(j)) < 0; + } + }; + class CLegacyAddrComparer + { + public: + IServerBrowserHttp *m_pHttp; + bool operator()(int i, int j) + { + return net_addr_comp(&m_pHttp->LegacyServer(i), &m_pHttp->LegacyServer(j)) < 0; + } + }; + + std::sort(aWantedAddresses.begin(), aWantedAddresses.end(), CWantedAddrComparer()); + std::sort(aSortedServers.begin(), aSortedServers.end(), CAddrComparer{m_pHttp}); + std::sort(aSortedLegacyServers.begin(), aSortedLegacyServers.end(), CLegacyAddrComparer{m_pHttp}); + + unsigned i = 0; + unsigned j = 0; + int p = 0; + while(i < aWantedAddresses.size() && j < aSortedServers.size()) + { + int Cmp = net_addr_comp(&aWantedAddresses[i].m_Addr, &m_pHttp->ServerAddress(aSortedServers[j])); + if(Cmp != 0) + { + if(Cmp < 0) + { + i++; + } + else + { + j++; + } + continue; + } + aWantedAddresses[i].m_Got = true; + NETADDR Addr; + CServerInfo Info; + m_pHttp->Server(aSortedServers[j], &Addr, &Info); + ServerBrowserFillEstimatedLatency(OwnLocation, pPingEntries, NumPingEntries, &p, Addr, &Info); + Info.m_HasRank = HasRank(Info.m_aMap); + Set(Addr, IServerBrowser::SET_HTTPINFO, -1, &Info); + i++; + j++; + } + i = 0; + j = 0; + while(i < aWantedAddresses.size() && j < aSortedLegacyServers.size()) + { + int Cmp = net_addr_comp(&aWantedAddresses[i].m_Addr, &m_pHttp->LegacyServer(aSortedLegacyServers[j])); + if(Cmp != 0) + { + if(Cmp < 0) + { + i++; + } + else + { + j++; + } + continue; + } + aWantedAddresses[i].m_Got = true; + Set(m_pHttp->LegacyServer(aSortedLegacyServers[j]), LegacySetType, -1, nullptr); + i++; + j++; + } + for(const CWantedAddr &Wanted : aWantedAddresses) + { + if(!Wanted.m_Got) + { + if(Wanted.m_FallbackToPing) + { + Set(Wanted.m_Addr, LegacySetType, -1, nullptr); + } + else + { + // Also add favorites we're not allowed to ping. + if(LegacySetType == IServerBrowser::SET_FAV_ADD && !Find(Wanted.m_Addr)) + { + Add(Wanted.m_Addr); + } + } + } + } + return; + } + int p = 0; + for(int i = 0; i < NumServers; i++) + { + NETADDR Addr; + CServerInfo Info; + m_pHttp->Server(i, &Addr, &Info); + ServerBrowserFillEstimatedLatency(OwnLocation, pPingEntries, NumPingEntries, &p, Addr, &Info); + Info.m_HasRank = HasRank(Info.m_aMap); + Set(Addr, IServerBrowser::SET_HTTPINFO, -1, &Info); + } + for(int i = 0; i < NumLegacyServers; i++) + { + NETADDR Addr = m_pHttp->LegacyServer(i); + Set(Addr, IServerBrowser::SET_MASTER_ADD, -1, nullptr); + } } void CServerBrowser::Update(bool ForceResort) { int64 Timeout = time_freq(); int64 Now = time_get(); - int Count; - CServerEntry *pEntry, *pNext; - // do server list requests - if(m_NeedRefresh && !m_pMasterServer->IsRefreshing()) + if(!m_pHttp) { - NETADDR Addr; - CNetChunk Packet; - int i = 0; - - m_NeedRefresh = 0; - m_MasterServerCount = -1; - mem_zero(&Packet, sizeof(Packet)); - Packet.m_ClientID = -1; - Packet.m_Flags = NETSENDFLAG_CONNLESS; - Packet.m_DataSize = sizeof(SERVERBROWSE_GETCOUNT); - Packet.m_pData = SERVERBROWSE_GETCOUNT; - - for(i = 0; i < IMasterServer::MAX_MASTERSERVERS; i++) - { - if(!m_pMasterServer->IsValid(i)) - continue; - - Addr = m_pMasterServer->GetAddr(i); - m_pMasterServer->SetCount(i, -1); - Packet.m_Address = Addr; - m_pNetClient->Send(&Packet); - if(g_Config.m_Debug) - { - dbg_msg("client_srvbrowse", "count-request sent to %d", i); - } - } + m_pHttp = CreateServerBrowserHttp(m_pEngine, m_pConsole, m_pStorage, g_Config.m_BrCachedBestServerinfoUrl); } - //Check if all server counts arrived - if(m_MasterServerCount == -1) + const char *pHttpBestUrl; + if(!m_pHttp->GetBestUrl(&pHttpBestUrl) && pHttpBestUrl != m_pHttpPrevBestUrl) { - m_MasterServerCount = 0; - for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; i++) - { - if(!m_pMasterServer->IsValid(i)) - continue; - int Count = m_pMasterServer->GetCount(i); - if(Count == -1) - { - /* ignore Server - m_MasterServerCount = -1; - return; - // we don't have the required server information - */ - } - else - m_MasterServerCount += Count; - } - //request Server-List - NETADDR Addr; - CNetChunk Packet; - mem_zero(&Packet, sizeof(Packet)); - Packet.m_ClientID = -1; - Packet.m_Flags = NETSENDFLAG_CONNLESS; - Packet.m_DataSize = sizeof(SERVERBROWSE_GETLIST); - Packet.m_pData = SERVERBROWSE_GETLIST; + str_copy(g_Config.m_BrCachedBestServerinfoUrl, pHttpBestUrl, sizeof(g_Config.m_BrCachedBestServerinfoUrl)); + m_pHttpPrevBestUrl = pHttpBestUrl; + } - for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; i++) - { - if(!m_pMasterServer->IsValid(i)) - continue; + m_pHttp->Update(); - Addr = m_pMasterServer->GetAddr(i); - Packet.m_Address = Addr; - m_pNetClient->Send(&Packet); - } - if(g_Config.m_Debug) - { - dbg_msg("client_srvbrowse", "servercount: %d, requesting server list", m_MasterServerCount); - } - m_LastPacketTick = 0; - } - else if(m_MasterServerCount > -1) + if(m_ServerlistType != TYPE_LAN && m_RefreshingHttp && !m_pHttp->IsRefreshing()) { - m_MasterServerCount = 0; - for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; i++) - { - if(!m_pMasterServer->IsValid(i)) - continue; - int Count = m_pMasterServer->GetCount(i); - if(Count == -1) - { - /* ignore Server - m_MasterServerCount = -1; - return; - // we don't have the required server information - */ - } - else - m_MasterServerCount += Count; - } - //if(g_Config.m_Debug) - //{ - // dbg_msg("client_srvbrowse", "ServerCount2: %d", m_MasterServerCount); - //} + m_RefreshingHttp = false; + UpdateFromHttp(); + // TODO: move this somewhere else + if(m_Sorthash != SortHash() || ForceResort) + Sort(); + return; } - if(m_MasterServerCount > m_NumRequests + m_LastPacketTick) - { - ++m_LastPacketTick; - return; //wait for more packets - } - pEntry = m_pFirstReqServer; - Count = 0; + + CServerEntry *pEntry = m_pFirstReqServer; + int Count = 0; while(1) { if(!pEntry) // no more entries @@ -938,7 +1189,7 @@ void CServerBrowser::Update(bool ForceResort) if(pEntry->m_Request64Legacy) RequestImpl64(pEntry->m_Addr, pEntry); else - RequestImpl(pEntry->m_Addr, pEntry); + RequestImpl(pEntry->m_Addr, pEntry, nullptr, nullptr); } Count++; @@ -969,7 +1220,7 @@ void CServerBrowser::Update(bool ForceResort) { if(!pEntry) // no more entries break; - pNext = pEntry->m_pNextReq; + CServerEntry *pNext = pEntry->m_pNextReq; RemoveRequest(pEntry); //release request pEntry = pNext; } @@ -983,16 +1234,33 @@ void CServerBrowser::Update(bool ForceResort) } } -bool CServerBrowser::IsFavorite(const NETADDR &Addr) const +int CServerBrowser::FindFavorite(const NETADDR &Addr) const { // search for the address - int i; - for(i = 0; i < m_NumFavoriteServers; i++) + for(int i = 0; i < m_NumFavoriteServers; i++) { if(net_addr_comp(&Addr, &m_aFavoriteServers[i]) == 0) - return true; + return i; } - return false; + return -1; +} + +bool CServerBrowser::GotInfo(const NETADDR &Addr) const +{ + CServerEntry *pEntry = ((CServerBrowser *)this)->Find(Addr); + return pEntry && pEntry->m_GotInfo; +} + +bool CServerBrowser::IsFavorite(const NETADDR &Addr) const +{ + return FindFavorite(Addr) >= 0; +} + +bool CServerBrowser::IsFavoritePingAllowed(const NETADDR &Addr) const +{ + int i = FindFavorite(Addr); + dbg_assert(i >= 0, "invalid favorite"); + return i >= 0 && m_aFavoriteServersAllowPing[i]; } void CServerBrowser::AddFavorite(const NETADDR &Addr) @@ -1003,14 +1271,15 @@ void CServerBrowser::AddFavorite(const NETADDR &Addr) return; // make sure that we don't already have the server in our list - for(int i = 0; i < m_NumFavoriteServers; i++) + if(IsFavorite(Addr)) { - if(net_addr_comp(&Addr, &m_aFavoriteServers[i]) == 0) - return; + return; } // add the server to the list - m_aFavoriteServers[m_NumFavoriteServers++] = Addr; + m_aFavoriteServers[m_NumFavoriteServers] = Addr; + m_aFavoriteServersAllowPing[m_NumFavoriteServers] = false; + m_NumFavoriteServers++; pEntry = Find(Addr); if(pEntry) pEntry->m_Info.m_Favorite = true; @@ -1025,25 +1294,27 @@ void CServerBrowser::AddFavorite(const NETADDR &Addr) } } +void CServerBrowser::FavoriteAllowPing(const NETADDR &Addr, bool AllowPing) +{ + int i = FindFavorite(Addr); + dbg_assert(i >= 0, "invalid favorite"); + m_aFavoriteServersAllowPing[i] = AllowPing; +} + void CServerBrowser::RemoveFavorite(const NETADDR &Addr) { - int i; - CServerEntry *pEntry; - - for(i = 0; i < m_NumFavoriteServers; i++) + int i = FindFavorite(Addr); + if(i < 0) { - if(net_addr_comp(&Addr, &m_aFavoriteServers[i]) == 0) - { - mem_move(&m_aFavoriteServers[i], &m_aFavoriteServers[i + 1], sizeof(NETADDR) * (m_NumFavoriteServers - (i + 1))); - m_NumFavoriteServers--; - - pEntry = Find(Addr); - if(pEntry) - pEntry->m_Info.m_Favorite = false; - - return; - } + return; } + mem_move(&m_aFavoriteServers[i], &m_aFavoriteServers[i + 1], sizeof(NETADDR) * (m_NumFavoriteServers - (i + 1))); + mem_move(&m_aFavoriteServersAllowPing[i], &m_aFavoriteServersAllowPing[i + 1], sizeof(bool) * (m_NumFavoriteServers - (i + 1))); + m_NumFavoriteServers--; + + CServerEntry *pEntry = Find(Addr); + if(pEntry) + pEntry->m_Info.m_Favorite = false; } void CServerBrowser::LoadDDNetServers() @@ -1190,9 +1461,7 @@ int CServerBrowser::HasRank(const char *pMap) void CServerBrowser::LoadDDNetInfoJson() { - IStorage *pStorage = Kernel()->RequestInterface(); - IOHANDLE File = pStorage->OpenFile(DDNET_INFO, IOFLAG_READ, IStorage::TYPE_SAVE); - + IOHANDLE File = m_pStorage->OpenFile(DDNET_INFO, IOFLAG_READ, IStorage::TYPE_SAVE); if(!File) return; @@ -1221,6 +1490,18 @@ void CServerBrowser::LoadDDNetInfoJson() json_value_free(m_pDDNetInfo); m_pDDNetInfo = 0; } + + 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)) + { + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "cannot parse location from info.sjon: '%s'", (const char *)Location); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse", aBuf); + } + } } const json_value *CServerBrowser::LoadDDNetInfo() @@ -1228,15 +1509,8 @@ const json_value *CServerBrowser::LoadDDNetInfo() LoadDDNetInfoJson(); LoadDDNetServers(); - if(m_NumServers == 0) - { - Refresh(m_ServerlistType); - } - else - { - RecheckOfficial(); - LoadDDNetRanks(); - } + RecheckOfficial(); + LoadDDNetRanks(); return m_pDDNetInfo; } @@ -1246,9 +1520,9 @@ bool CServerBrowser::IsRefreshing() const return m_pFirstReqServer != 0; } -bool CServerBrowser::IsRefreshingMasters() const +bool CServerBrowser::IsGettingServerlist() const { - return m_pMasterServer->IsRefreshing(); + return m_pHttp->IsRefreshing(); } int CServerBrowser::LoadingProgression() const @@ -1270,7 +1544,17 @@ void CServerBrowser::ConfigSaveCallback(IConfigManager *pConfigManager, void *pU for(int i = 0; i < pSelf->m_NumFavoriteServers; i++) { net_addr_str(&pSelf->m_aFavoriteServers[i], aAddrStr, sizeof(aAddrStr), true); - str_format(aBuffer, sizeof(aBuffer), "add_favorite %s", aAddrStr); + if(!pSelf->m_aFavoriteServersAllowPing[i]) + { + str_format(aBuffer, sizeof(aBuffer), "add_favorite %s", aAddrStr); + } + else + { + // Add quotes to the first parameter for backward + // compatibility with versions that took a `r` console + // parameter. + str_format(aBuffer, sizeof(aBuffer), "add_favorite \"%s\" allow_ping", aAddrStr); + } pConfigManager->WriteLine(aBuffer); } } @@ -1355,3 +1639,113 @@ void CServerBrowser::TypeFilterClean(int Network) str_copy(pExcludeTypes, aNewList, sizeof(g_Config.m_BrFilterExcludeTypes)); } + +int CServerInfo::EstimateLatency(int Loc1, int Loc2) +{ + if(Loc1 == LOC_UNKNOWN || Loc2 == LOC_UNKNOWN) + { + return 999; + } + if(Loc1 != Loc2) + { + return 149; + } + return 49; +} +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 LOCATIONS[][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 = sizeof(LOCATIONS) / sizeof(LOCATIONS[0]) - 1; i >= 0; i--) + { + if(str_startswith(pString, LOCATIONS[i])) + { + *pResult = i; + return false; + } + } + return true; +} + +bool IsVanilla(const CServerInfo *pInfo) +{ + return !str_comp(pInfo->m_aGameType, "DM") || !str_comp(pInfo->m_aGameType, "TDM") || !str_comp(pInfo->m_aGameType, "CTF"); +} + +bool IsCatch(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "catch"); +} + +bool IsInsta(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "idm") || str_find_nocase(pInfo->m_aGameType, "itdm") || str_find_nocase(pInfo->m_aGameType, "ictf"); +} + +bool IsFNG(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "fng"); +} + +bool IsRace(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "race") || str_find_nocase(pInfo->m_aGameType, "fastcap"); +} + +bool IsFastCap(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "fastcap"); +} + +bool IsBlockInfectionZ(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "blockz") || + str_find_nocase(pInfo->m_aGameType, "infectionz"); +} + +bool IsBlockWorlds(const CServerInfo *pInfo) +{ + return (str_comp_nocase_num(pInfo->m_aGameType, "bw ", 4) == 0) || (str_comp_nocase(pInfo->m_aGameType, "bw") == 0); +} + +bool IsCity(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "city"); +} + +bool IsDDRace(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "ddrace") || str_find_nocase(pInfo->m_aGameType, "mkrace"); +} + +bool IsDDNet(const CServerInfo *pInfo) +{ + return str_find_nocase(pInfo->m_aGameType, "ddracenet") || str_find_nocase(pInfo->m_aGameType, "ddnet"); +} + +// other + +bool Is64Player(const CServerInfo *pInfo) +{ + return str_find(pInfo->m_aGameType, "64") || str_find(pInfo->m_aName, "64") || IsDDNet(pInfo); +} + +bool IsPlus(const CServerInfo *pInfo) +{ + return str_find(pInfo->m_aGameType, "+"); +} diff --git a/src/engine/client/serverbrowser.h b/src/engine/client/serverbrowser.h index 9409dc22f..ebc5adfb9 100644 --- a/src/engine/client/serverbrowser.h +++ b/src/engine/client/serverbrowser.h @@ -3,13 +3,18 @@ #ifndef ENGINE_CLIENT_SERVERBROWSER_H #define ENGINE_CLIENT_SERVERBROWSER_H +#include #include +#include #include #include #include #include #include +class IServerBrowserHttp; +class IServerBrowserPingCache; + class CServerBrowser : public IServerBrowser { public: @@ -18,6 +23,7 @@ public: public: NETADDR m_Addr; int64 m_RequestTime; + bool m_RequestIgnoreInfo; int m_GotInfo; bool m_Request64Legacy; CServerInfo m_Info; @@ -79,7 +85,7 @@ public: // interface functions void Refresh(int Type); bool IsRefreshing() const; - bool IsRefreshingMasters() const; + bool IsGettingServerlist() const; int LoadingProgression() const; int NumServers() const { return m_NumServers; } @@ -97,8 +103,11 @@ public: int NumSortedServers() const { return m_NumSortedServers; } const CServerInfo *SortedGet(int Index) const; + bool GotInfo(const NETADDR &Addr) const; bool IsFavorite(const NETADDR &Addr) const; + bool IsFavoritePingAllowed(const NETADDR &Addr) const; void AddFavorite(const NETADDR &Addr); + void FavoriteAllowPing(const NETADDR &Addr, bool AllowPing); void RemoveFavorite(const NETADDR &Addr); void LoadDDNetRanks(); @@ -123,7 +132,8 @@ public: // void Update(bool ForceResort); void Set(const NETADDR &Addr, int Type, int Token, const CServerInfo *pInfo); - void RequestCurrentServer(const NETADDR &Addr) const; + void RequestCurrentServer(const NETADDR &Addr, int *pBasicToken, int *pToken) const; + void SetCurrentServerPing(const NETADDR &Addr, int Ping); void SetBaseInfo(class CNetClient *pClient, const char *pNetVersion); @@ -134,19 +144,27 @@ public: private: CNetClient *m_pNetClient; - IMasterServer *m_pMasterServer; class IConsole *m_pConsole; + class IEngine *m_pEngine; class IFriends *m_pFriends; + class IStorage *m_pStorage; char m_aNetVersion[128]; + bool m_RefreshingHttp = false; + IServerBrowserHttp *m_pHttp = nullptr; + IServerBrowserPingCache *m_pPingCache = nullptr; + const char *m_pHttpPrevBestUrl = nullptr; + CHeap m_ServerlistHeap; CServerEntry **m_ppServerlist; int *m_pSortedServerlist; NETADDR m_aFavoriteServers[MAX_FAVORITES]; + bool m_aFavoriteServersAllowPing[MAX_FAVORITES]; int m_NumFavoriteServers; CNetwork m_aNetworks[NUM_NETWORKS]; + int m_OwnLocation = CServerInfo::LOC_UNKNOWN; json_value *m_pDDNetInfo; @@ -155,13 +173,10 @@ private: CServerEntry *m_pFirstReqServer; // request list CServerEntry *m_pLastReqServer; int m_NumRequests; - int m_MasterServerCount; //used instead of g_Config.br_max_requests to get more servers int m_CurrentMaxRequests; - int m_LastPacketTick; - int m_NeedRefresh; int m_NumSortedServers; @@ -180,6 +195,8 @@ private: bool m_SortOnNextUpdate; + int FindFavorite(const NETADDR &Addr) const; + int GenerateToken(const NETADDR &Addr) const; static int GetBasicToken(int Token); static int GetExtraToken(int Token); @@ -198,13 +215,18 @@ private: void Sort(); int SortHash() const; + void UpdateFromHttp(); CServerEntry *Add(const NETADDR &Addr); void RemoveRequest(CServerEntry *pEntry); - void RequestImpl(const NETADDR &Addr, CServerEntry *pEntry) const; + void RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int *pBasicToken, int *pToken) const; + + void RegisterCommands(); + static void Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData); void SetInfo(CServerEntry *pEntry, const CServerInfo &Info); + void SetLatency(const NETADDR Addr, int Latency); static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData); }; diff --git a/src/engine/client/serverbrowser_http.cpp b/src/engine/client/serverbrowser_http.cpp new file mode 100644 index 000000000..1a4321ee0 --- /dev/null +++ b/src/engine/client/serverbrowser_http.cpp @@ -0,0 +1,474 @@ +#include "serverbrowser_http.h" + +#include "http.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +class CChooseMaster +{ +public: + typedef bool (*VALIDATOR)(json_value *pJson); + + enum + { + MAX_URLS = 16, + }; + CChooseMaster(IEngine *pEngine, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex); + virtual ~CChooseMaster() {} + + bool GetBestUrl(const char **pBestUrl) const; + void Reset(); + bool IsRefreshing() const { return m_pJob && m_pJob->Status() != IJob::STATE_DONE; } + void Refresh(); + +private: + int GetBestIndex() const; + + class CData + { + public: + std::atomic_int m_BestIndex{-1}; + // Constant after construction. + VALIDATOR m_pfnValidator; + int m_NumUrls; + char m_aaUrls[MAX_URLS][256]; + }; + class CJob : public IJob + { + std::shared_ptr m_pData; + virtual void Run(); + + public: + CJob(std::shared_ptr pData) : + m_pData(std::move(pData)) {} + virtual ~CJob() {} + }; + + IEngine *m_pEngine; + int m_PreviousBestIndex; + std::shared_ptr m_pData; + std::shared_ptr m_pJob; +}; + +CChooseMaster::CChooseMaster(IEngine *pEngine, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex) : + m_pEngine(pEngine), + m_PreviousBestIndex(PreviousBestIndex) +{ + dbg_assert(NumUrls >= 0, "no master URLs"); + dbg_assert(NumUrls <= MAX_URLS, "too many master URLs"); + dbg_assert(PreviousBestIndex >= -1, "previous best index negative and not -1"); + dbg_assert(PreviousBestIndex < NumUrls, "previous best index too high"); + m_pData = std::make_shared(); + m_pData->m_pfnValidator = pfnValidator; + m_pData->m_NumUrls = NumUrls; + for(int i = 0; i < m_pData->m_NumUrls; i++) + { + str_copy(m_pData->m_aaUrls[i], ppUrls[i], sizeof(m_pData->m_aaUrls[i])); + } +} + +int CChooseMaster::GetBestIndex() const +{ + int BestIndex = m_pData->m_BestIndex.load(); + if(BestIndex >= 0) + { + return BestIndex; + } + else + { + return m_PreviousBestIndex; + } +} + +bool CChooseMaster::GetBestUrl(const char **ppBestUrl) const +{ + int Index = GetBestIndex(); + if(Index < 0) + { + *ppBestUrl = nullptr; + return true; + } + *ppBestUrl = m_pData->m_aaUrls[Index]; + return false; +} + +void CChooseMaster::Reset() +{ + m_PreviousBestIndex = -1; + m_pData->m_BestIndex.store(-1); +} + +void CChooseMaster::Refresh() +{ + m_pEngine->AddJob(m_pJob = std::make_shared(m_pData)); +} + +void CChooseMaster::CJob::Run() +{ + // Check masters in a random order. + int aRandomized[MAX_URLS] = {0}; + for(int i = 0; i < m_pData->m_NumUrls; i++) + { + aRandomized[i] = i; + } + // https://en.wikipedia.org/w/index.php?title=Fisher%E2%80%93Yates_shuffle&oldid=1002922479#The_modern_algorithm + // The equivalent version. + for(int i = 0; i <= m_pData->m_NumUrls - 2; i++) + { + int j = i + secure_rand_below(m_pData->m_NumUrls - i); + std::swap(aRandomized[i], aRandomized[j]); + } + // Do a HEAD request to ensure that a connection is established and + // then do a GET request to check how fast we can get the server list. + // + // 10 seconds connection timeout, lower than 8KB/s for 10 seconds to + // fail. + CTimeout Timeout{10000, 8000, 10}; + int aTimeMs[MAX_URLS]; + for(int i = 0; i < m_pData->m_NumUrls; i++) + { + aTimeMs[i] = -1; + const char *pUrl = m_pData->m_aaUrls[aRandomized[i]]; + CHead Head(pUrl, Timeout); + IEngine::RunJobBlocking(&Head); + if(Head.State() != HTTP_DONE) + { + continue; + } + int64 StartTime = time_get(); + CGet Get(pUrl, Timeout); + IEngine::RunJobBlocking(&Get); + int Time = (time_get() - StartTime) * 1000 / time_freq(); + if(Get.State() != HTTP_DONE) + { + continue; + } + json_value *pJson = Get.ResultJson(); + if(!pJson) + { + continue; + } + bool ParseFailure = m_pData->m_pfnValidator(pJson); + json_value_free(pJson); + if(ParseFailure) + { + continue; + } + dbg_msg("serverbrowse_http", "found master, url='%s' time=%dms", pUrl, Time); + aTimeMs[i] = Time; + } + // Determine index of the minimum time. + int BestIndex = -1; + int BestTime = 0; + for(int i = 0; i < m_pData->m_NumUrls; i++) + { + if(aTimeMs[i] < 0) + { + continue; + } + if(BestIndex == -1 || aTimeMs[i] < BestTime) + { + BestTime = aTimeMs[i]; + BestIndex = aRandomized[i]; + } + } + if(BestIndex == -1) + { + dbg_msg("serverbrowse_http", "WARNING: no usable masters found"); + return; + } + dbg_msg("serverbrowse_http", "determined best master, url='%s' time=%dms", m_pData->m_aaUrls[BestIndex], BestTime); + m_pData->m_BestIndex.store(BestIndex); +} + +class CServerBrowserHttp : public IServerBrowserHttp +{ +public: + CServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, const char **ppUrls, int NumUrls, int PreviousBestIndex); + virtual ~CServerBrowserHttp() {} + void Update(); + bool IsRefreshing() { return m_State != STATE_DONE; } + void Refresh(); + bool GetBestUrl(const char **pBestUrl) const { return m_pChooseMaster->GetBestUrl(pBestUrl); } + + int NumServers() const + { + return m_aServers.size(); + } + const NETADDR &ServerAddress(int Index) const + { + return m_aServers[Index].m_Addr; + } + void Server(int Index, NETADDR *pAddr, CServerInfo *pInfo) const + { + const CEntry &Entry = m_aServers[Index]; + *pAddr = Entry.m_Addr; + *pInfo = Entry.m_Info; + } + int NumLegacyServers() const + { + return m_aLegacyServers.size(); + } + const NETADDR &LegacyServer(int Index) const + { + return m_aLegacyServers[Index]; + } + +private: + enum + { + STATE_DONE, + STATE_WANTREFRESH, + STATE_REFRESHING, + }; + + class CEntry + { + public: + NETADDR m_Addr; + CServerInfo m_Info; + }; + + static bool Validate(json_value *pJson); + static bool Parse(json_value *pJson, std::vector *paServers, std::vector *paLegacyServers); + + IEngine *m_pEngine; + IConsole *m_pConsole; + + int m_State = STATE_DONE; + std::shared_ptr m_pGetServers; + std::unique_ptr m_pChooseMaster; + + std::vector m_aServers; + std::vector m_aLegacyServers; +}; + +CServerBrowserHttp::CServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, const char **ppUrls, int NumUrls, int PreviousBestIndex) : + m_pEngine(pEngine), + m_pConsole(pConsole), + m_pChooseMaster(new CChooseMaster(pEngine, Validate, ppUrls, NumUrls, PreviousBestIndex)) +{ + m_pChooseMaster->Refresh(); +} +void CServerBrowserHttp::Update() +{ + if(m_State == STATE_WANTREFRESH) + { + const char *pBestUrl; + if(m_pChooseMaster->GetBestUrl(&pBestUrl)) + { + if(!m_pChooseMaster->IsRefreshing()) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_http", "no working serverlist URL found"); + m_State = STATE_DONE; + } + return; + } + m_pEngine->AddJob(m_pGetServers = std::make_shared(pBestUrl, CTimeout{0, 0, 0})); + m_State = STATE_REFRESHING; + } + else if(m_State == STATE_REFRESHING) + { + if(m_pGetServers->State() == HTTP_QUEUED || m_pGetServers->State() == HTTP_RUNNING) + { + return; + } + m_State = STATE_DONE; + std::shared_ptr pGetServers = nullptr; + std::swap(m_pGetServers, pGetServers); + + bool Success = true; + json_value *pJson = pGetServers->ResultJson(); + Success = Success && pJson; + Success = Success && !Parse(pJson, &m_aServers, &m_aLegacyServers); + json_value_free(pJson); + if(!Success) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_http", "failed getting serverlist, trying to find best URL"); + m_pChooseMaster->Reset(); + m_pChooseMaster->Refresh(); + } + } +} +void CServerBrowserHttp::Refresh() +{ + if(m_State == STATE_WANTREFRESH) + { + m_pChooseMaster->Refresh(); + } + m_State = STATE_WANTREFRESH; + Update(); +} +bool ServerbrowserParseUrl(NETADDR *pOut, const char *pUrl) +{ + char aHost[128]; + const char *pRest = str_startswith(pUrl, "tw-0.6+udp://"); + if(!pRest) + { + return true; + } + int Length = str_length(pRest); + int Start = 0; + int End = Length; + for(int i = 0; i < Length; i++) + { + if(pRest[i] == '@') + { + if(Start != 0) + { + // Two at signs. + return true; + } + Start = i + 1; + } + else if(pRest[i] == '/' || pRest[i] == '?' || pRest[i] == '#') + { + End = i; + break; + } + } + str_truncate(aHost, sizeof(aHost), pRest + Start, End - Start); + if(net_addr_from_str(pOut, aHost)) + { + return true; + } + return false; +} +bool CServerBrowserHttp::Validate(json_value *pJson) +{ + std::vector aServers; + std::vector aLegacyServers; + return Parse(pJson, &aServers, &aLegacyServers); +} +bool CServerBrowserHttp::Parse(json_value *pJson, std::vector *paServers, std::vector *paLegacyServers) +{ + std::vector aServers; + std::vector aLegacyServers; + + const json_value &Json = *pJson; + const json_value &Servers = Json["servers"]; + const json_value &LegacyServers = Json["servers_legacy"]; + if(Servers.type != json_array || (LegacyServers.type != json_array && LegacyServers.type != json_none)) + { + return true; + } + for(unsigned int i = 0; i < Servers.u.array.length; i++) + { + const json_value &Server = Servers[i]; + const json_value &Addresses = Server["addresses"]; + const json_value &Info = Server["info"]; + const json_value &Location = Server["location"]; + int ParsedLocation = CServerInfo::LOC_UNKNOWN; + CServerInfo2 ParsedInfo; + if(Addresses.type != json_array || (Location.type != json_string && Location.type != json_none)) + { + return true; + } + if(Location.type == json_string) + { + if(CServerInfo::ParseLocation(&ParsedLocation, Location)) + { + return true; + } + } + if(CServerInfo2::FromJson(&ParsedInfo, &Info)) + { + //dbg_msg("dbg/serverbrowser", "skipped due to info, i=%d", i); + // Only skip the current server on parsing + // failure; the server info is "user input" by + // the game server and can be set to arbitrary + // values. + continue; + } + CServerInfo SetInfo = ParsedInfo; + SetInfo.m_Location = ParsedLocation; + for(unsigned int a = 0; a < Addresses.u.array.length; a++) + { + const json_value &Address = Addresses[a]; + if(Address.type != json_string) + { + return true; + } + // TODO: Address address handling :P + NETADDR ParsedAddr; + if(ServerbrowserParseUrl(&ParsedAddr, Addresses[a])) + { + //dbg_msg("dbg/serverbrowser", "unknown address, i=%d a=%d", i, a); + // Skip unknown addresses. + continue; + } + aServers.push_back({ParsedAddr, SetInfo}); + } + } + if(LegacyServers.type == json_array) + { + for(unsigned int i = 0; i < LegacyServers.u.array.length; i++) + { + const json_value &Address = LegacyServers[i]; + NETADDR ParsedAddr; + if(Address.type != json_string || net_addr_from_str(&ParsedAddr, Address)) + { + return true; + } + aLegacyServers.push_back(ParsedAddr); + } + } + *paServers = aServers; + *paLegacyServers = aLegacyServers; + return false; +} + +static const char *DEFAULT_SERVERLIST_URLS[] = { + "https://master1.ddnet.tw/ddnet/15/servers.json", + "https://master2.ddnet.tw/ddnet/15/servers.json", + "https://master3.ddnet.tw/ddnet/15/servers.json", + "https://master4.ddnet.tw/ddnet/15/servers.json", +}; + +IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, IStorage *pStorage, const char *pPreviousBestUrl) +{ + char aaUrls[CChooseMaster::MAX_URLS][256]; + const char *apUrls[CChooseMaster::MAX_URLS] = {0}; + const char **ppUrls = apUrls; + int NumUrls = 0; + IOHANDLE File = pStorage->OpenFile("serverlist_urls.cfg", IOFLAG_READ, IStorage::TYPE_ALL); + if(File) + { + CLineReader Lines; + Lines.Init(File); + while(NumUrls < CChooseMaster::MAX_URLS) + { + const char *pLine = Lines.Get(); + if(!pLine) + { + break; + } + str_copy(aaUrls[NumUrls], pLine, sizeof(aaUrls[NumUrls])); + apUrls[NumUrls] = aaUrls[NumUrls]; + NumUrls += 1; + } + } + if(NumUrls == 0) + { + ppUrls = DEFAULT_SERVERLIST_URLS; + NumUrls = sizeof(DEFAULT_SERVERLIST_URLS) / sizeof(DEFAULT_SERVERLIST_URLS[0]); + } + int PreviousBestIndex = -1; + for(int i = 0; i < NumUrls; i++) + { + if(str_comp(ppUrls[i], pPreviousBestUrl) == 0) + { + PreviousBestIndex = i; + break; + } + } + return new CServerBrowserHttp(pEngine, pConsole, ppUrls, NumUrls, PreviousBestIndex); +} diff --git a/src/engine/client/serverbrowser_http.h b/src/engine/client/serverbrowser_http.h new file mode 100644 index 000000000..6501ec075 --- /dev/null +++ b/src/engine/client/serverbrowser_http.h @@ -0,0 +1,30 @@ +#ifndef ENGINE_CLIENT_SERVERBROWSER_HTTP_H +#define ENGINE_CLIENT_SERVERBROWSER_HTTP_H +#include + +class CServerInfo; +class IConsole; +class IEngine; +class IStorage; + +class IServerBrowserHttp +{ +public: + virtual ~IServerBrowserHttp() {} + + virtual void Update() = 0; + + virtual bool IsRefreshing() = 0; + virtual void Refresh() = 0; + + virtual bool GetBestUrl(const char **pBestUrl) const = 0; + + virtual int NumServers() const = 0; + virtual const NETADDR &ServerAddress(int Index) const = 0; + virtual void Server(int Index, NETADDR *pAddr, CServerInfo *pInfo) const = 0; + virtual int NumLegacyServers() const = 0; + virtual const NETADDR &LegacyServer(int Index) const = 0; +}; + +IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, IStorage *pStorage, const char *pPreviousBestUrl); +#endif // ENGINE_CLIENT_SERVERBROWSER_HTTP_H diff --git a/src/engine/client/serverbrowser_ping_cache.cpp b/src/engine/client/serverbrowser_ping_cache.cpp new file mode 100644 index 000000000..67b60c65e --- /dev/null +++ b/src/engine/client/serverbrowser_ping_cache.cpp @@ -0,0 +1,207 @@ +#include "serverbrowser_ping_cache.h" + +#include +#include + +#include + +#include +#include +#include + +class CServerBrowserPingCache : public IServerBrowserPingCache +{ +public: + CServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage); + virtual ~CServerBrowserPingCache() {} + + void Load(); + + void CachePing(NETADDR Addr, int Ping); + void GetPingCache(const CEntry **ppEntries, int *pNumEntries); + +private: + IConsole *m_pConsole; + + CSqlite m_pDisk; + CSqliteStmt m_pLoadStmt; + CSqliteStmt m_pStoreStmt; + + std::vector m_aEntries; + std::vector m_aNewEntries; +}; + +CServerBrowserPingCache::CServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage) : + m_pConsole(pConsole) +{ + m_pDisk = SqliteOpen(pConsole, pStorage, "cache.sqlite3"); + if(!m_pDisk) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to open cache.sqlite3"); + return; + } + sqlite3 *pSqlite = m_pDisk.get(); + static const char TABLE[] = "CREATE TABLE IF NOT EXISTS server_pings (ip_address TEXT PRIMARY KEY NOT NULL, ping INTEGER NOT NULL, utc_timestamp TEXT NOT NULL)"; + if(SQLITE_HANDLE_ERROR(sqlite3_exec(pSqlite, TABLE, nullptr, nullptr, nullptr))) + { + m_pDisk = nullptr; + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to create server_pings table"); + return; + } + m_pLoadStmt = SqlitePrepare(pConsole, pSqlite, "SELECT ip_address, ping FROM server_pings"); + m_pStoreStmt = SqlitePrepare(pConsole, pSqlite, "INSERT OR REPLACE INTO server_pings (ip_address, ping, utc_timestamp) VALUES (?, ?, datetime('now'))"); +} + +void CServerBrowserPingCache::Load() +{ + if(m_pDisk) + { + int PrevNewEntriesSize = m_aNewEntries.size(); + + sqlite3 *pSqlite = m_pDisk.get(); + IConsole *pConsole = m_pConsole; + bool Error = false; + bool WarnedForBadAddress = false; + Error = Error || !m_pLoadStmt; + while(!Error) + { + int StepResult = SQLITE_HANDLE_ERROR(sqlite3_step(m_pLoadStmt.get())); + if(StepResult == SQLITE_DONE) + { + break; + } + else if(StepResult == SQLITE_ROW) + { + const char *pIpAddress = (const char *)sqlite3_column_text(m_pLoadStmt.get(), 0); + int Ping = sqlite3_column_int(m_pLoadStmt.get(), 1); + NETADDR Addr; + if(net_addr_from_str(&Addr, pIpAddress)) + { + if(!WarnedForBadAddress) + { + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "invalid address: %s", pIpAddress); + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", aBuf); + WarnedForBadAddress = true; + } + continue; + } + m_aNewEntries.push_back(CEntry{Addr, Ping}); + } + else + { + Error = true; + } + } + if(Error) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to load ping cache"); + m_aNewEntries.resize(PrevNewEntriesSize); + } + } +} + +void CServerBrowserPingCache::CachePing(NETADDR Addr, int Ping) +{ + Addr.port = 0; + m_aNewEntries.push_back(CEntry{Addr, Ping}); + if(m_pDisk) + { + sqlite3 *pSqlite = m_pDisk.get(); + IConsole *pConsole = m_pConsole; + char aAddr[NETADDR_MAXSTRSIZE]; + net_addr_str(&Addr, aAddr, sizeof(aAddr), false); + + bool Error = false; + Error = Error || !m_pStoreStmt; + Error = Error || SQLITE_HANDLE_ERROR(sqlite3_reset(m_pStoreStmt.get())) != SQLITE_OK; + Error = Error || SQLITE_HANDLE_ERROR(sqlite3_bind_text(m_pStoreStmt.get(), 1, aAddr, -1, SQLITE_STATIC)) != SQLITE_OK; + Error = Error || SQLITE_HANDLE_ERROR(sqlite3_bind_int(m_pStoreStmt.get(), 2, Ping)) != SQLITE_OK; + Error = Error || SQLITE_HANDLE_ERROR(sqlite3_step(m_pStoreStmt.get())) != SQLITE_DONE; + if(Error) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to store ping"); + } + } +} + +void CServerBrowserPingCache::GetPingCache(const CEntry **ppEntries, int *pNumEntries) +{ + if(!m_aNewEntries.empty()) + { + class CAddrComparer + { + public: + bool operator()(const CEntry &a, const CEntry &b) + { + return net_addr_comp(&a.m_Addr, &b.m_Addr) < 0; + } + }; + std::vector aOldEntries; + std::swap(m_aEntries, aOldEntries); + + // Remove duplicates, keeping newer ones. + std::stable_sort(m_aNewEntries.begin(), m_aNewEntries.end(), CAddrComparer()); + { + unsigned To = 0; + for(unsigned int From = 0; From < m_aNewEntries.size(); From++) + { + if(To < From) + { + m_aNewEntries[To] = m_aNewEntries[From]; + } + if(From + 1 >= m_aNewEntries.size() || + net_addr_comp(&m_aNewEntries[From].m_Addr, &m_aNewEntries[From + 1].m_Addr) != 0) + { + To++; + } + } + m_aNewEntries.resize(To); + } + // Only keep the new entries where there are duplicates. + m_aEntries.reserve(m_aNewEntries.size() + aOldEntries.size()); + { + unsigned i = 0; + unsigned j = 0; + while(i < aOldEntries.size() && j < m_aNewEntries.size()) + { + int Cmp = net_addr_comp(&aOldEntries[i].m_Addr, &m_aNewEntries[j].m_Addr); + if(Cmp != 0) + { + if(Cmp < 0) + { + m_aEntries.push_back(aOldEntries[i]); + i++; + } + else + { + m_aEntries.push_back(m_aNewEntries[j]); + j++; + } + } + else + { + // Ignore the old element if we have both. + i++; + } + } + // Add the remaining elements. + for(; i < aOldEntries.size(); i++) + { + m_aEntries.push_back(aOldEntries[i]); + } + for(; j < m_aNewEntries.size(); j++) + { + m_aEntries.push_back(m_aNewEntries[j]); + } + } + m_aNewEntries.clear(); + } + *ppEntries = &m_aEntries[0]; + *pNumEntries = m_aEntries.size(); +} + +IServerBrowserPingCache *CreateServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage) +{ + return new CServerBrowserPingCache(pConsole, pStorage); +} diff --git a/src/engine/client/serverbrowser_ping_cache.h b/src/engine/client/serverbrowser_ping_cache.h new file mode 100644 index 000000000..5e4c2d8e4 --- /dev/null +++ b/src/engine/client/serverbrowser_ping_cache.h @@ -0,0 +1,29 @@ +#ifndef ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H +#define ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H +#include + +class IConsole; +class IStorage; + +class IServerBrowserPingCache +{ +public: + class CEntry + { + public: + NETADDR m_Addr; + int m_Ping; + }; + + virtual ~IServerBrowserPingCache() {} + + virtual void Load() = 0; + + virtual void CachePing(NETADDR Addr, int Ping) = 0; + // The returned list is sorted by address, the addresses don't have a + // port. + virtual void GetPingCache(const CEntry **ppEntries, int *pNumEntries) = 0; +}; + +IServerBrowserPingCache *CreateServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage); +#endif // ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H diff --git a/src/engine/client/sqlite.cpp b/src/engine/client/sqlite.cpp new file mode 100644 index 000000000..a6bb6f8b4 --- /dev/null +++ b/src/engine/client/sqlite.cpp @@ -0,0 +1,60 @@ +#include +#include +#include + +#include + +void CSqliteDeleter::operator()(sqlite3 *pSqlite) +{ + sqlite3_close(pSqlite); +} + +void CSqliteStmtDeleter::operator()(sqlite3_stmt *pStmt) +{ + sqlite3_finalize(pStmt); +} + +int SqliteHandleError(IConsole *pConsole, int Error, sqlite3 *pSqlite, const char *pContext) +{ + if(Error != SQLITE_OK && Error != SQLITE_DONE && Error != SQLITE_ROW) + { + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), "%s at %s", sqlite3_errmsg(pSqlite), pContext); + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "sqlite3", aBuf); + } + return Error; +} + +CSqlite SqliteOpen(IConsole *pConsole, IStorage *pStorage, const char *pPath) +{ + char aFullPath[MAX_PATH_LENGTH]; + pStorage->GetCompletePath(IStorage::TYPE_SAVE, pPath, aFullPath, sizeof(aFullPath)); + sqlite3 *pSqlite = nullptr; + bool ErrorOpening = SQLITE_HANDLE_ERROR(sqlite3_open(aFullPath, &pSqlite)) != SQLITE_OK; + // Even on error, the database is initialized and needs to be freed. + // Except on allocation failure, but then it'll be nullptr which is + // also fine. + CSqlite pResult{pSqlite}; + if(ErrorOpening) + { + return nullptr; + } + bool Error = false; + Error = Error || SQLITE_HANDLE_ERROR(sqlite3_exec(pSqlite, "PRAGMA journal_mode = WAL", nullptr, nullptr, nullptr)); + Error = Error || SQLITE_HANDLE_ERROR(sqlite3_exec(pSqlite, "PRAGMA synchronous = NORMAL", nullptr, nullptr, nullptr)); + if(Error) + { + return nullptr; + } + return pResult; +} + +CSqliteStmt SqlitePrepare(IConsole *pConsole, sqlite3 *pSqlite, const char *pStatement) +{ + sqlite3_stmt *pTemp; + if(SQLITE_HANDLE_ERROR(sqlite3_prepare_v2(pSqlite, pStatement, -1, &pTemp, nullptr))) + { + return nullptr; + } + return CSqliteStmt{pTemp}; +} diff --git a/src/engine/engine.h b/src/engine/engine.h index e32382658..aaad9e519 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -32,8 +32,10 @@ public: virtual void Init() = 0; virtual void InitLogfile() = 0; virtual void AddJob(std::shared_ptr pJob) = 0; + static void RunJobBlocking(IJob *pJob); }; extern IEngine *CreateEngine(const char *pAppname, bool Silent, int Jobs); +extern IEngine *CreateTestEngine(const char *pAppname, int Jobs); #endif diff --git a/src/engine/external/json-parser/json.h b/src/engine/external/json-parser/json.h index 610d072b6..b8a0acce4 100644 --- a/src/engine/external/json-parser/json.h +++ b/src/engine/external/json-parser/json.h @@ -226,6 +226,12 @@ typedef struct _json_value }; } + /* DDNet additions */ + inline operator int () const + { + return (json_int_t) *this; + } + inline operator bool () const { if (type != json_boolean) diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index b0013ffc5..8a10f5c2d 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -1138,7 +1138,7 @@ void CServer::SendCapabilities(int ClientID) { CMsgPacker Msg(NETMSG_CAPABILITIES, true); Msg.AddInt(SERVERCAP_CURVERSION); // version - Msg.AddInt(SERVERCAPFLAG_DDNET | SERVERCAPFLAG_CHATTIMEOUTCODE | SERVERCAPFLAG_ANYPLAYERFLAG); // flags + Msg.AddInt(SERVERCAPFLAG_DDNET | SERVERCAPFLAG_CHATTIMEOUTCODE | SERVERCAPFLAG_ANYPLAYERFLAG | SERVERCAPFLAG_PINGEX); // flags SendMsg(&Msg, MSGFLAG_VITAL, ClientID); } @@ -1699,6 +1699,17 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) CMsgPacker Msg(NETMSG_PING_REPLY, true); SendMsg(&Msg, 0, ClientID); } + else if(Msg == NETMSG_PINGEX) + { + CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID)); + if(Unpacker.Error()) + { + return; + } + CMsgPacker Msg(NETMSG_PONGEX, true); + Msg.AddRaw(pID, sizeof(*pID)); + SendMsg(&Msg, MSGFLAG_FLUSH, ClientID); + } else { if(g_Config.m_Debug) diff --git a/src/engine/serverbrowser.h b/src/engine/serverbrowser.h index 6677d6897..84fcfd105 100644 --- a/src/engine/serverbrowser.h +++ b/src/engine/serverbrowser.h @@ -11,15 +11,26 @@ #define DDNET_INFO "ddnet-info.json" -/* - Structure: CServerInfo -*/ class CServerInfo { public: - /* - Structure: CInfoClient - */ + enum + { + LOC_UNKNOWN = 0, + LOC_AFRICA, + LOC_ASIA, + LOC_AUSTRALIA, + LOC_EUROPE, + LOC_NORTH_AMERICA, + LOC_SOUTH_AMERICA, + // Special case China because it has an exceptionally bad + // connection to the outside due to the Great Firewall of + // China: + // https://en.wikipedia.org/w/index.php?title=Great_Firewall&oldid=1019589632 + LOC_CHINA, + NUM_LOCS, + }; + class CClient { public: @@ -50,6 +61,8 @@ public: int m_Flags; bool m_Favorite; bool m_Official; + int m_Location; + bool m_LatencyIsEstimated; int m_Latency; // in ms int m_HasRank; char m_aGameType[16]; @@ -63,6 +76,9 @@ public: mutable int m_NumFilteredPlayers; mutable CUIElement *m_pUIElement; + + static int EstimateLatency(int Loc1, int Loc2); + static bool ParseLocation(int *pResult, const char *pString); }; bool IsVanilla(const CServerInfo *pInfo); @@ -114,6 +130,7 @@ public: SET_DDNET_ADD, SET_KOG_ADD, SET_TOKEN, + SET_HTTPINFO, NETWORK_DDNET = 0, NETWORK_KOG = 1, @@ -121,8 +138,8 @@ public: }; virtual void Refresh(int Type) = 0; + virtual bool IsGettingServerlist() const = 0; virtual bool IsRefreshing() const = 0; - virtual bool IsRefreshingMasters() const = 0; virtual int LoadingProgression() const = 0; virtual int NumServers() const = 0; @@ -133,8 +150,11 @@ public: virtual int NumSortedServers() const = 0; virtual const CServerInfo *SortedGet(int Index) const = 0; + virtual bool GotInfo(const NETADDR &Addr) const = 0; virtual bool IsFavorite(const NETADDR &Addr) const = 0; + virtual bool IsFavoritePingAllowed(const NETADDR &Addr) const = 0; virtual void AddFavorite(const NETADDR &Addr) = 0; + virtual void FavoriteAllowPing(const NETADDR &Addr, bool AllowPing) = 0; virtual void RemoveFavorite(const NETADDR &Addr) = 0; virtual int NumCountries(int Network) = 0; diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index b3c88f711..47ef04342 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -63,6 +63,8 @@ MACRO_CONFIG_INT(BrFilterUnfinishedMap, br_filter_unfinished_map, 0, 0, 1, CFGFL MACRO_CONFIG_STR(BrFilterExcludeCountries, br_filter_exclude_countries, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out DDNet servers by country") MACRO_CONFIG_STR(BrFilterExcludeTypes, br_filter_exclude_types, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out DDNet servers by type (mod)") MACRO_CONFIG_INT(BrIndicateFinished, br_indicate_finished, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Show whether you have finished a DDNet map (transmits your player name to info2.ddnet.tw/info)") +MACRO_CONFIG_STR(BrLocation, br_location, 16, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Override location for ping estimation, available: auto, af, as, as:cn, eu, na, oc, sa (Automatic, Africa, Asia, China, Europe, North America, Oceania/Australia, South America") +MACRO_CONFIG_STR(BrCachedBestServerinfoUrl, br_cached_best_serverinfo_url, 256, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Do not set this variable, instead create a serverlist_urls.cfg next to settings_ddnet.cfg to specify all possible serverlist URLs") MACRO_CONFIG_STR(BrFilterExcludeCountriesKoG, br_filter_exclude_countries_kog, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out kog servers by country") MACRO_CONFIG_STR(BrFilterExcludeTypesKoG, br_filter_exclude_types_kog, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out kog servers by type (mod)") diff --git a/src/engine/shared/engine.cpp b/src/engine/shared/engine.cpp index 7308480e3..86cd5bf7f 100644 --- a/src/engine/shared/engine.cpp +++ b/src/engine/shared/engine.cpp @@ -53,25 +53,28 @@ public: } } - CEngine(const char *pAppname, bool Silent, int Jobs) + CEngine(bool Test, const char *pAppname, bool Silent, int Jobs) { - if(!Silent) - dbg_logger_stdout(); - dbg_logger_debugger(); + if(!Test) + { + if(!Silent) + dbg_logger_stdout(); + dbg_logger_debugger(); - // - dbg_msg("engine", "running on %s-%s-%s", CONF_FAMILY_STRING, CONF_PLATFORM_STRING, CONF_ARCH_STRING); + // + dbg_msg("engine", "running on %s-%s-%s", CONF_FAMILY_STRING, CONF_PLATFORM_STRING, CONF_ARCH_STRING); #ifdef CONF_ARCH_ENDIAN_LITTLE - dbg_msg("engine", "arch is little endian"); + dbg_msg("engine", "arch is little endian"); #elif defined(CONF_ARCH_ENDIAN_BIG) - dbg_msg("engine", "arch is big endian"); + dbg_msg("engine", "arch is big endian"); #else - dbg_msg("engine", "unknown endian"); + dbg_msg("engine", "unknown endian"); #endif - // init the network - net_init(); - CNetBase::Init(); + // init the network + net_init(); + CNetBase::Init(); + } m_JobPool.Init(Jobs); @@ -104,4 +107,10 @@ public: } }; -IEngine *CreateEngine(const char *pAppname, bool Silent, int Jobs) { return new CEngine(pAppname, Silent, Jobs); } +void IEngine::RunJobBlocking(IJob *pJob) +{ + CJobPool::RunBlocking(pJob); +} + +IEngine *CreateEngine(const char *pAppname, bool Silent, int Jobs) { return new CEngine(false, pAppname, Silent, Jobs); } +IEngine *CreateTestEngine(const char *pAppname, int Jobs) { return new CEngine(true, pAppname, true, Jobs); } diff --git a/src/engine/shared/jobs.cpp b/src/engine/shared/jobs.cpp index 83c14cb55..2fb623690 100644 --- a/src/engine/shared/jobs.cpp +++ b/src/engine/shared/jobs.cpp @@ -75,9 +75,7 @@ void CJobPool::WorkerThread(void *pUser) // do the job if we have one if(pJob) { - pJob->m_Status = IJob::STATE_RUNNING; - pJob->Run(); - pJob->m_Status = IJob::STATE_DONE; + RunBlocking(pJob.get()); } } } @@ -104,3 +102,10 @@ void CJobPool::Add(std::shared_ptr pJob) lock_unlock(m_Lock); sphore_signal(&m_Semaphore); } + +void CJobPool::RunBlocking(IJob *pJob) +{ + pJob->m_Status = IJob::STATE_RUNNING; + pJob->Run(); + pJob->m_Status = IJob::STATE_DONE; +} diff --git a/src/engine/shared/jobs.h b/src/engine/shared/jobs.h index 97e98058e..3ae5b9872 100644 --- a/src/engine/shared/jobs.h +++ b/src/engine/shared/jobs.h @@ -59,5 +59,6 @@ public: void Init(int NumThreads); void Add(std::shared_ptr pJob); + static void RunBlocking(IJob *pJob); }; #endif diff --git a/src/engine/shared/protocol_ex.h b/src/engine/shared/protocol_ex.h index 81d2b444d..2310164b8 100644 --- a/src/engine/shared/protocol_ex.h +++ b/src/engine/shared/protocol_ex.h @@ -20,10 +20,11 @@ enum UNPACKMESSAGE_OK, UNPACKMESSAGE_ANSWER, - SERVERCAP_CURVERSION = 2, + SERVERCAP_CURVERSION = 3, SERVERCAPFLAG_DDNET = 1 << 0, SERVERCAPFLAG_CHATTIMEOUTCODE = 1 << 1, SERVERCAPFLAG_ANYPLAYERFLAG = 1 << 2, + SERVERCAPFLAG_PINGEX = 1 << 3, }; void RegisterUuids(class CUuidManager *pManager); diff --git a/src/engine/shared/protocol_ex_msgs.h b/src/engine/shared/protocol_ex_msgs.h index b0253c6cb..7d976645c 100644 --- a/src/engine/shared/protocol_ex_msgs.h +++ b/src/engine/shared/protocol_ex_msgs.h @@ -27,3 +27,5 @@ UUID(NETMSG_RCONTYPE, "rcon-type@ddnet.tw") UUID(NETMSG_MAP_DETAILS, "map-details@ddnet.tw") UUID(NETMSG_CAPABILITIES, "capabilities@ddnet.tw") UUID(NETMSG_CLIENTVER, "clientver@ddnet.tw") +UUID(NETMSG_PINGEX, "ping@ddnet.tw") +UUID(NETMSG_PONGEX, "pong@ddnet.tw") diff --git a/src/engine/shared/serverbrowser.cpp b/src/engine/shared/serverbrowser.cpp deleted file mode 100644 index e3a594a0d..000000000 --- a/src/engine/shared/serverbrowser.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include -#include - -// gametypes - -bool IsVanilla(const CServerInfo *pInfo) -{ - return !str_comp(pInfo->m_aGameType, "DM") || !str_comp(pInfo->m_aGameType, "TDM") || !str_comp(pInfo->m_aGameType, "CTF"); -} - -bool IsCatch(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "catch"); -} - -bool IsInsta(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "idm") || str_find_nocase(pInfo->m_aGameType, "itdm") || str_find_nocase(pInfo->m_aGameType, "ictf"); -} - -bool IsFNG(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "fng"); -} - -bool IsRace(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "race") || IsFastCap(pInfo) || IsDDRace(pInfo); -} - -bool IsFastCap(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "fastcap"); -} - -bool IsDDRace(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "ddrace") || str_find_nocase(pInfo->m_aGameType, "mkrace") || IsDDNet(pInfo); -} - -bool IsBlockInfectionZ(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "blockz") || - str_find_nocase(pInfo->m_aGameType, "infectionz"); -} - -bool IsBlockWorlds(const CServerInfo *pInfo) -{ - return (str_comp_nocase_num(pInfo->m_aGameType, "bw ", 4) == 0) || (str_comp_nocase(pInfo->m_aGameType, "bw") == 0); -} - -bool IsCity(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "city"); -} - -bool IsDDNet(const CServerInfo *pInfo) -{ - return str_find_nocase(pInfo->m_aGameType, "ddracenet") || str_find_nocase(pInfo->m_aGameType, "ddnet") || IsBlockInfectionZ(pInfo); -} - -// other - -bool Is64Player(const CServerInfo *pInfo) -{ - return str_find(pInfo->m_aGameType, "64") || str_find(pInfo->m_aName, "64") || IsDDNet(pInfo) || IsBlockInfectionZ(pInfo) || IsBlockWorlds(pInfo); -} - -bool IsPlus(const CServerInfo *pInfo) -{ - return str_find(pInfo->m_aGameType, "+"); -} diff --git a/src/engine/shared/serverinfo.cpp b/src/engine/shared/serverinfo.cpp new file mode 100644 index 000000000..e93015a7f --- /dev/null +++ b/src/engine/shared/serverinfo.cpp @@ -0,0 +1,256 @@ +#include "serverinfo.h" + +#include "json.h" +#include +#include + +static bool IsAllowedHex(char c) +{ + static const char ALLOWED[] = "0123456789abcdefABCDEF"; + for(int i = 0; i < (int)sizeof(ALLOWED) - 1; i++) + { + if(c == ALLOWED[i]) + { + return true; + } + } + return false; +} + +bool ParseCrc(unsigned int *pResult, const char *pString) +{ + if(str_length(pString) != 8) + { + return true; + } + for(int i = 0; i < 8; i++) + { + if(!IsAllowedHex(pString[i])) + { + return true; + } + } + return sscanf(pString, "%08x", pResult) != 1; +} + +bool CServerInfo2::FromJson(CServerInfo2 *pOut, const json_value *pJson) +{ + bool Result = FromJsonRaw(pOut, pJson); + if(Result) + { + return Result; + } + return pOut->Validate(); +} + +bool CServerInfo2::Validate() const +{ + bool Error = false; + Error = Error || m_MaxClients < m_MaxPlayers; + Error = Error || m_NumClients < m_NumPlayers; + Error = Error || m_MaxClients < m_NumClients; + Error = Error || m_MaxPlayers < m_NumPlayers; + return Error; +} + +bool CServerInfo2::FromJsonRaw(CServerInfo2 *pOut, const json_value *pJson) +{ + /* + TODO: What to do if we have more players than we can store? + TODO: What to do on incomplete infos? + */ + + mem_zero(pOut, sizeof(*pOut)); + bool Error; + + const json_value &ServerInfo = *pJson; + const json_value &MaxClients = ServerInfo["max_clients"]; + const json_value &MaxPlayers = ServerInfo["max_players"]; + const json_value &Passworded = ServerInfo["passworded"]; + const json_value &GameType = ServerInfo["game_type"]; + const json_value &Name = ServerInfo["name"]; + const json_value &MapName = ServerInfo["map"]["name"]; + //const json_value &MapCrc = ServerInfo["map"]["crc"]; + //const json_value &MapSize = ServerInfo["map"]["size"]; + const json_value &Version = ServerInfo["version"]; + const json_value &Clients = ServerInfo["clients"]; + Error = false; + Error = Error || MaxClients.type != json_integer; + Error = Error || MaxPlayers.type != json_integer; + Error = Error || Passworded.type != json_boolean; + Error = Error || GameType.type != json_string; + Error = Error || Name.type != json_string; + Error = Error || MapName.type != json_string; + //Error = Error || MapCrc.type != json_string; + //Error = Error || MapSize.type != json_integer; + Error = Error || Version.type != json_string; + Error = Error || Clients.type != json_array; + if(Error) + { + return true; + } + pOut->m_MaxClients = MaxClients; + pOut->m_MaxPlayers = MaxPlayers; + pOut->m_Passworded = Passworded; + str_copy(pOut->m_aGameType, GameType, sizeof(pOut->m_aGameType)); + str_copy(pOut->m_aName, Name, sizeof(pOut->m_aName)); + str_copy(pOut->m_aMapName, MapName, sizeof(pOut->m_aMapName)); + //if(ParseCrc(&pOut->m_MapCrc, MapCrc)) { return true; } + //pOut->m_MapSize = MapSize; + str_copy(pOut->m_aVersion, Version, sizeof(pOut->m_aVersion)); + + pOut->m_NumClients = 0; + pOut->m_NumPlayers = 0; + for(unsigned i = 0; i < Clients.u.array.length; i++) + { + const json_value &Client = Clients[i]; + const json_value &Name = Client["name"]; + const json_value &Clan = Client["clan"]; + const json_value &Country = Client["country"]; + const json_value &Score = Client["score"]; + //const json_value &Team = Client["team"]; + const json_value &IsPlayer = Client["is_player"]; + Error = false; + Error = Error || Name.type != json_string; + Error = Error || Clan.type != json_string; + Error = Error || Country.type != json_integer; + Error = Error || Score.type != json_integer; + //Error = Error || Team.type != json_integer; + Error = Error || IsPlayer.type != json_boolean; + if(Error) + { + return true; + } + if(i < MAX_CLIENTS) + { + CClient *pClient = &pOut->m_aClients[i]; + str_copy(pClient->m_aName, Name, sizeof(pClient->m_aName)); + str_copy(pClient->m_aClan, Clan, sizeof(pClient->m_aClan)); + pClient->m_Country = Country; + pClient->m_Score = Score; + pClient->m_IsPlayer = IsPlayer; + } + + pOut->m_NumClients++; + if((bool)IsPlayer) + { + pOut->m_NumPlayers++; + } + } + return false; +} + +bool CServerInfo2::operator==(const CServerInfo2 &Other) const +{ + bool Unequal; + Unequal = false; + Unequal = Unequal || m_MaxClients != Other.m_MaxClients; + Unequal = Unequal || m_NumClients != Other.m_NumClients; + Unequal = Unequal || m_MaxPlayers != Other.m_MaxPlayers; + Unequal = Unequal || m_NumPlayers != Other.m_NumPlayers; + Unequal = Unequal || m_Passworded != Other.m_Passworded; + Unequal = Unequal || str_comp(m_aGameType, Other.m_aGameType) != 0; + Unequal = Unequal || str_comp(m_aName, Other.m_aName) != 0; + Unequal = Unequal || str_comp(m_aMapName, Other.m_aMapName) != 0; + //Unequal = Unequal || m_MapCrc != Other.m_MapCrc; + //Unequal = Unequal || m_MapSize != Other.m_MapSize; + Unequal = Unequal || str_comp(m_aVersion, Other.m_aVersion) != 0; + if(Unequal) + { + return false; + } + for(int i = 0; i < m_NumClients; i++) + { + Unequal = false; + Unequal = Unequal || str_comp(m_aClients[i].m_aName, Other.m_aClients[i].m_aName) != 0; + Unequal = Unequal || str_comp(m_aClients[i].m_aClan, Other.m_aClients[i].m_aClan) != 0; + Unequal = Unequal || m_aClients[i].m_Country != Other.m_aClients[i].m_Country; + Unequal = Unequal || m_aClients[i].m_Score != Other.m_aClients[i].m_Score; + //Unequal = Unequal || m_aClients[i].m_Team != Other.m_aClients[i].m_Team; + Unequal = Unequal || m_aClients[i].m_IsPlayer != Other.m_aClients[i].m_IsPlayer; + if(Unequal) + { + return false; + } + } + return true; +} + +void CServerInfo2::ToJson(char *pBuffer, int BufferSize) const +{ + dbg_assert(BufferSize > 0, "empty buffer"); + /* + char aGameType[32]; + char aName[128]; + char aMapName[64]; + char aVersion[64]; + str_format(pBuffer, BufferSize, "{\"max_clients\":%d,\"max_players\":%d,\"passworded\":%s,\"game_type\":\"%s\",\"name\":\"%s\",\"map\":{\"name\":\"%s\",\"crc\":\"%08x\",\"size\":%d},\"version\":\"%s\",\"clients\":[", + m_MaxClients, + m_MaxPlayers, + JsonBool(m_Passworded), + EscapeJson(aGameType, sizeof(aGameType), m_aGameType), + EscapeJson(aName, sizeof(aName), m_aName), + EscapeJson(aMapName, sizeof(aMapName), m_aMapName), + m_MapCrc, + m_MapSize, + EscapeJson(aVersion, sizeof(aVersion), m_aVersion)); + + { + int Length = str_length(pBuffer); + pBuffer += Length; + BufferSize -= Length; + } + + for(int i = 0; i < m_NumClients; i++) + { + char aName[MAX_NAME_LENGTH * 2]; + char aClan[MAX_NAME_LENGTH * 2]; + str_format(pBuffer, BufferSize, "%s{\"name\":\"%s\",\"clan\":\"%s\",\"country\":%d,\"score\":%d,\"team\":%d}", + i != 0 ? "," : "", + EscapeJson(aName, sizeof(aName), m_aClients[i].m_aName), + EscapeJson(aClan, sizeof(aClan), m_aClients[i].m_aClan), + m_aClients[i].m_Country, + m_aClients[i].m_Score, + m_aClients[i].m_Team); + + { + int Length = str_length(pBuffer); + pBuffer += Length; + BufferSize -= Length; + } + } + + str_format(pBuffer, BufferSize, "]}"); + */ +} + +CServerInfo2::operator CServerInfo() const +{ + CServerInfo Result = {0}; + Result.m_MaxClients = m_MaxClients; + Result.m_NumClients = m_NumClients; + Result.m_MaxPlayers = m_MaxPlayers; + Result.m_NumPlayers = m_NumPlayers; + Result.m_Flags = m_Passworded ? SERVER_FLAG_PASSWORD : 0; + str_copy(Result.m_aGameType, m_aGameType, sizeof(Result.m_aGameType)); + str_copy(Result.m_aName, m_aName, sizeof(Result.m_aName)); + str_copy(Result.m_aMap, m_aMapName, sizeof(Result.m_aMap)); + //Result.m_MapCrc = m_MapCrc; + //Result.m_MapSize = m_MapSize; + str_copy(Result.m_aVersion, m_aVersion, sizeof(Result.m_aVersion)); + + for(int i = 0; i < std::min(m_NumClients, (int)MAX_CLIENTS); i++) + { + str_copy(Result.m_aClients[i].m_aName, m_aClients[i].m_aName, sizeof(Result.m_aClients[i].m_aName)); + str_copy(Result.m_aClients[i].m_aClan, m_aClients[i].m_aClan, sizeof(Result.m_aClients[i].m_aClan)); + Result.m_aClients[i].m_Country = m_aClients[i].m_Country; + Result.m_aClients[i].m_Score = m_aClients[i].m_Score; + //Result.m_aClients[i].m_Team = m_aClients[i].m_Team; + Result.m_aClients[i].m_Player = m_aClients[i].m_IsPlayer; + } + + Result.m_NumReceivedClients = std::min(m_NumClients, (int)MAX_CLIENTS); + Result.m_Latency = -1; + + return Result; +} diff --git a/src/engine/shared/serverinfo.h b/src/engine/shared/serverinfo.h new file mode 100644 index 000000000..68188a57b --- /dev/null +++ b/src/engine/shared/serverinfo.h @@ -0,0 +1,48 @@ +#ifndef ENGINE_SHARED_SERVERINFO_H +#define ENGINE_SHARED_SERVERINFO_H + +#include "protocol.h" + +typedef struct _json_value json_value; +class CServerInfo; + +class CServerInfo2 +{ +public: + class CClient + { + public: + char m_aName[MAX_NAME_LENGTH]; + char m_aClan[MAX_CLAN_LENGTH]; + int m_Country; + int m_Score; + //int m_Team; + bool m_IsPlayer; + }; + + CClient m_aClients[MAX_CLIENTS]; + int m_MaxClients; + int m_NumClients; // Indirectly serialized. + int m_MaxPlayers; + int m_NumPlayers; // Not serialized. + bool m_Passworded; + char m_aGameType[16]; + char m_aName[64]; + char m_aMapName[32]; + //unsigned int m_MapCrc; + //int m_MapSize; + char m_aVersion[32]; + + bool operator==(const CServerInfo2 &Other) const; + bool operator!=(const CServerInfo2 &Other) const { return !(*this == Other); } + static bool FromJson(CServerInfo2 *pOut, const json_value *pJson); + static bool FromJsonRaw(CServerInfo2 *pOut, const json_value *pJson); + bool Validate() const; + void ToJson(char *pBuffer, int BufferSize) const; + + operator CServerInfo() const; +}; + +bool ParseCrc(unsigned int *pResult, const char *pString); + +#endif // ENGINE_SHARED_SERVERINFO_H diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index 9b6f9a292..4f9a21eb8 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -594,7 +594,10 @@ void IStorage::StripPathAndExtension(const char *pFilename, char *pBuffer, int B str_copy(pBuffer, pExtractedName, Length); } -IStorage *CreateStorage(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments) { return CStorage::Create(pApplicationName, StorageType, NumArgs, ppArguments); } +IStorage *CreateStorage(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments) +{ + return CStorage::Create(pApplicationName, StorageType, NumArgs, ppArguments); +} IStorage *CreateLocalStorage() { @@ -610,3 +613,13 @@ IStorage *CreateLocalStorage() } return pStorage; } +IStorage *CreateTempStorage(const char *pDirectory) +{ + CStorage *pStorage = new CStorage(); + if(!pStorage) + { + return nullptr; + } + pStorage->AddPath(pDirectory); + return pStorage; +} diff --git a/src/engine/sqlite.h b/src/engine/sqlite.h new file mode 100644 index 000000000..562e8d436 --- /dev/null +++ b/src/engine/sqlite.h @@ -0,0 +1,28 @@ +#ifndef ENGINE_SQLITE_H +#define ENGINE_SQLITE_H +#include + +typedef struct sqlite3 sqlite3; +typedef struct sqlite3_stmt sqlite3_stmt; +class IConsole; +class IStorage; + +class CSqliteDeleter +{ +public: + void operator()(sqlite3 *pSqlite); +}; +class CSqliteStmtDeleter +{ +public: + void operator()(sqlite3_stmt *pStmt); +}; +typedef std::unique_ptr CSqlite; +typedef std::unique_ptr CSqliteStmt; + +int SqliteHandleError(IConsole *pConsole, int Error, sqlite3 *pSqlite, const char *pContext); +#define SQLITE_HANDLE_ERROR(x) SqliteHandleError(pConsole, x, &*pSqlite, #x) + +CSqlite SqliteOpen(IConsole *pConsole, IStorage *pStorage, const char *pPath); +CSqliteStmt SqlitePrepare(IConsole *pConsole, sqlite3 *pSqlite, const char *pStatement); +#endif // ENGINE_SQLITE_H diff --git a/src/engine/storage.h b/src/engine/storage.h index 80d9c43d5..5326d091c 100644 --- a/src/engine/storage.h +++ b/src/engine/storage.h @@ -44,5 +44,6 @@ public: extern IStorage *CreateStorage(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments); extern IStorage *CreateLocalStorage(); +extern IStorage *CreateTempStorage(const char *pDirectory); #endif diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp index 5f13a81d5..27580a38a 100644 --- a/src/game/client/components/menus_browser.cpp +++ b/src/game/client/components/menus_browser.cpp @@ -31,6 +31,27 @@ static const int g_OffsetColPlayers = g_OffsetColMap + 3; static const int g_OffsetColPing = g_OffsetColPlayers + 3; static const int g_OffsetColVersion = g_OffsetColPing + 3; +void FormatServerbrowserPing(char *pBuffer, int BufferLength, const CServerInfo *pInfo) +{ + if(!pInfo->m_LatencyIsEstimated) + { + str_format(pBuffer, BufferLength, "%d", pInfo->m_Latency); + return; + } + static const char *LOCATION_NAMES[CServerInfo::NUM_LOCS] = { + "", // LOC_UNKNOWN + "AFR", // LOC_AFRICA // Localize("AFR") + "ASI", // LOC_ASIA // Localize("ASI") + "AUS", // LOC_AUSTRALIA // Localize("AUS") + "EUR", // LOC_EUROPE // Localize("EUR") + "NA", // LOC_NORTH_AMERICA // Localize("NA") + "SA", // LOC_SOUTH_AMERICA // Localize("SA") + "CHN", // LOC_CHINA // Localize("CHN") + }; + dbg_assert(0 <= pInfo->m_Location && pInfo->m_Location < CServerInfo::NUM_LOCS, "location out of range"); + str_format(pBuffer, BufferLength, "%s", Localize(LOCATION_NAMES[pInfo->m_Location])); +} + void CMenus::RenderServerbrowserServerList(CUIRect View) { CUIRect Headers; @@ -157,8 +178,8 @@ void CMenus::RenderServerbrowserServerList(CUIRect View) { CUIRect MsgBox = View; - if(m_ActivePage == PAGE_INTERNET && ServerBrowser()->IsRefreshingMasters()) - UI()->DoLabelScaled(&MsgBox, Localize("Refreshing master servers"), 16.0f, 0); + if(ServerBrowser()->IsGettingServerlist()) + UI()->DoLabelScaled(&MsgBox, Localize("Getting serverlist from masterserver"), 16.0f, 0); else if(!ServerBrowser()->NumServers()) UI()->DoLabelScaled(&MsgBox, Localize("No servers found"), 16.0f, 0); else if(ServerBrowser()->NumServers() && !NumServers) @@ -405,7 +426,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View) } else if(ID == COL_PING) { - str_format(aTemp, sizeof(aTemp), "%i", pItem->m_Latency); + FormatServerbrowserPing(aTemp, sizeof(aTemp), pItem); if(g_Config.m_UiColorizePing) { ColorRGBA rgb = color_cast(ColorHSLA((300.0f - clamp(pItem->m_Latency, 0, 300)) / 1000.0f, 1.0f, 0.5f)); @@ -1017,14 +1038,34 @@ void CMenus::RenderServerbrowserServerDetail(CUIRect View) { CUIRect Button; ServerDetails.HSplitBottom(20.0f, &ServerDetails, &Button); - Button.VSplitLeft(5.0f, 0, &Button); + CUIRect ButtonAddFav; + CUIRect ButtonLeakIp; + Button.VSplitMid(&ButtonAddFav, &ButtonLeakIp); + ButtonAddFav.VSplitLeft(5.0f, 0, &ButtonAddFav); static int s_AddFavButton = 0; - if(DoButton_CheckBox(&s_AddFavButton, Localize("Favorite"), pSelectedServer->m_Favorite, &Button)) + static int s_LeakIpButton = 0; + if(DoButton_CheckBox(&s_AddFavButton, Localize("Favorite"), pSelectedServer->m_Favorite, &ButtonAddFav)) { if(pSelectedServer->m_Favorite) + { ServerBrowser()->RemoveFavorite(pSelectedServer->m_NetAddr); + } else + { ServerBrowser()->AddFavorite(pSelectedServer->m_NetAddr); + if(g_Config.m_UiPage == PAGE_LAN) + { + ServerBrowser()->FavoriteAllowPing(pSelectedServer->m_NetAddr, true); + } + } + } + if(pSelectedServer->m_Favorite) + { + bool IpLeak = ServerBrowser()->IsFavoritePingAllowed(pSelectedServer->m_NetAddr); + if(DoButton_CheckBox(&s_LeakIpButton, Localize("Leak IP"), IpLeak, &ButtonLeakIp)) + { + ServerBrowser()->FavoriteAllowPing(pSelectedServer->m_NetAddr, !IpLeak); + } } } @@ -1048,7 +1089,7 @@ void CMenus::RenderServerbrowserServerDetail(CUIRect View) TextRender()->TextEx(&Cursor, pSelectedServer->m_aGameType, -1); char aTemp[16]; - str_format(aTemp, sizeof(aTemp), "%d", pSelectedServer->m_Latency); + FormatServerbrowserPing(aTemp, sizeof(aTemp), pSelectedServer); RightColumn.HSplitTop(15.0f, &Row, &RightColumn); TextRender()->SetCursor(&Cursor, Row.x, Row.y + (15.f - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); Cursor.m_LineWidth = Row.w; diff --git a/src/test/fs.cpp b/src/test/fs.cpp index fc10c7ed4..9b03f0773 100644 --- a/src/test/fs.cpp +++ b/src/test/fs.cpp @@ -12,3 +12,39 @@ TEST(Filesystem, CreateCloseDelete) EXPECT_FALSE(io_close(File)); EXPECT_FALSE(fs_remove(Info.m_aFilename)); } + +TEST(Filesystem, CreateDeleteDirectory) +{ + CTestInfo Info; + char aFilename[128]; + str_format(aFilename, sizeof(aFilename), "%s/test.txt", Info.m_aFilename); + + EXPECT_FALSE(fs_makedir(Info.m_aFilename)); + IOHANDLE File = io_open(aFilename, IOFLAG_WRITE); + ASSERT_TRUE(File); + EXPECT_FALSE(io_close(File)); + + // Directory removal fails if there are any files left in the directory. + EXPECT_TRUE(fs_removedir(Info.m_aFilename)); + + EXPECT_FALSE(fs_remove(aFilename)); + EXPECT_FALSE(fs_removedir(Info.m_aFilename)); +} + +TEST(Filesystem, CantDeleteDirectoryWithRemove) +{ + CTestInfo Info; + EXPECT_FALSE(fs_makedir(Info.m_aFilename)); + EXPECT_TRUE(fs_remove(Info.m_aFilename)); // Cannot remove directory with file removal function. + EXPECT_FALSE(fs_removedir(Info.m_aFilename)); +} + +TEST(Filesystem, CantDeleteFileWithRemoveDirectory) +{ + CTestInfo Info; + IOHANDLE File = io_open(Info.m_aFilename, IOFLAG_WRITE); + ASSERT_TRUE(File); + EXPECT_FALSE(io_close(File)); + EXPECT_TRUE(fs_removedir(Info.m_aFilename)); // Cannot remove file with directory removal function. + EXPECT_FALSE(fs_remove(Info.m_aFilename)); +} diff --git a/src/test/jobs.cpp b/src/test/jobs.cpp index e8d567ba7..aaa12c497 100644 --- a/src/test/jobs.cpp +++ b/src/test/jobs.cpp @@ -23,6 +23,10 @@ protected: { m_Pool.Add(std::move(pJob)); } + void RunBlocking(IJob *pJob) + { + CJobPool::RunBlocking(pJob); + } }; class CJob : public IJob @@ -44,6 +48,15 @@ TEST_F(Jobs, Simple) Add(std::make_shared([] {})); } +TEST_F(Jobs, RunBlocking) +{ + int Result = 0; + CJob Job([&] { Result = 1; }); + EXPECT_EQ(Result, 0); + RunBlocking(&Job); + EXPECT_EQ(Result, 1); +} + TEST_F(Jobs, Wait) { SEMAPHORE sphore; diff --git a/src/test/secure_random.cpp b/src/test/secure_random.cpp new file mode 100644 index 000000000..9bd58fb0b --- /dev/null +++ b/src/test/secure_random.cpp @@ -0,0 +1,35 @@ +#include "test.h" +#include + +#include + +TEST(SecureRandom, Fill) +{ + unsigned int Bits = 0; + while(~Bits) + { + unsigned int Random; + secure_random_fill(&Random, sizeof(Random)); + Bits |= Random; + } +} + +TEST(SecureRandom, Below1) +{ + EXPECT_EQ(secure_rand_below(1), 0); +} + +TEST(SecureRandom, Below) +{ + int BOUNDS[] = {2, 3, 4, 5, 10, 100, 127, 128, 129}; + for(unsigned i = 0; i < sizeof(BOUNDS) / sizeof(BOUNDS[0]); i++) + { + int Below = BOUNDS[i]; + for(int j = 0; j < 10; j++) + { + int Random = secure_rand_below(Below); + EXPECT_GE(Random, 0); + EXPECT_LT(Random, Below); + } + } +} diff --git a/src/test/serverbrowser.cpp b/src/test/serverbrowser.cpp new file mode 100644 index 000000000..eb0b607d0 --- /dev/null +++ b/src/test/serverbrowser.cpp @@ -0,0 +1,106 @@ +#include + +#include +#include +#include +#include +#include +#include + +TEST(ServerBrowser, PingCache) +{ + CTestInfo Info; + IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT); + IStorage *pStorage = Info.CreateTestStorage(); + IServerBrowserPingCache *pPingCache = CreateServerBrowserPingCache(pConsole, pStorage); + + const IServerBrowserPingCache::CEntry *pEntries; + int NumEntries; + + pPingCache->GetPingCache(&pEntries, &NumEntries); + EXPECT_EQ(NumEntries, 0); + + pPingCache->Load(); + + pPingCache->GetPingCache(&pEntries, &NumEntries); + EXPECT_EQ(NumEntries, 0); + + NETADDR Localhost4, Localhost4Tw, Localhost6, Localhost6Tw; + ASSERT_FALSE(net_addr_from_str(&Localhost4, "127.0.0.1")); + ASSERT_FALSE(net_addr_from_str(&Localhost4Tw, "127.0.0.1:8303")); + ASSERT_FALSE(net_addr_from_str(&Localhost6, "[::1]")); + ASSERT_FALSE(net_addr_from_str(&Localhost6Tw, "[::1]:8304")); + EXPECT_LT(net_addr_comp(&Localhost4, &Localhost6), 0); + + // Newer pings overwrite older. + pPingCache->CachePing(Localhost4Tw, 123); + pPingCache->CachePing(Localhost4Tw, 234); + pPingCache->CachePing(Localhost4Tw, 345); + pPingCache->CachePing(Localhost4Tw, 456); + pPingCache->CachePing(Localhost4Tw, 567); + pPingCache->CachePing(Localhost4Tw, 678); + pPingCache->CachePing(Localhost4Tw, 789); + pPingCache->CachePing(Localhost4Tw, 890); + pPingCache->CachePing(Localhost4Tw, 901); + pPingCache->CachePing(Localhost4Tw, 135); + pPingCache->CachePing(Localhost4Tw, 246); + pPingCache->CachePing(Localhost4Tw, 357); + pPingCache->CachePing(Localhost4Tw, 468); + pPingCache->CachePing(Localhost4Tw, 579); + pPingCache->CachePing(Localhost4Tw, 680); + pPingCache->CachePing(Localhost4Tw, 791); + pPingCache->CachePing(Localhost4Tw, 802); + pPingCache->CachePing(Localhost4Tw, 913); + + pPingCache->GetPingCache(&pEntries, &NumEntries); + EXPECT_EQ(NumEntries, 1); + if(NumEntries >= 1) + { + EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0); + EXPECT_EQ(pEntries[0].m_Ping, 913); + } + + pPingCache->CachePing(Localhost4Tw, 234); + pPingCache->CachePing(Localhost6Tw, 345); + pPingCache->GetPingCache(&pEntries, &NumEntries); + EXPECT_EQ(NumEntries, 2); + if(NumEntries >= 2) + { + EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0); + EXPECT_TRUE(net_addr_comp(&pEntries[1].m_Addr, &Localhost6) == 0); + EXPECT_EQ(pEntries[0].m_Ping, 234); + EXPECT_EQ(pEntries[1].m_Ping, 345); + } + + // Port doesn't matter for overwriting. + pPingCache->CachePing(Localhost4, 1337); + pPingCache->GetPingCache(&pEntries, &NumEntries); + EXPECT_EQ(NumEntries, 2); + if(NumEntries >= 2) + { + EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0); + EXPECT_TRUE(net_addr_comp(&pEntries[1].m_Addr, &Localhost6) == 0); + EXPECT_EQ(pEntries[0].m_Ping, 1337); + EXPECT_EQ(pEntries[1].m_Ping, 345); + } + + delete pPingCache; + pPingCache = CreateServerBrowserPingCache(pConsole, pStorage); + + // Persistence. + pPingCache->Load(); + pPingCache->GetPingCache(&pEntries, &NumEntries); + EXPECT_EQ(NumEntries, 2); + if(NumEntries >= 2) + { + EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0); + EXPECT_TRUE(net_addr_comp(&pEntries[1].m_Addr, &Localhost6) == 0); + EXPECT_EQ(pEntries[0].m_Ping, 1337); + EXPECT_EQ(pEntries[1].m_Ping, 345); + } + + delete pPingCache; + delete pStorage; + + Info.DeleteTestStorageFilesOnSuccess(); +} diff --git a/src/test/serverinfo.cpp b/src/test/serverinfo.cpp new file mode 100644 index 000000000..b8ef86a6c --- /dev/null +++ b/src/test/serverinfo.cpp @@ -0,0 +1,149 @@ +#include + +#include +#include + +#include + +TEST(ServerInfo, ParseLocation) +{ + int Result; + EXPECT_TRUE(CServerInfo::ParseLocation(&Result, "xx")); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "an")); + EXPECT_EQ(Result, CServerInfo::LOC_UNKNOWN); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "af")); + EXPECT_EQ(Result, CServerInfo::LOC_AFRICA); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "eu-n")); + EXPECT_EQ(Result, CServerInfo::LOC_EUROPE); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "na")); + EXPECT_EQ(Result, CServerInfo::LOC_NORTH_AMERICA); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "sa")); + EXPECT_EQ(Result, CServerInfo::LOC_SOUTH_AMERICA); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "as:e")); + EXPECT_EQ(Result, CServerInfo::LOC_ASIA); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "as:cn")); + EXPECT_EQ(Result, CServerInfo::LOC_CHINA); + EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "oc")); + EXPECT_EQ(Result, CServerInfo::LOC_AUSTRALIA); +} + +/* +static CServerInfo2 Parse(const char *pJson) +{ + CServerInfo2 Out = {0}; + json_value *pParsed = json_parse(pJson, str_length(pJson)); + EXPECT_TRUE(pParsed); + if(pParsed) + { + EXPECT_FALSE(CServerInfo2::FromJson(&Out, pParsed)); + } + return Out; +} + +TEST(ServerInfo, Empty) +{ + static const char EMPTY[] = "{\"max_clients\":0,\"max_players\":0,\"passworded\":false,\"game_type\":\"\",\"name\":\"\",\"map\":{\"name\":\"\",\"crc\":\"00000000\",\"size\":0},\"version\":\"\",\"clients\":[]}"; + + CServerInfo2 Empty = {0}; + EXPECT_EQ(Parse(EMPTY), Empty); + char aBuf[1024]; + Empty.ToJson(aBuf, sizeof(aBuf)); + EXPECT_STREQ(aBuf, EMPTY); +} + +CServerInfo2 SomethingImpl() +{ + CServerInfo2 Something = {0}; + + str_copy(Something.m_aClients[0].m_aName, "Learath2", sizeof(Something.m_aClients[0].m_aName)); + Something.m_aClients[0].m_Country = -1; + Something.m_aClients[0].m_Score = -1; + Something.m_aClients[0].m_Team = 1; + + str_copy(Something.m_aClients[1].m_aName, "deen", sizeof(Something.m_aClients[1].m_aName)); + str_copy(Something.m_aClients[1].m_aClan, "DDNet", sizeof(Something.m_aClients[1].m_aClan)); + Something.m_aClients[1].m_Country = 276; + Something.m_aClients[1].m_Score = 0; + Something.m_aClients[1].m_Team = 0; + + Something.m_MaxClients = 16; + Something.m_NumClients = 2; + Something.m_MaxPlayers = 8; + Something.m_NumPlayers = 1; + Something.m_Passworded = true; + str_copy(Something.m_aGameType, "DM", sizeof(Something.m_aGameType)); + str_copy(Something.m_aName, "unnamed server", sizeof(Something.m_aName)); + str_copy(Something.m_aMapName, "dm1", sizeof(Something.m_aMapName)); + Something.m_MapCrc = 0xf2159e6e; + Something.m_MapSize = 5805; + str_copy(Something.m_aVersion, "0.6.4", sizeof(Something.m_aVersion)); + + return Something; +} + +static const CServerInfo2 s_Something = SomethingImpl(); + +TEST(ServerInfo, Something) +{ + static const char SOMETHING[] = "{\"max_clients\":16,\"max_players\":8,\"passworded\":true,\"game_type\":\"DM\",\"name\":\"unnamed server\",\"map\":{\"name\":\"dm1\",\"crc\":\"f2159e6e\",\"size\":5805},\"version\":\"0.6.4\",\"clients\":[{\"name\":\"Learath2\",\"clan\":\"\",\"country\":-1,\"score\":-1,\"team\":1},{\"name\":\"deen\",\"clan\":\"DDNet\",\"country\":276,\"score\":0,\"team\":0}]}"; + + EXPECT_EQ(Parse(SOMETHING), s_Something); + + char aBuf[1024]; + s_Something.ToJson(aBuf, sizeof(aBuf)); + EXPECT_EQ(Parse(aBuf), s_Something); +} + +TEST(ServerInfo, ToServerBrowserServerInfo) +{ + CServerInfo Sbsi = s_Something; + + EXPECT_EQ(Sbsi.m_MaxClients, s_Something.m_MaxClients); + EXPECT_EQ(Sbsi.m_NumClients, s_Something.m_NumClients); + EXPECT_EQ(Sbsi.m_MaxPlayers, s_Something.m_MaxPlayers); + EXPECT_EQ(Sbsi.m_NumPlayers, s_Something.m_NumPlayers); + EXPECT_EQ((Sbsi.m_Flags&SERVER_FLAG_PASSWORD) != 0, s_Something.m_Passworded); + EXPECT_STREQ(Sbsi.m_aGameType, s_Something.m_aGameType); + EXPECT_STREQ(Sbsi.m_aName, s_Something.m_aName); + EXPECT_STREQ(Sbsi.m_aMap, s_Something.m_aMapName); + EXPECT_EQ(Sbsi.m_MapCrc, s_Something.m_MapCrc); + EXPECT_EQ(Sbsi.m_MapSize, s_Something.m_MapSize); + EXPECT_STREQ(Sbsi.m_aVersion, s_Something.m_aVersion); + + for(int i = 0; i < Sbsi.m_NumClients; i++) + { + EXPECT_STREQ(Sbsi.m_aClients[i].m_aName, s_Something.m_aClients[i].m_aName); + EXPECT_STREQ(Sbsi.m_aClients[i].m_aClan, s_Something.m_aClients[i].m_aClan); + EXPECT_EQ(Sbsi.m_aClients[i].m_Country, s_Something.m_aClients[i].m_Country); + EXPECT_EQ(Sbsi.m_aClients[i].m_Score, s_Something.m_aClients[i].m_Score); + EXPECT_EQ(Sbsi.m_aClients[i].m_Player, s_Something.m_aClients[i].m_Team != 0); + } +} +*/ + +static unsigned int ParseCrcOrDeadbeef(const char *pString) +{ + unsigned int Result; + if(ParseCrc(&Result, pString)) + { + Result = 0xdeadbeef; + } + return Result; +} + +TEST(ServerInfo, Crc) +{ + EXPECT_EQ(ParseCrcOrDeadbeef("00000000"), 0); + EXPECT_EQ(ParseCrcOrDeadbeef("00000001"), 1); + EXPECT_EQ(ParseCrcOrDeadbeef("12345678"), 0x12345678); + EXPECT_EQ(ParseCrcOrDeadbeef("9abcdef0"), 0x9abcdef0); + + EXPECT_EQ(ParseCrcOrDeadbeef(""), 0xdeadbeef); + EXPECT_EQ(ParseCrcOrDeadbeef("a"), 0xdeadbeef); + EXPECT_EQ(ParseCrcOrDeadbeef("x"), 0xdeadbeef); + EXPECT_EQ(ParseCrcOrDeadbeef("ç"), 0xdeadbeef); + EXPECT_EQ(ParseCrcOrDeadbeef("😢"), 0xdeadbeef); + EXPECT_EQ(ParseCrcOrDeadbeef("0"), 0xdeadbeef); + EXPECT_EQ(ParseCrcOrDeadbeef("000000000"), 0xdeadbeef); + EXPECT_EQ(ParseCrcOrDeadbeef("00000000x"), 0xdeadbeef); +} diff --git a/src/test/test.cpp b/src/test/test.cpp index 761d661f7..997eda25a 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -2,6 +2,7 @@ #include #include +#include CTestInfo::CTestInfo() { @@ -11,9 +12,106 @@ CTestInfo::CTestInfo() pTestInfo->test_case_name(), pTestInfo->name(), pid()); } +IStorage *CTestInfo::CreateTestStorage() +{ + bool Error = fs_makedir(m_aFilename); + EXPECT_FALSE(Error); + if(Error) + { + return nullptr; + } + return CreateTempStorage(m_aFilename); +} + +class CTestInfoPath +{ +public: + bool m_IsDirectory; + char m_aData[MAX_PATH_LENGTH]; + + bool operator<(const CTestInfoPath &Other) const + { + if(m_IsDirectory != Other.m_IsDirectory) + { + return m_IsDirectory < Other.m_IsDirectory; + } + return str_comp(m_aData, Other.m_aData) < 0; + } +}; + +class CTestCollectData +{ +public: + char m_aCurrentDir[MAX_PATH_LENGTH]; + std::vector *m_paEntries; +}; + +int TestCollect(const char *pName, int IsDir, int Unused, void *pUser) +{ + CTestCollectData *pData = (CTestCollectData *)pUser; + + if(str_comp(pName, ".") == 0 || str_comp(pName, "..") == 0) + { + return 0; + } + + CTestInfoPath Path; + Path.m_IsDirectory = IsDir; + str_format(Path.m_aData, sizeof(Path.m_aData), "%s/%s", pData->m_aCurrentDir, pName); + pData->m_paEntries->push_back(Path); + if(Path.m_IsDirectory) + { + CTestCollectData DataRecursive; + str_copy(DataRecursive.m_aCurrentDir, Path.m_aData, sizeof(DataRecursive.m_aCurrentDir)); + DataRecursive.m_paEntries = pData->m_paEntries; + fs_listdir(DataRecursive.m_aCurrentDir, TestCollect, 0, &DataRecursive); + } + return 0; +} + +void CTestInfo::DeleteTestStorageFilesOnSuccess() +{ + if(::testing::Test::HasFailure()) + { + return; + } + std::vector aEntries; + CTestCollectData Data; + str_copy(Data.m_aCurrentDir, m_aFilename, sizeof(Data.m_aCurrentDir)); + Data.m_paEntries = &aEntries; + fs_listdir(Data.m_aCurrentDir, TestCollect, 0, &Data); + + CTestInfoPath Path; + Path.m_IsDirectory = true; + str_copy(Path.m_aData, Data.m_aCurrentDir, sizeof(Path.m_aData)); + aEntries.push_back(Path); + + // Sorts directories after files. + std::sort(aEntries.begin(), aEntries.end()); + + // Don't delete too many files. + ASSERT_LE(aEntries.size(), 10); + for(auto &Entry : aEntries) + { + if(Entry.m_IsDirectory) + { + ASSERT_FALSE(fs_removedir(Entry.m_aData)); + } + else + { + ASSERT_FALSE(fs_remove(Entry.m_aData)); + } + } +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); net_init(); + if(secure_random_init()) + { + fprintf(stderr, "random init failed\n"); + return 1; + } return RUN_ALL_TESTS(); } diff --git a/src/test/test.h b/src/test/test.h index 71f13c9b0..27bc36bd8 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -1,9 +1,13 @@ #ifndef TEST_TEST_H #define TEST_TEST_H +class IStorage; + class CTestInfo { public: CTestInfo(); + IStorage *CreateTestStorage(); + void DeleteTestStorageFilesOnSuccess(); char m_aFilename[64]; }; #endif // TEST_TEST_H