/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CConsole.h" #include "graphics/Canvas2D.h" #include "graphics/FontMetrics.h" #include "graphics/TextRenderer.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "lib/code_generation.h" #include "lib/timer.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/VideoMode.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include #include #include namespace { // For text being typed into the console. constexpr int CONSOLE_BUFFER_SIZE = 1024; const char* CONSOLE_FONT = "mono-10"; } // anonymous namespace CConsole* g_Console = 0; CConsole::CConsole() { m_Toggle = false; m_Visible = false; m_VisibleFrac = 0.0f; m_Buffer = std::make_unique(CONSOLE_BUFFER_SIZE); FlushBuffer(); m_MsgHistPos = 1; m_CharsPerPage = 0; m_PrevTime = 0.0; m_CursorVisState = true; m_CursorBlinkRate = 0.5; m_QuitHotkeyWasShown = false; InsertMessage("[ 0 A.D. Console v0.15 ]"); InsertMessage(""); } CConsole::~CConsole() = default; void CConsole::Init() { // Initialise console history file m_MaxHistoryLines = 200; CFG_GET_VAL("console.history.size", m_MaxHistoryLines); m_HistoryFile = L"config/console.txt"; LoadHistory(); UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing const CFontMetrics font{CStrIntern(CONSOLE_FONT)}; m_FontHeight = font.GetLineSpacing(); m_FontWidth = font.GetCharacterWidth(L'C'); m_CharsPerPage = static_cast(g_xres / m_FontWidth); // Offset by an arbitrary amount, to make it fit more nicely m_FontOffset = 7; m_CursorBlinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate); } void CConsole::UpdateScreenSize(int w, int h) { m_X = 0; m_Y = 0; float height = h * 0.6f; m_Width = w / g_VideoMode.GetScale(); m_Height = height / g_VideoMode.GetScale(); } void CConsole::ShowQuitHotkeys() { if (m_QuitHotkeyWasShown) return; std::string str; for (const std::pair& key : g_HotkeyMap) if (key.second.front().name == "console.toggle") str += (str.empty() ? "Press " : " / ") + FindScancodeName(static_cast(key.first)); if (!str.empty()) InsertMessage(str + " to quit."); m_QuitHotkeyWasShown = true; } void CConsole::ToggleVisible() { m_Toggle = true; m_Visible = !m_Visible; // TODO: this should be based on input focus, not visibility if (m_Visible) { ShowQuitHotkeys(); SDL_StartTextInput(); return; } SDL_StopTextInput(); } void CConsole::SetVisible(bool visible) { if (visible != m_Visible) m_Toggle = true; m_Visible = visible; if (visible) { m_PrevTime = 0.0; m_CursorVisState = false; } } void CConsole::FlushBuffer() { // Clear the buffer and set the cursor and length to 0 memset(m_Buffer.get(), '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE); m_BufferPos = m_BufferLength = 0; } void CConsole::Update(const float deltaRealTime) { if (m_Toggle) { const float AnimateTime = .30f; const float Delta = deltaRealTime / AnimateTime; if (m_Visible) { m_VisibleFrac += Delta; if (m_VisibleFrac > 1.0f) { m_VisibleFrac = 1.0f; m_Toggle = false; } } else { m_VisibleFrac -= Delta; if (m_VisibleFrac < 0.0f) { m_VisibleFrac = 0.0f; m_Toggle = false; } } } } void CConsole::Render(CCanvas2D& canvas) { if (!(m_Visible || m_Toggle)) return; PROFILE3_GPU("console"); DrawWindow(canvas); CTextRenderer textRenderer; textRenderer.SetCurrentFont(CStrIntern(CONSOLE_FONT)); // Animation: slide in from top of screen. const float deltaY = (1.0f - m_VisibleFrac) * m_Height; textRenderer.Translate(m_X, m_Y - deltaY); DrawHistory(textRenderer); DrawBuffer(textRenderer); canvas.DrawText(textRenderer); } void CConsole::DrawWindow(CCanvas2D& canvas) { std::vector points = { CVector2D{m_Width, 0.0f}, CVector2D{1.0f, 0.0f}, CVector2D{1.0f, m_Height - 1.0f}, CVector2D{m_Width, m_Height - 1.0f}, CVector2D{m_Width, 0.0f} }; for (CVector2D& point : points) point += CVector2D{m_X, m_Y - (1.0f - m_VisibleFrac) * m_Height}; canvas.DrawRect(CRect(points[1], points[3]), CColor(0.0f, 0.0f, 0.5f, 0.6f)); canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f)); if (m_Height > m_FontHeight + 4) { points = { CVector2D{0.0f, m_Height - static_cast(m_FontHeight) - 4.0f}, CVector2D{m_Width, m_Height - static_cast(m_FontHeight) - 4.0f} }; for (CVector2D& point : points) point += CVector2D{m_X, m_Y - (1.0f - m_VisibleFrac) * m_Height}; canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f)); } } void CConsole::DrawHistory(CTextRenderer& textRenderer) { int i = 1; std::deque::iterator it; //History iterator std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f)); for (it = m_MsgHistory.begin(); it != m_MsgHistory.end() && (((i - m_MsgHistPos + 1) * m_FontHeight) < m_Height); ++it) { if (i >= m_MsgHistPos) { textRenderer.Put( 9.0f, m_Height - static_cast(m_FontOffset) - static_cast(m_FontHeight) * (i - m_MsgHistPos + 1), it->c_str()); } i++; } } // Renders the buffer to the screen. void CConsole::DrawBuffer(CTextRenderer& textRenderer) { if (m_Height < m_FontHeight) return; const CVector2D savedTranslate = textRenderer.GetTranslate(); textRenderer.Translate(2.0f, m_Height - static_cast(m_FontOffset) + 1.0f); textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 1.0f)); textRenderer.PutAdvance(L"]"); textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f)); if (m_BufferPos == 0) DrawCursor(textRenderer); for (int i = 0; i < m_BufferLength; ++i) { textRenderer.PrintfAdvance(L"%lc", m_Buffer[i]); if (m_BufferPos - 1 == i) DrawCursor(textRenderer); } textRenderer.ResetTranslate(savedTranslate); } void CConsole::DrawCursor(CTextRenderer& textRenderer) { if (m_CursorBlinkRate > 0.0) { // check if the cursor visibility state needs to be changed double currTime = timer_Time(); if ((currTime - m_PrevTime) >= m_CursorBlinkRate) { m_CursorVisState = !m_CursorVisState; m_PrevTime = currTime; } } else { // Should always be visible m_CursorVisState = true; } if(m_CursorVisState) { // Slightly translucent yellow textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 0.8f)); // Cursor character is chosen to be an underscore textRenderer.Put(0.0f, 0.0f, L"_"); // Revert to the standard text color textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f)); } } bool CConsole::IsEOB() const { return m_BufferPos == m_BufferLength; } bool CConsole::IsBOB() const { return m_BufferPos == 0; } bool CConsole::IsFull() const { return m_BufferLength == CONSOLE_BUFFER_SIZE; } bool CConsole::IsEmpty() const { return m_BufferLength == 0; } //Inserts a character into the buffer. void CConsole::InsertChar(const int szChar, const wchar_t cooked) { static int historyPos = -1; if (!m_Visible) return; switch (szChar) { case SDLK_RETURN: historyPos = -1; m_MsgHistPos = 1; ProcessBuffer(m_Buffer.get()); FlushBuffer(); return; case SDLK_TAB: // Auto Complete return; case SDLK_BACKSPACE: if (IsEmpty() || IsBOB()) return; if (m_BufferPos == m_BufferLength) m_Buffer[m_BufferPos - 1] = '\0'; else { for (int j = m_BufferPos-1; j < m_BufferLength - 1; ++j) m_Buffer[j] = m_Buffer[j + 1]; // move chars to left m_Buffer[m_BufferLength-1] = '\0'; } m_BufferPos--; m_BufferLength--; return; case SDLK_DELETE: if (IsEmpty() || IsEOB()) return; if (m_BufferPos == m_BufferLength - 1) { m_Buffer[m_BufferPos] = '\0'; m_BufferLength--; } else { if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL]) { // Make Ctrl-Delete delete up to end of line m_Buffer[m_BufferPos] = '\0'; m_BufferLength = m_BufferPos; } else { // Delete just one char and move the others left for(int j = m_BufferPos; j < m_BufferLength - 1; ++j) m_Buffer[j] = m_Buffer[j + 1]; m_Buffer[m_BufferLength - 1] = '\0'; m_BufferLength--; } } return; case SDLK_HOME: if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL]) { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory const int linesShown = static_cast(m_Height / m_FontHeight) - 4; m_MsgHistPos = Clamp(static_cast(m_MsgHistory.size()) - linesShown, 1, static_cast(m_MsgHistory.size())); } else { m_BufferPos = 0; } return; case SDLK_END: if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL]) { m_MsgHistPos = 1; } else { m_BufferPos = m_BufferLength; } return; case SDLK_LEFT: if (m_BufferPos) m_BufferPos--; return; case SDLK_RIGHT: if (m_BufferPos != m_BufferLength) m_BufferPos++; return; // BEGIN: Buffer History Lookup case SDLK_UP: if (m_BufHistory.size() && historyPos != static_cast(m_BufHistory.size()) - 1) { historyPos++; SetBuffer(m_BufHistory.at(historyPos).c_str()); m_BufferPos = m_BufferLength; } return; case SDLK_DOWN: if (m_BufHistory.size()) { if (historyPos > 0) { historyPos--; SetBuffer(m_BufHistory.at(historyPos).c_str()); m_BufferPos = m_BufferLength; } else if (historyPos == 0) { historyPos--; FlushBuffer(); } } return; // END: Buffer History Lookup // BEGIN: Message History Lookup case SDLK_PAGEUP: { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory if (m_MsgHistPos != static_cast(m_MsgHistory.size())) m_MsgHistPos++; return; } case SDLK_PAGEDOWN: if (m_MsgHistPos != 1) m_MsgHistPos--; return; // END: Message History Lookup default: //Insert a character if (IsFull() || cooked == 0) return; if (IsEOB()) //are we at the end of the buffer? m_Buffer[m_BufferPos] = cooked; //cat char onto end else { //we need to insert int i; for (i = m_BufferLength; i > m_BufferPos; --i) m_Buffer[i] = m_Buffer[i - 1]; // move chars to right m_Buffer[i] = cooked; } m_BufferPos++; m_BufferLength++; return; } } void CConsole::InsertMessage(const std::string& message) { // (TODO: this text-wrapping is rubbish since we now use variable-width fonts) //Insert newlines to wraparound text where needed std::wstring wrapAround = wstring_from_utf8(message.c_str()); std::wstring newline(L"\n"); size_t oldNewline=0; size_t distance; //make sure everything has been initialized if (m_CharsPerPage != 0) { while (oldNewline + m_CharsPerPage < wrapAround.length()) { distance = wrapAround.find(newline, oldNewline) - oldNewline; if (distance > m_CharsPerPage) { oldNewline += m_CharsPerPage; wrapAround.insert(oldNewline++, newline); } else oldNewline += distance+1; } } // Split into lines and add each one individually oldNewline = 0; { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos) { distance -= oldNewline; m_MsgHistory.push_front(wrapAround.substr(oldNewline, distance)); oldNewline += distance+1; } wrapAround.erase(0, oldNewline); m_MsgHistory.push_front(std::move(wrapAround)); } } const wchar_t* CConsole::GetBuffer() { m_Buffer[m_BufferLength] = 0; return m_Buffer.get(); } void CConsole::SetBuffer(const wchar_t* szMessage) { int oldBufferPos = m_BufferPos; // remember since FlushBuffer will set it to 0 FlushBuffer(); wcsncpy(m_Buffer.get(), szMessage, CONSOLE_BUFFER_SIZE); m_Buffer[CONSOLE_BUFFER_SIZE-1] = 0; m_BufferLength = static_cast(wcslen(m_Buffer.get())); m_BufferPos = std::min(oldBufferPos, m_BufferLength); } void CConsole::ProcessBuffer(const wchar_t* szLine) { if (!szLine || wcslen(szLine) <= 0) return; ENSURE(wcslen(szLine) < CONSOLE_BUFFER_SIZE); m_BufHistory.push_front(szLine); SaveHistory(); // Do this each line for the moment; if a script causes // a crash it's a useful record. // Process it as JavaScript std::shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(*pScriptInterface); JS::RootedValue rval(rq.cx); pScriptInterface->Eval(CStrW(szLine).ToUTF8().c_str(), &rval); if (!rval.isUndefined()) InsertMessage(Script::ToString(rq, &rval)); } void CConsole::LoadHistory() { // note: we don't care if this file doesn't exist or can't be read; // just don't load anything in that case. // do this before LoadFile to avoid an error message if file not found. if (!VfsFileExists(m_HistoryFile)) return; std::shared_ptr buf; size_t buflen; if (g_VFS->LoadFile(m_HistoryFile, buf, buflen) < 0) return; CStr bytes ((char*)buf.get(), buflen); CStrW str (bytes.FromUTF8()); size_t pos = 0; while (pos != CStrW::npos) { pos = str.find('\n'); if (pos != CStrW::npos) { if (pos > 0) m_BufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos)); str.erase(0, pos + 1); } else if (str.length() > 0) m_BufHistory.push_front(str); } } void CConsole::SaveHistory() { WriteBuffer buffer; const int linesToSkip = static_cast(m_BufHistory.size()) - m_MaxHistoryLines; std::deque::reverse_iterator it = m_BufHistory.rbegin(); if(linesToSkip > 0) std::advance(it, linesToSkip); for (; it != m_BufHistory.rend(); ++it) { CStr8 line = CStrW(*it).ToUTF8(); buffer.Append(line.data(), line.length()); static const char newline = '\n'; buffer.Append(&newline, 1); } if (g_VFS->CreateFile(m_HistoryFile, buffer.Data(), buffer.Size()) == INFO::OK) ONCE(debug_printf("FILES| Console command history written to '%s'\n", m_HistoryFile.string8().c_str())); else debug_printf("FILES| Failed to write console command history to '%s'\n", m_HistoryFile.string8().c_str()); } static bool isUnprintableChar(SDL_Keysym key) { switch (key.sym) { // We want to allow some, which are handled specially case SDLK_RETURN: case SDLK_TAB: case SDLK_BACKSPACE: case SDLK_DELETE: case SDLK_HOME: case SDLK_END: case SDLK_LEFT: case SDLK_RIGHT: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: return true; // Ignore the others default: return false; } } InReaction conInputHandler(const SDL_Event_* ev) { if (!g_Console) return IN_PASS; if (static_cast(ev->ev.type) == SDL_HOTKEYPRESS) { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "console.toggle") { ResetActiveHotkeys(); g_Console->ToggleVisible(); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "copy") { std::string text = utf8_from_wstring(g_Console->GetBuffer()); SDL_SetClipboardText(text.c_str()); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "paste") { char* utf8_text = SDL_GetClipboardText(); if (!utf8_text) return IN_HANDLED; std::wstring text = wstring_from_utf8(utf8_text); SDL_free(utf8_text); for (wchar_t c : text) g_Console->InsertChar(0, c); return IN_HANDLED; } } if (!g_Console->IsActive()) return IN_PASS; // In SDL2, we no longer get Unicode wchars via SDL_Keysym // we use text input events instead and they provide UTF-8 chars if (ev->ev.type == SDL_TEXTINPUT) { // TODO: this could be more efficient with an interface to insert UTF-8 strings directly std::wstring wstr = wstring_from_utf8(ev->ev.text.text); for (size_t i = 0; i < wstr.length(); ++i) g_Console->InsertChar(0, wstr[i]); return IN_HANDLED; } // TODO: text editing events for IME support if (ev->ev.type != SDL_KEYDOWN && ev->ev.type != SDL_KEYUP) return IN_PASS; int sym = ev->ev.key.keysym.sym; // Stop unprintable characters (ctrl+, alt+ and escape). if (ev->ev.type == SDL_KEYDOWN && isUnprintableChar(ev->ev.key.keysym) && !HotkeyIsPressed("console.toggle")) { g_Console->InsertChar(sym, 0); return IN_HANDLED; } // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys. // However, if Ctrl/Meta modifiers are active (or it's escape), just pass it through instead, // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste). // Also ignore the key if we are trying to toggle the console off. // See also similar logic in CInput.cpp if (EventWillFireHotkey(ev, "console.toggle") || g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL] || g_scancodes[SDL_SCANCODE_LGUI] || g_scancodes[SDL_SCANCODE_RGUI]) return IN_PASS; return IN_HANDLED; }