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