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
4#include <base/dbg.h>
5#include <base/io.h>
6#include <base/log.h>
7#include <base/str.h>
8
9#include <engine/config.h>
10#include <engine/console.h>
11#include <engine/shared/config.h>
12#include <engine/shared/console.h>
13#include <engine/shared/protocol.h>
14#include <engine/storage.h>
15
16CConfig g_Config;
17
18// ----------------------- Config Variables
19
20static void EscapeParam(char *pDst, const char *pSrc, int Size)
21{
22 str_escape(dst: &pDst, src: pSrc, end: pDst + Size);
23}
24
25void SConfigVariable::ExecuteLine(const char *pLine) const
26{
27 m_pConsole->ExecuteLine(pStr: pLine, ClientId: (m_Flags & CFGFLAG_GAME) != 0 ? IConsole::CLIENT_ID_GAME : IConsole::CLIENT_ID_UNSPECIFIED);
28}
29
30bool SConfigVariable::CheckReadOnly() const
31{
32 if(!m_ReadOnly)
33 return false;
34 log_error("config", "The config variable '%s' cannot be changed right now.", m_pScriptName);
35 return true;
36}
37
38// -----
39
40void SIntConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData)
41{
42 SIntConfigVariable *pData = static_cast<SIntConfigVariable *>(pUserData);
43
44 if(pResult->NumArguments())
45 {
46 if(pData->CheckReadOnly())
47 return;
48
49 int Value = pResult->GetInteger(Index: 0);
50
51 // do clamping
52 if(pData->m_Min != pData->m_Max)
53 {
54 if(Value < pData->m_Min)
55 Value = pData->m_Min;
56 if(pData->m_Max != 0 && Value > pData->m_Max)
57 Value = pData->m_Max;
58 }
59
60 *pData->m_pVariable = Value;
61 if(pResult->m_ClientId != IConsole::CLIENT_ID_GAME)
62 pData->m_OldValue = Value;
63 }
64 else
65 {
66 log_info("config", "Value: %d", *pData->m_pVariable);
67 }
68}
69
70void SIntConfigVariable::Register()
71{
72 m_pConsole->Register(pName: m_pScriptName, pParams: "?i", Flags: m_Flags, pfnFunc: CommandCallback, pUser: this, pHelp: m_pHelp);
73}
74
75bool SIntConfigVariable::IsDefault() const
76{
77 return *m_pVariable == m_Default;
78}
79
80void SIntConfigVariable::Serialize(char *pOut, size_t Size, int Value) const
81{
82 str_format(buffer: pOut, buffer_size: Size, format: "%s %i", m_pScriptName, Value);
83}
84
85void SIntConfigVariable::Serialize(char *pOut, size_t Size) const
86{
87 Serialize(pOut, Size, Value: *m_pVariable);
88}
89
90void SIntConfigVariable::SetValue(int Value)
91{
92 if(CheckReadOnly())
93 return;
94 char aBuf[IConsole::CMDLINE_LENGTH];
95 Serialize(pOut: aBuf, Size: sizeof(aBuf), Value);
96 ExecuteLine(pLine: aBuf);
97}
98
99void SIntConfigVariable::ResetToDefault()
100{
101 SetValue(m_Default);
102}
103
104void SIntConfigVariable::ResetToOld()
105{
106 *m_pVariable = m_OldValue;
107}
108
109// -----
110
111void SColorConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData)
112{
113 SColorConfigVariable *pData = static_cast<SColorConfigVariable *>(pUserData);
114 if(pResult->NumArguments())
115 {
116 if(pData->CheckReadOnly())
117 return;
118
119 const auto Color = pResult->GetColor(Index: 0, DarkestLighting: pData->m_DarkestLighting);
120 const unsigned Value = Color.Pack(Darkest: pData->m_DarkestLighting, Alpha: pData->m_Alpha);
121
122 *pData->m_pVariable = Value;
123 if(pResult->m_ClientId != IConsole::CLIENT_ID_GAME)
124 pData->m_OldValue = Value;
125 }
126 else
127 {
128 log_info("config", "Value: %u", *pData->m_pVariable);
129
130 const ColorHSLA Hsla = ColorHSLA(*pData->m_pVariable, true).UnclampLighting(Darkest: pData->m_DarkestLighting);
131 log_info("config", "H: %d°, S: %d%%, L: %d%%", round_to_int(Hsla.h * 360), round_to_int(Hsla.s * 100), round_to_int(Hsla.l * 100));
132
133 const ColorRGBA Rgba = color_cast<ColorRGBA>(hsl: Hsla);
134 log_info("config", "R: %d, G: %d, B: %d, #%06X", round_to_int(Rgba.r * 255), round_to_int(Rgba.g * 255), round_to_int(Rgba.b * 255), Rgba.Pack(false));
135
136 if(pData->m_Alpha)
137 {
138 log_info("config", "A: %d%%", round_to_int(Hsla.a * 100));
139 }
140 }
141}
142
143void SColorConfigVariable::Register()
144{
145 m_pConsole->Register(pName: m_pScriptName, pParams: "?c", Flags: m_Flags, pfnFunc: CommandCallback, pUser: this, pHelp: m_pHelp);
146}
147
148bool SColorConfigVariable::IsDefault() const
149{
150 return *m_pVariable == m_Default;
151}
152
153void SColorConfigVariable::Serialize(char *pOut, size_t Size, unsigned Value) const
154{
155 str_format(buffer: pOut, buffer_size: Size, format: "%s %u", m_pScriptName, Value);
156}
157
158void SColorConfigVariable::Serialize(char *pOut, size_t Size) const
159{
160 Serialize(pOut, Size, Value: *m_pVariable);
161}
162
163void SColorConfigVariable::SetValue(unsigned Value)
164{
165 if(CheckReadOnly())
166 return;
167 char aBuf[IConsole::CMDLINE_LENGTH];
168 Serialize(pOut: aBuf, Size: sizeof(aBuf), Value);
169 ExecuteLine(pLine: aBuf);
170}
171
172void SColorConfigVariable::ResetToDefault()
173{
174 SetValue(m_Default);
175}
176
177void SColorConfigVariable::ResetToOld()
178{
179 *m_pVariable = m_OldValue;
180}
181
182// -----
183
184SStringConfigVariable::SStringConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, char *pStr, const char *pDefault, size_t MaxSize, char *pOldValue) :
185 SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp),
186 m_pStr(pStr),
187 m_pDefault(pDefault),
188 m_MaxSize(MaxSize),
189 m_pOldValue(pOldValue)
190{
191 str_copy(dst: m_pStr, src: m_pDefault, dst_size: m_MaxSize);
192 str_copy(dst: m_pOldValue, src: m_pDefault, dst_size: m_MaxSize);
193}
194
195void SStringConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData)
196{
197 SStringConfigVariable *pData = static_cast<SStringConfigVariable *>(pUserData);
198
199 if(pResult->NumArguments())
200 {
201 if(pData->CheckReadOnly())
202 return;
203
204 const char *pString = pResult->GetString(Index: 0);
205 str_copy(dst: pData->m_pStr, src: pString, dst_size: pData->m_MaxSize);
206
207 if(pResult->m_ClientId != IConsole::CLIENT_ID_GAME)
208 str_copy(dst: pData->m_pOldValue, src: pData->m_pStr, dst_size: pData->m_MaxSize);
209 }
210 else
211 {
212 log_info("config", "Value: %s", pData->m_pStr);
213 }
214}
215
216void SStringConfigVariable::Register()
217{
218 m_pConsole->Register(pName: m_pScriptName, pParams: "?r", Flags: m_Flags, pfnFunc: CommandCallback, pUser: this, pHelp: m_pHelp);
219}
220
221bool SStringConfigVariable::IsDefault() const
222{
223 return str_comp(a: m_pStr, b: m_pDefault) == 0;
224}
225
226void SStringConfigVariable::Serialize(char *pOut, size_t Size, const char *pValue) const
227{
228 str_copy(dst: pOut, src: m_pScriptName, dst_size: Size);
229 str_append(dst: pOut, src: " \"", dst_size: Size);
230 const int OutLen = str_length(str: pOut);
231 EscapeParam(pDst: pOut + OutLen, pSrc: pValue, Size: Size - OutLen - 1); // -1 to ensure space for final quote
232 str_append(dst: pOut, src: "\"", dst_size: Size);
233}
234
235void SStringConfigVariable::Serialize(char *pOut, size_t Size) const
236{
237 Serialize(pOut, Size, pValue: m_pStr);
238}
239
240void SStringConfigVariable::SetValue(const char *pValue)
241{
242 if(CheckReadOnly())
243 return;
244 char aBuf[2048];
245 Serialize(pOut: aBuf, Size: sizeof(aBuf), pValue);
246 ExecuteLine(pLine: aBuf);
247}
248
249void SStringConfigVariable::ResetToDefault()
250{
251 SetValue(m_pDefault);
252}
253
254void SStringConfigVariable::ResetToOld()
255{
256 str_copy(dst: m_pStr, src: m_pOldValue, dst_size: m_MaxSize);
257}
258
259// ----------------------- Config Manager
260CConfigManager::CConfigManager()
261{
262 m_pConsole = nullptr;
263 m_pStorage = nullptr;
264 m_ConfigFile = nullptr;
265 m_Failed = false;
266}
267
268void CConfigManager::Init()
269{
270 m_pConsole = Kernel()->RequestInterface<IConsole>();
271 m_pStorage = Kernel()->RequestInterface<IStorage>();
272
273 const auto &&AddVariable = [this](SConfigVariable *pVariable) {
274 m_vpAllVariables.push_back(x: pVariable);
275 if((pVariable->m_Flags & CFGFLAG_GAME) != 0)
276 m_vpGameVariables.push_back(x: pVariable);
277 pVariable->Register();
278 };
279
280 const auto &&AddIntVariable = [this, AddVariable](const char *pScriptName, int Flags, const char *pDesc, int *pVariable, int Default, int Min, int Max) {
281 dbg_assert(Min == 0 || Max == 0 || Min < Max, "MACRO_CONFIG_INT(%s): minimum (%d) must be less than maximum (%d)", pScriptName, Min, Max);
282 dbg_assert((Min == 0 || Default >= Min) && (Max == 0 || Default <= Max), "MACRO_CONFIG_INT(%s): default (%d) must be in range of minimum (%d) and maximum (%d)", pScriptName, Default, Min, Max);
283 char aHelp[512];
284 size_t HelpSize;
285 if(Min == 0 && Max == 0)
286 HelpSize = str_format(buffer: aHelp, buffer_size: sizeof(aHelp), format: "%s (default: %d)", pDesc, Default);
287 else if(Max == 0)
288 HelpSize = str_format(buffer: aHelp, buffer_size: sizeof(aHelp), format: "%s (default: %d, min: %d)", pDesc, Default, Min);
289 else
290 HelpSize = str_format(buffer: aHelp, buffer_size: sizeof(aHelp), format: "%s (default: %d, min: %d, max: %d)", pDesc, Default, Min, Max);
291 dbg_assert(HelpSize < sizeof(aHelp) - UTF8_BYTE_LENGTH - 1, "MACRO_CONFIG_INT(%s): help text possibly truncated. Increase size of aHelp.", pScriptName);
292
293 AddVariable(m_ConfigHeap.Allocate<SIntConfigVariable>(
294 Args&: m_pConsole, Args&: pScriptName, Args: SConfigVariable::VAR_INT, Args&: Flags, Args: m_ConfigHeap.StoreString(pSrc: aHelp), Args&: pVariable, Args&: Default, Args&: Min, Args&: Max));
295 };
296
297#define MACRO_CONFIG_INT(Name, ScriptName, Def, Min, Max, Flags, Desc) \
298 { \
299 AddIntVariable(#ScriptName, Flags, Desc, &g_Config.m_##Name, Def, Min, Max); \
300 }
301
302#define MACRO_CONFIG_COL(Name, ScriptName, Def, Flags, Desc) \
303 { \
304 const char *pScriptName = #ScriptName; \
305 const bool Alpha = ((Flags) & CFGFLAG_COLALPHA) != 0; \
306 char aHelp[512]; \
307 const size_t HelpSize = str_format(aHelp, sizeof(aHelp), "%s (default: $%0*X)", Desc, Alpha ? 8 : 6, color_cast<ColorRGBA>(ColorHSLA(Def, Alpha)).Pack(Alpha)); \
308 dbg_assert(HelpSize < sizeof(aHelp) - UTF8_BYTE_LENGTH - 1, "MACRO_CONFIG_COL(%s): help text possibly truncated. Increase size of aHelp.", pScriptName); \
309 AddVariable(m_ConfigHeap.Allocate<SColorConfigVariable>( \
310 m_pConsole, pScriptName, SConfigVariable::VAR_COLOR, Flags, m_ConfigHeap.StoreString(aHelp), &g_Config.m_##Name, Def)); \
311 }
312
313#define MACRO_CONFIG_STR(Name, ScriptName, Len, Def, Flags, Desc) \
314 { \
315 const char *pScriptName = #ScriptName; \
316 char aHelp[512]; \
317 const size_t HelpSize = str_format(aHelp, sizeof(aHelp), "%s (default: \"%s\", max length: %d)", Desc, Def, Len - 1); \
318 dbg_assert(HelpSize < sizeof(aHelp) - UTF8_BYTE_LENGTH - 1, "MACRO_CONFIG_STR(%s): help text possibly truncated. Increase size of aHelp.", pScriptName); \
319 char *pOldValue = static_cast<char *>(m_ConfigHeap.Allocate(Len)); \
320 AddVariable(m_ConfigHeap.Allocate<SStringConfigVariable>( \
321 m_pConsole, pScriptName, SConfigVariable::VAR_STRING, Flags, m_ConfigHeap.StoreString(aHelp), g_Config.m_##Name, Def, Len, pOldValue)); \
322 }
323
324#include "config_variables.h"
325
326#undef MACRO_CONFIG_INT
327#undef MACRO_CONFIG_COL
328#undef MACRO_CONFIG_STR
329
330 m_pConsole->Register(pName: "reset", pParams: "s[config-name]", Flags: CFGFLAG_SERVER | CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Reset, pUser: this, pHelp: "Reset a config to its default value");
331 m_pConsole->Register(pName: "toggle", pParams: "s[config-option] s[value 1] s[value 2]", Flags: CFGFLAG_SERVER | CFGFLAG_CLIENT, pfnFunc: Con_Toggle, pUser: this, pHelp: "Toggle config value");
332 m_pConsole->Register(pName: "+toggle", pParams: "s[config-option] s[value 1] s[value 2]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_ToggleStroke, pUser: this, pHelp: "Toggle config value via keypress");
333}
334
335void CConfigManager::Reset(const char *pScriptName)
336{
337 for(SConfigVariable *pVariable : m_vpAllVariables)
338 {
339 if((pVariable->m_Flags & m_pConsole->FlagMask()) != 0 && str_comp(a: pScriptName, b: pVariable->m_pScriptName) == 0)
340 {
341 pVariable->ResetToDefault();
342 return;
343 }
344 }
345
346 log_error("config", "Invalid command: '%s'.", pScriptName);
347}
348
349void CConfigManager::ResetGameSettings()
350{
351 for(SConfigVariable *pVariable : m_vpGameVariables)
352 {
353 pVariable->ResetToOld();
354 }
355}
356
357void CConfigManager::SetReadOnly(const char *pScriptName, bool ReadOnly)
358{
359 for(SConfigVariable *pVariable : m_vpAllVariables)
360 {
361 if(str_comp(a: pScriptName, b: pVariable->m_pScriptName) == 0)
362 {
363 pVariable->m_ReadOnly = ReadOnly;
364 return;
365 }
366 }
367 dbg_assert_failed("Invalid command for SetReadOnly: '%s'", pScriptName);
368}
369
370void CConfigManager::SetGameSettingsReadOnly(bool ReadOnly)
371{
372 for(SConfigVariable *pVariable : m_vpGameVariables)
373 {
374 pVariable->m_ReadOnly = ReadOnly;
375 }
376}
377
378bool CConfigManager::Save()
379{
380 if(!m_pStorage || !g_Config.m_ClSaveSettings)
381 return true;
382
383 char aConfigFileTmp[IO_MAX_PATH_LENGTH];
384 m_ConfigFile = m_pStorage->OpenFile(pFilename: IStorage::FormatTmpPath(aBuf: aConfigFileTmp, BufSize: sizeof(aConfigFileTmp), CONFIG_FILE), Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
385
386 if(!m_ConfigFile)
387 {
388 log_error("config", "ERROR: opening %s failed", aConfigFileTmp);
389 return false;
390 }
391
392 m_Failed = false;
393
394 char aLineBuf[2048];
395 for(const SConfigVariable *pVariable : m_vpAllVariables)
396 {
397 if((pVariable->m_Flags & CFGFLAG_SAVE) != 0 && !pVariable->IsDefault())
398 {
399 pVariable->Serialize(pOut: aLineBuf, Size: sizeof(aLineBuf));
400 WriteLine(pLine: aLineBuf);
401 }
402 }
403
404 for(const auto &Callback : m_vCallbacks)
405 {
406 Callback.m_pfnFunc(this, Callback.m_pUserData);
407 }
408
409 for(const char *pCommand : m_vpUnknownCommands)
410 {
411 WriteLine(pLine: pCommand);
412 }
413
414 if(m_Failed)
415 {
416 log_error("config", "ERROR: writing to %s failed", aConfigFileTmp);
417 }
418
419 if(io_sync(io: m_ConfigFile) != 0)
420 {
421 m_Failed = true;
422 log_error("config", "ERROR: synchronizing %s failed", aConfigFileTmp);
423 }
424
425 if(io_close(io: m_ConfigFile) != 0)
426 {
427 m_Failed = true;
428 log_error("config", "ERROR: closing %s failed", aConfigFileTmp);
429 }
430
431 m_ConfigFile = nullptr;
432
433 if(m_Failed)
434 {
435 return false;
436 }
437
438 if(!m_pStorage->RenameFile(pOldFilename: aConfigFileTmp, CONFIG_FILE, Type: IStorage::TYPE_SAVE))
439 {
440 log_error("config", "ERROR: renaming %s to " CONFIG_FILE " failed", aConfigFileTmp);
441 return false;
442 }
443
444 log_info("config", "saved to " CONFIG_FILE);
445 return true;
446}
447
448void CConfigManager::RegisterCallback(SAVECALLBACKFUNC pfnFunc, void *pUserData)
449{
450 m_vCallbacks.emplace_back(args&: pfnFunc, args&: pUserData);
451}
452
453void CConfigManager::WriteLine(const char *pLine)
454{
455 if(!m_ConfigFile ||
456 io_write(io: m_ConfigFile, buffer: pLine, size: str_length(str: pLine)) != static_cast<unsigned>(str_length(str: pLine)) ||
457 !io_write_newline(io: m_ConfigFile))
458 {
459 m_Failed = true;
460 }
461}
462
463void CConfigManager::StoreUnknownCommand(const char *pCommand)
464{
465 m_vpUnknownCommands.push_back(x: m_ConfigHeap.StoreString(pSrc: pCommand));
466}
467
468void CConfigManager::PossibleConfigVariables(const char *pStr, int FlagMask, POSSIBLECFGFUNC pfnCallback, void *pUserData)
469{
470 for(const SConfigVariable *pVariable : m_vpAllVariables)
471 {
472 if(pVariable->m_Flags & FlagMask)
473 {
474 if(str_find_nocase(haystack: pVariable->m_pScriptName, needle: pStr))
475 {
476 pfnCallback(pVariable, pUserData);
477 }
478 }
479 }
480}
481
482void CConfigManager::Con_Reset(IConsole::IResult *pResult, void *pUserData)
483{
484 static_cast<CConfigManager *>(pUserData)->Reset(pScriptName: pResult->GetString(Index: 0));
485}
486
487void CConfigManager::Con_Toggle(IConsole::IResult *pResult, void *pUserData)
488{
489 CConfigManager *pConfigManager = static_cast<CConfigManager *>(pUserData);
490 IConsole *pConsole = pConfigManager->m_pConsole;
491
492 const char *pScriptName = pResult->GetString(Index: 0);
493 for(SConfigVariable *pVariable : pConfigManager->m_vpAllVariables)
494 {
495 if((pVariable->m_Flags & pConsole->FlagMask()) == 0 ||
496 str_comp(a: pScriptName, b: pVariable->m_pScriptName) != 0)
497 {
498 continue;
499 }
500
501 if(pVariable->m_Type == SConfigVariable::VAR_INT)
502 {
503 SIntConfigVariable *pIntVariable = static_cast<SIntConfigVariable *>(pVariable);
504 const bool EqualToFirst = *pIntVariable->m_pVariable == pResult->GetInteger(Index: 1);
505 pIntVariable->SetValue(pResult->GetInteger(Index: EqualToFirst ? 2 : 1));
506 }
507 else if(pVariable->m_Type == SConfigVariable::VAR_COLOR)
508 {
509 SColorConfigVariable *pColorVariable = static_cast<SColorConfigVariable *>(pVariable);
510 const bool EqualToFirst = *pColorVariable->m_pVariable == pResult->GetColor(Index: 1, DarkestLighting: pColorVariable->m_DarkestLighting).Pack(Darkest: pColorVariable->m_DarkestLighting, Alpha: pColorVariable->m_Alpha);
511 const std::optional<ColorHSLA> Value = pResult->GetColor(Index: EqualToFirst ? 2 : 1, DarkestLighting: pColorVariable->m_DarkestLighting);
512 pColorVariable->SetValue(Value.value_or(u: ColorHSLA(0, 0, 0)).Pack(Darkest: pColorVariable->m_DarkestLighting, Alpha: pColorVariable->m_Alpha));
513 }
514 else if(pVariable->m_Type == SConfigVariable::VAR_STRING)
515 {
516 SStringConfigVariable *pStringVariable = static_cast<SStringConfigVariable *>(pVariable);
517 const bool EqualToFirst = str_comp(a: pStringVariable->m_pStr, b: pResult->GetString(Index: 1)) == 0;
518 pStringVariable->SetValue(pResult->GetString(Index: EqualToFirst ? 2 : 1));
519 }
520 return;
521 }
522
523 log_error("config", "Invalid command: '%s'.", pScriptName);
524}
525
526void CConfigManager::Con_ToggleStroke(IConsole::IResult *pResult, void *pUserData)
527{
528 CConfigManager *pConfigManager = static_cast<CConfigManager *>(pUserData);
529 IConsole *pConsole = pConfigManager->m_pConsole;
530
531 const char *pScriptName = pResult->GetString(Index: 1);
532 for(SConfigVariable *pVariable : pConfigManager->m_vpAllVariables)
533 {
534 if((pVariable->m_Flags & pConsole->FlagMask()) == 0 ||
535 pVariable->m_Type != SConfigVariable::VAR_INT ||
536 str_comp(a: pScriptName, b: pVariable->m_pScriptName) != 0)
537 {
538 continue;
539 }
540
541 SIntConfigVariable *pIntVariable = static_cast<SIntConfigVariable *>(pVariable);
542 pIntVariable->SetValue(pResult->GetInteger(Index: 0) == 0 ? pResult->GetInteger(Index: 3) : pResult->GetInteger(Index: 2));
543 return;
544 }
545
546 log_error("config", "Invalid command: '%s'.", pScriptName);
547}
548
549IConfigManager *CreateConfigManager() { return new CConfigManager; }
550