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"
29 #include "lib/timer.h"
31 #include "maths/MathUtil.h"
32 #include "ps/CLogger.h"
33 #include "ps/ConfigDB.h"
34 #include "ps/CStrInternStatic.h"
35 #include "ps/Filesystem.h"
36 #include "ps/GameSetup/Config.h"
37 #include "ps/Globals.h"
38 #include "ps/Hotkey.h"
39 #include "ps/Profile.h"
40 #include "ps/Pyrogenesis.h"
41 #include "ps/VideoMode.h"
42 #include "scriptinterface/ScriptInterface.h"
43 #include "scriptinterface/JSON.h"
51 // For text being typed into the console.
52 constexpr int CONSOLE_BUFFER_SIZE
= 1024;
54 const char* CONSOLE_FONT
= "mono-10";
56 } // anonymous namespace
58 CConsole
* g_Console
= 0;
67 m_Buffer
= std::make_unique
<wchar_t[]>(CONSOLE_BUFFER_SIZE
);
74 m_CursorVisState
= true;
75 m_CursorBlinkRate
= 0.5;
77 m_QuitHotkeyWasShown
= false;
79 InsertMessage("[ 0 A.D. Console v0.15 ]");
83 CConsole::~CConsole() = default;
87 // Initialise console history file
88 m_MaxHistoryLines
= 200;
89 CFG_GET_VAL("console.history.size", m_MaxHistoryLines
);
91 m_HistoryFile
= L
"config/console.txt";
94 UpdateScreenSize(g_xres
, g_yres
);
96 // Calculate and store the line spacing
97 const CFontMetrics font
{CStrIntern(CONSOLE_FONT
)};
98 m_FontHeight
= font
.GetLineSpacing();
99 m_FontWidth
= font
.GetCharacterWidth(L
'C');
100 m_CharsPerPage
= static_cast<size_t>(g_xres
/ m_FontWidth
);
101 // Offset by an arbitrary amount, to make it fit more nicely
104 m_CursorBlinkRate
= 0.5;
105 CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate
);
108 void CConsole::UpdateScreenSize(int w
, int h
)
112 float height
= h
* 0.6f
;
113 m_Width
= w
/ g_VideoMode
.GetScale();
114 m_Height
= height
/ g_VideoMode
.GetScale();
117 void CConsole::ShowQuitHotkeys()
119 if (m_QuitHotkeyWasShown
)
123 for (const std::pair
<const SDL_Scancode_
, KeyMapping
>& key
: g_HotkeyMap
)
124 if (key
.second
.front().name
== "console.toggle")
125 str
+= (str
.empty() ? "Press " : " / ") + FindScancodeName(static_cast<SDL_Scancode
>(key
.first
));
128 InsertMessage(str
+ " to quit.");
130 m_QuitHotkeyWasShown
= true;
133 void CConsole::ToggleVisible()
136 m_Visible
= !m_Visible
;
138 // TODO: this should be based on input focus, not visibility
142 SDL_StartTextInput();
148 void CConsole::SetVisible(bool visible
)
150 if (visible
!= m_Visible
)
156 m_CursorVisState
= false;
160 void CConsole::FlushBuffer()
162 // Clear the buffer and set the cursor and length to 0
163 memset(m_Buffer
.get(), '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE
);
164 m_BufferPos
= m_BufferLength
= 0;
167 void CConsole::Update(const float deltaRealTime
)
171 const float AnimateTime
= .30f
;
172 const float Delta
= deltaRealTime
/ AnimateTime
;
175 m_VisibleFrac
+= Delta
;
176 if (m_VisibleFrac
> 1.0f
)
178 m_VisibleFrac
= 1.0f
;
184 m_VisibleFrac
-= Delta
;
185 if (m_VisibleFrac
< 0.0f
)
187 m_VisibleFrac
= 0.0f
;
194 void CConsole::Render(CCanvas2D
& canvas
)
196 if (!(m_Visible
|| m_Toggle
))
199 PROFILE3_GPU("console");
203 CTextRenderer textRenderer
;
204 textRenderer
.SetCurrentFont(CStrIntern(CONSOLE_FONT
));
205 // Animation: slide in from top of screen.
206 const float deltaY
= (1.0f
- m_VisibleFrac
) * m_Height
;
207 textRenderer
.Translate(m_X
, m_Y
- deltaY
);
209 DrawHistory(textRenderer
);
210 DrawBuffer(textRenderer
);
212 canvas
.DrawText(textRenderer
);
215 void CConsole::DrawWindow(CCanvas2D
& canvas
)
217 std::vector
<CVector2D
> points
=
219 CVector2D
{m_Width
, 0.0f
},
220 CVector2D
{1.0f
, 0.0f
},
221 CVector2D
{1.0f
, m_Height
- 1.0f
},
222 CVector2D
{m_Width
, m_Height
- 1.0f
},
223 CVector2D
{m_Width
, 0.0f
}
225 for (CVector2D
& point
: points
)
226 point
+= CVector2D
{m_X
, m_Y
- (1.0f
- m_VisibleFrac
) * m_Height
};
228 canvas
.DrawRect(CRect(points
[1], points
[3]), CColor(0.0f
, 0.0f
, 0.5f
, 0.6f
));
229 canvas
.DrawLine(points
, 1.0f
, CColor(0.5f
, 0.5f
, 0.0f
, 0.6f
));
231 if (m_Height
> m_FontHeight
+ 4)
234 CVector2D
{0.0f
, m_Height
- static_cast<float>(m_FontHeight
) - 4.0f
},
235 CVector2D
{m_Width
, m_Height
- static_cast<float>(m_FontHeight
) - 4.0f
}
237 for (CVector2D
& point
: points
)
238 point
+= CVector2D
{m_X
, m_Y
- (1.0f
- m_VisibleFrac
) * m_Height
};
239 canvas
.DrawLine(points
, 1.0f
, CColor(0.5f
, 0.5f
, 0.0f
, 0.6f
));
243 void CConsole::DrawHistory(CTextRenderer
& textRenderer
)
247 std::deque
<std::wstring
>::iterator it
; //History iterator
249 std::lock_guard
<std::mutex
> lock(m_Mutex
); // needed for safe access to m_deqMsgHistory
251 textRenderer
.SetCurrentColor(CColor(1.0f
, 1.0f
, 1.0f
, 1.0f
));
253 for (it
= m_MsgHistory
.begin();
254 it
!= m_MsgHistory
.end()
255 && (((i
- m_MsgHistPos
+ 1) * m_FontHeight
) < m_Height
);
258 if (i
>= m_MsgHistPos
)
262 m_Height
- static_cast<float>(m_FontOffset
) - static_cast<float>(m_FontHeight
) * (i
- m_MsgHistPos
+ 1),
270 // Renders the buffer to the screen.
271 void CConsole::DrawBuffer(CTextRenderer
& textRenderer
)
273 if (m_Height
< m_FontHeight
)
276 const CVector2D savedTranslate
= textRenderer
.GetTranslate();
278 textRenderer
.Translate(2.0f
, m_Height
- static_cast<float>(m_FontOffset
) + 1.0f
);
280 textRenderer
.SetCurrentColor(CColor(1.0f
, 1.0f
, 0.0f
, 1.0f
));
281 textRenderer
.PutAdvance(L
"]");
283 textRenderer
.SetCurrentColor(CColor(1.0f
, 1.0f
, 1.0f
, 1.0f
));
285 if (m_BufferPos
== 0)
286 DrawCursor(textRenderer
);
288 for (int i
= 0; i
< m_BufferLength
; ++i
)
290 textRenderer
.PrintfAdvance(L
"%lc", m_Buffer
[i
]);
291 if (m_BufferPos
- 1 == i
)
292 DrawCursor(textRenderer
);
295 textRenderer
.ResetTranslate(savedTranslate
);
298 void CConsole::DrawCursor(CTextRenderer
& textRenderer
)
300 if (m_CursorBlinkRate
> 0.0)
302 // check if the cursor visibility state needs to be changed
303 double currTime
= timer_Time();
304 if ((currTime
- m_PrevTime
) >= m_CursorBlinkRate
)
306 m_CursorVisState
= !m_CursorVisState
;
307 m_PrevTime
= currTime
;
312 // Should always be visible
313 m_CursorVisState
= true;
318 // Slightly translucent yellow
319 textRenderer
.SetCurrentColor(CColor(1.0f
, 1.0f
, 0.0f
, 0.8f
));
321 // Cursor character is chosen to be an underscore
322 textRenderer
.Put(0.0f
, 0.0f
, L
"_");
324 // Revert to the standard text color
325 textRenderer
.SetCurrentColor(CColor(1.0f
, 1.0f
, 1.0f
, 1.0f
));
329 bool CConsole::IsEOB() const
331 return m_BufferPos
== m_BufferLength
;
334 bool CConsole::IsBOB() const
336 return m_BufferPos
== 0;
339 bool CConsole::IsFull() const
341 return m_BufferLength
== CONSOLE_BUFFER_SIZE
;
344 bool CConsole::IsEmpty() const
346 return m_BufferLength
== 0;
349 //Inserts a character into the buffer.
350 void CConsole::InsertChar(const int szChar
, const wchar_t cooked
)
352 static int historyPos
= -1;
354 if (!m_Visible
) return;
361 ProcessBuffer(m_Buffer
.get());
370 if (IsEmpty() || IsBOB()) return;
372 if (m_BufferPos
== m_BufferLength
)
373 m_Buffer
[m_BufferPos
- 1] = '\0';
376 for (int j
= m_BufferPos
-1; j
< m_BufferLength
- 1; ++j
)
377 m_Buffer
[j
] = m_Buffer
[j
+ 1]; // move chars to left
378 m_Buffer
[m_BufferLength
-1] = '\0';
386 if (IsEmpty() || IsEOB())
389 if (m_BufferPos
== m_BufferLength
- 1)
391 m_Buffer
[m_BufferPos
] = '\0';
396 if (g_scancodes
[SDL_SCANCODE_LCTRL
] || g_scancodes
[SDL_SCANCODE_RCTRL
])
398 // Make Ctrl-Delete delete up to end of line
399 m_Buffer
[m_BufferPos
] = '\0';
400 m_BufferLength
= m_BufferPos
;
404 // Delete just one char and move the others left
405 for(int j
= m_BufferPos
; j
< m_BufferLength
- 1; ++j
)
406 m_Buffer
[j
] = m_Buffer
[j
+ 1];
407 m_Buffer
[m_BufferLength
- 1] = '\0';
415 if (g_scancodes
[SDL_SCANCODE_LCTRL
] || g_scancodes
[SDL_SCANCODE_RCTRL
])
417 std::lock_guard
<std::mutex
> lock(m_Mutex
); // needed for safe access to m_deqMsgHistory
419 const int linesShown
= static_cast<int>(m_Height
/ m_FontHeight
) - 4;
420 m_MsgHistPos
= Clamp(static_cast<int>(m_MsgHistory
.size()) - linesShown
, 1, static_cast<int>(m_MsgHistory
.size()));
429 if (g_scancodes
[SDL_SCANCODE_LCTRL
] || g_scancodes
[SDL_SCANCODE_RCTRL
])
435 m_BufferPos
= m_BufferLength
;
445 if (m_BufferPos
!= m_BufferLength
)
449 // BEGIN: Buffer History Lookup
451 if (m_BufHistory
.size() && historyPos
!= static_cast<int>(m_BufHistory
.size()) - 1)
454 SetBuffer(m_BufHistory
.at(historyPos
).c_str());
455 m_BufferPos
= m_BufferLength
;
460 if (m_BufHistory
.size())
465 SetBuffer(m_BufHistory
.at(historyPos
).c_str());
466 m_BufferPos
= m_BufferLength
;
468 else if (historyPos
== 0)
475 // END: Buffer History Lookup
477 // BEGIN: Message History Lookup
480 std::lock_guard
<std::mutex
> lock(m_Mutex
); // needed for safe access to m_deqMsgHistory
482 if (m_MsgHistPos
!= static_cast<int>(m_MsgHistory
.size()))
488 if (m_MsgHistPos
!= 1)
491 // END: Message History Lookup
493 default: //Insert a character
494 if (IsFull() || cooked
== 0)
497 if (IsEOB()) //are we at the end of the buffer?
498 m_Buffer
[m_BufferPos
] = cooked
; //cat char onto end
500 { //we need to insert
502 for (i
= m_BufferLength
; i
> m_BufferPos
; --i
)
503 m_Buffer
[i
] = m_Buffer
[i
- 1]; // move chars to right
504 m_Buffer
[i
] = cooked
;
515 void CConsole::InsertMessage(const std::string
& message
)
517 // (TODO: this text-wrapping is rubbish since we now use variable-width fonts)
519 //Insert newlines to wraparound text where needed
520 std::wstring wrapAround
= wstring_from_utf8(message
.c_str());
521 std::wstring
newline(L
"\n");
525 //make sure everything has been initialized
526 if (m_CharsPerPage
!= 0)
528 while (oldNewline
+ m_CharsPerPage
< wrapAround
.length())
530 distance
= wrapAround
.find(newline
, oldNewline
) - oldNewline
;
531 if (distance
> m_CharsPerPage
)
533 oldNewline
+= m_CharsPerPage
;
534 wrapAround
.insert(oldNewline
++, newline
);
537 oldNewline
+= distance
+1;
540 // Split into lines and add each one individually
544 std::lock_guard
<std::mutex
> lock(m_Mutex
); // needed for safe access to m_deqMsgHistory
546 while ( (distance
= wrapAround
.find(newline
, oldNewline
)) != wrapAround
.npos
)
548 distance
-= oldNewline
;
549 m_MsgHistory
.push_front(wrapAround
.substr(oldNewline
, distance
));
550 oldNewline
+= distance
+1;
552 m_MsgHistory
.push_front(wrapAround
.substr(oldNewline
));
556 const wchar_t* CConsole::GetBuffer()
558 m_Buffer
[m_BufferLength
] = 0;
559 return m_Buffer
.get();
562 void CConsole::SetBuffer(const wchar_t* szMessage
)
564 int oldBufferPos
= m_BufferPos
; // remember since FlushBuffer will set it to 0
568 wcsncpy(m_Buffer
.get(), szMessage
, CONSOLE_BUFFER_SIZE
);
569 m_Buffer
[CONSOLE_BUFFER_SIZE
-1] = 0;
570 m_BufferLength
= static_cast<int>(wcslen(m_Buffer
.get()));
571 m_BufferPos
= std::min(oldBufferPos
, m_BufferLength
);
574 void CConsole::ProcessBuffer(const wchar_t* szLine
)
576 if (!szLine
|| wcslen(szLine
) <= 0)
579 ENSURE(wcslen(szLine
) < CONSOLE_BUFFER_SIZE
);
581 m_BufHistory
.push_front(szLine
);
582 SaveHistory(); // Do this each line for the moment; if a script causes
583 // a crash it's a useful record.
585 // Process it as JavaScript
586 std::shared_ptr
<ScriptInterface
> pScriptInterface
= g_GUI
->GetActiveGUI()->GetScriptInterface();
587 ScriptRequest
rq(*pScriptInterface
);
589 JS::RootedValue
rval(rq
.cx
);
590 pScriptInterface
->Eval(CStrW(szLine
).ToUTF8().c_str(), &rval
);
591 if (!rval
.isUndefined())
592 InsertMessage(Script::ToString(rq
, &rval
));
595 void CConsole::LoadHistory()
597 // note: we don't care if this file doesn't exist or can't be read;
598 // just don't load anything in that case.
600 // do this before LoadFile to avoid an error message if file not found.
601 if (!VfsFileExists(m_HistoryFile
))
604 std::shared_ptr
<u8
> buf
; size_t buflen
;
605 if (g_VFS
->LoadFile(m_HistoryFile
, buf
, buflen
) < 0)
608 CStr
bytes ((char*)buf
.get(), buflen
);
610 CStrW
str (bytes
.FromUTF8());
612 while (pos
!= CStrW::npos
)
614 pos
= str
.find('\n');
615 if (pos
!= CStrW::npos
)
618 m_BufHistory
.push_front(str
.Left(str
[pos
-1] == '\r' ? pos
- 1 : pos
));
619 str
= str
.substr(pos
+ 1);
621 else if (str
.length() > 0)
622 m_BufHistory
.push_front(str
);
626 void CConsole::SaveHistory()
629 const int linesToSkip
= static_cast<int>(m_BufHistory
.size()) - m_MaxHistoryLines
;
630 std::deque
<std::wstring
>::reverse_iterator it
= m_BufHistory
.rbegin();
632 std::advance(it
, linesToSkip
);
633 for (; it
!= m_BufHistory
.rend(); ++it
)
635 CStr8 line
= CStrW(*it
).ToUTF8();
636 buffer
.Append(line
.data(), line
.length());
637 static const char newline
= '\n';
638 buffer
.Append(&newline
, 1);
641 if (g_VFS
->CreateFile(m_HistoryFile
, buffer
.Data(), buffer
.Size()) == INFO::OK
)
642 ONCE(debug_printf("FILES| Console command history written to %s\n", m_HistoryFile
.string8().c_str()));
644 debug_printf("FILES| Failed to write console command history to %s\n", m_HistoryFile
.string8().c_str());
647 static bool isUnprintableChar(SDL_Keysym key
)
651 // We want to allow some, which are handled specially
652 case SDLK_RETURN
: case SDLK_TAB
:
653 case SDLK_BACKSPACE
: case SDLK_DELETE
:
654 case SDLK_HOME
: case SDLK_END
:
655 case SDLK_LEFT
: case SDLK_RIGHT
:
656 case SDLK_UP
: case SDLK_DOWN
:
657 case SDLK_PAGEUP
: case SDLK_PAGEDOWN
:
666 InReaction
conInputHandler(const SDL_Event_
* ev
)
671 if (static_cast<int>(ev
->ev
.type
) == SDL_HOTKEYPRESS
)
673 std::string hotkey
= static_cast<const char*>(ev
->ev
.user
.data1
);
675 if (hotkey
== "console.toggle")
677 ResetActiveHotkeys();
678 g_Console
->ToggleVisible();
681 else if (g_Console
->IsActive() && hotkey
== "copy")
683 std::string text
= utf8_from_wstring(g_Console
->GetBuffer());
684 SDL_SetClipboardText(text
.c_str());
687 else if (g_Console
->IsActive() && hotkey
== "paste")
689 char* utf8_text
= SDL_GetClipboardText();
693 std::wstring text
= wstring_from_utf8(utf8_text
);
696 for (wchar_t c
: text
)
697 g_Console
->InsertChar(0, c
);
703 if (!g_Console
->IsActive())
706 // In SDL2, we no longer get Unicode wchars via SDL_Keysym
707 // we use text input events instead and they provide UTF-8 chars
708 if (ev
->ev
.type
== SDL_TEXTINPUT
)
710 // TODO: this could be more efficient with an interface to insert UTF-8 strings directly
711 std::wstring wstr
= wstring_from_utf8(ev
->ev
.text
.text
);
712 for (size_t i
= 0; i
< wstr
.length(); ++i
)
713 g_Console
->InsertChar(0, wstr
[i
]);
716 // TODO: text editing events for IME support
718 if (ev
->ev
.type
!= SDL_KEYDOWN
&& ev
->ev
.type
!= SDL_KEYUP
)
721 int sym
= ev
->ev
.key
.keysym
.sym
;
723 // Stop unprintable characters (ctrl+, alt+ and escape).
724 if (ev
->ev
.type
== SDL_KEYDOWN
&& isUnprintableChar(ev
->ev
.key
.keysym
) &&
725 !HotkeyIsPressed("console.toggle"))
727 g_Console
->InsertChar(sym
, 0);
731 // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys.
732 // However, if Ctrl/Meta modifiers are active (or it's escape), just pass it through instead,
733 // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste).
734 // Also ignore the key if we are trying to toggle the console off.
735 // See also similar logic in CInput.cpp
736 if (EventWillFireHotkey(ev
, "console.toggle") ||
737 g_scancodes
[SDL_SCANCODE_LCTRL
] || g_scancodes
[SDL_SCANCODE_RCTRL
] ||
738 g_scancodes
[SDL_SCANCODE_LGUI
] || g_scancodes
[SDL_SCANCODE_RGUI
])