From e947a26982004309b43bab437d065422085f6432 Mon Sep 17 00:00:00 2001 From: SNAILX Date: Tue, 4 Nov 2025 02:15:22 +0100 Subject: [PATCH 1/2] Add points and zones import modules (files) --- [core]/esx_lib/imports/points/client.lua | 0 [core]/esx_lib/imports/zones/shared.lua | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 [core]/esx_lib/imports/points/client.lua create mode 100644 [core]/esx_lib/imports/zones/shared.lua diff --git a/[core]/esx_lib/imports/points/client.lua b/[core]/esx_lib/imports/points/client.lua new file mode 100644 index 000000000..e69de29bb diff --git a/[core]/esx_lib/imports/zones/shared.lua b/[core]/esx_lib/imports/zones/shared.lua new file mode 100644 index 000000000..e69de29bb From ce0c40e36e806866ebb0ec7a7928eee496a303dd Mon Sep 17 00:00:00 2001 From: SNAILX Date: Fri, 7 Nov 2025 03:03:49 +0100 Subject: [PATCH 2/2] Add grid, points, and zones modules, extend table utilities --- [core]/esx_lib/imports/grid/shared.lua | 194 +++++++++ [core]/esx_lib/imports/points/client.lua | 187 +++++++++ [core]/esx_lib/imports/table/shared.lua | 354 ++++++++++++++++ [core]/esx_lib/imports/zones/shared.lua | 489 +++++++++++++++++++++++ 4 files changed, 1224 insertions(+) create mode 100644 [core]/esx_lib/imports/grid/shared.lua diff --git a/[core]/esx_lib/imports/grid/shared.lua b/[core]/esx_lib/imports/grid/shared.lua new file mode 100644 index 000000000..375bb1fb8 --- /dev/null +++ b/[core]/esx_lib/imports/grid/shared.lua @@ -0,0 +1,194 @@ +--[[ + 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 = {} + +xLib.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 xLib.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 xLib.grid.getCell(point) + local x, y = xLib.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 xLib.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 = xLib.table.array:new() + 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 xLib.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 xLib.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 + +return xLib.grid \ No newline at end of file diff --git a/[core]/esx_lib/imports/points/client.lua b/[core]/esx_lib/imports/points/client.lua index e69de29bb..25b08b7c5 100644 --- a/[core]/esx_lib/imports/points/client.lua +++ b/[core]/esx_lib/imports/points/client.lua @@ -0,0 +1,187 @@ +--!DISCLAIMER +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---@class PointProperties +---@field coords vector3 +---@field distance number +---@field onEnter? fun(self: CPoint) +---@field onExit? fun(self: CPoint) +---@field nearby? fun(self: CPoint) +---@field [string] any + +---@class CPoint : PointProperties +---@field id number +---@field currentDistance number +---@field isClosest? boolean +---@field remove fun() + +---@type table +local points = {} +---@type CPoint[] +local nearbyPoints = {} +local nearbyCount = 0 +---@type CPoint? +local closestPoint +local tick + +local function removePoint(self) + if closestPoint?.id == self.id then + closestPoint = nil + end + + xLib.grid.removeEntry(self) + + points[self.id] = nil +end + +CreateThread(function() + while true do + local coords = GetEntityCoords(PlayerPedId()) + local newPoints = xLib.grid.getNearbyEntries(coords, function(entry) return entry.remove == removePoint end) --[[@as CPoint[] ]] + local cellX, cellY = xLib.grid.getCellPosition(coords) + closestPoint = nil + + for i = 1, nearbyCount do + local point = nearbyPoints[i] + + if point.inside then + local distance = #(coords - point.coords) + + if distance > point.radius then + if point.onExit then point:onExit() end + + point.inside = nil + point.currentDistance = nil + end + end + end + + if nearbyCount ~= 0 then + table.wipe(nearbyPoints) + nearbyCount = 0 + end + + for i = 1, #newPoints do + local point = newPoints[i] + local distance = #(coords - point.coords) + + if distance <= point.radius then + point.currentDistance = distance + + if not closestPoint or distance < (closestPoint.currentDistance or point.radius) then + if closestPoint then closestPoint.isClosest = nil end + + point.isClosest = true + closestPoint = point + end + + nearbyCount += 1 + nearbyPoints[nearbyCount] = point + + if point.onEnter and not point.inside then + point.inside = true + point:onEnter() + end + elseif point.currentDistance then + if point.onExit then point:onExit() end + + point.inside = nil + point.currentDistance = nil + end + end + + if not tick then + if nearbyCount ~= 0 then + tick = true + CreateThread(function() + while tick do + for i = nearbyCount, 1, -1 do + local point = nearbyPoints[i] + + if point and point.nearby then + point:nearby() + end + end + Wait(0) + end + end) + end + elseif nearbyCount == 0 then + tick = false + end + + Wait(300) + end +end) + +local function toVector(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 + +xLib.points = {} + +---@return CPoint +---@overload fun(data: PointProperties): CPoint +---@overload fun(coords: vector3, distance: number, data?: PointProperties): CPoint +function xLib.points.new(...) + local args = { ... } + local id = #points + 1 + local self + + -- Support sending a single argument containing point data + if type(args[1]) == 'table' then + self = args[1] + self.id = id + self.remove = removePoint + else + -- Backwards compatibility for original implementation (args: coords, distance, data) + self = { + id = id, + coords = args[1], + remove = removePoint, + } + end + + self.coords = toVector(self.coords) + self.distance = self.distance or args[2] + self.radius = self.distance + + if args[3] then + for k, v in pairs(args[3]) do + self[k] = v + end + end + + xLib.grid.addEntry(self) + points[id] = self + + return self +end + +function xLib.points.getAllPoints() return points end + +function xLib.points.getNearbyPoints() return nearbyPoints end + +---@return CPoint? +function xLib.points.getClosestPoint() return closestPoint end + +---@deprecated +xLib.points.closest = xLib.points.getClosestPoint + +return xLib.points \ No newline at end of file diff --git a/[core]/esx_lib/imports/table/shared.lua b/[core]/esx_lib/imports/table/shared.lua index c35507177..238bcdce8 100644 --- a/[core]/esx_lib/imports/table/shared.lua +++ b/[core]/esx_lib/imports/table/shared.lua @@ -1,6 +1,15 @@ ---@class tablelib xLib.table = table +---@class Array: table +xLib.table.array = xLib.class() + +local table_unpack = table.unpack +local table_remove = table.remove +local table_clone = table.clone +local table_concat = table.concat +local table_type = table.type + ---@param tbl table ---@return boolean function xLib.table.isArray(tbl) @@ -130,4 +139,349 @@ function xLib.table.dump(tbl) end end +--- https://github.com/overextended/ox_lib/blob/master/imports/array/shared.lua +---@private +function xLib.table.array:constructor(...) + local arr = { ... } + + for i = 1, #arr do + self[i] = arr[i] + end +end + +---@private +function xLib.table.array:__newindex(index, value) + if type(index) ~= 'number' then error(("Cannot insert non-number index '%s' into an array."):format(index)) end + + rawset(self, index, value) +end + +---Creates a new array from an iteratable value. +---@param iter table | function | string +---@return Array +function xLib.table.array:from(iter) + local iterType = type(iter) + + if iterType == 'table' then + return xLib.table.array:new(table_unpack(iter)) + end + + if iterType == 'string' then + return xLib.table.array:new(string.strsplit('', iter)) + end + + if iterType == 'function' then + local arr = xLib.table.array:new() + local length = 0 + + for value in iter do + length += 1 + arr[length] = value + end + + return arr + end + + error(('Array.from argument was not a valid iterable value (received %s)'):format(iterType)) +end + +---Returns the element at the given index, with negative numbers counting backwards from the end of the array. +---@param index number +---@return unknown +function xLib.table.array:at(index) + if index < 0 then + index = #self + index + 1 + end + + return self[index] +end + +---Create a new array containing the elements of two or more arrays. +---@param ... ArrayLike +function xLib.table.array:merge(...) + local newArr = table_clone(self) + local length = #self + local arrays = { ... } + + for i = 1, #arrays do + local arr = arrays[i] + + for j = 1, #arr do + length += 1 + newArr[length] = arr[j] + end + end + + return xLib.table.array:new(table_unpack(newArr)) +end + +---Tests if all elements in an array succeed in passing the provided test function. +---@param testFn fun(element: unknown): boolean +function xLib.table.array:every(testFn) + for i = 1, #self do + if not testFn(self[i]) then + return false + end + end + + return true +end + +---Sets all elements within a range to the given value and returns the modified array. +---@param value any +---@param start? number +---@param endIndex? number +function xLib.table.array:fill(value, start, endIndex) + local length = #self + start = start or 1 + endIndex = endIndex or length + + if start < 1 then start = 1 end + if endIndex > length then endIndex = length end + + for i = start, endIndex do + self[i] = value + end + + return self +end + +---Creates a new array containing the elements from an array that pass the test of the provided function. +---@param testFn fun(element: unknown): boolean +function xLib.table.array:filter(testFn) + local newArr = {} + local length = 0 + + for i = 1, #self do + local element = self[i] + + if testFn(element) then + length += 1 + newArr[length] = element + end + end + + return xLib.table.array:new(table_unpack(newArr)) +end + +---Returns the first or last element of an array that passes the provided test function. +---@param testFn fun(element: unknown): boolean +---@param last? boolean +function xLib.table.array:find(testFn, last) + local a = last and #self or 1 + local b = last and 1 or #self + local c = last and -1 or 1 + + for i = a, b, c do + local element = self[i] + + if testFn(element) then + return element + end + end +end + +---Returns the first or last index of the first element of an array that passes the provided test function. +---@param testFn fun(element: unknown): boolean +---@param last? boolean +function xLib.table.array:findIndex(testFn, last) + local a = last and #self or 1 + local b = last and 1 or #self + local c = last and -1 or 1 + + for i = a, b, c do + local element = self[i] + + if testFn(element) then + return i + end + end +end + +---Returns the first or last index of the first element of an array that matches the provided value. +---@param value unknown +---@param last? boolean +function xLib.table.array:indexOf(value, last) + local a = last and #self or 1 + local b = last and 1 or #self + local c = last and -1 or 1 + + for i = a, b, c do + local element = self[i] + + if element == value then + return i + end + end +end + +---Executes the provided function for each element in an array. +---@param cb fun(element: unknown) +function xLib.table.array:forEach(cb) + for i = 1, #self do + cb(self[i]) + end +end + +---Determines if a given element exists inside an array. +---@param element unknown The value to find in the array. +---@param fromIndex? number The position in the array to begin searching from. +function xLib.table.array:includes(element, fromIndex) + for i = (fromIndex or 1), #self do + if self[i] == element then return true end + end + + return false +end + +---Concatenates all array elements into a string, seperated by commas or the specified seperator. +---@param seperator? string +function xLib.table.array:join(seperator) + return table_concat(self, seperator or ',') +end + +---Create a new array containing the results from calling the provided function on every element in an array. +---@param cb fun(element: unknown, index: number, array: self): unknown +function xLib.table.array:map(cb) + local arr = {} + + for i = 1, #self do + arr[i] = cb(self[i], i, self) + end + + return xLib.table.array:new(table_unpack(arr)) +end + +---Removes the last element from an array and returns the removed element. +function xLib.table.array:pop() + return table_remove(self) +end + +---Adds the given elements to the end of an array and returns the new array length. +---@param ... any +function xLib.table.array:push(...) + local elements = { ... } + local length = #self + + for i = 1, #elements do + length += 1 + self[length] = elements[i] + end + + return length +end + +---The "reducer" function is applied to every element within an array, with the previous element's result serving as the accumulator. +---If an initial value is provided, it's used as the accumulator for index 1; otherwise, index 1 itself serves as the initial value, and iteration begins from index 2. +---@generic T +---@param reducer fun(accumulator: T, currentValue: T, index?: number): T +---@param initialValue? T +---@param reverse? boolean Iterate over the array from right-to-left. +---@return T +function xLib.table.array:reduce(reducer, initialValue, reverse) + local length = #self + local initialIndex = initialValue and 1 or 2 + local accumulator = initialValue or self[1] + + if reverse then + for i = initialIndex, length do + local index = length - i + initialIndex + accumulator = reducer(accumulator, self[index], index) + end + else + for i = initialIndex, length do + accumulator = reducer(accumulator, self[i], i) + end + end + + return accumulator +end + +---Reverses the elements inside an array. +function xLib.table.array:reverse() + local i, j = 1, #self + + while i < j do + self[i], self[j] = self[j], self[i] + i += 1 + j -= 1 + end + + return self +end + +---Removes the first element from an array and returns the removed element. +function xLib.table.array:shift() + return table_remove(self, 1) +end + +---Creates a shallow copy of a portion of an array as a new array. +---@param start? number +---@param finish? number +function xLib.table.array:slice(start, finish) + local length = #self + start = start or 1 + finish = finish or length + + if start < 0 then start = length + start + 1 end + if finish < 0 then finish = length + finish + 1 end + if start < 1 then start = 1 end + if finish > length then finish = length end + + local arr = xLib.table.array:new() + local index = 0 + + for i = start, finish do + index += 1 + arr[index] = self[i] + end + + return arr +end + +---Creates a new array with reversed elements from the given array. +function xLib.table.array:toReversed() + local reversed = xLib.table.array:new() + + for i = #self, 1, -1 do + reversed:push(self[i]) + end + + return reversed +end + +---Inserts the given elements to the start of an array and returns the new array length. +---@param ... any +function xLib.table.array:unshift(...) + local elements = { ... } + local length = #self + local eLength = #elements + + for i = length, 1, -1 do + self[i + eLength] = self[i] + end + + for i = 1, #elements do + self[i] = elements[i] + end + + return length + eLength +end + +---Returns true if the given table is an instance of array or an array-like table. +---@param tbl ArrayLike +---@return boolean +function xLib.table.array.isArray(tbl) + local tableType = table_type(tbl) + + if not tableType then return false end + + if tableType == 'array' or tableType == 'empty' then + return true + end + + local mt = getmetatable(tbl) + return mt and mt == xLib.table.array +end + return xLib.table diff --git a/[core]/esx_lib/imports/zones/shared.lua b/[core]/esx_lib/imports/zones/shared.lua index e69de29bb..5dff5780f 100644 --- a/[core]/esx_lib/imports/zones/shared.lua +++ b/[core]/esx_lib/imports/zones/shared.lua @@ -0,0 +1,489 @@ +--!DISCLAIMER +--[[ + 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 = not IsDuplicityVersion() and {} --[[@as table]] +local exitingZones = not IsDuplicityVersion() and xLib.table.array:new() --[[@as Array]] +local enteringZones = not IsDuplicityVersion() and xLib.table.array:new() --[[@as Array]] +local nearbyZones = xLib.table.array:new() --[[@as Array]] +local glm_polygon_contains = glm.polygon.contains +local tick + +---@param zone CZone +local function removeZone(zone) + Zones[zone.id] = nil + + xLib.grid.removeEntry(zone) + + if IsDuplicityVersion() then return end + + insideZones[zone.id] = nil + + table.remove(exitingZones, exitingZones:indexOf(zone)) + table.remove(enteringZones, enteringZones:indexOf(zone)) +end + +CreateThread(function() + if IsDuplicityVersion() then return end + + while true do + local coords = GetEntityCoords(PlayerPedId()) + local zones = xLib.grid.getNearbyEntries(coords, function(entry) return entry.remove == removeZone end) --[[@as Array]] + local cellX, cellY = xLib.grid.getCellPosition(coords) + + 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 + + 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 = true + CreateThread(function() + while tick and next(insideZones) do + 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 + Wait(0) + end + tick = nil + end) + end + elseif not next(insideZones) then + tick = false + 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 not IsDuplicityVersion() 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 + xLib.grid.addEntry(data) + + return data +end + +xLib.zones = {} + +---@class PolyZone : ZoneProperties +---@field points vector3[] +---@field thickness? number + +---@param data PolyZone +---@return CZone +function xLib.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 = xLib.table.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 xLib.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 xLib.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 xLib.zones.getAllZones() return Zones end + +function xLib.zones.getCurrentZones() return insideZones end + +function xLib.zones.getNearbyZones() return nearbyZones end + +return xLib.zones \ No newline at end of file