├── .gitignore ├── BoxZone.lua ├── CircleZone.lua ├── ComboZone.lua ├── EntityZone.lua ├── LICENSE ├── README.md ├── client.lua ├── creation ├── client │ ├── BoxZone.lua │ ├── CircleZone.lua │ ├── PolyZone.lua │ ├── commands.lua │ ├── creation.lua │ └── utils.lua └── server │ ├── config.lua │ └── creation.lua ├── fxmanifest.lua └── server.lua /.gitignore: -------------------------------------------------------------------------------- 1 | polyzone_created_zones.txt 2 | -------------------------------------------------------------------------------- /BoxZone.lua: -------------------------------------------------------------------------------- 1 | BoxZone = {} 2 | -- Inherits from PolyZone 3 | setmetatable(BoxZone, { __index = PolyZone }) 4 | 5 | -- Utility functions 6 | local rad, cos, sin = math.rad, math.cos, math.sin 7 | function PolyZone.rotate(origin, point, theta) 8 | if theta == 0.0 then return point end 9 | 10 | local p = point - origin 11 | local pX, pY = p.x, p.y 12 | theta = rad(theta) 13 | local cosTheta = cos(theta) 14 | local sinTheta = sin(theta) 15 | local x = pX * cosTheta - pY * sinTheta 16 | local y = pX * sinTheta + pY * cosTheta 17 | return vector2(x, y) + origin 18 | end 19 | 20 | function BoxZone.calculateMinAndMaxZ(minZ, maxZ, scaleZ, offsetZ) 21 | local minScaleZ, maxScaleZ, minOffsetZ, maxOffsetZ = scaleZ[1] or 1.0, scaleZ[2] or 1.0, offsetZ[1] or 0.0, offsetZ[2] or 0.0 22 | if (minZ == nil and maxZ == nil) or (minScaleZ == 1.0 and maxScaleZ == 1.0 and minOffsetZ == 0.0 and maxOffsetZ == 0.0) then 23 | return minZ, maxZ 24 | end 25 | 26 | if minScaleZ ~= 1.0 or maxScaleZ ~= 1.0 then 27 | if minZ ~= nil and maxZ ~= nil then 28 | local halfHeight = (maxZ - minZ) / 2 29 | local centerZ = minZ + halfHeight 30 | minZ = centerZ - halfHeight * minScaleZ 31 | maxZ = centerZ + halfHeight * maxScaleZ 32 | else 33 | print(string.format( 34 | "[PolyZone] Warning: The minZ/maxZ of a BoxZone can only be scaled if both minZ and maxZ are non-nil (minZ=%s, maxZ=%s)", 35 | tostring(minZ), 36 | tostring(maxZ) 37 | )) 38 | end 39 | end 40 | 41 | if minZ then minZ = minZ - minOffsetZ end 42 | if maxZ then maxZ = maxZ + maxOffsetZ end 43 | 44 | return minZ, maxZ 45 | end 46 | 47 | local function _calculateScaleAndOffset(options) 48 | -- Scale and offset tables are both formatted as {forward, back, left, right, up, down} 49 | -- or if symmetrical {forward/back, left/right, up/down} 50 | local scale = options.scale or {1.0, 1.0, 1.0, 1.0, 1.0, 1.0} 51 | local offset = options.offset or {0.0, 0.0, 0.0, 0.0, 0.0, 0.0} 52 | assert(#scale == 3 or #scale == 6, "Scale must be of length 3 or 6") 53 | assert(#offset == 3 or #offset == 6, "Offset must be of length 3 or 6") 54 | if #scale == 3 then 55 | scale = {scale[1], scale[1], scale[2], scale[2], scale[3], scale[3]} 56 | end 57 | if #offset == 3 then 58 | offset = {offset[1], offset[1], offset[2], offset[2], offset[3], offset[3]} 59 | end 60 | local minOffset = vector3(offset[3], offset[2], offset[6]) 61 | local maxOffset = vector3(offset[4], offset[1], offset[5]) 62 | local minScale = vector3(scale[3], scale[2], scale[6]) 63 | local maxScale = vector3(scale[4], scale[1], scale[5]) 64 | return minOffset, maxOffset, minScale, maxScale 65 | end 66 | 67 | local function _calculatePoints(center, length, width, minScale, maxScale, minOffset, maxOffset) 68 | local halfLength, halfWidth = length / 2, width / 2 69 | local min = vector3(-halfWidth, -halfLength, 0.0) 70 | local max = vector3(halfWidth, halfLength, 0.0) 71 | 72 | min = min * minScale - minOffset 73 | max = max * maxScale + maxOffset 74 | 75 | -- Box vertices 76 | local p1 = center.xy + vector2(min.x, min.y) 77 | local p2 = center.xy + vector2(max.x, min.y) 78 | local p3 = center.xy + vector2(max.x, max.y) 79 | local p4 = center.xy + vector2(min.x, max.y) 80 | return {p1, p2, p3, p4} 81 | end 82 | 83 | -- Debug drawing functions 84 | function BoxZone:TransformPoint(point) 85 | -- Overriding TransformPoint function to take into account rotation and position offset 86 | return PolyZone.rotate(self.startPos, point, self.offsetRot) + self.offsetPos 87 | end 88 | 89 | 90 | -- Initialization functions 91 | local function _initDebug(zone, options) 92 | if options.debugBlip then zone:addDebugBlip() end 93 | if not options.debugPoly then 94 | return 95 | end 96 | 97 | Citizen.CreateThread(function() 98 | while not zone.destroyed do 99 | zone:draw(false) 100 | Citizen.Wait(0) 101 | end 102 | end) 103 | end 104 | 105 | local defaultMinOffset, defaultMaxOffset, defaultMinScale, defaultMaxScale = vector3(0.0, 0.0, 0.0), vector3(0.0, 0.0, 0.0), vector3(1.0, 1.0, 1.0), vector3(1.0, 1.0, 1.0) 106 | local defaultScaleZ, defaultOffsetZ = {defaultMinScale.z, defaultMaxScale.z}, {defaultMinOffset.z, defaultMaxOffset.z} 107 | function BoxZone:new(center, length, width, options) 108 | local minOffset, maxOffset, minScale, maxScale = defaultMinOffset, defaultMaxOffset, defaultMinScale, defaultMaxScale 109 | local scaleZ, offsetZ = defaultScaleZ, defaultOffsetZ 110 | if options.scale ~= nil or options.offset ~= nil then 111 | minOffset, maxOffset, minScale, maxScale = _calculateScaleAndOffset(options) 112 | scaleZ, offsetZ = {minScale.z, maxScale.z}, {minOffset.z, maxOffset.z} 113 | end 114 | 115 | local points = _calculatePoints(center, length, width, minScale, maxScale, minOffset, maxOffset) 116 | local min = points[1] 117 | local max = points[3] 118 | local size = max - min 119 | 120 | local minZ, maxZ = BoxZone.calculateMinAndMaxZ(options.minZ, options.maxZ, scaleZ, offsetZ) 121 | options.minZ = minZ 122 | options.maxZ = maxZ 123 | 124 | -- Box Zones don't use the grid optimization because they are already rectangles/cubes 125 | options.useGrid = false 126 | 127 | -- Pre-setting all these values to avoid PolyZone:new() having to calculate them 128 | options.min = min 129 | options.max = max 130 | options.size = size 131 | options.center = center 132 | options.area = size.x * size.y 133 | 134 | local zone = PolyZone:new(points, options) 135 | zone.length = length 136 | zone.width = width 137 | zone.startPos = center.xy 138 | zone.offsetPos = vector2(0.0, 0.0) 139 | zone.offsetRot = options.heading or 0.0 140 | zone.minScale, zone.maxScale = minScale, maxScale 141 | zone.minOffset, zone.maxOffset = minOffset, maxOffset 142 | zone.scaleZ, zone.offsetZ = scaleZ, offsetZ 143 | zone.isBoxZone = true 144 | 145 | setmetatable(zone, self) 146 | self.__index = self 147 | return zone 148 | end 149 | 150 | function BoxZone:Create(center, length, width, options) 151 | local zone = BoxZone:new(center, length, width, options) 152 | _initDebug(zone, options) 153 | return zone 154 | end 155 | 156 | 157 | -- Helper functions 158 | function BoxZone:isPointInside(point) 159 | if self.destroyed then 160 | print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}") 161 | return false 162 | end 163 | 164 | local startPos = self.startPos 165 | local actualPos = point.xy - self.offsetPos 166 | if #(actualPos - startPos) > self.boundingRadius then 167 | return false 168 | end 169 | 170 | local rotatedPoint = PolyZone.rotate(startPos, actualPos, -self.offsetRot) 171 | local pX, pY, pZ = rotatedPoint.x, rotatedPoint.y, point.z 172 | local min, max = self.min, self.max 173 | local minX, minY, maxX, maxY = min.x, min.y, max.x, max.y 174 | local minZ, maxZ = self.minZ, self.maxZ 175 | if pX < minX or pX > maxX or pY < minY or pY > maxY then 176 | return false 177 | end 178 | if (minZ and pZ < minZ) or (maxZ and pZ > maxZ) then 179 | return false 180 | end 181 | return true 182 | end 183 | 184 | function BoxZone:getHeading() 185 | return self.offsetRot 186 | end 187 | 188 | function BoxZone:setHeading(heading) 189 | if not heading then 190 | return 191 | end 192 | self.offsetRot = heading 193 | end 194 | 195 | function BoxZone:setCenter(center) 196 | if not center or center == self.center then 197 | return 198 | end 199 | self.center = center 200 | self.startPos = center.xy 201 | self.points = _calculatePoints(self.center, self.length, self.width, self.minScale, self.maxScale, self.minOffset, self.maxOffset) 202 | end 203 | 204 | function BoxZone:getLength() 205 | return self.length 206 | end 207 | 208 | function BoxZone:setLength(length) 209 | if not length or length == self.length then 210 | return 211 | end 212 | self.length = length 213 | self.points = _calculatePoints(self.center, self.length, self.width, self.minScale, self.maxScale, self.minOffset, self.maxOffset) 214 | end 215 | 216 | function BoxZone:getWidth() 217 | return self.width 218 | end 219 | 220 | function BoxZone:setWidth(width) 221 | if not width or width == self.width then 222 | return 223 | end 224 | self.width = width 225 | self.points = _calculatePoints(self.center, self.length, self.width, self.minScale, self.maxScale, self.minOffset, self.maxOffset) 226 | end 227 | -------------------------------------------------------------------------------- /CircleZone.lua: -------------------------------------------------------------------------------- 1 | CircleZone = {} 2 | -- Inherits from PolyZone 3 | setmetatable(CircleZone, { __index = PolyZone }) 4 | 5 | function CircleZone:draw(forceDraw) 6 | if not forceDraw and not self.debugPoly then return end 7 | local center = self.center 8 | local debugColor = self.debugColor 9 | local r, g, b = debugColor[1], debugColor[2], debugColor[3] 10 | if self.useZ then 11 | local radius = self.radius 12 | DrawMarker(28, center.x, center.y, center.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, radius, radius, radius, r, g, b, 48, false, false, 2, nil, nil, false) 13 | else 14 | local diameter = self.diameter 15 | DrawMarker(1, center.x, center.y, -500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, diameter, diameter, 1000.0, r, g, b, 96, false, false, 2, nil, nil, false) 16 | end 17 | end 18 | 19 | 20 | local function _initDebug(zone, options) 21 | if options.debugBlip then zone:addDebugBlip() end 22 | if not options.debugPoly then 23 | return 24 | end 25 | 26 | Citizen.CreateThread(function() 27 | while not zone.destroyed do 28 | zone:draw(false) 29 | Citizen.Wait(0) 30 | end 31 | end) 32 | end 33 | 34 | function CircleZone:new(center, radius, options) 35 | options = options or {} 36 | local zone = { 37 | name = tostring(options.name) or nil, 38 | center = center, 39 | radius = radius + 0.0, 40 | diameter = radius * 2.0, 41 | useZ = options.useZ or false, 42 | debugPoly = options.debugPoly or false, 43 | debugColor = options.debugColor or {0, 255, 0}, 44 | data = options.data or {}, 45 | isCircleZone = true, 46 | } 47 | if zone.useZ then 48 | assert(type(zone.center) == "vector3", "Center must be vector3 if useZ is true {center=" .. center .. "}") 49 | end 50 | setmetatable(zone, self) 51 | self.__index = self 52 | return zone 53 | end 54 | 55 | function CircleZone:Create(center, radius, options) 56 | local zone = CircleZone:new(center, radius, options) 57 | _initDebug(zone, options) 58 | return zone 59 | end 60 | 61 | function CircleZone:isPointInside(point) 62 | if self.destroyed then 63 | print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}") 64 | return false 65 | end 66 | 67 | local center = self.center 68 | local radius = self.radius 69 | 70 | if self.useZ then 71 | return #(point - center) < radius 72 | else 73 | return #(point.xy - center.xy) < radius 74 | end 75 | end 76 | 77 | function CircleZone:getRadius() 78 | return self.radius 79 | end 80 | 81 | function CircleZone:setRadius(radius) 82 | if not radius or radius == self.radius then 83 | return 84 | end 85 | self.radius = radius 86 | self.diameter = radius * 2.0 87 | end 88 | 89 | function CircleZone:getCenter() 90 | return self.center 91 | end 92 | 93 | function CircleZone:setCenter(center) 94 | if not center or center == self.center then 95 | return 96 | end 97 | self.center = center 98 | end 99 | -------------------------------------------------------------------------------- /ComboZone.lua: -------------------------------------------------------------------------------- 1 | local mapMinX, mapMinY, mapMaxX, mapMaxY = -3700, -4400, 4500, 8000 2 | local xDivisions = 34 3 | local yDivisions = 50 4 | local xDelta = (mapMaxX - mapMinX) / xDivisions 5 | local yDelta = (mapMaxY - mapMinY) / yDivisions 6 | 7 | ComboZone = {} 8 | 9 | -- Finds all values in tblA that are not in tblB, using the "id" property 10 | local function tblDifference(tblA, tblB) 11 | local diff 12 | for _, a in ipairs(tblA) do 13 | local found = false 14 | for _, b in ipairs(tblB) do 15 | if b.id == a.id then 16 | found = true 17 | break 18 | end 19 | end 20 | if not found then 21 | diff = diff or {} 22 | diff[#diff+1] = a 23 | end 24 | end 25 | return diff 26 | end 27 | 28 | local function _differenceBetweenInsideZones(insideZones, newInsideZones) 29 | local insideZonesCount, newInsideZonesCount = #insideZones, #newInsideZones 30 | if insideZonesCount == 0 and newInsideZonesCount == 0 then 31 | -- No zones to check 32 | return false, nil, nil 33 | elseif insideZonesCount == 0 and newInsideZonesCount > 0 then 34 | -- Was in no zones last check, but in 1 or more zones now (just entered all zones in newInsideZones) 35 | return true, copyTbl(newInsideZones), nil 36 | elseif insideZonesCount > 0 and newInsideZonesCount == 0 then 37 | -- Was in 1 or more zones last check, but in no zones now (just left all zones in insideZones) 38 | return true, nil, copyTbl(insideZones) 39 | end 40 | 41 | -- Check for zones that were in insideZones, but are not in newInsideZones (zones the player just left) 42 | local leftZones = tblDifference(insideZones, newInsideZones) 43 | -- Check for zones that are in newInsideZones, but were not in insideZones (zones the player just entered) 44 | local enteredZones = tblDifference(newInsideZones, insideZones) 45 | 46 | local isDifferent = enteredZones ~= nil or leftZones ~= nil 47 | return isDifferent, enteredZones, leftZones 48 | end 49 | 50 | local function _getZoneBounds(zone) 51 | local center = zone.center 52 | local radius = zone.radius or zone.boundingRadius 53 | local minY = (center.y - radius - mapMinY) // yDelta 54 | local maxY = (center.y + radius - mapMinY) // yDelta 55 | local minX = (center.x - radius - mapMinX) // xDelta 56 | local maxX = (center.x + radius - mapMinX) // xDelta 57 | return minY, maxY, minX, maxX 58 | end 59 | 60 | local function _removeZoneByFunction(predicateFn, zones) 61 | if predicateFn == nil or zones == nil or #zones == 0 then return end 62 | 63 | for i=1, #zones do 64 | local possibleZone = zones[i] 65 | if possibleZone and predicateFn(possibleZone) then 66 | table.remove(zones, i) 67 | return possibleZone 68 | end 69 | end 70 | return nil 71 | end 72 | 73 | local function _addZoneToGrid(grid, zone) 74 | local minY, maxY, minX, maxX = _getZoneBounds(zone) 75 | for y=minY, maxY do 76 | local row = grid[y] or {} 77 | for x=minX, maxX do 78 | local cell = row[x] or {} 79 | cell[#cell+1] = zone 80 | row[x] = cell 81 | end 82 | grid[y] = row 83 | end 84 | end 85 | 86 | local function _getGridCell(pos) 87 | local x = (pos.x - mapMinX) // xDelta 88 | local y = (pos.y - mapMinY) // yDelta 89 | return x, y 90 | end 91 | 92 | 93 | function ComboZone:draw(forceDraw) 94 | local zones = self.zones 95 | for i=1, #zones do 96 | local zone = zones[i] 97 | if zone and not zone.destroyed then 98 | zone:draw(forceDraw) 99 | end 100 | end 101 | end 102 | 103 | 104 | local function _initDebug(zone, options) 105 | if options.debugBlip then zone:addDebugBlip() end 106 | if not options.debugPoly then 107 | return 108 | end 109 | 110 | Citizen.CreateThread(function() 111 | while not zone.destroyed do 112 | zone:draw(false) 113 | Citizen.Wait(0) 114 | end 115 | end) 116 | end 117 | 118 | function ComboZone:new(zones, options) 119 | options = options or {} 120 | local useGrid = options.useGrid 121 | if useGrid == nil then useGrid = true end 122 | 123 | local grid = {} 124 | -- Add a unique id for each zone in the ComboZone and add to grid cache 125 | for i=1, #zones do 126 | local zone = zones[i] 127 | if zone then 128 | zone.id = i 129 | end 130 | if useGrid then _addZoneToGrid(grid, zone) end 131 | end 132 | 133 | local zone = { 134 | name = tostring(options.name) or nil, 135 | zones = zones, 136 | useGrid = useGrid, 137 | grid = grid, 138 | debugPoly = options.debugPoly or false, 139 | data = options.data or {}, 140 | isComboZone = true, 141 | } 142 | setmetatable(zone, self) 143 | self.__index = self 144 | return zone 145 | end 146 | 147 | function ComboZone:Create(zones, options) 148 | local zone = ComboZone:new(zones, options) 149 | _initDebug(zone, options) 150 | AddEventHandler("polyzone:pzcomboinfo", function () 151 | zone:printInfo() 152 | end) 153 | return zone 154 | end 155 | 156 | function ComboZone:getZones(point) 157 | if not self.useGrid then 158 | return self.zones 159 | end 160 | 161 | local grid = self.grid 162 | local x, y = _getGridCell(point) 163 | local row = grid[y] 164 | if row == nil or row[x] == nil then 165 | return nil 166 | end 167 | return row[x] 168 | end 169 | 170 | function ComboZone:AddZone(zone) 171 | local zones = self.zones 172 | local newIndex = #zones+1 173 | zone.id = newIndex 174 | zones[newIndex] = zone 175 | if self.useGrid then 176 | _addZoneToGrid(self.grid, zone) 177 | end 178 | if self.debugBlip then zone:addDebugBlip() end 179 | end 180 | 181 | function ComboZone:RemoveZone(nameOrFn) 182 | local predicateFn = nameOrFn 183 | if type(nameOrFn) == "string" then 184 | -- Create on the fly predicate function if nameOrFn is a string (zone name) 185 | predicateFn = function (zone) return zone.name == nameOrFn end 186 | elseif type(nameOrFn) ~= "function" then 187 | return nil 188 | end 189 | 190 | -- Remove from zones table 191 | local zone = _removeZoneByFunction(predicateFn, self.zones) 192 | if not zone then return nil end 193 | 194 | -- Remove from grid cache 195 | local grid = self.grid 196 | local minY, maxY, minX, maxX = _getZoneBounds(zone) 197 | for y=minY, maxY do 198 | local row = grid[y] 199 | if row then 200 | for x=minX, maxX do 201 | _removeZoneByFunction(predicateFn, row[x]) 202 | end 203 | end 204 | end 205 | return zone 206 | end 207 | 208 | function ComboZone:isPointInside(point, zoneName) 209 | if self.destroyed then 210 | print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}") 211 | return false, {} 212 | end 213 | 214 | local zones = self:getZones(point) 215 | if not zones or #zones == 0 then return false end 216 | 217 | for i=1, #zones do 218 | local zone = zones[i] 219 | if zone and (zoneName == nil or zoneName == zone.name) and zone:isPointInside(point) then 220 | return true, zone 221 | end 222 | end 223 | return false, nil 224 | end 225 | 226 | function ComboZone:isPointInsideExhaustive(point, insideZones) 227 | if self.destroyed then 228 | print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}") 229 | return false, {} 230 | end 231 | 232 | if insideZones ~= nil then 233 | insideZones = clearTbl(insideZones) 234 | else 235 | insideZones = {} 236 | end 237 | local zones = self:getZones(point) 238 | if not zones or #zones == 0 then return false, insideZones end 239 | for i=1, #zones do 240 | local zone = zones[i] 241 | if zone and zone:isPointInside(point) then 242 | insideZones[#insideZones+1] = zone 243 | end 244 | end 245 | return #insideZones > 0, insideZones 246 | end 247 | 248 | function ComboZone:destroy() 249 | PolyZone.destroy(self) 250 | local zones = self.zones 251 | for i=1, #zones do 252 | local zone = zones[i] 253 | if zone and not zone.destroyed then 254 | zone:destroy() 255 | end 256 | end 257 | end 258 | 259 | function ComboZone:onPointInOut(getPointCb, onPointInOutCb, waitInMS) 260 | -- Localize the waitInMS value for performance reasons (default of 500 ms) 261 | local _waitInMS = 500 262 | if waitInMS ~= nil then _waitInMS = waitInMS end 263 | 264 | Citizen.CreateThread(function() 265 | local isInside = nil 266 | local insideZone = nil 267 | while not self.destroyed do 268 | if not self.paused then 269 | local point = getPointCb() 270 | local newIsInside, newInsideZone = self:isPointInside(point) 271 | if newIsInside ~= isInside then 272 | onPointInOutCb(newIsInside, point, newInsideZone or insideZone) 273 | isInside = newIsInside 274 | insideZone = newInsideZone 275 | end 276 | end 277 | Citizen.Wait(_waitInMS) 278 | end 279 | end) 280 | end 281 | 282 | function ComboZone:onPointInOutExhaustive(getPointCb, onPointInOutCb, waitInMS) 283 | -- Localize the waitInMS value for performance reasons (default of 500 ms) 284 | local _waitInMS = 500 285 | if waitInMS ~= nil then _waitInMS = waitInMS end 286 | 287 | Citizen.CreateThread(function() 288 | local isInside, insideZones = nil, {} 289 | local newIsInside, newInsideZones = nil, {} 290 | while not self.destroyed do 291 | if not self.paused then 292 | local point = getPointCb() 293 | newIsInside, newInsideZones = self:isPointInsideExhaustive(point, newInsideZones) 294 | local isDifferent, enteredZones, leftZones = _differenceBetweenInsideZones(insideZones, newInsideZones) 295 | if newIsInside ~= isInside or isDifferent then 296 | isInside = newIsInside 297 | insideZones = copyTbl(newInsideZones) 298 | onPointInOutCb(isInside, point, insideZones, enteredZones, leftZones) 299 | end 300 | end 301 | Citizen.Wait(_waitInMS) 302 | end 303 | end) 304 | end 305 | 306 | function ComboZone:onPlayerInOut(onPointInOutCb, waitInMS) 307 | self:onPointInOut(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS) 308 | end 309 | 310 | function ComboZone:onPlayerInOutExhaustive(onPointInOutCb, waitInMS) 311 | self:onPointInOutExhaustive(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS) 312 | end 313 | 314 | function ComboZone:addEvent(eventName, zoneName) 315 | if self.events == nil then self.events = {} end 316 | local internalEventName = eventPrefix .. eventName 317 | RegisterNetEvent(internalEventName) 318 | self.events[eventName] = AddEventHandler(internalEventName, function (...) 319 | if self:isPointInside(PolyZone.getPlayerPosition(), zoneName) then 320 | TriggerEvent(eventName, ...) 321 | end 322 | end) 323 | end 324 | 325 | function ComboZone:removeEvent(name) 326 | PolyZone.removeEvent(self, name) 327 | end 328 | 329 | function ComboZone:addDebugBlip() 330 | self.debugBlip = true 331 | local zones = self.zones 332 | for i=1, #zones do 333 | local zone = zones[i] 334 | if zone then zone:addDebugBlip() end 335 | end 336 | end 337 | 338 | function ComboZone:printInfo() 339 | local zones = self.zones 340 | local polyCount, boxCount, circleCount, entityCount, comboCount = 0, 0, 0, 0, 0 341 | for i=1, #zones do 342 | local zone = zones[i] 343 | if zone then 344 | if zone.isEntityZone then entityCount = entityCount + 1 345 | elseif zone.isCircleZone then circleCount = circleCount + 1 346 | elseif zone.isComboZone then comboCount = comboCount + 1 347 | elseif zone.isBoxZone then boxCount = boxCount + 1 348 | elseif zone.isPolyZone then polyCount = polyCount + 1 end 349 | end 350 | end 351 | local name = self.name ~= nil and ("\"" .. self.name .. "\"") or nil 352 | print("-----------------------------------------------------") 353 | print("[PolyZone] Info for ComboZone { name = " .. tostring(name) .. " }:") 354 | print("[PolyZone] Total zones: " .. #zones) 355 | if boxCount > 0 then print("[PolyZone] BoxZones: " .. boxCount) end 356 | if circleCount > 0 then print("[PolyZone] CircleZones: " .. circleCount) end 357 | if polyCount > 0 then print("[PolyZone] PolyZones: " .. polyCount) end 358 | if entityCount > 0 then print("[PolyZone] EntityZones: " .. entityCount) end 359 | if comboCount > 0 then print("[PolyZone] ComboZones: " .. comboCount) end 360 | print("-----------------------------------------------------") 361 | end 362 | 363 | function ComboZone:setPaused(paused) 364 | self.paused = paused 365 | end 366 | 367 | function ComboZone:isPaused() 368 | return self.paused 369 | end 370 | -------------------------------------------------------------------------------- /EntityZone.lua: -------------------------------------------------------------------------------- 1 | EntityZone = {} 2 | -- Inherits from BoxZone 3 | setmetatable(EntityZone, { __index = BoxZone }) 4 | 5 | -- Utility functions 6 | local deg, atan2 = math.deg, math.atan2 7 | local function GetRotation(entity) 8 | local fwdVector = GetEntityForwardVector(entity) 9 | return deg(atan2(fwdVector.y, fwdVector.x)) 10 | end 11 | 12 | local function _calculateMinAndMaxZ(entity, dimensions, scaleZ, offsetZ) 13 | local min, max = dimensions[1], dimensions[2] 14 | local minX, minY, minZ, maxX, maxY, maxZ = min.x, min.y, min.z, max.x, max.y, max.z 15 | 16 | -- Bottom vertices 17 | local p1 = GetOffsetFromEntityInWorldCoords(entity, minX, minY, minZ).z 18 | local p2 = GetOffsetFromEntityInWorldCoords(entity, maxX, minY, minZ).z 19 | local p3 = GetOffsetFromEntityInWorldCoords(entity, maxX, maxY, minZ).z 20 | local p4 = GetOffsetFromEntityInWorldCoords(entity, minX, maxY, minZ).z 21 | 22 | -- Top vertices 23 | local p5 = GetOffsetFromEntityInWorldCoords(entity, minX, minY, maxZ).z 24 | local p6 = GetOffsetFromEntityInWorldCoords(entity, maxX, minY, maxZ).z 25 | local p7 = GetOffsetFromEntityInWorldCoords(entity, maxX, maxY, maxZ).z 26 | local p8 = GetOffsetFromEntityInWorldCoords(entity, minX, maxY, maxZ).z 27 | 28 | local entityMinZ = math.min(p1, p2, p3, p4, p5, p6, p7, p8) 29 | local entityMaxZ = math.max(p1, p2, p3, p4, p5, p6, p7, p8) 30 | return BoxZone.calculateMinAndMaxZ(entityMinZ, entityMaxZ, scaleZ, offsetZ) 31 | end 32 | 33 | -- Initialization functions 34 | local function _initDebug(zone, options) 35 | if options.debugBlip then zone:addDebugBlip() end 36 | if not options.debugPoly and not options.debugBlip then 37 | return 38 | end 39 | 40 | Citizen.CreateThread(function() 41 | local entity = zone.entity 42 | local shouldDraw = options.debugPoly 43 | while not zone.destroyed do 44 | UpdateOffsets(entity, zone) 45 | if shouldDraw then zone:draw(false) end 46 | Citizen.Wait(0) 47 | end 48 | end) 49 | end 50 | 51 | function EntityZone:new(entity, options) 52 | assert(DoesEntityExist(entity), "Entity does not exist") 53 | 54 | local min, max = GetModelDimensions(GetEntityModel(entity)) 55 | local dimensions = {min, max} 56 | 57 | local length = max.y - min.y 58 | local width = max.x - min.x 59 | local pos = GetEntityCoords(entity) 60 | 61 | local zone = BoxZone:new(pos, length, width, options) 62 | if options.useZ == true then 63 | options.minZ, options.maxZ = _calculateMinAndMaxZ(entity, dimensions, zone.scaleZ, zone.offsetZ) 64 | else 65 | options.minZ = nil 66 | options.maxZ = nil 67 | end 68 | zone.entity = entity 69 | zone.dimensions = dimensions 70 | zone.useZ = options.useZ 71 | zone.damageEventHandlers = {} 72 | zone.isEntityZone = true 73 | setmetatable(zone, self) 74 | self.__index = self 75 | return zone 76 | end 77 | 78 | function EntityZone:Create(entity, options) 79 | local zone = EntityZone:new(entity, options) 80 | _initDebug(zone, options) 81 | return zone 82 | end 83 | 84 | function UpdateOffsets(entity, zone) 85 | local pos = GetEntityCoords(entity) 86 | local rot = GetRotation(entity) 87 | zone.offsetPos = pos.xy - zone.startPos 88 | zone.offsetRot = rot - 90.0 89 | 90 | if zone.useZ then 91 | zone.minZ, zone.maxZ = _calculateMinAndMaxZ(entity, zone.dimensions, zone.scaleZ, zone.offsetZ) 92 | end 93 | if zone.debugBlip then SetBlipCoords(zone.debugBlip, pos.x, pos.y, 0.0) end 94 | end 95 | 96 | 97 | -- Helper functions 98 | function EntityZone:isPointInside(point) 99 | local entity = self.entity 100 | if entity == nil then 101 | print("[PolyZone] Error: Called isPointInside on Entity zone with no entity {name=" .. self.name .. "}") 102 | return false 103 | end 104 | 105 | UpdateOffsets(entity, self) 106 | return BoxZone.isPointInside(self, point) 107 | end 108 | 109 | function EntityZone:onEntityDamaged(onDamagedCb) 110 | local entity = self.entity 111 | if not entity then 112 | print("[PolyZone] Error: Called onEntityDamage on Entity Zone with no entity {name=" .. self.name .. "}") 113 | return 114 | end 115 | 116 | self.damageEventHandlers[#self.damageEventHandlers + 1] = AddEventHandler('gameEventTriggered', function (name, args) 117 | if self.destroyed or self.paused then 118 | return 119 | end 120 | 121 | if name == 'CEventNetworkEntityDamage' then 122 | local victim, attacker, victimDied, weaponHash, isMelee = args[1], args[2], args[4], args[5], args[10] 123 | --print(entity, victim, attacker, victimDied, weaponHash, isMelee) 124 | if victim ~= entity then return end 125 | onDamagedCb(victimDied == 1, attacker, weaponHash, isMelee == 1) 126 | end 127 | end) 128 | end 129 | 130 | function EntityZone:destroy() 131 | for i=1, #self.damageEventHandlers do 132 | print("Destroying damageEventHandler:", self.damageEventHandlers[i]) 133 | RemoveEventHandler(self.damageEventHandlers[i]) 134 | end 135 | self.damageEventHandlers = {} 136 | PolyZone.destroy(self) 137 | end 138 | 139 | function EntityZone:addDebugBlip() 140 | local blip = PolyZone.addDebugBlip(self) 141 | self.debugBlip = blip 142 | return blip 143 | end 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Michael Afrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PolyZone 2 | PolyZone is a FiveM mod to define zones of different shapes and test whether a point is inside or outside of the zone 3 | 4 | ![PolyZone around the prison](https://i.imgur.com/InKNaoL.jpg) 5 | 6 | ## Download 7 | 8 | Click [here](https://github.com/mkafrin/PolyZone/releases) to go to the releases page and download the latest release 9 | 10 | ## Using PolyZone in a Script 11 | 12 | In order to use PolyZone in your script, you must _at least_ include PolyZone's client.lua directly in your __resource.lua or fxmanifest.lua. You can do that by using FiveM's @ syntax for importing resource files: 13 | 14 | ```lua 15 | client_scripts { 16 | '@PolyZone/client.lua', 17 | 'your_scripts_client.lua', 18 | } 19 | ``` 20 | 21 | This will allow you to create PolyZones in your script, but will not import other zones, such as CircleZone, BoxZone, etc. All the other zones are extra, and require their own explicit imports. Here is a `client_scripts` value that will include all the zones. Note the relative order of these imports, as the ordering is necessary! Many zones rely on each other, for example EntityZone inherits from BoxZone, and all zones inherit from PolyZone (client.lua). 22 | 23 | ```lua 24 | client_scripts { 25 | '@PolyZone/client.lua', 26 | '@PolyZone/BoxZone.lua', 27 | '@PolyZone/EntityZone.lua', 28 | '@PolyZone/CircleZone.lua', 29 | '@PolyZone/ComboZone.lua', 30 | 'your_scripts_client.lua' 31 | } 32 | ``` 33 | 34 | ## Documentation 35 | For additional information on how to use PolyZone, please take a look at the [wiki](https://github.com/mkafrin/PolyZone/wiki) 36 | 37 | ## Troubleshooting and Support 38 | For help troubleshooting issues you've encountered (that aren't in the FAQ), or to suggest new features, use the [issues page](https://github.com/mkafrin/PolyZone/issues). Just a reminder though, I do this in my free time and so there is no guarantee an issue will be fixed or a feature will be added. In lieu of my limited time, I will prioritize issues and bugs over features. 39 | 40 | ## FAQ - Frequently Asked Questions 41 | **I'm getting the error `attempt to index a nil value` when creating a zone, what's wrong?** 42 | > Did you include all the necessary scripts in your \_\_resource.lua or fxmanifest.lua? Remember some zones require other zones, like EntityZone.lua requires BoxZone.lua and BoxZone.lua requires client.lua. 43 | 44 | **I'm getting no errors, but I can't see my zone in the right place when I turn on debug drawing** 45 | > If you are using them, is minZ and maxZ set correctly? Or if you are using a CircleZone with useZ=true, is your center's Z value correct? If using a PolyZone, did you manually select all your points, or use the creation script? If you did it manually, the ordering of the points could be causing issues. Are you using the correct option to enable debug drawing? For PolyZones, you can use `debugPoly` and `debugGrid`, but for other zones, `debugPoly` is the only one that works. 46 | 47 | **Is PolyZone faster than a distance check?** 48 | > There's a page in the wiki for that, [here](https://github.com/mkafrin/PolyZone/wiki/Is-PolyZone-faster-than-a-distance-check%3F). 49 | 50 | ## License 51 | **Please see the LICENSE file. That file will always overrule anything mentioned in the README.md or wiki** 52 | -------------------------------------------------------------------------------- /client.lua: -------------------------------------------------------------------------------- 1 | eventPrefix = '__PolyZone__:' 2 | PolyZone = {} 3 | 4 | local defaultColorWalls = {0, 255, 0} 5 | local defaultColorOutline = {255, 0, 0} 6 | local defaultColorGrid = {255, 255, 255} 7 | 8 | -- Utility functions 9 | local abs = math.abs 10 | local function _isLeft(p0, p1, p2) 11 | local p0x = p0.x 12 | local p0y = p0.y 13 | return ((p1.x - p0x) * (p2.y - p0y)) - ((p2.x - p0x) * (p1.y - p0y)) 14 | end 15 | 16 | local function _wn_inner_loop(p0, p1, p2, wn) 17 | local p2y = p2.y 18 | if (p0.y <= p2y) then 19 | if (p1.y > p2y) then 20 | if (_isLeft(p0, p1, p2) > 0) then 21 | return wn + 1 22 | end 23 | end 24 | else 25 | if (p1.y <= p2y) then 26 | if (_isLeft(p0, p1, p2) < 0) then 27 | return wn - 1 28 | end 29 | end 30 | end 31 | return wn 32 | end 33 | 34 | function addBlip(pos) 35 | local blip = AddBlipForCoord(pos.x, pos.y, 0.0) 36 | SetBlipColour(blip, 7) 37 | SetBlipDisplay(blip, 8) 38 | SetBlipScale(blip, 1.0) 39 | SetBlipAsShortRange(blip, true) 40 | return blip 41 | end 42 | 43 | function clearTbl(tbl) 44 | -- Only works with contiguous (array-like) tables 45 | if tbl == nil then return end 46 | for i=1, #tbl do 47 | tbl[i] = nil 48 | end 49 | return tbl 50 | end 51 | 52 | function copyTbl(tbl) 53 | -- Only a shallow copy, and only works with contiguous (array-like) tables 54 | if tbl == nil then return end 55 | local ret = {} 56 | for i=1, #tbl do 57 | ret[i] = tbl[i] 58 | end 59 | return ret 60 | end 61 | 62 | -- Winding Number Algorithm - http://geomalgorithms.com/a03-_inclusion.html 63 | local function _windingNumber(point, poly) 64 | local wn = 0 -- winding number counter 65 | 66 | -- loop through all edges of the polygon 67 | for i = 1, #poly - 1 do 68 | wn = _wn_inner_loop(poly[i], poly[i + 1], point, wn) 69 | end 70 | -- test last point to first point, completing the polygon 71 | wn = _wn_inner_loop(poly[#poly], poly[1], point, wn) 72 | 73 | -- the point is outside only when this winding number wn===0, otherwise it's inside 74 | return wn ~= 0 75 | end 76 | 77 | -- Detects intersection between two lines 78 | local function _isIntersecting(a, b, c, d) 79 | -- Store calculations in local variables for performance 80 | local ax_minus_cx = a.x - c.x 81 | local bx_minus_ax = b.x - a.x 82 | local dx_minus_cx = d.x - c.x 83 | local ay_minus_cy = a.y - c.y 84 | local by_minus_ay = b.y - a.y 85 | local dy_minus_cy = d.y - c.y 86 | local denominator = ((bx_minus_ax) * (dy_minus_cy)) - ((by_minus_ay) * (dx_minus_cx)) 87 | local numerator1 = ((ay_minus_cy) * (dx_minus_cx)) - ((ax_minus_cx) * (dy_minus_cy)) 88 | local numerator2 = ((ay_minus_cy) * (bx_minus_ax)) - ((ax_minus_cx) * (by_minus_ay)) 89 | 90 | -- Detect coincident lines 91 | if denominator == 0 then return numerator1 == 0 and numerator2 == 0 end 92 | 93 | local r = numerator1 / denominator 94 | local s = numerator2 / denominator 95 | 96 | return (r >= 0 and r <= 1) and (s >= 0 and s <= 1) 97 | end 98 | 99 | -- https://rosettacode.org/wiki/Shoelace_formula_for_polygonal_area#Lua 100 | local function _calculatePolygonArea(points) 101 | local function det2(i,j) 102 | return points[i].x*points[j].y-points[j].x*points[i].y 103 | end 104 | local sum = #points>2 and det2(#points,1) or 0 105 | for i=1,#points-1 do sum = sum + det2(i,i+1)end 106 | return abs(0.5 * sum) 107 | end 108 | 109 | 110 | -- Debug drawing functions 111 | function _drawWall(p1, p2, minZ, maxZ, r, g, b, a) 112 | local bottomLeft = vector3(p1.x, p1.y, minZ) 113 | local topLeft = vector3(p1.x, p1.y, maxZ) 114 | local bottomRight = vector3(p2.x, p2.y, minZ) 115 | local topRight = vector3(p2.x, p2.y, maxZ) 116 | 117 | DrawPoly(bottomLeft,topLeft,bottomRight,r,g,b,a) 118 | DrawPoly(topLeft,topRight,bottomRight,r,g,b,a) 119 | DrawPoly(bottomRight,topRight,topLeft,r,g,b,a) 120 | DrawPoly(bottomRight,topLeft,bottomLeft,r,g,b,a) 121 | end 122 | 123 | function PolyZone:TransformPoint(point) 124 | -- No point transform necessary for regular PolyZones, unlike zones like Entity Zones, whose points can be rotated and offset 125 | return point 126 | end 127 | 128 | function PolyZone:draw(forceDraw) 129 | if not forceDraw and not self.debugPoly and not self.debugGrid then return end 130 | 131 | local zDrawDist = 45.0 132 | local oColor = self.debugColors.outline or defaultColorOutline 133 | local oR, oG, oB = oColor[1], oColor[2], oColor[3] 134 | local wColor = self.debugColors.walls or defaultColorWalls 135 | local wR, wG, wB = wColor[1], wColor[2], wColor[3] 136 | local plyPed = PlayerPedId() 137 | local plyPos = GetEntityCoords(plyPed) 138 | local minZ = self.minZ or plyPos.z - zDrawDist 139 | local maxZ = self.maxZ or plyPos.z + zDrawDist 140 | 141 | local points = self.points 142 | for i=1, #points do 143 | local point = self:TransformPoint(points[i]) 144 | DrawLine(point.x, point.y, minZ, point.x, point.y, maxZ, oR, oG, oB, 164) 145 | 146 | if i < #points then 147 | local p2 = self:TransformPoint(points[i+1]) 148 | DrawLine(point.x, point.y, maxZ, p2.x, p2.y, maxZ, oR, oG, oB, 184) 149 | _drawWall(point, p2, minZ, maxZ, wR, wG, wB, 48) 150 | end 151 | end 152 | 153 | if #points > 2 then 154 | local firstPoint = self:TransformPoint(points[1]) 155 | local lastPoint = self:TransformPoint(points[#points]) 156 | DrawLine(firstPoint.x, firstPoint.y, maxZ, lastPoint.x, lastPoint.y, maxZ, oR, oG, oB, 184) 157 | _drawWall(firstPoint, lastPoint, minZ, maxZ, wR, wG, wB, 48) 158 | end 159 | end 160 | 161 | function PolyZone.drawPoly(poly, forceDraw) 162 | PolyZone.draw(poly, forceDraw) 163 | end 164 | 165 | -- Debug drawing all grid cells that are completly within the polygon 166 | local function _drawGrid(poly) 167 | local minZ = poly.minZ 168 | local maxZ = poly.maxZ 169 | if not minZ or not maxZ then 170 | local plyPed = PlayerPedId() 171 | local plyPos = GetEntityCoords(plyPed) 172 | minZ = plyPos.z - 46.0 173 | maxZ = plyPos.z - 45.0 174 | end 175 | 176 | local lines = poly.lines 177 | local color = poly.debugColors.grid or defaultColorGrid 178 | local r, g, b = color[1], color[2], color[3] 179 | for i=1, #lines do 180 | local line = lines[i] 181 | local min = line.min 182 | local max = line.max 183 | DrawLine(min.x + 0.0, min.y + 0.0, maxZ + 0.0, max.x + 0.0, max.y + 0.0, maxZ + 0.0, r, g, b, 196) 184 | end 185 | end 186 | 187 | 188 | local function _pointInPoly(point, poly) 189 | local x = point.x 190 | local y = point.y 191 | local min = poly.min 192 | local minX = min.x 193 | local minY = min.y 194 | local max = poly.max 195 | 196 | -- Checks if point is within the polygon's bounding box 197 | if x < minX or 198 | x > max.x or 199 | y < minY or 200 | y > max.y then 201 | return false 202 | end 203 | 204 | -- Checks if point is within the polygon's height bounds 205 | local minZ = poly.minZ 206 | local maxZ = poly.maxZ 207 | local z = point.z 208 | if (minZ and z < minZ) or (maxZ and z > maxZ) then 209 | return false 210 | end 211 | 212 | -- Returns true if the grid cell associated with the point is entirely inside the poly 213 | local grid = poly.grid 214 | if grid then 215 | local gridDivisions = poly.gridDivisions 216 | local size = poly.size 217 | local gridPosX = x - minX 218 | local gridPosY = y - minY 219 | local gridCellX = (gridPosX * gridDivisions) // size.x 220 | local gridCellY = (gridPosY * gridDivisions) // size.y 221 | local gridCellValue = grid[gridCellY + 1][gridCellX + 1] 222 | if gridCellValue == nil and poly.lazyGrid then 223 | gridCellValue = _isGridCellInsidePoly(gridCellX, gridCellY, poly) 224 | grid[gridCellY + 1][gridCellX + 1] = gridCellValue 225 | end 226 | if gridCellValue then return true end 227 | end 228 | 229 | return _windingNumber(point, poly.points) 230 | end 231 | 232 | 233 | -- Grid creation functions 234 | -- Calculates the points of the rectangle that make up the grid cell at grid position (cellX, cellY) 235 | local function _calculateGridCellPoints(cellX, cellY, poly) 236 | local gridCellWidth = poly.gridCellWidth 237 | local gridCellHeight = poly.gridCellHeight 238 | local min = poly.min 239 | -- min added to initial point, in order to shift the grid cells to the poly's starting position 240 | local x = cellX * gridCellWidth + min.x 241 | local y = cellY * gridCellHeight + min.y 242 | return { 243 | vector2(x, y), 244 | vector2(x + gridCellWidth, y), 245 | vector2(x + gridCellWidth, y + gridCellHeight), 246 | vector2(x, y + gridCellHeight), 247 | vector2(x, y) 248 | } 249 | end 250 | 251 | 252 | function _isGridCellInsidePoly(cellX, cellY, poly) 253 | gridCellPoints = _calculateGridCellPoints(cellX, cellY, poly) 254 | local polyPoints = {table.unpack(poly.points)} 255 | -- Connect the polygon to its starting point 256 | polyPoints[#polyPoints + 1] = polyPoints[1] 257 | 258 | -- If none of the points of the grid cell are in the polygon, the grid cell can't be in it 259 | local isOnePointInPoly = false 260 | for i=1, #gridCellPoints - 1 do 261 | local cellPoint = gridCellPoints[i] 262 | local x = cellPoint.x 263 | local y = cellPoint.y 264 | if _windingNumber(cellPoint, poly.points) then 265 | isOnePointInPoly = true 266 | -- If we are drawing the grid (poly.lines ~= nil), we need to go through all the points, 267 | -- and therefore can't break out of the loop early 268 | if poly.lines then 269 | if not poly.gridXPoints[x] then poly.gridXPoints[x] = {} end 270 | if not poly.gridYPoints[y] then poly.gridYPoints[y] = {} end 271 | poly.gridXPoints[x][y] = true 272 | poly.gridYPoints[y][x] = true 273 | else break end 274 | end 275 | end 276 | if isOnePointInPoly == false then 277 | return false 278 | end 279 | 280 | -- If any of the grid cell's lines intersects with any of the polygon's lines 281 | -- then the grid cell is not completely within the poly 282 | for i=1, #gridCellPoints - 1 do 283 | local gridCellP1 = gridCellPoints[i] 284 | local gridCellP2 = gridCellPoints[i+1] 285 | for j=1, #polyPoints - 1 do 286 | if _isIntersecting(gridCellP1, gridCellP2, polyPoints[j], polyPoints[j+1]) then 287 | return false 288 | end 289 | end 290 | end 291 | 292 | return true 293 | end 294 | 295 | 296 | local function _calculateLinesForDrawingGrid(poly) 297 | local lines = {} 298 | for x, tbl in pairs(poly.gridXPoints) do 299 | local yValues = {} 300 | -- Turn dict/set of values into array 301 | for y, _ in pairs(tbl) do yValues[#yValues + 1] = y end 302 | if #yValues >= 2 then 303 | table.sort(yValues) 304 | local minY = yValues[1] 305 | local lastY = yValues[1] 306 | for i=1, #yValues do 307 | local y = yValues[i] 308 | -- Checks for breaks in the grid. If the distance between the last value and the current one 309 | -- is greater than the size of a grid cell, that means the line between them must go outside the polygon. 310 | -- Therefore, a line must be created between minY and the lastY, and a new line started at the current y 311 | if y - lastY > poly.gridCellHeight + 0.01 then 312 | lines[#lines+1] = {min=vector2(x, minY), max=vector2(x, lastY)} 313 | minY = y 314 | elseif i == #yValues then 315 | -- If at the last point, create a line between minY and the last point 316 | lines[#lines+1] = {min=vector2(x, minY), max=vector2(x, y)} 317 | end 318 | lastY = y 319 | end 320 | end 321 | end 322 | -- Setting nil to allow the GC to clear it out of memory, since we no longer need this 323 | poly.gridXPoints = nil 324 | 325 | -- Same as above, but for gridYPoints instead of gridXPoints 326 | for y, tbl in pairs(poly.gridYPoints) do 327 | local xValues = {} 328 | for x, _ in pairs(tbl) do xValues[#xValues + 1] = x end 329 | if #xValues >= 2 then 330 | table.sort(xValues) 331 | local minX = xValues[1] 332 | local lastX = xValues[1] 333 | for i=1, #xValues do 334 | local x = xValues[i] 335 | if x - lastX > poly.gridCellWidth + 0.01 then 336 | lines[#lines+1] = {min=vector2(minX, y), max=vector2(lastX, y)} 337 | minX = x 338 | elseif i == #xValues then 339 | lines[#lines+1] = {min=vector2(minX, y), max=vector2(x, y)} 340 | end 341 | lastX = x 342 | end 343 | end 344 | end 345 | poly.gridYPoints = nil 346 | return lines 347 | end 348 | 349 | 350 | -- Calculate for each grid cell whether it is entirely inside the polygon, and store if true 351 | local function _createGrid(poly, options) 352 | poly.gridArea = 0.0 353 | poly.gridCellWidth = poly.size.x / poly.gridDivisions 354 | poly.gridCellHeight = poly.size.y / poly.gridDivisions 355 | Citizen.CreateThread(function() 356 | -- Calculate all grid cells that are entirely inside the polygon 357 | local isInside = {} 358 | local gridCellArea = poly.gridCellWidth * poly.gridCellHeight 359 | for y=1, poly.gridDivisions do 360 | Citizen.Wait(0) 361 | isInside[y] = {} 362 | for x=1, poly.gridDivisions do 363 | if _isGridCellInsidePoly(x-1, y-1, poly) then 364 | poly.gridArea = poly.gridArea + gridCellArea 365 | isInside[y][x] = true 366 | end 367 | end 368 | end 369 | poly.grid = isInside 370 | poly.gridCoverage = poly.gridArea / poly.area 371 | -- A lot of memory is used by this pre-calc. Force a gc collect after to clear it out 372 | collectgarbage("collect") 373 | 374 | if options.debugGrid then 375 | local coverage = string.format("%.2f", poly.gridCoverage * 100) 376 | print("[PolyZone] Debug: Grid Coverage at " .. coverage .. "% with " .. poly.gridDivisions 377 | .. " divisions. Optimal coverage for memory usage and startup time is 80-90%") 378 | 379 | Citizen.CreateThread(function() 380 | poly.lines = _calculateLinesForDrawingGrid(poly) 381 | -- A lot of memory is used by this pre-calc. Force a gc collect after to clear it out 382 | collectgarbage("collect") 383 | end) 384 | end 385 | end) 386 | end 387 | 388 | 389 | -- Initialization functions 390 | local function _calculatePoly(poly, options) 391 | if not poly.min or not poly.max or not poly.size or not poly.center or not poly.area then 392 | local minX, minY = math.maxinteger, math.maxinteger 393 | local maxX, maxY = math.mininteger, math.mininteger 394 | for _, p in ipairs(poly.points) do 395 | minX = math.min(minX, p.x) 396 | minY = math.min(minY, p.y) 397 | maxX = math.max(maxX, p.x) 398 | maxY = math.max(maxY, p.y) 399 | end 400 | poly.min = vector2(minX, minY) 401 | poly.max = vector2(maxX, maxY) 402 | poly.size = poly.max - poly.min 403 | poly.center = (poly.max + poly.min) / 2 404 | poly.area = _calculatePolygonArea(poly.points) 405 | end 406 | 407 | poly.boundingRadius = math.sqrt(poly.size.y * poly.size.y + poly.size.x * poly.size.x) / 2 408 | 409 | if poly.useGrid and not poly.lazyGrid then 410 | if options.debugGrid then 411 | poly.gridXPoints = {} 412 | poly.gridYPoints = {} 413 | poly.lines = {} 414 | end 415 | _createGrid(poly, options) 416 | elseif poly.useGrid then 417 | local isInside = {} 418 | for y=1, poly.gridDivisions do 419 | isInside[y] = {} 420 | end 421 | poly.grid = isInside 422 | poly.gridCellWidth = poly.size.x / poly.gridDivisions 423 | poly.gridCellHeight = poly.size.y / poly.gridDivisions 424 | end 425 | end 426 | 427 | 428 | local function _initDebug(poly, options) 429 | if options.debugBlip then poly:addDebugBlip() end 430 | local debugEnabled = options.debugPoly or options.debugGrid 431 | if not debugEnabled then 432 | return 433 | end 434 | 435 | Citizen.CreateThread(function() 436 | while not poly.destroyed do 437 | poly:draw(false) 438 | if options.debugGrid and poly.lines then 439 | _drawGrid(poly) 440 | end 441 | Citizen.Wait(0) 442 | end 443 | end) 444 | end 445 | 446 | function PolyZone:new(points, options) 447 | if not points then 448 | print("[PolyZone] Error: Passed nil points table to PolyZone:Create() {name=" .. options.name .. "}") 449 | return 450 | end 451 | if #points < 3 then 452 | print("[PolyZone] Warning: Passed points table with less than 3 points to PolyZone:Create() {name=" .. options.name .. "}") 453 | end 454 | 455 | options = options or {} 456 | local useGrid = options.useGrid 457 | if useGrid == nil then useGrid = true end 458 | local lazyGrid = options.lazyGrid 459 | if lazyGrid == nil then lazyGrid = true end 460 | local poly = { 461 | name = tostring(options.name) or nil, 462 | points = points, 463 | center = options.center, 464 | size = options.size, 465 | max = options.max, 466 | min = options.min, 467 | area = options.area, 468 | minZ = tonumber(options.minZ) or nil, 469 | maxZ = tonumber(options.maxZ) or nil, 470 | useGrid = useGrid, 471 | lazyGrid = lazyGrid, 472 | gridDivisions = tonumber(options.gridDivisions) or 30, 473 | debugColors = options.debugColors or {}, 474 | debugPoly = options.debugPoly or false, 475 | debugGrid = options.debugGrid or false, 476 | data = options.data or {}, 477 | isPolyZone = true, 478 | } 479 | if poly.debugGrid then poly.lazyGrid = false end 480 | _calculatePoly(poly, options) 481 | setmetatable(poly, self) 482 | self.__index = self 483 | return poly 484 | end 485 | 486 | function PolyZone:Create(points, options) 487 | local poly = PolyZone:new(points, options) 488 | _initDebug(poly, options) 489 | return poly 490 | end 491 | 492 | function PolyZone:isPointInside(point) 493 | if self.destroyed then 494 | print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}") 495 | return false 496 | end 497 | 498 | return _pointInPoly(point, self) 499 | end 500 | 501 | function PolyZone:destroy() 502 | self.destroyed = true 503 | if self.debugPoly or self.debugGrid then 504 | print("[PolyZone] Debug: Destroying zone {name=" .. self.name .. "}") 505 | end 506 | end 507 | 508 | -- Helper functions 509 | function PolyZone.getPlayerPosition() 510 | return GetEntityCoords(PlayerPedId()) 511 | end 512 | 513 | HeadBone = 0x796e; 514 | function PolyZone.getPlayerHeadPosition() 515 | return GetPedBoneCoords(PlayerPedId(), HeadBone); 516 | end 517 | 518 | function PolyZone.ensureMetatable(zone) 519 | if zone.isComboZone then 520 | setmetatable(zone, ComboZone) 521 | elseif zone.isEntityZone then 522 | setmetatable(zone, EntityZone) 523 | elseif zone.isBoxZone then 524 | setmetatable(zone, BoxZone) 525 | elseif zone.isCircleZone then 526 | setmetatable(zone, CircleZone) 527 | elseif zone.isPolyZone then 528 | setmetatable(zone, PolyZone) 529 | end 530 | end 531 | 532 | function PolyZone:onPointInOut(getPointCb, onPointInOutCb, waitInMS) 533 | -- Localize the waitInMS value for performance reasons (default of 500 ms) 534 | local _waitInMS = 500 535 | if waitInMS ~= nil then _waitInMS = waitInMS end 536 | 537 | Citizen.CreateThread(function() 538 | local isInside = false 539 | while not self.destroyed do 540 | if not self.paused then 541 | local point = getPointCb() 542 | local newIsInside = self:isPointInside(point) 543 | if newIsInside ~= isInside then 544 | onPointInOutCb(newIsInside, point) 545 | isInside = newIsInside 546 | end 547 | end 548 | Citizen.Wait(_waitInMS) 549 | end 550 | end) 551 | end 552 | 553 | function PolyZone:onPlayerInOut(onPointInOutCb, waitInMS) 554 | self:onPointInOut(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS) 555 | end 556 | 557 | function PolyZone:addEvent(eventName) 558 | if self.events == nil then self.events = {} end 559 | local internalEventName = eventPrefix .. eventName 560 | RegisterNetEvent(internalEventName) 561 | self.events[eventName] = AddEventHandler(internalEventName, function (...) 562 | if self:isPointInside(PolyZone.getPlayerPosition()) then 563 | TriggerEvent(eventName, ...) 564 | end 565 | end) 566 | end 567 | 568 | function PolyZone:removeEvent(eventName) 569 | if self.events and self.events[eventName] then 570 | RemoveEventHandler(self.events[eventName]) 571 | self.events[eventName] = nil 572 | end 573 | end 574 | 575 | function PolyZone:addDebugBlip() 576 | return addBlip(self.center or self:getBoundingBoxCenter()) 577 | end 578 | 579 | function PolyZone:setPaused(paused) 580 | self.paused = paused 581 | end 582 | 583 | function PolyZone:isPaused() 584 | return self.paused 585 | end 586 | 587 | function PolyZone:getBoundingBoxMin() 588 | return self.min 589 | end 590 | 591 | function PolyZone:getBoundingBoxMax() 592 | return self.max 593 | end 594 | 595 | function PolyZone:getBoundingBoxSize() 596 | return self.size 597 | end 598 | 599 | function PolyZone:getBoundingBoxCenter() 600 | return self.center 601 | end 602 | -------------------------------------------------------------------------------- /creation/client/BoxZone.lua: -------------------------------------------------------------------------------- 1 | local function handleInput(useZ, heading, length, width, center) 2 | if not useZ then 3 | local scaleDelta, headingDelta = 0.2, 5 4 | BlockWeaponWheelThisFrame() 5 | 6 | if IsDisabledControlPressed(0, 36) then -- ctrl held down 7 | scaleDelta, headingDelta = 0.05, 1 8 | end 9 | 10 | if IsDisabledControlJustPressed(0, 81) then -- scroll wheel down just pressed 11 | 12 | if IsDisabledControlPressed(0, 19) then -- alt held down 13 | return heading, length, math.max(0.0, width - scaleDelta), center 14 | end 15 | if IsDisabledControlPressed(0, 21) then -- shift held down 16 | return heading, math.max(0.0, length - scaleDelta), width, center 17 | end 18 | return (heading - headingDelta) % 360, length, width, center 19 | end 20 | 21 | 22 | if IsDisabledControlJustPressed(0, 99) then -- scroll wheel up just pressed 23 | 24 | if IsDisabledControlPressed(0, 19) then -- alt held down 25 | return heading, length, math.max(0.0, width + scaleDelta), center 26 | end 27 | if IsDisabledControlPressed(0, 21) then -- shift held down 28 | return heading, math.max(0.0, length + scaleDelta), width, center 29 | end 30 | return (heading + headingDelta) % 360, length, width, center 31 | end 32 | end 33 | 34 | local rot = GetGameplayCamRot(2) 35 | center = handleArrowInput(center, rot.z) 36 | 37 | return heading, length, width, center 38 | end 39 | 40 | function handleZ(minZ, maxZ) 41 | local delta = 0.2 42 | 43 | if IsDisabledControlPressed(0, 36) then -- ctrl held down 44 | delta = 0.05 45 | end 46 | 47 | BlockWeaponWheelThisFrame() 48 | 49 | if IsDisabledControlJustPressed(0, 81) then -- scroll wheel down just pressed 50 | 51 | if IsDisabledControlPressed(0, 19) then -- alt held down 52 | return minZ - delta, maxZ 53 | end 54 | if IsDisabledControlPressed(0, 21) then -- shift held down 55 | return minZ, maxZ - delta 56 | end 57 | return minZ - delta, maxZ - delta 58 | end 59 | 60 | if IsDisabledControlJustPressed(0, 99) then -- scroll wheel up just pressed 61 | 62 | if IsDisabledControlPressed(0, 19) then -- alt held down 63 | return minZ + delta, maxZ 64 | end 65 | if IsDisabledControlPressed(0, 21) then -- shift held down 66 | return minZ, maxZ + delta 67 | end 68 | return minZ + delta, maxZ + delta 69 | end 70 | return minZ, maxZ 71 | end 72 | 73 | function boxStart(name, heading, length, width, minHeight, maxHeight) 74 | local center = GetEntityCoords(PlayerPedId()) 75 | createdZone = BoxZone:Create(center, length, width, {name = tostring(name)}) 76 | local useZ, minZ, maxZ = false, center.z - 1.0, center.z + 3.0 77 | if minHeight then 78 | minZ = center.z - minHeight 79 | createdZone.minZ = minZ 80 | end 81 | if maxHeight then 82 | maxZ = center.z + maxHeight 83 | createdZone.maxZ = maxZ 84 | end 85 | Citizen.CreateThread(function() 86 | while createdZone do 87 | if IsDisabledControlJustPressed(0, 20) then -- Z pressed 88 | useZ = not useZ 89 | if useZ then 90 | createdZone.debugColors.walls = {255, 0, 0} 91 | else 92 | createdZone.debugColors.walls = {0, 255, 0} 93 | end 94 | end 95 | heading, length, width, center = handleInput(useZ, heading, length, width, center) 96 | if useZ then 97 | minZ, maxZ = handleZ(minZ, maxZ) 98 | createdZone.minZ = minZ 99 | createdZone.maxZ = maxZ 100 | end 101 | createdZone:setLength(length) 102 | createdZone:setWidth(width) 103 | createdZone:setHeading(heading) 104 | createdZone:setCenter(center) 105 | Wait(0) 106 | end 107 | end) 108 | end 109 | 110 | function boxFinish() 111 | TriggerServerEvent("polyzone:printBox", 112 | {name=createdZone.name, center=createdZone.center, length=createdZone.length, width=createdZone.width, heading=createdZone.offsetRot, minZ=createdZone.minZ, maxZ=createdZone.maxZ}) 113 | end -------------------------------------------------------------------------------- /creation/client/CircleZone.lua: -------------------------------------------------------------------------------- 1 | local function handleInput(radius, center, useZ) 2 | local delta = 0.05 3 | BlockWeaponWheelThisFrame() 4 | 5 | if IsDisabledControlPressed(0, 36) then -- ctrl held down 6 | delta = 0.01 7 | end 8 | 9 | if IsDisabledControlJustPressed(0, 81) then -- scroll wheel down just pressed 10 | 11 | if IsDisabledControlPressed(0, 19) then -- alt held down 12 | return radius, vector3(center.x, center.y, center.z - delta), useZ 13 | end 14 | return math.max(0.0, radius - delta), center, useZ 15 | end 16 | 17 | 18 | if IsDisabledControlJustPressed(0, 99) then -- scroll wheel up just pressed 19 | 20 | if IsDisabledControlPressed(0, 19) then -- alt held down 21 | return radius, vector3(center.x, center.y, center.z + delta), useZ 22 | end 23 | return radius + delta, center, useZ 24 | end 25 | 26 | if IsDisabledControlJustPressed(0, 20) then -- Z pressed 27 | return radius, center, not useZ 28 | end 29 | 30 | local rot = GetGameplayCamRot(2) 31 | center = handleArrowInput(center, rot.z) 32 | 33 | return radius, center, useZ 34 | end 35 | 36 | function circleStart(name, radius, useZ) 37 | local center = GetEntityCoords(PlayerPedId()) 38 | useZ = useZ or false 39 | createdZone = CircleZone:Create(center, radius, {name = tostring(name), useZ = useZ}) 40 | Citizen.CreateThread(function() 41 | while createdZone do 42 | radius, center, useZ = handleInput(radius, center, useZ) 43 | createdZone:setRadius(radius) 44 | createdZone:setCenter(center) 45 | createdZone.useZ = useZ 46 | Wait(0) 47 | end 48 | end) 49 | end 50 | 51 | function circleFinish() 52 | TriggerServerEvent("polyzone:printCircle", 53 | {name=createdZone.name, center=createdZone.center, radius=createdZone.radius, useZ=createdZone.useZ}) 54 | end -------------------------------------------------------------------------------- /creation/client/PolyZone.lua: -------------------------------------------------------------------------------- 1 | local minZ, maxZ = nil, nil 2 | 3 | local function handleInput(center) 4 | local rot = GetGameplayCamRot(2) 5 | center = handleArrowInput(center, rot.z) 6 | return center 7 | end 8 | 9 | function polyStart(name) 10 | local coords = GetEntityCoords(PlayerPedId()) 11 | createdZone = PolyZone:Create({vector2(coords.x, coords.y)}, {name = tostring(name), useGrid=false}) 12 | Citizen.CreateThread(function() 13 | while createdZone do 14 | -- Have to convert the point to a vector3 prior to calling handleInput, 15 | -- then convert it back to vector2 afterwards 16 | lastPoint = createdZone.points[#createdZone.points] 17 | lastPoint = vector3(lastPoint.x, lastPoint.y, 0.0) 18 | lastPoint = handleInput(lastPoint) 19 | createdZone.points[#createdZone.points] = lastPoint.xy 20 | Wait(0) 21 | end 22 | end) 23 | minZ, maxZ = coords.z, coords.z 24 | end 25 | 26 | function polyFinish() 27 | TriggerServerEvent("polyzone:printPoly", 28 | {name=createdZone.name, points=createdZone.points, minZ=minZ, maxZ=maxZ}) 29 | end 30 | 31 | RegisterNetEvent("polyzone:pzadd") 32 | AddEventHandler("polyzone:pzadd", function() 33 | if createdZone == nil or createdZoneType ~= 'poly' then 34 | return 35 | end 36 | 37 | local coords = GetEntityCoords(PlayerPedId()) 38 | 39 | if (coords.z > maxZ) then 40 | maxZ = coords.z 41 | end 42 | 43 | if (coords.z < minZ) then 44 | minZ = coords.z 45 | end 46 | 47 | createdZone.points[#createdZone.points + 1] = vector2(coords.x, coords.y) 48 | end) 49 | 50 | RegisterNetEvent("polyzone:pzundo") 51 | AddEventHandler("polyzone:pzundo", function() 52 | if createdZone == nil or createdZoneType ~= 'poly' then 53 | return 54 | end 55 | 56 | createdZone.points[#createdZone.points] = nil 57 | if #createdZone.points == 0 then 58 | TriggerEvent("polyzone:pzcancel") 59 | end 60 | end) -------------------------------------------------------------------------------- /creation/client/commands.lua: -------------------------------------------------------------------------------- 1 | RegisterCommand("pzcreate", function(src, args) 2 | local zoneType = args[1] 3 | if zoneType == nil then 4 | TriggerEvent('chat:addMessage', { 5 | color = { 255, 0, 0}, 6 | multiline = true, 7 | args = {"Me", "Please add zone type to create (poly, circle, box)!"} 8 | }) 9 | return 10 | end 11 | if zoneType ~= 'poly' and zoneType ~= 'circle' and zoneType ~= 'box' then 12 | TriggerEvent('chat:addMessage', { 13 | color = { 255, 0, 0}, 14 | multiline = true, 15 | args = {"Me", "Zone type must be one of: poly, circle, box"} 16 | }) 17 | return 18 | end 19 | local name = nil 20 | if #args >= 2 then name = args[2] 21 | else name = GetUserInput("Enter name of zone:") end 22 | if name == nil or name == "" then 23 | TriggerEvent('chat:addMessage', { 24 | color = { 255, 0, 0}, 25 | multiline = true, 26 | args = {"Me", "Please add a name!"} 27 | }) 28 | return 29 | end 30 | TriggerEvent("polyzone:pzcreate", zoneType, name, args) 31 | end) 32 | 33 | RegisterCommand("pzadd", function(src, args) 34 | TriggerEvent("polyzone:pzadd") 35 | end) 36 | 37 | RegisterCommand("pzundo", function(src, args) 38 | TriggerEvent("polyzone:pzundo") 39 | end) 40 | 41 | RegisterCommand("pzfinish", function(src, args) 42 | TriggerEvent("polyzone:pzfinish") 43 | end) 44 | 45 | RegisterCommand("pzlast", function(src, args) 46 | TriggerEvent("polyzone:pzlast") 47 | end) 48 | 49 | RegisterCommand("pzcancel", function(src, args) 50 | TriggerEvent("polyzone:pzcancel") 51 | end) 52 | 53 | RegisterCommand("pzcomboinfo", function (src, args) 54 | TriggerEvent("polyzone:pzcomboinfo") 55 | end) 56 | 57 | Citizen.CreateThread(function() 58 | TriggerEvent('chat:addSuggestion', '/pzcreate', 'Starts creation of a zone for PolyZone of one of the available types: circle, box, poly', { 59 | {name="zoneType", help="Zone Type (required)"}, 60 | }) 61 | 62 | TriggerEvent('chat:addSuggestion', '/pzadd', 'Adds point to zone.', {}) 63 | TriggerEvent('chat:addSuggestion', '/pzundo', 'Undoes the last point added.', {}) 64 | TriggerEvent('chat:addSuggestion', '/pzfinish', 'Finishes and prints zone.', {}) 65 | TriggerEvent('chat:addSuggestion', '/pzlast', 'Starts creation of the last zone you finished (only works on BoxZone and CircleZone)', {}) 66 | TriggerEvent('chat:addSuggestion', '/pzcancel', 'Cancel zone creation.', {}) 67 | TriggerEvent('chat:addSuggestion', '/pzcomboinfo', 'Prints some useful info for all created ComboZones', {}) 68 | end) -------------------------------------------------------------------------------- /creation/client/creation.lua: -------------------------------------------------------------------------------- 1 | lastCreatedZoneType = nil 2 | lastCreatedZone = nil 3 | createdZoneType = nil 4 | createdZone = nil 5 | drawZone = false 6 | 7 | RegisterNetEvent("polyzone:pzcreate") 8 | AddEventHandler("polyzone:pzcreate", function(zoneType, name, args) 9 | if createdZone ~= nil then 10 | TriggerEvent('chat:addMessage', { 11 | color = { 255, 0, 0}, 12 | multiline = true, 13 | args = {"Me", "A shape is already being created!"} 14 | }) 15 | return 16 | end 17 | 18 | if zoneType == 'poly' then 19 | polyStart(name) 20 | elseif zoneType == "circle" then 21 | local radius = nil 22 | if #args >= 3 then radius = tonumber(args[3]) 23 | else radius = tonumber(GetUserInput("Enter radius:")) end 24 | if radius == nil then 25 | TriggerEvent('chat:addMessage', { 26 | color = { 255, 0, 0}, 27 | multiline = true, 28 | args = {"Me", "CircleZone requires a radius (must be a number)!"} 29 | }) 30 | return 31 | end 32 | circleStart(name, radius) 33 | elseif zoneType == "box" then 34 | local length = nil 35 | if #args >= 3 then length = tonumber(args[3]) 36 | else length = tonumber(GetUserInput("Enter length:")) end 37 | if length == nil or length < 0.0 then 38 | TriggerEvent('chat:addMessage', { 39 | color = { 255, 0, 0}, 40 | multiline = true, 41 | args = {"Me", "BoxZone requires a length (must be a positive number)!"} 42 | }) 43 | return 44 | end 45 | local width = nil 46 | if #args >= 4 then width = tonumber(args[4]) 47 | else width = tonumber(GetUserInput("Enter width:")) end 48 | if width == nil or width < 0.0 then 49 | TriggerEvent('chat:addMessage', { 50 | color = { 255, 0, 0}, 51 | multiline = true, 52 | args = {"Me", "BoxZone requires a width (must be a positive number)!"} 53 | }) 54 | return 55 | end 56 | boxStart(name, 0, length, width) 57 | else 58 | return 59 | end 60 | createdZoneType = zoneType 61 | drawZone = true 62 | disableControlKeyInput() 63 | drawThread() 64 | end) 65 | 66 | RegisterNetEvent("polyzone:pzfinish") 67 | AddEventHandler("polyzone:pzfinish", function() 68 | if createdZone == nil then 69 | return 70 | end 71 | 72 | if createdZoneType == 'poly' then 73 | polyFinish() 74 | elseif createdZoneType == "circle" then 75 | circleFinish() 76 | elseif createdZoneType == "box" then 77 | boxFinish() 78 | end 79 | 80 | TriggerEvent('chat:addMessage', { 81 | color = { 0, 255, 0}, 82 | multiline = true, 83 | args = {"Me", "Check PolyZone's root folder for polyzone_created_zones.txt to get the zone!"} 84 | }) 85 | 86 | lastCreatedZoneType = createdZoneType 87 | lastCreatedZone = createdZone 88 | 89 | drawZone = false 90 | createdZone = nil 91 | createdZoneType = nil 92 | end) 93 | 94 | RegisterNetEvent("polyzone:pzlast") 95 | AddEventHandler("polyzone:pzlast", function() 96 | if createdZone ~= nil or lastCreatedZone == nil then 97 | return 98 | end 99 | if lastCreatedZoneType == 'poly' then 100 | TriggerEvent('chat:addMessage', { 101 | color = { 0, 255, 0}, 102 | multiline = true, 103 | args = {"Me", "The command pzlast only supports BoxZone and CircleZone for now"} 104 | }) 105 | end 106 | 107 | local name = GetUserInput("Enter name (or leave empty to reuse last zone's name):") 108 | if name == nil then 109 | return 110 | elseif name == "" then 111 | name = lastCreatedZone.name 112 | end 113 | createdZoneType = lastCreatedZoneType 114 | if createdZoneType == 'box' then 115 | local minHeight, maxHeight 116 | if lastCreatedZone.minZ then 117 | minHeight = lastCreatedZone.center.z - lastCreatedZone.minZ 118 | end 119 | if lastCreatedZone.maxZ then 120 | maxHeight = lastCreatedZone.maxZ - lastCreatedZone.center.z 121 | end 122 | boxStart(name, lastCreatedZone.offsetRot, lastCreatedZone.length, lastCreatedZone.width, minHeight, maxHeight) 123 | elseif createdZoneType == 'circle' then 124 | circleStart(name, lastCreatedZone.radius, lastCreatedZone.useZ) 125 | end 126 | drawZone = true 127 | disableControlKeyInput() 128 | drawThread() 129 | end) 130 | 131 | RegisterNetEvent("polyzone:pzcancel") 132 | AddEventHandler("polyzone:pzcancel", function() 133 | if createdZone == nil then 134 | return 135 | end 136 | 137 | TriggerEvent('chat:addMessage', { 138 | color = {255, 0, 0}, 139 | multiline = true, 140 | args = {"Me", "Zone creation canceled!"} 141 | }) 142 | 143 | drawZone = false 144 | createdZone = nil 145 | createdZoneType = nil 146 | end) 147 | 148 | -- Drawing 149 | function drawThread() 150 | Citizen.CreateThread(function() 151 | while drawZone do 152 | if createdZone then 153 | createdZone:draw(true) 154 | end 155 | Wait(0) 156 | end 157 | end) 158 | end 159 | -------------------------------------------------------------------------------- /creation/client/utils.lua: -------------------------------------------------------------------------------- 1 | -- GetUserInput function inspired by vMenu (https://github.com/TomGrobbe/vMenu/blob/master/vMenu/CommonFunctions.cs) 2 | function GetUserInput(windowTitle, defaultText, maxInputLength) 3 | -- Create the window title string. 4 | local resourceName = string.upper(GetCurrentResourceName()) 5 | local textEntry = resourceName .. "_WINDOW_TITLE" 6 | if windowTitle == nil then 7 | windowTitle = "Enter:" 8 | end 9 | AddTextEntry(textEntry, windowTitle) 10 | 11 | -- Display the input box. 12 | DisplayOnscreenKeyboard(1, textEntry, "", defaultText or "", "", "", "", maxInputLength or 30) 13 | Wait(0) 14 | -- Wait for a result. 15 | while true do 16 | local keyboardStatus = UpdateOnscreenKeyboard(); 17 | if keyboardStatus == 3 then -- not displaying input field anymore somehow 18 | return nil 19 | elseif keyboardStatus == 2 then -- cancelled 20 | return nil 21 | elseif keyboardStatus == 1 then -- finished editing 22 | return GetOnscreenKeyboardResult() 23 | else 24 | Wait(0) 25 | end 26 | end 27 | end 28 | 29 | function handleArrowInput(center, heading) 30 | delta = 0.05 31 | 32 | if IsDisabledControlPressed(0, 36) then -- ctrl held down 33 | delta = 0.01 34 | end 35 | 36 | if IsDisabledControlPressed(0, 172) then -- arrow up 37 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x, center.y + delta), heading) 38 | return vector3(newCenter.x, newCenter.y, center.z) 39 | end 40 | 41 | if IsDisabledControlPressed(0, 173) then -- arrow down 42 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x, center.y - delta), heading) 43 | return vector3(newCenter.x, newCenter.y, center.z) 44 | end 45 | 46 | if IsDisabledControlPressed(0, 174) then -- arrow left 47 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x - delta, center.y), heading) 48 | return vector3(newCenter.x, newCenter.y, center.z) 49 | end 50 | 51 | if IsDisabledControlPressed(0, 175) then -- arrow right 52 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x + delta, center.y), heading) 53 | return vector3(newCenter.x, newCenter.y, center.z) 54 | end 55 | 56 | return center 57 | end 58 | 59 | function disableControlKeyInput() 60 | Citizen.CreateThread(function() 61 | while drawZone do 62 | DisableControlAction(0, 36, true) -- Ctrl 63 | DisableControlAction(0, 19, true) -- Alt 64 | DisableControlAction(0, 20, true) -- 'Z' 65 | DisableControlAction(0, 21, true) -- Shift 66 | DisableControlAction(0, 81, true) -- Scroll Wheel Down 67 | DisableControlAction(0, 99, true) -- Scroll Wheel Up 68 | DisableControlAction(0, 172, true) -- Arrow Up 69 | DisableControlAction(0, 173, true) -- Arrow Down 70 | DisableControlAction(0, 174, true) -- Arrow Left 71 | DisableControlAction(0, 175, true) -- Arrow Right 72 | Wait(0) 73 | end 74 | end) 75 | end -------------------------------------------------------------------------------- /creation/server/config.lua: -------------------------------------------------------------------------------- 1 | Config = Config or {} 2 | Config.ConfigFormatEnabled = false 3 | -- Default Format 4 | 5 | -- Name: TestBox | 2022-04-13T22:46:17Z 6 | -- BoxZone:Create(vector3(-344.16, -103.25, 39.02), 1, 1, { 7 | -- name = "TestBox", 8 | -- heading = 0, 9 | -- --debugPoly = true 10 | -- }) 11 | 12 | -- Name: TestCircle | 2022-04-13T22:46:39Z 13 | -- CircleZone:Create(vector3(-344.16, -103.25, 39.02), 1.0, { 14 | -- name = "TestCircle", 15 | -- useZ = false, 16 | -- --debugPoly = true 17 | -- }) 18 | 19 | -- Name: TestPoly | 2022-04-13T22:46:55Z 20 | -- PolyZone:Create({ 21 | -- vector2(-344.15713500977, -103.24993896484), 22 | -- vector2(-343.69491577148, -100.99839019775), 23 | -- vector2(-345.53350830078, -102.00588226318) 24 | -- }, { 25 | -- name = "TestPoly", 26 | -- minZ = 39.015644073486, 27 | -- maxZ = 39.015865325928 28 | -- }) 29 | 30 | -- Config Format 31 | 32 | -- Name: TestBox | 2022-04-13T22:34:48Z 33 | -- coords = vector3(-342.92, -102.09, 39.02), 34 | -- length = 1, 35 | -- width = 1, 36 | -- name = "TestBox", 37 | -- heading = 0, 38 | -- debugPoly = true 39 | 40 | -- Name: TestCircle | 2022-04-13T22:35:09Z 41 | -- coords = vector3(-342.92, -102.09, 39.02), 42 | -- radius = 1.0, 43 | -- name = "TestCircle", 44 | -- useZ = false, 45 | -- debugPoly = true 46 | 47 | -- Name: TestPoly | 2022-04-13T22:35:43Z 48 | -- points = { 49 | -- vector2(-342.91537475586, -102.09281158447), 50 | -- vector2(-344.09732055664, -104.0821762085), 51 | -- vector2(-342.01580810547, -105.60903167725) 52 | -- }, 53 | -- name = "TestPoly", 54 | -- minZ = 39.015701293945, 55 | -- maxZ = 39.015705108643, 56 | -- debugPoly = true 57 | -------------------------------------------------------------------------------- /creation/server/creation.lua: -------------------------------------------------------------------------------- 1 | RegisterNetEvent("polyzone:printPoly") 2 | AddEventHandler("polyzone:printPoly", function(zone) 3 | local created_zones = LoadResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt") or "" 4 | local output = created_zones .. parsePoly(zone) 5 | SaveResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt", output, -1) 6 | end) 7 | 8 | RegisterNetEvent("polyzone:printCircle") 9 | AddEventHandler("polyzone:printCircle", function(zone) 10 | local created_zones = LoadResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt") or "" 11 | local output = created_zones .. parseCircle(zone) 12 | SaveResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt", output, -1) 13 | end) 14 | 15 | RegisterNetEvent("polyzone:printBox") 16 | AddEventHandler("polyzone:printBox", function(zone) 17 | local created_zones = LoadResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt") or "" 18 | local output = created_zones .. parseBox(zone) 19 | SaveResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt", output, -1) 20 | end) 21 | 22 | function round(num, numDecimalPlaces) 23 | local mult = 10^(numDecimalPlaces or 0) 24 | return math.floor(num * mult + 0.5) / mult 25 | end 26 | 27 | function printoutHeader(name) 28 | return "-- Name: " .. name .. " | " .. os.date("!%Y-%m-%dT%H:%M:%SZ\n") 29 | end 30 | 31 | function parsePoly(zone) 32 | if Config.ConfigFormatEnabled then 33 | local printout = printoutHeader(zone.name) 34 | printout = printout .. "points = {\n" 35 | for i = 1, #zone.points do 36 | if i ~= #zone.points then 37 | printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) .."),\n" 38 | else 39 | printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) ..")\n" 40 | end 41 | end 42 | printout = printout .. "},\nname = \"" .. zone.name .. "\",\n--minZ = " .. zone.minZ .. ",\n--maxZ = " .. zone.maxZ .. ",\n--debugPoly = true\n\n" 43 | return printout 44 | else 45 | local printout = printoutHeader(zone.name) 46 | printout = printout .. "PolyZone:Create({\n" 47 | for i = 1, #zone.points do 48 | if i ~= #zone.points then 49 | printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) .."),\n" 50 | else 51 | printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) ..")\n" 52 | end 53 | end 54 | printout = printout .. "}, {\n name = \"" .. zone.name .. "\",\n --minZ = " .. zone.minZ .. ",\n --maxZ = " .. zone.maxZ .. "\n})\n\n" 55 | return printout 56 | end 57 | end 58 | 59 | function parseCircle(zone) 60 | if Config.ConfigFormatEnabled then 61 | local printout = printoutHeader(zone.name) 62 | printout = printout .. "coords = " 63 | printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."),\n" 64 | printout = printout .. "radius = " .. tostring(zone.radius) .. ",\n" 65 | printout = printout .. "name = \"" .. zone.name .. "\",\nuseZ = " .. tostring(zone.useZ) .. ",\n--debugPoly = true\n\n" 66 | return printout 67 | else 68 | local printout = printoutHeader(zone.name) 69 | printout = printout .. "CircleZone:Create(" 70 | printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."), " 71 | printout = printout .. tostring(zone.radius) .. ", " 72 | printout = printout .. "{\n name = \"" .. zone.name .. "\",\n useZ = " .. tostring(zone.useZ) .. ",\n --debugPoly = true\n})\n\n" 73 | return printout 74 | end 75 | end 76 | 77 | function parseBox(zone) 78 | if Config.ConfigFormatEnabled then 79 | local printout = printoutHeader(zone.name) 80 | printout = printout .. "coords = " 81 | printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."),\n" 82 | printout = printout .. "length = " .. tostring(zone.length) .. ",\n" 83 | printout = printout .. "width = " .. tostring(zone.width) .. ",\n" 84 | printout = printout .. "name = \"" .. zone.name .. "\",\nheading = " .. zone.heading .. ",\n--debugPoly = true" 85 | if zone.minZ then 86 | printout = printout .. ",\nminZ = " .. tostring(round(zone.minZ, 2)) 87 | end 88 | if zone.maxZ then 89 | printout = printout .. ",\nmaxZ = " .. tostring(round(zone.maxZ, 2)) 90 | end 91 | printout = printout .. "\n\n" 92 | return printout 93 | else 94 | local printout = printoutHeader(zone.name) 95 | printout = printout .. "BoxZone:Create(" 96 | printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."), " 97 | printout = printout .. tostring(zone.length) .. ", " 98 | printout = printout .. tostring(zone.width) .. ", " 99 | printout = printout .. "{\n name = \"" .. zone.name .. "\",\n heading = " .. zone.heading .. ",\n --debugPoly = true" 100 | if zone.minZ then 101 | printout = printout .. ",\n minZ = " .. tostring(round(zone.minZ, 2)) 102 | end 103 | if zone.maxZ then 104 | printout = printout .. ",\n maxZ = " .. tostring(round(zone.maxZ, 2)) 105 | end 106 | printout = printout .. "\n})\n\n" 107 | return printout 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | games {'gta5'} 2 | 3 | fx_version 'cerulean' 4 | 5 | description 'Define zones of different shapes and test whether a point is inside or outside of the zone' 6 | version '2.6.2' 7 | 8 | client_scripts { 9 | 'client.lua', 10 | 'BoxZone.lua', 11 | 'EntityZone.lua', 12 | 'CircleZone.lua', 13 | 'ComboZone.lua', 14 | 'creation/client/*.lua' 15 | } 16 | 17 | server_scripts { 18 | 'creation/server/*.lua', 19 | 'server.lua' 20 | } 21 | -------------------------------------------------------------------------------- /server.lua: -------------------------------------------------------------------------------- 1 | local eventPrefix = '__PolyZone__:' 2 | 3 | function triggerZoneEvent(eventName, ...) 4 | TriggerClientEvent(eventPrefix .. eventName, -1, ...) 5 | end 6 | 7 | RegisterNetEvent("PolyZone:TriggerZoneEvent") 8 | AddEventHandler("PolyZone:TriggerZoneEvent", triggerZoneEvent) 9 | 10 | exports("TriggerZoneEvent", triggerZoneEvent) --------------------------------------------------------------------------------