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