diff --git a/src/client/cmd.qc b/src/client/cmd.qc index d87657da..d0e3f9d2 100644 --- a/src/client/cmd.qc +++ b/src/client/cmd.qc @@ -374,6 +374,9 @@ Cmd_Parse(string sCMD) case "player_geomtest": CMD_player_geomtest(); break; + case "motd": + print(MOTD_GetTextBody()); + break; /* XR binds, engine binds them currently */ case "+attack_left": @@ -455,6 +458,9 @@ Cmd_Init(void) registercommand("vote"); registercommand("callvote"); + /* motd */ + registercommand("motd"); + /* hud weapon selection system */ registercommand("slot1"); registercommand("slot2"); diff --git a/src/client/entry.qc b/src/client/entry.qc index 3ee52162..46f6b468 100644 --- a/src/client/entry.qc +++ b/src/client/entry.qc @@ -76,6 +76,7 @@ CSQC_Init(float apilevel, string enginename, float engineversion) Decals_Init(); Way_Init(); Materials_Init(); + MOTD_Init(); /* let the menu know we're a multi or a singleplayer game */ if (serverkeyfloat("sv_playerslots") == 1) diff --git a/src/client/font.h b/src/client/font.h index d76264ba..3208b4e1 100644 --- a/src/client/font.h +++ b/src/client/font.h @@ -104,6 +104,16 @@ void Font_DrawRText_RGBA(vector vecOrigin, string strText, vector col, float a, @param iAlignFlags sets how the text may be aligned. */ void Font_DrawField(vector vecOrigin, vector vecSize, string strText, font_s fnt, alignflags_t iAlignFlags); +/** Draws a textfield with line wrapping at a custom text height. + +@param vecOrigin is the absolute starting position. +@param vecSize is the total area in pixels that the field takes up on the screen. +@param iTextHeight is the desired text height in pixels. +@param strText is the text to be drawn onto the screen. +@param fnt is the font to be used for rendering the text. +@param iAlignFlags sets how the text may be aligned. */ +void Font_DrawFieldAtHeight(vector vecOrigin, vector vecSize, int iTextHeight, string strText, font_s fnt, alignflags_t iAlignFlags); + /** Converts a normalized RGB color vector to a hex color string. @param vecColor is the normalized input color. E.g. [1.0f, 0.0f, 0.0f] diff --git a/src/client/font.qc b/src/client/font.qc index 28d48ad9..97ff5f1c 100644 --- a/src/client/font.qc +++ b/src/client/font.qc @@ -162,6 +162,17 @@ Font_DrawField(vector vecOrigin, vector vecSize, string strText, font_s fnt, ali drawfontscale = [1,1,0]; } +void +Font_DrawFieldAtHeight(vector vecOrigin, vector vecSize, int iTextHeight, string strText, font_s fnt, alignflags_t iAlignFlags) +{ + drawfont = (float)fnt.iID; + drawfontscale[0] = (float)iTextHeight / 8; + drawfontscale[1] = (float)iTextHeight / 8; + drawtextfield(vecOrigin, vecSize, (float)iAlignFlags, strText); + drawfont = 0; + drawfontscale = [1,1,0]; +} + string Font_RGBtoHex(vector vecColor) { diff --git a/src/server/entry.qc b/src/server/entry.qc index a5729d56..da7cf5b6 100644 --- a/src/server/entry.qc +++ b/src/server/entry.qc @@ -374,6 +374,7 @@ initents(void) { /* sound shader init */ Materials_Init(); + MOTD_Init(); PMove_Init(); /* TODO: turn these effects into sound shaders */ diff --git a/src/shared/defs.h b/src/shared/defs.h index 50b428a0..b7fa26fe 100644 --- a/src/shared/defs.h +++ b/src/shared/defs.h @@ -89,6 +89,7 @@ string __fullspawndata; #include "decalgroups.h" #include "colors.h" #include "weapons.h" +#include "motd.h" #define BSPVER_PREREL 28 #define BSPVER_Q1 29 diff --git a/src/shared/include.src b/src/shared/include.src index b50fa96e..774d5902 100644 --- a/src/shared/include.src +++ b/src/shared/include.src @@ -33,6 +33,7 @@ sentences.qc NSSpraylogo.qc util.qc weapons.qc +motd.qc ../xr/include.src #endlist diff --git a/src/shared/motd.h b/src/shared/motd.h new file mode 100644 index 00000000..a00adb3d --- /dev/null +++ b/src/shared/motd.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Vera Visions LLC. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifdef SERVER +var string autocvar_motdfile = "motd.txt"; +#define MOTD_FILE autocvar_motdfile + +/** Hard-limit to how many lines are allowed within a message of the day. */ +#define MOTD_LINES 32 + +/** Called on the server to load the default 'message of the day' file. */ +void MOTD_LoadDefault(void); + +/** Called on the server to load a specific 'message of the day' file. */ +void MOT_LoadFromFile(string); +#endif + +/** Called by CSQC_Init() on the client game and by initents() on the server game to +set up a networked 'message of the day'. */ +void MOTD_Init(void); + +#ifdef CLIENT +/** Returns the full text body of the 'message of the day' that is on the server. Can be retrieved by clients via the `motd` console command. */ +string MOTD_GetTextBody(void); +/** Returns how many individual lines are present in the message of the day. */ +int MOTD_GetLineCount(void); +#endif \ No newline at end of file diff --git a/src/shared/motd.qc b/src/shared/motd.qc new file mode 100644 index 00000000..2e661f77 --- /dev/null +++ b/src/shared/motd.qc @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 Vera Visions LLC. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifdef CLIENT +static string g_motd_body; +static int g_motd_lines; +#endif + +void +MOTD_Init(void) +{ +#ifdef SERVER + /* wipe the slate clean */ + for (int i = 0; i < MOTD_LINES; i++) { + localcmd(sprintf("serverinfo motdline%i \"\"\n", i)); + } + localcmd("serverinfo motdlines \"\"\n"); +#endif +#ifdef CLIENT + int motdLines = stoi(serverkey("motdlines")); + g_motd_body = ""; + + for (int i = 0; i < motdLines; i++) { + string line = serverkey(sprintf("motdline%i", i)); + if (line != "/") { + g_motd_body = strcat(g_motd_body, line, "\n"); + } else { + g_motd_body = strcat(g_motd_body, "\n"); + } + } + + g_motd_lines = motdLines; +#endif +} + +#ifdef SERVER +void +MOTD_LoadDefault(void) +{ + MOT_LoadFromFile(MOTD_FILE); +} + +void +MOT_LoadFromFile(string fileName) +{ + /* init motd */ + string tempString; + int motdLines = 0; + + filestream fileMotd = fopen(fileName, FILE_READ); + + /* now read in the new motd */ + if (fileMotd != -1) { + for (int i = 0; i < MOTD_LINES; i++) { + tempString = fgets(fileMotd); + if not (tempString) { + break; + } else if (tempString == __NULL__) { + localcmd(sprintf("serverinfo motdline%i /\n", motdLines)); + } else { + localcmd(sprintf("serverinfo motdline%i %S\n", motdLines, tempString)); + } + + motdLines++; + } + localcmd(sprintf("serverinfo motdlines \"%i\"\n", motdLines)); + fclose(fileMotd); + } else { + NSLog("Failed to parse motd from %S", MOTD_FILE); + } +} +#endif + +#ifdef CLIENT +string +MOTD_GetTextBody(void) +{ + if (g_motd_lines > 0i) + return g_motd_body; + else + return "Server advertises no message of the day."; +} + +int +MOTD_GetLineCount(void) +{ + return g_motd_lines; +} +#endif \ No newline at end of file diff --git a/src/vgui/ui_button.qc b/src/vgui/ui_button.qc index 173ea81f..a04ab82e 100644 --- a/src/vgui/ui_button.qc +++ b/src/vgui/ui_button.qc @@ -89,6 +89,7 @@ VGUIButton::SetTitle(string strName) m_strTitle = strName; m_strTitleActive = sprintf("^3%s", m_strTitle); + m_strTitle = sprintf("%s%s", Font_RGBtoHex(UI_MAINCOLOR), strName); drawfont = g_fntDefault.iID; newsize[0] = stringwidth(m_strTitle, TRUE, [g_fntDefault.iScaleX, g_fntDefault.iScaleY]) + 16; diff --git a/src/vgui/ui_label.qc b/src/vgui/ui_label.qc index 55536b6b..3859f648 100644 --- a/src/vgui/ui_label.qc +++ b/src/vgui/ui_label.qc @@ -14,6 +14,7 @@ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +/** A VGUI label/text field. */ class VGUILabel:VGUIWidget { public: @@ -24,15 +25,19 @@ public: virtual void Draw(void); virtual bool Input(float, float, float, float); virtual void Spawned(void); + virtual void SetTextSize(int); private: string m_strTitle; + int m_labelFlags; + int m_textSize; }; void VGUILabel::VGUILabel(void) { m_strTitle = __NULL__; + m_labelFlags = AF_TOP | AF_LEFT; } void @@ -56,11 +61,29 @@ VGUILabel::SetTitle (string strName) SetSize([2 + stringwidth(m_strTitle, TRUE, [g_fntDefault.iScaleX, g_fntDefault.iScaleY]), 16]); } +void +VGUILabel::SetTextSize(int val) +{ + m_textSize = val; +} + void VGUILabel::Draw(void) { if (m_strTitle) { - Font_DrawField(m_parent.m_vecOrigin + m_vecOrigin, m_vecSize, m_strTitle, g_fntDefault, 0); + vector pos; + vector size; + + pos = m_parent.m_vecOrigin + m_vecOrigin; + size = m_vecSize; + drawsetcliparea(pos[0], pos[1], size[0], size[1]); + + if (!m_textSize) + Font_DrawField(pos, size, m_strTitle, g_fntDefault, m_labelFlags); + else + Font_DrawFieldAtHeight(pos, size, m_textSize, m_strTitle, g_fntDefault, m_labelFlags); + + drawresetcliparea(); } } diff --git a/src/vgui/ui_window.qc b/src/vgui/ui_window.qc index cbf89eaf..5ab3c656 100644 --- a/src/vgui/ui_window.qc +++ b/src/vgui/ui_window.qc @@ -18,9 +18,18 @@ enumflags { WINDOW_DRAGGING, WINDOW_RESIZING, - WINDOW_CANRESIZE }; +typedef enumflags +{ + VGUIWindowBorderless, + VGUIWindowTitled, + VGUIWindowClosable, + VGUIWindowMiniaturizable, + VGUIWindowResizeable, + VGUIWindowMovable, +} vguiWindowStyle_t; + /** Top-most window class in VGUILib */ class VGUIWindow:VGUIWidget { @@ -42,6 +51,9 @@ public: /** Called when the window has been repositioned by the user. */ nonvirtual void CallOnMove(void(void) vFunc); + /** Sets the style mask of the specified window. */ + nonvirtual void SetStyleMask(vguiWindowStyle_t); + /* overrides */ virtual void Draw(void); virtual void SizeChanged(vector, vector); @@ -54,6 +66,7 @@ private: vector m_vecDragOffset; string m_strTitle; string m_strIcon; + vguiWindowStyle_t m_styleMask; VGUIButton m_btnClose; @@ -66,6 +79,21 @@ VGUIWindow::VGUIWindow(void) { m_vecColor = UI_MAINCOLOR; m_flAlpha = 1.0f; + m_styleMask = VGUIWindowMovable | + VGUIWindowTitled | + VGUIWindowClosable | + VGUIWindowMiniaturizable | + VGUIWindowResizeable; +} + +void +VGUIWindow::SetStyleMask(vguiWindowStyle_t val) +{ + if (!(val & VGUIWindowClosable)) { + m_btnClose.Hide(); + } + + m_styleMask = val; } void @@ -140,7 +168,7 @@ void VGUIWindow::Draw(void) drawfill(m_vecOrigin + [0, 1], [1, m_vecSize[1] - 2], m_vecColor, 1.0f); drawfill(m_vecOrigin + [m_vecSize[0] - 1, 1], [1, m_vecSize[1] - 2], m_vecColor, 1.0f); - if (m_iFlags & WINDOW_CANRESIZE) { + if (m_styleMask & VGUIWindowResizeable) { drawpic(m_vecOrigin + m_vecSize - [16,16], "textures/ui/steam/icon_resizer", [16,16], m_vecColor, 1.0f, 0); } #else @@ -150,11 +178,12 @@ void VGUIWindow::Draw(void) drawfill(m_vecOrigin + [0, 1], [1, m_vecSize[1] - 2], [1,1,1], 0.5f); drawfill(m_vecOrigin + [m_vecSize[0] - 1, 1], [1, m_vecSize[1] - 2], [0,0,0], 0.5f); - if (m_iFlags & WINDOW_CANRESIZE) { + if (m_styleMask & VGUIWindowResizeable) { drawpic(m_vecOrigin + m_vecSize - [16,16], "textures/ui/steam/icon_resizer", [16,16], m_vecColor, 1.0f, 0); } #endif + if (m_styleMask & VGUIWindowTitled) if (m_strTitle) { if (m_strIcon) { Font_DrawText(m_vecOrigin + [26, 8], m_strTitle, g_fntDefault); @@ -163,7 +192,7 @@ void VGUIWindow::Draw(void) Font_DrawText(m_vecOrigin + [8, 8], m_strTitle, g_fntDefault); } } - + #ifdef UI_DEVELOPER if (m_iFlags & WINDOW_DRAGGING) { Font_DrawText([8, video_res[1] - 18], sprintf("Window Position: %d, %d\n", m_vecOrigin[0], m_vecOrigin[1]), g_fntDefault); @@ -180,9 +209,9 @@ bool VGUIWindow::Input (float flEVType, float flKey, float flChar, float flDevID if (flEVType == IE_KEYDOWN) { if (flKey == K_MOUSE1) { - if (m_iFlags & WINDOW_CANRESIZE && Util_MouseAbove(g_vecMousePos, m_vecOrigin + (m_vecSize - [16,16]), [16,16])) { + if (m_styleMask & VGUIWindowResizeable && Util_MouseAbove(g_vecMousePos, m_vecOrigin + (m_vecSize - [16,16]), [16,16])) { m_iFlags |= WINDOW_RESIZING; - } else if (Util_MouseAbove(g_vecMousePos, m_vecOrigin, [m_vecSize[0] - 32, 16])) { + } else if (m_styleMask & VGUIWindowMovable && Util_MouseAbove(g_vecMousePos, m_vecOrigin, [m_vecSize[0] - 32, 16])) { m_iFlags |= WINDOW_DRAGGING; m_vecDragOffset = m_vecOrigin - g_vecMousePos; }