Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions client/modules/raycast.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
local vector3 <const> = vector3
local abs <const> = math.abs
local cos <const> = math.cos
local sin <const> = math.sin
local rad <const> = math.rad

---@class RAYCAST_RESULT
---@field public hit boolean
---@field public state integer
---@field public handle integer
---@field public didHit integer
---@field public coords vector3
---@field public normal vector3
---@field public entity integer
---@field public material integer?

---@class RAYCAST
local Raycast = {}

Raycast.Flags = {
World = 1,
Vehicles = 2,
Peds = 4,
Ragdolls = 8,
Objects = 16,
Pickups = 32,
Glass = 64,
Rivers = 128,
Foliage = 256,
All = 511
}

local function toVector3(value, label)
if value == nil then
error(("raycast: %s is required"):format(label), 3)
end

local valueType <const> = type(value)
if valueType == "vector3" then
return value
end

if valueType ~= "table" then
error(("raycast: %s must be a vector3 or table, received %s"):format(label, valueType), 3)
end

local x <const> = value.x or value[1]
local y <const> = value.y or value[2]
local z <const> = value.z or value[3]

if x == nil or y == nil or z == nil then
error(("raycast: %s requires x, y and z values"):format(label), 3)
end

return vector3(x + 0.0, y + 0.0, z + 0.0)
end

local function rotationToDirection(rotation)
local pitch <const> = rad(rotation.x)
local yaw <const> = rad(rotation.z)
local cosPitch <const> = abs(cos(pitch))

return vector3(-sin(yaw) * cosPitch, cos(yaw) * cosPitch, sin(pitch))
end

local function buildResult(state, handle, didHit, hitCoords, surfaceNormal, entityHit, materialHash)
return {
state = state,
handle = handle,
didHit = didHit,
hit = didHit == 1,
coords = hitCoords,
normal = surfaceNormal,
entity = entityHit,
material = materialHash
}
end

local function getShapeTestResult(handle)
if GetShapeTestResultIncludingMaterial then
local state, didHit, hitCoords, surfaceNormal, materialHash, entityHit = GetShapeTestResultIncludingMaterial(handle)
return buildResult(state, handle, didHit, hitCoords, surfaceNormal, entityHit, materialHash)
end

local state, didHit, hitCoords, surfaceNormal, entityHit = GetShapeTestResult(handle)
return buildResult(state, handle, didHit, hitCoords, surfaceNormal, entityHit)
end

---@param startCoords vector3 | {x:number, y:number, z:number}
---@param endCoords vector3 | {x:number, y:number, z:number}
---@param flags integer?
---@param ignoreEntity integer?
---@param options {traceType?: integer, timeout?: integer, wait?: integer}?
---@return RAYCAST_RESULT
function Raycast.Cast(startCoords, endCoords, flags, ignoreEntity, options)
local start <const> = toVector3(startCoords, "startCoords")
local finish <const> = toVector3(endCoords, "endCoords")
local mask <const> = flags or Raycast.Flags.All
local target <const> = ignoreEntity or PlayerPedId()
local traceType <const> = options?.traceType or 7
local timeout <const> = options?.timeout or 1000
local delay <const> = options?.wait or 0

local handle <const> = StartShapeTestLosProbe(
start.x, start.y, start.z,
finish.x, finish.y, finish.z,
mask,
target,
traceType
)

local startTime <const> = GetGameTimer()
local result = getShapeTestResult(handle)

while result.state == 1 do
if (GetGameTimer() - startTime) > timeout then
break
end

Wait(delay)
result = getShapeTestResult(handle)
end

return result
end

---@param distance number?
---@param flags integer?
---@param ignoreEntity integer?
---@param options {offset?: vector3|{x:number,y:number,z:number}, traceType?: integer, timeout?: integer, wait?: integer}?
---@return RAYCAST_RESULT
function Raycast.FromCamera(distance, flags, ignoreEntity, options)
local camCoords <const> = GetGameplayCamCoord()
local camRotation <const> = GetGameplayCamRot(2)
local direction <const> = rotationToDirection(camRotation)
local origin <const> = options?.offset and (camCoords + toVector3(options.offset, "options.offset")) or camCoords
local rayDistance <const> = distance or 10.0
local destination <const> = origin + (direction * rayDistance)

return Raycast.Cast(origin, destination, flags, ignoreEntity, options)
end

---@param entity integer
---@param distance number?
---@param flags integer?
---@param ignoreEntity integer?
---@param options {offset?: vector3|{x:number,y:number,z:number}, traceType?: integer, timeout?: integer, wait?: integer}?
---@return RAYCAST_RESULT
function Raycast.FromEntity(entity, distance, flags, ignoreEntity, options)
if not entity or not DoesEntityExist(entity) then
error("raycast: entity does not exist", 2)
end

local origin <const> = GetEntityCoords(entity)
local offset <const> = options?.offset and toVector3(options.offset, "options.offset") or vector3(0.0, 0.0, 0.0)
local direction <const> = GetEntityForwardVector(entity)
local rayDistance <const> = distance or 10.0
local start <const> = origin + offset
local destination <const> = start + (direction * rayDistance)

return Raycast.Cast(start, destination, flags, ignoreEntity or entity, options)
end

return {
Raycast = Raycast
}
1 change: 1 addition & 0 deletions import.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ local content <const> = {
class = true,
functions = true,
notify = true,
logger = true,
},
}

Expand Down
187 changes: 187 additions & 0 deletions shared/logger.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---@class LOGGER_CONTEXT: table<string, any>

---@class LOGGER_OPTIONS
---@field public resource string?
---@field public prefix string?
---@field public debug boolean?
---@field public colorize boolean?

---@class LOGGER
local Logger = {}

local DEBUG_ENABLED = false

local LEVELS <const> = {
INFO = { label = "INFO", color = "^2" },
WARN = { label = "WARN", color = "^3" },
ERROR = { label = "ERROR", color = "^1" },
DEBUG = { label = "DEBUG", color = "^4" }
}

local function getResourceName()
local invoking = GetInvokingResource and GetInvokingResource() or nil
if invoking and invoking ~= "" then
return invoking
end

return GetCurrentResourceName()
end

local function padTime(value)
return ("%02d"):format(value)
end

local function getServerTime()
return os.date("%H:%M:%S")
end

local function getClientTime()
local totalSeconds <const> = math.floor(GetGameTimer() / 1000)
local hours <const> = math.floor(totalSeconds / 3600) % 24
local minutes <const> = math.floor((totalSeconds % 3600) / 60)
local seconds <const> = totalSeconds % 60

return ("%s:%s:%s"):format(padTime(hours), padTime(minutes), padTime(seconds))
end

local function getTime()
if IsDuplicityVersion() then
return getServerTime()
end

return getClientTime()
end

local function encodeValue(value)
local valueType <const> = type(value)
if valueType == "string" then
return value
end

if valueType == "number" or valueType == "boolean" then
return tostring(value)
end

if value == nil then
return "nil"
end

if valueType == "table" and json and json.encode then
local ok, encoded = pcall(json.encode, value)
if ok and encoded then
return encoded
end
end

return tostring(value)
end

local function buildContext(context)
if context == nil then
return nil
end

if type(context) ~= "table" then
return tostring(context)
end

local parts <const> = {}
for key, value in pairs(context) do
parts[#parts + 1] = ("%s=%s"):format(tostring(key), encodeValue(value))
end

table.sort(parts)

if #parts == 0 then
return nil
end

return table.concat(parts, " ")
end

local function applyColor(enabled, color, text)
if enabled == false then
return text
end

return ("%s%s^7"):format(color, text)
end

local function normalizeLevel(level)
local key <const> = tostring(level or "INFO"):upper()
return LEVELS[key] and key or "INFO"
end

---@param level string
---@param message any
---@param context LOGGER_CONTEXT?
---@param options LOGGER_OPTIONS?
function Logger.Log(level, message, context, options)
local normalizedLevel <const> = normalizeLevel(level)
local metadata <const> = LEVELS[normalizedLevel]
local shouldForceDebug <const> = options?.debug == true

if normalizedLevel == "DEBUG" and not DEBUG_ENABLED and not shouldForceDebug then
return
end

local colorize <const> = options?.colorize ~= false
local resourceName <const> = options?.resource or getResourceName()
local timestamp <const> = getTime()
local prefix <const> = options?.prefix and ("[%s] "):format(options.prefix) or ""
local contextString <const> = buildContext(context)

local resourcePart <const> = applyColor(colorize, "^6", ("[%s]"):format(resourceName))
local timePart <const> = applyColor(colorize, "^5", ("[%s]"):format(timestamp))
local levelPart <const> = applyColor(colorize, metadata.color, ("[%s]"):format(metadata.label))
local body <const> = ("%s%s"):format(prefix, tostring(message))

local line = ("%s %s %s %s"):format(resourcePart, timePart, levelPart, body)
if contextString then
line = ("%s | %s"):format(line, contextString)
end

print(line)
end

---@param message any
---@param context LOGGER_CONTEXT?
---@param options LOGGER_OPTIONS?
function Logger.Info(message, context, options)
Logger.Log("INFO", message, context, options)
end

---@param message any
---@param context LOGGER_CONTEXT?
---@param options LOGGER_OPTIONS?
function Logger.Warn(message, context, options)
Logger.Log("WARN", message, context, options)
end

---@param message any
---@param context LOGGER_CONTEXT?
---@param options LOGGER_OPTIONS?
function Logger.Error(message, context, options)
Logger.Log("ERROR", message, context, options)
end

---@param message any
---@param context LOGGER_CONTEXT?
---@param options LOGGER_OPTIONS?
function Logger.Debug(message, context, options)
Logger.Log("DEBUG", message, context, options)
end

---@param enabled boolean
function Logger.SetDebugEnabled(enabled)
DEBUG_ENABLED = enabled == true
end

---@return boolean
function Logger.GetDebugEnabled()
return DEBUG_ENABLED
end

return {
Logger = Logger
}
30 changes: 30 additions & 0 deletions types/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,36 @@
---@field public LoadClipSet fun(clipSet: string, timeout: number?)
---@field public LoadScene fun(pos: vector3 | {x: number, y: number, z: number}, offset: vector3 | {x: number, y: number, z: number}, radius: number, p7: integer)

---@class RAYCAST_RESULT
---@field public hit boolean
---@field public state integer
---@field public handle integer
---@field public didHit integer
---@field public coords vector3
---@field public normal vector3
---@field public entity integer
---@field public material integer?

---@class RAYCAST
---@field public Flags {World: integer, Vehicles: integer, Peds: integer, Ragdolls: integer, Objects: integer, Pickups: integer, Glass: integer, Rivers: integer, Foliage: integer, All: integer}
---@field public Cast fun(startCoords: vector3 | {x: number, y: number, z: number}, endCoords: vector3 | {x: number, y: number, z: number}, flags: integer?, ignoreEntity: integer?, options: {traceType?: integer, timeout?: integer, wait?: integer}?): RAYCAST_RESULT
---@field public FromCamera fun(distance: number?, flags: integer?, ignoreEntity: integer?, options: {offset?: vector3 | {x: number, y: number, z: number}, traceType?: integer, timeout?: integer, wait?: integer}?): RAYCAST_RESULT
---@field public FromEntity fun(entity: integer, distance: number?, flags: integer?, ignoreEntity: integer?, options: {offset?: vector3 | {x: number, y: number, z: number}, traceType?: integer, timeout?: integer, wait?: integer}?): RAYCAST_RESULT

---@class LOGGER_OPTIONS
---@field public resource string?
---@field public prefix string?
---@field public debug boolean?
---@field public colorize boolean?

---@class LOGGER
---@field public Log fun(level: string, message: any, context: table<string, any>?, options: LOGGER_OPTIONS?)
---@field public Info fun(message: any, context: table<string, any>?, options: LOGGER_OPTIONS?)
---@field public Warn fun(message: any, context: table<string, any>?, options: LOGGER_OPTIONS?)
---@field public Error fun(message: any, context: table<string, any>?, options: LOGGER_OPTIONS?)
---@field public Debug fun(message: any, context: table<string, any>?, options: LOGGER_OPTIONS?)
---@field public SetDebugEnabled fun(enabled: boolean)
---@field public GetDebugEnabled fun(): boolean

---@class MAP
---@field public New fun(self:MAP, handle:number):Blip
Expand Down