local ScrW = ScrW local ScrH = ScrH local Color = Color local STENCIL_EQUAL = STENCIL_EQUAL local STENCIL_KEEP = STENCIL_KEEP local STENCIL_NEVER = STENCIL_NEVER local STENCIL_REPLACE = STENCIL_REPLACE local draw = draw local math = math local render = render local surface = surface local draw_NoTexture = draw.NoTexture local math_rad = math.rad local math_sin = math.sin local math_cos = math.cos local render_ClearStencil = render.ClearStencil local render_SetStencilCompareFunction = render.SetStencilCompareFunction local render_SetStencilEnable = render.SetStencilEnable local render_SetStencilFailOperation = render.SetStencilFailOperation local render_SetStencilPassOperation = render.SetStencilPassOperation local render_SetStencilReferenceValue = render.SetStencilReferenceValue local render_SetStencilTestMask = render.SetStencilTestMask local render_SetStencilWriteMask = render.SetStencilWriteMask local render_SetStencilZFailOperation = render.SetStencilZFailOperation local render_UpdateScreenEffectTexture = render.UpdateScreenEffectTexture local surface_DrawPoly = surface.DrawPoly local surface_DrawRect = surface.DrawRect local surface_DrawTexturedRect = surface.DrawTexturedRect local surface_SetDrawColor = surface.SetDrawColor local surface_SetMaterial = surface.SetMaterial cbox.chatbox = cbox.chatbox or {} cbox.chatbox.tabs = cbox.chatbox.tabs or {} cbox.chatbox.panels = cbox.chatbox.panels or {} cbox.chatbox.modes = cbox.chatbox.modes or {} local CHATBOX_COLOR = CreateClientConVar("cbox_chatbox_color", "160 160 160", true, false, "Chatbox background color") local CHATBOX_ALPHA = CreateClientConVar("cbox_chatbox_alpha", "128", true, false, "Chatbox background alpha") local CHATBOX_BLUR = CreateClientConVar("cbox_chatbox_blur", "0", true, false, "Chatbox background is blurred") local CHATBOX_FADE = CreateClientConVar("cbox_chatbox_fade", "1", true, false, "Chatbox fades in and out") local CHATBOX_MODE1 = CreateClientConVar("cbox_chatbox_mode1", "say", true, false, "Default mode when pressing messagemode") local CHATBOX_MODE2 = CreateClientConVar("cbox_chatbox_mode2", "say_team", true, false, "Default mode when pressing messagemode2") ---@param height number ---@return number local function _ScreenScaleH(height) return height * (ScrH() / 480) end local ScreenScaleH = ScreenScaleH or _ScreenScaleH ---@return number x ---@return number y ---@return number w ---@return number h local function GetDefaultBounds() return ScreenScaleH(10), ScreenScaleH(275), ScreenScaleH(320), ScreenScaleH(120) end ---Add a new chatbox tab ---@param key string Internal unique name ---@param name string Display name ---@param icon string Path to icon ---@param callback fun(): Panel What to do when the tab is created function cbox.chatbox.AddTab(key, name, icon, callback) cbox.chatbox.tabs[key] = { name = name, icon = icon, callback = callback, } end ---Add a new chatbox send mode ---@param key string Internal unique name ---@param name string Display name ---@param callback fun(text) What to do when enter is pressed on this mode function cbox.chatbox.AddMode(key, name, callback) cbox.chatbox.modes[key] = { name = name, callback = callback, } end local MATERIAL_BLUR = Material("pp/blurscreen") local function add_rounded_poly(poly, x, y, rad, seg, offset) offset = offset or 0 for i = 0, seg do local r = math_rad(((i + offset) / seg) * -90) poly[#poly + 1] = { x = x + math_sin(r) * rad, y = y + math_cos(r) * rad, u = math_sin(r) / 2 + 0.5, v = math_cos(r) / 2 + 0.5, } end end local function RoundedBoxPoly(w, h, rad, seg) local poly = {} local x, y = 0, 0 add_rounded_poly(poly, x + rad, y + rad, rad, seg, seg) poly[#poly + 1] = { x = x + (w - rad), y = y, u = 0.5, v = 0.5, } add_rounded_poly(poly, x + (w - rad), y + rad, rad, seg, seg * 2) poly[#poly + 1] = { x = x + w, y = y + (h - rad), u = 0.5, v = 0.5, } add_rounded_poly(poly, x + (w - rad), y + (h - rad), rad, seg, seg * 3) poly[#poly + 1] = { x = x + rad, y = y + h, u = 0.5, v = 0.5, } add_rounded_poly(poly, x + rad, y + (h - rad), rad, seg) return poly end local PREV_W, PREV_H, BACKGROUND_POLY local function CreateChatbox() if IsValid(cbox.chatbox.panels.frame) then cbox.chatbox.panels.frame:Remove() end local frame = vgui.Create("DFrame", nil, "cbox.chatbox") frame:SetCookieName("cbox_chatbox") frame:SetDeleteOnClose(false) frame:SetSizable(true) frame:SetScreenLock(true) frame:DockPadding(8, 8, 8, 8) local dx, dy, dw, dh = GetDefaultBounds() -- TODO: make this configurable frame:SetMinWidth(dw) frame:SetMinHeight(dh) local x = frame:GetCookie("pos_x", dx) local y = frame:GetCookie("pos_y", dy) local w = frame:GetCookie("width", dw) local h = frame:GetCookie("height", dh) frame:SetPos(x, y) frame:SetSize(w, h) PREV_W = w PREV_H = h BACKGROUND_POLY = RoundedBoxPoly(w, h, 8, 24) frame:SetVisible(false) function frame:PerformLayout() end frame.btnClose:Remove() frame.btnMinim:Remove() frame.btnMaxim:Remove() frame.lblTitle:Remove() function frame:CrossFade(anim, delta, out) if anim.Finished then if out then self:Close() self:SetVisible(false) else self:MakePopup() end end if anim.Started then if out then self:SetAlpha(255) else self:SetAlpha(0) end end self:SetAlpha(out and 255 * (1 - delta) or 255 * delta) end frame.animFade = Derma_Anim("Fade", frame, frame.CrossFade) frame.oldThink = frame.Think function frame:Think() self:oldThink() self.animFade:Run() local w, h = self:GetSize() if w ~= PREV_W or h ~= PREV_H then PREV_W = w PREV_H = h BACKGROUND_POLY = RoundedBoxPoly(w, h, 8, 24) end end function frame:Paint(w, h) local alpha = CHATBOX_ALPHA:GetInt() -- Reset everything to known good render_SetStencilWriteMask(0xFF) render_SetStencilTestMask(0xFF) render_SetStencilReferenceValue(0) render_SetStencilPassOperation(STENCIL_KEEP) render_SetStencilZFailOperation(STENCIL_KEEP) render_ClearStencil() -- Enable stencils render_SetStencilEnable(true) -- Set everything up everything draws to the stencil buffer instead of the screen render_SetStencilReferenceValue(1) render_SetStencilCompareFunction(STENCIL_NEVER) render_SetStencilFailOperation(STENCIL_REPLACE) draw_NoTexture() surface_SetDrawColor(255, 255, 255) surface_DrawPoly(BACKGROUND_POLY) -- Only draw things that are in the stencil buffer render_SetStencilCompareFunction(STENCIL_EQUAL) render_SetStencilFailOperation(STENCIL_KEEP) if CHATBOX_BLUR:GetBool() and alpha ~= 255 then local x, y = self:LocalToScreen(0, 0) surface_SetMaterial(MATERIAL_BLUR) surface_SetDrawColor(255, 255, 255, 255) for i = 0.33, 1, 0.33 do MATERIAL_BLUR:SetFloat("$blur", 5 * i) MATERIAL_BLUR:Recompute() render_UpdateScreenEffectTexture() surface_DrawTexturedRect(x * -1, y * -1, ScrW(), ScrH()) end end local r, g, b = cbox.utils.ParseColorString(CHATBOX_COLOR:GetString()) surface_SetDrawColor(r, g, b, alpha) surface_DrawRect(0, 0, w, h) -- Let everything render normally again render_SetStencilEnable(false) end function frame:OnClose() local x, y = frame:GetPos() local w, h = frame:GetSize() frame:SetCookie("pos_x", x) frame:SetCookie("pos_y", y) frame:SetCookie("width", w) frame:SetCookie("height", h) end function frame:OnKeyCodeTyped(key) if key == KEY_ESCAPE then cbox.chatbox.Close() gui.HideGameUI() end end cbox.chatbox.panels.frame = frame local tabs = vgui.Create("DPropertySheet", frame, "cbox.chatbox.tabs") tabs:Dock(FILL) tabs:SetPadding(0) function tabs:Paint(w, h) end function tabs:OnActiveTabChanged( old, new ) if new:GetPanel().cbox_id == "\1chat" then cbox.chatbox.panels.input:RequestFocus() end end cbox.chatbox.panels.tabs = tabs for id, tab in next, cbox.chatbox.tabs do local function catch(err) cbox.utils.RealmError(("Failed to create tab %q:"):format(id), err) end local ok, ret = xpcall(tab.callback, catch) if not ok then continue end if not ispanel(ret) then cbox.utils.RealmError(("Got non-panel for tab %q!"):format(id)) continue end local sheet = tabs:AddSheet(tab.name, ret, tab.icon) sheet.Panel.cbox_id = id function sheet.Tab:ApplySchemeSettings() local ExtraInset = 8 if self.Image then ExtraInset = ExtraInset + self.Image:GetWide() end self:SetTextInset(ExtraInset, 2) local w, h = self:GetContentSize() h = self:GetTabHeight() self:SetSize(w + 8, h) DLabel.ApplySchemeSettings( self ) end -- TODO: configurable font sheet.Tab:SetFont("ChatFont") function sheet.Tab:Paint(w, h) if self:IsActive() then surface_SetDrawColor(0, 0, 0, 128) surface_DrawRect(0, 0, w, 20) else surface_SetDrawColor(0, 0, 0, 64) surface_DrawRect(0, 2, w, 18) end end end end local function Init() include("cbox/cl_modes.lua") local tab_files = file.Find("cbox/tabs/*", "LUA") for _, name in ipairs(tab_files) do cbox.utils.RealmPrint("Loading chatbox tab:", name) include("cbox/tabs/" .. name) end if not IsValid(cbox.chatbox.panels.frame) then CreateChatbox() end hook.Add("PlayerBindPress", "cbox.chatbox", function(ply, bind, pressed) local lply = LocalPlayer() if IsValid(lply) and ply ~= lply then cbox.utils.RealmError("Got mismatched player. How???", ply) return end if bind ~= "messagemode" and bind ~= "messagemode2" then return end if not pressed then return end cbox.chatbox.Open(bind == "messagemode2") return true end) hook.Add("Think", "cbox.chatbox", function() if cbox.chatbox.IsOpen() and input.IsKeyDown(KEY_ESCAPE) then cbox.chatbox.Close() gui.HideGameUI() end end) hook.Run("cbox.chatbox.Initialize") end ---Opens the chatbox ---@param alt? boolean Was this request to open made by messagemode2 (team chat) function cbox.chatbox.Open(alt) alt = alt ~= nil and alt or false local frame = cbox.chatbox.panels.frame if not IsValid(frame) then CreateChatbox() end frame:SetVisible(true) if frame.animFade ~= nil and CHATBOX_FADE:GetBool() then frame.animFade:Start(0.1, false) else frame:SetAlpha(255) frame:MakePopup() end if not IsValid(cbox.chatbox.panels.input) or not IsValid(cbox.chatbox.panels.tabs) then -- attempt to reinit if IsValid(frame) then frame:Remove() end Init() end if not IsValid(cbox.chatbox.panels.input) or not IsValid(cbox.chatbox.panels.tabs) then cbox.utils.RealmError("Input or tabs aren't valid, chat tab failed to load, bailing!") if IsValid(frame) then frame:Remove() end hook.Remove("PlayerBindPress", "cbox.chatbox") return end for _, tab in ipairs(cbox.chatbox.panels.tabs:GetItems()) do if tab.Panel.cbox_id == "\1chat" and cbox.chatbox.panels.tabs:GetActiveTab() ~= tab.Tab then cbox.chatbox.panels.tabs:SetActiveTab(tab.Tab) end end cbox.chatbox.panels.input:RequestFocus() if IsValid(cbox.chatbox.panels.mode_switch) then if alt then cbox.chatbox.panels.mode_switch:UpdateMode(CHATBOX_MODE2:GetString()) else cbox.chatbox.panels.mode_switch:UpdateMode(CHATBOX_MODE1:GetString()) end end hook.Run("StartChat") end ---Closes the chatbox function cbox.chatbox.Close() local frame = cbox.chatbox.panels.frame if frame.animFade ~= nil and CHATBOX_FADE:GetBool() then frame.animFade:Start(0.1, true) else frame:Close() frame:SetVisible(false) end hook.Run("FinishChat") cbox.chatbox.panels.input:SetText("") hook.Run("ChatTextChanged", "") end ---Gets if the chatbox is open ---@return boolean function cbox.chatbox.IsOpen() local frame = cbox.chatbox.panels.frame return IsValid(frame) and frame:IsVisible() == true and frame:GetAlpha() == 255 end hook.Add("Initialize", "cbox.chatbox", Init) concommand.Add("cbox_chatbox_reload", function() CreateChatbox() end, nil, "Reloads the chatbox") concommand.Add("_cbox_chatbox_fullreload", function() if IsValid(cbox.chatbox.panels.frame) then cbox.chatbox.panels.frame:Remove() end Init() end, nil, "Fully reinitializes the chatbox, use only in extreme breakage.")