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