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