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