Files
ox_lib/imports/zones/shared.lua

497 lines
14 KiB
Lua
Raw Normal View History

--[[
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>
2025-03-29 21:44:03 +11:00
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local glm = require 'glm'
2025-03-10 12:02:45 +11:00
---@class ZoneProperties
---@field debug? boolean
---@field debugColour? vector4
---@field onEnter fun(self: CZone)?
---@field onExit fun(self: CZone)?
---@field inside fun(self: CZone)?
2022-10-02 20:19:35 +11:00
---@field [string] any
2025-03-10 12:02:45 +11:00
---@class CZone : PolyZone, BoxZone, SphereZone
---@field id number
---@field __type 'poly' | 'sphere' | 'box'
---@field remove fun(self: self)
2025-03-10 12:02:45 +11:00
---@field setDebug fun(self: CZone, enable?: boolean, colour?: vector)
2025-03-14 10:02:37 +11:00
---@field contains fun(self: CZone, coords?: vector3, updateDistance?: boolean): boolean
2025-03-10 12:02:45 +11:00
2023-04-20 15:52:55 +10:00
---@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
2025-03-10 11:52:20 +11:00
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>]]
2025-03-13 19:31:21 +11:00
local nearbyZones = lib.array:new() --[[@as Array<CZone>]]
local glm_polygon_contains = glm.polygon.contains
2025-03-10 11:52:20 +11:00
local tick
---@param zone CZone
local function removeZone(zone)
Zones[zone.id] = nil
2025-03-10 11:52:20 +11:00
lib.grid.removeEntry(zone)
2025-03-10 11:52:20 +11:00
if lib.context == 'server' then return end
insideZones[zone.id] = nil
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
end
CreateThread(function()
2025-03-10 11:52:20 +11:00
if lib.context == 'server' then return end
while true do
local coords = GetEntityCoords(cache.ped)
2025-03-13 19:31:21 +11:00
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
2025-03-13 19:31:21 +11:00
for i = 1, #nearbyZones do
local zone = nearbyZones[i]
if zone.insideZone then
2025-03-14 10:02:37 +11:00
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]
2025-03-14 10:02:37 +11:00
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
2025-03-14 10:02:37 +11:00
local function insideSphere(self, coords, updateDistance)
local distance = #(self.coords - coords)
2025-03-14 10:02:37 +11:00
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
2025-03-10 12:02:45 +11:00
---@param data ZoneProperties
2025-03-10 11:52:20 +11:00
---@return CZone
local function setZone(data)
2025-03-10 12:02:45 +11:00
---@cast data CZone
2025-03-10 11:52:20 +11:00
data.remove = removeZone
data.contains = data.contains or contains
2025-03-10 11:52:20 +11:00
if lib.context == 'client' then
data.setDebug = setDebug
2025-03-10 11:52:20 +11:00
if data.debug then
data.debug = nil
2025-03-10 11:52:20 +11:00
data:setDebug(true, data.debugColour)
end
else
data.debug = nil
end
2025-03-10 11:52:20 +11:00
Zones[data.id] = data
lib.grid.addEntry(data)
2025-03-10 11:52:20 +11:00
return data
end
2025-03-10 11:52:20 +11:00
lib.zones = {}
2025-03-10 12:02:45 +11:00
---@class PolyZone : ZoneProperties
---@field points vector3[]
---@field thickness? number
2025-03-10 12:02:45 +11:00
---@param data PolyZone
---@return CZone
2025-03-10 11:52:20 +11:00
function lib.zones.poly(data)
data.id = #Zones + 1
data.thickness = data.thickness or 4
2025-03-10 11:52:20 +11:00
local pointN = #data.points
local points = table.create(pointN, 0)
2025-03-10 11:52:20 +11:00
for i = 1, pointN do
points[i] = convertToVector(data.points[i])
end
2025-03-10 11:52:20 +11:00
data.polygon = glm.polygon.new(points)
2025-03-10 11:52:20 +11:00
if not data.polygon:isPlanar() then
local zCoords = {}
2025-03-10 11:52:20 +11:00
for i = 1, pointN do
local zCoord = points[i].z
2025-03-10 11:52:20 +11:00
if zCoords[zCoord] then
zCoords[zCoord] += 1
else
zCoords[zCoord] = 1
end
2025-03-10 11:52:20 +11:00
end
2025-03-10 11:52:20 +11:00
local coordsArray = {}
2025-03-10 11:52:20 +11:00
for coord, count in pairs(zCoords) do
coordsArray[#coordsArray + 1] = {
coord = coord,
count = count
}
end
2025-03-10 11:52:20 +11:00
table.sort(coordsArray, function(a, b)
return a.count > b.count
end)
2025-03-10 11:52:20 +11:00
local zCoord = coordsArray[1].coord
local averageTo = 1
2025-03-10 11:52:20 +11:00
for i = 1, #coordsArray do
if coordsArray[i].count < coordsArray[1].count then
averageTo = i - 1
break
end
end
2025-03-10 11:52:20 +11:00
if averageTo > 1 then
for i = 2, averageTo do
zCoord += coordsArray[i].coord
end
2025-03-10 11:52:20 +11:00
zCoord /= averageTo
end
2025-03-10 11:52:20 +11:00
for i = 1, pointN do
2025-03-10 12:02:45 +11:00
---@diagnostic disable-next-line: param-type-mismatch
2025-03-10 11:52:20 +11:00
points[i] = vec3(data.points[i].xy, zCoord)
end
2025-03-10 11:52:20 +11:00
data.polygon = glm.polygon.new(points)
end
2025-03-10 11:52:20 +11:00
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)
2025-03-10 11:52:20 +11:00
return setZone(data)
end
2025-03-10 12:02:45 +11:00
---@class BoxZone : ZoneProperties
---@field coords vector3
---@field size? vector3
---@field rotation? number | vector3 | vector4 | matrix
2025-03-10 12:02:45 +11:00
---@param data BoxZone
---@return CZone
2025-03-10 11:52:20 +11:00
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
2025-03-10 11:52:20 +11:00
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
2025-03-10 11:52:20 +11:00
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
2025-03-10 12:02:45 +11:00
---@class SphereZone : ZoneProperties
---@field coords vector3
2025-03-10 12:02:45 +11:00
---@field radius? number
---@param data SphereZone
---@return CZone
2025-03-10 11:52:20 +11:00
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
2023-04-20 15:52:55 +10:00
2025-03-10 11:52:20 +11:00
function lib.zones.getAllZones() return Zones end
2023-04-20 15:52:55 +10:00
2025-03-10 11:52:20 +11:00
function lib.zones.getCurrentZones() return insideZones end
2025-03-13 19:31:21 +11:00
function lib.zones.getNearbyZones() return nearbyZones end
return lib.zones