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"
22 #include "graphics/Canvas2D.h"
23 #include "graphics/FontMetrics.h"
24 #include "graphics/TextRenderer.h"
26 #include "gui/GUIManager.h"
27 #include "lib/code_generation.h"
28 #include "lib/timer.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"
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;
66 m_Buffer
= std::make_unique
<wchar_t[]>(CONSOLE_BUFFER_SIZE
);
73 m_CursorVisState
= true;
74 m_CursorBlinkRate
= 0.5;
76 m_QuitHotkeyWasShown
= false;
78 InsertMessage("[ 0 A.D. Console v0.15 ]");
82 CConsole::~CConsole() = default;
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";
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
103 m_CursorBlinkRate
= 0.5;
104 CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate
);
107 void CConsole::UpdateScreenSize(int w
, int h
)
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
)
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
));
127 InsertMessage(str
+ " to quit.");
129 m_QuitHotkeyWasShown
= true;
132 void CConsole::ToggleVisible()
135 m_Visible
= !m_Visible
;
137 // TODO: this should be based on input focus, not visibility
141 SDL_StartTextInput();
147 void CConsole::SetVisible(bool visible
)
149 if (visible
!= m_Visible
)
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
)
170 const float AnimateTime
= .30f
;
171 const float Delta
= deltaRealTime
/ AnimateTime
;
174 m_VisibleFrac
+= Delta
;
175 if (m_VisibleFrac
> 1.0f
)
177 m_VisibleFrac
= 1.0f
;
183 m_VisibleFrac
-= Delta
;
184 if (m_VisibleFrac
< 0.0f
)
186 m_VisibleFrac
= 0.0f
;
193 void CConsole::Render(CCanvas2D
& canvas
)
195 if (!(m_Visible
|| m_Toggle
))
198 PROFILE3_GPU("console");
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)
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
)
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
);
257 if (i
>= m_MsgHistPos
)
261 m_Height
- static_cast<float>(m_FontOffset
) - static_cast<float>(m_FontHeight
) * (i
- m_MsgHistPos
+ 1),
269 // Renders the buffer to the screen.
270 void CConsole::DrawBuffer(CTextRenderer
& textRenderer
)
272 if (m_Height
< m_FontHeight
)
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
;
311 // Should always be visible
312 m_CursorVisState
= true;
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;
360 ProcessBuffer(m_Buffer
.get());
369 if (IsEmpty() || IsBOB()) return;
371 if (m_BufferPos
== m_BufferLength
)
372 m_Buffer
[m_BufferPos
- 1] = '\0';
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';
385 if (IsEmpty() || IsEOB())
388 if (m_BufferPos
== m_BufferLength
- 1)
390 m_Buffer
[m_BufferPos
] = '\0';
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
;
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';
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()));
428 if (g_scancodes
[SDL_SCANCODE_LCTRL
] || g_scancodes
[SDL_SCANCODE_RCTRL
])
434 m_BufferPos
= m_BufferLength
;
444 if (m_BufferPos
!= m_BufferLength
)
448 // BEGIN: Buffer History Lookup
450 if (m_BufHistory
.size() && historyPos
!= static_cast<int>(m_BufHistory
.size()) - 1)
453 SetBuffer(m_BufHistory
.at(historyPos
).c_str());
454 m_BufferPos
= m_BufferLength
;
459 if (m_BufHistory
.size())
464 SetBuffer(m_BufHistory
.at(historyPos
).c_str());
465 m_BufferPos
= m_BufferLength
;
467 else if (historyPos
== 0)
474 // END: Buffer History Lookup
476 // BEGIN: Message History Lookup
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()))
487 if (m_MsgHistPos
!= 1)
490 // END: Message History Lookup
492 default: //Insert a character
493 if (IsFull() || cooked
== 0)
496 if (IsEOB()) //are we at the end of the buffer?
497 m_Buffer
[m_BufferPos
] = cooked
; //cat char onto end
499 { //we need to insert
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
;
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");
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
);
536 oldNewline
+= distance
+1;
539 // Split into lines and add each one individually
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
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)
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
))
603 std::shared_ptr
<u8
> buf
; size_t buflen
;
604 if (g_VFS
->LoadFile(m_HistoryFile
, buf
, buflen
) < 0)
607 CStr
bytes ((char*)buf
.get(), buflen
);
609 CStrW
str (bytes
.FromUTF8());
611 while (pos
!= CStrW::npos
)
613 pos
= str
.find('\n');
614 if (pos
!= CStrW::npos
)
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()
628 const int linesToSkip
= static_cast<int>(m_BufHistory
.size()) - m_MaxHistoryLines
;
629 std::deque
<std::wstring
>::reverse_iterator it
= m_BufHistory
.rbegin();
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()));
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
)
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
:
665 InReaction
conInputHandler(const SDL_Event_
* ev
)
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();
680 else if (g_Console
->IsActive() && hotkey
== "copy")
682 std::string text
= utf8_from_wstring(g_Console
->GetBuffer());
683 SDL_SetClipboardText(text
.c_str());
686 else if (g_Console
->IsActive() && hotkey
== "paste")
688 char* utf8_text
= SDL_GetClipboardText();
692 std::wstring text
= wstring_from_utf8(utf8_text
);
695 for (wchar_t c
: text
)
696 g_Console
->InsertChar(0, c
);
702 if (!g_Console
->IsActive())
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
]);
715 // TODO: text editing events for IME support
717 if (ev
->ev
.type
!= SDL_KEYDOWN
&& ev
->ev
.type
!= SDL_KEYUP
)
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);
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
])