496 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			496 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 © 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
 | |
| 
 | |
|     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()
 | |
|     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
 | 
