diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index ec2fb277081..28ce42a4718 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -3675,7 +3675,8 @@ 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) + + if(net_addr_from_url(&Addr, pResult->GetString(0), nullptr, 0) != 0 && net_addr_from_str(&Addr, pResult->GetString(0)) != 0) { char aBuf[128]; str_format(aBuf, sizeof(aBuf), "invalid address '%s'", pResult->GetString(0)); diff --git a/src/engine/client/favorites.cpp b/src/engine/client/favorites.cpp index ab3c5822303..0e713a51512 100644 --- a/src/engine/client/favorites.cpp +++ b/src/engine/client/favorites.cpp @@ -42,7 +42,21 @@ void CFavorites::OnConfigSave(IConfigManager *pConfigManager) { char aAddr[NETADDR_MAXSTRSIZE]; char aBuffer[128]; - net_addr_str(&Entry.m_aAddrs[i], aAddr, sizeof(aAddr), true); + net_addr_str(&Entry.m_aAddrs[i], aBuffer, sizeof(aBuffer), true); + + if(Entry.m_aAddrs[i].type & NETTYPE_TW7) + { + str_format( + aAddr, + sizeof(aAddr), + "tw-0.7+udp://%s", + aBuffer); + } + else + { + str_copy(aAddr, aBuffer); + } + if(!Entry.m_AllowPing) { str_format(aBuffer, sizeof(aBuffer), "add_favorite %s", aAddr); @@ -135,16 +149,14 @@ void CFavorites::Add(const NETADDR *pAddrs, int NumAddrs) // other favorite. for(int i = 0; i < NumAddrs; i++) { - NETADDR Addr = pAddrs[i]; - Addr.type &= ~(NETTYPE_TW7); - CEntry *pEntry = Entry(Addr); + CEntry *pEntry = Entry(pAddrs[i]); if(pEntry == nullptr) { continue; } for(int j = 0; j < pEntry->m_NumAddrs; j++) { - if(pEntry->m_aAddrs[j] == Addr) + if(pEntry->m_aAddrs[j] == pAddrs[i]) { pEntry->m_aAddrs[j] = pEntry->m_aAddrs[pEntry->m_NumAddrs - 1]; pEntry->m_NumAddrs -= 1; @@ -164,10 +176,8 @@ void CFavorites::Add(const NETADDR *pAddrs, int NumAddrs) NewEntry.m_NumAddrs = std::min(NumAddrs, (int)std::size(NewEntry.m_aAddrs)); for(int i = 0; i < NewEntry.m_NumAddrs; i++) { - NETADDR Addr = pAddrs[i]; - Addr.type &= ~(NETTYPE_TW7); - NewEntry.m_aAddrs[i] = Addr; - m_ByAddr[Addr] = m_vEntries.size(); + NewEntry.m_aAddrs[i] = pAddrs[i]; + m_ByAddr[pAddrs[i]] = m_vEntries.size(); } NewEntry.m_AllowPing = false; m_vEntries.push_back(NewEntry); @@ -211,10 +221,7 @@ void CFavorites::AllEntries(const CEntry **ppEntries, int *pNumEntries) CFavorites::CEntry *CFavorites::Entry(const NETADDR &Addr) { - NETADDR AddrAnyProtocol = Addr; - AddrAnyProtocol.type &= ~(NETTYPE_TW7); - - auto Entry = m_ByAddr.find(AddrAnyProtocol); + auto Entry = m_ByAddr.find(Addr); if(Entry == m_ByAddr.end()) { return nullptr; @@ -224,10 +231,7 @@ CFavorites::CEntry *CFavorites::Entry(const NETADDR &Addr) const CFavorites::CEntry *CFavorites::Entry(const NETADDR &Addr) const { - NETADDR AddrAnyProtocol = Addr; - AddrAnyProtocol.type &= ~(NETTYPE_TW7); - - auto Entry = m_ByAddr.find(AddrAnyProtocol); + auto Entry = m_ByAddr.find(Addr); if(Entry == m_ByAddr.end()) { return nullptr; diff --git a/src/engine/shared/config.cpp b/src/engine/shared/config.cpp index 28f0e30e844..57ddfccd528 100644 --- a/src/engine/shared/config.cpp +++ b/src/engine/shared/config.cpp @@ -323,8 +323,8 @@ void CConfigManager::Init() #undef MACRO_CONFIG_STR m_pConsole->Register("reset", "s[config-name]", CFGFLAG_SERVER | CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Reset, this, "Reset a config to its default value"); - m_pConsole->Register("toggle", "s[config-option] i[value 1] i[value 2]", CFGFLAG_SERVER | CFGFLAG_CLIENT, Con_Toggle, this, "Toggle config value"); - m_pConsole->Register("+toggle", "s[config-option] i[value 1] i[value 2]", CFGFLAG_CLIENT, Con_ToggleStroke, this, "Toggle config value via keypress"); + m_pConsole->Register("toggle", "s[config-option] s[value 1] s[value 2]", CFGFLAG_SERVER | CFGFLAG_CLIENT, Con_Toggle, this, "Toggle config value"); + m_pConsole->Register("+toggle", "s[config-option] s[value 1] s[value 2]", CFGFLAG_CLIENT, Con_ToggleStroke, this, "Toggle config value via keypress"); } void CConfigManager::Reset(const char *pScriptName) diff --git a/src/engine/shared/http.cpp b/src/engine/shared/http.cpp index c69da07dcda..e2e1928e9f4 100644 --- a/src/engine/shared/http.cpp +++ b/src/engine/shared/http.cpp @@ -126,6 +126,11 @@ bool CHttpRequest::ConfigureHandle(void *pHandle) { curl_easy_setopt(pH, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)m_MaxResponseSize); } + if(m_IfModifiedSince >= 0) + { + curl_easy_setopt(pH, CURLOPT_TIMEVALUE_LARGE, (curl_off_t)m_IfModifiedSince); + curl_easy_setopt(pH, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE); + } // ‘CURLOPT_PROTOCOLS’ is deprecated: since 7.85.0. Use CURLOPT_PROTOCOLS_STR // Wait until all platforms have 7.85.0 @@ -378,7 +383,11 @@ void CHttpRequest::OnCompletionInternal(void *pHandle, unsigned int Result) // or other threads may try to access the result of a completed HTTP request, // before the result has been initialized/updated in OnCompletion. OnCompletion(State); - m_State = State; + { + std::unique_lock WaitLock(m_WaitMutex); + m_State = State; + } + m_WaitCondition.notify_all(); } void CHttpRequest::WriteToFile(IStorage *pStorage, const char *pDest, int StorageType) @@ -402,18 +411,11 @@ void CHttpRequest::Header(const char *pNameColonValue) void CHttpRequest::Wait() { - using namespace std::chrono_literals; - - // This is so uncommon that polling just might work - for(;;) - { + std::unique_lock Lock(m_WaitMutex); + m_WaitCondition.wait(Lock, [this]() { EHttpState State = m_State.load(std::memory_order_seq_cst); - if(State != EHttpState::QUEUED && State != EHttpState::RUNNING) - { - return; - } - std::this_thread::sleep_for(10ms); - } + return State != EHttpState::QUEUED && State != EHttpState::RUNNING; + }); } void CHttpRequest::Result(unsigned char **ppResult, size_t *pResultLength) const @@ -604,6 +606,10 @@ void CHttp::RunLoop() goto error_configure; } + { + std::unique_lock WaitLock(pRequest->m_WaitMutex); + pRequest->m_State = EHttpState::RUNNING; + } m_RunningRequests.emplace(pEH, std::move(pRequest)); NewRequests.pop_front(); continue; diff --git a/src/engine/shared/http.h b/src/engine/shared/http.h index 86e53fac9fd..27ecbb05d4a 100644 --- a/src/engine/shared/http.h +++ b/src/engine/shared/http.h @@ -86,6 +86,7 @@ class CHttpRequest : public IHttpRequest CTimeout m_Timeout = CTimeout{0, 0, 0, 0}; int64_t m_MaxResponseSize = -1; + int64_t m_IfModifiedSince = -1; REQUEST m_Type = REQUEST::GET; SHA256_DIGEST m_ActualSha256 = SHA256_ZEROED; @@ -116,6 +117,8 @@ class CHttpRequest : public IHttpRequest char m_aErr[256]; // 256 == CURL_ERROR_SIZE std::atomic m_State{EHttpState::QUEUED}; std::atomic m_Abort{false}; + std::mutex m_WaitMutex; + std::condition_variable m_WaitCondition; int m_StatusCode = 0; bool m_HeadersEnded = false; @@ -150,6 +153,7 @@ class CHttpRequest : public IHttpRequest void Timeout(CTimeout Timeout) { m_Timeout = Timeout; } void MaxResponseSize(int64_t MaxResponseSize) { m_MaxResponseSize = MaxResponseSize; } + void IfModifiedSince(int64_t IfModifiedSince) { m_IfModifiedSince = IfModifiedSince; } void LogProgress(HTTPLOG LogProgress) { m_LogProgress = LogProgress; } void IpResolve(IPRESOLVE IpResolve) { m_IpResolve = IpResolve; } void FailOnErrorStatus(bool FailOnErrorStatus) { m_FailOnErrorStatus = FailOnErrorStatus; } diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index ac3d7d2f28e..02038f76264 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -578,6 +578,14 @@ class CStorage : public IStorage return pResult; } + bool RetrieveTimes(const char *pFilename, int Type, time_t *pCreated, time_t *pModified) override + { + dbg_assert(Type == TYPE_ABSOLUTE || (Type >= TYPE_SAVE && Type < m_NumPaths), "Type invalid"); + + char aBuffer[IO_MAX_PATH_LENGTH]; + return fs_file_time(GetPath(Type, pFilename, aBuffer, sizeof(aBuffer)), pCreated, pModified) == 0; + } + bool CalculateHashes(const char *pFilename, int Type, SHA256_DIGEST *pSha256, unsigned *pCrc) override { dbg_assert(pSha256 != nullptr || pCrc != nullptr, "At least one output argument required"); diff --git a/src/engine/storage.h b/src/engine/storage.h index 9380d0dd5e2..eeb146588ca 100644 --- a/src/engine/storage.h +++ b/src/engine/storage.h @@ -54,6 +54,7 @@ class IStorage : public IInterface virtual bool FolderExists(const char *pFilename, int Type) = 0; virtual bool ReadFile(const char *pFilename, int Type, void **ppResult, unsigned *pResultLen) = 0; virtual char *ReadFileStr(const char *pFilename, int Type) = 0; + virtual bool RetrieveTimes(const char *pFilename, int Type, time_t *pCreated, time_t *pModified) = 0; virtual bool CalculateHashes(const char *pFilename, int Type, SHA256_DIGEST *pSha256, unsigned *pCrc = nullptr) = 0; virtual bool FindFile(const char *pFilename, const char *pPath, int Type, char *pBuffer, int BufferSize) = 0; virtual size_t FindFiles(const char *pFilename, const char *pPath, int Type, std::set *pEntries) = 0; diff --git a/src/game/client/components/maplayers.cpp b/src/game/client/components/maplayers.cpp index d1f06b958a7..b79ebbdb308 100644 --- a/src/game/client/components/maplayers.cpp +++ b/src/game/client/components/maplayers.cpp @@ -24,13 +24,9 @@ using namespace std::chrono_literals; -CMapLayers::CMapLayers(int t, bool OnlineOnly) +CMapLayers::CMapLayers(int Type, bool OnlineOnly) { - m_Type = t; - m_pLayers = 0; - m_CurrentLocalTick = 0; - m_LastLocalTick = 0; - m_EnvelopeUpdate = false; + m_Type = Type; m_OnlineOnly = OnlineOnly; } @@ -45,17 +41,6 @@ CCamera *CMapLayers::GetCurCamera() return &m_pClient->m_Camera; } -void CMapLayers::EnvelopeUpdate() -{ - if(Client()->State() == IClient::STATE_DEMOPLAYBACK) - { - const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo(); - m_CurrentLocalTick = pInfo->m_CurrentTick; - m_LastLocalTick = pInfo->m_CurrentTick; - m_EnvelopeUpdate = true; - } -} - void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser) { CMapLayers *pThis = (CMapLayers *)pUser; @@ -75,73 +60,31 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, if(EnvelopePoints.NumPoints() == 0) return; - const auto TickToNanoSeconds = std::chrono::nanoseconds(1s) / (int64_t)pThis->Client()->GameTickSpeed(); - static std::chrono::nanoseconds s_Time{0}; static auto s_LastLocalTime = time_get_nanoseconds(); - if(pThis->Client()->State() == IClient::STATE_DEMOPLAYBACK) + if(pThis->m_OnlineOnly && (pItem->m_Version < 2 || pItem->m_Synchronized)) { - const IDemoPlayer::CInfo *pInfo = pThis->DemoPlayer()->BaseInfo(); - - if(!pInfo->m_Paused || pThis->m_EnvelopeUpdate) + if(pThis->m_pClient->m_Snap.m_pGameInfoObj) { - if(pThis->m_CurrentLocalTick != pInfo->m_CurrentTick) - { - pThis->m_LastLocalTick = pThis->m_CurrentLocalTick; - pThis->m_CurrentLocalTick = pInfo->m_CurrentTick; - } - if(pItem->m_Version < 2 || pItem->m_Synchronized) - { - if(pThis->m_pClient->m_Snap.m_pGameInfoObj) - { - // get the lerp of the current tick and prev - int MinTick = pThis->Client()->PrevGameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick; - int CurTick = pThis->Client()->GameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick; - s_Time = std::chrono::nanoseconds((int64_t)(mix( - 0, - (CurTick - MinTick), - (double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) * - TickToNanoSeconds.count())) + - MinTick * TickToNanoSeconds; - } - } - else - { - int MinTick = pThis->m_LastLocalTick; - s_Time = std::chrono::nanoseconds((int64_t)(mix(0, - pThis->m_CurrentLocalTick - MinTick, - (double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) * - TickToNanoSeconds.count())) + - MinTick * TickToNanoSeconds; - } + // get the lerp of the current tick and prev + const auto TickToNanoSeconds = std::chrono::nanoseconds(1s) / (int64_t)pThis->Client()->GameTickSpeed(); + const int MinTick = pThis->Client()->PrevGameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick; + const int CurTick = pThis->Client()->GameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick; + s_Time = std::chrono::nanoseconds((int64_t)(mix( + 0, + (CurTick - MinTick), + (double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) * + TickToNanoSeconds.count())) + + MinTick * TickToNanoSeconds; } - CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Result, Channels); } else { - if(pThis->m_OnlineOnly && (pItem->m_Version < 2 || pItem->m_Synchronized)) - { - if(pThis->m_pClient->m_Snap.m_pGameInfoObj) // && !(pThis->m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_PAUSED)) - { - // get the lerp of the current tick and prev - int MinTick = pThis->Client()->PrevGameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick; - int CurTick = pThis->Client()->GameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick; - s_Time = std::chrono::nanoseconds((int64_t)(mix( - 0, - (CurTick - MinTick), - (double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) * - TickToNanoSeconds.count())) + - MinTick * TickToNanoSeconds; - } - } - else - { - auto CurTime = time_get_nanoseconds(); - s_Time += CurTime - s_LastLocalTime; - s_LastLocalTime = CurTime; - } - CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Result, Channels); + const auto CurTime = time_get_nanoseconds(); + s_Time += CurTime - s_LastLocalTime; + s_LastLocalTime = CurTime; } + CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Result, Channels); } void FillTmpTile(SGraphicTile *pTmpTile, SGraphicTileTexureCoords *pTmpTex, unsigned char Flags, unsigned char Index, int x, int y, const ivec2 &Offset, int Scale, CMapItemGroup *pGroup) diff --git a/src/game/client/components/maplayers.h b/src/game/client/components/maplayers.h index 6b729219c0c..0065c237be0 100644 --- a/src/game/client/components/maplayers.h +++ b/src/game/client/components/maplayers.h @@ -30,11 +30,8 @@ class CMapLayers : public CComponent CLayers *m_pLayers; CMapImages *m_pImages; - int m_Type; - int m_CurrentLocalTick; - int m_LastLocalTick; - bool m_EnvelopeUpdate; + int m_Type; bool m_OnlineOnly; struct STileLayerVisuals @@ -162,8 +159,6 @@ class CMapLayers : public CComponent void RenderKillTileBorder(int LayerIndex, const ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup); void RenderQuadLayer(int LayerIndex, CMapItemLayerQuads *pQuadLayer, CMapItemGroup *pGroup, bool ForceRender = false); - void EnvelopeUpdate(); - static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser); }; diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 139b72dbeaf..73656f6d41d 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -931,6 +931,11 @@ void CMenus::OnInit() // load community icons m_vCommunityIcons.clear(); Storage()->ListDirectory(IStorage::TYPE_ALL, "communityicons", CommunityIconScan, this); + + // Quad for the direction arrows above the player + m_DirectionQuadContainerIndex = Graphics()->CreateQuadContainer(false); + RenderTools()->QuadContainerAddSprite(m_DirectionQuadContainerIndex, 0.f, 0.f, 22.f); + Graphics()->QuadContainerUpload(m_DirectionQuadContainerIndex); } void CMenus::OnConsoleInit() diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index 721c0a0ec5c..c82b45763f7 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -96,6 +96,8 @@ class CMenus : public CComponent bool m_SkinListNeedsUpdate = false; + int m_DirectionQuadContainerIndex; + // menus_settings_assets.cpp public: struct SCustomItem diff --git a/src/game/client/components/menus_demo.cpp b/src/game/client/components/menus_demo.cpp index 9da978a1f34..dd671c14227 100644 --- a/src/game/client/components/menus_demo.cpp +++ b/src/game/client/components/menus_demo.cpp @@ -90,8 +90,6 @@ void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek) else DemoPlayer()->SeekPercent(PositionToSeek); m_pClient->m_SuppressEvents = false; - m_pClient->m_MapLayersBackground.EnvelopeUpdate(); - m_pClient->m_MapLayersForeground.EnvelopeUpdate(); if(!DemoPlayer()->BaseInfo()->m_Paused && PositionToSeek == 1.0f) DemoPlayer()->Pause(); } @@ -103,8 +101,6 @@ void CMenus::DemoSeekTick(IDemoPlayer::ETickOffset TickOffset) DemoPlayer()->SeekTick(TickOffset); m_pClient->m_SuppressEvents = false; DemoPlayer()->Pause(); - m_pClient->m_MapLayersBackground.EnvelopeUpdate(); - m_pClient->m_MapLayersForeground.EnvelopeUpdate(); } void CMenus::RenderDemoPlayer(CUIRect MainView) diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index e8fbea83cd6..e82cddb1149 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -707,21 +707,16 @@ void CMenus::RenderSettingsTee(CUIRect MainView) static std::vector s_vSkinList; static std::vector s_vSkinListHelper; static std::vector s_vFavoriteSkinListHelper; - static int s_SkinCount = 0; static CListBox s_ListBox; // be nice to the CPU - static auto s_SkinLastRebuildTime = time_get_nanoseconds(); - const auto CurTime = time_get_nanoseconds(); - if(m_SkinListNeedsUpdate || m_pClient->m_Skins.Num() != s_SkinCount || m_SkinFavoritesChanged || (m_pClient->m_Skins.IsDownloadingSkins() && (CurTime - s_SkinLastRebuildTime > 500ms))) + static std::chrono::nanoseconds s_SkinLastRefreshTime = m_pClient->m_Skins.LastRefreshTime(); + if(m_SkinListNeedsUpdate || m_SkinFavoritesChanged || s_SkinLastRefreshTime != m_pClient->m_Skins.LastRefreshTime()) { - s_SkinLastRebuildTime = CurTime; + s_SkinLastRefreshTime = m_pClient->m_Skins.LastRefreshTime(); s_vSkinList.clear(); s_vSkinListHelper.clear(); s_vFavoriteSkinListHelper.clear(); - // set skin count early, since Find of the skin class might load - // a downloading skin - s_SkinCount = m_pClient->m_Skins.Num(); m_SkinFavoritesChanged = false; auto &&SkinNotFiltered = [&](const CSkin *pSkinToBeSelected) { @@ -2789,6 +2784,107 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) static CButtonContainer s_AuthedColor, s_SameClanColor; DoLine_ColorPicker(&s_AuthedColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Authed name color in scoreboard"), &g_Config.m_ClAuthedPlayerColor, GreenDefault, false); DoLine_ColorPicker(&s_SameClanColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Same clan color in scoreboard"), &g_Config.m_ClSameClanColor, GreenDefault, false); + + // ***** Name Plate Preview ***** // + RightView.HSplitTop(HeadlineHeight, &Label, &RightView); + Ui()->DoLabel(&Label, Localize("Preview"), HeadlineFontSize, TEXTALIGN_ML); + RightView.HSplitTop(2 * MarginSmall, nullptr, &RightView); + + CTeeRenderInfo TeeRenderInfo; + TeeRenderInfo.Apply(m_pClient->m_Skins.Find(g_Config.m_ClPlayerSkin)); + TeeRenderInfo.ApplyColors(g_Config.m_ClPlayerUseCustomColor, g_Config.m_ClPlayerColorBody, g_Config.m_ClPlayerColorFeet); + TeeRenderInfo.m_Size = 64.0f; + + const vec2 TeeRenderPos = vec2(RightView.x + RightView.w / 2, RightView.y + RightView.h / 2); + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeRenderInfo, 0, vec2(1.0f, 0.0f), TeeRenderPos); + + const float FontSize = 18.0f + 20.0f * g_Config.m_ClNameplatesSize / 100.0f; + const float FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNameplatesClanSize / 100.0f; + const ColorRGBA Rgb = g_Config.m_ClNameplatesTeamcolors ? m_pClient->GetDDTeamColor(13, 0.75f) : TextRender()->DefaultTextColor(); + float YOffset = TeeRenderPos.y - 38; + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE); + + if(g_Config.m_ClShowDirection) + { + const float ShowDirectionImgSize = 22.0f; + YOffset -= ShowDirectionImgSize; + const vec2 ShowDirectionPos = vec2(TeeRenderPos.x - 11.0f, YOffset); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + + // Left + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id); + Graphics()->QuadsSetRotation(pi); + Graphics()->RenderQuadContainerAsSprite(m_DirectionQuadContainerIndex, 0, ShowDirectionPos.x - 30.f, ShowDirectionPos.y); + + // Right + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id); + Graphics()->QuadsSetRotation(0); + Graphics()->RenderQuadContainerAsSprite(m_DirectionQuadContainerIndex, 0, ShowDirectionPos.x + 30.f, ShowDirectionPos.y); + + // Jump + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id); + Graphics()->QuadsSetRotation(pi * 3 / 2); + Graphics()->RenderQuadContainerAsSprite(m_DirectionQuadContainerIndex, 0, ShowDirectionPos.x, ShowDirectionPos.y); + + Graphics()->QuadsSetRotation(0); + } + + if(g_Config.m_ClNameplates) + { + YOffset -= FontSize; + TextRender()->TextColor(Rgb); + TextRender()->TextOutlineColor(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f)); + TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, g_Config.m_PlayerName) / 2.0f, YOffset, FontSize, g_Config.m_PlayerName); + if(g_Config.m_ClNameplatesClan) + { + YOffset -= FontSizeClan; + TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSizeClan, g_Config.m_PlayerClan) / 2.0f, YOffset, FontSizeClan, g_Config.m_PlayerClan); + } + TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); + + if(g_Config.m_ClNameplatesFriendMark) + { + YOffset -= FontSize; + TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); + TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, "♥") / 2.0f, YOffset, FontSize, "♥"); + } + + if(g_Config.m_ClNameplatesIds) + { + YOffset -= FontSize; + TextRender()->TextColor(Rgb); + TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, "0") / 2.0f, YOffset, FontSize, "0"); + } + + if(g_Config.m_ClNameplatesStrong) + { + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_STRONGWEAK].m_Id); + Graphics()->QuadsBegin(); + const ColorRGBA StrongStatusColor = color_cast(ColorHSLA(6401973)); + const int StrongSpriteId = SPRITE_HOOK_STRONG; + + Graphics()->SetColor(StrongStatusColor); + float ScaleX, ScaleY; + RenderTools()->SelectSprite(StrongSpriteId); + RenderTools()->GetSpriteScale(StrongSpriteId, ScaleX, ScaleY); + TextRender()->TextColor(StrongStatusColor); + + const float StrongImgSize = 40.0f; + YOffset -= StrongImgSize * ScaleY; + RenderTools()->DrawSprite(TeeRenderPos.x, YOffset + (StrongImgSize / 2.0f) * ScaleY, StrongImgSize); + Graphics()->QuadsEnd(); + + if(g_Config.m_ClNameplatesStrong == 2) + { + YOffset -= FontSize; + TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, "0") / 2.0f, YOffset, FontSize, "0"); + } + } + } + + TextRender()->TextColor(TextRender()->DefaultTextColor()); + TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); + TextRender()->SetRenderFlags(0); } else if(s_CurTab == APPEARANCE_TAB_HOOK_COLLISION) { diff --git a/src/game/client/components/skins.cpp b/src/game/client/components/skins.cpp index 922d71b1ac8..b67c8c7a72e 100644 --- a/src/game/client/components/skins.cpp +++ b/src/game/client/components/skins.cpp @@ -1,6 +1,8 @@ /* (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 "skins.h" + #include #include #include @@ -9,15 +11,13 @@ #include #include #include +#include #include -#include - #include +#include #include -#include "skins.h" - CSkins::CSkins() : m_PlaceholderSkin("dummy") { @@ -43,33 +43,16 @@ bool CSkins::IsVanillaSkin(const char *pName) return std::any_of(std::begin(VANILLA_SKINS), std::end(VANILLA_SKINS), [pName](const char *pVanillaSkin) { return str_comp(pName, pVanillaSkin) == 0; }); } -void CSkins::CGetPngFile::OnCompletion(EHttpState State) -{ - // Maybe this should start another thread to load the png in instead of stalling the curl thread - if(State == EHttpState::DONE) - { - m_pSkins->LoadSkinPng(m_Info, Dest(), Dest(), IStorage::TYPE_SAVE); - } -} - -CSkins::CGetPngFile::CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest) : - CHttpRequest(pUrl), - m_pSkins(pSkins) -{ - WriteToFile(pStorage, pDest, IStorage::TYPE_SAVE); - Timeout(CTimeout{0, 0, 0, 0}); - LogProgress(HTTPLOG::NONE); -} - -struct SSkinScanUser +class CSkinScanUser { +public: CSkins *m_pThis; - CSkins::TSkinLoadedCBFunc m_SkinLoadedFunc; + CSkins::TSkinLoadedCallback m_SkinLoadedCallback; }; int CSkins::SkinScan(const char *pName, int IsDir, int DirType, void *pUser) { - auto *pUserReal = static_cast(pUser); + auto *pUserReal = static_cast(pUser); CSkins *pSelf = pUserReal->m_pThis; if(IsDir) @@ -94,7 +77,7 @@ int CSkins::SkinScan(const char *pName, int IsDir, int DirType, void *pUser) char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "skins/%s", pName); pSelf->LoadSkin(aSkinName, aPath, DirType); - pUserReal->m_SkinLoadedFunc((int)pSelf->m_Skins.size()); + pUserReal->m_SkinLoadedCallback(); return 0; } @@ -136,19 +119,12 @@ static void CheckMetrics(CSkin::SSkinMetricVariable &Metrics, const uint8_t *pIm const CSkin *CSkins::LoadSkin(const char *pName, const char *pPath, int DirType) { CImageInfo Info; - if(!LoadSkinPng(Info, pName, pPath, DirType)) - return nullptr; - return LoadSkin(pName, Info); -} - -bool CSkins::LoadSkinPng(CImageInfo &Info, const char *pName, const char *pPath, int DirType) -{ if(!Graphics()->LoadPng(Info, pPath, DirType)) { log_error("skins", "Failed to load skin PNG: %s", pName); - return false; + return nullptr; } - return true; + return LoadSkin(pName, Info); } const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) @@ -298,6 +274,8 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) auto &&pSkin = std::make_unique(std::move(Skin)); const auto SkinInsertIt = m_Skins.insert({pSkin->GetName(), std::move(pSkin)}); + m_LastRefreshTime = time_get_nanoseconds(); + return SkinInsertIt.first->second.get(); } @@ -313,32 +291,34 @@ void CSkins::OnInit() } } - // load skins; - Refresh([this](int SkinCounter) { + // load skins + Refresh([this]() { GameClient()->m_Menus.RenderLoading(Localize("Loading chillerbot-ux"), Localize("Loading skin files"), 0); }); } -void CSkins::Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc) +void CSkins::OnShutdown() +{ + m_LoadingSkins.clear(); +} + +void CSkins::Refresh(TSkinLoadedCallback &&SkinLoadedCallback) { + m_LoadingSkins.clear(); + for(const auto &[_, pSkin] : m_Skins) { pSkin->m_OriginalSkin.Unload(Graphics()); pSkin->m_ColorableSkin.Unload(Graphics()); } - m_Skins.clear(); - m_DownloadSkins.clear(); - m_DownloadingSkins = 0; - SSkinScanUser SkinScanUser; + + CSkinScanUser SkinScanUser; SkinScanUser.m_pThis = this; - SkinScanUser.m_SkinLoadedFunc = SkinLoadedFunc; + SkinScanUser.m_SkinLoadedCallback = SkinLoadedCallback; Storage()->ListDirectory(IStorage::TYPE_ALL, "skins", SkinScan, &SkinScanUser); -} -int CSkins::Num() -{ - return m_Skins.size(); + m_LastRefreshTime = time_get_nanoseconds(); } const CSkin *CSkins::Find(const char *pName) @@ -365,7 +345,7 @@ const CSkin *CSkins::FindOrNullptr(const char *pName, bool IgnorePrefix) const char *pSkinPrefix = m_aEventSkinPrefix[0] != '\0' ? m_aEventSkinPrefix : g_Config.m_ClSkinPrefix; if(!IgnorePrefix && pSkinPrefix[0] != '\0') { - char aNameWithPrefix[48]; // Larger than skin name length to allow IsValidName to check if it's too long + char aNameWithPrefix[2 * MAX_SKIN_LENGTH + 2]; // Larger than skin name length to allow IsValidName to check if it's too long str_format(aNameWithPrefix, sizeof(aNameWithPrefix), "%s_%s", pSkinPrefix, pName); // If we find something, use it, otherwise fall back to normal skins. const auto *pResult = FindImpl(aNameWithPrefix); @@ -393,44 +373,26 @@ const CSkin *CSkins::FindImpl(const char *pName) if(!CSkin::IsValidName(pName)) return nullptr; - const auto SkinDownloadIt = m_DownloadSkins.find(pName); - if(SkinDownloadIt != m_DownloadSkins.end()) + auto ExistingLoadingSkin = m_LoadingSkins.find(pName); + if(ExistingLoadingSkin != m_LoadingSkins.end()) { - if(SkinDownloadIt->second->m_pTask && SkinDownloadIt->second->m_pTask->State() == EHttpState::DONE && SkinDownloadIt->second->m_pTask->m_Info.m_pData) - { - char aPath[IO_MAX_PATH_LENGTH]; - str_format(aPath, sizeof(aPath), "downloadedskins/%s.png", SkinDownloadIt->second->GetName()); - Storage()->RenameFile(SkinDownloadIt->second->m_aPath, aPath, IStorage::TYPE_SAVE); - const auto *pSkin = LoadSkin(SkinDownloadIt->second->GetName(), SkinDownloadIt->second->m_pTask->m_Info); - SkinDownloadIt->second->m_pTask = nullptr; - --m_DownloadingSkins; - return pSkin; - } - if(SkinDownloadIt->second->m_pTask && (SkinDownloadIt->second->m_pTask->State() == EHttpState::ERROR || SkinDownloadIt->second->m_pTask->State() == EHttpState::ABORTED)) + std::unique_ptr &pLoadingSkin = ExistingLoadingSkin->second; + if(!pLoadingSkin->m_pDownloadJob || !pLoadingSkin->m_pDownloadJob->Done()) + return nullptr; + + if(pLoadingSkin->m_pDownloadJob->State() == IJob::STATE_DONE && pLoadingSkin->m_pDownloadJob->ImageInfo().m_pData) { - SkinDownloadIt->second->m_pTask = nullptr; - --m_DownloadingSkins; + LoadSkin(pLoadingSkin->Name(), pLoadingSkin->m_pDownloadJob->ImageInfo()); } + pLoadingSkin->m_pDownloadJob = nullptr; return nullptr; } - CDownloadSkin Skin{pName}; - - char aEscapedName[256]; - EscapeUrl(aEscapedName, sizeof(aEscapedName), pName); - char aUrl[IO_MAX_PATH_LENGTH]; - str_format(aUrl, sizeof(aUrl), "%s%s.png", g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl, aEscapedName); - - char aBuf[IO_MAX_PATH_LENGTH]; - str_format(Skin.m_aPath, sizeof(Skin.m_aPath), "downloadedskins/%s", IStorage::FormatTmpPath(aBuf, sizeof(aBuf), pName)); - - Skin.m_pTask = std::make_shared(this, aUrl, Storage(), Skin.m_aPath); - Http()->Run(Skin.m_pTask); - - auto &&pDownloadSkin = std::make_unique(std::move(Skin)); - m_DownloadSkins.insert({pDownloadSkin->GetName(), std::move(pDownloadSkin)}); - ++m_DownloadingSkins; - + CLoadingSkin LoadingSkin(pName); + LoadingSkin.m_pDownloadJob = std::make_shared(this, pName); + Engine()->AddJob(LoadingSkin.m_pDownloadJob); + auto &&pLoadingSkin = std::make_unique(std::move(LoadingSkin)); + m_LoadingSkins.insert({pLoadingSkin->Name(), std::move(pLoadingSkin)}); return nullptr; } @@ -463,15 +425,191 @@ void CSkins::RandomizeSkin(int Dummy) const size_t SkinNameSize = Dummy ? sizeof(g_Config.m_ClDummySkin) : sizeof(g_Config.m_ClPlayerSkin); char aRandomSkinName[MAX_SKIN_LENGTH]; str_copy(aRandomSkinName, "default", SkinNameSize); - if(!m_pClient->m_Skins.GetSkinsUnsafe().empty()) + if(!m_Skins.empty()) { do { - auto it = m_pClient->m_Skins.GetSkinsUnsafe().begin(); - std::advance(it, rand() % m_pClient->m_Skins.GetSkinsUnsafe().size()); + auto it = m_Skins.begin(); + std::advance(it, rand() % m_Skins.size()); str_copy(aRandomSkinName, (*it).second->GetName(), SkinNameSize); } while(!str_comp(aRandomSkinName, "x_ninja") || !str_comp(aRandomSkinName, "x_spec")); } char *pSkinName = Dummy ? g_Config.m_ClDummySkin : g_Config.m_ClPlayerSkin; str_copy(pSkinName, aRandomSkinName, SkinNameSize); } + +CSkins::CSkinDownloadJob::CSkinDownloadJob(CSkins *pSkins, const char *pName) : + m_pSkins(pSkins) +{ + str_copy(m_aName, pName); + Abortable(true); +} + +CSkins::CSkinDownloadJob::~CSkinDownloadJob() +{ + m_ImageInfo.Free(); +} + +bool CSkins::CSkinDownloadJob::Abort() +{ + if(!IJob::Abort()) + { + return false; + } + + const CLockScope LockScope(m_Lock); + if(m_pGetRequest) + { + m_pGetRequest->Abort(); + m_pGetRequest = nullptr; + } + return true; +} + +void CSkins::CSkinDownloadJob::Run() +{ + const char *pBaseUrl = g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl; + + char aEscapedName[256]; + EscapeUrl(aEscapedName, sizeof(aEscapedName), m_aName); + + char aUrl[IO_MAX_PATH_LENGTH]; + str_format(aUrl, sizeof(aUrl), "%s%s.png", pBaseUrl, aEscapedName); + + char aPathReal[IO_MAX_PATH_LENGTH]; + str_format(aPathReal, sizeof(aPathReal), "downloadedskins/%s.png", m_aName); + + const CTimeout Timeout{10000, 0, 8192, 10}; + const size_t MaxResponseSize = 10 * 1024 * 1024; // 10 MiB + + // We assume the file does not exist if we could not get the times + time_t FileCreatedTime, FileModifiedTime; + const bool GotFileTimes = m_pSkins->Storage()->RetrieveTimes(aPathReal, IStorage::TYPE_SAVE, &FileCreatedTime, &FileModifiedTime); + + std::shared_ptr pGet = HttpGet(aUrl); + pGet->Timeout(Timeout); + pGet->MaxResponseSize(MaxResponseSize); + if(GotFileTimes) + { + pGet->IfModifiedSince(FileModifiedTime); + pGet->FailOnErrorStatus(false); + } + pGet->LogProgress(HTTPLOG::NONE); + { + const CLockScope LockScope(m_Lock); + m_pGetRequest = pGet; + } + m_pSkins->Http()->Run(pGet); + + // Load existing file while waiting for the HTTP request + if(GotFileTimes) + { + m_pSkins->Graphics()->LoadPng(m_ImageInfo, aPathReal, IStorage::TYPE_SAVE); + } + + pGet->Wait(); + { + const CLockScope LockScope(m_Lock); + m_pGetRequest = nullptr; + } + if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED || pGet->StatusCode() >= 400) + { + return; + } + if(pGet->StatusCode() == 304) // 304 Not Modified + { + if(m_ImageInfo.m_pData != nullptr) + { + return; // Local skin is up-to-date and was loaded successfully + } + + log_error("skins", "Failed to load PNG of existing downloaded skin '%s' from '%s', downloading it again", m_aName, aPathReal); + pGet = HttpGet(aUrl); + pGet->Timeout(Timeout); + pGet->MaxResponseSize(MaxResponseSize); + pGet->LogProgress(HTTPLOG::NONE); + { + const CLockScope LockScope(m_Lock); + m_pGetRequest = pGet; + } + m_pSkins->Http()->Run(pGet); + pGet->Wait(); + { + const CLockScope LockScope(m_Lock); + m_pGetRequest = nullptr; + } + if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED) + { + return; + } + } + + unsigned char *pResult; + size_t ResultSize; + pGet->Result(&pResult, &ResultSize); + + if(!m_pSkins->Graphics()->LoadPng(m_ImageInfo, pResult, ResultSize, aUrl)) + { + log_error("skins", "Failed to load PNG of skin '%s' downloaded from '%s'", m_aName, aUrl); + return; + } + + if(State() == IJob::STATE_ABORTED) + { + return; + } + + char aBuf[IO_MAX_PATH_LENGTH]; + char aPathTemp[IO_MAX_PATH_LENGTH]; + str_format(aPathTemp, sizeof(aPathTemp), "downloadedskins/%s", IStorage::FormatTmpPath(aBuf, sizeof(aBuf), m_aName)); + + IOHANDLE TempFile = m_pSkins->Storage()->OpenFile(aPathTemp, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!TempFile) + { + log_error("skins", "Failed to open temporary skin file '%s' for writing", aPathTemp); + return; + } + if(io_write(TempFile, pResult, ResultSize) != ResultSize) + { + log_error("skins", "Failed to write downloaded skin data to '%s'", aPathTemp); + io_close(TempFile); + m_pSkins->Storage()->RemoveFile(aPathTemp, IStorage::TYPE_SAVE); + return; + } + io_close(TempFile); + + if(!m_pSkins->Storage()->RenameFile(aPathTemp, aPathReal, IStorage::TYPE_SAVE)) + { + log_error("skins", "Failed to rename temporary skin file '%s' to '%s'", aPathTemp, aPathReal); + m_pSkins->Storage()->RemoveFile(aPathTemp, IStorage::TYPE_SAVE); + return; + } +} + +CSkins::CLoadingSkin::CLoadingSkin(const char *pName) +{ + str_copy(m_aName, pName); +} + +CSkins::CLoadingSkin::~CLoadingSkin() +{ + if(m_pDownloadJob) + { + m_pDownloadJob->Abort(); + } +} + +bool CSkins::CLoadingSkin::operator<(const CLoadingSkin &Other) const +{ + return str_comp(m_aName, Other.m_aName) < 0; +} + +bool CSkins::CLoadingSkin::operator<(const char *pOther) const +{ + return str_comp(m_aName, pOther) < 0; +} + +bool CSkins::CLoadingSkin::operator==(const char *pOther) const +{ + return !str_comp(m_aName, pOther); +} diff --git a/src/game/client/components/skins.h b/src/game/client/components/skins.h index 748f85b9da6..3c3eab99cb8 100644 --- a/src/game/client/components/skins.h +++ b/src/game/client/components/skins.h @@ -3,88 +3,97 @@ #ifndef GAME_CLIENT_COMPONENTS_SKINS_H #define GAME_CLIENT_COMPONENTS_SKINS_H -#include -#include +#include + +#include + #include #include + +#include #include #include +class CHttpRequest; + class CSkins : public CComponent { public: CSkins(); - class CGetPngFile : public CHttpRequest + typedef std::function TSkinLoadedCallback; + + int Sizeof() const override { return sizeof(*this); } + void OnInit() override; + void OnShutdown() override; + + void Refresh(TSkinLoadedCallback &&SkinLoadedCallback); + std::chrono::nanoseconds LastRefreshTime() const { return m_LastRefreshTime; } + + const std::unordered_map> &GetSkinsUnsafe() const { return m_Skins; } + const CSkin *FindOrNullptr(const char *pName, bool IgnorePrefix = false); + const CSkin *Find(const char *pName); + + void RandomizeSkin(int Dummy); + + static bool IsVanillaSkin(const char *pName); + + constexpr static const char *VANILLA_SKINS[] = {"bluekitty", "bluestripe", "brownbear", + "cammo", "cammostripes", "coala", "default", "limekitty", + "pinky", "redbopp", "redstripe", "saddo", "toptri", + "twinbop", "twintri", "warpaint", "x_ninja", "x_spec"}; + +private: + class CSkinDownloadJob : public IJob { - CSkins *m_pSkins; + public: + CSkinDownloadJob(CSkins *pSkins, const char *pName); + ~CSkinDownloadJob(); + + bool Abort() override REQUIRES(!m_Lock); + + CImageInfo &ImageInfo() { return m_ImageInfo; } protected: - virtual void OnCompletion(EHttpState State) override; + void Run() override REQUIRES(!m_Lock); - public: - CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest); - CImageInfo m_Info; + private: + CSkins *m_pSkins; + char m_aName[MAX_SKIN_LENGTH]; + CLock m_Lock; + std::shared_ptr m_pGetRequest; + CImageInfo m_ImageInfo; }; - struct CDownloadSkin + class CLoadingSkin { private: char m_aName[MAX_SKIN_LENGTH]; public: - std::shared_ptr m_pTask; - char m_aPath[IO_MAX_PATH_LENGTH]; - - CDownloadSkin(CDownloadSkin &&Other) = default; - CDownloadSkin(const char *pName) - { - str_copy(m_aName, pName); - } - - ~CDownloadSkin() - { - if(m_pTask) - m_pTask->Abort(); - } - bool operator<(const CDownloadSkin &Other) const { return str_comp(m_aName, Other.m_aName) < 0; } - bool operator<(const char *pOther) const { return str_comp(m_aName, pOther) < 0; } - bool operator==(const char *pOther) const { return !str_comp(m_aName, pOther); } - - CDownloadSkin &operator=(CDownloadSkin &&Other) = default; - - const char *GetName() const { return m_aName; } - }; + std::shared_ptr m_pDownloadJob = nullptr; - typedef std::function TSkinLoadedCBFunc; + CLoadingSkin(CLoadingSkin &&Other) = default; + CLoadingSkin(const char *pName); + ~CLoadingSkin(); - virtual int Sizeof() const override { return sizeof(*this); } - void OnInit() override; + bool operator<(const CLoadingSkin &Other) const; + bool operator<(const char *pOther) const; + bool operator==(const char *pOther) const; - void Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc); - int Num(); - std::unordered_map> &GetSkinsUnsafe() { return m_Skins; } - const CSkin *FindOrNullptr(const char *pName, bool IgnorePrefix = false); - const CSkin *Find(const char *pName); - void RandomizeSkin(int Dummy); + CLoadingSkin &operator=(CLoadingSkin &&Other) = default; - bool IsDownloadingSkins() { return m_DownloadingSkins; } + const char *Name() const { return m_aName; } + }; - static bool IsVanillaSkin(const char *pName); + std::unordered_map> m_Skins; - constexpr static const char *VANILLA_SKINS[] = {"bluekitty", "bluestripe", "brownbear", - "cammo", "cammostripes", "coala", "default", "limekitty", - "pinky", "redbopp", "redstripe", "saddo", "toptri", - "twinbop", "twintri", "warpaint", "x_ninja", "x_spec"}; + std::unordered_map> m_LoadingSkins; + std::chrono::nanoseconds m_LastRefreshTime; -private: - std::unordered_map> m_Skins; - std::unordered_map> m_DownloadSkins; CSkin m_PlaceholderSkin; - size_t m_DownloadingSkins = 0; char m_aEventSkinPrefix[MAX_SKIN_LENGTH]; - bool LoadSkinPng(CImageInfo &Info, const char *pName, const char *pPath, int DirType); const CSkin *LoadSkin(const char *pName, const char *pPath, int DirType); const CSkin *LoadSkin(const char *pName, CImageInfo &Info); const CSkin *FindImpl(const char *pName); diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 13d0fad92ae..492e72473ed 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -3820,7 +3820,7 @@ void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir) void CGameClient::RefreshSkins() { const auto SkinStartLoadTime = time_get_nanoseconds(); - m_Skins.Refresh([&](int) { + m_Skins.Refresh([&]() { // if skin refreshing takes to long, swap to a loading screen if(time_get_nanoseconds() - SkinStartLoadTime > 500ms) { diff --git a/src/game/server/scoreworker.cpp b/src/game/server/scoreworker.cpp index 50d7b952fc5..11cf356de8b 100644 --- a/src/game/server/scoreworker.cpp +++ b/src/game/server/scoreworker.cpp @@ -1125,9 +1125,9 @@ bool CScoreWorker::ShowTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGame " FROM (" // teamrank score board " SELECT RANK() OVER w AS Ranking, COUNT(*) AS Teamsize, Id, Server " " FROM (" - " SELECT * FROM %s_teamrace as tr " + " SELECT tr.Map, tr.Time, tr.Id, rr.Server FROM %s_teamrace as tr " " INNER JOIN %s_race as rr ON tr.Map = rr.Map AND tr.Name = rr.Name AND tr.Time = rr.Time AND tr.Timestamp = rr.Timestamp" - " ) " + " ) AS ll " " WHERE Map = ? " " GROUP BY ID " " WINDOW w AS (ORDER BY Min(Time))"