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