diff --git a/resources/BigDaddy-AnimalKingdom/settings.ini b/resources/BigDaddy-AnimalKingdom/settings.ini index ea573094c..99d2b2c36 100644 --- a/resources/BigDaddy-AnimalKingdom/settings.ini +++ b/resources/BigDaddy-AnimalKingdom/settings.ini @@ -1,5 +1,5 @@ [licensing] -key1=productkeyhere +key1=DOPBeTupQ2%2bjPs4ddblY0w42jdo6YNXJFlP1IPwiaD5uzbmuUjbGBw%3d%3d [settings] diff --git a/resources/PolyZone/BoxZone.lua b/resources/PolyZone/BoxZone.lua new file mode 100644 index 000000000..d07eb008a --- /dev/null +++ b/resources/PolyZone/BoxZone.lua @@ -0,0 +1,226 @@ +BoxZone = {} +-- 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 diff --git a/resources/PolyZone/CircleZone.lua b/resources/PolyZone/CircleZone.lua new file mode 100644 index 000000000..30c1b956f --- /dev/null +++ b/resources/PolyZone/CircleZone.lua @@ -0,0 +1,98 @@ +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 diff --git a/resources/PolyZone/ComboZone.lua b/resources/PolyZone/ComboZone.lua new file mode 100644 index 000000000..63a70e9e6 --- /dev/null +++ b/resources/PolyZone/ComboZone.lua @@ -0,0 +1,369 @@ +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 diff --git a/resources/PolyZone/EntityZone.lua b/resources/PolyZone/EntityZone.lua new file mode 100644 index 000000000..5213cf029 --- /dev/null +++ b/resources/PolyZone/EntityZone.lua @@ -0,0 +1,143 @@ +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 diff --git a/resources/PolyZone/LICENSE b/resources/PolyZone/LICENSE new file mode 100644 index 000000000..abfbc57de --- /dev/null +++ b/resources/PolyZone/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2021 Michael Afrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/PolyZone/README.md b/resources/PolyZone/README.md new file mode 100644 index 000000000..1bef136e5 --- /dev/null +++ b/resources/PolyZone/README.md @@ -0,0 +1,51 @@ +# PolyZone +PolyZone is a FiveM mod to define zones of different shapes and test whether a point is inside or outside of the zone + +![PolyZone around the prison](https://i.imgur.com/InKNaoL.jpg) + +## Download + +Click [here](https://github.com/mkafrin/PolyZone/releases) to go to the releases page and download the latest release + +## Using PolyZone in a Script + +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: + +```lua +client_scripts { + '@PolyZone/client.lua', + 'your_scripts_client.lua', +} +``` + +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). + +```lua +client_scripts { + '@PolyZone/client.lua', + '@PolyZone/BoxZone.lua', + '@PolyZone/EntityZone.lua', + '@PolyZone/CircleZone.lua', + '@PolyZone/ComboZone.lua', + 'your_scripts_client.lua' +} +``` + +## Documentation +For additional information on how to use PolyZone, please take a look at the [wiki](https://github.com/mkafrin/PolyZone/wiki) + +## Troubleshooting and Support +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. + +## FAQ - Frequently Asked Questions +**I'm getting the error `attempt to index a nil value` when creating a zone, what's wrong?** +> 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. + +**I'm getting no errors, but I can't see my zone in the right place when I turn on debug drawing** +> 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. + +**Is PolyZone faster than a distance check?** +> There's a page in the wiki for that, [here](https://github.com/mkafrin/PolyZone/wiki/Is-PolyZone-faster-than-a-distance-check%3F). + +## License +**Please see the LICENSE file. That file will always overrule anything mentioned in the README.md or wiki** diff --git a/resources/PolyZone/client.lua b/resources/PolyZone/client.lua new file mode 100644 index 000000000..a71a5e7b4 --- /dev/null +++ b/resources/PolyZone/client.lua @@ -0,0 +1,601 @@ +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 diff --git a/resources/PolyZone/creation/client/BoxZone.lua b/resources/PolyZone/creation/client/BoxZone.lua new file mode 100644 index 000000000..69eeec139 --- /dev/null +++ b/resources/PolyZone/creation/client/BoxZone.lua @@ -0,0 +1,113 @@ +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}) +end \ No newline at end of file diff --git a/resources/PolyZone/creation/client/CircleZone.lua b/resources/PolyZone/creation/client/CircleZone.lua new file mode 100644 index 000000000..5e6cb8f3a --- /dev/null +++ b/resources/PolyZone/creation/client/CircleZone.lua @@ -0,0 +1,54 @@ +local 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}) +end \ No newline at end of file diff --git a/resources/PolyZone/creation/client/PolyZone.lua b/resources/PolyZone/creation/client/PolyZone.lua new file mode 100644 index 000000000..e5ff53266 --- /dev/null +++ b/resources/PolyZone/creation/client/PolyZone.lua @@ -0,0 +1,60 @@ +local 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) \ No newline at end of file diff --git a/resources/PolyZone/creation/client/commands.lua b/resources/PolyZone/creation/client/commands.lua new file mode 100644 index 000000000..c3f6652a3 --- /dev/null +++ b/resources/PolyZone/creation/client/commands.lua @@ -0,0 +1,68 @@ +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) \ No newline at end of file diff --git a/resources/PolyZone/creation/client/creation.lua b/resources/PolyZone/creation/client/creation.lua new file mode 100644 index 000000000..ea6c36a81 --- /dev/null +++ b/resources/PolyZone/creation/client/creation.lua @@ -0,0 +1,158 @@ +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 diff --git a/resources/PolyZone/creation/client/utils.lua b/resources/PolyZone/creation/client/utils.lua new file mode 100644 index 000000000..786397dd0 --- /dev/null +++ b/resources/PolyZone/creation/client/utils.lua @@ -0,0 +1,75 @@ +-- 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) +end \ No newline at end of file diff --git a/resources/PolyZone/creation/server/config.lua b/resources/PolyZone/creation/server/config.lua new file mode 100644 index 000000000..099d06946 --- /dev/null +++ b/resources/PolyZone/creation/server/config.lua @@ -0,0 +1,56 @@ +Config = Config or {} +Config.ConfigFormatEnabled = false +-- Default Format + +-- Name: TestBox | 2022-04-13T22:46:17Z +-- BoxZone:Create(vector3(-344.16, -103.25, 39.02), 1, 1, { +-- name = "TestBox", +-- heading = 0, +-- --debugPoly = true +-- }) + +-- Name: TestCircle | 2022-04-13T22:46:39Z +-- CircleZone:Create(vector3(-344.16, -103.25, 39.02), 1.0, { +-- name = "TestCircle", +-- useZ = false, +-- --debugPoly = true +-- }) + +-- Name: TestPoly | 2022-04-13T22:46:55Z +-- PolyZone:Create({ +-- vector2(-344.15713500977, -103.24993896484), +-- vector2(-343.69491577148, -100.99839019775), +-- vector2(-345.53350830078, -102.00588226318) +-- }, { +-- name = "TestPoly", +-- minZ = 39.015644073486, +-- maxZ = 39.015865325928 +-- }) + +-- Config Format + +-- Name: TestBox | 2022-04-13T22:34:48Z +-- coords = vector3(-342.92, -102.09, 39.02), +-- length = 1, +-- width = 1, +-- name = "TestBox", +-- heading = 0, +-- debugPoly = true + +-- Name: TestCircle | 2022-04-13T22:35:09Z +-- coords = vector3(-342.92, -102.09, 39.02), +-- radius = 1.0, +-- name = "TestCircle", +-- useZ = false, +-- debugPoly = true + +-- Name: TestPoly | 2022-04-13T22:35:43Z +-- points = { +-- vector2(-342.91537475586, -102.09281158447), +-- vector2(-344.09732055664, -104.0821762085), +-- vector2(-342.01580810547, -105.60903167725) +-- }, +-- name = "TestPoly", +-- minZ = 39.015701293945, +-- maxZ = 39.015705108643, +-- debugPoly = true diff --git a/resources/PolyZone/creation/server/creation.lua b/resources/PolyZone/creation/server/creation.lua new file mode 100644 index 000000000..c21b87443 --- /dev/null +++ b/resources/PolyZone/creation/server/creation.lua @@ -0,0 +1,109 @@ +RegisterNetEvent("polyzone:printPoly") +AddEventHandler("polyzone:printPoly", function(zone) + local created_zones = LoadResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt") or "" + local output = created_zones .. parsePoly(zone) + SaveResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt", output, -1) +end) + +RegisterNetEvent("polyzone:printCircle") +AddEventHandler("polyzone:printCircle", function(zone) + local created_zones = LoadResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt") or "" + local output = created_zones .. parseCircle(zone) + SaveResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt", output, -1) +end) + +RegisterNetEvent("polyzone:printBox") +AddEventHandler("polyzone:printBox", function(zone) + local created_zones = LoadResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt") or "" + local output = created_zones .. parseBox(zone) + SaveResourceFile(GetCurrentResourceName(), "polyzone_created_zones.txt", output, -1) +end) + +function round(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +function printoutHeader(name) + return "-- Name: " .. name .. " | " .. os.date("!%Y-%m-%dT%H:%M:%SZ\n") +end + +function parsePoly(zone) + if Config.ConfigFormatEnabled then + local printout = printoutHeader(zone.name) + printout = printout .. "points = {\n" + for i = 1, #zone.points do + if i ~= #zone.points then + printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) .."),\n" + else + printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) ..")\n" + end + end + printout = printout .. "},\nname = \"" .. zone.name .. "\",\n--minZ = " .. zone.minZ .. ",\n--maxZ = " .. zone.maxZ .. ",\n--debugPoly = true\n\n" + return printout + else + local printout = printoutHeader(zone.name) + printout = printout .. "PolyZone:Create({\n" + for i = 1, #zone.points do + if i ~= #zone.points then + printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) .."),\n" + else + printout = printout .. " vector2(" .. tostring(zone.points[i].x) .. ", " .. tostring(zone.points[i].y) ..")\n" + end + end + printout = printout .. "}, {\n name = \"" .. zone.name .. "\",\n --minZ = " .. zone.minZ .. ",\n --maxZ = " .. zone.maxZ .. "\n})\n\n" + return printout + end +end + +function parseCircle(zone) + if Config.ConfigFormatEnabled then + local printout = printoutHeader(zone.name) + printout = printout .. "coords = " + printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."),\n" + printout = printout .. "radius = " .. tostring(zone.radius) .. ",\n" + printout = printout .. "name = \"" .. zone.name .. "\",\nuseZ = " .. tostring(zone.useZ) .. ",\n--debugPoly = true\n\n" + return printout + else + local printout = printoutHeader(zone.name) + printout = printout .. "CircleZone:Create(" + printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."), " + printout = printout .. tostring(zone.radius) .. ", " + printout = printout .. "{\n name = \"" .. zone.name .. "\",\n useZ = " .. tostring(zone.useZ) .. ",\n --debugPoly = true\n})\n\n" + return printout + end +end + +function parseBox(zone) + if Config.ConfigFormatEnabled then + local printout = printoutHeader(zone.name) + printout = printout .. "coords = " + printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."),\n" + printout = printout .. "length = " .. tostring(zone.length) .. ",\n" + printout = printout .. "width = " .. tostring(zone.width) .. ",\n" + printout = printout .. "name = \"" .. zone.name .. "\",\nheading = " .. zone.heading .. ",\n--debugPoly = true" + if zone.minZ then + printout = printout .. ",\nminZ = " .. tostring(round(zone.minZ, 2)) + end + if zone.maxZ then + printout = printout .. ",\nmaxZ = " .. tostring(round(zone.maxZ, 2)) + end + printout = printout .. "\n\n" + return printout + else + local printout = printoutHeader(zone.name) + printout = printout .. "BoxZone:Create(" + printout = printout .. "vector3(" .. tostring(round(zone.center.x, 2)) .. ", " .. tostring(round(zone.center.y, 2)) .. ", " .. tostring(round(zone.center.z, 2)) .."), " + printout = printout .. tostring(zone.length) .. ", " + printout = printout .. tostring(zone.width) .. ", " + printout = printout .. "{\n name = \"" .. zone.name .. "\",\n heading = " .. zone.heading .. ",\n --debugPoly = true" + if zone.minZ then + printout = printout .. ",\n minZ = " .. tostring(round(zone.minZ, 2)) + end + if zone.maxZ then + printout = printout .. ",\n maxZ = " .. tostring(round(zone.maxZ, 2)) + end + printout = printout .. "\n})\n\n" + return printout + end +end diff --git a/resources/PolyZone/fxmanifest.lua b/resources/PolyZone/fxmanifest.lua new file mode 100644 index 000000000..19fa7a020 --- /dev/null +++ b/resources/PolyZone/fxmanifest.lua @@ -0,0 +1,20 @@ +games {'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' +} diff --git a/resources/PolyZone/server.lua b/resources/PolyZone/server.lua new file mode 100644 index 000000000..185a2ac67 --- /dev/null +++ b/resources/PolyZone/server.lua @@ -0,0 +1,10 @@ +local eventPrefix = '__PolyZone__:' + +function triggerZoneEvent(eventName, ...) + TriggerClientEvent(eventPrefix .. eventName, -1, ...) +end + +RegisterNetEvent("PolyZone:TriggerZoneEvent") +AddEventHandler("PolyZone:TriggerZoneEvent", triggerZoneEvent) + +exports("TriggerZoneEvent", triggerZoneEvent) \ No newline at end of file diff --git a/server.cfg b/server.cfg index 6ea023575..885c5900f 100644 --- a/server.cfg +++ b/server.cfg @@ -269,7 +269,7 @@ ensure BigDaddy-EAS ensure BigDaddy-Trains ensure BigDaddy-Jobs-BusDriver ensure BigDaddy-AnimalKingdom -ensure +ensure PolyZone ensure ensure ensure