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 "console.h"
5
6#include "config.h"
7#include "linereader.h"
8
9#include <base/color.h>
10#include <base/dbg.h>
11#include <base/io.h>
12#include <base/log.h>
13#include <base/math.h>
14#include <base/mem.h>
15#include <base/str.h>
16
17#include <engine/client/checksum.h>
18#include <engine/console.h>
19#include <engine/shared/protocol.h>
20#include <engine/storage.h>
21
22#include <algorithm>
23#include <iterator> // std::size
24#include <new>
25
26// todo: rework this
27
28CConsole::CResult::CResult(int ClientId) :
29 IResult(ClientId)
30{
31 mem_zero(block: m_aStringStorage, size: sizeof(m_aStringStorage));
32 m_pArgsStart = nullptr;
33 m_pCommand = nullptr;
34 std::fill(first: std::begin(arr&: m_apArgs), last: std::end(arr&: m_apArgs), value: nullptr);
35}
36
37CConsole::CResult::CResult(const CResult &Other) :
38 IResult(Other)
39{
40 mem_copy(dest: m_aStringStorage, source: Other.m_aStringStorage, size: sizeof(m_aStringStorage));
41 m_pArgsStart = m_aStringStorage + (Other.m_pArgsStart - Other.m_aStringStorage);
42 m_pCommand = m_aStringStorage + (Other.m_pCommand - Other.m_aStringStorage);
43 for(unsigned i = 0; i < Other.m_NumArgs; ++i)
44 m_apArgs[i] = m_aStringStorage + (Other.m_apArgs[i] - Other.m_aStringStorage);
45}
46
47void CConsole::CResult::AddArgument(const char *pArg)
48{
49 m_apArgs[m_NumArgs++] = pArg;
50}
51
52void CConsole::CResult::RemoveArgument(unsigned Index)
53{
54 dbg_assert(Index < m_NumArgs, "invalid argument index");
55 for(unsigned i = Index; i < m_NumArgs - 1; i++)
56 m_apArgs[i] = m_apArgs[i + 1];
57
58 m_apArgs[m_NumArgs--] = nullptr;
59}
60
61const char *CConsole::CResult::GetString(unsigned Index) const
62{
63 if(Index >= m_NumArgs)
64 return "";
65 return m_apArgs[Index];
66}
67
68int CConsole::CResult::GetInteger(unsigned Index) const
69{
70 if(Index >= m_NumArgs)
71 return 0;
72 int Out;
73 return str_toint(str: m_apArgs[Index], out: &Out) ? Out : 0;
74}
75
76float CConsole::CResult::GetFloat(unsigned Index) const
77{
78 if(Index >= m_NumArgs)
79 return 0.0f;
80 float Out;
81 return str_tofloat(str: m_apArgs[Index], out: &Out) ? Out : 0.0f;
82}
83
84ColorHSLA CConsole::CResult::GetColor(unsigned Index, float DarkestLighting) const
85{
86 if(Index >= m_NumArgs)
87 return ColorHSLA(0, 0, 0);
88 return ColorParse(pStr: m_apArgs[Index], DarkestLighting).value_or(u: ColorHSLA(0, 0, 0));
89}
90
91void CConsole::CCommand::SetAccessLevel(EAccessLevel AccessLevel)
92{
93 m_AccessLevel = AccessLevel;
94}
95
96const IConsole::ICommandInfo *CConsole::FirstCommandInfo(int ClientId, int FlagMask) const
97{
98 for(const CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
99 {
100 if(pCommand->m_Flags & FlagMask && CanUseCommand(ClientId, pCommand))
101 return pCommand;
102 }
103
104 return nullptr;
105}
106
107const IConsole::ICommandInfo *CConsole::NextCommandInfo(const IConsole::ICommandInfo *pInfo, int ClientId, int FlagMask) const
108{
109 const CCommand *pNext = ((CCommand *)pInfo)->Next();
110 while(pNext)
111 {
112 if(pNext->m_Flags & FlagMask && CanUseCommand(ClientId, pCommand: pNext))
113 break;
114 pNext = pNext->Next();
115 }
116 return pNext;
117}
118
119std::optional<CConsole::EAccessLevel> CConsole::AccessLevelToEnum(const char *pAccessLevel)
120{
121 // alias for legacy integer access levels
122 if(!str_comp(a: pAccessLevel, b: "0"))
123 return EAccessLevel::ADMIN;
124 if(!str_comp(a: pAccessLevel, b: "1"))
125 return EAccessLevel::MODERATOR;
126 if(!str_comp(a: pAccessLevel, b: "2"))
127 return EAccessLevel::HELPER;
128 if(!str_comp(a: pAccessLevel, b: "3"))
129 return EAccessLevel::USER;
130
131 // string access levels
132 if(!str_comp(a: pAccessLevel, b: "admin"))
133 return EAccessLevel::ADMIN;
134 if(!str_comp(a: pAccessLevel, b: "moderator"))
135 return EAccessLevel::MODERATOR;
136 if(!str_comp(a: pAccessLevel, b: "helper"))
137 return EAccessLevel::HELPER;
138 if(!str_comp(a: pAccessLevel, b: "all"))
139 return EAccessLevel::USER;
140 return std::nullopt;
141}
142
143const char *CConsole::AccessLevelToString(EAccessLevel AccessLevel)
144{
145 switch(AccessLevel)
146 {
147 case EAccessLevel::ADMIN:
148 return "admin";
149 case EAccessLevel::MODERATOR:
150 return "moderator";
151 case EAccessLevel::HELPER:
152 return "helper";
153 case EAccessLevel::USER:
154 return "all";
155 }
156 dbg_assert_failed("invalid access level: %d", (int)AccessLevel);
157}
158
159// the maximum number of tokens occurs in a string of length CONSOLE_MAX_STR_LENGTH with tokens size 1 separated by single spaces
160
161int CConsole::ParseStart(CResult *pResult, const char *pString, int Length)
162{
163 char *pStr;
164 int Len = sizeof(pResult->m_aStringStorage);
165 if(Length < Len)
166 Len = Length;
167
168 str_copy(dst: pResult->m_aStringStorage, src: pString, dst_size: Len);
169 pStr = pResult->m_aStringStorage;
170
171 // get command
172 pStr = str_skip_whitespaces(str: pStr);
173 pResult->m_pCommand = pStr;
174 pStr = str_skip_to_whitespace(str: pStr);
175
176 if(*pStr)
177 {
178 pStr[0] = 0;
179 pStr++;
180 }
181
182 pResult->m_pArgsStart = pStr;
183 return 0;
184}
185
186int CConsole::ParseArgs(CResult *pResult, const char *pFormat)
187{
188 char *pStr = pResult->m_pArgsStart;
189 bool Optional = false;
190
191 pResult->ResetVictim();
192
193 for(char Command = *pFormat; Command != '\0'; Command = NextParam(pFormat))
194 {
195 if(Command == '?')
196 {
197 Optional = true;
198 continue;
199 }
200
201 pStr = str_skip_whitespaces(str: pStr);
202
203 if(*pStr == '\0') // error, non optional command needs value
204 {
205 if(!Optional)
206 {
207 return PARSEARGS_MISSING_VALUE;
208 }
209
210 while(Command)
211 {
212 if(Command == 'v')
213 {
214 pResult->SetVictim(CResult::VICTIM_ME);
215 break;
216 }
217 Command = NextParam(pFormat);
218 }
219 return PARSEARGS_OK;
220 }
221
222 // add token
223 if(*pStr == '"')
224 {
225 pStr++;
226 pResult->AddArgument(pArg: pStr);
227
228 char *pDst = pStr; // we might have to process escape data
229 while(pStr[0] != '"')
230 {
231 if(pStr[0] == '\\')
232 {
233 if(pStr[1] == '\\')
234 pStr++; // skip due to escape
235 else if(pStr[1] == '"')
236 pStr++; // skip due to escape
237 }
238 else if(pStr[0] == '\0')
239 {
240 return PARSEARGS_MISSING_VALUE; // return error
241 }
242
243 *pDst = *pStr;
244 pDst++;
245 pStr++;
246 }
247 *pDst = '\0';
248
249 pStr++;
250 }
251 else
252 {
253 pResult->AddArgument(pArg: pStr);
254
255 if(Command == 'r') // rest of the string
256 {
257 return PARSEARGS_OK;
258 }
259
260 pStr = str_skip_to_whitespace(str: pStr);
261 if(pStr[0] != '\0') // check for end of string
262 {
263 pStr[0] = '\0';
264 pStr++;
265 }
266
267 // validate arguments
268 if(Command == 'v')
269 {
270 pResult->SetVictim(pResult->GetString(Index: pResult->NumArguments() - 1));
271 }
272 else if(Command == 'i')
273 {
274 int Value;
275 if(!str_toint(str: pResult->GetString(Index: pResult->NumArguments() - 1), out: &Value) ||
276 Value == std::numeric_limits<int>::max() ||
277 Value == std::numeric_limits<int>::min())
278 {
279 return PARSEARGS_INVALID_INTEGER;
280 }
281 }
282 else if(Command == 'c')
283 {
284 auto Color = ColorParse(pStr: pResult->GetString(Index: pResult->NumArguments() - 1), DarkestLighting: 0.0f);
285 if(!Color.has_value())
286 {
287 return PARSEARGS_INVALID_COLOR;
288 }
289 }
290 else if(Command == 'f')
291 {
292 float Value;
293 if(!str_tofloat(str: pResult->GetString(Index: pResult->NumArguments() - 1), out: &Value) ||
294 Value == std::numeric_limits<float>::max() ||
295 Value == std::numeric_limits<float>::min())
296 {
297 return PARSEARGS_INVALID_FLOAT;
298 }
299 }
300 // 's' and unknown commands are handled as strings
301 }
302 }
303
304 return PARSEARGS_OK;
305}
306
307char CConsole::NextParam(const char *&pFormat)
308{
309 if(*pFormat)
310 {
311 pFormat++;
312
313 if(*pFormat == '[')
314 {
315 // skip bracket contents
316 for(; *pFormat != ']'; pFormat++)
317 {
318 if(!*pFormat)
319 return *pFormat;
320 }
321
322 // skip ']'
323 pFormat++;
324
325 // skip space if there is one
326 if(*pFormat == ' ')
327 pFormat++;
328 }
329 }
330 return *pFormat;
331}
332
333LEVEL IConsole::ToLogLevel(int Level)
334{
335 switch(Level)
336 {
337 case IConsole::OUTPUT_LEVEL_STANDARD:
338 return LEVEL_INFO;
339 case IConsole::OUTPUT_LEVEL_ADDINFO:
340 return LEVEL_DEBUG;
341 case IConsole::OUTPUT_LEVEL_DEBUG:
342 return LEVEL_TRACE;
343 }
344 dbg_assert(0, "invalid log level");
345 return LEVEL_INFO;
346}
347
348int IConsole::ToLogLevelFilter(int Level)
349{
350 if(!(-3 <= Level && Level <= 2))
351 {
352 dbg_assert(0, "invalid log level filter");
353 }
354 return Level + 2;
355}
356
357static LOG_COLOR ColorToLogColor(ColorRGBA Color)
358{
359 return LOG_COLOR{
360 .r: (uint8_t)(Color.r * 255.0),
361 .g: (uint8_t)(Color.g * 255.0),
362 .b: (uint8_t)(Color.b * 255.0)};
363}
364
365void CConsole::Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor) const
366{
367 LEVEL LogLevel = IConsole::ToLogLevel(Level);
368 // if console colors are not enabled or if the color is pure white, use default terminal color
369 if(g_Config.m_ConsoleEnableColors && PrintColor != CONSOLE_DEFAULT_COLOR)
370 {
371 log_log_color(level: LogLevel, color: ColorToLogColor(Color: PrintColor), sys: pFrom, fmt: "%s", pStr);
372 }
373 else
374 {
375 log_log(level: LogLevel, sys: pFrom, fmt: "%s", pStr);
376 }
377}
378
379void CConsole::SetTeeHistorianCommandCallback(FTeeHistorianCommandCallback pfnCallback, void *pUser)
380{
381 m_pfnTeeHistorianCommandCallback = pfnCallback;
382 m_pTeeHistorianCommandUserdata = pUser;
383}
384
385void CConsole::SetUnknownCommandCallback(FUnknownCommandCallback pfnCallback, void *pUser)
386{
387 m_pfnUnknownCommandCallback = pfnCallback;
388 m_pUnknownCommandUserdata = pUser;
389}
390
391void CConsole::SetCanUseCommandCallback(FCanUseCommandCallback pfnCallback, void *pUser)
392{
393 m_pfnCanUseCommandCallback = pfnCallback;
394 m_pCanUseCommandUserData = pUser;
395}
396
397void CConsole::InitChecksum(CChecksumData *pData) const
398{
399 pData->m_NumCommands = 0;
400 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
401 {
402 if(pData->m_NumCommands < (int)(std::size(pData->m_aCommandsChecksum)))
403 {
404 FCommandCallback pfnCallback = pCommand->m_pfnCallback;
405 void *pUserData = pCommand->m_pUserData;
406 TraverseChain(ppfnCallback: &pfnCallback, ppUserData: &pUserData);
407 int CallbackBits = (uintptr_t)pfnCallback & 0xfff;
408 int *pTarget = &pData->m_aCommandsChecksum[pData->m_NumCommands];
409 *pTarget = ((uint8_t)pCommand->m_pName[0]) | ((uint8_t)pCommand->m_pName[1] << 8) | (CallbackBits << 16);
410 }
411 pData->m_NumCommands += 1;
412 }
413}
414
415bool CConsole::LineIsValid(const char *pStr)
416{
417 if(!pStr || *pStr == 0)
418 return false;
419
420 do
421 {
422 CResult Result(IConsole::CLIENT_ID_UNSPECIFIED);
423 const char *pEnd = pStr;
424 const char *pNextPart = nullptr;
425 int InString = 0;
426
427 while(*pEnd)
428 {
429 if(*pEnd == '"')
430 InString ^= 1;
431 else if(*pEnd == '\\') // escape sequences
432 {
433 if(pEnd[1] == '"')
434 pEnd++;
435 }
436 else if(!InString)
437 {
438 if(*pEnd == ';') // command separator
439 {
440 pNextPart = pEnd + 1;
441 break;
442 }
443 else if(*pEnd == '#') // comment, no need to do anything more
444 break;
445 }
446
447 pEnd++;
448 }
449
450 if(ParseStart(pResult: &Result, pString: pStr, Length: (pEnd - pStr) + 1) != 0)
451 return false;
452
453 CCommand *pCommand = FindCommand(pName: Result.m_pCommand, FlagMask: m_FlagMask);
454 if(!pCommand || ParseArgs(pResult: &Result, pFormat: pCommand->m_pParams))
455 return false;
456
457 pStr = pNextPart;
458 } while(pStr && *pStr);
459
460 return true;
461}
462
463void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientId, bool InterpretSemicolons)
464{
465 const char *pWithoutPrefix = str_startswith(str: pStr, prefix: "mc;");
466 if(pWithoutPrefix)
467 {
468 InterpretSemicolons = true;
469 pStr = pWithoutPrefix;
470 }
471 while(pStr && *pStr)
472 {
473 CResult Result(ClientId);
474 const char *pEnd = pStr;
475 const char *pNextPart = nullptr;
476 int InString = 0;
477
478 while(*pEnd)
479 {
480 if(*pEnd == '"')
481 InString ^= 1;
482 else if(*pEnd == '\\') // escape sequences
483 {
484 if(pEnd[1] == '"')
485 pEnd++;
486 }
487 else if(!InString && InterpretSemicolons)
488 {
489 if(*pEnd == ';') // command separator
490 {
491 pNextPart = pEnd + 1;
492 break;
493 }
494 else if(*pEnd == '#') // comment, no need to do anything more
495 break;
496 }
497
498 pEnd++;
499 }
500
501 if(ParseStart(pResult: &Result, pString: pStr, Length: (pEnd - pStr) + 1) != 0)
502 return;
503
504 if(!*Result.m_pCommand)
505 {
506 if(pNextPart)
507 {
508 pStr = pNextPart;
509 continue;
510 }
511 return;
512 }
513
514 CCommand *pCommand;
515 if(ClientId == IConsole::CLIENT_ID_GAME)
516 pCommand = FindCommand(pName: Result.m_pCommand, FlagMask: m_FlagMask | CFGFLAG_GAME);
517 else
518 pCommand = FindCommand(pName: Result.m_pCommand, FlagMask: m_FlagMask);
519
520 if(pCommand)
521 {
522 if(ClientId == IConsole::CLIENT_ID_GAME && !(pCommand->m_Flags & CFGFLAG_GAME))
523 {
524 if(Stroke)
525 {
526 char aBuf[CMDLINE_LENGTH + 64];
527 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Command '%s' cannot be executed from a map.", Result.m_pCommand);
528 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
529 }
530 }
531 else if(ClientId == IConsole::CLIENT_ID_NO_GAME && pCommand->m_Flags & CFGFLAG_GAME)
532 {
533 if(Stroke)
534 {
535 char aBuf[CMDLINE_LENGTH + 64];
536 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Command '%s' cannot be executed from a non-map config file.", Result.m_pCommand);
537 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
538 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Hint: Put the command in '%s.cfg' instead of '%s.map.cfg' ", g_Config.m_SvMap, g_Config.m_SvMap);
539 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
540 }
541 }
542 else if(CanUseCommand(ClientId: Result.m_ClientId, pCommand))
543 {
544 int IsStrokeCommand = 0;
545 if(Result.m_pCommand[0] == '+')
546 {
547 // insert the stroke direction token
548 Result.AddArgument(pArg: m_apStrokeStr[Stroke]);
549 IsStrokeCommand = 1;
550 }
551
552 if(Stroke || IsStrokeCommand)
553 {
554 if(int Error = ParseArgs(pResult: &Result, pFormat: pCommand->m_pParams))
555 {
556 char aBuf[CMDLINE_LENGTH + 64];
557 if(Error == PARSEARGS_INVALID_INTEGER)
558 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s is not a valid integer.", Result.GetString(Index: Result.NumArguments() - 1));
559 else if(Error == PARSEARGS_INVALID_COLOR)
560 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s is not a valid color.", Result.GetString(Index: Result.NumArguments() - 1));
561 else if(Error == PARSEARGS_INVALID_FLOAT)
562 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s is not a valid decimal number.", Result.GetString(Index: Result.NumArguments() - 1));
563 else
564 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Invalid arguments. Usage: %s %s", pCommand->m_pName, pCommand->m_pParams);
565 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
566 }
567 else if(m_StoreCommands && pCommand->m_Flags & CFGFLAG_STORE)
568 {
569 m_vExecutionQueue.emplace_back(args&: pCommand, args&: Result);
570 }
571 else
572 {
573 if(pCommand->m_Flags & CMDFLAG_TEST && !g_Config.m_SvTestingCommands)
574 {
575 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: "Test commands aren't allowed, enable them with 'sv_test_cmds 1' in your initial config.");
576 return;
577 }
578
579 if(m_pfnTeeHistorianCommandCallback && !(pCommand->m_Flags & CFGFLAG_NONTEEHISTORIC))
580 {
581 m_pfnTeeHistorianCommandCallback(ClientId, m_FlagMask, pCommand->m_pName, &Result, m_pTeeHistorianCommandUserdata);
582 }
583
584 if(Result.GetVictim() == CResult::VICTIM_ME)
585 Result.SetVictim(ClientId);
586
587 if(Result.HasVictim() && Result.GetVictim() == CResult::VICTIM_ALL)
588 {
589 for(int i = 0; i < MAX_CLIENTS; i++)
590 {
591 Result.SetVictim(i);
592 pCommand->m_pfnCallback(&Result, pCommand->m_pUserData);
593 }
594 }
595 else
596 {
597 pCommand->m_pfnCallback(&Result, pCommand->m_pUserData);
598 }
599
600 if(pCommand->m_Flags & CMDFLAG_TEST)
601 m_Cheated = true;
602 }
603 }
604 }
605 else if(Stroke)
606 {
607 char aBuf[CMDLINE_LENGTH + 32];
608 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Access for command %s denied.", Result.m_pCommand);
609 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
610 }
611 }
612 else if(Stroke)
613 {
614 // Pass the original string to the unknown command callback instead of the parsed command, as the latter
615 // ends at the first whitespace, which breaks for unknown commands (filenames) containing spaces.
616 if(!m_pfnUnknownCommandCallback(pStr, m_pUnknownCommandUserdata))
617 {
618 char aBuf[CMDLINE_LENGTH + 32];
619 if(m_FlagMask & CFGFLAG_CHAT)
620 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such command: %s. Use /cmdlist for a list of all commands.", Result.m_pCommand);
621 else
622 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such command: %s.", Result.m_pCommand);
623 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
624 }
625 }
626
627 pStr = pNextPart;
628 }
629}
630
631bool CConsole::CanUseCommand(int ClientId, const IConsole::ICommandInfo *pCommand) const
632{
633 // the fallback is needed for the client and rust tests
634 if(!m_pfnCanUseCommandCallback)
635 return true;
636 return m_pfnCanUseCommandCallback(ClientId, pCommand, m_pCanUseCommandUserData);
637}
638
639int CConsole::PossibleCommands(const char *pStr, int FlagMask, bool Temp, FPossibleCallback pfnCallback, void *pUser)
640{
641 int Index = 0;
642 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
643 {
644 if(pCommand->m_Flags & FlagMask && pCommand->m_Temp == Temp)
645 {
646 if(str_find_nocase(haystack: pCommand->m_pName, needle: pStr))
647 {
648 pfnCallback(Index, pCommand->m_pName, pUser);
649 Index++;
650 }
651 }
652 }
653 return Index;
654}
655
656CConsole::CCommand *CConsole::FindCommand(const char *pName, int FlagMask)
657{
658 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
659 {
660 if(pCommand->m_Flags & FlagMask)
661 {
662 if(str_comp_nocase(a: pCommand->m_pName, b: pName) == 0)
663 return pCommand;
664 }
665 }
666
667 return nullptr;
668}
669
670void CConsole::ExecuteLine(const char *pStr, int ClientId, bool InterpretSemicolons)
671{
672 CConsole::ExecuteLineStroked(Stroke: 1, pStr, ClientId, InterpretSemicolons); // press it
673 CConsole::ExecuteLineStroked(Stroke: 0, pStr, ClientId, InterpretSemicolons); // then release it
674}
675
676void CConsole::ExecuteLineFlag(const char *pStr, int FlagMask, int ClientId, bool InterpretSemicolons)
677{
678 int Temp = m_FlagMask;
679 m_FlagMask = FlagMask;
680 ExecuteLine(pStr, ClientId, InterpretSemicolons);
681 m_FlagMask = Temp;
682}
683
684bool CConsole::ExecuteFile(const char *pFilename, int ClientId, bool LogFailure, int StorageType)
685{
686 int Count = 0;
687 // make sure that this isn't being executed already and that recursion limit isn't met
688 for(CExecFile *pCur = m_pFirstExec; pCur; pCur = pCur->m_pPrev)
689 {
690 Count++;
691
692 if(str_comp(a: pFilename, b: pCur->m_pFilename) == 0 || Count > FILE_RECURSION_LIMIT)
693 return false;
694 }
695 if(!m_pStorage)
696 return false;
697
698 // push this one to the stack
699 CExecFile ThisFile;
700 CExecFile *pPrev = m_pFirstExec;
701 ThisFile.m_pFilename = pFilename;
702 ThisFile.m_pPrev = m_pFirstExec;
703 m_pFirstExec = &ThisFile;
704
705 // exec the file
706 CLineReader LineReader;
707 bool Success = false;
708 char aBuf[32 + IO_MAX_PATH_LENGTH];
709 if(LineReader.OpenFile(File: m_pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: StorageType)))
710 {
711 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "executing '%s'", pFilename);
712 Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
713
714 while(const char *pLine = LineReader.Get())
715 {
716 ExecuteLine(pStr: pLine, ClientId);
717 }
718
719 Success = true;
720 }
721 else if(LogFailure)
722 {
723 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "failed to open '%s'", pFilename);
724 Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
725 }
726
727 m_pFirstExec = pPrev;
728 return Success;
729}
730
731void CConsole::Con_Echo(IResult *pResult, void *pUserData)
732{
733 ((CConsole *)pUserData)->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: pResult->GetString(Index: 0));
734}
735
736void CConsole::Con_Exec(IResult *pResult, void *pUserData)
737{
738 ((CConsole *)pUserData)->ExecuteFile(pFilename: pResult->GetString(Index: 0), ClientId: pResult->m_ClientId, LogFailure: true, StorageType: IStorage::TYPE_ALL);
739}
740
741void CConsole::ConCommandAccess(IResult *pResult, void *pUser)
742{
743 CConsole *pConsole = static_cast<CConsole *>(pUser);
744 char aBuf[CMDLINE_LENGTH + 64];
745 CCommand *pCommand = pConsole->FindCommand(pName: pResult->GetString(Index: 0), FlagMask: CFGFLAG_SERVER);
746 if(pCommand)
747 {
748 if(pResult->NumArguments() == 2)
749 {
750 std::optional<EAccessLevel> AccessLevel = AccessLevelToEnum(pAccessLevel: pResult->GetString(Index: 1));
751 if(!AccessLevel.has_value())
752 {
753 log_error("console", "Invalid access level '%s'. Allowed values are admin, moderator, helper and all.", pResult->GetString(1));
754 return;
755 }
756 pCommand->SetAccessLevel(AccessLevel.value());
757 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "moderator access for '%s' is now %s", pResult->GetString(Index: 0), pCommand->GetAccessLevel() >= EAccessLevel::MODERATOR ? "enabled" : "disabled");
758 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
759 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "helper access for '%s' is now %s", pResult->GetString(Index: 0), pCommand->GetAccessLevel() >= EAccessLevel::HELPER ? "enabled" : "disabled");
760 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
761 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "user access for '%s' is now %s", pResult->GetString(Index: 0), pCommand->GetAccessLevel() >= EAccessLevel::USER ? "enabled" : "disabled");
762 }
763 else
764 {
765 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "moderator access for '%s' is %s", pResult->GetString(Index: 0), pCommand->GetAccessLevel() >= EAccessLevel::MODERATOR ? "enabled" : "disabled");
766 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
767 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "helper access for '%s' is %s", pResult->GetString(Index: 0), pCommand->GetAccessLevel() >= EAccessLevel::HELPER ? "enabled" : "disabled");
768 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
769 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "user access for '%s' is %s", pResult->GetString(Index: 0), pCommand->GetAccessLevel() >= EAccessLevel::USER ? "enabled" : "disabled");
770 }
771 }
772 else
773 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such command: '%s'.", pResult->GetString(Index: 0));
774
775 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
776}
777
778void CConsole::PrintCommandList(EAccessLevel MinAccessLevel, int ExcludeFlagMask)
779{
780 char aBuf[240] = "";
781 int Used = 0;
782
783 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
784 {
785 if((pCommand->m_Flags & m_FlagMask) &&
786 !(pCommand->m_Flags & ExcludeFlagMask) &&
787 pCommand->GetAccessLevel() >= MinAccessLevel)
788 {
789 int Length = str_length(str: pCommand->m_pName);
790 if(Used + Length + 2 < (int)(sizeof(aBuf)))
791 {
792 if(Used > 0)
793 {
794 Used += 2;
795 str_append(dst&: aBuf, src: ", ");
796 }
797 str_append(dst&: aBuf, src: pCommand->m_pName);
798 Used += Length;
799 }
800 else
801 {
802 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
803 str_copy(dst&: aBuf, src: pCommand->m_pName);
804 Used = Length;
805 }
806 }
807 }
808 if(Used > 0)
809 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
810}
811
812void CConsole::ConCommandStatus(IResult *pResult, void *pUser)
813{
814 CConsole *pConsole = static_cast<CConsole *>(pUser);
815 std::optional<EAccessLevel> AccessLevel = AccessLevelToEnum(pAccessLevel: pResult->GetString(Index: 0));
816 if(!AccessLevel.has_value())
817 {
818 log_error("console", "Invalid access level '%s'. Allowed values are admin, moderator, helper and all.", pResult->GetString(0));
819 return;
820 }
821 pConsole->PrintCommandList(MinAccessLevel: AccessLevel.value(), ExcludeFlagMask: 0);
822}
823
824void CConsole::ConUserCommandStatus(IResult *pResult, void *pUser)
825{
826 CConsole *pConsole = static_cast<CConsole *>(pUser);
827 pConsole->PrintCommandList(MinAccessLevel: EAccessLevel::USER, ExcludeFlagMask: CMDFLAG_PRACTICE);
828}
829
830void CConsole::TraverseChain(FCommandCallback *ppfnCallback, void **ppUserData)
831{
832 while(*ppfnCallback == Con_Chain)
833 {
834 CChain *pChainInfo = static_cast<CChain *>(*ppUserData);
835 *ppfnCallback = pChainInfo->m_pfnCallback;
836 *ppUserData = pChainInfo->m_pCallbackUserData;
837 }
838}
839
840CConsole::CConsole(int FlagMask)
841{
842 m_FlagMask = FlagMask;
843 m_pRecycleList = nullptr;
844 m_TempCommands.Reset();
845 m_StoreCommands = true;
846 m_apStrokeStr[0] = "0";
847 m_apStrokeStr[1] = "1";
848 m_pFirstCommand = nullptr;
849 m_pFirstExec = nullptr;
850 m_pfnTeeHistorianCommandCallback = nullptr;
851 m_pTeeHistorianCommandUserdata = nullptr;
852
853 m_pStorage = nullptr;
854
855 // register some basic commands
856 Register(pName: "echo", pParams: "r[text]", Flags: CFGFLAG_SERVER, pfnFunc: Con_Echo, pUser: this, pHelp: "Echo the text");
857 Register(pName: "exec", pParams: "r[file]", Flags: CFGFLAG_SERVER | CFGFLAG_CLIENT, pfnFunc: Con_Exec, pUser: this, pHelp: "Execute the specified file");
858
859 Register(pName: "access_level", pParams: "s[command] ?s['admin'|'moderator'|'helper'|'all']", Flags: CFGFLAG_SERVER, pfnFunc: ConCommandAccess, pUser: this, pHelp: "Specify command accessibility for given access level");
860 Register(pName: "access_status", pParams: "s['admin'|'moderator'|'helper'|'all']", Flags: CFGFLAG_SERVER, pfnFunc: ConCommandStatus, pUser: this, pHelp: "List all commands which are accessible for given access level");
861 Register(pName: "cmdlist", pParams: "", Flags: CFGFLAG_SERVER | CFGFLAG_CHAT, pfnFunc: ConUserCommandStatus, pUser: this, pHelp: "List all commands which are accessible for users");
862
863 // DDRace
864
865 m_Cheated = false;
866}
867
868CConsole::~CConsole()
869{
870 CCommand *pCommand = m_pFirstCommand;
871 while(pCommand)
872 {
873 CCommand *pNext = pCommand->Next();
874 {
875 FCommandCallback pfnCallback = pCommand->m_pfnCallback;
876 void *pUserData = pCommand->m_pUserData;
877 CChain *pChain = nullptr;
878 while(pfnCallback == Con_Chain)
879 {
880 pChain = static_cast<CChain *>(pUserData);
881 pfnCallback = pChain->m_pfnCallback;
882 pUserData = pChain->m_pCallbackUserData;
883 delete pChain;
884 }
885 }
886 // Temp commands are on m_TempCommands heap, so don't delete them
887 if(!pCommand->m_Temp)
888 delete pCommand;
889 pCommand = pNext;
890 }
891}
892
893void CConsole::Init()
894{
895 m_pStorage = Kernel()->RequestInterface<IStorage>();
896}
897
898void CConsole::ParseArguments(int NumArgs, const char **ppArguments)
899{
900 for(int i = 0; i < NumArgs; i++)
901 {
902 // check for scripts to execute
903 if(ppArguments[i][0] == '-' && ppArguments[i][1] == 'f' && ppArguments[i][2] == 0)
904 {
905 if(NumArgs - i > 1)
906 ExecuteFile(pFilename: ppArguments[i + 1], ClientId: IConsole::CLIENT_ID_UNSPECIFIED, LogFailure: true, StorageType: IStorage::TYPE_ABSOLUTE);
907 i++;
908 }
909 else if(!str_comp(a: "-s", b: ppArguments[i]) || !str_comp(a: "--silent", b: ppArguments[i]))
910 {
911 // skip silent param
912 continue;
913 }
914 else
915 {
916 // search arguments for overrides
917 ExecuteLine(pStr: ppArguments[i]);
918 }
919 }
920}
921
922void CConsole::AddCommandSorted(CCommand *pCommand)
923{
924 if(!m_pFirstCommand || str_comp(a: pCommand->m_pName, b: m_pFirstCommand->m_pName) <= 0)
925 {
926 if(m_pFirstCommand && m_pFirstCommand->Next())
927 pCommand->SetNext(m_pFirstCommand);
928 else
929 pCommand->SetNext(nullptr);
930 m_pFirstCommand = pCommand;
931 }
932 else
933 {
934 for(CCommand *p = m_pFirstCommand; p; p = p->Next())
935 {
936 if(!p->Next() || str_comp(a: pCommand->m_pName, b: p->Next()->m_pName) <= 0)
937 {
938 pCommand->SetNext(p->Next());
939 p->SetNext(pCommand);
940 break;
941 }
942 }
943 }
944}
945
946void CConsole::Register(const char *pName, const char *pParams,
947 int Flags, FCommandCallback pfnFunc, void *pUser, const char *pHelp)
948{
949 CCommand *pCommand = FindCommand(pName, FlagMask: Flags);
950 bool DoAdd = false;
951 if(pCommand == nullptr)
952 {
953 pCommand = new CCommand();
954 DoAdd = true;
955 }
956 pCommand->m_pfnCallback = pfnFunc;
957 pCommand->m_pUserData = pUser;
958
959 pCommand->m_pName = pName;
960 pCommand->m_pHelp = pHelp;
961 pCommand->m_pParams = pParams;
962
963 pCommand->m_Flags = Flags;
964 pCommand->m_Temp = false;
965
966 if(DoAdd)
967 AddCommandSorted(pCommand);
968
969 if(pCommand->m_Flags & CFGFLAG_CHAT)
970 pCommand->SetAccessLevel(EAccessLevel::USER);
971}
972
973void CConsole::RegisterTemp(const char *pName, const char *pParams, int Flags, const char *pHelp)
974{
975 CCommand *pCommand;
976 if(m_pRecycleList)
977 {
978 pCommand = m_pRecycleList;
979 str_copy(dst: const_cast<char *>(pCommand->m_pName), src: pName, dst_size: TEMPCMD_NAME_LENGTH);
980 str_copy(dst: const_cast<char *>(pCommand->m_pHelp), src: pHelp, dst_size: TEMPCMD_HELP_LENGTH);
981 str_copy(dst: const_cast<char *>(pCommand->m_pParams), src: pParams, dst_size: TEMPCMD_PARAMS_LENGTH);
982
983 m_pRecycleList = m_pRecycleList->Next();
984 }
985 else
986 {
987 pCommand = new(m_TempCommands.Allocate(Size: sizeof(CCommand))) CCommand;
988 char *pMem = static_cast<char *>(m_TempCommands.Allocate(Size: TEMPCMD_NAME_LENGTH));
989 str_copy(dst: pMem, src: pName, dst_size: TEMPCMD_NAME_LENGTH);
990 pCommand->m_pName = pMem;
991 pMem = static_cast<char *>(m_TempCommands.Allocate(Size: TEMPCMD_HELP_LENGTH));
992 str_copy(dst: pMem, src: pHelp, dst_size: TEMPCMD_HELP_LENGTH);
993 pCommand->m_pHelp = pMem;
994 pMem = static_cast<char *>(m_TempCommands.Allocate(Size: TEMPCMD_PARAMS_LENGTH));
995 str_copy(dst: pMem, src: pParams, dst_size: TEMPCMD_PARAMS_LENGTH);
996 pCommand->m_pParams = pMem;
997 }
998
999 pCommand->m_pfnCallback = nullptr;
1000 pCommand->m_pUserData = nullptr;
1001 pCommand->m_Flags = Flags;
1002 pCommand->m_Temp = true;
1003
1004 AddCommandSorted(pCommand);
1005}
1006
1007void CConsole::DeregisterTemp(const char *pName)
1008{
1009 if(!m_pFirstCommand)
1010 return;
1011
1012 CCommand *pRemoved = nullptr;
1013
1014 // remove temp entry from command list
1015 if(m_pFirstCommand->m_Temp && str_comp(a: m_pFirstCommand->m_pName, b: pName) == 0)
1016 {
1017 pRemoved = m_pFirstCommand;
1018 m_pFirstCommand = m_pFirstCommand->Next();
1019 }
1020 else
1021 {
1022 for(CCommand *pCommand = m_pFirstCommand; pCommand->Next(); pCommand = pCommand->Next())
1023 if(pCommand->Next()->m_Temp && str_comp(a: pCommand->Next()->m_pName, b: pName) == 0)
1024 {
1025 pRemoved = pCommand->Next();
1026 pCommand->SetNext(pCommand->Next()->Next());
1027 break;
1028 }
1029 }
1030
1031 // add to recycle list
1032 if(pRemoved)
1033 {
1034 pRemoved->SetNext(m_pRecycleList);
1035 m_pRecycleList = pRemoved;
1036 }
1037}
1038
1039void CConsole::DeregisterTempAll()
1040{
1041 // set non temp as first one
1042 for(; m_pFirstCommand && m_pFirstCommand->m_Temp; m_pFirstCommand = m_pFirstCommand->Next())
1043 ;
1044
1045 // remove temp entries from command list
1046 for(CCommand *pCommand = m_pFirstCommand; pCommand && pCommand->Next(); pCommand = pCommand->Next())
1047 {
1048 CCommand *pNext = pCommand->Next();
1049 if(pNext->m_Temp)
1050 {
1051 for(; pNext && pNext->m_Temp; pNext = pNext->Next())
1052 ;
1053 pCommand->SetNext(pNext);
1054 }
1055 }
1056
1057 m_TempCommands.Reset();
1058 m_pRecycleList = nullptr;
1059}
1060
1061void CConsole::Con_Chain(IResult *pResult, void *pUserData)
1062{
1063 CChain *pInfo = (CChain *)pUserData;
1064 pInfo->m_pfnChainCallback(pResult, pInfo->m_pUserData, pInfo->m_pfnCallback, pInfo->m_pCallbackUserData);
1065}
1066
1067void CConsole::Chain(const char *pName, FChainCommandCallback pfnChainFunc, void *pUser)
1068{
1069 CCommand *pCommand = FindCommand(pName, FlagMask: m_FlagMask);
1070
1071 if(!pCommand)
1072 {
1073 char aBuf[256];
1074 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "failed to chain '%s'", pName);
1075 Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "console", pStr: aBuf);
1076 return;
1077 }
1078
1079 CChain *pChainInfo = new CChain();
1080
1081 // store info
1082 pChainInfo->m_pfnChainCallback = pfnChainFunc;
1083 pChainInfo->m_pUserData = pUser;
1084 pChainInfo->m_pfnCallback = pCommand->m_pfnCallback;
1085 pChainInfo->m_pCallbackUserData = pCommand->m_pUserData;
1086
1087 // chain
1088 pCommand->m_pfnCallback = Con_Chain;
1089 pCommand->m_pUserData = pChainInfo;
1090}
1091
1092void CConsole::StoreCommands(bool Store)
1093{
1094 if(!Store)
1095 {
1096 for(CExecutionQueueEntry &Entry : m_vExecutionQueue)
1097 {
1098 Entry.m_pCommand->m_pfnCallback(&Entry.m_Result, Entry.m_pCommand->m_pUserData);
1099 }
1100 m_vExecutionQueue.clear();
1101 }
1102 m_StoreCommands = Store;
1103}
1104
1105const IConsole::ICommandInfo *CConsole::GetCommandInfo(const char *pName, int FlagMask, bool Temp)
1106{
1107 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
1108 {
1109 if(pCommand->m_Flags & FlagMask && pCommand->m_Temp == Temp)
1110 {
1111 if(str_comp_nocase(a: pCommand->Name(), b: pName) == 0)
1112 return pCommand;
1113 }
1114 }
1115
1116 return nullptr;
1117}
1118
1119std::unique_ptr<IConsole> CreateConsole(int FlagMask) { return std::make_unique<CConsole>(args&: FlagMask); }
1120
1121int CConsole::CResult::GetVictim() const
1122{
1123 return m_Victim;
1124}
1125
1126void CConsole::CResult::ResetVictim()
1127{
1128 m_Victim = VICTIM_NONE;
1129}
1130
1131bool CConsole::CResult::HasVictim() const
1132{
1133 return m_Victim != VICTIM_NONE;
1134}
1135
1136void CConsole::CResult::SetVictim(int Victim)
1137{
1138 m_Victim = std::clamp<int>(val: Victim, lo: VICTIM_NONE, hi: MAX_CLIENTS - 1);
1139}
1140
1141void CConsole::CResult::SetVictim(const char *pVictim)
1142{
1143 if(!str_comp(a: pVictim, b: "me"))
1144 m_Victim = VICTIM_ME;
1145 else if(!str_comp(a: pVictim, b: "all"))
1146 m_Victim = VICTIM_ALL;
1147 else
1148 m_Victim = std::clamp<int>(val: str_toint(str: pVictim), lo: 0, hi: MAX_CLIENTS - 1);
1149}
1150
1151std::optional<ColorHSLA> CConsole::ColorParse(const char *pStr, float DarkestLighting)
1152{
1153 if(str_isallnum(str: pStr) || ((pStr[0] == '-' || pStr[0] == '+') && str_isallnum(str: pStr + 1))) // Teeworlds Color (Packed HSL)
1154 {
1155 unsigned long Value = str_toulong_base(str: pStr, base: 10);
1156 if(Value == std::numeric_limits<unsigned long>::max())
1157 return std::nullopt;
1158 return ColorHSLA(Value, true).UnclampLighting(Darkest: DarkestLighting);
1159 }
1160 else if(*pStr == '$') // Hex RGB/RGBA
1161 {
1162 auto ParsedColor = color_parse<ColorRGBA>(pStr: pStr + 1);
1163 if(ParsedColor)
1164 return color_cast<ColorHSLA>(rgb: ParsedColor.value());
1165 else
1166 return std::nullopt;
1167 }
1168 else if(!str_comp_nocase(a: pStr, b: "red"))
1169 return ColorHSLA(0.0f / 6.0f, 1.0f, 0.5f);
1170 else if(!str_comp_nocase(a: pStr, b: "yellow"))
1171 return ColorHSLA(1.0f / 6.0f, 1.0f, 0.5f);
1172 else if(!str_comp_nocase(a: pStr, b: "green"))
1173 return ColorHSLA(2.0f / 6.0f, 1.0f, 0.5f);
1174 else if(!str_comp_nocase(a: pStr, b: "cyan"))
1175 return ColorHSLA(3.0f / 6.0f, 1.0f, 0.5f);
1176 else if(!str_comp_nocase(a: pStr, b: "blue"))
1177 return ColorHSLA(4.0f / 6.0f, 1.0f, 0.5f);
1178 else if(!str_comp_nocase(a: pStr, b: "magenta"))
1179 return ColorHSLA(5.0f / 6.0f, 1.0f, 0.5f);
1180 else if(!str_comp_nocase(a: pStr, b: "white"))
1181 return ColorHSLA(0.0f, 0.0f, 1.0f);
1182 else if(!str_comp_nocase(a: pStr, b: "gray"))
1183 return ColorHSLA(0.0f, 0.0f, 0.5f);
1184 else if(!str_comp_nocase(a: pStr, b: "black"))
1185 return ColorHSLA(0.0f, 0.0f, 0.0f);
1186
1187 return std::nullopt;
1188}
1189