diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bc91eb12e..20ba884a9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1072,6 +1072,7 @@ set (PCH_SOURCES common/utility/name.cpp common/utility/r_memory.cpp common/utility/writezip.cpp + common/utility/vga2ansi.cpp common/thirdparty/base64.cpp common/thirdparty/md5.cpp common/thirdparty/superfasthash.cpp diff --git a/src/common/console/c_enginecmds.cpp b/src/common/console/c_enginecmds.cpp index d6a2e027e..36a27cadf 100644 --- a/src/common/console/c_enginecmds.cpp +++ b/src/common/console/c_enginecmds.cpp @@ -58,13 +58,16 @@ extern FILE* Logfile; +void ConsoleEndoom(); CCMD (quit) { + ConsoleEndoom(); throw CExitEvent(0); } CCMD (exit) { + ConsoleEndoom(); throw CExitEvent(0); } diff --git a/src/common/startscreen/endoom.cpp b/src/common/startscreen/endoom.cpp index 6e8e7af35..1dd4c7dde 100644 --- a/src/common/startscreen/endoom.cpp +++ b/src/common/startscreen/endoom.cpp @@ -73,6 +73,12 @@ CUSTOM_CVAR(Int, showendoom, 1, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) else if (self > 2) self=2; } +CVAR(Bool, consoleendoom, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) + +#ifdef _WIN32 +extern bool FancyStdOut; +#endif + // PRIVATE DATA DEFINITIONS ------------------------------------------------ // CODE -------------------------------------------------------------------- @@ -192,9 +198,36 @@ int RunEndoom() return 0; } +void vga_to_ansi(const uint8_t *buf); + +void ConsoleEndoom() +{ +#ifdef _WIN32 + // old versions of Windows don't get an ansi endoom + if (!FancyStdOut) + return; +#endif + + if (!consoleendoom || endoomName.Len() == 0) + return; + + uint8_t buffer[4000]; + + int endoom_lump = fileSystem.CheckNumForFullName (endoomName.GetChars(), true); + + if (endoom_lump < 0 || fileSystem.FileLength (endoom_lump) != 4000) + { + return; + } + fileSystem.ReadFile(endoom_lump, buffer); + + vga_to_ansi(buffer); +} + [[noreturn]] void ST_Endoom() { + ConsoleEndoom(); int code = RunEndoom(); throw CExitEvent(code); } diff --git a/src/common/startscreen/startscreen.cpp b/src/common/startscreen/startscreen.cpp index ccca6d906..e105cedc1 100644 --- a/src/common/startscreen/startscreen.cpp +++ b/src/common/startscreen/startscreen.cpp @@ -725,3 +725,33 @@ void FStartScreen::ValidateTexture() } } +void unicode_to_utf8(char* result, uint16_t code_point) +{ + if (code_point <= 0x7F) + { + // 1-byte sequence (ASCII) + result[0] = (unsigned char)code_point; + result[1] = 0; + } + else if (code_point <= 0x7FF) + { + // 2-byte sequence + result[0] = (unsigned char)(0xC0 | ((code_point >> 6) & 0x1F)); + result[1] = (unsigned char)(0x80 | (code_point & 0x3F)); + result[2] = 0; + } + else + { + // 3-byte sequence (up to U+FFFF) + result[0] = (unsigned char)(0xE0 | ((code_point >> 12) & 0x0F)); + result[1] = (unsigned char)(0x80 | ((code_point >> 6) & 0x3F)); + result[2] = (unsigned char)(0x80 | (code_point & 0x3F)); + result[3] = 0; + } +} + +void ibm437_to_utf8(char* result, char in) +{ + unicode_to_utf8(result, IBM437ToUnicode[(unsigned char)in]); +} + diff --git a/src/common/utility/vga2ansi.cpp b/src/common/utility/vga2ansi.cpp new file mode 100644 index 000000000..6553e6e3b --- /dev/null +++ b/src/common/utility/vga2ansi.cpp @@ -0,0 +1,158 @@ +// Copyright (c) 2025 Rachael Alexanderson +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include +#include +#include +#include + +#ifdef _WIN32 +// windows platform code uses special stdout handling, let's make sure to use that +#include +extern HANDLE StdOut; +static void CPrint(const char* in) +{ + DWORD bytes_written; + if (!StdOut) + return; + WriteFile(StdOut, in, strlen(in), &bytes_written, NULL); +} +#else +static void CPrint(const char* in) +{ + fputs(in, stdout); +} +#endif + +static const char *ansi_esc[2] = {"\x1b[", ";"}; +static const char *ansi_end[2] = {"", "m"}; + +// Map DOS color to ANSI escape code +static const char *ansi_fg[16] = +{ + "30", "34", "32", "36", "31", "35", "33", "37", + "90", "94", "92", "96", "91", "95", "93", "97" +}; +// Only standard backgrounds (no bright backgrounds in classic ANSI) +static const char *ansi_bg[8] = +{ + "40", "44", "42", "46", + "41", "45", "43", "47" +}; +// ANSI codes for truecolor DOS colors +static const char *ansi_tc_fg[16] = +{ + "38;2;0;0;0", "38;2;0;0;170", "38;2;0;170;0", "38;2;0;170;170", + "38;2;170;0;0", "38;2;170;0;170", "38;2;170;85;0", "38;2;170;170;170", + "38;2;85;85;85", "38;2;85;85;255", "38;2;85;255;0", "38;2;85;255;255", + "38;2;255;85;85", "38;2;255;85;255", "38;2;255;255;85", "38;2;255;255;255" +}; +static const char *ansi_tc_bg[8] = +{ + "48;2;0;0;0", "48;2;0;0;170", "48;2;0;170;0", "48;2;0;170;170", + "48;2;170;0;0", "48;2;170;0;170", "48;2;170;85;0", "48;2;170;170;170" +}; + +static const char *ansi_flash[2] = { "25", "5" }; + +inline void ansi_ctrl(bool open) +{ + static bool is_ansi_open = false; + if (open) + CPrint(ansi_esc[is_ansi_open]); + else + CPrint(ansi_end[is_ansi_open]); + is_ansi_open = open; +} + +void ibm437_to_utf8(char* result, char in); + +void vga_to_ansi(const uint8_t *buf) +{ +#ifdef _WIN32 + bool truecolor = true; +#else + const char *ct = getenv("COLORTERM"); + bool truecolor = ct && (strcmp(ct, "truecolor") || strcmp(ct, "24bit")); +#endif + + for (int row = 0; row < 25; ++row) + { + int last_fg = -1, last_bg = -1; + bool last_blink = false; + for (int col = 0; col < 80; ++col) + { + int off = (row * 80 + col) * 2; + uint8_t ch = buf[off]; + uint8_t attr = buf[off + 1]; + int fg = attr & 0x0F; + int bg = (attr >> 4) & 0x07; + bool blink = !!(attr & 0x80); + bool spacer = (ch == 0) || (ch == 32) || (ch == 255); + + // Output color if changed + if ((fg != last_fg) && !spacer) + { + ansi_ctrl(1); + if (truecolor) + CPrint(ansi_tc_fg[fg]); + else + CPrint(ansi_fg[fg]); + last_fg = fg; + } + if (bg != last_bg) + { + ansi_ctrl(1); + if (truecolor) + CPrint(ansi_tc_bg[bg]); + else + CPrint(ansi_bg[bg]); + last_bg = bg; + } + if (blink != last_blink) + { + ansi_ctrl(1); + CPrint(ansi_flash[blink]); + last_blink = blink; + } + ansi_ctrl(0); + + // Output character, convert CP437 to UTF-8 + if (spacer) + CPrint(" "); + else + { + char result[4]; + ibm437_to_utf8(result, ch); + CPrint(result); + } + } + CPrint("\x1b[0m\n"); // Reset colors and end the line + } +} +