local mapMinX, mapMinY, mapMaxX, mapMaxY = -3700, -4400, 4500, 8000 local xDivisions = 34 local yDivisions = 50 local xDelta = (mapMaxX - mapMinX) / xDivisions local yDelta = (mapMaxY - mapMinY) / yDivisions ComboZone = {} -- Finds all values in tblA that are not in tblB, using the "id" property local function tblDifference(tblA, tblB) local diff for _, a in ipairs(tblA) do local found = false for _, b in ipairs(tblB) do if b.id == a.id then found = true break end end if not found then diff = diff or {} diff[#diff+1] = a end end return diff end local function _differenceBetweenInsideZones(insideZones, newInsideZones) local insideZonesCount, newInsideZonesCount = #insideZones, #newInsideZones if insideZonesCount == 0 and newInsideZonesCount == 0 then -- No zones to check return false, nil, nil elseif insideZonesCount == 0 and newInsideZonesCount > 0 then -- Was in no zones last check, but in 1 or more zones now (just entered all zones in newInsideZones) return true, copyTbl(newInsideZones), nil elseif insideZonesCount > 0 and newInsideZonesCount == 0 then -- Was in 1 or more zones last check, but in no zones now (just left all zones in insideZones) return true, nil, copyTbl(insideZones) end -- Check for zones that were in insideZones, but are not in newInsideZones (zones the player just left) local leftZones = tblDifference(insideZones, newInsideZones) -- Check for zones that are in newInsideZones, but were not in insideZones (zones the player just entered) local enteredZones = tblDifference(newInsideZones, insideZones) local isDifferent = enteredZones ~= nil or leftZones ~= nil return isDifferent, enteredZones, leftZones end local function _getZoneBounds(zone) local center = zone.center local radius = zone.radius or zone.boundingRadius local minY = (center.y - radius - mapMinY) // yDelta local maxY = (center.y + radius - mapMinY) // yDelta local minX = (center.x - radius - mapMinX) // xDelta local maxX = (center.x + radius - mapMinX) // xDelta return minY, maxY, minX, maxX end local function _removeZoneByFunction(predicateFn, zones) if predicateFn == nil or zones == nil or #zones == 0 then return end for i=1, #zones do local possibleZone = zones[i] if possibleZone and predicateFn(possibleZone) then table.remove(zones, i) return possibleZone end end return nil end local function _addZoneToGrid(grid, zone) local minY, maxY, minX, maxX = _getZoneBounds(zone) for y=minY, maxY do local row = grid[y] or {} for x=minX, maxX do local cell = row[x] or {} cell[#cell+1] = zone row[x] = cell end grid[y] = row end end local function _getGridCell(pos) local x = (pos.x - mapMinX) // xDelta local y = (pos.y - mapMinY) // yDelta return x, y end function ComboZone:draw() local zones = self.zones for i=1, #zones do local zone = zones[i] if zone and not zone.destroyed then zone:draw() end end end local function _initDebug(zone, options) if options.debugBlip then zone:addDebugBlip() end if not options.debugPoly then return end Citizen.CreateThread(function() while not zone.destroyed do zone:draw() Citizen.Wait(0) end end) end function ComboZone:new(zones, options) options = options or {} local useGrid = options.useGrid if useGrid == nil then useGrid = true end local grid = {} -- Add a unique id for each zone in the ComboZone and add to grid cache for i=1, #zones do local zone = zones[i] if zone then zone.id = i end if useGrid then _addZoneToGrid(grid, zone) end end local zone = { name = tostring(options.name) or nil, zones = zones, useGrid = useGrid, grid = grid, debugPoly = options.debugPoly or false, data = options.data or {}, isComboZone = true, } setmetatable(zone, self) self.__index = self return zone end function ComboZone:Create(zones, options) local zone = ComboZone:new(zones, options) _initDebug(zone, options) AddEventHandler("polyzone:pzcomboinfo", function () zone:printInfo() end) return zone end function ComboZone:getZones(point) if not self.useGrid then return self.zones end local grid = self.grid local x, y = _getGridCell(point) local row = grid[y] if row == nil or row[x] == nil then return nil end return row[x] end function ComboZone:AddZone(zone) local zones = self.zones local newIndex = #zones+1 zone.id = newIndex zones[newIndex] = zone if self.useGrid then _addZoneToGrid(self.grid, zone) end if self.debugBlip then zone:addDebugBlip() end end function ComboZone:RemoveZone(nameOrFn) local predicateFn = nameOrFn if type(nameOrFn) == "string" then -- Create on the fly predicate function if nameOrFn is a string (zone name) predicateFn = function (zone) return zone.name == nameOrFn end elseif type(nameOrFn) ~= "function" then return nil end -- Remove from zones table local zone = _removeZoneByFunction(predicateFn, self.zones) if not zone then return nil end -- Remove from grid cache local grid = self.grid local minY, maxY, minX, maxX = _getZoneBounds(zone) for y=minY, maxY do local row = grid[y] if row then for x=minX, maxX do _removeZoneByFunction(predicateFn, row[x]) end end end return zone end function ComboZone:isPointInside(point, zoneName) if self.destroyed then print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}") return false, {} end local zones = self:getZones(point) if not zones or #zones == 0 then return false end for i=1, #zones do local zone = zones[i] if zone and (zoneName == nil or zoneName == zone.name) and zone:isPointInside(point) then return true, zone end end return false, nil end function ComboZone:isPointInsideExhaustive(point, insideZones) if self.destroyed then print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}") return false, {} end if insideZones ~= nil then insideZones = clearTbl(insideZones) else insideZones = {} end local zones = self:getZones(point) if not zones or #zones == 0 then return false, insideZones end for i=1, #zones do local zone = zones[i] if zone and zone:isPointInside(point) then insideZones[#insideZones+1] = zone end end return #insideZones > 0, insideZones end function ComboZone:destroy() PolyZone.destroy(self) local zones = self.zones for i=1, #zones do local zone = zones[i] if zone and not zone.destroyed then zone:destroy() end end end function ComboZone:onPointInOut(getPointCb, onPointInOutCb, waitInMS) -- Localize the waitInMS value for performance reasons (default of 500 ms) local _waitInMS = 500 if waitInMS ~= nil then _waitInMS = waitInMS end Citizen.CreateThread(function() local isInside = nil local insideZone = nil while not self.destroyed do if not self.paused then local point = getPointCb() local newIsInside, newInsideZone = self:isPointInside(point) if newIsInside ~= isInside then onPointInOutCb(newIsInside, point, newInsideZone or insideZone) isInside = newIsInside insideZone = newInsideZone end end Citizen.Wait(_waitInMS) end end) end function ComboZone:onPointInOutExhaustive(getPointCb, onPointInOutCb, waitInMS) -- Localize the waitInMS value for performance reasons (default of 500 ms) local _waitInMS = 500 if waitInMS ~= nil then _waitInMS = waitInMS end Citizen.CreateThread(function() local isInside, insideZones = nil, {} local newIsInside, newInsideZones = nil, {} while not self.destroyed do if not self.paused then local point = getPointCb() newIsInside, newInsideZones = self:isPointInsideExhaustive(point, newInsideZones) local isDifferent, enteredZones, leftZones = _differenceBetweenInsideZones(insideZones, newInsideZones) if newIsInside ~= isInside or isDifferent then isInside = newIsInside insideZones = copyTbl(newInsideZones) onPointInOutCb(isInside, point, insideZones, enteredZones, leftZones) end end Citizen.Wait(_waitInMS) end end) end function ComboZone:onPlayerInOut(onPointInOutCb, waitInMS) self:onPointInOut(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS) end function ComboZone:onPlayerInOutExhaustive(onPointInOutCb, waitInMS) self:onPointInOutExhaustive(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS) end function ComboZone:addEvent(eventName, zoneName) if self.events == nil then self.events = {} end local internalEventName = eventPrefix .. eventName RegisterNetEvent(internalEventName) self.events[eventName] = AddEventHandler(internalEventName, function (...) if self:isPointInside(PolyZone.getPlayerPosition(), zoneName) then TriggerEvent(eventName, ...) end end) end function ComboZone:removeEvent(name) PolyZone.removeEvent(self, name) end function ComboZone:addDebugBlip() self.debugBlip = true local zones = self.zones for i=1, #zones do local zone = zones[i] if zone then zone:addDebugBlip() end end end function ComboZone:printInfo() local zones = self.zones local polyCount, boxCount, circleCount, entityCount, comboCount = 0, 0, 0, 0, 0 for i=1, #zones do local zone = zones[i] if zone then if zone.isEntityZone then entityCount = entityCount + 1 elseif zone.isCircleZone then circleCount = circleCount + 1 elseif zone.isComboZone then comboCount = comboCount + 1 elseif zone.isBoxZone then boxCount = boxCount + 1 elseif zone.isPolyZone then polyCount = polyCount + 1 end end end local name = self.name ~= nil and ("\"" .. self.name .. "\"") or nil print("-----------------------------------------------------") print("[PolyZone] Info for ComboZone { name = " .. tostring(name) .. " }:") print("[PolyZone] Total zones: " .. #zones) if boxCount > 0 then print("[PolyZone] BoxZones: " .. boxCount) end if circleCount > 0 then print("[PolyZone] CircleZones: " .. circleCount) end if polyCount > 0 then print("[PolyZone] PolyZones: " .. polyCount) end if entityCount > 0 then print("[PolyZone] EntityZones: " .. entityCount) end if comboCount > 0 then print("[PolyZone] ComboZones: " .. comboCount) end print("-----------------------------------------------------") end function ComboZone:setPaused(paused) self.paused = paused end function ComboZone:isPaused() return self.paused end