You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
559 lines
14 KiB
559 lines
14 KiB
#include "ClientPlatformSpecific.hpp"
|
|
|
|
// signal types
|
|
#include <csignal>
|
|
// std::filesystem
|
|
#include <filesystem>
|
|
// std::*fstream
|
|
#include <fstream>
|
|
// CoreSdk_ShutDown
|
|
#include "ManusSDK.h"
|
|
// std::map
|
|
#include <map>
|
|
// Ncurses terminal functions.
|
|
#include <ncursesw/ncurses.h>
|
|
// Termios terminal functions.
|
|
#include <termios.h>
|
|
#include <cstring>
|
|
#include <unistd.h>
|
|
// spdlog replacement
|
|
#include "ClientLogging.hpp"
|
|
|
|
using namespace ManusSDK;
|
|
|
|
/// @brief Reset a signal handler to its default, and then call it.
|
|
/// For signal types and explanation, see:
|
|
/// https://www.gnu.org/software/libc/manual/html_node/Standard-Signals.html
|
|
#define CALL_DEFAULT_SIGNAL_HANDLER(p_SignalType) \
|
|
/* Reset the handler for this signal to the default. */ \
|
|
signal(p_SignalType, SIG_DFL); \
|
|
/* Re-raise this signal, causing the normal handler to run. */ \
|
|
raise(p_SignalType);
|
|
|
|
const std::string SDKClientPlatformSpecific::s_SlashForFilesystemPath = "/";
|
|
|
|
/// @brief Handle a signal telling the SDK client to quit.
|
|
/// A generic signal used to "politely ask a program to terminate".
|
|
/// On Linux, this can be sent by using the Gnome System Monitor and telling
|
|
/// the SDK client process to end.
|
|
static void HandleTerminationSignal(int p_Parameter)
|
|
{
|
|
ClientLog::error(
|
|
"Termination signal sent with parameter {}.",
|
|
p_Parameter);
|
|
|
|
CoreSdk_ShutDown();
|
|
|
|
CALL_DEFAULT_SIGNAL_HANDLER(SIGTERM);
|
|
}
|
|
|
|
/// @brief Handle an interrupt signal.
|
|
/// Called when the INTR character is typed - usually ctrl + c.
|
|
static void HandleInterruptSignal(int p_Parameter)
|
|
{
|
|
ClientLog::error(
|
|
"Interrupt signal sent with parameter {}.",
|
|
p_Parameter);
|
|
|
|
CoreSdk_ShutDown();
|
|
|
|
CALL_DEFAULT_SIGNAL_HANDLER(SIGINT);
|
|
}
|
|
|
|
/// @brief Handle a quit signal.
|
|
/// Called when the QUIT character is typed - usually ctrl + \.
|
|
static void HandleQuitSignal(int p_Parameter)
|
|
{
|
|
ClientLog::error(
|
|
"Quit signal sent with parameter {}.",
|
|
p_Parameter);
|
|
|
|
CoreSdk_ShutDown();
|
|
|
|
CALL_DEFAULT_SIGNAL_HANDLER(SIGQUIT);
|
|
}
|
|
|
|
/// @brief Handle a hangup signal.
|
|
/// Called to report that the user's terminal has disconnected.
|
|
/// This can happen when connecting over the network, for example.
|
|
/// It also happens if the terminal window is closed while debugging.
|
|
static void HandleHangupSignal(int p_Parameter)
|
|
{
|
|
ClientLog::error(
|
|
"Hang-up signal sent with parameter {}.",
|
|
p_Parameter);
|
|
|
|
CoreSdk_ShutDown();
|
|
|
|
CALL_DEFAULT_SIGNAL_HANDLER(SIGHUP);
|
|
}
|
|
|
|
/// @brief Initialise Ncurses so that we can check for input.
|
|
static bool InitializeNcurses(void)
|
|
{
|
|
const WINDOW* const t_Window = initscr();
|
|
if (!t_Window)
|
|
{
|
|
ClientLog::error("Failed to initialise the screen.");
|
|
|
|
return false;
|
|
}
|
|
|
|
// Don't buffer input until a newline or carriage return is typed.
|
|
if (cbreak() != OK)
|
|
{
|
|
ClientLog::error("Failed to make input unbuffered.");
|
|
|
|
return false;
|
|
}
|
|
|
|
// Don't echo input.
|
|
if (noecho() != OK)
|
|
{
|
|
ClientLog::error("Failed to disable input echoing.");
|
|
|
|
return false;
|
|
}
|
|
|
|
// Don't make newlines when the return key is pressed.
|
|
if (nonl() != OK)
|
|
{
|
|
ClientLog::error("Failed to disable newlines.");
|
|
|
|
return false;
|
|
}
|
|
|
|
// Do not flush the screen when interrupt/break/quit is pressed.
|
|
if (intrflush(stdscr, FALSE) != OK)
|
|
{
|
|
ClientLog::error("Failed to disable screen flushing.");
|
|
|
|
return false;
|
|
}
|
|
|
|
// Make getch non-blocking.
|
|
if (nodelay(stdscr, TRUE) != OK)
|
|
{
|
|
ClientLog::error("Failed to make nodelay non-blocking.");
|
|
|
|
return false;
|
|
}
|
|
|
|
// Enable handling the keypad ("function keys" like the arrow keys).
|
|
if (keypad(stdscr, TRUE) != OK)
|
|
{
|
|
ClientLog::error("Failed to enable keypad input.");
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// @brief Initialise Termios so that terminal output is correct.
|
|
static bool InitializeTermios(void)
|
|
{
|
|
// For some reason, printf and spdlog strings require a carriage return
|
|
// in addition to a newline after running initscr().
|
|
// This code sets the "output modes" flag to treat newline characters
|
|
// as newline+carriage-return characters.
|
|
// https://arstechnica.com/civis/viewtopic.php?t=70699
|
|
termios t_Settings;
|
|
if (tcgetattr(STDIN_FILENO, &t_Settings) != 0)
|
|
{
|
|
ClientLog::error("Failed to get Termios settings.");
|
|
|
|
return false;
|
|
}
|
|
|
|
t_Settings.c_oflag |= ONLCR;
|
|
if (tcsetattr(0, TCSANOW, &t_Settings) != 0)
|
|
{
|
|
ClientLog::error("Failed to set Termios settings.");
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// @brief Register our signal handling functions.
|
|
static bool SetUpSignalHandlers(void)
|
|
{
|
|
{
|
|
const __sighandler_t t_OldTerminationHandler = signal(
|
|
SIGTERM,
|
|
HandleTerminationSignal);
|
|
if (t_OldTerminationHandler == SIG_ERR)
|
|
{
|
|
ClientLog::error("Failed to set termination signal handler.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
{
|
|
const __sighandler_t t_OldInterruptHandler = signal(
|
|
SIGINT,
|
|
HandleInterruptSignal);
|
|
if (t_OldInterruptHandler == SIG_ERR)
|
|
{
|
|
ClientLog::error("Failed to set interrupt signal handler.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
{
|
|
const __sighandler_t t_OldQuitHandler = signal(
|
|
SIGQUIT,
|
|
HandleQuitSignal);
|
|
if (t_OldQuitHandler == SIG_ERR)
|
|
{
|
|
ClientLog::error("Failed to set quit signal handler.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
{
|
|
const __sighandler_t t_OldHangupHandler = signal(
|
|
SIGHUP,
|
|
HandleHangupSignal);
|
|
if (t_OldHangupHandler == SIG_ERR)
|
|
{
|
|
ClientLog::error("Failed to set hang-up signal handler.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// @brief Handles keyboard input.
|
|
/// Requires things to be set up with Ncurses and Termios.
|
|
class ClientInput
|
|
{
|
|
public:
|
|
/// @brief Update the state of the keyboard.
|
|
void Update(void)
|
|
{
|
|
// Reset the state of the last update.
|
|
for (
|
|
InputMap_t::iterator t_Key = m_PressedLastUpdate.begin();
|
|
t_Key != m_PressedLastUpdate.end();
|
|
t_Key++)
|
|
{
|
|
t_Key->second = false;
|
|
}
|
|
|
|
// Copy the current state to the last update's state, and clear the
|
|
// current state.
|
|
for (
|
|
InputMap_t::iterator t_Key = m_CurrentlyPressed.begin();
|
|
t_Key != m_CurrentlyPressed.end();
|
|
t_Key++)
|
|
{
|
|
m_PressedLastUpdate[t_Key->first] =
|
|
t_Key->second;
|
|
t_Key->second = false;
|
|
}
|
|
|
|
// Get the new state.
|
|
int t_Ch = getch();
|
|
while (t_Ch != ERR)
|
|
{
|
|
if (t_Ch >= 'a' && t_Ch <= 'z')
|
|
{
|
|
// Unlike with Windows' GetAsyncKeyState(), upper case and
|
|
// lower case characters have different key numbers with
|
|
// getch().
|
|
// Since all WasKeyPressed calls (as of writing this) use
|
|
// upper case, lower case keys need to be converted to work
|
|
// on Linux.
|
|
// Note that this does break the ability to check for lower
|
|
// case key presses.
|
|
t_Ch = toupper(t_Ch);
|
|
}
|
|
|
|
m_CurrentlyPressed[t_Ch] = true;
|
|
|
|
t_Ch = getch();
|
|
}
|
|
}
|
|
|
|
/// @brief Get the key's current state.
|
|
/// Note that unlike IsPressed(), this also stores the result for use in
|
|
/// the next key state check.
|
|
bool GetKey(const int p_Key)
|
|
{
|
|
bool t_IsPressed = IsPressed(p_Key);
|
|
m_PreviousKeyState[p_Key] = t_IsPressed;
|
|
|
|
return t_IsPressed;
|
|
}
|
|
|
|
/// @brief Was this key pressed since the last check?
|
|
/// Note that unlike WasJustPressed(), this checks if the key was pressed
|
|
/// since the last time a GetKey* function was called.
|
|
bool GetKeyDown(const int p_Key)
|
|
{
|
|
const bool t_IsPressed = IsPressed(p_Key);
|
|
|
|
const auto t_PreviousState = m_PreviousKeyState.find(p_Key);
|
|
const bool t_PreviousValue =
|
|
t_PreviousState == m_PreviousKeyState.end()
|
|
? false
|
|
: t_PreviousState->second;
|
|
|
|
const bool t_Down = t_IsPressed && !t_PreviousValue;
|
|
m_PreviousKeyState[p_Key] = t_Down;
|
|
|
|
return t_Down;
|
|
}
|
|
|
|
/// @brief Was this key released since the last check?
|
|
/// Note that unlike WasJustReleased(), this checks if the key was released
|
|
/// since the last time a GetKey* function was called.
|
|
bool GetKeyUp(const int p_Key)
|
|
{
|
|
const bool t_IsPressed = IsPressed(p_Key);
|
|
|
|
const auto t_PreviousState = m_PreviousKeyState.find(p_Key);
|
|
const bool t_PreviousValue =
|
|
t_PreviousState == m_PreviousKeyState.end()
|
|
? false
|
|
: t_PreviousState->second;
|
|
|
|
const bool t_Up = !t_IsPressed && t_PreviousValue;
|
|
m_PreviousKeyState[p_Key] = t_Up;
|
|
|
|
return t_Up;
|
|
}
|
|
|
|
private:
|
|
/// @brief Get the key's current state.
|
|
/// Note that unlike GetKey(), this does not store the result for use in
|
|
/// the next key state check.
|
|
bool IsPressed(const int p_Key) const
|
|
{
|
|
auto t_CurrentlyPressed = m_CurrentlyPressed.find(p_Key);
|
|
if (t_CurrentlyPressed == m_CurrentlyPressed.end())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return t_CurrentlyPressed->second;
|
|
}
|
|
|
|
/// @brief Was this key pressed since the last input update?
|
|
/// Note that unlike GetKeyDown(), this function will return the same value
|
|
/// until the next keyboard state update.
|
|
bool WasJustPressed(const int p_Key) const
|
|
{
|
|
return !WasPressedLastUpdate(p_Key) && IsPressed(p_Key);
|
|
}
|
|
|
|
/// @brief Was this key released since the last input update?
|
|
/// Note that unlike GetKeyUp(), this function will return the same value
|
|
/// until the next keyboard state update.
|
|
bool WasJustReleased(const int p_Key) const
|
|
{
|
|
return WasPressedLastUpdate(p_Key) && !IsPressed(p_Key);
|
|
}
|
|
|
|
/// @brief Was this key pressed the previous input update?
|
|
bool WasPressedLastUpdate(const int p_Key) const
|
|
{
|
|
auto t_StateLastUpdate = m_PressedLastUpdate.find(p_Key);
|
|
if (t_StateLastUpdate == m_PressedLastUpdate.end())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return t_StateLastUpdate->second;
|
|
}
|
|
|
|
typedef std::map<int, bool> InputMap_t;
|
|
InputMap_t m_CurrentlyPressed;
|
|
InputMap_t m_PressedLastUpdate;
|
|
// The Windows GetKey* functions only update the key state when a GetKey*
|
|
// function gets called. To make the Linux input work the same way, this
|
|
// map is used.
|
|
InputMap_t m_PreviousKeyState;
|
|
};
|
|
|
|
static ClientInput g_Input;
|
|
|
|
bool SDKClientPlatformSpecific::PlatformSpecificInitialization(void)
|
|
{
|
|
const bool t_NcursesResult = InitializeNcurses();
|
|
const bool t_TermiosResult = InitializeTermios();
|
|
const bool t_SignalResult = SetUpSignalHandlers();
|
|
|
|
return t_NcursesResult && t_TermiosResult && t_SignalResult;
|
|
}
|
|
|
|
bool SDKClientPlatformSpecific::PlatformSpecificShutdown(void)
|
|
{
|
|
// Ncurses.
|
|
endwin();
|
|
|
|
return true;
|
|
}
|
|
|
|
void SDKClientPlatformSpecific::UpdateInput(void)
|
|
{
|
|
g_Input.Update();
|
|
}
|
|
|
|
/*static*/ bool SDKClientPlatformSpecific::CopyString(
|
|
char* const p_Target,
|
|
const size_t p_MaxLengthThatWillFitInTarget,
|
|
const std::string& p_Source)
|
|
{
|
|
if (!p_Target)
|
|
{
|
|
ClientLog::error(
|
|
"Tried to copy a string, but the target was null. The string was \"{}\".",
|
|
p_Source.c_str());
|
|
|
|
return false;
|
|
}
|
|
|
|
if (p_MaxLengthThatWillFitInTarget == 0)
|
|
{
|
|
ClientLog::error(
|
|
"Tried to copy a string, but the target's size is zero. The string was \"{}\".",
|
|
p_Source.c_str());
|
|
|
|
return false;
|
|
}
|
|
|
|
if (p_MaxLengthThatWillFitInTarget <= p_Source.length())
|
|
{
|
|
ClientLog::error(
|
|
"Tried to copy a string that was longer than {} characters, which makes it too big for its target buffer. The string was \"{}\".",
|
|
p_MaxLengthThatWillFitInTarget,
|
|
p_Source.c_str());
|
|
|
|
return false;
|
|
}
|
|
|
|
strcpy(p_Target, p_Source.c_str());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SDKClientPlatformSpecific::ResizeWindow(
|
|
const short int p_ConsoleWidth,
|
|
const short int p_ConsoleHeight,
|
|
const short int p_ConsoleScrollback)
|
|
{
|
|
// https://apple.stackexchange.com/questions/33736/can-a-terminal-window-be-resized-with-a-terminal-command/47841#47841
|
|
// Use a control sequence to resize the window.
|
|
// Seems to be supported by the default terminal used in Gnome, as well
|
|
// as Mac OS.
|
|
// \\e[ -> ASCII ESC character (number 27, or 0x1B)
|
|
// -> control sequence introducer
|
|
// 8; -> resize the window
|
|
// y;xt -> The first number is the height, the second the width.
|
|
// Scrollback can't and doesn't need to be set here for Linux.
|
|
printf("\e[8;%d;%dt", p_ConsoleHeight, p_ConsoleWidth);
|
|
|
|
ClearConsole();
|
|
|
|
// None of the ncurses functions for resizing the terminal actually
|
|
// seem to do anything.
|
|
/*if (resizeterm(180, 180) != OK)
|
|
{
|
|
return false;
|
|
}*/
|
|
|
|
return true;
|
|
}
|
|
|
|
void SDKClientPlatformSpecific::ApplyConsolePosition(
|
|
const int p_ConsoleCurrentOffset)
|
|
{
|
|
printf("\e[%d;1H", p_ConsoleCurrentOffset);
|
|
}
|
|
|
|
/*static*/ void SDKClientPlatformSpecific::ClearConsole(void)
|
|
{
|
|
// https://stackoverflow.com/questions/4062045/clearing-terminal-in-linux-with-c-code
|
|
// Use a control sequence to clear the terminal and move the cursor.
|
|
// Seems to be supported by the default terminal used in Gnome,
|
|
// as well as Mac OS.
|
|
// \\e[ -> ASCII ESC character (number 27, or 0x1B) -> control sequence introducer
|
|
// 2 -> the entire screen
|
|
// J -> clear the screen
|
|
//printf("\e[2J\n");
|
|
|
|
// Move the cursor to row 1 column 1.
|
|
//printf("\e[1;1H\n");
|
|
|
|
if (clear() != OK)
|
|
{
|
|
ClientLog::error("Failed to clear the screen.");
|
|
}
|
|
|
|
refresh();
|
|
}
|
|
|
|
bool SDKClientPlatformSpecific::GetKey(const int p_Key)
|
|
{
|
|
return g_Input.GetKey(p_Key);
|
|
}
|
|
|
|
bool SDKClientPlatformSpecific::GetKeyDown(const int p_Key)
|
|
{
|
|
return g_Input.GetKeyDown(p_Key);
|
|
}
|
|
|
|
bool SDKClientPlatformSpecific::GetKeyUp(const int p_Key)
|
|
{
|
|
return g_Input.GetKeyUp(p_Key);
|
|
}
|
|
|
|
std::string SDKClientPlatformSpecific::GetDocumentsDirectoryPath_UTF8(void)
|
|
{
|
|
const char* const t_Xdg = getenv("XDG_DOCUMENTS_DIR");
|
|
|
|
// Backup - the documents folder is usually going to be in $HOME/Documents.
|
|
const char* const t_Home = getenv("HOME");
|
|
if (!t_Xdg && !t_Home)
|
|
{
|
|
return std::string("");
|
|
}
|
|
|
|
const std::string t_DocumentsDir =
|
|
(!t_Xdg || strlen(t_Xdg) == 0)
|
|
? std::string(t_Home) + std::string("/Documents")
|
|
: std::string(t_Xdg);
|
|
|
|
return t_DocumentsDir;
|
|
}
|
|
|
|
std::ifstream SDKClientPlatformSpecific::GetInputFileStream(
|
|
std::string p_Path_UTF8)
|
|
{
|
|
return std::ifstream(p_Path_UTF8, std::ifstream::binary);
|
|
}
|
|
|
|
std::ofstream SDKClientPlatformSpecific::GetOutputFileStream(
|
|
std::string p_Path_UTF8)
|
|
{
|
|
return std::ofstream(p_Path_UTF8, std::ofstream::binary);
|
|
}
|
|
|
|
bool SDKClientPlatformSpecific::DoesFolderOrFileExist(std::string p_Path_UTF8)
|
|
{
|
|
return std::filesystem::exists(p_Path_UTF8);
|
|
}
|
|
|
|
void SDKClientPlatformSpecific::CreateFolderIfItDoesNotExist(
|
|
std::string p_Path_UTF8)
|
|
{
|
|
if (!DoesFolderOrFileExist(p_Path_UTF8))
|
|
{
|
|
std::filesystem::create_directory(p_Path_UTF8);
|
|
}
|
|
}
|