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