2026-04-14 17:41:39 +02:00

365 lines
14 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ============================================================
-- 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