New addon: SprayMesh Extended

This commit is contained in:
Chev 2023-11-04 13:40:49 -07:00
parent 6f29f9e3a4
commit 4d57449766
Signed by: chev2
GPG key ID: BE0CFBD5DCBB2511
29 changed files with 2464 additions and 0 deletions

View file

@ -18,5 +18,6 @@ Listed below is all of the addons you can find in this repo.
- `ror2_hud`: A Risk of Rain 2-inspired replacement to the default HUD. [Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2210715789)
- `screenshot_editor`: Lets you customize & save your Garry's Mod screenshots by applying effects and filters. [Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2910871996)
- `simple_bunnyhop`: A gamemode designed for bunnyhopping. [Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=1767781900)
- `spraymesh_extended`: An improvement to the original SprayMesh with various new features, bug fixes and optimizations. [Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=3072351693)
- `vector_loc`: A developer tool that allows you to visualize the locations of vector coordinates on a map. [Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=1782161573)
- `vm_velocity`: Makes viewmodels move up and down depending on how fast you moving vertically. [Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2294290206)

View file

@ -0,0 +1,6 @@
{
"title": "SprayMesh Extended",
"type": "effects",
"tags": ["fun", "build"],
"ignore": []
}

View file

@ -0,0 +1,2 @@
-- Initialize SprayMesh Extended on the client
include("spraymesh/client/cl_init.lua")

View file

@ -0,0 +1,11 @@
-- Initialize SprayMesh Extended on the server
include("spraymesh/server/sv_init.lua")
-- Send Lua files to the client
AddCSLuaFile("spraymesh/sh_init.lua")
AddCSLuaFile("spraymesh/sh_config.lua")
AddCSLuaFile("spraymesh/client/cl_init.lua")
AddCSLuaFile("spraymesh/client/cl_spray_list_db.lua")
AddCSLuaFile("spraymesh/client/cl_derma_utils.lua")
AddCSLuaFile("spraymesh/client/cl_sandbox_context_menu.lua")

View file

@ -0,0 +1,107 @@
spraymesh_derma_utils = spraymesh_derma_utils or {}
-- Enables the maximize button on DFrame panels, used by SprayMesh Extended panels
function spraymesh_derma_utils.EnableMaximizeButton(dframe)
dframe.btnMaxim.Maximized = false
dframe.btnMaxim.OriginalSize = {panelWidth, panelHeight}
dframe.btnMaxim.OriginalPos = {dframe:GetX(), dframe:GetY()}
dframe.btnMaxim:SetDisabled(false)
dframe.btnMaxim.DoClick = function(pnl)
local targetSize = {512, 512}
local targetPos = {0, 0}
-- If we're maximized, unmaximize
if pnl.Maximized then
targetSize = pnl.OriginalSize
targetPos = pnl.OriginalPos
-- If we're unmaximized, maximize
else
-- Store current position and size if the user decides to unmaximize later
pnl.OriginalSize = {dframe:GetSize()}
pnl.OriginalPos = {dframe:GetPos()}
targetSize = {ScrW(), ScrH()}
targetPos = {0, 0}
end
pnl.Maximized = not pnl.Maximized
-- Don't allow the button to be clicked while the transition animation plays
pnl:SetEnabled(false)
local animData = dframe:NewAnimation(0.4, 0, 0.3, function(animTable, tgtPanel)
if IsValid(pnl) then
pnl:SetEnabled(true)
end
end)
animData.StartSize = {dframe:GetSize()}
animData.EndSize = targetSize
animData.StartPos = {dframe:GetPos()}
animData.EndPos = targetPos
animData.Think = function(animTable, tgtPanel, fraction)
local easedFraction = math.ease.OutSine(fraction)
local easedPosX = Lerp(easedFraction, animTable.StartPos[1], animTable.EndPos[1])
local easedPosY = Lerp(easedFraction, animTable.StartPos[2], animTable.EndPos[2])
local easedSizeW = Lerp(easedFraction, animTable.StartSize[1], animTable.EndSize[1])
local easedSizeH = Lerp(easedFraction, animTable.StartSize[2], animTable.EndSize[2])
tgtPanel:SetPos(math.Round(easedPosX, 0), math.Round(easedPosY, 0))
tgtPanel:SetSize(math.Round(easedSizeW, 0), math.Round(easedSizeH, 0))
end
end
end
-- Get preview HTML to preview sprays using DHTML
local PREVIEW_HTML_BASE = [=[
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
background: transparent;
}
html {
overflow: hidden;
}
img, video {
width: %spx;
height: %spx;
object-fit: contain;
}
</style>
</head>
<body>
%s
</body>
</html>
]=]
local PREVIEW_HTML_IMAGE = [=[<img src="%s"]=]
local PREVIEW_HTML_VIDEO = [=[<video src="%s" muted autoplay loop>]=]
function spraymesh_derma_utils.GetPreviewHTML(previewSize, sprayURL)
local elementFormatted = ""
if spraymesh.IsVideoExtension(sprayURL) then
elementFormatted = Format(PREVIEW_HTML_VIDEO, string.JavascriptSafe(sprayURL))
elseif spraymesh.IsImageExtension(sprayURL) then
elementFormatted = Format(PREVIEW_HTML_IMAGE, string.JavascriptSafe(sprayURL))
-- If we can't figure out the type, assume it's an image
else
elementFormatted = Format(PREVIEW_HTML_IMAGE, string.JavascriptSafe(sprayURL))
spraymesh.DebugPrint("(spraymesh_derma_utils.GetPreviewHTML) Could not figure out image/video type for URL " .. sprayURL)
end
return Format(
PREVIEW_HTML_BASE,
previewSize,
previewSize,
elementFormatted
)
end

View file

@ -0,0 +1,997 @@
-- Include shared Lua files
include("spraymesh/sh_init.lua")
-- Include clientside Lua files (usually dependencies before we run the main script here)
include("spraymesh/client/cl_spray_list_db.lua")
include("spraymesh/client/cl_derma_utils.lua")
include("spraymesh/client/cl_sandbox_context_menu.lua")
--
-- Create ConVars
--
local CVAR_ENABLE_SPRAYS = CreateClientConVar("spraymesh_enablesprays", "1", true, false, "Whether or not to show all player sprays.", 0, 1)
local CVAR_ENABLE_ANIMATED_SPRAYS = CreateClientConVar("spraymesh_enableanimated", "1", true, false, "Whether or not to show animated sprays.", 0, 1)
CreateClientConVar("spraymesh_url", spraymesh.SPRAY_URL_DEFAULT, true, true, "The URL to use for your spray.")
--
-- Clientside variables and such
--
-- Used by the client to render sprays in order
-- Done so sprays can be "overwritten", and also for performance
spraymesh.RENDER_ITER_CLIENT = spraymesh.RENDER_ITER_CLIENT or {}
setmetatable(spraymesh.SPRAYDATA, {
-- Reset render iteration table cache when the main spraymesh table is modified
__newindex = function(tb, key, value)
rawset(tb, key, value)
spraymesh.RENDER_ITER_CLIENT = nil
end,
})
-- Whether or not we're currently rendering names over player sprays (via spraymesh_shownames)
local SPRAY_SHOWING_NAMES = false
-- Sprays that need to be reloaded will be put in here
local SPRAY_RELOAD_QUEUE = {}
function spraymesh.ReloadSprays()
spraymesh.RemoveSprays()
for id64, data in pairs(spraymesh.SPRAYDATA) do
SPRAY_RELOAD_QUEUE[id64] = data
end
end
function spraymesh.ReloadSpray(id64)
if not spraymesh.SPRAYDATA[id64] then return end
SPRAY_RELOAD_QUEUE[id64] = spraymesh.SPRAYDATA[id64]
end
function spraymesh.RemoveSpray(id64)
if not spraymesh.SPRAYDATA[id64] then return end
local meshData = spraymesh.SPRAYDATA[id64].meshdata
if meshData and meshData.mesh and IsValid(meshData.mesh) then
meshData.mesh:Destroy()
meshData.mesh = nil
end
spraymesh.SPRAYDATA[id64] = nil
end
function spraymesh.Instructions()
chat.AddText(
spraymesh.PRIMARY_CHAT_COLOR, "This server uses ",
spraymesh.ACCENT_CHAT_COLOR, "SprayMesh Extended! ",
spraymesh.PRIMARY_CHAT_COLOR, "Use /spraymesh to change your spray."
)
end
-- URL material solver
local imats = {}
-- Where panels are during loading
local htmlpanels = {}
-- Where panels are for animation, after loading
local htmlpanelsanim = {}
local RT_SPRAY_PENDING = GetRenderTargetEx(
"spraymesh_pending_spray",
spraymesh.IMAGE_RESOLUTION,
spraymesh.IMAGE_RESOLUTION,
RT_SIZE_DEFAULT,
MATERIAL_RT_DEPTH_SEPARATE,
bit.bor(4, 8, 16, 256),
0,
IMAGE_FORMAT_BGR888
)
local MAT_SPRAY_PENDING = CreateMaterial("spraymesh/pending_spray_placeholder", "UnlitGeneric", {
["$basetexture"] = RT_SPRAY_PENDING:GetName(),
-- Allows custom coloring
["$vertexcolor"] = 1,
["$vertexalpha"] = 1,
["$model"] = 1,
["$nocull"] = 1,
["$receiveflashlight"] = 1
})
local RT_SPRAY_DISABLEDVIDEO = GetRenderTargetEx(
"spraymesh_disabled_video",
spraymesh.IMAGE_RESOLUTION,
spraymesh.IMAGE_RESOLUTION,
RT_SIZE_DEFAULT,
MATERIAL_RT_DEPTH_SEPARATE,
bit.bor(4, 8, 16, 256),
0,
IMAGE_FORMAT_BGR888
)
local RT_SPRAY_DISABLEDSPRAY = GetRenderTargetEx(
"spraymesh_disabled_spray",
spraymesh.IMAGE_RESOLUTION,
spraymesh.IMAGE_RESOLUTION,
RT_SIZE_DEFAULT,
MATERIAL_RT_DEPTH_SEPARATE,
bit.bor(4, 8, 16, 256),
0,
IMAGE_FORMAT_BGR888
)
-- "url" is the URL with ?uniquerequest= stuff added at the end and https:// added to the front
-- "urloriginal" is simply the original URL
local function generateHTMLPanel(url, urloriginal, callback)
if not string.find(url, "^https?://", 0, false) then
url = "https://" .. url
end
spraymesh.DebugPrint("Generating HTML panel: ", url)
-- Use spray image resolution from config
local size = spraymesh.IMAGE_RESOLUTION
-- Persisting container, for cutting short anims but also drawing an overlay
local panelContainer = {}
local panelHTML = vgui.Create("DHTML")
panelHTML:SetSize(size, size)
panelHTML:SetAllowLua(false)
panelHTML:SetAlpha(0)
panelHTML:SetMouseInputEnabled(false)
panelHTML:SetScrollbars(false)
panelHTML.ConsoleMessage = function(panel, msg)
spraymesh.DebugPrint("HTML ConsoleMessage: " .. tostring(msg))
end
panelContainer.panel = panelHTML
-- Set image/video HTML for the panel
spraymesh.HTMLHandlers.Get(url, size, panelContainer)
panelContainer.origurl = urloriginal
panelContainer.callback = callback
panelContainer.IsAnimated = (spraymesh.GetURLInfo(urloriginal) == SPRAYTYPE_VIDEO or string.EndsWith(urloriginal, ".gif"))
panelContainer.RT = GetRenderTargetEx(
"SprayMesh_URL_" .. util.SHA256(url),
size,
size,
RT_SIZE_DEFAULT,
MATERIAL_RT_DEPTH_SEPARATE,
bit.bor(4, 8, 16, 256),
0,
IMAGE_FORMAT_BGRA8888
)
function panelContainer:PaintSpray()
if not self.FinalMaterial then return end
-- If sprays aren't enabled AT ALL
if not CVAR_ENABLE_SPRAYS:GetBool() then
self.FinalMaterial:SetTexture("$basetexture", RT_SPRAY_DISABLEDSPRAY)
return
end
-- If animated sprays aren't enabled
if self.IsAnimated and not CVAR_ENABLE_ANIMATED_SPRAYS:GetBool() then
self.FinalMaterial:SetTexture("$basetexture", RT_SPRAY_DISABLEDVIDEO)
return
end
-- If spraymesh_shownames was called, show a black background
if SPRAY_SHOWING_NAMES then
-- This makes the spray invisible/black for animkilled sprays...
self.FinalMaterial:SetTexture("$basetexture", self.RT)
else
-- FPS saver when not showing names
self.FinalMaterial:SetTexture("$basetexture", self.htmlmat:GetName())
return
end
render.PushRenderTarget(self.RT)
cam.Start2D()
local sW, sH = ScrW(), ScrH()
local spraytex = surface.GetTextureID(self.htmlmat:GetName())
surface.SetDrawColor(255, 255, 255, 255)
surface.SetTexture(spraytex)
surface.DrawTexturedRect(0, 0, sW, sH)
if SPRAY_SHOWING_NAMES then
local count = 1
for id64, data in pairs(spraymesh.SPRAYDATA) do
-- * This is stupid and inefficient, but it only runs when spraymesh_shownames is called,
-- * in which case, good FPS probably isn't important at that very moment
if data.url == urloriginal then
surface.SetDrawColor(0, 255, 0, 255)
surface.DrawOutlinedRect(0, 0, sW, sH, 3)
local text = ("%s (%s)"):format(data.PlayerName, id64)
draw.WordBox(4, 10, (32 * count) - 22, text, "TargetID", color_black, color_white)
count = count + 1
end
end
draw.WordBox(4, 10, sH - 38, urloriginal, "TargetID", color_black, color_white)
end
cam.End2D()
render.PopRenderTarget()
end
table.insert(htmlpanels, panelContainer)
end
local function generateHTMLTexture(url, meshData, callback)
--[[
how to use:
MyNewImaterial = generateHTMLTexture(url, meshData, function(imat)
-- custom callback code, for when the image is fully loaded and the meshData has been applied
-- imat argument is the loaded imaterial
end)
meshData is a table pointer, and needs to contain an imaterial key
]]
spraymesh.DebugPrint("Generating HTML material for " .. url)
-- If the IMaterial doesn't exist yet, initialize it
if imats[url] == nil then
-- Pending table
imats[url] = {}
table.insert(imats[url], {meshData, callback})
-- The uniquerequest guff is to stop the game from ever using its internal cache of web resources, because it returns bonkers sizes at random
local newURL = url .. "?uniquerequest=" .. math.floor(SysTime() * 1000)
generateHTMLPanel(newURL, url, function(imat)
-- Should be
if type(imats[url]) == "table" then
for k, v in pairs(imats[url]) do
local meshDataCurrent = v[1]
local optionalCallback = v[2]
meshDataCurrent.imaterial = imat
if optionalCallback then
optionalCallback(imat)
end
spraymesh.DebugPrint("Finished generating HTML material; replacing dummy texture")
end
imats[url] = imat
end
end)
spraymesh.DebugPrint("Generating, giving dummy texture")
return MAT_SPRAY_PENDING
elseif type(imats[url]) == "table" then
-- Pending table; texture is still generating
spraymesh.DebugPrint("Generated texture is currently pending...")
table.insert(imats[url], {meshData, callback})
return MAT_SPRAY_PENDING
else
spraymesh.DebugPrint("Generated texture already exists")
return imats[url]
end
end
local function copyVert(copy, u, v, norm, bnorm, tang)
u = u or 0
v = v or 0
norm = norm or 1
bnorm = bnorm or Vector(0, 0, 0)
tang = tang or 1
local t = table.Copy(copy)
t.u, t.v, t.normal, t.bitnormal, t.tangent = u, v, norm, bnorm, tang
return t
end
-- D C = ix+0,iy+1 ix+1,iy+1
-- A B = ix+0,iy+0 ix+1,iy+0
-- Bottom left corner coord
local function addSquareToPoints(x, y, points, coords)
--[[local _a = copyVert(coords[x+0][y+0],0,0) -- Repeating texture per square
local _b = copyVert(coords[x+1][y+0],1,0) -- Probably also needs a y flip
local _c = copyVert(coords[x+1][y+1],1,1)
local _d = copyVert(coords[x+0][y+1],0,1)]]
local rm1 = spraymesh.MESH_RESOLUTION - 1
local __a = coords[x + 0][y + 0]
local __b = coords[x + 1][y + 0]
local __c = coords[x + 1][y + 1]
local __d = coords[x + 0][y + 1]
if __a.bad then
__a = coords[x + 0][math.Clamp(y + 1, 0, spraymesh.MESH_RESOLUTION - 1)]
end
if __b.bad then
__b = coords[x + 1][math.Clamp(y + 1, 0, spraymesh.MESH_RESOLUTION - 1)]
end
if __c.bad then
__c = coords[x + 1][math.Clamp(y + 0, 0, spraymesh.MESH_RESOLUTION - 1)]
end
if __d.bad then
__d = coords[x + 0][math.Clamp(y + 0, 0, spraymesh.MESH_RESOLUTION - 1)]
end
-- Probably could simply replace the other but eh
if __a.bad then
__a = coords[math.Clamp(x + 1, 0, spraymesh.MESH_RESOLUTION - 1)][math.Clamp(y + 1, 0, spraymesh.MESH_RESOLUTION - 1)]
end
if __b.bad then
__b = coords[math.Clamp(x + 0, 0, spraymesh.MESH_RESOLUTION - 1)][math.Clamp(y + 1, 0, spraymesh.MESH_RESOLUTION - 1)]
end
if __c.bad then
__c = coords[math.Clamp(x + 0, 0, spraymesh.MESH_RESOLUTION - 1)][math.Clamp(y + 0, 0, spraymesh.MESH_RESOLUTION - 1)]
end
if __d.bad then
__d = coords[math.Clamp(x + 1, 0, spraymesh.MESH_RESOLUTION - 1)][math.Clamp(y + 0, 0, spraymesh.MESH_RESOLUTION - 1)]
end
local _a = copyVert(__a, (x + 0) / rm1, 1 - ((y + 0) / rm1)) -- Stretch texture over all squares
local _b = copyVert(__b, (x + 1) / rm1, 1 - ((y + 0) / rm1))
local _c = copyVert(__c, (x + 1) / rm1, 1 - ((y + 1) / rm1))
local _d = copyVert(__d, (x + 0) / rm1, 1 - ((y + 1) / rm1))
table.insert(points, _a) -- Adccba
table.insert(points, _d)
table.insert(points, _c)
table.insert(points, _c)
table.insert(points, _b)
table.insert(points, _a)
end
function spraymesh.PlaceSpray(sprayData)
local id64 = sprayData.SteamID64
local nick = sprayData.PlayerName
local hitpos = sprayData.HitPos
local hitnormal = sprayData.HitNormal
local url = sprayData.URL
local playSpraySound = sprayData.PlaySpraySound
local coordDist = sprayData.CoordDistance
local sprayTime = sprayData.SprayTime
local tracenormal = sprayData.TraceNormal
local anglenormal = tracenormal:Angle()
anglenormal:Normalize()
local URLToSpray = url
local lpid64 = LocalPlayer():SteamID64()
-- Give other code a chance to block the spray on the client
local shouldAllowSpray = hook.Run("SprayMesh.ClientShouldAllowSpray", sprayData) ~= false
if not shouldAllowSpray then return end
-- If the local player is spraying the default spray, show them help instructions in chat
local sprayIsDefault = url == spraymesh.SPRAY_URL_DEFAULT
sprayIsDefault = sprayIsDefault or url == "http://" .. spraymesh.SPRAY_URL_DEFAULT
sprayIsDefault = sprayIsDefault or url == "https://" .. spraymesh.SPRAY_URL_DEFAULT
if id64 == lpid64 and sprayIsDefault then
spraymesh.Instructions()
end
-- Play the spray sound
if playSpraySound then sound.Play("SprayCan.Paint", hitpos, 60, 100, .3) end
--
-- Create spray mesh
--
-- Benchmark how long it takes to create the spray mesh
local timestart = SysTime()
local pos = hitpos + hitnormal -- One unit out
local points = {}
local coords = {}
--
-- Calculate spray angle
--
local tangang = hitnormal:Angle()
tangang:Normalize()
-- Note to anyone who reads this:
-- I pretty much just fiddled with random values and equations until I got it right.
-- If you're a math person and can understand it, great.
local angToRotateBy = 0
if tangang.p < 0 then
angToRotateBy = 180 + (anglenormal - tangang).y
elseif tangang.p > 0 then
angToRotateBy = 180 + (tangang - anglenormal).y
end
tangang:RotateAroundAxis(tangang:Forward(), angToRotateBy)
--
-- Calculate spray's mesh coordinates
--
coordDist = coordDist or spraymesh.COORD_DIST_DEFAULT
-- Sizing formula to keep the spray the same size (roughly) when mesh resolution changes
coordDist = coordDist * (1 / spraymesh.MESH_RESOLUTION) * 30
for ix = 0, spraymesh.MESH_RESOLUTION - 1 do
coords[ix] = {}
for iy = 0, spraymesh.MESH_RESOLUTION - 1 do
coords[ix][iy] = {}
local coord = coords[ix][iy]
--local yawMultiplier = math.abs(tangang.p) / 180
--tangang.y = math.Remap(yawMultiplier, 0, 1, anglenormal.y, tangang.p)
coord.pos = pos + (-(tangang:Right() * ix) + (tangang:Up() * iy)) * coordDist
coord.pos = coord.pos + (tangang:Right() * coordDist * spraymesh.MESH_RESOLUTION / 2) - (tangang:Up() * coordDist * spraymesh.MESH_RESOLUTION / 1.8)
if not (ix == 0 and iy == 0) then
local testtr = util.TraceLine({
start = coord.pos + hitnormal * 16,
endpos = coord.pos - hitnormal * 16,
filter = function(ent)
if ent:IsWorld() then return true end
end
})
if not testtr.Hit or not testtr.HitWorld then
if ix == 0 then
coord.pos = coords[ix][iy - 1].pos
else
coord.pos = coords[ix - 1][iy].pos
end
coord.bad = true
else
coord.pos = testtr.HitPos + hitnormal
end
end
coord.u, coord.v = 0, 0
coord.bitnormal = 1
coord.tangent = 1
coord.normal = hitnormal
--
-- Calculate vertex color
--
local lcol = render.ComputeLighting(coord.pos, hitnormal) + render.GetAmbientLightColor()
lcol = lcol * 255
local baseBrightness = 60
local finalCol = Color(255, 255, 255)
finalCol.r = math.min(lcol.x + baseBrightness, 255)
finalCol.g = math.min(lcol.y + baseBrightness, 255)
finalCol.b = math.min(lcol.z + baseBrightness, 255)
coord.color = finalCol
end
end
for ix = 0, spraymesh.MESH_RESOLUTION - 2 do
for iy = 0, spraymesh.MESH_RESOLUTION - 2 do
addSquareToPoints(ix, iy, points, coords)
end
end
-- Create the actual mesh for the spray
local meshdata = {}
meshdata.mesh = Mesh()
meshdata.mesh:BuildFromTriangles(points)
meshdata.imaterial = generateHTMLTexture(URLToSpray, meshdata)
-- Remove the existing spray, if any
spraymesh.RemoveSpray(id64)
-- Put together new spray info table
local sprayInfo = spraymesh.SPRAYDATA[id64] or {}
sprayInfo.meshdata = meshdata
sprayInfo.meshdata.url = URLToSpray
sprayInfo.hitpos = hitpos
sprayInfo.hitnormal = hitnormal
sprayInfo.TraceNormal = tracenormal
sprayInfo.url = url
sprayInfo.PlayerName = nick
sprayInfo.CoordDistance = coordDist
sprayInfo.Time = sprayTime or CurTime()
spraymesh.SPRAYDATA[id64] = sprayInfo
spraymesh.DebugPrint("Spray mesh created in: " .. SysTime() - timestart .. "s")
end
-- Removes all fully loaded sprays
function spraymesh.RemoveSprays()
for k, v in pairs(htmlpanelsanim) do
imats[v.origurl] = nil
v.panel:Remove()
end
for k, v in pairs(spraymesh.SPRAYDATA) do
if v.meshdata and v.meshdata.mesh then
v.meshdata.mesh:Destroy()
v.meshdata.mesh = nil
end
end
htmlpanelsanim = {}
end
--
-- HTML handlers
--
-- This is the HTML that prepares the spray to be displayed
--
spraymesh.HTMLHandlers = {}
function spraymesh.HTMLHandlers.Get(url, size, panelcontainer)
-- Remove uniquerequest garbage
url = string.Explode("?", url, false)[1]
-- Needs redoing for the extension
local sprayType = spraymesh.GetURLInfo(url)
if sprayType == SPRAYTYPE_IMAGE then
spraymesh.DebugPrint("Using HTMLHandlers.Image for URL: " .. url)
return spraymesh.HTMLHandlers.Image(url, size, panelcontainer)
elseif sprayType == SPRAYTYPE_VIDEO then
spraymesh.DebugPrint("Using HTMLHandlers.Video for URL: " .. url)
return spraymesh.HTMLHandlers.Video(url, size, panelcontainer)
end
spraymesh.DebugPrint("Using (FALLBACK) HTMLHandlers.Image for URL: " .. url)
return spraymesh.HTMLHandlers.Image(url, size, panelcontainer)
end
local SPRAY_HTML_IMAGE = [=[
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>title</title>
<style type = "text/css">
html {
overflow: hidden;
}
body {
margin: 0;
background: transparent;
}
img {
width: 100%;
height: 100%;
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
object-fit: contain;
}
</style>
</head>
<body>
<div id="sprayimage"></div>
<script>
// Thanks to http://www.andygup.net/tag/magic-number/
var imageContainer = document.getElementById("sprayimage");
function getImageType(arrayBuffer) {
var type = "";
var dv = new DataView(arrayBuffer, 0, 5);
var nume1 = dv.getUint8(0);
var nume2 = dv.getUint8(1);
var hex = nume1.toString(16) + nume2.toString(16);
switch (hex) {
case "8950":
type = "image/png";
break;
case "4749":
type = "image/gif";
break;
case "424d":
type = "image/bmp";
break;
case "ffd8":
type = "image/jpeg";
break;
default:
type = "application/octet-stream";
break;
}
return type;
}
function getImageFromServer(path, callback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", path, true);
xhr.responseType = "arraybuffer";
xhr.onload = function (e) {
if (this.status == 200) {
var imageType = getImageType(this.response);
callback(imageType);
}
else {
//console.log("Problem retrieving image " + JSON.stringify(e))
callback("NIL");
}
}
xhr.send();
}
function makeimage() {
var src = "{SPRAY_URL}";
getImageFromServer(src, function (imageType) {
console.log("Image Type: " + imageType);
// Anti-GIF
// TODO: Do GIF files still drain FPS on current-day Garry's Mod?
// It might not even be necessary to limit them nowadays
/*if (imageType == "image/gif") {
src = "https://{SPRAY_URL_ANTIGIF}";
}*/
var sprayImage = document.createElement("img");
sprayImage.src = src;
console.log(src);
// Check to ensure image container is valid before appending our img element
if (!!imageContainer) {
imageContainer.appendChild(sprayImage);
}
});
};
makeimage();
</script>
</body>
</html>
]=]
function spraymesh.HTMLHandlers.Image(url, size, panelcontainer)
local sprayHTML = SPRAY_HTML_IMAGE
sprayHTML = sprayHTML:Replace("{SPRAY_URL}", string.JavascriptSafe(url))
sprayHTML = sprayHTML:Replace("{SIZE}", string.JavascriptSafe(size))
--sprayHTML = sprayHTML:Replace("{SPRAY_URL_ANTIGIF}", string.JavascriptSafe(spraymesh.SPRAY_URL_ANTIGIF))
panelcontainer.panel:SetHTML(sprayHTML)
end
local SPRAY_HTML_VIDEO = [=[
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
html {
overflow: hidden;
}
body {
margin: 0;
background: transparent;
}
video {
width: 100%;
height: 100%;
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
object-fit: contain;
}
</style>
</head>
<body>
<video id="sprayimage" onload="fiximage()" src="{SPRAY_URL}" autoplay loop muted>
<script>
function fiximage() {
var videoElem = document.getElementById("sprayimage");
if (!!videoElem && videoElem.height > videoElem.width) {
videoElem.style.height = "{SIZE}px";
videoElem.style.width = "auto";
}
};
</script>
</body>
</html>
]=]
function spraymesh.HTMLHandlers.Video(url, size, panelcontainer)
local sprayHTML = SPRAY_HTML_VIDEO
sprayHTML = sprayHTML:Replace("{SPRAY_URL}", string.JavascriptSafe(url))
sprayHTML = sprayHTML:Replace("{SIZE}", string.JavascriptSafe(size))
panelcontainer.panel:SetHTML(sprayHTML)
end
--
-- Network handlers
--
-- Received when the server wants to place a player's spray
net.Receive("SprayMesh.SV_SendSpray", function(length)
local id64 = net.ReadString()
local nick = net.ReadString()
local hitPos = net.ReadVector()
local hitNormal = net.ReadVector()
local traceNormal = net.ReadNormal()
local url = net.ReadString()
local coordDist = net.ReadFloat()
local sprayTime = net.ReadFloat()
spraymesh.DebugPrint("Receiving spray: " .. url)
local sprayData = {
SteamID64 = id64,
PlayerName = nick,
HitPos = hitPos,
HitNormal = hitNormal,
TraceNormal = traceNormal,
URL = url,
CoordDistance = coordDist,
SprayTime = sprayTime,
PlaySpraySound = true
}
spraymesh.PlaceSpray(sprayData)
end)
-- Received when the server wants to remove a player's spray
net.Receive("SprayMesh.SV_ClearSpray", function()
local id64 = net.ReadString()
spraymesh.RemoveSpray(id64)
end)
--
-- Hooks
--
hook.Add("Think", "SprayMesh.Generate", function()
for k, v in pairs(htmlpanels) do
local htmlmat = v.panel:GetHTMLMaterial()
if v and htmlmat then
spraymesh.DebugPrint("FINISHED")
local uid = string.Replace(htmlmat:GetName(), "__vgui_texture_", "")
spraymesh.DebugPrint("Material name: spraymesh_" .. uid)
local FinalMaterial = CreateMaterial("spraymesh_" .. uid, "UnlitGeneric", {
["$basetexture"] = htmlmat:GetName(),
["$vertexcolor"] = 1,
["$vertexalpha"] = 1,
["$model"] = 1,
["$nocull"] = 1,
["$receiveflashlight"] = 1
})
v.callback(FinalMaterial)
table.remove(htmlpanels, k)
table.insert(htmlpanelsanim, v)
v.FinalMaterial = FinalMaterial
v.htmlmat = htmlmat
break
else
spraymesh.DebugPrint("GENERATING...")
end
end
end)
-- Ensures animated sprays are still animating properly (e.g. IMaterial is still valid)
hook.Add("Think", "SprayMesh.HandleAnimatedSprays", function()
for index, panelData in ipairs(htmlpanelsanim) do
if panelData then
if panelData.origurl then
if imats[panelData.origurl] == nil then
panelData.panel:Remove()
panelData = nil
table.remove(htmlpanelsanim, index)
break
end
else
table.remove(htmlpanelsanim, index)
break
end
else
table.remove(htmlpanelsanim, index)
break
end
end
end)
hook.Add("PostDrawHUD", "SprayMesh.AnimatedSpraysPaint", function()
for k, panelData in ipairs(htmlpanelsanim) do
if not panelData then
table.remove(htmlpanelsanim, k)
break
end
if panelData.PaintSpray then
panelData:PaintSpray()
end
end
end)
hook.Add("PostDrawHUD", "SprayMesh.GenerateSprayPlaceholderTextures", function()
render.PushRenderTarget(RT_SPRAY_PENDING)
render.Clear(0, 0, 0, 255, true, true)
cam.Start2D()
draw.SimpleText("Loading spray...", "DermaLarge", ScrW() / 2, ScrH() / 2, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
cam.End2D()
render.PopRenderTarget()
render.PushRenderTarget(RT_SPRAY_DISABLEDVIDEO)
render.Clear(0, 0, 0, 255, true, true)
cam.Start2D()
surface.SetDrawColor(255, 0, 0, 255)
surface.DrawOutlinedRect(0, 0, ScrW(), ScrH(), 3)
draw.SimpleText("This spray is animated, but you have animated sprays turned off.", "DermaDefaultBold", ScrW() / 2, ScrH() / 2 - 16, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
draw.SimpleText("Use /spraymesh to enable animated sprays.", "DermaDefaultBold", ScrW() / 2, ScrH() / 2 + 16, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
cam.End2D()
render.PopRenderTarget()
render.PushRenderTarget(RT_SPRAY_DISABLEDSPRAY)
render.Clear(0, 0, 0, 255, true, true)
cam.Start2D()
surface.SetDrawColor(255, 255, 0, 255)
surface.DrawOutlinedRect(0, 0, ScrW(), ScrH(), 3)
draw.SimpleText("[spray disabled]", "DermaLarge", ScrW() / 2, ScrH() / 2, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
cam.End2D()
render.PopRenderTarget()
hook.Remove("PostDrawHUD", "SprayMesh.GenerateSprayPlaceholderTextures")
end)
-- Draw meshes for all player sprays
hook.Add("PostDrawTranslucentRenderables", "SprayMesh.DrawSprays", function(isDrawingDepth, isDrawingSkybox, isDrawing3DSkybox)
if isDrawingDepth then return end
-- If render order doesn't exist yet, rebuild it
if not spraymesh.RENDER_ITER_CLIENT then
spraymesh.RENDER_ITER_CLIENT = {}
local i = 1
for id64, sprayData in SortedPairsByMemberValue(spraymesh.SPRAYDATA, "Time") do
spraymesh.RENDER_ITER_CLIENT[i] = sprayData.meshdata
i = i + 1
end
end
-- Render all sprays
for _, meshData in ipairs(spraymesh.RENDER_ITER_CLIENT) do
local meshToDraw = meshData.mesh
if meshData and meshToDraw and IsValid(meshToDraw) then
render.SetMaterial(meshData.imaterial)
meshToDraw:Draw()
end
end
end)
-- Coroutine function; used to reload sprays with spraymesh_reload
local function cycleReloadSprays()
for id64, data in pairs(SPRAY_RELOAD_QUEUE) do
print(("Reloading spray for %s (%s) at %s"):format(id64, data.PlayerName, data.hitpos))
local sprayData = {
SteamID64 = id64,
PlayerName = data.PlayerName,
HitPos = data.hitpos,
HitNormal = data.hitnormal,
TraceNormal = data.TraceNormal,
URL = data.url,
CoordDistance = data.CoordDistance,
SprayTime = data.Time,
PlaySpraySound = false
}
spraymesh.PlaceSpray(sprayData)
SPRAY_RELOAD_QUEUE[id64] = nil
coroutine.yield()
break
end
end
local sprayThread = nil
hook.Add("Think", "SprayMesh.ManageSpraysCoroutine", function()
if (not sprayThread or not coroutine.resume(sprayThread)) and not table.IsEmpty(SPRAY_RELOAD_QUEUE) then
sprayThread = coroutine.create(cycleReloadSprays)
coroutine.resume(sprayThread)
end
end)
--
-- Console commands
--
concommand.Add("spraymesh_reload", function()
spraymesh.ReloadSprays()
end)
local SETTINGS_PANEL = nil
concommand.Add("spraymesh_settings", function()
if IsValid(SETTINGS_PANEL) then SETTINGS_PANEL:Close() end
SETTINGS_PANEL = vgui.Create("DSprayConfiguration")
end)
local VIEWER_PANEL = nil
concommand.Add("spraymesh_viewer", function()
if IsValid(VIEWER_PANEL) then VIEWER_PANEL:Close() end
VIEWER_PANEL = vgui.Create("DSprayViewer")
end)
local HELP_PANEL = nil
concommand.Add("spraymesh_help", function()
if IsValid(HELP_PANEL) then HELP_PANEL:Close() end
HELP_PANEL = vgui.Create("DSprayHelp")
end)
concommand.Add("spraymesh_shownames", function(ply, cmd, args, argstr)
local t = CurTime()
SPRAY_SHOWING_NAMES_TIME = CurTime()
SPRAY_SHOWING_NAMES = true
timer.Simple(10, function()
-- Easy way to allow overlapping commands
if t == SPRAY_SHOWING_NAMES_TIME then
SPRAY_SHOWING_NAMES = false
end
end)
-- Print data to console
for id64, data in pairs(spraymesh.SPRAYDATA) do
local plyStr = ([[%s (%s)]]):format(data.PlayerName, id64)
local URLStr = "No URL"
if data.meshdata and data.meshdata.url then
URLStr = data.meshdata.url
end
print(([[%s %s]]):format(plyStr, URLStr))
end
end)

View file

@ -0,0 +1,21 @@
-- Add SprayMesh Extended to the Sandbox context menu
list.Set("DesktopWindows", "SprayMeshExtended", {
title = "SprayMesh",
icon = "icon64/spraymesh.png",
width = 960,
height = 700,
onewindow = true,
init = function(icon, window)
-- Remove basic frame and replace with our custom VGUI element
window:Remove()
if IsValid(screenshot_editor.PANEL) then screenshot_editor.PANEL:Remove() end
local mainWindow = vgui.Create("DSprayConfiguration")
screenshot_editor.PANEL = mainWindow
icon.Window = mainWindow
end
})

View file

@ -0,0 +1,49 @@
--
-- This code is responsible for managing the player's saved spray list.
--
spraylist = spraylist or {}
if not sql.TableExists("spraymesh_extended_spray_list") then
spraymesh.DebugPrint("Creating sqlite table spraymesh_extended_spray_list")
sql.Query([[
CREATE TABLE spraymesh_extended_spray_list (
"url" VARCHAR(512),
"name" VARCHAR(64),
PRIMARY KEY("url")
)]])
end
function spraylist.AddSpray(url, name)
if #url > 512 then
error("The provided URL is too long! (>512 characters)")
end
if #name > 64 then
name = string.sub(name, 1, 64)
end
local queryFmt = Format(
"REPLACE INTO spraymesh_extended_spray_list (url, name) VALUES (%s, %s)",
sql.SQLStr(url),
sql.SQLStr(name)
)
sql.Query(queryFmt)
end
function spraylist.RemoveSpray(url)
local queryFmt = Format(
"DELETE FROM spraymesh_extended_spray_list WHERE url = %s",
sql.SQLStr(url)
)
sql.Query(queryFmt)
end
function spraylist.GetSprays()
local queryResults = sql.Query("SELECT url, name FROM spraymesh_extended_spray_list ORDER BY name DESC")
return queryResults
end

View file

@ -0,0 +1,208 @@
include("spraymesh/sh_init.lua")
util.AddNetworkString("SprayMesh.SV_SendSpray")
util.AddNetworkString("SprayMesh.SV_ClearSpray")
-- Remove various HTML/JS characters from the URL
local function SanitizeURL(url)
local ban = [=[{}[]:'",<>()]=]
local bad = string.Explode("", ban, false)
for k, v in pairs(bad) do
url = string.Replace(url, v, "") -- Gsub uses patterns that conflict with the special characters
end
return url
end
-- Called when the player sprays something--their set URL will be sanitized and checked to ensure it's valid.
local function FixURL(url, ply)
-- If the URL is nil, invalid data type or an empty string
if url == nil or type(url) ~= "string" or url == "" then
spraymesh.DebugPrint("URL is wrong type or is empty data!")
url = spraymesh.SPRAY_URL_DEFAULT
end
-- If the URL contains bad/exploitable characters
if url ~= SanitizeURL(url) then
spraymesh.DebugPrint("URL is not equal to its sanitized counterpart!")
url = spraymesh.SPRAY_URL_DEFAULT
end
-- Check to see if the spray is a valid image or video spray
-- This is where the whitelisted domains/extensions for the spray is checked
local allowed = false
local sprayType = spraymesh.GetURLInfo(url)
spraymesh.DebugPrint("FixURL spray type: " .. sprayType)
if sprayType ~= SPRAYTYPE_INVALID then allowed = true end
spraymesh.DebugPrint("FixURL allowed?: " .. tostring(allowed))
if not allowed then
spraymesh.DebugPrint("INVALID URL: " .. url)
url = spraymesh.SPRAY_URL_DEFAULT
end
return url
end
function spraymesh.RemoveSpray(id64)
if not spraymesh.SPRAYDATA[id64] then return end
net.Start("SprayMesh.SV_ClearSpray")
net.WriteString(id64)
net.Broadcast()
spraymesh.SPRAYDATA[id64] = nil
end
function spraymesh.SendSpray(hitpos, hitnormal, tracenormal, ply)
if IsValid(ply) and ply.SteamID64 then
local id64 = ply:SteamID64()
local url = FixURL(ply:GetInfo("spraymesh_url"), ply)
local sprayInfo = spraymesh.SPRAYDATA[id64] or {}
sprayInfo.url = url
sprayInfo.pos = hitpos
sprayInfo.normal = hitnormal
sprayInfo.TraceNormal = -tracenormal
sprayInfo.PlayerName = ply:Nick()
sprayInfo.Time = ply.LastSprayTime or CurTime()
local coordDist = spraymesh.COORD_DIST_DEFAULT
sprayInfo.CoordDistance = coordDist
hook.Run("SprayMesh.OnSpraySent", ply, url, hitpos)
spraymesh.DebugPrint("sending spray: " .. sprayInfo.url)
net.Start("SprayMesh.SV_SendSpray")
net.WriteString(id64)
net.WriteString(sprayInfo.PlayerName)
net.WriteVector(hitpos)
net.WriteVector(hitnormal)
net.WriteNormal(tracenormal)
net.WriteString(url)
net.WriteFloat(sprayInfo.CoordDistance)
net.WriteFloat(sprayInfo.Time)
net.Broadcast()
spraymesh.SPRAYDATA[id64] = sprayInfo
end
end
local PLAYER = FindMetaTable("Player")
local OldAllowImmediateDecalPainting = PLAYER.AllowImmediateDecalPainting
-- We need to adjust this metamethod to interface with SprayMesh.
function PLAYER:AllowImmediateDecalPainting(allow)
local id64 = self:SteamID64()
spraymesh.SPRAYDATA[id64] = spraymesh.SPRAYDATA[id64] or {}
spraymesh.SPRAYDATA[id64].immediate = allow
return OldAllowImmediateDecalPainting(self, allow)
end
-- The default, built-in spray gets overriden here
hook.Add("PlayerSpray", "SprayMesh.OverrideNativeSpray", function(ply)
spraymesh.DebugPrint("playerspray")
local id64 = ply:SteamID64()
local sprayInfo = spraymesh.SPRAYDATA[id64] or {}
-- The player must be alive, and the spray must not be on cooldown
if (not sprayInfo.delay or sprayInfo.immediate) and ply:Alive() then
-- Apply spray cooldown
if not sprayInfo.immediate then
sprayInfo.delay = true
timer.Simple(spraymesh.SPRAY_COOLDOWN, function()
if sprayInfo then
sprayInfo.delay = false
end
end)
end
-- Perform trace
local tr = util.TraceLine({
start = ply:EyePos(),
endpos = ply:EyePos() + (ply:GetAimVector() * (4096 * 8)),
filter = function(ent)
if ent:IsWorld() then return true end
end
})
-- No spraying on invisible walls
if tr.HitTexture:lower() == "tools/toolsinvisible" then return true end
ply.LastSprayTime = CurTime()
-- The spray must hit the world
local sprayHitValidSpot = tr.Hit and tr.Entity:IsWorld()
-- Give other code a chance to block the spray
local shouldAllowSpray = hook.Run("SprayMesh.ShouldAllowSpray", ply, tr) ~= false
if sprayHitValidSpot and shouldAllowSpray then
spraymesh.SendSpray(tr.HitPos, tr.HitNormal, tr.Normal, ply)
end
end
spraymesh.SPRAYDATA[id64] = sprayInfo
-- Disables regular sprays
return true
end)
-- See https://wiki.facepunch.com/gmod/GM:PlayerInitialSpawn
-- for why this is needed.
local LOAD_QUEUE = {}
local function SendSpraysToClient(ply)
for id64, data in pairs(spraymesh.SPRAYDATA) do
if data.pos and data.normal then
spraymesh.DebugPrint("- Sending spray: " .. data.url)
net.Start("SprayMesh.SV_SendSpray")
net.WriteString(id64)
net.WriteString(data.PlayerName)
net.WriteVector(data.pos)
net.WriteVector(data.normal)
net.WriteVector(data.TraceNormal)
net.WriteString(data.url)
net.WriteFloat(data.CoordDistance)
net.WriteFloat(data.Time)
net.Send(ply)
else
spraymesh.DebugPrint(("Invalid SprayMesh data found for %s, skipping..."):format(id64))
end
end
end
-- Send existing spraymesh sprays to joining players.
hook.Add("PlayerInitialSpawn", "SprayMesh.SendExistingSpraysToConnectedPlayer", function(ply)
LOAD_QUEUE[ply] = true
end)
hook.Add("SetupMove", "SprayMesh.NetworkSpraysOnceReady", function(ply, mv, cmd)
if LOAD_QUEUE[ply] and not cmd:IsForced() then
LOAD_QUEUE[ply] = nil
spraymesh.DebugPrint("Player " .. ply:Nick() .. " is ready to receive networking")
SendSpraysToClient(ply)
end
end)
-- Spraymesh chat command, using the provided prefixes
hook.Add("PlayerSay", "SprayMesh.OpenSprayMeshConfigurationMenu", function(ply, text, isTeam)
for _, prefix in ipairs(spraymesh.CHAT_COMMAND_PREFIXES) do
if string.StartsWith(text:lower(), prefix .. "spraymesh") then
ply:ConCommand("spraymesh_settings")
return ""
end
end
end)

View file

@ -0,0 +1,83 @@
--
-- * SPRAYMESH EXTENDED CONFIGURATION FILE
-- * If you are a server owner, feel free to edit the settings in here to your liking.
--
-- Ideally, this should be the only Lua file you have to modify--if you want to add or remove other features,
-- leave a suggestion and I might make it something configurable. :)
--
-- You can leave suggestions on either:
-- - The Steam Workshop page, or
-- - The GitHub repository: https://github.com/chev2/gmod-addons/issues
--
-- Default spray, often used when player's current spray is invalid or they otherwise haven't set a spray yet
spraymesh.SPRAY_URL_DEFAULT = "files.catbox.moe/xsdikl.png"
-- Units between points (default: 2); resXcoorddist = the dimensions (size) of all player sprays
-- Bigger values means spray sizes will increase
spraymesh.COORD_DIST_DEFAULT = 1.75
-- Mesh resolution (default: 30); this controls how many points make up the mesh grid, such as 30x30,
-- which affects how smooth or jagged the mesh cuts off or wraps around the map diagonally
-- * Tip: try to keep res as 10x the coord dist, and remember that the maximum res is 105 before this breaks
spraymesh.MESH_RESOLUTION = 30
-- The image resolution used for sprays
-- e.g. 512 means the image is resized to be 512x512 pixels
-- Default: 512
-- * The resolution MUST be a power of 2 (256, 512, 1024, etc.), otherwise sprays will be sized weirdly
spraymesh.IMAGE_RESOLUTION = 512
-- How often players can spray (in seconds).
spraymesh.SPRAY_COOLDOWN = 3
-- Command prefixes for the spraymesh command, e.g. "!", "/" will allow both !spraymesh and /spraymesh.
-- * If you want to disable chat commands, just remove all the entries in this list.
spraymesh.CHAT_COMMAND_PREFIXES = {
"!",
"/",
"."
}
-- A list of valid IMAGE domains that sprays can use.
spraymesh.VALID_URL_DOMAINS_IMAGE = {
["i.imgur.com"] = true,
["files.catbox.moe"] = true,
["litter.catbox.moe"] = true,
["cdn.discordapp.com"] = true,
}
-- A list of valid VIDEO domains that sprays can use.
spraymesh.VALID_URL_DOMAINS_VIDEO = {
["i.imgur.com"] = true,
}
-- A list of valid IMAGE extensions that sprays can use.
-- * NOTE: SprayMesh (the original addon) disabled GIF sprays due to heavy performance impact.
-- * I'm not sure if modern Garry's Mod still has the same issue, but if it does (at least, in your tests),
-- * simply remove gif from this list to disable GIFs.
spraymesh.VALID_URL_EXTENSIONS_IMAGE = {
["jpeg"] = true,
["jpg"] = true,
["png"] = true,
["webp"] = true,
["gif"] = true,
["avif"] = true,
}
-- A list of valid VIDEO extensions that sprays can use.
spraymesh.VALID_URL_EXTENSIONS_VIDEO = {
["webm"] = true,
["gifv"] = true,
["mp4"] = true,
}
-- The primary color to use when SprayMesh prints messages to chat. (R, G, B)
spraymesh.PRIMARY_CHAT_COLOR = Color(114, 192, 255)
-- The secondary/accent color to use when SprayMesh prints messages to chat. (R, G, B)
spraymesh.ACCENT_CHAT_COLOR = Color(255, 255, 255)
-- Set to true to enable boring debugging stuff like filling the console with various print statements.
spraymesh.DEBUG_MODE = false

View file

@ -0,0 +1,109 @@
spraymesh = spraymesh or {}
include("spraymesh/sh_config.lua")
-- To ensure that the URLs in the config don't start with HTTP or HTTPS
-- ConVars (such as spraymesh_url) don't allow "//" in the string, so we need to remove it
spraymesh.SPRAY_URL_DEFAULT = string.Replace(spraymesh.SPRAY_URL_DEFAULT, "http://", "https://")
spraymesh.SPRAY_URL_DEFAULT = string.Replace(spraymesh.SPRAY_URL_DEFAULT, "https://", "")
--spraymesh.SPRAY_URL_DISABLED = string.Replace(spraymesh.SPRAY_URL_DISABLED, "http://", "https://")
--spraymesh.SPRAY_URL_DISABLED = string.Replace(spraymesh.SPRAY_URL_DISABLED, "https://", "")
--spraymesh.SPRAY_URL_ANTIGIF = string.Replace(spraymesh.SPRAY_URL_ANTIGIF, "http://", "https://")
--spraymesh.SPRAY_URL_ANTIGIF = string.Replace(spraymesh.SPRAY_URL_ANTIGIF, "https://", "")
-- Stores SteamID64 keys that contain url and delay/immediate vars serverside, and a meshdata var clientside
spraymesh.SPRAYDATA = spraymesh.SPRAYDATA or {}
-- Enums for spray types.
-- SPRAYTYPE_INVALID: The spray is not a valid spray.
-- SPRAYTYPE_IMAGE: The spray is an image.
-- SPRAYTYPE_VIDEO: The spray is a video.
SPRAYTYPE_INVALID = 0
SPRAYTYPE_IMAGE = 1
SPRAYTYPE_VIDEO = 2
-- Checks if the spray URL is valid (image OR video)
function spraymesh.IsValidAnyURL(url)
return spraymesh.IsValidImageURL(url) or spraymesh.IsValidVideoURL(url)
end
-- Checks if the spray URL is valid (images ONLY)
function spraymesh.IsValidImageURL(url)
-- Needs to be HTTPS
if not url:StartWith("https://") then return false end
-- Needs to be from a whitelisted domain
if not url:EndsWith("/") then url = url .. "/" end
urlDomain = string.match(url, "https://(.-)/")
if not spraymesh.VALID_URL_DOMAINS_IMAGE[urlDomain] then return false end
-- Must have a valid file extension
local extension = string.match(url, "%.(%w+)/$")
if not extension or not spraymesh.VALID_URL_EXTENSIONS_IMAGE[extension] then return false end
-- Must be 512 characters or fewer
if #url > 512 then return false end
return true
end
-- Checks if the spray URL is valid (videos ONLY)
function spraymesh.IsValidVideoURL(url)
-- Needs to be HTTPS
if not url:StartWith("https://") then return false end
-- Needs to be from a whitelisted domain
if not url:EndsWith("/") then url = url .. "/" end
urlDomain = string.match(url, "https://(.-)/")
if not spraymesh.VALID_URL_DOMAINS_VIDEO[urlDomain] then return false end
-- Must have a valid file extension
local extension = string.match(url, "%.(%w+)/$")
if not extension or not spraymesh.VALID_URL_EXTENSIONS_VIDEO[extension] then return false end
-- Must be 512 characters or fewer
if #url > 512 then return false end
return true
end
-- Checks if the URL has an IMAGE extension
function spraymesh.IsImageExtension(url)
local extension = string.match(url, "%.(%w+)$")
return spraymesh.VALID_URL_EXTENSIONS_IMAGE[extension] == true
end
-- Checks if the URL has a VIDEO extension
function spraymesh.IsVideoExtension(url)
local extension = string.match(url, "%.(%w+)$")
return spraymesh.VALID_URL_EXTENSIONS_VIDEO[extension] == true
end
-- Checks if the URL is valid, and returns the type (image or video)
function spraymesh.GetURLInfo(url)
-- Ensure URL is set to HTTPS
url = string.Replace(url, "http://", "https://")
url = string.Replace(url, "https://", "")
url = "https://" .. url
if not spraymesh.IsValidAnyURL(url) then
spraymesh.DebugPrint("URL does not pass IsValidAnyURL check!")
return SPRAYTYPE_INVALID
end
local sprayType = SPRAYTYPE_INVALID
if spraymesh.IsImageExtension(url) then
sprayType = SPRAYTYPE_IMAGE
elseif spraymesh.IsVideoExtension(url) then
sprayType = SPRAYTYPE_VIDEO
end
return sprayType
end
-- Print debug info to console
function spraymesh.DebugPrint(msg)
if spraymesh.DEBUG_MODE then print("[SprayMesh Extended Debug]: " .. msg) end
end

View file

@ -0,0 +1,498 @@
--
-- Allows the user to configure their saved sprays & SprayMesh Extended settings.
--
surface.CreateFont("DSprayConfiguration.SprayText", {
font = "Roboto-Regular",
size = 16
})
local PANEL = {}
local SPRAY_NAME_BG_COLOR = Color(0, 0, 0, 240)
function PANEL:Init()
self.Sprays = {}
self.SprayPreviewSize = 256
self.URL_CVar = GetConVar("spraymesh_url")
local sprayLayoutSpace = 4
local settingsWidth = 330
self:SetTitle("Spray Configuration")
self:SetIcon("icon16/world_edit.png")
local minPanelWidth = (self.SprayPreviewSize * 3) + (sprayLayoutSpace * 5) + settingsWidth + 28 + 16
local panelWidth = math.max(ScrW() * 0.6, minPanelWidth)
local panelHeight = math.max(ScrH() * 0.8, 700)
self:SetSize(panelWidth, panelHeight)
self:SetMinWidth(minPanelWidth)
self:SetMinHeight(700)
self:SetSizable(true)
self:SetDraggable(true)
self:SetScreenLock(true)
self:Center()
self:MakePopup()
spraymesh_derma_utils.EnableMaximizeButton(self)
local BG_COLOR_EMBEDDED = Color(0, 0, 0, 120)
--
-- Settings base panel
--
self.SettingsPanel = self:Add("DPanel")
self.SettingsPanel:SetWide(settingsWidth)
self.SettingsPanel:DockMargin(4, 0, 0, 0)
self.SettingsPanel:DockPadding(12, 8, 12, 8)
self.SettingsPanel:Dock(RIGHT)
self.SettingsPanel:SetBackgroundColor(BG_COLOR_EMBEDDED)
--
-- Input bar label & base
--
self.AddSprayPanel = self:Add("DPanel")
self.AddSprayPanel:Dock(TOP)
self.AddSprayPanel:DockMargin(0, 0, 0, 4)
self.AddSprayPanel:DockPadding(12, 8, 12, 8)
self.AddSprayPanel:SetTall(256)
self.AddSprayPanel:SetBackgroundColor(BG_COLOR_EMBEDDED)
self.AddSprayLabel = self.AddSprayPanel:Add("DLabel")
self.AddSprayLabel:SetContentAlignment(5)
self.AddSprayLabel:SetFont("DermaLarge")
self.AddSprayLabel:SetText("Add Spray")
self.AddSprayLabel:SetTextColor(color_white)
self.AddSprayLabel:SizeToContents()
self.AddSprayLabel:DockMargin(0, 0, 0, 4)
self.AddSprayLabel:Dock(TOP)
self.TopInputBar = self.AddSprayPanel:Add("DPanel")
self.TopInputBar:SetTall(28)
self.TopInputBar:Dock(TOP)
self.TopInputBar:SetBackgroundColor(BG_COLOR_EMBEDDED)
self.TopInputBar:DockMargin(0, 0, 0, 4)
self.TopInputBar:DockPadding(4, 4, 4, 4)
self.AddSprayPanel:InvalidateChildren()
self.AddSprayPanel:SizeToChildren(false, true)
--
-- Spray preview grid
--
self.SavedSpraysPanel = self:Add("DPanel")
self.SavedSpraysPanel:Dock(FILL)
self.SavedSpraysPanel:DockMargin(0, 0, 0, 0)
self.SavedSpraysPanel:DockPadding(12, 8, 12, 8)
self.SavedSpraysPanel:SetBackgroundColor(BG_COLOR_EMBEDDED)
self.SavedSpraysLabel = self.SavedSpraysPanel:Add("DLabel")
self.SavedSpraysLabel:SetContentAlignment(5)
self.SavedSpraysLabel:SetFont("DermaLarge")
self.SavedSpraysLabel:SetText("Saved Sprays")
self.SavedSpraysLabel:SetTextColor(color_white)
self.SavedSpraysLabel:SizeToContents()
self.SavedSpraysLabel:DockMargin(0, 0, 0, 4)
self.SavedSpraysLabel:Dock(TOP)
self.SavedSpraySearch = self.SavedSpraysPanel:Add("DTextEntry")
self.SavedSpraySearch:DockMargin(0, 0, 0, 8)
self.SavedSpraySearch:Dock(TOP)
self.SavedSpraySearch:SetPlaceholderText("Enter a name for your spray...")
self.SavedSpraySearch:SetUpdateOnType(true)
self.SavedSpraySearch:SetMaximumCharCount(256)
self.SavedSpraySearch:SetTextColor(color_white)
self.SavedSpraySearch.CursorColor = color_white
self.SavedSpraySearch.BGColor = Color(0, 0, 0, 220)
self.SavedSpraySearch.BGColorDisabled = Color(50, 50, 50, 220)
self.SavedSpraySearch.BaseIndicatorColor = Color(130, 130, 130)
self.SavedSpraySearch.IndicatorColor = self.SavedSpraySearch.BaseIndicatorColor
self.SavedSpraySearch.Paint = function(panel, w, h)
draw.RoundedBox(6, 0, 0, w, h, panel.IndicatorColor)
draw.RoundedBox(4, 2, 2, w - 4, h - 4, panel.BGColor)
local text = panel:GetText()
if (not text or text == "") and panel:IsEnabled() then
panel:SetText("Search for a spray...")
panel:DrawTextEntryText(panel:GetPlaceholderColor(), panel:GetHighlightColor(), panel.CursorColor)
panel:SetText(text)
else
panel:DrawTextEntryText(panel:GetTextColor(), panel:GetHighlightColor(), panel.CursorColor)
end
end
self.SavedSpraySearch.OnValueChange = function(panel, text)
for url, sprayPanel in pairs(self.Sprays) do
-- Since spray panels are wrapped inside a parent, we want to target visibility for the parent instead
local panelParent = sprayPanel:GetParent()
local queryIsEmpty = string.Trim(text, " ") == ""
local textInURL = string.find(url:lower(), text:lower(), 0, true) ~= nil
local textInName = string.find(sprayPanel.Name:lower(), text:lower(), 0, true) ~= nil
if queryIsEmpty or textInURL or textInName then
panelParent:SetVisible(true)
else
panelParent:SetVisible(false)
end
end
self.IconLayout:Layout()
end
self.Scroll = self.SavedSpraysPanel:Add("DScrollPanel")
self.Scroll:Dock(FILL)
self.IconLayout = self.Scroll:Add("DIconLayout")
self.IconLayout:Dock(FILL)
self.IconLayout:SetSpaceX(sprayLayoutSpace)
self.IconLayout:SetSpaceY(sprayLayoutSpace)
-- Load saved sprays
for _, savedSprayData in ipairs(spraylist.GetSprays()) do
local url = savedSprayData.url
local name = savedSprayData.name
self:AddSpray(url, name)
end
--
-- Settings
--
self.SettingsLabel = self.SettingsPanel:Add("DLabel")
self.SettingsLabel:SetContentAlignment(5)
self.SettingsLabel:SetFont("DermaLarge")
self.SettingsLabel:SetText("Settings")
self.SettingsLabel:SetTextColor(color_white)
self.SettingsLabel:SizeToContents()
self.SettingsLabel:DockMargin(0, 0, 0, 4)
self.SettingsLabel:Dock(TOP)
self.EnableSprays = self.SettingsPanel:Add("DCheckBoxLabel")
self.EnableSprays:SetText("Enable sprays")
self.EnableSprays:SetTextColor(color_white)
self.EnableSprays:SetConVar("spraymesh_enablesprays")
self.EnableSprays:DockMargin(0, 0, 0, 4)
self.EnableSprays:Dock(TOP)
self.EnableAnimatedSprays = self.SettingsPanel:Add("DCheckBoxLabel")
self.EnableAnimatedSprays:SetText("Enable animated sprays")
self.EnableAnimatedSprays:SetTextColor(color_white)
self.EnableAnimatedSprays:SetConVar("spraymesh_enableanimated")
self.EnableAnimatedSprays:DockMargin(0, 0, 0, 4)
self.EnableAnimatedSprays:Dock(TOP)
self.ShowActiveSpraysButton = self.SettingsPanel:Add("DButton")
self.ShowActiveSpraysButton:SetText("Show all active player sprays")
self.ShowActiveSpraysButton:Dock(BOTTOM)
self.ShowActiveSpraysButton:DockMargin(16, 4, 16, 0)
self.ShowActiveSpraysButton.DoClick = function()
vgui.Create("DSprayViewer")
end
self.ReloadSpraysButton = self.SettingsPanel:Add("DButton")
self.ReloadSpraysButton:SetText("Reload all sprays")
self.ReloadSpraysButton:Dock(BOTTOM)
self.ReloadSpraysButton:DockMargin(16, 4, 16, 0)
self.ReloadSpraysButton.DoClick = function()
RunConsoleCommand("spraymesh_reload")
notification.AddLegacy("Reloaded sprays.", NOTIFY_GENERIC, 5)
end
self.HelpButton = self.SettingsPanel:Add("DButton")
self.HelpButton:SetText("Help & Info")
self.HelpButton:Dock(BOTTOM)
self.HelpButton:DockMargin(16, 4, 16, 0)
self.HelpButton.DoClick = function()
RunConsoleCommand("spraymesh_help")
end
--
-- Input bar (URL input, name input, 'add spray' button)
--
self.AddButton = self.TopInputBar:Add("DButton")
self.AddButton:SetText("Add spray")
self.AddButton:SizeToContents()
self.AddButton:Dock(RIGHT)
self.AddButton.DoClick = function()
if IsValid(self.InputURL) then
local urlToAdd = self.InputURL:GetText()
local name = self.InputSprayName:GetText()
name = Either(name == "", nil, name)
if self:IsValidURL(urlToAdd) and name then
-- Let player know that the spray was successfully added
surface.PlaySound("ui/buttonclick.wav")
self:AddSpray(urlToAdd, name)
spraylist.AddSpray(urlToAdd, name)
-- Stop the flashing animations for the input boxes if they're still playing
self.InputURL:Stop()
self.InputSprayName:Stop()
-- Set the input box text, color & enabled status to defaults
self.InputURL:SetText("")
self.InputURL.IndicatorColor = self.InputURL.BaseIndicatorColor
self.InputSprayName:SetText("")
self.InputSprayName.IndicatorColor = self.InputSprayName.BaseIndicatorColor
self.InputSprayName:SetEnabled(false)
else
surface.PlaySound("resource/warning.wav")
self.InputURL:Stop()
self.InputSprayName:Stop()
local panelToFlash = self.InputURL
if self:IsValidURL(urlToAdd) and not name then
panelToFlash = self.InputSprayName
end
local baseColor = panelToFlash.BaseIndicatorColor
-- Play flash animation on the incomplete text entry, so the player knows what is failing
local anim = panelToFlash:NewAnimation(0.4, 0, 0.5, function(animTable, panel)
panel.IndicatorColor = baseColor
end)
anim.StartColor = Color(255, 0, 0)
anim.EndColor = Color(baseColor.r, baseColor.g, baseColor.b)
anim.Think = function(animData, panel, fraction)
local lerpR = Lerp(fraction, animData.StartColor.r, animData.EndColor.r)
local lerpG = Lerp(fraction, animData.StartColor.g, animData.EndColor.g)
local lerpB = Lerp(fraction, animData.StartColor.b, animData.EndColor.b)
panel.IndicatorColor.r = lerpR
panel.IndicatorColor.g = lerpG
panel.IndicatorColor.b = lerpB
end
end
end
if IsValid(self.InputURL) then
self.InputURL:RequestFocus()
end
end
self.InputSprayName = self.TopInputBar:Add("DTextEntry")
self.InputSprayName:DockMargin(0, 0, 4, 0)
self.InputSprayName:Dock(RIGHT)
self.InputSprayName:SetWide(panelWidth * 0.25)
self.InputSprayName:SetPlaceholderText("Enter a name for your spray...")
self.InputSprayName:SetUpdateOnType(true)
self.InputSprayName:SetEnabled(false)
self.InputSprayName:SetMaximumCharCount(64)
self.InputSprayName:SetTextColor(color_white)
self.InputSprayName.CursorColor = color_white
self.InputSprayName.BGColor = Color(0, 0, 0, 220)
self.InputSprayName.BGColorDisabled = Color(50, 50, 50, 220)
self.InputSprayName.BaseIndicatorColor = Color(170, 170, 170)
self.InputSprayName.IndicatorColor = self.InputSprayName.BaseIndicatorColor
self.InputSprayName.Paint = function(panel, w, h)
if not panel:IsEnabled() then
draw.RoundedBox(4, 2, 2, w - 4, h - 4, panel.BGColorDisabled)
else
draw.RoundedBox(6, 0, 0, w, h, panel.IndicatorColor)
draw.RoundedBox(4, 2, 2, w - 4, h - 4, panel.BGColor)
end
local text = panel:GetText()
if (not text or text == "") and panel:IsEnabled() then
panel:SetText("Enter a name for your spray...")
panel:DrawTextEntryText(panel:GetPlaceholderColor(), panel:GetHighlightColor(), panel.CursorColor)
panel:SetText(text)
else
panel:DrawTextEntryText(panel:GetTextColor(), panel:GetHighlightColor(), panel.CursorColor)
end
end
self.InputSprayName.OnEnter = function(pnl)
if IsValid(self.AddButton) then
self.AddButton:DoClick()
end
end
self.InputURL = self.TopInputBar:Add("DTextEntry")
self.InputURL:DockMargin(0, 0, 4, 0)
self.InputURL:Dock(FILL)
self.InputURL:SetPlaceholderText("Enter a URL...")
self.InputURL:SetUpdateOnType(true)
self.InputURL:SetTextColor(color_white)
self.InputURL.CursorColor = color_white
self.InputURL.BGColor = Color(0, 0, 0, 220)
self.InputURL.BaseIndicatorColor = Color(179, 0, 0)
self.InputURL.IndicatorColor = self.InputURL.BaseIndicatorColor
self.InputURL.Paint = function(panel, w, h)
draw.RoundedBox(6, 0, 0, w, h, panel.IndicatorColor)
draw.RoundedBox(4, 2, 2, w - 4, h - 4, panel.BGColor)
local text = panel:GetText()
if not text or text == "" then
panel:SetText("Enter a URL...")
panel:DrawTextEntryText(panel:GetPlaceholderColor(), panel:GetHighlightColor(), panel.CursorColor)
panel:SetText(text)
else
panel:DrawTextEntryText(panel:GetTextColor(), panel:GetHighlightColor(), panel.CursorColor)
end
end
self.InputURL.OnValueChange = function(panel, text)
self.InputURL:Stop()
self.InputSprayName:Stop()
if self:IsValidURL(text) then
panel.IndicatorColor = Color(0, 179, 0)
self.InputSprayName:SetEnabled(true)
else
panel.IndicatorColor = Color(179, 0, 0)
self.InputSprayName:SetEnabled(false)
end
end
self.InputURL.OnEnter = function(pnl)
if IsValid(self.AddButton) then
self.AddButton:DoClick()
end
end
end
function PANEL:IsValidURL(urlToCheck)
return spraymesh.IsValidAnyURL(urlToCheck)
end
local MAT_FAKE_TRANSPARENT = Material("spraymesh/fake_transparent.png", "noclamp")
function PANEL:AddSpray(url, name)
-- If the spray already exists
if self.Sprays[url] then
self.IconLayout:Layout()
return
end
-- A transparency grid background, to indicate which sprays are transparent
local newSprayTransparentBase = self.IconLayout:Add("DPanel")
newSprayTransparentBase:SetTooltip("Right-click for options")
newSprayTransparentBase:SetMouseInputEnabled(true)
newSprayTransparentBase:SetCursor("hand")
newSprayTransparentBase:SetSize(self.SprayPreviewSize, self.SprayPreviewSize)
newSprayTransparentBase.URL = string.gsub(url, "https?://", "")
newSprayTransparentBase.Name = name
newSprayTransparentBase.Paint = function(panel, width, height)
surface.SetDrawColor(255, 255, 255, 255)
surface.SetMaterial(MAT_FAKE_TRANSPARENT)
surface.DrawTexturedRect(0, 0, width, height)
end
newSprayTransparentBase.OnMousePressed = function(panel, keyCode)
if keyCode == MOUSE_LEFT then
surface.PlaySound("ui/buttonclick.wav")
notification.AddLegacy("Selected spray '" .. name .. "'.", NOTIFY_GENERIC, 3)
RunConsoleCommand("spraymesh_url", panel.URL)
elseif keyCode == MOUSE_RIGHT then
local dmenu = DermaMenu()
local copyURL = dmenu:AddOption("Copy URL", function()
local copiedURL = "https://" .. panel.URL
SetClipboardText(copiedURL)
notification.AddLegacy("Copied spray URL clipboard.", NOTIFY_GENERIC, 5)
end)
copyURL:SetIcon("icon16/page_white_copy.png")
local sayInChat = dmenu:AddOption("Send URL to chat", function()
local urlToSend = panel.URL
RunConsoleCommand("say", "https://" .. urlToSend)
self:Close()
end)
sayInChat:SetIcon("icon16/comment_add.png")
local sayInTeamChat = dmenu:AddOption("Send URL to team chat", function()
local urlToSend = panel.URL
RunConsoleCommand("say_team", "https://" .. urlToSend)
self:Close()
end)
sayInTeamChat:SetIcon("icon16/comments_add.png")
local remove = dmenu:AddOption("Remove", function()
Derma_Query(
"Are you sure you want to delete the spray \"" .. panel.Name .. "\"?",
"Confirmation:",
"Delete",
function()
self.Sprays[panel.URL] = nil
spraylist.RemoveSpray("https://" .. panel.URL)
panel:Remove()
notification.AddLegacy("Spray deleted.", NOTIFY_GENERIC, 5)
end,
"Cancel",
function() end
)
end)
remove:SetIcon("icon16/cross.png")
dmenu:Open()
end
end
local newSpray = newSprayTransparentBase:Add("DHTML")
newSpray:SetAllowLua(false)
newSpray:Dock(FILL)
newSpray:SetMouseInputEnabled(false)
local sprayHTML = spraymesh_derma_utils.GetPreviewHTML(self.SprayPreviewSize, url)
newSpray:SetHTML(sprayHTML)
newSpray.URL = string.gsub(url, "https?://", "")
newSpray.Name = name
newSpray.PaintOver = function(panel, width, height)
if panel.URL == self.URL_CVar:GetString() then
--surface.SetDrawColor(255, 255, 255, 30)
--surface.DrawRect(0, 0, width, height)
local blink = Lerp((math.sin(RealTime() * 5) + 1) / 2, 170, 255)
surface.SetDrawColor(0, 127, blink, 255)
surface.DrawOutlinedRect(0, 0, width, height, 6)
end
draw.WordBox(8, width / 2, height - 8, panel.Name, "DSprayConfiguration.SprayText", SPRAY_NAME_BG_COLOR, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_BOTTOM)
end
newSprayTransparentBase.Panel = newSpray
self.Sprays[url] = newSpray
-- Sort sprays
local sortedChildrenTb = self.IconLayout:GetChildren()
table.sort(sortedChildrenTb, function(a, b)
return (a.Name or ""):lower() < (b.Name or ""):lower()
end)
for index, panel in ipairs(sortedChildrenTb) do
panel:SetZPos(index)
end
self.IconLayout:Layout()
end
-- Register control
derma.DefineControl("DSprayConfiguration", "Spraymesh Extended - Configurator", PANEL, "DFrame")

View file

@ -0,0 +1,225 @@
--
-- Displays helpful info on how to use SprayMesh Extended.
--
local PANEL = {}
local HELP_HTML = [=[
<html>
<head>
<style>
body {
margin: 16px;
background: rgba(0, 0, 0, 80%);
color: white;
font-family: 'Arial', 'Helvetica', sans-serif;
}
li {
line-height: 1.5;
}
a {
color: lightblue;
}
p {
line-height: 1.2;
}
code {
background: hsl(0, 0%, 20%);
color: rgb(81, 161, 106);
border-radius: 4px;
padding: 4px;
line-height: 2;
}
#wrapper {
width: 768px;
margin: auto;
}
</style>
</head>
<body>
<div id="wrapper">
<h1>SprayMesh Extended</h1>
<p>SprayMesh Extended is an improvement to the original addon, SprayMesh.</p>
<p>It comes with various new features and improvements, included but not limited to:</p>
<ul>
<li>A built-in menu for SprayMesh Extended:</li>
<ul>
<li>Comes with a settings panel to adjust some SprayMesh Extended settings.</li>
<li>Comes with a spray manager to save, name & search sprays.</li>
<li>Has a pop-up menu to view all active sprays on the server.</li>
<li>Has a pop-up menu which contains a guide to using SprayMesh Extended, as well as viewing what spray types (like image & video extensions) are whitelisted.</li>
</ul>
<li>Sprays can be rotated on floors and ceilings.</li>
<li>Sprays are now easier to see in dark areas.</li>
<li>Sprays render in the order they're sprayed (so that players can spray over each others' sprays).</li>
<li>Sprays will be kept when a player re-joins the server (however, sprays will still reset upon a server shutdown/restart).</li>
<li>A cleaner codebase, and optimized code a bit.</li>
<li>Config (and Lua hooks) for developers and server owners to customize SprayMesh Extended to their liking.</li>
<li>Ability to block sprays from muted players.</li>
<li>Support for CatBox & LitterBox natively included.</li>
</ul>
<p>Keep in mind that SprayMesh Extended is designed to be a <strong>replacement</strong> for SprayMesh, not an addition.</p>
<p><strong>You will run into various issues if you try to use both addons at the same time!</strong></p>
<h2>Credits</h2>
<p>SprayMesh Extended is made by Chev <code>(STEAM_0:0:71541002)</code>. Thanks for supporting my work!</p>
<p>SprayMesh (the original) is made by <strong>Bletotum</strong>: <a href="https://steamcommunity.com/sharedfiles/filedetails/?id=394091909" target="_blank">Steam Workshop Link</a></p>
<p>Also, thanks to <strong>Sony</strong> for making Spray Manager V2, which inspired SprayMesh Extended's own manager: <a href="https://steamcommunity.com/sharedfiles/filedetails/?id=1805554541" target="_blank">Steam Workshop Link</a></p>
<h2>Using SprayMesh Extended</h2>
<p>First, find an image you like. The whitelisted websites on <strong>this server</strong> are as follows:</p>
<p>Image URLs:</p>
<ul>
{{WHITELISTED_IMAGE_DOMAINS}}
</ul>
<p>Video/Animated URLs:</p>
<ul>
{{WHITELISTED_VIDEO_DOMAINS}}
</ul>
<br>
<p>Next, open the configurator. It can be opened in the following ways:</p>
<ul>
<li>Typing <code>/spraymesh</code> into chat (if the server supports it)</li>
<li>Typing <code>spraymesh_settings</code> into the developer console</li>
<li>Opening the context (C) menu in gamemodes like Sandbox and clicking on <code>SprayMesh</code></li>
</ul>
<p>Then, paste the image's URL into the red bar in the configurator. If the bar turns green, the URL is valid.</p>
<p>Click the <code>Add Spray</code> button or press enter and your spray will be added to your spray list.</p>
<br>
<p>If the bar is still red, then the URL you entered isn't valid.</p>
<p>It either contains invalid characters (such as <code>&lt;, &gt;, [, ]</code> etc.) or it's from a website that isn't whitelisted for use on this server.</p>
<br>
<p>All sprays get resized to a resolution of <code>{{SPRAY_RESOLUTION}}</code> on this server. You can use images larger than this--they will simply be downsized.</p>
<p>Images do not need to be perfectly square. Wide and tall images work just fine.</p>
<br>
<p>Right-clicking on a spray in the spray manager will present additional options, such as copying the URL or removing the spray.</p>
<br>
<p>Whitelisted image extensions on this server:</p>
<ul>
{{WHITELISTED_IMAGE_EXTENSIONS}}
</ul>
<p>Whitelisted video extensions on this server:</p>
<ul>
{{WHITELISTED_VIDEO_EXTENSIONS}}
</ul>
</div>
</body>
</html>
]=]
function PANEL:Init()
self:SetTitle("SprayMesh Extended Help")
self:SetIcon("icon16/help.png")
local fWidth = math.max(ScrW() * 0.72, 1056)
local fHeight = math.max(ScrH() * 0.8, 594)
self:SetSize(fWidth, fHeight)
self:SetMinWidth(1056)
self:SetMinHeight(594)
self:SetSizable(true)
self:SetDraggable(true)
self:SetScreenLock(true)
self:Center()
self:MakePopup()
spraymesh_derma_utils.EnableMaximizeButton(self)
local html = vgui.Create("DHTML", self)
html:Dock(FILL)
html:SetAllowLua(false)
html.OnChildViewCreated = function(panel, sourceURL, targetURL, isPopup)
gui.OpenURL(targetURL)
end
local finalHTML = HELP_HTML
--
-- Image domains
--
local imageDomains = {}
for domain, val in SortedPairs(spraymesh.VALID_URL_DOMAINS_IMAGE) do
if val ~= true then continue end
imageDomains[#imageDomains + 1] = "<li><code>" .. string.JavascriptSafe(domain) .. "</code></li>"
end
if #imageDomains == 0 then
imageDomains = {"<li>No image domains whitelisted</li>"}
end
finalHTML = string.Replace(finalHTML, "{{WHITELISTED_IMAGE_DOMAINS}}", table.concat(imageDomains, "\n"))
--
-- Video domains
--
local videoDomains = {}
for domain, val in SortedPairs(spraymesh.VALID_URL_DOMAINS_VIDEO) do
if val ~= true then continue end
videoDomains[#videoDomains + 1] = "<li><code>" .. string.JavascriptSafe(domain) .. "</code></li>"
end
if #videoDomains == 0 then
videoDomains = {"<li>No video domains whitelisted</li>"}
end
finalHTML = string.Replace(finalHTML, "{{WHITELISTED_VIDEO_DOMAINS}}", table.concat(videoDomains, "\n"))
--
-- Image extensions
--
local imageExts = {}
for domain, val in SortedPairs(spraymesh.VALID_URL_EXTENSIONS_IMAGE) do
if val ~= true then continue end
imageExts[#imageExts + 1] = "<li><code>." .. string.JavascriptSafe(domain) .. "</code></li>"
end
if #imageExts == 0 then
imageExts = {"<li>No image extensions whitelisted</li>"}
end
finalHTML = string.Replace(finalHTML, "{{WHITELISTED_IMAGE_EXTENSIONS}}", table.concat(imageExts, "\n"))
--
-- Video extensions
--
local videoExts = {}
for domain, val in SortedPairs(spraymesh.VALID_URL_EXTENSIONS_VIDEO) do
if val ~= true then continue end
videoExts[#videoExts + 1] = "<li><code>." .. string.JavascriptSafe(domain) .. "</code></li>"
end
if #videoExts == 0 then
videoExts = {"<li>No video extensions whitelisted</li>"}
end
finalHTML = string.Replace(finalHTML, "{{WHITELISTED_VIDEO_EXTENSIONS}}", table.concat(videoExts, "\n"))
--
-- Image resolution
--
local sprayRes = string.JavascriptSafe(spraymesh.IMAGE_RESOLUTION)
finalHTML = string.Replace(finalHTML, "{{SPRAY_RESOLUTION}}", sprayRes .. "x" .. sprayRes)
html:SetHTML(finalHTML)
end
-- Register control
derma.DefineControl("DSprayHelp", "SprayMesh Extended - Help", PANEL, "DFrame")

View file

@ -0,0 +1,147 @@
--
-- Displays all active sprays in a viewable panel.
-- Useful for moderation, or if you just want to get the URL for other peoples' sprays.
--
surface.CreateFont("DSprayViewer.NameDisplay", {
font = "Roboto-Regular",
size = 17
})
local PANEL = {}
function PANEL:Init()
self:SetTitle("Player Spray Viewer")
self:SetIcon("icon16/user.png")
local fWidth = math.max(ScrW() * 0.72, 1056)
local fHeight = math.max(ScrH() * 0.7, 594)
self:SetSize(fWidth, fHeight)
self:SetMinWidth(1056)
self:SetMinHeight(594)
self:SetSizable(true)
self:SetDraggable(true)
self:SetScreenLock(true)
self:Center()
self:MakePopup()
spraymesh_derma_utils.EnableMaximizeButton(self)
local scroll = vgui.Create("DScrollPanel", self)
scroll:Dock(FILL)
local iconlayout = vgui.Create("DIconLayout", scroll)
iconlayout:DockMargin(4, 4, 4, 4)
iconlayout:Dock(FILL)
iconlayout:SetSpaceX(12)
iconlayout:SetSpaceY(12)
self.Layout = iconlayout
for id64, sprayData in pairs(spraymesh.SPRAYDATA) do
local display = vgui.Create("DSprayDisplay", self.Layout)
display:SetURL(sprayData.url)
display:SetPlayer(sprayData.PlayerName, id64)
end
end
-- Register control
derma.DefineControl("DSprayViewer", "SprayMesh Spray Layout", PANEL, "DFrame")
local DISPLAY = {}
DISPLAY.SprayPreviewSize = 256
local MAT_FAKE_TRANSPARENT = Material("spraymesh/fake_transparent.png", "noclamp")
function DISPLAY:Init()
self:DockPadding(4, 4, 4, 4)
self:SetMouseInputEnabled(true)
self:SetCursor("hand")
self.NameDisplay = vgui.Create("DLabel", self)
self.NameDisplay:SetFont("DSprayViewer.NameDisplay")
self.NameDisplay:SetText("Unknown - Unknown")
self.NameDisplay:SetTextColor(color_white)
self.NameDisplay:SetContentAlignment(5)
self.NameDisplay:DockMargin(0, 0, 0, 4)
self.NameDisplay:Dock(BOTTOM)
self.ImageDisplay = vgui.Create("DHTML", self)
self.ImageDisplay:SetAllowLua(false)
self.ImageDisplay:Dock(FILL)
self.ImageDisplay:SetMouseInputEnabled(false)
self:SetSize(self.SprayPreviewSize + 8, self.SprayPreviewSize + 32 + 8)
end
function DISPLAY:OnMousePressed(keyCode)
if keyCode == MOUSE_LEFT then
gui.OpenURL(self.URL)
elseif keyCode == MOUSE_RIGHT then
local dMenu = DermaMenu()
local copyURL = dMenu:AddOption("Copy URL", function()
SetClipboardText(self.URL)
notification.AddLegacy("Copied Spray URL to clipboard.", NOTIFY_GENERIC, 5)
end)
copyURL:SetIcon("icon16/page_white_copy.png")
local copySteamID = dMenu:AddOption("Copy Steam ID", function()
SetClipboardText(util.SteamIDFrom64(self.PlayerID64))
notification.AddLegacy("Copied " .. self.PlayerName .. "'s Steam ID to clipboard.", NOTIFY_GENERIC, 5)
end)
copySteamID:SetIcon("icon16/page_white_copy.png")
local copySteamID64 = dMenu:AddOption("Copy Steam ID 64", function()
SetClipboardText(self.PlayerID64)
notification.AddLegacy("Copied " .. self.PlayerName .. "'s Steam ID 64 to clipboard.", NOTIFY_GENERIC, 5)
end)
copySteamID64:SetIcon("icon16/page_white_copy.png")
dMenu:Open()
end
end
function DISPLAY:Paint(w, h)
local padding = 4
surface.SetDrawColor(0, 0, 0, 200)
surface.DrawRect(0, 0, w, h)
surface.SetDrawColor(255, 255, 255, 255)
surface.SetMaterial(MAT_FAKE_TRANSPARENT)
surface.DrawTexturedRect(padding, padding, w - (padding * 2), h - 32 - (padding * 2))
end
function DISPLAY:SetURL(url)
if not url:StartWith("http") then
url = "https://" .. url
end
self.URL = url
local sprayHTML = spraymesh_derma_utils.GetPreviewHTML(self.SprayPreviewSize, url)
self.ImageDisplay:SetHTML(sprayHTML)
end
-- Builds the caption (e.g. Player - 12345678)
function DISPLAY:BuildText()
local msgFormatted = Format(
"%s - %s",
self.PlayerName or "Unknown",
self.PlayerID64 or "Unknown"
)
self.NameDisplay:SetText(msgFormatted)
self.NameDisplay:SizeToContents()
end
function DISPLAY:SetPlayer(name, id64)
self.PlayerName = name
self.PlayerID64 = id64
self:BuildText()
end
derma.DefineControl("DSprayDisplay", "SprayMesh Extended - Spray Display", DISPLAY, "DPanel")

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB