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