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#include "skins7.h"
4
5#include "menus.h"
6
7#include <base/color.h>
8#include <base/log.h>
9#include <base/math.h>
10#include <base/system.h>
11
12#include <engine/external/json-parser/json.h>
13#include <engine/gfx/image_manipulation.h>
14#include <engine/graphics.h>
15#include <engine/shared/config.h>
16#include <engine/shared/jsonwriter.h>
17#include <engine/shared/localization.h>
18#include <engine/shared/protocol7.h>
19#include <engine/storage.h>
20
21#include <game/client/gameclient.h>
22#include <game/localization.h>
23
24const char *const CSkins7::ms_apSkinPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"};
25const char *const CSkins7::ms_apSkinPartNamesLocalized[protocol7::NUM_SKINPARTS] = {Localizable(pStr: "Body", pContext: "skins"), Localizable(pStr: "Marking", pContext: "skins"), Localizable(pStr: "Decoration", pContext: "skins"), Localizable(pStr: "Hands", pContext: "skins"), Localizable(pStr: "Feet", pContext: "skins"), Localizable(pStr: "Eyes", pContext: "skins")};
26const char *const CSkins7::ms_apColorComponents[NUM_COLOR_COMPONENTS] = {"hue", "sat", "lgt", "alp"};
27
28char *CSkins7::ms_apSkinNameVariables[NUM_DUMMIES] = {nullptr};
29char *CSkins7::ms_apSkinVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{nullptr}};
30int *CSkins7::ms_apUCCVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{nullptr}};
31int unsigned *CSkins7::ms_apColorVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{nullptr}};
32
33#define SKINS_DIR "skins7"
34
35// TODO: uncomment
36// const float MIN_EYE_BODY_COLOR_DIST = 80.f; // between body and eyes (LAB color space)
37
38void CSkins7::CSkinPart::ApplyTo(CTeeRenderInfo::CSixup &SixupRenderInfo) const
39{
40 SixupRenderInfo.m_aOriginalTextures[m_Type] = m_OriginalTexture;
41 SixupRenderInfo.m_aColorableTextures[m_Type] = m_ColorableTexture;
42 if(m_Type == protocol7::SKINPART_BODY)
43 {
44 SixupRenderInfo.m_BloodColor = m_BloodColor;
45 }
46}
47
48bool CSkins7::CSkinPart::operator<(const CSkinPart &Other) const
49{
50 return str_comp_nocase(a: m_aName, b: Other.m_aName) < 0;
51}
52
53bool CSkins7::CSkin::operator<(const CSkin &Other) const
54{
55 return str_comp_nocase(a: m_aName, b: Other.m_aName) < 0;
56}
57
58bool CSkins7::CSkin::operator==(const CSkin &Other) const
59{
60 return str_comp(a: m_aName, b: Other.m_aName) == 0;
61}
62
63bool CSkins7::IsSpecialSkin(const char *pName)
64{
65 return str_startswith(str: pName, prefix: "x_") != nullptr;
66}
67
68class CSkinPartScanData
69{
70public:
71 CSkins7 *m_pThis;
72 CSkins7::TSkinLoadedCallback m_SkinLoadedCallback;
73 int m_Part;
74};
75
76int CSkins7::SkinPartScan(const char *pName, int IsDir, int DirType, void *pUser)
77{
78 if(IsDir || !str_endswith(str: pName, suffix: ".png"))
79 return 0;
80
81 CSkinPartScanData *pScanData = static_cast<CSkinPartScanData *>(pUser);
82 pScanData->m_pThis->LoadSkinPart(PartType: pScanData->m_Part, pName, DirType);
83 pScanData->m_SkinLoadedCallback();
84 return 0;
85}
86
87static ColorRGBA DetermineBloodColor(int PartType, const CImageInfo &Info)
88{
89 if(PartType != protocol7::SKINPART_BODY)
90 {
91 return ColorRGBA(1.0f, 1.0f, 1.0f);
92 }
93
94 const size_t Step = Info.PixelSize();
95 const size_t Pitch = Info.m_Width * Step;
96 const size_t PartX = Info.m_Width / 2;
97 const size_t PartY = 0;
98 const size_t PartWidth = Info.m_Width / 2;
99 const size_t PartHeight = Info.m_Height / 2;
100
101 int64_t aColors[3] = {0};
102 for(size_t y = PartY; y < PartY + PartHeight; y++)
103 {
104 for(size_t x = PartX; x < PartX + PartWidth; x++)
105 {
106 const size_t Offset = y * Pitch + x * Step;
107 if(Info.m_pData[Offset + 3] > 128)
108 {
109 for(size_t c = 0; c < 3; c++)
110 {
111 aColors[c] += Info.m_pData[Offset + c];
112 }
113 }
114 }
115 }
116
117 const vec3 NormalizedColor = normalize(v: vec3(aColors[0], aColors[1], aColors[2]));
118 return ColorRGBA(NormalizedColor.x, NormalizedColor.y, NormalizedColor.z);
119}
120
121bool CSkins7::LoadSkinPart(int PartType, const char *pName, int DirType)
122{
123 size_t PartNameSize, PartNameCount;
124 str_utf8_stats(str: pName, max_size: str_length(str: pName) - str_length(str: ".png") + 1, max_count: IO_MAX_PATH_LENGTH, size: &PartNameSize, count: &PartNameCount);
125 if(PartNameSize >= protocol7::MAX_SKIN_ARRAY_SIZE || PartNameCount > protocol7::MAX_SKIN_LENGTH)
126 {
127 log_error("skins7", "Failed to load skin part '%s/%s': name too long", CSkins7::ms_apSkinPartNames[PartType], pName);
128 return false;
129 }
130
131 char aFilename[IO_MAX_PATH_LENGTH];
132 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), SKINS_DIR "/%s/%s", CSkins7::ms_apSkinPartNames[PartType], pName);
133 CImageInfo Info;
134 if(!Graphics()->LoadPng(Image&: Info, pFilename: aFilename, StorageType: DirType))
135 {
136 log_error("skins7", "Failed to load skin part '%s/%s': failed to load PNG file", CSkins7::ms_apSkinPartNames[PartType], pName);
137 return false;
138 }
139 if(!Graphics()->IsImageFormatRgba(pContextName: aFilename, Image: Info))
140 {
141 log_error("skins7", "Failed to load skin part '%s/%s': must be RGBA format", CSkins7::ms_apSkinPartNames[PartType], pName);
142 Info.Free();
143 return false;
144 }
145
146 CSkinPart Part;
147 Part.m_Type = PartType;
148 Part.m_Flags = 0;
149 if(IsSpecialSkin(pName))
150 {
151 Part.m_Flags |= SKINFLAG_SPECIAL;
152 }
153 if(DirType != IStorage::TYPE_SAVE)
154 {
155 Part.m_Flags |= SKINFLAG_STANDARD;
156 }
157 str_copy(dst: Part.m_aName, src: pName, dst_size: minimum<int>(a: PartNameSize + 1, b: sizeof(Part.m_aName)));
158 Part.m_OriginalTexture = Graphics()->LoadTextureRaw(Image: Info, Flags: 0, pTexName: aFilename);
159 Part.m_BloodColor = DetermineBloodColor(PartType: Part.m_Type, Info);
160 ConvertToGrayscale(Image: Info);
161 Part.m_ColorableTexture = Graphics()->LoadTextureRawMove(Image&: Info, Flags: 0, pTexName: aFilename);
162
163 if(Config()->m_Debug)
164 {
165 log_trace("skins7", "Loaded skin part '%s/%s'", CSkins7::ms_apSkinPartNames[PartType], Part.m_aName);
166 }
167 m_avSkinParts[PartType].emplace_back(args&: Part);
168 return true;
169}
170
171class CSkinScanData
172{
173public:
174 CSkins7 *m_pThis;
175 CSkins7::TSkinLoadedCallback m_SkinLoadedCallback;
176};
177
178int CSkins7::SkinScan(const char *pName, int IsDir, int DirType, void *pUser)
179{
180 if(IsDir || !str_endswith(str: pName, suffix: ".json"))
181 return 0;
182
183 CSkinScanData *pScanData = static_cast<CSkinScanData *>(pUser);
184 pScanData->m_pThis->LoadSkin(pName, DirType);
185 pScanData->m_SkinLoadedCallback();
186 return 0;
187}
188
189bool CSkins7::LoadSkin(const char *pName, int DirType)
190{
191 char aFilename[IO_MAX_PATH_LENGTH];
192 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), SKINS_DIR "/%s", pName);
193 void *pFileData;
194 unsigned JsonFileSize;
195 if(!Storage()->ReadFile(pFilename: aFilename, Type: DirType, ppResult: &pFileData, pResultLen: &JsonFileSize))
196 {
197 log_error("skins7", "Failed to read skin json file '%s'", aFilename);
198 return false;
199 }
200
201 CSkin Skin;
202 str_copy(dst: Skin.m_aName, src: pName, dst_size: 1 + str_length(str: pName) - str_length(str: ".json"));
203 const bool SpecialSkin = IsSpecialSkin(pName: Skin.m_aName);
204 Skin.m_Flags = 0;
205 if(SpecialSkin)
206 {
207 Skin.m_Flags |= SKINFLAG_SPECIAL;
208 }
209 if(DirType != IStorage::TYPE_SAVE)
210 {
211 Skin.m_Flags |= SKINFLAG_STANDARD;
212 }
213
214 json_settings JsonSettings{};
215 char aError[256];
216 json_value *pJsonData = json_parse_ex(settings: &JsonSettings, json: static_cast<const json_char *>(pFileData), length: JsonFileSize, error: aError);
217 free(ptr: pFileData);
218 if(pJsonData == nullptr)
219 {
220 log_error("skins7", "Failed to parse skin json file '%s': %s", aFilename, aError);
221 return false;
222 }
223
224 const json_value &Start = (*pJsonData)["skin"];
225 if(Start.type != json_object)
226 {
227 log_error("skins7", "Failed to parse skin json file '%s': root must be an object", aFilename);
228 json_value_free(pJsonData);
229 return false;
230 }
231
232 for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; ++PartIndex)
233 {
234 Skin.m_aUseCustomColors[PartIndex] = 0;
235 Skin.m_aPartColors[PartIndex] = (PartIndex == protocol7::SKINPART_MARKING ? 0xFF000000u : 0u) + 0x00FF80u;
236
237 const json_value &Part = Start[(const char *)ms_apSkinPartNames[PartIndex]];
238 if(Part.type == json_none)
239 {
240 Skin.m_apParts[PartIndex] = FindDefaultSkinPart(Part: PartIndex);
241 continue;
242 }
243 if(Part.type != json_object)
244 {
245 log_error("skins7", "Failed to parse skin json file '%s': attribute '%s' must specify an object", aFilename, ms_apSkinPartNames[PartIndex]);
246 json_value_free(pJsonData);
247 return false;
248 }
249
250 const json_value &Filename = Part["filename"];
251 if(Filename.type == json_string)
252 {
253 Skin.m_apParts[PartIndex] = FindSkinPart(Part: PartIndex, pName: (const char *)Filename, AllowSpecialPart: SpecialSkin);
254 }
255 else
256 {
257 log_error("skins7", "Failed to parse skin json file '%s': part '%s' attribute 'filename' must specify a string", aFilename, ms_apSkinPartNames[PartIndex]);
258 json_value_free(pJsonData);
259 return false;
260 }
261
262 bool UseCustomColors = false;
263 const json_value &Color = Part["custom_colors"];
264 if(Color.type == json_string)
265 UseCustomColors = str_comp(a: (const char *)Color, b: "true") == 0;
266 else if(Color.type == json_boolean)
267 UseCustomColors = Color.u.boolean;
268 Skin.m_aUseCustomColors[PartIndex] = UseCustomColors;
269
270 if(!UseCustomColors)
271 continue;
272
273 for(int i = 0; i < NUM_COLOR_COMPONENTS; i++)
274 {
275 if(PartIndex != protocol7::SKINPART_MARKING && i == 3)
276 continue;
277
278 const json_value &Component = Part[(const char *)ms_apColorComponents[i]];
279 if(Component.type == json_integer)
280 {
281 switch(i)
282 {
283 case 0: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFF00FFFFu) | (Component.u.integer << 16); break;
284 case 1: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFFFF00FFu) | (Component.u.integer << 8); break;
285 case 2: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFFFFFF00u) | Component.u.integer; break;
286 case 3: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0x00FFFFFFu) | (Component.u.integer << 24); break;
287 }
288 }
289 }
290 }
291
292 json_value_free(pJsonData);
293
294 if(Config()->m_Debug)
295 {
296 log_trace("skins7", "Loaded skin '%s'", Skin.m_aName);
297 }
298 m_vSkins.insert(position: std::lower_bound(first: m_vSkins.begin(), last: m_vSkins.end(), val: Skin), x: Skin);
299 return true;
300}
301
302void CSkins7::OnInit()
303{
304 int Dummy = 0;
305 ms_apSkinNameVariables[Dummy] = Config()->m_ClPlayer7Skin;
306 ms_apSkinVariables[Dummy][protocol7::SKINPART_BODY] = Config()->m_ClPlayer7SkinBody;
307 ms_apSkinVariables[Dummy][protocol7::SKINPART_MARKING] = Config()->m_ClPlayer7SkinMarking;
308 ms_apSkinVariables[Dummy][protocol7::SKINPART_DECORATION] = Config()->m_ClPlayer7SkinDecoration;
309 ms_apSkinVariables[Dummy][protocol7::SKINPART_HANDS] = Config()->m_ClPlayer7SkinHands;
310 ms_apSkinVariables[Dummy][protocol7::SKINPART_FEET] = Config()->m_ClPlayer7SkinFeet;
311 ms_apSkinVariables[Dummy][protocol7::SKINPART_EYES] = Config()->m_ClPlayer7SkinEyes;
312 ms_apUCCVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClPlayer7UseCustomColorBody;
313 ms_apUCCVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClPlayer7UseCustomColorMarking;
314 ms_apUCCVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClPlayer7UseCustomColorDecoration;
315 ms_apUCCVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClPlayer7UseCustomColorHands;
316 ms_apUCCVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClPlayer7UseCustomColorFeet;
317 ms_apUCCVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClPlayer7UseCustomColorEyes;
318 ms_apColorVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClPlayer7ColorBody;
319 ms_apColorVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClPlayer7ColorMarking;
320 ms_apColorVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClPlayer7ColorDecoration;
321 ms_apColorVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClPlayer7ColorHands;
322 ms_apColorVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClPlayer7ColorFeet;
323 ms_apColorVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClPlayer7ColorEyes;
324
325 Dummy = 1;
326 ms_apSkinNameVariables[Dummy] = Config()->m_ClDummy7Skin;
327 ms_apSkinVariables[Dummy][protocol7::SKINPART_BODY] = Config()->m_ClDummy7SkinBody;
328 ms_apSkinVariables[Dummy][protocol7::SKINPART_MARKING] = Config()->m_ClDummy7SkinMarking;
329 ms_apSkinVariables[Dummy][protocol7::SKINPART_DECORATION] = Config()->m_ClDummy7SkinDecoration;
330 ms_apSkinVariables[Dummy][protocol7::SKINPART_HANDS] = Config()->m_ClDummy7SkinHands;
331 ms_apSkinVariables[Dummy][protocol7::SKINPART_FEET] = Config()->m_ClDummy7SkinFeet;
332 ms_apSkinVariables[Dummy][protocol7::SKINPART_EYES] = Config()->m_ClDummy7SkinEyes;
333 ms_apUCCVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClDummy7UseCustomColorBody;
334 ms_apUCCVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClDummy7UseCustomColorMarking;
335 ms_apUCCVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClDummy7UseCustomColorDecoration;
336 ms_apUCCVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClDummy7UseCustomColorHands;
337 ms_apUCCVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClDummy7UseCustomColorFeet;
338 ms_apUCCVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClDummy7UseCustomColorEyes;
339 ms_apColorVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClDummy7ColorBody;
340 ms_apColorVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClDummy7ColorMarking;
341 ms_apColorVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClDummy7ColorDecoration;
342 ms_apColorVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClDummy7ColorHands;
343 ms_apColorVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClDummy7ColorFeet;
344 ms_apColorVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClDummy7ColorEyes;
345
346 InitPlaceholderSkinParts();
347
348 Refresh(SkinLoadedCallback: [this]() {
349 GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading DDNet Client"), pContent: Localize(pStr: "Loading skin files"), IncreaseCounter: 0);
350 });
351}
352
353void CSkins7::InitPlaceholderSkinParts()
354{
355 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
356 {
357 CSkinPart &SkinPart = m_aPlaceholderSkinParts[Part];
358 SkinPart.m_Type = Part;
359 SkinPart.m_Flags = SKINFLAG_STANDARD;
360 str_copy(dst&: SkinPart.m_aName, src: "dummy");
361 SkinPart.m_OriginalTexture.Invalidate();
362 SkinPart.m_ColorableTexture.Invalidate();
363 SkinPart.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
364 }
365}
366
367void CSkins7::Refresh(TSkinLoadedCallback &&SkinLoadedCallback)
368{
369 m_vSkins.clear();
370
371 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
372 {
373 for(CSkinPart &SkinPart : m_avSkinParts[Part])
374 {
375 Graphics()->UnloadTexture(pIndex: &SkinPart.m_OriginalTexture);
376 Graphics()->UnloadTexture(pIndex: &SkinPart.m_ColorableTexture);
377 }
378 m_avSkinParts[Part].clear();
379
380 if(Part == protocol7::SKINPART_MARKING || Part == protocol7::SKINPART_DECORATION)
381 {
382 CSkinPart NoneSkinPart;
383 NoneSkinPart.m_Type = Part;
384 NoneSkinPart.m_Flags = SKINFLAG_STANDARD;
385 NoneSkinPart.m_aName[0] = '\0';
386 NoneSkinPart.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
387 m_avSkinParts[Part].emplace_back(args&: NoneSkinPart);
388 }
389
390 CSkinPartScanData SkinPartScanData;
391 SkinPartScanData.m_pThis = this;
392 SkinPartScanData.m_SkinLoadedCallback = SkinLoadedCallback;
393 SkinPartScanData.m_Part = Part;
394 char aPartsDirectory[IO_MAX_PATH_LENGTH];
395 str_format(buffer: aPartsDirectory, buffer_size: sizeof(aPartsDirectory), SKINS_DIR "/%s", ms_apSkinPartNames[Part]);
396 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: aPartsDirectory, pfnCallback: SkinPartScan, pUser: &SkinPartScanData);
397 }
398
399 CSkinScanData SkinScanData;
400 SkinScanData.m_pThis = this;
401 SkinScanData.m_SkinLoadedCallback = SkinLoadedCallback;
402 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, SKINS_DIR, pfnCallback: SkinScan, pUser: &SkinScanData);
403
404 LoadXmasHat();
405 LoadBotDecoration();
406 SkinLoadedCallback();
407
408 m_LastRefreshTime = time_get_nanoseconds();
409}
410
411void CSkins7::LoadXmasHat()
412{
413 Graphics()->UnloadTexture(pIndex: &m_XmasHatTexture);
414
415 const char *pFilename = SKINS_DIR "/xmas_hat.png";
416 CImageInfo Info;
417 if(!Graphics()->LoadPng(Image&: Info, pFilename, StorageType: IStorage::TYPE_ALL) ||
418 !Graphics()->IsImageFormatRgba(pContextName: pFilename, Image: Info) ||
419 !Graphics()->CheckImageDivisibility(pContextName: pFilename, Image&: Info, DivX: 1, DivY: 4, AllowResize: false))
420 {
421 log_error("skins7", "Failed to load xmas hat '%s'", pFilename);
422 Info.Free();
423 }
424 else
425 {
426 if(Config()->m_Debug)
427 {
428 log_trace("skins7", "Loaded xmas hat '%s'", pFilename);
429 }
430 m_XmasHatTexture = Graphics()->LoadTextureRawMove(Image&: Info, Flags: 0, pTexName: pFilename);
431 }
432}
433
434void CSkins7::LoadBotDecoration()
435{
436 Graphics()->UnloadTexture(pIndex: &m_BotTexture);
437
438 const char *pFilename = SKINS_DIR "/bot.png";
439 CImageInfo Info;
440 if(!Graphics()->LoadPng(Image&: Info, pFilename, StorageType: IStorage::TYPE_ALL) ||
441 !Graphics()->IsImageFormatRgba(pContextName: pFilename, Image: Info) ||
442 !Graphics()->CheckImageDivisibility(pContextName: pFilename, Image&: Info, DivX: 12, DivY: 5, AllowResize: false))
443 {
444 log_error("skins7", "Failed to load bot decoration '%s'", pFilename);
445 Info.Free();
446 }
447 else
448 {
449 if(Config()->m_Debug)
450 {
451 log_trace("skins7", "Loaded bot decoration '%s'", pFilename);
452 }
453 m_BotTexture = Graphics()->LoadTextureRawMove(Image&: Info, Flags: 0, pTexName: pFilename);
454 }
455}
456
457void CSkins7::AddSkinFromConfigVariables(const char *pName, int Dummy)
458{
459 auto OldSkin = std::find_if(first: m_vSkins.begin(), last: m_vSkins.end(), pred: [pName](const CSkin &Skin) {
460 return str_comp(a: Skin.m_aName, b: pName) == 0;
461 });
462 if(OldSkin != m_vSkins.end())
463 {
464 m_vSkins.erase(position: OldSkin);
465 }
466
467 CSkin NewSkin;
468 NewSkin.m_Flags = 0;
469 str_copy(dst&: NewSkin.m_aName, src: pName);
470 for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; ++PartIndex)
471 {
472 NewSkin.m_apParts[PartIndex] = FindSkinPart(Part: PartIndex, pName: ms_apSkinVariables[Dummy][PartIndex], AllowSpecialPart: false);
473 NewSkin.m_aUseCustomColors[PartIndex] = *ms_apUCCVariables[Dummy][PartIndex];
474 NewSkin.m_aPartColors[PartIndex] = *ms_apColorVariables[Dummy][PartIndex];
475 }
476 m_vSkins.insert(position: std::lower_bound(first: m_vSkins.begin(), last: m_vSkins.end(), val: NewSkin), x: NewSkin);
477 m_LastRefreshTime = time_get_nanoseconds();
478}
479
480bool CSkins7::RemoveSkin(const CSkin *pSkin)
481{
482 char aBuf[IO_MAX_PATH_LENGTH];
483 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), SKINS_DIR "/%s.json", pSkin->m_aName);
484 if(!Storage()->RemoveFile(pFilename: aBuf, Type: IStorage::TYPE_SAVE))
485 {
486 return false;
487 }
488
489 auto FoundSkin = std::find(first: m_vSkins.begin(), last: m_vSkins.end(), val: *pSkin);
490 dbg_assert(FoundSkin != m_vSkins.end(), "Skin not found");
491 m_vSkins.erase(position: FoundSkin);
492 m_LastRefreshTime = time_get_nanoseconds();
493 return true;
494}
495
496const std::vector<CSkins7::CSkin> &CSkins7::GetSkins() const
497{
498 return m_vSkins;
499}
500
501const std::vector<CSkins7::CSkinPart> &CSkins7::GetSkinParts(int Part) const
502{
503 return m_avSkinParts[Part];
504}
505
506const CSkins7::CSkinPart *CSkins7::FindSkinPartOrNullptr(int Part, const char *pName, bool AllowSpecialPart) const
507{
508 auto FoundPart = std::find_if(first: m_avSkinParts[Part].begin(), last: m_avSkinParts[Part].end(), pred: [pName](const CSkinPart &SkinPart) {
509 return str_comp(a: SkinPart.m_aName, b: pName) == 0;
510 });
511 if(FoundPart == m_avSkinParts[Part].end())
512 {
513 return nullptr;
514 }
515 if((FoundPart->m_Flags & SKINFLAG_SPECIAL) != 0 && !AllowSpecialPart)
516 {
517 return nullptr;
518 }
519 return &*FoundPart;
520}
521
522const CSkins7::CSkinPart *CSkins7::FindDefaultSkinPart(int Part) const
523{
524 const char *pDefaultPartName = Part == protocol7::SKINPART_MARKING || Part == protocol7::SKINPART_DECORATION ? "" : "standard";
525 const CSkinPart *pDefault = FindSkinPartOrNullptr(Part, pName: pDefaultPartName, AllowSpecialPart: false);
526 if(pDefault != nullptr)
527 {
528 return pDefault;
529 }
530 return &m_aPlaceholderSkinParts[Part];
531}
532
533const CSkins7::CSkinPart *CSkins7::FindSkinPart(int Part, const char *pName, bool AllowSpecialPart) const
534{
535 const CSkinPart *pSkinPart = FindSkinPartOrNullptr(Part, pName, AllowSpecialPart);
536 if(pSkinPart != nullptr)
537 {
538 return pSkinPart;
539 }
540 return FindDefaultSkinPart(Part);
541}
542
543void CSkins7::RandomizeSkin(int Dummy) const
544{
545 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
546 {
547 int Hue = rand() % 255;
548 int Sat = rand() % 255;
549 int Lgt = rand() % 255;
550 int Alp = 0;
551 if(Part == protocol7::SKINPART_MARKING)
552 Alp = rand() % 255;
553 int ColorVariable = (Alp << 24) | (Hue << 16) | (Sat << 8) | Lgt;
554 *ms_apUCCVariables[Dummy][Part] = true;
555 *ms_apColorVariables[Dummy][Part] = ColorVariable;
556 }
557
558 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
559 {
560 std::vector<const CSkins7::CSkinPart *> vpConsideredSkinParts;
561 for(const CSkinPart &SkinPart : GetSkinParts(Part))
562 {
563 if((SkinPart.m_Flags & CSkins7::SKINFLAG_SPECIAL) != 0)
564 continue;
565 vpConsideredSkinParts.push_back(x: &SkinPart);
566 }
567 const CSkins7::CSkinPart *pRandomPart;
568 if(vpConsideredSkinParts.empty())
569 {
570 pRandomPart = FindDefaultSkinPart(Part);
571 }
572 else
573 {
574 pRandomPart = vpConsideredSkinParts[rand() % vpConsideredSkinParts.size()];
575 }
576 str_copy(dst: CSkins7::ms_apSkinVariables[Dummy][Part], src: pRandomPart->m_aName, dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
577 }
578
579 ms_apSkinNameVariables[Dummy][0] = '\0';
580}
581
582ColorRGBA CSkins7::GetColor(int Value, bool UseAlpha) const
583{
584 return color_cast<ColorRGBA>(hsl: ColorHSLA(Value, UseAlpha).UnclampLighting(Darkest: ColorHSLA::DARKEST_LGT7));
585}
586
587void CSkins7::ApplyColorTo(CTeeRenderInfo::CSixup &SixupRenderInfo, bool UseCustomColors, int Value, int Part) const
588{
589 SixupRenderInfo.m_aUseCustomColors[Part] = UseCustomColors;
590 if(UseCustomColors)
591 {
592 SixupRenderInfo.m_aColors[Part] = GetColor(Value, UseAlpha: Part == protocol7::SKINPART_MARKING);
593 }
594 else
595 {
596 SixupRenderInfo.m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
597 }
598}
599
600ColorRGBA CSkins7::GetTeamColor(int UseCustomColors, int PartColor, int Team, int Part) const
601{
602 static const int s_aTeamColors[3] = {0xC4C34E, 0x00FF6B, 0x9BFF6B};
603
604 int TeamHue = (s_aTeamColors[Team + 1] >> 16) & 0xff;
605 int TeamSat = (s_aTeamColors[Team + 1] >> 8) & 0xff;
606 int TeamLgt = s_aTeamColors[Team + 1] & 0xff;
607 int PartSat = (PartColor >> 8) & 0xff;
608 int PartLgt = PartColor & 0xff;
609
610 if(!UseCustomColors)
611 {
612 PartSat = 255;
613 PartLgt = 255;
614 }
615
616 int MinSat = 160;
617 int MaxSat = 255;
618
619 int h = TeamHue;
620 int s = std::clamp(val: mix(a: TeamSat, b: PartSat, amount: 0.2), lo: MinSat, hi: MaxSat);
621 int l = std::clamp(val: mix(a: TeamLgt, b: PartLgt, amount: 0.2), lo: (int)ColorHSLA::DARKEST_LGT7, hi: 200);
622
623 int ColorVal = (h << 16) + (s << 8) + l;
624
625 return GetColor(Value: ColorVal, UseAlpha: Part == protocol7::SKINPART_MARKING);
626}
627
628bool CSkins7::ValidateSkinParts(char *apPartNames[protocol7::NUM_SKINPARTS], int *pUseCustomColors, int *pPartColors, int GameFlags) const
629{
630 // force standard (black) eyes on team skins
631 if(GameFlags & GAMEFLAG_TEAMS)
632 {
633 // TODO: adjust eye color here as well?
634 if(str_comp(a: apPartNames[protocol7::SKINPART_EYES], b: "colorable") == 0 || str_comp(a: apPartNames[protocol7::SKINPART_EYES], b: "negative") == 0)
635 {
636 str_copy(dst: apPartNames[protocol7::SKINPART_EYES], src: "standard", dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
637 return false;
638 }
639 }
640 return true;
641}
642
643bool CSkins7::SaveSkinfile(const char *pName, int Dummy)
644{
645 dbg_assert(!IsSpecialSkin(pName), "Cannot save special skins");
646
647 char aBuf[IO_MAX_PATH_LENGTH];
648 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), SKINS_DIR "/%s.json", pName);
649 IOHANDLE File = Storage()->OpenFile(pFilename: aBuf, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
650 if(!File)
651 return false;
652
653 CJsonFileWriter Writer(File);
654
655 Writer.BeginObject();
656 Writer.WriteAttribute(pName: "skin");
657 Writer.BeginObject();
658 for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; PartIndex++)
659 {
660 if(!ms_apSkinVariables[Dummy][PartIndex][0])
661 continue;
662
663 // part start
664 Writer.WriteAttribute(pName: ms_apSkinPartNames[PartIndex]);
665 Writer.BeginObject();
666 {
667 Writer.WriteAttribute(pName: "filename");
668 Writer.WriteStrValue(pValue: ms_apSkinVariables[Dummy][PartIndex]);
669
670 const bool CustomColors = *ms_apUCCVariables[Dummy][PartIndex];
671 Writer.WriteAttribute(pName: "custom_colors");
672 Writer.WriteBoolValue(Value: CustomColors);
673
674 if(CustomColors)
675 {
676 for(int ColorComponent = 0; ColorComponent < NUM_COLOR_COMPONENTS - 1; ColorComponent++)
677 {
678 int Val = (*ms_apColorVariables[Dummy][PartIndex] >> (2 - ColorComponent) * 8) & 0xff;
679 Writer.WriteAttribute(pName: ms_apColorComponents[ColorComponent]);
680 Writer.WriteIntValue(Value: Val);
681 }
682 if(PartIndex == protocol7::SKINPART_MARKING)
683 {
684 int Val = (*ms_apColorVariables[Dummy][PartIndex] >> 24) & 0xff;
685 Writer.WriteAttribute(pName: ms_apColorComponents[3]);
686 Writer.WriteIntValue(Value: Val);
687 }
688 }
689 }
690 Writer.EndObject();
691 }
692 Writer.EndObject();
693 Writer.EndObject();
694
695 AddSkinFromConfigVariables(pName, Dummy);
696 return true;
697}
698