New addon: SprayMesh Extended
|
|
@ -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)
|
||||
|
|
|
|||
6
addons/spraymesh_extended/addon.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"title": "SprayMesh Extended",
|
||||
"type": "effects",
|
||||
"tags": ["fun", "build"],
|
||||
"ignore": []
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- Initialize SprayMesh Extended on the client
|
||||
include("spraymesh/client/cl_init.lua")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
997
addons/spraymesh_extended/lua/spraymesh/client/cl_init.lua
Normal 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)
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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
|
||||
208
addons/spraymesh_extended/lua/spraymesh/server/sv_init.lua
Normal 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)
|
||||
83
addons/spraymesh_extended/lua/spraymesh/sh_config.lua
Normal 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
|
||||
|
||||
109
addons/spraymesh_extended/lua/spraymesh/sh_init.lua
Normal 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
|
||||
498
addons/spraymesh_extended/lua/vgui/dsprayconfiguration.lua
Normal 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")
|
||||
225
addons/spraymesh_extended/lua/vgui/dsprayhelp.lua
Normal 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><, >, [, ]</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")
|
||||
147
addons/spraymesh_extended/lua/vgui/dsprayviewer.lua
Normal 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")
|
||||
BIN
addons/spraymesh_extended/materials/icon64/spraymesh.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 211 B |
BIN
images/icons/spraymesh-extended.png
Normal file
|
After Width: | Height: | Size: 463 KiB |
BIN
images/icons/spraymesh-extended.xcf
Normal file
BIN
images/screenshots/spraymesh-extended-01.jpg
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
images/screenshots/spraymesh-extended-02.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
images/screenshots/spraymesh-extended-03.jpg
Normal file
|
After Width: | Height: | Size: 454 KiB |
BIN
images/screenshots/spraymesh-extended-04.png
Normal file
|
After Width: | Height: | Size: 603 KiB |
BIN
images/screenshots/spraymesh-extended-04_unedited.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
images/screenshots/spraymesh-extended-05.png
Normal file
|
After Width: | Height: | Size: 519 KiB |
BIN
images/screenshots/spraymesh-extended-06.png
Normal file
|
After Width: | Height: | Size: 600 KiB |
BIN
images/screenshots/spraymesh-extended-06_unedited.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
images/screenshots/spraymesh-extended-07.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
images/screenshots/spraymesh-extended-07_unedited.jpg
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
images/screenshots/spraymesh-extended-icon.jpg
Normal file
|
After Width: | Height: | Size: 462 KiB |