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