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