code
```lua
local M = {}
--
-- dependencies
--
local ffi = require "ffi"
local vtable = require "vtable"
local csgo_weapons = require "csgo_weapons"
local string_gsub = string.gsub
local math_floor = math.floor
local cast = ffi.cast
--
-- ffi structs
-- (mostly for image parsing)
--
local png_ihdr_t =
ffi.typeof(
[[
struct {
char type[4];
uint32_t width;
uint32_t height;
char bitDepth;
char colorType;
char compression;
char filter;
char interlace;
} *
]]
)
local jpg_segment_t = ffi.typeof([[
struct {
char type[2];
uint16_t size;
} *
]])
local jpg_segment_sof0_t =
ffi.typeof([[
struct {
uint16_t size;
char precision;
uint16_t height;
uint16_t width;
} __attribute__((packed)) *
]])
local uint16_t_ptr = ffi.typeof("uint16_t*")
local charbuffer = ffi.typeof("char[?]")
local uintbuffer = ffi.typeof("unsigned int[?]")
--
-- constants
--
local INVALID_TEXTURE = -1
local PNG_MAGIC = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"
local JPG_MAGIC_1 = "\xFF\xD8\xFF\xDB"
local JPG_MAGIC_2 = "\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01"
local JPG_SEGMENT_SOI = "\xFF\xD8"
local JPG_SEGMENT_SOF0 = "\xFF\xC0"
local JPG_SEGMENT_SOS = "\xFF\xDA"
local JPG_SEGMENT_EOI = "\xFF\xD9"
local RENDERER_LOAD_FUNCS = {
png = function(contents, width, height)
local rgba, _width, _height = common.DecodePNG(contents)
return draw.CreateTexture(rgba, _width, _height)
end,
svg = function(contents, width, height)
local rgba, _width, _height = common.RasterizeSVG(contents)
return draw.CreateTexture(rgba, _width, _height)
end,
jpg = function(contents, width, height)
local rgba, _width, _height = common.DecodeJPEG(contents)
return draw.CreateTexture(rgba, _width, _height)
end,
rgba = draw.CreateTexture
}
--
-- utility functions
--
local function bswap_16(x)
return bit.rshift(bit.bswap(x), 16)
end
local function hexdump(str)
local out = {}
str:gsub(
".",
function(chr)
table.insert(out, string.format("%02x", string.byte(chr)))
end
)
return table.concat(out, " ")
end
--
-- small filesystem implementation
--
local native_ReadFile = vtable.bind("filesystem_stdio.dll", "VBaseFileSystem011", 0, "int(__thiscall*)(void*, void*, int, void*)")
local native_OpenFile =
vtable.bind("filesystem_stdio.dll", "VBaseFileSystem011", 2, "void*(__thiscall*)(void*, const char*, const char*, const char*)")
local native_CloseFile = vtable.bind("filesystem_stdio.dll", "VBaseFileSystem011", 3, "void(__thiscall*)(void*, void*)")
local native_GetFileSize = vtable.bind("filesystem_stdio.dll", "VBaseFileSystem011", 7, "unsigned int(__thiscall*)(void*, void*)")
local function engine_read_file(filename)
local handle = native_OpenFile(filename, "r", "MOD")
if handle == nil then
return
end
local filesize = native_GetFileSize(handle)
if filesize == nil or filesize < 0 then
return
end
local buffer = charbuffer(filesize + 1)
if buffer == nil then
return
end
local read_success = native_ReadFile(buffer, filesize, handle)
if not read_success then
return
end
return ffi.string(buffer, filesize)
end
--
-- ISteamFriends / ISteamUtils
--
-- That shit now use ingame context of steamapi instead of connecting to global user
-- enjoy, by w7rus
ffi.cdef(
[[
typedef struct
{
void* steam_client;
void* steam_user;
void* steam_friends;
void* steam_utils;
void* steam_matchmaking;
void* steam_user_stats;
void* steam_apps;
void* steam_matchmakingservers;
void* steam_networking;
void* steam_remotestorage;
void* steam_screenshots;
void* steam_http;
void* steam_unidentifiedmessages;
void* steam_controller;
void* steam_ugc;
void* steam_applist;
void* steam_music;
void* steam_musicremote;
void* steam_htmlsurface;
void* steam_inventory;
void* steam_video;
} S_steamApiCtx_t;
]]
)
local pS_SteamApiCtx =
ffi.cast("S_steamApiCtx_t**", ffi.cast("char*", mem.FindPattern("client.dll", "FF 15 ?? ?? ?? ?? B9 ?? ?? ?? ?? E8 ?? ?? ?? ?? 6A")) + 7)[0] or
error("invalid interface", 2)
local native_ISteamFriends = ffi.cast("void***", pS_SteamApiCtx.steam_friends)
local native_ISteamUtils = ffi.cast("void***", pS_SteamApiCtx.steam_utils)
local native_ISteamFriends_GetSmallFriendAvatar = vtable.thunk(34, "int(__thiscall*)(void*, uint64_t)")
local native_ISteamFriends_GetMediumFriendAvatar = vtable.thunk(35, "int(__thiscall*)(void*, uint64_t)")
local native_ISteamFriends_GetLargeFriendAvatar = vtable.thunk(36, "int(__thiscall*)(void*, uint64_t)")
local native_ISteamUtils_GetImageSize = vtable.thunk(5, "bool(__thiscall*)(void*, int, uint32_t*, uint32_t*)")
local native_ISteamUtils_GetImageRGBA = vtable.thunk(6, "bool(__thiscall*)(void*, int, unsigned char*, int)")
--
-- image object implementation
--
local function image_measure(self, width, height)
if width ~= nil and height ~= nil then
return math_floor(width), math_floor(height)
else
if self.width == nil or self.height == nil then
error("Image dimensions not known, full size is required")
elseif width == nil then
height = height or self.height
local width = math_floor(self.width * (height / self.height))
return width, height
elseif height == nil then
width = width or self.width
local height = math_floor(self.height * (width / self.width))
return width, height
else
return math_floor(self.width), math_floor(self.height)
end
end
end
local function image_draw(self, x, y, width, height, r, g, b, a, force_same_res_render)
width, height = image_measure(self, width, height)
local id = string.format("%f_%f", width, height)
local texture = self.textures[id]
-- no texture with same width and height has been loaded
if texture == nil then
if ({next(self.textures)})[2] == nil or force_same_res_render or force_same_res_render == nil then
-- try and load the texture
local func = RENDERER_LOAD_FUNCS[self.type]
if func then
if self.type == "rgba" then
width, height = self.width, self.height
end
texture = func(self.contents, width, height)
end
if texture == nil then
self.textures[id] = INVALID_TEXTURE
error("failed to load texture for " .. width .. "x" .. height, 2)
else
-- client.log("loaded svg ", self.name, " for ", width, "x", height)
self.textures[id] = texture
end
else
--right now we just choose a random texture (determined by the pairs order aka unordered)
--todo: select the texture with the highest or closest resolution?
texture = ({next(self.textures)})[2]
end
end
if texture == nil or texture == INVALID_TEXTURE then
return
elseif a == nil or a > 0 then
draw.SetTexture(texture)
draw.Color(r or 255, g or 255, b or 255, a or 255)
draw.FilledRect(x, y, x + width, y + height)
draw.SetTexture(nil)
end
return width, height
end
local image_mt = {
__index = {
measure = image_measure,
draw = image_draw
}
}
--
-- functions for loading images
--
local function load_png(contents)
if contents:sub(1, 8) ~= PNG_MAGIC then
error("Invalid magic", 2)
return
end
local ihdr_raw = contents:sub(13, 30)
if ihdr_raw:len() < 17 then
error("Incomplete data", 2)
return
end
local ihdr = cast(png_ihdr_t, cast("const uint8_t *", cast("const char*", ihdr_raw)))
if ffi.string(ihdr.type, 4) ~= "IHDR" then
error("Invalid chunk type, expected IHDR", 2)
return
end
local width = bit.bswap(ihdr.width)
local height = bit.bswap(ihdr.height)
if width <= 0 or height <= 0 then
error("Invalid width or height", 2)
return
end
return setmetatable(
{
type = "png",
width = width,
height = height,
contents = contents,
textures = {}
},
image_mt
)
end
local function load_jpg(contents)
local buffer = ffi.cast("const uint8_t *", ffi.cast("const char *", contents))
local len_remaining = contents:len()
local width, height
if contents:sub(1, 4) == JPG_MAGIC_1 or contents:sub(1, 12) == JPG_MAGIC_2 then
local got_soi, got_sos = false, false
-- read segments until we find a SOF0 header (containing width/height)
while len_remaining > 0 do
local segment = ffi.cast(jpg_segment_t, buffer)
local typ = ffi.string(segment.type, 2)
buffer = buffer + 2
len_remaining = len_remaining - 2
if typ == JPG_SEGMENT_SOI then
got_soi = true
elseif not got_soi then
error("expected SOI segment", 2)
elseif typ == JPG_SEGMENT_SOS or typ == JPG_SEGMENT_EOI then
if typ == JPG_SEGMENT_SOS then
got_sos = true
end
break
else
-- endian convert of the size (be -> le)
local size = bswap_16(segment.size)
if typ == JPG_SEGMENT_SOF0 then
local sof0 = cast(jpg_segment_sof0_t, buffer)
height = bswap_16(sof0.height)
width = bswap_16(sof0.width)
if width <= 0 or height <= 0 then
error("Invalid width or height")
return
end
end
buffer = buffer + size
len_remaining = len_remaining - size
end
end
if not got_soi then
error("Incomplete image, missing SOI segment", 2)
return
elseif not got_sos then
error("Incomplete image, missing SOS segment", 2)
return
elseif width == nil then
error("Incomplete image, missing SOF0 segment", 2)
return
end
else
error("Invalid magic", 2)
return
end
return setmetatable(
{
type = "jpg",
width = width,
height = height,
contents = contents,
textures = {}
},
image_mt
)
end
local function load_svg(contents)
-- try and find <svg> tag
local match = contents:match("<svg(.*)>.*</svg>")
if match == nil then
error("Invalid svg, missing <svg> tag", 2)
return
end
match = match:gsub("\r\n", ""):gsub("\n", "")
-- parse tag contents
local in_quote = false
local key, value = "", ""
local attributes = {}
local offset = 1
while true do
local chr = match:sub(offset, offset)
if chr == "" then
break
end
if in_quote then
-- text inside quotation marks
if chr == '"' then
in_quote = false
attributes[key:gsub("\t", ""):lower()] = value
key, value = "", ""
else
value = value .. chr
end
else
-- normal text, not inside quotes
if chr == ">" then
break
elseif chr == "=" then
if match:sub(offset, offset + 1) == '="' then
in_quote = true
offset = offset + 1
end
elseif chr == " " then
key = ""
else
key = key .. chr
end
end
offset = offset + 1
end
-- heuristics to find valid image width and height
local width, height
if attributes["width"] ~= nil then
width = tonumber((attributes["width"]:gsub("px$", ""):gsub("pt$", ""):gsub("mm$", "")))
if width ~= nil and 0 >= width then
width = nil
end
end
if attributes["height"] ~= nil then
height = tonumber((attributes["height"]:gsub("px$", ""):gsub("pt$", ""):gsub("mm$", "")))
if height ~= nil and 0 >= height then
height = nil
end
end
if width == nil or height == nil and attributes["viewbox"] ~= nil then
local x, y, w, h = attributes["viewbox"]:match("^%s*([%d.]*) ([%d.]*) ([%d.]*) ([%d.]*)%s*$")
width, height = tonumber(width), tonumber(height)
if width ~= nil and height ~= nil and (0 >= width or 0 >= height) then
width, height = nil, nil
end
end
local self =
setmetatable(
{
type = "svg",
contents = contents,
textures = {}
},
image_mt
)
if width ~= nil and height ~= nil and width > 0 and height > 0 then
self.width, self.height = width, height
end
return self
end
local function load_rgba(contents, width, height)
if width == nil or height == nil or width <= 0 or height <= 0 then
error("Invalid size: width and height are required and have to be greater than zero.")
return
end
local size = width * height * 4
if contents:len() ~= size then
error("invalid buffer length, expected width*height*4", 2)
return
end
-- load texture
local texture = draw.CreateTexture(contents, width, height)
if texture == nil then
return
end
return setmetatable(
{
type = "rgba",
width = width,
height = height,
contents = contents,
textures = {[string.format("%f_%f", width, height)] = texture}
},
image_mt
)
end
local function load_image(contents)
if type(contents) == "table" then
if getmetatable(contents) == image_mt then
return error("trying to load an existing image")
else
local result = {}
for key, value in pairs(contents) do
result[key] = load_image(value)
end
return result
end
else
-- try and determine type etc by looking for magic value
if type(contents) == "string" then
if contents:sub(1, 8) == PNG_MAGIC then
return load_png(contents)
elseif contents:sub(1, 4) == JPG_MAGIC_1 or contents:sub(1, 12) == JPG_MAGIC_2 then
return load_jpg(contents)
elseif contents:match("^%s*%<%?xml") ~= nil then
return load_svg(contents)
else
return error("Failed to determine image type")
end
end
end
end
local panorama_images = setmetatable({}, {__mode = "k"})
local function get_panorama_image(path)
if panorama_images[path] == nil then
local path_cleaned = string_gsub(string_gsub(string_gsub(string_gsub(string_gsub(path, "%z", ""), "%c", ""), "\\", "/"), "%.%./", ""), "^/+", "")
local contents = engine_read_file("materials/panorama/images/" .. path_cleaned)
if contents then
local image = load_image(contents)
panorama_images[path] = image
else
panorama_images[path] = false
end
end
if panorama_images[path] then
return panorama_images[path]
end
end
local weapon_icons = setmetatable({}, {__mode = "k"})
local function get_weapon_icon(weapon_name)
if weapon_icons[weapon_name] == nil then
local weapon_name_cleaned
local typ = type(weapon_name)
if typ == "table" and weapon_name.console_name ~= nil then
weapon_name_cleaned = weapon_name.console_name
elseif typ == "number" then
local weapon = csgo_weapons[weapon_name]
if weapon == nil then
weapon_icons[weapon_name] = false
return
end
weapon_name_cleaned = weapon.console_name
elseif typ == "string" then
weapon_name_cleaned = tostring(weapon_name)
elseif weapon_name ~= nil then
weapon_icons[weapon_name] = nil
return
else
return
end
weapon_name_cleaned = string_gsub(string_gsub(weapon_name_cleaned, "^weapon_", ""), "^item_", "")
local image = get_panorama_image("icons/equipment/" .. weapon_name_cleaned .. ".svg")
weapon_icons[weapon_name] = image or false
end
if weapon_icons[weapon_name] then
return weapon_icons[weapon_name]
end
end
local steam_avatars = {}
local function get_steam_avatar(steamid3_or_steamid64, size)
local cache_key = string.format("%s_%d", steamid3_or_steamid64, size or 32)
if steam_avatars[cache_key] == nil then
local func
if size == nil then
func = native_ISteamFriends_GetSmallFriendAvatar
elseif size > 64 then
func = native_ISteamFriends_GetLargeFriendAvatar
elseif size > 32 then
func = native_ISteamFriends_GetMediumFriendAvatar
else
func = native_ISteamFriends_GetSmallFriendAvatar
end
local steamid
if type(steamid3_or_steamid64) == "string" then
steamid = 76500000000000000ULL + tonumber(steamid3_or_steamid64:sub(4, -1))
elseif type(steamid3_or_steamid64) == "number" then
steamid = 76561197960265728ULL + steamid3_or_steamid64
else
return
end
local handle = func(native_ISteamFriends, steamid)
if handle > 0 then
local width = uintbuffer(1)
local height = uintbuffer(1)
if native_ISteamUtils_GetImageSize(native_ISteamUtils, handle, width, height) then
if width[0] > 0 and height[0] > 0 then
local rgba_buffer_size = width[0] * height[0] * 4
local rgba_buffer = charbuffer(rgba_buffer_size)
if native_ISteamUtils_GetImageRGBA(native_ISteamUtils, handle, rgba_buffer, rgba_buffer_size) then
steam_avatars[cache_key] = load_rgba(ffi.string(rgba_buffer, rgba_buffer_size), width[0], height[0])
end
end
end
elseif handle ~= -1 then
steam_avatars[cache_key] = false
end
end
if steam_avatars[cache_key] then
return steam_avatars[cache_key]
end
end
return {
load = load_image,
load_png = load_png,
load_jpg = load_jpg,
load_svg = load_svg,
load_rgba = load_rgba,
get_weapon_icon = get_weapon_icon,
get_panorama_image = get_panorama_image,
get_steam_avatar = get_steam_avatar
}
```