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