1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#ifndef GAME_CLIENT_COMPONENTS_SKINS_H
4#define GAME_CLIENT_COMPONENTS_SKINS_H
5
6#include <base/lock.h>
7
8#include <engine/shared/config.h>
9#include <engine/shared/jobs.h>
10
11#include <game/client/component.h>
12#include <game/client/skin.h>
13
14#include <chrono>
15#include <list>
16#include <optional>
17#include <set>
18#include <string_view>
19#include <unordered_map>
20#include <utility>
21
22class CHttpRequest;
23
24class CSkins : public CComponent
25{
26private:
27 /**
28 * The data of a skin that can be loaded in a separate thread.
29 */
30 class CSkinLoadData
31 {
32 public:
33 CImageInfo m_Info;
34 CImageInfo m_InfoGrayscale;
35 CSkin::CSkinMetrics m_Metrics;
36 ColorRGBA m_BloodColor;
37 };
38
39 /**
40 * An abstract job to load a skin from a source determined by the derived class.
41 */
42 class CAbstractSkinLoadJob : public IJob
43 {
44 public:
45 CAbstractSkinLoadJob(CSkins *pSkins, const char *pName);
46 ~CAbstractSkinLoadJob() override;
47
48 CSkinLoadData m_Data;
49 bool m_NotFound = false;
50
51 protected:
52 CSkins *m_pSkins;
53 char m_aName[MAX_SKIN_LENGTH];
54 };
55
56public:
57 /**
58 * Container for a skin, its loading state, job and various meta data.
59 */
60 class CSkinContainer
61 {
62 friend class CSkins;
63
64 public:
65 enum class EType
66 {
67 /**
68 * Skin should be loaded locally (from skins folder).
69 */
70 LOCAL,
71 /**
72 * Skin should be downloaded (or loaded from downloadedskins).
73 */
74 DOWNLOAD,
75 };
76
77 enum class EState
78 {
79 /**
80 * Skin is unloaded and loading is not desired.
81 */
82 UNLOADED,
83 /**
84 * Skin is unloaded and should be loaded when a slot is free. Skin will enter @link LOADING @endlink
85 * state when maximum number of loaded skins is not exceeded.
86 */
87 PENDING,
88 /**
89 * Skin is currently loading, iff @link m_pLoadJob @endlink is set.
90 */
91 LOADING,
92 /**
93 * Skin is loaded, iff @link m_pSkin @endlink is set.
94 */
95 LOADED,
96 /**
97 * Skin failed to be loaded due to an unexpected error.
98 */
99 ERROR,
100 /**
101 * Skin failed to be downloaded because it could not be found.
102 */
103 NOT_FOUND,
104 };
105
106 CSkinContainer(CSkinContainer &&Other) = default;
107 CSkinContainer(CSkins *pSkins, const char *pName, EType Type, int StorageType);
108 ~CSkinContainer();
109
110 bool operator<(const CSkinContainer &Other) const;
111 CSkinContainer &operator=(CSkinContainer &&Other) = default;
112
113 const char *Name() const { return m_aName; }
114 EType Type() const { return m_Type; }
115 int StorageType() const { return m_StorageType; }
116 bool IsVanilla() const { return m_Vanilla; }
117 bool IsSpecial() const { return m_Special; }
118 bool IsAlwaysLoaded() const { return m_AlwaysLoaded; }
119 EState State() const { return m_State; }
120 const std::unique_ptr<CSkin> &Skin() const { return m_pSkin; }
121
122 /**
123 * Request that this skin should be loaded and should stay loaded.
124 */
125 void RequestLoad();
126
127 private:
128 CSkins *m_pSkins;
129 char m_aName[MAX_SKIN_LENGTH];
130 EType m_Type;
131 int m_StorageType;
132 bool m_Vanilla;
133 bool m_Special;
134 bool m_AlwaysLoaded;
135
136 EState m_State = EState::UNLOADED;
137 std::unique_ptr<CSkin> m_pSkin = nullptr;
138 std::shared_ptr<CAbstractSkinLoadJob> m_pLoadJob = nullptr;
139
140 /**
141 * The time when loading of this skin was first requested.
142 */
143 std::optional<std::chrono::nanoseconds> m_FirstLoadRequest;
144 /**
145 * The time when loading of this skin was most recently requested.
146 */
147 std::optional<std::chrono::nanoseconds> m_LastLoadRequest;
148 /**
149 * Iterator into @link CSkins::m_SkinsUsageList @endlink for this skin container.
150 */
151 std::optional<std::list<std::string_view>::iterator> m_UsageEntryIterator;
152
153 EState DetermineInitialState() const;
154 void SetState(EState State);
155 };
156
157 /**
158 * Represents a skin being displayed in a list in the UI.
159 */
160 class CSkinListEntry
161 {
162 public:
163 CSkinListEntry() :
164 m_pSkinContainer(nullptr),
165 m_Favorite(false) {}
166 CSkinListEntry(CSkinContainer *pSkinContainer, bool Favorite, bool SelectedMain, bool SelectedDummy, std::optional<std::pair<int, int>> NameMatch) :
167 m_pSkinContainer(pSkinContainer),
168 m_Favorite(Favorite),
169 m_SelectedMain(SelectedMain),
170 m_SelectedDummy(SelectedDummy),
171 m_NameMatch(NameMatch) {}
172
173 bool operator<(const CSkinListEntry &Other) const;
174
175 const CSkinContainer *SkinContainer() const { return m_pSkinContainer; }
176 bool IsFavorite() const { return m_Favorite; }
177 bool IsSelectedMain() const { return m_SelectedMain; }
178 bool IsSelectedDummy() const { return m_SelectedDummy; }
179 const std::optional<std::pair<int, int>> &NameMatch() const { return m_NameMatch; }
180
181 const void *ListItemId() const { return &m_ListItemId; }
182 const void *FavoriteButtonId() const { return &m_FavoriteButtonId; }
183 const void *ErrorTooltipId() const { return &m_ErrorTooltipId; }
184
185 /**
186 * Request that this skin should be loaded and should stay loaded.
187 */
188 void RequestLoad();
189
190 private:
191 CSkinContainer *m_pSkinContainer;
192 bool m_Favorite;
193 bool m_SelectedMain;
194 bool m_SelectedDummy;
195 std::optional<std::pair<int, int>> m_NameMatch;
196 char m_ListItemId;
197 char m_FavoriteButtonId;
198 char m_ErrorTooltipId;
199 };
200
201 class CSkinList
202 {
203 friend class CSkins;
204
205 public:
206 std::vector<CSkinListEntry> &Skins() { return m_vSkins; }
207 int UnfilteredCount() const { return m_UnfilteredCount; }
208 void ForceRefresh() { m_NeedsUpdate = true; }
209
210 private:
211 std::vector<CSkinListEntry> m_vSkins;
212 int m_UnfilteredCount;
213 bool m_NeedsUpdate = true;
214 };
215
216 class CSkinLoadingStats
217 {
218 public:
219 size_t m_NumUnloaded = 0;
220 size_t m_NumPending = 0;
221 size_t m_NumLoading = 0;
222 size_t m_NumLoaded = 0;
223 size_t m_NumError = 0;
224 size_t m_NumNotFound = 0;
225 };
226
227 CSkins();
228
229 typedef std::function<void()> TSkinLoadedCallback;
230
231 int Sizeof() const override { return sizeof(*this); }
232 void OnConsoleInit() override;
233 void OnInit() override;
234 void OnShutdown() override;
235 void OnUpdate() override;
236
237 void RefreshEventSkins();
238 void Refresh(TSkinLoadedCallback &&SkinLoadedCallback);
239 CSkinLoadingStats LoadingStats() const;
240 CSkinList &SkinList();
241
242 const CSkinContainer *FindContainerOrNullptr(const char *pName);
243 const CSkin *FindOrNullptr(const char *pName);
244 const CSkin *Find(const char *pName);
245
246 void AddFavorite(const char *pName);
247 void RemoveFavorite(const char *pName);
248 bool IsFavorite(const char *pName) const;
249
250 void RandomizeSkin(int Dummy);
251
252 const char *SkinPrefix() const;
253
254 static bool IsSpecialSkin(const char *pName);
255
256private:
257 static bool IsVanillaSkin(const char *pName);
258
259 /**
260 * Names of all vanilla and special skins.
261 *
262 * The names have to be in lower case for efficient comparison.
263 */
264 constexpr static const char *VANILLA_SKINS[] = {"bluekitty", "bluestripe", "brownbear",
265 "cammo", "cammostripes", "coala", "default", "limekitty",
266 "pinky", "redbopp", "redstripe", "saddo", "toptri",
267 "twinbop", "twintri", "warpaint", "x_ninja", "x_spec"};
268
269 class CSkinLoadJob : public CAbstractSkinLoadJob
270 {
271 public:
272 CSkinLoadJob(CSkins *pSkins, const char *pName, int StorageType);
273
274 protected:
275 void Run() override;
276
277 private:
278 int m_StorageType;
279 };
280
281 class CSkinDownloadJob : public CAbstractSkinLoadJob
282 {
283 public:
284 CSkinDownloadJob(CSkins *pSkins, const char *pName);
285
286 bool Abort() override REQUIRES(!m_Lock);
287
288 protected:
289 void Run() override REQUIRES(!m_Lock);
290
291 private:
292 CLock m_Lock;
293 std::shared_ptr<CHttpRequest> m_pGetRequest GUARDED_BY(m_Lock);
294 };
295
296 std::unordered_map<std::string_view, std::unique_ptr<CSkinContainer>> m_Skins;
297 std::optional<std::chrono::nanoseconds> m_ContainerUpdateTime;
298 /**
299 * Sorted from most recently to least recently used. Must be kept synchronized with the skin containers.
300 * Only contains pending and loaded skins as only these are unloaded.
301 */
302 std::list<std::string_view> m_SkinsUsageList;
303
304 CSkinList m_SkinList;
305 std::set<std::string> m_Favorites;
306
307 CSkin m_PlaceholderSkin;
308 char m_aEventSkinPrefix[MAX_SKIN_LENGTH];
309
310 bool LoadSkinData(const char *pName, CSkinLoadData &Data) const;
311 void LoadSkinFinish(CSkinContainer *pSkinContainer, const CSkinLoadData &Data);
312 void LoadSkinDirect(const char *pName);
313 const CSkinContainer *FindContainerImpl(const char *pName);
314 static int SkinScan(const char *pName, int IsDir, int StorageType, void *pUser);
315
316 void UpdateUnloadSkins(CSkinLoadingStats &Stats);
317 void UpdateStartLoading(CSkinLoadingStats &Stats);
318 void UpdateFinishLoading(CSkinLoadingStats &Stats, std::chrono::nanoseconds StartTime, std::chrono::nanoseconds MaxTime);
319
320 static void ConAddFavoriteSkin(IConsole::IResult *pResult, void *pUserData);
321 static void ConRemFavoriteSkin(IConsole::IResult *pResult, void *pUserData);
322 static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData);
323 void OnConfigSave(IConfigManager *pConfigManager);
324 static void ConchainRefreshSkinList(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
325};
326#endif
327