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/lock.h>
5#include <base/logger.h>
6#include <base/math.h>
7#include <base/system.h>
8
9#include <game/generated/client_data.h>
10
11#include <engine/console.h>
12#include <engine/engine.h>
13#include <engine/graphics.h>
14#include <engine/keys.h>
15#include <engine/shared/config.h>
16#include <engine/shared/ringbuffer.h>
17#include <engine/storage.h>
18#include <engine/textrender.h>
19
20#include <game/localization.h>
21#include <game/version.h>
22
23#include <game/client/gameclient.h>
24#include <game/client/render.h>
25#include <game/client/ui.h>
26
27#include <iterator>
28
29#include "console.h"
30
31static constexpr float FONT_SIZE = 10.0f;
32static constexpr float LINE_SPACING = 1.0f;
33
34class CConsoleLogger : public ILogger
35{
36 CGameConsole *m_pConsole;
37 CLock m_ConsoleMutex;
38
39public:
40 CConsoleLogger(CGameConsole *pConsole) :
41 m_pConsole(pConsole)
42 {
43 dbg_assert(pConsole != nullptr, "console pointer must not be null");
44 }
45
46 void Log(const CLogMessage *pMessage) override REQUIRES(!m_ConsoleMutex);
47 void OnConsoleDeletion() REQUIRES(!m_ConsoleMutex);
48};
49
50void CConsoleLogger::Log(const CLogMessage *pMessage)
51{
52 if(m_Filter.Filters(pMessage))
53 {
54 return;
55 }
56 ColorRGBA Color = gs_ConsoleDefaultColor;
57 if(pMessage->m_HaveColor)
58 {
59 Color.r = pMessage->m_Color.r / 255.0;
60 Color.g = pMessage->m_Color.g / 255.0;
61 Color.b = pMessage->m_Color.b / 255.0;
62 }
63 const CLockScope LockScope(m_ConsoleMutex);
64 if(m_pConsole)
65 {
66 m_pConsole->m_LocalConsole.PrintLine(pLine: pMessage->m_aLine, Len: pMessage->m_LineLength, PrintColor: Color);
67 }
68}
69
70void CConsoleLogger::OnConsoleDeletion()
71{
72 const CLockScope LockScope(m_ConsoleMutex);
73 m_pConsole = nullptr;
74}
75
76enum class EArgumentCompletionType
77{
78 NONE,
79 TUNE,
80 SETTING,
81 KEY,
82};
83
84class CArgumentCompletionEntry
85{
86public:
87 EArgumentCompletionType m_Type;
88 const char *m_pCommandName;
89 int m_ArgumentIndex;
90};
91
92static const CArgumentCompletionEntry gs_aArgumentCompletionEntries[] = {
93 {.m_Type: EArgumentCompletionType::TUNE, .m_pCommandName: "tune", .m_ArgumentIndex: 0},
94 {.m_Type: EArgumentCompletionType::TUNE, .m_pCommandName: "tune_reset", .m_ArgumentIndex: 0},
95 {.m_Type: EArgumentCompletionType::TUNE, .m_pCommandName: "toggle_tune", .m_ArgumentIndex: 0},
96 {.m_Type: EArgumentCompletionType::TUNE, .m_pCommandName: "tune_zone", .m_ArgumentIndex: 1},
97 {.m_Type: EArgumentCompletionType::SETTING, .m_pCommandName: "reset", .m_ArgumentIndex: 0},
98 {.m_Type: EArgumentCompletionType::SETTING, .m_pCommandName: "toggle", .m_ArgumentIndex: 0},
99 {.m_Type: EArgumentCompletionType::SETTING, .m_pCommandName: "access_level", .m_ArgumentIndex: 0},
100 {.m_Type: EArgumentCompletionType::SETTING, .m_pCommandName: "+toggle", .m_ArgumentIndex: 0},
101 {.m_Type: EArgumentCompletionType::KEY, .m_pCommandName: "bind", .m_ArgumentIndex: 0},
102 {.m_Type: EArgumentCompletionType::KEY, .m_pCommandName: "binds", .m_ArgumentIndex: 0},
103 {.m_Type: EArgumentCompletionType::KEY, .m_pCommandName: "unbind", .m_ArgumentIndex: 0},
104};
105
106static std::pair<EArgumentCompletionType, int> ArgumentCompletion(const char *pStr)
107{
108 const char *pCommandStart = pStr;
109 const char *pIt = pStr;
110 pIt = str_skip_to_whitespace_const(str: pIt);
111 int CommandLength = pIt - pCommandStart;
112 const char *pCommandEnd = pIt;
113
114 if(!CommandLength)
115 return {EArgumentCompletionType::NONE, -1};
116
117 pIt = str_skip_whitespaces_const(str: pIt);
118 if(pIt == pCommandEnd)
119 return {EArgumentCompletionType::NONE, -1};
120
121 for(const auto &Entry : gs_aArgumentCompletionEntries)
122 {
123 int Length = maximum(a: str_length(str: Entry.m_pCommandName), b: CommandLength);
124 if(str_comp_nocase_num(a: Entry.m_pCommandName, b: pCommandStart, num: Length) == 0)
125 {
126 int CurrentArg = 0;
127 const char *pArgStart = nullptr, *pArgEnd = nullptr;
128 while(CurrentArg < Entry.m_ArgumentIndex)
129 {
130 pArgStart = pIt;
131 pIt = str_skip_to_whitespace_const(str: pIt); // Skip argument value
132 pArgEnd = pIt;
133
134 if(!pIt[0] || pArgStart == pIt) // Check that argument is not empty
135 return {EArgumentCompletionType::NONE, -1};
136
137 pIt = str_skip_whitespaces_const(str: pIt); // Go to next argument position
138 CurrentArg++;
139 }
140 if(pIt == pArgEnd)
141 return {EArgumentCompletionType::NONE, -1}; // Check that there is at least one space after
142 return {Entry.m_Type, pIt - pStr};
143 }
144 }
145 return {EArgumentCompletionType::NONE, -1};
146}
147
148static int PossibleTunings(const char *pStr, IConsole::FPossibleCallback pfnCallback = IConsole::EmptyPossibleCommandCallback, void *pUser = nullptr)
149{
150 int Index = 0;
151 for(int i = 0; i < CTuningParams::Num(); i++)
152 {
153 if(str_find_nocase(haystack: CTuningParams::Name(Index: i), needle: pStr))
154 {
155 pfnCallback(Index, CTuningParams::Name(Index: i), pUser);
156 Index++;
157 }
158 }
159 return Index;
160}
161
162static int PossibleKeys(const char *pStr, IInput *pInput, IConsole::FPossibleCallback pfnCallback = IConsole::EmptyPossibleCommandCallback, void *pUser = nullptr)
163{
164 int Index = 0;
165 for(int Key = KEY_A; Key < KEY_JOY_AXIS_11_RIGHT; Key++)
166 {
167 // Ignore unnamed keys starting with '&'
168 const char *pKeyName = pInput->KeyName(Key);
169 if(pKeyName[0] != '&' && str_find_nocase(haystack: pKeyName, needle: pStr))
170 {
171 pfnCallback(Index, pKeyName, pUser);
172 Index++;
173 }
174 }
175 return Index;
176}
177
178const ColorRGBA CGameConsole::ms_SearchHighlightColor = ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f);
179const ColorRGBA CGameConsole::ms_SearchSelectedColor = ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f);
180
181CGameConsole::CInstance::CInstance(int Type)
182{
183 m_pHistoryEntry = 0x0;
184
185 m_Type = Type;
186
187 if(Type == CGameConsole::CONSOLETYPE_LOCAL)
188 {
189 m_pName = "local_console";
190 m_CompletionFlagmask = CFGFLAG_CLIENT;
191 }
192 else
193 {
194 m_pName = "remote_console";
195 m_CompletionFlagmask = CFGFLAG_SERVER;
196 }
197
198 m_aCompletionBuffer[0] = 0;
199 m_CompletionChosen = -1;
200 m_aCompletionBufferArgument[0] = 0;
201 m_CompletionChosenArgument = -1;
202 m_CompletionArgumentPosition = 0;
203 Reset();
204
205 m_aUser[0] = '\0';
206 m_UserGot = false;
207 m_UsernameReq = false;
208
209 m_IsCommand = false;
210
211 m_Backlog.SetPopCallback([this](CBacklogEntry *pEntry) {
212 if(pEntry->m_LineCount != -1)
213 {
214 m_NewLineCounter -= pEntry->m_LineCount;
215 }
216 });
217
218 m_Input.SetClipboardLineCallback([this](const char *pStr) { ExecuteLine(pLine: pStr); });
219
220 m_CurrentMatchIndex = -1;
221 m_aCurrentSearchString[0] = '\0';
222}
223
224void CGameConsole::CInstance::Init(CGameConsole *pGameConsole)
225{
226 m_pGameConsole = pGameConsole;
227}
228
229void CGameConsole::CInstance::ClearBacklog()
230{
231 {
232 // We must ensure that no log messages are printed while owning
233 // m_BacklogPendingLock or this will result in a dead lock.
234 const CLockScope LockScope(m_BacklogPendingLock);
235 m_BacklogPending.Init();
236 }
237
238 m_Backlog.Init();
239 m_BacklogCurLine = 0;
240 UpdateSearch();
241}
242
243void CGameConsole::CInstance::UpdateBacklogTextAttributes()
244{
245 // Pending backlog entries are not handled because they don't have text attributes yet.
246 for(CBacklogEntry *pEntry = m_Backlog.First(); pEntry; pEntry = m_Backlog.Next(pCurrent: pEntry))
247 {
248 UpdateEntryTextAttributes(pEntry);
249 }
250}
251
252void CGameConsole::CInstance::PumpBacklogPending()
253{
254 {
255 // We must ensure that no log messages are printed while owning
256 // m_BacklogPendingLock or this will result in a dead lock.
257 const CLockScope LockScopePending(m_BacklogPendingLock);
258 for(CBacklogEntry *pPendingEntry = m_BacklogPending.First(); pPendingEntry; pPendingEntry = m_BacklogPending.Next(pCurrent: pPendingEntry))
259 {
260 const size_t EntrySize = sizeof(CBacklogEntry) + pPendingEntry->m_Length;
261 CBacklogEntry *pEntry = m_Backlog.Allocate(Size: EntrySize);
262 mem_copy(dest: pEntry, source: pPendingEntry, size: EntrySize);
263 }
264
265 m_BacklogPending.Init();
266 }
267
268 // Update text attributes and count number of added lines
269 m_pGameConsole->Ui()->MapScreen();
270 for(CBacklogEntry *pEntry = m_Backlog.First(); pEntry; pEntry = m_Backlog.Next(pCurrent: pEntry))
271 {
272 if(pEntry->m_LineCount == -1)
273 {
274 UpdateEntryTextAttributes(pEntry);
275 m_NewLineCounter += pEntry->m_LineCount;
276 }
277 }
278}
279
280void CGameConsole::CInstance::ClearHistory()
281{
282 m_History.Init();
283 m_pHistoryEntry = 0;
284}
285
286void CGameConsole::CInstance::Reset()
287{
288 m_CompletionRenderOffset = 0.0f;
289 m_CompletionRenderOffsetChange = 0.0f;
290 m_pCommandName = "";
291 m_pCommandHelp = "";
292 m_pCommandParams = "";
293 m_CompletionArgumentPosition = 0;
294}
295
296void CGameConsole::CInstance::ExecuteLine(const char *pLine)
297{
298 if(m_Type == CGameConsole::CONSOLETYPE_LOCAL)
299 m_pGameConsole->m_pConsole->ExecuteLine(pStr: pLine);
300 else
301 {
302 if(m_pGameConsole->Client()->RconAuthed())
303 m_pGameConsole->Client()->Rcon(pLine);
304 else
305 {
306 if(!m_UserGot && m_UsernameReq)
307 {
308 m_UserGot = true;
309 str_copy(dst&: m_aUser, src: pLine);
310 }
311 else
312 {
313 m_pGameConsole->Client()->RconAuth(pUsername: m_aUser, pPassword: pLine);
314 m_UserGot = false;
315 }
316 }
317 }
318}
319
320void CGameConsole::CInstance::PossibleCommandsCompleteCallback(int Index, const char *pStr, void *pUser)
321{
322 CGameConsole::CInstance *pInstance = (CGameConsole::CInstance *)pUser;
323 if(pInstance->m_CompletionChosen == Index)
324 pInstance->m_Input.Set(pStr);
325}
326
327static void StrCopyUntilSpace(char *pDest, size_t DestSize, const char *pSrc)
328{
329 const char *pSpace = str_find(haystack: pSrc, needle: " ");
330 str_copy(dst: pDest, src: pSrc, dst_size: minimum<size_t>(a: pSpace ? pSpace - pSrc + 1 : 1, b: DestSize));
331}
332
333void CGameConsole::CInstance::PossibleArgumentsCompleteCallback(int Index, const char *pStr, void *pUser)
334{
335 CGameConsole::CInstance *pInstance = (CGameConsole::CInstance *)pUser;
336 if(pInstance->m_CompletionChosenArgument == Index)
337 {
338 // get command
339 char aBuf[IConsole::CMDLINE_LENGTH];
340 str_copy(dst: aBuf, src: pInstance->GetString(), dst_size: pInstance->m_CompletionArgumentPosition);
341 str_append(dst&: aBuf, src: " ");
342
343 // append argument
344 str_append(dst&: aBuf, src: pStr);
345 pInstance->m_Input.Set(aBuf);
346 }
347}
348
349bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event)
350{
351 bool Handled = false;
352
353 auto &&SelectNextSearchMatch = [&](int Direction) {
354 if(!m_vSearchMatches.empty())
355 {
356 m_CurrentMatchIndex += Direction;
357 if(m_CurrentMatchIndex >= (int)m_vSearchMatches.size())
358 m_CurrentMatchIndex = 0;
359 if(m_CurrentMatchIndex < 0)
360 m_CurrentMatchIndex = (int)m_vSearchMatches.size() - 1;
361 m_HasSelection = false;
362 // Also scroll to the correct line
363 ScrollToCenter(StartLine: m_vSearchMatches[m_CurrentMatchIndex].m_StartLine, EndLine: m_vSearchMatches[m_CurrentMatchIndex].m_EndLine);
364 }
365 };
366
367 const int BacklogPrevLine = m_BacklogCurLine;
368 if(Event.m_Flags & IInput::FLAG_PRESS)
369 {
370 if(Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER)
371 {
372 if(!m_Searching)
373 {
374 if(!m_Input.IsEmpty() || (m_UsernameReq && !m_pGameConsole->Client()->RconAuthed() && !m_UserGot))
375 {
376 if(m_Type == CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed())
377 {
378 const char *pPrevEntry = m_History.Last();
379 if(pPrevEntry == nullptr || str_comp(a: pPrevEntry, b: m_Input.GetString()) != 0)
380 {
381 char *pEntry = m_History.Allocate(Size: m_Input.GetLength() + 1);
382 str_copy(dst: pEntry, src: m_Input.GetString(), dst_size: m_Input.GetLength() + 1);
383 }
384 // print out the user's commands before they get run
385 char aBuf[IConsole::CMDLINE_LENGTH + 3];
386 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "> %s", m_Input.GetString());
387 m_pGameConsole->PrintLine(Type: m_Type, pLine: aBuf);
388 }
389 ExecuteLine(pLine: m_Input.GetString());
390 m_Input.Clear();
391 m_pHistoryEntry = 0x0;
392 }
393 }
394 else
395 {
396 SelectNextSearchMatch(m_pGameConsole->m_pClient->Input()->ShiftIsPressed() ? -1 : 1);
397 }
398
399 Handled = true;
400 }
401 else if(Event.m_Key == KEY_UP)
402 {
403 if(m_Searching)
404 {
405 SelectNextSearchMatch(-1);
406 }
407 else if(m_Type == CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed())
408 {
409 if(m_pHistoryEntry)
410 {
411 char *pTest = m_History.Prev(pCurrent: m_pHistoryEntry);
412
413 if(pTest)
414 m_pHistoryEntry = pTest;
415 }
416 else
417 m_pHistoryEntry = m_History.Last();
418
419 if(m_pHistoryEntry)
420 m_Input.Set(m_pHistoryEntry);
421 }
422 Handled = true;
423 }
424 else if(Event.m_Key == KEY_DOWN)
425 {
426 if(m_Searching)
427 {
428 SelectNextSearchMatch(1);
429 }
430 else if(m_Type == CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed())
431 {
432 if(m_pHistoryEntry)
433 m_pHistoryEntry = m_History.Next(pCurrent: m_pHistoryEntry);
434
435 if(m_pHistoryEntry)
436 m_Input.Set(m_pHistoryEntry);
437 else
438 m_Input.Clear();
439 }
440 Handled = true;
441 }
442 else if(Event.m_Key == KEY_TAB)
443 {
444 const int Direction = m_pGameConsole->m_pClient->Input()->ShiftIsPressed() ? -1 : 1;
445
446 if(!m_Searching)
447 {
448 // command completion
449 const bool UseTempCommands = m_Type == CGameConsole::CONSOLETYPE_REMOTE && m_pGameConsole->Client()->RconAuthed() && m_pGameConsole->Client()->UseTempRconCommands();
450 int CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(pStr: m_aCompletionBuffer, FlagMask: m_CompletionFlagmask, Temp: UseTempCommands);
451 if(m_Type == CGameConsole::CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed())
452 {
453 if(CompletionEnumerationCount)
454 {
455 if(m_CompletionChosen == -1 && Direction < 0)
456 m_CompletionChosen = 0;
457 m_CompletionChosen = (m_CompletionChosen + Direction + CompletionEnumerationCount) % CompletionEnumerationCount;
458 m_CompletionArgumentPosition = 0;
459 m_pGameConsole->m_pConsole->PossibleCommands(pStr: m_aCompletionBuffer, FlagMask: m_CompletionFlagmask, Temp: UseTempCommands, pfnCallback: PossibleCommandsCompleteCallback, pUser: this);
460 }
461 else if(m_CompletionChosen != -1)
462 {
463 m_CompletionChosen = -1;
464 Reset();
465 }
466 }
467
468 // Argument completion
469 const auto [CompletionType, CompletionPos] = ArgumentCompletion(pStr: GetString());
470 if(CompletionType == EArgumentCompletionType::TUNE)
471 CompletionEnumerationCount = PossibleTunings(pStr: m_aCompletionBufferArgument);
472 else if(CompletionType == EArgumentCompletionType::SETTING)
473 CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(pStr: m_aCompletionBufferArgument, FlagMask: m_CompletionFlagmask, Temp: UseTempCommands);
474 else if(CompletionType == EArgumentCompletionType::KEY)
475 CompletionEnumerationCount = PossibleKeys(pStr: m_aCompletionBufferArgument, pInput: m_pGameConsole->Input());
476
477 if(CompletionEnumerationCount)
478 {
479 if(m_CompletionChosenArgument == -1 && Direction < 0)
480 m_CompletionChosenArgument = 0;
481 m_CompletionChosenArgument = (m_CompletionChosenArgument + Direction + CompletionEnumerationCount) % CompletionEnumerationCount;
482 m_CompletionArgumentPosition = CompletionPos;
483 if(CompletionType == EArgumentCompletionType::TUNE)
484 PossibleTunings(pStr: m_aCompletionBufferArgument, pfnCallback: PossibleArgumentsCompleteCallback, pUser: this);
485 else if(CompletionType == EArgumentCompletionType::SETTING)
486 m_pGameConsole->m_pConsole->PossibleCommands(pStr: m_aCompletionBufferArgument, FlagMask: m_CompletionFlagmask, Temp: UseTempCommands, pfnCallback: PossibleArgumentsCompleteCallback, pUser: this);
487 else if(CompletionType == EArgumentCompletionType::KEY)
488 PossibleKeys(pStr: m_aCompletionBufferArgument, pInput: m_pGameConsole->Input(), pfnCallback: PossibleArgumentsCompleteCallback, pUser: this);
489 }
490 else if(m_CompletionChosenArgument != -1)
491 {
492 m_CompletionChosenArgument = -1;
493 Reset();
494 }
495 }
496 else
497 {
498 // Use Tab / Shift-Tab to cycle through search matches
499 SelectNextSearchMatch(Direction);
500 }
501 }
502 else if(Event.m_Key == KEY_PAGEUP)
503 {
504 m_BacklogCurLine += GetLinesToScroll(Direction: -1, LinesToScroll: m_LinesRendered);
505 }
506 else if(Event.m_Key == KEY_PAGEDOWN)
507 {
508 m_BacklogCurLine -= GetLinesToScroll(Direction: 1, LinesToScroll: m_LinesRendered);
509 if(m_BacklogCurLine < 0)
510 m_BacklogCurLine = 0;
511 }
512 else if(Event.m_Key == KEY_MOUSE_WHEEL_UP)
513 {
514 m_BacklogCurLine += GetLinesToScroll(Direction: -1, LinesToScroll: 1);
515 }
516 else if(Event.m_Key == KEY_MOUSE_WHEEL_DOWN)
517 {
518 --m_BacklogCurLine;
519 if(m_BacklogCurLine < 0)
520 m_BacklogCurLine = 0;
521 }
522 // in order not to conflict with CLineInput's handling of Home/End only
523 // react to it when the input is empty
524 else if(Event.m_Key == KEY_HOME && m_Input.IsEmpty())
525 {
526 m_BacklogCurLine += GetLinesToScroll(Direction: -1, LinesToScroll: -1);
527 m_BacklogLastActiveLine = m_BacklogCurLine;
528 }
529 else if(Event.m_Key == KEY_END && m_Input.IsEmpty())
530 {
531 m_BacklogCurLine = 0;
532 }
533 else if(Event.m_Key == KEY_F && m_pGameConsole->Input()->ModifierIsPressed() && Event.m_Flags & IInput::FLAG_PRESS)
534 {
535 m_Searching = !m_Searching;
536 ClearSearch();
537
538 Handled = true;
539 }
540 }
541
542 if(m_BacklogCurLine != BacklogPrevLine)
543 {
544 m_HasSelection = false;
545 }
546
547 if(!Handled)
548 {
549 Handled = m_Input.ProcessInput(Event);
550 if(Handled)
551 UpdateSearch();
552 }
553
554 if(Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_TEXT))
555 {
556 if(Event.m_Key != KEY_TAB && Event.m_Key != KEY_LSHIFT && Event.m_Key != KEY_RSHIFT)
557 {
558 const char *pInputStr = m_Input.GetString();
559
560 m_CompletionChosen = -1;
561 str_copy(dst&: m_aCompletionBuffer, src: pInputStr);
562
563 const auto [CompletionType, CompletionPos] = ArgumentCompletion(pStr: GetString());
564 if(CompletionType != EArgumentCompletionType::NONE)
565 {
566 for(const auto &Entry : gs_aArgumentCompletionEntries)
567 {
568 if(Entry.m_Type != CompletionType)
569 continue;
570 const int Len = str_length(str: Entry.m_pCommandName);
571 if(str_comp_nocase_num(a: pInputStr, b: Entry.m_pCommandName, num: Len) == 0 && str_isspace(c: pInputStr[Len]))
572 {
573 m_CompletionChosenArgument = -1;
574 str_copy(dst&: m_aCompletionBufferArgument, src: &pInputStr[CompletionPos]);
575 }
576 }
577 }
578
579 Reset();
580 }
581
582 // find the current command
583 {
584 char aBuf[IConsole::CMDLINE_LENGTH];
585 StrCopyUntilSpace(pDest: aBuf, DestSize: sizeof(aBuf), pSrc: GetString());
586 const IConsole::CCommandInfo *pCommand = m_pGameConsole->m_pConsole->GetCommandInfo(pName: aBuf, FlagMask: m_CompletionFlagmask,
587 Temp: m_Type != CGameConsole::CONSOLETYPE_LOCAL && m_pGameConsole->Client()->RconAuthed() && m_pGameConsole->Client()->UseTempRconCommands());
588 if(pCommand)
589 {
590 m_IsCommand = true;
591 m_pCommandName = pCommand->m_pName;
592 m_pCommandHelp = pCommand->m_pHelp;
593 m_pCommandParams = pCommand->m_pParams;
594 }
595 else
596 m_IsCommand = false;
597 }
598 }
599
600 return Handled;
601}
602
603void CGameConsole::CInstance::PrintLine(const char *pLine, int Len, ColorRGBA PrintColor)
604{
605 // We must ensure that no log messages are printed while owning
606 // m_BacklogPendingLock or this will result in a dead lock.
607 const CLockScope LockScope(m_BacklogPendingLock);
608 CBacklogEntry *pEntry = m_BacklogPending.Allocate(Size: sizeof(CBacklogEntry) + Len);
609 pEntry->m_YOffset = -1.0f;
610 pEntry->m_PrintColor = PrintColor;
611 pEntry->m_Length = Len;
612 pEntry->m_LineCount = -1;
613 str_copy(dst: pEntry->m_aText, src: pLine, dst_size: Len + 1);
614}
615
616int CGameConsole::CInstance::GetLinesToScroll(int Direction, int LinesToScroll)
617{
618 auto *pEntry = m_Backlog.Last();
619 int Line = 0;
620 int LinesToSkip = (Direction == -1 ? m_BacklogCurLine + m_LinesRendered : m_BacklogCurLine - 1);
621 while(Line < LinesToSkip && pEntry)
622 {
623 if(pEntry->m_LineCount == -1)
624 UpdateEntryTextAttributes(pEntry);
625 Line += pEntry->m_LineCount;
626 pEntry = m_Backlog.Prev(pCurrent: pEntry);
627 }
628
629 int Amount = maximum(a: 0, b: Line - LinesToSkip);
630 while(pEntry && (LinesToScroll > 0 ? Amount < LinesToScroll : true))
631 {
632 if(pEntry->m_LineCount == -1)
633 UpdateEntryTextAttributes(pEntry);
634 Amount += pEntry->m_LineCount;
635 pEntry = Direction == -1 ? m_Backlog.Prev(pCurrent: pEntry) : m_Backlog.Next(pCurrent: pEntry);
636 }
637
638 return LinesToScroll > 0 ? minimum(a: Amount, b: LinesToScroll) : Amount;
639}
640
641void CGameConsole::CInstance::ScrollToCenter(int StartLine, int EndLine)
642{
643 // This method is used to scroll lines from `StartLine` to `EndLine` to the center of the screen, if possible.
644
645 // Find target line
646 int Target = maximum(a: 0, b: (int)ceil(x: StartLine - minimum(a: StartLine - EndLine, b: m_LinesRendered) / 2) - m_LinesRendered / 2);
647 if(m_BacklogCurLine == Target)
648 return;
649
650 // Compute acutal amount of lines to scroll to make sure lines fit in viewport and we don't have empty space
651 int Direction = m_BacklogCurLine - Target < 0 ? -1 : 1;
652 int LinesToScroll = absolute(a: Target - m_BacklogCurLine);
653 int ComputedLines = GetLinesToScroll(Direction, LinesToScroll);
654
655 if(Direction == -1)
656 m_BacklogCurLine += ComputedLines;
657 else
658 m_BacklogCurLine -= ComputedLines;
659}
660
661void CGameConsole::CInstance::UpdateEntryTextAttributes(CBacklogEntry *pEntry) const
662{
663 CTextCursor Cursor;
664 m_pGameConsole->TextRender()->SetCursor(pCursor: &Cursor, x: 0.0f, y: 0.0f, FontSize: FONT_SIZE, Flags: 0);
665 Cursor.m_LineWidth = m_pGameConsole->Ui()->Screen()->w - 10;
666 Cursor.m_MaxLines = 10;
667 Cursor.m_LineSpacing = LINE_SPACING;
668 m_pGameConsole->TextRender()->TextEx(pCursor: &Cursor, pText: pEntry->m_aText, Length: -1);
669 pEntry->m_YOffset = Cursor.Height();
670 pEntry->m_LineCount = Cursor.m_LineCount;
671}
672
673void CGameConsole::CInstance::ClearSearch()
674{
675 m_vSearchMatches.clear();
676 m_CurrentMatchIndex = -1;
677 m_Input.Clear();
678 m_aCurrentSearchString[0] = '\0';
679}
680
681void CGameConsole::CInstance::UpdateSearch()
682{
683 if(!m_Searching)
684 return;
685
686 const char *pSearchText = m_Input.GetString();
687 bool SearchChanged = str_utf8_comp_nocase(a: pSearchText, b: m_aCurrentSearchString) != 0;
688
689 int SearchLength = m_Input.GetLength();
690 str_copy(dst&: m_aCurrentSearchString, src: pSearchText);
691
692 m_vSearchMatches.clear();
693 if(pSearchText[0] == '\0')
694 {
695 m_CurrentMatchIndex = -1;
696 return;
697 }
698
699 if(SearchChanged)
700 {
701 m_CurrentMatchIndex = -1;
702 m_HasSelection = false;
703 }
704
705 ITextRender *pTextRender = m_pGameConsole->Ui()->TextRender();
706 const int LineWidth = m_pGameConsole->Ui()->Screen()->w - 10.0f;
707
708 CBacklogEntry *pEntry = m_Backlog.Last();
709 int EntryLine = 0, LineToScrollStart = 0, LineToScrollEnd = 0;
710
711 for(; pEntry; EntryLine += pEntry->m_LineCount, pEntry = m_Backlog.Prev(pCurrent: pEntry))
712 {
713 const char *pSearchPos = str_utf8_find_nocase(haystack: pEntry->m_aText, needle: pSearchText);
714 if(!pSearchPos)
715 continue;
716
717 int EntryLineCount = pEntry->m_LineCount;
718
719 // Find all occurences of the search string and save their positions
720 while(pSearchPos)
721 {
722 int Pos = pSearchPos - pEntry->m_aText;
723
724 if(EntryLineCount == 1)
725 {
726 m_vSearchMatches.emplace_back(args&: Pos, args&: EntryLine, args&: EntryLine, args&: EntryLine);
727 if(EntryLine > LineToScrollStart)
728 {
729 LineToScrollStart = EntryLine;
730 LineToScrollEnd = EntryLine;
731 }
732 }
733 else
734 {
735 // A match can span multiple lines in case of a multiline entry, so we need to know which line the match starts at
736 // and which line it ends at in order to put it in viewport properly
737 STextSizeProperties Props;
738 int LineCount;
739 Props.m_pLineCount = &LineCount;
740
741 // Compute line of end match
742 pTextRender->TextWidth(Size: FONT_SIZE, pText: pEntry->m_aText, StrLength: Pos + SearchLength, LineWidth, Flags: 0, TextSizeProps: Props);
743 int EndLine = (EntryLineCount - LineCount);
744 int MatchEndLine = EntryLine + EndLine;
745
746 // Compute line of start of match
747 int MatchStartLine = MatchEndLine;
748 if(LineCount > 1)
749 {
750 pTextRender->TextWidth(Size: FONT_SIZE, pText: pEntry->m_aText, StrLength: Pos, LineWidth, Flags: 0, TextSizeProps: Props);
751 int StartLine = (EntryLineCount - LineCount);
752 MatchStartLine = EntryLine + StartLine;
753 }
754
755 if(MatchStartLine > LineToScrollStart)
756 {
757 LineToScrollStart = MatchStartLine;
758 LineToScrollEnd = MatchEndLine;
759 }
760
761 m_vSearchMatches.emplace_back(args&: Pos, args&: MatchStartLine, args&: MatchEndLine, args&: EntryLine);
762 }
763
764 pSearchPos = str_utf8_find_nocase(haystack: pEntry->m_aText + Pos + SearchLength, needle: pSearchText);
765 }
766 }
767
768 if(!m_vSearchMatches.empty() && SearchChanged)
769 m_CurrentMatchIndex = 0;
770 else
771 m_CurrentMatchIndex = std::clamp(val: m_CurrentMatchIndex, lo: -1, hi: (int)m_vSearchMatches.size() - 1);
772
773 // Reverse order of lines by sorting so we have matches from top to bottom instead of bottom to top
774 std::sort(first: m_vSearchMatches.begin(), last: m_vSearchMatches.end(), comp: [](const SSearchMatch &MatchA, const SSearchMatch &MatchB) {
775 if(MatchA.m_StartLine == MatchB.m_StartLine)
776 return MatchA.m_Pos < MatchB.m_Pos; // Make sure to keep position order
777 return MatchA.m_StartLine > MatchB.m_StartLine;
778 });
779
780 if(!m_vSearchMatches.empty() && SearchChanged)
781 {
782 ScrollToCenter(StartLine: LineToScrollStart, EndLine: LineToScrollEnd);
783 }
784}
785
786void CGameConsole::CInstance::Dump()
787{
788 char aTimestamp[20];
789 str_timestamp(buffer: aTimestamp, buffer_size: sizeof(aTimestamp));
790 char aFilename[IO_MAX_PATH_LENGTH];
791 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "dumps/%s_dump_%s.txt", m_pName, aTimestamp);
792 IOHANDLE File = m_pGameConsole->Storage()->OpenFile(pFilename: aFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
793 if(File)
794 {
795 PumpBacklogPending();
796 for(CInstance::CBacklogEntry *pEntry = m_Backlog.First(); pEntry; pEntry = m_Backlog.Next(pCurrent: pEntry))
797 {
798 io_write(io: File, buffer: pEntry->m_aText, size: pEntry->m_Length);
799 io_write_newline(io: File);
800 }
801 io_close(io: File);
802 log_info("console", "%s contents were written to '%s'", m_pName, aFilename);
803 }
804 else
805 {
806 log_error("console", "Failed to open '%s'", aFilename);
807 }
808}
809
810CGameConsole::CGameConsole() :
811 m_LocalConsole(CONSOLETYPE_LOCAL), m_RemoteConsole(CONSOLETYPE_REMOTE)
812{
813 m_ConsoleType = CONSOLETYPE_LOCAL;
814 m_ConsoleState = CONSOLE_CLOSED;
815 m_StateChangeEnd = 0.0f;
816 m_StateChangeDuration = 0.1f;
817
818 m_pConsoleLogger = new CConsoleLogger(this);
819}
820
821CGameConsole::~CGameConsole()
822{
823 if(m_pConsoleLogger)
824 m_pConsoleLogger->OnConsoleDeletion();
825}
826
827CGameConsole::CInstance *CGameConsole::CurrentConsole()
828{
829 if(m_ConsoleType == CONSOLETYPE_REMOTE)
830 return &m_RemoteConsole;
831 return &m_LocalConsole;
832}
833
834void CGameConsole::OnReset()
835{
836 m_RemoteConsole.Reset();
837}
838
839// only defined for 0<=t<=1
840static float ConsoleScaleFunc(float t)
841{
842 return std::sin(x: std::acos(x: 1.0f - t));
843}
844
845struct CCompletionOptionRenderInfo
846{
847 CGameConsole *m_pSelf;
848 CTextCursor m_Cursor;
849 const char *m_pCurrentCmd;
850 int m_WantedCompletion;
851 float m_Offset;
852 float *m_pOffsetChange;
853 float m_Width;
854 float m_TotalWidth;
855};
856
857void CGameConsole::PossibleCommandsRenderCallback(int Index, const char *pStr, void *pUser)
858{
859 CCompletionOptionRenderInfo *pInfo = static_cast<CCompletionOptionRenderInfo *>(pUser);
860
861 if(Index == pInfo->m_WantedCompletion)
862 {
863 float TextWidth = pInfo->m_pSelf->TextRender()->TextWidth(Size: pInfo->m_Cursor.m_FontSize, pText: pStr, StrLength: -1, LineWidth: -1.0f);
864 const CUIRect Rect = {.x: pInfo->m_Cursor.m_X - 2.5f, .y: pInfo->m_Cursor.m_Y - 4.f / 2.f, .w: TextWidth + 5.f, .h: pInfo->m_Cursor.m_FontSize + 4.f};
865 Rect.Draw(Color: ColorRGBA(229.0f / 255.0f, 185.0f / 255.0f, 4.0f / 255.0f, 0.85f), Corners: IGraphics::CORNER_ALL, Rounding: pInfo->m_Cursor.m_FontSize / 3.f);
866
867 // scroll when out of sight
868 const bool MoveLeft = Rect.x - *pInfo->m_pOffsetChange < 0.0f;
869 const bool MoveRight = Rect.x + Rect.w - *pInfo->m_pOffsetChange > pInfo->m_Width;
870 if(MoveLeft && !MoveRight)
871 *pInfo->m_pOffsetChange -= -Rect.x + pInfo->m_Width / 4.0f;
872 else if(!MoveLeft && MoveRight)
873 *pInfo->m_pOffsetChange += Rect.x + Rect.w - pInfo->m_Width + pInfo->m_Width / 4.0f;
874
875 pInfo->m_pSelf->TextRender()->TextColor(r: 0.05f, g: 0.05f, b: 0.05f, a: 1);
876 pInfo->m_pSelf->TextRender()->TextEx(pCursor: &pInfo->m_Cursor, pText: pStr, Length: -1);
877 }
878 else
879 {
880 const char *pMatchStart = str_find_nocase(haystack: pStr, needle: pInfo->m_pCurrentCmd);
881
882 if(pMatchStart)
883 {
884 pInfo->m_pSelf->TextRender()->TextColor(r: 0.5f, g: 0.5f, b: 0.5f, a: 1);
885 pInfo->m_pSelf->TextRender()->TextEx(pCursor: &pInfo->m_Cursor, pText: pStr, Length: pMatchStart - pStr);
886 pInfo->m_pSelf->TextRender()->TextColor(r: 229.0f / 255.0f, g: 185.0f / 255.0f, b: 4.0f / 255.0f, a: 1);
887 pInfo->m_pSelf->TextRender()->TextEx(pCursor: &pInfo->m_Cursor, pText: pMatchStart, Length: str_length(str: pInfo->m_pCurrentCmd));
888 pInfo->m_pSelf->TextRender()->TextColor(r: 0.5f, g: 0.5f, b: 0.5f, a: 1);
889 pInfo->m_pSelf->TextRender()->TextEx(pCursor: &pInfo->m_Cursor, pText: pMatchStart + str_length(str: pInfo->m_pCurrentCmd), Length: -1);
890 }
891 else
892 {
893 pInfo->m_pSelf->TextRender()->TextColor(r: 0.75f, g: 0.75f, b: 0.75f, a: 1);
894 pInfo->m_pSelf->TextRender()->TextEx(pCursor: &pInfo->m_Cursor, pText: pStr, Length: -1);
895 }
896 }
897
898 pInfo->m_Cursor.m_X += 7.0f;
899 pInfo->m_TotalWidth = pInfo->m_Cursor.m_X + pInfo->m_Offset;
900}
901
902void CGameConsole::Prompt(char (&aPrompt)[32])
903{
904 CInstance *pConsole = CurrentConsole();
905 if(pConsole->m_Searching)
906 {
907 str_format(buffer: aPrompt, buffer_size: sizeof(aPrompt), format: "%s: ", Localize(pStr: "Searching"));
908 }
909 else if(m_ConsoleType == CONSOLETYPE_REMOTE)
910 {
911 if(Client()->State() == IClient::STATE_LOADING || Client()->State() == IClient::STATE_ONLINE)
912 {
913 if(Client()->RconAuthed())
914 str_copy(dst&: aPrompt, src: "rcon> ");
915 else if(pConsole->m_UsernameReq && !pConsole->m_UserGot)
916 str_format(buffer: aPrompt, buffer_size: sizeof(aPrompt), format: "%s> ", Localize(pStr: "Enter Username"));
917 else
918 str_format(buffer: aPrompt, buffer_size: sizeof(aPrompt), format: "%s> ", Localize(pStr: "Enter Password"));
919 }
920 else
921 str_format(buffer: aPrompt, buffer_size: sizeof(aPrompt), format: "%s> ", Localize(pStr: "NOT CONNECTED"));
922 }
923 else
924 {
925 str_copy(dst&: aPrompt, src: "> ");
926 }
927}
928
929void CGameConsole::OnRender()
930{
931 CUIRect Screen = *Ui()->Screen();
932 CInstance *pConsole = CurrentConsole();
933
934 const float MaxConsoleHeight = Screen.h * 3 / 5.0f;
935 float Progress = (Client()->GlobalTime() - (m_StateChangeEnd - m_StateChangeDuration)) / m_StateChangeDuration;
936
937 if(Progress >= 1.0f)
938 {
939 if(m_ConsoleState == CONSOLE_CLOSING)
940 {
941 m_ConsoleState = CONSOLE_CLOSED;
942 pConsole->m_Input.Deactivate();
943 pConsole->m_BacklogLastActiveLine = -1;
944 }
945 else if(m_ConsoleState == CONSOLE_OPENING)
946 {
947 m_ConsoleState = CONSOLE_OPEN;
948 pConsole->m_Input.Activate(Priority: EInputPriority::CONSOLE);
949 }
950
951 Progress = 1.0f;
952 }
953
954 if(m_ConsoleState == CONSOLE_OPEN && g_Config.m_ClEditor)
955 Toggle(Type: CONSOLETYPE_LOCAL);
956
957 if(m_ConsoleState == CONSOLE_CLOSED)
958 return;
959
960 if(m_ConsoleState == CONSOLE_OPEN)
961 Input()->MouseModeAbsolute();
962
963 float ConsoleHeightScale;
964 if(m_ConsoleState == CONSOLE_OPENING)
965 ConsoleHeightScale = ConsoleScaleFunc(t: Progress);
966 else if(m_ConsoleState == CONSOLE_CLOSING)
967 ConsoleHeightScale = ConsoleScaleFunc(t: 1.0f - Progress);
968 else // CONSOLE_OPEN
969 ConsoleHeightScale = ConsoleScaleFunc(t: 1.0f);
970
971 const float ConsoleHeight = ConsoleHeightScale * MaxConsoleHeight;
972
973 Ui()->MapScreen();
974
975 // do console shadow
976 Graphics()->TextureClear();
977 Graphics()->QuadsBegin();
978 IGraphics::CColorVertex Array[4] = {
979 IGraphics::CColorVertex(0, 0, 0, 0, 0.5f),
980 IGraphics::CColorVertex(1, 0, 0, 0, 0.5f),
981 IGraphics::CColorVertex(2, 0, 0, 0, 0.0f),
982 IGraphics::CColorVertex(3, 0, 0, 0, 0.0f)};
983 Graphics()->SetColorVertex(pArray: Array, Num: 4);
984 IGraphics::CQuadItem QuadItem(0, ConsoleHeight, Screen.w, 10.0f);
985 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
986 Graphics()->QuadsEnd();
987
988 // do background
989 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_CONSOLE_BG].m_Id);
990 Graphics()->QuadsBegin();
991 Graphics()->SetColor(r: 0.2f, g: 0.2f, b: 0.2f, a: 0.9f);
992 if(m_ConsoleType == CONSOLETYPE_REMOTE)
993 Graphics()->SetColor(r: 0.4f, g: 0.2f, b: 0.2f, a: 0.9f);
994 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: -ConsoleHeight * 0.075f, BottomRightU: Screen.w * 0.075f * 0.5f, BottomRightV: 0);
995 QuadItem = IGraphics::CQuadItem(0, 0, Screen.w, ConsoleHeight);
996 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
997 Graphics()->QuadsEnd();
998
999 // do small bar shadow
1000 Graphics()->TextureClear();
1001 Graphics()->QuadsBegin();
1002 Array[0] = IGraphics::CColorVertex(0, 0, 0, 0, 0.0f);
1003 Array[1] = IGraphics::CColorVertex(1, 0, 0, 0, 0.0f);
1004 Array[2] = IGraphics::CColorVertex(2, 0, 0, 0, 0.25f);
1005 Array[3] = IGraphics::CColorVertex(3, 0, 0, 0, 0.25f);
1006
1007 Graphics()->SetColorVertex(pArray: Array, Num: 4);
1008 QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 20, Screen.w, 10);
1009 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
1010 Graphics()->QuadsEnd();
1011
1012 // do the lower bar
1013 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_CONSOLE_BAR].m_Id);
1014 Graphics()->QuadsBegin();
1015 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.9f);
1016 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0.1f, BottomRightU: Screen.w * 0.015f, BottomRightV: 1 - 0.1f);
1017 QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 10.0f, Screen.w, 10.0f);
1018 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
1019 Graphics()->QuadsEnd();
1020
1021 {
1022 // Get height of 1 line
1023 const float LineHeight = TextRender()->TextBoundingBox(Size: FONT_SIZE, pText: " ", StrLength: -1, LineWidth: -1.0f, LineSpacing: LINE_SPACING).m_H;
1024
1025 const float RowHeight = FONT_SIZE * 1.5f;
1026
1027 float x = 3;
1028 float y = ConsoleHeight - RowHeight - 27.0f;
1029
1030 const float InitialX = x;
1031 const float InitialY = y;
1032
1033 // render prompt
1034 CTextCursor Cursor;
1035 TextRender()->SetCursor(pCursor: &Cursor, x, y, FontSize: FONT_SIZE, Flags: TEXTFLAG_RENDER);
1036
1037 char aPrompt[32];
1038 Prompt(aPrompt);
1039 TextRender()->TextEx(pCursor: &Cursor, pText: aPrompt);
1040
1041 // check if mouse is pressed
1042 if(!pConsole->m_MouseIsPress && Input()->NativeMousePressed(Index: 1))
1043 {
1044 pConsole->m_MouseIsPress = true;
1045 ivec2 MousePress;
1046 Input()->NativeMousePos(pX: &MousePress.x, pY: &MousePress.y);
1047 pConsole->m_MousePress.x = (MousePress.x / (float)Graphics()->WindowWidth()) * Screen.w;
1048 pConsole->m_MousePress.y = (MousePress.y / (float)Graphics()->WindowHeight()) * Screen.h;
1049 }
1050 if(pConsole->m_MouseIsPress)
1051 {
1052 ivec2 MouseRelease;
1053 Input()->NativeMousePos(pX: &MouseRelease.x, pY: &MouseRelease.y);
1054 pConsole->m_MouseRelease.x = (MouseRelease.x / (float)Graphics()->WindowWidth()) * Screen.w;
1055 pConsole->m_MouseRelease.y = (MouseRelease.y / (float)Graphics()->WindowHeight()) * Screen.h;
1056 }
1057 if(pConsole->m_MouseIsPress && !Input()->NativeMousePressed(Index: 1))
1058 {
1059 pConsole->m_MouseIsPress = false;
1060 }
1061
1062 x = Cursor.m_X;
1063
1064 if(m_ConsoleState == CONSOLE_OPEN)
1065 {
1066 if(pConsole->m_MousePress.y >= pConsole->m_BoundingBox.m_Y && pConsole->m_MousePress.y < pConsole->m_BoundingBox.m_Y + pConsole->m_BoundingBox.m_H)
1067 {
1068 CLineInput::SMouseSelection *pMouseSelection = pConsole->m_Input.GetMouseSelection();
1069 pMouseSelection->m_Selecting = pConsole->m_MouseIsPress;
1070 pMouseSelection->m_PressMouse = pConsole->m_MousePress;
1071 pMouseSelection->m_ReleaseMouse = pConsole->m_MouseRelease;
1072 }
1073 else if(pConsole->m_MouseIsPress)
1074 {
1075 pConsole->m_Input.SelectNothing();
1076 }
1077 }
1078
1079 // render console input (wrap line)
1080 pConsole->m_Input.SetHidden(m_ConsoleType == CONSOLETYPE_REMOTE && Client()->State() == IClient::STATE_ONLINE && !Client()->RconAuthed() && (pConsole->m_UserGot || !pConsole->m_UsernameReq));
1081 pConsole->m_Input.Activate(Priority: EInputPriority::CONSOLE); // Ensure that the input is active
1082 const CUIRect InputCursorRect = {.x: x, .y: y + FONT_SIZE, .w: 0.0f, .h: 0.0f};
1083 const bool WasChanged = pConsole->m_Input.WasChanged();
1084 const bool WasCursorChanged = pConsole->m_Input.WasCursorChanged();
1085 const bool Changed = WasChanged || WasCursorChanged;
1086 pConsole->m_BoundingBox = pConsole->m_Input.Render(pRect: &InputCursorRect, FontSize: FONT_SIZE, Align: TEXTALIGN_BL, Changed, LineWidth: Screen.w - 10.0f - x, LineSpacing: LINE_SPACING);
1087 if(pConsole->m_LastInputHeight == 0.0f && pConsole->m_BoundingBox.m_H != 0.0f)
1088 pConsole->m_LastInputHeight = pConsole->m_BoundingBox.m_H;
1089 if(pConsole->m_Input.HasSelection())
1090 pConsole->m_HasSelection = false; // Clear console selection if we have a line input selection
1091
1092 y -= pConsole->m_BoundingBox.m_H - FONT_SIZE;
1093 TextRender()->SetCursor(pCursor: &Cursor, x, y, FontSize: FONT_SIZE, Flags: TEXTFLAG_RENDER);
1094 Cursor.m_LineWidth = Screen.w - 10.0f - x;
1095 Cursor.m_LineSpacing = LINE_SPACING;
1096
1097 if(pConsole->m_LastInputHeight != pConsole->m_BoundingBox.m_H)
1098 {
1099 pConsole->m_HasSelection = false;
1100 pConsole->m_MouseIsPress = false;
1101 pConsole->m_LastInputHeight = pConsole->m_BoundingBox.m_H;
1102 }
1103
1104 // render possible commands
1105 if(!pConsole->m_Searching && (m_ConsoleType == CONSOLETYPE_LOCAL || Client()->RconAuthed()) && !pConsole->m_Input.IsEmpty())
1106 {
1107 CCompletionOptionRenderInfo Info;
1108 Info.m_pSelf = this;
1109 Info.m_WantedCompletion = pConsole->m_CompletionChosen;
1110 Info.m_Offset = pConsole->m_CompletionRenderOffset;
1111 Info.m_pOffsetChange = &pConsole->m_CompletionRenderOffsetChange;
1112 Info.m_Width = Screen.w;
1113 Info.m_TotalWidth = 0.0f;
1114 Info.m_pCurrentCmd = pConsole->m_aCompletionBuffer;
1115 TextRender()->SetCursor(pCursor: &Info.m_Cursor, x: InitialX - Info.m_Offset, y: InitialY + RowHeight + 2.0f, FontSize: FONT_SIZE, Flags: TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END);
1116 Info.m_Cursor.m_LineWidth = std::numeric_limits<float>::max();
1117 const int NumCommands = m_pConsole->PossibleCommands(pStr: Info.m_pCurrentCmd, FlagMask: pConsole->m_CompletionFlagmask, Temp: m_ConsoleType != CGameConsole::CONSOLETYPE_LOCAL && Client()->RconAuthed() && Client()->UseTempRconCommands(), pfnCallback: PossibleCommandsRenderCallback, pUser: &Info);
1118 pConsole->m_CompletionRenderOffset = Info.m_Offset;
1119
1120 if(NumCommands <= 0 && pConsole->m_IsCommand)
1121 {
1122 const auto [CompletionType, _] = ArgumentCompletion(pStr: Info.m_pCurrentCmd);
1123 int NumArguments = 0;
1124 if(CompletionType != EArgumentCompletionType::NONE)
1125 {
1126 Info.m_WantedCompletion = pConsole->m_CompletionChosenArgument;
1127 Info.m_TotalWidth = 0.0f;
1128 Info.m_pCurrentCmd = pConsole->m_aCompletionBufferArgument;
1129 if(CompletionType == EArgumentCompletionType::TUNE)
1130 NumArguments = PossibleTunings(pStr: Info.m_pCurrentCmd, pfnCallback: PossibleCommandsRenderCallback, pUser: &Info);
1131 else if(CompletionType == EArgumentCompletionType::SETTING)
1132 NumArguments = m_pConsole->PossibleCommands(pStr: Info.m_pCurrentCmd, FlagMask: pConsole->m_CompletionFlagmask, Temp: m_ConsoleType != CGameConsole::CONSOLETYPE_LOCAL && Client()->RconAuthed() && Client()->UseTempRconCommands(), pfnCallback: PossibleCommandsRenderCallback, pUser: &Info);
1133 else if(CompletionType == EArgumentCompletionType::KEY)
1134 NumArguments = PossibleKeys(pStr: Info.m_pCurrentCmd, pInput: Input(), pfnCallback: PossibleCommandsRenderCallback, pUser: &Info);
1135 pConsole->m_CompletionRenderOffset = Info.m_Offset;
1136 }
1137
1138 if(NumArguments <= 0 && pConsole->m_IsCommand)
1139 {
1140 char aBuf[1024];
1141 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Help: %s ", pConsole->m_pCommandHelp);
1142 TextRender()->TextEx(pCursor: &Info.m_Cursor, pText: aBuf, Length: -1);
1143 TextRender()->TextColor(r: 0.75f, g: 0.75f, b: 0.75f, a: 1);
1144 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Usage: %s %s", pConsole->m_pCommandName, pConsole->m_pCommandParams);
1145 TextRender()->TextEx(pCursor: &Info.m_Cursor, pText: aBuf, Length: -1);
1146 }
1147 }
1148
1149 Ui()->DoSmoothScrollLogic(pScrollOffset: &pConsole->m_CompletionRenderOffset, pScrollOffsetChange: &pConsole->m_CompletionRenderOffsetChange, ViewPortSize: Info.m_Width, TotalSize: Info.m_TotalWidth);
1150 }
1151 else if(pConsole->m_Searching && !pConsole->m_Input.IsEmpty())
1152 { // Render current match and match count
1153 CTextCursor MatchInfoCursor;
1154 TextRender()->SetCursor(pCursor: &MatchInfoCursor, x: InitialX, y: InitialY + RowHeight + 2.0f, FontSize: FONT_SIZE, Flags: TEXTFLAG_RENDER);
1155 TextRender()->TextColor(r: 0.8f, g: 0.8f, b: 0.8f, a: 1.0f);
1156 if(!pConsole->m_vSearchMatches.empty())
1157 {
1158 char aBuf[64];
1159 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Match %d of %d"), pConsole->m_CurrentMatchIndex + 1, (int)pConsole->m_vSearchMatches.size());
1160 TextRender()->TextEx(pCursor: &MatchInfoCursor, pText: aBuf, Length: -1);
1161 }
1162 else
1163 {
1164 TextRender()->TextEx(pCursor: &MatchInfoCursor, pText: Localize(pStr: "No results"), Length: -1);
1165 }
1166 }
1167
1168 pConsole->PumpBacklogPending();
1169 if(pConsole->m_NewLineCounter != 0)
1170 {
1171 pConsole->UpdateSearch();
1172
1173 // keep scroll position when new entries are printed.
1174 if(pConsole->m_BacklogCurLine != 0 || pConsole->m_HasSelection)
1175 {
1176 pConsole->m_BacklogCurLine += pConsole->m_NewLineCounter;
1177 pConsole->m_BacklogLastActiveLine += pConsole->m_NewLineCounter;
1178 }
1179 if(pConsole->m_NewLineCounter < 0)
1180 pConsole->m_NewLineCounter = 0;
1181 }
1182
1183 // render console log (current entry, status, wrap lines)
1184 CInstance::CBacklogEntry *pEntry = pConsole->m_Backlog.Last();
1185 float OffsetY = 0.0f;
1186
1187 std::string SelectionString;
1188
1189 if(pConsole->m_BacklogLastActiveLine < 0)
1190 pConsole->m_BacklogLastActiveLine = pConsole->m_BacklogCurLine;
1191
1192 int LineNum = -1;
1193 pConsole->m_LinesRendered = 0;
1194
1195 int SkippedLines = 0;
1196 bool First = true;
1197
1198 const float XScale = Graphics()->ScreenWidth() / Screen.w;
1199 const float YScale = Graphics()->ScreenHeight() / Screen.h;
1200 const float CalcOffsetY = LineHeight * std::floor(x: (y - RowHeight) / LineHeight);
1201 const float ClipStartY = (y - CalcOffsetY) * YScale;
1202 Graphics()->ClipEnable(x: 0, y: ClipStartY, w: Screen.w * XScale, h: y * YScale - ClipStartY);
1203
1204 while(pEntry)
1205 {
1206 if(pEntry->m_LineCount == -1)
1207 pConsole->UpdateEntryTextAttributes(pEntry);
1208
1209 LineNum += pEntry->m_LineCount;
1210 if(LineNum < pConsole->m_BacklogLastActiveLine)
1211 {
1212 SkippedLines += pEntry->m_LineCount;
1213 pEntry = pConsole->m_Backlog.Prev(pCurrent: pEntry);
1214 continue;
1215 }
1216 TextRender()->TextColor(rgb: pEntry->m_PrintColor);
1217
1218 if(First)
1219 {
1220 OffsetY -= (pConsole->m_BacklogLastActiveLine - SkippedLines) * LineHeight;
1221 }
1222
1223 const float LocalOffsetY = OffsetY + pEntry->m_YOffset / (float)pEntry->m_LineCount;
1224 OffsetY += pEntry->m_YOffset;
1225
1226 // Only apply offset if we do not keep scroll position (m_BacklogCurLine == 0)
1227 if((pConsole->m_HasSelection || pConsole->m_MouseIsPress) && pConsole->m_NewLineCounter > 0 && pConsole->m_BacklogCurLine == 0)
1228 {
1229 pConsole->m_MousePress.y -= pEntry->m_YOffset;
1230 if(!pConsole->m_MouseIsPress)
1231 pConsole->m_MouseRelease.y -= pEntry->m_YOffset;
1232 }
1233
1234 // stop rendering when lines reach the top
1235 const bool Outside = y - OffsetY <= RowHeight;
1236 const bool CanRenderOneLine = y - LocalOffsetY > RowHeight;
1237 if(Outside && !CanRenderOneLine)
1238 break;
1239
1240 const int LinesNotRendered = pEntry->m_LineCount - minimum(a: (int)std::floor(x: (y - LocalOffsetY) / RowHeight), b: pEntry->m_LineCount);
1241 pConsole->m_LinesRendered -= LinesNotRendered;
1242
1243 TextRender()->SetCursor(pCursor: &Cursor, x: 0.0f, y: y - OffsetY, FontSize: FONT_SIZE, Flags: TEXTFLAG_RENDER);
1244 Cursor.m_LineWidth = Screen.w - 10.0f;
1245 Cursor.m_MaxLines = pEntry->m_LineCount;
1246 Cursor.m_LineSpacing = LINE_SPACING;
1247 Cursor.m_CalculateSelectionMode = (m_ConsoleState == CONSOLE_OPEN && pConsole->m_MousePress.y < pConsole->m_BoundingBox.m_Y && (pConsole->m_MouseIsPress || (pConsole->m_CurSelStart != pConsole->m_CurSelEnd) || pConsole->m_HasSelection)) ? TEXT_CURSOR_SELECTION_MODE_CALCULATE : TEXT_CURSOR_SELECTION_MODE_NONE;
1248 Cursor.m_PressMouse = pConsole->m_MousePress;
1249 Cursor.m_ReleaseMouse = pConsole->m_MouseRelease;
1250
1251 if(pConsole->m_Searching && pConsole->m_CurrentMatchIndex != -1)
1252 {
1253 std::vector<CInstance::SSearchMatch> vMatches;
1254 std::copy_if(first: pConsole->m_vSearchMatches.begin(), last: pConsole->m_vSearchMatches.end(), result: std::back_inserter(x&: vMatches), pred: [&](const CInstance::SSearchMatch &Match) { return Match.m_EntryLine == LineNum + 1 - pEntry->m_LineCount; });
1255
1256 auto CurrentSelectedOccurrence = pConsole->m_vSearchMatches[pConsole->m_CurrentMatchIndex];
1257
1258 std::vector<STextColorSplit> vColorSplits;
1259 for(const auto &Match : vMatches)
1260 {
1261 bool IsSelected = CurrentSelectedOccurrence.m_EntryLine == Match.m_EntryLine && CurrentSelectedOccurrence.m_Pos == Match.m_Pos;
1262 Cursor.m_vColorSplits.emplace_back(
1263 args: Match.m_Pos,
1264 args: pConsole->m_Input.GetLength(),
1265 args: IsSelected ? ms_SearchSelectedColor : ms_SearchHighlightColor);
1266 }
1267 }
1268
1269 TextRender()->TextEx(pCursor: &Cursor, pText: pEntry->m_aText, Length: -1);
1270 Cursor.m_vColorSplits = {};
1271
1272 if(Cursor.m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE)
1273 {
1274 pConsole->m_CurSelStart = minimum(a: Cursor.m_SelectionStart, b: Cursor.m_SelectionEnd);
1275 pConsole->m_CurSelEnd = maximum(a: Cursor.m_SelectionStart, b: Cursor.m_SelectionEnd);
1276 }
1277 pConsole->m_LinesRendered += First ? pEntry->m_LineCount - (pConsole->m_BacklogLastActiveLine - SkippedLines) : pEntry->m_LineCount;
1278
1279 if(pConsole->m_CurSelStart != pConsole->m_CurSelEnd)
1280 {
1281 if(m_WantsSelectionCopy)
1282 {
1283 const bool HasNewLine = !SelectionString.empty();
1284 const size_t OffUTF8Start = str_utf8_offset_chars_to_bytes(str: pEntry->m_aText, char_offset: pConsole->m_CurSelStart);
1285 const size_t OffUTF8End = str_utf8_offset_chars_to_bytes(str: pEntry->m_aText, char_offset: pConsole->m_CurSelEnd);
1286 SelectionString.insert(pos1: 0, str: (std::string(&pEntry->m_aText[OffUTF8Start], OffUTF8End - OffUTF8Start) + (HasNewLine ? "\n" : "")));
1287 }
1288 pConsole->m_HasSelection = true;
1289 }
1290
1291 if(pConsole->m_NewLineCounter > 0) // Decrease by the entry line count since we can have multiline entries
1292 pConsole->m_NewLineCounter -= pEntry->m_LineCount;
1293
1294 pEntry = pConsole->m_Backlog.Prev(pCurrent: pEntry);
1295
1296 // reset color
1297 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
1298 First = false;
1299
1300 if(!pEntry)
1301 break;
1302 }
1303
1304 // Make sure to reset m_NewLineCounter when we are done drawing
1305 // This is because otherwise, if many entries are printed at once while console is
1306 // hidden, m_NewLineCounter will always be > 0 since the console won't be able to render
1307 // them all, thus wont be able to decrease m_NewLineCounter to 0.
1308 // This leads to an infinite increase of m_BacklogCurLine and m_BacklogLastActiveLine
1309 // when we want to keep scroll position.
1310 pConsole->m_NewLineCounter = 0;
1311
1312 Graphics()->ClipDisable();
1313
1314 pConsole->m_BacklogLastActiveLine = pConsole->m_BacklogCurLine;
1315
1316 if(m_WantsSelectionCopy && !SelectionString.empty())
1317 {
1318 pConsole->m_HasSelection = false;
1319 pConsole->m_CurSelStart = -1;
1320 pConsole->m_CurSelEnd = -1;
1321 Input()->SetClipboardText(SelectionString.c_str());
1322 m_WantsSelectionCopy = false;
1323 }
1324
1325 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
1326
1327 // render current lines and status (locked, following)
1328 char aBuf[128];
1329 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Lines %d - %d (%s)"), pConsole->m_BacklogCurLine + 1, pConsole->m_BacklogCurLine + pConsole->m_LinesRendered, pConsole->m_BacklogCurLine != 0 ? Localize(pStr: "Locked") : Localize(pStr: "Following"));
1330 TextRender()->Text(x: 10.0f, y: FONT_SIZE / 2.f, Size: FONT_SIZE, pText: aBuf);
1331
1332 if(m_ConsoleType == CONSOLETYPE_REMOTE && Client()->ReceivingRconCommands())
1333 {
1334 float Percentage = Client()->GotRconCommandsPercentage();
1335 SProgressSpinnerProperties ProgressProps;
1336 ProgressProps.m_Progress = Percentage;
1337 Ui()->RenderProgressSpinner(Center: vec2(Screen.w / 4.0f + FONT_SIZE / 2.f, FONT_SIZE), OuterRadius: FONT_SIZE / 2.f, Props: ProgressProps);
1338
1339 char aLoading[128];
1340 str_copy(dst&: aLoading, src: Localize(pStr: "Loading commands…"));
1341 if(Percentage > 0)
1342 {
1343 char aPercentage[8];
1344 str_format(buffer: aPercentage, buffer_size: sizeof(aPercentage), format: " %d%%", (int)(Percentage * 100));
1345 str_append(dst&: aLoading, src: aPercentage);
1346 }
1347 TextRender()->Text(x: Screen.w / 4.0f + FONT_SIZE + 2.0f, y: FONT_SIZE / 2.f, Size: FONT_SIZE, pText: aLoading);
1348 }
1349
1350 // render version
1351 str_copy(dst&: aBuf, src: "v" GAME_VERSION " on " CONF_PLATFORM_STRING " " CONF_ARCH_STRING);
1352 TextRender()->Text(x: Screen.w - TextRender()->TextWidth(Size: FONT_SIZE, pText: aBuf) - 10.0f, y: FONT_SIZE / 2.f, Size: FONT_SIZE, pText: aBuf);
1353 }
1354}
1355
1356void CGameConsole::OnMessage(int MsgType, void *pRawMsg)
1357{
1358}
1359
1360bool CGameConsole::OnInput(const IInput::CEvent &Event)
1361{
1362 // accept input when opening, but not at first frame to discard the input that caused the console to open
1363 if(m_ConsoleState != CONSOLE_OPEN && (m_ConsoleState != CONSOLE_OPENING || m_StateChangeEnd == Client()->GlobalTime() + m_StateChangeDuration))
1364 return false;
1365 if((Event.m_Key >= KEY_F1 && Event.m_Key <= KEY_F12) || (Event.m_Key >= KEY_F13 && Event.m_Key <= KEY_F24))
1366 return false;
1367
1368 if(Event.m_Key == KEY_ESCAPE && (Event.m_Flags & IInput::FLAG_PRESS))
1369 Toggle(Type: m_ConsoleType);
1370 else if(!CurrentConsole()->OnInput(Event))
1371 {
1372 if(m_pClient->Input()->ModifierIsPressed() && Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_C)
1373 m_WantsSelectionCopy = true;
1374 }
1375
1376 return true;
1377}
1378
1379void CGameConsole::Toggle(int Type)
1380{
1381 if(m_ConsoleType != Type && (m_ConsoleState == CONSOLE_OPEN || m_ConsoleState == CONSOLE_OPENING))
1382 {
1383 // don't toggle console, just switch what console to use
1384 }
1385 else
1386 {
1387 if(m_ConsoleState == CONSOLE_CLOSED || m_ConsoleState == CONSOLE_OPEN)
1388 {
1389 m_StateChangeEnd = Client()->GlobalTime() + m_StateChangeDuration;
1390 }
1391 else
1392 {
1393 float Progress = m_StateChangeEnd - Client()->GlobalTime();
1394 float ReversedProgress = m_StateChangeDuration - Progress;
1395
1396 m_StateChangeEnd = Client()->GlobalTime() + ReversedProgress;
1397 }
1398
1399 if(m_ConsoleState == CONSOLE_CLOSED || m_ConsoleState == CONSOLE_CLOSING)
1400 {
1401 Ui()->SetEnabled(false);
1402 m_ConsoleState = CONSOLE_OPENING;
1403 }
1404 else
1405 {
1406 Input()->MouseModeRelative();
1407 Ui()->SetEnabled(true);
1408 m_pClient->OnRelease();
1409 m_ConsoleState = CONSOLE_CLOSING;
1410 }
1411 }
1412 m_ConsoleType = Type;
1413}
1414
1415void CGameConsole::ConToggleLocalConsole(IConsole::IResult *pResult, void *pUserData)
1416{
1417 ((CGameConsole *)pUserData)->Toggle(Type: CONSOLETYPE_LOCAL);
1418}
1419
1420void CGameConsole::ConToggleRemoteConsole(IConsole::IResult *pResult, void *pUserData)
1421{
1422 ((CGameConsole *)pUserData)->Toggle(Type: CONSOLETYPE_REMOTE);
1423}
1424
1425void CGameConsole::ConClearLocalConsole(IConsole::IResult *pResult, void *pUserData)
1426{
1427 ((CGameConsole *)pUserData)->m_LocalConsole.ClearBacklog();
1428}
1429
1430void CGameConsole::ConClearRemoteConsole(IConsole::IResult *pResult, void *pUserData)
1431{
1432 ((CGameConsole *)pUserData)->m_RemoteConsole.ClearBacklog();
1433}
1434
1435void CGameConsole::ConDumpLocalConsole(IConsole::IResult *pResult, void *pUserData)
1436{
1437 ((CGameConsole *)pUserData)->m_LocalConsole.Dump();
1438}
1439
1440void CGameConsole::ConDumpRemoteConsole(IConsole::IResult *pResult, void *pUserData)
1441{
1442 ((CGameConsole *)pUserData)->m_RemoteConsole.Dump();
1443}
1444
1445void CGameConsole::ConConsolePageUp(IConsole::IResult *pResult, void *pUserData)
1446{
1447 CInstance *pConsole = ((CGameConsole *)pUserData)->CurrentConsole();
1448 pConsole->m_BacklogCurLine += pConsole->GetLinesToScroll(Direction: -1, LinesToScroll: pConsole->m_LinesRendered);
1449 pConsole->m_HasSelection = false;
1450}
1451
1452void CGameConsole::ConConsolePageDown(IConsole::IResult *pResult, void *pUserData)
1453{
1454 CInstance *pConsole = ((CGameConsole *)pUserData)->CurrentConsole();
1455 pConsole->m_BacklogCurLine -= pConsole->GetLinesToScroll(Direction: 1, LinesToScroll: pConsole->m_LinesRendered);
1456 pConsole->m_HasSelection = false;
1457 if(pConsole->m_BacklogCurLine < 0)
1458 pConsole->m_BacklogCurLine = 0;
1459}
1460
1461void CGameConsole::ConchainConsoleOutputLevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1462{
1463 CGameConsole *pSelf = (CGameConsole *)pUserData;
1464 pfnCallback(pResult, pCallbackUserData);
1465 if(pResult->NumArguments())
1466 {
1467 pSelf->m_pConsoleLogger->SetFilter(CLogFilter{.m_MaxLevel: IConsole::ToLogLevelFilter(ConsoleLevel: g_Config.m_ConsoleOutputLevel)});
1468 }
1469}
1470
1471void CGameConsole::RequireUsername(bool UsernameReq)
1472{
1473 if((m_RemoteConsole.m_UsernameReq = UsernameReq))
1474 {
1475 m_RemoteConsole.m_aUser[0] = '\0';
1476 m_RemoteConsole.m_UserGot = false;
1477 }
1478}
1479
1480void CGameConsole::PrintLine(int Type, const char *pLine)
1481{
1482 if(Type == CONSOLETYPE_LOCAL)
1483 m_LocalConsole.PrintLine(pLine, Len: str_length(str: pLine), PrintColor: TextRender()->DefaultTextColor());
1484 else if(Type == CONSOLETYPE_REMOTE)
1485 m_RemoteConsole.PrintLine(pLine, Len: str_length(str: pLine), PrintColor: TextRender()->DefaultTextColor());
1486}
1487
1488void CGameConsole::OnConsoleInit()
1489{
1490 // init console instances
1491 m_LocalConsole.Init(pGameConsole: this);
1492 m_RemoteConsole.Init(pGameConsole: this);
1493
1494 m_pConsole = Kernel()->RequestInterface<IConsole>();
1495
1496 Console()->Register(pName: "toggle_local_console", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConToggleLocalConsole, pUser: this, pHelp: "Toggle local console");
1497 Console()->Register(pName: "toggle_remote_console", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConToggleRemoteConsole, pUser: this, pHelp: "Toggle remote console");
1498 Console()->Register(pName: "clear_local_console", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConClearLocalConsole, pUser: this, pHelp: "Clear local console");
1499 Console()->Register(pName: "clear_remote_console", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConClearRemoteConsole, pUser: this, pHelp: "Clear remote console");
1500 Console()->Register(pName: "dump_local_console", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConDumpLocalConsole, pUser: this, pHelp: "Write local console contents to a text file");
1501 Console()->Register(pName: "dump_remote_console", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConDumpRemoteConsole, pUser: this, pHelp: "Write remote console contents to a text file");
1502
1503 Console()->Register(pName: "console_page_up", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConConsolePageUp, pUser: this, pHelp: "Previous page in console");
1504 Console()->Register(pName: "console_page_down", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConConsolePageDown, pUser: this, pHelp: "Next page in console");
1505 Console()->Chain(pName: "console_output_level", pfnChainFunc: ConchainConsoleOutputLevel, pUser: this);
1506}
1507
1508void CGameConsole::OnInit()
1509{
1510 Engine()->SetAdditionalLogger(std::unique_ptr<ILogger>(m_pConsoleLogger));
1511 // add resize event
1512 Graphics()->AddWindowResizeListener(pFunc: [this]() {
1513 m_LocalConsole.UpdateBacklogTextAttributes();
1514 m_LocalConsole.m_HasSelection = false;
1515 m_RemoteConsole.UpdateBacklogTextAttributes();
1516 m_RemoteConsole.m_HasSelection = false;
1517 });
1518}
1519
1520void CGameConsole::OnStateChange(int NewState, int OldState)
1521{
1522 if(OldState <= IClient::STATE_ONLINE && NewState == IClient::STATE_OFFLINE)
1523 {
1524 m_RemoteConsole.m_UserGot = false;
1525 m_RemoteConsole.m_aUser[0] = '\0';
1526 m_RemoteConsole.m_Input.Clear();
1527 m_RemoteConsole.m_UsernameReq = false;
1528 }
1529}
1530