Files
red-valley/cache/files/PolyZone/resource.rpf

1985 lines
78 KiB
Plaintext
Raw Normal View History

2026-03-29 21:41:17 +03:00
RPF2<00><00><00>@
0@
<00>+@<00>++\p\:<00>I<00><00>IE<00><00>|0|N <00>US<00>Sal<00>lp<00><00><00>}<00> <00><00> <00><00><00><00><00>
<00>
/BoxZone.luaCircleZone.luaComboZone.luaEntityZone.luaclient.luacreationclientBoxZone.luaCircleZone.luaPolyZone.luacommands.luacreation.luautils.luafxmanifest.luaBoxZone = {}
-- Inherits from PolyZone
setmetatable(BoxZone, { __index = PolyZone })
-- Utility functions
local rad, cos, sin = math.rad, math.cos, math.sin
function PolyZone.rotate(origin, point, theta)
if theta == 0.0 then return point end
local p = point - origin
local pX, pY = p.x, p.y
theta = rad(theta)
local cosTheta = cos(theta)
local sinTheta = sin(theta)
local x = pX * cosTheta - pY * sinTheta
local y = pX * sinTheta + pY * cosTheta
return vector2(x, y) + origin
end
function BoxZone.calculateMinAndMaxZ(minZ, maxZ, scaleZ, offsetZ)
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
if (minZ == nil and maxZ == nil) or (minScaleZ == 1.0 and maxScaleZ == 1.0 and minOffsetZ == 0.0 and maxOffsetZ == 0.0) then
return minZ, maxZ
end
if minScaleZ ~= 1.0 or maxScaleZ ~= 1.0 then
if minZ ~= nil and maxZ ~= nil then
local halfHeight = (maxZ - minZ) / 2
local centerZ = minZ + halfHeight
minZ = centerZ - halfHeight * minScaleZ
maxZ = centerZ + halfHeight * maxScaleZ
else
print(string.format(
"[PolyZone] Warning: The minZ/maxZ of a BoxZone can only be scaled if both minZ and maxZ are non-nil (minZ=%s, maxZ=%s)",
tostring(minZ),
tostring(maxZ)
))
end
end
if minZ then minZ = minZ - minOffsetZ end
if maxZ then maxZ = maxZ + maxOffsetZ end
return minZ, maxZ
end
local function _calculateScaleAndOffset(options)
-- Scale and offset tables are both formatted as {forward, back, left, right, up, down}
-- or if symmetrical {forward/back, left/right, up/down}
local scale = options.scale or {1.0, 1.0, 1.0, 1.0, 1.0, 1.0}
local offset = options.offset or {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}
assert(#scale == 3 or #scale == 6, "Scale must be of length 3 or 6")
assert(#offset == 3 or #offset == 6, "Offset must be of length 3 or 6")
if #scale == 3 then
scale = {scale[1], scale[1], scale[2], scale[2], scale[3], scale[3]}
end
if #offset == 3 then
offset = {offset[1], offset[1], offset[2], offset[2], offset[3], offset[3]}
end
local minOffset = vector3(offset[3], offset[2], offset[6])
local maxOffset = vector3(offset[4], offset[1], offset[5])
local minScale = vector3(scale[3], scale[2], scale[6])
local maxScale = vector3(scale[4], scale[1], scale[5])
return minOffset, maxOffset, minScale, maxScale
end
local function _calculatePoints(center, length, width, minScale, maxScale, minOffset, maxOffset)
local halfLength, halfWidth = length / 2, width / 2
local min = vector3(-halfWidth, -halfLength, 0.0)
local max = vector3(halfWidth, halfLength, 0.0)
min = min * minScale - minOffset
max = max * maxScale + maxOffset
-- Box vertices
local p1 = center.xy + vector2(min.x, min.y)
local p2 = center.xy + vector2(max.x, min.y)
local p3 = center.xy + vector2(max.x, max.y)
local p4 = center.xy + vector2(min.x, max.y)
return {p1, p2, p3, p4}
end
-- Debug drawing functions
function BoxZone:TransformPoint(point)
-- Overriding TransformPoint function to take into account rotation and position offset
return PolyZone.rotate(self.startPos, point, self.offsetRot) + self.offsetPos
end
-- Initialization functions
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(false)
Citizen.Wait(0)
end
end)
end
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)
local defaultScaleZ, defaultOffsetZ = {defaultMinScale.z, defaultMaxScale.z}, {defaultMinOffset.z, defaultMaxOffset.z}
function BoxZone:new(center, length, width, options)
local minOffset, maxOffset, minScale, maxScale = defaultMinOffset, defaultMaxOffset, defaultMinScale, defaultMaxScale
local scaleZ, offsetZ = defaultScaleZ, defaultOffsetZ
if options.scale ~= nil or options.offset ~= nil then
minOffset, maxOffset, minScale, maxScale = _calculateScaleAndOffset(options)
scaleZ, offsetZ = {minScale.z, maxScale.z}, {minOffset.z, maxOffset.z}
end
local points = _calculatePoints(center, length, width, minScale, maxScale, minOffset, maxOffset)
local min = points[1]
local max = points[3]
local size = max - min
local minZ, maxZ = BoxZone.calculateMinAndMaxZ(options.minZ, options.maxZ, scaleZ, offsetZ)
options.minZ = minZ
options.maxZ = maxZ
-- Box Zones don't use the grid optimization because they are already rectangles/cubes
options.useGrid = false
-- Pre-setting all these values to avoid PolyZone:new() having to calculate them
options.min = min
options.max = max
options.size = size
options.center = center
options.area = size.x * size.y
local zone = PolyZone:new(points, options)
zone.length = length
zone.width = width
zone.startPos = center.xy
zone.offsetPos = vector2(0.0, 0.0)
zone.offsetRot = options.heading or 0.0
zone.minScale, zone.maxScale = minScale, maxScale
zone.minOffset, zone.maxOffset = minOffset, maxOffset
zone.scaleZ, zone.offsetZ = scaleZ, offsetZ
zone.isBoxZone = true
setmetatable(zone, self)
self.__index = self
return zone
end
function BoxZone:Create(center, length, width, options)
local zone = BoxZone:new(center, length, width, options)
_initDebug(zone, options)
return zone
end
-- Helper functions
function BoxZone:isPointInside(point)
if self.destroyed then
print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}")
return false
end
local startPos = self.startPos
local actualPos = point.xy - self.offsetPos
if #(actualPos - startPos) > self.boundingRadius then
return false
end
local rotatedPoint = PolyZone.rotate(startPos, actualPos, -self.offsetRot)
local pX, pY, pZ = rotatedPoint.x, rotatedPoint.y, point.z
local min, max = self.min, self.max
local minX, minY, maxX, maxY = min.x, min.y, max.x, max.y
local minZ, maxZ = self.minZ, self.maxZ
if pX < minX or pX > maxX or pY < minY or pY > maxY then
return false
end
if (minZ and pZ < minZ) or (maxZ and pZ > maxZ) then
return false
end
return true
end
function BoxZone:getHeading()
return self.offsetRot
end
function BoxZone:setHeading(heading)
if not heading then
return
end
self.offsetRot = heading
end
function BoxZone:setCenter(center)
if not center or center == self.center then
return
end
self.center = center
self.startPos = center.xy
self.points = _calculatePoints(self.center, self.length, self.width, self.minScale, self.maxScale, self.minOffset, self.maxOffset)
end
function BoxZone:getLength()
return self.length
end
function BoxZone:setLength(length)
if not length or length == self.length then
return
end
self.length = length
self.points = _calculatePoints(self.center, self.length, self.width, self.minScale, self.maxScale, self.minOffset, self.maxOffset)
end
function BoxZone:getWidth()
return self.width
end
function BoxZone:setWidth(width)
if not width or width == self.width then
return
end
self.width = width
self.points = _calculatePoints(self.center, self.length, self.width, self.minScale, self.maxScale, self.minOffset, self.maxOffset)
end
CircleZone = {}
-- Inherits from PolyZone
setmetatable(CircleZone, { __index = PolyZone })
function CircleZone:draw(forceDraw)
if not forceDraw and not self.debugPoly then return end
local center = self.center
local debugColor = self.debugColor
local r, g, b = debugColor[1], debugColor[2], debugColor[3]
if self.useZ then
local radius = self.radius
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)
else
local diameter = self.diameter
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)
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(false)
Citizen.Wait(0)
end
end)
end
function CircleZone:new(center, radius, options)
options = options or {}
local zone = {
name = tostring(options.name) or nil,
center = center,
radius = radius + 0.0,
diameter = radius * 2.0,
useZ = options.useZ or false,
debugPoly = options.debugPoly or false,
debugColor = options.debugColor or {0, 255, 0},
data = options.data or {},
isCircleZone = true,
}
if zone.useZ then
assert(type(zone.center) == "vector3", "Center must be vector3 if useZ is true {center=" .. center .. "}")
end
setmetatable(zone, self)
self.__index = self
return zone
end
function CircleZone:Create(center, radius, options)
local zone = CircleZone:new(center, radius, options)
_initDebug(zone, options)
return zone
end
function CircleZone:isPointInside(point)
if self.destroyed then
print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}")
return false
end
local center = self.center
local radius = self.radius
if self.useZ then
return #(point - center) < radius
else
return #(point.xy - center.xy) < radius
end
end
function CircleZone:getRadius()
return self.radius
end
function CircleZone:setRadius(radius)
if not radius or radius == self.radius then
return
end
self.radius = radius
self.diameter = radius * 2.0
end
function CircleZone:getCenter()
return self.center
end
function CircleZone:setCenter(center)
if not center or center == self.center then
return
end
self.center = center
end
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(forceDraw)
local zones = self.zones
for i=1, #zones do
local zone = zones[i]
if zone and not zone.destroyed then
zone:draw(forceDraw)
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(false)
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
EntityZone = {}
-- Inherits from BoxZone
setmetatable(EntityZone, { __index = BoxZone })
-- Utility functions
local deg, atan2 = math.deg, math.atan2
local function GetRotation(entity)
local fwdVector = GetEntityForwardVector(entity)
return deg(atan2(fwdVector.y, fwdVector.x))
end
local function _calculateMinAndMaxZ(entity, dimensions, scaleZ, offsetZ)
local min, max = dimensions[1], dimensions[2]
local minX, minY, minZ, maxX, maxY, maxZ = min.x, min.y, min.z, max.x, max.y, max.z
-- Bottom vertices
local p1 = GetOffsetFromEntityInWorldCoords(entity, minX, minY, minZ).z
local p2 = GetOffsetFromEntityInWorldCoords(entity, maxX, minY, minZ).z
local p3 = GetOffsetFromEntityInWorldCoords(entity, maxX, maxY, minZ).z
local p4 = GetOffsetFromEntityInWorldCoords(entity, minX, maxY, minZ).z
-- Top vertices
local p5 = GetOffsetFromEntityInWorldCoords(entity, minX, minY, maxZ).z
local p6 = GetOffsetFromEntityInWorldCoords(entity, maxX, minY, maxZ).z
local p7 = GetOffsetFromEntityInWorldCoords(entity, maxX, maxY, maxZ).z
local p8 = GetOffsetFromEntityInWorldCoords(entity, minX, maxY, maxZ).z
local entityMinZ = math.min(p1, p2, p3, p4, p5, p6, p7, p8)
local entityMaxZ = math.max(p1, p2, p3, p4, p5, p6, p7, p8)
return BoxZone.calculateMinAndMaxZ(entityMinZ, entityMaxZ, scaleZ, offsetZ)
end
-- Initialization functions
local function _initDebug(zone, options)
if options.debugBlip then zone:addDebugBlip() end
if not options.debugPoly and not options.debugBlip then
return
end
Citizen.CreateThread(function()
local entity = zone.entity
local shouldDraw = options.debugPoly
while not zone.destroyed do
UpdateOffsets(entity, zone)
if shouldDraw then zone:draw(false) end
Citizen.Wait(0)
end
end)
end
function EntityZone:new(entity, options)
assert(DoesEntityExist(entity), "Entity does not exist")
local min, max = GetModelDimensions(GetEntityModel(entity))
local dimensions = {min, max}
local length = max.y - min.y
local width = max.x - min.x
local pos = GetEntityCoords(entity)
local zone = BoxZone:new(pos, length, width, options)
if options.useZ == true then
options.minZ, options.maxZ = _calculateMinAndMaxZ(entity, dimensions, zone.scaleZ, zone.offsetZ)
else
options.minZ = nil
options.maxZ = nil
end
zone.entity = entity
zone.dimensions = dimensions
zone.useZ = options.useZ
zone.damageEventHandlers = {}
zone.isEntityZone = true
setmetatable(zone, self)
self.__index = self
return zone
end
function EntityZone:Create(entity, options)
local zone = EntityZone:new(entity, options)
_initDebug(zone, options)
return zone
end
function UpdateOffsets(entity, zone)
local pos = GetEntityCoords(entity)
local rot = GetRotation(entity)
zone.offsetPos = pos.xy - zone.startPos
zone.offsetRot = rot - 90.0
if zone.useZ then
zone.minZ, zone.maxZ = _calculateMinAndMaxZ(entity, zone.dimensions, zone.scaleZ, zone.offsetZ)
end
if zone.debugBlip then SetBlipCoords(zone.debugBlip, pos.x, pos.y, 0.0) end
end
-- Helper functions
function EntityZone:isPointInside(point)
local entity = self.entity
if entity == nil then
print("[PolyZone] Error: Called isPointInside on Entity zone with no entity {name=" .. self.name .. "}")
return false
end
UpdateOffsets(entity, self)
return BoxZone.isPointInside(self, point)
end
function EntityZone:onEntityDamaged(onDamagedCb)
local entity = self.entity
if not entity then
print("[PolyZone] Error: Called onEntityDamage on Entity Zone with no entity {name=" .. self.name .. "}")
return
end
self.damageEventHandlers[#self.damageEventHandlers + 1] = AddEventHandler('gameEventTriggered', function (name, args)
if self.destroyed or self.paused then
return
end
if name == 'CEventNetworkEntityDamage' then
local victim, attacker, victimDied, weaponHash, isMelee = args[1], args[2], args[4], args[5], args[10]
--print(entity, victim, attacker, victimDied, weaponHash, isMelee)
if victim ~= entity then return end
onDamagedCb(victimDied == 1, attacker, weaponHash, isMelee == 1)
end
end)
end
function EntityZone:destroy()
for i=1, #self.damageEventHandlers do
print("Destroying damageEventHandler:", self.damageEventHandlers[i])
RemoveEventHandler(self.damageEventHandlers[i])
end
self.damageEventHandlers = {}
PolyZone.destroy(self)
end
function EntityZone:addDebugBlip()
local blip = PolyZone.addDebugBlip(self)
self.debugBlip = blip
return blip
end
eventPrefix = '__PolyZone__:'
PolyZone = {}
local defaultColorWalls = {0, 255, 0}
local defaultColorOutline = {255, 0, 0}
local defaultColorGrid = {255, 255, 255}
-- Utility functions
local abs = math.abs
local function _isLeft(p0, p1, p2)
local p0x = p0.x
local p0y = p0.y
return ((p1.x - p0x) * (p2.y - p0y)) - ((p2.x - p0x) * (p1.y - p0y))
end
local function _wn_inner_loop(p0, p1, p2, wn)
local p2y = p2.y
if (p0.y <= p2y) then
if (p1.y > p2y) then
if (_isLeft(p0, p1, p2) > 0) then
return wn + 1
end
end
else
if (p1.y <= p2y) then
if (_isLeft(p0, p1, p2) < 0) then
return wn - 1
end
end
end
return wn
end
function addBlip(pos)
local blip = AddBlipForCoord(pos.x, pos.y, 0.0)
SetBlipColour(blip, 7)
SetBlipDisplay(blip, 8)
SetBlipScale(blip, 1.0)
SetBlipAsShortRange(blip, true)
return blip
end
function clearTbl(tbl)
-- Only works with contiguous (array-like) tables
if tbl == nil then return end
for i=1, #tbl do
tbl[i] = nil
end
return tbl
end
function copyTbl(tbl)
-- Only a shallow copy, and only works with contiguous (array-like) tables
if tbl == nil then return end
local ret = {}
for i=1, #tbl do
ret[i] = tbl[i]
end
return ret
end
-- Winding Number Algorithm - http://geomalgorithms.com/a03-_inclusion.html
local function _windingNumber(point, poly)
local wn = 0 -- winding number counter
-- loop through all edges of the polygon
for i = 1, #poly - 1 do
wn = _wn_inner_loop(poly[i], poly[i + 1], point, wn)
end
-- test last point to first point, completing the polygon
wn = _wn_inner_loop(poly[#poly], poly[1], point, wn)
-- the point is outside only when this winding number wn===0, otherwise it's inside
return wn ~= 0
end
-- Detects intersection between two lines
local function _isIntersecting(a, b, c, d)
-- Store calculations in local variables for performance
local ax_minus_cx = a.x - c.x
local bx_minus_ax = b.x - a.x
local dx_minus_cx = d.x - c.x
local ay_minus_cy = a.y - c.y
local by_minus_ay = b.y - a.y
local dy_minus_cy = d.y - c.y
local denominator = ((bx_minus_ax) * (dy_minus_cy)) - ((by_minus_ay) * (dx_minus_cx))
local numerator1 = ((ay_minus_cy) * (dx_minus_cx)) - ((ax_minus_cx) * (dy_minus_cy))
local numerator2 = ((ay_minus_cy) * (bx_minus_ax)) - ((ax_minus_cx) * (by_minus_ay))
-- Detect coincident lines
if denominator == 0 then return numerator1 == 0 and numerator2 == 0 end
local r = numerator1 / denominator
local s = numerator2 / denominator
return (r >= 0 and r <= 1) and (s >= 0 and s <= 1)
end
-- https://rosettacode.org/wiki/Shoelace_formula_for_polygonal_area#Lua
local function _calculatePolygonArea(points)
local function det2(i,j)
return points[i].x*points[j].y-points[j].x*points[i].y
end
local sum = #points>2 and det2(#points,1) or 0
for i=1,#points-1 do sum = sum + det2(i,i+1)end
return abs(0.5 * sum)
end
-- Debug drawing functions
function _drawWall(p1, p2, minZ, maxZ, r, g, b, a)
local bottomLeft = vector3(p1.x, p1.y, minZ)
local topLeft = vector3(p1.x, p1.y, maxZ)
local bottomRight = vector3(p2.x, p2.y, minZ)
local topRight = vector3(p2.x, p2.y, maxZ)
DrawPoly(bottomLeft,topLeft,bottomRight,r,g,b,a)
DrawPoly(topLeft,topRight,bottomRight,r,g,b,a)
DrawPoly(bottomRight,topRight,topLeft,r,g,b,a)
DrawPoly(bottomRight,topLeft,bottomLeft,r,g,b,a)
end
function PolyZone:TransformPoint(point)
-- No point transform necessary for regular PolyZones, unlike zones like Entity Zones, whose points can be rotated and offset
return point
end
function PolyZone:draw(forceDraw)
if not forceDraw and not self.debugPoly and not self.debugGrid then return end
local zDrawDist = 45.0
local oColor = self.debugColors.outline or defaultColorOutline
local oR, oG, oB = oColor[1], oColor[2], oColor[3]
local wColor = self.debugColors.walls or defaultColorWalls
local wR, wG, wB = wColor[1], wColor[2], wColor[3]
local plyPed = PlayerPedId()
local plyPos = GetEntityCoords(plyPed)
local minZ = self.minZ or plyPos.z - zDrawDist
local maxZ = self.maxZ or plyPos.z + zDrawDist
local points = self.points
for i=1, #points do
local point = self:TransformPoint(points[i])
DrawLine(point.x, point.y, minZ, point.x, point.y, maxZ, oR, oG, oB, 164)
if i < #points then
local p2 = self:TransformPoint(points[i+1])
DrawLine(point.x, point.y, maxZ, p2.x, p2.y, maxZ, oR, oG, oB, 184)
_drawWall(point, p2, minZ, maxZ, wR, wG, wB, 48)
end
end
if #points > 2 then
local firstPoint = self:TransformPoint(points[1])
local lastPoint = self:TransformPoint(points[#points])
DrawLine(firstPoint.x, firstPoint.y, maxZ, lastPoint.x, lastPoint.y, maxZ, oR, oG, oB, 184)
_drawWall(firstPoint, lastPoint, minZ, maxZ, wR, wG, wB, 48)
end
end
function PolyZone.drawPoly(poly, forceDraw)
PolyZone.draw(poly, forceDraw)
end
-- Debug drawing all grid cells that are completly within the polygon
local function _drawGrid(poly)
local minZ = poly.minZ
local maxZ = poly.maxZ
if not minZ or not maxZ then
local plyPed = PlayerPedId()
local plyPos = GetEntityCoords(plyPed)
minZ = plyPos.z - 46.0
maxZ = plyPos.z - 45.0
end
local lines = poly.lines
local color = poly.debugColors.grid or defaultColorGrid
local r, g, b = color[1], color[2], color[3]
for i=1, #lines do
local line = lines[i]
local min = line.min
local max = line.max
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)
end
end
local function _pointInPoly(point, poly)
local x = point.x
local y = point.y
local min = poly.min
local minX = min.x
local minY = min.y
local max = poly.max
-- Checks if point is within the polygon's bounding box
if x < minX or
x > max.x or
y < minY or
y > max.y then
return false
end
-- Checks if point is within the polygon's height bounds
local minZ = poly.minZ
local maxZ = poly.maxZ
local z = point.z
if (minZ and z < minZ) or (maxZ and z > maxZ) then
return false
end
-- Returns true if the grid cell associated with the point is entirely inside the poly
local grid = poly.grid
if grid then
local gridDivisions = poly.gridDivisions
local size = poly.size
local gridPosX = x - minX
local gridPosY = y - minY
local gridCellX = (gridPosX * gridDivisions) // size.x
local gridCellY = (gridPosY * gridDivisions) // size.y
local gridCellValue = grid[gridCellY + 1][gridCellX + 1]
if gridCellValue == nil and poly.lazyGrid then
gridCellValue = _isGridCellInsidePoly(gridCellX, gridCellY, poly)
grid[gridCellY + 1][gridCellX + 1] = gridCellValue
end
if gridCellValue then return true end
end
return _windingNumber(point, poly.points)
end
-- Grid creation functions
-- Calculates the points of the rectangle that make up the grid cell at grid position (cellX, cellY)
local function _calculateGridCellPoints(cellX, cellY, poly)
local gridCellWidth = poly.gridCellWidth
local gridCellHeight = poly.gridCellHeight
local min = poly.min
-- min added to initial point, in order to shift the grid cells to the poly's starting position
local x = cellX * gridCellWidth + min.x
local y = cellY * gridCellHeight + min.y
return {
vector2(x, y),
vector2(x + gridCellWidth, y),
vector2(x + gridCellWidth, y + gridCellHeight),
vector2(x, y + gridCellHeight),
vector2(x, y)
}
end
function _isGridCellInsidePoly(cellX, cellY, poly)
gridCellPoints = _calculateGridCellPoints(cellX, cellY, poly)
local polyPoints = {table.unpack(poly.points)}
-- Connect the polygon to its starting point
polyPoints[#polyPoints + 1] = polyPoints[1]
-- If none of the points of the grid cell are in the polygon, the grid cell can't be in it
local isOnePointInPoly = false
for i=1, #gridCellPoints - 1 do
local cellPoint = gridCellPoints[i]
local x = cellPoint.x
local y = cellPoint.y
if _windingNumber(cellPoint, poly.points) then
isOnePointInPoly = true
-- If we are drawing the grid (poly.lines ~= nil), we need to go through all the points,
-- and therefore can't break out of the loop early
if poly.lines then
if not poly.gridXPoints[x] then poly.gridXPoints[x] = {} end
if not poly.gridYPoints[y] then poly.gridYPoints[y] = {} end
poly.gridXPoints[x][y] = true
poly.gridYPoints[y][x] = true
else break end
end
end
if isOnePointInPoly == false then
return false
end
-- If any of the grid cell's lines intersects with any of the polygon's lines
-- then the grid cell is not completely within the poly
for i=1, #gridCellPoints - 1 do
local gridCellP1 = gridCellPoints[i]
local gridCellP2 = gridCellPoints[i+1]
for j=1, #polyPoints - 1 do
if _isIntersecting(gridCellP1, gridCellP2, polyPoints[j], polyPoints[j+1]) then
return false
end
end
end
return true
end
local function _calculateLinesForDrawingGrid(poly)
local lines = {}
for x, tbl in pairs(poly.gridXPoints) do
local yValues = {}
-- Turn dict/set of values into array
for y, _ in pairs(tbl) do yValues[#yValues + 1] = y end
if #yValues >= 2 then
table.sort(yValues)
local minY = yValues[1]
local lastY = yValues[1]
for i=1, #yValues do
local y = yValues[i]
-- Checks for breaks in the grid. If the distance between the last value and the current one
-- is greater than the size of a grid cell, that means the line between them must go outside the polygon.
-- Therefore, a line must be created between minY and the lastY, and a new line started at the current y
if y - lastY > poly.gridCellHeight + 0.01 then
lines[#lines+1] = {min=vector2(x, minY), max=vector2(x, lastY)}
minY = y
elseif i == #yValues then
-- If at the last point, create a line between minY and the last point
lines[#lines+1] = {min=vector2(x, minY), max=vector2(x, y)}
end
lastY = y
end
end
end
-- Setting nil to allow the GC to clear it out of memory, since we no longer need this
poly.gridXPoints = nil
-- Same as above, but for gridYPoints instead of gridXPoints
for y, tbl in pairs(poly.gridYPoints) do
local xValues = {}
for x, _ in pairs(tbl) do xValues[#xValues + 1] = x end
if #xValues >= 2 then
table.sort(xValues)
local minX = xValues[1]
local lastX = xValues[1]
for i=1, #xValues do
local x = xValues[i]
if x - lastX > poly.gridCellWidth + 0.01 then
lines[#lines+1] = {min=vector2(minX, y), max=vector2(lastX, y)}
minX = x
elseif i == #xValues then
lines[#lines+1] = {min=vector2(minX, y), max=vector2(x, y)}
end
lastX = x
end
end
end
poly.gridYPoints = nil
return lines
end
-- Calculate for each grid cell whether it is entirely inside the polygon, and store if true
local function _createGrid(poly, options)
poly.gridArea = 0.0
poly.gridCellWidth = poly.size.x / poly.gridDivisions
poly.gridCellHeight = poly.size.y / poly.gridDivisions
Citizen.CreateThread(function()
-- Calculate all grid cells that are entirely inside the polygon
local isInside = {}
local gridCellArea = poly.gridCellWidth * poly.gridCellHeight
for y=1, poly.gridDivisions do
Citizen.Wait(0)
isInside[y] = {}
for x=1, poly.gridDivisions do
if _isGridCellInsidePoly(x-1, y-1, poly) then
poly.gridArea = poly.gridArea + gridCellArea
isInside[y][x] = true
end
end
end
poly.grid = isInside
poly.gridCoverage = poly.gridArea / poly.area
-- A lot of memory is used by this pre-calc. Force a gc collect after to clear it out
collectgarbage("collect")
if options.debugGrid then
local coverage = string.format("%.2f", poly.gridCoverage * 100)
print("[PolyZone] Debug: Grid Coverage at " .. coverage .. "% with " .. poly.gridDivisions
.. " divisions. Optimal coverage for memory usage and startup time is 80-90%")
Citizen.CreateThread(function()
poly.lines = _calculateLinesForDrawingGrid(poly)
-- A lot of memory is used by this pre-calc. Force a gc collect after to clear it out
collectgarbage("collect")
end)
end
end)
end
-- Initialization functions
local function _calculatePoly(poly, options)
if not poly.min or not poly.max or not poly.size or not poly.center or not poly.area then
local minX, minY = math.maxinteger, math.maxinteger
local maxX, maxY = math.mininteger, math.mininteger
for _, p in ipairs(poly.points) do
minX = math.min(minX, p.x)
minY = math.min(minY, p.y)
maxX = math.max(maxX, p.x)
maxY = math.max(maxY, p.y)
end
poly.min = vector2(minX, minY)
poly.max = vector2(maxX, maxY)
poly.size = poly.max - poly.min
poly.center = (poly.max + poly.min) / 2
poly.area = _calculatePolygonArea(poly.points)
end
poly.boundingRadius = math.sqrt(poly.size.y * poly.size.y + poly.size.x * poly.size.x) / 2
if poly.useGrid and not poly.lazyGrid then
if options.debugGrid then
poly.gridXPoints = {}
poly.gridYPoints = {}
poly.lines = {}
end
_createGrid(poly, options)
elseif poly.useGrid then
local isInside = {}
for y=1, poly.gridDivisions do
isInside[y] = {}
end
poly.grid = isInside
poly.gridCellWidth = poly.size.x / poly.gridDivisions
poly.gridCellHeight = poly.size.y / poly.gridDivisions
end
end
local function _initDebug(poly, options)
if options.debugBlip then poly:addDebugBlip() end
local debugEnabled = options.debugPoly or options.debugGrid
if not debugEnabled then
return
end
Citizen.CreateThread(function()
while not poly.destroyed do
poly:draw(false)
if options.debugGrid and poly.lines then
_drawGrid(poly)
end
Citizen.Wait(0)
end
end)
end
function PolyZone:new(points, options)
if not points then
print("[PolyZone] Error: Passed nil points table to PolyZone:Create() {name=" .. options.name .. "}")
return
end
if #points < 3 then
print("[PolyZone] Warning: Passed points table with less than 3 points to PolyZone:Create() {name=" .. options.name .. "}")
end
options = options or {}
local useGrid = options.useGrid
if useGrid == nil then useGrid = true end
local lazyGrid = options.lazyGrid
if lazyGrid == nil then lazyGrid = true end
local poly = {
name = tostring(options.name) or nil,
points = points,
center = options.center,
size = options.size,
max = options.max,
min = options.min,
area = options.area,
minZ = tonumber(options.minZ) or nil,
maxZ = tonumber(options.maxZ) or nil,
useGrid = useGrid,
lazyGrid = lazyGrid,
gridDivisions = tonumber(options.gridDivisions) or 30,
debugColors = options.debugColors or {},
debugPoly = options.debugPoly or false,
debugGrid = options.debugGrid or false,
data = options.data or {},
isPolyZone = true,
}
if poly.debugGrid then poly.lazyGrid = false end
_calculatePoly(poly, options)
setmetatable(poly, self)
self.__index = self
return poly
end
function PolyZone:Create(points, options)
local poly = PolyZone:new(points, options)
_initDebug(poly, options)
return poly
end
function PolyZone:isPointInside(point)
if self.destroyed then
print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}")
return false
end
return _pointInPoly(point, self)
end
function PolyZone:destroy()
self.destroyed = true
if self.debugPoly or self.debugGrid then
print("[PolyZone] Debug: Destroying zone {name=" .. self.name .. "}")
end
end
-- Helper functions
function PolyZone.getPlayerPosition()
return GetEntityCoords(PlayerPedId())
end
HeadBone = 0x796e;
function PolyZone.getPlayerHeadPosition()
return GetPedBoneCoords(PlayerPedId(), HeadBone);
end
function PolyZone.ensureMetatable(zone)
if zone.isComboZone then
setmetatable(zone, ComboZone)
elseif zone.isEntityZone then
setmetatable(zone, EntityZone)
elseif zone.isBoxZone then
setmetatable(zone, BoxZone)
elseif zone.isCircleZone then
setmetatable(zone, CircleZone)
elseif zone.isPolyZone then
setmetatable(zone, PolyZone)
end
end
function PolyZone: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 = false
while not self.destroyed do
if not self.paused then
local point = getPointCb()
local newIsInside = self:isPointInside(point)
if newIsInside ~= isInside then
onPointInOutCb(newIsInside, point)
isInside = newIsInside
end
end
Citizen.Wait(_waitInMS)
end
end)
end
function PolyZone:onPlayerInOut(onPointInOutCb, waitInMS)
self:onPointInOut(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS)
end
function PolyZone:addEvent(eventName)
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()) then
TriggerEvent(eventName, ...)
end
end)
end
function PolyZone:removeEvent(eventName)
if self.events and self.events[eventName] then
RemoveEventHandler(self.events[eventName])
self.events[eventName] = nil
end
end
function PolyZone:addDebugBlip()
return addBlip(self.center or self:getBoundingBoxCenter())
end
function PolyZone:setPaused(paused)
self.paused = paused
end
function PolyZone:isPaused()
return self.paused
end
function PolyZone:getBoundingBoxMin()
return self.min
end
function PolyZone:getBoundingBoxMax()
return self.max
end
function PolyZone:getBoundingBoxSize()
return self.size
end
function PolyZone:getBoundingBoxCenter()
return self.center
end
local function handleInput(useZ, heading, length, width, center)
if not useZ then
local scaleDelta, headingDelta = 0.2, 5
BlockWeaponWheelThisFrame()
if IsDisabledControlPressed(0, 36) then -- ctrl held down
scaleDelta, headingDelta = 0.05, 1
end
if IsDisabledControlJustPressed(0, 81) then -- scroll wheel down just pressed
if IsDisabledControlPressed(0, 19) then -- alt held down
return heading, length, math.max(0.0, width - scaleDelta), center
end
if IsDisabledControlPressed(0, 21) then -- shift held down
return heading, math.max(0.0, length - scaleDelta), width, center
end
return (heading - headingDelta) % 360, length, width, center
end
if IsDisabledControlJustPressed(0, 99) then -- scroll wheel up just pressed
if IsDisabledControlPressed(0, 19) then -- alt held down
return heading, length, math.max(0.0, width + scaleDelta), center
end
if IsDisabledControlPressed(0, 21) then -- shift held down
return heading, math.max(0.0, length + scaleDelta), width, center
end
return (heading + headingDelta) % 360, length, width, center
end
end
local rot = GetGameplayCamRot(2)
center = handleArrowInput(center, rot.z)
return heading, length, width, center
end
function handleZ(minZ, maxZ)
local delta = 0.2
if IsDisabledControlPressed(0, 36) then -- ctrl held down
delta = 0.05
end
BlockWeaponWheelThisFrame()
if IsDisabledControlJustPressed(0, 81) then -- scroll wheel down just pressed
if IsDisabledControlPressed(0, 19) then -- alt held down
return minZ - delta, maxZ
end
if IsDisabledControlPressed(0, 21) then -- shift held down
return minZ, maxZ - delta
end
return minZ - delta, maxZ - delta
end
if IsDisabledControlJustPressed(0, 99) then -- scroll wheel up just pressed
if IsDisabledControlPressed(0, 19) then -- alt held down
return minZ + delta, maxZ
end
if IsDisabledControlPressed(0, 21) then -- shift held down
return minZ, maxZ + delta
end
return minZ + delta, maxZ + delta
end
return minZ, maxZ
end
function boxStart(name, heading, length, width, minHeight, maxHeight)
local center = GetEntityCoords(PlayerPedId())
createdZone = BoxZone:Create(center, length, width, {name = tostring(name)})
local useZ, minZ, maxZ = false, center.z - 1.0, center.z + 3.0
if minHeight then
minZ = center.z - minHeight
createdZone.minZ = minZ
end
if maxHeight then
maxZ = center.z + maxHeight
createdZone.maxZ = maxZ
end
Citizen.CreateThread(function()
while createdZone do
if IsDisabledControlJustPressed(0, 20) then -- Z pressed
useZ = not useZ
if useZ then
createdZone.debugColors.walls = {255, 0, 0}
else
createdZone.debugColors.walls = {0, 255, 0}
end
end
heading, length, width, center = handleInput(useZ, heading, length, width, center)
if useZ then
minZ, maxZ = handleZ(minZ, maxZ)
createdZone.minZ = minZ
createdZone.maxZ = maxZ
end
createdZone:setLength(length)
createdZone:setWidth(width)
createdZone:setHeading(heading)
createdZone:setCenter(center)
Wait(0)
end
end)
end
function boxFinish()
TriggerServerEvent("polyzone:printBox",
{name=createdZone.name, center=createdZone.center, length=createdZone.length, width=createdZone.width, heading=createdZone.offsetRot, minZ=createdZone.minZ, maxZ=createdZone.maxZ})
endlocal function handleInput(radius, center, useZ)
local delta = 0.05
BlockWeaponWheelThisFrame()
if IsDisabledControlPressed(0, 36) then -- ctrl held down
delta = 0.01
end
if IsDisabledControlJustPressed(0, 81) then -- scroll wheel down just pressed
if IsDisabledControlPressed(0, 19) then -- alt held down
return radius, vector3(center.x, center.y, center.z - delta), useZ
end
return math.max(0.0, radius - delta), center, useZ
end
if IsDisabledControlJustPressed(0, 99) then -- scroll wheel up just pressed
if IsDisabledControlPressed(0, 19) then -- alt held down
return radius, vector3(center.x, center.y, center.z + delta), useZ
end
return radius + delta, center, useZ
end
if IsDisabledControlJustPressed(0, 20) then -- Z pressed
return radius, center, not useZ
end
local rot = GetGameplayCamRot(2)
center = handleArrowInput(center, rot.z)
return radius, center, useZ
end
function circleStart(name, radius, useZ)
local center = GetEntityCoords(PlayerPedId())
useZ = useZ or false
createdZone = CircleZone:Create(center, radius, {name = tostring(name), useZ = useZ})
Citizen.CreateThread(function()
while createdZone do
radius, center, useZ = handleInput(radius, center, useZ)
createdZone:setRadius(radius)
createdZone:setCenter(center)
createdZone.useZ = useZ
Wait(0)
end
end)
end
function circleFinish()
TriggerServerEvent("polyzone:printCircle",
{name=createdZone.name, center=createdZone.center, radius=createdZone.radius, useZ=createdZone.useZ})
endlocal minZ, maxZ = nil, nil
local function handleInput(center)
local rot = GetGameplayCamRot(2)
center = handleArrowInput(center, rot.z)
return center
end
function polyStart(name)
local coords = GetEntityCoords(PlayerPedId())
createdZone = PolyZone:Create({vector2(coords.x, coords.y)}, {name = tostring(name), useGrid=false})
Citizen.CreateThread(function()
while createdZone do
-- Have to convert the point to a vector3 prior to calling handleInput,
-- then convert it back to vector2 afterwards
lastPoint = createdZone.points[#createdZone.points]
lastPoint = vector3(lastPoint.x, lastPoint.y, 0.0)
lastPoint = handleInput(lastPoint)
createdZone.points[#createdZone.points] = lastPoint.xy
Wait(0)
end
end)
minZ, maxZ = coords.z, coords.z
end
function polyFinish()
TriggerServerEvent("polyzone:printPoly",
{name=createdZone.name, points=createdZone.points, minZ=minZ, maxZ=maxZ})
end
RegisterNetEvent("polyzone:pzadd")
AddEventHandler("polyzone:pzadd", function()
if createdZone == nil or createdZoneType ~= 'poly' then
return
end
local coords = GetEntityCoords(PlayerPedId())
if (coords.z > maxZ) then
maxZ = coords.z
end
if (coords.z < minZ) then
minZ = coords.z
end
createdZone.points[#createdZone.points + 1] = vector2(coords.x, coords.y)
end)
RegisterNetEvent("polyzone:pzundo")
AddEventHandler("polyzone:pzundo", function()
if createdZone == nil or createdZoneType ~= 'poly' then
return
end
createdZone.points[#createdZone.points] = nil
if #createdZone.points == 0 then
TriggerEvent("polyzone:pzcancel")
end
end)-- RegisterCommand("pzcreate", function(src, args)
-- local zoneType = args[1]
-- if zoneType == nil then
-- TriggerEvent('chat:addMessage', {
-- color = { 255, 0, 0},
-- multiline = true,
-- args = {"Me", "Please add zone type to create (poly, circle, box)!"}
-- })
-- return
-- end
-- if zoneType ~= 'poly' and zoneType ~= 'circle' and zoneType ~= 'box' then
-- TriggerEvent('chat:addMessage', {
-- color = { 255, 0, 0},
-- multiline = true,
-- args = {"Me", "Zone type must be one of: poly, circle, box"}
-- })
-- return
-- end
-- local name = nil
-- if #args >= 2 then name = args[2]
-- else name = GetUserInput("Enter name of zone:") end
-- if name == nil or name == "" then
-- TriggerEvent('chat:addMessage', {
-- color = { 255, 0, 0},
-- multiline = true,
-- args = {"Me", "Please add a name!"}
-- })
-- return
-- end
-- TriggerEvent("polyzone:pzcreate", zoneType, name, args)
-- end)
-- RegisterCommand("pzadd", function(src, args)
-- TriggerEvent("polyzone:pzadd")
-- end)
-- RegisterCommand("pzundo", function(src, args)
-- TriggerEvent("polyzone:pzundo")
-- end)
-- RegisterCommand("pzfinish", function(src, args)
-- TriggerEvent("polyzone:pzfinish")
-- end)
-- RegisterCommand("pzlast", function(src, args)
-- TriggerEvent("polyzone:pzlast")
-- end)
-- RegisterCommand("pzcancel", function(src, args)
-- TriggerEvent("polyzone:pzcancel")
-- end)
-- RegisterCommand("pzcomboinfo", function (src, args)
-- TriggerEvent("polyzone:pzcomboinfo")
-- end)
-- Citizen.CreateThread(function()
-- TriggerEvent('chat:addSuggestion', '/pzcreate', 'Starts creation of a zone for PolyZone of one of the available types: circle, box, poly', {
-- {name="zoneType", help="Zone Type (required)"},
-- })
-- TriggerEvent('chat:addSuggestion', '/pzadd', 'Adds point to zone.', {})
-- TriggerEvent('chat:addSuggestion', '/pzundo', 'Undoes the last point added.', {})
-- TriggerEvent('chat:addSuggestion', '/pzfinish', 'Finishes and prints zone.', {})
-- TriggerEvent('chat:addSuggestion', '/pzlast', 'Starts creation of the last zone you finished (only works on BoxZone and CircleZone)', {})
-- TriggerEvent('chat:addSuggestion', '/pzcancel', 'Cancel zone creation.', {})
-- TriggerEvent('chat:addSuggestion', '/pzcomboinfo', 'Prints some useful info for all created ComboZones', {})
-- end)lastCreatedZoneType = nil
lastCreatedZone = nil
createdZoneType = nil
createdZone = nil
drawZone = false
RegisterNetEvent("polyzone:pzcreate")
AddEventHandler("polyzone:pzcreate", function(zoneType, name, args)
if createdZone ~= nil then
TriggerEvent('chat:addMessage', {
color = { 255, 0, 0},
multiline = true,
args = {"Me", "A shape is already being created!"}
})
return
end
if zoneType == 'poly' then
polyStart(name)
elseif zoneType == "circle" then
local radius = nil
if #args >= 3 then radius = tonumber(args[3])
else radius = tonumber(GetUserInput("Enter radius:")) end
if radius == nil then
TriggerEvent('chat:addMessage', {
color = { 255, 0, 0},
multiline = true,
args = {"Me", "CircleZone requires a radius (must be a number)!"}
})
return
end
circleStart(name, radius)
elseif zoneType == "box" then
local length = nil
if #args >= 3 then length = tonumber(args[3])
else length = tonumber(GetUserInput("Enter length:")) end
if length == nil or length < 0.0 then
TriggerEvent('chat:addMessage', {
color = { 255, 0, 0},
multiline = true,
args = {"Me", "BoxZone requires a length (must be a positive number)!"}
})
return
end
local width = nil
if #args >= 4 then width = tonumber(args[4])
else width = tonumber(GetUserInput("Enter width:")) end
if width == nil or width < 0.0 then
TriggerEvent('chat:addMessage', {
color = { 255, 0, 0},
multiline = true,
args = {"Me", "BoxZone requires a width (must be a positive number)!"}
})
return
end
boxStart(name, 0, length, width)
else
return
end
createdZoneType = zoneType
drawZone = true
disableControlKeyInput()
drawThread()
end)
RegisterNetEvent("polyzone:pzfinish")
AddEventHandler("polyzone:pzfinish", function()
if createdZone == nil then
return
end
if createdZoneType == 'poly' then
polyFinish()
elseif createdZoneType == "circle" then
circleFinish()
elseif createdZoneType == "box" then
boxFinish()
end
TriggerEvent('chat:addMessage', {
color = { 0, 255, 0},
multiline = true,
args = {"Me", "Check PolyZone's root folder for polyzone_created_zones.txt to get the zone!"}
})
lastCreatedZoneType = createdZoneType
lastCreatedZone = createdZone
drawZone = false
createdZone = nil
createdZoneType = nil
end)
RegisterNetEvent("polyzone:pzlast")
AddEventHandler("polyzone:pzlast", function()
if createdZone ~= nil or lastCreatedZone == nil then
return
end
if lastCreatedZoneType == 'poly' then
TriggerEvent('chat:addMessage', {
color = { 0, 255, 0},
multiline = true,
args = {"Me", "The command pzlast only supports BoxZone and CircleZone for now"}
})
end
local name = GetUserInput("Enter name (or leave empty to reuse last zone's name):")
if name == nil then
return
elseif name == "" then
name = lastCreatedZone.name
end
createdZoneType = lastCreatedZoneType
if createdZoneType == 'box' then
local minHeight, maxHeight
if lastCreatedZone.minZ then
minHeight = lastCreatedZone.center.z - lastCreatedZone.minZ
end
if lastCreatedZone.maxZ then
maxHeight = lastCreatedZone.maxZ - lastCreatedZone.center.z
end
boxStart(name, lastCreatedZone.offsetRot, lastCreatedZone.length, lastCreatedZone.width, minHeight, maxHeight)
elseif createdZoneType == 'circle' then
circleStart(name, lastCreatedZone.radius, lastCreatedZone.useZ)
end
drawZone = true
disableControlKeyInput()
drawThread()
end)
RegisterNetEvent("polyzone:pzcancel")
AddEventHandler("polyzone:pzcancel", function()
if createdZone == nil then
return
end
TriggerEvent('chat:addMessage', {
color = {255, 0, 0},
multiline = true,
args = {"Me", "Zone creation canceled!"}
})
drawZone = false
createdZone = nil
createdZoneType = nil
end)
-- Drawing
function drawThread()
Citizen.CreateThread(function()
while drawZone do
if createdZone then
createdZone:draw(true)
end
Wait(0)
end
end)
end
-- GetUserInput function inspired by vMenu (https://github.com/TomGrobbe/vMenu/blob/master/vMenu/CommonFunctions.cs)
function GetUserInput(windowTitle, defaultText, maxInputLength)
-- Create the window title string.
local resourceName = string.upper(GetCurrentResourceName())
local textEntry = resourceName .. "_WINDOW_TITLE"
if windowTitle == nil then
windowTitle = "Enter:"
end
AddTextEntry(textEntry, windowTitle)
-- Display the input box.
DisplayOnscreenKeyboard(1, textEntry, "", defaultText or "", "", "", "", maxInputLength or 30)
Wait(0)
-- Wait for a result.
while true do
local keyboardStatus = UpdateOnscreenKeyboard();
if keyboardStatus == 3 then -- not displaying input field anymore somehow
return nil
elseif keyboardStatus == 2 then -- cancelled
return nil
elseif keyboardStatus == 1 then -- finished editing
return GetOnscreenKeyboardResult()
else
Wait(0)
end
end
end
function handleArrowInput(center, heading)
delta = 0.05
if IsDisabledControlPressed(0, 36) then -- ctrl held down
delta = 0.01
end
if IsDisabledControlPressed(0, 172) then -- arrow up
local newCenter = PolyZone.rotate(center.xy, vector2(center.x, center.y + delta), heading)
return vector3(newCenter.x, newCenter.y, center.z)
end
if IsDisabledControlPressed(0, 173) then -- arrow down
local newCenter = PolyZone.rotate(center.xy, vector2(center.x, center.y - delta), heading)
return vector3(newCenter.x, newCenter.y, center.z)
end
if IsDisabledControlPressed(0, 174) then -- arrow left
local newCenter = PolyZone.rotate(center.xy, vector2(center.x - delta, center.y), heading)
return vector3(newCenter.x, newCenter.y, center.z)
end
if IsDisabledControlPressed(0, 175) then -- arrow right
local newCenter = PolyZone.rotate(center.xy, vector2(center.x + delta, center.y), heading)
return vector3(newCenter.x, newCenter.y, center.z)
end
return center
end
function disableControlKeyInput()
Citizen.CreateThread(function()
while drawZone do
DisableControlAction(0, 36, true) -- Ctrl
DisableControlAction(0, 19, true) -- Alt
DisableControlAction(0, 20, true) -- 'Z'
DisableControlAction(0, 21, true) -- Shift
DisableControlAction(0, 81, true) -- Scroll Wheel Down
DisableControlAction(0, 99, true) -- Scroll Wheel Up
DisableControlAction(0, 172, true) -- Arrow Up
DisableControlAction(0, 173, true) -- Arrow Down
DisableControlAction(0, 174, true) -- Arrow Left
DisableControlAction(0, 175, true) -- Arrow Right
Wait(0)
end
end)
endgames {'gta5'}
fx_version 'cerulean'
description 'Define zones of different shapes and test whether a point is inside or outside of the zone'
version '2.6.2'
client_scripts {
'client.lua',
'BoxZone.lua',
'EntityZone.lua',
'CircleZone.lua',
'ComboZone.lua',
'creation/client/*.lua'
}
server_scripts {
'creation/server/*.lua',
'server.lua'
}