-- 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 = [=[ title
]=] 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 = [=[