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 readonly char[] SIMPLE_BOX_CHARS = { '─', '│', '┌', '┐', '└', '┘' }; private readonly char[] DOUBLE_BOX_CHARS = { '═', '║', '╔', '╗', '╚', '╝' }; private readonly char[] FALLBACK_BOX_CHARS = { '-', '|', '+', '+', '+', '+' }; 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); } public void ShowFatalError(string text) { ClearWithColor(0xfc5d2d); DrawTextBox(HAlignment.Center, VAlignment.Center, BoxType.Double, windowHeight / 2 + 1, windowWidth / 2 + 1, text, 0xffffff, 0xffffff, 0xfc5d2d, windowWidth / 2); } 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); } void DrawTextBox(HAlignment hAlign, VAlignment vAlign, BoxType boxType, int y, int x, string text, int borderColor, int textColor, int bgColor = -1, int preferredWidth = -1) { int maxWidth = windowWidth; switch (hAlign) { case HAlignment.Left: maxWidth = windowWidth - x + 1; break; case HAlignment.Center: maxWidth = Math.Min(x - 1, windowWidth - x + 1) * 2; break; case HAlignment.Right: maxWidth = x - 1; break; } int maxHeight = windowHeight; switch (vAlign) { case VAlignment.Top: maxHeight = windowHeight - y + 1; break; case VAlignment.Center: maxHeight = Math.Min(y - 1, windowHeight - y + 1) * 2; break; case VAlignment.Bottom: maxHeight = y - 1; break; } string[] lines = WordWrap(text, (preferredWidth == -1 ? maxWidth : preferredWidth) - 4, out bool overflow); bool reflowed = false; if (preferredWidth != -1 && lines.Length > maxHeight - 4) { reflowed = true; lines = WordWrap(text, maxWidth - 4, out overflow); } int actualWidth = Math.Min(preferredWidth == -1 ? maxWidth : (reflowed ? maxWidth : preferredWidth), lines.Max(s => s.Length) + 4); int actualHeight = Math.Min(maxHeight, lines.Length + 4); int left = hAlign == HAlignment.Left ? x : (hAlign == HAlignment.Right ? x - actualWidth + 1 : x - (actualWidth - 1) / 2); int top = vAlign == VAlignment.Top ? y : (vAlign == VAlignment.Bottom ? y - actualHeight + 1 : y - (actualHeight - 1) / 2); DrawBox(boxType, top, left, actualHeight, actualWidth, borderColor, bgColor); SetColors(textColor, -1); for (int i = 0; i < actualHeight - 4; ++i) { SetCursor(top + 2 + i, left + 2); WriteColored(lines[i], textColor, bgColor); } } public void DrawBox(BoxType boxType, int y, int x, int h, int w, int borderColor, int bgColor = -1) { char[] boxChars = boxType == BoxType.Simple ? SIMPLE_BOX_CHARS : (boxType == BoxType.Double ? DOUBLE_BOX_CHARS : FALLBACK_BOX_CHARS); SetColors(borderColor, bgColor); SetCursor(y, x); StringBuilder output = new StringBuilder(); output.Append(boxChars[2]); output.Append(boxChars[0], w - 2); output.Append(boxChars[3]); WriteColored(output.ToString(), borderColor, bgColor); for (int i = 0; i < h - 2; ++i) { output.Clear(); SetCursor(y + 1 + i, x); output.Append(boxChars[1]); output.Append(' ', w - 2); output.Append(boxChars[1]); WriteColored(output.ToString(), borderColor, bgColor); } SetCursor(y + h - 1, x); output.Clear(); output.Append(boxChars[4]); output.Append(boxChars[0], w - 2); output.Append(boxChars[5]); WriteColored(output.ToString(), borderColor, 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)); } public string[] WordWrap(string text, int width, out bool overflow) { overflow = false; List lines = new List(); StringBuilder line = new StringBuilder(text.Length / width * 4 / 3); int remaining = width; foreach (string word in text.Split(new char[] {' ', '\n'}, StringSplitOptions.RemoveEmptyEntries)) { if (word.Length <= remaining) { if (line.Length == 0 || line[^1] != ' ') { line.Append(' '); remaining -= 1; } line.Append(word); remaining -= word.Length; } else { lines.Add(line.ToString()); line.Clear(); line.Append(word); remaining = width - word.Length; if (remaining < 0) { overflow = true; line.Remove(width + 1, line.Length - (width + 1)); lines.Add(line.ToString()); line.Clear(); } } } if (line.Length > 0) lines.Add(line.ToString()); return lines.ToArray(); } public enum HAlignment { Left, Center, Right } public enum VAlignment { Top, Center, Bottom } public enum BoxType { Simple, Double, Fallback, } } 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); } }