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