--[[ https://github.com/overextended/ox_lib This file is licensed under LGPL-3.0 or higher Copyright © 2025 Linden ]] 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 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]] local exitingZones = lib.context == 'client' and lib.array:new() --[[@as Array]] local enteringZones = lib.context == 'client' and lib.array:new() --[[@as Array]] local nearbyZones = lib.array:new() --[[@as Array]] 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]] 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