Compare commits

...

2 Commits

Author SHA1 Message Date
Cynthia Foxwell 27d4dda2a2 basic chatbox functionality 2023-12-17 23:59:16 -07:00
Cynthia Foxwell 5cfe0c8ea5 starting stuff 2023-12-17 21:24:15 -07:00
9 changed files with 552 additions and 1 deletions

View File

@ -220,7 +220,7 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
cbox
Copyright (C) 2023 Cynosphere-gmod
Copyright (C) 2023 Cynthia Foxwell
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

View File

@ -0,0 +1 @@
include("cbox/sh_init.lua")

180
lua/cbox/cl_chatbox.lua Normal file
View File

@ -0,0 +1,180 @@
local DisableClipping = DisableClipping
local ScrW = ScrW
local ScrH = ScrH
local render = render
local string = string
local surface = surface
local render_UpdateScreenEffectTexture = render.UpdateScreenEffectTexture
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 {}
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")
---@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
local MATERIAL_BLUR = Material("pp/blurscreen")
local oldSkin = SKIN
SKIN = derma.GetDefaultSkin()
local CLOSE_BUTTON = GWEN.CreateTextureNormal(32, 452, 29, 17)
local CLOSE_BUTTON_HOVER = GWEN.CreateTextureNormal(64, 452, 29, 17)
local CLOSE_BUTTON_DOWN = GWEN.CreateTextureNormal(96, 452, 29, 17)
SKIN = oldSkin
local COLOR_DISABLED = Color(255, 255, 255, 50)
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(4, 4, 4, 4)
frame.btnMinim:SetVisible(false)
frame.btnMaxim:SetVisible(false)
frame.lblTitle:SetVisible(false)
function frame:Paint(w, h)
if CHATBOX_BLUR:GetBool() 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 color = string.Explode(" ", CHATBOX_COLOR:GetString())
surface_SetDrawColor(tonumber(color[1]), tonumber(color[2]), tonumber(color[3]), CHATBOX_ALPHA:GetInt())
surface_DrawRect(0, 0, w, h)
end
local dx, dy, dw, dh = GetDefaultBounds()
-- TODO: make this configurable
frame:SetMinWidth(dw)
frame:SetMinWidth(dh)
-- TODO: save resizing/moving
frame:SetPos(dx, dy)
frame:SetSize(dw, dh)
frame:SetVisible(false)
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)
surface_SetDrawColor(0, 0, 0, 72)
surface_DrawRect(0, 20, w, h - 20)
end
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
tabs:AddSheet(tab.name, ret, tab.icon)
end
frame.btnClose:SetZPos(99)
end
if not IsValid(cbox.chatbox.panels.frame) then
CreateChatbox()
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
if not IsValid(cbox.chatbox.panels.frame) then
CreateChatbox()
end
cbox.chatbox.panels.frame:SetVisible(true)
cbox.chatbox.panels.frame:MakePopup()
cbox.chatbox.panels.input:RequestFocus()
hook.Run("StartChat")
end
---Closes the chatbox
function cbox.chatbox.Close()
cbox.chatbox.panels.frame:Close()
hook.Run("FinishChat")
cbox.chatbox.panels.input:SetText("")
hook.Run("ChatTextChanged", "")
end
hook.Add("PlayerBindPress", "cbox.chatbox", function(ply, bind, pressed)
if bind ~= "messagemode" and bind ~= "messagemode2" then return end
cbox.chatbox.Open(bind == "messagemode2")
return true
end)
concommand.Add("cbox_chatbox_reload", function()
CreateChatbox()
end, nil, "Reloads the chatbox")

65
lua/cbox/cl_hooks.lua Normal file
View File

@ -0,0 +1,65 @@
local hookTable = {}
if cbox.hooks then
hookTable = cbox.hooks.GetTable()
end
cbox.hooks = cbox.hooks or {}
---Returns all hooks
---@return table
function cbox.hooks.GetTable()
return hookTable
end
---@alias ValidHooks
---| '"PreChatAddText"' # Runs before the true chat.AddText is called, allows modification to arguments
---Adds a hook
---@param name ValidHooks
---@param identifier string
---@param callback function
function cbox.hooks.Add(name, identifier, callback)
hookTable[name] = hookTable[name] or {}
hookTable[name][identifier] = callback
end
---Removes a hook
---@param name ValidHooks
---@param identifier string
function cbox.hooks.Remove(name, identifier)
if not hookTable[name] then return end
hookTable[name][identifier] = nil
end
cbox.detours = cbox.detours or {}
cbox.detours.chat_AddText = cbox.detours.chat_AddText or chat.AddText
local origChatAddText = cbox.detours.chat_AddText
function chat.AddText(...)
local args = {...}
local hooks = hookTable.PreChatAddText or {}
for id, callback in next, hooks do
local function catch(err)
cbox.utils.RealmError(("Failed to run callback for PreChatAddText hook %q:"):format(id), err)
end
local ok, ret = xpcall(callback, catch, args)
if not ok then continue end
if ret ~= nil then
if not istable(ret) then
cbox.utils.RealmError(("Got return for PreChatAddText hook %q, but it was not a table."):format(id))
continue
end
args = ret
end
end
origChatAddText(unpack(args))
hook.Run("OnChatAddText", args)
end

View File

@ -0,0 +1,20 @@
local ENABLED = CreateClientConVar("cbox_greentext", "1", true, false, "Enables greentext", 0, 1)
local COLOR = CreateClientConVar("cbox_greentext_color", "175 201 96", true, false, "Color greentext should be")
local color_white = Color(255, 255, 255)
cbox.hooks.Add("PreChatAddText", "cbox.greentext", function(args)
if not ENABLED:GetBool() then return end
for i, arg in ipairs(args) do
if isstring(arg) and arg:StartsWith(": >") then
args[i] = arg:sub(3)
table.insert(args, i, cbox.utils.ParseColorString(COLOR:GetString()))
table.insert(args, i, ": ")
table.insert(args, i, color_white)
break
end
end
return args
end)

View File

@ -0,0 +1,36 @@
local ENABLED = CreateClientConVar("cbox_timestamps", "1", true, false, "Enables timestamps", 0, 1)
local TRUETIME = CreateClientConVar("cbox_timestamps_24hr", "1", true, false, "Whether timestamps should be 24 hour instead of 12 hour", 0, 1)
local SECONDS = CreateClientConVar("cbox_timestamps_seconds", "0", true, false, "Whether timestamps should display seconds", 0, 1)
local COLOR = CreateClientConVar("cbox_timestamps_color", "151 211 255", true, false, "Color timestamps should be")
local color_white = Color(255, 255, 255)
cbox.hooks.Add("PreChatAddText", "cbox.timestamps", function(args)
if not ENABLED:GetBool() then return end
local use24 = TRUETIME:GetBool()
local stamp = ""
if use24 then
stamp = stamp .. os.date("%H:")
else
stamp = stamp .. os.date("%I:")
end
stamp = stamp .. os.date("%M")
if SECONDS:GetBool() then
stamp = stamp .. os.date(":%S")
end
if not use24 then
stamp = stamp .. os.date(" %p")
end
local new_args = {cbox.utils.ParseColorString(COLOR:GetString()), stamp, color_white, " - "}
for _, arg in ipairs(args) do
new_args[#new_args + 1] = arg
end
return new_args
end)

40
lua/cbox/sh_init.lua Normal file
View File

@ -0,0 +1,40 @@
AddCSLuaFile()
cbox = cbox or {}
include("cbox/sh_utils.lua")
if SERVER then
AddCSLuaFile("cbox/cl_hooks.lua")
AddCSLuaFile("cbox/cl_chatbox.lua")
local tab_files = file.Find("cbox/tabs/*", "LUA")
for _, name in ipairs(tab_files) do
AddCSLuaFile("cbox/tabs/" .. name)
end
elseif CLIENT then
include("cbox/cl_hooks.lua")
include("cbox/cl_chatbox.lua")
end
local module_files = file.Find("cbox/modules/*", "LUA")
for _, name in ipairs(module_files) do
local full_path = "cbox/modules/" .. name
if name:StartsWith("cl_") then
if SERVER then
AddCSLuaFile(full_path)
elseif CLIENT then
cbox.utils.RealmPrint("Loading module:", name)
include(full_path)
end
elseif name:StartsWith("sv_") and SERVER then
cbox.utils.RealmPrint("Loading module:", name)
include(full_path)
else
if SERVER then
AddCSLuaFile(full_path)
end
cbox.utils.SharedPrint("Loading module:", name)
include(full_path)
end
end

126
lua/cbox/sh_utils.lua Normal file
View File

@ -0,0 +1,126 @@
AddCSLuaFile()
local cbox = cbox
local MsgC = MsgC
local Color = Color
local ipairs = ipairs
local tonumber = tonumber
local tostring = tostring
local string = string
local string_Explode = string.Explode
local color_white = Color(255, 255, 255)
local utils = {}
utils.colors = {
BRAND = Color(132, 206, 181),
CLIENT = Color(143, 218, 230),
SERVER = Color(230, 218, 112),
SHARED = Color( 93, 200, 92),
ERROR = Color(255, 90, 90),
}
---Parses a string into a color
---@param str string Input color as "R G B" or "R G B A"
---@param alpha? boolean Whether to parse alpha or not (default false)
---@return Color
function utils.ParseColorString(str, alpha)
alpha = alpha ~= nil and alpha or false
local split = string_Explode(" ", str)
local r = tonumber(split[1]) or 255
local g = tonumber(split[2]) or 255
local b = tonumber(split[3]) or 255
local a = alpha and tonumber(split[4] or 255) or 255
return Color(r, g, b, a)
end
---Prints to console with realm prefix
---@param ... any
function utils.RealmPrint(...)
local args = {...}
for i, arg in ipairs(args) do
args[i] = tostring(arg)
end
MsgC(utils.colors.BRAND, "[cbox -> ")
local realm, realm_color = "???", utils.colors.SHARED
if CLIENT then
realm = "CLIENT"
realm_color = utils.colors.CLIENT
elseif SERVER then
realm = "SERVER"
realm_color = utils.colors.SERVER
end
MsgC(realm_color, realm)
MsgC(utils.colors.BRAND, "] ")
MsgC(color_white, table.concat(args, " "), "\n")
end
---Prints to console with the "shared" prefix
---@param ... any
function utils.SharedPrint(...)
local args = {...}
for i, arg in ipairs(args) do
args[i] = tostring(arg)
end
MsgC(utils.colors.BRAND, "[cbox -> ")
MsgC(utils.colors.SHARED, "SHARED")
MsgC(utils.colors.BRAND, "] ")
MsgC(color_white, table.concat(args, " "), "\n")
end
---Prints to console with realm prefix, but in red
---@param ... any
function utils.RealmError(...)
local args = {...}
for i, arg in ipairs(args) do
args[i] = tostring(arg)
end
MsgC(utils.colors.BRAND, "[cbox -> ")
local realm, realm_color = "???", utils.colors.SHARED
if CLIENT then
realm = "CLIENT"
realm_color = utils.colors.CLIENT
elseif SERVER then
realm = "SERVER"
realm_color = utils.colors.SERVER
end
MsgC(realm_color, realm)
MsgC(utils.colors.BRAND, "] ")
MsgC(utils.colors.ERROR, table.concat(args, " "), "\n")
end
---Prints to console with the "shared" prefix, but in red
---@param ... any
function utils.SharedError(...)
local args = {...}
for i, arg in ipairs(args) do
args[i] = tostring(arg)
end
MsgC(utils.colors.BRAND, "[cbox -> ")
MsgC(utils.colors.SHARED, "SHARED")
MsgC(utils.colors.BRAND, "] ")
MsgC(utils.colors.ERROR, table.concat(args, " "), "\n")
end
cbox.utils = utils

83
lua/cbox/tabs/chat.lua Normal file
View File

@ -0,0 +1,83 @@
cbox.chatbox.AddTab("\1chat", "Chat", "icon16/comments.png", function()
local wrapper = vgui.Create("EditablePanel")
-- TODO: custom richtext panel
local history = vgui.Create("RichText", wrapper)
history:Dock(FILL)
cbox.chatbox.panels.history = history
function history:PerformLayout()
-- TODO: configurable font
history:SetFontInternal("ChatFont")
end
local input_wrapper = vgui.Create("EditablePanel", wrapper)
input_wrapper:SetHeight(20)
input_wrapper:Dock(BOTTOM)
local input = vgui.Create("DTextEntry", input_wrapper)
input:Dock(FILL)
cbox.chatbox.panels.input = input
local mode_switch = vgui.Create("DButton", input_wrapper)
mode_switch:SetText("Say")
mode_switch:SizeToContents()
mode_switch:Dock(LEFT)
cbox.chatbox.panels.mode_switch = mode_switch
function mode_switch:DoClick()
-- TODO
end
function mode_switch:DoRightClick()
-- TODO
end
function input:OnKeyCodeTyped(key)
if key == KEY_ESCAPE then
cbox.chatbox.Close()
gui.HideGameUI()
elseif key == KEY_BACKQUOTE then
gui.HideGameUI()
elseif key == KEY_ENTER then
local text = self:GetText():Trim()
if text ~= "" then
RunConsoleCommand("say", text)
end
cbox.chatbox.Close()
elseif key == KEY_TAB then
if #self:GetText() == 0 then
-- TODO: mode switch
else
-- TODO: autocomplete
end
else
hook.Run("ChatTextChanged", self:GetText())
end
end
return wrapper
end)
hook.Add("OnChatAddText", "cbox.chatbox.history", function(args)
local history = cbox.chatbox.panels.history
for _, arg in ipairs(args) do
if IsColor(arg) then
history:InsertColorChange(arg.r, arg.g, arg.b, 255)
elseif isstring(arg) then
history:AppendText(arg)
elseif isentity(arg) then
if IsValid(arg) and arg:IsPlayer() then
local col = hook.Run("GetTeamColor", arg)
history:InsertColorChange(col.r, col.g, col.b, 255)
history:AppendText(arg:Name())
else
local col = cbox.utils.colors.BRAND
history:InsertColorChange(col.r, col.g, col.b, 255)
history:AppendText("???")
end
end
end
history:AppendText("\n")
end)