From bcea0954ddadf9ce709c628f62cb5bfdd624e7d4 Mon Sep 17 00:00:00 2001 From: bones_was_here Date: Fri, 16 Aug 2024 00:11:22 +1000 Subject: [PATCH] con: implement 24-bit RGB terminal output Mode 2 behaves like the familiar mode 1 for best compatibility with terminal themes and expectations. Mode 3 behaves like the ingame console. 2 is now the default on Linux. Moves the r_text* cvar change from d987323e8a9bcc3102c96a237f74875d5b37c4e4 to a better place, and documents it. Signed-off-by: bones_was_here --- cmd.c | 3 ++ console.c | 94 ++++++++++++++++++++++++++++++++++++++++++++----------- draw.h | 3 ++ gl_draw.c | 7 +++-- 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/cmd.c b/cmd.c index cbb3feca..ec1c6baf 100644 --- a/cmd.c +++ b/cmd.c @@ -666,6 +666,9 @@ static void Cmd_Exec(cmd_state_t *cmd, const char *filename) "sv_gameplayfix_q1bsptracelinereportstexture 0\n" "sv_gameplayfix_swiminbmodels 0\n" "sv_gameplayfix_downtracesupportsongroundflag 0\n" +// Work around low brightness and poor legibility of Quake font +"r_textbrightness 0.25\n" +"r_textcontrast 1.25\n" ); break; default: diff --git a/console.c b/console.c index f46eddb5..180f8957 100644 --- a/console.c +++ b/console.c @@ -63,11 +63,7 @@ cvar_t con_chatsound_team_file = {CF_CLIENT, "con_chatsound_team_file","sound/mi cvar_t con_chatsound_team_mask = {CF_CLIENT, "con_chatsound_team_mask","40","Magic ASCII code that denotes a team chat message"}; cvar_t sys_specialcharactertranslation = {CF_CLIENT | CF_SERVER, "sys_specialcharactertranslation", "1", "terminal console conchars to ASCII translation (set to 0 if your conchars.tga is for an 8bit character set or if you want raw output)"}; -#ifdef WIN32 -cvar_t sys_colortranslation = {CF_CLIENT | CF_SERVER, "sys_colortranslation", "0", "terminal console color translation (supported values: 0 = strip color codes, 1 = translate to ANSI codes, 2 = no translation)"}; -#else -cvar_t sys_colortranslation = {CF_CLIENT | CF_SERVER, "sys_colortranslation", "1", "terminal console color translation (supported values: 0 = strip color codes, 1 = translate to ANSI codes, 2 = no translation)"}; -#endif +cvar_t sys_colortranslation = {CF_CLIENT | CF_SERVER, "sys_colortranslation", "1", "terminal console color translation (supported values: -1 = print codes without translation, 0 = strip color codes, 1 = translate to ANSI codes, 2 = translate DP RGB to 24-bit and Quake colors to ANSI, 3 = translate all colors to 24-bit RGB)"}; cvar_t con_nickcompletion = {CF_CLIENT | CF_ARCHIVE, "con_nickcompletion", "1", "tab-complete nicks in console and message input"}; @@ -876,6 +872,15 @@ void Con_Init (void) Cvar_RegisterVariable (&sys_colortranslation); Cvar_RegisterVariable (&sys_specialcharactertranslation); +#if defined(__linux__) + // Linux terminals natively support RGB 8bpc codes or convert them to a palette. + Cvar_SetQuick(&sys_colortranslation, "2"); +#elif defined(WIN32) + // Windows 10 default PowerShell and cmd.exe have no RGB or ANSI support by default. + // TODO: it can be enabled on current versions using a platform-specific call, + // issue: https://gitlab.com/xonotic/darkplaces/-/issues/426 + Cvar_SetQuick(&sys_colortranslation, "0"); +#endif Cvar_RegisterVariable (&log_file); Cvar_RegisterVariable (&log_file_stripcolors); @@ -1151,6 +1156,8 @@ Con_MaskPrint */ extern cvar_t timestamps; extern cvar_t timeformat; +extern cvar_t r_textcontrast; +extern cvar_t r_textbrightness; void Con_MaskPrint(unsigned additionalmask, const char *msg) { static unsigned mask = 0; @@ -1205,6 +1212,7 @@ void Con_MaskPrint(unsigned additionalmask, const char *msg) // append the character line[index++] = *msg; // if this is a newline character, we have a complete line to print + // bones_was_here: why do we only use half the line buffer? if (*msg == '\n' || index >= (int)sizeof(line) / 2) { // terminate the line @@ -1240,15 +1248,20 @@ void Con_MaskPrint(unsigned additionalmask, const char *msg) } } - if(sys_colortranslation.integer == 1) // ANSI + if(sys_colortranslation.integer > 0) // ANSI, RGB, or both { - static char printline[MAX_INPUTLINE * 4 + 3]; + // ANSI translation: // 2 can become 7 bytes, rounding that up to 8, and 3 bytes are added at the end // a newline can transform into four bytes, but then prevents the three extra bytes from appearing + // 8bpc RGB brings new worst-cases: + // 5 can become 21 bytes, rounding that up to * 5, plenty of space for extra bytes at the end. + // sys_colortranslation 3: 2 can become 21 bytes, rounding that up to * 11 + char printline[sizeof(line) * 11]; int lastcolor = 0; const char *in; char *out; int color; + u8 rgb[3]; for(in = line, out = printline; *in; ++in) { switch(*in) @@ -1256,23 +1269,66 @@ void Con_MaskPrint(unsigned additionalmask, const char *msg) case STRING_COLOR_TAG: if( in[1] == STRING_COLOR_RGB_TAG_CHAR && isxdigit(in[2]) && isxdigit(in[3]) && isxdigit(in[4]) ) { - char r = tolower(in[2]); - char g = tolower(in[3]); - char b = tolower(in[4]); + VectorCopy(in + 2, rgb); // it's a hex digit already, so the else part needs no check --blub - if(isdigit(r)) r -= '0'; - else r -= 87; - if(isdigit(g)) g -= '0'; - else g -= 87; - if(isdigit(b)) b -= '0'; - else b -= 87; + for (int i = 0; i < 3; ++i) + { + if (isdigit(rgb[i])) rgb[i] -= '0'; + else rgb[i] = tolower(rgb[i]) - 87; + rgb[i] *= 17; + } + + if (sys_colortranslation.integer > 1) // 8bpc RGB + { + char *p; + float B; + + in += 4; + rgbout: + color = rgb[0]<<16 | rgb[1]<<8 | rgb[2] | /* disambiguates from quake colours */ 0x40000000; + if (lastcolor == color) + break; + else + lastcolor = color; + + B = r_textbrightness.value * 255; + for (int i = 0; i < 3; ++i) + rgb[i] = bound(0, rgb[i] * r_textcontrast.value + B, 255); + + // format must be decimal 0-255, max length is 21 bytes + if (sys_colortranslation.integer == 2) + memcpy(out, "\x1B[1;38;2", 8); + else + memcpy(out, "\x1B[0;38;2", 8); + out += 8; + for (int i = 0; i < 3; ++i) + { + *out++ = ';'; + p = out += ((rgb[i] > 99) ? 3 : (rgb[i] > 9) ? 2 : 1); + do { *--p = (rgb[i] % 10) + '0'; + } while ((rgb[i] /= 10) > 0); + } + *out++ = 'm'; + + break; + } - color = Sys_Con_NearestColor(r * 17, g * 17, b * 17); + color = Sys_Con_NearestColor(rgb[0], rgb[1], rgb[2]); in += 3; // 3 only, the switch down there does the fourth } else + { color = in[1]; + if (sys_colortranslation.integer == 3 && isdigit(color)) // Quake to RGB + { + color -= '0'; + VectorScale(string_colors[color], 255 * string_colors[color][3], rgb); + ++in; + goto rgbout; + } + } + switch(color) { case STRING_COLOR_TAG: @@ -1357,13 +1413,13 @@ void Con_MaskPrint(unsigned additionalmask, const char *msg) *out = '\0'; Sys_Print(printline, out - printline); } - else if(sys_colortranslation.integer == 2) // Quake + else if(sys_colortranslation.integer == -1) // print as text { Sys_Print(line, index); } else // strip { - static char printline[MAX_INPUTLINE]; // it can only get shorter here + char printline[MAX_INPUTLINE]; // it can only get shorter here const char *in; char *out; for(in = line, out = printline; *in; ++in) diff --git a/draw.h b/draw.h index 8496bb73..76b24e0d 100644 --- a/draw.h +++ b/draw.h @@ -159,6 +159,7 @@ void DrawQ_Pic(float x, float y, cachepic_t *pic, float width, float height, flo void DrawQ_RotPic(float x, float y, cachepic_t *pic, float width, float height, float org_x, float org_y, float angle, float red, float green, float blue, float alpha, int flags); // draw a filled rectangle (slightly faster than DrawQ_Pic with pic = NULL) void DrawQ_Fill(float x, float y, float width, float height, float red, float green, float blue, float alpha, int flags); + // draw a text string, // with optional color tag support, // returns final unclipped x coordinate @@ -166,12 +167,14 @@ void DrawQ_Fill(float x, float y, float width, float height, float red, float gr // the color is tinted by the provided base color // if r_textshadow is not zero, an additional instance of the text is drawn first at an offset with an inverted shade of gray (black text produces a white shadow, brightly colored text produces a black shadow) extern float DrawQ_Color[4]; +extern const vec4_t string_colors[]; float DrawQ_String(float x, float y, const char *text, size_t maxlen, float scalex, float scaley, float basered, float basegreen, float baseblue, float basealpha, int flags, int *outcolor, qbool ignorecolorcodes, const dp_font_t *fnt); float DrawQ_String_Scale(float x, float y, const char *text, size_t maxlen, float sizex, float sizey, float scalex, float scaley, float basered, float basegreen, float baseblue, float basealpha, int flags, int *outcolor, qbool ignorecolorcodes, const dp_font_t *fnt); float DrawQ_TextWidth(const char *text, size_t maxlen, float w, float h, qbool ignorecolorcodes, const dp_font_t *fnt); float DrawQ_TextWidth_UntilWidth(const char *text, size_t *maxlen, float w, float h, qbool ignorecolorcodes, const dp_font_t *fnt, float maxWidth); float DrawQ_TextWidth_UntilWidth_TrackColors(const char *text, size_t *maxlen, float w, float h, int *outcolor, qbool ignorecolorcodes, const dp_font_t *fnt, float maxwidth); float DrawQ_TextWidth_UntilWidth_TrackColors_Scale(const char *text, size_t *maxlen, float w, float h, float sw, float sh, int *outcolor, qbool ignorecolorcodes, const dp_font_t *fnt, float maxwidth); + // draw a very fancy pic (per corner texcoord/color control), the order is tl, tr, bl, br void DrawQ_SuperPic(float x, float y, cachepic_t *pic, float width, float height, float s1, float t1, float r1, float g1, float b1, float a1, float s2, float t2, float r2, float g2, float b2, float a2, float s3, float t3, float r3, float g3, float b3, float a3, float s4, float t4, float r4, float g4, float b4, float a4, int flags); // set the clipping area diff --git a/gl_draw.c b/gl_draw.c index 3eb4c131..11a080af 100644 --- a/gl_draw.c +++ b/gl_draw.c @@ -51,8 +51,9 @@ dp_fonts_t dp_fonts; static mempool_t *fonts_mempool = NULL; cvar_t r_textshadow = {CF_CLIENT | CF_ARCHIVE, "r_textshadow", "0", "draws a shadow on all text to improve readability (note: value controls offset, 1 = 1 pixel, 1.5 = 1.5 pixels, etc)"}; -cvar_t r_textbrightness = {CF_CLIENT | CF_ARCHIVE, "r_textbrightness", "0.25", "additional brightness for text color codes (0 keeps colors as is, 1 makes them all white)"}; -cvar_t r_textcontrast = {CF_CLIENT | CF_ARCHIVE, "r_textcontrast", "1.25", "additional contrast for text color codes (1 keeps colors as is, 0 makes them all black)"}; +// these are also read by the dedicated server when sys_colortranslation > 1 +cvar_t r_textbrightness = {CF_SHARED | CF_ARCHIVE, "r_textbrightness", "0", "additional brightness for text color codes (0 keeps colors as is, 1 makes them all white)"}; +cvar_t r_textcontrast = {CF_SHARED | CF_ARCHIVE, "r_textcontrast", "1", "additional contrast for text color codes (1 keeps colors as is, 0 makes them all black)"}; cvar_t r_font_postprocess_blur = {CF_CLIENT | CF_ARCHIVE, "r_font_postprocess_blur", "0", "font blur amount"}; cvar_t r_font_postprocess_outline = {CF_CLIENT | CF_ARCHIVE, "r_font_postprocess_outline", "0", "font outline amount"}; @@ -849,7 +850,7 @@ void DrawQ_Fill(float x, float y, float width, float height, float red, float gr } /// color tag printing -static const vec4_t string_colors[] = +const vec4_t string_colors[] = { // Quake3 colors // LadyHavoc: why on earth is cyan before magenta in Quake3? -- 2.39.2