diff --git a/gamemodes/terrortown/gamemode/client/cl_player_ext.lua b/gamemodes/terrortown/gamemode/client/cl_player_ext.lua index c270296e6..84f4c65de 100644 --- a/gamemodes/terrortown/gamemode/client/cl_player_ext.lua +++ b/gamemodes/terrortown/gamemode/client/cl_player_ext.lua @@ -242,7 +242,7 @@ local function CacheAllPlayerAvatars(ply) for i = 1, #plys do local plyid64 = plys[i]:SteamID64() - draw.RefreshAvatars(plyid64) + playeravatar.Refresh(plyid64) end end diff --git a/gamemodes/terrortown/gamemode/client/cl_search.lua b/gamemodes/terrortown/gamemode/client/cl_search.lua index 319e0b447..67b083297 100644 --- a/gamemodes/terrortown/gamemode/client/cl_search.lua +++ b/gamemodes/terrortown/gamemode/client/cl_search.lua @@ -42,7 +42,7 @@ net.Receive("TTT2SendConfirmMsg", function() sid64 = nil end - local img = draw.GetAvatarMaterial(sid64, "medium") + local img = playeravatar.GetMaterial(sid64, PLAYERAVATAR_SIZE.medium) --- -- @realm client @@ -214,7 +214,7 @@ function SEARCHSCREEN:Show(data) profileBox:Dock(LEFT) profileBox:SetModel(data.playerModel) profileBox:SetPlayerIcon( - playerDataKnown and draw.GetAvatarMaterial(data.sid64, "medium") + playerDataKnown and playeravatar.GetMaterial(data.sid64, PLAYERAVATAR_SIZE.medium) or materialPlayerIconUnknown ) profileBox:SetPlayerRoleColor(playerDataKnown and data.roleColor or COLOR_SLATEGRAY) diff --git a/gamemodes/terrortown/gamemode/shared/hud_elements/tttvoice/pure_skin_voice.lua b/gamemodes/terrortown/gamemode/shared/hud_elements/tttvoice/pure_skin_voice.lua index 53e35663e..0d32e0dbe 100644 --- a/gamemodes/terrortown/gamemode/shared/hud_elements/tttvoice/pure_skin_voice.lua +++ b/gamemodes/terrortown/gamemode/shared/hud_elements/tttvoice/pure_skin_voice.lua @@ -118,7 +118,7 @@ if CLIENT then yPos, h, h, - draw.GetAvatarMaterial(ply:SteamID64(), "medium"), + playeravatar.GetMaterial(ply:SteamID64(), PLAYERAVATAR_SIZE.medium), 255, COLOR_WHITE ) diff --git a/gamemodes/terrortown/gamemode/shared/sh_init.lua b/gamemodes/terrortown/gamemode/shared/sh_init.lua index c39c9a02d..e6339e5e9 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_init.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_init.lua @@ -593,6 +593,7 @@ include("ttt2/libraries/events.lua") include("ttt2/libraries/eventdata.lua") include("ttt2/libraries/targetid.lua") include("ttt2/libraries/playermodels.lua") +include("ttt2/libraries/playeravatar.lua") include("ttt2/libraries/entspawnscript.lua") include("ttt2/libraries/bodysearch.lua") include("ttt2/libraries/keyhelp.lua") diff --git a/lua/autorun/client/b-draw_lib.lua b/lua/autorun/client/b-draw_lib.lua deleted file mode 100644 index 8663a0428..000000000 --- a/lua/autorun/client/b-draw_lib.lua +++ /dev/null @@ -1,256 +0,0 @@ -if engine.ActiveGamemode() ~= "terrortown" then - return -end - ---- --- A Simple Garry's mod drawing library --- Copyright (C) 2016 Bull [STEAM_0:0:42437032] [76561198045139792] --- Freely acquirable at https://github.com/bull29/b_draw-lib --- You can use this anywhere for any purpose as long as you acredit the work to the original author with this notice. --- Optionally, if you choose to use this within your own software, it would be much appreciated if you could inform me of it. --- I love to see what people have done with my code! :) --- This code got fixed and modified by the TTT2 dev group, ty for the lib @Bull ! --- --- @author Bull --- @author Alf21 --- --- @module draw - -file.CreateDir("downloaded_assets") - -local exists = file.Exists -local write = file.Write -local delete = file.Delete -local white = Color(255, 255, 255) -local surface = surface -local crc = util.CRC -local _bot_avatar = Material("vgui/ttt/b-draw/icon_avatar_bot.vmt") -local _default_avatar = Material("vgui/ttt/b-draw/icon_avatar_default.vmt") -local mats = {} - ---- --- Captures the content of a render target as a PNG image --- @param RenderTarget renderTarget The render target to capture --- @param Material material The material to use for rendering --- @param number width The width of the capture --- @param number height The height of the capture --- @return string The captured PNG data --- @realm client -local function CaptureRenderTarget(renderTarget, material, width, height) - render.PushRenderTarget(renderTarget) - cam.Start2D() - surface.SetMaterial(material) - cam.End2D() - - local data = render.Capture({ - format = "png", - x = 0, - y = 0, - w = width, - h = height, - }) - - render.PopRenderTarget() - - return data -end - ---- --- Creates an avatar material for a given SteamID64 if it doesn't exist --- @param string id64 The SteamID64 of the user --- @param string size The avatar's size, this can be small, medium or large --- @return Material The created avatar material or the default material --- @realm client -local function CreateAvatarMaterial(id64, size) - if not id64 then - return _bot_avatar - end - - local avatarSize = 32 - - if size == "large" then - avatarSize = 184 - elseif size == "medium" then - avatarSize = 64 - end - - local renderTargetName = id64 .. avatarSize - local crcUrl = crc(renderTargetName) - local filePath = "downloaded_assets/" .. crcUrl .. ".png" - - if mats[renderTargetName] then - return mats[renderTargetName] - end - - --- - -- @realm client - local data = hook.Run("TTT2FetchAvatar", id64, avatarSize) - - if data then - write(filePath, data) - mats[renderTargetName] = Material("data/" .. filePath) - - return mats[renderTargetName] - end - - local avatarImage = vgui.Create("AvatarImage") - avatarImage:SetSize(avatarSize, avatarSize) - avatarImage:SetSteamID(id64, avatarSize) - avatarImage:SetPaintedManually(true) - - local renderTarget = GetRenderTargetEx( - renderTargetName, - avatarSize, - avatarSize, - RT_SIZE_NO_CHANGE, - MATERIAL_RT_DEPTH_NONE, - 16, - CREATERENDERTARGETFLAGS_HDR, - IMAGE_FORMAT_RGBA8888 - ) - - if not renderTarget then - avatarImage:Remove() - - return _default_avatar - end - - render.PushRenderTarget(renderTarget) - cam.Start2D() - avatarImage:PaintManual(true) - cam.End2D() - render.PopRenderTarget() - - local material = CreateMaterial(id64 .. avatarSize, "UnlitGeneric", { - ["$basetexture"] = renderTargetName, - ["$nodecal"] = 1, - ["$nolod"] = 1, - ["$translucent"] = 1, - ["$vertexalpha"] = 1, - ["$vertexcolor"] = 1, - }) - - local captureData = CaptureRenderTarget(renderTarget, material, avatarSize, avatarSize) - - if captureData then - write(filePath, captureData) - end - - avatarImage:Remove() - mats[renderTargetName] = material - - return material -end - ---- --- Refreshes the avatar images by deleting old cached images and generating new ones --- @param string id64 The steamid64 of the player --- @realm client -function draw.RefreshAvatars(id64) - draw.DropCacheAvatar(id64, "small") - draw.DropCacheAvatar(id64, "medium") - draw.DropCacheAvatar(id64, "large") - draw.CacheAvatar(id64, "small") - draw.CacheAvatar(id64, "medium") - draw.CacheAvatar(id64, "large") -end - ---- --- Creates the avatar material for a steamid64 --- When an avatar is found it will be cached --- @param string id64 The steamid64 --- @param string size The avatar's size, this can be small, medium or large --- @realm client -function draw.CacheAvatar(id64, size) - CreateAvatarMaterial(id64, size) -end - ---- --- Deletes the avatar material for a steamid64 --- When a cached avatar is found it will be destroyed --- @param string id64 The player's steamid64 --- @param string size The avatar's size, this can be small, medium or large --- @realm client -function draw.DropCacheAvatar(id64, size) - local avatarSize = 32 - - if size == "large" then - avatarSize = 184 - elseif size == "medium" then - avatarSize = 64 - end - - local key = id64 .. avatarSize - local crcUrl = crc(key) - local uri = "downloaded_assets/" .. crcUrl .. ".png" - - if exists(uri, "DATA") then - delete(uri, "DATA") - end - - mats[key] = nil -end - ---- --- Draws an Image --- @param Material material the material to draw --- @param number x --- @param number y --- @param number width --- @param number height --- @param Color color --- @param Angle angle --- @param boolean cornerorigin If it is set to true, the WebImage will be centered based on the x- and y-coordinate --- @realm client -local function DrawImage(material, x, y, width, height, color, angle, cornerorigin) - color = color or white - - surface.SetDrawColor(color.r, color.g, color.b, color.a) - surface.SetMaterial(material) - - if not angle then - surface.DrawTexturedRect(x, y, width, height) - else - if not cornerorigin then - surface.DrawTexturedRectRotated(x, y, width, height, angle) - else - surface.DrawTexturedRectRotated(x + width * 0.5, y + height * 0.5, width, height, angle) - end - end -end - ---- --- Draws a SteamAvatar while caching it before --- @param string id64 The steamid64 --- @param string size The avatar's size, this can be small, medium or large --- @param number x --- @param number y --- @param number width --- @param number height --- @param Color color --- @param Angle angle --- @param boolean cornerorigin If it is set to true, the WebImage will be centered based on the x- and y-coordinate --- @realm client -function draw.SteamAvatar(id64, size, x, y, width, height, color, angle, cornerorigin) - DrawImage( - CreateAvatarMaterial(id64, size) or _default_avatar, - x, - y, - width, - height, - color, - angle, - cornerorigin - ) -end - ---- --- Creates and returns the avatar material for a steamid64 --- When an avatar is found it will be cached --- @param string id64 The steamid64 --- @param string size The avatar's size, this can be small, medium or large --- @return Material The created avatar material or the default material --- @realm client -function draw.GetAvatarMaterial(id64, size) - return CreateAvatarMaterial(id64, size) or _default_avatar -end diff --git a/lua/ttt2/libraries/playeravatar.lua b/lua/ttt2/libraries/playeravatar.lua new file mode 100644 index 000000000..3b1aa16af --- /dev/null +++ b/lua/ttt2/libraries/playeravatar.lua @@ -0,0 +1,212 @@ +--- +-- +-- +-- @author +-- +-- @module playeravatar + +if SERVER then + AddCSLuaFile() + + return +end + +file.CreateDir("player_avatars") + +local fileExists = file.Exists +local fileWrite = file.Write +local fileDelete = file.Delete +local surface = surface +local crc = util.CRC +local bot_avatar = Material("vgui/ttt/b-draw/icon_avatar_bot") +local default_avatar = Material("vgui/ttt/b-draw/icon_avatar_default") +local playeravatar_cache = {} + +playeravatar = {} + +---@enum +PLAYERAVATAR_SIZE = { + small = 32, + medium = 64, + large = 128, + -- `large` was 184, but we use this for the `width/height` parameter of `GetRenderTargetEx` + -- which according to the wiki must be a power of 2 + -- ref.: https://wiki.facepunch.com/gmod/Global.GetRenderTargetEx#arguments +} + +--- +-- Captures the content of a render target as a PNG image +-- @param ITexture renderTarget The render target to capture +-- @param IMaterial material The material to use for rendering +-- @param number width The width of the capture +-- @param number height The height of the capture +-- @return string|nil The captured binary PNG data +-- @realm client +local function CaptureRenderTarget(renderTarget, material, width, height) + render.PushRenderTarget(renderTarget) + cam.Start2D() + surface.SetMaterial(material) + cam.End2D() + + local data = render.Capture({ + format = "png", + x = 0, + y = 0, + w = width, + h = height, + }) + + render.PopRenderTarget() + + return data +end + +local function GetAvatarCacheKey(id64, avatarSize) + return id64 .. avatarSize +end + +local function GetAvatarUri(id64, avatarSize) + local key = GetAvatarCacheKey(id64, avatarSize) + local crcUrl = crc(key) + return "player_avatars/" .. crcUrl .. ".png" +end + +--- +-- Creates an avatar material for a given SteamID64 if it doesn't exist +-- @param string id64 The SteamID64 of the user +-- @param PLAYERAVATAR_SIZE avatarSize The avatar's size +-- @return IMaterial The created avatar material or the default material +-- @realm client +local function CreateAvatarMaterial(id64, avatarSize) + if not id64 then + return bot_avatar + end + + local cacheKey = GetAvatarCacheKey(id64, avatarSize) + local uri = GetAvatarUri(id64, avatarSize) + + --- + -- @realm client + -- TODO: declare that this needs to return a PNG, JPEG, GIF, or TGA file + -- the file extension does not matter we will append `.png` to it + local data = hook.Run("TTT2FetchAvatar", id64, avatarSize) + + if data then + fileWrite(uri, data) + playeravatar_cache[cacheKey] = Material("data/" .. uri) + + return playeravatar_cache[cacheKey] + end + + -- TODO: according to the wiki render targets are not garbage collected and remain in memory + -- until disconnect. + -- ref: https://wiki.facepunch.com/gmod/Global.GetRenderTarget#description + -- Test if this is actually the case and if we can get away with reusing a single render target + local renderTarget = GetRenderTargetEx( + cacheKey, + avatarSize, + avatarSize, + RT_SIZE_NO_CHANGE, + MATERIAL_RT_DEPTH_NONE, + 16, + CREATERENDERTARGETFLAGS_HDR, + IMAGE_FORMAT_RGBA8888 + ) + + if not renderTarget then + return default_avatar + end + + local avatarImage = vgui.Create("AvatarImage") + avatarImage:SetSize(avatarSize, avatarSize) + avatarImage:SetSteamID(id64, avatarSize) + avatarImage:SetPaintedManually(true) + + render.PushRenderTarget(renderTarget) + cam.Start2D() + avatarImage:PaintManual(true) + cam.End2D() + render.PopRenderTarget() + + local material = CreateMaterial(cacheKey, "UnlitGeneric", { + ["$basetexture"] = cacheKey, + ["$nodecal"] = 1, + ["$nolod"] = 1, + ["$translucent"] = 1, + ["$vertexalpha"] = 1, + ["$vertexcolor"] = 1, + }) + + local captureData = CaptureRenderTarget(renderTarget, material, avatarSize, avatarSize) + + if captureData then + fileWrite(uri, captureData) + end + + avatarImage:Remove() + playeravatar_cache[cacheKey] = material + + return material +end + +--- +-- Refreshes the avatar images by deleting old cached images and generating new ones +-- @param string id64 The steamid64 of the player +-- @realm client +function playeravatar.Refresh(id64) + playeravatar.DropCache(id64, PLAYERAVATAR_SIZE.small) + playeravatar.DropCache(id64, PLAYERAVATAR_SIZE.medium) + playeravatar.DropCache(id64, PLAYERAVATAR_SIZE.large) + playeravatar.Cache(id64, PLAYERAVATAR_SIZE.small) + playeravatar.Cache(id64, PLAYERAVATAR_SIZE.medium) + playeravatar.Cache(id64, PLAYERAVATAR_SIZE.large) +end + +--- +-- Creates the avatar material for a steamid64 +-- When an avatar is found it will be cached +-- @param string id64 The steamid64 +-- @param PLAYERAVATAR_SIZE size The avatar's size +-- @realm client +-- TODO: Make this local? Or actually use this somewhere? Or merge/replace with +-- `playeravatar.GetMaterial()` +function playeravatar.Cache(id64, size) + CreateAvatarMaterial(id64, size) +end + +--- +-- Deletes the avatar material for a steamid64 +-- When a cached avatar is found it will be destroyed +-- @param string id64 The player's steamid64 +-- @param PLAYERAVATAR_SIZE avatarSize The avatar's size +-- @realm client +-- TODO: Make this local? Or actually use this somewhere? +-- TODO: Also it does not really make much sense to only drop a single avatarSize by default. We +-- probably should by default just iterate over small,medium,large +function playeravatar.DropCache(id64, avatarSize) + local key = GetAvatarCacheKey(id64, avatarSize) + local uri = GetAvatarUri(id64, avatarSize) + + if fileExists(uri, "DATA") then + fileDelete(uri, "DATA") + end + + playeravatar_cache[key] = nil +end + +--- +-- Creates and returns the avatar material for a steamid64 +-- When an avatar is found it will be cached +-- @param string id64 The steamid64 +-- @param PLAYERAVATAR_SIZE avatarSize The avatar's size +-- @return IMaterial The created avatar material or the default material +-- @realm client +function playeravatar.GetMaterial(id64, avatarSize) + local rendertargetKey = GetAvatarCacheKey(id64, avatarSize) + + if playeravatar_cache[rendertargetKey] then + return playeravatar_cache[rendertargetKey] + end + + return CreateAvatarMaterial(id64, avatarSize) +end