-- ============================================================ -- shared/functions.lua – Shared utility functions -- ============================================================ -- ────────────────────────────────────────────── -- Character/number tables for random generation -- ────────────────────────────────────────────── local DIGITS = {} local LETTERS = {} for code = 48, 57 do -- '0'–'9' table.insert(DIGITS, string.char(code)) end for code = 65, 90 do -- 'A'–'Z' table.insert(LETTERS, string.char(code)) end for code = 97, 122 do -- 'a'–'z' table.insert(LETTERS, string.char(code)) end local resourceVersion = GetResourceMetadata(GetCurrentResourceName(), "version", 0) -- ────────────────────────────────────────────── -- GetRandomNumber -- Returns a string of `count` random digits. -- ────────────────────────────────────────────── function GetRandomNumber(count) Citizen.Wait(0) if count > 0 then return GetRandomNumber(count - 1) .. DIGITS[math.random(1, #DIGITS)] end return "" end -- ────────────────────────────────────────────── -- GetRandomLetter -- Returns a string of `count` random letters. -- ────────────────────────────────────────────── function GetRandomLetter(count) Citizen.Wait(0) if count > 0 then return GetRandomLetter(count - 1) .. LETTERS[math.random(1, #LETTERS)] end return "" end -- ────────────────────────────────────────────── -- MathTrim -- Strips leading/trailing whitespace from a string. -- ────────────────────────────────────────────── function MathTrim(value) if value then return string.gsub(value, "^%s*(.-)%s*$", "%1") end return nil end -- ────────────────────────────────────────────── -- Debug -- Prints all arguments if Config.Debug is enabled. -- Tables are JSON-encoded for readability. -- ────────────────────────────────────────────── function Debug(...) if not Config.Debug then return end local args = { ... } for idx, value in ipairs(args) do if type(value) == "table" then args[idx] = json.encode(value) end end print("^5[DEBUG " .. resourceVersion .. "]^7", table.unpack(args)) end -- ────────────────────────────────────────────── -- Warning -- ────────────────────────────────────────────── function Warning(...) local message = "^3GARAGES WARNING:^0 " for _, value in pairs({ ... }) do message = message .. tostring(value) .. "\t" end print(message) end -- ────────────────────────────────────────────── -- Info -- ────────────────────────────────────────────── function Info(...) local message = "^5GARAGES INFO:^0 " for _, value in pairs({ ... }) do if type(value) == "table" then message = message .. json.encode(value) .. "\t" else message = message .. tostring(value) .. "\t" end end print(message) end -- ────────────────────────────────────────────── -- Error -- ────────────────────────────────────────────── function Error(...) local message = "^1GARAGES ERROR:^0 " for _, value in pairs({ ... }) do if type(value) == "table" then message = message .. json.encode(value) .. "\t" else message = message .. tostring(value) .. "\t" end end print(message) end -- ────────────────────────────────────────────── -- LoopError -- Repeatedly prints an error every 2 seconds in a thread. -- ────────────────────────────────────────────── function LoopError(...) local firstArg = (...) CreateThread(function() while true do print("^1[ERROR]^7", firstArg) Wait(2000) end end) end -- ────────────────────────────────────────────── -- table extensions -- ────────────────────────────────────────────── -- table.includes(tbl, value) → bool function table.includes(tbl, value) if not tbl then return false end for _, v in pairs(tbl) do if v == value then return true end end return false end -- table.find(tbl, predicate|value) → value, key function table.find(tbl, predicate) if not tbl then return false, false end for key, value in pairs(tbl) do if type(predicate) == "function" then if predicate(value, key) then return value, key end elseif value == predicate then return value, key end end return false, false end -- table.filter(tbl, predicate) → filtered table function table.filter(tbl, predicate) local result = {} for key, value in pairs(tbl) do if predicate(value, key, tbl) then result[#result + 1] = value end end return result end -- table.map(tbl, transform) → mapped table function table.map(tbl, transform) local result = {} for key, value in pairs(tbl) do result[#result + 1] = transform(value, key, tbl) end return result end -- table.slice(tbl, from, to, step) → sliced table function table.slice(tbl, from, to, step) local result = {} from = from or 1 to = to or #tbl step = step or 1 for i = from, to, step do result[#result + 1] = tbl[i] end return result end -- ────────────────────────────────────────────── -- string extensions -- ────────────────────────────────────────────── -- string.split(str, separator) → table of parts function string.split(str, separator) separator = separator or ":" local parts = {} local pattern = string.format("([^%s]+)", separator) str:gsub(pattern, function(part) parts[#parts + 1] = part end) return parts end -- ────────────────────────────────────────────── -- DependencyCheck -- Checks a map of { resourceName = value } and returns -- the value for the first resource that is started. -- ────────────────────────────────────────────── function DependencyCheck(dependencyMap) for resourceName, value in pairs(dependencyMap) do local state = GetResourceState(resourceName) if state ~= nil and state:find("started") then return value end end return false end -- ────────────────────────────────────────────── -- FormatTime -- Converts a number of seconds to a human-readable string. -- ────────────────────────────────────────────── function FormatTime(seconds) if seconds < 60 then return seconds .. " seconds" elseif seconds < 3600 then return math.floor(seconds / 60) .. " min" elseif seconds < 86400 then return math.floor(seconds / 3600) .. " hours" else return math.floor(seconds / 86400) .. " days" end end -- ────────────────────────────────────────────── -- GetCoordsWithOffset -- Applies a local-space offset (vec2 x/y + z) to a -- world position with heading (vec4), returning a new vec4. -- ────────────────────────────────────────────── function GetCoordsWithOffset(position, offset) local angleRad = math.rad(position.w + 90) local cosA = math.cos(angleRad) local sinA = math.sin(angleRad) local newX = position.x + offset.x * cosA - offset.y * sinA local newY = position.y + offset.x * sinA + offset.y * cosA local newZ = position.z + offset.z return vec4(newX, newY, newZ, position.w) end -- ────────────────────────────────────────────── -- RotationToDirection -- Converts a rotation (degrees) to a unit direction vector. -- ────────────────────────────────────────────── function RotationToDirection(rotation) local radX = math.pi / 180 * rotation.x local radY = math.pi / 180 * rotation.y local radZ = math.pi / 180 * rotation.z local cosX = math.abs(math.cos(radX)) return { x = -math.sin(radZ) * cosX, y = math.cos(radZ) * cosX, z = math.sin(radX), } end -- ────────────────────────────────────────────── -- RayCastGamePlayCamera -- Returns the world-space end-point and camera rotation -- for a ray cast from the gameplay camera. -- ────────────────────────────────────────────── function RayCastGamePlayCamera(distance) local camRot = GetGameplayCamRot() local camCoords = GetGameplayCamCoord() local dir = RotationToDirection(camRot) local endPoint = vec3( camCoords.x + dir.x * distance, camCoords.y + dir.y * distance, camCoords.z + dir.z * distance ) return endPoint, camRot end -- ────────────────────────────────────────────── -- GetClosestPlayer -- Returns the player id closest to the given coords, -- within the initial maxDistance radius. -- ────────────────────────────────────────────── function GetClosestPlayer(coords, maxDistance) local closestPlayer = nil local closestDist = maxDistance for _, playerId in pairs(GetActivePlayers()) do local pedCoords = GetEntityCoords(GetPlayerPed(playerId)) Debug("GetClosestPlayer", pedCoords, coords) local dist = #(pedCoords - coords) if dist < closestDist then closestDist = dist closestPlayer = playerId end end return closestPlayer end -- ────────────────────────────────────────────── -- GetRelativePosition -- Returns the world-space position of a named relative -- point (e.g. "front-left", "back-right") around a vehicle. -- ────────────────────────────────────────────── function GetRelativePosition(entity, positionName, radius) local entityCoords = GetEntityCoords(entity) local forwardVector = GetEntityForwardVector(entity) local upVector = vector3(0.0, 0.0, 1.0) local rightVector = vector3(-forwardVector.y, forwardVector.x, 0.0) local minDim, maxDim = GetModelDimensions(GetEntityModel(entity)) local height = (maxDim.z - minDim.z) * 2 local width = (maxDim.x - minDim.x) * 2 local halfWidth = width * 0.5 + radius * 0.5 local positions = { ["front-left"] = forwardVector * radius - rightVector * halfWidth, ["front-middle"] = forwardVector * radius, ["front-right"] = forwardVector * radius + rightVector * halfWidth, ["back-left"] = -forwardVector * radius - rightVector * halfWidth, ["back-middle"] = -forwardVector * radius, ["back-right"] = -forwardVector * radius + rightVector * halfWidth, ["left"] = -rightVector * (radius + width * 0.5), ["right"] = rightVector * (radius + width * 0.5), ["top-left"] = forwardVector * radius - rightVector * halfWidth + upVector * (height + radius), ["top-middle"] = upVector * radius, ["top-right"] = forwardVector * radius + rightVector * halfWidth + upVector * (height + radius), ["front-left-diagonal"] = forwardVector * radius * 0.7 - rightVector * halfWidth * 0.7, ["front-right-diagonal"] = forwardVector * radius * 0.7 + rightVector * halfWidth * 0.7, ["back-left-diagonal"] = -forwardVector * radius * 0.7 - rightVector * halfWidth * 0.7, ["back-right-diagonal"] = -forwardVector * radius * 0.7 + rightVector * halfWidth * 0.7, } return entityCoords + positions[positionName] end