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

1058 lines
42 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/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