--[[ This file and module was made by Linden and all overextended contributors. You can find the ox_lib repository here: https://github.com/overextended/ox_lib This file is licensed under LGPL-3.0 or higher Copyright © 2025 Linden Major Thank you to them for providing an amazing zoning and grid system system. The only changes made to this file will be to make the structure work in line with our library and will comment out the original code to show what has been changed. all lib. calls will be ps. calls. some instances where where table:push() was used have been changed to table.insert() to work with the current library structure. ]] --- GRID: --[[ Based on PolyZone's grid system (https://github.com/mkafrin/PolyZone/blob/master/ComboZone.lua) MIT License Copyright © 2019-2021 Michael Afrin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local mapMinX = -3700 local mapMinY = -4400 local mapMaxX = 4500 local mapMaxY = 8000 local xDelta = (mapMaxX - mapMinX) / 34 local yDelta = (mapMaxY - mapMinY) / 50 local grid = {} local lastCell = {} local gridCache = {} local entrySet = {} ps.grid = {} ---@class GridEntry ---@field coords vector ---@field length? number ---@field width? number ---@field radius? number ---@field [string] any ---@param point vector ---@param length number ---@param width number ---@return number, number, number, number local function getGridDimensions(point, length, width) local minX = (point.x - width - mapMinX) // xDelta local maxX = (point.x + width - mapMinX) // xDelta local minY = (point.y - length - mapMinY) // yDelta local maxY = (point.y + length - mapMinY) // yDelta return minX, maxX, minY, maxY end ---@param point vector ---@return number, number function ps.grid.getCellPosition(point) local x = (point.x - mapMinX) // xDelta local y = (point.y - mapMinY) // yDelta return x, y end ---@param point vector ---@return GridEntry[] function ps.grid.getCell(point) local x, y = ps.grid.getCellPosition(point) if lastCell.x ~= x or lastCell.y ~= y then lastCell.x = x lastCell.y = y lastCell.cell = grid[y] and grid[y][x] or {} end return lastCell.cell end ---@param point vector ---@param filter? fun(entry: GridEntry): boolean ---@return Array function ps.grid.getNearbyEntries(point, filter) local minX, maxX, minY, maxY = getGridDimensions(point, xDelta, yDelta) if gridCache.filter == filter and gridCache.minX == minX and gridCache.maxX == maxX and gridCache.minY == minY and gridCache.maxY == maxY then return gridCache.entries end local entries = {} local n = 0 table.wipe(entrySet) for y = minY, maxY do local row = grid[y] for x = minX, maxX do local cell = row and row[x] if cell then for j = 1, #cell do local entry = cell[j] if not entrySet[entry] and (not filter or filter(entry)) then n = n + 1 entrySet[entry] = true entries[n] = entry end end end end end gridCache.minX = minX gridCache.maxX = maxX gridCache.minY = minY gridCache.maxY = maxY gridCache.entries = entries gridCache.filter = filter return entries end ---@param entry { coords: vector, length?: number, width?: number, radius?: number, [string]: any } function ps.grid.addEntry(entry) entry.length = entry.length or entry.radius * 2 entry.width = entry.width or entry.radius * 2 local minX, maxX, minY, maxY = getGridDimensions(entry.coords, entry.length, entry.width) for y = minY, maxY do local row = grid[y] or {} for x = minX, maxX do local cell = row[x] or {} cell[#cell + 1] = entry row[x] = cell end grid[y] = row table.wipe(gridCache) end end ---@param entry table A table that was added to the grid previously. function ps.grid.removeEntry(entry) local minX, maxX, minY, maxY = getGridDimensions(entry.coords, entry.length, entry.width) local success = false for y = minY, maxY do local row = grid[y] if not row then goto continue end for x = minX, maxX do local cell = row[x] if cell then for i = 1, #cell do if cell[i] == entry then table.remove(cell, i) success = true break end end if #cell == 0 then row[x] = nil end end end if not next(row) then grid[y] = nil end ::continue:: end table.wipe(gridCache) return success end --- Zones: 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 {} --[[@as Array]] local enteringZones = {} -- lib.context == 'client' and {} --[[@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 ps.grid.removeEntry(zone) insideZones[zone.id] = nil table.remove(exitingZones, exitingZones:indexOf(zone)) table.remove(enteringZones, enteringZones:indexOf(zone)) end CreateThread(function() while true do local coords = GetEntityCoords(PlayerPedId()) local zones = ps.grid.getNearbyEntries(coords, function(entry) return entry.remove == removeZone end) --[[@as Array]] local cellX, cellY = ps.grid.getCellPosition(coords) local cache = ps.grid.getCell(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) table.insert(enteringZones, 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 table.insert(exitingZones, zone) --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 data.setDebug = setDebug if data.debug then data.debug = nil data:setDebug(true, data.debugColour) end Zones[data.id] = data ps.grid.addEntry(data) return data end ps.zones = {} ---@class PolyZone : ZoneProperties ---@field points vector3[] ---@field thickness? number ---@param data PolyZone ---@return CZone function ps.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 ps.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 ps.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 ps.zones.getAllZones() return Zones end function ps.zones.getCurrentZones() return insideZones end function ps.zones.getNearbyZones() return nearbyZones end