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 "skins.h"
5
6#include <base/dbg.h>
7#include <base/log.h>
8#include <base/math.h>
9#include <base/str.h>
10#include <base/time.h>
11
12#include <engine/engine.h>
13#include <engine/gfx/image_manipulation.h>
14#include <engine/graphics.h>
15#include <engine/shared/config.h>
16#include <engine/shared/http.h>
17#include <engine/storage.h>
18
19#include <generated/client_data.h>
20
21#include <game/client/gameclient.h>
22#include <game/localization.h>
23
24using namespace std::chrono_literals;
25
26CSkins::CAbstractSkinLoadJob::CAbstractSkinLoadJob(CSkins *pSkins, const char *pName) :
27 m_pSkins(pSkins)
28{
29 str_copy(dst&: m_aName, src: pName);
30 Abortable(Abortable: true);
31}
32
33CSkins::CAbstractSkinLoadJob::~CAbstractSkinLoadJob()
34{
35 m_Data.m_Info.Free();
36 m_Data.m_InfoGrayscale.Free();
37}
38
39CSkins::CSkinLoadJob::CSkinLoadJob(CSkins *pSkins, const char *pName, int StorageType) :
40 CAbstractSkinLoadJob(pSkins, pName),
41 m_StorageType(StorageType)
42{
43}
44
45CSkins::CSkinContainer::CSkinContainer(CSkins *pSkins, const char *pName, EType Type, int StorageType) :
46 m_pSkins(pSkins),
47 m_Type(Type),
48 m_StorageType(StorageType)
49{
50 str_copy(dst&: m_aName, src: pName);
51 m_Vanilla = IsVanillaSkin(pName: m_aName);
52 m_Special = IsSpecialSkin(pName: m_aName);
53 m_AlwaysLoaded = m_Vanilla; // Vanilla skins are loaded immediately and not unloaded
54}
55
56CSkins::CSkinContainer::~CSkinContainer()
57{
58 if(m_pLoadJob)
59 {
60 m_pLoadJob->Abort();
61 }
62}
63
64bool CSkins::CSkinContainer::operator<(const CSkinContainer &Other) const
65{
66 return str_comp(a: m_aName, b: Other.m_aName) < 0;
67}
68
69static constexpr std::chrono::nanoseconds MIN_REQUESTED_TIME_FOR_PENDING = 250ms;
70static constexpr std::chrono::nanoseconds MAX_REQUESTED_TIME_FOR_PENDING = 500ms;
71static constexpr std::chrono::nanoseconds MIN_UNLOAD_TIME_PENDING = 1s;
72static constexpr std::chrono::nanoseconds MIN_UNLOAD_TIME_LOADED = 2s;
73static_assert(MIN_REQUESTED_TIME_FOR_PENDING < MAX_REQUESTED_TIME_FOR_PENDING);
74static_assert(MIN_REQUESTED_TIME_FOR_PENDING < MIN_UNLOAD_TIME_PENDING, "Unloading pending skins must take longer than adding more pending skins");
75
76void CSkins::CSkinContainer::RequestLoad()
77{
78 if(m_AlwaysLoaded)
79 {
80 return;
81 }
82
83 // Delay loading skins a bit after the load has been requested to avoid loading a lot of skins
84 // when quickly scrolling through lists or if a player with a new skin quickly joins and leaves.
85 if(m_State == EState::UNLOADED)
86 {
87 const std::chrono::nanoseconds Now = time_get_nanoseconds();
88 if(!m_FirstLoadRequest.has_value() ||
89 !m_LastLoadRequest.has_value() ||
90 Now - m_LastLoadRequest.value() > MAX_REQUESTED_TIME_FOR_PENDING)
91 {
92 m_FirstLoadRequest = Now;
93 m_LastLoadRequest = m_FirstLoadRequest;
94 }
95 else if(Now - m_FirstLoadRequest.value() > MIN_REQUESTED_TIME_FOR_PENDING)
96 {
97 m_State = EState::PENDING;
98 }
99 }
100 else if(m_State == EState::PENDING ||
101 m_State == EState::LOADING ||
102 m_State == EState::LOADED)
103 {
104 m_LastLoadRequest = time_get_nanoseconds();
105 }
106
107 if(m_State == EState::PENDING ||
108 m_State == EState::LOADED)
109 {
110 if(m_UsageEntryIterator.has_value())
111 {
112 m_pSkins->m_SkinsUsageList.erase(position: m_UsageEntryIterator.value());
113 }
114 m_pSkins->m_SkinsUsageList.emplace_front(args: Name());
115 m_UsageEntryIterator = m_pSkins->m_SkinsUsageList.begin();
116 }
117}
118
119CSkins::CSkinContainer::EState CSkins::CSkinContainer::DetermineInitialState() const
120{
121 if(m_AlwaysLoaded)
122 {
123 // Load immediately if it should always be loaded
124 return EState::PENDING;
125 }
126 else if((g_Config.m_ClVanillaSkinsOnly && !m_Vanilla) ||
127 (m_Type == EType::DOWNLOAD && !g_Config.m_ClDownloadSkins))
128 {
129 // Fail immediately if it shouldn't be loaded
130 return EState::NOT_FOUND;
131 }
132 else
133 {
134 return EState::UNLOADED;
135 }
136}
137
138void CSkins::CSkinContainer::SetState(EState State)
139{
140 m_State = State;
141
142 if(m_State == EState::PENDING ||
143 m_State == EState::LOADING ||
144 m_State == EState::LOADED)
145 {
146 RequestLoad();
147 }
148 else
149 {
150 m_FirstLoadRequest = std::nullopt;
151 m_LastLoadRequest = std::nullopt;
152 }
153
154 if(m_State != EState::PENDING &&
155 m_State != EState::LOADED &&
156 m_UsageEntryIterator.has_value())
157 {
158 m_pSkins->m_SkinsUsageList.erase(position: m_UsageEntryIterator.value());
159 m_UsageEntryIterator = std::nullopt;
160 }
161
162 m_pSkins->m_SkinList.ForceRefresh();
163}
164
165bool CSkins::CSkinListEntry::operator<(const CSkins::CSkinListEntry &Other) const
166{
167 if(m_Favorite && !Other.m_Favorite)
168 {
169 return true;
170 }
171 if(!m_Favorite && Other.m_Favorite)
172 {
173 return false;
174 }
175 return str_comp(a: m_pSkinContainer->Name(), b: Other.m_pSkinContainer->Name()) < 0;
176}
177
178void CSkins::CSkinListEntry::RequestLoad()
179{
180 m_pSkinContainer->RequestLoad();
181}
182
183CSkins::CSkins() :
184 m_PlaceholderSkin("dummy")
185{
186 m_PlaceholderSkin.m_OriginalSkin.Reset();
187 m_PlaceholderSkin.m_ColorableSkin.Reset();
188 m_PlaceholderSkin.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
189 m_PlaceholderSkin.m_Metrics.m_Body.m_Width = 64;
190 m_PlaceholderSkin.m_Metrics.m_Body.m_Height = 64;
191 m_PlaceholderSkin.m_Metrics.m_Body.m_OffsetX = 16;
192 m_PlaceholderSkin.m_Metrics.m_Body.m_OffsetY = 16;
193 m_PlaceholderSkin.m_Metrics.m_Body.m_MaxWidth = 96;
194 m_PlaceholderSkin.m_Metrics.m_Body.m_MaxHeight = 96;
195 m_PlaceholderSkin.m_Metrics.m_Feet.m_Width = 32;
196 m_PlaceholderSkin.m_Metrics.m_Feet.m_Height = 16;
197 m_PlaceholderSkin.m_Metrics.m_Feet.m_OffsetX = 16;
198 m_PlaceholderSkin.m_Metrics.m_Feet.m_OffsetY = 8;
199 m_PlaceholderSkin.m_Metrics.m_Feet.m_MaxWidth = 64;
200 m_PlaceholderSkin.m_Metrics.m_Feet.m_MaxHeight = 32;
201}
202
203bool CSkins::IsSpecialSkin(const char *pName)
204{
205 return str_utf8_comp_nocase_num(a: pName, b: "x_", num: 2) == 0;
206}
207
208bool CSkins::IsVanillaSkin(const char *pName)
209{
210 return std::any_of(first: std::begin(arr: VANILLA_SKINS), last: std::end(arr: VANILLA_SKINS), pred: [pName](const char *pVanillaSkin) {
211 return str_comp(a: pName, b: pVanillaSkin) == 0;
212 });
213}
214
215class CSkinScanUser
216{
217public:
218 CSkins *m_pThis;
219 CSkins::TSkinLoadedCallback m_SkinLoadedCallback;
220};
221
222int CSkins::SkinScan(const char *pName, int IsDir, int StorageType, void *pUser)
223{
224 auto *pUserReal = static_cast<CSkinScanUser *>(pUser);
225 CSkins *pSelf = pUserReal->m_pThis;
226
227 if(IsDir)
228 {
229 return 0;
230 }
231
232 const char *pSuffix = str_endswith(str: pName, suffix: ".png");
233 if(pSuffix == nullptr)
234 {
235 return 0;
236 }
237
238 char aSkinName[IO_MAX_PATH_LENGTH];
239 str_truncate(dst: aSkinName, dst_size: sizeof(aSkinName), src: pName, truncation_len: pSuffix - pName);
240 if(!CSkin::IsValidName(pName: aSkinName))
241 {
242 log_error("skins", "Skin name is not valid: %s", aSkinName);
243 log_error("skins", "%s", CSkin::m_aSkinNameRestrictions);
244 return 0;
245 }
246
247 CSkinContainer SkinContainer(pSelf, aSkinName, CSkinContainer::EType::LOCAL, StorageType);
248 auto &&pSkinContainer = std::make_unique<CSkinContainer>(args: std::move(SkinContainer));
249 pSkinContainer->SetState(pSkinContainer->DetermineInitialState());
250 pSelf->m_Skins.insert(x: {pSkinContainer->Name(), std::move(pSkinContainer)});
251 pUserReal->m_SkinLoadedCallback();
252 return 0;
253}
254
255static void CheckMetrics(CSkin::CSkinMetricVariable &Metrics, const uint8_t *pImg, int ImgWidth, int ImgX, int ImgY, int CheckWidth, int CheckHeight)
256{
257 int MaxY = -1;
258 int MinY = CheckHeight + 1;
259 int MaxX = -1;
260 int MinX = CheckWidth + 1;
261
262 for(int y = 0; y < CheckHeight; y++)
263 {
264 for(int x = 0; x < CheckWidth; x++)
265 {
266 int OffsetAlpha = (y + ImgY) * ImgWidth + (x + ImgX) * 4 + 3;
267 uint8_t AlphaValue = pImg[OffsetAlpha];
268 if(AlphaValue > 0)
269 {
270 if(MaxY < y)
271 MaxY = y;
272 if(MinY > y)
273 MinY = y;
274 if(MaxX < x)
275 MaxX = x;
276 if(MinX > x)
277 MinX = x;
278 }
279 }
280 }
281
282 Metrics.m_Width = std::clamp(val: (MaxX - MinX) + 1, lo: 1, hi: CheckWidth);
283 Metrics.m_Height = std::clamp(val: (MaxY - MinY) + 1, lo: 1, hi: CheckHeight);
284 Metrics.m_OffsetX = std::clamp(val: MinX, lo: 0, hi: CheckWidth - 1);
285 Metrics.m_OffsetY = std::clamp(val: MinY, lo: 0, hi: CheckHeight - 1);
286 Metrics.m_MaxWidth = CheckWidth;
287 Metrics.m_MaxHeight = CheckHeight;
288}
289
290bool CSkins::LoadSkinData(const char *pName, CSkinLoadData &Data) const
291{
292 if(!Graphics()->CheckImageDivisibility(pContextName: pName, Image&: Data.m_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))
293 {
294 log_error("skins", "Skin failed image divisibility: %s", pName);
295 Data.m_Info.Free();
296 return false;
297 }
298 if(!Graphics()->IsImageFormatRgba(pContextName: pName, Image: Data.m_Info))
299 {
300 log_error("skins", "Skin format is not RGBA: %s", pName);
301 Data.m_Info.Free();
302 return false;
303 }
304 const size_t BodyWidth = g_pData->m_aSprites[SPRITE_TEE_BODY].m_W * (Data.m_Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridx);
305 const size_t BodyHeight = g_pData->m_aSprites[SPRITE_TEE_BODY].m_H * (Data.m_Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridy);
306 if(BodyWidth > Data.m_Info.m_Width || BodyHeight > Data.m_Info.m_Height)
307 {
308 log_error("skins", "Skin size unsupported (w=%" PRIzu ", h=%" PRIzu "): %s", Data.m_Info.m_Width, Data.m_Info.m_Height, pName);
309 Data.m_Info.Free();
310 return false;
311 }
312
313 int FeetGridPixelsWidth = Data.m_Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_FOOT].m_pSet->m_Gridx;
314 int FeetGridPixelsHeight = Data.m_Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_FOOT].m_pSet->m_Gridy;
315 int FeetWidth = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_W * FeetGridPixelsWidth;
316 int FeetHeight = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_H * FeetGridPixelsHeight;
317 int FeetOffsetX = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_X * FeetGridPixelsWidth;
318 int FeetOffsetY = g_pData->m_aSprites[SPRITE_TEE_FOOT].m_Y * FeetGridPixelsHeight;
319
320 int FeetOutlineGridPixelsWidth = Data.m_Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_pSet->m_Gridx;
321 int FeetOutlineGridPixelsHeight = Data.m_Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_pSet->m_Gridy;
322 int FeetOutlineWidth = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_W * FeetOutlineGridPixelsWidth;
323 int FeetOutlineHeight = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_H * FeetOutlineGridPixelsHeight;
324 int FeetOutlineOffsetX = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_X * FeetOutlineGridPixelsWidth;
325 int FeetOutlineOffsetY = g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE].m_Y * FeetOutlineGridPixelsHeight;
326
327 int BodyOutlineGridPixelsWidth = Data.m_Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_pSet->m_Gridx;
328 int BodyOutlineGridPixelsHeight = Data.m_Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_pSet->m_Gridy;
329 int BodyOutlineWidth = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_W * BodyOutlineGridPixelsWidth;
330 int BodyOutlineHeight = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_H * BodyOutlineGridPixelsHeight;
331 int BodyOutlineOffsetX = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_X * BodyOutlineGridPixelsWidth;
332 int BodyOutlineOffsetY = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_Y * BodyOutlineGridPixelsHeight;
333
334 const size_t PixelStep = Data.m_Info.PixelSize();
335 const size_t Pitch = Data.m_Info.m_Width * PixelStep;
336
337 // dig out blood color
338 {
339 int64_t aColors[3] = {0};
340 for(size_t y = 0; y < BodyHeight; y++)
341 {
342 for(size_t x = 0; x < BodyWidth; x++)
343 {
344 const size_t Offset = y * Pitch + x * PixelStep;
345 if(Data.m_Info.m_pData[Offset + 3] > 128)
346 {
347 for(size_t c = 0; c < 3; c++)
348 {
349 aColors[c] += Data.m_Info.m_pData[Offset + c];
350 }
351 }
352 }
353 }
354 const vec3 NormalizedColor = normalize(v: vec3(aColors[0], aColors[1], aColors[2]));
355 Data.m_BloodColor = ColorRGBA(NormalizedColor.x, NormalizedColor.y, NormalizedColor.z);
356 }
357
358 CheckMetrics(Metrics&: Data.m_Metrics.m_Body, pImg: Data.m_Info.m_pData, ImgWidth: Pitch, ImgX: 0, ImgY: 0, CheckWidth: BodyWidth, CheckHeight: BodyHeight);
359 CheckMetrics(Metrics&: Data.m_Metrics.m_Body, pImg: Data.m_Info.m_pData, ImgWidth: Pitch, ImgX: BodyOutlineOffsetX, ImgY: BodyOutlineOffsetY, CheckWidth: BodyOutlineWidth, CheckHeight: BodyOutlineHeight);
360 CheckMetrics(Metrics&: Data.m_Metrics.m_Feet, pImg: Data.m_Info.m_pData, ImgWidth: Pitch, ImgX: FeetOffsetX, ImgY: FeetOffsetY, CheckWidth: FeetWidth, CheckHeight: FeetHeight);
361 CheckMetrics(Metrics&: Data.m_Metrics.m_Feet, pImg: Data.m_Info.m_pData, ImgWidth: Pitch, ImgX: FeetOutlineOffsetX, ImgY: FeetOutlineOffsetY, CheckWidth: FeetOutlineWidth, CheckHeight: FeetOutlineHeight);
362
363 Data.m_InfoGrayscale = Data.m_Info.DeepCopy();
364 ConvertToGrayscale(Image: Data.m_InfoGrayscale);
365
366 int aFreq[256] = {0};
367 uint8_t OrgWeight = 1;
368 uint8_t NewWeight = 192;
369
370 // find most common non-zero frequency
371 for(size_t y = 0; y < BodyHeight; y++)
372 {
373 for(size_t x = 0; x < BodyWidth; x++)
374 {
375 const size_t Offset = y * Pitch + x * PixelStep;
376 if(Data.m_InfoGrayscale.m_pData[Offset + 3] > 128)
377 {
378 aFreq[Data.m_InfoGrayscale.m_pData[Offset]]++;
379 }
380 }
381 }
382
383 for(int i = 1; i < 256; i++)
384 {
385 if(aFreq[OrgWeight] < aFreq[i])
386 {
387 OrgWeight = i;
388 }
389 }
390
391 // reorder
392 for(size_t y = 0; y < BodyHeight; y++)
393 {
394 for(size_t x = 0; x < BodyWidth; x++)
395 {
396 const size_t Offset = y * Pitch + x * PixelStep;
397 uint8_t v = Data.m_InfoGrayscale.m_pData[Offset];
398 if(v <= OrgWeight)
399 {
400 v = (uint8_t)((v / (float)OrgWeight) * NewWeight);
401 }
402 else
403 {
404 v = (uint8_t)(((v - OrgWeight) / (float)(255 - OrgWeight)) * (255 - NewWeight) + NewWeight);
405 }
406 Data.m_InfoGrayscale.m_pData[Offset] = v;
407 Data.m_InfoGrayscale.m_pData[Offset + 1] = v;
408 Data.m_InfoGrayscale.m_pData[Offset + 2] = v;
409 }
410 }
411
412 return true;
413}
414
415void CSkins::LoadSkinFinish(CSkinContainer *pSkinContainer, const CSkinLoadData &Data)
416{
417 CSkin Skin{pSkinContainer->Name()};
418
419 Skin.m_OriginalSkin.m_Body = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY]);
420 Skin.m_OriginalSkin.m_BodyOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE]);
421 Skin.m_OriginalSkin.m_Feet = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT]);
422 Skin.m_OriginalSkin.m_FeetOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE]);
423 Skin.m_OriginalSkin.m_Hands = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND]);
424 Skin.m_OriginalSkin.m_HandsOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND_OUTLINE]);
425 for(size_t i = 0; i < std::size(Skin.m_OriginalSkin.m_aEyes); ++i)
426 {
427 Skin.m_OriginalSkin.m_aEyes[i] = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_Info, pSprite: &g_pData->m_aSprites[SPRITE_TEE_EYE_NORMAL + i]);
428 }
429
430 Skin.m_ColorableSkin.m_Body = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_InfoGrayscale, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY]);
431 Skin.m_ColorableSkin.m_BodyOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_InfoGrayscale, pSprite: &g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE]);
432 Skin.m_ColorableSkin.m_Feet = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_InfoGrayscale, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT]);
433 Skin.m_ColorableSkin.m_FeetOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_InfoGrayscale, pSprite: &g_pData->m_aSprites[SPRITE_TEE_FOOT_OUTLINE]);
434 Skin.m_ColorableSkin.m_Hands = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_InfoGrayscale, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND]);
435 Skin.m_ColorableSkin.m_HandsOutline = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_InfoGrayscale, pSprite: &g_pData->m_aSprites[SPRITE_TEE_HAND_OUTLINE]);
436 for(size_t i = 0; i < std::size(Skin.m_ColorableSkin.m_aEyes); ++i)
437 {
438 Skin.m_ColorableSkin.m_aEyes[i] = Graphics()->LoadSpriteTexture(FromImageInfo: Data.m_InfoGrayscale, pSprite: &g_pData->m_aSprites[SPRITE_TEE_EYE_NORMAL + i]);
439 }
440
441 Skin.m_Metrics = Data.m_Metrics;
442 Skin.m_BloodColor = Data.m_BloodColor;
443
444 if(g_Config.m_Debug)
445 {
446 log_trace("skins", "Loaded skin '%s'", Skin.GetName());
447 }
448
449 auto SkinIt = m_Skins.find(key: pSkinContainer->Name());
450 dbg_assert(SkinIt != m_Skins.end(), "LoadSkinFinish on skin '%s' which is not in m_Skins", pSkinContainer->Name());
451 SkinIt->second->m_pSkin = std::make_unique<CSkin>(args: std::move(Skin));
452 pSkinContainer->SetState(CSkinContainer::EState::LOADED);
453}
454
455void CSkins::LoadSkinDirect(const char *pName)
456{
457 if(m_Skins.contains(x: pName))
458 {
459 return;
460 }
461 CSkinContainer SkinContainer(this, pName, CSkinContainer::EType::LOCAL, IStorage::TYPE_ALL);
462 auto &&pSkinContainer = std::make_unique<CSkinContainer>(args: std::move(SkinContainer));
463 pSkinContainer->SetState(pSkinContainer->DetermineInitialState());
464 const auto &[SkinIt, _] = m_Skins.insert(x: {pSkinContainer->Name(), std::move(pSkinContainer)});
465
466 char aPath[IO_MAX_PATH_LENGTH];
467 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "skins/%s.png", pName);
468 CSkinLoadData DefaultSkinData;
469 SkinIt->second->SetState(CSkinContainer::EState::LOADING);
470 if(!Graphics()->LoadPng(Image&: DefaultSkinData.m_Info, pFilename: aPath, StorageType: SkinIt->second->StorageType()))
471 {
472 log_error("skins", "Failed to load PNG of skin '%s' from '%s'", pName, aPath);
473 SkinIt->second->SetState(CSkinContainer::EState::ERROR);
474 }
475 else if(LoadSkinData(pName, Data&: DefaultSkinData))
476 {
477 LoadSkinFinish(pSkinContainer: SkinIt->second.get(), Data: DefaultSkinData);
478 }
479 else
480 {
481 SkinIt->second->SetState(CSkinContainer::EState::ERROR);
482 }
483 DefaultSkinData.m_Info.Free();
484 DefaultSkinData.m_InfoGrayscale.Free();
485}
486
487void CSkins::OnConsoleInit()
488{
489 ConfigManager()->RegisterCallback(pfnFunc: CSkins::ConfigSaveCallback, pUserData: this);
490 Console()->Register(pName: "add_favorite_skin", pParams: "s[skin_name]", Flags: CFGFLAG_CLIENT, pfnFunc: ConAddFavoriteSkin, pUser: this, pHelp: "Add a skin as a favorite");
491 Console()->Register(pName: "remove_favorite_skin", pParams: "s[skin_name]", Flags: CFGFLAG_CLIENT, pfnFunc: ConRemFavoriteSkin, pUser: this, pHelp: "Remove a skin from the favorites");
492
493 Console()->Chain(pName: "player_skin", pfnChainFunc: ConchainRefreshSkinList, pUser: this);
494 Console()->Chain(pName: "dummy_skin", pfnChainFunc: ConchainRefreshSkinList, pUser: this);
495}
496
497void CSkins::OnInit()
498{
499 RefreshEventSkins();
500
501 // load skins
502 Refresh(SkinLoadedCallback: [this]() {
503 GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading DDNet Client"), pContent: Localize(pStr: "Loading skin files"), IncreaseCounter: 0);
504 });
505}
506
507void CSkins::OnShutdown()
508{
509 for(auto &[_, pSkinContainer] : m_Skins)
510 {
511 if(pSkinContainer->m_pLoadJob)
512 {
513 pSkinContainer->m_pLoadJob->Abort();
514 }
515 }
516 m_Skins.clear();
517}
518
519void CSkins::OnUpdate()
520{
521 // Only update skins periodically to reduce FPS impact
522 const std::chrono::nanoseconds StartTime = time_get_nanoseconds();
523 const std::chrono::nanoseconds MaxTime = std::chrono::milliseconds(std::clamp(val: round_to_int(f: Client()->RenderFrameTime() * 50000.0f), lo: 25, hi: 500));
524 if(m_ContainerUpdateTime.has_value() && StartTime - m_ContainerUpdateTime.value() < MaxTime)
525 {
526 return;
527 }
528 m_ContainerUpdateTime = StartTime;
529
530 // Update loaded state of managed skins which are not retrieved with the FindOrNullptr function
531 GameClient()->CollectManagedTeeRenderInfos(ActiveSkinAcceptor: [&](const char *pSkinName) {
532 // This will update the loaded state of the container
533 dbg_assert(FindContainerOrNullptr(pSkinName) != nullptr, "No skin container found for managed tee render info: %s", pSkinName);
534 });
535 // Keep player and dummy skin loaded
536 FindContainerOrNullptr(pName: g_Config.m_ClPlayerSkin);
537 FindContainerOrNullptr(pName: g_Config.m_ClDummySkin);
538
539 CSkinLoadingStats Stats = LoadingStats();
540 UpdateUnloadSkins(Stats);
541 UpdateStartLoading(Stats);
542 UpdateFinishLoading(Stats, StartTime, MaxTime);
543}
544
545void CSkins::UpdateUnloadSkins(CSkinLoadingStats &Stats)
546{
547 if(Stats.m_NumPending + Stats.m_NumLoaded + Stats.m_NumLoading <= (size_t)g_Config.m_ClSkinsLoadedMax)
548 {
549 return;
550 }
551
552 const std::chrono::nanoseconds UnloadStart = time_get_nanoseconds();
553 size_t NumToUnload = std::min<size_t>(a: Stats.m_NumPending + Stats.m_NumLoaded + Stats.m_NumLoading - (size_t)g_Config.m_ClSkinsLoadedMax, b: 16);
554 const size_t MaxSkipped = m_SkinsUsageList.size() / 8;
555 size_t NumSkipped = 0;
556 for(auto It = m_SkinsUsageList.rbegin(); It != m_SkinsUsageList.rend() && NumToUnload != 0 && NumSkipped < MaxSkipped; ++It)
557 {
558 auto SkinIt = m_Skins.find(key: *It);
559 dbg_assert(SkinIt != m_Skins.end(), "m_SkinsUsageList contains skin not in m_Skins");
560 auto &pSkinContainer = SkinIt->second;
561 dbg_assert(!pSkinContainer->m_AlwaysLoaded, "m_SkinsUsageList contains skins with m_AlwaysLoaded");
562 if(pSkinContainer->m_State != CSkinContainer::EState::PENDING &&
563 pSkinContainer->m_State != CSkinContainer::EState::LOADED)
564 {
565 dbg_assert(pSkinContainer->m_State == CSkinContainer::EState::LOADING, "m_SkinsUsageList contains skin which is not PENDING, LOADING or LOADED");
566 NumSkipped++;
567 continue;
568 }
569 const std::chrono::nanoseconds TimeUnused = UnloadStart - pSkinContainer->m_LastLoadRequest.value();
570 if(TimeUnused < (pSkinContainer->m_State == CSkinContainer::EState::LOADED ? MIN_UNLOAD_TIME_LOADED : MIN_UNLOAD_TIME_PENDING))
571 {
572 NumSkipped++;
573 continue;
574 }
575 if(pSkinContainer->m_State == CSkinContainer::EState::LOADED)
576 {
577 pSkinContainer->m_pSkin->m_OriginalSkin.Unload(pGraphics: Graphics());
578 pSkinContainer->m_pSkin->m_ColorableSkin.Unload(pGraphics: Graphics());
579 pSkinContainer->m_pSkin = nullptr;
580 Stats.m_NumLoaded--;
581 }
582 else
583 {
584 Stats.m_NumPending--;
585 }
586 Stats.m_NumUnloaded++;
587 pSkinContainer->SetState(CSkinContainer::EState::UNLOADED);
588 NumToUnload--;
589 }
590}
591
592void CSkins::UpdateStartLoading(CSkinLoadingStats &Stats)
593{
594 for(auto &[_, pSkinContainer] : m_Skins)
595 {
596 if(Stats.m_NumPending == 0 || Stats.m_NumLoading + Stats.m_NumLoaded >= (size_t)g_Config.m_ClSkinsLoadedMax)
597 {
598 break;
599 }
600 if(pSkinContainer->m_State != CSkinContainer::EState::PENDING)
601 {
602 continue;
603 }
604 switch(pSkinContainer->Type())
605 {
606 case CSkinContainer::EType::LOCAL:
607 pSkinContainer->m_pLoadJob = std::make_shared<CSkinLoadJob>(args: this, args: pSkinContainer->Name(), args: pSkinContainer->StorageType());
608 break;
609 case CSkinContainer::EType::DOWNLOAD:
610 pSkinContainer->m_pLoadJob = std::make_shared<CSkinDownloadJob>(args: this, args: pSkinContainer->Name());
611 break;
612 default:
613 dbg_assert_failed("pSkinContainer->Type() invalid");
614 }
615 Engine()->AddJob(pJob: pSkinContainer->m_pLoadJob);
616 pSkinContainer->SetState(CSkinContainer::EState::LOADING);
617 Stats.m_NumPending--;
618 Stats.m_NumLoading++;
619 }
620}
621
622void CSkins::UpdateFinishLoading(CSkinLoadingStats &Stats, std::chrono::nanoseconds StartTime, std::chrono::nanoseconds MaxTime)
623{
624 for(auto &[_, pSkinContainer] : m_Skins)
625 {
626 if(Stats.m_NumLoading == 0)
627 {
628 break;
629 }
630 if(pSkinContainer->m_State != CSkinContainer::EState::LOADING)
631 {
632 continue;
633 }
634 dbg_assert(pSkinContainer->m_pLoadJob != nullptr, "Skin container in loading state must have a load job");
635 if(!pSkinContainer->m_pLoadJob->Done())
636 {
637 continue;
638 }
639 Stats.m_NumLoading--;
640 if(pSkinContainer->m_pLoadJob->State() == IJob::STATE_DONE && pSkinContainer->m_pLoadJob->m_Data.m_Info.m_pData)
641 {
642 LoadSkinFinish(pSkinContainer: pSkinContainer.get(), Data: pSkinContainer->m_pLoadJob->m_Data);
643 GameClient()->OnSkinUpdate(pSkinName: pSkinContainer->Name());
644 pSkinContainer->m_pLoadJob = nullptr;
645 Stats.m_NumLoaded++;
646 if(time_get_nanoseconds() - StartTime >= MaxTime)
647 {
648 // Avoid using too much frame time for loading skins
649 break;
650 }
651 }
652 else
653 {
654 if(pSkinContainer->m_pLoadJob->State() == IJob::STATE_DONE && pSkinContainer->m_pLoadJob->m_NotFound)
655 {
656 pSkinContainer->SetState(CSkinContainer::EState::NOT_FOUND);
657 Stats.m_NumNotFound++;
658 }
659 else
660 {
661 pSkinContainer->SetState(CSkinContainer::EState::ERROR);
662 Stats.m_NumError++;
663 }
664 pSkinContainer->m_pLoadJob = nullptr;
665 }
666 }
667}
668
669void CSkins::RefreshEventSkins()
670{
671 m_aEventSkinPrefix[0] = '\0';
672
673 if(g_Config.m_Events)
674 {
675 if(time_season() == ETimeSeason::XMAS)
676 {
677 str_copy(dst&: m_aEventSkinPrefix, src: "santa");
678 }
679 }
680}
681
682void CSkins::Refresh(TSkinLoadedCallback &&SkinLoadedCallback)
683{
684 for(auto &[_, pSkinContainer] : m_Skins)
685 {
686 if(pSkinContainer->m_pLoadJob)
687 {
688 pSkinContainer->m_pLoadJob->Abort();
689 }
690 if(pSkinContainer->m_pSkin)
691 {
692 pSkinContainer->m_pSkin->m_OriginalSkin.Unload(pGraphics: Graphics());
693 pSkinContainer->m_pSkin->m_ColorableSkin.Unload(pGraphics: Graphics());
694 }
695 }
696 m_Skins.clear();
697 m_SkinsUsageList.clear();
698
699 LoadSkinDirect(pName: "default");
700 SkinLoadedCallback();
701
702 CSkinScanUser SkinScanUser;
703 SkinScanUser.m_pThis = this;
704 SkinScanUser.m_SkinLoadedCallback = SkinLoadedCallback;
705 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "skins", pfnCallback: SkinScan, pUser: &SkinScanUser);
706}
707
708CSkins::CSkinLoadingStats CSkins::LoadingStats() const
709{
710 CSkinLoadingStats Stats;
711 for(const auto &[_, pSkinContainer] : m_Skins)
712 {
713 switch(pSkinContainer->m_State)
714 {
715 case CSkinContainer::EState::UNLOADED:
716 Stats.m_NumUnloaded++;
717 break;
718 case CSkinContainer::EState::PENDING:
719 Stats.m_NumPending++;
720 break;
721 case CSkinContainer::EState::LOADING:
722 Stats.m_NumLoading++;
723 break;
724 case CSkinContainer::EState::LOADED:
725 Stats.m_NumLoaded++;
726 break;
727 case CSkinContainer::EState::ERROR:
728 Stats.m_NumError++;
729 break;
730 case CSkinContainer::EState::NOT_FOUND:
731 Stats.m_NumNotFound++;
732 break;
733 }
734 }
735 return Stats;
736}
737
738CSkins::CSkinList &CSkins::SkinList()
739{
740 if(!m_SkinList.m_NeedsUpdate)
741 {
742 return m_SkinList;
743 }
744
745 m_SkinList.m_vSkins.clear();
746 m_SkinList.m_UnfilteredCount = 0;
747
748 // Ensure all favorite skins are present as skin containers so they are included in the next loop.
749 for(const auto &FavoriteSkin : m_Favorites)
750 {
751 FindContainerOrNullptr(pName: FavoriteSkin.c_str());
752 }
753
754 m_SkinList.m_vSkins.reserve(n: m_Skins.size());
755 for(const auto &[_, pSkinContainer] : m_Skins)
756 {
757 if(pSkinContainer->IsSpecial())
758 {
759 continue;
760 }
761
762 const bool SelectedMain = str_comp(a: pSkinContainer->Name(), b: g_Config.m_ClPlayerSkin) == 0;
763 const bool SelectedDummy = str_comp(a: pSkinContainer->Name(), b: g_Config.m_ClDummySkin) == 0;
764 const bool Favorite = IsFavorite(pName: pSkinContainer->Name());
765
766 // Don't include skins in the list that couldn't be found in the database except the current player
767 // and dummy skins to avoid showing a lot of not-found entries while the user is typing a skin name.
768 if(pSkinContainer->m_State == CSkinContainer::EState::NOT_FOUND &&
769 !pSkinContainer->IsSpecial() &&
770 !SelectedMain &&
771 !SelectedDummy &&
772 !Favorite)
773 {
774 continue;
775 }
776 m_SkinList.m_UnfilteredCount++;
777
778 std::optional<std::pair<int, int>> NameMatch;
779 if(g_Config.m_ClSkinFilterString[0] != '\0')
780 {
781 const char *pNameMatchEnd;
782 const char *pNameMatchStart = str_utf8_find_nocase(haystack: pSkinContainer->Name(), needle: g_Config.m_ClSkinFilterString, end: &pNameMatchEnd);
783 if(pNameMatchStart == nullptr)
784 {
785 continue;
786 }
787 NameMatch = std::make_pair<int, int>(x: pNameMatchStart - pSkinContainer->Name(), y: pNameMatchEnd - pNameMatchStart);
788 }
789 m_SkinList.m_vSkins.emplace_back(args: pSkinContainer.get(), args: Favorite, args: SelectedMain, args: SelectedDummy, args&: NameMatch);
790 }
791
792 std::sort(first: m_SkinList.m_vSkins.begin(), last: m_SkinList.m_vSkins.end());
793 m_SkinList.m_NeedsUpdate = false;
794 return m_SkinList;
795}
796
797const CSkin *CSkins::Find(const char *pName)
798{
799 const auto *pSkin = FindOrNullptr(pName);
800 if(pSkin == nullptr)
801 {
802 pSkin = FindOrNullptr(pName: "default");
803 }
804 if(pSkin == nullptr)
805 {
806 pSkin = &m_PlaceholderSkin;
807 }
808 return pSkin;
809}
810
811const CSkins::CSkinContainer *CSkins::FindContainerOrNullptr(const char *pName)
812{
813 const char *pSkinPrefix = SkinPrefix();
814 if(pSkinPrefix[0] != '\0')
815 {
816 char aNameWithPrefix[2 * MAX_SKIN_LENGTH + 2]; // Larger than skin name length to allow IsValidName to check if it's too long
817 str_format(buffer: aNameWithPrefix, buffer_size: sizeof(aNameWithPrefix), format: "%s_%s", pSkinPrefix, pName);
818 // If we find something, use it, otherwise fall back to normal skins.
819 const CSkinContainer *pSkinContainer = FindContainerImpl(pName: aNameWithPrefix);
820 if(pSkinContainer != nullptr && pSkinContainer->State() == CSkinContainer::EState::LOADED)
821 {
822 return pSkinContainer;
823 }
824 }
825 return FindContainerImpl(pName);
826}
827
828const CSkins::CSkinContainer *CSkins::FindContainerImpl(const char *pName)
829{
830 if(!CSkin::IsValidName(pName))
831 {
832 return nullptr;
833 }
834
835 auto ExistingSkin = m_Skins.find(key: pName);
836 if(ExistingSkin == m_Skins.end())
837 {
838 CSkinContainer SkinContainer(this, pName, CSkinContainer::EType::DOWNLOAD, IStorage::TYPE_SAVE);
839 auto &&pSkinContainer = std::make_unique<CSkinContainer>(args: std::move(SkinContainer));
840 pSkinContainer->SetState(pSkinContainer->DetermineInitialState());
841 ExistingSkin = m_Skins.insert(x: {pSkinContainer->Name(), std::move(pSkinContainer)}).first;
842 }
843 ExistingSkin->second->RequestLoad();
844 return ExistingSkin->second.get();
845}
846
847const CSkin *CSkins::FindOrNullptr(const char *pName)
848{
849 const CSkinContainer *pSkinContainer = FindContainerOrNullptr(pName);
850 if(pSkinContainer == nullptr || pSkinContainer->m_State != CSkinContainer::EState::LOADED)
851 {
852 return nullptr;
853 }
854 return pSkinContainer->m_pSkin.get();
855}
856
857void CSkins::AddFavorite(const char *pName)
858{
859 if(!CSkin::IsValidName(pName))
860 {
861 log_error("skins", "Favorite skin name '%s' is not valid", pName);
862 log_error("skins", "%s", CSkin::m_aSkinNameRestrictions);
863 return;
864 }
865
866 const auto &[_, Inserted] = m_Favorites.emplace(args&: pName);
867 if(Inserted)
868 {
869 m_SkinList.ForceRefresh();
870 }
871}
872
873void CSkins::RemoveFavorite(const char *pName)
874{
875 const auto FavoriteIt = m_Favorites.find(x: pName);
876 if(FavoriteIt != m_Favorites.end())
877 {
878 m_Favorites.erase(position: FavoriteIt);
879 m_SkinList.ForceRefresh();
880 }
881}
882
883bool CSkins::IsFavorite(const char *pName) const
884{
885 return m_Favorites.contains(x: pName);
886}
887
888void CSkins::RandomizeSkin(int Dummy)
889{
890 static const float s_aSchemes[] = {1.0f / 2.0f, 1.0f / 3.0f, 1.0f / -3.0f, 1.0f / 12.0f, 1.0f / -12.0f}; // complementary, triadic, analogous
891 const bool UseCustomColor = Dummy ? g_Config.m_ClDummyUseCustomColor : g_Config.m_ClPlayerUseCustomColor;
892 if(UseCustomColor)
893 {
894 float GoalSat = random_float(min: 0.3f, max: 1.0f);
895 float MaxBodyLht = 1.0f - GoalSat * GoalSat; // max allowed lightness before we start losing saturation
896
897 ColorHSLA Body;
898 Body.h = random_float();
899 Body.l = random_float(min: 0.0f, max: MaxBodyLht);
900 Body.s = std::clamp(val: GoalSat * GoalSat / (1.0f - Body.l), lo: 0.0f, hi: 1.0f);
901
902 ColorHSLA Feet;
903 Feet.h = std::fmod(x: Body.h + s_aSchemes[rand() % std::size(s_aSchemes)], y: 1.0f);
904 Feet.l = random_float();
905 Feet.s = std::clamp(val: GoalSat * GoalSat / (1.0f - Feet.l), lo: 0.0f, hi: 1.0f);
906
907 unsigned *pColorBody = Dummy ? &g_Config.m_ClDummyColorBody : &g_Config.m_ClPlayerColorBody;
908 unsigned *pColorFeet = Dummy ? &g_Config.m_ClDummyColorFeet : &g_Config.m_ClPlayerColorFeet;
909
910 *pColorBody = Body.Pack(Alpha: false);
911 *pColorFeet = Feet.Pack(Alpha: false);
912 }
913
914 std::vector<const CSkinContainer *> vpConsideredSkins;
915 for(const auto &[_, pSkinContainer] : m_Skins)
916 {
917 if(pSkinContainer->m_State == CSkinContainer::EState::ERROR ||
918 pSkinContainer->m_State == CSkinContainer::EState::NOT_FOUND ||
919 pSkinContainer->IsSpecial())
920 {
921 continue;
922 }
923 vpConsideredSkins.push_back(x: pSkinContainer.get());
924 }
925 const char *pRandomSkin;
926 if(vpConsideredSkins.empty())
927 {
928 pRandomSkin = "default";
929 }
930 else
931 {
932 pRandomSkin = vpConsideredSkins[rand() % vpConsideredSkins.size()]->Name();
933 }
934
935 char *pSkinName = Dummy ? g_Config.m_ClDummySkin : g_Config.m_ClPlayerSkin;
936 const size_t SkinNameSize = Dummy ? sizeof(g_Config.m_ClDummySkin) : sizeof(g_Config.m_ClPlayerSkin);
937 str_copy(dst: pSkinName, src: pRandomSkin, dst_size: SkinNameSize);
938 m_SkinList.ForceRefresh();
939}
940
941const char *CSkins::SkinPrefix() const
942{
943 if(g_Config.m_ClVanillaSkinsOnly)
944 {
945 return "";
946 }
947 if(m_aEventSkinPrefix[0] != '\0')
948 {
949 return m_aEventSkinPrefix;
950 }
951 return g_Config.m_ClSkinPrefix;
952}
953
954void CSkins::CSkinLoadJob::Run()
955{
956 char aPath[IO_MAX_PATH_LENGTH];
957 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "skins/%s.png", m_aName);
958 if(m_pSkins->Graphics()->LoadPng(Image&: m_Data.m_Info, pFilename: aPath, StorageType: m_StorageType))
959 {
960 if(State() == IJob::STATE_ABORTED)
961 {
962 return;
963 }
964 m_pSkins->LoadSkinData(pName: m_aName, Data&: m_Data);
965 }
966 else
967 {
968 log_error("skins", "Failed to load PNG of skin '%s' from '%s'", m_aName, aPath);
969 }
970}
971
972CSkins::CSkinDownloadJob::CSkinDownloadJob(CSkins *pSkins, const char *pName) :
973 CAbstractSkinLoadJob(pSkins, pName)
974{
975}
976
977bool CSkins::CSkinDownloadJob::Abort()
978{
979 if(!CAbstractSkinLoadJob::Abort())
980 {
981 return false;
982 }
983
984 const CLockScope LockScope(m_Lock);
985 if(m_pGetRequest)
986 {
987 m_pGetRequest->Abort();
988 m_pGetRequest = nullptr;
989 }
990 return true;
991}
992
993void CSkins::CSkinDownloadJob::Run()
994{
995 const char *pBaseUrl = g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl;
996
997 char aEscapedName[256];
998 EscapeUrl(aBuf&: aEscapedName, pStr: m_aName);
999
1000 char aUrl[IO_MAX_PATH_LENGTH];
1001 str_format(buffer: aUrl, buffer_size: sizeof(aUrl), format: "%s%s.png", pBaseUrl, aEscapedName);
1002
1003 char aPathReal[IO_MAX_PATH_LENGTH];
1004 str_format(buffer: aPathReal, buffer_size: sizeof(aPathReal), format: "downloadedskins/%s.png", m_aName);
1005
1006 const CTimeout Timeout{.m_ConnectTimeoutMs: 10000, .m_TimeoutMs: 0, .m_LowSpeedLimit: 8192, .m_LowSpeedTime: 10};
1007 const size_t MaxResponseSize = 10 * 1024 * 1024; // 10 MiB
1008
1009 std::shared_ptr<CHttpRequest> pGet = HttpGetBoth(pUrl: aUrl, pStorage: m_pSkins->Storage(), pOutputFile: aPathReal, StorageType: IStorage::TYPE_SAVE);
1010 pGet->Timeout(Timeout);
1011 pGet->MaxResponseSize(MaxResponseSize);
1012 pGet->ValidateBeforeOverwrite(ValidateBeforeOverwrite: true);
1013 pGet->LogProgress(LogProgress: HTTPLOG::NONE);
1014 pGet->FailOnErrorStatus(FailOnErrorStatus: false);
1015 {
1016 const CLockScope LockScope(m_Lock);
1017 m_pGetRequest = pGet;
1018 }
1019 m_pSkins->Http()->Run(pRequest: pGet);
1020
1021 // Load existing file while waiting for the HTTP request
1022 {
1023 void *pPngData;
1024 unsigned PngSize;
1025 if(m_pSkins->Storage()->ReadFile(pFilename: aPathReal, Type: IStorage::TYPE_SAVE, ppResult: &pPngData, pResultLen: &PngSize))
1026 {
1027 if(m_pSkins->Graphics()->LoadPng(Image&: m_Data.m_Info, pData: static_cast<uint8_t *>(pPngData), DataSize: PngSize, pContextName: aPathReal))
1028 {
1029 if(State() == IJob::STATE_ABORTED)
1030 {
1031 return;
1032 }
1033 m_pSkins->LoadSkinData(pName: m_aName, Data&: m_Data);
1034 }
1035 free(ptr: pPngData);
1036 }
1037 }
1038
1039 pGet->Wait();
1040 {
1041 const CLockScope LockScope(m_Lock);
1042 m_pGetRequest = nullptr;
1043 }
1044 if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED || pGet->StatusCode() >= 400)
1045 {
1046 m_NotFound = pGet->State() == EHttpState::DONE && pGet->StatusCode() == 404; // 404 Not Found
1047 return;
1048 }
1049 if(pGet->StatusCode() == 304) // 304 Not Modified
1050 {
1051 bool Success = m_Data.m_Info.m_pData != nullptr;
1052 pGet->OnValidation(Success);
1053 if(Success)
1054 {
1055 return; // Local skin is up-to-date and was loaded successfully
1056 }
1057
1058 log_error("skins", "Failed to load PNG of existing downloaded skin '%s' from '%s', downloading it again", m_aName, aPathReal);
1059 pGet = HttpGetBoth(pUrl: aUrl, pStorage: m_pSkins->Storage(), pOutputFile: aPathReal, StorageType: IStorage::TYPE_SAVE);
1060 pGet->Timeout(Timeout);
1061 pGet->MaxResponseSize(MaxResponseSize);
1062 pGet->ValidateBeforeOverwrite(ValidateBeforeOverwrite: true);
1063 pGet->SkipByFileTime(SkipByFileTime: false);
1064 pGet->LogProgress(LogProgress: HTTPLOG::NONE);
1065 pGet->FailOnErrorStatus(FailOnErrorStatus: false);
1066 {
1067 const CLockScope LockScope(m_Lock);
1068 m_pGetRequest = pGet;
1069 }
1070 m_pSkins->Http()->Run(pRequest: pGet);
1071 pGet->Wait();
1072 {
1073 const CLockScope LockScope(m_Lock);
1074 m_pGetRequest = nullptr;
1075 }
1076 if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED || pGet->StatusCode() >= 400)
1077 {
1078 m_NotFound = pGet->State() == EHttpState::DONE && pGet->StatusCode() == 404; // 404 Not Found
1079 return;
1080 }
1081 }
1082
1083 unsigned char *pResult;
1084 size_t ResultSize;
1085 pGet->Result(ppResult: &pResult, pResultLength: &ResultSize);
1086
1087 m_Data.m_Info.Free();
1088 m_Data.m_InfoGrayscale.Free();
1089 const bool Success = m_pSkins->Graphics()->LoadPng(Image&: m_Data.m_Info, pData: pResult, DataSize: ResultSize, pContextName: aUrl);
1090 if(Success)
1091 {
1092 if(State() == IJob::STATE_ABORTED)
1093 {
1094 return;
1095 }
1096 m_pSkins->LoadSkinData(pName: m_aName, Data&: m_Data);
1097 }
1098 else
1099 {
1100 log_error("skins", "Failed to load PNG of skin '%s' downloaded from '%s' (size %" PRIzu ")", m_aName, aUrl, ResultSize);
1101 }
1102 pGet->OnValidation(Success);
1103}
1104
1105void CSkins::ConAddFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
1106{
1107 auto *pSelf = static_cast<CSkins *>(pUserData);
1108 pSelf->AddFavorite(pName: pResult->GetString(Index: 0));
1109}
1110
1111void CSkins::ConRemFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
1112{
1113 auto *pSelf = static_cast<CSkins *>(pUserData);
1114 pSelf->RemoveFavorite(pName: pResult->GetString(Index: 0));
1115}
1116
1117void CSkins::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData)
1118{
1119 auto *pSelf = static_cast<CSkins *>(pUserData);
1120 pSelf->OnConfigSave(pConfigManager);
1121}
1122
1123void CSkins::OnConfigSave(IConfigManager *pConfigManager)
1124{
1125 for(const auto &Favorite : m_Favorites)
1126 {
1127 char aBuffer[32 + MAX_SKIN_LENGTH];
1128 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "add_favorite_skin \"%s\"", Favorite.c_str());
1129 pConfigManager->WriteLine(pLine: aBuffer);
1130 }
1131}
1132
1133void CSkins::ConchainRefreshSkinList(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1134{
1135 CSkins *pThis = static_cast<CSkins *>(pUserData);
1136 pfnCallback(pResult, pCallbackUserData);
1137 if(pResult->NumArguments())
1138 {
1139 pThis->m_SkinList.ForceRefresh();
1140 }
1141}
1142