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