Civ backgrounds for minimap
[0ad.git] / source / ps / CConsole.cpp
blob4df1624b1741d88949aa05ec8b1427748a94c905
1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "CConsole.h"
22 #include "graphics/Canvas2D.h"
23 #include "graphics/FontMetrics.h"
24 #include "graphics/TextRenderer.h"
25 #include "gui/CGUI.h"
26 #include "gui/GUIManager.h"
27 #include "lib/code_generation.h"
28 #include "lib/timer.h"
29 #include "lib/utf8.h"
30 #include "maths/MathUtil.h"
31 #include "ps/CLogger.h"
32 #include "ps/ConfigDB.h"
33 #include "ps/CStrInternStatic.h"
34 #include "ps/Filesystem.h"
35 #include "ps/GameSetup/Config.h"
36 #include "ps/Globals.h"
37 #include "ps/Hotkey.h"
38 #include "ps/Profile.h"
39 #include "ps/Pyrogenesis.h"
40 #include "ps/VideoMode.h"
41 #include "scriptinterface/ScriptInterface.h"
42 #include "scriptinterface/JSON.h"
44 #include <vector>
45 #include <wctype.h>
47 namespace
50 // For text being typed into the console.
51 constexpr int CONSOLE_BUFFER_SIZE = 1024;
53 const char* CONSOLE_FONT = "mono-10";
55 } // anonymous namespace
57 CConsole* g_Console = 0;
59 CConsole::CConsole()
61 m_Toggle = false;
62 m_Visible = false;
64 m_VisibleFrac = 0.0f;
66 m_Buffer = std::make_unique<wchar_t[]>(CONSOLE_BUFFER_SIZE);
67 FlushBuffer();
69 m_MsgHistPos = 1;
70 m_CharsPerPage = 0;
72 m_PrevTime = 0.0;
73 m_CursorVisState = true;
74 m_CursorBlinkRate = 0.5;
76 m_QuitHotkeyWasShown = false;
78 InsertMessage("[ 0 A.D. Console v0.15 ]");
79 InsertMessage("");
82 CConsole::~CConsole() = default;
84 void CConsole::Init()
86 // Initialise console history file
87 m_MaxHistoryLines = 200;
88 CFG_GET_VAL("console.history.size", m_MaxHistoryLines);
90 m_HistoryFile = L"config/console.txt";
91 LoadHistory();
93 UpdateScreenSize(g_xres, g_yres);
95 // Calculate and store the line spacing
96 const CFontMetrics font{CStrIntern(CONSOLE_FONT)};
97 m_FontHeight = font.GetLineSpacing();
98 m_FontWidth = font.GetCharacterWidth(L'C');
99 m_CharsPerPage = static_cast<size_t>(g_xres / m_FontWidth);
100 // Offset by an arbitrary amount, to make it fit more nicely
101 m_FontOffset = 7;
103 m_CursorBlinkRate = 0.5;
104 CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate);
107 void CConsole::UpdateScreenSize(int w, int h)
109 m_X = 0;
110 m_Y = 0;
111 float height = h * 0.6f;
112 m_Width = w / g_VideoMode.GetScale();
113 m_Height = height / g_VideoMode.GetScale();
116 void CConsole::ShowQuitHotkeys()
118 if (m_QuitHotkeyWasShown)
119 return;
121 std::string str;
122 for (const std::pair<const SDL_Scancode_, KeyMapping>& key : g_HotkeyMap)
123 if (key.second.front().name == "console.toggle")
124 str += (str.empty() ? "Press " : " / ") + FindScancodeName(static_cast<SDL_Scancode>(key.first));
126 if (!str.empty())
127 InsertMessage(str + " to quit.");
129 m_QuitHotkeyWasShown = true;
132 void CConsole::ToggleVisible()
134 m_Toggle = true;
135 m_Visible = !m_Visible;
137 // TODO: this should be based on input focus, not visibility
138 if (m_Visible)
140 ShowQuitHotkeys();
141 SDL_StartTextInput();
142 return;
144 SDL_StopTextInput();
147 void CConsole::SetVisible(bool visible)
149 if (visible != m_Visible)
150 m_Toggle = true;
151 m_Visible = visible;
152 if (visible)
154 m_PrevTime = 0.0;
155 m_CursorVisState = false;
159 void CConsole::FlushBuffer()
161 // Clear the buffer and set the cursor and length to 0
162 memset(m_Buffer.get(), '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE);
163 m_BufferPos = m_BufferLength = 0;
166 void CConsole::Update(const float deltaRealTime)
168 if (m_Toggle)
170 const float AnimateTime = .30f;
171 const float Delta = deltaRealTime / AnimateTime;
172 if (m_Visible)
174 m_VisibleFrac += Delta;
175 if (m_VisibleFrac > 1.0f)
177 m_VisibleFrac = 1.0f;
178 m_Toggle = false;
181 else
183 m_VisibleFrac -= Delta;
184 if (m_VisibleFrac < 0.0f)
186 m_VisibleFrac = 0.0f;
187 m_Toggle = false;
193 void CConsole::Render(CCanvas2D& canvas)
195 if (!(m_Visible || m_Toggle))
196 return;
198 PROFILE3_GPU("console");
200 DrawWindow(canvas);
202 CTextRenderer textRenderer;
203 textRenderer.SetCurrentFont(CStrIntern(CONSOLE_FONT));
204 // Animation: slide in from top of screen.
205 const float deltaY = (1.0f - m_VisibleFrac) * m_Height;
206 textRenderer.Translate(m_X, m_Y - deltaY);
208 DrawHistory(textRenderer);
209 DrawBuffer(textRenderer);
211 canvas.DrawText(textRenderer);
214 void CConsole::DrawWindow(CCanvas2D& canvas)
216 std::vector<CVector2D> points =
218 CVector2D{m_Width, 0.0f},
219 CVector2D{1.0f, 0.0f},
220 CVector2D{1.0f, m_Height - 1.0f},
221 CVector2D{m_Width, m_Height - 1.0f},
222 CVector2D{m_Width, 0.0f}
224 for (CVector2D& point : points)
225 point += CVector2D{m_X, m_Y - (1.0f - m_VisibleFrac) * m_Height};
227 canvas.DrawRect(CRect(points[1], points[3]), CColor(0.0f, 0.0f, 0.5f, 0.6f));
228 canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f));
230 if (m_Height > m_FontHeight + 4)
232 points = {
233 CVector2D{0.0f, m_Height - static_cast<float>(m_FontHeight) - 4.0f},
234 CVector2D{m_Width, m_Height - static_cast<float>(m_FontHeight) - 4.0f}
236 for (CVector2D& point : points)
237 point += CVector2D{m_X, m_Y - (1.0f - m_VisibleFrac) * m_Height};
238 canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f));
242 void CConsole::DrawHistory(CTextRenderer& textRenderer)
244 int i = 1;
246 std::deque<std::wstring>::iterator it; //History iterator
248 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
250 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
252 for (it = m_MsgHistory.begin();
253 it != m_MsgHistory.end()
254 && (((i - m_MsgHistPos + 1) * m_FontHeight) < m_Height);
255 ++it)
257 if (i >= m_MsgHistPos)
259 textRenderer.Put(
260 9.0f,
261 m_Height - static_cast<float>(m_FontOffset) - static_cast<float>(m_FontHeight) * (i - m_MsgHistPos + 1),
262 it->c_str());
265 i++;
269 // Renders the buffer to the screen.
270 void CConsole::DrawBuffer(CTextRenderer& textRenderer)
272 if (m_Height < m_FontHeight)
273 return;
275 const CVector2D savedTranslate = textRenderer.GetTranslate();
277 textRenderer.Translate(2.0f, m_Height - static_cast<float>(m_FontOffset) + 1.0f);
279 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 1.0f));
280 textRenderer.PutAdvance(L"]");
282 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
284 if (m_BufferPos == 0)
285 DrawCursor(textRenderer);
287 for (int i = 0; i < m_BufferLength; ++i)
289 textRenderer.PrintfAdvance(L"%lc", m_Buffer[i]);
290 if (m_BufferPos - 1 == i)
291 DrawCursor(textRenderer);
294 textRenderer.ResetTranslate(savedTranslate);
297 void CConsole::DrawCursor(CTextRenderer& textRenderer)
299 if (m_CursorBlinkRate > 0.0)
301 // check if the cursor visibility state needs to be changed
302 double currTime = timer_Time();
303 if ((currTime - m_PrevTime) >= m_CursorBlinkRate)
305 m_CursorVisState = !m_CursorVisState;
306 m_PrevTime = currTime;
309 else
311 // Should always be visible
312 m_CursorVisState = true;
315 if(m_CursorVisState)
317 // Slightly translucent yellow
318 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 0.8f));
320 // Cursor character is chosen to be an underscore
321 textRenderer.Put(0.0f, 0.0f, L"_");
323 // Revert to the standard text color
324 textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
328 bool CConsole::IsEOB() const
330 return m_BufferPos == m_BufferLength;
333 bool CConsole::IsBOB() const
335 return m_BufferPos == 0;
338 bool CConsole::IsFull() const
340 return m_BufferLength == CONSOLE_BUFFER_SIZE;
343 bool CConsole::IsEmpty() const
345 return m_BufferLength == 0;
348 //Inserts a character into the buffer.
349 void CConsole::InsertChar(const int szChar, const wchar_t cooked)
351 static int historyPos = -1;
353 if (!m_Visible) return;
355 switch (szChar)
357 case SDLK_RETURN:
358 historyPos = -1;
359 m_MsgHistPos = 1;
360 ProcessBuffer(m_Buffer.get());
361 FlushBuffer();
362 return;
364 case SDLK_TAB:
365 // Auto Complete
366 return;
368 case SDLK_BACKSPACE:
369 if (IsEmpty() || IsBOB()) return;
371 if (m_BufferPos == m_BufferLength)
372 m_Buffer[m_BufferPos - 1] = '\0';
373 else
375 for (int j = m_BufferPos-1; j < m_BufferLength - 1; ++j)
376 m_Buffer[j] = m_Buffer[j + 1]; // move chars to left
377 m_Buffer[m_BufferLength-1] = '\0';
380 m_BufferPos--;
381 m_BufferLength--;
382 return;
384 case SDLK_DELETE:
385 if (IsEmpty() || IsEOB())
386 return;
388 if (m_BufferPos == m_BufferLength - 1)
390 m_Buffer[m_BufferPos] = '\0';
391 m_BufferLength--;
393 else
395 if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL])
397 // Make Ctrl-Delete delete up to end of line
398 m_Buffer[m_BufferPos] = '\0';
399 m_BufferLength = m_BufferPos;
401 else
403 // Delete just one char and move the others left
404 for(int j = m_BufferPos; j < m_BufferLength - 1; ++j)
405 m_Buffer[j] = m_Buffer[j + 1];
406 m_Buffer[m_BufferLength - 1] = '\0';
407 m_BufferLength--;
411 return;
413 case SDLK_HOME:
414 if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL])
416 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
418 const int linesShown = static_cast<int>(m_Height / m_FontHeight) - 4;
419 m_MsgHistPos = Clamp(static_cast<int>(m_MsgHistory.size()) - linesShown, 1, static_cast<int>(m_MsgHistory.size()));
421 else
423 m_BufferPos = 0;
425 return;
427 case SDLK_END:
428 if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL])
430 m_MsgHistPos = 1;
432 else
434 m_BufferPos = m_BufferLength;
436 return;
438 case SDLK_LEFT:
439 if (m_BufferPos)
440 m_BufferPos--;
441 return;
443 case SDLK_RIGHT:
444 if (m_BufferPos != m_BufferLength)
445 m_BufferPos++;
446 return;
448 // BEGIN: Buffer History Lookup
449 case SDLK_UP:
450 if (m_BufHistory.size() && historyPos != static_cast<int>(m_BufHistory.size()) - 1)
452 historyPos++;
453 SetBuffer(m_BufHistory.at(historyPos).c_str());
454 m_BufferPos = m_BufferLength;
456 return;
458 case SDLK_DOWN:
459 if (m_BufHistory.size())
461 if (historyPos > 0)
463 historyPos--;
464 SetBuffer(m_BufHistory.at(historyPos).c_str());
465 m_BufferPos = m_BufferLength;
467 else if (historyPos == 0)
469 historyPos--;
470 FlushBuffer();
473 return;
474 // END: Buffer History Lookup
476 // BEGIN: Message History Lookup
477 case SDLK_PAGEUP:
479 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
481 if (m_MsgHistPos != static_cast<int>(m_MsgHistory.size()))
482 m_MsgHistPos++;
483 return;
486 case SDLK_PAGEDOWN:
487 if (m_MsgHistPos != 1)
488 m_MsgHistPos--;
489 return;
490 // END: Message History Lookup
492 default: //Insert a character
493 if (IsFull() || cooked == 0)
494 return;
496 if (IsEOB()) //are we at the end of the buffer?
497 m_Buffer[m_BufferPos] = cooked; //cat char onto end
498 else
499 { //we need to insert
500 int i;
501 for (i = m_BufferLength; i > m_BufferPos; --i)
502 m_Buffer[i] = m_Buffer[i - 1]; // move chars to right
503 m_Buffer[i] = cooked;
506 m_BufferPos++;
507 m_BufferLength++;
509 return;
514 void CConsole::InsertMessage(const std::string& message)
516 // (TODO: this text-wrapping is rubbish since we now use variable-width fonts)
518 //Insert newlines to wraparound text where needed
519 std::wstring wrapAround = wstring_from_utf8(message.c_str());
520 std::wstring newline(L"\n");
521 size_t oldNewline=0;
522 size_t distance;
524 //make sure everything has been initialized
525 if (m_CharsPerPage != 0)
527 while (oldNewline + m_CharsPerPage < wrapAround.length())
529 distance = wrapAround.find(newline, oldNewline) - oldNewline;
530 if (distance > m_CharsPerPage)
532 oldNewline += m_CharsPerPage;
533 wrapAround.insert(oldNewline++, newline);
535 else
536 oldNewline += distance+1;
539 // Split into lines and add each one individually
540 oldNewline = 0;
543 std::lock_guard<std::mutex> lock(m_Mutex); // needed for safe access to m_deqMsgHistory
545 while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos)
547 distance -= oldNewline;
548 m_MsgHistory.push_front(wrapAround.substr(oldNewline, distance));
549 oldNewline += distance+1;
551 m_MsgHistory.push_front(wrapAround.substr(oldNewline));
555 const wchar_t* CConsole::GetBuffer()
557 m_Buffer[m_BufferLength] = 0;
558 return m_Buffer.get();
561 void CConsole::SetBuffer(const wchar_t* szMessage)
563 int oldBufferPos = m_BufferPos; // remember since FlushBuffer will set it to 0
565 FlushBuffer();
567 wcsncpy(m_Buffer.get(), szMessage, CONSOLE_BUFFER_SIZE);
568 m_Buffer[CONSOLE_BUFFER_SIZE-1] = 0;
569 m_BufferLength = static_cast<int>(wcslen(m_Buffer.get()));
570 m_BufferPos = std::min(oldBufferPos, m_BufferLength);
573 void CConsole::ProcessBuffer(const wchar_t* szLine)
575 if (!szLine || wcslen(szLine) <= 0)
576 return;
578 ENSURE(wcslen(szLine) < CONSOLE_BUFFER_SIZE);
580 m_BufHistory.push_front(szLine);
581 SaveHistory(); // Do this each line for the moment; if a script causes
582 // a crash it's a useful record.
584 // Process it as JavaScript
585 std::shared_ptr<ScriptInterface> pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
586 ScriptRequest rq(*pScriptInterface);
588 JS::RootedValue rval(rq.cx);
589 pScriptInterface->Eval(CStrW(szLine).ToUTF8().c_str(), &rval);
590 if (!rval.isUndefined())
591 InsertMessage(Script::ToString(rq, &rval));
594 void CConsole::LoadHistory()
596 // note: we don't care if this file doesn't exist or can't be read;
597 // just don't load anything in that case.
599 // do this before LoadFile to avoid an error message if file not found.
600 if (!VfsFileExists(m_HistoryFile))
601 return;
603 std::shared_ptr<u8> buf; size_t buflen;
604 if (g_VFS->LoadFile(m_HistoryFile, buf, buflen) < 0)
605 return;
607 CStr bytes ((char*)buf.get(), buflen);
609 CStrW str (bytes.FromUTF8());
610 size_t pos = 0;
611 while (pos != CStrW::npos)
613 pos = str.find('\n');
614 if (pos != CStrW::npos)
616 if (pos > 0)
617 m_BufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos));
618 str = str.substr(pos + 1);
620 else if (str.length() > 0)
621 m_BufHistory.push_front(str);
625 void CConsole::SaveHistory()
627 WriteBuffer buffer;
628 const int linesToSkip = static_cast<int>(m_BufHistory.size()) - m_MaxHistoryLines;
629 std::deque<std::wstring>::reverse_iterator it = m_BufHistory.rbegin();
630 if(linesToSkip > 0)
631 std::advance(it, linesToSkip);
632 for (; it != m_BufHistory.rend(); ++it)
634 CStr8 line = CStrW(*it).ToUTF8();
635 buffer.Append(line.data(), line.length());
636 static const char newline = '\n';
637 buffer.Append(&newline, 1);
640 if (g_VFS->CreateFile(m_HistoryFile, buffer.Data(), buffer.Size()) == INFO::OK)
641 ONCE(debug_printf("FILES| Console command history written to '%s'\n", m_HistoryFile.string8().c_str()));
642 else
643 debug_printf("FILES| Failed to write console command history to '%s'\n", m_HistoryFile.string8().c_str());
646 static bool isUnprintableChar(SDL_Keysym key)
648 switch (key.sym)
650 // We want to allow some, which are handled specially
651 case SDLK_RETURN: case SDLK_TAB:
652 case SDLK_BACKSPACE: case SDLK_DELETE:
653 case SDLK_HOME: case SDLK_END:
654 case SDLK_LEFT: case SDLK_RIGHT:
655 case SDLK_UP: case SDLK_DOWN:
656 case SDLK_PAGEUP: case SDLK_PAGEDOWN:
657 return true;
659 // Ignore the others
660 default:
661 return false;
665 InReaction conInputHandler(const SDL_Event_* ev)
667 if (!g_Console)
668 return IN_PASS;
670 if (static_cast<int>(ev->ev.type) == SDL_HOTKEYPRESS)
672 std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
674 if (hotkey == "console.toggle")
676 ResetActiveHotkeys();
677 g_Console->ToggleVisible();
678 return IN_HANDLED;
680 else if (g_Console->IsActive() && hotkey == "copy")
682 std::string text = utf8_from_wstring(g_Console->GetBuffer());
683 SDL_SetClipboardText(text.c_str());
684 return IN_HANDLED;
686 else if (g_Console->IsActive() && hotkey == "paste")
688 char* utf8_text = SDL_GetClipboardText();
689 if (!utf8_text)
690 return IN_HANDLED;
692 std::wstring text = wstring_from_utf8(utf8_text);
693 SDL_free(utf8_text);
695 for (wchar_t c : text)
696 g_Console->InsertChar(0, c);
698 return IN_HANDLED;
702 if (!g_Console->IsActive())
703 return IN_PASS;
705 // In SDL2, we no longer get Unicode wchars via SDL_Keysym
706 // we use text input events instead and they provide UTF-8 chars
707 if (ev->ev.type == SDL_TEXTINPUT)
709 // TODO: this could be more efficient with an interface to insert UTF-8 strings directly
710 std::wstring wstr = wstring_from_utf8(ev->ev.text.text);
711 for (size_t i = 0; i < wstr.length(); ++i)
712 g_Console->InsertChar(0, wstr[i]);
713 return IN_HANDLED;
715 // TODO: text editing events for IME support
717 if (ev->ev.type != SDL_KEYDOWN && ev->ev.type != SDL_KEYUP)
718 return IN_PASS;
720 int sym = ev->ev.key.keysym.sym;
722 // Stop unprintable characters (ctrl+, alt+ and escape).
723 if (ev->ev.type == SDL_KEYDOWN && isUnprintableChar(ev->ev.key.keysym) &&
724 !HotkeyIsPressed("console.toggle"))
726 g_Console->InsertChar(sym, 0);
727 return IN_HANDLED;
730 // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys.
731 // However, if Ctrl/Meta modifiers are active (or it's escape), just pass it through instead,
732 // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste).
733 // Also ignore the key if we are trying to toggle the console off.
734 // See also similar logic in CInput.cpp
735 if (EventWillFireHotkey(ev, "console.toggle") ||
736 g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL] ||
737 g_scancodes[SDL_SCANCODE_LGUI] || g_scancodes[SDL_SCANCODE_RGUI])
738 return IN_PASS;
740 return IN_HANDLED;