Merge pull request #7923 from Robyt3/Http-DDNet-Info-Sha-Check

Write DDNet info file only when it changed, initialize HTTP later on client launch, show message box on error, refactoring
This commit is contained in:
Dennis Felsing 2024-02-05 12:31:06 +00:00 committed by GitHub
commit 68ee8a758b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 116 additions and 130 deletions

View file

@ -84,7 +84,6 @@ CClient::CClient() :
for(auto &DemoRecorder : m_aDemoRecorder)
DemoRecorder = CDemoRecorder(&m_SnapshotDelta);
m_LastRenderTime = time_get();
IStorage::FormatTmpPath(m_aDDNetInfoTmp, sizeof(m_aDDNetInfoTmp), DDNET_INFO_FILE);
mem_zero(m_aInputs, sizeof(m_aInputs));
mem_zero(m_aapSnapshots, sizeof(m_aapSnapshots));
for(auto &SnapshotStorage : m_aSnapshotStorage)
@ -2054,7 +2053,7 @@ void CClient::FinishMapDownload()
}
}
void CClient::ResetDDNetInfo()
void CClient::ResetDDNetInfoTask()
{
if(m_pDDNetInfoTask)
{
@ -2063,56 +2062,50 @@ void CClient::ResetDDNetInfo()
}
}
bool CClient::IsDDNetInfoChanged()
{
IOHANDLE OldFile = m_pStorage->OpenFile(DDNET_INFO_FILE, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_SAVE);
if(!OldFile)
return true;
IOHANDLE NewFile = m_pStorage->OpenFile(m_aDDNetInfoTmp, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_SAVE);
if(NewFile)
{
char aOldData[4096];
char aNewData[4096];
unsigned OldBytes;
unsigned NewBytes;
do
{
OldBytes = io_read(OldFile, aOldData, sizeof(aOldData));
NewBytes = io_read(NewFile, aNewData, sizeof(aNewData));
if(OldBytes != NewBytes || mem_comp(aOldData, aNewData, OldBytes) != 0)
{
io_close(NewFile);
io_close(OldFile);
return true;
}
} while(OldBytes > 0);
io_close(NewFile);
}
io_close(OldFile);
return false;
}
void CClient::FinishDDNetInfo()
{
ResetDDNetInfo();
if(IsDDNetInfoChanged())
if(m_ServerBrowser.DDNetInfoSha256() == m_pDDNetInfoTask->Sha256())
{
m_pStorage->RenameFile(m_aDDNetInfoTmp, DDNET_INFO_FILE, IStorage::TYPE_SAVE);
LoadDDNetInfo();
if(m_ServerBrowser.GetCurrentType() == IServerBrowser::TYPE_INTERNET || m_ServerBrowser.GetCurrentType() == IServerBrowser::TYPE_FAVORITES)
m_ServerBrowser.Refresh(m_ServerBrowser.GetCurrentType());
log_debug("client/info", "DDNet info already up-to-date");
return;
}
else
char aTempFilename[IO_MAX_PATH_LENGTH];
IStorage::FormatTmpPath(aTempFilename, sizeof(aTempFilename), DDNET_INFO_FILE);
IOHANDLE File = Storage()->OpenFile(aTempFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!File)
{
m_pStorage->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE);
log_error("client/info", "Failed to open temporary DDNet info '%s' for writing", aTempFilename);
return;
}
unsigned char *pResult;
size_t ResultLength;
m_pDDNetInfoTask->Result(&pResult, &ResultLength);
dbg_assert(pResult != nullptr, "Invalid info task state");
bool Error = io_write(File, pResult, ResultLength) != ResultLength;
Error |= io_close(File) != 0;
if(Error)
{
log_error("client/info", "Error writing temporary DDNet info to file '%s'", aTempFilename);
return;
}
if(Storage()->FileExists(DDNET_INFO_FILE, IStorage::TYPE_SAVE) && !Storage()->RemoveFile(DDNET_INFO_FILE, IStorage::TYPE_SAVE))
{
log_error("client/info", "Failed to remove old DDNet info '%s'", DDNET_INFO_FILE);
Storage()->RemoveFile(aTempFilename, IStorage::TYPE_SAVE);
return;
}
if(!Storage()->RenameFile(aTempFilename, DDNET_INFO_FILE, IStorage::TYPE_SAVE))
{
log_error("client/info", "Failed to rename temporary DDNet info '%s' to '%s'", aTempFilename, DDNET_INFO_FILE);
Storage()->RemoveFile(aTempFilename, IStorage::TYPE_SAVE);
return;
}
log_debug("client/info", "Loading new DDNet info");
LoadDDNetInfo();
}
typedef std::tuple<int, int, int> TVersion;
@ -2590,16 +2583,13 @@ void CClient::Update()
if(m_pDDNetInfoTask)
{
if(m_pDDNetInfoTask->State() == EHttpState::DONE)
{
FinishDDNetInfo();
else if(m_pDDNetInfoTask->State() == EHttpState::ERROR)
{
Storage()->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE);
ResetDDNetInfo();
ResetDDNetInfoTask();
}
else if(m_pDDNetInfoTask->State() == EHttpState::ABORTED)
else if(m_pDDNetInfoTask->State() == EHttpState::ERROR || m_pDDNetInfoTask->State() == EHttpState::ABORTED)
{
Storage()->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE);
m_pDDNetInfoTask = NULL;
ResetDDNetInfoTask();
}
}
@ -2685,8 +2675,6 @@ void CClient::InitInterfaces()
m_DemoEditor.Init(m_pGameClient->NetVersion(), &m_SnapshotDelta, m_pConsole, m_pStorage);
m_Http.Init(std::chrono::seconds{1});
m_ServerBrowser.SetBaseInfo(&m_aNetClient[CONN_CONTACT], m_pGameClient->NetVersion());
#if defined(CONF_AUTOUPDATE)
@ -2757,6 +2745,14 @@ void CClient::Run()
}
#endif
if(!m_Http.Init(std::chrono::seconds{1}))
{
const char *pErrorMessage = "Failed to initialize the HTTP client.";
log_error("client", "%s", pErrorMessage);
ShowMessageBox("HTTP Error", pErrorMessage);
return;
}
// init text render
m_pTextRender = Kernel()->RequestInterface<IEngineTextRender>();
m_pTextRender->Init();
@ -3002,11 +2998,6 @@ void CClient::Run()
s_SavedConfig = true;
}
if(m_pStorage->FileExists(m_aDDNetInfoTmp, IStorage::TYPE_SAVE))
{
m_pStorage->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE);
}
if(m_vWarnings.empty() && !GameClient()->IsDisplayingWarning())
break;
}
@ -4572,7 +4563,7 @@ void CClient::RequestDDNetInfo()
}
// Use ipv4 so we can know the ingame ip addresses of players before they join game servers
m_pDDNetInfoTask = HttpGetFile(aUrl, Storage(), m_aDDNetInfoTmp, IStorage::TYPE_SAVE);
m_pDDNetInfoTask = HttpGet(aUrl);
m_pDDNetInfoTask->Timeout(CTimeout{10000, 0, 500, 10});
m_pDDNetInfoTask->IpResolve(IPRESOLVE::V4);
Http()->Run(m_pDDNetInfoTask);

View file

@ -157,7 +157,6 @@ class CClient : public IClient, public CDemoPlayer::IListener
SHA256_DIGEST m_MapDetailsSha256 = SHA256_ZEROED;
char m_aMapDetailsUrl[256] = "";
char m_aDDNetInfoTmp[64];
std::shared_ptr<CHttpRequest> m_pDDNetInfoTask = nullptr;
// time
@ -353,8 +352,7 @@ public:
void FinishMapDownload();
void RequestDDNetInfo() override;
void ResetDDNetInfo();
bool IsDDNetInfoChanged();
void ResetDDNetInfoTask();
void FinishDDNetInfo();
void LoadDDNetInfo();

View file

@ -66,9 +66,6 @@ CServerBrowser::CServerBrowser() :
m_BroadcastTime = 0;
secure_random_fill(m_aTokenSeed, sizeof(m_aTokenSeed));
m_pDDNetInfo = nullptr;
m_DDNetInfoUpdateTime = 0;
CleanUp();
}
@ -1272,7 +1269,6 @@ const json_value *CServerBrowser::LoadDDNetInfo()
UpdateServerCommunity(&m_ppServerlist[i]->m_Info);
UpdateServerRank(&m_ppServerlist[i]->m_Info);
}
m_DDNetInfoUpdateTime = time_get();
return m_pDDNetInfo;
}
@ -1281,7 +1277,12 @@ void CServerBrowser::LoadDDNetInfoJson()
void *pBuf;
unsigned Length;
if(!m_pStorage->ReadFile(DDNET_INFO_FILE, IStorage::TYPE_SAVE, &pBuf, &Length))
{
m_DDNetInfoSha256 = SHA256_ZEROED;
return;
}
m_DDNetInfoSha256 = sha256(pBuf, Length);
json_value_free(m_pDDNetInfo);
json_settings JsonSettings{};

View file

@ -3,6 +3,7 @@
#ifndef ENGINE_CLIENT_SERVERBROWSER_H
#define ENGINE_CLIENT_SERVERBROWSER_H
#include <base/hash.h>
#include <base/system.h>
#include <engine/console.h>
@ -252,7 +253,7 @@ public:
unsigned CurrentCommunitiesHash() const override;
bool DDNetInfoAvailable() const override { return m_pDDNetInfo != nullptr; }
int64_t DDNetInfoUpdateTime() const override { return m_DDNetInfoUpdateTime; }
SHA256_DIGEST DDNetInfoSha256() const override { return m_DDNetInfoSha256; }
CFavoriteCommunityFilterList &FavoriteCommunitiesFilter() override { return m_FavoriteCommunitiesFilter; }
CExcludedCommunityFilterList &CommunitiesFilter() override { return m_CommunitiesFilter; }
@ -311,8 +312,8 @@ private:
CExcludedCommunityCountryFilterList m_CountriesFilter;
CExcludedCommunityTypeFilterList m_TypesFilter;
json_value *m_pDDNetInfo;
int64_t m_DDNetInfoUpdateTime;
json_value *m_pDDNetInfo = nullptr;
SHA256_DIGEST m_DDNetInfoSha256 = SHA256_ZEROED;
CServerEntry *m_pFirstReqServer; // request list
CServerEntry *m_pLastReqServer;

View file

@ -306,7 +306,7 @@ public:
virtual unsigned CurrentCommunitiesHash() const = 0;
virtual bool DDNetInfoAvailable() const = 0;
virtual int64_t DDNetInfoUpdateTime() const = 0;
virtual SHA256_DIGEST DDNetInfoSha256() const = 0;
virtual IFilterList &FavoriteCommunitiesFilter() = 0;
virtual IFilterList &CommunitiesFilter() = 0;

View file

@ -67,26 +67,15 @@ bool HttpHasIpresolveBug()
CHttpRequest::CHttpRequest(const char *pUrl)
{
str_copy(m_aUrl, pUrl);
sha256_init(&m_ActualSha256);
sha256_init(&m_ActualSha256Ctx);
}
CHttpRequest::~CHttpRequest()
{
m_ResponseLength = 0;
if(!m_WriteToFile)
{
m_BufferSize = 0;
free(m_pBuffer);
m_pBuffer = nullptr;
}
dbg_assert(m_File == nullptr, "HTTP request file was not closed");
free(m_pBuffer);
curl_slist_free_all((curl_slist *)m_pHeaders);
m_pHeaders = nullptr;
if(m_pBody)
{
m_BodyLength = 0;
free(m_pBody);
m_pBody = nullptr;
}
free(m_pBody);
}
bool CHttpRequest::BeforeInit()
@ -95,14 +84,14 @@ bool CHttpRequest::BeforeInit()
{
if(fs_makedir_rec_for(m_aDestAbsolute) < 0)
{
dbg_msg("http", "i/o error, cannot create folder for: %s", m_aDest);
log_error("http", "i/o error, cannot create folder for: %s", m_aDest);
return false;
}
m_File = io_open(m_aDestAbsolute, IOFLAG_WRITE);
if(!m_File)
{
dbg_msg("http", "i/o error, cannot open file: %s", m_aDest);
log_error("http", "i/o error, cannot open file: %s", m_aDest);
return false;
}
}
@ -224,7 +213,7 @@ size_t CHttpRequest::OnData(char *pData, size_t DataSize)
return 0;
}
sha256_update(&m_ActualSha256, pData, DataSize);
sha256_update(&m_ActualSha256Ctx, pData, DataSize);
if(!m_WriteToFile)
{
@ -277,34 +266,38 @@ void CHttpRequest::OnCompletionInternal(std::optional<unsigned int> Result)
if(Code != CURLE_OK)
{
if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE)
dbg_msg("http", "%s failed. libcurl error (%u): %s", m_aUrl, Code, m_aErr);
{
log_error("http", "%s failed. libcurl error (%u): %s", m_aUrl, Code, m_aErr);
}
State = (Code == CURLE_ABORTED_BY_CALLBACK) ? EHttpState::ABORTED : EHttpState::ERROR;
}
else
{
if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL)
dbg_msg("http", "task done: %s", m_aUrl);
{
log_info("http", "task done: %s", m_aUrl);
}
State = EHttpState::DONE;
}
}
else
{
dbg_msg("http", "%s failed. internal error: %s", m_aUrl, m_aErr);
log_error("http", "%s failed. internal error: %s", m_aUrl, m_aErr);
State = EHttpState::ERROR;
}
if(State == EHttpState::DONE && m_ExpectedSha256 != SHA256_ZEROED)
if(State == EHttpState::DONE)
{
const SHA256_DIGEST ActualSha256 = sha256_finish(&m_ActualSha256);
if(ActualSha256 != m_ExpectedSha256)
m_ActualSha256 = sha256_finish(&m_ActualSha256Ctx);
if(m_ExpectedSha256 != SHA256_ZEROED && m_ActualSha256 != m_ExpectedSha256)
{
if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE)
{
char aActualSha256[SHA256_MAXSTRSIZE];
sha256_str(ActualSha256, aActualSha256, sizeof(aActualSha256));
sha256_str(m_ActualSha256, aActualSha256, sizeof(aActualSha256));
char aExpectedSha256[SHA256_MAXSTRSIZE];
sha256_str(m_ExpectedSha256, aExpectedSha256, sizeof(aExpectedSha256));
dbg_msg("http", "SHA256 mismatch: got=%s, expected=%s, url=%s", aActualSha256, aExpectedSha256, m_aUrl);
log_error("http", "SHA256 mismatch: got=%s, expected=%s, url=%s", aActualSha256, aExpectedSha256, m_aUrl);
}
State = EHttpState::ERROR;
}
@ -314,9 +307,10 @@ void CHttpRequest::OnCompletionInternal(std::optional<unsigned int> Result)
{
if(m_File && io_close(m_File) != 0)
{
dbg_msg("http", "i/o error, cannot close file: %s", m_aDest);
log_error("http", "i/o error, cannot close file: %s", m_aDest);
State = EHttpState::ERROR;
}
m_File = nullptr;
if(State == EHttpState::ERROR || State == EHttpState::ABORTED)
{
@ -422,7 +416,7 @@ void CHttp::RunLoop()
std::unique_lock Lock(m_Lock);
if(curl_global_init(CURL_GLOBAL_DEFAULT))
{
dbg_msg("http", "curl_global_init failed");
log_error("http", "curl_global_init failed");
m_State = CHttp::ERROR;
m_Cv.notify_all();
return;
@ -431,7 +425,7 @@ void CHttp::RunLoop()
m_pMultiH = curl_multi_init();
if(!m_pMultiH)
{
dbg_msg("http", "curl_multi_init failed");
log_error("http", "curl_multi_init failed");
m_State = CHttp::ERROR;
m_Cv.notify_all();
return;
@ -440,19 +434,18 @@ void CHttp::RunLoop()
// print curl version
{
curl_version_info_data *pVersion = curl_version_info(CURLVERSION_NOW);
dbg_msg("http", "libcurl version %s (compiled = " LIBCURL_VERSION ")", pVersion->version);
log_info("http", "libcurl version %s (compiled = " LIBCURL_VERSION ")", pVersion->version);
}
m_State = CHttp::RUNNING;
m_Cv.notify_all();
dbg_msg("http", "running");
Lock.unlock();
while(m_State == CHttp::RUNNING)
{
static int NextTimeout = std::numeric_limits<int>::max();
static int s_NextTimeout = std::numeric_limits<int>::max();
int Events = 0;
CURLMcode mc = curl_multi_poll(m_pMultiH, NULL, 0, NextTimeout, &Events);
const CURLMcode PollCode = curl_multi_poll(m_pMultiH, nullptr, 0, s_NextTimeout, &Events);
// We may have been woken up for a shutdown
if(m_Shutdown)
@ -461,7 +454,7 @@ void CHttp::RunLoop()
if(!m_ShutdownTime.has_value())
{
m_ShutdownTime = Now + m_ShutdownDelay;
NextTimeout = m_ShutdownDelay.count();
s_NextTimeout = m_ShutdownDelay.count();
}
else if(m_ShutdownTime < Now || m_RunningRequests.empty())
{
@ -469,36 +462,36 @@ void CHttp::RunLoop()
}
}
if(mc != CURLM_OK)
if(PollCode != CURLM_OK)
{
Lock.lock();
dbg_msg("http", "Failed multi wait: %s", curl_multi_strerror(mc));
log_error("http", "Failed multi wait: %s", curl_multi_strerror(PollCode));
m_State = CHttp::ERROR;
break;
}
mc = curl_multi_perform(m_pMultiH, &Events);
if(mc != CURLM_OK)
const CURLMcode PerformCode = curl_multi_perform(m_pMultiH, &Events);
if(PerformCode != CURLM_OK)
{
Lock.lock();
dbg_msg("http", "Failed multi perform: %s", curl_multi_strerror(mc));
log_error("http", "Failed multi perform: %s", curl_multi_strerror(PerformCode));
m_State = CHttp::ERROR;
break;
}
struct CURLMsg *m;
while((m = curl_multi_info_read(m_pMultiH, &Events)))
struct CURLMsg *pMsg;
while((pMsg = curl_multi_info_read(m_pMultiH, &Events)))
{
if(m->msg == CURLMSG_DONE)
if(pMsg->msg == CURLMSG_DONE)
{
auto RequestIt = m_RunningRequests.find(m->easy_handle);
auto RequestIt = m_RunningRequests.find(pMsg->easy_handle);
dbg_assert(RequestIt != m_RunningRequests.end(), "Running handle not added to map");
auto pRequest = std::move(RequestIt->second);
m_RunningRequests.erase(RequestIt);
pRequest->OnCompletionInternal(m->data.result);
curl_multi_remove_handle(m_pMultiH, m->easy_handle);
curl_easy_cleanup(m->easy_handle);
pRequest->OnCompletionInternal(pMsg->data.result);
curl_multi_remove_handle(m_pMultiH, pMsg->easy_handle);
curl_easy_cleanup(pMsg->easy_handle);
}
}
@ -511,7 +504,7 @@ void CHttp::RunLoop()
{
auto &pRequest = NewRequests.front();
if(g_Config.m_DbgCurl)
dbg_msg("http", "task: %s %s", CHttpRequest::GetRequestType(pRequest->m_Type), pRequest->m_aUrl);
log_debug("http", "task: %s %s", CHttpRequest::GetRequestType(pRequest->m_Type), pRequest->m_aUrl);
CURL *pEH = curl_easy_init();
if(!pEH)
@ -520,8 +513,7 @@ void CHttp::RunLoop()
if(!pRequest->ConfigureHandle(pEH))
goto error_configure;
mc = curl_multi_add_handle(m_pMultiH, pEH);
if(mc != CURLM_OK)
if(curl_multi_add_handle(m_pMultiH, pEH) != CURLM_OK)
goto error_configure;
m_RunningRequests.emplace(pEH, std::move(pRequest));
@ -532,7 +524,7 @@ void CHttp::RunLoop()
error_configure:
curl_easy_cleanup(pEH);
error_init:
dbg_msg("http", "failed to start new request");
log_error("http", "failed to start new request");
Lock.lock();
m_State = CHttp::ERROR;
break;

View file

@ -88,7 +88,8 @@ class CHttpRequest : public IHttpRequest
int64_t m_MaxResponseSize = -1;
REQUEST m_Type = REQUEST::GET;
SHA256_CTX m_ActualSha256;
SHA256_DIGEST m_ActualSha256 = SHA256_ZEROED;
SHA256_CTX m_ActualSha256Ctx;
SHA256_DIGEST m_ExpectedSha256 = SHA256_ZEROED;
bool m_WriteToFile = false;
@ -197,6 +198,8 @@ public:
void Result(unsigned char **ppResult, size_t *pResultLength) const;
json_value *ResultJson() const;
const SHA256_DIGEST &Sha256() const { return m_ActualSha256; }
};
inline std::unique_ptr<CHttpRequest> HttpHead(const char *pUrl)

View file

@ -506,7 +506,7 @@ protected:
static void ConchainUiPageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
struct SCommunityCache
{
int64_t m_UpdateTime = 0;
SHA256_DIGEST m_InfoSha256 = SHA256_ZEROED;
int m_LastPage = 0;
unsigned m_SelectedCommunitiesHash;
std::vector<const CCommunity *> m_vpSelectedCommunities;
@ -560,7 +560,7 @@ protected:
std::vector<SCommunityIcon> m_vCommunityIcons;
std::deque<std::shared_ptr<CCommunityIconLoadJob>> m_CommunityIconLoadJobs;
std::deque<std::shared_ptr<CCommunityIconDownloadJob>> m_CommunityIconDownloadJobs;
int64_t m_CommunityIconsUpdateTime = 0;
SHA256_DIGEST m_CommunityIconsInfoSha256 = SHA256_ZEROED;
static int CommunityIconScan(const char *pName, int IsDir, int DirType, void *pUser);
const SCommunityIcon *FindCommunityIcon(const char *pCommunityId);
bool LoadCommunityIconFile(const char *pPath, int DirType, CImageInfo &Info, SHA256_DIGEST &Sha256);

View file

@ -1840,8 +1840,8 @@ void CMenus::UpdateCommunityCache(bool Force)
ServerBrowser()->Refresh(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + IServerBrowser::TYPE_FAVORITE_COMMUNITY_1, true);
}
if(!Force && m_CommunityCache.m_UpdateTime != 0 &&
m_CommunityCache.m_UpdateTime == ServerBrowser()->DDNetInfoUpdateTime() &&
if(!Force && m_CommunityCache.m_InfoSha256 != SHA256_ZEROED &&
m_CommunityCache.m_InfoSha256 == ServerBrowser()->DDNetInfoSha256() &&
!CurrentCommunitiesChanged && !PageChanged)
{
return;
@ -1849,7 +1849,7 @@ void CMenus::UpdateCommunityCache(bool Force)
ServerBrowser()->CleanFilters();
m_CommunityCache.m_UpdateTime = ServerBrowser()->DDNetInfoUpdateTime();
m_CommunityCache.m_InfoSha256 = ServerBrowser()->DDNetInfoSha256();
m_CommunityCache.m_LastPage = g_Config.m_UiPage;
m_CommunityCache.m_SelectedCommunitiesHash = CommunitiesHash;
m_CommunityCache.m_vpSelectedCommunities = ServerBrowser()->CurrentCommunities();
@ -2055,9 +2055,9 @@ void CMenus::UpdateCommunityIcons()
}
// Rescan for changed communities only when necessary
if(!ServerBrowser()->DDNetInfoAvailable() || (m_CommunityIconsUpdateTime != 0 && m_CommunityIconsUpdateTime == ServerBrowser()->DDNetInfoUpdateTime()))
if(!ServerBrowser()->DDNetInfoAvailable() || (m_CommunityIconsInfoSha256 != SHA256_ZEROED && m_CommunityIconsInfoSha256 == ServerBrowser()->DDNetInfoSha256()))
return;
m_CommunityIconsUpdateTime = ServerBrowser()->DDNetInfoUpdateTime();
m_CommunityIconsInfoSha256 = ServerBrowser()->DDNetInfoSha256();
// Remove icons for removed communities
auto RemovalIterator = m_vCommunityIcons.begin();