2025-06-07 08:51:21 +02:00
|
|
|
--[[
|
|
|
|
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 © 2025 Linden <https://github.com/thelindat>
|
|
|
|
]]
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2025-07-09 19:26:17 +02:00
|
|
|
local exitingIndex = exitingZones:indexOf(zone)
|
|
|
|
if exitingIndex then
|
|
|
|
table.remove(exitingZones, exitingIndex)
|
|
|
|
end
|
|
|
|
|
|
|
|
local enteringIndex = enteringZones:indexOf(zone)
|
|
|
|
if enteringIndex then
|
|
|
|
table.remove(enteringZones, enteringIndex)
|
|
|
|
end
|
2025-06-07 08:51:21 +02:00
|
|
|
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
|