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
4#include <base/log.h>
5#include <base/math.h>
6#include <base/system.h>
7
8#include <engine/engine.h>
9#include <engine/graphics.h>
10#include <engine/shared/config.h>
11#include <engine/storage.h>
12
13#include <game/generated/client_data.h>
14
15#include <game/client/gameclient.h>
16#include <game/localization.h>
17
18#include "skins.h"
19
20CSkins::CSkins() :
21 m_PlaceholderSkin("dummy")
22{
23 m_PlaceholderSkin.m_OriginalSkin.Reset();
24 m_PlaceholderSkin.m_ColorableSkin.Reset();
25 m_PlaceholderSkin.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
26 m_PlaceholderSkin.m_Metrics.m_Body.m_Width = 64;
27 m_PlaceholderSkin.m_Metrics.m_Body.m_Height = 64;
28 m_PlaceholderSkin.m_Metrics.m_Body.m_OffsetX = 16;
29 m_PlaceholderSkin.m_Metrics.m_Body.m_OffsetY = 16;
30 m_PlaceholderSkin.m_Metrics.m_Body.m_MaxWidth = 96;
31 m_PlaceholderSkin.m_Metrics.m_Body.m_MaxHeight = 96;
32 m_PlaceholderSkin.m_Metrics.m_Feet.m_Width = 32;
33 m_PlaceholderSkin.m_Metrics.m_Feet.m_Height = 16;
34 m_PlaceholderSkin.m_Metrics.m_Feet.m_OffsetX = 16;
35 m_PlaceholderSkin.m_Metrics.m_Feet.m_OffsetY = 8;
36 m_PlaceholderSkin.m_Metrics.m_Feet.m_MaxWidth = 64;
37 m_PlaceholderSkin.m_Metrics.m_Feet.m_MaxHeight = 32;
38}
39
40bool CSkins::IsVanillaSkin(const char *pName)
41{
42 return std::any_of(first: std::begin(arr: VANILLA_SKINS), last: std::end(arr: VANILLA_SKINS), pred: [pName](const char *pVanillaSkin) { return str_comp(a: pName, b: pVanillaSkin) == 0; });
43}
44
45void CSkins::CGetPngFile::OnCompletion(EHttpState State)
46{
47 // Maybe this should start another thread to load the png in instead of stalling the curl thread
48 if(State == EHttpState::DONE)
49 {
50 m_pSkins->LoadSkinPng(Info&: m_Info, pName: Dest(), pPath: Dest(), DirType: IStorage::TYPE_SAVE);
51 }
52}
53
54CSkins::CGetPngFile::CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest) :
55 CHttpRequest(pUrl),
56 m_pSkins(pSkins)
57{
58 WriteToFile(pStorage, pDest, StorageType: IStorage::TYPE_SAVE);
59 Timeout(Timeout: CTimeout{.ConnectTimeoutMs: 0, .TimeoutMs: 0, .LowSpeedLimit: 0, .LowSpeedTime: 0});
60 LogProgress(LogProgress: HTTPLOG::NONE);
61}
62
63struct SSkinScanUser
64{
65 CSkins *m_pThis;
66 CSkins::TSkinLoadedCBFunc m_SkinLoadedFunc;
67};
68
69int CSkins::SkinScan(const char *pName, int IsDir, int DirType, void *pUser)
70{
71 auto *pUserReal = static_cast<SSkinScanUser *>(pUser);
72 CSkins *pSelf = pUserReal->m_pThis;
73
74 if(IsDir)
75 return 0;
76
77 const char *pSuffix = str_endswith(str: pName, suffix: ".png");
78 if(pSuffix == nullptr)
79 return 0;
80
81 char aSkinName[IO_MAX_PATH_LENGTH];
82 str_truncate(dst: aSkinName, dst_size: sizeof(aSkinName), src: pName, truncation_len: pSuffix - pName);
83 if(!CSkin::IsValidName(pName: aSkinName))
84 {
85 log_error("skins", "Skin name is not valid: %s", aSkinName);
86 log_error("skins", "%s", CSkin::m_aSkinNameRestrictions);
87 return 0;
88 }
89
90 if(g_Config.m_ClVanillaSkinsOnly && !IsVanillaSkin(pName: aSkinName))
91 return 0;
92
93 char aPath[IO_MAX_PATH_LENGTH];
94 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "skins/%s", pName);
95 pSelf->LoadSkin(pName: aSkinName, pPath: aPath, DirType);
96 pUserReal->m_SkinLoadedFunc((int)pSelf->m_Skins.size());
97 return 0;
98}
99
100static void CheckMetrics(CSkin::SSkinMetricVariable &Metrics, const uint8_t *pImg, int ImgWidth, int ImgX, int ImgY, int CheckWidth, int CheckHeight)
101{
102 int MaxY = -1;
103 int MinY = CheckHeight + 1;
104 int MaxX = -1;
105 int MinX = CheckWidth + 1;
106
107 for(int y = 0; y < CheckHeight; y++)
108 {
109 for(int x = 0; x < CheckWidth; x++)
110 {
111 int OffsetAlpha = (y + ImgY) * ImgWidth + (x + ImgX) * 4 + 3;
112 uint8_t AlphaValue = pImg[OffsetAlpha];
113 if(AlphaValue > 0)
114 {
115 if(MaxY < y)
116 MaxY = y;
117 if(MinY > y)
118 MinY = y;
119 if(MaxX < x)
120 MaxX = x;
121 if(MinX > x)
122 MinX = x;
123 }
124 }
125 }
126
127 Metrics.m_Width = clamp(val: (MaxX - MinX) + 1, lo: 1, hi: CheckWidth);
128 Metrics.m_Height = clamp(val: (MaxY - MinY) + 1, lo: 1, hi: CheckHeight);
129 Metrics.m_OffsetX = clamp(val: MinX, lo: 0, hi: CheckWidth - 1);
130 Metrics.m_OffsetY = clamp(val: MinY, lo: 0, hi: CheckHeight - 1);
131 Metrics.m_MaxWidth = CheckWidth;
132 Metrics.m_MaxHeight = CheckHeight;
133}
134
135const CSkin *CSkins::LoadSkin(const char *pName, const char *pPath, int DirType)
136{
137 CImageInfo Info;
138 if(!LoadSkinPng(Info, pName, pPath, DirType))
139 return nullptr;
140 return LoadSkin(pName, Info);
141}
142
143bool CSkins::LoadSkinPng(CImageInfo &Info, const char *pName, const char *pPath, int DirType)
144{
145 if(!Graphics()->LoadPng(Image&: Info, pFilename: pPath, StorageType: DirType))
146 {
147 log_error("skins", "Failed to load skin PNG: %s", pName);
148 return false;
149 }
150 return true;
151}
152
153const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info)
154{
155 if(!Graphics()->CheckImageDivisibility(pFileName: pName, Img&: Info, DivX: g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridx, DivY: g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridy, AllowResize: true))
156 {
157 log_error("skins", "Skin failed image divisibility: %s", pName);
158 return nullptr;
159 }
160 if(!Graphics()->IsImageFormatRGBA(pFileName: pName, Img&: Info))
161 {
162 log_error("skins", "Skin format is not RGBA: %s", pName);
163 return nullptr;
164 }
165
166 CSkin Skin{pName};
167 Skin.m_OriginalSkin.m_Body = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY]);
168 Skin.m_OriginalSkin.m_BodyOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE]);
169 Skin.m_OriginalSkin.m_Feet = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT]);
170 Skin.m_OriginalSkin.m_FeetOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE]);
171 Skin.m_OriginalSkin.m_Hands = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND]);
172 Skin.m_OriginalSkin.m_HandsOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND_OUTLINE]);
173
174 for(int i = 0; i < 6; ++i)
175 Skin.m_OriginalSkin.m_aEyes[i] = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_EYE_NORMAL + i]);
176
177 int FeetGridPixelsWidth = (Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_FOOT].m_pSet->m_Gridx);
178 int FeetGridPixelsHeight = (Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_FOOT].m_pSet->m_Gridy);
179 int FeetWidth = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_W * FeetGridPixelsWidth;
180 int FeetHeight = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_H * FeetGridPixelsHeight;
181
182 int FeetOffsetX = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_X * FeetGridPixelsWidth;
183 int FeetOffsetY = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_Y * FeetGridPixelsHeight;
184
185 int FeetOutlineGridPixelsWidth = (Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_pSet->m_Gridx);
186 int FeetOutlineGridPixelsHeight = (Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_pSet->m_Gridy);
187 int FeetOutlineWidth = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_W * FeetOutlineGridPixelsWidth;
188 int FeetOutlineHeight = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_H * FeetOutlineGridPixelsHeight;
189
190 int FeetOutlineOffsetX = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_X * FeetOutlineGridPixelsWidth;
191 int FeetOutlineOffsetY = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_Y * FeetOutlineGridPixelsHeight;
192
193 int BodyOutlineGridPixelsWidth = (Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_pSet->m_Gridx);
194 int BodyOutlineGridPixelsHeight = (Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_pSet->m_Gridy);
195 int BodyOutlineWidth = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_W * BodyOutlineGridPixelsWidth;
196 int BodyOutlineHeight = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_H * BodyOutlineGridPixelsHeight;
197
198 int BodyOutlineOffsetX = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_X * BodyOutlineGridPixelsWidth;
199 int BodyOutlineOffsetY = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_Y * BodyOutlineGridPixelsHeight;
200
201 size_t BodyWidth = g_pData->m_aSprites[SPRITE_TEE_BODY].m_W * (Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridx); // body width
202 size_t BodyHeight = g_pData->m_aSprites[SPRITE_TEE_BODY].m_H * (Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridy); // body height
203 if(BodyWidth > Info.m_Width || BodyHeight > Info.m_Height)
204 return nullptr;
205 uint8_t *pData = Info.m_pData;
206 const int PixelStep = 4;
207 int Pitch = Info.m_Width * PixelStep;
208
209 // dig out blood color
210 {
211 int64_t aColors[3] = {0};
212 for(size_t y = 0; y < BodyHeight; y++)
213 {
214 for(size_t x = 0; x < BodyWidth; x++)
215 {
216 uint8_t AlphaValue = pData[y * Pitch + x * PixelStep + 3];
217 if(AlphaValue > 128)
218 {
219 aColors[0] += pData[y * Pitch + x * PixelStep + 0];
220 aColors[1] += pData[y * Pitch + x * PixelStep + 1];
221 aColors[2] += pData[y * Pitch + x * PixelStep + 2];
222 }
223 }
224 }
225
226 Skin.m_BloodColor = ColorRGBA(normalize(v: vec3(aColors[0], aColors[1], aColors[2])));
227 }
228
229 CheckMetrics(Metrics&: Skin.m_Metrics.m_Body, pImg: pData, ImgWidth: Pitch, ImgX: 0, ImgY: 0, CheckWidth: BodyWidth, CheckHeight: BodyHeight);
230
231 // body outline metrics
232 CheckMetrics(Metrics&: Skin.m_Metrics.m_Body, pImg: pData, ImgWidth: Pitch, ImgX: BodyOutlineOffsetX, ImgY: BodyOutlineOffsetY, CheckWidth: BodyOutlineWidth, CheckHeight: BodyOutlineHeight);
233
234 // get feet size
235 CheckMetrics(Metrics&: Skin.m_Metrics.m_Feet, pImg: pData, ImgWidth: Pitch, ImgX: FeetOffsetX, ImgY: FeetOffsetY, CheckWidth: FeetWidth, CheckHeight: FeetHeight);
236
237 // get feet outline size
238 CheckMetrics(Metrics&: Skin.m_Metrics.m_Feet, pImg: pData, ImgWidth: Pitch, ImgX: FeetOutlineOffsetX, ImgY: FeetOutlineOffsetY, CheckWidth: FeetOutlineWidth, CheckHeight: FeetOutlineHeight);
239
240 // make the texture gray scale
241 for(size_t i = 0; i < Info.m_Width * Info.m_Height; i++)
242 {
243 int v = (pData[i * PixelStep] + pData[i * PixelStep + 1] + pData[i * PixelStep + 2]) / 3;
244 pData[i * PixelStep] = v;
245 pData[i * PixelStep + 1] = v;
246 pData[i * PixelStep + 2] = v;
247 }
248
249 int aFreq[256] = {0};
250 int OrgWeight = 0;
251 int NewWeight = 192;
252
253 // find most common frequency
254 for(size_t y = 0; y < BodyHeight; y++)
255 for(size_t x = 0; x < BodyWidth; x++)
256 {
257 if(pData[y * Pitch + x * PixelStep + 3] > 128)
258 aFreq[pData[y * Pitch + x * PixelStep]]++;
259 }
260
261 for(int i = 1; i < 256; i++)
262 {
263 if(aFreq[OrgWeight] < aFreq[i])
264 OrgWeight = i;
265 }
266
267 // reorder
268 int InvOrgWeight = 255 - OrgWeight;
269 int InvNewWeight = 255 - NewWeight;
270 for(size_t y = 0; y < BodyHeight; y++)
271 for(size_t x = 0; x < BodyWidth; x++)
272 {
273 int v = pData[y * Pitch + x * PixelStep];
274 if(v <= OrgWeight && OrgWeight == 0)
275 v = 0;
276 else if(v <= OrgWeight)
277 v = (int)(((v / (float)OrgWeight) * NewWeight));
278 else if(InvOrgWeight == 0)
279 v = NewWeight;
280 else
281 v = (int)(((v - OrgWeight) / (float)InvOrgWeight) * InvNewWeight + NewWeight);
282 pData[y * Pitch + x * PixelStep] = v;
283 pData[y * Pitch + x * PixelStep + 1] = v;
284 pData[y * Pitch + x * PixelStep + 2] = v;
285 }
286
287 Skin.m_ColorableSkin.m_Body = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY]);
288 Skin.m_ColorableSkin.m_BodyOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE]);
289 Skin.m_ColorableSkin.m_Feet = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT]);
290 Skin.m_ColorableSkin.m_FeetOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE]);
291 Skin.m_ColorableSkin.m_Hands = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND]);
292 Skin.m_ColorableSkin.m_HandsOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND_OUTLINE]);
293
294 for(int i = 0; i < 6; ++i)
295 Skin.m_ColorableSkin.m_aEyes[i] = Graphics()->LoadSpriteTexture(FromImageInfo: Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_EYE_NORMAL + i]);
296
297 Info.Free();
298
299 if(g_Config.m_Debug)
300 {
301 log_trace("skins", "Loaded skin %s", Skin.GetName());
302 }
303
304 auto &&pSkin = std::make_unique<CSkin>(args: std::move(Skin));
305 const auto SkinInsertIt = m_Skins.insert(x: {pSkin->GetName(), std::move(pSkin)});
306
307 return SkinInsertIt.first->second.get();
308}
309
310void CSkins::OnInit()
311{
312 m_aEventSkinPrefix[0] = '\0';
313
314 if(g_Config.m_Events)
315 {
316 if(time_season() == SEASON_XMAS)
317 {
318 str_copy(dst&: m_aEventSkinPrefix, src: "santa");
319 }
320 }
321
322 // load skins;
323 Refresh(SkinLoadedFunc: [this](int SkinCounter) {
324 GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading DDNet Client"), pContent: Localize(pStr: "Loading skin files"), IncreaseCounter: 0);
325 });
326}
327
328void CSkins::Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc)
329{
330 for(const auto &[_, pSkin] : m_Skins)
331 {
332 pSkin->m_OriginalSkin.Unload(pGraphics: Graphics());
333 pSkin->m_ColorableSkin.Unload(pGraphics: Graphics());
334 }
335
336 m_Skins.clear();
337 m_DownloadSkins.clear();
338 m_DownloadingSkins = 0;
339 SSkinScanUser SkinScanUser;
340 SkinScanUser.m_pThis = this;
341 SkinScanUser.m_SkinLoadedFunc = SkinLoadedFunc;
342 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "skins", pfnCallback: SkinScan, pUser: &SkinScanUser);
343}
344
345int CSkins::Num()
346{
347 return m_Skins.size();
348}
349
350const CSkin *CSkins::Find(const char *pName)
351{
352 const auto *pSkin = FindOrNullptr(pName);
353 if(pSkin == nullptr)
354 {
355 pSkin = FindOrNullptr(pName: "default");
356 }
357 if(pSkin == nullptr)
358 {
359 pSkin = &m_PlaceholderSkin;
360 }
361 return pSkin;
362}
363
364const CSkin *CSkins::FindOrNullptr(const char *pName, bool IgnorePrefix)
365{
366 if(g_Config.m_ClVanillaSkinsOnly && !IsVanillaSkin(pName))
367 {
368 return nullptr;
369 }
370
371 const char *pSkinPrefix = m_aEventSkinPrefix[0] != '\0' ? m_aEventSkinPrefix : g_Config.m_ClSkinPrefix;
372 if(!IgnorePrefix && pSkinPrefix[0] != '\0')
373 {
374 char aNameWithPrefix[48]; // Larger than skin name length to allow IsValidName to check if it's too long
375 str_format(buffer: aNameWithPrefix, buffer_size: sizeof(aNameWithPrefix), format: "%s_%s", pSkinPrefix, pName);
376 // If we find something, use it, otherwise fall back to normal skins.
377 const auto *pResult = FindImpl(pName: aNameWithPrefix);
378 if(pResult != nullptr)
379 {
380 return pResult;
381 }
382 }
383
384 return FindImpl(pName);
385}
386
387const CSkin *CSkins::FindImpl(const char *pName)
388{
389 auto SkinIt = m_Skins.find(x: pName);
390 if(SkinIt != m_Skins.end())
391 return SkinIt->second.get();
392
393 if(str_comp(a: pName, b: "default") == 0)
394 return nullptr;
395
396 if(!g_Config.m_ClDownloadSkins)
397 return nullptr;
398
399 if(!CSkin::IsValidName(pName))
400 return nullptr;
401
402 const auto SkinDownloadIt = m_DownloadSkins.find(x: pName);
403 if(SkinDownloadIt != m_DownloadSkins.end())
404 {
405 if(SkinDownloadIt->second->m_pTask && SkinDownloadIt->second->m_pTask->State() == EHttpState::DONE && SkinDownloadIt->second->m_pTask->m_Info.m_pData)
406 {
407 char aPath[IO_MAX_PATH_LENGTH];
408 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "downloadedskins/%s.png", SkinDownloadIt->second->GetName());
409 Storage()->RenameFile(pOldFilename: SkinDownloadIt->second->m_aPath, pNewFilename: aPath, Type: IStorage::TYPE_SAVE);
410 const auto *pSkin = LoadSkin(pName: SkinDownloadIt->second->GetName(), Info&: SkinDownloadIt->second->m_pTask->m_Info);
411 SkinDownloadIt->second->m_pTask = nullptr;
412 --m_DownloadingSkins;
413 return pSkin;
414 }
415 if(SkinDownloadIt->second->m_pTask && (SkinDownloadIt->second->m_pTask->State() == EHttpState::ERROR || SkinDownloadIt->second->m_pTask->State() == EHttpState::ABORTED))
416 {
417 SkinDownloadIt->second->m_pTask = nullptr;
418 --m_DownloadingSkins;
419 }
420 return nullptr;
421 }
422
423 CDownloadSkin Skin{pName};
424
425 char aEscapedName[256];
426 EscapeUrl(pBuf: aEscapedName, Size: sizeof(aEscapedName), pStr: pName);
427 char aUrl[IO_MAX_PATH_LENGTH];
428 str_format(buffer: aUrl, buffer_size: sizeof(aUrl), format: "%s%s.png", g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl, aEscapedName);
429
430 char aBuf[IO_MAX_PATH_LENGTH];
431 str_format(buffer: Skin.m_aPath, buffer_size: sizeof(Skin.m_aPath), format: "downloadedskins/%s", IStorage::FormatTmpPath(aBuf, BufSize: sizeof(aBuf), pPath: pName));
432
433 Skin.m_pTask = std::make_shared<CGetPngFile>(args: this, args&: aUrl, args: Storage(), args&: Skin.m_aPath);
434 Http()->Run(pRequest: Skin.m_pTask);
435
436 auto &&pDownloadSkin = std::make_unique<CDownloadSkin>(args: std::move(Skin));
437 m_DownloadSkins.insert(x: {pDownloadSkin->GetName(), std::move(pDownloadSkin)});
438 ++m_DownloadingSkins;
439
440 return nullptr;
441}
442