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