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 Refresh(TSkinLoadedCallback &&SkinLoadedCallback);
238 CSkinLoadingStats LoadingStats() const;
239 CSkinList &SkinList();
240
241 const CSkinContainer *FindContainerOrNullptr(const char *pName);
242 const CSkin *FindOrNullptr(const char *pName);
243 const CSkin *Find(const char *pName);
244
245 void AddFavorite(const char *pName);
246 void RemoveFavorite(const char *pName);
247 bool IsFavorite(const char *pName) const;
248
249 void RandomizeSkin(int Dummy);
250
251 const char *SkinPrefix() const;
252
253 static bool IsSpecialSkin(const char *pName);
254
255private:
256 static bool IsVanillaSkin(const char *pName);
257
258 /**
259 * Names of all vanilla and special skins.
260 *
261 * The names have to be in lower case for efficient comparison.
262 */
263 constexpr static const char *VANILLA_SKINS[] = {"bluekitty", "bluestripe", "brownbear",
264 "cammo", "cammostripes", "coala", "default", "limekitty",
265 "pinky", "redbopp", "redstripe", "saddo", "toptri",
266 "twinbop", "twintri", "warpaint", "x_ninja", "x_spec"};
267
268 class CSkinLoadJob : public CAbstractSkinLoadJob
269 {
270 public:
271 CSkinLoadJob(CSkins *pSkins, const char *pName, int StorageType);
272
273 protected:
274 void Run() override;
275
276 private:
277 int m_StorageType;
278 };
279
280 class CSkinDownloadJob : public CAbstractSkinLoadJob
281 {
282 public:
283 CSkinDownloadJob(CSkins *pSkins, const char *pName);
284
285 bool Abort() override REQUIRES(!m_Lock);
286
287 protected:
288 void Run() override REQUIRES(!m_Lock);
289
290 private:
291 CLock m_Lock;
292 std::shared_ptr<CHttpRequest> m_pGetRequest GUARDED_BY(m_Lock);
293 };
294
295 std::unordered_map<std::string_view, std::unique_ptr<CSkinContainer>> m_Skins;
296 std::optional<std::chrono::nanoseconds> m_ContainerUpdateTime;
297 /**
298 * Sorted from most recently to least recently used. Must be kept synchronized with the skin containers.
299 * Only contains pending and loaded skins as only these are unloaded.
300 */
301 std::list<std::string_view> m_SkinsUsageList;
302
303 CSkinList m_SkinList;
304 std::set<std::string> m_Favorites;
305
306 CSkin m_PlaceholderSkin;
307 char m_aEventSkinPrefix[MAX_SKIN_LENGTH];
308
309 bool LoadSkinData(const char *pName, CSkinLoadData &Data) const;
310 void LoadSkinFinish(CSkinContainer *pSkinContainer, const CSkinLoadData &Data);
311 void LoadSkinDirect(const char *pName);
312 const CSkinContainer *FindContainerImpl(const char *pName);
313 static int SkinScan(const char *pName, int IsDir, int StorageType, void *pUser);
314
315 void UpdateUnloadSkins(CSkinLoadingStats &Stats);
316 void UpdateStartLoading(CSkinLoadingStats &Stats);
317 void UpdateFinishLoading(CSkinLoadingStats &Stats, std::chrono::nanoseconds StartTime, std::chrono::nanoseconds MaxTime);
318
319 static void ConAddFavoriteSkin(IConsole::IResult *pResult, void *pUserData);
320 static void ConRemFavoriteSkin(IConsole::IResult *pResult, void *pUserData);
321 static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData);
322 void OnConfigSave(IConfigManager *pConfigManager);
323 static void ConchainRefreshSkinList(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
324};
325#endif
326