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