365 lines
14 KiB
Lua
365 lines
14 KiB
Lua
-- ============================================================
|
||
-- 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
|