mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-16 22:46:03 +01:00
Some people accidentally, mistakenly, and without any malicious intent forget to attribute code from this resource to its source. This change will hopefully assist people, so that they do not make that mistake again.
490 lines
14 KiB
Lua
490 lines
14 KiB
Lua
--[[
|
|
https://github.com/overextended/ox_lib
|
|
|
|
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
|
|
|
|
Copyright (c) 2025 Linden <https://github.com/thelindat/fivem>
|
|
]]
|
|
|
|
local glm = require 'glm'
|
|
|
|
---@class ZoneProperties
|
|
---@field debug? boolean
|
|
---@field debugColour? vector4
|
|
---@field onEnter fun(self: CZone)?
|
|
---@field onExit fun(self: CZone)?
|
|
---@field inside fun(self: CZone)?
|
|
---@field [string] any
|
|
|
|
---@class CZone : PolyZone, BoxZone, SphereZone
|
|
---@field id number
|
|
---@field __type 'poly' | 'sphere' | 'box'
|
|
---@field remove fun(self: self)
|
|
---@field setDebug fun(self: CZone, enable?: boolean, colour?: vector)
|
|
---@field contains fun(self: CZone, coords?: vector3, updateDistance?: boolean): boolean
|
|
|
|
---@type table<number, CZone>
|
|
local Zones = {}
|
|
_ENV.Zones = Zones
|
|
|
|
local function nextFreePoint(points, b, len)
|
|
for i = 1, len do
|
|
local n = (i + b) % len
|
|
|
|
n = n ~= 0 and n or len
|
|
|
|
if points[n] then
|
|
return n
|
|
end
|
|
end
|
|
end
|
|
|
|
local function unableToSplit(polygon)
|
|
print('The following polygon is malformed and has failed to be split into triangles for debug')
|
|
|
|
for k, v in pairs(polygon) do
|
|
print(k, v)
|
|
end
|
|
end
|
|
|
|
local function getTriangles(polygon)
|
|
local triangles = {}
|
|
|
|
if polygon:isConvex() then
|
|
for i = 2, #polygon - 1 do
|
|
triangles[#triangles + 1] = mat(polygon[1], polygon[i], polygon[i + 1])
|
|
end
|
|
|
|
return triangles
|
|
end
|
|
|
|
if not polygon:isSimple() then
|
|
unableToSplit(polygon)
|
|
|
|
return triangles
|
|
end
|
|
|
|
local points = {}
|
|
local polygonN = #polygon
|
|
|
|
for i = 1, polygonN do
|
|
points[i] = polygon[i]
|
|
end
|
|
|
|
local a, b, c = 1, 2, 3
|
|
local zValue = polygon[1].z
|
|
local count = 0
|
|
|
|
while polygonN - #triangles > 2 do
|
|
local a2d = polygon[a].xy
|
|
local c2d = polygon[c].xy
|
|
|
|
if polygon:containsSegment(vec3(glm.segment2d.getPoint(a2d, c2d, 0.01), zValue), vec3(glm.segment2d.getPoint(a2d, c2d, 0.99), zValue)) then
|
|
triangles[#triangles + 1] = mat(polygon[a], polygon[b], polygon[c])
|
|
points[b] = false
|
|
|
|
b = c
|
|
c = nextFreePoint(points, b, polygonN)
|
|
else
|
|
a = b
|
|
b = c
|
|
c = nextFreePoint(points, b, polygonN)
|
|
end
|
|
|
|
count += 1
|
|
|
|
if count > polygonN and #triangles == 0 then
|
|
unableToSplit(polygon)
|
|
|
|
return triangles
|
|
end
|
|
|
|
Wait(0)
|
|
end
|
|
|
|
return triangles
|
|
end
|
|
|
|
local insideZones = lib.context == 'client' and {} --[[@as table<number, CZone>]]
|
|
local exitingZones = lib.context == 'client' and lib.array:new() --[[@as Array<CZone>]]
|
|
local enteringZones = lib.context == 'client' and lib.array:new() --[[@as Array<CZone>]]
|
|
local nearbyZones = lib.array:new() --[[@as Array<CZone>]]
|
|
local glm_polygon_contains = glm.polygon.contains
|
|
local tick
|
|
|
|
---@param zone CZone
|
|
local function removeZone(zone)
|
|
Zones[zone.id] = nil
|
|
|
|
lib.grid.removeEntry(zone)
|
|
|
|
if lib.context == 'server' then return end
|
|
|
|
insideZones[zone.id] = nil
|
|
|
|
table.remove(exitingZones, exitingZones:indexOf(zone))
|
|
table.remove(enteringZones, enteringZones:indexOf(zone))
|
|
end
|
|
|
|
CreateThread(function()
|
|
if lib.context == 'server' then return end
|
|
|
|
while true do
|
|
local coords = GetEntityCoords(cache.ped)
|
|
local zones = lib.grid.getNearbyEntries(coords, function(entry) return entry.remove == removeZone end) --[[@as Array<CZone>]]
|
|
local cellX, cellY = lib.grid.getCellPosition(coords)
|
|
cache.coords = coords
|
|
|
|
if cellX ~= cache.lastCellX or cellY ~= cache.lastCellY then
|
|
for i = 1, #nearbyZones do
|
|
local zone = nearbyZones[i]
|
|
|
|
if zone.insideZone then
|
|
local contains = zone:contains(coords, true)
|
|
|
|
if not contains then
|
|
zone.insideZone = false
|
|
insideZones[zone.id] = nil
|
|
|
|
if zone.onExit then
|
|
exitingZones:push(zone)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
cache.lastCellX = cellX
|
|
cache.lastCellY = cellY
|
|
end
|
|
|
|
nearbyZones = zones
|
|
|
|
for i = 1, #zones do
|
|
local zone = zones[i]
|
|
local contains = zone:contains(coords, true)
|
|
|
|
if contains then
|
|
if not zone.insideZone then
|
|
zone.insideZone = true
|
|
|
|
if zone.onEnter then
|
|
enteringZones:push(zone)
|
|
end
|
|
|
|
if zone.inside or zone.debug then
|
|
insideZones[zone.id] = zone
|
|
end
|
|
end
|
|
else
|
|
if zone.insideZone then
|
|
zone.insideZone = false
|
|
insideZones[zone.id] = nil
|
|
|
|
if zone.onExit then
|
|
exitingZones:push(zone)
|
|
end
|
|
end
|
|
|
|
if zone.debug then
|
|
insideZones[zone.id] = zone
|
|
end
|
|
end
|
|
end
|
|
|
|
local exitingSize = #exitingZones
|
|
local enteringSize = #enteringZones
|
|
|
|
if exitingSize > 0 then
|
|
table.sort(exitingZones, function(a, b)
|
|
return a.distance < b.distance
|
|
end)
|
|
|
|
for i = exitingSize, 1, -1 do
|
|
exitingZones[i]:onExit()
|
|
end
|
|
|
|
table.wipe(exitingZones)
|
|
end
|
|
|
|
if enteringSize > 0 then
|
|
table.sort(enteringZones, function(a, b)
|
|
return a.distance < b.distance
|
|
end)
|
|
|
|
for i = 1, enteringSize do
|
|
enteringZones[i]:onEnter()
|
|
end
|
|
|
|
table.wipe(enteringZones)
|
|
end
|
|
|
|
if not tick then
|
|
if next(insideZones) then
|
|
tick = SetInterval(function()
|
|
for _, zone in pairs(insideZones) do
|
|
if zone.debug then
|
|
zone:debug()
|
|
|
|
if zone.inside and zone.insideZone then
|
|
zone:inside()
|
|
end
|
|
else
|
|
zone:inside()
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
elseif not next(insideZones) then
|
|
tick = ClearInterval(tick)
|
|
end
|
|
|
|
Wait(300)
|
|
end
|
|
end)
|
|
|
|
local DrawLine = DrawLine
|
|
local DrawPoly = DrawPoly
|
|
|
|
local function debugPoly(self)
|
|
for i = 1, #self.triangles do
|
|
local triangle = self.triangles[i]
|
|
DrawPoly(triangle[1].x, triangle[1].y, triangle[1].z, triangle[2].x, triangle[2].y, triangle[2].z, triangle[3].x, triangle[3].y, triangle[3].z,
|
|
self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
|
DrawPoly(triangle[2].x, triangle[2].y, triangle[2].z, triangle[1].x, triangle[1].y, triangle[1].z, triangle[3].x, triangle[3].y, triangle[3].z,
|
|
self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
|
end
|
|
for i = 1, #self.polygon do
|
|
local thickness = vec(0, 0, self.thickness / 2)
|
|
local a = self.polygon[i] + thickness
|
|
local b = self.polygon[i] - thickness
|
|
local c = (self.polygon[i + 1] or self.polygon[1]) + thickness
|
|
local d = (self.polygon[i + 1] or self.polygon[1]) - thickness
|
|
DrawLine(a.x, a.y, a.z, b.x, b.y, b.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, 225)
|
|
DrawLine(a.x, a.y, a.z, c.x, c.y, c.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, 225)
|
|
DrawLine(b.x, b.y, b.z, d.x, d.y, d.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, 225)
|
|
DrawPoly(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
|
DrawPoly(c.x, c.y, c.z, b.x, b.y, b.z, a.x, a.y, a.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
|
DrawPoly(b.x, b.y, b.z, c.x, c.y, c.z, d.x, d.y, d.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
|
DrawPoly(d.x, d.y, d.z, c.x, c.y, c.z, b.x, b.y, b.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
|
end
|
|
end
|
|
|
|
local function debugSphere(self)
|
|
DrawMarker(28, self.coords.x, self.coords.y, self.coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, self.radius, self.radius, self.radius, self.debugColour.r,
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
self.debugColour.g, self.debugColour.b, self.debugColour.a, false, false, 0, false, false, false, false)
|
|
end
|
|
|
|
local function contains(self, coords, updateDistance)
|
|
if updateDistance then self.distance = #(self.coords - coords) end
|
|
|
|
return glm_polygon_contains(self.polygon, coords, self.thickness / 4)
|
|
end
|
|
|
|
local function insideSphere(self, coords, updateDistance)
|
|
local distance = #(self.coords - coords)
|
|
|
|
if updateDistance then self.distance = distance end
|
|
|
|
return distance < self.radius
|
|
end
|
|
|
|
local function convertToVector(coords)
|
|
local _type = type(coords)
|
|
|
|
if _type ~= 'vector3' then
|
|
if _type == 'table' or _type == 'vector4' then
|
|
return vec3(coords[1] or coords.x, coords[2] or coords.y, coords[3] or coords.z)
|
|
end
|
|
|
|
error(("expected type 'vector3' or 'table' (received %s)"):format(_type))
|
|
end
|
|
|
|
return coords
|
|
end
|
|
|
|
local function setDebug(self, bool, colour)
|
|
if not bool and insideZones[self.id] then
|
|
insideZones[self.id] = nil
|
|
end
|
|
|
|
self.debugColour = bool and
|
|
{
|
|
r = glm.tointeger(colour?.r or self.debugColour?.r or 255),
|
|
g = glm.tointeger(colour?.g or self.debugColour?.g or 42),
|
|
b = glm.tointeger(colour?.b or
|
|
self.debugColour?.b or 24),
|
|
a = glm.tointeger(colour?.a or self.debugColour?.a or 100)
|
|
} or nil
|
|
|
|
if not bool and self.debug then
|
|
self.triangles = nil
|
|
self.debug = nil
|
|
return
|
|
end
|
|
|
|
if bool and self.debug and self.debug ~= true then return end
|
|
|
|
self.triangles = self.__type == 'poly' and getTriangles(self.polygon) or
|
|
self.__type == 'box' and { mat(self.polygon[1], self.polygon[2], self.polygon[3]), mat(self.polygon[1], self.polygon[3], self.polygon[4]) } or nil
|
|
self.debug = self.__type == 'sphere' and debugSphere or debugPoly or nil
|
|
end
|
|
|
|
---@param data ZoneProperties
|
|
---@return CZone
|
|
local function setZone(data)
|
|
---@cast data CZone
|
|
data.remove = removeZone
|
|
data.contains = data.contains or contains
|
|
|
|
if lib.context == 'client' then
|
|
data.setDebug = setDebug
|
|
|
|
if data.debug then
|
|
data.debug = nil
|
|
|
|
data:setDebug(true, data.debugColour)
|
|
end
|
|
else
|
|
data.debug = nil
|
|
end
|
|
|
|
Zones[data.id] = data
|
|
lib.grid.addEntry(data)
|
|
|
|
return data
|
|
end
|
|
|
|
lib.zones = {}
|
|
|
|
---@class PolyZone : ZoneProperties
|
|
---@field points vector3[]
|
|
---@field thickness? number
|
|
|
|
---@param data PolyZone
|
|
---@return CZone
|
|
function lib.zones.poly(data)
|
|
data.id = #Zones + 1
|
|
data.thickness = data.thickness or 4
|
|
|
|
local pointN = #data.points
|
|
local points = table.create(pointN, 0)
|
|
|
|
for i = 1, pointN do
|
|
points[i] = convertToVector(data.points[i])
|
|
end
|
|
|
|
data.polygon = glm.polygon.new(points)
|
|
|
|
if not data.polygon:isPlanar() then
|
|
local zCoords = {}
|
|
|
|
for i = 1, pointN do
|
|
local zCoord = points[i].z
|
|
|
|
if zCoords[zCoord] then
|
|
zCoords[zCoord] += 1
|
|
else
|
|
zCoords[zCoord] = 1
|
|
end
|
|
end
|
|
|
|
local coordsArray = {}
|
|
|
|
for coord, count in pairs(zCoords) do
|
|
coordsArray[#coordsArray + 1] = {
|
|
coord = coord,
|
|
count = count
|
|
}
|
|
end
|
|
|
|
table.sort(coordsArray, function(a, b)
|
|
return a.count > b.count
|
|
end)
|
|
|
|
local zCoord = coordsArray[1].coord
|
|
local averageTo = 1
|
|
|
|
for i = 1, #coordsArray do
|
|
if coordsArray[i].count < coordsArray[1].count then
|
|
averageTo = i - 1
|
|
break
|
|
end
|
|
end
|
|
|
|
if averageTo > 1 then
|
|
for i = 2, averageTo do
|
|
zCoord += coordsArray[i].coord
|
|
end
|
|
|
|
zCoord /= averageTo
|
|
end
|
|
|
|
for i = 1, pointN do
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
points[i] = vec3(data.points[i].xy, zCoord)
|
|
end
|
|
|
|
data.polygon = glm.polygon.new(points)
|
|
end
|
|
|
|
data.coords = data.polygon:centroid()
|
|
data.__type = 'poly'
|
|
data.radius = lib.array.reduce(data.polygon, function(acc, point)
|
|
local distance = #(point - data.coords)
|
|
return distance > acc and distance or acc
|
|
end, 0)
|
|
|
|
return setZone(data)
|
|
end
|
|
|
|
---@class BoxZone : ZoneProperties
|
|
---@field coords vector3
|
|
---@field size? vector3
|
|
---@field rotation? number | vector3 | vector4 | matrix
|
|
|
|
---@param data BoxZone
|
|
---@return CZone
|
|
function lib.zones.box(data)
|
|
data.id = #Zones + 1
|
|
data.coords = convertToVector(data.coords)
|
|
data.size = data.size and convertToVector(data.size) / 2 or vec3(2)
|
|
data.thickness = data.size.z * 2
|
|
data.rotation = quat(data.rotation or 0, vec3(0, 0, 1))
|
|
data.__type = 'box'
|
|
data.width = data.size.x * 2
|
|
data.length = data.size.y * 2
|
|
data.polygon = (data.rotation * glm.polygon.new({
|
|
vec3(data.size.x, data.size.y, 0),
|
|
vec3(-data.size.x, data.size.y, 0),
|
|
vec3(-data.size.x, -data.size.y, 0),
|
|
vec3(data.size.x, -data.size.y, 0),
|
|
}) + data.coords)
|
|
|
|
return setZone(data)
|
|
end
|
|
|
|
---@class SphereZone : ZoneProperties
|
|
---@field coords vector3
|
|
---@field radius? number
|
|
|
|
---@param data SphereZone
|
|
---@return CZone
|
|
function lib.zones.sphere(data)
|
|
data.id = #Zones + 1
|
|
data.coords = convertToVector(data.coords)
|
|
data.radius = (data.radius or 2) + 0.0
|
|
data.__type = 'sphere'
|
|
data.contains = insideSphere
|
|
|
|
return setZone(data)
|
|
end
|
|
|
|
function lib.zones.getAllZones() return Zones end
|
|
|
|
function lib.zones.getCurrentZones() return insideZones end
|
|
|
|
function lib.zones.getNearbyZones() return nearbyZones end
|
|
|
|
return lib.zones
|