diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp
index b86cce8b7..80ace42ce 100644
--- a/src/game/client/components/menus.cpp
+++ b/src/game/client/components/menus.cpp
@@ -11,6 +11,7 @@
#include
#include
+#include
#include
#include
#include
@@ -1037,6 +1038,15 @@ void CMenus::OnInit()
Storage()->ListDirectory(IStorage::TYPE_ALL, "menuimages", MenuImageScan, this);
}
+void CMenus::OnConsoleInit()
+{
+ auto *pConfigManager = Kernel()->RequestInterface();
+ if(pConfigManager != nullptr)
+ pConfigManager->RegisterCallback(CMenus::ConfigSaveCallback, this);
+ Console()->Register("add_favorite_skin", "s[skin_name]", CFGFLAG_CLIENT, Con_AddFavoriteSkin, this, "Add a skin as a favorite");
+ Console()->Register("remove_favorite_skin", "s[skin_name]", CFGFLAG_CLIENT, Con_RemFavoriteSkin, this, "Remove a skin from the favorites");
+}
+
void CMenus::ConchainUpdateMusicState(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h
index d0b5354b2..d73e5f41f 100644
--- a/src/game/client/components/menus.h
+++ b/src/game/client/components/menus.h
@@ -7,6 +7,7 @@
#include
#include
+#include
#include
#include
@@ -521,6 +522,14 @@ protected:
static void ConchainFriendlistUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
static void ConchainServerbrowserUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
+ // skin favorite list
+ bool m_SkinFavoritesChanged = false;
+ std::unordered_set m_SkinFavorites;
+ static void Con_AddFavoriteSkin(IConsole::IResult *pResult, void *pUserData);
+ static void Con_RemFavoriteSkin(IConsole::IResult *pResult, void *pUserData);
+ static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData);
+ void OnConfigSave(IConfigManager *pConfigManager);
+
// found in menus_settings.cpp
void RenderLanguageSelection(CUIRect MainView);
void RenderThemeSelection(CUIRect MainView, bool Header = true);
@@ -561,6 +570,7 @@ public:
void KillServer();
virtual void OnInit() override;
+ void OnConsoleInit() override;
virtual void OnStateChange(int NewState, int OldState) override;
virtual void OnReset() override;
diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp
index 2224e7f3d..044b817ac 100644
--- a/src/game/client/components/menus_settings.cpp
+++ b/src/game/client/components/menus_settings.cpp
@@ -426,6 +426,49 @@ void CMenus::RefreshSkins()
}
}
+void CMenus::Con_AddFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
+{
+ auto *pSelf = (CMenus *)pUserData;
+ if(pResult->NumArguments() >= 1)
+ {
+ pSelf->m_SkinFavorites.emplace(pResult->GetString(0));
+ pSelf->m_SkinFavoritesChanged = true;
+ }
+}
+
+void CMenus::Con_RemFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
+{
+ auto *pSelf = (CMenus *)pUserData;
+ if(pResult->NumArguments() >= 1)
+ {
+ const auto it = pSelf->m_SkinFavorites.find(pResult->GetString(0));
+ if(it != pSelf->m_SkinFavorites.end())
+ {
+ pSelf->m_SkinFavorites.erase(it);
+ pSelf->m_SkinFavoritesChanged = true;
+ }
+ }
+}
+
+void CMenus::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData)
+{
+ auto *pSelf = (CMenus *)pUserData;
+ pSelf->OnConfigSave(pConfigManager);
+}
+
+void CMenus::OnConfigSave(IConfigManager *pConfigManager)
+{
+ for(const auto &Entry : m_SkinFavorites)
+ {
+ char aBuffer[256];
+ char aNameEscaped[256];
+ char *pDst = aNameEscaped;
+ str_escape(&pDst, Entry.c_str(), aNameEscaped + std::size(aNameEscaped));
+ str_format(aBuffer, std::size(aBuffer), "add_favorite_skin \"%s\"", Entry.c_str());
+ pConfigManager->WriteLine(aBuffer);
+ }
+}
+
void CMenus::RenderSettingsTee(CUIRect MainView)
{
CUIRect Button, Label, Dummy, DummyLabel, SkinList, QuickSearch, QuickSearchClearButton, SkinDB, SkinPrefix, SkinPrefixLabel, DirectoryButton, RefreshButton, Eyes, EyesLabel, EyesTee, EyesRight;
@@ -658,33 +701,92 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
MainView.HSplitTop(20.0f, 0, &MainView);
MainView.HSplitTop(230.0f - RenderEyesBelow * 25.0f, &SkinList, &MainView);
static std::vector s_vSkinList;
+ static std::vector s_vSkinListHelper;
+ static std::vector s_vFavoriteSkinListHelper;
static int s_SkinCount = 0;
static float s_ScrollValue = 0.0f;
- if(s_InitSkinlist || m_pClient->m_Skins.Num() != s_SkinCount)
+ // be nice to the CPU
+ static auto s_SkinLastRebuildTime = time_get_nanoseconds();
+ const auto CurTime = time_get_nanoseconds();
+ if(s_InitSkinlist || m_pClient->m_Skins.Num() != s_SkinCount || m_SkinFavoritesChanged || (m_pClient->m_Skins.IsDownloadingSkins() && (CurTime - s_SkinLastRebuildTime > 500ms)))
{
+ s_SkinLastRebuildTime = CurTime;
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;
+ bool RequiresRebuild = false;
+
+ auto &&SkinNotFiltered = [&](const CSkin *pSkinToBeSelected) {
+ // filter quick search
+ if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(pSkinToBeSelected->m_aName, g_Config.m_ClSkinFilterString))
+ return false;
+
+ // no special skins
+ if((pSkinToBeSelected->m_aName[0] == 'x' && pSkinToBeSelected->m_aName[1] == '_'))
+ return false;
+
+ if(pSkinToBeSelected == 0)
+ return false;
+
+ return true;
+ };
+
+ for(const auto &it : m_SkinFavorites)
+ {
+ const auto FirstSkinIndex = m_pClient->m_Skins.Find(it.c_str());
+ // second call is intended, our implemention doesnt return the index in the call where the download finished
+ const auto SkinIndex = m_pClient->m_Skins.Find(it.c_str());
+ if(SkinIndex == -1)
+ continue;
+ if(FirstSkinIndex == -1 && SkinIndex != -1)
+ {
+ // skin list changed, rebuild next frame
+ RequiresRebuild = true;
+ }
+ const CSkin *pSkinToBeSelected = m_pClient->m_Skins.Get(SkinIndex);
+
+ if(!SkinNotFiltered(pSkinToBeSelected))
+ continue;
+
+ s_vFavoriteSkinListHelper.emplace_back(pSkinToBeSelected);
+ }
for(int i = 0; i < m_pClient->m_Skins.Num(); ++i)
{
const CSkin *pSkinToBeSelected = m_pClient->m_Skins.Get(i);
- // filter quick search
- if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(pSkinToBeSelected->m_aName, g_Config.m_ClSkinFilterString))
+ if(!SkinNotFiltered(pSkinToBeSelected))
continue;
- // no special skins
- if((pSkinToBeSelected->m_aName[0] == 'x' && pSkinToBeSelected->m_aName[1] == '_'))
- continue;
-
- if(pSkinToBeSelected == 0)
- continue;
-
- s_vSkinList.emplace_back(pSkinToBeSelected);
+ if(std::find(m_SkinFavorites.begin(), m_SkinFavorites.end(), pSkinToBeSelected->m_aName) == m_SkinFavorites.end())
+ s_vSkinListHelper.emplace_back(pSkinToBeSelected);
}
- std::sort(s_vSkinList.begin(), s_vSkinList.end());
- s_InitSkinlist = false;
- s_SkinCount = m_pClient->m_Skins.Num();
+ std::sort(s_vSkinListHelper.begin(), s_vSkinListHelper.end());
+ std::sort(s_vFavoriteSkinListHelper.begin(), s_vFavoriteSkinListHelper.end());
+ s_vSkinList = s_vFavoriteSkinListHelper;
+ s_vSkinList.insert(s_vSkinList.end(), s_vSkinListHelper.begin(), s_vSkinListHelper.end());
+ s_InitSkinlist = RequiresRebuild;
}
+ auto &&RenderFavIcon = [&](const CUIRect &FavIcon, bool AsFav) {
+ TextRender()->SetCurFont(TextRender()->GetFont(TEXT_FONT_ICON_FONT));
+ TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
+ if(AsFav)
+ TextRender()->TextColor({1, 1, 0, 1});
+ else
+ TextRender()->TextColor({0.5f, 0.5f, 0.5f, 1});
+ TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
+ SLabelProperties Props;
+ Props.m_MaxWidth = FavIcon.w;
+ UI()->DoLabel(&FavIcon, "\xef\x80\x85", 12.0f, TEXTALIGN_RIGHT, Props);
+ TextRender()->TextColor(TextRender()->DefaultTextColor());
+ TextRender()->SetRenderFlags(0);
+ TextRender()->SetCurFont(nullptr);
+ };
+
int OldSelected = -1;
UiDoListboxStart(&s_InitSkinlist, &SkinList, 50.0f, Localize("Skins"), "", s_vSkinList.size(), 4, OldSelected, s_ScrollValue);
for(size_t i = 0; i < s_vSkinList.size(); ++i)
@@ -697,6 +799,8 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
CListboxItem Item = UiDoListboxNextItem(pSkinToBeDraw, OldSelected >= 0 && (size_t)OldSelected == i);
if(Item.m_Visible)
{
+ auto OriginalRect = Item.m_Rect;
+
CTeeRenderInfo Info = OwnSkinInfo;
Info.m_CustomColoredSkin = *pUseCustomColor;
@@ -709,9 +813,11 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
RenderTools()->RenderTee(pIdleState, &Info, Emote, vec2(1.0f, 0.0f), TeeRenderPos);
Item.m_Rect.VSplitLeft(60.0f, 0, &Item.m_Rect);
- SLabelProperties Props;
- Props.m_MaxWidth = Item.m_Rect.w;
- UI()->DoLabel(&Item.m_Rect, pSkinToBeDraw->m_aName, 12.0f, TEXTALIGN_LEFT, Props);
+ {
+ SLabelProperties Props;
+ Props.m_MaxWidth = Item.m_Rect.w;
+ UI()->DoLabel(&Item.m_Rect, pSkinToBeDraw->m_aName, 12.0f, TEXTALIGN_LEFT, Props);
+ }
if(g_Config.m_Debug)
{
ColorRGBA BloodColor = *pUseCustomColor ? color_cast(ColorHSLA(*pColorBody)) : pSkinToBeDraw->m_BloodColor;
@@ -722,6 +828,38 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
+
+ // render skin favorite icon
+ {
+ const auto SkinItFav = m_SkinFavorites.find(pSkinToBeDraw->m_aName);
+ const auto IsFav = SkinItFav != m_SkinFavorites.end();
+ CUIRect FavIcon;
+ OriginalRect.HSplitTop(20.0f, &FavIcon, nullptr);
+ FavIcon.VSplitRight(20.0f, nullptr, &FavIcon);
+ if(IsFav)
+ {
+ RenderFavIcon(FavIcon, IsFav);
+ }
+ else
+ {
+ if(UI()->MouseInside(&FavIcon))
+ {
+ RenderFavIcon(FavIcon, IsFav);
+ }
+ }
+ if(UI()->DoButtonLogic(pSkinToBeDraw->m_aName, 0, &FavIcon))
+ {
+ if(IsFav)
+ {
+ m_SkinFavorites.erase(SkinItFav);
+ }
+ else
+ {
+ m_SkinFavorites.emplace(pSkinToBeDraw->m_aName);
+ }
+ s_InitSkinlist = true;
+ }
+ }
}
}
diff --git a/src/game/client/components/skins.cpp b/src/game/client/components/skins.cpp
index c9af05e10..d759410a8 100644
--- a/src/game/client/components/skins.cpp
+++ b/src/game/client/components/skins.cpp
@@ -349,6 +349,7 @@ void CSkins::Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc)
m_vSkins.clear();
m_vDownloadSkins.clear();
+ m_DownloadingSkins = 0;
SSkinScanUser SkinScanUser;
SkinScanUser.m_pThis = this;
SkinScanUser.m_SkinLoadedFunc = SkinLoadedFunc;
@@ -432,10 +433,12 @@ int CSkins::FindImpl(const char *pName)
Storage()->RenameFile(RangeBegin->m_aPath, aPath, IStorage::TYPE_SAVE);
LoadSkin(RangeBegin->m_aName, RangeBegin->m_pTask->m_Info);
RangeBegin->m_pTask = nullptr;
+ --m_DownloadingSkins;
}
if(RangeBegin->m_pTask && (RangeBegin->m_pTask->State() == HTTP_ERROR || RangeBegin->m_pTask->State() == HTTP_ABORTED))
{
RangeBegin->m_pTask = nullptr;
+ --m_DownloadingSkins;
}
return -1;
}
@@ -452,5 +455,6 @@ int CSkins::FindImpl(const char *pName)
Skin.m_pTask = std::make_shared(this, aUrl, Storage(), Skin.m_aPath);
m_pClient->Engine()->AddJob(Skin.m_pTask);
m_vDownloadSkins.insert(std::lower_bound(m_vDownloadSkins.begin(), m_vDownloadSkins.end(), Skin), std::move(Skin));
+ ++m_DownloadingSkins;
return -1;
}
diff --git a/src/game/client/components/skins.h b/src/game/client/components/skins.h
index b824f100b..d089ecab6 100644
--- a/src/game/client/components/skins.h
+++ b/src/game/client/components/skins.h
@@ -54,9 +54,12 @@ public:
const CSkin *Get(int Index);
int Find(const char *pName);
+ bool IsDownloadingSkins() { return m_DownloadingSkins; }
+
private:
std::vector m_vSkins;
std::vector m_vDownloadSkins;
+ size_t m_DownloadingSkins = 0;
char m_aEventSkinPrefix[24];
bool LoadSkinPNG(CImageInfo &Info, const char *pName, const char *pPath, int DirType);