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