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