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("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::SetGetVictimsCommandCallback(FGetVictimsCommandCallback pfnCallback, void *pUser)
380{
381 m_pfnGetVictimsCommandCallback = pfnCallback;
382 m_pGetVictimsCommandUserData = pUser;
383}
384
385void CConsole::SetTeeHistorianCommandCallback(FTeeHistorianCommandCallback pfnCallback, void *pUser)
386{
387 m_pfnTeeHistorianCommandCallback = pfnCallback;
388 m_pTeeHistorianCommandUserdata = pUser;
389}
390
391void CConsole::SetUnknownCommandCallback(FUnknownCommandCallback pfnCallback, void *pUser)
392{
393 m_pfnUnknownCommandCallback = pfnCallback;
394 m_pUnknownCommandUserdata = pUser;
395}
396
397void CConsole::SetCanUseCommandCallback(FCanUseCommandCallback pfnCallback, void *pUser)
398{
399 m_pfnCanUseCommandCallback = pfnCallback;
400 m_pCanUseCommandUserData = pUser;
401}
402
403void CConsole::InitChecksum(CChecksumData *pData) const
404{
405 pData->m_NumCommands = 0;
406 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
407 {
408 if(pData->m_NumCommands < (int)(std::size(pData->m_aCommandsChecksum)))
409 {
410 FCommandCallback pfnCallback = pCommand->m_pfnCallback;
411 void *pUserData = pCommand->m_pUserData;
412 TraverseChain(ppfnCallback: &pfnCallback, ppUserData: &pUserData);
413 int CallbackBits = (uintptr_t)pfnCallback & 0xfff;
414 int *pTarget = &pData->m_aCommandsChecksum[pData->m_NumCommands];
415 *pTarget = ((uint8_t)pCommand->m_pName[0]) | ((uint8_t)pCommand->m_pName[1] << 8) | (CallbackBits << 16);
416 }
417 pData->m_NumCommands += 1;
418 }
419}
420
421bool CConsole::LineIsValid(const char *pStr)
422{
423 if(!pStr || *pStr == 0)
424 return false;
425
426 do
427 {
428 CResult Result(IConsole::CLIENT_ID_UNSPECIFIED);
429 const char *pEnd = pStr;
430 const char *pNextPart = nullptr;
431 bool InString = false;
432 bool IsEscaping = false;
433
434 while(*pEnd)
435 {
436 if(IsEscaping)
437 {
438 IsEscaping = false;
439 }
440 else if(*pEnd == '"')
441 {
442 InString = !InString;
443 }
444 else if(InString && *pEnd == '\\') // escape sequences
445 {
446 IsEscaping = true;
447 }
448
449 if(!InString)
450 {
451 if(*pEnd == ';') // command separator
452 {
453 pNextPart = pEnd + 1;
454 break;
455 }
456 else if(*pEnd == '#') // comment, no need to do anything more
457 {
458 break;
459 }
460 }
461
462 pEnd++;
463 }
464
465 if(ParseStart(pResult: &Result, pString: pStr, Length: (pEnd - pStr) + 1) != 0)
466 return false;
467
468 CCommand *pCommand = FindCommand(pName: Result.m_pCommand, FlagMask: m_FlagMask);
469 if(!pCommand || ParseArgs(pResult: &Result, pFormat: pCommand->m_pParams))
470 return false;
471
472 pStr = pNextPart;
473 } while(pStr && *pStr);
474
475 return true;
476}
477
478void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientId, bool InterpretSemicolons)
479{
480 const char *pWithoutPrefix = str_startswith(str: pStr, prefix: "mc;");
481 if(pWithoutPrefix)
482 {
483 InterpretSemicolons = true;
484 pStr = pWithoutPrefix;
485 }
486 while(pStr && *pStr)
487 {
488 CResult Result(ClientId);
489 const char *pEnd = pStr;
490 const char *pNextPart = nullptr;
491 bool InString = false;
492 bool IsEscaping = false;
493
494 while(*pEnd)
495 {
496 if(IsEscaping)
497 {
498 IsEscaping = false;
499 }
500 else if(*pEnd == '"')
501 {
502 InString = !InString;
503 }
504 else if(InString && *pEnd == '\\') // escape sequences
505 {
506 IsEscaping = true;
507 }
508
509 if(!InString && InterpretSemicolons)
510 {
511 if(*pEnd == ';') // command separator
512 {
513 pNextPart = pEnd + 1;
514 break;
515 }
516 else if(*pEnd == '#') // comment, no need to do anything more
517 {
518 break;
519 }
520 }
521
522 pEnd++;
523 }
524
525 if(ParseStart(pResult: &Result, pString: pStr, Length: (pEnd - pStr) + 1) != 0)
526 return;
527
528 if(!*Result.m_pCommand)
529 {
530 if(pNextPart)
531 {
532 pStr = pNextPart;
533 continue;
534 }
535 return;
536 }
537
538 CCommand *pCommand;
539 if(ClientId == IConsole::CLIENT_ID_GAME)
540 pCommand = FindCommand(pName: Result.m_pCommand, FlagMask: m_FlagMask | CFGFLAG_GAME);
541 else
542 pCommand = FindCommand(pName: Result.m_pCommand, FlagMask: m_FlagMask);
543
544 if(pCommand)
545 {
546 if(ClientId == IConsole::CLIENT_ID_GAME && !(pCommand->m_Flags & CFGFLAG_GAME))
547 {
548 if(Stroke)
549 {
550 char aBuf[CMDLINE_LENGTH + 64];
551 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Command '%s' cannot be executed from a map.", Result.m_pCommand);
552 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
553 }
554 }
555 else if(ClientId == IConsole::CLIENT_ID_NO_GAME && pCommand->m_Flags & CFGFLAG_GAME)
556 {
557 if(Stroke)
558 {
559 char aBuf[CMDLINE_LENGTH + 64];
560 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Command '%s' cannot be executed from a non-map config file.", Result.m_pCommand);
561 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
562 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);
563 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
564 }
565 }
566 else if(CanUseCommand(ClientId: Result.m_ClientId, pCommand))
567 {
568 int IsStrokeCommand = 0;
569 if(Result.m_pCommand[0] == '+')
570 {
571 // insert the stroke direction token
572 Result.AddArgument(pArg: m_apStrokeStr[Stroke]);
573 IsStrokeCommand = 1;
574 }
575
576 if(Stroke || IsStrokeCommand)
577 {
578 if(int Error = ParseArgs(pResult: &Result, pFormat: pCommand->m_pParams))
579 {
580 char aBuf[CMDLINE_LENGTH + 64];
581 if(Error == PARSEARGS_INVALID_INTEGER)
582 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s is not a valid integer.", Result.GetString(Index: Result.NumArguments() - 1));
583 else if(Error == PARSEARGS_INVALID_COLOR)
584 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s is not a valid color.", Result.GetString(Index: Result.NumArguments() - 1));
585 else if(Error == PARSEARGS_INVALID_FLOAT)
586 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s is not a valid decimal number.", Result.GetString(Index: Result.NumArguments() - 1));
587 else
588 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Invalid arguments. Usage: %s %s", pCommand->m_pName, pCommand->m_pParams);
589 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
590 }
591 else if(m_StoreCommands && pCommand->m_Flags & CFGFLAG_STORE)
592 {
593 m_vExecutionQueue.emplace_back(args&: pCommand, args&: Result);
594 }
595 else
596 {
597 if(pCommand->m_Flags & CMDFLAG_TEST && !g_Config.m_SvTestingCommands)
598 {
599 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: "Test commands aren't allowed, enable them with 'sv_test_cmds 1' in your initial config.");
600 return;
601 }
602
603 if(m_pfnTeeHistorianCommandCallback && !(pCommand->m_Flags & CFGFLAG_NONTEEHISTORIC))
604 {
605 m_pfnTeeHistorianCommandCallback(ClientId, m_FlagMask, pCommand->m_pName, &Result, m_pTeeHistorianCommandUserdata);
606 }
607
608 if(Result.m_aSpecialVictim[0])
609 {
610 std::optional<std::vector<int>> Victims;
611 if(m_pfnGetVictimsCommandCallback)
612 {
613 Victims = m_pfnGetVictimsCommandCallback(ClientId, Result.m_aSpecialVictim, m_pGetVictimsCommandUserData);
614 }
615 else
616 {
617 Victims = std::nullopt;
618 }
619
620 if(!Victims.has_value())
621 {
622 log_error("console", "Invalid victim '%s'", Result.m_aSpecialVictim);
623 return;
624 }
625 for(const int VictimId : Victims.value())
626 {
627 Result.SetVictim(VictimId);
628 pCommand->m_pfnCallback(&Result, pCommand->m_pUserData);
629 }
630 }
631 else
632 {
633 pCommand->m_pfnCallback(&Result, pCommand->m_pUserData);
634 }
635
636 if(pCommand->m_Flags & CMDFLAG_TEST)
637 m_Cheated = true;
638 }
639 }
640 }
641 else if(Stroke)
642 {
643 char aBuf[CMDLINE_LENGTH + 32];
644 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Access for command %s denied.", Result.m_pCommand);
645 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
646 }
647 }
648 else if(Stroke)
649 {
650 // Pass the original string to the unknown command callback instead of the parsed command, as the latter
651 // ends at the first whitespace, which breaks for unknown commands (filenames) containing spaces.
652 if(!m_pfnUnknownCommandCallback(pStr, m_pUnknownCommandUserdata))
653 {
654 char aBuf[CMDLINE_LENGTH + 32];
655 if(m_FlagMask & CFGFLAG_CHAT)
656 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such command: %s. Use /cmdlist for a list of all commands.", Result.m_pCommand);
657 else
658 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such command: %s.", Result.m_pCommand);
659 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
660 }
661 }
662
663 pStr = pNextPart;
664 }
665}
666
667bool CConsole::CanUseCommand(int ClientId, const IConsole::ICommandInfo *pCommand) const
668{
669 // the fallback is needed for the client and rust tests
670 if(!m_pfnCanUseCommandCallback)
671 return true;
672 return m_pfnCanUseCommandCallback(ClientId, pCommand, m_pCanUseCommandUserData);
673}
674
675int CConsole::PossibleCommands(const char *pStr, int FlagMask, bool Temp, FPossibleCallback pfnCallback, void *pUser)
676{
677 int Index = 0;
678 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
679 {
680 if(pCommand->m_Flags & FlagMask && pCommand->m_Temp == Temp)
681 {
682 if(str_find_nocase(haystack: pCommand->m_pName, needle: pStr))
683 {
684 pfnCallback(Index, pCommand->m_pName, pUser);
685 Index++;
686 }
687 }
688 }
689 return Index;
690}
691
692CConsole::CCommand *CConsole::FindCommand(const char *pName, int FlagMask)
693{
694 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
695 {
696 if(pCommand->m_Flags & FlagMask)
697 {
698 if(str_comp_nocase(a: pCommand->m_pName, b: pName) == 0)
699 return pCommand;
700 }
701 }
702
703 return nullptr;
704}
705
706void CConsole::ExecuteLine(const char *pStr, int ClientId, bool InterpretSemicolons)
707{
708 CConsole::ExecuteLineStroked(Stroke: 1, pStr, ClientId, InterpretSemicolons); // press it
709 CConsole::ExecuteLineStroked(Stroke: 0, pStr, ClientId, InterpretSemicolons); // then release it
710}
711
712void CConsole::ExecuteLineFlag(const char *pStr, int FlagMask, int ClientId, bool InterpretSemicolons)
713{
714 int Temp = m_FlagMask;
715 m_FlagMask = FlagMask;
716 ExecuteLine(pStr, ClientId, InterpretSemicolons);
717 m_FlagMask = Temp;
718}
719
720bool CConsole::ExecuteFile(const char *pFilename, int ClientId, bool LogFailure, int StorageType)
721{
722 int Count = 0;
723 // make sure that this isn't being executed already and that recursion limit isn't met
724 for(CExecFile *pCur = m_pFirstExec; pCur; pCur = pCur->m_pPrev)
725 {
726 Count++;
727
728 if(str_comp(a: pFilename, b: pCur->m_pFilename) == 0 || Count > FILE_RECURSION_LIMIT)
729 return false;
730 }
731 if(!m_pStorage)
732 return false;
733
734 // push this one to the stack
735 CExecFile ThisFile;
736 CExecFile *pPrev = m_pFirstExec;
737 ThisFile.m_pFilename = pFilename;
738 ThisFile.m_pPrev = m_pFirstExec;
739 m_pFirstExec = &ThisFile;
740
741 // exec the file
742 CLineReader LineReader;
743 bool Success = false;
744 char aBuf[32 + IO_MAX_PATH_LENGTH];
745 if(LineReader.OpenFile(File: m_pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: StorageType)))
746 {
747 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "executing '%s'", pFilename);
748 Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
749
750 while(const char *pLine = LineReader.Get())
751 {
752 ExecuteLine(pStr: pLine, ClientId);
753 }
754
755 Success = true;
756 }
757 else if(LogFailure)
758 {
759 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "failed to open '%s'", pFilename);
760 Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
761 }
762
763 m_pFirstExec = pPrev;
764 return Success;
765}
766
767void CConsole::Con_Echo(IResult *pResult, void *pUserData)
768{
769 ((CConsole *)pUserData)->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: pResult->GetString(Index: 0));
770}
771
772void CConsole::Con_Exec(IResult *pResult, void *pUserData)
773{
774 ((CConsole *)pUserData)->ExecuteFile(pFilename: pResult->GetString(Index: 0), ClientId: pResult->m_ClientId, LogFailure: true, StorageType: IStorage::TYPE_ALL);
775}
776
777void CConsole::ConCommandAccess(IResult *pResult, void *pUser)
778{
779 CConsole *pConsole = static_cast<CConsole *>(pUser);
780 char aBuf[CMDLINE_LENGTH + 64];
781 CCommand *pCommand = pConsole->FindCommand(pName: pResult->GetString(Index: 0), FlagMask: CFGFLAG_SERVER);
782 if(pCommand)
783 {
784 if(pResult->NumArguments() == 2)
785 {
786 std::optional<EAccessLevel> AccessLevel = AccessLevelToEnum(pAccessLevel: pResult->GetString(Index: 1));
787 if(!AccessLevel.has_value())
788 {
789 log_error("console", "Invalid access level '%s'. Allowed values are admin, moderator, helper and all.", pResult->GetString(1));
790 return;
791 }
792 pCommand->SetAccessLevel(AccessLevel.value());
793 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");
794 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
795 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");
796 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
797 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");
798 }
799 else
800 {
801 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");
802 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
803 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");
804 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
805 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");
806 }
807 }
808 else
809 {
810 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "No such command: '%s'.", pResult->GetString(Index: 0));
811 }
812
813 pConsole->Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "console", pStr: aBuf);
814}
815
816void CConsole::PrintCommandList(EAccessLevel MinAccessLevel, int ExcludeFlagMask)
817{
818 char aBuf[240] = "";
819 int Used = 0;
820
821 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
822 {
823 if((pCommand->m_Flags & m_FlagMask) &&
824 !(pCommand->m_Flags & ExcludeFlagMask) &&
825 pCommand->GetAccessLevel() >= MinAccessLevel)
826 {
827 int Length = str_length(str: pCommand->m_pName);
828 if(Used + Length + 2 < (int)(sizeof(aBuf)))
829 {
830 if(Used > 0)
831 {
832 Used += 2;
833 str_append(dst&: aBuf, src: ", ");
834 }
835 str_append(dst&: aBuf, src: pCommand->m_pName);
836 Used += Length;
837 }
838 else
839 {
840 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
841 str_copy(dst&: aBuf, src: pCommand->m_pName);
842 Used = Length;
843 }
844 }
845 }
846 if(Used > 0)
847 Print(Level: OUTPUT_LEVEL_STANDARD, pFrom: "chatresp", pStr: aBuf);
848}
849
850void CConsole::ConCommandStatus(IResult *pResult, void *pUser)
851{
852 CConsole *pConsole = static_cast<CConsole *>(pUser);
853 std::optional<EAccessLevel> AccessLevel = AccessLevelToEnum(pAccessLevel: pResult->GetString(Index: 0));
854 if(!AccessLevel.has_value())
855 {
856 log_error("console", "Invalid access level '%s'. Allowed values are admin, moderator, helper and all.", pResult->GetString(0));
857 return;
858 }
859 pConsole->PrintCommandList(MinAccessLevel: AccessLevel.value(), ExcludeFlagMask: 0);
860}
861
862void CConsole::ConUserCommandStatus(IResult *pResult, void *pUser)
863{
864 CConsole *pConsole = static_cast<CConsole *>(pUser);
865 pConsole->PrintCommandList(MinAccessLevel: EAccessLevel::USER, ExcludeFlagMask: CMDFLAG_PRACTICE);
866}
867
868void CConsole::TraverseChain(FCommandCallback *ppfnCallback, void **ppUserData)
869{
870 while(*ppfnCallback == Con_Chain)
871 {
872 CChain *pChainInfo = static_cast<CChain *>(*ppUserData);
873 *ppfnCallback = pChainInfo->m_pfnCallback;
874 *ppUserData = pChainInfo->m_pCallbackUserData;
875 }
876}
877
878CConsole::CConsole(int FlagMask)
879{
880 m_FlagMask = FlagMask;
881 m_pRecycleList = nullptr;
882 m_TempCommands.Reset();
883 m_StoreCommands = true;
884 m_apStrokeStr[0] = "0";
885 m_apStrokeStr[1] = "1";
886 m_pFirstCommand = nullptr;
887 m_pFirstExec = nullptr;
888 m_pfnTeeHistorianCommandCallback = nullptr;
889 m_pTeeHistorianCommandUserdata = nullptr;
890 m_pfnGetVictimsCommandCallback = nullptr;
891 m_pGetVictimsCommandUserData = nullptr;
892
893 m_pStorage = nullptr;
894
895 // register some basic commands
896 Register(pName: "echo", pParams: "r[text]", Flags: CFGFLAG_SERVER, pfnFunc: Con_Echo, pUser: this, pHelp: "Echo the text");
897 Register(pName: "exec", pParams: "r[file]", Flags: CFGFLAG_SERVER | CFGFLAG_CLIENT, pfnFunc: Con_Exec, pUser: this, pHelp: "Execute the specified file");
898
899 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");
900 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");
901 Register(pName: "cmdlist", pParams: "", Flags: CFGFLAG_SERVER | CFGFLAG_CHAT, pfnFunc: ConUserCommandStatus, pUser: this, pHelp: "List all commands which are accessible for users");
902
903 // DDRace
904
905 m_Cheated = false;
906}
907
908CConsole::~CConsole()
909{
910 CCommand *pCommand = m_pFirstCommand;
911 while(pCommand)
912 {
913 CCommand *pNext = pCommand->Next();
914 {
915 FCommandCallback pfnCallback = pCommand->m_pfnCallback;
916 void *pUserData = pCommand->m_pUserData;
917 CChain *pChain = nullptr;
918 while(pfnCallback == Con_Chain)
919 {
920 pChain = static_cast<CChain *>(pUserData);
921 pfnCallback = pChain->m_pfnCallback;
922 pUserData = pChain->m_pCallbackUserData;
923 delete pChain;
924 }
925 }
926 // Temp commands are on m_TempCommands heap, so don't delete them
927 if(!pCommand->m_Temp)
928 delete pCommand;
929 pCommand = pNext;
930 }
931}
932
933void CConsole::Init()
934{
935 m_pStorage = Kernel()->RequestInterface<IStorage>();
936}
937
938void CConsole::ParseArguments(int NumArgs, const char **ppArguments)
939{
940 for(int i = 0; i < NumArgs; i++)
941 {
942 // check for scripts to execute
943 if(ppArguments[i][0] == '-' && ppArguments[i][1] == 'f' && ppArguments[i][2] == 0)
944 {
945 if(NumArgs - i > 1)
946 ExecuteFile(pFilename: ppArguments[i + 1], ClientId: IConsole::CLIENT_ID_UNSPECIFIED, LogFailure: true, StorageType: IStorage::TYPE_ABSOLUTE);
947 i++;
948 }
949 else if(!str_comp(a: "-s", b: ppArguments[i]) || !str_comp(a: "--silent", b: ppArguments[i]))
950 {
951 // skip silent param
952 continue;
953 }
954 else
955 {
956 // search arguments for overrides
957 ExecuteLine(pStr: ppArguments[i]);
958 }
959 }
960}
961
962void CConsole::AddCommandSorted(CCommand *pCommand)
963{
964 if(!m_pFirstCommand || str_comp(a: pCommand->m_pName, b: m_pFirstCommand->m_pName) <= 0)
965 {
966 if(m_pFirstCommand && m_pFirstCommand->Next())
967 pCommand->SetNext(m_pFirstCommand);
968 else
969 pCommand->SetNext(nullptr);
970 m_pFirstCommand = pCommand;
971 }
972 else
973 {
974 for(CCommand *p = m_pFirstCommand; p; p = p->Next())
975 {
976 if(!p->Next() || str_comp(a: pCommand->m_pName, b: p->Next()->m_pName) <= 0)
977 {
978 pCommand->SetNext(p->Next());
979 p->SetNext(pCommand);
980 break;
981 }
982 }
983 }
984}
985
986void CConsole::Register(const char *pName, const char *pParams,
987 int Flags, FCommandCallback pfnFunc, void *pUser, const char *pHelp)
988{
989 CCommand *pCommand = FindCommand(pName, FlagMask: Flags);
990 bool DoAdd = false;
991 if(pCommand == nullptr)
992 {
993 pCommand = new CCommand();
994 DoAdd = true;
995 }
996 pCommand->m_pfnCallback = pfnFunc;
997 pCommand->m_pUserData = pUser;
998
999 pCommand->m_pName = pName;
1000 pCommand->m_pHelp = pHelp;
1001 pCommand->m_pParams = pParams;
1002
1003 pCommand->m_Flags = Flags;
1004 pCommand->m_Temp = false;
1005
1006 if(DoAdd)
1007 AddCommandSorted(pCommand);
1008
1009 if(pCommand->m_Flags & CFGFLAG_CHAT)
1010 pCommand->SetAccessLevel(EAccessLevel::USER);
1011}
1012
1013void CConsole::RegisterTemp(const char *pName, const char *pParams, int Flags, const char *pHelp)
1014{
1015 CCommand *pCommand;
1016 if(m_pRecycleList)
1017 {
1018 pCommand = m_pRecycleList;
1019 str_copy(dst: const_cast<char *>(pCommand->m_pName), src: pName, dst_size: TEMPCMD_NAME_LENGTH);
1020 str_copy(dst: const_cast<char *>(pCommand->m_pHelp), src: pHelp, dst_size: TEMPCMD_HELP_LENGTH);
1021 str_copy(dst: const_cast<char *>(pCommand->m_pParams), src: pParams, dst_size: TEMPCMD_PARAMS_LENGTH);
1022
1023 m_pRecycleList = m_pRecycleList->Next();
1024 }
1025 else
1026 {
1027 pCommand = new(m_TempCommands.Allocate(Size: sizeof(CCommand))) CCommand;
1028 char *pMem = static_cast<char *>(m_TempCommands.Allocate(Size: TEMPCMD_NAME_LENGTH));
1029 str_copy(dst: pMem, src: pName, dst_size: TEMPCMD_NAME_LENGTH);
1030 pCommand->m_pName = pMem;
1031 pMem = static_cast<char *>(m_TempCommands.Allocate(Size: TEMPCMD_HELP_LENGTH));
1032 str_copy(dst: pMem, src: pHelp, dst_size: TEMPCMD_HELP_LENGTH);
1033 pCommand->m_pHelp = pMem;
1034 pMem = static_cast<char *>(m_TempCommands.Allocate(Size: TEMPCMD_PARAMS_LENGTH));
1035 str_copy(dst: pMem, src: pParams, dst_size: TEMPCMD_PARAMS_LENGTH);
1036 pCommand->m_pParams = pMem;
1037 }
1038
1039 pCommand->m_pfnCallback = nullptr;
1040 pCommand->m_pUserData = nullptr;
1041 pCommand->m_Flags = Flags;
1042 pCommand->m_Temp = true;
1043
1044 AddCommandSorted(pCommand);
1045}
1046
1047void CConsole::DeregisterTemp(const char *pName)
1048{
1049 if(!m_pFirstCommand)
1050 return;
1051
1052 CCommand *pRemoved = nullptr;
1053
1054 // remove temp entry from command list
1055 if(m_pFirstCommand->m_Temp && str_comp(a: m_pFirstCommand->m_pName, b: pName) == 0)
1056 {
1057 pRemoved = m_pFirstCommand;
1058 m_pFirstCommand = m_pFirstCommand->Next();
1059 }
1060 else
1061 {
1062 for(CCommand *pCommand = m_pFirstCommand; pCommand->Next(); pCommand = pCommand->Next())
1063 if(pCommand->Next()->m_Temp && str_comp(a: pCommand->Next()->m_pName, b: pName) == 0)
1064 {
1065 pRemoved = pCommand->Next();
1066 pCommand->SetNext(pCommand->Next()->Next());
1067 break;
1068 }
1069 }
1070
1071 // add to recycle list
1072 if(pRemoved)
1073 {
1074 pRemoved->SetNext(m_pRecycleList);
1075 m_pRecycleList = pRemoved;
1076 }
1077}
1078
1079void CConsole::DeregisterTempAll()
1080{
1081 // set non temp as first one
1082 for(; m_pFirstCommand && m_pFirstCommand->m_Temp; m_pFirstCommand = m_pFirstCommand->Next())
1083 ;
1084
1085 // remove temp entries from command list
1086 for(CCommand *pCommand = m_pFirstCommand; pCommand && pCommand->Next(); pCommand = pCommand->Next())
1087 {
1088 CCommand *pNext = pCommand->Next();
1089 if(pNext->m_Temp)
1090 {
1091 for(; pNext && pNext->m_Temp; pNext = pNext->Next())
1092 ;
1093 pCommand->SetNext(pNext);
1094 }
1095 }
1096
1097 m_TempCommands.Reset();
1098 m_pRecycleList = nullptr;
1099}
1100
1101void CConsole::Con_Chain(IResult *pResult, void *pUserData)
1102{
1103 CChain *pInfo = (CChain *)pUserData;
1104 pInfo->m_pfnChainCallback(pResult, pInfo->m_pUserData, pInfo->m_pfnCallback, pInfo->m_pCallbackUserData);
1105}
1106
1107void CConsole::Chain(const char *pName, FChainCommandCallback pfnChainFunc, void *pUser)
1108{
1109 CCommand *pCommand = FindCommand(pName, FlagMask: m_FlagMask);
1110
1111 if(!pCommand)
1112 {
1113 char aBuf[256];
1114 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "failed to chain '%s'", pName);
1115 Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "console", pStr: aBuf);
1116 return;
1117 }
1118
1119 CChain *pChainInfo = new CChain();
1120
1121 // store info
1122 pChainInfo->m_pfnChainCallback = pfnChainFunc;
1123 pChainInfo->m_pUserData = pUser;
1124 pChainInfo->m_pfnCallback = pCommand->m_pfnCallback;
1125 pChainInfo->m_pCallbackUserData = pCommand->m_pUserData;
1126
1127 // chain
1128 pCommand->m_pfnCallback = Con_Chain;
1129 pCommand->m_pUserData = pChainInfo;
1130}
1131
1132void CConsole::StoreCommands(bool Store)
1133{
1134 if(!Store)
1135 {
1136 for(CExecutionQueueEntry &Entry : m_vExecutionQueue)
1137 {
1138 Entry.m_pCommand->m_pfnCallback(&Entry.m_Result, Entry.m_pCommand->m_pUserData);
1139 }
1140 m_vExecutionQueue.clear();
1141 }
1142 m_StoreCommands = Store;
1143}
1144
1145const IConsole::ICommandInfo *CConsole::GetCommandInfo(const char *pName, int FlagMask, bool Temp)
1146{
1147 for(CCommand *pCommand = m_pFirstCommand; pCommand; pCommand = pCommand->Next())
1148 {
1149 if(pCommand->m_Flags & FlagMask && pCommand->m_Temp == Temp)
1150 {
1151 if(str_comp_nocase(a: pCommand->Name(), b: pName) == 0)
1152 return pCommand;
1153 }
1154 }
1155
1156 return nullptr;
1157}
1158
1159std::unique_ptr<IConsole> CreateConsole(int FlagMask) { return std::make_unique<CConsole>(args&: FlagMask); }
1160
1161int CConsole::CResult::GetVictim() const
1162{
1163 dbg_assert(m_VictimId.has_value(), "m_VictimId has no value");
1164 return m_VictimId.value();
1165}
1166
1167void CConsole::CResult::ResetVictim()
1168{
1169 m_VictimId = std::nullopt;
1170 m_aSpecialVictim[0] = '\0';
1171}
1172
1173void CConsole::CResult::SetVictim(int Victim)
1174{
1175 dbg_assert(in_range(Victim, 0, MAX_CLIENTS - 1), "Victim ID %d out of range [0, %d]", Victim, MAX_CLIENTS - 1);
1176 m_VictimId = Victim;
1177}
1178
1179void CConsole::CResult::SetVictim(const char *pVictim)
1180{
1181 int Value;
1182 if(!str_toint(str: pVictim, out: &Value) || !in_range(a: Value, lower: 0, upper: MAX_CLIENTS - 1))
1183 {
1184 str_copy(dst&: m_aSpecialVictim, src: pVictim);
1185 return;
1186 }
1187
1188 SetVictim(Value);
1189}
1190
1191std::optional<ColorHSLA> CConsole::ColorParse(const char *pStr, float DarkestLighting)
1192{
1193 if(str_isallnum(str: pStr) || ((pStr[0] == '-' || pStr[0] == '+') && str_isallnum(str: pStr + 1))) // Teeworlds Color (Packed HSL)
1194 {
1195 unsigned long Value = str_toulong_base(str: pStr, base: 10);
1196 if(Value == std::numeric_limits<unsigned long>::max())
1197 return std::nullopt;
1198 return ColorHSLA(Value, true).UnclampLighting(Darkest: DarkestLighting);
1199 }
1200 else if(*pStr == '$') // Hex RGB/RGBA
1201 {
1202 auto ParsedColor = color_parse<ColorRGBA>(pStr: pStr + 1);
1203 if(ParsedColor)
1204 return color_cast<ColorHSLA>(rgb: ParsedColor.value());
1205 else
1206 return std::nullopt;
1207 }
1208 else if(!str_comp_nocase(a: pStr, b: "red"))
1209 return ColorHSLA(0.0f / 6.0f, 1.0f, 0.5f);
1210 else if(!str_comp_nocase(a: pStr, b: "yellow"))
1211 return ColorHSLA(1.0f / 6.0f, 1.0f, 0.5f);
1212 else if(!str_comp_nocase(a: pStr, b: "green"))
1213 return ColorHSLA(2.0f / 6.0f, 1.0f, 0.5f);
1214 else if(!str_comp_nocase(a: pStr, b: "cyan"))
1215 return ColorHSLA(3.0f / 6.0f, 1.0f, 0.5f);
1216 else if(!str_comp_nocase(a: pStr, b: "blue"))
1217 return ColorHSLA(4.0f / 6.0f, 1.0f, 0.5f);
1218 else if(!str_comp_nocase(a: pStr, b: "magenta"))
1219 return ColorHSLA(5.0f / 6.0f, 1.0f, 0.5f);
1220 else if(!str_comp_nocase(a: pStr, b: "white"))
1221 return ColorHSLA(0.0f, 0.0f, 1.0f);
1222 else if(!str_comp_nocase(a: pStr, b: "gray"))
1223 return ColorHSLA(0.0f, 0.0f, 0.5f);
1224 else if(!str_comp_nocase(a: pStr, b: "black"))
1225 return ColorHSLA(0.0f, 0.0f, 0.0f);
1226
1227 return std::nullopt;
1228}
1229