1058 lines
42 KiB
Lua
1058 lines
42 KiB
Lua
-- ============================================================
|
||
-- shared/utils.lua – Shared utility library (Utils)
|
||
-- ============================================================
|
||
|
||
Utils = {}
|
||
|
||
Utils.RenderList = {}
|
||
Utils.Characters = {}
|
||
Utils.Numbers = {}
|
||
|
||
-- Build character / number tables
|
||
for code = 48, 57 do table.insert(Utils.Numbers, string.char(code)) end -- '0'–'9'
|
||
for code = 65, 90 do table.insert(Utils.Characters, string.char(code)) end -- 'A'–'Z'
|
||
for code = 97, 122 do table.insert(Utils.Characters, string.char(code)) end -- 'a'–'z'
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GenerateRandomUid
|
||
-- letterCount : number of random letters
|
||
-- digitCount : number of random digits appended
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.GenerateRandomUid(letterCount, digitCount)
|
||
math.randomseed(GetGameTimer())
|
||
local uid = ""
|
||
for _ = 1, letterCount do
|
||
uid = uid .. Utils.Characters[math.random(#Utils.Characters)]
|
||
end
|
||
for _ = 1, digitCount do
|
||
uid = uid .. Utils.Numbers[math.random(#Utils.Numbers)]
|
||
end
|
||
return uid
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GenerateUniqueId
|
||
-- Returns a uid that does not already exist as a key
|
||
-- in the provided `existingTable`.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.GenerateUniqueId(existingTable, letterCount, digitCount)
|
||
local uid = Utils.GenerateRandomUid(letterCount, digitCount)
|
||
while existingTable[uid] do
|
||
uid = Utils.GenerateRandomUid(letterCount, digitCount)
|
||
end
|
||
return uid
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GetForwardVector
|
||
-- Converts a rotation (degrees) to a unit forward vec3.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.GetForwardVector(rotation)
|
||
local radX = rotation * math.pi / 180.0
|
||
local cosX = math.abs(math.cos(radX.x))
|
||
return vec3(
|
||
-math.sin(radX.z) * cosX,
|
||
math.cos(radX.z) * cosX,
|
||
math.sin(radX.x)
|
||
)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- SplitString
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.SplitString(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
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- BreakString
|
||
-- Truncates a string to maxLength and appends "..."
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.BreakString(str, maxLength)
|
||
if not str then return "" end
|
||
if maxLength >= #str then return str end
|
||
|
||
local truncated = str:sub(1, maxLength)
|
||
local spacePos = truncated:find(" ", #truncated - 5)
|
||
|
||
if spacePos then
|
||
truncated = truncated:sub(1, spacePos - 1)
|
||
end
|
||
return truncated .. "..."
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- JsonEncode
|
||
-- Encodes a table to JSON, converting FiveM vector
|
||
-- types to plain tables first.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function VectorToTable(value)
|
||
local result = {}
|
||
for key, val in pairs(value) do
|
||
local valType = type(val)
|
||
if valType == "vector4" then
|
||
result[key] = { x = val.x, y = val.y, z = val.z, w = val.w }
|
||
elseif valType == "vector3" then
|
||
result[key] = { x = val.x, y = val.y, z = val.z }
|
||
elseif valType == "vector2" then
|
||
result[key] = { x = val.x, y = val.y }
|
||
elseif valType == "table" then
|
||
result[key] = VectorToTable(val)
|
||
else
|
||
result[key] = val
|
||
end
|
||
end
|
||
return result
|
||
end
|
||
__utilsJsonEncodeInternalDecode = VectorToTable
|
||
|
||
function Utils.JsonEncode(value)
|
||
return json.encode(VectorToTable(value))
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- JsonDecode
|
||
-- Decodes JSON and converts detected {x,y,z[,w]}
|
||
-- tables back to FiveM vector types.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function TableToVector(tbl)
|
||
local result = {}
|
||
for key, val in pairs(tbl) do
|
||
if type(val) == "table" then
|
||
local count = Utils.TableCount(val)
|
||
if val.x and val.y and val.z and val.w and count == 4 then
|
||
result[key] = vector4(val.x, val.y, val.z, val.w)
|
||
elseif val.x and val.y and val.z and count == 3 then
|
||
result[key] = vector3(val.x, val.y, val.z)
|
||
elseif val.x and val.y and count == 2 then
|
||
result[key] = vector2(val.x, val.y)
|
||
else
|
||
result[key] = TableToVector(val)
|
||
end
|
||
else
|
||
result[key] = val
|
||
end
|
||
end
|
||
return result
|
||
end
|
||
__utilsJsonDecodeInternalDecode = TableToVector
|
||
|
||
function Utils.JsonDecode(jsonString)
|
||
return TableToVector(json.decode(jsonString))
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- TableCopy (deep copy)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.TableCopy(tbl)
|
||
local copy = {}
|
||
for key, value in pairs(tbl) do
|
||
if type(value) == "table" then
|
||
copy[key] = Utils.TableCopy(value)
|
||
else
|
||
copy[key] = value
|
||
end
|
||
end
|
||
return copy
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- TableCount (counts all keys, not just sequential)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.TableCount(tbl)
|
||
local count = 0
|
||
for _ in pairs(tbl) do
|
||
count = count + 1
|
||
end
|
||
return count
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- PrintT (pretty-print a table)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.PrintT(value)
|
||
local seen = {}
|
||
|
||
local function printRecursive(val, indent)
|
||
local key = tostring(val)
|
||
if seen[key] then
|
||
print(indent .. "*" .. key)
|
||
return
|
||
end
|
||
seen[key] = true
|
||
|
||
if type(val) == "table" then
|
||
for k, v in pairs(val) do
|
||
if type(v) == "table" then
|
||
print(indent .. "[" .. k .. "] => " .. tostring(val) .. " {")
|
||
printRecursive(v, indent .. string.rep(" ", string.len(k) + 8))
|
||
print(indent .. string.rep(" ", string.len(k) + 6) .. "}")
|
||
else
|
||
print(indent .. "[" .. k .. "] => " .. tostring(v))
|
||
end
|
||
end
|
||
else
|
||
print(indent .. tostring(val))
|
||
end
|
||
end
|
||
|
||
printRecursive(value, " ")
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Log
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.Log(...)
|
||
print(string.format("[%s]", Protected.ResourceName), ...)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Blip helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.CreateBlip(blipData)
|
||
local blip = AddBlipForCoord(blipData.location.x, blipData.location.y, blipData.location.z)
|
||
SetBlipSprite(blip, blipData.sprite or 1)
|
||
SetBlipColour(blip, blipData.color or 4)
|
||
SetBlipScale(blip, blipData.scale or 1.0)
|
||
SetBlipDisplay(blip, blipData.display or 4)
|
||
SetBlipAsShortRange(blip, blipData.shortRange or false)
|
||
SetBlipHighDetail(blip, blipData.highDetail or true)
|
||
BeginTextCommandSetBlipName("STRING")
|
||
AddTextComponentString(blipData.text)
|
||
EndTextCommandSetBlipName(blip)
|
||
return blip
|
||
end
|
||
|
||
function Utils.RemoveBlip(blip)
|
||
RemoveBlip(blip)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- RenderList helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.AddMarkerToRenderList(name, opts)
|
||
if not name or opts then return end
|
||
local entry = { name = name, type = "marker", opts = opts }
|
||
table.insert(Utils.RenderList, entry)
|
||
return entry
|
||
end
|
||
|
||
function Utils.RemoveMarkerFromRenderList(entry)
|
||
for idx, renderItem in ipairs(Utils.RenderList) do
|
||
if renderItem == entry then
|
||
table.remove(Utils.RenderList, idx)
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
function Utils.AddDrawTextToRenderList(name, opts)
|
||
if not name or not opts then return end
|
||
local entry = { name = name, type = "drawText", opts = opts }
|
||
table.insert(Utils.RenderList, entry)
|
||
return entry
|
||
end
|
||
|
||
function Utils.RemoveDrawTextFromRenderList(entry)
|
||
for idx, renderItem in ipairs(Utils.RenderList) do
|
||
if renderItem == entry then
|
||
table.remove(Utils.RenderList, idx)
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- RotateVectorFlat
|
||
-- Rotates a 2D/3D/4D vector by the given degrees around Z.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.RotateVectorFlat(vector, degrees)
|
||
local rad = degrees / 57.2958
|
||
local cosA = math.cos(rad)
|
||
local sinA = math.sin(rad)
|
||
|
||
local vType = type(vector)
|
||
if vType == "vector4" then
|
||
return vector4(
|
||
cosA * vector.x - sinA * vector.y,
|
||
sinA * vector.x + cosA * vector.y,
|
||
vector.z, vector.w
|
||
)
|
||
elseif vType == "vector3" then
|
||
return vector3(
|
||
cosA * vector.x - sinA * vector.y,
|
||
sinA * vector.x + cosA * vector.y,
|
||
vector.z
|
||
)
|
||
elseif vType == "vector2" then
|
||
return vector2(
|
||
cosA * vector.x - sinA * vector.y,
|
||
sinA * vector.x + cosA * vector.y
|
||
)
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- HandleRenderList
|
||
-- Renders all entries in RenderList each frame.
|
||
-- Returns true if anything was drawn.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.HandleRenderList()
|
||
local playerCoords = GetEntityCoords(PlayerPedId())
|
||
local drewSomething = false
|
||
|
||
for _, entry in ipairs(Utils.RenderList) do
|
||
local entryType = entry.type
|
||
local entryCoords = entry.opts.location and entry.opts.location.xyz
|
||
local distToPlayer = entryCoords and #(entryCoords - playerCoords) or math.huge
|
||
|
||
if entryType == "marker" then
|
||
if distToPlayer < entry.opts.renderDist then
|
||
Utils.DrawMarker(entry.opts)
|
||
drewSomething = true
|
||
end
|
||
|
||
elseif entryType == "drawText" then
|
||
if distToPlayer < entry.opts.renderDist then
|
||
if distToPlayer < entry.opts.interactDist then
|
||
entry.opts.text = string.format("%s %s", entry.opts.interactText, entry.opts.drawText)
|
||
if IsControlJustPressed(0, entry.opts.interactControl) then
|
||
entry.opts.onInteract(entry)
|
||
end
|
||
else
|
||
entry.opts.text = entry.opts.drawText
|
||
end
|
||
Utils.DrawText3D(entry.opts)
|
||
drewSomething = true
|
||
end
|
||
|
||
elseif entryType == "helpText" then
|
||
local withinDist = not entry.opts.renderDist or (distToPlayer < entry.opts.renderDist)
|
||
if withinDist then
|
||
Utils.ShowHelpNotification(entry.opts.text)
|
||
drewSomething = true
|
||
end
|
||
end
|
||
end
|
||
|
||
return drewSomething
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Camera helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.CreateCamera(camName, coords, rotation, activate, pointAtEntity, transitionTime)
|
||
local cam = CreateCamWithParams(camName, coords.x, coords.y, coords.z, 0, 0, 0, 50.0)
|
||
SetCamCoord(cam, coords.x, coords.y, coords.z)
|
||
SetCamRot(cam, rotation.x, rotation.y, rotation.z, 2)
|
||
|
||
if activate then
|
||
SetCamActive(cam, true)
|
||
RenderScriptCams(true, true, transitionTime or 1000, true, true)
|
||
end
|
||
|
||
if pointAtEntity then
|
||
PointCamAtEntity(cam, pointAtEntity)
|
||
end
|
||
|
||
return cam
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- DrawMarker
|
||
-- Wraps the native DrawMarker call with named params.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.DrawMarker(opts)
|
||
if not (opts.location and opts.location.x and opts.location.y and opts.location.z) then
|
||
return
|
||
end
|
||
|
||
DrawMarker(
|
||
opts.type or 0,
|
||
opts.location.x, opts.location.y, opts.location.z,
|
||
(opts.direction and opts.direction.x) or 1.0,
|
||
(opts.direction and opts.direction.y) or 0.0,
|
||
(opts.direction and opts.direction.z) or 0.0,
|
||
(opts.rotation and opts.rotation.x) or 1.0,
|
||
(opts.rotation and opts.rotation.y) or 0.0,
|
||
(opts.rotation and opts.rotation.z) or 0.0,
|
||
(opts.scale and opts.scale.x) or 1.0,
|
||
(opts.scale and opts.scale.y) or 1.0,
|
||
(opts.scale and opts.scale.z) or 1.0,
|
||
opts.red or 255,
|
||
opts.green or 255,
|
||
opts.blue or 255,
|
||
opts.alpha or 255,
|
||
opts.bobUpAndDown or false,
|
||
(opts.faceCamera == nil) and true or opts.faceCamera,
|
||
opts.p19 or 2,
|
||
opts.rotate or false
|
||
)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Notification helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.ShowNotification(message)
|
||
SetNotificationTextEntry("STRING")
|
||
AddTextComponentSubstringPlayerName(message)
|
||
DrawNotification(false, true)
|
||
end
|
||
|
||
function Utils.ShowHelpNotification(message)
|
||
AddTextEntry("housingHelp", message)
|
||
DisplayHelpTextThisFrame("housingHelp", false)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- DrawText3D
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.DrawText3D(opts)
|
||
local worldPos = vector3(opts.location.x, opts.location.y, opts.location.z)
|
||
local onScreen, screenX, screenY = World3dToScreen2d(worldPos.x, worldPos.y, worldPos.z)
|
||
local camCoords = GetGameplayCamCoords()
|
||
local dist = GetDistanceBetweenCoords(camCoords, worldPos.x, worldPos.y, worldPos.z, true)
|
||
local size = (opts.size or 1)
|
||
local scaledSize = (size / dist) * 2 * (1 / GetGameplayCamFov()) * 100
|
||
|
||
if onScreen then
|
||
SetTextScale(0.0 * scaledSize, 0.55 * scaledSize)
|
||
SetTextFont(opts.font or 1)
|
||
SetTextColour(opts.red or 255, opts.green or 255, opts.blue or 255, opts.alpha or 255)
|
||
SetTextDropshadow(0, 0, 0, 0, 255)
|
||
SetTextDropShadow()
|
||
SetTextOutline()
|
||
SetTextEntry("STRING")
|
||
SetTextCentre(1)
|
||
AddTextComponentString(opts.text)
|
||
DrawText(screenX, screenY)
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- TriggerClientEvent / TriggerServerEvent
|
||
-- (namespaced with Protected.ResourceName)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
if IsDuplicityVersion() then
|
||
function Utils.TriggerClientEvent(eventName, targetPlayer, ...)
|
||
local fullName = string.format("%s:%s", Protected.ResourceName, eventName)
|
||
TriggerClientEvent(fullName, targetPlayer, ...)
|
||
if Config.Debug then
|
||
Utils.Log(string.format("Triggering client event: %s (%i).", fullName, targetPlayer))
|
||
end
|
||
end
|
||
|
||
function Utils.GetDatabaseName()
|
||
local connStr = GetConvar("mysql_connection_string", "Empty")
|
||
if not connStr or connStr == "Empty" then return false end
|
||
|
||
local dbStart, dbEnd = connStr:find("database=")
|
||
if dbStart and dbEnd then
|
||
local semiPos = connStr:find(";", dbEnd + 1)
|
||
local endPos = semiPos and semiPos - 1 or #connStr
|
||
return connStr:sub(dbEnd + 1, endPos)
|
||
end
|
||
|
||
local mysqlStart, mysqlEnd = connStr:find("mysql://")
|
||
if not mysqlStart then return false end
|
||
|
||
local atStart, atEnd = connStr:find("@", mysqlEnd)
|
||
local slashStart, slashEnd = connStr:find("/", atEnd + 1)
|
||
local qStart, qEnd = connStr:find("?")
|
||
local dbEndPos = qEnd and qEnd - 1 or #connStr
|
||
return connStr:sub(slashEnd + 1, dbEndPos)
|
||
end
|
||
else
|
||
function Utils.TriggerServerEvent(eventName, ...)
|
||
local fullName = string.format("%s:%s", Protected.ResourceName, eventName)
|
||
TriggerServerEvent(fullName, ...)
|
||
if Config.Debug then
|
||
Utils.Log(string.format("Triggering server event: %s.", fullName))
|
||
end
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- RegisterNetEvent / RegisterEvent (namespaced)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.RegisterNetEvent(eventName, handler)
|
||
local fullName = string.format("%s:%s", Protected.ResourceName, eventName)
|
||
RegisterNetEvent(fullName)
|
||
|
||
if Config.Debug then
|
||
Utils.Log(string.format("Net event %s registered.", fullName))
|
||
AddEventHandler(fullName, function(...)
|
||
Utils.Log(string.format("Net event %s triggered.", fullName))
|
||
handler(...)
|
||
end)
|
||
else
|
||
AddEventHandler(fullName, handler)
|
||
end
|
||
end
|
||
|
||
function Utils.RegisterEvent(eventName, handler)
|
||
local fullName = string.format("%s:%s", Protected.ResourceName, eventName)
|
||
AddEventHandler(fullName, handler)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- DisableControlActions
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.DisableControlActions(...)
|
||
local controls = { ... }
|
||
for _, control in ipairs(controls) do
|
||
DisableControlAction(0, control, true)
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Bounding box helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.DrawEntityBoundingBox(entity, r, g, b, a)
|
||
local bb = Utils.GetEntityBoundingBox(entity)
|
||
Utils.DrawBoundingBox(bb, r, g, b, a)
|
||
end
|
||
|
||
function Utils.GetEntityBoundingBox(entity)
|
||
local minDim, maxDim = GetModelDimensions(GetEntityModel(entity))
|
||
local eps = 0.001
|
||
local offs = GetOffsetFromEntityInWorldCoords
|
||
|
||
return {
|
||
offs(entity, minDim.x - eps, minDim.y - eps, minDim.z - eps),
|
||
offs(entity, maxDim.x + eps, minDim.y - eps, minDim.z - eps),
|
||
offs(entity, maxDim.x + eps, maxDim.y + eps, minDim.z - eps),
|
||
offs(entity, minDim.x - eps, maxDim.y + eps, minDim.z - eps),
|
||
offs(entity, minDim.x - eps, minDim.y - eps, maxDim.z + eps),
|
||
offs(entity, maxDim.x + eps, minDim.y - eps, maxDim.z + eps),
|
||
offs(entity, maxDim.x + eps, maxDim.y + eps, maxDim.z + eps),
|
||
offs(entity, minDim.x - eps, maxDim.y + eps, maxDim.z + eps),
|
||
}
|
||
end
|
||
|
||
function Utils.Get2DEntityBoundingBox(entity)
|
||
local minDim, maxDim = GetModelDimensions(GetEntityModel(entity))
|
||
local eps = 0.001
|
||
local offs = GetOffsetFromEntityInWorldCoords
|
||
|
||
return {
|
||
offs(entity, minDim.x - eps, minDim.y - eps, minDim.z - eps),
|
||
offs(entity, maxDim.x + eps, minDim.y - eps, minDim.z - eps),
|
||
offs(entity, maxDim.x + eps, maxDim.y + eps, minDim.z - eps),
|
||
offs(entity, minDim.x - eps, maxDim.y + eps, minDim.z - eps),
|
||
}
|
||
end
|
||
|
||
function Utils.GetBoundingBoxPolyMatrix(corners)
|
||
local c = corners
|
||
return {
|
||
{ c[3], c[2], c[1] }, { c[4], c[3], c[1] },
|
||
{ c[5], c[6], c[7] }, { c[5], c[7], c[8] },
|
||
{ c[3], c[4], c[7] }, { c[8], c[7], c[4] },
|
||
{ c[1], c[2], c[5] }, { c[6], c[5], c[2] },
|
||
{ c[2], c[3], c[6] }, { c[3], c[7], c[6] },
|
||
{ c[5], c[8], c[4] }, { c[5], c[4], c[1] },
|
||
}
|
||
end
|
||
|
||
function Utils.GetBoundingBoxEdgeMatrix(corners)
|
||
local c = corners
|
||
return {
|
||
{ c[1], c[2] }, { c[2], c[3] }, { c[3], c[4] }, { c[4], c[1] },
|
||
{ c[5], c[6] }, { c[6], c[7] }, { c[7], c[8] }, { c[8], c[5] },
|
||
{ c[1], c[5] }, { c[2], c[6] }, { c[3], c[7] }, { c[4], c[8] },
|
||
}
|
||
end
|
||
|
||
function Utils.DrawPolyMatrix(polyMatrix, r, g, b, a)
|
||
for _, tri in ipairs(polyMatrix) do
|
||
DrawPoly(
|
||
tri[1].x, tri[1].y, tri[1].z,
|
||
tri[2].x, tri[2].y, tri[2].z,
|
||
tri[3].x, tri[3].y, tri[3].z,
|
||
r, g, b, a
|
||
)
|
||
end
|
||
end
|
||
|
||
function Utils.DrawEdgeMatrix(edgeMatrix, r, g, b, a)
|
||
for _, edge in ipairs(edgeMatrix) do
|
||
DrawLine(
|
||
edge[1].x, edge[1].y, edge[1].z,
|
||
edge[2].x, edge[2].y, edge[2].z,
|
||
r, g, b, a
|
||
)
|
||
end
|
||
end
|
||
|
||
function Utils.DrawBoundingBox(bbox, r, g, b, a)
|
||
Utils.DrawPolyMatrix(Utils.GetBoundingBoxPolyMatrix(bbox), r, g, b, a)
|
||
Utils.DrawEdgeMatrix(Utils.GetBoundingBoxEdgeMatrix(bbox), 255, 255, 255, 255)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Scaleform helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.DrawScaleform(scaleformHandle)
|
||
DrawScaleformMovieFullscreen(scaleformHandle, 255, 255, 255, 255)
|
||
end
|
||
|
||
function Utils.DisableControlAction(control)
|
||
DisableControlAction(0, control, true)
|
||
end
|
||
|
||
function Utils.CreateInstructional(controls)
|
||
local scaleform = Scaleforms.LoadMovie("INSTRUCTIONAL_BUTTONS")
|
||
Scaleforms.PopVoid(scaleform, "CLEAR_ALL")
|
||
Scaleforms.PopInt(scaleform, "SET_CLEAR_SPACE", 200)
|
||
|
||
for idx, control in ipairs(controls) do
|
||
PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT")
|
||
PushScaleformMovieFunctionParameterInt(idx - 1)
|
||
for _, code in ipairs(control.codes) do
|
||
ScaleformMovieMethodAddParamPlayerNameString(GetControlInstructionalButton(0, code, true))
|
||
end
|
||
BeginTextCommandScaleformString("STRING")
|
||
AddTextComponentScaleform(control.label)
|
||
EndTextCommandScaleformString()
|
||
PopScaleformMovieFunctionVoid()
|
||
end
|
||
|
||
Scaleforms.PopVoid(scaleform, "DRAW_INSTRUCTIONAL_BUTTONS")
|
||
return scaleform
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GetControls
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.GetControls(...)
|
||
local keys = { ... }
|
||
local result = {}
|
||
for _, key in ipairs(keys) do
|
||
local control = nil
|
||
if type(key) == "table" then
|
||
control = ActionControls[key.key]
|
||
if not control then
|
||
Error("Utils.GetControls ::: " .. key.key .. " not found")
|
||
return
|
||
end
|
||
control.label = key.label
|
||
else
|
||
control = ActionControls[key]
|
||
end
|
||
result[#result + 1] = control
|
||
end
|
||
return result
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Fly camera
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local _flyCamMoved = false
|
||
local _flyCamRotated = false
|
||
|
||
function Utils.HandleFlyCam(cam, opts)
|
||
if not opts then opts = {} end
|
||
|
||
opts.mouse = (opts.mouse == nil) and true or opts.mouse
|
||
opts.keyboard = (opts.keyboard == nil) and true or opts.keyboard
|
||
|
||
local camCoords = GetCamCoord(cam)
|
||
local camRot = GetCamRot(cam, 2)
|
||
local mouseX = GetDisabledControlNormal(0, 1)
|
||
local mouseY = GetDisabledControlNormal(0, 2)
|
||
|
||
local _, _, _, camMatrix = table.unpack({ GetCamMatrix(cam) })
|
||
local upVector = vector3(0.0, 0.0, 1.0)
|
||
local rightVector = norm(vector3(camMatrix.x, camMatrix.y, 0.0))
|
||
local fwdVector = norm(vector3(camMatrix.x, camMatrix.y, 0.0)) -- approximation
|
||
local frameTime = GetFrameTime()
|
||
|
||
_flyCamMoved = false
|
||
_flyCamRotated = false
|
||
|
||
if opts.keyboard then
|
||
if IsDisabledControlPressed(0, ActionControls.up.codes[2]) then
|
||
camCoords = camCoords + upVector * (CameraOptions.climbSpeed * frameTime)
|
||
_flyCamMoved = true
|
||
elseif IsDisabledControlPressed(0, ActionControls.up.codes[1]) then
|
||
camCoords = camCoords - upVector * (CameraOptions.climbSpeed * frameTime)
|
||
_flyCamMoved = true
|
||
end
|
||
|
||
if IsDisabledControlPressed(0, ActionControls.forward.codes[2]) then
|
||
camCoords = camCoords + fwdVector * (CameraOptions.moveSpeed * frameTime)
|
||
_flyCamMoved = true
|
||
elseif IsDisabledControlPressed(0, ActionControls.forward.codes[1]) then
|
||
camCoords = camCoords - fwdVector * (CameraOptions.moveSpeed * frameTime)
|
||
_flyCamMoved = true
|
||
end
|
||
|
||
if IsDisabledControlPressed(0, ActionControls.right.codes[1]) then
|
||
camCoords = camCoords + rightVector * (CameraOptions.moveSpeed * frameTime)
|
||
_flyCamMoved = true
|
||
elseif IsDisabledControlPressed(0, ActionControls.right.codes[2]) then
|
||
camCoords = camCoords - rightVector * (CameraOptions.moveSpeed * frameTime)
|
||
_flyCamMoved = true
|
||
end
|
||
end
|
||
|
||
if opts.mouse then
|
||
if mouseY ~= 0.0 then
|
||
local newX = math.max(-80.0, math.min(80.0, camRot.x - mouseY * CameraOptions.lookSpeedX * frameTime))
|
||
camRot = vector3(newX, camRot.y, camRot.z)
|
||
_flyCamRotated = true
|
||
end
|
||
if mouseX ~= 0.0 then
|
||
camRot = vector3(camRot.x, camRot.y, camRot.z - mouseX * CameraOptions.lookSpeedY * frameTime)
|
||
_flyCamRotated = true
|
||
end
|
||
end
|
||
|
||
if _flyCamMoved then SetCamCoord(cam, camCoords) end
|
||
if _flyCamRotated then SetCamRot(cam, camRot, 2) end
|
||
|
||
if opts.boundPos and opts.boundDist then
|
||
local dist = #(camCoords - opts.boundPos)
|
||
if dist > opts.boundDist then
|
||
local clamped = opts.boundPos + norm(camCoords - opts.boundPos) * opts.boundDist
|
||
SetCamCoord(cam, clamped)
|
||
end
|
||
end
|
||
|
||
if opts.updatePlayerCoords then
|
||
SetEntityCoords(cache.ped, camCoords.x, camCoords.y, camCoords.z, false, false, false, false)
|
||
SetEntityHeading(cache.ped, camRot.z)
|
||
end
|
||
|
||
return camCoords, camRot
|
||
end
|
||
|
||
function Utils.DestroyFlyCam(cam, transitionTime)
|
||
SetCamActive(cam, false)
|
||
RenderScriptCams(false, true, transitionTime or 0, true, true)
|
||
DestroyCam(cam, false)
|
||
SetFocusEntity(cache.ped)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Raycast helpers
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.ScreenToWorld()
|
||
local camRot = GetGameplayCamRot(0)
|
||
local camCoords = GetGameplayCamCoord()
|
||
local mouseX = GetControlNormal(0, 239)
|
||
local mouseY = GetControlNormal(0, 240)
|
||
local cursor = vector2(mouseX, mouseY)
|
||
|
||
local worldPos, worldDir = Utils.ScreenRelToWorld(camCoords, camRot, cursor)
|
||
local endPoint = camCoords + worldDir * 50.0
|
||
|
||
local rayHandle = StartShapeTestRay(
|
||
worldPos.x, worldPos.y, worldPos.z,
|
||
endPoint.x, endPoint.y, endPoint.z,
|
||
-1, 0, 4
|
||
)
|
||
local _, hit, hitCoords, _, hitEntity = GetShapeTestResult(rayHandle)
|
||
return hit, hitCoords, hitEntity
|
||
end
|
||
|
||
function Utils.RotationToDirection(rotation)
|
||
local radZ = math.rad(rotation.z)
|
||
local radX = math.rad(rotation.x)
|
||
local cosX = math.abs(math.cos(radX))
|
||
return vec3(
|
||
-math.sin(radZ) * cosX,
|
||
math.cos(radZ) * cosX,
|
||
math.sin(radX)
|
||
)
|
||
end
|
||
|
||
function Utils.World3DToScreen2D(worldCoords)
|
||
local _, screenX, screenY = GetScreenCoordFromWorldCoord(worldCoords.x, worldCoords.y, worldCoords.z)
|
||
return vector2(screenX, screenY)
|
||
end
|
||
|
||
function Utils.ScreenRelToWorld(camCoords, camRot, screenPos)
|
||
local dir = Utils.RotationToDirection(camRot)
|
||
local rotPlusX = vector3(camRot.x + 1.0, camRot.y, camRot.z)
|
||
local rotMinusX = vector3(camRot.x - 1.0, camRot.y, camRot.z)
|
||
local rotPlusZ = vector3(camRot.x, camRot.y, camRot.z + 1.0)
|
||
local rotMinusZ = vector3(camRot.x, camRot.y, camRot.z - 1.0)
|
||
|
||
local rightDelta = Utils.RotationToDirection(rotPlusX) - Utils.RotationToDirection(rotMinusX)
|
||
local upDelta = Utils.RotationToDirection(rotPlusZ) - Utils.RotationToDirection(rotMinusZ)
|
||
|
||
local radY = -(camRot.y * math.pi / 180.0)
|
||
local rightX = rightDelta * math.cos(radY) - upDelta * math.sin(radY)
|
||
local rightY = rightDelta * math.sin(radY) + upDelta * math.cos(radY)
|
||
|
||
local farPoint = camCoords + dir * 1.0 + rightX + rightY
|
||
local basePoint = camCoords + dir * 1.0
|
||
|
||
local farScreen = Utils.World3DToScreen2D(farPoint)
|
||
local baseScreen = Utils.World3DToScreen2D(basePoint)
|
||
|
||
local ratioX = (screenPos.x - baseScreen.x) / (farScreen.x - baseScreen.x)
|
||
local ratioY = (screenPos.y - baseScreen.y) / (farScreen.y - baseScreen.y)
|
||
|
||
local worldPoint = basePoint + rightX * ratioX + rightY * ratioY
|
||
local worldDir = rightDelta * ratioX + upDelta * ratioY
|
||
|
||
return worldPoint, worldDir
|
||
end
|
||
|
||
function Utils.CreateObject(model, coords)
|
||
if type(model) == "string" then
|
||
model = joaat(model) or model
|
||
end
|
||
lib.requestModel(model)
|
||
RequestModel(model)
|
||
while not HasModelLoaded(model) do
|
||
Wait(0)
|
||
end
|
||
local obj = CreateObject(model, coords.x, coords.y, coords.z, false, false, false)
|
||
SetModelAsNoLongerNeeded(model)
|
||
return obj
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GetCamera
|
||
-- Returns coords and rotation from the editor or gameplay camera.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.GetCamera()
|
||
local coords = EditorCamera and GetCamCoord(EditorCamera) or GetGameplayCamCoord()
|
||
local rotation = EditorCamera and GetCamRot(EditorCamera, 2) or GetGameplayCamRot(0)
|
||
return { coords = coords, rotation = rotation }
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- getCursorHitCoords
|
||
-- Returns world hit coords and entity using a screen-space swept sphere.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.getCursorHitCoords(ignoreEntity)
|
||
local mouseX = GetDisabledControlNormal(0, 239)
|
||
local mouseY = GetDisabledControlNormal(0, 240)
|
||
|
||
local startPos, dir = GetWorldCoordFromScreenCoord(mouseX, mouseY)
|
||
local endPos = startPos + dir * 120
|
||
|
||
local pedToIgnore = ignoreEntity or cache.ped
|
||
|
||
local rayHandle = StartShapeTestSweptSphere(
|
||
startPos.x, startPos.y, startPos.z,
|
||
endPos.x, endPos.y, endPos.z,
|
||
0.01, 17, pedToIgnore, 4
|
||
)
|
||
local status, hitCoords, _, hitEntity = GetShapeTestResult(rayHandle)
|
||
|
||
if not status then return nil, nil end
|
||
return hitCoords, hitEntity
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- GetAllPeds / GetAllObjects / GetAllVehicles
|
||
-- ──────────────────────────────────────────────
|
||
|
||
local function iterateEntities(findFirst, findNext, endFind)
|
||
local entities = {}
|
||
local entityId, found = findFirst()
|
||
while found do
|
||
entities[#entities + 1] = found
|
||
found = findNext(entityId)
|
||
end
|
||
endFind(entityId)
|
||
return entities
|
||
end
|
||
|
||
function Utils.GetAllPeds() return iterateEntities(FindFirstPed, FindNextPed, EndFindPed) end
|
||
function Utils.GetAllObjects() return iterateEntities(FindFirstObject, FindNextObject, EndFindObject) end
|
||
function Utils.GetAllVehicles()return iterateEntities(FindFirstVehicle, FindNextVehicle, EndFindVehicle) end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- FindNthInString
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.FindNthInString(str, pattern, n)
|
||
local startPos = nil
|
||
local endPos = nil
|
||
for _ = 1, n do
|
||
startPos, endPos = str:find(pattern, endPos and endPos + 1 or 0)
|
||
end
|
||
return startPos, endPos
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- SelectPlayer
|
||
-- Lets the local player select a nearby player
|
||
-- using instructional button controls.
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function Utils.SelectPlayer()
|
||
local localCoords = GetEntityCoords(PlayerPedId())
|
||
local nearbyPeds = {}
|
||
|
||
for _, playerId in ipairs(GetActivePlayers()) do
|
||
local ped = GetPlayerPed(playerId)
|
||
if ped > 0 and DoesEntityExist(ped) then
|
||
local dist = #(GetEntityCoords(ped) - localCoords)
|
||
if dist <= 20.0 then
|
||
nearbyPeds[#nearbyPeds + 1] = ped
|
||
end
|
||
end
|
||
end
|
||
|
||
local controls = Utils.GetControls("select_player", "change_player", "cancel")
|
||
local scaleform = Utils.CreateInstructional(controls)
|
||
local selectedIdx = 1
|
||
|
||
while true do
|
||
if IsControlJustPressed(0, ActionControls.cancel.codes[1]) then return end
|
||
|
||
if IsControlJustPressed(0, ActionControls.select_player.codes[1]) then
|
||
return NetworkGetEntityOwner(nearbyPeds[selectedIdx])
|
||
end
|
||
|
||
if IsControlJustPressed(0, ActionControls.change_player.codes[1]) then
|
||
selectedIdx = selectedIdx + 1
|
||
if selectedIdx > #nearbyPeds then selectedIdx = 1 end
|
||
elseif IsControlJustPressed(0, ActionControls.change_player.codes[2]) then
|
||
selectedIdx = selectedIdx - 1
|
||
if selectedIdx < 1 then selectedIdx = #nearbyPeds end
|
||
end
|
||
|
||
Utils.DrawMarker({
|
||
type = 0,
|
||
scale = vector3(0.2, 0.2, 0.2),
|
||
location = GetEntityCoords(nearbyPeds[selectedIdx]) + vector3(0.0, 0.0, 1.0),
|
||
})
|
||
Utils.DrawScaleform(scaleform)
|
||
Wait(0)
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- ScreenRelToWorld global alias (for gizmo plugins)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function ScreenRelToWorld(worldPos, camRot, screenPos)
|
||
return Utils.ScreenRelToWorld(worldPos, camRot, screenPos)
|
||
end
|
||
|
||
function LocationInWorld(targetCoords, cam, flags)
|
||
local camCoords = GetCamCoord(cam)
|
||
local playerPed = PlayerPedId()
|
||
local _, hit, hitCoords, _, hitEntity = GetShapeTestResult(
|
||
StartShapeTestRay(
|
||
camCoords.x, camCoords.y, camCoords.z,
|
||
targetCoords.x, targetCoords.y, targetCoords.z,
|
||
flags, playerPed, 0
|
||
)
|
||
)
|
||
currentCoords = hitCoords
|
||
return hit, hitCoords, hitEntity
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- RotationToDirection global alias
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function RotationToDirection(rotation)
|
||
return Utils.RotationToDirection(rotation)
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- string.includes
|
||
-- ──────────────────────────────────────────────
|
||
|
||
function string.includes(str, value)
|
||
if type(value) == "string" then
|
||
return str == value
|
||
elseif type(value) == "table" then
|
||
for _, v in ipairs(value) do
|
||
if str == v then return true end
|
||
end
|
||
return false
|
||
end
|
||
end
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Keys table (key names → input codes)
|
||
-- ──────────────────────────────────────────────
|
||
|
||
Keys = {
|
||
ESC = 322, F1 = 288, F2 = 289, F3 = 170, F5 = 166, F6 = 167, F7 = 168, F8 = 169, F9 = 56, F10 = 57,
|
||
["~"] = 243, ["1"] = 157, ["2"] = 158, ["3"] = 160, ["4"] = 164, ["5"] = 165, ["6"] = 159,
|
||
["7"] = 161, ["8"] = 162, ["9"] = 163, ["-"] = 84, ["="] = 83,
|
||
BACKSPACE = 177, TAB = 37, Q = 44, W = 32, E = 38, R = 45, T = 245, Y = 246, U = 303, P = 199,
|
||
["["] = 39, ["]"] = 40, ENTER = 18, CAPS = 137,
|
||
A = 34, S = 8, D = 9, F = 23, G = 47, H = 74, K = 311, L = 182,
|
||
LEFTSHIFT = 21, Z = 20, X = 73, C = 26, V = 0, B = 29, N = 249, M = 244,
|
||
[","] = 82, ["."] = 81, LEFTCTRL = 36, LEFTALT = 19, SPACE = 22, RIGHTCTRL = 70,
|
||
HOME = 213, PAGEUP = 10, PAGEDOWN = 11, DELETE = 178,
|
||
LEFT = 174, RIGHT = 175, TOP = 27, DOWN = 173,
|
||
NENTER = 201, N4 = 108, N5 = 60, N6 = 107, ["N+"] = 96, ["N-"] = 97, N7 = 117, N8 = 61, N9 = 118,
|
||
}
|
||
|
||
-- ──────────────────────────────────────────────
|
||
-- Instructional drawing loop
|
||
-- ──────────────────────────────────────────────
|
||
|
||
DrawingInstructional = false
|
||
|
||
function Utils.DrawInstructional(controlKeys)
|
||
if DrawingInstructional then
|
||
Debug("Instructional", "Instructional already being drawn, updating keys.")
|
||
return
|
||
end
|
||
|
||
CreateThread(function()
|
||
DrawingInstructional = true
|
||
while DrawingInstructional do
|
||
Wait(0)
|
||
local controls = Utils.GetControls(controlKeys)
|
||
local scaleform = Utils.CreateInstructional(controls)
|
||
Utils.DrawScaleform(scaleform)
|
||
end
|
||
end)
|
||
end
|
||
|
||
function Utils.RemoveInstructional()
|
||
DrawingInstructional = false
|
||
end
|