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 RefreshEventSkins();
498
499 // load skins
500 Refresh(SkinLoadedCallback: [this]() {
501 GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading DDNet Client"), pContent: Localize(pStr: "Loading skin files"), IncreaseCounter: 0);
502 });
503}
504
505void CSkins::OnShutdown()
506{
507 for(auto &[_, pSkinContainer] : m_Skins)
508 {
509 if(pSkinContainer->m_pLoadJob)
510 {
511 pSkinContainer->m_pLoadJob->Abort();
512 }
513 }
514 m_Skins.clear();
515}
516
517void CSkins::OnUpdate()
518{
519 // Only update skins periodically to reduce FPS impact
520 const std::chrono::nanoseconds StartTime = time_get_nanoseconds();
521 const std::chrono::nanoseconds MaxTime = std::chrono::milliseconds(std::clamp(val: round_to_int(f: Client()->RenderFrameTime() * 50000.0f), lo: 25, hi: 500));
522 if(m_ContainerUpdateTime.has_value() && StartTime - m_ContainerUpdateTime.value() < MaxTime)
523 {
524 return;
525 }
526 m_ContainerUpdateTime = StartTime;
527
528 // Update loaded state of managed skins which are not retrieved with the FindOrNullptr function
529 GameClient()->CollectManagedTeeRenderInfos(ActiveSkinAcceptor: [&](const char *pSkinName) {
530 // This will update the loaded state of the container
531 dbg_assert(FindContainerOrNullptr(pSkinName) != nullptr, "No skin container found for managed tee render info: %s", pSkinName);
532 });
533 // Keep player and dummy skin loaded
534 FindContainerOrNullptr(pName: g_Config.m_ClPlayerSkin);
535 FindContainerOrNullptr(pName: g_Config.m_ClDummySkin);
536
537 CSkinLoadingStats Stats = LoadingStats();
538 UpdateUnloadSkins(Stats);
539 UpdateStartLoading(Stats);
540 UpdateFinishLoading(Stats, StartTime, MaxTime);
541}
542
543void CSkins::UpdateUnloadSkins(CSkinLoadingStats &Stats)
544{
545 if(Stats.m_NumPending + Stats.m_NumLoaded + Stats.m_NumLoading <= (size_t)g_Config.m_ClSkinsLoadedMax)
546 {
547 return;
548 }
549
550 const std::chrono::nanoseconds UnloadStart = time_get_nanoseconds();
551 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);
552 const size_t MaxSkipped = m_SkinsUsageList.size() / 8;
553 size_t NumSkipped = 0;
554 for(auto It = m_SkinsUsageList.rbegin(); It != m_SkinsUsageList.rend() && NumToUnload != 0 && NumSkipped < MaxSkipped; ++It)
555 {
556 auto SkinIt = m_Skins.find(x: *It);
557 dbg_assert(SkinIt != m_Skins.end(), "m_SkinsUsageList contains skin not in m_Skins");
558 auto &pSkinContainer = SkinIt->second;
559 dbg_assert(!pSkinContainer->m_AlwaysLoaded, "m_SkinsUsageList contains skins with m_AlwaysLoaded");
560 if(pSkinContainer->m_State != CSkinContainer::EState::PENDING &&
561 pSkinContainer->m_State != CSkinContainer::EState::LOADED)
562 {
563 dbg_assert(pSkinContainer->m_State == CSkinContainer::EState::LOADING, "m_SkinsUsageList contains skin which is not PENDING, LOADING or LOADED");
564 NumSkipped++;
565 continue;
566 }
567 const std::chrono::nanoseconds TimeUnused = UnloadStart - pSkinContainer->m_LastLoadRequest.value();
568 if(TimeUnused < (pSkinContainer->m_State == CSkinContainer::EState::LOADED ? MIN_UNLOAD_TIME_LOADED : MIN_UNLOAD_TIME_PENDING))
569 {
570 NumSkipped++;
571 continue;
572 }
573 if(pSkinContainer->m_State == CSkinContainer::EState::LOADED)
574 {
575 pSkinContainer->m_pSkin->m_OriginalSkin.Unload(pGraphics: Graphics());
576 pSkinContainer->m_pSkin->m_ColorableSkin.Unload(pGraphics: Graphics());
577 pSkinContainer->m_pSkin = nullptr;
578 Stats.m_NumLoaded--;
579 }
580 else
581 {
582 Stats.m_NumPending--;
583 }
584 Stats.m_NumUnloaded++;
585 pSkinContainer->SetState(CSkinContainer::EState::UNLOADED);
586 NumToUnload--;
587 }
588}
589
590void CSkins::UpdateStartLoading(CSkinLoadingStats &Stats)
591{
592 for(auto &[_, pSkinContainer] : m_Skins)
593 {
594 if(Stats.m_NumPending == 0 || Stats.m_NumLoading + Stats.m_NumLoaded >= (size_t)g_Config.m_ClSkinsLoadedMax)
595 {
596 break;
597 }
598 if(pSkinContainer->m_State != CSkinContainer::EState::PENDING)
599 {
600 continue;
601 }
602 switch(pSkinContainer->Type())
603 {
604 case CSkinContainer::EType::LOCAL:
605 pSkinContainer->m_pLoadJob = std::make_shared<CSkinLoadJob>(args: this, args: pSkinContainer->Name(), args: pSkinContainer->StorageType());
606 break;
607 case CSkinContainer::EType::DOWNLOAD:
608 pSkinContainer->m_pLoadJob = std::make_shared<CSkinDownloadJob>(args: this, args: pSkinContainer->Name());
609 break;
610 default:
611 dbg_assert_failed("pSkinContainer->Type() invalid");
612 }
613 Engine()->AddJob(pJob: pSkinContainer->m_pLoadJob);
614 pSkinContainer->SetState(CSkinContainer::EState::LOADING);
615 Stats.m_NumPending--;
616 Stats.m_NumLoading++;
617 }
618}
619
620void CSkins::UpdateFinishLoading(CSkinLoadingStats &Stats, std::chrono::nanoseconds StartTime, std::chrono::nanoseconds MaxTime)
621{
622 for(auto &[_, pSkinContainer] : m_Skins)
623 {
624 if(Stats.m_NumLoading == 0)
625 {
626 break;
627 }
628 if(pSkinContainer->m_State != CSkinContainer::EState::LOADING)
629 {
630 continue;
631 }
632 dbg_assert(pSkinContainer->m_pLoadJob != nullptr, "Skin container in loading state must have a load job");
633 if(!pSkinContainer->m_pLoadJob->Done())
634 {
635 continue;
636 }
637 Stats.m_NumLoading--;
638 if(pSkinContainer->m_pLoadJob->State() == IJob::STATE_DONE && pSkinContainer->m_pLoadJob->m_Data.m_Info.m_pData)
639 {
640 LoadSkinFinish(pSkinContainer: pSkinContainer.get(), Data: pSkinContainer->m_pLoadJob->m_Data);
641 GameClient()->OnSkinUpdate(pSkinName: pSkinContainer->Name());
642 pSkinContainer->m_pLoadJob = nullptr;
643 Stats.m_NumLoaded++;
644 if(time_get_nanoseconds() - StartTime >= MaxTime)
645 {
646 // Avoid using too much frame time for loading skins
647 break;
648 }
649 }
650 else
651 {
652 if(pSkinContainer->m_pLoadJob->State() == IJob::STATE_DONE && pSkinContainer->m_pLoadJob->m_NotFound)
653 {
654 pSkinContainer->SetState(CSkinContainer::EState::NOT_FOUND);
655 Stats.m_NumNotFound++;
656 }
657 else
658 {
659 pSkinContainer->SetState(CSkinContainer::EState::ERROR);
660 Stats.m_NumError++;
661 }
662 pSkinContainer->m_pLoadJob = nullptr;
663 }
664 }
665}
666
667void CSkins::RefreshEventSkins()
668{
669 m_aEventSkinPrefix[0] = '\0';
670
671 if(g_Config.m_Events)
672 {
673 if(time_season() == SEASON_XMAS)
674 {
675 str_copy(dst&: m_aEventSkinPrefix, src: "santa");
676 }
677 }
678}
679
680void CSkins::Refresh(TSkinLoadedCallback &&SkinLoadedCallback)
681{
682 for(auto &[_, pSkinContainer] : m_Skins)
683 {
684 if(pSkinContainer->m_pLoadJob)
685 {
686 pSkinContainer->m_pLoadJob->Abort();
687 }
688 if(pSkinContainer->m_pSkin)
689 {
690 pSkinContainer->m_pSkin->m_OriginalSkin.Unload(pGraphics: Graphics());
691 pSkinContainer->m_pSkin->m_ColorableSkin.Unload(pGraphics: Graphics());
692 }
693 }
694 m_Skins.clear();
695 m_SkinsUsageList.clear();
696
697 LoadSkinDirect(pName: "default");
698 SkinLoadedCallback();
699
700 CSkinScanUser SkinScanUser;
701 SkinScanUser.m_pThis = this;
702 SkinScanUser.m_SkinLoadedCallback = SkinLoadedCallback;
703 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "skins", pfnCallback: SkinScan, pUser: &SkinScanUser);
704}
705
706CSkins::CSkinLoadingStats CSkins::LoadingStats() const
707{
708 CSkinLoadingStats Stats;
709 for(const auto &[_, pSkinContainer] : m_Skins)
710 {
711 switch(pSkinContainer->m_State)
712 {
713 case CSkinContainer::EState::UNLOADED:
714 Stats.m_NumUnloaded++;
715 break;
716 case CSkinContainer::EState::PENDING:
717 Stats.m_NumPending++;
718 break;
719 case CSkinContainer::EState::LOADING:
720 Stats.m_NumLoading++;
721 break;
722 case CSkinContainer::EState::LOADED:
723 Stats.m_NumLoaded++;
724 break;
725 case CSkinContainer::EState::ERROR:
726 Stats.m_NumError++;
727 break;
728 case CSkinContainer::EState::NOT_FOUND:
729 Stats.m_NumNotFound++;
730 break;
731 }
732 }
733 return Stats;
734}
735
736CSkins::CSkinList &CSkins::SkinList()
737{
738 if(!m_SkinList.m_NeedsUpdate)
739 {
740 return m_SkinList;
741 }
742
743 m_SkinList.m_vSkins.clear();
744 m_SkinList.m_UnfilteredCount = 0;
745
746 // Ensure all favorite skins are present as skin containers so they are included in the next loop.
747 for(const auto &FavoriteSkin : m_Favorites)
748 {
749 FindContainerOrNullptr(pName: FavoriteSkin.c_str());
750 }
751
752 m_SkinList.m_vSkins.reserve(n: m_Skins.size());
753 for(const auto &[_, pSkinContainer] : m_Skins)
754 {
755 if(pSkinContainer->IsSpecial())
756 {
757 continue;
758 }
759
760 const bool SelectedMain = str_comp(a: pSkinContainer->Name(), b: g_Config.m_ClPlayerSkin) == 0;
761 const bool SelectedDummy = str_comp(a: pSkinContainer->Name(), b: g_Config.m_ClDummySkin) == 0;
762 const bool Favorite = IsFavorite(pName: pSkinContainer->Name());
763
764 // Don't include skins in the list that couldn't be found in the database except the current player
765 // and dummy skins to avoid showing a lot of not-found entries while the user is typing a skin name.
766 if(pSkinContainer->m_State == CSkinContainer::EState::NOT_FOUND &&
767 !pSkinContainer->IsSpecial() &&
768 !SelectedMain &&
769 !SelectedDummy &&
770 !Favorite)
771 {
772 continue;
773 }
774 m_SkinList.m_UnfilteredCount++;
775
776 std::optional<std::pair<int, int>> NameMatch;
777 if(g_Config.m_ClSkinFilterString[0] != '\0')
778 {
779 const char *pNameMatchEnd;
780 const char *pNameMatchStart = str_utf8_find_nocase(haystack: pSkinContainer->Name(), needle: g_Config.m_ClSkinFilterString, end: &pNameMatchEnd);
781 if(pNameMatchStart == nullptr)
782 {
783 continue;
784 }
785 NameMatch = std::make_pair<int, int>(x: pNameMatchStart - pSkinContainer->Name(), y: pNameMatchEnd - pNameMatchStart);
786 }
787 m_SkinList.m_vSkins.emplace_back(args: pSkinContainer.get(), args: Favorite, args: SelectedMain, args: SelectedDummy, args&: NameMatch);
788 }
789
790 std::sort(first: m_SkinList.m_vSkins.begin(), last: m_SkinList.m_vSkins.end());
791 m_SkinList.m_NeedsUpdate = false;
792 return m_SkinList;
793}
794
795const CSkin *CSkins::Find(const char *pName)
796{
797 const auto *pSkin = FindOrNullptr(pName);
798 if(pSkin == nullptr)
799 {
800 pSkin = FindOrNullptr(pName: "default");
801 }
802 if(pSkin == nullptr)
803 {
804 pSkin = &m_PlaceholderSkin;
805 }
806 return pSkin;
807}
808
809const CSkins::CSkinContainer *CSkins::FindContainerOrNullptr(const char *pName)
810{
811 const char *pSkinPrefix = SkinPrefix();
812 if(pSkinPrefix[0] != '\0')
813 {
814 char aNameWithPrefix[2 * MAX_SKIN_LENGTH + 2]; // Larger than skin name length to allow IsValidName to check if it's too long
815 str_format(buffer: aNameWithPrefix, buffer_size: sizeof(aNameWithPrefix), format: "%s_%s", pSkinPrefix, pName);
816 // If we find something, use it, otherwise fall back to normal skins.
817 const CSkinContainer *pSkinContainer = FindContainerImpl(pName: aNameWithPrefix);
818 if(pSkinContainer != nullptr && pSkinContainer->State() == CSkinContainer::EState::LOADED)
819 {
820 return pSkinContainer;
821 }
822 }
823 return FindContainerImpl(pName);
824}
825
826const CSkins::CSkinContainer *CSkins::FindContainerImpl(const char *pName)
827{
828 if(!CSkin::IsValidName(pName))
829 {
830 return nullptr;
831 }
832
833 auto ExistingSkin = m_Skins.find(x: pName);
834 if(ExistingSkin == m_Skins.end())
835 {
836 CSkinContainer SkinContainer(this, pName, CSkinContainer::EType::DOWNLOAD, IStorage::TYPE_SAVE);
837 auto &&pSkinContainer = std::make_unique<CSkinContainer>(args: std::move(SkinContainer));
838 pSkinContainer->SetState(pSkinContainer->DetermineInitialState());
839 ExistingSkin = m_Skins.insert(x: {pSkinContainer->Name(), std::move(pSkinContainer)}).first;
840 }
841 ExistingSkin->second->RequestLoad();
842 return ExistingSkin->second.get();
843}
844
845const CSkin *CSkins::FindOrNullptr(const char *pName)
846{
847 const CSkinContainer *pSkinContainer = FindContainerOrNullptr(pName);
848 if(pSkinContainer == nullptr || pSkinContainer->m_State != CSkinContainer::EState::LOADED)
849 {
850 return nullptr;
851 }
852 return pSkinContainer->m_pSkin.get();
853}
854
855void CSkins::AddFavorite(const char *pName)
856{
857 if(!CSkin::IsValidName(pName))
858 {
859 log_error("skins", "Favorite skin name '%s' is not valid", pName);
860 log_error("skins", "%s", CSkin::m_aSkinNameRestrictions);
861 return;
862 }
863
864 const auto &[_, Inserted] = m_Favorites.emplace(args&: pName);
865 if(Inserted)
866 {
867 m_SkinList.ForceRefresh();
868 }
869}
870
871void CSkins::RemoveFavorite(const char *pName)
872{
873 const auto FavoriteIt = m_Favorites.find(x: pName);
874 if(FavoriteIt != m_Favorites.end())
875 {
876 m_Favorites.erase(position: FavoriteIt);
877 m_SkinList.ForceRefresh();
878 }
879}
880
881bool CSkins::IsFavorite(const char *pName) const
882{
883 return m_Favorites.contains(x: pName);
884}
885
886void CSkins::RandomizeSkin(int Dummy)
887{
888 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
889 const bool UseCustomColor = Dummy ? g_Config.m_ClDummyUseCustomColor : g_Config.m_ClPlayerUseCustomColor;
890 if(UseCustomColor)
891 {
892 float GoalSat = random_float(min: 0.3f, max: 1.0f);
893 float MaxBodyLht = 1.0f - GoalSat * GoalSat; // max allowed lightness before we start losing saturation
894
895 ColorHSLA Body;
896 Body.h = random_float();
897 Body.l = random_float(min: 0.0f, max: MaxBodyLht);
898 Body.s = std::clamp(val: GoalSat * GoalSat / (1.0f - Body.l), lo: 0.0f, hi: 1.0f);
899
900 ColorHSLA Feet;
901 Feet.h = std::fmod(x: Body.h + s_aSchemes[rand() % std::size(s_aSchemes)], y: 1.0f);
902 Feet.l = random_float();
903 Feet.s = std::clamp(val: GoalSat * GoalSat / (1.0f - Feet.l), lo: 0.0f, hi: 1.0f);
904
905 unsigned *pColorBody = Dummy ? &g_Config.m_ClDummyColorBody : &g_Config.m_ClPlayerColorBody;
906 unsigned *pColorFeet = Dummy ? &g_Config.m_ClDummyColorFeet : &g_Config.m_ClPlayerColorFeet;
907
908 *pColorBody = Body.Pack(Alpha: false);
909 *pColorFeet = Feet.Pack(Alpha: false);
910 }
911
912 std::vector<const CSkinContainer *> vpConsideredSkins;
913 for(const auto &[_, pSkinContainer] : m_Skins)
914 {
915 if(pSkinContainer->m_State == CSkinContainer::EState::ERROR ||
916 pSkinContainer->m_State == CSkinContainer::EState::NOT_FOUND ||
917 pSkinContainer->IsSpecial())
918 {
919 continue;
920 }
921 vpConsideredSkins.push_back(x: pSkinContainer.get());
922 }
923 const char *pRandomSkin;
924 if(vpConsideredSkins.empty())
925 {
926 pRandomSkin = "default";
927 }
928 else
929 {
930 pRandomSkin = vpConsideredSkins[rand() % vpConsideredSkins.size()]->Name();
931 }
932
933 char *pSkinName = Dummy ? g_Config.m_ClDummySkin : g_Config.m_ClPlayerSkin;
934 const size_t SkinNameSize = Dummy ? sizeof(g_Config.m_ClDummySkin) : sizeof(g_Config.m_ClPlayerSkin);
935 str_copy(dst: pSkinName, src: pRandomSkin, dst_size: SkinNameSize);
936 m_SkinList.ForceRefresh();
937}
938
939const char *CSkins::SkinPrefix() const
940{
941 if(g_Config.m_ClVanillaSkinsOnly)
942 {
943 return "";
944 }
945 if(m_aEventSkinPrefix[0] != '\0')
946 {
947 return m_aEventSkinPrefix;
948 }
949 return g_Config.m_ClSkinPrefix;
950}
951
952void CSkins::CSkinLoadJob::Run()
953{
954 char aPath[IO_MAX_PATH_LENGTH];
955 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "skins/%s.png", m_aName);
956 if(m_pSkins->Graphics()->LoadPng(Image&: m_Data.m_Info, pFilename: aPath, StorageType: m_StorageType))
957 {
958 if(State() == IJob::STATE_ABORTED)
959 {
960 return;
961 }
962 m_pSkins->LoadSkinData(pName: m_aName, Data&: m_Data);
963 }
964 else
965 {
966 log_error("skins", "Failed to load PNG of skin '%s' from '%s'", m_aName, aPath);
967 }
968}
969
970CSkins::CSkinDownloadJob::CSkinDownloadJob(CSkins *pSkins, const char *pName) :
971 CAbstractSkinLoadJob(pSkins, pName)
972{
973}
974
975bool CSkins::CSkinDownloadJob::Abort()
976{
977 if(!CAbstractSkinLoadJob::Abort())
978 {
979 return false;
980 }
981
982 const CLockScope LockScope(m_Lock);
983 if(m_pGetRequest)
984 {
985 m_pGetRequest->Abort();
986 m_pGetRequest = nullptr;
987 }
988 return true;
989}
990
991void CSkins::CSkinDownloadJob::Run()
992{
993 const char *pBaseUrl = g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl;
994
995 char aEscapedName[256];
996 EscapeUrl(aBuf&: aEscapedName, pStr: m_aName);
997
998 char aUrl[IO_MAX_PATH_LENGTH];
999 str_format(buffer: aUrl, buffer_size: sizeof(aUrl), format: "%s%s.png", pBaseUrl, aEscapedName);
1000
1001 char aPathReal[IO_MAX_PATH_LENGTH];
1002 str_format(buffer: aPathReal, buffer_size: sizeof(aPathReal), format: "downloadedskins/%s.png", m_aName);
1003
1004 const CTimeout Timeout{.m_ConnectTimeoutMs: 10000, .m_TimeoutMs: 0, .m_LowSpeedLimit: 8192, .m_LowSpeedTime: 10};
1005 const size_t MaxResponseSize = 10 * 1024 * 1024; // 10 MiB
1006
1007 std::shared_ptr<CHttpRequest> pGet = HttpGetBoth(pUrl: aUrl, pStorage: m_pSkins->Storage(), pOutputFile: aPathReal, StorageType: IStorage::TYPE_SAVE);
1008 pGet->Timeout(Timeout);
1009 pGet->MaxResponseSize(MaxResponseSize);
1010 pGet->ValidateBeforeOverwrite(ValidateBeforeOverwrite: true);
1011 pGet->LogProgress(LogProgress: HTTPLOG::NONE);
1012 pGet->FailOnErrorStatus(FailOnErrorStatus: false);
1013 {
1014 const CLockScope LockScope(m_Lock);
1015 m_pGetRequest = pGet;
1016 }
1017 m_pSkins->Http()->Run(pRequest: pGet);
1018
1019 // Load existing file while waiting for the HTTP request
1020 {
1021 void *pPngData;
1022 unsigned PngSize;
1023 if(m_pSkins->Storage()->ReadFile(pFilename: aPathReal, Type: IStorage::TYPE_SAVE, ppResult: &pPngData, pResultLen: &PngSize))
1024 {
1025 if(m_pSkins->Graphics()->LoadPng(Image&: m_Data.m_Info, pData: static_cast<uint8_t *>(pPngData), DataSize: PngSize, pContextName: aPathReal))
1026 {
1027 if(State() == IJob::STATE_ABORTED)
1028 {
1029 return;
1030 }
1031 m_pSkins->LoadSkinData(pName: m_aName, Data&: m_Data);
1032 }
1033 free(ptr: pPngData);
1034 }
1035 }
1036
1037 pGet->Wait();
1038 {
1039 const CLockScope LockScope(m_Lock);
1040 m_pGetRequest = nullptr;
1041 }
1042 if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED || pGet->StatusCode() >= 400)
1043 {
1044 m_NotFound = pGet->State() == EHttpState::DONE && pGet->StatusCode() == 404; // 404 Not Found
1045 return;
1046 }
1047 if(pGet->StatusCode() == 304) // 304 Not Modified
1048 {
1049 bool Success = m_Data.m_Info.m_pData != nullptr;
1050 pGet->OnValidation(Success);
1051 if(Success)
1052 {
1053 return; // Local skin is up-to-date and was loaded successfully
1054 }
1055
1056 log_error("skins", "Failed to load PNG of existing downloaded skin '%s' from '%s', downloading it again", m_aName, aPathReal);
1057 pGet = HttpGetBoth(pUrl: aUrl, pStorage: m_pSkins->Storage(), pOutputFile: aPathReal, StorageType: IStorage::TYPE_SAVE);
1058 pGet->Timeout(Timeout);
1059 pGet->MaxResponseSize(MaxResponseSize);
1060 pGet->ValidateBeforeOverwrite(ValidateBeforeOverwrite: true);
1061 pGet->SkipByFileTime(SkipByFileTime: false);
1062 pGet->LogProgress(LogProgress: HTTPLOG::NONE);
1063 pGet->FailOnErrorStatus(FailOnErrorStatus: false);
1064 {
1065 const CLockScope LockScope(m_Lock);
1066 m_pGetRequest = pGet;
1067 }
1068 m_pSkins->Http()->Run(pRequest: pGet);
1069 pGet->Wait();
1070 {
1071 const CLockScope LockScope(m_Lock);
1072 m_pGetRequest = nullptr;
1073 }
1074 if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED || pGet->StatusCode() >= 400)
1075 {
1076 m_NotFound = pGet->State() == EHttpState::DONE && pGet->StatusCode() == 404; // 404 Not Found
1077 return;
1078 }
1079 }
1080
1081 unsigned char *pResult;
1082 size_t ResultSize;
1083 pGet->Result(ppResult: &pResult, pResultLength: &ResultSize);
1084
1085 m_Data.m_Info.Free();
1086 m_Data.m_InfoGrayscale.Free();
1087 const bool Success = m_pSkins->Graphics()->LoadPng(Image&: m_Data.m_Info, pData: pResult, DataSize: ResultSize, pContextName: aUrl);
1088 if(Success)
1089 {
1090 if(State() == IJob::STATE_ABORTED)
1091 {
1092 return;
1093 }
1094 m_pSkins->LoadSkinData(pName: m_aName, Data&: m_Data);
1095 }
1096 else
1097 {
1098 log_error("skins", "Failed to load PNG of skin '%s' downloaded from '%s' (size %" PRIzu ")", m_aName, aUrl, ResultSize);
1099 }
1100 pGet->OnValidation(Success);
1101}
1102
1103void CSkins::ConAddFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
1104{
1105 auto *pSelf = static_cast<CSkins *>(pUserData);
1106 pSelf->AddFavorite(pName: pResult->GetString(Index: 0));
1107}
1108
1109void CSkins::ConRemFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
1110{
1111 auto *pSelf = static_cast<CSkins *>(pUserData);
1112 pSelf->RemoveFavorite(pName: pResult->GetString(Index: 0));
1113}
1114
1115void CSkins::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData)
1116{
1117 auto *pSelf = static_cast<CSkins *>(pUserData);
1118 pSelf->OnConfigSave(pConfigManager);
1119}
1120
1121void CSkins::OnConfigSave(IConfigManager *pConfigManager)
1122{
1123 for(const auto &Favorite : m_Favorites)
1124 {
1125 char aBuffer[32 + MAX_SKIN_LENGTH];
1126 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "add_favorite_skin \"%s\"", Favorite.c_str());
1127 pConfigManager->WriteLine(pLine: aBuffer);
1128 }
1129}
1130
1131void CSkins::ConchainRefreshSkinList(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1132{
1133 CSkins *pThis = static_cast<CSkins *>(pUserData);
1134 pfnCallback(pResult, pCallbackUserData);
1135 if(pResult->NumArguments())
1136 {
1137 pThis->m_SkinList.ForceRefresh();
1138 }
1139}
1140