add support for translations; basic terminal functions and splash screen

- create I18n class for translations
- create TextDisplay class to draw on console, and to initialize and reset console configuration
- create stub for future command-line option handling
This commit is contained in:
Slavasil 2025-03-23 19:46:25 +03:00
parent 609de00254
commit f2653e66d4
5 changed files with 247 additions and 2 deletions

View File

@ -0,0 +1,7 @@
namespace Game {
public struct CommandLineOptions {
public static CommandLineOptions Parse(string[] args) {
return new CommandLineOptions();
}
}
}

46
App/I18n.cs Normal file
View File

@ -0,0 +1,46 @@
namespace Game {
public class I18n {
public static readonly string LANGUAGE = "ru-RU";
static readonly Dictionary<string, string[]> LANGUAGES = new Dictionary<string, string[]>() {
{"en-US", new string[] {
"No answers added for question",
"Expected `=>`",
"Expected `True` or `False`",
"Expected `||` between answers",
"The output is not a terminal",
"Syntax error in the question database: {0} ({1}:{2}:{3})",
"Syntax error in the question database: {0} (line {1} column {2})",
"Cannot get console mode",
"Cannot set console mode",
}},
{"ru-RU", new string[] {
"Не указано ни одного варианта ответа на вопрос",
"Ожидалось `=>`",
"Ожидалось `True` или `False`",
"Ожидалось `||` между вариантами ответа",
"Поток вывода не идёт в терминал",
"Ошибка синтаксиса в банке вопросов: {0} ({1}:{2}:{3})",
"Ошибка синтаксиса в банке вопросов: {0} (строка {1} символ {2})",
"Не удалось узнать режим консоли",
"Не удалось установить режим консоли",
}}
};
public static string? GetMessage(Message id) {
return LANGUAGES[LANGUAGE][(int)id];
}
public enum Message {
ERROR_NO_ANSWER_OPTIONS = 0,
ERROR_EXPECTED_ARROW_TOKEN,
ERROR_EXPECTED_TRUE_OR_FALSE,
ERROR_EXPECTED_ANSWER_SEPARATOR,
OUTPUT_IS_NOT_TERMINAL,
SYNTAX_ERROR_WITH_FILENAME,
SYNTAX_ERROR,
ERROR_GETCONSOLEMODE,
ERROR_SETCONSOLEMODE,
}
}
}

View File

@ -1,7 +1,23 @@
namespace Game {
using Game.UI;
namespace Game {
internal class Program {
static int Main(string[] args) {
CommandLineOptions opts = CommandLineOptions.Parse(args);
TextDisplay display = new TextDisplay();
if (!display.InitWindow()) {
return 1;
}
display.PrintSimpleWelcome();
Thread.Sleep(1200);
display.ShowSplash();
Thread.Sleep(2500);
display.ResetWindow();
return 0;
}
}
}
}

4
App/UI/Colors.cs Normal file
View File

@ -0,0 +1,4 @@
namespace App.UI {
internal class Colors {
}
}

172
App/UI/TextDisplay.cs Normal file
View File

@ -0,0 +1,172 @@
using static Game.I18n;
using System.Runtime.InteropServices;
using System.Text;
namespace Game.UI {
public class TextDisplay {
private const string RGB_FG_SEQ_START = "\e[38;2;";
private const string RGB_BG_SEQ_START = "\e[48;2;";
private const string ESC_SEQ_START = "\e[";
private const string SETCURSOR_END = "H";
private const string GRAPHICS_SEQ_END = "m";
private const string HIDECURSOR_SEQ = "\e[?25l";
private const string SHOWCURSOR_SEQ = "\e[?25h";
private int currentFgColor = 0xffffff, currentBgColor = 0;
private int cursorRow = 0, cursorCol = 0;
private int windowWidth = 0, windowHeight = 0;
public bool InitWindow() {
if (Console.IsOutputRedirected) {
Console.WriteLine(GetMessage(Message.OUTPUT_IS_NOT_TERMINAL));
return false;
}
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
IntPtr consoleInHandle = Win32Imports.GetStdHandle(Win32Imports.STD_INPUT_HANDLE);
IntPtr consoleOutHandle = Win32Imports.GetStdHandle(Win32Imports.STD_OUTPUT_HANDLE);
uint inConsoleMode, outConsoleMode;
if (!Win32Imports.GetConsoleMode(consoleInHandle, out inConsoleMode)) {
Console.WriteLine(GetMessage(Message.ERROR_GETCONSOLEMODE));
return false;
}
if (!Win32Imports.GetConsoleMode(consoleOutHandle, out outConsoleMode)) {
Console.WriteLine(GetMessage(Message.ERROR_GETCONSOLEMODE));
return false;
}
inConsoleMode |= Win32Imports.ENABLE_VIRTUAL_TERMINAL_INPUT;
//inConsoleMode &= ~Win32Imports.ENABLE_LINE_INPUT;
outConsoleMode |= Win32Imports.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if (!Win32Imports.SetConsoleMode(consoleInHandle, inConsoleMode)) {
Console.WriteLine(GetMessage(Message.ERROR_SETCONSOLEMODE));
return false;
}
if (!Win32Imports.SetConsoleMode(consoleOutHandle, outConsoleMode)) {
Console.WriteLine(GetMessage(Message.ERROR_SETCONSOLEMODE));
return false;
}
SetColors(currentFgColor, currentBgColor);
HideCursor();
SetCursor(cursorRow, cursorCol);
windowWidth = Console.BufferWidth;
windowHeight = Console.BufferHeight;
return true;
}
public void ResetWindow() {
SetColors(0xffffff, 0);
SetCursor(1, 1);
ShowCursor();
}
public void PrintSimpleWelcome() {
WriteColored("Welcome to КТО ХОЧЕТ СТАТЬ СЕКУНДОМЕРОМ", 0x5e61ed);
}
public void ShowSplash() {
ClearWithColor(0x5e61ed);
DrawTitle(windowHeight / 2 + 1 - 3, 0x5e61ed);
DrawCopyright(0x5e61ed);
}
void DrawTitle(int y, int bgColor) {
string text1 = "КТО ХОЧЕТ СТАТЬ";
string text2 = "СЕКУНДОМЕРОМ";
SetColors(0xffffff, 0);
int text1Start = (windowWidth - text1.Length) / 2 + 1;
SetCursor(y, text1Start - 6);
for (int i = 0; i < 6; ++i) {
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
}
WriteColored(text1, 0xffffff, 0);
for (int i = 6; i >= 0; --i) {
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
}
int text2Start = (windowWidth - text2.Length) / 2 + 1;
SetCursor(y + 2, text2Start - 6);
for (int i = 0; i < 6; ++i) {
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
}
WriteColored(text2, 0xffffff, 0);
for (int i = 6; i >= 0; --i) {
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
}
}
void DrawCopyright(int bgColor) {
string bottomText = "(c) subvia, 2025";
SetCursor(windowHeight, (windowWidth - bottomText.Length) / 2 + 1);
WriteColored(bottomText, 0x4d4dc0, bgColor);
}
public void WriteColored(string s, int color, int bgColor = -1) {
if (color == -1) color = currentFgColor;
(int r, int g, int b) = ((color >> 16) & 255, (color >> 8) & 255, color & 255);
currentFgColor = color;
if (bgColor == -1) {
Console.Write(RGB_FG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END + s);
} else {
(int br, int bg, int bb) = ((bgColor >> 16) & 255, (bgColor >> 8) & 255, bgColor & 255);
Console.Write(RGB_FG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END + RGB_BG_SEQ_START + br + ';' + bg + ';' + bb + GRAPHICS_SEQ_END + s);
currentBgColor = bgColor;
}
}
public void SetColors(int fg, int bg) {
StringBuilder command = new StringBuilder();
if (fg != -1) {
(int r, int g, int b) = ((fg >> 16) & 255, (fg >> 8) & 255, fg & 255);
command.Append(RGB_FG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END);
}
if (bg != -1) {
(int r, int g, int b) = ((bg >> 16) & 255, (bg >> 8) & 255, bg & 255);
command.Append(RGB_BG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END);
}
Console.Write(command.ToString());
}
public void ClearWithColor(int color) {
(int r, int g, int b) = ((color >> 16) & 255, (color >> 8) & 255, color & 255);
SetColors(-1, color);
Console.Write("\e[2J");
}
public void SetCursor(int row, int column) {
Console.Write(ESC_SEQ_START + row + ';' + column + SETCURSOR_END);
}
public void HideCursor() {
Console.Write(HIDECURSOR_SEQ);
}
public void ShowCursor() {
Console.Write(SHOWCURSOR_SEQ);
}
public int ColorBlend(int fg, int bg, float alpha) {
//return (int)(fg * alpha + bg * (1f - alpha));
return ((int)(((fg >> 16) & 255) * alpha + ((bg >> 16) & 255) * (1 - alpha)) << 16) + ((int)(((fg >> 8) & 255) * alpha + ((bg >> 8) & 255) * (1 - alpha)) << 8) + (int)((fg & 255) * alpha + (bg & 255) * (1 - alpha));
}
}
static class Win32Imports
{
public const int STD_INPUT_HANDLE = -10;
public const int STD_OUTPUT_HANDLE = -11;
public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
public const uint ENABLE_LINE_INPUT = 2;
public const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(int stdHandle);
[DllImport("kernel32.dll")]
public static extern bool SetConsoleMode(IntPtr consoleHandle, uint mode);
[DllImport("kernel32.dll")]
public static extern bool GetConsoleMode(IntPtr consoleHandle, out uint mode);
}
}