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