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