qbx
This commit is contained in:
parent
7cedbd835c
commit
7788fc731c
@ -24,3 +24,7 @@ set "txAdmin-hideDefaultWarning" "false"
|
||||
set "txAdmin-hideDefaultScheduledRestartWarning" "false"
|
||||
### 2025-04-05 21:18:19.739
|
||||
txaEvent "configChanged" "{}"
|
||||
### 2025-04-05 23:19:03.114
|
||||
txaEvent "serverShuttingDown" "{\"delay\":5000,\"author\":\"androxaaa\",\"message\":\"Server restarting (admin request).\"}"
|
||||
### 2025-04-07 00:39:23.693
|
||||
txaEvent "serverShuttingDown" "{\"delay\":5000,\"author\":\"androxaaa\",\"message\":\"Server shutting down (admin request).\"}"
|
||||
|
||||
1
resources/[core]/PolyZone/.gitignore
vendored
1
resources/[core]/PolyZone/.gitignore
vendored
@ -1 +0,0 @@
|
||||
polyzone_created_zones.txt
|
||||
@ -1,226 +0,0 @@
|
||||
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
|
||||
@ -1,98 +0,0 @@
|
||||
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
|
||||
@ -1,369 +0,0 @@
|
||||
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
|
||||
@ -1,143 +0,0 @@
|
||||
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
|
||||
@ -1,21 +0,0 @@
|
||||
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.
|
||||
@ -1,51 +0,0 @@
|
||||
# PolyZone
|
||||
PolyZone is a FiveM mod to define zones of different shapes and test whether a point is inside or outside of the zone
|
||||
|
||||

|
||||
|
||||
## 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**
|
||||
@ -1,601 +0,0 @@
|
||||
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
|
||||
@ -1,113 +0,0 @@
|
||||
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
|
||||
@ -1,54 +0,0 @@
|
||||
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
|
||||
@ -1,60 +0,0 @@
|
||||
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)
|
||||
@ -1,68 +0,0 @@
|
||||
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)
|
||||
@ -1,158 +0,0 @@
|
||||
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
|
||||
@ -1,75 +0,0 @@
|
||||
-- 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
|
||||
@ -1,56 +0,0 @@
|
||||
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
|
||||
@ -1,109 +0,0 @@
|
||||
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
|
||||
@ -1,20 +0,0 @@
|
||||
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'
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
local eventPrefix = '__PolyZone__:'
|
||||
|
||||
function triggerZoneEvent(eventName, ...)
|
||||
TriggerClientEvent(eventPrefix .. eventName, -1, ...)
|
||||
end
|
||||
|
||||
RegisterNetEvent("PolyZone:TriggerZoneEvent")
|
||||
AddEventHandler("PolyZone:TriggerZoneEvent", triggerZoneEvent)
|
||||
|
||||
exports("TriggerZoneEvent", triggerZoneEvent)
|
||||
@ -1,195 +0,0 @@
|
||||
# Fix holes and customize the map (Updated to Bottom Dollar Bounties DLC)
|
||||
|
||||
The purpose of this script is to fix the holes in the map by loading zones that aren’t loaded by default. I’ve added quite a lot of places to load, based on [Mikeeh’s script](https://forum.fivem.net/t/release-load-unloaded-ipls/5911). If you just want to fix the holes in the map, then use this resource as provided.
|
||||
|
||||
This resource has been completely rewritten from scratch since v2.0. You can customize almost every storymode and online purchasable interiors from your own resources.
|
||||
|
||||
## Download
|
||||
- Latest version: https://github.com/Bob74/bob74_ipl/releases/latest
|
||||
|
||||
- Source code: https://github.com/Bob74/bob74_ipl
|
||||
|
||||
## [Wiki](https://github.com/Bob74/bob74_ipl/wiki)
|
||||
- The Wiki has been created to help you customize your interiors as you wish. It contains every function you can use for each interior.
|
||||
- Each Wiki page has an example at the bottom of the page to show how you can use it in your own resource.
|
||||
- Also at the bottom of the Wiki will show you the default values set by `IPL_NAME.LoadDefault()`.
|
||||
|
||||
## Install
|
||||
1. Download the [latest version](https://github.com/Bob74/bob74_ipl/releases/latest).
|
||||
2. Extract `bob74_ipl.zip` and copy the `bob74_ipl` into your `resources` folder.
|
||||
3. Add `start bob74_ipl` to your your `server.cfg` file.
|
||||
|
||||
## Screenshots
|
||||
- [After Hours Album](https://imgur.com/a/Qg96l0D)
|
||||
- [Misc. Album](https://imgur.com/a/cs9Ip4d)
|
||||
- [IPL Fix Album](https://imgur.com/a/1Sfl4)
|
||||
|
||||
## Changelog
|
||||
|
||||
<details><summary>Click to view</summary>
|
||||
(DD/MM/YYYY)
|
||||
|
||||
---
|
||||
24/08/2024 - 2.3.2
|
||||
- Added Kosatka and "The Music Locker" interiors.
|
||||
- Removed `Citizen` prefix from code.
|
||||
|
||||
10/08/2024 - 2.3.1
|
||||
- Fix world not rendering when inside security offices
|
||||
- Fix typos in "Los Santos Tuners" files
|
||||
|
||||
02/07/2024 - 2.3.0
|
||||
- Added "Bottom Dollar Bounties" support
|
||||
|
||||
14/04/2024 - 2.2.1
|
||||
- Allow disabling San Andreas Mercenaries fixes
|
||||
- Allow setting base game cargo ship as sunk
|
||||
- Rename `ChopShopSalvage.Ipl.Load()` to `ChopShopSalvage.Ipl.Exterior.Load()`
|
||||
- Rename `DrugWarsFreakshop.Ipl.Load()` to `DrugWarsFreakshop.Ipl.Exterior.Load()`
|
||||
- Rename `DrugWarsGarage.Ipl.Load()` to `DrugWarsGarage.Ipl.Exterior.Load()`
|
||||
|
||||
06/04/2024 - 2.2.0
|
||||
- Added "Los Santos Drug Wars" support
|
||||
- Added "San Andreas Mercenaries" support
|
||||
- Added "The Chop Shop" support
|
||||
- Added missing base IPLs
|
||||
|
||||
27/03/2024 - 2.1.4
|
||||
- North Yankton improvements (https://github.com/Bob74/bob74_ipl/pull/131 @TheIndra55)
|
||||
|
||||
05/12/2023 - 2.1.3
|
||||
- Added missing train track near Davis Quartz (https://github.com/Bob74/bob74_ipl/pull/129 @TheIndra55)
|
||||
|
||||
10/01/2023 - 2.1.2
|
||||
- Fix native and update native names (@NeenGame )
|
||||
|
||||
24/10/2022 - 2.1.1
|
||||
- Fix vespucci beach wall hole
|
||||
- Fix Boat House Door in Sandy Shores
|
||||
- Fix GTA 5 24/7 Roof in Sandy Shores
|
||||
- Fix Industrial Building near Lesters Warehouse
|
||||
- Fix Collision Holes near Lost MC compound
|
||||
|
||||
11/10/2022 - 2.1.0a
|
||||
- Make Doomsday Facility Objects non network
|
||||
|
||||
03/08/2022 - 2.1.0
|
||||
- Added "The Criminal Enterprises" support
|
||||
|
||||
02/05/2022 - 2.0.15
|
||||
- Reformatted code
|
||||
- Removed unused .gitignore
|
||||
- Bumped version in fxmanifest.lua
|
||||
- Improved performance
|
||||
|
||||
21/04/2022 - 2.0.14
|
||||
- Fix casino penthouse carpet patterns colors
|
||||
|
||||
12/02/2022 - 2.0.13a
|
||||
- Fix Music Roof
|
||||
|
||||
12/02/2022 - 2.0.13
|
||||
- Added Contract IPLs: Garage, Studio, Offices, Music Roof, Billboards
|
||||
|
||||
10/02/2022 - 2.0.12
|
||||
- Fix FIB roof
|
||||
|
||||
07/02/2022 - 2.0.11
|
||||
- Added Tuners IPLs: Garage, Meth Lab, Meetup
|
||||
|
||||
18/01/2022 - 2.0.10b
|
||||
- Change water in yachts to be non-networked.
|
||||
|
||||
01/08/2021 - 2.0.10a
|
||||
- Improved performance
|
||||
- Fixed hole in the FIB fountain
|
||||
- Fixed error appearing if casino IPL is loaded, but the game build is not sufficient
|
||||
- Fixed a few typos in the README file
|
||||
|
||||
19/07/2021 - 2.0.10
|
||||
- Added Diamond Casino IPLs: Casino, Garage, VIP garage, Penthouse
|
||||
- Import: Forced refresh of CEO Garages
|
||||
- Updated fxmanifest fx_version to cerulean
|
||||
- Updated IPL list link in fxmanifest nad removed outdated Props list and Interior ID list
|
||||
- Fixed export typo in `michael.lua`
|
||||
- Removed unnecessary space in north_yankton IPL
|
||||
|
||||
27/05/2020 - 2.0.9a
|
||||
- Fixed disabling Pillbox Hospital
|
||||
- Fixed `ResetInteriorVariables`
|
||||
|
||||
23/04/2020 - 2.0.9
|
||||
- Replaced deprecated __resource.lua with fxmanifest.lua
|
||||
- Added ferris wheel on the Del Perro Pier
|
||||
- Reformatted client.lua
|
||||
|
||||
20/10/2019 - 2.0.8
|
||||
- Nightclubs: Added dry ice emitters
|
||||
- Heist & Gunrunning: Added water to the yachts hot tubs (to enable/disable)
|
||||
- Offices: Added a way to open and close the safes
|
||||
- Facility: Added privacy glass
|
||||
- Moved Bahama Mamas and PillBox Hospital in their own files
|
||||
- Fixed error `ReleaseNamedRendertarget`
|
||||
- Cleaned and optimized the code
|
||||
|
||||
22/03/2019 - 2.0.7c
|
||||
- CEO Offices: Changed the default loaded garage to ImportCEOGarage4.Part.Garage2 in order to avoid Office glitches
|
||||
|
||||
15/01/2019 - 2.0.7b
|
||||
- Nightclubs: Fixed a typo for the fake lights
|
||||
|
||||
15/01/2019 - 2.0.7a
|
||||
- Nightclubs: Added the ability to set no podium (using `AfterHoursNightclubs.Interior.Podium.none`)
|
||||
|
||||
14/01/2019 - 2.0.7
|
||||
- Changed the way Trevor’s trailer is handled and added a Wiki entry.
|
||||
- Added a way to open or close Zancudo’s gates with a Wiki entry.
|
||||
|
||||
12/01/2019 - 2.0.6
|
||||
- Added nightclubs interior and exteriors
|
||||
- Removed Zancudo gates by default (file bob74_ipl/gtav/base.lua: RequestIpl("CS3_07_MPGates") is now commented)
|
||||
|
||||
29/12/2018 - 2.0.5a
|
||||
- Fixed the name of the BikerClubhouse1 export
|
||||
|
||||
19/12/2018 - 2.0.5
|
||||
- Fixed a typo that prevents the printers, security stuff, and cash piles to spawn in the counterfeit cash factory
|
||||
|
||||
10/11/2018 - 2.0.4
|
||||
- Fixed an issue where the clubhouse2 lower walls wouldn’t be colored on the first resource start
|
||||
- Fixed gang members names using an old format
|
||||
- Disabled the Mod shop from CEO garage 3 (ImportCEOGarage3) because it is overlapping with CEO office 3 (FinanceOffice3)
|
||||
|
||||
08/11/2018 - 2.0.3
|
||||
- Added biker gang’s name, missions, and members pictures
|
||||
- Added CEO office organization’s name
|
||||
|
||||
05/11/2018 - 2.0.1
|
||||
- Removed overlapping Zancudo River
|
||||
- Added the trailer near Zancudo River
|
||||
|
||||
04/11/2018 - 2.0.0
|
||||
- Plugin totally rewritten
|
||||
- Support for all DLC (up to The Doomsday Heist)
|
||||
- Ability to easily customize story mode and online purchasable interiors
|
||||
- You can still use it as it is if you want IPL and interiors to be loaded, the plugin sets a default style for each one
|
||||
- Check out the Wiki to find out how: https://github.com/Bob74/bob74_ipl/wiki
|
||||
|
||||
26/06/2017
|
||||
- Added optional IPL
|
||||
- Bunkers exteriors (enabled)
|
||||
- Bunkers interior
|
||||
- CEO Offices
|
||||
- Bikers places (some are still buggy)
|
||||
- Import/Export locations
|
||||
- Removed the trick to open Lost’s safehouse since the last update already opens it
|
||||
|
||||
19/06/2017
|
||||
- Fix hole in Zancudo River
|
||||
- Fix hole in Cassidy Creek
|
||||
- Add optional graffiti on some billboards (enabled by default)
|
||||
- Opened Lost’s safehouse interior
|
||||
|
||||
14/06/2017
|
||||
- Original release
|
||||
</details>
|
||||
@ -1,223 +0,0 @@
|
||||
CreateThread(function()
|
||||
-- ====================================================================
|
||||
-- =--------------------- [GTA V: Single player] ---------------------=
|
||||
-- ====================================================================
|
||||
|
||||
-- Michael: -802.311, 175.056, 72.8446
|
||||
Michael.LoadDefault()
|
||||
|
||||
-- Simeon: -47.16170 -1115.3327 26.5
|
||||
Simeon.LoadDefault()
|
||||
|
||||
-- Franklin's aunt: -9.96562, -1438.54, 31.1015
|
||||
FranklinAunt.LoadDefault()
|
||||
|
||||
-- Franklin
|
||||
Franklin.LoadDefault()
|
||||
|
||||
-- Floyd: -1150.703, -1520.713, 10.633
|
||||
Floyd.LoadDefault()
|
||||
|
||||
-- Trevor: 1985.48132, 3828.76757, 32.5
|
||||
TrevorsTrailer.LoadDefault()
|
||||
|
||||
-- Bahama Mamas: -1388.0013, -618.41967, 30.819599
|
||||
BahamaMamas.Enable(true)
|
||||
|
||||
-- Pillbox hospital: 307.1680, -590.807, 43.280
|
||||
PillboxHospital.Enable(true)
|
||||
|
||||
-- Zancudo Gates (GTAO like): -1600.30100000, 2806.73100000, 18.79683000
|
||||
ZancudoGates.LoadDefault()
|
||||
|
||||
-- Other
|
||||
Ammunations.LoadDefault()
|
||||
LesterFactory.LoadDefault()
|
||||
StripClub.LoadDefault()
|
||||
CargoShip.LoadDefault()
|
||||
|
||||
Graffitis.Enable(true)
|
||||
|
||||
-- UFO
|
||||
UFO.Hippie.Enable(false) -- 2490.47729, 3774.84351, 2414.035
|
||||
UFO.Chiliad.Enable(false) -- 501.52880000, 5593.86500000, 796.23250000
|
||||
UFO.Zancudo.Enable(false) -- -2051.99463, 3237.05835, 1456.97021
|
||||
|
||||
-- Red Carpet: 300.5927, 199.7589, 104.3776
|
||||
RedCarpet.Enable(false)
|
||||
|
||||
-- North Yankton: 3217.697, -4834.826, 111.8152
|
||||
NorthYankton.Enable(false)
|
||||
|
||||
-- ====================================================================
|
||||
-- =-------------------------- [GTA Online] --------------------------=
|
||||
-- ====================================================================
|
||||
GTAOApartmentHi1.LoadDefault() -- -35.31277 -580.4199 88.71221 (4 Integrity Way, Apt 30)
|
||||
GTAOApartmentHi2.LoadDefault() -- -1477.14 -538.7499 55.5264 (Dell Perro Heights, Apt 7)
|
||||
GTAOHouseHi1.LoadDefault() -- -169.286 486.4938 137.4436 (3655 Wild Oats Drive)
|
||||
GTAOHouseHi2.LoadDefault() -- 340.9412 437.1798 149.3925 (2044 North Conker Avenue)
|
||||
GTAOHouseHi3.LoadDefault() -- 373.023 416.105 145.7006 (2045 North Conker Avenue)
|
||||
GTAOHouseHi4.LoadDefault() -- -676.127 588.612 145.1698 (2862 Hillcrest Avenue)
|
||||
GTAOHouseHi5.LoadDefault() -- -763.107 615.906 144.1401 (2868 Hillcrest Avenue)
|
||||
GTAOHouseHi6.LoadDefault() -- -857.798 682.563 152.6529 (2874 Hillcrest Avenue)
|
||||
GTAOHouseHi7.LoadDefault() -- 120.5 549.952 184.097 (2677 Whispymound Drive)
|
||||
GTAOHouseHi8.LoadDefault() -- -1288 440.748 97.69459 (2133 Mad Wayne Thunder)
|
||||
GTAOHouseMid1.LoadDefault() -- 347.2686 -999.2955 -99.19622
|
||||
GTAOHouseLow1.LoadDefault() -- 261.4586 -998.8196 -99.00863
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------------ [DLC: High life] ------------------------=
|
||||
-- ====================================================================
|
||||
HLApartment1.LoadDefault() -- -1468.14 -541.815 73.4442 (Dell Perro Heights, Apt 4)
|
||||
HLApartment2.LoadDefault() -- -915.811 -379.432 113.6748 (Richard Majestic, Apt 2)
|
||||
HLApartment3.LoadDefault() -- -614.86 40.6783 97.60007 (Tinsel Towers, Apt 42)
|
||||
HLApartment4.LoadDefault() -- -773.407 341.766 211.397 (EclipseTowers, Apt 3)
|
||||
HLApartment5.LoadDefault() -- -18.07856 -583.6725 79.46569 (4 Integrity Way, Apt 28)
|
||||
HLApartment6.LoadDefault() -- -609.56690000 51.28212000 -183.98080
|
||||
|
||||
-- ====================================================================
|
||||
-- =-------------------------- [DLC: Heists] -------------------------=
|
||||
-- ====================================================================
|
||||
HeistCarrier.Enable(true) -- 3082.3117, -4717.1191, 15.2622
|
||||
HeistYacht.LoadDefault() -- -2043.974,-1031.582, 11.981
|
||||
|
||||
-- ====================================================================
|
||||
-- =--------------- [DLC: Executives & Other Criminals] --------------=
|
||||
-- ====================================================================
|
||||
ExecApartment1.LoadDefault() -- -787.7805 334.9232 215.8384 (EclipseTowers, Penthouse Suite 1)
|
||||
ExecApartment2.LoadDefault() -- -773.2258 322.8252 194.8862 (EclipseTowers, Penthouse Suite 2)
|
||||
ExecApartment3.LoadDefault() -- -787.7805 334.9232 186.1134 (EclipseTowers, Penthouse Suite 3)
|
||||
|
||||
-- ====================================================================
|
||||
-- =-------------------- [DLC: Finance & Felony] --------------------=
|
||||
-- ====================================================================
|
||||
FinanceOffice1.LoadDefault() -- -141.1987, -620.913, 168.8205 (Arcadius Business Centre)
|
||||
FinanceOffice2.LoadDefault() -- -75.8466, -826.9893, 243.3859 (Maze Bank Building)
|
||||
FinanceOffice3.LoadDefault() -- -1579.756, -565.0661, 108.523 (Lom Bank)
|
||||
FinanceOffice4.LoadDefault() -- -1392.667, -480.4736, 72.04217 (Maze Bank West)
|
||||
|
||||
-- ====================================================================
|
||||
-- =-------------------------- [DLC: Bikers] -------------------------=
|
||||
-- ====================================================================
|
||||
BikerCocaine.LoadDefault() -- Cocaine lockup: 1093.6, -3196.6, -38.99841
|
||||
BikerCounterfeit.LoadDefault() -- Counterfeit cash factory: 1121.897, -3195.338, -40.4025
|
||||
BikerDocumentForgery.LoadDefault() -- Document forgery: 1165, -3196.6, -39.01306
|
||||
BikerMethLab.LoadDefault() -- Meth lab: 1009.5, -3196.6, -38.99682
|
||||
BikerWeedFarm.LoadDefault() -- Weed farm: 1051.491, -3196.536, -39.14842
|
||||
BikerClubhouse1.LoadDefault() -- 1107.04, -3157.399, -37.51859
|
||||
BikerClubhouse2.LoadDefault() -- 998.4809, -3164.711, -38.90733
|
||||
|
||||
-- ====================================================================
|
||||
-- =---------------------- [DLC: Import/Export] ----------------------=
|
||||
-- ====================================================================
|
||||
ImportCEOGarage1.LoadDefault() -- Arcadius Business Centre
|
||||
ImportCEOGarage2.LoadDefault() -- Maze Bank Building /!\ Do not load parts Garage1, Garage2 and Garage3 at the same time (overlaping issues)
|
||||
ImportCEOGarage3.LoadDefault() -- Lom Bank /!\ Do not load parts Garage1, Garage2 and Garage3 at the same time (overlaping issues)
|
||||
ImportCEOGarage4.LoadDefault() -- Maze Bank West /!\ Do not load parts Garage1, Garage2 and Garage3 at the same time (overlaping issues)
|
||||
ImportVehicleWarehouse.LoadDefault() -- Vehicle warehouse: 994.5925, -3002.594, -39.64699
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------------ [DLC: Gunrunning] -----------------------=
|
||||
-- ====================================================================
|
||||
GunrunningBunker.LoadDefault() -- 892.6384, -3245.8664, -98.2645
|
||||
GunrunningYacht.LoadDefault() -- -1363.724, 6734.108, 2.44598
|
||||
|
||||
-- ====================================================================
|
||||
-- =---------------------- [DLC: Smuggler's Run] ---------------------=
|
||||
-- ====================================================================
|
||||
SmugglerHangar.LoadDefault() -- -1267.0 -3013.135 -49.5
|
||||
|
||||
-- ====================================================================
|
||||
-- =-------------------- [DLC: The Doomsday Heist] -------------------=
|
||||
-- ====================================================================
|
||||
DoomsdayFacility.LoadDefault()
|
||||
|
||||
-- ====================================================================
|
||||
-- =----------------------- [DLC: After Hours] -----------------------=
|
||||
-- ====================================================================
|
||||
AfterHoursNightclubs.LoadDefault() -- -1604.664, -3012.583, -78.000
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------- [DLC: Diamond Casino Resort] -----------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 2060 then
|
||||
DiamondCasino.LoadDefault() -- 1100.000, 220.000, -50.000
|
||||
DiamondPenthouse.LoadDefault() -- 976.636, 70.295, 115.164
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =-------------------- [DLC: Cayo Perico Heist] --------------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 2189 then
|
||||
CayoPericoNightclub.LoadDefault() -- 1550.0, 250.0, -50.0
|
||||
CayoPericoSubmarine.LoadDefault() -- 1560.0, 400.0, -50.0
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------- [DLC: Los Santos Tuners] ---------------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 2372 then
|
||||
TunerGarage.LoadDefault() -- -1350.0, 160.0, -100.0
|
||||
TunerMethLab.LoadDefault() -- 981.9999, -143.0, -50.0
|
||||
TunerMeetup.LoadDefault() -- -2000.0, 1113.211, -25.36243
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------- [DLC: Los Santos The Contract] ---------------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 2545 then
|
||||
MpSecurityGarage.LoadDefault() -- -1071.4387, -77.033875, -93.525505
|
||||
MpSecurityMusicRoofTop.LoadDefault() -- -592.6896, 273.1052, 116.302444
|
||||
MpSecurityStudio.LoadDefault() -- -1000.7252, -70.559875, -98.10669
|
||||
MpSecurityBillboards.LoadDefault() -- -592.6896, 273.1052, 116.302444
|
||||
MpSecurityOffice1.LoadDefault() -- -1021.86084, -427.74564, 68.95764
|
||||
MpSecurityOffice2.LoadDefault() -- 383.4156, -59.878227, 108.4595
|
||||
MpSecurityOffice3.LoadDefault() -- -1004.23035, -761.2084, 66.99069
|
||||
MpSecurityOffice4.LoadDefault() -- -587.87213, -716.84937, 118.10156
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------- [DLC: The Criminal Enterprise] ---------------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 2699 then
|
||||
CriminalEnterpriseSmeonFix.LoadDefault() -- -50.2248, -1098.8325, 26.049742
|
||||
CriminalEnterpriseVehicleWarehouse.LoadDefault() -- 800.13696, -3001.4297, -65.14074
|
||||
CriminalEnterpriseWarehouse.LoadDefault() -- 849.1047, -3000.209, -45.974354
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------- [DLC: Los Santos Drug Wars] ------------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 2802 then
|
||||
DrugWarsFreakshop.LoadDefault() -- 570.9713, -420.0727, -70.000
|
||||
DrugWarsGarage.LoadDefault() -- 519.2477, -2618.788, -50.000
|
||||
DrugWarsLab.LoadDefault() -- 483.4252, -2625.071, -50.000
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------- [DLC: San Andreas Mercenaries] ---------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 2944 then
|
||||
MercenariesClub.LoadDefault() -- 1202.407, -3251.251, -50.000
|
||||
MercenariesLab.LoadDefault() -- -1916.119, 3749.719, -100.000
|
||||
MercenariesFixes.LoadDefault()
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------- [DLC: The Chop Shop] -------------------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 3095 then
|
||||
ChopShopCargoShip.LoadDefault() -- -344.4349, -4062.832, 17.000
|
||||
ChopShopCartelGarage.LoadDefault() -- 1220.133, -2277.844, -50.000
|
||||
ChopShopLifeguard.LoadDefault() -- -1488.153, -1021.166, 5.000
|
||||
ChopShopSalvage.LoadDefault() -- 1077.276, -2274.876, -50.000
|
||||
end
|
||||
|
||||
-- ====================================================================
|
||||
-- =------------------ [DLC: Bottom Dollar Bounties] -----------------=
|
||||
-- ====================================================================
|
||||
if GetGameBuildNumber() >= 3258 then
|
||||
SummerCarrier.LoadDefault() -- -3208.03, 3954.54, 14.0
|
||||
SummerOffice.LoadDefault() -- 565.886, -2688.761, -50.0
|
||||
end
|
||||
end)
|
||||
@ -1,114 +0,0 @@
|
||||
-- Eclipse Boulevard Garage: 519.2477, -2618.788, -50.000
|
||||
exports('GetDrugWarsGarageObject', function()
|
||||
return DrugWarsGarage
|
||||
end)
|
||||
|
||||
DrugWarsGarage = {
|
||||
interiorId = 290561,
|
||||
|
||||
Ipl = {
|
||||
Exterior = {
|
||||
ipl = "xm3_garage_fix",
|
||||
|
||||
Load = function()
|
||||
EnableIpl(DrugWarsGarage.Ipl.Exterior.ipl, true)
|
||||
end,
|
||||
Remove = function()
|
||||
EnableIpl(DrugWarsGarage.Ipl.Exterior.ipl, false)
|
||||
end
|
||||
}
|
||||
},
|
||||
|
||||
Banner = {
|
||||
model = `ss1_13_clth_ss1_13`,
|
||||
position = vector3(-277.1116, 281.5493, 98.6691),
|
||||
|
||||
Hide = function()
|
||||
CreateModelHide(DrugWarsGarage.Banner.position, 10.0, DrugWarsGarage.Banner.model, true)
|
||||
end,
|
||||
Restore = function()
|
||||
RemoveModelHide(DrugWarsGarage.Banner.position, 10.0, DrugWarsGarage.Banner.model, false)
|
||||
end
|
||||
},
|
||||
|
||||
Numbering = {
|
||||
none = "",
|
||||
level1 = "entity_set_numbers_01",
|
||||
level2 = "entity_set_numbers_02",
|
||||
level3 = "entity_set_numbers_03",
|
||||
level4 = "entity_set_numbers_04",
|
||||
level5 = "entity_set_numbers_05",
|
||||
|
||||
Set = function(num, refresh)
|
||||
DrugWarsGarage.Numbering.Clear(false)
|
||||
|
||||
if num ~= "" then
|
||||
SetIplPropState(DrugWarsGarage.interiorId, num, true, refresh)
|
||||
else
|
||||
if refresh then
|
||||
RefreshInterior(DrugWarsGarage.interiorId)
|
||||
end
|
||||
end
|
||||
end,
|
||||
Clear = function(refresh)
|
||||
SetIplPropState(DrugWarsGarage.interiorId, {
|
||||
DrugWarsGarage.Numbering.level1,
|
||||
DrugWarsGarage.Numbering.level2,
|
||||
DrugWarsGarage.Numbering.level3,
|
||||
DrugWarsGarage.Numbering.level4,
|
||||
DrugWarsGarage.Numbering.level5
|
||||
}, false, refresh)
|
||||
end
|
||||
},
|
||||
|
||||
Style = {
|
||||
immaculate = "entity_set_shell_01",
|
||||
industrial = "entity_set_shell_02",
|
||||
indulgent = "entity_set_shell_03",
|
||||
|
||||
Set = function(style, refresh)
|
||||
DrugWarsGarage.Style.Clear(false)
|
||||
|
||||
SetIplPropState(DrugWarsGarage.interiorId, style, true, refresh)
|
||||
end,
|
||||
Clear = function(refresh)
|
||||
SetIplPropState(DrugWarsGarage.interiorId, {
|
||||
DrugWarsGarage.Style.immaculate,
|
||||
DrugWarsGarage.Style.industrial,
|
||||
DrugWarsGarage.Style.indulgent
|
||||
}, false, refresh)
|
||||
end
|
||||
},
|
||||
|
||||
Tint = {
|
||||
white = 1,
|
||||
gray = 2,
|
||||
black = 3,
|
||||
purple = 4,
|
||||
orange = 5,
|
||||
yellow = 6,
|
||||
blue = 7,
|
||||
red = 8,
|
||||
green = 9,
|
||||
lightBlue = 10,
|
||||
lightGreen = 11,
|
||||
|
||||
SetColor = function(color, refresh)
|
||||
SetIplPropState(DrugWarsGarage.interiorId, "entity_set_tint_01", true, refresh)
|
||||
SetInteriorEntitySetColor(DrugWarsGarage.interiorId, "entity_set_tint_01", color)
|
||||
end
|
||||
},
|
||||
|
||||
LoadDefault = function()
|
||||
-- Exterior
|
||||
DrugWarsGarage.Ipl.Exterior.Load()
|
||||
DrugWarsGarage.Banner.Hide()
|
||||
|
||||
-- Interior
|
||||
DrugWarsGarage.Numbering.Set(DrugWarsGarage.Numbering.level1, false)
|
||||
DrugWarsGarage.Style.Set(DrugWarsGarage.Style.immaculate, false)
|
||||
DrugWarsGarage.Tint.SetColor(DrugWarsGarage.Tint.white, false)
|
||||
|
||||
RefreshInterior(DrugWarsGarage.interiorId)
|
||||
end
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
exports('GetMpSecurityGarageObject', function()
|
||||
return MpSecurityGarage
|
||||
end)
|
||||
|
||||
MpSecurityGarage = {
|
||||
InteriorId = 286721,
|
||||
|
||||
Ipl = {
|
||||
Interior = {
|
||||
ipl = {
|
||||
'sf_int_placement_sec_interior_2_dlc_garage_sec_milo_'
|
||||
}
|
||||
},
|
||||
|
||||
Load = function()
|
||||
EnableIpl(MpSecurityGarage.Ipl.Interior.ipl, true)
|
||||
end,
|
||||
Remove = function()
|
||||
EnableIpl(MpSecurityGarage.Ipl.Interior.ipl, false)
|
||||
end
|
||||
},
|
||||
Entities = {
|
||||
Entity_Set_Workshop_Wall = false,
|
||||
Entity_Set_Wallpaper_01 = false,
|
||||
Entity_Set_Wallpaper_02 = false,
|
||||
Entity_Set_Wallpaper_03 = false,
|
||||
Entity_Set_Wallpaper_04 = false,
|
||||
Entity_Set_Wallpaper_05 = false,
|
||||
Entity_Set_Wallpaper_06 = false,
|
||||
Entity_Set_Wallpaper_07 = true,
|
||||
Entity_Set_Wallpaper_08 = false,
|
||||
Entity_Set_Wallpaper_09 = false,
|
||||
Entity_Set_Art_1 = false,
|
||||
Entity_Set_Art_2 = false,
|
||||
Entity_Set_Art_3 = false,
|
||||
Entity_Set_Art_1_NoMod = false,
|
||||
Entity_Set_Art_2_NoMod = false,
|
||||
Entity_Set_Art_3_NoMod = false,
|
||||
entity_set_tints = true,
|
||||
Entity_Set_Workshop_Lights = true,
|
||||
|
||||
Set = function(name, state)
|
||||
for entity, _ in pairs(MpSecurityGarage.Entities) do
|
||||
if entity == name then
|
||||
MpSecurityGarage.Entities[entity] = state
|
||||
MpSecurityGarage.Entities.Clear()
|
||||
MpSecurityGarage.Entities.Load()
|
||||
end
|
||||
end
|
||||
end,
|
||||
Load = function()
|
||||
for entity, state in pairs(MpSecurityGarage.Entities) do
|
||||
if type(entity) == 'string' and state then
|
||||
ActivateInteriorEntitySet(MpSecurityGarage.InteriorId, entity)
|
||||
end
|
||||
end
|
||||
end,
|
||||
Clear = function()
|
||||
for entity, _ in pairs(MpSecurityGarage.Entities) do
|
||||
if type(entity) == 'string' then
|
||||
DeactivateInteriorEntitySet(MpSecurityGarage.InteriorId, entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
},
|
||||
|
||||
LoadDefault = function()
|
||||
MpSecurityGarage.Ipl.Load()
|
||||
MpSecurityGarage.Entities.Load()
|
||||
|
||||
RefreshInterior(MpSecurityGarage.interiorId)
|
||||
end
|
||||
}
|
||||
@ -1,157 +0,0 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
author 'Bob_74'
|
||||
description 'Load and customize your map'
|
||||
version '2.3.2'
|
||||
|
||||
lua54 "yes"
|
||||
|
||||
client_scripts {
|
||||
"lib/common.lua"
|
||||
, "lib/observers/interiorIdObserver.lua"
|
||||
, "lib/observers/officeSafeDoorHandler.lua"
|
||||
, "lib/observers/officeCullHandler.lua"
|
||||
, "client.lua"
|
||||
|
||||
-- GTA V
|
||||
, "gtav/base.lua" -- Base IPLs to fix holes
|
||||
, "gtav/ammunations.lua"
|
||||
, "gtav/bahama.lua"
|
||||
, "gtav/cargoship.lua"
|
||||
, "gtav/floyd.lua"
|
||||
, "gtav/franklin.lua"
|
||||
, "gtav/franklin_aunt.lua"
|
||||
, "gtav/graffitis.lua"
|
||||
, "gtav/pillbox_hospital.lua"
|
||||
, "gtav/lester_factory.lua"
|
||||
, "gtav/michael.lua"
|
||||
, "gtav/north_yankton.lua"
|
||||
, "gtav/red_carpet.lua"
|
||||
, "gtav/simeon.lua"
|
||||
, "gtav/stripclub.lua"
|
||||
, "gtav/trevors_trailer.lua"
|
||||
, "gtav/ufo.lua"
|
||||
, "gtav/zancudo_gates.lua"
|
||||
|
||||
-- GTA Online
|
||||
, "gta_online/apartment_hi_1.lua"
|
||||
, "gta_online/apartment_hi_2.lua"
|
||||
, "gta_online/house_hi_1.lua"
|
||||
, "gta_online/house_hi_2.lua"
|
||||
, "gta_online/house_hi_3.lua"
|
||||
, "gta_online/house_hi_4.lua"
|
||||
, "gta_online/house_hi_5.lua"
|
||||
, "gta_online/house_hi_6.lua"
|
||||
, "gta_online/house_hi_7.lua"
|
||||
, "gta_online/house_hi_8.lua"
|
||||
, "gta_online/house_mid_1.lua"
|
||||
, "gta_online/house_low_1.lua"
|
||||
|
||||
-- DLC High Life
|
||||
, "dlc_high_life/apartment1.lua"
|
||||
, "dlc_high_life/apartment2.lua"
|
||||
, "dlc_high_life/apartment3.lua"
|
||||
, "dlc_high_life/apartment4.lua"
|
||||
, "dlc_high_life/apartment5.lua"
|
||||
, "dlc_high_life/apartment6.lua"
|
||||
|
||||
-- DLC Heists
|
||||
, "dlc_heists/carrier.lua"
|
||||
, "dlc_heists/yacht.lua"
|
||||
|
||||
-- DLC Executives & Other Criminals
|
||||
, "dlc_executive/apartment1.lua"
|
||||
, "dlc_executive/apartment2.lua"
|
||||
, "dlc_executive/apartment3.lua"
|
||||
|
||||
-- DLC Finance & Felony
|
||||
, "dlc_finance/office1.lua"
|
||||
, "dlc_finance/office2.lua"
|
||||
, "dlc_finance/office3.lua"
|
||||
, "dlc_finance/office4.lua"
|
||||
, "dlc_finance/organization.lua"
|
||||
|
||||
-- DLC Bikers
|
||||
, "dlc_bikers/cocaine.lua"
|
||||
, "dlc_bikers/counterfeit_cash.lua"
|
||||
, "dlc_bikers/document_forgery.lua"
|
||||
, "dlc_bikers/meth.lua"
|
||||
, "dlc_bikers/weed.lua"
|
||||
, "dlc_bikers/clubhouse1.lua"
|
||||
, "dlc_bikers/clubhouse2.lua"
|
||||
, "dlc_bikers/gang.lua"
|
||||
|
||||
-- DLC Import/Export
|
||||
, "dlc_import/garage1.lua"
|
||||
, "dlc_import/garage2.lua"
|
||||
, "dlc_import/garage3.lua"
|
||||
, "dlc_import/garage4.lua"
|
||||
, "dlc_import/vehicle_warehouse.lua"
|
||||
|
||||
-- DLC Gunrunning
|
||||
, "dlc_gunrunning/bunkers.lua"
|
||||
, "dlc_gunrunning/yacht.lua"
|
||||
|
||||
-- DLC Smuggler's Run
|
||||
, "dlc_smuggler/hangar.lua"
|
||||
|
||||
-- DLC Doomsday Heist
|
||||
, "dlc_doomsday/facility.lua"
|
||||
|
||||
-- DLC After Hours
|
||||
, "dlc_afterhours/nightclubs.lua"
|
||||
|
||||
-- DLC Diamond Casino (Requires forced build 2060 or higher)
|
||||
, "dlc_casino/casino.lua"
|
||||
, "dlc_casino/penthouse.lua"
|
||||
|
||||
-- DLC Cayo Perico Heist (Requires forced build 2189 or higher)
|
||||
, "dlc_cayoperico/base.lua"
|
||||
, "dlc_cayoperico/nightclub.lua"
|
||||
, "dlc_cayoperico/submarine.lua"
|
||||
|
||||
-- DLC Tuners (Requires forced build 2372 or higher)
|
||||
, "dlc_tuner/garage.lua"
|
||||
, "dlc_tuner/meetup.lua"
|
||||
, "dlc_tuner/methlab.lua"
|
||||
|
||||
-- DLC The Contract (Requires forced build 2545 or higher)
|
||||
, "dlc_security/studio.lua"
|
||||
, "dlc_security/billboards.lua"
|
||||
, "dlc_security/musicrooftop.lua"
|
||||
, "dlc_security/garage.lua"
|
||||
, "dlc_security/office1.lua"
|
||||
, "dlc_security/office2.lua"
|
||||
, "dlc_security/office3.lua"
|
||||
, "dlc_security/office4.lua"
|
||||
|
||||
-- DLC The Criminal Enterprises (Requires forced build 2699 or higher)
|
||||
, "gta_mpsum2/simeonfix.lua"
|
||||
, "gta_mpsum2/vehicle_warehouse.lua"
|
||||
, "gta_mpsum2/warehouse.lua"
|
||||
|
||||
-- DLC Los Santos Drug Wars (Requires forced build 2802 or higher)
|
||||
, "dlc_drugwars/base.lua"
|
||||
, "dlc_drugwars/freakshop.lua"
|
||||
, "dlc_drugwars/garage.lua"
|
||||
, "dlc_drugwars/lab.lua"
|
||||
, "dlc_drugwars/traincrash.lua"
|
||||
|
||||
-- DLC San Andreas Mercenaries (Requires forced build 2944 or higher)
|
||||
, "dlc_mercenaries/club.lua"
|
||||
, "dlc_mercenaries/lab.lua"
|
||||
, "dlc_mercenaries/fixes.lua"
|
||||
|
||||
-- DLC The Chop Shop (Requires forced build 3095 or higher)
|
||||
, "dlc_chopshop/base.lua"
|
||||
, "dlc_chopshop/cargoship.lua"
|
||||
, "dlc_chopshop/cartel_garage.lua"
|
||||
, "dlc_chopshop/lifeguard.lua"
|
||||
, "dlc_chopshop/salvage.lua"
|
||||
|
||||
-- DLC Bottom Dollar Bounties (Requires forced build 3258 or higher)
|
||||
, "dlc_summer/base.lua"
|
||||
, "dlc_summer/carrier.lua"
|
||||
, "dlc_summer/office.lua"
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
# ConnectQueue
|
||||
---
|
||||
Easy to use queue system for FiveM with:
|
||||
- Simple API
|
||||
- Priority System
|
||||
- Config
|
||||
- Ability for whitelist only
|
||||
- Require steam
|
||||
- Language options
|
||||
|
||||
**Please report any bugs on the release thread [Here](https://forum.fivem.net/t/alpha-connectqueue-a-server-queue-system-fxs/22228) or through [GitHub](https://github.com/Nick78111/ConnectQueue/issues).**
|
||||
|
||||
## How to install
|
||||
---
|
||||
- Drop the folder inside your resources folder.
|
||||
- Add `start connectqueue` inside your server.cfg. - *Preferrably at the top*
|
||||
- Set convars to your liking.
|
||||
- Open `connectqueue/server/sv_queue_config.lua` and edit to your liking.
|
||||
- Renaming the resource may cause problems.
|
||||
|
||||
## ConVars
|
||||
---
|
||||
set sv_debugqueue true # prints debug messages to console
|
||||
set sv_displayqueue true # shows queue count in the server name '[count] server name'
|
||||
|
||||
## How to use / Examples
|
||||
---
|
||||
To use the API add `server_script "@connectqueue/connectqueue.lua"` at the top of the `__resource.lua` file in question.
|
||||
I would also suggest adding `dependency "connectqueue"` to it aswell.
|
||||
You may now use any of the functions below, anywhere in that resource.
|
||||
|
||||
### OnReady
|
||||
This is called when the queue functions are ready to be used.
|
||||
```Lua
|
||||
Queue.OnReady(function()
|
||||
print("HI")
|
||||
end)
|
||||
```
|
||||
All of the functions below must be called **AFTER** the queue is ready.
|
||||
|
||||
### OnJoin
|
||||
This is called when a player tries to join the server.
|
||||
Calling `allow` with no arguments will let them through.
|
||||
Calling `allow` with a string will prevent them from joining with the given message.
|
||||
`allow` must be called or the player will hang on connecting...
|
||||
```Lua
|
||||
Queue.OnJoin(function(source, allow)
|
||||
allow("No, you can't join")
|
||||
end)
|
||||
```
|
||||
|
||||
## AddPriority
|
||||
Call this to add an identifier to the priority list.
|
||||
The integer is how much power they have over other users with priority.
|
||||
This function can take a table of ids or individually.
|
||||
```Lua
|
||||
-- individual
|
||||
Queue.AddPriority("STEAM_0:1:33459672", 100)
|
||||
Queue.AddPriority("steam:110000103fd1bb1", 10)
|
||||
Queue.AddPriority("ip:127.0.0.1", 25)
|
||||
|
||||
-- table
|
||||
local prioritize = {
|
||||
["STEAM_0:1:33459672"] = 100,
|
||||
["steam:110000103fd1bb1"] = 10,
|
||||
["ip:127.0.0.1"] = 25,
|
||||
}
|
||||
Queue.AddPriority(prioritize)
|
||||
```
|
||||
|
||||
## RemovePriority
|
||||
Removes priority from a user.
|
||||
```Lua
|
||||
Queue.RemovePriority("STEAM_0:1:33459672")
|
||||
```
|
||||
|
||||
## IsReady
|
||||
Will return whether or not the queue's exports are ready to be called.
|
||||
```Lua
|
||||
print(Queue.IsReady())
|
||||
```
|
||||
|
||||
## Other Queue Functions
|
||||
You can call every queue function within sh_queue.lua.
|
||||
```Lua
|
||||
local ids = Queue.Exports:GetIds(src)
|
||||
|
||||
-- sets the player to position 1 in queue
|
||||
Queue.Exports:SetPos(ids, 1)
|
||||
-- returns whether or not the player has any priority
|
||||
Queue.Exports:IsPriority(ids)
|
||||
--- returns size of queue
|
||||
Queue.Exports:GetSize()
|
||||
-- plus many more...
|
||||
```
|
||||
@ -1,62 +0,0 @@
|
||||
Queue = {}
|
||||
Queue.Ready = false
|
||||
Queue.Exports = nil
|
||||
Queue.ReadyCbs = {}
|
||||
Queue.CurResource = GetCurrentResourceName()
|
||||
|
||||
if Queue.CurResource == "connectqueue" then return end
|
||||
|
||||
function Queue.OnReady(cb)
|
||||
if not cb then return end
|
||||
if Queue.IsReady() then cb() return end
|
||||
table.insert(Queue.ReadyCbs, cb)
|
||||
end
|
||||
|
||||
function Queue.OnJoin(cb)
|
||||
if not cb then return end
|
||||
Queue.Exports:OnJoin(cb, Queue.CurResource)
|
||||
end
|
||||
|
||||
function Queue.AddPriority(id, power, temp)
|
||||
if not Queue.IsReady() then return end
|
||||
Queue.Exports:AddPriority(id, power, temp)
|
||||
end
|
||||
|
||||
function Queue.RemovePriority(id)
|
||||
if not Queue.IsReady() then return end
|
||||
Queue.Exports:RemovePriority(id)
|
||||
end
|
||||
|
||||
function Queue.IsReady()
|
||||
return Queue.Ready
|
||||
end
|
||||
|
||||
function Queue.LoadExports()
|
||||
Queue.Exports = exports.connectqueue:GetQueueExports()
|
||||
Queue.Ready = true
|
||||
Queue.ReadyCallbacks()
|
||||
end
|
||||
|
||||
function Queue.ReadyCallbacks()
|
||||
if not Queue.IsReady() then return end
|
||||
for _, cb in ipairs(Queue.ReadyCbs) do
|
||||
cb()
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler("onResourceStart", function(resource)
|
||||
if resource == "connectqueue" then
|
||||
while GetResourceState(resource) ~= "started" do Citizen.Wait(0) end
|
||||
Citizen.Wait(1)
|
||||
Queue.LoadExports()
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource)
|
||||
if resource == "connectqueue" then
|
||||
Queue.Ready = false
|
||||
Queue.Exports = nil
|
||||
end
|
||||
end)
|
||||
|
||||
SetTimeout(1, function() Queue.LoadExports() end)
|
||||
@ -1,8 +0,0 @@
|
||||
fx_version 'bodacious'
|
||||
game 'common'
|
||||
|
||||
server_script "server/sv_queue_config.lua"
|
||||
server_script "connectqueue.lua"
|
||||
|
||||
server_script "shared/sh_queue.lua"
|
||||
client_script "shared/sh_queue.lua"
|
||||
@ -1,61 +0,0 @@
|
||||
Config = {}
|
||||
|
||||
-- priority list can be any identifier. (hex steamid, steamid32, ip) Integer = power over other people with priority
|
||||
-- a lot of the steamid converting websites are broken rn and give you the wrong steamid. I use https://steamid.xyz/ with no problems.
|
||||
-- you can also give priority through the API, read the examples/readme.
|
||||
Config.Priority = {
|
||||
["STEAM_0:1:0000####"] = 1,
|
||||
["steam:110000######"] = 25,
|
||||
["ip:127.0.0.0"] = 85
|
||||
}
|
||||
|
||||
-- require people to run steam
|
||||
Config.RequireSteam = false
|
||||
|
||||
-- "whitelist" only server
|
||||
Config.PriorityOnly = false
|
||||
|
||||
-- disables hardcap, should keep this true
|
||||
Config.DisableHardCap = true
|
||||
|
||||
-- will remove players from connecting if they don't load within: __ seconds; May need to increase this if you have a lot of downloads.
|
||||
-- i have yet to find an easy way to determine whether they are still connecting and downloading content or are hanging in the loadscreen.
|
||||
-- This may cause session provider errors if it is too low because the removed player may still be connecting, and will let the next person through...
|
||||
-- even if the server is full. 10 minutes should be enough
|
||||
Config.ConnectTimeOut = 600
|
||||
|
||||
-- will remove players from queue if the server doesn't recieve a message from them within: __ seconds
|
||||
Config.QueueTimeOut = 90
|
||||
|
||||
-- will give players temporary priority when they disconnect and when they start loading in
|
||||
Config.EnableGrace = false
|
||||
|
||||
-- how much priority power grace time will give
|
||||
Config.GracePower = 5
|
||||
|
||||
-- how long grace time lasts in seconds
|
||||
Config.GraceTime = 480
|
||||
|
||||
Config.AntiSpam = false
|
||||
Config.AntiSpamTimer = 30
|
||||
Config.PleaseWait = "Please wait %f seconds. The connection will start automatically!"
|
||||
|
||||
-- on resource start, players can join the queue but will not let them join for __ milliseconds
|
||||
-- this will let the queue settle and lets other resources finish initializing
|
||||
Config.JoinDelay = 30000
|
||||
|
||||
-- will show how many people have temporary priority in the connection message
|
||||
Config.ShowTemp = false
|
||||
|
||||
-- simple localization
|
||||
Config.Language = {
|
||||
joining = "\xF0\x9F\x8E\x89Joining...",
|
||||
connecting = "\xE2\x8F\xB3Connecting...",
|
||||
idrr = "\xE2\x9D\x97[Queue] Error: Couldn't retrieve any of your id's, try restarting.",
|
||||
err = "\xE2\x9D\x97[Queue] There was an error",
|
||||
pos = "\xF0\x9F\x90\x8CYou are %d/%d in queue \xF0\x9F\x95\x9C%s",
|
||||
connectingerr = "\xE2\x9D\x97[Queue] Error: Error adding you to connecting list",
|
||||
timedout = "\xE2\x9D\x97[Queue] Error: Timed out?",
|
||||
wlonly = "\xE2\x9D\x97[Queue] You must be whitelisted to join this server",
|
||||
steam = "\xE2\x9D\x97 [Queue] Error: Steam must be running"
|
||||
}
|
||||
@ -1,883 +0,0 @@
|
||||
if not IsDuplicityVersion() then
|
||||
Citizen.CreateThread(function()
|
||||
while true do
|
||||
Citizen.Wait(0)
|
||||
if NetworkIsSessionStarted() then
|
||||
TriggerServerEvent("Queue:playerActivated")
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
local Queue = {}
|
||||
-- EDIT THESE IN SERVER.CFG + OTHER OPTIONS IN CONFIG.LUA
|
||||
Queue.MaxPlayers = GetConvarInt("sv_maxclients", 30)
|
||||
Queue.Debug = GetConvar("sv_debugqueue", "true") == "true" and true or false
|
||||
Queue.DisplayQueue = GetConvar("sv_displayqueue", "true") == "true" and true or false
|
||||
Queue.InitHostName = GetConvar("sv_hostname")
|
||||
|
||||
|
||||
-- This is needed because msgpack will break when tables are too large
|
||||
local _Queue = {}
|
||||
_Queue.QueueList = {}
|
||||
_Queue.PlayerList = {}
|
||||
_Queue.PlayerCount = 0
|
||||
_Queue.Priority = {}
|
||||
_Queue.Connecting = {}
|
||||
_Queue.JoinCbs = {}
|
||||
_Queue.TempPriority = {}
|
||||
_Queue.JoinDelay = GetGameTimer() + Config.JoinDelay and Config.JoinDelay or 0
|
||||
|
||||
local tostring = tostring
|
||||
local tonumber = tonumber
|
||||
local ipairs = ipairs
|
||||
local pairs = pairs
|
||||
local print = print
|
||||
local string_len = string.len
|
||||
local string_sub = string.sub
|
||||
local string_format = string.format
|
||||
local string_lower = string.lower
|
||||
local math_abs = math.abs
|
||||
local math_floor = math.floor
|
||||
local math_random = math.random
|
||||
local os_time = os.time
|
||||
local table_insert = table.insert
|
||||
local table_remove = table.remove
|
||||
|
||||
Queue.InitHostName = Queue.InitHostName ~= "default FXServer" and Queue.InitHostName or false
|
||||
|
||||
for id, power in pairs(Config.Priority) do
|
||||
_Queue.Priority[string_lower(id)] = power
|
||||
end
|
||||
|
||||
function Queue:DebugPrint(msg)
|
||||
if Queue.Debug then
|
||||
msg = "^3QUEUE: ^0" .. tostring(msg) .. "^7"
|
||||
print(msg)
|
||||
end
|
||||
end
|
||||
|
||||
function Queue:HexIdToSteamId(hexId)
|
||||
local cid = math_floor(tonumber(string_sub(hexId, 7), 16))
|
||||
local steam64 = math_floor(tonumber(string_sub( cid, 2)))
|
||||
local a = steam64 % 2 == 0 and 0 or 1
|
||||
local b = math_floor(math_abs(6561197960265728 - steam64 - a) / 2)
|
||||
local sid = "steam_0:"..a..":"..(a == 1 and b -1 or b)
|
||||
return sid
|
||||
end
|
||||
|
||||
function Queue:IsSteamRunning(src)
|
||||
for _, id in ipairs(GetPlayerIdentifiers(src)) do
|
||||
if string_sub(id, 1, 5) == "steam" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Queue:GetPlayerCount()
|
||||
return _Queue.PlayerCount
|
||||
end
|
||||
|
||||
function Queue:GetSize()
|
||||
return #_Queue.QueueList
|
||||
end
|
||||
|
||||
function Queue:ConnectingSize()
|
||||
return #_Queue.Connecting
|
||||
end
|
||||
|
||||
function Queue:GetQueueList()
|
||||
return _Queue.QueueList
|
||||
end
|
||||
|
||||
function Queue:GetPriorityList()
|
||||
return _Queue.Priority
|
||||
end
|
||||
|
||||
function Queue:GetPlayerList()
|
||||
return _Queue.PlayerList
|
||||
end
|
||||
|
||||
function Queue:GetTempPriorityList()
|
||||
return _Queue.TempPriority
|
||||
end
|
||||
|
||||
function Queue:GetConnectingList()
|
||||
return _Queue.Connecting
|
||||
end
|
||||
|
||||
function Queue:IsInQueue(ids, rtnTbl, bySource, connecting)
|
||||
local connList = Queue:GetConnectingList()
|
||||
local queueList = Queue:GetQueueList()
|
||||
|
||||
for genericKey1, genericValue1 in ipairs(connecting and connList or queueList) do
|
||||
local inQueue = false
|
||||
|
||||
if not bySource then
|
||||
for genericKey2, genericValue2 in ipairs(genericValue1.ids) do
|
||||
if inQueue then break end
|
||||
|
||||
for genericKey3, genericValue3 in ipairs(ids) do
|
||||
if genericValue3 == genericValue2 then inQueue = true break end
|
||||
end
|
||||
end
|
||||
else
|
||||
inQueue = ids == genericValue1.source
|
||||
end
|
||||
|
||||
if inQueue then
|
||||
if rtnTbl then
|
||||
return genericKey1, connecting and connList[genericKey1] or queueList[genericKey1]
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Queue:IsPriority(ids)
|
||||
local prio = false
|
||||
local tempPower, tempEnd = Queue:HasTempPriority(ids)
|
||||
local prioList = Queue:GetPriorityList()
|
||||
|
||||
for _, id in ipairs(ids) do
|
||||
id = string_lower(id)
|
||||
|
||||
if prioList[id] then prio = prioList[id] break end
|
||||
|
||||
if string_sub(id, 1, 5) == "steam" then
|
||||
local steamid = Queue:HexIdToSteamId(id)
|
||||
if prioList[steamid] then prio = prioList[steamid] break end
|
||||
end
|
||||
end
|
||||
|
||||
if tempPower or prio then
|
||||
if tempPower and prio then
|
||||
return tempPower > prio and tempPower or prio
|
||||
else
|
||||
return tempPower or prio
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Queue:HasTempPriority(ids)
|
||||
local tmpPrio = Queue:GetTempPriorityList()
|
||||
|
||||
for _, id in pairs(ids) do
|
||||
id = string_lower(id)
|
||||
|
||||
if tmpPrio[id] then return tmpPrio[id].power, tmpPrio[id].endTime, id end
|
||||
|
||||
if string_sub(id, 1, 5) == "steam" then
|
||||
local steamid = Queue:HexIdToSteamId(id)
|
||||
if tmpPrio[steamid] then return tmpPrio[steamid].power, tmpPrio[steamid].endTime, id end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Queue:AddToQueue(ids, connectTime, name, src, deferrals)
|
||||
if Queue:IsInQueue(ids) then return end
|
||||
|
||||
local tmp = {
|
||||
source = src,
|
||||
ids = ids,
|
||||
name = name,
|
||||
priority = Queue:IsPriority(ids) or (src == "debug" and math_random(0, 15)),
|
||||
timeout = 0,
|
||||
deferrals = deferrals,
|
||||
firstconnect = connectTime,
|
||||
queuetime = function() return (os_time() - connectTime) end
|
||||
}
|
||||
|
||||
local _pos = false
|
||||
local queueCount = Queue:GetSize() + 1
|
||||
local queueList = Queue:GetQueueList()
|
||||
|
||||
for pos, data in ipairs(queueList) do
|
||||
if tmp.priority then
|
||||
if not data.priority then
|
||||
_pos = pos
|
||||
else
|
||||
if tmp.priority > data.priority then
|
||||
_pos = pos
|
||||
end
|
||||
end
|
||||
|
||||
if _pos then
|
||||
Queue:DebugPrint(string_format("%s[%s] was prioritized and placed %d/%d in queue", tmp.name, ids[1], _pos, queueCount))
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not _pos then
|
||||
_pos = Queue:GetSize() + 1
|
||||
Queue:DebugPrint(string_format("%s[%s] was placed %d/%d in queue", tmp.name, ids[1], _pos, queueCount))
|
||||
end
|
||||
|
||||
table_insert(queueList, _pos, tmp)
|
||||
end
|
||||
|
||||
function Queue:RemoveFromQueue(ids, bySource, byIndex)
|
||||
local queueList = Queue:GetQueueList()
|
||||
|
||||
if byIndex then
|
||||
if queueList[byIndex] then
|
||||
table_remove(queueList, byIndex)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if Queue:IsInQueue(ids, false, bySource) then
|
||||
local pos, data = Queue:IsInQueue(ids, true, bySource)
|
||||
table_remove(queueList, pos)
|
||||
end
|
||||
end
|
||||
|
||||
function Queue:TempSize()
|
||||
local count = 0
|
||||
|
||||
for _pos, data in pairs(Queue:GetQueueList()) do
|
||||
if Queue:HasTempPriority(data.ids) then count = count +1 end
|
||||
end
|
||||
|
||||
return count > 0 and count or false
|
||||
end
|
||||
|
||||
function Queue:IsInConnecting(ids, bySource, refresh)
|
||||
local inConnecting, tbl = Queue:IsInQueue(ids, refresh and true or false, bySource and true or false, true)
|
||||
|
||||
if not inConnecting then return false end
|
||||
|
||||
if refresh and inConnecting and tbl then
|
||||
Queue:GetConnectingList()[inConnecting].timeout = 0
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function Queue:RemoveFromConnecting(ids, bySource, byIndex)
|
||||
local connList = Queue:GetConnectingList()
|
||||
|
||||
if byIndex then
|
||||
if connList[byIndex] then
|
||||
table_remove(connList, byIndex)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
for genericKey1, genericValue1 in ipairs(connList) do
|
||||
local inConnecting = false
|
||||
|
||||
if not bySource then
|
||||
for genericKey2, genericValue2 in ipairs(genericValue1.ids) do
|
||||
if inConnecting then break end
|
||||
|
||||
for genericKey3, genericValue3 in ipairs(ids) do
|
||||
if genericValue3 == genericValue2 then inConnecting = true break end
|
||||
end
|
||||
end
|
||||
else
|
||||
inConnecting = ids == genericValue1.source
|
||||
end
|
||||
|
||||
if inConnecting then
|
||||
table_remove(connList, genericKey1)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Queue:AddToConnecting(ids, ignorePos, autoRemove, done)
|
||||
local function remove()
|
||||
if not autoRemove then return end
|
||||
|
||||
done(Config.Language.connectingerr)
|
||||
Queue:RemoveFromConnecting(ids)
|
||||
Queue:RemoveFromQueue(ids)
|
||||
Queue:DebugPrint("Player could not be added to the connecting list")
|
||||
end
|
||||
|
||||
local connList = Queue:GetConnectingList()
|
||||
|
||||
if Queue:ConnectingSize() + Queue:GetPlayerCount() + 1 > Queue.MaxPlayers then remove() return false end
|
||||
|
||||
if ids[1] == "debug" then
|
||||
table_insert(connList, {source = ids[1], ids = ids, name = ids[1], firstconnect = ids[1], priority = ids[1], timeout = 0})
|
||||
return true
|
||||
end
|
||||
|
||||
if Queue:IsInConnecting(ids) then Queue:RemoveFromConnecting(ids) end
|
||||
|
||||
local pos, data = Queue:IsInQueue(ids, true)
|
||||
if not ignorePos and (not pos or pos > 1) then remove() return false end
|
||||
|
||||
table_insert(connList, data)
|
||||
Queue:RemoveFromQueue(ids)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function Queue:GetIds(src)
|
||||
local ids = GetPlayerIdentifiers(src)
|
||||
local ip = GetPlayerEndpoint(src)
|
||||
|
||||
ids = (ids and ids[1]) and ids or (ip and {"ip:" .. ip} or false)
|
||||
ids = ids ~= nil and ids or false
|
||||
|
||||
if ids and #ids > 1 then
|
||||
for k, id in ipairs(ids) do
|
||||
if string_sub(id, 1, 3) == "ip:" and not Queue:IsPriority({id}) then table_remove(ids, k) end
|
||||
end
|
||||
end
|
||||
|
||||
return ids
|
||||
end
|
||||
|
||||
function Queue:AddPriority(id, power, temp)
|
||||
if not id then return false end
|
||||
|
||||
if type(id) == "table" then
|
||||
for _id, power in pairs(id) do
|
||||
if _id and type(_id) == "string" and power and type(power) == "number" then
|
||||
Queue:GetPriorityList()[_id] = power
|
||||
else
|
||||
Queue:DebugPrint("Error adding a priority id, invalid data passed")
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
power = (power and type(power) == "number") and power or 10
|
||||
|
||||
if temp then
|
||||
local tempPower, tempEnd, tempId = Queue:HasTempPriority({id})
|
||||
id = tempId or id
|
||||
|
||||
Queue:GetTempPriorityList()[string_lower(id)] = {power = power, endTime = os_time() + temp}
|
||||
else
|
||||
Queue:GetPriorityList()[string_lower(id)] = power
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function Queue:RemovePriority(id)
|
||||
if not id then return false end
|
||||
id = string_lower(id)
|
||||
Queue:GetPriorityList()[id] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
function Queue:UpdatePosData(src, ids, deferrals)
|
||||
local pos, data = Queue:IsInQueue(ids, true)
|
||||
data.source = src
|
||||
data.ids = ids
|
||||
data.timeout = 0
|
||||
data.firstconnect = os_time()
|
||||
data.name = GetPlayerName(src)
|
||||
data.deferrals = deferrals
|
||||
end
|
||||
|
||||
function Queue:NotFull(firstJoin)
|
||||
local canJoin = Queue:GetPlayerCount() + Queue:ConnectingSize() < Queue.MaxPlayers
|
||||
if firstJoin and canJoin then canJoin = Queue:GetSize() <= 1 end
|
||||
return canJoin
|
||||
end
|
||||
|
||||
function Queue:SetPos(ids, newPos)
|
||||
if newPos <= 0 or newPos > Queue:GetSize() then return false end
|
||||
|
||||
local pos, data = Queue:IsInQueue(ids, true)
|
||||
local queueList = Queue:GetQueueList()
|
||||
|
||||
table_remove(queueList, pos)
|
||||
table_insert(queueList, newPos, data)
|
||||
end
|
||||
|
||||
function Queue:CanJoin(src, cb)
|
||||
local allow = true
|
||||
|
||||
for _, data in ipairs(_Queue.JoinCbs) do
|
||||
local await = true
|
||||
|
||||
data.func(src, function(reason)
|
||||
if reason and type(reason) == "string" then allow = false cb(reason) end
|
||||
await = false
|
||||
end)
|
||||
|
||||
while await do Citizen.Wait(0) end
|
||||
|
||||
if not allow then return end
|
||||
end
|
||||
|
||||
if allow then cb(false) end
|
||||
end
|
||||
|
||||
function Queue:OnJoin(cb, resource)
|
||||
if not cb then return end
|
||||
|
||||
local tmp = {resource = resource, func = cb}
|
||||
table_insert(_Queue.JoinCbs, tmp)
|
||||
end
|
||||
|
||||
exports("GetQueueExports", function()
|
||||
return Queue
|
||||
end)
|
||||
|
||||
local function playerConnect(name, setKickReason, deferrals)
|
||||
local src = source
|
||||
local ids = Queue:GetIds(src)
|
||||
local name = GetPlayerName(src)
|
||||
local connectTime = os_time()
|
||||
local connecting = true
|
||||
|
||||
deferrals.defer()
|
||||
|
||||
if Config.AntiSpam then
|
||||
for i=Config.AntiSpamTimer,0,-1 do
|
||||
deferrals.update(string.format(Config.PleaseWait, i))
|
||||
Citizen.Wait(1000)
|
||||
end
|
||||
end
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
while connecting do
|
||||
Citizen.Wait(100)
|
||||
if not connecting then return end
|
||||
deferrals.update(Config.Language.connecting)
|
||||
end
|
||||
end)
|
||||
|
||||
Citizen.Wait(500)
|
||||
|
||||
local function done(msg, _deferrals)
|
||||
connecting = false
|
||||
|
||||
local deferrals = _deferrals or deferrals
|
||||
|
||||
if msg then deferrals.update(tostring(msg) or "") end
|
||||
|
||||
Citizen.Wait(500)
|
||||
|
||||
if not msg then
|
||||
deferrals.done()
|
||||
if Config.EnableGrace then Queue:AddPriority(ids[1], Config.GracePower, Config.GraceTime) end
|
||||
else
|
||||
deferrals.done(tostring(msg) or "") CancelEvent()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local function update(msg, _deferrals)
|
||||
local deferrals = _deferrals or deferrals
|
||||
connecting = false
|
||||
deferrals.update(tostring(msg) or "")
|
||||
end
|
||||
|
||||
if not ids then
|
||||
-- prevent joining
|
||||
done(Config.Language.idrr)
|
||||
CancelEvent()
|
||||
Queue:DebugPrint("Dropped " .. name .. ", couldn't retrieve any of their id's")
|
||||
return
|
||||
end
|
||||
|
||||
if Config.RequireSteam and not Queue:IsSteamRunning(src) then
|
||||
-- prevent joining
|
||||
done(Config.Language.steam)
|
||||
CancelEvent()
|
||||
return
|
||||
end
|
||||
|
||||
local allow
|
||||
|
||||
Queue:CanJoin(src, function(reason)
|
||||
if reason == nil or allow ~= nil then return end
|
||||
if reason == false or #_Queue.JoinCbs <= 0 then allow = true return end
|
||||
|
||||
if reason then
|
||||
-- prevent joining
|
||||
allow = false
|
||||
done(reason and tostring(reason) or "You were blocked from joining")
|
||||
Queue:RemoveFromQueue(ids)
|
||||
Queue:RemoveFromConnecting(ids)
|
||||
Queue:DebugPrint(string_format("%s[%s] was blocked from joining; Reason: %s", name, ids[1], reason))
|
||||
CancelEvent()
|
||||
return
|
||||
end
|
||||
|
||||
allow = true
|
||||
end)
|
||||
|
||||
while allow == nil do Citizen.Wait(0) end
|
||||
if not allow then return end
|
||||
|
||||
if Config.PriorityOnly and not Queue:IsPriority(ids) then done(Config.Language.wlonly) return end
|
||||
|
||||
local rejoined = false
|
||||
|
||||
if Queue:IsInConnecting(ids, false, true) then
|
||||
Queue:RemoveFromConnecting(ids)
|
||||
|
||||
if Queue:NotFull() then
|
||||
-- let them in the server
|
||||
|
||||
if not Queue:IsInQueue(ids) then
|
||||
Queue:AddToQueue(ids, connectTime, name, src, deferrals)
|
||||
end
|
||||
|
||||
local added = Queue:AddToConnecting(ids, true, true, done)
|
||||
if not added then CancelEvent() return end
|
||||
done()
|
||||
|
||||
return
|
||||
else
|
||||
rejoined = true
|
||||
end
|
||||
end
|
||||
|
||||
if Queue:IsInQueue(ids) then
|
||||
rejoined = true
|
||||
Queue:UpdatePosData(src, ids, deferrals)
|
||||
Queue:DebugPrint(string_format("%s[%s] has rejoined queue after cancelling", name, ids[1]))
|
||||
else
|
||||
Queue:AddToQueue(ids, connectTime, name, src, deferrals)
|
||||
|
||||
if rejoined then
|
||||
Queue:SetPos(ids, 1)
|
||||
rejoined = false
|
||||
end
|
||||
end
|
||||
|
||||
local pos, data = Queue:IsInQueue(ids, true)
|
||||
|
||||
if not pos or not data then
|
||||
done(Config.Language.err .. " [1]")
|
||||
|
||||
Queue:RemoveFromQueue(ids)
|
||||
Queue:RemoveFromConnecting(ids)
|
||||
|
||||
CancelEvent()
|
||||
return
|
||||
end
|
||||
|
||||
if Queue:NotFull(true) and _Queue.JoinDelay <= GetGameTimer() then
|
||||
-- let them in the server
|
||||
local added = Queue:AddToConnecting(ids, true, true, done)
|
||||
if not added then CancelEvent() return end
|
||||
|
||||
done()
|
||||
Queue:DebugPrint(name .. "[" .. ids[1] .. "] is loading into the server")
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
update(string_format(Config.Language.pos .. ((Queue:TempSize() and Config.ShowTemp) and " (" .. Queue:TempSize() .. " temp)" or "00:00:00"), pos, Queue:GetSize(), ""))
|
||||
|
||||
if rejoined then return end
|
||||
|
||||
while true do
|
||||
Citizen.Wait(500)
|
||||
|
||||
local pos, data = Queue:IsInQueue(ids, true)
|
||||
|
||||
local function remove(msg)
|
||||
if data then
|
||||
if msg then
|
||||
update(msg, data.deferrals)
|
||||
end
|
||||
|
||||
Queue:RemoveFromQueue(data.source, true)
|
||||
Queue:RemoveFromConnecting(data.source, true)
|
||||
else
|
||||
Queue:RemoveFromQueue(ids)
|
||||
Queue:RemoveFromConnecting(ids)
|
||||
end
|
||||
end
|
||||
|
||||
if not data or not data.deferrals or not data.source or not pos then
|
||||
remove("[Queue] Removed from queue, queue data invalid :(")
|
||||
Queue:DebugPrint(tostring(name .. "[" .. ids[1] .. "] was removed from the queue because they had invalid data"))
|
||||
return
|
||||
end
|
||||
|
||||
local endPoint = GetPlayerEndpoint(data.source)
|
||||
if not endPoint then data.timeout = data.timeout + 0.5 else data.timeout = 0 end
|
||||
|
||||
if data.timeout >= Config.QueueTimeOut and os_time() - connectTime > 5 then
|
||||
remove("[Queue] Removed due to timeout")
|
||||
Queue:DebugPrint(name .. "[" .. ids[1] .. "] was removed from the queue because they timed out")
|
||||
return
|
||||
end
|
||||
|
||||
if pos <= 1 and Queue:NotFull() and _Queue.JoinDelay <= GetGameTimer() then
|
||||
-- let them in the server
|
||||
local added = Queue:AddToConnecting(ids)
|
||||
|
||||
update(Config.Language.joining, data.deferrals)
|
||||
Citizen.Wait(500)
|
||||
|
||||
if not added then
|
||||
done(Config.Language.connectingerr)
|
||||
CancelEvent()
|
||||
return
|
||||
end
|
||||
|
||||
done(nil, data.deferrals)
|
||||
|
||||
if Config.EnableGrace then Queue:AddPriority(ids[1], Config.GracePower, Config.GraceTime) end
|
||||
|
||||
Queue:RemoveFromQueue(ids)
|
||||
Queue:DebugPrint(name .. "[" .. ids[1] .. "] is loading into the server")
|
||||
return
|
||||
end
|
||||
|
||||
local seconds = data.queuetime()
|
||||
local qTime = string_format("%02d", math_floor((seconds % 86400) / 3600)) .. ":" .. string_format("%02d", math_floor((seconds % 3600) / 60)) .. ":" .. string_format("%02d", math_floor(seconds % 60))
|
||||
|
||||
local msg = string_format(Config.Language.pos .. ((Queue:TempSize() and Config.ShowTemp) and " (" .. Queue:TempSize() .. " temp)" or ""), pos, Queue:GetSize(), qTime)
|
||||
update(msg, data.deferrals)
|
||||
end
|
||||
end
|
||||
AddEventHandler("playerConnecting", playerConnect)
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
local function remove(data, pos, msg)
|
||||
if data and data.source then
|
||||
Queue:RemoveFromQueue(data.source, true)
|
||||
Queue:RemoveFromConnecting(data.source, true)
|
||||
elseif pos then
|
||||
table_remove(Queue:GetQueueList(), pos)
|
||||
end
|
||||
end
|
||||
|
||||
while true do
|
||||
Citizen.Wait(1000)
|
||||
|
||||
local i = 1
|
||||
|
||||
while i <= Queue:ConnectingSize() do
|
||||
local data = Queue:GetConnectingList()[i]
|
||||
|
||||
local endPoint = GetPlayerEndpoint(data.source)
|
||||
|
||||
data.timeout = data.timeout + 1
|
||||
|
||||
if ((data.timeout >= 300 and not endPoint) or data.timeout >= Config.ConnectTimeOut) and data.source ~= "debug" and os_time() - data.firstconnect > 5 then
|
||||
remove(data)
|
||||
Queue:DebugPrint(data.name .. "[" .. data.ids[1] .. "] was removed from the connecting queue because they timed out")
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
for id, data in pairs(Queue:GetTempPriorityList()) do
|
||||
if os_time() >= data.endTime then
|
||||
Queue:GetTempPriorityList()[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
Queue.MaxPlayers = GetConvarInt("sv_maxclients", 30)
|
||||
Queue.Debug = GetConvar("sv_debugqueue", "true") == "true" and true or false
|
||||
Queue.DisplayQueue = GetConvar("sv_displayqueue", "true") == "true" and true or false
|
||||
|
||||
local qCount = Queue:GetSize()
|
||||
|
||||
if Queue.DisplayQueue then
|
||||
if Queue.InitHostName then
|
||||
SetConvar("sv_hostname", (qCount > 0 and "[" .. tostring(qCount) .. "] " or "") .. Queue.InitHostName)
|
||||
else
|
||||
Queue.InitHostName = GetConvar("sv_hostname")
|
||||
Queue.InitHostName = Queue.InitHostName ~= "default FXServer" and Queue.InitHostName or false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterServerEvent("Queue:playerActivated")
|
||||
AddEventHandler("Queue:playerActivated", function()
|
||||
local src = source
|
||||
local ids = Queue:GetIds(src)
|
||||
|
||||
if not Queue:GetPlayerList()[src] then
|
||||
_Queue.PlayerCount = Queue:GetPlayerCount() + 1
|
||||
Queue:GetPlayerList()[src] = true
|
||||
Queue:RemoveFromQueue(ids)
|
||||
Queue:RemoveFromConnecting(ids)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("playerDropped", function()
|
||||
local src = source
|
||||
local ids = Queue:GetIds(src)
|
||||
|
||||
if Queue:GetPlayerList()[src] then
|
||||
_Queue.PlayerCount = Queue:GetPlayerCount() - 1
|
||||
Queue:GetPlayerList()[src] = nil
|
||||
Queue:RemoveFromQueue(ids)
|
||||
Queue:RemoveFromConnecting(ids)
|
||||
if Config.EnableGrace then Queue:AddPriority(ids[1], Config.GracePower, Config.GraceTime) end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource)
|
||||
if Queue.DisplayQueue and Queue.InitHostName and resource == GetCurrentResourceName() then SetConvar("sv_hostname", Queue.InitHostName) end
|
||||
|
||||
for k, data in ipairs(_Queue.JoinCbs) do
|
||||
if data.resource == resource then
|
||||
table_remove(_Queue.JoinCbs, k)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if Config.DisableHardCap then
|
||||
Queue:DebugPrint("^1 [connectqueue] Disabling hardcap ^7")
|
||||
|
||||
AddEventHandler("onResourceStarting", function(resource)
|
||||
if resource == "hardcap" then CancelEvent() return end
|
||||
end)
|
||||
|
||||
StopResource("hardcap")
|
||||
end
|
||||
|
||||
local testAdds = 0
|
||||
local commands = {}
|
||||
|
||||
commands.addq = function()
|
||||
Queue:DebugPrint("ADDED DEBUG QUEUE")
|
||||
Queue:AddToQueue({"steam:110000103fd1bb1"..testAdds}, os_time(), "TestAdd: " .. testAdds, "debug")
|
||||
testAdds = testAdds + 1
|
||||
end
|
||||
|
||||
commands.removeq = function(args)
|
||||
args[1] = tonumber(args[1])
|
||||
local name = Queue:GetQueueList()[args[1]] and Queue:GetQueueList()[args[1]].name or nil
|
||||
Queue:RemoveFromQueue(nil, nil, args[1])
|
||||
Queue:DebugPrint("REMOVED " .. tostring(name) .. " FROM THE QUEUE")
|
||||
end
|
||||
|
||||
commands.printq = function()
|
||||
Queue:DebugPrint("CURRENT QUEUE LIST")
|
||||
|
||||
for pos, data in ipairs(Queue:GetQueueList()) do
|
||||
Queue:DebugPrint(pos .. ": [src: " .. data.source .. "] " .. data.name .. "[" .. data.ids[1] .. "] | Priority: " .. (tostring(data.priority and data.priority or false)) .. " | Last Msg: " .. (data.source ~= "debug" and GetPlayerLastMsg(data.source) or "debug") .. " | Timeout: " .. data.timeout .. " | Queue Time: " .. data.queuetime() .. " Seconds")
|
||||
end
|
||||
end
|
||||
|
||||
commands.addc = function()
|
||||
Queue:AddToConnecting({"debug"})
|
||||
Queue:DebugPrint("ADDED DEBUG CONNECTING QUEUE")
|
||||
end
|
||||
|
||||
commands.removec = function(args)
|
||||
args[1] = tonumber(args[1])
|
||||
local name = Queue:GetConnectingList()[args[1]] and Queue:GetConnectingList()[args[1]].name or nil
|
||||
Queue:RemoveFromConnecting(nil, nil, args[1])
|
||||
Queue:DebugPrint("REMOVED " .. tostring(name) .. " FROM THE CONNECTING LIST")
|
||||
end
|
||||
|
||||
commands.printc = function()
|
||||
Queue:DebugPrint("CURRENT CONNECTING LIST")
|
||||
|
||||
for pos, data in ipairs(Queue:GetConnectingList()) do
|
||||
Queue:DebugPrint(pos .. ": [src: " .. data.source .. "] " .. data.name .. "[" .. data.ids[1] .. "] | Priority: " .. (tostring(data.priority and data.priority or false)) .. " | Last Msg: " .. (data.source ~= "debug" and GetPlayerLastMsg(data.source) or "debug") .. " | Timeout: " .. data.timeout)
|
||||
end
|
||||
end
|
||||
|
||||
commands.printl = function()
|
||||
for k, joined in pairs(Queue:GetPlayerList()) do
|
||||
Queue:DebugPrint(k .. ": " .. tostring(joined))
|
||||
end
|
||||
end
|
||||
|
||||
commands.printp = function()
|
||||
Queue:DebugPrint("CURRENT PRIORITY LIST")
|
||||
|
||||
for id, power in pairs(Queue:GetPriorityList()) do
|
||||
Queue:DebugPrint(id .. ": " .. tostring(power))
|
||||
end
|
||||
end
|
||||
|
||||
commands.printcount = function()
|
||||
Queue:DebugPrint("Player Count: " .. Queue:GetPlayerCount())
|
||||
end
|
||||
|
||||
commands.printtp = function()
|
||||
Queue:DebugPrint("CURRENT TEMP PRIORITY LIST")
|
||||
|
||||
for k, data in pairs(Queue:GetTempPriorityList()) do
|
||||
Queue:DebugPrint(k .. ": Power: " .. tostring(data.power) .. " | EndTime: " .. tostring(data.endTime) .. " | CurTime: " .. tostring(os_time()))
|
||||
end
|
||||
end
|
||||
|
||||
commands.removetp = function(args)
|
||||
if not args[1] then return end
|
||||
|
||||
Queue:GetTempPriorityList()[args[1]] = nil
|
||||
Queue:DebugPrint("REMOVED " .. args[1] .. " FROM THE TEMP PRIORITY LIST")
|
||||
end
|
||||
|
||||
commands.setpos = function(args)
|
||||
if not args[1] or not args[2] then return end
|
||||
|
||||
args[1], args[2] = tonumber(args[1]), tonumber(args[2])
|
||||
|
||||
local data = Queue:GetQueueList()[args[1]]
|
||||
|
||||
Queue:SetPos(data.ids, args[2])
|
||||
|
||||
Queue:DebugPrint("SET " .. data.name .. "'s QUEUE POSITION TO: " .. args[2])
|
||||
end
|
||||
|
||||
commands.setdata = function(args)
|
||||
if not args[1] or not args[2] or not args[3] then return end
|
||||
args[1] = tonumber(args[1])
|
||||
|
||||
local num = tonumber(args[3])
|
||||
local data = Queue:GetQueueList()[args[1]]
|
||||
|
||||
if args[2] == "queuetime" then
|
||||
local time = data.queuetime()
|
||||
local dif = time - num
|
||||
|
||||
data.firstconnect = data.firstconnect + dif
|
||||
data.queuetime = function() return (os_time() - data.firstconnect) end
|
||||
else
|
||||
data[args[2]] = num and num or args[3]
|
||||
end
|
||||
|
||||
Queue:DebugPrint("SET " .. data.name .. "'s " .. args[2] .. " DATA TO " .. args[3])
|
||||
end
|
||||
|
||||
commands.commands = function()
|
||||
for cmd, func in pairs(commands) do
|
||||
Queue:DebugPrint(tostring(cmd))
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler("rconCommand", function(command, args)
|
||||
if command == "queue" and commands[args[1]] then
|
||||
command = args[1]
|
||||
table_remove(args, 1)
|
||||
commands[command](args)
|
||||
CancelEvent()
|
||||
end
|
||||
end)
|
||||
11
resources/[core]/menuv/.gitattributes
vendored
11
resources/[core]/menuv/.gitattributes
vendored
@ -1,11 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
*.sh text eol=lf
|
||||
*.bat text eol=crlf
|
||||
|
||||
*.ytd binary
|
||||
*.jpeg binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
*.psd binary
|
||||
210
resources/[core]/menuv/.gitignore
vendored
210
resources/[core]/menuv/.gitignore
vendored
@ -1,210 +0,0 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/vscode,lua,vue,vuejs,node,yarn
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=vscode,lua,vue,vuejs,node,yarn
|
||||
|
||||
### Lua ###
|
||||
# Compiled Lua sources
|
||||
luac.out
|
||||
|
||||
# luarocks build files
|
||||
*.src.rock
|
||||
*.zip
|
||||
*.tar.gz
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.os
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
*.def
|
||||
*.exp
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
package-lock.json
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
build/*
|
||||
build/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env*.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
### vscode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
### IntelliJ ###
|
||||
.idea
|
||||
|
||||
### Vue ###
|
||||
# gitignore template for Vue.js projects
|
||||
#
|
||||
# Recommended template: Node.gitignore
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
docs/_book
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
test/
|
||||
|
||||
### Vuejs ###
|
||||
# Recommended template: Node.gitignore
|
||||
|
||||
dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
### yarn ###
|
||||
# https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored
|
||||
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.yarn.installed
|
||||
|
||||
# if you are NOT using Zero-installs, then:
|
||||
# comment the following lines
|
||||
!.yarn/cache
|
||||
yarn.lock
|
||||
*.lock
|
||||
|
||||
# and uncomment the following lines
|
||||
# .pnp.*
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/vscode,lua,vue,vuejs,node,yarn
|
||||
@ -1,124 +0,0 @@
|
||||
# MenuV | Standalone Menu for FiveM | NUI Menu
|
||||
[](https://github.com/ThymonA/menuv)
|
||||
|
||||
[](https://github.com/ThymonA/menuv/issues)
|
||||
[](https://github.com/ThymonA/menuv/blob/master/LICENSE)
|
||||
[](https://github.com/ThymonA/menuv)
|
||||
[](https://github.com/ThymonA/menuv)
|
||||
[](https://discordapp.com/users/733686533873467463)
|
||||
|
||||
---
|
||||
|
||||
**[MenuV](https://github.com/ThymonA/menuv)** is a library written for **[FiveM](https://fivem.net/)** and only uses NUI functionalities. This library allows you to create menus in **[FiveM](https://fivem.net/)**. This project is open-source and you must respect the [license](https://github.com/ThymonA/menuv/blob/master/LICENSE) and the hard work.
|
||||
|
||||
## Features
|
||||
- Support for simple buttons, sliders, checkboxes, lists and confirms
|
||||
- Support for emojis on items
|
||||
- Support for custom colors (RGB)
|
||||
- Support for all screen resolutions.
|
||||
- Item descriptions
|
||||
- Rebindable keys
|
||||
- Event-based callbacks
|
||||
- Uses `2 msec` while menu open and idle.
|
||||
- Documentation on [menuv.fivem.io/api/](https://menuv.fivem.io/api/)
|
||||
- Themes: **[default](https://i.imgur.com/xGagIBm.png)** or **[native](https://i.imgur.com/KSkeiQm.png)**
|
||||
|
||||
## Compile files
|
||||
**[MenuV](https://github.com/ThymonA/menuv)** uses **[VueJS](https://vuejs.org/v2/guide/installation.html#NPM)** and **[TypeScript](https://www.npmjs.com/package/typescript)** with **[NodeJS](https://nodejs.org/en/)**. If you want to use the **`master`** files, you need to build the hole project by doing:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
After you have downloaded/loaded all dependencies, you can build **[MenuV](https://github.com/ThymonA/menuv)** files by executing the following command:
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
After the command is executed you will see a `build` folder containing all the resource files.
|
||||
Copy those files to a resource folder called `menuv` or create a symbolic link like that:
|
||||
|
||||
### Windows
|
||||
|
||||
```batch
|
||||
mklink /J "repositoryPath\build" "fxResourcesPath\menuv"
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```sh
|
||||
ln -s "repositoryPath\build" "fxResourcesPath\menuv"
|
||||
```
|
||||
|
||||
You can also check this tutorial on how to make a link:
|
||||
[https://www.howtogeek.com/howto/16226/complete-guide-to-symbolic-links-symlinks-on-windows-or-linux/](https://www.howtogeek.com/howto/16226/complete-guide-to-symbolic-links-symlinks-on-windows-or-linux/)
|
||||
|
||||
**When your downloading a [release](https://github.com/ThymonA/menuv/releases), you don't have to follow this step, because all [release](https://github.com/ThymonA/menuv/releases) version are build version.**
|
||||
|
||||
## How to use?
|
||||
> ⚠️ **example.lua** can't be added in the **menuv** fxmanifest, you must make an seperate resource like: **[menuv_example](https://github.com/ThymonA/menuv/tree/master/example)**
|
||||
1. Add `start menuv` to your **server.cfg** before the resources that's uses **menuv**
|
||||
2. To use **[MenuV](https://github.com/ThymonA/menuv)** you must add **@menuv/menuv.lua** in your **fxmanifest.lua** file.
|
||||
|
||||
```lua
|
||||
client_scripts {
|
||||
'@menuv/menuv.lua',
|
||||
'example.lua'
|
||||
}
|
||||
```
|
||||
|
||||
### Create a menu
|
||||
Create a menu by calling the **MenuV:CreateMenu** function.
|
||||
```ts
|
||||
MenuV:CreateMenu(title: string, subtitle: string, position: string, red: number, green: number, blue: number, texture: string, disctionary: string, namespace: string, theme: string)
|
||||
```
|
||||
**Example:**
|
||||
```lua
|
||||
local menu = MenuV:CreateMenu('MenuV', 'Welcome to MenuV', 'topleft', 255, 0, 0, 'size-125', 'default', 'menuv', 'example_namespace', 'native')
|
||||
```
|
||||
|
||||
### Create menu items
|
||||
Create a item by calling **AddButton**, **AddConfirm**, **AddRange**, **AddCheckbox** or **AddSlider** in the created menu
|
||||
```ts
|
||||
/** CREATE A BUTTON */
|
||||
menu:AddButton({ icon: string, label: string, description: string, value: any, disabled: boolean });
|
||||
|
||||
/** CREATE A CONFIRM */
|
||||
menu:AddConfirm({ icon: string, label: string, description: string, value: boolean, disabled: boolean });
|
||||
|
||||
/** CREATE A RANGE */
|
||||
menu:AddRange({ icon: string, label: string, description: string, value: number, min: number, max: number, disabled: boolean });
|
||||
|
||||
/** CREATE A CHECKBOX */
|
||||
menu:AddCheckbox({ icon: string, label: string, description: string, value: boolean, disabled: boolean });
|
||||
|
||||
/** CREATE A SLIDER */
|
||||
menu:AddSlider({ icon: string, label: string, description: string, value: number, values: [] { label: string, value: any, description: string }, disabled: boolean });
|
||||
```
|
||||
To see example in practice, see [example.lua](https://github.com/ThymonA/menuv/blob/master/example/example.lua)
|
||||
|
||||
### Events
|
||||
In **[MenuV](https://github.com/ThymonA/menuv)** you can register event-based callbacks on menu and/or items.
|
||||
```ts
|
||||
/** REGISTER A EVENT ON MENU */
|
||||
menu:On(event: string, callback: function);
|
||||
|
||||
/** REGISTER A EVENT ON ANY ITEM */
|
||||
item:On(event: string, callback: function);
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Read **[MenuV documentation](https://menuv.fivem.io/api/)**
|
||||
|
||||
## License
|
||||
Project is written by **[ThymonA](https://github.com/ThymonA/)** and published under
|
||||
**GNU General Public License v3.0**
|
||||
[Read License](https://github.com/ThymonA/menuv/blob/master/LICENSE)
|
||||
|
||||
## Screenshot
|
||||
**How is this menu made?** see **[example.lua](https://github.com/ThymonA/menuv/blob/master/example/example.lua)**
|
||||
|
||||
|
||||
Default | Native
|
||||
:-------|:--------
|
||||
 | 
|
||||
[Default Theme](https://i.imgur.com/xGagIBm.png) | [Native Theme](https://i.imgur.com/KSkeiQm.png)
|
||||
@ -1,163 +0,0 @@
|
||||
/**
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
|
||||
const process = require("process");
|
||||
const colors = require('colors/safe');
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
const path = require('path');
|
||||
const recursive = require('recursive-readdir');
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const DEBUG = {
|
||||
PRINT: function(msg) {
|
||||
console.log(colors.bgBlack(`[${colors.bold(colors.blue('MenuV'))}][${colors.bold(colors.green('BUILD'))}] ${colors.bold(colors.white(msg))}`));
|
||||
},
|
||||
ERROR: function(msg) {
|
||||
console.log(colors.bgBlack(`[${colors.bold(colors.blue('MenuV'))}][${colors.bold(colors.green('BUILD'))}][${colors.bold(colors.red('ERROR'))}] ${colors.bold(colors.white(msg))}`));
|
||||
}
|
||||
}
|
||||
|
||||
const PATHS = {
|
||||
SOURCE: path.resolve(`${__dirname}/source`),
|
||||
BUILD: path.resolve(`${__dirname}/build`),
|
||||
VERSION: path.resolve(`${__dirname}/source/VERSION`),
|
||||
APP: path.resolve(`${__dirname}/source/app`),
|
||||
MENUV: path.resolve(`${__dirname}/source/menuv.lua`)
|
||||
}
|
||||
|
||||
const version = fs.readFileSync(PATHS.VERSION, { encoding: 'utf8' });
|
||||
const COPY_FILES = [
|
||||
{ from: `${__dirname}/source/VERSION`, to: `${PATHS.BUILD}/VERSION`, type: 'file' },
|
||||
{ from: `${__dirname}/README.md`, to: `${PATHS.BUILD}/README.md`, type: 'file' },
|
||||
{ from: `${PATHS.APP}/menuv.lua`, to: `${PATHS.BUILD}/menuv/menuv.lua`, type: 'file' },
|
||||
{ from: `${PATHS.APP}/fxmanifest.lua`, to: `${PATHS.BUILD}/fxmanifest.lua`, type: 'file' },
|
||||
{ from: `${__dirname}/LICENSE`, to: `${PATHS.BUILD}/LICENSE`, type: 'file' },
|
||||
{ from: `${__dirname}/example`, to: `${PATHS.BUILD}/menuv_example`, type: 'dir' },
|
||||
{ from: `${__dirname}/source/config.lua`, to: `${PATHS.BUILD}/config.lua`, type: 'file' },
|
||||
{ from: `${__dirname}/templates`, to: `${PATHS.BUILD}/templates`, type: 'dir' },
|
||||
{ from: `${__dirname}/templates/menuv.ytd`, to: `${PATHS.BUILD}/stream/menuv.ytd`, type: 'file' },
|
||||
{ from: `${__dirname}/source/languages`, to: `${PATHS.BUILD}/languages`, type: 'dir' },
|
||||
{ from: `${__dirname}/dist`, to: `${PATHS.BUILD}/dist`, type: 'dir', deleteAfter: true },
|
||||
{ from: `${PATHS.APP}/lua_components`, to: `${PATHS.BUILD}/menuv/components`, type: 'dir' }
|
||||
];
|
||||
|
||||
DEBUG.PRINT(`Building ${colors.yellow('MenuV')} version ${colors.yellow(version)}...`)
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith("--mode=")) {
|
||||
const configuration = args[i].substr(7).toLowerCase();
|
||||
|
||||
switch (configuration) {
|
||||
case "production":
|
||||
case "release":
|
||||
args[i] = '--mode=production';
|
||||
break;
|
||||
case "development":
|
||||
case "debug":
|
||||
args[i] = '--mode=development';
|
||||
break;
|
||||
default:
|
||||
args[i] = '--mode=none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let argumentString = args.join(" ");
|
||||
|
||||
if (argumentString.length > 0) {
|
||||
argumentString = ` ${argumentString}`;
|
||||
} else {
|
||||
argumentString = ` --mode=production`;
|
||||
}
|
||||
|
||||
exec(`npx webpack${argumentString}`, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
DEBUG.ERROR(err.stack);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(PATHS.BUILD)) {
|
||||
fs.mkdirSync(PATHS.BUILD, { recursive: true });
|
||||
}
|
||||
|
||||
fse.emptyDirSync(PATHS.BUILD);
|
||||
|
||||
for (var i = 0; i < COPY_FILES.length; i++) {
|
||||
const copy_file = COPY_FILES[i];
|
||||
const from_file_path = path.resolve(copy_file.from);
|
||||
const to_file_path = path.resolve(copy_file.to);
|
||||
|
||||
if (copy_file.type == 'file') {
|
||||
const to_file_path_directory = path.dirname(to_file_path);
|
||||
|
||||
if (!fs.existsSync(to_file_path_directory))
|
||||
fs.mkdirSync(to_file_path_directory, { recursive: true });
|
||||
|
||||
fs.copyFileSync(from_file_path, to_file_path)
|
||||
} else {
|
||||
if (!fs.existsSync(to_file_path))
|
||||
fs.mkdirSync(to_file_path, { recursive: true });
|
||||
|
||||
fse.copySync(from_file_path, to_file_path, { recursive: true });
|
||||
}
|
||||
|
||||
if (copy_file.deleteAfter)
|
||||
fse.rmdirSync(from_file_path, { recursive: true });
|
||||
}
|
||||
|
||||
let menuv_file = fs.readFileSync(PATHS.MENUV, { encoding: 'utf8' });
|
||||
const regex = /---@load '(.*?)'/gm;
|
||||
|
||||
let m;
|
||||
|
||||
while ((m = regex.exec(menuv_file)) != null) {
|
||||
if (m.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
|
||||
m.forEach((match, groupIndex) => {
|
||||
if (groupIndex == 1) {
|
||||
const content_path = path.resolve(`${PATHS.SOURCE}/${match}`);
|
||||
|
||||
if (fs.existsSync(content_path)) {
|
||||
const content = fs.readFileSync(content_path, { encoding: 'utf8' });
|
||||
const content_regex = new RegExp(`---@load '${match}'`, 'g');
|
||||
|
||||
menuv_file = menuv_file.replace(content_regex, content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const final_menuv_path = path.resolve(`${PATHS.BUILD}/menuv.lua`);
|
||||
|
||||
fs.writeFileSync(final_menuv_path, menuv_file);
|
||||
|
||||
recursive(PATHS.BUILD, ['*.woff', '*.ytd', '*.png', '*.psd'], function (err, files) {
|
||||
const version_regex = /Version: 1\.0\.0/g
|
||||
const version_regex2 = /version '1\.0\.0'/g
|
||||
|
||||
for(var i = 0; i < files.length; i++) {
|
||||
const file = path.resolve(files[i]);
|
||||
const file_content = fs.readFileSync(file, { encoding: 'utf8' })
|
||||
.replace(version_regex, `Version: ${version}`)
|
||||
.replace(version_regex2, `version '${version}'`);
|
||||
|
||||
fs.writeFileSync(file, file_content);
|
||||
}
|
||||
|
||||
DEBUG.PRINT(`${colors.yellow('MenuV')} version ${colors.yellow(version)} successfully build\n${colors.bold('Location: ')} ${PATHS.BUILD}`);
|
||||
});
|
||||
});
|
||||
@ -1,45 +0,0 @@
|
||||
--- MenuV Menu
|
||||
---@type Menu
|
||||
local menu = MenuV:CreateMenu(false, 'Welcome to MenuV', 'topleft', 255, 0, 0, 'size-125', 'example', 'menuv', 'example_namespace')
|
||||
local menu2 = MenuV:CreateMenu('Demo 2', 'Open this demo menu in MenuV', 'topleft', 255, 0, 0)
|
||||
|
||||
local menu_button = menu:AddButton({ icon = '😃', label = 'Open Demo 2 Menu', value = menu2, description = 'YEA :D from first menu' })
|
||||
local menu2_button = menu2:AddButton({ icon = '😃', label = 'Open First Menu', value = menu, description = 'YEA :D from second menu' })
|
||||
local confirm = menu:AddConfirm({ icon = '🔥', label = 'Confirm', value = 'no' })
|
||||
local range = menu:AddRange({ icon = '⚽', label = 'Range Item', min = 0, max = 10, value = 0, saveOnUpdate = true })
|
||||
local checkbox = menu:AddCheckbox({ icon = '💡', label = 'Checkbox Item', value = 'n' })
|
||||
local checkbox_disabled = menu:AddCheckbox({ icon = '💡', label = 'Checkbox Disabled', value = 'n', disabled = true })
|
||||
local slider = menu:AddSlider({ icon = '❤️', label = 'Slider', value = 'demo', values = {
|
||||
{ label = 'Demo Item', value = 'demo', description = 'Demo Item 1' },
|
||||
{ label = 'Demo Item 2', value = 'demo2', description = 'Demo Item 2' },
|
||||
{ label = 'Demo Item 3', value = 'demo3', description = 'Demo Item 3' },
|
||||
{ label = 'Demo Item 4', value = 'demo4', description = 'Demo Item 4' }
|
||||
}})
|
||||
|
||||
--- Events
|
||||
confirm:On('confirm', function(item) print('YOU ACCEPTED THE TERMS') end)
|
||||
confirm:On('deny', function(item) print('YOU DENIED THE TERMS') end)
|
||||
|
||||
range:On('select', function(item, value) print(('FROM %s to %s YOU SELECTED %s'):format(item.Min, item.Max, value)) end)
|
||||
range:On('change', function(item, newValue, oldValue)
|
||||
menu.Title = ('MenuV %s'):format(newValue)
|
||||
end)
|
||||
|
||||
slider:On('select', function(item, value) print(('YOU SELECTED %s'):format(value)) end)
|
||||
|
||||
confirm:On('enter', function(item) print('YOU HAVE NOW A CONFIRM ACTIVE') end)
|
||||
confirm:On('leave', function(item) print('YOU LEFT OUR CONFIRM :(') end)
|
||||
|
||||
menu:On('switch', function(item, currentItem, prevItem) print(('YOU HAVE SWITCH THE ITEMS FROM %s TO %s'):format(prevItem.__type, currentItem.__type)) end)
|
||||
|
||||
menu2:On('open', function(m)
|
||||
m:ClearItems()
|
||||
|
||||
for i = 1, 10, 1 do
|
||||
math.randomseed(GetGameTimer() + i)
|
||||
|
||||
m:AddButton({ ignoreUpdate = i ~= 10, icon = '❤️', label = ('Open Menu %s'):format(math.random(0, 1000)), value = menu, description = ('YEA! ANOTHER RANDOM NUMBER: %s'):format(math.random(0, 1000)), select = function(i) print('YOU CLICKED ON THIS ITEM!!!!') end })
|
||||
end
|
||||
end)
|
||||
|
||||
menu:OpenWith('KEYBOARD', 'F1') -- Press F1 to open Menu
|
||||
@ -1,28 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu libarary for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
name 'MenuV'
|
||||
version '1.0.0'
|
||||
description 'FiveM menu libarary for creating menu\'s'
|
||||
author 'ThymonA'
|
||||
contact 'contact@arens.io'
|
||||
url 'https://github.com/ThymonA/menuv/'
|
||||
|
||||
client_scripts {
|
||||
'@menuv/menuv.lua',
|
||||
'example.lua'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
'menuv'
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "menuv",
|
||||
"version": "1.0.0",
|
||||
"description": "FiveM menu library for creating menu's",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node ./build.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ThymonA/menuv.git"
|
||||
},
|
||||
"keywords": [
|
||||
"menuv",
|
||||
"fivem",
|
||||
"library",
|
||||
"thymona",
|
||||
"tigodevelopment"
|
||||
],
|
||||
"author": "ThymonA",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ThymonA/menuv/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ThymonA/menuv#readme",
|
||||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"copy-webpack-plugin": "^7.0.0",
|
||||
"css-loader": "^5.0.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"html-webpack-plugin": "^4.5.1",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"ts-loader": "^8.0.14",
|
||||
"typescript": "^4.1.3",
|
||||
"vue": "^2.6.12",
|
||||
"vue-loader": "^15.9.6",
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"command-line-args": "^5.1.1",
|
||||
"webpack": "^5.12.3",
|
||||
"webpack-cli": "^4.3.1",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
1.5-beta
|
||||
@ -1,37 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
lua54 'yes'
|
||||
|
||||
name 'MenuV'
|
||||
version '1.0.0'
|
||||
description 'FiveM menu library for creating menu\'s'
|
||||
author 'ThymonA'
|
||||
contact 'contact@arens.io'
|
||||
url 'https://github.com/ThymonA/menuv/'
|
||||
|
||||
files {
|
||||
'menuv.lua',
|
||||
'menuv/components/*.lua',
|
||||
'dist/*.html',
|
||||
'dist/assets/css/*.css',
|
||||
'dist/assets/js/*.js',
|
||||
'dist/assets/fonts/*.woff',
|
||||
'languages/*.json'
|
||||
}
|
||||
|
||||
ui_page 'dist/menuv.html'
|
||||
|
||||
client_scripts {
|
||||
'config.lua',
|
||||
'menuv/components/utilities.lua',
|
||||
'menuv/menuv.lua'
|
||||
}
|
||||
@ -1,487 +0,0 @@
|
||||
/*
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
@import url('https://fonts.googleapis.com/css2?family=Epilogue:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
font-family: 'Epilogue', sans-serif;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
* .hide,
|
||||
html .hide,
|
||||
body .hide,
|
||||
div .hide,
|
||||
.menuv.default.hide {
|
||||
display: none !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.menuv.default {
|
||||
min-width: 20em;
|
||||
max-width: 20em;
|
||||
max-height: 90vh;
|
||||
margin-top: 1em;
|
||||
margin-left: 1em;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.menuv.default.topcenter {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.menuv.default.topright {
|
||||
margin-right: 1em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.menuv.default.centerleft {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.menuv.default.center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.menuv.default.centerright {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.menuv.default.bottomleft {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
}
|
||||
|
||||
.menuv.default.bottomcenter {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.menuv.default.bottomright {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.menuv.default.size-100 {
|
||||
zoom: 1;
|
||||
}
|
||||
.menuv.default.size-110 {
|
||||
zoom: 1.1;
|
||||
}
|
||||
.menuv.default.size-125 {
|
||||
zoom: 1.25;
|
||||
}
|
||||
.menuv.default.size-150 {
|
||||
zoom: 1.50;
|
||||
}
|
||||
.menuv.default.size-175 {
|
||||
zoom: 1.75;
|
||||
}
|
||||
.menuv.default.size-200 {
|
||||
zoom: 2;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-header {
|
||||
height: 3.5em;
|
||||
max-height: 3.5em;
|
||||
line-height: 3.5em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
letter-spacing: 0.25em;
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
background-color: black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-header strong {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-header .menuv-bg-icon {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
max-height: 3.5em;
|
||||
max-width: 3.5em;
|
||||
overflow: hidden;
|
||||
top: 0.6em;
|
||||
margin-left: 9.8em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-header .menuv-bg-icon i,
|
||||
.menuv.default .menuv-header .menuv-bg-icon svg {
|
||||
font-size: 5em;
|
||||
opacity: 0.5;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-subheader {
|
||||
background-color: blue;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
font-size: 0.9em;
|
||||
line-height: 2.5em;
|
||||
text-transform: uppercase;
|
||||
padding-left: 1em;
|
||||
height: 2.5em;
|
||||
max-height: 2.5em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items {
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
max-height: 50.75vh;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.default.size-100 .menuv-items {
|
||||
max-height: 66.1vh;
|
||||
}
|
||||
|
||||
.menuv.default.size-110 .menuv-items {
|
||||
max-height: 59.2vh;
|
||||
}
|
||||
|
||||
.menuv.default.size-125 .menuv-items {
|
||||
max-height: 50.2vh;
|
||||
}
|
||||
|
||||
.menuv.default.size-150 .menuv-items {
|
||||
max-height: 45.8vh;
|
||||
}
|
||||
|
||||
.menuv.default.size-175 .menuv-items {
|
||||
max-height: 39vh;
|
||||
}
|
||||
|
||||
.menuv.default.size-200 .menuv-items {
|
||||
max-height: 32.2vh;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item {
|
||||
padding: 0.25em 0.50em;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
max-height: auto;
|
||||
height: auto;
|
||||
min-height: 2em;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
color: white;
|
||||
width: 100%;
|
||||
min-width: 20em;
|
||||
max-width: 30em;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item i,
|
||||
.menuv.default .menuv-items .menuv-item svg {
|
||||
float: right;
|
||||
margin-top: 0.125em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.disabled {
|
||||
opacity: 0.75;
|
||||
background: #383838;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active i,
|
||||
.menuv.default .menuv-items .menuv-item.active svg {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .item-title {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active .item-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-icon {
|
||||
margin-left: 2.5px;
|
||||
margin-right: 5px;
|
||||
border-right: 1px solid white;
|
||||
padding-right: 5px;
|
||||
float: left;
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .flex-left {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-title {
|
||||
word-break: break-all;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
max-height: 2em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 14em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .item-icon {
|
||||
width: 2.5em;
|
||||
max-width: 2.5em;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item {
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
|
||||
align-items: center;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active span.menuv-icon {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-options {
|
||||
float: right;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.85em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active span.menuv-options {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-options i,
|
||||
.menuv.default .menuv-items span.menuv-options svg {
|
||||
float: unset;
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-options i:first-child,
|
||||
.menuv.default .menuv-items span.menuv-options svg:first-child {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-options i:last-child,
|
||||
.menuv.default .menuv-items span.menuv-options svg:last-child {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-options span.menuv-btn {
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 0.25em 0.5em;
|
||||
margin: 0.125em;
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
border-radius: 0.125em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item span.menuv-options span.menuv-btn {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active span.menuv-options span.menuv-btn {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items span.menuv-options span.menuv-btn.active {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active span.menuv-options span.menuv-btn.active {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items input[type="range"] {
|
||||
display: flex;
|
||||
float: right;
|
||||
-webkit-appearance: none;
|
||||
max-width: 6.5em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items input[type="range"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items input[type="range"]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
background: blue;
|
||||
border-radius: 0;
|
||||
border: 0px solid #000000;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items input[type="range"]::-webkit-slider-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
height: 18px;
|
||||
width: 5px;
|
||||
border-radius: 0;
|
||||
border: 1px solid white;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-thumb {
|
||||
background: blue;
|
||||
border: 1px solid rgba(0, 0, 255, 0.25);
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items input[type="range"]:focus::-webkit-slider-runnable-track {
|
||||
background: blue;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active input[type="range"]:focus::-webkit-slider-runnable-track {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-desc {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
color: white;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: 17.5em;
|
||||
margin-left: 17.5em;
|
||||
margin-top: -0.25em;
|
||||
font-weight: 400;
|
||||
font-size: 0.9em;
|
||||
padding: 0.75em 1em;
|
||||
line-height: 1.25em;
|
||||
border-left: 0.375em solid blue;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-item.active .menuv-desc {
|
||||
display: initial;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-desc strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-desc table {
|
||||
margin-left: -0.75em;
|
||||
width: calc(100% + 0.75em);
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-desc table th {
|
||||
color: white;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-desc table td {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-items .menuv-label {
|
||||
float: right;
|
||||
font-size: 1.125em;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-pagination {
|
||||
padding: 0.5em;
|
||||
max-width: 20em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
border-top: 2px solid white;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-pagination .menu-pagination-option {
|
||||
display: inline-block;
|
||||
height: 1.5em;
|
||||
width: 3em;
|
||||
background-color: white;
|
||||
color: black;
|
||||
text-align: center;
|
||||
border-radius: 2.5px;
|
||||
margin-left: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-pagination .menu-pagination-option.active {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-pagination .menu-pagination-ellipsis {
|
||||
display: inline-block;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-description {
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
width: 100%;
|
||||
max-width: 20em;
|
||||
padding: 0.5em 1em;
|
||||
margin-top: 0.5em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menuv.default .menuv-description strong {
|
||||
color: white;
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
}
|
||||
@ -1,507 +0,0 @@
|
||||
/*
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'SignPainter';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: local('../fonts/SignPainter'), url('../fonts/SignPainterHouseScript.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'TTCommons';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: local('../fonts/TTCommons'), url('../fonts/TTCommons.woff') format('woff');
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
* .hide,
|
||||
html .hide,
|
||||
body .hide,
|
||||
div .hide,
|
||||
.menuv.native.hide {
|
||||
display: none !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.menuv.native {
|
||||
min-width: 30em;
|
||||
max-width: 30em;
|
||||
max-height: 90vh;
|
||||
margin-top: 1em;
|
||||
margin-left: 1em;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.menuv.native.topcenter {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.menuv.native.topright {
|
||||
margin-right: 1em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.menuv.native.centerleft {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.menuv.native.center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.menuv.native.centerright {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.menuv.native.bottomleft {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
}
|
||||
|
||||
.menuv.native.bottomcenter {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.menuv.native.bottomright {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.menuv.native.size-100 {
|
||||
zoom: 1;
|
||||
}
|
||||
.menuv.native.size-110 {
|
||||
zoom: 1.1;
|
||||
}
|
||||
.menuv.native.size-125 {
|
||||
zoom: 1.25;
|
||||
}
|
||||
.menuv.native.size-150 {
|
||||
zoom: 1.50;
|
||||
}
|
||||
.menuv.native.size-175 {
|
||||
zoom: 1.75;
|
||||
}
|
||||
.menuv.native.size-200 {
|
||||
zoom: 2;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-header {
|
||||
height: 2em;
|
||||
max-height: 4.25em;
|
||||
line-height: 2.25em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
letter-spacing: auto;
|
||||
font-size: 3.5em;
|
||||
font-weight: normal;
|
||||
font-family: 'SignPainter';
|
||||
background-color: black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-header strong {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
max-width: 1em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-header .menuv-bg-icon {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
max-height: 4.25em;
|
||||
max-width: 4.25em;
|
||||
overflow: hidden;
|
||||
top: 0.6em;
|
||||
margin-left: 9.8em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-header .menuv-bg-icon i,
|
||||
.menuv.native .menuv-header .menuv-bg-icon svg {
|
||||
font-size: 5em;
|
||||
opacity: 0.5;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-subheader {
|
||||
background-color: black !important;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
font-size: 1.125em;
|
||||
line-height: auto;
|
||||
text-transform: uppercase;
|
||||
padding-top: 0.125em;
|
||||
padding-bottom: 0.375em;
|
||||
padding-left: 0.5em;
|
||||
height: auto;
|
||||
color: #2e69bb !important;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items {
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
padding-bottom: 0.5em;
|
||||
max-height: 50.75vh;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.native.size-100 .menuv-items {
|
||||
max-height: 66.1vh;
|
||||
}
|
||||
|
||||
.menuv.native.size-110 .menuv-items {
|
||||
max-height: 59.2vh;
|
||||
}
|
||||
|
||||
.menuv.native.size-125 .menuv-items {
|
||||
max-height: 50.2vh;
|
||||
}
|
||||
|
||||
.menuv.native.size-150 .menuv-items {
|
||||
max-height: 45.8vh;
|
||||
}
|
||||
|
||||
.menuv.native.size-175 .menuv-items {
|
||||
max-height: 39vh;
|
||||
}
|
||||
|
||||
.menuv.native.size-200 .menuv-items {
|
||||
max-height: 32.2vh;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item {
|
||||
padding: 0.25em 0.50em;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
max-height: auto;
|
||||
height: auto;
|
||||
line-height: 1.25em;
|
||||
color: white;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 30em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .flex-left {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .item-title {
|
||||
font-family: 'TTCommons';
|
||||
font-weight: 400;
|
||||
font-size: 1.35em;
|
||||
padding-top: 0.25em;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .item-icon {
|
||||
width: 2.5em;
|
||||
max-width: 2.5em;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.disabled .item-icon {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item i,
|
||||
.menuv.native .menuv-items .menuv-item svg {
|
||||
float: right;
|
||||
margin-top: 0.125em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.disabled {
|
||||
opacity: 0.75;
|
||||
background: #383838;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active i,
|
||||
.menuv.native .menuv-items .menuv-item.active svg {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-icon {
|
||||
margin-left: 2.5px;
|
||||
margin-right: 5px;
|
||||
border-right: 1px solid white;
|
||||
padding-right: 5px;
|
||||
float: left;
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-title {
|
||||
word-break: break-all;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
letter-spacing: normal;
|
||||
padding: none;
|
||||
margin: none;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active span.menuv-icon {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item {
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
|
||||
align-items: center;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-options {
|
||||
float: right;
|
||||
font-size: 0.9em;
|
||||
vertical-align: middle;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active span.menuv-options {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-options i,
|
||||
.menuv.native .menuv-items span.menuv-options svg {
|
||||
float: unset;
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-options i:first-child,
|
||||
.menuv.native .menuv-items span.menuv-options svg:first-child {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-options i:last-child,
|
||||
.menuv.native .menuv-items span.menuv-options svg:last-child {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-options span.menuv-btn {
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 0.25em 0.5em;
|
||||
margin: 0.125em;
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
border-radius: 0.125em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item span.menuv-options span.menuv-btn {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active span.menuv-options span.menuv-btn {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items span.menuv-options span.menuv-btn.active {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active span.menuv-options span.menuv-btn.active {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items input[type="range"] {
|
||||
display: flex;
|
||||
float: right;
|
||||
-webkit-appearance: none;
|
||||
max-width: 6.5em;
|
||||
margin-top: 0.15em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items input[type="range"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items input[type="range"]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
background: blue;
|
||||
border-radius: 0;
|
||||
border: 0px solid #000000;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items input[type="range"]::-webkit-slider-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
height: 18px;
|
||||
width: 5px;
|
||||
border-radius: 0;
|
||||
border: 1px solid white;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-thumb {
|
||||
background: blue;
|
||||
border: 1px solid rgba(0, 0, 255, 0.25);
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items input[type="range"]:focus::-webkit-slider-runnable-track {
|
||||
background: blue;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active input[type="range"]:focus::-webkit-slider-runnable-track {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-desc {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
color: white;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: 17.5em;
|
||||
margin-left: 17.5em;
|
||||
margin-top: -0.25em;
|
||||
font-weight: 400;
|
||||
font-size: 0.9em;
|
||||
padding: 0.75em 1em;
|
||||
line-height: 1.25em;
|
||||
border-left: 0.375em solid blue;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-item.active .menuv-desc {
|
||||
display: initial;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-desc strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-desc table {
|
||||
margin-left: -0.75em;
|
||||
width: calc(100% + 0.75em);
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-desc table th {
|
||||
color: white;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-desc table td {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-items .menuv-label {
|
||||
float: right;
|
||||
font-size: 1.125em;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-pagination {
|
||||
padding: 0.5em;
|
||||
max-width: 20em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
border-top: 2px solid white;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-pagination .menu-pagination-option {
|
||||
display: inline-block;
|
||||
height: 1.5em;
|
||||
width: 3em;
|
||||
background-color: white;
|
||||
color: black;
|
||||
text-align: center;
|
||||
border-radius: 2.5px;
|
||||
margin-left: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-pagination .menu-pagination-option.active {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-pagination .menu-pagination-ellipsis {
|
||||
display: inline-block;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-description {
|
||||
border-top: 2px solid black;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
width: 100%;
|
||||
max-width: 30em;
|
||||
padding: 0.5em 1em;
|
||||
margin-top: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menuv.native .menuv-description strong {
|
||||
color: white;
|
||||
font-family: 'TTCommons';
|
||||
font-weight: 400;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@ -1,34 +0,0 @@
|
||||
<!--
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-->
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MenuV</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="cache-control" content="max-age=0" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.1/css/bulma.min.css" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="./assets/css/main.css">
|
||||
<link rel="stylesheet" href="./assets/css/native_theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="menuv"></div>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
resources/[core]/menuv/source/app/index.d.ts
vendored
16
resources/[core]/menuv/source/app/index.d.ts
vendored
@ -1,16 +0,0 @@
|
||||
/**
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
declare module '*.vue' {
|
||||
import VUE from 'vue';
|
||||
|
||||
export default VUE
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
/**
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
import VUE from 'vue';
|
||||
import menuv from './vue_templates/menuv.vue';
|
||||
|
||||
const instance = new VUE({
|
||||
el: '#menuv',
|
||||
render: h => h(menuv)
|
||||
});
|
||||
@ -1,273 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
local assert = assert
|
||||
---@type Utilities
|
||||
local U = assert(Utilities)
|
||||
local type = assert(type)
|
||||
local pairs = assert(pairs)
|
||||
local lower = assert(string.lower)
|
||||
local upper = assert(string.upper)
|
||||
local sub = assert(string.sub)
|
||||
local pack = assert(table.pack)
|
||||
local unpack = assert(table.unpack)
|
||||
local insert = assert(table.insert)
|
||||
local rawset = assert(rawset)
|
||||
local rawget = assert(rawget)
|
||||
local setmetatable = assert(setmetatable)
|
||||
|
||||
--- FiveM globals
|
||||
local CreateThread = assert(Citizen.CreateThread)
|
||||
|
||||
--- Create a new menu item
|
||||
---@param info table Menu information
|
||||
---@return Item New item
|
||||
function CreateMenuItem(info)
|
||||
info = U:Ensure(info, {})
|
||||
|
||||
local item = {
|
||||
---@type Menu|nil
|
||||
__menu = U:Ensure(info.__Menu or info.__menu, { __class = 'Menu', __type = 'Menu' }, true) or nil,
|
||||
---@type string
|
||||
__event = U:Ensure(info.PrimaryEvent or info.primaryEvent, 'unknown'),
|
||||
---@type string
|
||||
UUID = U:UUID(),
|
||||
---@type string
|
||||
Icon = U:Ensure(info.Icon or info.icon, 'none'),
|
||||
---@type string
|
||||
Label = U:Ensure(info.Label or info.label, ''),
|
||||
---@type string
|
||||
Description = U:Ensure(info.Description or info.description, ''),
|
||||
---@type any
|
||||
Value = info.Value or info.value,
|
||||
---@type table[]
|
||||
Values = {},
|
||||
---@type number
|
||||
Min = U:Ensure(info.Min or info.min, 0),
|
||||
---@type number
|
||||
Max = U:Ensure(info.Max or info.max, 0),
|
||||
---@type boolean
|
||||
Disabled = U:Ensure(info.Disabled or info.disabled, false),
|
||||
---@type table
|
||||
Events = U:Ensure(info.Events or info.events, {}),
|
||||
---@type boolean
|
||||
SaveOnUpdate = U:Ensure(info.SaveOnUpdate or info.saveOnUpdate, false),
|
||||
---@param t Item
|
||||
---@param event string Name of Event
|
||||
Trigger = function(t, event, ...)
|
||||
event = lower(U:Ensure(event, 'unknown'))
|
||||
|
||||
if (event == 'unknown') then return end
|
||||
if (U:StartsWith(event, 'on')) then
|
||||
event = 'On' .. sub(event, 3):gsub('^%l', upper)
|
||||
else
|
||||
event = 'On' .. event:gsub('^%l', upper)
|
||||
end
|
||||
|
||||
if (not U:Any(event, (t.Events or {}), 'key')) then
|
||||
return
|
||||
end
|
||||
|
||||
local args = pack(...)
|
||||
|
||||
for _, v in pairs(t.Events[event]) do
|
||||
CreateThread(function()
|
||||
v(t, unpack(args))
|
||||
end)
|
||||
end
|
||||
end,
|
||||
---@param t Item
|
||||
---@param event string Name of event
|
||||
---@param func function|Menu Function or Menu to trigger
|
||||
On = function(t, event, func)
|
||||
event = lower(U:Ensure(event, 'unknown'))
|
||||
|
||||
if (event == 'unknown') then return end
|
||||
if (U:StartsWith(event, 'on')) then
|
||||
event = 'On' .. sub(event, 3):gsub('^%l', upper)
|
||||
else
|
||||
event = 'On' .. event:gsub('^%l', upper)
|
||||
end
|
||||
|
||||
if (not U:Any(event, (t.Events or {}), 'key')) then
|
||||
return
|
||||
end
|
||||
|
||||
local _type = U:Typeof(func)
|
||||
|
||||
if (_type == 'Menu') then
|
||||
local menu_t = {
|
||||
__class = 'function',
|
||||
__type = 'function',
|
||||
func = function(t) MenuV:OpenMenu(t.uuid) end,
|
||||
uuid = func.UUID or func.uuid or U:UUID()
|
||||
}
|
||||
local menu_mt = { __index = menu_t, __call = function(t) t:func() end }
|
||||
local menu_item = setmetatable(menu_t, menu_mt)
|
||||
|
||||
insert(t.Events[event], menu_item)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
func = U:Ensure(func, function() end)
|
||||
|
||||
insert(t.Events[event], func)
|
||||
end,
|
||||
---@param t Item
|
||||
---@param k string
|
||||
---@param v string
|
||||
Validate = U:Ensure(info.Validate or info.validate, function(t, k, v)
|
||||
return true
|
||||
end),
|
||||
---@param t Item
|
||||
---@param k string
|
||||
---@param v string
|
||||
Parser = U:Ensure(info.Parser or info.parser, function(t, k, v)
|
||||
return v
|
||||
end),
|
||||
---@param t Item
|
||||
---@param k string
|
||||
---@param v string
|
||||
NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v)
|
||||
end),
|
||||
---@param t Item
|
||||
---@return any
|
||||
GetValue = function(t)
|
||||
local itemType = U:Ensure(t.__type, 'unknown')
|
||||
|
||||
if (itemType == 'button' or itemType == 'menu' or itemType == 'unknown') then
|
||||
return t.Value
|
||||
end
|
||||
|
||||
if (itemType == 'checkbox' or itemType == 'confirm') then
|
||||
return U:Ensure(t.Value, false)
|
||||
end
|
||||
|
||||
if (itemType == 'slider') then
|
||||
for _, item in pairs(t.Values) do
|
||||
if (item.Value == t.Value) then
|
||||
return item.Value
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
if (itemType == 'range') then
|
||||
local rawValue = U:Ensure(t.Value, 0)
|
||||
|
||||
if (t.Min > rawValue) then
|
||||
return t.Min
|
||||
end
|
||||
|
||||
if (t.Max < rawValue) then
|
||||
return t.Max
|
||||
end
|
||||
|
||||
return rawValue
|
||||
end
|
||||
end,
|
||||
---@return Menu|nil
|
||||
GetParentMenu = function(t)
|
||||
return t.__menu or nil
|
||||
end
|
||||
}
|
||||
|
||||
item.Events.OnEnter = {}
|
||||
item.Events.OnLeave = {}
|
||||
item.Events.OnUpdate = {}
|
||||
item.Events.OnDestroy = {}
|
||||
|
||||
local mt = {
|
||||
__index = function(t, k)
|
||||
return rawget(t.data, k)
|
||||
end,
|
||||
__tostring = function(t)
|
||||
return t.UUID
|
||||
end,
|
||||
__call = function(t, ...)
|
||||
if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
|
||||
t:Trigger(t.__event, ...)
|
||||
end
|
||||
end,
|
||||
__newindex = function(t, k, v)
|
||||
local key = U:Ensure(k, 'unknown')
|
||||
local oldValue = rawget(t.data, k)
|
||||
local checkInput = t.Validate ~= nil and type(t.Validate) == 'function'
|
||||
local inputParser = t.Parser ~= nil and type(t.Parser) == 'function'
|
||||
local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function'
|
||||
|
||||
if (checkInput) then
|
||||
local result = t:Validate(key, v)
|
||||
result = U:Ensure(result, true)
|
||||
|
||||
if (not result) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if (inputParser) then
|
||||
local parsedValue = t:Parser(key, v)
|
||||
|
||||
v = parsedValue or v
|
||||
end
|
||||
|
||||
rawset(t.data, k, v)
|
||||
|
||||
if (updateIndexTrigger) then
|
||||
t:NewIndex(key, v)
|
||||
end
|
||||
|
||||
if (t.__menu ~= nil and U:Typeof(t.__menu) == 'Menu' and t.__menu.Trigger ~= nil and U:Typeof( t.__menu.Trigger) == 'function') then
|
||||
t.__menu:Trigger('update', 'UpdateItem', t)
|
||||
end
|
||||
|
||||
if (key == 'Value' and t.Trigger ~= nil and type(t.Trigger) == 'function') then
|
||||
t:Trigger('update', key, v, oldValue)
|
||||
end
|
||||
end,
|
||||
__metatable = 'MenuV'
|
||||
}
|
||||
|
||||
---@class Item
|
||||
---@filed private __event string Name of primary event
|
||||
---@field public UUID string UUID of Item
|
||||
---@field public Icon string Icon/Emoji for Item
|
||||
---@field public Label string Label of Item
|
||||
---@field public Description string Description of Item
|
||||
---@field public Value any Value of Item
|
||||
---@field public Values table[] List of values
|
||||
---@field public Min number Min range value
|
||||
---@field public Max number Max range value
|
||||
---@field public Disabled boolean Disabled state of Item
|
||||
---@field public SaveOnUpdate boolean Save on `update`
|
||||
---@field private Events table<string, function[]> List of registered `on` events
|
||||
---@field public Trigger fun(t: Item, event: string)
|
||||
---@field public On fun(t: Item, event: string, func: function|Menu)
|
||||
---@field public Validate fun(t: Item, k: string, v:any)
|
||||
---@field public NewIndex fun(t: Item, k: string, v: any)
|
||||
---@field public Parser fun(t: Item, k: string, v: any)
|
||||
---@field public GetValue fun(t: Item):any
|
||||
---@field public GetParentMenu func(t: Item):Menu|nil
|
||||
local i = setmetatable({ data = item, __class = 'Item', __type = U:Ensure(info.Type or info.type, 'unknown') }, mt)
|
||||
|
||||
for k, v in pairs(info or {}) do
|
||||
local key = U:Ensure(k, 'unknown')
|
||||
|
||||
if (key == 'unknown') then return end
|
||||
|
||||
i:On(key, v)
|
||||
end
|
||||
|
||||
return i
|
||||
end
|
||||
|
||||
_ENV.CreateMenuItem = CreateMenuItem
|
||||
_G.CreateMenuItem = CreateMenuItem
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,35 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
local assert = assert
|
||||
local decode = assert(json.decode)
|
||||
|
||||
--- FiveM globals
|
||||
local LoadResourceFile = assert(LoadResourceFile)
|
||||
|
||||
--- MenuV globals
|
||||
---@type Utilities
|
||||
local Utilities = assert(Utilities)
|
||||
|
||||
--- Empty translations table
|
||||
local translations = {}
|
||||
|
||||
--- Load all translations
|
||||
local lang = Utilities:Ensure((Config or {}).Language, 'en')
|
||||
local translations_path = ('languages/%s.json'):format(lang)
|
||||
local translations_raw = LoadResourceFile('menuv', translations_path)
|
||||
|
||||
if (translations_raw) then
|
||||
local transFile = decode(translations_raw)
|
||||
|
||||
if (transFile) then translations = Utilities:Ensure(transFile.translations, {}) end
|
||||
end
|
||||
|
||||
_ENV.translations = translations
|
||||
_G.translations = translations
|
||||
@ -1,427 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
local assert = assert
|
||||
local type = assert(type)
|
||||
local tonumber = assert(tonumber)
|
||||
local tostring = assert(tostring)
|
||||
local lower = assert(string.lower)
|
||||
local upper = assert(string.upper)
|
||||
local sub = assert(string.sub)
|
||||
local encode = assert(json.encode)
|
||||
local decode = assert(json.decode)
|
||||
local floor = assert(math.floor)
|
||||
local random = assert(math.random)
|
||||
local randomseed = assert(math.randomseed)
|
||||
local rawget = assert(rawget)
|
||||
local setmetatable = assert(setmetatable)
|
||||
|
||||
--- FiveM globals
|
||||
local GET_GAME_TIMER = assert(GetGameTimer)
|
||||
local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
|
||||
|
||||
--- Utilities for MenuV
|
||||
---@class Utilities
|
||||
local Utilities = setmetatable({ __class = 'Utilities' }, {})
|
||||
|
||||
--- Returns `true` if `input` starts with `start`, otherwise `false`
|
||||
---@param input string Checks if this string starts with `start`
|
||||
---@param start string Checks if `input` starts with this
|
||||
---@return boolean `true` if `input` starts with `start`, otherwise `false`
|
||||
function Utilities:StartsWith(input, start)
|
||||
if (self:Typeof(input) ~= 'string') then return false end
|
||||
if (self:Typeof(start) == 'number') then start = tostring(start) end
|
||||
if (self:Typeof(start) ~= 'string') then return false end
|
||||
|
||||
return sub(input, 1, #start) == start
|
||||
end
|
||||
|
||||
--- Returns `true` if `input` ends with `ends`, otherwise `false`
|
||||
---@param input string Checks if this string ends with `ends`
|
||||
---@param ends string Checks if `input` ends with this
|
||||
---@return boolean `true` if `input` ends with `ends`, otherwise `false`
|
||||
function Utilities:EndsWith(input, ends)
|
||||
if (self:Typeof(input) ~= 'string') then return false end
|
||||
if (self:Typeof(ends) == 'number') then ends = tostring(ends) end
|
||||
if (self:Typeof(ends) ~= 'string') then return false end
|
||||
|
||||
return sub(input, -#ends) == ends
|
||||
end
|
||||
|
||||
--- Returns the type of given `input`
|
||||
---@param input any Any input
|
||||
---@return string Type of given input
|
||||
function Utilities:Typeof(input)
|
||||
if (input == nil) then return 'nil' end
|
||||
|
||||
local rawType = type(input) or 'nil'
|
||||
|
||||
if (rawType ~= 'table') then return rawType end
|
||||
|
||||
local isFXFunction = rawget(input, '__cfx_functionReference') ~= nil or
|
||||
rawget(input, '__cfx_async_retval') ~= nil
|
||||
|
||||
if (isFXFunction) then return 'function' end
|
||||
if (rawget(input, '__cfx_functionSource') ~= nil) then return 'number' end
|
||||
|
||||
local rawClass = rawget(input, '__class')
|
||||
|
||||
if (rawClass ~= nil) then return type(rawClass) == 'string' and rawClass or 'class' end
|
||||
|
||||
local rawTableType = rawget(input, '__type')
|
||||
|
||||
if (rawTableType ~= nil) then return type(rawTableType) == 'string' and rawTableType or 'table' end
|
||||
|
||||
return rawType
|
||||
end
|
||||
|
||||
local INPUT_GROUPS = {
|
||||
[0] = "KEYBOARD",
|
||||
[2] = "CONTROLLER"
|
||||
}
|
||||
|
||||
local INPUT_TYPE_GROUPS = {
|
||||
["KEYBOARD"] = 0,
|
||||
["MOUSE_ABSOLUTEAXIS"] = 0,
|
||||
["MOUSE_CENTEREDAXIS"] = 0,
|
||||
["MOUSE_RELATIVEAXIS"] = 0,
|
||||
["MOUSE_SCALEDAXIS"] = 0,
|
||||
["MOUSE_NORMALIZED"] = 0,
|
||||
["MOUSE_WHEEL"] = 0,
|
||||
["MOUSE_BUTTON"] = 0,
|
||||
["MOUSE_BUTTONANY"] = 0,
|
||||
["MKB_AXIS"] = 0,
|
||||
["PAD_AXIS"] = 2,
|
||||
["PAD_DIGITALBUTTON"] = 2,
|
||||
["PAD_DIGITALBUTTONANY"] = 2,
|
||||
["PAD_ANALOGBUTTON"] = 2,
|
||||
["JOYSTICK_POV"] = 2,
|
||||
["JOYSTICK_POV_AXIS"] = 2,
|
||||
["JOYSTICK_BUTTON"] = 2,
|
||||
["JOYSTICK_AXIS"] = 2,
|
||||
["JOYSTICK_IAXIS"] = 2,
|
||||
["JOYSTICK_AXIS_NEGATIVE"] = 2,
|
||||
["JOYSTICK_AXIS_POSITIVE"] = 2,
|
||||
["PAD_DEBUGBUTTON"] = 2,
|
||||
["GAME_CONTROLLED"] = 2,
|
||||
["DIGITALBUTTON_AXIS"] = 2,
|
||||
}
|
||||
|
||||
function Utilities:GetInputTypeGroup(inputType)
|
||||
return INPUT_TYPE_GROUPS[inputType] or 0
|
||||
end
|
||||
|
||||
function Utilities:GetInputGroupName(inputTypeGroup)
|
||||
return INPUT_GROUPS[inputTypeGroup] or "KEYBOARD"
|
||||
end
|
||||
|
||||
--- Transform any `input` to the same type as `defaultValue`
|
||||
---@type function
|
||||
---@param input any Transform this `input` to `defaultValue`'s type
|
||||
---@param defaultValue any Returns this if `input` can't transformed to this type
|
||||
---@param ignoreDefault boolean Don't return default value if this is true
|
||||
---@return any Returns `input` matches the `defaultValue` type or `defaultValue`
|
||||
function Utilities:Ensure(input, defaultValue, ignoreDefault)
|
||||
ignoreDefault = type(ignoreDefault) == 'boolean' and ignoreDefault or false
|
||||
|
||||
if (defaultValue == nil) then return nil end
|
||||
|
||||
local requiredType = self:Typeof(defaultValue)
|
||||
|
||||
if (requiredType == 'nil') then return nil end
|
||||
|
||||
local inputType = self:Typeof(input)
|
||||
|
||||
if (inputType == requiredType) then return input end
|
||||
if (inputType == 'nil') then return defaultValue end
|
||||
|
||||
if (requiredType == 'number') then
|
||||
if (inputType == 'boolean') then return input and 1 or 0 end
|
||||
|
||||
return tonumber(input) or (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
if (requiredType == 'string') then
|
||||
if (inputType == 'boolean') then return input and 'yes' or 'no' end
|
||||
if (inputType == 'vector3') then return encode({ x = input.x, y = input.y, z = input.z }) or (not ignoreDefault and defaultValue or nil) end
|
||||
if (inputType == 'vector2') then return encode({ x = input.x, y = input.y }) or (not ignoreDefault and defaultValue or nil) end
|
||||
if (inputType == 'table') then return encode(input) or (not ignoreDefault and defaultValue or nil) end
|
||||
|
||||
local result = tostring(input)
|
||||
|
||||
if (result == 'nil') then
|
||||
return not ignoreDefault and defaultValue or 'nil'
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
if (requiredType == 'boolean') then
|
||||
if (inputType == 'string') then
|
||||
input = lower(input)
|
||||
|
||||
if (input == 'true' or input == '1' or input == 'yes' or input == 'y') then return true end
|
||||
if (input == 'false' or input == '0' or input == 'no' or input == 'n') then return false end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
if (inputType == 'number') then
|
||||
if (input == 1) then return true end
|
||||
if (input == 0) then return false end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
if (requiredType == 'table') then
|
||||
if (inputType == 'string') then
|
||||
if (self:StartsWith(input, '{') and self:EndsWith(input, '}')) then
|
||||
return decode(input) or (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
if (self:StartsWith(input, '[') and self:EndsWith(input, ']')) then
|
||||
return decode(input) or (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
if (inputType == 'vector3') then return { x = input.x or 0, y = input.y or 0, z = input.z or 0 } end
|
||||
if (inputType == 'vector2') then return { x = input.x or 0, y = input.y or 0 } end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
if (requiredType == 'vector3') then
|
||||
if (inputType == 'table') then
|
||||
local _x = self:Ensure(input.x, defaultValue.x)
|
||||
local _y = self:Ensure(input.y, defaultValue.y)
|
||||
local _z = self:Ensure(input.z, defaultValue.z)
|
||||
|
||||
return vector3(_x, _y, _z)
|
||||
end
|
||||
|
||||
if (inputType == 'vector2') then
|
||||
local _x = self:Ensure(input.x, defaultValue.x)
|
||||
local _y = self:Ensure(input.y, defaultValue.y)
|
||||
|
||||
return vector3(_x, _y, 0)
|
||||
end
|
||||
|
||||
if (inputType == 'number') then
|
||||
return vector3(input, input, input)
|
||||
end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
if (requiredType == 'vector2') then
|
||||
if (inputType == 'table') then
|
||||
local _x = self:Ensure(input.x, defaultValue.x)
|
||||
local _y = self:Ensure(input.y, defaultValue.y)
|
||||
|
||||
return vector2(_x, _y)
|
||||
end
|
||||
|
||||
if (inputType == 'vector3') then
|
||||
local _x = self:Ensure(input.x, defaultValue.x)
|
||||
local _y = self:Ensure(input.y, defaultValue.y)
|
||||
|
||||
return vector2(_x, _y)
|
||||
end
|
||||
|
||||
if (inputType == 'number') then
|
||||
return vector2(input, input)
|
||||
end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
return (not ignoreDefault and defaultValue or nil)
|
||||
end
|
||||
|
||||
--- Checks if input exists in inputs
|
||||
--- '0' and 0 are both the same '0' == 0 equals `true`
|
||||
--- 'yes' and true are both the same 'yes' == true equals `true`
|
||||
---@param input any Any input
|
||||
---@param inputs any[] Any table
|
||||
---@param checkType string | "'value'" | "'key'" | "'both'"
|
||||
---@return boolean Returns `true` if input has been found as `key` and/or `value`
|
||||
function Utilities:Any(input, inputs, checkType)
|
||||
if (input == nil) then return false end
|
||||
if (inputs == nil) then return false end
|
||||
|
||||
inputs = self:Ensure(inputs, {})
|
||||
checkType = lower(self:Ensure(checkType, 'value'))
|
||||
|
||||
local checkMethod = 1
|
||||
|
||||
if (checkType == 'value' or checkType == 'v') then
|
||||
checkMethod = 1
|
||||
elseif (checkType == 'key' or checkType == 'k') then
|
||||
checkMethod = -1
|
||||
elseif (checkType == 'both' or checkType == 'b') then
|
||||
checkMethod = 0
|
||||
end
|
||||
|
||||
for k, v in pairs(inputs) do
|
||||
if (checkMethod == 0 or checkMethod == -1) then
|
||||
local checkK = self:Ensure(input, k, true)
|
||||
|
||||
if (checkK ~= nil and checkK == k) then return true end
|
||||
end
|
||||
|
||||
if (checkMethod == 0 or checkMethod == 1) then
|
||||
local checkV = self:Ensure(input, v, true)
|
||||
|
||||
if (checkV ~= nil and checkV == v) then return true end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Round any `value`
|
||||
---@param value number Round this value
|
||||
---@param decimal number Number of decimals
|
||||
---@return number Rounded number
|
||||
function Utilities:Round(value, decimal)
|
||||
value = self:Ensure(value, 0)
|
||||
decimal = self:Ensure(decimal, 0)
|
||||
|
||||
if (decimal > 0) then
|
||||
return floor((value * 10 ^ decimal) + 0.5) / (10 ^ decimal)
|
||||
end
|
||||
|
||||
return floor(value + 0.5)
|
||||
end
|
||||
|
||||
--- Checks if `item1` equals `item2`
|
||||
---@param item1 any Item1
|
||||
---@param item2 any Item2
|
||||
---@return boolean `true` if both are equal, otherwise `false`
|
||||
function Utilities:Equal(item1, item2)
|
||||
if (item1 == nil and item2 == nil) then return true end
|
||||
if (item1 == nil or item2 == nil) then return false end
|
||||
|
||||
if (type(item1) == 'table') then
|
||||
local item1EQ = rawget(item1, '__eq')
|
||||
|
||||
if (item1EQ ~= nil and self:Typeof(item1EQ) == 'function') then
|
||||
return item1EQ(item1, item2)
|
||||
end
|
||||
|
||||
return item1 == item2
|
||||
end
|
||||
|
||||
if (type(item2) == 'table') then
|
||||
local item2EQ = rawget(item2, '__eq')
|
||||
|
||||
if (item2EQ ~= nil and self:Typeof(item2EQ) == 'function') then
|
||||
return item2EQ(item2, item1)
|
||||
end
|
||||
|
||||
return item2 == item1
|
||||
end
|
||||
|
||||
return item1 == item2
|
||||
end
|
||||
|
||||
local function tohex(x)
|
||||
x = Utilities:Ensure(x, 32)
|
||||
|
||||
local s, base, d = '', 16
|
||||
|
||||
while x > 0 do
|
||||
d = x % base + 1
|
||||
x = floor(x / base)
|
||||
s = sub('0123456789abcdef', d, d) .. s
|
||||
end
|
||||
|
||||
while #s < 2 do s = ('0%s'):format(s) end
|
||||
|
||||
return s
|
||||
end
|
||||
|
||||
local function bitwise(x, y, matrix)
|
||||
x = Utilities:Ensure(x, 32)
|
||||
y = Utilities:Ensure(y, 16)
|
||||
matrix = Utilities:Ensure(matrix, {{0,0}, {0, 1}})
|
||||
|
||||
local z, pow = 0, 1
|
||||
|
||||
while x > 0 or y > 0 do
|
||||
z = z + (matrix[x %2 + 1][y %2 + 1] * pow)
|
||||
pow = pow * 2
|
||||
x = floor(x / 2)
|
||||
y = floor(y / 2)
|
||||
end
|
||||
|
||||
return z
|
||||
end
|
||||
|
||||
--- Generates a random UUID like: 00000000-0000-0000-0000-000000000000
|
||||
---@return string Random generated UUID
|
||||
function Utilities:UUID()
|
||||
randomseed(GET_GAME_TIMER() + random(30720, 92160))
|
||||
|
||||
---@type number[]
|
||||
local bytes = {
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255),
|
||||
random(0, 255)
|
||||
}
|
||||
|
||||
bytes[7] = bitwise(bytes[7], 0x0f, {{0,0},{0,1}})
|
||||
bytes[7] = bitwise(bytes[7], 0x40, {{0,1},{1,1}})
|
||||
bytes[9] = bitwise(bytes[7], 0x3f, {{0,0},{0,1}})
|
||||
bytes[9] = bitwise(bytes[7], 0x80, {{0,1},{1,1}})
|
||||
|
||||
return upper(('%s%s%s%s-%s%s-%s%s-%s%s-%s%s%s%s%s%s'):format(
|
||||
tohex(bytes[1]), tohex(bytes[2]), tohex(bytes[3]), tohex(bytes[4]),
|
||||
tohex(bytes[5]), tohex(bytes[6]),
|
||||
tohex(bytes[7]), tohex(bytes[8]),
|
||||
tohex(bytes[9]), tohex(bytes[10]),
|
||||
tohex(bytes[11]), tohex(bytes[12]), tohex(bytes[13]), tohex(bytes[14]), tohex(bytes[15]), tohex(bytes[16])
|
||||
))
|
||||
end
|
||||
|
||||
--- Replace a string that contains `this` to `that`
|
||||
---@param str string String where to replace in
|
||||
---@param this string Word that's need to be replaced
|
||||
---@param that string Replace `this` whit given string
|
||||
---@return string String where `this` has been replaced with `that`
|
||||
function Utilities:Replace(str, this, that)
|
||||
local b, e = str:find(this, 1, true)
|
||||
|
||||
if b == nil then
|
||||
return str
|
||||
else
|
||||
return str:sub(1, b - 1) .. that .. self:Replace(str:sub(e + 1), this, that)
|
||||
end
|
||||
end
|
||||
|
||||
_G.Utilities = Utilities
|
||||
_ENV.Utilities = Utilities
|
||||
@ -1,293 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
local assert = assert
|
||||
local load = assert(load)
|
||||
local xpcall = assert(xpcall)
|
||||
local lower = assert(string.lower)
|
||||
local upper = assert(string.upper)
|
||||
local rawget = assert(rawget)
|
||||
local rawset = assert(rawset)
|
||||
local traceback = assert(debug.traceback)
|
||||
local setmetatable = assert(setmetatable)
|
||||
|
||||
--- FiveM globals
|
||||
local GetInvokingResource = assert(GetInvokingResource)
|
||||
local LoadResourceFile = assert(LoadResourceFile)
|
||||
local RegisterKeyMapping = assert(RegisterKeyMapping)
|
||||
local RegisterCommand = assert(RegisterCommand)
|
||||
local SendNUIMessage = assert(SendNUIMessage)
|
||||
local RegisterNUICallback = assert(RegisterNUICallback)
|
||||
local IsScreenFadedOut = assert(IsScreenFadedOut)
|
||||
local IsPauseMenuActive = assert(IsPauseMenuActive)
|
||||
local PlaySoundFrontend = assert(PlaySoundFrontend)
|
||||
local CreateThread = assert(Citizen.CreateThread)
|
||||
local Wait = assert(Citizen.Wait)
|
||||
local exports = assert(exports)
|
||||
|
||||
--- MenuV globals
|
||||
---@type Utilities
|
||||
local Utilities = assert(Utilities)
|
||||
|
||||
--- Load a file from `menuv`
|
||||
---@param path string Path in `menuv`
|
||||
---@return any|nil Results of nil
|
||||
local function load_file(path)
|
||||
if (path == nil or type(path) ~= 'string') then return nil end
|
||||
|
||||
local raw_file = LoadResourceFile('menuv', path)
|
||||
|
||||
if (raw_file) then
|
||||
local raw_func, _ = load(raw_file, ('menuv/%s'):format(path), 't', _ENV)
|
||||
|
||||
if (raw_func) then
|
||||
local ok, result = xpcall(raw_func, traceback)
|
||||
|
||||
if (ok) then
|
||||
return result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
load_file('menuv/components/translations.lua')
|
||||
|
||||
local MenuV = setmetatable({
|
||||
---@type string
|
||||
__class = 'MenuV',
|
||||
---@type string
|
||||
__type = 'MenuV',
|
||||
---@type boolean
|
||||
Loaded = false,
|
||||
---@type number
|
||||
ThreadWait = Utilities:Ensure((Config or {}).HideInterval, 250),
|
||||
---@type table<string, string>
|
||||
Translations = translations or {},
|
||||
---@type table<string, table>
|
||||
Sounds = Utilities:Ensure((Config or {}).Sounds, {}),
|
||||
---@type boolean
|
||||
Hidden = false
|
||||
}, {})
|
||||
|
||||
MenuV.Keys = setmetatable({ data = {}, __class = 'MenuVKeys', __type = 'keys' }, {
|
||||
__index = function(t, k)
|
||||
return rawget(t.data, k)
|
||||
end,
|
||||
__newindex = function(t, k, v)
|
||||
k = Utilities:Ensure(k, 'unknown')
|
||||
|
||||
if (k == 'unknown') then return end
|
||||
|
||||
local rawKey = rawget(t.data, k)
|
||||
local keyExists = rawKey ~= nil
|
||||
local prevState = Utilities:Ensure((rawKey or {}).status, false)
|
||||
local newState = Utilities:Ensure(v, false)
|
||||
|
||||
if (keyExists and not MenuV.Hidden) then
|
||||
rawset(t.data[k], 'status', newState)
|
||||
|
||||
if (prevState ~= newState) then
|
||||
local action = newState and not prevState and 'KEY_PRESSED' or 'KEY_RELEASED'
|
||||
local key = Utilities:Ensure(rawKey.action, 'UNKNOWN')
|
||||
|
||||
SendNUIMessage({ action = action, key = key })
|
||||
end
|
||||
end
|
||||
end,
|
||||
__call = function(t, k, a, inputType)
|
||||
k = Utilities:Ensure(k, 'unknown')
|
||||
a = Utilities:Ensure(a, 'UNKNOWN')
|
||||
inputType = Utilities:Ensure(inputType, 0)
|
||||
|
||||
if (k == 'unknown') then return end
|
||||
|
||||
local rawKey = rawget(t.data, k)
|
||||
local keyExists = rawKey ~= nil
|
||||
|
||||
if (keyExists) then
|
||||
if not rawKey.inputTypes[inputType] then
|
||||
rawKey.inputTypes[inputType] = true
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
rawset(t.data, k, { status = false, action = a, inputTypes = { [inputType] = true } })
|
||||
end
|
||||
})
|
||||
|
||||
--- Register a `action` with custom keybind
|
||||
---@param action string Action like: UP, DOWN, LEFT...
|
||||
---@param description string Description of keybind
|
||||
---@param defaultType string Type like: keyboard, mouse etc.
|
||||
---@param defaultKey string Default key for this keybind
|
||||
function MenuV:RegisterKey(action, description, defaultType, defaultKey)
|
||||
action = Utilities:Ensure(action, 'UNKNOWN')
|
||||
description = Utilities:Ensure(description, 'unknown')
|
||||
defaultType = Utilities:Ensure(defaultType, 'KEYBOARD')
|
||||
defaultType = upper(defaultType)
|
||||
defaultKey = Utilities:Ensure(defaultKey, 'F12')
|
||||
|
||||
action = Utilities:Replace(action, ' ', '_')
|
||||
action = upper(action)
|
||||
|
||||
local typeGroup = Utilities:GetInputTypeGroup(defaultType)
|
||||
|
||||
if (self.Keys[action] and self.Keys[action].inputTypes[typeGroup]) then return end
|
||||
|
||||
self.Keys(action, action, typeGroup)
|
||||
|
||||
local k = lower(action)
|
||||
|
||||
if typeGroup > 0 then
|
||||
local inputGroupName = Utilities:GetInputGroupName(typeGroup)
|
||||
k = ('%s_%s'):format(lower(inputGroupName), k)
|
||||
end
|
||||
|
||||
k = ('menuv_%s'):format(k)
|
||||
|
||||
RegisterKeyMapping(('+%s'):format(k), description, defaultType, defaultKey)
|
||||
RegisterCommand(('+%s'):format(k), function() MenuV.Keys[action] = true end)
|
||||
RegisterCommand(('-%s'):format(k), function() MenuV.Keys[action] = false end)
|
||||
end
|
||||
|
||||
--- Load translation
|
||||
---@param k string Translation key
|
||||
---@return string Translation or 'MISSING TRANSLATION'
|
||||
local function T(k)
|
||||
k = Utilities:Ensure(k, 'unknown')
|
||||
|
||||
return Utilities:Ensure(MenuV.Translations[k], 'MISSING TRANSLATION')
|
||||
end
|
||||
|
||||
RegisterNUICallback('loaded', function(_, cb)
|
||||
MenuV.Loaded = true
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
RegisterNUICallback('sound', function(info, cb)
|
||||
local key = upper(Utilities:Ensure(info.key, 'UNKNOWN'))
|
||||
|
||||
if (MenuV.Sounds == nil and MenuV.Sounds[key] == nil) then cb('ok') return end
|
||||
|
||||
local sound = Utilities:Ensure(MenuV.Sounds[key], {})
|
||||
local soundType = lower(Utilities:Ensure(sound.type, 'unknown'))
|
||||
|
||||
if (soundType == 'native') then
|
||||
local name = Utilities:Ensure(sound.name, 'UNKNOWN')
|
||||
local library = Utilities:Ensure(sound.library, 'UNKNOWN')
|
||||
|
||||
PlaySoundFrontend(-1, name, library, true)
|
||||
end
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
--- Trigger the NUICallback for the right resource
|
||||
---@param name string Name of callback
|
||||
---@param info table Info returns from callback
|
||||
---@param cb function Trigger this when callback is done
|
||||
local function TriggerResourceCallback(name, info, cb)
|
||||
local r = Utilities:Ensure(info.r, 'menuv')
|
||||
|
||||
if (r == 'menuv') then cb('ok') return end
|
||||
|
||||
local resource = exports[r] or nil
|
||||
|
||||
if (resource == nil) then cb('ok') return end
|
||||
|
||||
local nuiCallback = resource['NUICallback'] or nil
|
||||
|
||||
if (nuiCallback == nil) then cb('ok') return end
|
||||
|
||||
exports[r]:NUICallback(name, info, cb)
|
||||
end
|
||||
|
||||
RegisterNUICallback('submit', function(info, cb) TriggerResourceCallback('submit', info, cb) end)
|
||||
RegisterNUICallback('close', function(info, cb) TriggerResourceCallback('close', info, cb) end)
|
||||
RegisterNUICallback('switch', function(info, cb) TriggerResourceCallback('switch', info, cb) end)
|
||||
RegisterNUICallback('update', function(info, cb) TriggerResourceCallback('update', info, cb) end)
|
||||
RegisterNUICallback('open', function(info, cb) TriggerResourceCallback('open', info, cb) end)
|
||||
RegisterNUICallback('opened', function(info, cb) TriggerResourceCallback('opened', info, cb) end)
|
||||
RegisterNUICallback('close_all', function(info, cb) TriggerResourceCallback('close_all', info, cb) end)
|
||||
|
||||
--- MenuV exports
|
||||
exports('IsLoaded', function(cb)
|
||||
cb = Utilities:Ensure(cb, function() end)
|
||||
|
||||
if (MenuV.Loaded) then
|
||||
cb()
|
||||
return
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
local callback = cb
|
||||
|
||||
repeat Wait(0) until MenuV.Loaded
|
||||
|
||||
callback()
|
||||
end)
|
||||
end)
|
||||
|
||||
exports('SendNUIMessage', function(input)
|
||||
local r = Utilities:Ensure(GetInvokingResource(), 'menuv')
|
||||
|
||||
if (Utilities:Typeof(input) == 'table') then
|
||||
if (input.menu) then
|
||||
rawset(input.menu, 'resource', r)
|
||||
rawset(input.menu, 'defaultSounds', MenuV.Sounds)
|
||||
rawset(input.menu, 'hidden', MenuV.Hidden)
|
||||
end
|
||||
|
||||
SendNUIMessage(input)
|
||||
end
|
||||
end)
|
||||
|
||||
--- Register `MenuV` keybinds
|
||||
MenuV:RegisterKey('UP', T('keybind_key_up'), 'KEYBOARD', 'UP')
|
||||
MenuV:RegisterKey('DOWN', T('keybind_key_down'), 'KEYBOARD', 'DOWN')
|
||||
MenuV:RegisterKey('LEFT', T('keybind_key_left'), 'KEYBOARD', 'LEFT')
|
||||
MenuV:RegisterKey('RIGHT', T('keybind_key_right'), 'KEYBOARD', 'RIGHT')
|
||||
MenuV:RegisterKey('ENTER', T('keybind_key_enter'), 'KEYBOARD', 'RETURN')
|
||||
MenuV:RegisterKey('CLOSE', T('keybind_key_close'), 'KEYBOARD', 'BACK')
|
||||
MenuV:RegisterKey('CLOSE_ALL', T('keybind_key_close_all'), 'KEYBOARD', 'PLUS')
|
||||
|
||||
MenuV:RegisterKey('UP', ('%s - %s'):format(T('controller'), T('keybind_key_up')), 'PAD_ANALOGBUTTON', 'LUP_INDEX')
|
||||
MenuV:RegisterKey('DOWN', ('%s - %s'):format(T('controller'), T('keybind_key_down')), 'PAD_ANALOGBUTTON', 'LDOWN_INDEX')
|
||||
MenuV:RegisterKey('LEFT', ('%s - %s'):format(T('controller'), T('keybind_key_left')), 'PAD_ANALOGBUTTON', 'LLEFT_INDEX')
|
||||
MenuV:RegisterKey('RIGHT', ('%s - %s'):format(T('controller'), T('keybind_key_right')), 'PAD_ANALOGBUTTON', 'LRIGHT_INDEX')
|
||||
MenuV:RegisterKey('ENTER', ('%s - %s'):format(T('controller'), T('keybind_key_enter')), 'PAD_ANALOGBUTTON', 'RDOWN_INDEX')
|
||||
MenuV:RegisterKey('CLOSE', ('%s - %s'):format(T('controller'), T('keybind_key_close')), 'PAD_ANALOGBUTTON', 'RRIGHT_INDEX')
|
||||
MenuV:RegisterKey('CLOSE_ALL', ('%s - %s'):format(T('controller'), T('keybind_key_close_all')), 'PAD_ANALOGBUTTON', 'R3_INDEX')
|
||||
|
||||
--- Hide menu when screen is faded out or pause menu ia active
|
||||
CreateThread(function()
|
||||
MenuV.Hidden = false
|
||||
|
||||
while true do
|
||||
repeat Wait(0) until MenuV.Loaded
|
||||
|
||||
local new_state = IsScreenFadedOut() or IsPauseMenuActive()
|
||||
|
||||
if (MenuV.Hidden ~= new_state) then
|
||||
SendNUIMessage({ action = 'UPDATE_STATUS', status = not new_state })
|
||||
end
|
||||
|
||||
MenuV.Hidden = new_state
|
||||
|
||||
Wait(MenuV.ThreadWait)
|
||||
end
|
||||
end)
|
||||
|
||||
--- When resource is stopped
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
SendNUIMessage({ action = 'RESOURCE_STOPPED', resource = resourceName })
|
||||
end)
|
||||
@ -1,826 +0,0 @@
|
||||
/**
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
import VUE from 'vue';
|
||||
import STYLE from './vue_components/style';
|
||||
import * as VueScrollTo from 'vue-scrollto';
|
||||
|
||||
VUE.use(VueScrollTo.default, {
|
||||
container: 'ul.menuv-items',
|
||||
duration: 500,
|
||||
easing: 'ease-in',
|
||||
offset: -25,
|
||||
force: true,
|
||||
cancelable: false,
|
||||
onStart: false,
|
||||
onDone: false,
|
||||
onCancel: false,
|
||||
x: false,
|
||||
y: true
|
||||
});
|
||||
|
||||
export interface Sounds {
|
||||
type: 'native' | 'custom';
|
||||
name: string;
|
||||
library: string;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
label: string;
|
||||
description: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
index: number;
|
||||
type: 'button' | 'menu' | 'checkbox' | 'confirm' | 'range' | 'slider' | 'label' | 'unknown';
|
||||
uuid: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
description: string;
|
||||
value: any;
|
||||
prev_value: any;
|
||||
values: Option[];
|
||||
min: number;
|
||||
max: number;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface Menu {
|
||||
hidden: boolean;
|
||||
theme: 'default' | 'native';
|
||||
resource: string;
|
||||
uuid: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
position: 'topleft' | 'topcenter' | 'topright' | 'centerleft' | 'center' | 'centerright' | 'bottomleft' | 'bottomcenter' | 'bottomright';
|
||||
size: 'size-100' | 'size-110' | 'size-125' | 'size-150' | 'size-175' | 'size-200';
|
||||
color: {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number
|
||||
};
|
||||
items: Item[];
|
||||
texture: string;
|
||||
dictionary: string;
|
||||
defaultSounds: Record<'UP' | 'DOWN' | 'LEFT' | 'RIGHT' | 'ENTER' | 'CLOSE', Sounds>;
|
||||
}
|
||||
|
||||
export default VUE.extend({
|
||||
template: '#menuv_template',
|
||||
name: 'menuv',
|
||||
components: {
|
||||
STYLE
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
theme: 'default',
|
||||
resource: 'menuv',
|
||||
uuid: '',
|
||||
menu: false,
|
||||
show: false,
|
||||
title: 'MenuV',
|
||||
subtitle: '',
|
||||
position: 'topleft',
|
||||
size: 'size-110',
|
||||
texture: 'none',
|
||||
dictionary: 'none',
|
||||
color: {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 255
|
||||
},
|
||||
items: [] as Item[],
|
||||
listener: (event: MessageEvent) => {},
|
||||
index: 0,
|
||||
sounds: {} as Record<'UP' | 'DOWN' | 'LEFT' | 'RIGHT' | 'ENTER' | 'CLOSE', Sounds>,
|
||||
cached_indexes: {} as Record<string, number>
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('message', this.listener)
|
||||
},
|
||||
mounted() {
|
||||
this.listener = (event: MessageEvent) => {
|
||||
const data: any = event.data ||(<any>event).detail;
|
||||
|
||||
if (!data || !data.action) { return; }
|
||||
|
||||
const typeRef = data.action as 'UPDATE_STATUS' | 'OPEN_MENU' | 'CLOSE_MENU' | 'UPDATE_TITLE' | 'UPDATE_SUBTITLE' | 'KEY_PRESSED' | 'RESOURCE_STOPPED' | 'UPDATE_ITEMS' | 'UPDATE_ITEM' | 'REFRESH_MENU'
|
||||
|
||||
if (this[typeRef]) {
|
||||
this[typeRef](data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', this.listener);
|
||||
|
||||
this.POST('https://menuv/loaded', {});
|
||||
},
|
||||
watch: {
|
||||
theme() {},
|
||||
title() {},
|
||||
subtitle() {},
|
||||
position() {},
|
||||
color() {},
|
||||
options() {},
|
||||
menu() {},
|
||||
show() {},
|
||||
size() {},
|
||||
index(newValue, oldValue) {
|
||||
let prev_uuid = null;
|
||||
let next_uuid = null;
|
||||
|
||||
if (oldValue >= 0 && oldValue < this.items.length) {
|
||||
prev_uuid = this.items[oldValue].uuid;
|
||||
}
|
||||
|
||||
if (newValue >= 0 && newValue < this.items.length) {
|
||||
next_uuid = this.items[newValue].uuid;
|
||||
}
|
||||
|
||||
this.cached_indexes[this.uuid] = newValue;
|
||||
this.POST(`https://menuv/switch`, { prev: prev_uuid, next: next_uuid, r: this.resource });
|
||||
},
|
||||
items: {
|
||||
deep: true,
|
||||
handler(newValue: Item[], oldValue: Item[]) {
|
||||
if (this.index >= newValue.length || this.index < 0) { return; }
|
||||
|
||||
let sameItem = null;
|
||||
const currentItem = newValue[this.index];
|
||||
|
||||
if (currentItem == null) { return; }
|
||||
|
||||
for (var i = 0; i < oldValue.length; i++) {
|
||||
if (currentItem.uuid == oldValue[i].uuid) {
|
||||
sameItem = oldValue[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (sameItem == null || currentItem.value == currentItem.prev_value) { return; }
|
||||
|
||||
currentItem.prev_value = currentItem.value;
|
||||
|
||||
this.POST(`https://menuv/update`, { uuid: currentItem.uuid, prev: sameItem.value, now: currentItem.value, r: this.resource });
|
||||
}
|
||||
}
|
||||
},
|
||||
updated: function() {
|
||||
if (this.index < 0) { return; }
|
||||
|
||||
const el = document.getElementsByTagName('li');
|
||||
|
||||
for (var i = 0; i < el.length; i++) {
|
||||
const index = el[i].getAttribute('index')
|
||||
|
||||
if (index === null) { continue; }
|
||||
|
||||
const idx = parseInt(index);
|
||||
|
||||
if (idx == this.index) {
|
||||
this.$scrollTo(`li[index="${this.index}"]`, 0, {});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
UPDATE_STATUS({ status }: { status: boolean }) {
|
||||
if (this.menu) { this.show = status; }
|
||||
},
|
||||
OPEN_MENU({ menu, reopen }: { menu: Menu, reopen: boolean }) {
|
||||
this.POST(`https://menuv/open`, { uuid: this.uuid, new_uuid: menu.uuid, r: this.resource });
|
||||
this.RESET_MENU();
|
||||
|
||||
this.theme = this.ENSURE(menu.theme, 'default');
|
||||
this.resource = this.ENSURE(menu.resource, 'menuv');
|
||||
this.uuid = this.ENSURE(menu.uuid, '00000000-0000-0000-0000-000000000000');
|
||||
this.title = this.ENSURE(menu.title, this.title);
|
||||
this.subtitle = this.ENSURE(menu.subtitle, this.subtitle);
|
||||
this.position = this.ENSURE(menu.position, 'topleft');
|
||||
this.size = this.ENSURE(menu.size, 'size-110');
|
||||
this.texture = this.ENSURE(menu.texture, 'none');
|
||||
this.dictionary = this.ENSURE(menu.dictionary, 'none');
|
||||
this.color = menu.color || this.color;
|
||||
this.sounds = menu.defaultSounds || this.sounds;
|
||||
this.show = !(menu.hidden || false);
|
||||
this.menu = true;
|
||||
|
||||
const _items = this.items = menu.items || [];
|
||||
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
_items[i].prev_value = _items[i].value;
|
||||
}
|
||||
|
||||
this.items = _items.sort((item1, item2) => {
|
||||
if (item1.index > item2.index) { return 1; }
|
||||
if (item1.index < item2.index) { return -1; }
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const index = (reopen || false) ? (this.cached_indexes[this.uuid] || 0) : 0;
|
||||
const nextIndex = this.NEXT_INDEX(index);
|
||||
const prevIndex = this.PREV_INDEX(nextIndex);
|
||||
|
||||
this.index = prevIndex;
|
||||
this.cached_indexes[this.uuid] = prevIndex;
|
||||
this.POST(`https://menuv/opened`, { uuid: this.uuid, r: this.resource });
|
||||
},
|
||||
REFRESH_MENU({ menu }: { menu: Menu }) {
|
||||
const current_index = this.index + 0;
|
||||
|
||||
this.RESET_MENU();
|
||||
|
||||
this.theme = this.ENSURE(menu.theme, 'default');
|
||||
this.resource = this.ENSURE(menu.resource, 'menuv');
|
||||
this.uuid = this.ENSURE(menu.uuid, '00000000-0000-0000-0000-000000000000');
|
||||
this.title = this.ENSURE(menu.title, this.title);
|
||||
this.subtitle = this.ENSURE(menu.subtitle, this.subtitle);
|
||||
this.position = this.ENSURE(menu.position, 'topleft');
|
||||
this.size = this.ENSURE(menu.size, 'size-110');
|
||||
this.texture = this.ENSURE(menu.texture, 'none');
|
||||
this.dictionary = this.ENSURE(menu.dictionary, 'none');
|
||||
this.color = menu.color || this.color;
|
||||
this.sounds = menu.defaultSounds || this.sounds;
|
||||
this.show = !(menu.hidden || false);
|
||||
this.menu = true;
|
||||
|
||||
const _items = this.items = menu.items || [];
|
||||
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
_items[i].prev_value = _items[i].value;
|
||||
}
|
||||
|
||||
this.items = this.items = _items.sort((item1, item2) => {
|
||||
if (item1.index > item2.index) { return 1; }
|
||||
if (item1.index < item2.index) { return -1; }
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const nextIndex = this.NEXT_INDEX(current_index);
|
||||
const prevIndex = this.PREV_INDEX(nextIndex);
|
||||
|
||||
this.index = prevIndex;
|
||||
},
|
||||
CLOSE_MENU({ uuid }: { uuid: string }) {
|
||||
if (this.uuid == uuid) {
|
||||
this.RESET_MENU();
|
||||
}
|
||||
},
|
||||
UPDATE_TITLE({ title, __uuid }: { title: string, __uuid: string }) {
|
||||
if (__uuid != this.uuid) { return; }
|
||||
|
||||
this.title = title || this.title;
|
||||
},
|
||||
UPDATE_SUBTITLE({ subtitle, __uuid }: { subtitle: string, __uuid: string }) {
|
||||
if (__uuid != this.uuid) { return; }
|
||||
|
||||
this.subtitle = subtitle || this.subtitle;
|
||||
},
|
||||
UPDATE_ITEMS({ items, __uuid }: { items: Item[], __uuid: string }) {
|
||||
if (__uuid != this.uuid) { return; }
|
||||
|
||||
const _items = items || this.items;
|
||||
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
_items[i].prev_value = _items[i].value;
|
||||
}
|
||||
|
||||
this.items = _items;
|
||||
|
||||
const nextIndex = this.NEXT_INDEX(this.index);
|
||||
const prevIndex = this.PREV_INDEX(nextIndex);
|
||||
|
||||
this.index = prevIndex;
|
||||
},
|
||||
UPDATE_ITEM({ item, __uuid }: { item: Item, __uuid: string }) {
|
||||
if (__uuid != this.uuid || item == null || typeof item == "undefined") { return; }
|
||||
|
||||
for (var i = 0; i < this.items.length; i++) {
|
||||
if (this.items[i].uuid == item.uuid) {
|
||||
this.items[i].icon = item.icon || this.items[i].icon;
|
||||
this.items[i].label = item.label || this.items[i].label;
|
||||
this.items[i].description = item.description || this.items[i].description;
|
||||
this.items[i].value = item.value || this.items[i].value;
|
||||
this.items[i].values = item.values || this.items[i].values;
|
||||
this.items[i].min = item.min || this.items[i].min;
|
||||
this.items[i].max = item.max || this.items[i].max;
|
||||
this.items[i].disabled = item.disabled || this.items[i].disabled;
|
||||
|
||||
if ((this.index == i && this.items[i].disabled) || (this.index < 0 && !this.items[i].disabled)) {
|
||||
this.index = this.NEXT_INDEX(this.index);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
ADD_ITEM({ item, index, __uuid }: { item: Item, index?: number, __uuid: string }) {
|
||||
if (__uuid != this.uuid) { return; }
|
||||
|
||||
item.prev_value = item.value;
|
||||
|
||||
for (var i = 0; i < this.items.length; i++) {
|
||||
if (this.items[i].uuid == item.uuid) {
|
||||
this.UPDATE_ITEM({ item: item, __uuid: __uuid });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const _items = this.items;
|
||||
|
||||
if (typeof index == 'undefined' || index == null || index < 0 || index >= _items.length) {
|
||||
_items.push(item);
|
||||
} else {
|
||||
_items.splice(index, 0, item);
|
||||
}
|
||||
|
||||
this.items = _items.sort((item1, item2) => {
|
||||
if (item1.index > item2.index) { return 1; }
|
||||
if (item1.index < item2.index) { return -1; }
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (this.index < 0 && !item.disabled) { this.index = this.NEXT_INDEX(this.index); }
|
||||
},
|
||||
REMOVE_ITEM({ uuid, __uuid }: { uuid: string, __uuid: string }) {
|
||||
if (__uuid != this.uuid || typeof uuid != 'string' || uuid == '') { return }
|
||||
|
||||
const _items = this.items;
|
||||
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
if (_items[i].uuid == uuid) {
|
||||
_items.splice(i, 1);
|
||||
}
|
||||
|
||||
if (i == this.index) {
|
||||
this.index = this.PREV_INDEX(this.index);
|
||||
}
|
||||
}
|
||||
|
||||
this.items = _items.sort((item1, item2) => {
|
||||
if (item1.index > item2.index) { return 1; }
|
||||
if (item1.index < item2.index) { return -1; }
|
||||
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
RESET_MENU() {
|
||||
this.theme = 'default'
|
||||
this.resource = 'menuv';
|
||||
this.menu = false;
|
||||
this.show = false;
|
||||
this.uuid = '00000000-0000-0000-0000-000000000000';
|
||||
this.title = 'MenuV';
|
||||
this.subtitle = '';
|
||||
this.position = 'topleft';
|
||||
this.size = 'size-110';
|
||||
this.texture = 'none';
|
||||
this.dictionary = 'none';
|
||||
this.color.r = 0;
|
||||
this.color.g = 0;
|
||||
this.color.b = 255;
|
||||
this.items = [];
|
||||
this.sounds['UP'] = { type: 'custom', name: 'unknown', library: 'unknown' } as Sounds;
|
||||
this.sounds['DOWN'] = { type: 'custom', name: 'unknown', library: 'unknown' } as Sounds;
|
||||
this.sounds['LEFT'] = { type: 'custom', name: 'unknown', library: 'unknown' } as Sounds;
|
||||
this.sounds['RIGHT'] = { type: 'custom', name: 'unknown', library: 'unknown' } as Sounds;
|
||||
this.sounds['ENTER'] = { type: 'custom', name: 'unknown', library: 'unknown' } as Sounds;
|
||||
this.sounds['CLOSE'] = { type: 'custom', name: 'unknown', library: 'unknown' } as Sounds;
|
||||
},
|
||||
GET_SLIDER_LABEL({ uuid }: { uuid: string }) {
|
||||
for (var i = 0; i < this.items.length; i++) {
|
||||
if (this.items[i].uuid == uuid && this.items[i].type == 'slider') {
|
||||
const currentValue = this.items[i].value as number;
|
||||
const values = this.items[i].values;
|
||||
|
||||
if (values.length == 0) { return ''; }
|
||||
|
||||
if (currentValue < 0 || currentValue >= values.length) {
|
||||
return this.FORMAT_TEXT(values[0].label || 'Unknown');
|
||||
}
|
||||
|
||||
return this.FORMAT_TEXT(values[currentValue].label || 'Unknown');
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
GET_CURRENT_DESCRIPTION() {
|
||||
const index = this.index || 0;
|
||||
|
||||
if (index >= 0 && index < this.items.length) {
|
||||
return this.FORMAT_TEXT(this.NL2BR(this.ENSURE(this.items[index].description, ''), true, false));
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
ENSURE: function<T>(input: any, output: T): T {
|
||||
const inputType = typeof input;
|
||||
const outputType = typeof output;
|
||||
|
||||
if (inputType == 'undefined') { return output as T; }
|
||||
|
||||
if (outputType == 'string') {
|
||||
if (inputType == 'string') {
|
||||
const isEmpty = input == null || (input as string) == 'nil' || (input as string) == '';
|
||||
|
||||
if (isEmpty) { return output as T; }
|
||||
|
||||
return input as T;
|
||||
}
|
||||
|
||||
if (inputType == 'number') { return (input as number).toString() as unknown as T || output as T; }
|
||||
|
||||
return output as T;
|
||||
}
|
||||
|
||||
if (outputType == 'number') {
|
||||
if (inputType == 'string') {
|
||||
const isEmpty = input == null || (input as string) == 'nil' || (input as string) == '';
|
||||
|
||||
if (isEmpty) { return output as T; }
|
||||
|
||||
return Number(input as string) as unknown as T || output as T;
|
||||
}
|
||||
|
||||
if (inputType == 'number') { return input as T; }
|
||||
|
||||
return output as T;
|
||||
}
|
||||
|
||||
return output as T;
|
||||
},
|
||||
TEXT_COLOR: function(r: number, g: number, b: number, o: number): string {
|
||||
o = o || 1.0
|
||||
|
||||
if (o > 1.0) { o = 1.0; }
|
||||
if (o < 0.0) { o = 0.0; }
|
||||
|
||||
const luminance = ( 0.299 * r + 0.587 * g + 0.114 * b)/255;
|
||||
|
||||
if (luminance > 0.5) {
|
||||
return `rgba(0, 0, 0, ${o})`;
|
||||
}
|
||||
|
||||
return `rgba(255, 255, 255, ${o})`;
|
||||
},
|
||||
IS_DEFAULT: function(input: any): boolean {
|
||||
if (typeof input == 'string') {
|
||||
return input == null || (input as string) == 'nil' || (input as string) == '';
|
||||
}
|
||||
|
||||
if (typeof input == 'number') {
|
||||
return (input as number) == 0
|
||||
}
|
||||
|
||||
if (typeof input == 'boolean') {
|
||||
return (input as boolean) == false
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
KEY_PRESSED({ key }: { key: string }) {
|
||||
if (!this.menu || !this.show) { return; }
|
||||
|
||||
const k = key as 'UP' | 'DOWN' | 'LEFT' | 'RIGHT' | 'ENTER' | 'CLOSE'
|
||||
|
||||
if (typeof k == 'undefined' || k == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const keyRef = `KEY_${k}` as 'KEY_UP' | 'KEY_DOWN' | 'KEY_LEFT' | 'KEY_RIGHT' | 'KEY_ENTER' | 'KEY_CLOSE' | 'KEY_CLOSE_ALL';
|
||||
|
||||
if (this[keyRef]) {
|
||||
this[keyRef]();
|
||||
}
|
||||
},
|
||||
RESOURCE_STOPPED({ resource }: { resource: string }) {
|
||||
if (!this.menu) { return; }
|
||||
|
||||
if (this.resource == resource) {
|
||||
this.RESET_MENU();
|
||||
}
|
||||
},
|
||||
KEY_UP: function() {
|
||||
const newIndex = this.PREV_INDEX(this.index);
|
||||
|
||||
if (this.index != newIndex) {
|
||||
this.index = newIndex;
|
||||
|
||||
if (this.sounds['UP'] && this.sounds['UP'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'UP' });
|
||||
}
|
||||
}
|
||||
},
|
||||
KEY_DOWN: function() {
|
||||
const newIndex = this.NEXT_INDEX(this.index);
|
||||
|
||||
if (this.index != newIndex) {
|
||||
this.index = newIndex;
|
||||
|
||||
if (this.sounds['DOWN'] && this.sounds['DOWN'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'DOWN' });
|
||||
}
|
||||
}
|
||||
},
|
||||
KEY_LEFT: function() {
|
||||
if (this.index < 0 || this.items.length <= this.index || this.items[this.index].disabled) { return; }
|
||||
|
||||
const item = this.items[this.index];
|
||||
|
||||
if (item.type == 'button' || item.type == 'menu' || item.type == 'label' || item.type == 'unknown') { return; }
|
||||
|
||||
switch(item.type) {
|
||||
case 'confirm':
|
||||
case 'checkbox':
|
||||
const boolean_value = item.value as boolean;
|
||||
|
||||
this.items[this.index].value = !boolean_value;
|
||||
|
||||
if (this.sounds['LEFT'] && this.sounds['LEFT'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'LEFT' });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'range':
|
||||
let new_range_index = null;
|
||||
let range_value = item.value as number;
|
||||
|
||||
if ((range_value - 1) <= item.min) { new_range_index = item.min; }
|
||||
else if ((range_value - 1) >= item.max) { new_range_index = item.max; }
|
||||
else { new_range_index = (this.items[this.index].value - 1); }
|
||||
|
||||
if (new_range_index != this.items[this.index].value) {
|
||||
this.items[this.index].value = new_range_index;
|
||||
|
||||
if (this.sounds['LEFT'] && this.sounds['LEFT'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'LEFT' });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'slider':
|
||||
let new_slider_index = null;
|
||||
const slider_value = item.value as number;
|
||||
const slider_values = item.values || [];
|
||||
|
||||
if (slider_values.length <= 0) { return; }
|
||||
if ((slider_value - 1) < 0 || (slider_value - 1) >= slider_values.length) { new_slider_index = (slider_values.length - 1); }
|
||||
else { new_slider_index = (this.items[this.index].value - 1); }
|
||||
|
||||
if (new_slider_index != this.items[this.index].value) {
|
||||
this.items[this.index].value = new_slider_index;
|
||||
|
||||
if (this.sounds['LEFT'] && this.sounds['LEFT'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'LEFT' });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
KEY_RIGHT: function() {
|
||||
if (this.index < 0 || this.items.length <= this.index || this.items[this.index].disabled) { return; }
|
||||
|
||||
const item = this.items[this.index];
|
||||
|
||||
if (item.type == 'button' || item.type == 'menu' || item.type == 'label' || item.type == 'unknown') { return; }
|
||||
|
||||
switch(item.type) {
|
||||
case 'confirm':
|
||||
case 'checkbox':
|
||||
const boolean_value = item.value as boolean;
|
||||
|
||||
this.items[this.index].value = !boolean_value;
|
||||
|
||||
if (this.sounds['RIGHT'] && this.sounds['RIGHT'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'RIGHT' });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'range':
|
||||
let new_range_index = null;
|
||||
let range_value = item.value as number;
|
||||
|
||||
if ((range_value + 1) <= item.min) { new_range_index = item.min; }
|
||||
else if ((range_value + 1) >= item.max) { new_range_index = item.max; }
|
||||
else { new_range_index = (this.items[this.index].value + 1); }
|
||||
|
||||
if (new_range_index != this.items[this.index].value) {
|
||||
this.items[this.index].value = new_range_index;
|
||||
|
||||
if (this.sounds['RIGHT'] && this.sounds['RIGHT'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'RIGHT' });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'slider':
|
||||
let new_slider_index = null;
|
||||
const slider_value = item.value as number;
|
||||
const slider_values = item.values || [];
|
||||
|
||||
if (slider_values.length <= 0) { return; }
|
||||
if ((slider_value + 1) < 0 || (slider_value + 1) >= slider_values.length) { new_slider_index = 0; }
|
||||
else { new_slider_index = (this.items[this.index].value + 1); }
|
||||
|
||||
if (new_slider_index != this.items[this.index].value) {
|
||||
this.items[this.index].value = new_slider_index;
|
||||
|
||||
if (this.sounds['RIGHT'] && this.sounds['RIGHT'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'RIGHT' });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
KEY_ENTER: function() {
|
||||
if (this.index < 0 || this.items.length <= this.index || this.items[this.index].disabled) { return; }
|
||||
|
||||
if (this.sounds['ENTER'] && this.sounds['ENTER'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'ENTER' });
|
||||
}
|
||||
|
||||
const item = this.items[this.index];
|
||||
|
||||
switch(item.type) {
|
||||
case 'button':
|
||||
case 'menu':
|
||||
this.POST(`https://menuv/submit`, { uuid: item.uuid, value: null, r: this.resource });
|
||||
break;
|
||||
case 'confirm':
|
||||
this.POST(`https://menuv/submit`, { uuid: item.uuid, value: item.value as boolean, r: this.resource });
|
||||
break;
|
||||
case 'range':
|
||||
let range_value = item.value as number;
|
||||
|
||||
if (range_value <= item.min) { range_value = item.min; }
|
||||
else if (range_value >= item.max) { range_value = item.max; }
|
||||
|
||||
this.POST(`https://menuv/submit`, { uuid: item.uuid, value: range_value, r: this.resource });
|
||||
break;
|
||||
case 'checkbox':
|
||||
const boolean_value = item.value as boolean;
|
||||
|
||||
this.items[this.index].value = !boolean_value;
|
||||
|
||||
this.POST(`https://menuv/submit`, { uuid: item.uuid, value: this.items[this.index].value, r: this.resource });
|
||||
break;
|
||||
case 'slider':
|
||||
let slider_value = item.value as number;
|
||||
const slider_values = item.values || [];
|
||||
|
||||
if (slider_values.length <= 0 || slider_value < 0 || slider_value >= slider_values.length) { return; }
|
||||
|
||||
this.POST(`https://menuv/submit`, { uuid: item.uuid, value: slider_value, r: this.resource });
|
||||
break;
|
||||
}
|
||||
},
|
||||
KEY_CLOSE: function() {
|
||||
if (this.sounds['CLOSE'] && this.sounds['CLOSE'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'CLOSE' });
|
||||
}
|
||||
|
||||
this.POST(`https://menuv/close`, { uuid: this.uuid, r: this.resource });
|
||||
this.CLOSE_MENU({ uuid: this.uuid });
|
||||
},
|
||||
KEY_CLOSE_ALL: function() {
|
||||
if (this.sounds['CLOSE'] && this.sounds['CLOSE'].type == 'native') {
|
||||
this.POST(`https://menuv/sound`, { key: 'CLOSE' });
|
||||
}
|
||||
|
||||
this.POST(`https://menuv/close_all`, { r: this.resource });
|
||||
this.RESET_MENU();
|
||||
},
|
||||
POST: function(url: string, data: object|[]) {
|
||||
var request = new XMLHttpRequest();
|
||||
|
||||
request.open('POST', url, true);
|
||||
request.open('POST', url, true);
|
||||
request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
request.send(JSON.stringify(data));
|
||||
},
|
||||
NEXT_INDEX: function(idx: number) {
|
||||
if (idx == null || typeof idx == "undefined") { idx = this.index; }
|
||||
|
||||
let index = 0;
|
||||
let newIndex = -2;
|
||||
|
||||
if (this.items.length <= 0) { return -1; }
|
||||
|
||||
while (newIndex < -1) {
|
||||
if ((idx + 1 + index) < this.items.length) {
|
||||
if (!this.items[(idx + 1 + index)].disabled) {
|
||||
newIndex = (idx + 1 + index);
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
} else if (index >= this.items.length) {
|
||||
return -1;
|
||||
} else {
|
||||
const addIndex = (idx + 1 + index) - this.items.length;
|
||||
|
||||
if (addIndex < this.items.length) {
|
||||
if (!this.items[addIndex].disabled) {
|
||||
newIndex = addIndex;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newIndex < 0) { return -1; }
|
||||
|
||||
return newIndex;
|
||||
},
|
||||
PREV_INDEX: function(idx: number) {
|
||||
if (idx == null || typeof idx == "undefined") { idx = this.index; }
|
||||
|
||||
let index = 0;
|
||||
let newIndex = -2;
|
||||
|
||||
if (this.items.length <= 0) { return -1; }
|
||||
|
||||
while (newIndex < -1) {
|
||||
if ((idx - 1 - index) >= 0) {
|
||||
if (!this.items[(idx - 1 - index)].disabled) {
|
||||
newIndex = (idx - 1 - index);
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
} else if (index >= this.items.length) {
|
||||
return -1;
|
||||
} else {
|
||||
const addIndex = (idx - 1 - index) + this.items.length;
|
||||
|
||||
if (addIndex < this.items.length && addIndex >= 0) {
|
||||
if (!this.items[addIndex].disabled) {
|
||||
newIndex = addIndex;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newIndex < 0) { return -1; }
|
||||
|
||||
return newIndex;
|
||||
},
|
||||
NL2BR: function(text: string, replaceMode: boolean, isXhtml: boolean) {
|
||||
var breakTag = (isXhtml) ? '<br />' : '<br>';
|
||||
var replaceStr = (replaceMode) ? '$1'+ breakTag : '$1'+ breakTag +'$2';
|
||||
|
||||
return (text + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, replaceStr);
|
||||
},
|
||||
FORMAT_TEXT: function(text: string) {
|
||||
text = this.ENSURE(text, '');
|
||||
|
||||
text = text.replace(/\^0/g, '<span style="color: black !important;">');
|
||||
text = text.replace(/\^1/g, '<span style="color: red !important;">');
|
||||
text = text.replace(/\^2/g, '<span style="color: green !important;">');
|
||||
text = text.replace(/\^3/g, '<span style="color: yellow !important;">');
|
||||
text = text.replace(/\^4/g, '<span style="color: blue !important;">');
|
||||
text = text.replace(/\^5/g, '<span style="color: cyan !important;">');
|
||||
text = text.replace(/\^6/g, '<span style="color: purple !important;">');
|
||||
text = text.replace(/\^7/g, '<span style="color: white !important;">');
|
||||
text = text.replace(/\^8/g, '<span style="color: darkred !important;">');
|
||||
text = text.replace(/\^9/g, '<span style="color: gray !important;">');
|
||||
text = text.replace(/~r~/g, '<span style="color: red !important;">');
|
||||
text = text.replace(/~g~/g, '<span style="color: green !important;">');
|
||||
text = text.replace(/~b~/g, '<span style="color: blue !important;">');
|
||||
text = text.replace(/~y~/g, '<span style="color: yellow !important;">');
|
||||
text = text.replace(/~p~/g, '<span style="color: purple !important;">');
|
||||
text = text.replace(/~c~/g, '<span style="color: gray !important;">');
|
||||
text = text.replace(/~m~/g, '<span style="color: darkgray !important;">');
|
||||
text = text.replace(/~u~/g, '<span style="color: black !important;">');
|
||||
text = text.replace(/~o~/g, '<span style="color: orange !important;">');
|
||||
text = text.replace(/~n~/g, '<br />');
|
||||
text = text.replace(/~s~/g, '<span style="color: white !important;">');
|
||||
text = text.replace(/~h~/g, '<strong>');
|
||||
|
||||
const d = new DOMParser();
|
||||
const domObj = d.parseFromString(text || "", "text/html");
|
||||
|
||||
return domObj.body.innerHTML;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./",
|
||||
"module": "es6",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"allowJs": true,
|
||||
"lib": [
|
||||
"es2017",
|
||||
"DOM"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
/**
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
import VUE from 'vue';
|
||||
import { CreateElement } from 'vue/types/umd';
|
||||
|
||||
export default VUE.component('v-style', {
|
||||
render: function(createElement: CreateElement) {
|
||||
return createElement('style', this.$slots.default);
|
||||
}
|
||||
});
|
||||
@ -1,132 +0,0 @@
|
||||
<!--
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-->
|
||||
<template>
|
||||
<div id="menuv" class="menuv" :class="[{'hide': !show || !menu}, position, size, theme]" :data-uuid="uuid">
|
||||
<v-style>
|
||||
html,
|
||||
body {
|
||||
color: {{TEXT_COLOR(color.r, color.g, color.b)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-header {
|
||||
background: url("https://nui-img/{{dictionary}}/{{texture}}") no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-header .menuv-bg-icon i,
|
||||
.menuv.{{theme}} .menuv-header .menuv-bg-icon svg {
|
||||
color: rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-subheader {
|
||||
background-color: rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active {
|
||||
border-left: 0.5em solid rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
border-right: 0.5em solid rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
background-color: rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
color: {{TEXT_COLOR(color.r, color.g, color.b)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active i,
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active svg {
|
||||
color: {{TEXT_COLOR(color.r, color.g, color.b)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active span.menuv-icon {
|
||||
border-right: 1px solid {{TEXT_COLOR(color.r, color.g, color.b)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items span.menuv-options span.menuv-btn {
|
||||
color: {{TEXT_COLOR(color.r, color.g, color.b)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items span.menuv-options span.menuv-btn.active {
|
||||
background-color: rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
color: {{TEXT_COLOR(color.r, color.g, color.b)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active span.menuv-options span.menuv-btn {
|
||||
background-color: rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
color: {{TEXT_COLOR(color.r, color.g, color.b)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active span.menuv-options span.menuv-btn.active {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: rgba({{color.r}},{{color.g}},{{color.b}}, 0.50);
|
||||
box-shadow: 0px 0px 0px {{TEXT_COLOR(color.r, color.g, color.b, 0.50)}};
|
||||
border: 0px solid {{TEXT_COLOR(color.r, color.g, color.b, 0.50)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items input[type="range"]::-webkit-slider-thumb {
|
||||
border: 1px solid rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
background: rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
box-shadow: 0px 0px 0px {{TEXT_COLOR(color.r, color.g, color.b, 0.50)}};
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-thumb {
|
||||
background: {{TEXT_COLOR(color.r, color.g, color.b)}} !important;
|
||||
border: 1px solid {{TEXT_COLOR(color.r, color.g, color.b, 0.50)}} !important;
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-runnable-track,
|
||||
.menuv.{{theme}} .menuv-items .menuv-item.active input[type="range"]:focus::-webkit-slider-runnable-track {
|
||||
background: {{TEXT_COLOR(color.r, color.g, color.b, 0.50)}} !important;
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items input[type="range"]:focus::-webkit-slider-runnable-track {
|
||||
background: rgba({{color.r}},{{color.g}},{{color.b}}, 0.50);
|
||||
}
|
||||
|
||||
.menuv.{{theme}} .menuv-items .menuv-desc {
|
||||
border-left: 0.375em solid rgb({{color.r}},{{color.g}},{{color.b}});
|
||||
}
|
||||
</v-style>
|
||||
<header class="menuv-header">
|
||||
<strong v-html="FORMAT_TEXT(title)"></strong>
|
||||
</header>
|
||||
<nav class="menuv-subheader" v-html="FORMAT_TEXT(subtitle)"></nav>
|
||||
<ul class="menuv-items" ref="items">
|
||||
<li class="menuv-item media" v-for="item in items" :key="item.uuid" :class="[{'active': (index + 1) == item.index, 'hasIcon': ENSURE(item.icon, 'none') != 'none', 'disabled': item.disabled }, (`menuv-${item.type}`)]" :index="(item.index - 1)">
|
||||
<div class="media-left item-icon" v-if="ENSURE(item.icon, 'none') != 'none'">
|
||||
<span class="menuv-icon">{{ENSURE(item.icon, 'none')}}</span>
|
||||
</div>
|
||||
<div class="media-content flex-left item-title" v-html="FORMAT_TEXT(item.label)"></div>
|
||||
<div class="media-right">
|
||||
<i class="fas fa-arrow-right" v-if="item.type == 'menu'"></i>
|
||||
<i v-if="item.type == 'checkbox'" :class="{'fas fa-check': item.value, 'far fa-square': !item.value}"></i>
|
||||
<input type="range" :min="item.min" :max="item.max" :value="(item.value)" v-if="item.type == 'range'">
|
||||
<span class="menuv-options" v-if="item.type == 'confirm'">
|
||||
<span class="menuv-btn" :class="{'active': item.value}">YES</span>
|
||||
<span class="menuv-btn" :class="{'active': !item.value}">NO</span>
|
||||
</span>
|
||||
<span class="menuv-label" v-if="item.type == 'label'" v-html="FORMAT_TEXT(item.value)"></span>
|
||||
<span class="menuv-options" v-if="item.type == 'slider'">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
<span v-html="GET_SLIDER_LABEL({ uuid: item.uuid })"></span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<footer class="menuv-description" :class="{'hide': IS_DEFAULT(GET_CURRENT_DESCRIPTION())}">
|
||||
<strong v-html="GET_CURRENT_DESCRIPTION()"></strong>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./../menuv.ts"></script>
|
||||
@ -1,48 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
Config = {
|
||||
Language = 'en',
|
||||
HideInterval = 250,
|
||||
Sounds = {
|
||||
UP = {
|
||||
type = 'native',
|
||||
name = 'NAV_UP_DOWN',
|
||||
library = 'HUD_FREEMODE_SOUNDSET'
|
||||
},
|
||||
DOWN = {
|
||||
type = 'native',
|
||||
name = 'NAV_UP_DOWN',
|
||||
library = 'HUD_FREEMODE_SOUNDSET'
|
||||
},
|
||||
LEFT = {
|
||||
type = 'native',
|
||||
name = 'NAV_LEFT_RIGHT',
|
||||
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
|
||||
},
|
||||
RIGHT = {
|
||||
type = 'native',
|
||||
name = 'NAV_LEFT_RIGHT',
|
||||
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
|
||||
},
|
||||
ENTER = {
|
||||
type = 'native',
|
||||
name = 'SELECT',
|
||||
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
|
||||
},
|
||||
CLOSE = {
|
||||
type = 'native',
|
||||
name = 'BACK',
|
||||
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_G.Config = Config
|
||||
_ENV.Config = Config
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"language": "da",
|
||||
"translators": [
|
||||
{
|
||||
"name": "Nylander",
|
||||
"emails": ["hej@hjaltenylander.dk"],
|
||||
"github": "https://github.com/NylanderrDK"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"keyboard": "Tastatur",
|
||||
"controller": "Controller",
|
||||
"keybind_key_up": "Op i en menu",
|
||||
"keybind_key_down": "Ned i en menu",
|
||||
"keybind_key_left": "Venstre i en menu",
|
||||
"keybind_key_right": "Højre i en menu",
|
||||
"keybind_key_enter": "Bekræft valg i en menu",
|
||||
"keybind_key_close": "Luk en menu",
|
||||
"keybind_key_close_all": "Luk alle menuer (inklusiv forældre)",
|
||||
"open_menu": "Åbn '%s' med denne tast"
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"language": "de",
|
||||
"translators": [
|
||||
{
|
||||
"name": "NirosTen",
|
||||
"github": "https://github.com/NirosTen"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"keyboard": "Tastatur",
|
||||
"controller": "Joystick",
|
||||
"keybind_key_up": "Oben in einem Menü ",
|
||||
"keybind_key_down": "Unten in einem Menü",
|
||||
"keybind_key_left": "Links in einem Menü",
|
||||
"keybind_key_right": "Rechts in einem Menü",
|
||||
"keybind_key_enter": "Bestätigen Sie in einem Menü",
|
||||
"keybind_key_close": "Schließen Sie ein Menü",
|
||||
"keybind_key_close_all": "Schließen Sie alle Menüs",
|
||||
"open_menu": "Öffnen Sie das '%s'Menü"
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"language": "en",
|
||||
"translators": [
|
||||
{
|
||||
"name": "ThymonA",
|
||||
"emails": ["contact@thymonarens.nl"],
|
||||
"github": "https://github.com/ThymonA"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"keyboard": "Keyboard",
|
||||
"controller": "Controller",
|
||||
"keybind_key_up": "Up in a menu",
|
||||
"keybind_key_down": "Down in a menu",
|
||||
"keybind_key_left": "Left in a menu",
|
||||
"keybind_key_right": "Right in a menu",
|
||||
"keybind_key_enter": "Confirm item in menu",
|
||||
"keybind_key_close": "Closing a menu",
|
||||
"keybind_key_close_all": "Close all menus (including parents)",
|
||||
"open_menu": "Open menu '%s' with this key"
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"language":"es",
|
||||
"translators":[
|
||||
{
|
||||
"name":"Apolo",
|
||||
"github":"https://github.com/apolo-sys"
|
||||
}
|
||||
],
|
||||
"translations":{
|
||||
"keyboard":"Teclado",
|
||||
"controller":"Mando",
|
||||
"keybind_key_up":"Tecla para subir",
|
||||
"keybind_key_down":"Tecla para bajar",
|
||||
"keybind_key_left":"Tecla para mover a la izquierda",
|
||||
"keybind_key_right":"Tecla para mover a la derecha",
|
||||
"keybind_key_enter":"Tecla para confirmar (ENTER)",
|
||||
"keybind_key_close":"Cerrar menú",
|
||||
"keybind_key_close_all":"Cerrar todos los menús",
|
||||
"open_menu":"El menú '%s' se abre con esta tecla"
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"language": "fr",
|
||||
"translators": [
|
||||
{
|
||||
"name": "Korioz",
|
||||
"emails": ["pro.korioz@protonmail.com"],
|
||||
"github": "https://github.com/Korioz"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"keyboard": "Clavier",
|
||||
"controller": "Manette",
|
||||
"keybind_key_up": "Haut dans un menu",
|
||||
"keybind_key_down": "Bas dans un menu",
|
||||
"keybind_key_left": "Gauche dans un menu",
|
||||
"keybind_key_right": "Droite dans un menu",
|
||||
"keybind_key_enter": "Confirmer dans un menu",
|
||||
"keybind_key_close": "Fermer un menu",
|
||||
"keybind_key_close_all": "Fermer tout les menus",
|
||||
"open_menu": "Ouvrir menu '%s'"
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"language": "nl",
|
||||
"translators": [
|
||||
{
|
||||
"name": "ThymonA",
|
||||
"emails": ["contact@thymonarens.nl"],
|
||||
"github": "https://github.com/ThymonA"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"keyboard": "Toetsenbord",
|
||||
"controller": "Controller",
|
||||
"keybind_key_up": "Omhoog in een menu",
|
||||
"keybind_key_down": "Omlaag in een menu",
|
||||
"keybind_key_left": "Links in een menu",
|
||||
"keybind_key_right": "Rechts in een menu",
|
||||
"keybind_key_enter": "Item bevestigen in menu",
|
||||
"keybind_key_close": "Sluiten van een menu",
|
||||
"keybind_key_close_all": "Sluit alle menu's (inclusief ouders)",
|
||||
"open_menu": "Menu '%s' openen met deze key"
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"language": "pt",
|
||||
"translators": [
|
||||
{
|
||||
"name": "Murilo Balbino",
|
||||
"github": "https://github.com/l1ru"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"keyboard": "Teclado",
|
||||
"controller": "Controle",
|
||||
"keybind_key_up": "Subir no Menu",
|
||||
"keybind_key_down": "Descer no Menu",
|
||||
"keybind_key_left": "Esquerda no Menu",
|
||||
"keybind_key_right": "Direita no Menu",
|
||||
"keybind_key_enter": "Confirmar item no menu",
|
||||
"keybind_key_close": "Fechando um menu",
|
||||
"keybind_key_close_all": "Feche todos os menus",
|
||||
"open_menu": "Abrir menu com a tecla '%s'"
|
||||
}
|
||||
}
|
||||
@ -1,638 +0,0 @@
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
local assert = assert
|
||||
local pairs = assert(pairs)
|
||||
local rawget = assert(rawget)
|
||||
local rawset = assert(rawset)
|
||||
local insert = assert(table.insert)
|
||||
local remove = assert(table.remove)
|
||||
local format = assert(string.format)
|
||||
local upper = assert(string.upper)
|
||||
local lower = assert(string.lower)
|
||||
local setmetatable = assert(setmetatable)
|
||||
|
||||
--- FiveM globals
|
||||
local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
|
||||
local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded)
|
||||
local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict)
|
||||
local REGISTER_KEY_MAPPING = assert(RegisterKeyMapping)
|
||||
local REGISTER_COMMAND = assert(RegisterCommand)
|
||||
local GET_HASH_KEY = assert(GetHashKey)
|
||||
local CreateThread = assert(Citizen.CreateThread)
|
||||
local Wait = assert(Citizen.Wait)
|
||||
|
||||
---@load 'config.lua'
|
||||
---@load 'app/lua_components/utilities.lua'
|
||||
---@load 'app/lua_components/item.lua'
|
||||
---@load 'app/lua_components/menu.lua'
|
||||
---@load 'app/lua_components/translations.lua'
|
||||
|
||||
--- MenuV table
|
||||
local menuv_table = {
|
||||
---@type string
|
||||
__class = 'MenuV',
|
||||
---@type string
|
||||
__type = 'MenuV',
|
||||
---@type Menu|nil
|
||||
CurrentMenu = nil,
|
||||
---@type string|nil
|
||||
CurrentUpdateUUID = nil,
|
||||
---@type string
|
||||
CurrentResourceName = GET_CURRENT_RESOURCE_NAME(),
|
||||
---@type boolean
|
||||
Loaded = false,
|
||||
---@type Menu[]
|
||||
Menus = {},
|
||||
---@type Menu[]
|
||||
ParentMenus = {},
|
||||
---@type table<string, function>
|
||||
NUICallbacks = {},
|
||||
---@type table<string, string>
|
||||
Translations = translations,
|
||||
---@class keys
|
||||
Keys = setmetatable({ data = {}, __class = 'MenuVKeys', __type = 'keys' }, {
|
||||
__index = function(t, k)
|
||||
return rawget(t.data, k)
|
||||
end,
|
||||
__newindex = function(t, actionHax, v)
|
||||
actionHax = Utilities:Ensure(actionHax, 'unknown')
|
||||
|
||||
if (actionHax == 'unknown') then return end
|
||||
|
||||
local rawKey = rawget(t.data, actionHax)
|
||||
local keyExists = rawKey ~= nil
|
||||
local prevState = Utilities:Ensure((rawKey or {}).status, false)
|
||||
local newState = Utilities:Ensure(v, false)
|
||||
|
||||
if (keyExists) then
|
||||
rawset(t.data[actionHax], 'status', newState)
|
||||
|
||||
if (prevState ~= newState and newState) then
|
||||
rawKey.func(rawKey.menu)
|
||||
end
|
||||
end
|
||||
end,
|
||||
__call = function(t, actionHax, m, actionFunc, inputType)
|
||||
actionHax = Utilities:Ensure(actionHax, 'unknown')
|
||||
m = Utilities:Typeof(m) == 'Menu' and m or nil
|
||||
actionFunc = Utilities:Ensure(actionFunc, function() end)
|
||||
inputType = Utilities:Ensure(inputType, 'KEYBOARD')
|
||||
inputType = upper(inputType)
|
||||
|
||||
if (actionHax == 'unknown') then return end
|
||||
|
||||
local rawKey = rawget(t.data, actionHax)
|
||||
local keyExists = rawKey ~= nil
|
||||
|
||||
if (keyExists) then
|
||||
if not rawKey.inputTypes[inputType] then
|
||||
rawKey.inputTypes[inputType] = true
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
rawset(t.data, actionHax, { status = false, menu = m, func = actionFunc, inputTypes = { [inputType] = true } })
|
||||
end
|
||||
})
|
||||
}
|
||||
|
||||
---@class MenuV
|
||||
MenuV = setmetatable(menuv_table, {})
|
||||
|
||||
--- Send a NUI message to MenuV resource
|
||||
---@param input any
|
||||
local SEND_NUI_MESSAGE = function(input)
|
||||
exports['menuv']:SendNUIMessage(input)
|
||||
end
|
||||
|
||||
--- Register a NUI callback event
|
||||
---@param name string Name of callback
|
||||
---@param cb function Callback to execute
|
||||
local REGISTER_NUI_CALLBACK = function(name, cb)
|
||||
name = Utilities:Ensure(name, 'unknown')
|
||||
cb = Utilities:Ensure(cb, function(_, cb) cb('ok') end)
|
||||
|
||||
MenuV.NUICallbacks[name] = cb
|
||||
end
|
||||
|
||||
--- Load translation
|
||||
---@param k string Translation key
|
||||
---@return string Translation or 'MISSING TRANSLATION'
|
||||
function MenuV:T(k)
|
||||
k = Utilities:Ensure(k, 'unknown')
|
||||
|
||||
return Utilities:Ensure(MenuV.Translations[k], 'MISSING TRANSLATION')
|
||||
end
|
||||
|
||||
--- Create a `MenuV` menu
|
||||
---@param title string Title of Menu
|
||||
---@param subtitle string Subtitle of Menu
|
||||
---@param position string Position of Menu
|
||||
---@param r number 0-255 RED
|
||||
---@param g number 0-255 GREEN
|
||||
---@param b number 0-255 BLUE
|
||||
---@param size string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'"
|
||||
---@param texture string Name of texture example: "default"
|
||||
---@param dictionary string Name of dictionary example: "menuv"
|
||||
---@param namespace string Namespace of Menu
|
||||
---@param theme string Theme of Menu
|
||||
---@return Menu
|
||||
function MenuV:CreateMenu(title, subtitle, position, r, g, b, size, texture, dictionary, namespace, theme)
|
||||
local menu = CreateMenu({
|
||||
Theme = theme,
|
||||
Title = title,
|
||||
Subtitle = subtitle,
|
||||
Position = position,
|
||||
R = r,
|
||||
G = g,
|
||||
B = b,
|
||||
Size = size,
|
||||
Texture = texture,
|
||||
Dictionary = dictionary,
|
||||
Namespace = namespace
|
||||
})
|
||||
|
||||
local index = #(self.Menus or {}) + 1
|
||||
|
||||
insert(self.Menus, index, menu)
|
||||
|
||||
return self.Menus[index] or menu
|
||||
end
|
||||
|
||||
--- Create a menu that inherits properties from another menu
|
||||
---@param parent Menu|string Menu or UUID of menu
|
||||
---@param overrides table<string, string|number> Properties to override in menu object (ignore parent)
|
||||
---@param namespace string Namespace of menu
|
||||
function MenuV:InheritMenu(parent, overrides, namespace)
|
||||
overrides = Utilities:Ensure(overrides, {})
|
||||
|
||||
local uuid = Utilities:Typeof(parent) == 'Menu' and parent.UUID or Utilities:Typeof(parent) == 'string' and parent
|
||||
|
||||
if (uuid == nil) then return end
|
||||
|
||||
local parentMenu = self:GetMenu(uuid)
|
||||
|
||||
if (parentMenu == nil) then return end
|
||||
|
||||
local menu = CreateMenu({
|
||||
Theme = Utilities:Ensure(overrides.theme or overrides.Theme, parentMenu.Theme),
|
||||
Title = Utilities:Ensure(overrides.title or overrides.Title, parentMenu.Title),
|
||||
Subtitle = Utilities:Ensure(overrides.subtitle or overrides.Subtitle, parentMenu.Subtitle),
|
||||
Position = Utilities:Ensure(overrides.position or overrides.Position, parentMenu.Position),
|
||||
R = Utilities:Ensure(overrides.r or overrides.R, parentMenu.Color.R),
|
||||
G = Utilities:Ensure(overrides.g or overrides.G, parentMenu.Color.G),
|
||||
B = Utilities:Ensure(overrides.b or overrides.B, parentMenu.Color.B),
|
||||
Size = Utilities:Ensure(overrides.size or overrides.Size, parentMenu.Size),
|
||||
Texture = Utilities:Ensure(overrides.texture or overrides.Texture, parentMenu.Texture),
|
||||
Dictionary = Utilities:Ensure(overrides.dictionary or overrides.Dictionary, parentMenu.Dictionary),
|
||||
Namespace = Utilities:Ensure(namespace, 'unknown')
|
||||
})
|
||||
|
||||
local index = #(self.Menus or {}) + 1
|
||||
|
||||
insert(self.Menus, index, menu)
|
||||
|
||||
return self.Menus[index] or menu
|
||||
end
|
||||
|
||||
--- Load a menu based on `uuid`
|
||||
---@param uuid string UUID of menu
|
||||
---@return Menu|nil Founded menu or `nil`
|
||||
function MenuV:GetMenu(uuid)
|
||||
uuid = Utilities:Ensure(uuid, '00000000-0000-0000-0000-000000000000')
|
||||
|
||||
for _, v in pairs(self.Menus) do
|
||||
if (v.UUID == uuid) then
|
||||
return v
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Open a menu
|
||||
---@param menu Menu|string Menu or UUID of Menu
|
||||
---@param cb function Execute this callback when menu has opened
|
||||
function MenuV:OpenMenu(menu, cb, reopen)
|
||||
local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
|
||||
|
||||
if (uuid == nil) then return end
|
||||
|
||||
cb = Utilities:Ensure(cb, function() end)
|
||||
|
||||
menu = self:GetMenu(uuid)
|
||||
|
||||
if (menu == nil) then return end
|
||||
|
||||
local dictionaryLoaded = HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary)
|
||||
|
||||
if (not self.Loaded or not dictionaryLoaded) then
|
||||
if (not dictionaryLoaded) then REQUEST_STREAMED_TEXTURE_DICT(menu.Dictionary) end
|
||||
|
||||
CreateThread(function()
|
||||
repeat Wait(0) until MenuV.Loaded
|
||||
|
||||
if (not dictionaryLoaded) then
|
||||
repeat Wait(10) until HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary)
|
||||
end
|
||||
|
||||
MenuV:OpenMenu(uuid, cb)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if (self.CurrentMenu ~= nil) then
|
||||
insert(self.ParentMenus, self.CurrentMenu)
|
||||
|
||||
self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
|
||||
self.CurrentMenu:DestroyThreads()
|
||||
end
|
||||
|
||||
self.CurrentMenu = menu
|
||||
self.CurrentUpdateUUID = menu:On('update', function(m, k, v)
|
||||
k = Utilities:Ensure(k, 'unknown')
|
||||
|
||||
if (k == 'Title' or k == 'title') then
|
||||
SEND_NUI_MESSAGE({ action = 'UPDATE_TITLE', title = Utilities:Ensure(v, 'MenuV'), __uuid = m.UUID })
|
||||
elseif (k == 'Subtitle' or k == 'subtitle') then
|
||||
SEND_NUI_MESSAGE({ action = 'UPDATE_SUBTITLE', subtitle = Utilities:Ensure(v, ''), __uuid = m.UUID })
|
||||
elseif (k == 'Items' or k == 'items') then
|
||||
SEND_NUI_MESSAGE({ action = 'UPDATE_ITEMS', items = (m.Items:ToTable() or {}), __uuid = m.UUID })
|
||||
elseif (k == 'Item' or k == 'item' and Utilities:Typeof(v) == 'Item') then
|
||||
SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID })
|
||||
elseif (k == 'AddItem' or k == 'additem' and Utilities:Typeof(v) == 'Item') then
|
||||
SEND_NUI_MESSAGE({ action = 'ADD_ITEM', item = m.Items:ItemToTable(v), __uuid = m.UUID })
|
||||
elseif (k == 'RemoveItem' or k == 'removeitem' and Utilities:Typeof(v) == 'Item') then
|
||||
SEND_NUI_MESSAGE({ action = 'REMOVE_ITEM', uuid = v.UUID, __uuid = m.UUID })
|
||||
elseif (k == 'UpdateItem' or k == 'updateitem' and Utilities:Typeof(v) == 'Item') then
|
||||
SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID })
|
||||
end
|
||||
end)
|
||||
|
||||
SEND_NUI_MESSAGE({
|
||||
action = 'OPEN_MENU',
|
||||
menu = menu:ToTable(),
|
||||
reopen = Utilities:Ensure(reopen, false)
|
||||
})
|
||||
|
||||
cb()
|
||||
end
|
||||
|
||||
function MenuV:Refresh()
|
||||
if (self.CurrentMenu == nil) then
|
||||
return
|
||||
end
|
||||
|
||||
SEND_NUI_MESSAGE({
|
||||
action = 'REFRESH_MENU',
|
||||
menu = self.CurrentMenu:ToTable()
|
||||
})
|
||||
end
|
||||
|
||||
--- Close a menu
|
||||
---@param menu Menu|string Menu or UUID of Menu
|
||||
---@param cb function Execute this callback when menu has is closed or parent menu has opened
|
||||
function MenuV:CloseMenu(menu, cb)
|
||||
local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
|
||||
|
||||
if (uuid == nil) then cb() return end
|
||||
|
||||
cb = Utilities:Ensure(cb, function() end)
|
||||
menu = self:GetMenu(uuid)
|
||||
|
||||
if (menu == nil or self.CurrentMenu == nil or self.CurrentMenu.UUID ~= uuid) then cb() return end
|
||||
|
||||
self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
|
||||
self.CurrentMenu:Trigger('close')
|
||||
self.CurrentMenu:DestroyThreads()
|
||||
self.CurrentMenu = nil
|
||||
|
||||
SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid })
|
||||
|
||||
if (#self.ParentMenus <= 0) then cb() return end
|
||||
|
||||
local prev_index = #self.ParentMenus
|
||||
local prev_menu = self.ParentMenus[prev_index] or nil
|
||||
|
||||
if (prev_menu == nil) then cb() return end
|
||||
|
||||
remove(self.ParentMenus, prev_index)
|
||||
|
||||
self:OpenMenu(prev_menu, function()
|
||||
cb()
|
||||
end, true)
|
||||
end
|
||||
|
||||
--- Close all menus
|
||||
---@param cb function Execute this callback when all menus are closed
|
||||
function MenuV:CloseAll(cb)
|
||||
cb = Utilities:Ensure(cb, function() end)
|
||||
|
||||
if (not self.Loaded) then
|
||||
CreateThread(function()
|
||||
repeat Wait(0) until MenuV.Loaded
|
||||
|
||||
MenuV:CloseAll(cb)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if (self.CurrentMenu == nil) then cb() return end
|
||||
|
||||
local uuid = Utilities:Ensure(self.CurrentMenu.UUID, '00000000-0000-0000-0000-000000000000')
|
||||
|
||||
self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
|
||||
self.CurrentMenu:Trigger('close')
|
||||
self.CurrentMenu:DestroyThreads()
|
||||
|
||||
SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid })
|
||||
|
||||
self.CurrentMenu = nil
|
||||
self.ParentMenus = {}
|
||||
|
||||
cb()
|
||||
end
|
||||
|
||||
--- Register keybind for specific menu
|
||||
---@param menu Menu|string MenuV menu
|
||||
---@param action string Name of action
|
||||
---@param func function This will be executed
|
||||
---@param description string Key description
|
||||
---@param defaultType string Default key type
|
||||
---@param defaultKey string Default key
|
||||
function MenuV:AddControlKey(menu, action, func, description, defaultType, defaultKey)
|
||||
local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
|
||||
|
||||
action = Utilities:Ensure(action, 'UNKNOWN')
|
||||
func = Utilities:Ensure(func, function() end)
|
||||
description = Utilities:Ensure(description, 'unknown')
|
||||
defaultType = Utilities:Ensure(defaultType, 'KEYBOARD')
|
||||
defaultType = upper(defaultType)
|
||||
defaultKey = Utilities:Ensure(defaultKey, 'F12')
|
||||
|
||||
local m = self:GetMenu(uuid)
|
||||
|
||||
if (m == nil) then return end
|
||||
|
||||
if (Utilities:Typeof(m.Namespace) ~= 'string' or m.Namespace == 'unknown') then
|
||||
error('[MenuV] Namespace is required for assigning keys.')
|
||||
return
|
||||
end
|
||||
|
||||
action = Utilities:Replace(action, ' ', '_')
|
||||
action = upper(action)
|
||||
|
||||
local resourceName = Utilities:Ensure(self.CurrentResourceName, 'unknown')
|
||||
local namespace = Utilities:Ensure(m.Namespace, 'unknown')
|
||||
local actionHash = GET_HASH_KEY(('%s_%s_%s'):format(resourceName, namespace, action))
|
||||
local actionHax = format('%x', actionHash)
|
||||
|
||||
local typeGroup = Utilities:GetInputTypeGroup(defaultType)
|
||||
|
||||
if (self.Keys[actionHax] and self.Keys[actionHax].inputTypes[typeGroup]) then return end
|
||||
|
||||
self.Keys(actionHax, m, func, typeGroup)
|
||||
|
||||
local k = actionHax
|
||||
|
||||
if typeGroup > 0 then
|
||||
local inputGroupName = Utilities:GetInputGroupName(typeGroup)
|
||||
k = ('%s_%s'):format(lower(inputGroupName), k)
|
||||
end
|
||||
|
||||
REGISTER_KEY_MAPPING(('+%s'):format(k), description, defaultType, defaultKey)
|
||||
REGISTER_COMMAND(('+%s'):format(k), function() MenuV.Keys[actionHax] = true end)
|
||||
REGISTER_COMMAND(('-%s'):format(k), function() MenuV.Keys[actionHax] = false end)
|
||||
end
|
||||
|
||||
--- Checks if namespace is available
|
||||
---@param namespace string Namespace
|
||||
---@return boolean Returns `true` if given namespace is available
|
||||
function MenuV:IsNamespaceAvailable(namespace)
|
||||
namespace = lower(Utilities:Ensure(namespace, 'unknown'))
|
||||
|
||||
if (namespace == 'unknown') then return true end
|
||||
|
||||
---@param v Menu
|
||||
for k, v in pairs(self.Menus or {}) do
|
||||
local v_namespace = Utilities:Ensure(v.Namespace, 'unknown')
|
||||
|
||||
if (namespace == lower(v_namespace)) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
--- Mark MenuV as loaded when `main` resource is loaded
|
||||
exports['menuv']:IsLoaded(function()
|
||||
MenuV.Loaded = true
|
||||
end)
|
||||
|
||||
--- Register callback handler for MenuV
|
||||
exports('NUICallback', function(name, info, cb)
|
||||
name = Utilities:Ensure(name, 'unknown')
|
||||
|
||||
if (MenuV.NUICallbacks == nil or MenuV.NUICallbacks[name] == nil) then
|
||||
return
|
||||
end
|
||||
|
||||
MenuV.NUICallbacks[name](info, cb)
|
||||
end)
|
||||
|
||||
REGISTER_NUI_CALLBACK('open', function(info, cb)
|
||||
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
|
||||
local new_uuid = Utilities:Ensure(info.new_uuid, '00000000-0000-0000-0000-000000000000')
|
||||
|
||||
cb('ok')
|
||||
|
||||
if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID == uuid or MenuV.CurrentMenu.UUID == new_uuid) then return end
|
||||
|
||||
for _, v in pairs(MenuV.ParentMenus) do
|
||||
if (v.UUID == uuid) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
|
||||
MenuV.CurrentMenu:Trigger('close')
|
||||
MenuV.CurrentMenu:DestroyThreads()
|
||||
MenuV.CurrentMenu = nil
|
||||
MenuV.ParentMenus = {}
|
||||
end)
|
||||
|
||||
REGISTER_NUI_CALLBACK('opened', function(info, cb)
|
||||
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
|
||||
|
||||
cb('ok')
|
||||
|
||||
if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then return end
|
||||
|
||||
MenuV.CurrentMenu:Trigger('open')
|
||||
end)
|
||||
|
||||
REGISTER_NUI_CALLBACK('submit', function(info, cb)
|
||||
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
|
||||
|
||||
cb('ok')
|
||||
|
||||
if (MenuV.CurrentMenu == nil) then return end
|
||||
|
||||
for k, v in pairs(MenuV.CurrentMenu.Items) do
|
||||
if (v.UUID == uuid) then
|
||||
if (v.__type == 'confirm' or v.__type == 'checkbox') then
|
||||
v.Value = Utilities:Ensure(info.value, false)
|
||||
elseif (v.__type == 'range') then
|
||||
v.Value = Utilities:Ensure(info.value, v.Min)
|
||||
elseif (v.__type == 'slider') then
|
||||
v.Value = Utilities:Ensure(info.value, 0) + 1
|
||||
end
|
||||
|
||||
MenuV.CurrentMenu:Trigger('select', v)
|
||||
|
||||
if (v.__type == 'button' or v.__type == 'menu') then
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('select')
|
||||
elseif (v.__type == 'range') then
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value)
|
||||
elseif (v.__type == 'slider') then
|
||||
local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil
|
||||
|
||||
if (option == nil) then return end
|
||||
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
REGISTER_NUI_CALLBACK('close', function(info, cb)
|
||||
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
|
||||
|
||||
if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then cb('ok') return end
|
||||
|
||||
MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
|
||||
MenuV.CurrentMenu:Trigger('close')
|
||||
MenuV.CurrentMenu:DestroyThreads()
|
||||
MenuV.CurrentMenu = nil
|
||||
|
||||
if (#MenuV.ParentMenus <= 0) then cb('ok') return end
|
||||
|
||||
local prev_index = #MenuV.ParentMenus
|
||||
local prev_menu = MenuV.ParentMenus[prev_index] or nil
|
||||
|
||||
if (prev_menu == nil) then cb('ok') return end
|
||||
|
||||
remove(MenuV.ParentMenus, prev_index)
|
||||
|
||||
MenuV:OpenMenu(prev_menu, function()
|
||||
cb('ok')
|
||||
end, true)
|
||||
end)
|
||||
|
||||
REGISTER_NUI_CALLBACK('close_all', function(info, cb)
|
||||
if (MenuV.CurrentMenu == nil) then
|
||||
for _, parentMenu in ipairs(MenuV.ParentMenus) do
|
||||
parentMenu:Trigger('close')
|
||||
end
|
||||
MenuV.ParentMenus = {}
|
||||
|
||||
cb('ok')
|
||||
return
|
||||
end
|
||||
|
||||
MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
|
||||
MenuV.CurrentMenu:Trigger('close')
|
||||
MenuV.CurrentMenu:DestroyThreads()
|
||||
MenuV.CurrentMenu = nil
|
||||
for _, parentMenu in ipairs(MenuV.ParentMenus) do
|
||||
parentMenu:Trigger('close')
|
||||
end
|
||||
MenuV.ParentMenus = {}
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
REGISTER_NUI_CALLBACK('switch', function(info, cb)
|
||||
local prev_uuid = Utilities:Ensure(info.prev, '00000000-0000-0000-0000-000000000000')
|
||||
local next_uuid = Utilities:Ensure(info.next, '00000000-0000-0000-0000-000000000000')
|
||||
local prev_item, next_item = nil, nil
|
||||
|
||||
cb('ok')
|
||||
|
||||
if (MenuV.CurrentMenu == nil) then return end
|
||||
|
||||
for k, v in pairs(MenuV.CurrentMenu.Items) do
|
||||
if (v.UUID == prev_uuid) then
|
||||
prev_item = v
|
||||
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('leave')
|
||||
end
|
||||
|
||||
if (v.UUID == next_uuid) then
|
||||
next_item = v
|
||||
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('enter')
|
||||
end
|
||||
end
|
||||
|
||||
if (prev_item ~= nil and next_item ~= nil) then
|
||||
MenuV.CurrentMenu:Trigger('switch', next_item, prev_item)
|
||||
end
|
||||
end)
|
||||
|
||||
REGISTER_NUI_CALLBACK('update', function(info, cb)
|
||||
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
|
||||
|
||||
cb('ok')
|
||||
|
||||
if (MenuV.CurrentMenu == nil) then return end
|
||||
|
||||
for k, v in pairs(MenuV.CurrentMenu.Items) do
|
||||
if (v.UUID == uuid) then
|
||||
local newValue, oldValue = nil, nil
|
||||
|
||||
if (v.__type == 'confirm' or v.__type == 'checkbox') then
|
||||
newValue = Utilities:Ensure(info.now, false)
|
||||
oldValue = Utilities:Ensure(info.prev, false)
|
||||
elseif (v.__type == 'range') then
|
||||
newValue = Utilities:Ensure(info.now, v.Min)
|
||||
oldValue = Utilities:Ensure(info.prev, v.Min)
|
||||
elseif (v.__type == 'slider') then
|
||||
newValue = Utilities:Ensure(info.now, 0) + 1
|
||||
oldValue = Utilities:Ensure(info.prev, 0) + 1
|
||||
end
|
||||
|
||||
if (Utilities:Any(v.__type, { 'button', 'menu', 'label' }, 'value')) then return end
|
||||
|
||||
MenuV.CurrentMenu:Trigger('update', v, newValue, oldValue)
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('change', newValue, oldValue)
|
||||
|
||||
if (v.SaveOnUpdate) then
|
||||
MenuV.CurrentMenu.Items[k].Value = newValue
|
||||
|
||||
if (v.__type == 'range') then
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value)
|
||||
elseif (v.__type == 'slider') then
|
||||
local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil
|
||||
|
||||
if (option == nil) then return end
|
||||
|
||||
MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 144 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB |
Binary file not shown.
Binary file not shown.
@ -1,56 +0,0 @@
|
||||
/**
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
-- GitHub: https://github.com/ThymonA/menuv/
|
||||
-- License: GNU General Public License v3.0
|
||||
-- https://choosealicense.com/licenses/gpl-3.0/
|
||||
-- Author: Thymon Arens <contact@arens.io>
|
||||
-- Name: MenuV
|
||||
-- Version: 1.0.0
|
||||
-- Description: FiveM menu library for creating menu's
|
||||
----------------------- [ MenuV ] -----------------------
|
||||
*/
|
||||
const HTML_WEBPACK_PLUGIN = require('html-webpack-plugin');
|
||||
const VUE_LOADER_PLUGIN = require('vue-loader/lib/plugin');
|
||||
const COPY_PLUGIN = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: './source/app/load.ts',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
options: { appendTsSuffixTo: [/\.vue$/] }
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VUE_LOADER_PLUGIN(),
|
||||
new HTML_WEBPACK_PLUGIN({
|
||||
inlineSource: '.(js|css)$',
|
||||
template: './source/app/html/menuv.html',
|
||||
filename: 'menuv.html'
|
||||
}),
|
||||
new COPY_PLUGIN({
|
||||
patterns: [
|
||||
{ from: 'source/app/html/assets/css/main.css', to: 'assets/css/main.css' },
|
||||
{ from: 'source/app/html/assets/css/native_theme.css', to: 'assets/css/native_theme.css' },
|
||||
{ from: 'source/app/html/assets/fonts/SignPainterHouseScript.woff', to: 'assets/fonts/SignPainterHouseScript.woff' },
|
||||
{ from: 'source/app/html/assets/fonts/TTCommons.woff', to: 'assets/fonts/TTCommons.woff' }
|
||||
],
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: './assets/js/menuv.js',
|
||||
path: __dirname + '/dist/'
|
||||
}
|
||||
};
|
||||
@ -1,127 +0,0 @@
|
||||
# Progressbar
|
||||
|
||||
Dependency for creating progressbars in QB-Core.
|
||||
|
||||
# Usage
|
||||
|
||||
## QB-Core Functions
|
||||
|
||||
### Client
|
||||
|
||||
- QBCore.Functions.Progressbar(**name**: string, **label**: string, **duration**: number, **useWhileDead**: boolean, **canCancel**: boolean, **disableControls**: table, **animation**: table, **prop**: table, **propTwo**: table, **onFinish**: function, **onCancel**: function)
|
||||
> Create a new progressbar from the built in qb-core functions.<br>
|
||||
> **Example:**
|
||||
> ```lua
|
||||
>QBCore.Functions.Progressbar("random_task", "Doing something", 5000, false, true, {
|
||||
> disableMovement = false,
|
||||
> disableCarMovement = false,
|
||||
> disableMouse = false,
|
||||
> disableCombat = true,
|
||||
>}, {
|
||||
> animDict = "mp_suicide",
|
||||
> anim = "pill",
|
||||
> flags = 49,
|
||||
>}, {}, {}, function()
|
||||
> -- Done
|
||||
>end, function()
|
||||
> -- Cancel
|
||||
>end)
|
||||
> ```
|
||||
|
||||
## Exports
|
||||
|
||||
### Client
|
||||
|
||||
- Progress(**data**: string, **handler**: function)
|
||||
> Creates a new progress bar directly from the export, always use the built in qb-core function if possible.<br>
|
||||
> **Example:**
|
||||
> ```lua
|
||||
>exports['progressbar']:Progress({
|
||||
> name = "random_task",
|
||||
> duration = 5000,
|
||||
> label = "Doing something",
|
||||
> useWhileDead = false,
|
||||
> canCancel = true,
|
||||
> controlDisables = {
|
||||
> disableMovement = false,
|
||||
> disableCarMovement = false,
|
||||
> disableMouse = false,
|
||||
> disableCombat = true,
|
||||
> },
|
||||
> animation = {
|
||||
> animDict = "mp_suicide",
|
||||
> anim = "pill",
|
||||
> flags = 49,
|
||||
> },
|
||||
> prop = {},
|
||||
> propTwo = {}
|
||||
>}, function(cancelled)
|
||||
> if not cancelled then
|
||||
> -- finished
|
||||
> else
|
||||
> -- cancelled
|
||||
> end
|
||||
>end)
|
||||
> ```
|
||||
> **Props Example:**
|
||||
> ```lua
|
||||
>exports['progressbar']:Progress({
|
||||
> name = "random_task",
|
||||
> duration = 5000,
|
||||
> label = "Doing something",
|
||||
> useWhileDead = false,
|
||||
> canCancel = true,
|
||||
> controlDisables = {
|
||||
> disableMovement = false,
|
||||
> disableCarMovement = false,
|
||||
> disableMouse = false,
|
||||
> disableCombat = true,
|
||||
> },
|
||||
> animation = {
|
||||
> animDict = "missheistdockssetup1clipboard@base",
|
||||
> anim = "pill",
|
||||
> flags = 49,
|
||||
> },
|
||||
> prop = {
|
||||
> model = 'prop_notepad_01',
|
||||
> bone = 18905,
|
||||
> coords = vec3(0.1, 0.02, 0.05),
|
||||
> rotation = vec3(10.0, 0.0, 0.0),
|
||||
> },
|
||||
> propTwo = {
|
||||
> model = 'prop_pencil_01',
|
||||
> bone = 58866,
|
||||
> coords = vec3(0.11, -0.02, 0.001),
|
||||
> rotation = vec3(-120.0, 0.0, 0.0),
|
||||
> }
|
||||
>}, function(cancelled)
|
||||
> if not cancelled then
|
||||
> -- finished
|
||||
> else
|
||||
> -- cancelled
|
||||
> end
|
||||
>end)
|
||||
> ```
|
||||
|
||||
- isDoingSomething()
|
||||
> Returns a boolean (true/false) depending on if a progressbar is present.<br>
|
||||
> **Example:**
|
||||
> ```lua
|
||||
> local busy = exports["progressbar"]:isDoingSomething()
|
||||
> ```
|
||||
|
||||
- ProgressWithStartEvent(**data**: table, **start**: function, **finish**: function)
|
||||
> Works like a normal progressbar, the data parameter should be the same as the data passed into the `Progress` export above.<br>
|
||||
> The start function gets triggered upon the start of the progressbar.<br>
|
||||
> The finish handler is the same as the `handler` parameter in the `Progress` export above.
|
||||
|
||||
- ProgressWithTickEvent(**data**: table, **tick**: function, **finish**: function)
|
||||
> Works like a normal progressbar, the data parameter should be the same as the data passed into the `Progress` export above.<br>
|
||||
> The tick function gets triggered every frame while the progressbar is active.<br>
|
||||
> The finish handler is the same as the `handler` parameter in the `Progress` export above.
|
||||
|
||||
- ProgressWithTickEvent(**data**: table, **start**: function, **tick**: function, **finish**: function)
|
||||
> Works like a normal progressbar, the data parameter should be the same as the data passed into the `Progress` export above.<br>
|
||||
> The start function gets triggered upon the start of the progressbar.<br>
|
||||
> The tick function gets triggered every frame while the progressbar is active.<br>
|
||||
> The finish handler is the same as the `handler` parameter in the `Progress` export above.
|
||||
@ -1,252 +0,0 @@
|
||||
local Action = {
|
||||
name = '',
|
||||
duration = 0,
|
||||
label = '',
|
||||
useWhileDead = false,
|
||||
canCancel = true,
|
||||
disarm = true,
|
||||
controlDisables = {
|
||||
disableMovement = false,
|
||||
disableCarMovement = false,
|
||||
disableMouse = false,
|
||||
disableCombat = false,
|
||||
},
|
||||
animation = {
|
||||
animDict = nil,
|
||||
anim = nil,
|
||||
flags = 0,
|
||||
task = nil,
|
||||
},
|
||||
prop = {
|
||||
model = nil,
|
||||
bone = nil,
|
||||
coords = vec3(0.0, 0.0, 0.0),
|
||||
rotation = vec3(0.0, 0.0, 0.0),
|
||||
},
|
||||
propTwo = {
|
||||
model = nil,
|
||||
bone = nil,
|
||||
coords = vec3(0.0, 0.0, 0.0),
|
||||
rotation = vec3(0.0, 0.0, 0.0),
|
||||
},
|
||||
}
|
||||
|
||||
local isDoingAction = false
|
||||
local wasCancelled = false
|
||||
local prop_net = nil
|
||||
local propTwo_net = nil
|
||||
local isAnim = false
|
||||
local isProp = false
|
||||
local isPropTwo = false
|
||||
|
||||
local controls = {
|
||||
disableMouse = { 1, 2, 106 },
|
||||
disableMovement = { 30, 31, 36, 21, 75 },
|
||||
disableCarMovement = { 63, 64, 71, 72 },
|
||||
disableCombat = { 24, 25, 37, 47, 58, 140, 141, 142, 143, 263, 264, 257 }
|
||||
}
|
||||
|
||||
-- Functions
|
||||
|
||||
local function loadAnimDict(dict)
|
||||
RequestAnimDict(dict)
|
||||
while not HasAnimDictLoaded(dict) do
|
||||
Wait(5)
|
||||
end
|
||||
end
|
||||
|
||||
local function loadModel(model)
|
||||
RequestModel(model)
|
||||
while not HasModelLoaded(model) do
|
||||
Wait(5)
|
||||
end
|
||||
end
|
||||
|
||||
local function createAndAttachProp(prop, ped)
|
||||
loadModel(prop.model)
|
||||
local coords = GetOffsetFromEntityInWorldCoords(ped, 0.0, 0.0, 0.0)
|
||||
local propEntity = CreateObject(GetHashKey(prop.model), coords.x, coords.y, coords.z, true, true, true)
|
||||
local netId = ObjToNet(propEntity)
|
||||
SetNetworkIdExistsOnAllMachines(netId, true)
|
||||
NetworkUseHighPrecisionBlending(netId, true)
|
||||
SetNetworkIdCanMigrate(netId, false)
|
||||
local boneIndex = GetPedBoneIndex(ped, prop.bone or 60309)
|
||||
AttachEntityToEntity(
|
||||
propEntity, ped, boneIndex,
|
||||
prop.coords.x, prop.coords.y, prop.coords.z,
|
||||
prop.rotation.x, prop.rotation.y, prop.rotation.z,
|
||||
true, true, false, true, 0, true
|
||||
)
|
||||
return netId
|
||||
end
|
||||
|
||||
local function disableControls()
|
||||
CreateThread(function()
|
||||
while isDoingAction do
|
||||
for disableType, isEnabled in pairs(Action.controlDisables) do
|
||||
if isEnabled and controls[disableType] then
|
||||
for _, control in ipairs(controls[disableType]) do
|
||||
DisableControlAction(0, control, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
if Action.controlDisables.disableCombat then
|
||||
DisablePlayerFiring(PlayerId(), true)
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function StartActions()
|
||||
local ped = PlayerPedId()
|
||||
if isDoingAction then
|
||||
if not isAnim and Action.animation then
|
||||
if Action.animation.task then
|
||||
TaskStartScenarioInPlace(ped, Action.animation.task, 0, true)
|
||||
else
|
||||
local anim = Action.animation
|
||||
if anim.animDict and anim.anim and DoesEntityExist(ped) and not IsEntityDead(ped) then
|
||||
loadAnimDict(anim.animDict)
|
||||
TaskPlayAnim(ped, anim.animDict, anim.anim, 3.0, 3.0, -1, anim.flags or 1, 0, false, false, false)
|
||||
end
|
||||
end
|
||||
isAnim = true
|
||||
end
|
||||
if not isProp and Action.prop and Action.prop.model then
|
||||
prop_net = createAndAttachProp(Action.prop, ped)
|
||||
isProp = true
|
||||
end
|
||||
if not isPropTwo and Action.propTwo and Action.propTwo.model then
|
||||
propTwo_net = createAndAttachProp(Action.propTwo, ped)
|
||||
isPropTwo = true
|
||||
end
|
||||
disableControls()
|
||||
end
|
||||
end
|
||||
|
||||
local function StartProgress(action, onStart, onTick, onFinish)
|
||||
local playerPed = PlayerPedId()
|
||||
local isPlayerDead = IsEntityDead(playerPed)
|
||||
if (not isPlayerDead or action.useWhileDead) and not isDoingAction then
|
||||
isDoingAction = true
|
||||
LocalPlayer.state:set('inv_busy', true, true)
|
||||
Action = action
|
||||
SendNUIMessage({
|
||||
action = 'progress',
|
||||
duration = action.duration,
|
||||
label = action.label
|
||||
})
|
||||
StartActions()
|
||||
CreateThread(function()
|
||||
if onStart then onStart() end
|
||||
while isDoingAction do
|
||||
Wait(1)
|
||||
if onTick then onTick() end
|
||||
if IsControlJustPressed(0, 200) and action.canCancel then
|
||||
TriggerEvent('progressbar:client:cancel')
|
||||
wasCancelled = true
|
||||
break
|
||||
end
|
||||
if IsEntityDead(playerPed) and not action.useWhileDead then
|
||||
TriggerEvent('progressbar:client:cancel')
|
||||
wasCancelled = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if onFinish then onFinish(wasCancelled) end
|
||||
isDoingAction = false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function ActionCleanup()
|
||||
local ped = PlayerPedId()
|
||||
if Action.animation then
|
||||
if Action.animation.task or (Action.animation.animDict and Action.animation.anim) then
|
||||
StopAnimTask(ped, Action.animation.animDict, Action.animation.anim, 1.0)
|
||||
ClearPedSecondaryTask(ped)
|
||||
else
|
||||
ClearPedTasks(ped)
|
||||
end
|
||||
end
|
||||
if prop_net then
|
||||
DetachEntity(NetToObj(prop_net), true, true)
|
||||
DeleteObject(NetToObj(prop_net))
|
||||
end
|
||||
if propTwo_net then
|
||||
DetachEntity(NetToObj(propTwo_net), true, true)
|
||||
DeleteObject(NetToObj(propTwo_net))
|
||||
end
|
||||
prop_net = nil
|
||||
propTwo_net = nil
|
||||
isDoingAction = false
|
||||
wasCancelled = false
|
||||
isAnim = false
|
||||
isProp = false
|
||||
isPropTwo = false
|
||||
LocalPlayer.state:set('inv_busy', false, true)
|
||||
end
|
||||
|
||||
-- Events
|
||||
|
||||
RegisterNetEvent('progressbar:client:ToggleBusyness', function(bool)
|
||||
isDoingAction = bool
|
||||
end)
|
||||
|
||||
RegisterNetEvent('progressbar:client:progress', function(action, finish)
|
||||
StartProgress(action, nil, nil, finish)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('progressbar:client:ProgressWithStartEvent', function(action, start, finish)
|
||||
StartProgress(action, start, nil, finish)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('progressbar:client:ProgressWithTickEvent', function(action, tick, finish)
|
||||
StartProgress(action, nil, tick, finish)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('progressbar:client:ProgressWithStartAndTick', function(action, start, tick, finish)
|
||||
StartProgress(action, start, tick, finish)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('progressbar:client:cancel', function()
|
||||
ActionCleanup()
|
||||
SendNUIMessage({
|
||||
action = 'cancel'
|
||||
})
|
||||
end)
|
||||
|
||||
-- NUI Callback
|
||||
|
||||
RegisterNUICallback('FinishAction', function(data, cb)
|
||||
ActionCleanup()
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
-- Exports
|
||||
|
||||
local function Progress(action, finish)
|
||||
StartProgress(action, nil, nil, finish)
|
||||
end
|
||||
exports('Progress', Progress)
|
||||
|
||||
local function ProgressWithStartEvent(action, start, finish)
|
||||
StartProgress(action, start, nil, finish)
|
||||
end
|
||||
exports('ProgressWithStartEvent', ProgressWithStartEvent)
|
||||
|
||||
local function ProgressWithTickEvent(action, tick, finish)
|
||||
StartProgress(action, nil, tick, finish)
|
||||
end
|
||||
exports('ProgressWithTickEvent', ProgressWithTickEvent)
|
||||
|
||||
local function ProgressWithStartAndTick(action, start, tick, finish)
|
||||
StartProgress(action, start, tick, finish)
|
||||
end
|
||||
exports('ProgressWithStartAndTick', ProgressWithStartAndTick)
|
||||
|
||||
local function isDoingSomething()
|
||||
return isDoingAction
|
||||
end
|
||||
exports('isDoingSomething', isDoingSomething)
|
||||
@ -1,17 +0,0 @@
|
||||
fx_version 'cerulean'
|
||||
lua54 'yes'
|
||||
game 'gta5'
|
||||
|
||||
author 'qbcore-framework'
|
||||
description 'Dependency for creating progressbars in QB-Core.'
|
||||
version '1.0.0'
|
||||
|
||||
ui_page 'html/index.html'
|
||||
|
||||
client_script 'client.lua'
|
||||
|
||||
files {
|
||||
'html/index.html',
|
||||
'html/style.css',
|
||||
'html/script.js'
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="./style.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="progress-container">
|
||||
<div class="progress-labels">
|
||||
<div id="progress-label">Loading...</div>
|
||||
<div id="progress-percentage">0%</div>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div id="progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./script.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,93 +0,0 @@
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
var ProgressBar = {
|
||||
init: function () {
|
||||
this.progressLabel = document.getElementById("progress-label");
|
||||
this.progressPercentage = document.getElementById("progress-percentage");
|
||||
this.progressBar = document.getElementById("progress-bar");
|
||||
this.progressContainer = document.querySelector(".progress-container");
|
||||
this.animationFrameRequest = null;
|
||||
this.setupListeners();
|
||||
},
|
||||
|
||||
setupListeners: function () {
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.data.action === "progress") {
|
||||
ProgressBar.update(event.data);
|
||||
} else if (event.data.action === "cancel") {
|
||||
ProgressBar.cancel();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
update: function (data) {
|
||||
if (this.animationFrameRequest) {
|
||||
cancelAnimationFrame(this.animationFrameRequest);
|
||||
}
|
||||
clearTimeout(this.cancelledTimer);
|
||||
|
||||
this.progressLabel.textContent = data.label;
|
||||
this.progressPercentage.textContent = "0%";
|
||||
this.progressContainer.style.display = "block";
|
||||
let startTime = Date.now();
|
||||
let duration = parseInt(data.duration, 10);
|
||||
|
||||
const animateProgress = () => {
|
||||
let timeElapsed = Date.now() - startTime;
|
||||
let progress = timeElapsed / duration;
|
||||
if (progress > 1) progress = 1;
|
||||
let percentage = Math.round(progress * 100);
|
||||
this.progressBar.style.width = percentage + "%";
|
||||
this.progressPercentage.textContent = percentage + "%";
|
||||
if (progress < 1) {
|
||||
this.animationFrameRequest = requestAnimationFrame(animateProgress);
|
||||
} else {
|
||||
this.onComplete();
|
||||
}
|
||||
};
|
||||
this.animationFrameRequest = requestAnimationFrame(animateProgress);
|
||||
},
|
||||
|
||||
cancel: function () {
|
||||
if (this.animationFrameRequest) {
|
||||
cancelAnimationFrame(this.animationFrameRequest);
|
||||
this.animationFrameRequest = null;
|
||||
}
|
||||
this.progressLabel.textContent = "CANCELLED";
|
||||
this.progressPercentage.textContent = "";
|
||||
this.progressBar.style.width = "100%";
|
||||
this.cancelledTimer = setTimeout(this.onCancel.bind(this), 1000);
|
||||
},
|
||||
|
||||
onComplete: function () {
|
||||
this.progressContainer.style.display = "none";
|
||||
this.progressBar.style.width = "0";
|
||||
this.progressPercentage.textContent = "";
|
||||
this.postAction("FinishAction");
|
||||
},
|
||||
|
||||
onCancel: function () {
|
||||
this.progressContainer.style.display = "none";
|
||||
this.progressBar.style.width = "0";
|
||||
this.progressPercentage.textContent = "";
|
||||
},
|
||||
|
||||
postAction: function (action) {
|
||||
fetch(`https://progressbar/${action}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
},
|
||||
|
||||
closeUI: function () {
|
||||
let mainContainer = document.querySelector(".main-container");
|
||||
if (mainContainer) {
|
||||
mainContainer.style.display = "none";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
ProgressBar.init();
|
||||
});
|
||||
@ -1,71 +0,0 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200&display=swap");
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: transparent !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: none;
|
||||
z-index: 5;
|
||||
color: #fff;
|
||||
width: 15%;
|
||||
position: fixed;
|
||||
bottom: 15%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.progress-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#progress-label {
|
||||
font-size: 1.3vh;
|
||||
line-height: 4vh;
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#progress-percentage {
|
||||
font-size: 1.3vh;
|
||||
line-height: 4vh;
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
background: rgba(0, 0, 0, 0.246);
|
||||
height: 0.5vh;
|
||||
position: relative;
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
background-color: #dc143c;
|
||||
width: 0%;
|
||||
height: 0.5vh;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
transition-timing-function: ease-out;
|
||||
box-shadow: 0 0 10px rgba(220, 20, 60, 0.6);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
# Set to true to add reviewers to pull requests
|
||||
addReviewers: true
|
||||
|
||||
# Set to true to add assignees to pull requests
|
||||
addAssignees: author
|
||||
|
||||
# A list of reviewers to be added to pull requests (GitHub user name)
|
||||
reviewers:
|
||||
- qbcore-framework/maintenance
|
||||
|
||||
# A list of keywords to be skipped the process that add reviewers if pull requests include it
|
||||
skipKeywords:
|
||||
- wip
|
||||
|
||||
# A number of reviewers added to the pull request
|
||||
# Set 0 to add all the reviewers (default: 0)
|
||||
numberOfReviewers: 0
|
||||
@ -1,201 +0,0 @@
|
||||
# Contributing to QBCore
|
||||
|
||||
First of all, thank you for taking the time to contribute!
|
||||
|
||||
These guidelines will help you help us in the best way possible regardless of your skill level. We ask that you try to read everything related to the way you'd like to contribute and try and use your best judgement for anything not covered.
|
||||
|
||||
### Table of Contents
|
||||
|
||||
[Code of Conduct](#code-of-conduct)
|
||||
|
||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||
|
||||
[How Can I Contribute?](#how-can-i-contribute)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Suggesting Features / Enhancements](#suggesting-features--enhancements)
|
||||
* [Your First Code Contribution](#your-first-code-contribution)
|
||||
* [Pull Requests](#pull-requests)
|
||||
|
||||
[Styleguides](#styleguides)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [Lua Styleguide](#lua-styleguide)
|
||||
* [JavaScript Styleguide](#javascript-styleguide)
|
||||
|
||||
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
- Refrain from using languages other than English.
|
||||
- Refrain from discussing any politically charged or inflammatory topics.
|
||||
- Uphold mature conversations and respect each other; excessive profanity, hate speech or any kind of harassment will not be tolerated.
|
||||
- No advertising of any kind.
|
||||
- Follow these guidelines.
|
||||
- Do not mention members of github unless a question is directed at them and can't be answered by anyone else.
|
||||
- Do not mention any of the development team for any reason. We will read things as we get to them.
|
||||
|
||||
## I don't want to read this whole thing I just have a question!!!
|
||||
|
||||
> **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below.
|
||||
|
||||
* [QBCore Website](https://qbcore.org)
|
||||
* [QBCore Discord](https://discord.gg/qbcore)
|
||||
* [FiveM Discord - #qbcore channel](https://discord.gg/fivem)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
The easiest way to contribute for most people is just to report bugs you find cause if nobody reports it there's a chance we'll never know it exists and then we'll never fix it.
|
||||
|
||||
Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out the bug-report template with the information it asks for helps us resolve issues faster.
|
||||
|
||||
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
||||
|
||||
#### Before Submitting A Bug Report
|
||||
|
||||
* **Check the docs** There's a chance what you see as a bug might just work differently than you expect and if you think it could work better consider a feature enhancement report instead.
|
||||
* **Search the [discord](https://discord.gg/qbcore)** to see if anyone else has run into the issue and see if it was solved through user error or code changes. (if the code change isn't pending a PR and you know what you're doing consider submitting one following [Pull Requests](#pull-requests) )
|
||||
* **Determine which resource the problem should be reported in**. If the bug is related to the inventory for example report this bug under qb-inventory rather than under qb-core or some other resource.
|
||||
* **Perform a [cursory search](https://github.com/search?q=+is%3Aissue+user%3Aqbcore-framework)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
|
||||
|
||||
#### How Do I Submit A (Good) Bug Report?
|
||||
|
||||
Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which resource your bug is related to, create an issue on that repository and provide the following information by filling in bug-report template.
|
||||
|
||||
Explain the problem and include additional details to help maintainers reproduce the problem:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the problem.
|
||||
* **Describe the exact steps which reproduce the problem** in as many details as possible.
|
||||
* **Provide specific examples to demonstrate the steps**. If something happened with only a specific group or single item but not others, specify that.
|
||||
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
|
||||
* **Explain which behavior you expected to see instead and why.**
|
||||
* **Include screenshots** which show the specific bug in action or before and after.
|
||||
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
|
||||
|
||||
Provide more context by answering these questions if possible:
|
||||
|
||||
* **Did the problem start happening recently** (e.g. after updating to a new version of QBCore?) or was this always a problem?
|
||||
* If the problem started happening recently, **can you reproduce the problem in an older version of QBCore?** What's the most recent commit in which the problem doesn't happen?
|
||||
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
||||
|
||||
Include details about your setup:
|
||||
|
||||
* **When was your QBCore last updated?**
|
||||
* **What OS is the server running on**?
|
||||
* **Which *extra* resources do you have installed?**
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Suggesting Features / Enhancements
|
||||
|
||||
This section guides you through submitting an enhancement suggestion for QBCore, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion.
|
||||
|
||||
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in feature request template, including the steps that you imagine you would take if the feature you're requesting existed.
|
||||
|
||||
#### Before Submitting An Enhancement Suggestion
|
||||
|
||||
* **Make sure it doesn't already exist.** Sounds silly, but there's a lot of features built in to qbcore that people don't realize so take a look through the docs and stuff to make sure it's not already there.
|
||||
* **Check if there's already PR which provides that enhancement.**
|
||||
* **Determine which resource the enhancement should be suggested in.** if it fits with another resource suggest it in that resource. if it would be it's own resource suggest it in the main qb-core repository.
|
||||
* **Perform a [cursory search](https://github.com/search?q=+is%3Aissue+user%3Aqbcore-framework)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||
|
||||
#### How Do I Submit A (Good) Enhancement Suggestion?
|
||||
|
||||
Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which resource your enhancement suggestion is related to, create an issue on that repository and provide the following information:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
||||
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
|
||||
* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
||||
* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
|
||||
* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of QBCore which the suggestion is related to.
|
||||
* **Explain why this enhancement would be useful.**
|
||||
* **Be creative and unique.** Stealing ideas from popular servers 1:1 detail isn't going to get accepted.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
Unsure where to begin contributing to QBCore? You can start by looking through these `beginner` and `help-wanted` issues.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Pull Requests
|
||||
|
||||
The process described here has several goals:
|
||||
|
||||
- Maintain QBCore's quality.
|
||||
- Fix problems that are important to users.
|
||||
- Engage the community in working toward the best possible QBCore.
|
||||
- Enable a sustainable system for QBCore's maintainers to review contributions.
|
||||
|
||||
Please follow these steps to have your contribution considered by the maintainers:
|
||||
|
||||
1. Follow all instructions in The Pull Request template.
|
||||
2. Follow the [styleguides](#styleguides).
|
||||
3. Await review by the reviewer(s).
|
||||
|
||||
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Styleguides
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
* Limit the first line to 72 characters or less.
|
||||
* Reference issues and pull requests liberally after the first line.
|
||||
* Consider starting the commit message with an applicable emoji:
|
||||
* :art: `:art:` when improving the format/structure of the code
|
||||
* :racehorse: `:racehorse:` when improving performance
|
||||
* :memo: `:memo:` when writing docs
|
||||
* :bug: `:bug:` when fixing a bug
|
||||
* :fire: `:fire:` when removing code or files
|
||||
* :white_check_mark: `:white_check_mark:` when adding tests
|
||||
* :lock: `:lock:` when dealing with security
|
||||
* :arrow_up: `:arrow_up:` when upgrading dependencies
|
||||
* :arrow_down: `:arrow_down:` when downgrading dependencies
|
||||
* :shirt: `:shirt:` when removing linter warnings
|
||||
|
||||
### Lua Styleguide
|
||||
|
||||
All lua code should be done using all the best practices of proper lua using the easiest to read yet fastest/most optimized methods of execution.
|
||||
|
||||
- Use 4 Space indentation
|
||||
- Aim for lua 5.4 (include `lua54 'yes'` in the fxmanifest.lua)
|
||||
- Use `PlayerPedId()` instead of `GetPlayerPed(-1)`
|
||||
- Use `#(vector3 - vector3)` instead of `GetDistanceBetweenCoords()`
|
||||
- Don't create unnecessary threads. always try to find a better method of triggering events
|
||||
- Don't repeat yourself.. if you're using the same operations in many different places convert them into a function with flexible variables
|
||||
- For distance checking loops set longer waits if you're outside of a range
|
||||
- Job specific loops should only run for players with that job, don't waste cycles
|
||||
- When possible don't trust the client, esspecially with transactions
|
||||
- Balance security and optimizations
|
||||
- [Consider this Lua Performance guide](https://springrts.com/wiki/Lua_Performance)
|
||||
- Use local varriables everywhere possible
|
||||
- Make use of config options where it makes sense making features optional or customizable
|
||||
- Instead of `table.insert(myTable, "Value")` use `myTable[#myTable + 1] = "Value"`
|
||||
- Instead of `table.insert(ages, "bob", 30)` use `ages["bob"] = 30`
|
||||
|
||||
|
||||
### JavaScript Styleguide
|
||||
|
||||
- Use 4 Space indentation
|
||||
- Don't repeat yourself.. if you're using the same operations in many different places convert them into a function with flexible variables.
|
||||
@ -1,10 +0,0 @@
|
||||
**Describe Pull request**
|
||||
First, make sure you've read and are following the contribution guidelines and style guide and your code reflects that.
|
||||
Write up a clear and concise description of what your pull request adds or fixes and if it's an added feature explain why you think it should be included in the core.
|
||||
|
||||
If your PR is to fix an issue mention that issue here
|
||||
|
||||
**Questions (please complete the following information):**
|
||||
- Have you personally loaded this code into an updated qbcore project and checked all it's functionality? [yes/no] (Be honest)
|
||||
- Does your code fit the style guidelines? [yes/no]
|
||||
- Does your PR fit the contribution guidelines? [yes/no]
|
||||
@ -1,23 +0,0 @@
|
||||
name: Lint
|
||||
on: [push, pull_request_target]
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Resource
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Lint
|
||||
uses: iLLeniumStudios/fivem-lua-lint-action@v2
|
||||
with:
|
||||
capture: "junit.xml"
|
||||
args: "-t --formatter JUnit"
|
||||
extra_libs: mysql+polyzone+qblocales
|
||||
- name: Generate Lint Report
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v3
|
||||
with:
|
||||
report_paths: "**/junit.xml"
|
||||
check_name: Linting Report
|
||||
fail_on_failure: false
|
||||
@ -1,29 +0,0 @@
|
||||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '41 15 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has had 60 days of inactivity & will close within 7 days'
|
||||
stale-pr-message: 'This PR has had 60 days of inactivity & will close within 7 days'
|
||||
close-issue-label: 'Stale Closed'
|
||||
close-pr-label: 'Stale Closed'
|
||||
exempt-issue-labels: 'Suggestion'
|
||||
exempt-pr-labels: 'Suggestion'
|
||||
@ -1,108 +0,0 @@
|
||||
# qb-apartments
|
||||
Apartments System for QB-Core Framework :office:
|
||||
|
||||
# License
|
||||
|
||||
QBCore Framework
|
||||
Copyright (C) 2021 Joshua Eger
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
|
||||
|
||||
## Dependencies
|
||||
- [qb-core](https://github.com/qbcore-framework/qb-core)
|
||||
- [qb-clothing](https://github.com/qbcore-framework/qb-clothing) - To save outfits
|
||||
- [qb-houses](https://github.com/qbcore-framework/qb-houses) - House logic
|
||||
- [qb-interior](https://github.com/qbcore-framework/qb-interior) - Interior logic
|
||||
- [qb-weathersync](https://github.com/qbcore-framework/qb-weathersync) - To desync weather while inside
|
||||
- [qb-spawn](https://github.com/qbcore-framework/qb-spawn) - To spawn the player at apartment if last location was in apartment
|
||||
|
||||
## Screenshots
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
- Door Bell
|
||||
- Stash
|
||||
- Log Out Marker
|
||||
- Saved Outfits
|
||||
|
||||
## Installation
|
||||
### Manual
|
||||
- Download the script and put it in the `[qb]` directory.
|
||||
- Import `qb-apartments.sql` in your database
|
||||
- Add the following code to your server.cfg/resouces.cfg
|
||||
```
|
||||
ensure qb-core
|
||||
ensure qb-interior
|
||||
ensure qb-weathersync
|
||||
ensure qb-clothing
|
||||
ensure qb-houses
|
||||
ensure qb-spawn
|
||||
ensure qb-apartments
|
||||
```
|
||||
|
||||
## Configuration
|
||||
```
|
||||
Apartments = {} -- Don't touch
|
||||
|
||||
Apartments.SpawnOffset = 30 -- Don't touch
|
||||
|
||||
Apartments.Locations = {
|
||||
["apartment1"] = { -- Needs to be unique
|
||||
name = "apartment1", -- Apartment id
|
||||
label = "South Rockford Drive", -- Apartment Label (for Blip and other stuff)
|
||||
coords = {
|
||||
enter = {x = -667.372, y = -1106.034, z = 14.629, h = 65.033}, -- Enter Apartment Marker Location
|
||||
doorbell = {x = -667.601, y = -1107.354, z = 15.133, h = 65.033}, -- Exit Apartment Marker Location
|
||||
}
|
||||
},
|
||||
["apartment2"] = {
|
||||
name = "apartment2",
|
||||
label = "Morningwood Blvd",
|
||||
coords = {
|
||||
enter = {x = -1288.046, y = -430.126, z = 35.077, h = 305.348},
|
||||
doorbell = {x = -667.682, y = -1105.876, z = 14.629, h = 65.033},
|
||||
}
|
||||
},
|
||||
["apartment3"] = {
|
||||
name = "apartment3",
|
||||
label = "Integrity Way",
|
||||
coords = {
|
||||
enter = {x = 269.075, y = -640.672, z = 42.02, h = 70.01},
|
||||
doorbell = {x = -667.682, y = -1105.876, z = 14.629, h = 65.033},
|
||||
}
|
||||
},
|
||||
["apartment4"] = {
|
||||
name = "apartment4",
|
||||
label = "Tinsel Towers",
|
||||
coords = {
|
||||
enter = {x = -621.016, y = 46.677, z = 43.591, h = 179.36},
|
||||
doorbell = {x = -667.682, y = -1105.876, z = 14.629, h = 65.033},
|
||||
}
|
||||
},
|
||||
["apartment5"] = {
|
||||
name = "apartment5",
|
||||
label = "Fantastic Plaza",
|
||||
coords = {
|
||||
enter = {x = 291.517, y = -1078.674, z = 29.405, h = 270.75},
|
||||
doorbell = {x = -667.682, y = -1105.876, z = 14.629, h = 65.033},
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
@ -1,788 +0,0 @@
|
||||
local QBCore = exports['qb-core']:GetCoreObject()
|
||||
local UseTarget = GetConvar('UseTarget', 'false') == 'true'
|
||||
local InApartment = false
|
||||
local ClosestHouse = nil
|
||||
local CurrentApartment = nil
|
||||
local IsOwned = false
|
||||
local CurrentDoorBell = 0
|
||||
local CurrentOffset = 0
|
||||
local HouseObj = {}
|
||||
local POIOffsets = nil
|
||||
local RangDoorbell = nil
|
||||
|
||||
-- target variables
|
||||
local InApartmentTargets = {}
|
||||
|
||||
-- polyzone variables
|
||||
local IsInsideEntranceZone = false
|
||||
local IsInsideExitZone = false
|
||||
local IsInsideStashZone = false
|
||||
local IsInsideOutfitsZone = false
|
||||
local IsInsideLogoutZone = false
|
||||
|
||||
-- polyzone integration
|
||||
|
||||
local function OpenEntranceMenu()
|
||||
local headerMenu = {}
|
||||
|
||||
if IsOwned then
|
||||
headerMenu[#headerMenu + 1] = {
|
||||
header = Lang:t('text.enter'),
|
||||
params = {
|
||||
event = 'apartments:client:EnterApartment',
|
||||
args = {}
|
||||
}
|
||||
}
|
||||
elseif not IsOwned then
|
||||
headerMenu[#headerMenu + 1] = {
|
||||
header = Lang:t('text.move_here'),
|
||||
params = {
|
||||
event = 'apartments:client:UpdateApartment',
|
||||
args = {}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
headerMenu[#headerMenu + 1] = {
|
||||
header = Lang:t('text.ring_doorbell'),
|
||||
params = {
|
||||
event = 'apartments:client:DoorbellMenu',
|
||||
args = {}
|
||||
}
|
||||
}
|
||||
|
||||
headerMenu[#headerMenu + 1] = {
|
||||
header = Lang:t('text.close_menu'),
|
||||
txt = '',
|
||||
params = {
|
||||
event = 'qb-menu:client:closeMenu'
|
||||
}
|
||||
}
|
||||
|
||||
exports['qb-menu']:openMenu(headerMenu)
|
||||
end
|
||||
|
||||
local function OpenExitMenu()
|
||||
local headerMenu = {}
|
||||
|
||||
headerMenu[#headerMenu + 1] = {
|
||||
header = Lang:t('text.open_door'),
|
||||
params = {
|
||||
event = 'apartments:client:OpenDoor',
|
||||
args = {}
|
||||
}
|
||||
}
|
||||
|
||||
headerMenu[#headerMenu + 1] = {
|
||||
header = Lang:t('text.leave'),
|
||||
params = {
|
||||
event = 'apartments:client:LeaveApartment',
|
||||
args = {}
|
||||
}
|
||||
}
|
||||
|
||||
headerMenu[#headerMenu + 1] = {
|
||||
header = Lang:t('text.close_menu'),
|
||||
txt = '',
|
||||
params = {
|
||||
event = 'qb-menu:client:closeMenu'
|
||||
}
|
||||
}
|
||||
|
||||
exports['qb-menu']:openMenu(headerMenu)
|
||||
end
|
||||
|
||||
-- exterior entrance (polyzone)
|
||||
|
||||
local function RegisterApartmentEntranceZone(apartmentID, apartmentData)
|
||||
local coords = apartmentData.coords['enter']
|
||||
local boxName = 'apartmentEntrance_' .. apartmentID
|
||||
local boxData = apartmentData.polyzoneBoxData
|
||||
|
||||
if boxData.created then
|
||||
return
|
||||
end
|
||||
|
||||
local zone = BoxZone:Create(coords, boxData.length, boxData.width, {
|
||||
name = boxName,
|
||||
heading = 340.0,
|
||||
minZ = coords.z - 1.0,
|
||||
maxZ = coords.z + 5.0,
|
||||
debugPoly = false
|
||||
})
|
||||
|
||||
zone:onPlayerInOut(function(isPointInside)
|
||||
if isPointInside and not InApartment then
|
||||
exports['qb-core']:DrawText(Lang:t('text.options'), 'left')
|
||||
else
|
||||
exports['qb-core']:HideText()
|
||||
end
|
||||
IsInsideEntranceZone = isPointInside
|
||||
end)
|
||||
|
||||
boxData.created = true
|
||||
boxData.zone = zone
|
||||
end
|
||||
|
||||
-- exterior entrance (target)
|
||||
|
||||
local function RegisterApartmentEntranceTarget(apartmentID, apartmentData)
|
||||
local coords = apartmentData.coords['enter']
|
||||
local boxName = 'apartmentEntrance_' .. apartmentID
|
||||
local boxData = apartmentData.polyzoneBoxData
|
||||
|
||||
if boxData.created then
|
||||
return
|
||||
end
|
||||
|
||||
local options = {}
|
||||
if apartmentID == ClosestHouse and IsOwned then
|
||||
options = {
|
||||
{
|
||||
type = 'client',
|
||||
event = 'apartments:client:EnterApartment',
|
||||
icon = 'fas fa-door-open',
|
||||
label = Lang:t('text.enter'),
|
||||
},
|
||||
}
|
||||
else
|
||||
options = {
|
||||
{
|
||||
type = 'client',
|
||||
event = 'apartments:client:UpdateApartment',
|
||||
icon = 'fas fa-hotel',
|
||||
label = Lang:t('text.move_here'),
|
||||
}
|
||||
}
|
||||
end
|
||||
options[#options + 1] = {
|
||||
type = 'client',
|
||||
event = 'apartments:client:DoorbellMenu',
|
||||
icon = 'fas fa-concierge-bell',
|
||||
label = Lang:t('text.ring_doorbell'),
|
||||
}
|
||||
|
||||
exports['qb-target']:AddBoxZone(boxName, coords, boxData.length, boxData.width, {
|
||||
name = boxName,
|
||||
heading = boxData.heading,
|
||||
debugPoly = boxData.debug,
|
||||
minZ = boxData.minZ,
|
||||
maxZ = boxData.maxZ,
|
||||
}, {
|
||||
options = options,
|
||||
distance = boxData.distance
|
||||
})
|
||||
|
||||
boxData.created = true
|
||||
end
|
||||
|
||||
-- interior interactable points (polyzone)
|
||||
|
||||
local function RegisterInApartmentZone(targetKey, coords, heading, text)
|
||||
if not InApartment then
|
||||
return
|
||||
end
|
||||
|
||||
if InApartmentTargets[targetKey] and InApartmentTargets[targetKey].created then
|
||||
return
|
||||
end
|
||||
|
||||
Wait(1500)
|
||||
|
||||
local boxName = 'inApartmentTarget_' .. targetKey
|
||||
|
||||
local zone = BoxZone:Create(coords, 1.5, 1.5, {
|
||||
name = boxName,
|
||||
heading = heading,
|
||||
minZ = coords.z - 1.0,
|
||||
maxZ = coords.z + 5.0,
|
||||
debugPoly = false
|
||||
})
|
||||
|
||||
zone:onPlayerInOut(function(isPointInside)
|
||||
if isPointInside and text then
|
||||
exports['qb-core']:DrawText(text, 'left')
|
||||
else
|
||||
exports['qb-core']:HideText()
|
||||
end
|
||||
|
||||
if targetKey == 'entrancePos' then
|
||||
IsInsideExitZone = isPointInside
|
||||
end
|
||||
|
||||
if targetKey == 'stashPos' then
|
||||
IsInsideStashZone = isPointInside
|
||||
end
|
||||
|
||||
if targetKey == 'outfitsPos' then
|
||||
IsInsideOutfitsZone = isPointInside
|
||||
end
|
||||
|
||||
if targetKey == 'logoutPos' then
|
||||
IsInsideLogoutZone = isPointInside
|
||||
end
|
||||
end)
|
||||
|
||||
InApartmentTargets[targetKey] = InApartmentTargets[targetKey] or {}
|
||||
InApartmentTargets[targetKey].created = true
|
||||
InApartmentTargets[targetKey].zone = zone
|
||||
end
|
||||
|
||||
-- interior interactable points (target)
|
||||
|
||||
local function RegisterInApartmentTarget(targetKey, coords, heading, options)
|
||||
if not InApartment then
|
||||
return
|
||||
end
|
||||
|
||||
if InApartmentTargets[targetKey] and InApartmentTargets[targetKey].created then
|
||||
return
|
||||
end
|
||||
|
||||
local boxName = 'inApartmentTarget_' .. targetKey
|
||||
exports['qb-target']:AddBoxZone(boxName, coords, 1.5, 1.5, {
|
||||
name = boxName,
|
||||
heading = heading,
|
||||
minZ = coords.z - 1.0,
|
||||
maxZ = coords.z + 5.0,
|
||||
debugPoly = false,
|
||||
}, {
|
||||
options = options,
|
||||
distance = 1
|
||||
})
|
||||
|
||||
InApartmentTargets[targetKey] = InApartmentTargets[targetKey] or {}
|
||||
InApartmentTargets[targetKey].created = true
|
||||
end
|
||||
|
||||
-- shared
|
||||
|
||||
local function SetApartmentsEntranceTargets()
|
||||
if Apartments.Locations and next(Apartments.Locations) then
|
||||
for id, apartment in pairs(Apartments.Locations) do
|
||||
if apartment and apartment.coords and apartment.coords['enter'] then
|
||||
if UseTarget then
|
||||
RegisterApartmentEntranceTarget(id, apartment)
|
||||
else
|
||||
RegisterApartmentEntranceZone(id, apartment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SetInApartmentTargets()
|
||||
if not POIOffsets then
|
||||
-- do nothing
|
||||
return
|
||||
end
|
||||
|
||||
local entrancePos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x + POIOffsets.exit.x, Apartments.Locations[ClosestHouse].coords.enter.y + POIOffsets.exit.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.exit.z)
|
||||
local stashPos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x - POIOffsets.stash.x, Apartments.Locations[ClosestHouse].coords.enter.y - POIOffsets.stash.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.stash.z)
|
||||
local outfitsPos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x - POIOffsets.clothes.x, Apartments.Locations[ClosestHouse].coords.enter.y - POIOffsets.clothes.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.clothes.z)
|
||||
local logoutPos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x - POIOffsets.logout.x, Apartments.Locations[ClosestHouse].coords.enter.y + POIOffsets.logout.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.logout.z)
|
||||
|
||||
if UseTarget then
|
||||
RegisterInApartmentTarget('entrancePos', entrancePos, 0, {
|
||||
{
|
||||
type = 'client',
|
||||
event = 'apartments:client:OpenDoor',
|
||||
icon = 'fas fa-door-open',
|
||||
label = Lang:t('text.open_door'),
|
||||
},
|
||||
{
|
||||
type = 'client',
|
||||
event = 'apartments:client:LeaveApartment',
|
||||
icon = 'fas fa-door-open',
|
||||
label = Lang:t('text.leave'),
|
||||
},
|
||||
})
|
||||
RegisterInApartmentTarget('stashPos', stashPos, 0, {
|
||||
{
|
||||
type = 'client',
|
||||
event = 'apartments:client:OpenStash',
|
||||
icon = 'fas fa-box-open',
|
||||
label = Lang:t('text.open_stash'),
|
||||
},
|
||||
})
|
||||
RegisterInApartmentTarget('outfitsPos', outfitsPos, 0, {
|
||||
{
|
||||
type = 'client',
|
||||
event = 'apartments:client:ChangeOutfit',
|
||||
icon = 'fas fa-tshirt',
|
||||
label = Lang:t('text.change_outfit'),
|
||||
},
|
||||
})
|
||||
RegisterInApartmentTarget('logoutPos', logoutPos, 0, {
|
||||
{
|
||||
type = 'client',
|
||||
event = 'apartments:client:Logout',
|
||||
icon = 'fas fa-sign-out-alt',
|
||||
label = Lang:t('text.logout'),
|
||||
},
|
||||
})
|
||||
else
|
||||
RegisterInApartmentZone('stashPos', stashPos, 0, '[E] ' .. Lang:t('text.open_stash'))
|
||||
RegisterInApartmentZone('outfitsPos', outfitsPos, 0, '[E] ' .. Lang:t('text.change_outfit'))
|
||||
RegisterInApartmentZone('logoutPos', logoutPos, 0, '[E] ' .. Lang:t('text.logout'))
|
||||
RegisterInApartmentZone('entrancePos', entrancePos, 0, Lang:t('text.options'))
|
||||
end
|
||||
end
|
||||
|
||||
local function DeleteApartmentsEntranceTargets()
|
||||
if Apartments.Locations and next(Apartments.Locations) then
|
||||
for id, apartment in pairs(Apartments.Locations) do
|
||||
if UseTarget then
|
||||
exports['qb-target']:RemoveZone('apartmentEntrance_' .. id)
|
||||
else
|
||||
if apartment.polyzoneBoxData.zone then
|
||||
apartment.polyzoneBoxData.zone:destroy()
|
||||
apartment.polyzoneBoxData.zone = nil
|
||||
end
|
||||
end
|
||||
apartment.polyzoneBoxData.created = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function DeleteInApartmentTargets()
|
||||
IsInsideExitZone = false
|
||||
IsInsideStashZone = false
|
||||
IsInsideOutfitsZone = false
|
||||
IsInsideLogoutZone = false
|
||||
|
||||
if InApartmentTargets and next(InApartmentTargets) then
|
||||
for id, apartmentTarget in pairs(InApartmentTargets) do
|
||||
if UseTarget then
|
||||
exports['qb-target']:RemoveZone('inApartmentTarget_' .. id)
|
||||
else
|
||||
if apartmentTarget.zone then
|
||||
apartmentTarget.zone:destroy()
|
||||
apartmentTarget.zone = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
InApartmentTargets = {}
|
||||
end
|
||||
|
||||
-- utility functions
|
||||
|
||||
local function loadAnimDict(dict)
|
||||
while (not HasAnimDictLoaded(dict)) do
|
||||
RequestAnimDict(dict)
|
||||
Wait(5)
|
||||
end
|
||||
end
|
||||
|
||||
local function openHouseAnim()
|
||||
loadAnimDict('anim@heists@keycard@')
|
||||
TaskPlayAnim(PlayerPedId(), 'anim@heists@keycard@', 'exit', 5.0, 1.0, -1, 16, 0, 0, 0, 0)
|
||||
Wait(400)
|
||||
ClearPedTasks(PlayerPedId())
|
||||
end
|
||||
|
||||
local function EnterApartment(house, apartmentId, new)
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_open', 0.1)
|
||||
openHouseAnim()
|
||||
Wait(250)
|
||||
QBCore.Functions.TriggerCallback('apartments:GetApartmentOffset', function(offset)
|
||||
if offset == nil or offset == 0 then
|
||||
QBCore.Functions.TriggerCallback('apartments:GetApartmentOffsetNewOffset', function(newoffset)
|
||||
if newoffset > 230 then
|
||||
newoffset = 210
|
||||
end
|
||||
CurrentOffset = newoffset
|
||||
TriggerServerEvent('apartments:server:AddObject', apartmentId, house, CurrentOffset)
|
||||
local coords = { x = Apartments.Locations[house].coords.enter.x, y = Apartments.Locations[house].coords.enter.y, z = Apartments.Locations[house].coords.enter.z - CurrentOffset }
|
||||
local data = exports['qb-interior']:CreateApartmentFurnished(coords)
|
||||
Wait(100)
|
||||
HouseObj = data[1]
|
||||
POIOffsets = data[2]
|
||||
InApartment = true
|
||||
CurrentApartment = apartmentId
|
||||
ClosestHouse = house
|
||||
RangDoorbell = nil
|
||||
Wait(500)
|
||||
TriggerEvent('qb-weathersync:client:DisableSync')
|
||||
Wait(100)
|
||||
TriggerServerEvent('qb-apartments:server:SetInsideMeta', house, apartmentId, true, false)
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_close', 0.1)
|
||||
TriggerServerEvent('apartments:server:setCurrentApartment', CurrentApartment)
|
||||
end, house)
|
||||
else
|
||||
if offset > 230 then
|
||||
offset = 210
|
||||
end
|
||||
CurrentOffset = offset
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_open', 0.1)
|
||||
TriggerServerEvent('apartments:server:AddObject', apartmentId, house, CurrentOffset)
|
||||
local coords = { x = Apartments.Locations[ClosestHouse].coords.enter.x, y = Apartments.Locations[ClosestHouse].coords.enter.y, z = Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset }
|
||||
local data = exports['qb-interior']:CreateApartmentFurnished(coords)
|
||||
Wait(100)
|
||||
HouseObj = data[1]
|
||||
POIOffsets = data[2]
|
||||
InApartment = true
|
||||
CurrentApartment = apartmentId
|
||||
Wait(500)
|
||||
TriggerEvent('qb-weathersync:client:DisableSync')
|
||||
Wait(100)
|
||||
TriggerServerEvent('qb-apartments:server:SetInsideMeta', house, apartmentId, true, true)
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_close', 0.1)
|
||||
TriggerServerEvent('apartments:server:setCurrentApartment', CurrentApartment)
|
||||
end
|
||||
|
||||
if new ~= nil then
|
||||
if new then
|
||||
TriggerEvent('qb-interior:client:SetNewState', true)
|
||||
else
|
||||
TriggerEvent('qb-interior:client:SetNewState', false)
|
||||
end
|
||||
else
|
||||
TriggerEvent('qb-interior:client:SetNewState', false)
|
||||
end
|
||||
end, apartmentId)
|
||||
end
|
||||
|
||||
local function LeaveApartment(house)
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_open', 0.1)
|
||||
openHouseAnim()
|
||||
TriggerServerEvent('qb-apartments:returnBucket')
|
||||
DoScreenFadeOut(500)
|
||||
while not IsScreenFadedOut() do Wait(10) end
|
||||
exports['qb-interior']:DespawnInterior(HouseObj, function()
|
||||
TriggerEvent('qb-weathersync:client:EnableSync')
|
||||
SetEntityCoords(PlayerPedId(), Apartments.Locations[house].coords.enter.x, Apartments.Locations[house].coords.enter.y, Apartments.Locations[house].coords.enter.z)
|
||||
SetEntityHeading(PlayerPedId(), Apartments.Locations[house].coords.enter.w)
|
||||
Wait(1000)
|
||||
TriggerServerEvent('apartments:server:RemoveObject', CurrentApartment, house)
|
||||
TriggerServerEvent('qb-apartments:server:SetInsideMeta', CurrentApartment, false)
|
||||
CurrentApartment = nil
|
||||
InApartment = false
|
||||
CurrentOffset = 0
|
||||
DoScreenFadeIn(1000)
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_close', 0.1)
|
||||
TriggerServerEvent('apartments:server:setCurrentApartment', nil)
|
||||
|
||||
DeleteInApartmentTargets()
|
||||
DeleteApartmentsEntranceTargets()
|
||||
end)
|
||||
end
|
||||
|
||||
local function SetClosestApartment()
|
||||
local pos = GetEntityCoords(PlayerPedId())
|
||||
local current = nil
|
||||
local dist = 100
|
||||
for id, _ in pairs(Apartments.Locations) do
|
||||
local distcheck = #(pos - vector3(Apartments.Locations[id].coords.enter.x, Apartments.Locations[id].coords.enter.y, Apartments.Locations[id].coords.enter.z))
|
||||
if distcheck < dist then
|
||||
current = id
|
||||
end
|
||||
end
|
||||
if current ~= ClosestHouse and LocalPlayer.state.isLoggedIn and not InApartment then
|
||||
ClosestHouse = current
|
||||
QBCore.Functions.TriggerCallback('apartments:IsOwner', function(result)
|
||||
IsOwned = result
|
||||
DeleteApartmentsEntranceTargets()
|
||||
DeleteInApartmentTargets()
|
||||
end, ClosestHouse)
|
||||
end
|
||||
end
|
||||
|
||||
function MenuOwners()
|
||||
QBCore.Functions.TriggerCallback('apartments:GetAvailableApartments', function(apartments)
|
||||
if next(apartments) == nil then
|
||||
QBCore.Functions.Notify(Lang:t('error.nobody_home'), 'error', 3500)
|
||||
CloseMenuFull()
|
||||
else
|
||||
local apartmentMenu = {
|
||||
{
|
||||
header = Lang:t('text.tennants'),
|
||||
isMenuHeader = true
|
||||
}
|
||||
}
|
||||
|
||||
for k, v in pairs(apartments) do
|
||||
apartmentMenu[#apartmentMenu + 1] = {
|
||||
header = v,
|
||||
txt = '',
|
||||
params = {
|
||||
event = 'apartments:client:RingMenu',
|
||||
args = {
|
||||
apartmentId = k
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
apartmentMenu[#apartmentMenu + 1] = {
|
||||
header = Lang:t('text.close_menu'),
|
||||
txt = '',
|
||||
params = {
|
||||
event = 'qb-menu:client:closeMenu'
|
||||
}
|
||||
|
||||
}
|
||||
exports['qb-menu']:openMenu(apartmentMenu)
|
||||
end
|
||||
end, ClosestHouse)
|
||||
end
|
||||
|
||||
function CloseMenuFull()
|
||||
exports['qb-menu']:closeMenu()
|
||||
end
|
||||
|
||||
-- Event Handlers
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource == GetCurrentResourceName() then
|
||||
if HouseObj ~= nil then
|
||||
exports['qb-interior']:DespawnInterior(HouseObj, function()
|
||||
CurrentApartment = nil
|
||||
TriggerEvent('qb-weathersync:client:EnableSync')
|
||||
DoScreenFadeIn(500)
|
||||
while not IsScreenFadedOut() do
|
||||
Wait(10)
|
||||
end
|
||||
SetEntityCoords(PlayerPedId(), Apartments.Locations[ClosestHouse].coords.enter.x, Apartments.Locations[ClosestHouse].coords.enter.y, Apartments.Locations[ClosestHouse].coords.enter.z)
|
||||
SetEntityHeading(PlayerPedId(), Apartments.Locations[ClosestHouse].coords.enter.w)
|
||||
Wait(1000)
|
||||
InApartment = false
|
||||
DoScreenFadeIn(1000)
|
||||
end)
|
||||
end
|
||||
|
||||
DeleteApartmentsEntranceTargets()
|
||||
DeleteInApartmentTargets()
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
-- Events
|
||||
|
||||
RegisterNetEvent('QBCore:Client:OnPlayerUnload', function()
|
||||
CurrentApartment = nil
|
||||
InApartment = false
|
||||
CurrentOffset = 0
|
||||
|
||||
DeleteApartmentsEntranceTargets()
|
||||
DeleteInApartmentTargets()
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:setupSpawnUI', function(cData)
|
||||
QBCore.Functions.TriggerCallback('apartments:GetOwnedApartment', function(result)
|
||||
if result then
|
||||
TriggerEvent('qb-spawn:client:setupSpawns', cData, false, nil)
|
||||
TriggerEvent('qb-spawn:client:openUI', true)
|
||||
TriggerEvent('apartments:client:SetHomeBlip', result.type)
|
||||
else
|
||||
if Apartments.Starting then
|
||||
TriggerEvent('qb-spawn:client:setupSpawns', cData, true, Apartments.Locations)
|
||||
TriggerEvent('qb-spawn:client:openUI', true)
|
||||
else
|
||||
TriggerEvent('qb-spawn:client:setupSpawns', cData, false, nil)
|
||||
TriggerEvent('qb-spawn:client:openUI', true)
|
||||
TriggerEvent('apartments:client:SetHomeBlip', nil)
|
||||
end
|
||||
end
|
||||
end, cData.citizenid)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:SpawnInApartment', function(apartmentId, apartment)
|
||||
local pos = GetEntityCoords(PlayerPedId())
|
||||
if RangDoorbell ~= nil then
|
||||
local doorbelldist = #(pos - vector3(Apartments.Locations[RangDoorbell].coords.enter.x, Apartments.Locations[RangDoorbell].coords.enter.y, Apartments.Locations[RangDoorbell].coords.enter.z))
|
||||
if doorbelldist > 5 then
|
||||
QBCore.Functions.Notify(Lang:t('error.to_far_from_door'))
|
||||
return
|
||||
end
|
||||
end
|
||||
ClosestHouse = apartment
|
||||
EnterApartment(apartment, apartmentId, true)
|
||||
IsOwned = true
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-apartments:client:LastLocationHouse', function(apartmentType, apartmentId)
|
||||
ClosestHouse = apartmentType
|
||||
EnterApartment(apartmentType, apartmentId, false)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:SetHomeBlip', function(home)
|
||||
CreateThread(function()
|
||||
SetClosestApartment()
|
||||
for name, _ in pairs(Apartments.Locations) do
|
||||
RemoveBlip(Apartments.Locations[name].blip)
|
||||
|
||||
Apartments.Locations[name].blip = AddBlipForCoord(Apartments.Locations[name].coords.enter.x, Apartments.Locations[name].coords.enter.y, Apartments.Locations[name].coords.enter.z)
|
||||
if (name == home) then
|
||||
SetBlipSprite(Apartments.Locations[name].blip, 475)
|
||||
SetBlipCategory(Apartments.Locations[name].blip, 11)
|
||||
else
|
||||
SetBlipSprite(Apartments.Locations[name].blip, 476)
|
||||
SetBlipCategory(Apartments.Locations[name].blip, 10)
|
||||
end
|
||||
SetBlipDisplay(Apartments.Locations[name].blip, 4)
|
||||
SetBlipScale(Apartments.Locations[name].blip, 0.65)
|
||||
SetBlipAsShortRange(Apartments.Locations[name].blip, true)
|
||||
SetBlipColour(Apartments.Locations[name].blip, 3)
|
||||
AddTextEntry(Apartments.Locations[name].label, Apartments.Locations[name].label)
|
||||
BeginTextCommandSetBlipName(Apartments.Locations[name].label)
|
||||
EndTextCommandSetBlipName(Apartments.Locations[name].blip)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:RingMenu', function(data)
|
||||
RangDoorbell = ClosestHouse
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'doorbell', 0.1)
|
||||
TriggerServerEvent('apartments:server:RingDoor', data.apartmentId, ClosestHouse)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:RingDoor', function(player, _)
|
||||
CurrentDoorBell = player
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'doorbell', 0.1)
|
||||
QBCore.Functions.Notify(Lang:t('info.at_the_door'))
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:DoorbellMenu', function()
|
||||
MenuOwners()
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:EnterApartment', function()
|
||||
QBCore.Functions.TriggerCallback('apartments:GetOwnedApartment', function(result)
|
||||
if result ~= nil then
|
||||
EnterApartment(ClosestHouse, result.name)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:UpdateApartment', function()
|
||||
local apartmentType = ClosestHouse
|
||||
local apartmentLabel = Apartments.Locations[ClosestHouse].label
|
||||
QBCore.Functions.TriggerCallback('apartments:GetOwnedApartment', function(result)
|
||||
if result == nil then
|
||||
TriggerServerEvent("apartments:server:CreateApartment", apartmentType, apartmentLabel, false)
|
||||
else
|
||||
TriggerServerEvent('apartments:server:UpdateApartment', apartmentType, apartmentLabel)
|
||||
end
|
||||
end)
|
||||
|
||||
IsOwned = true
|
||||
|
||||
DeleteApartmentsEntranceTargets()
|
||||
DeleteInApartmentTargets()
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:OpenDoor', function()
|
||||
if CurrentDoorBell == 0 then
|
||||
QBCore.Functions.Notify(Lang:t('error.nobody_at_door'))
|
||||
return
|
||||
end
|
||||
TriggerServerEvent('apartments:server:OpenDoor', CurrentDoorBell, CurrentApartment, ClosestHouse)
|
||||
CurrentDoorBell = 0
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:LeaveApartment', function()
|
||||
LeaveApartment(ClosestHouse)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:OpenStash', function()
|
||||
if CurrentApartment then
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'StashOpen', 0.4)
|
||||
TriggerServerEvent('apartments:server:openStash', CurrentApartment)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:ChangeOutfit', function()
|
||||
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'Clothes1', 0.4)
|
||||
TriggerEvent('qb-clothing:client:openOutfitMenu')
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:client:Logout', function()
|
||||
TriggerServerEvent('qb-houses:server:LogoutLocation')
|
||||
end)
|
||||
|
||||
|
||||
-- Threads
|
||||
|
||||
if UseTarget then
|
||||
CreateThread(function()
|
||||
local sleep = 5000
|
||||
while not LocalPlayer.state.isLoggedIn do
|
||||
-- do nothing
|
||||
Wait(sleep)
|
||||
end
|
||||
|
||||
while true do
|
||||
sleep = 1000
|
||||
|
||||
if not InApartment then
|
||||
SetClosestApartment()
|
||||
SetApartmentsEntranceTargets()
|
||||
elseif InApartment then
|
||||
SetInApartmentTargets()
|
||||
end
|
||||
Wait(sleep)
|
||||
end
|
||||
end)
|
||||
else
|
||||
CreateThread(function()
|
||||
local sleep = 5000
|
||||
while not LocalPlayer.state.isLoggedIn do
|
||||
-- do nothing
|
||||
Wait(sleep)
|
||||
end
|
||||
|
||||
while true do
|
||||
sleep = 1000
|
||||
|
||||
if not InApartment then
|
||||
SetClosestApartment()
|
||||
SetApartmentsEntranceTargets()
|
||||
|
||||
if IsInsideEntranceZone then
|
||||
sleep = 0
|
||||
if IsControlJustPressed(0, 38) then
|
||||
OpenEntranceMenu()
|
||||
exports['qb-core']:HideText()
|
||||
end
|
||||
end
|
||||
elseif InApartment then
|
||||
sleep = 0
|
||||
|
||||
SetInApartmentTargets()
|
||||
|
||||
if IsInsideExitZone then
|
||||
if IsControlJustPressed(0, 38) then
|
||||
OpenExitMenu()
|
||||
exports['qb-core']:HideText()
|
||||
end
|
||||
end
|
||||
|
||||
if IsInsideStashZone then
|
||||
if IsControlJustPressed(0, 38) then
|
||||
TriggerEvent('apartments:client:OpenStash')
|
||||
exports['qb-core']:HideText()
|
||||
end
|
||||
end
|
||||
|
||||
if IsInsideOutfitsZone then
|
||||
if IsControlJustPressed(0, 38) then
|
||||
TriggerEvent('apartments:client:ChangeOutfit')
|
||||
exports['qb-core']:HideText()
|
||||
end
|
||||
end
|
||||
|
||||
if IsInsideLogoutZone then
|
||||
if IsControlJustPressed(0, 38) then
|
||||
TriggerEvent('apartments:client:Logout')
|
||||
exports['qb-core']:HideText()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Wait(sleep)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@ -1,90 +0,0 @@
|
||||
Apartments = {}
|
||||
Apartments.Starting = true
|
||||
Apartments.SpawnOffset = 30
|
||||
Apartments.Locations = {
|
||||
["apartment1"] = {
|
||||
name = "apartment1",
|
||||
label = "South Rockford Drive",
|
||||
coords = {
|
||||
enter = vector4(-667.02, -1105.24, 14.63, 242.32),
|
||||
},
|
||||
polyzoneBoxData = {
|
||||
heading = 245,
|
||||
minZ = 13.5,
|
||||
maxZ = 16.0,
|
||||
debug = false,
|
||||
length = 1,
|
||||
width = 3,
|
||||
distance = 2.0,
|
||||
created = false
|
||||
}
|
||||
},
|
||||
["apartment2"] = {
|
||||
name = "apartment2",
|
||||
label = "Morningwood Blvd",
|
||||
coords = {
|
||||
enter = vector4(-1288.52, -430.51, 35.15, 124.81),
|
||||
},
|
||||
polyzoneBoxData = {
|
||||
heading = 124,
|
||||
minZ = 34.0,
|
||||
maxZ = 37.0,
|
||||
debug = false,
|
||||
length = 1,
|
||||
width = 3,
|
||||
distance = 2.0,
|
||||
created = false
|
||||
}
|
||||
},
|
||||
["apartment3"] = {
|
||||
name = "apartment3",
|
||||
label = "Integrity Way",
|
||||
coords = {
|
||||
enter = vector4(269.73, -640.75, 42.02, 249.07),
|
||||
},
|
||||
polyzoneBoxData = {
|
||||
heading = 250,
|
||||
minZ = 40,
|
||||
maxZ = 43.5,
|
||||
debug = false,
|
||||
length = 1,
|
||||
width = 1,
|
||||
distance = 2.0,
|
||||
created = false
|
||||
}
|
||||
},
|
||||
["apartment4"] = {
|
||||
name = "apartment4",
|
||||
label = "Tinsel Towers",
|
||||
coords = {
|
||||
enter = vector4(-619.29, 37.69, 43.59, 181.03),
|
||||
},
|
||||
polyzoneBoxData = {
|
||||
heading = 180,
|
||||
minZ = 41.0,
|
||||
maxZ = 45.5,
|
||||
debug = false,
|
||||
length = 1,
|
||||
width = 2,
|
||||
distance = 2.0,
|
||||
created = false
|
||||
}
|
||||
},
|
||||
["apartment5"] = {
|
||||
name = "apartment5",
|
||||
label = "Fantastic Plaza",
|
||||
coords = {
|
||||
enter = vector4(291.517, -1078.674, 29.405, 270.75),
|
||||
},
|
||||
polyzoneBoxData = {
|
||||
heading = 270,
|
||||
minZ = 28.5,
|
||||
maxZ = 31.0,
|
||||
debug = false,
|
||||
length = 1,
|
||||
width = 2,
|
||||
distance = 2.0,
|
||||
created = false
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
lua54 'yes'
|
||||
author 'Kakarot'
|
||||
description 'Provides players with an apartment on server join'
|
||||
version '2.2.1'
|
||||
|
||||
shared_scripts {
|
||||
'config.lua',
|
||||
'@qb-core/shared/locale.lua',
|
||||
'locales/en.lua',
|
||||
'locales/*.lua'
|
||||
}
|
||||
|
||||
server_scripts {
|
||||
'@oxmysql/lib/MySQL.lua',
|
||||
'server/main.lua'
|
||||
}
|
||||
|
||||
client_scripts {
|
||||
'client/main.lua',
|
||||
'@PolyZone/client.lua',
|
||||
'@PolyZone/BoxZone.lua',
|
||||
'@PolyZone/CircleZone.lua',
|
||||
}
|
||||
|
||||
dependencies {
|
||||
'qb-core',
|
||||
'qb-interior',
|
||||
'qb-clothing',
|
||||
'qb-weathersync',
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Твърде далеч сте от звънеца',
|
||||
nobody_home = 'Няма никой вкъщи..',
|
||||
nobody_at_door = 'Няма никой на вратата...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Получихте апартамент',
|
||||
changed_apart = 'Преместихте се да живеете тук',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Някой е на вратата!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Опции на апартамента',
|
||||
enter = 'Влезте в апартамента',
|
||||
ring_doorbell = 'Позвънете на звънеца',
|
||||
logout = 'Отписване на героя',
|
||||
change_outfit = 'Промяна на облеклото',
|
||||
open_stash = 'Отворете скривалището',
|
||||
move_here = 'Преместете се тук',
|
||||
open_door = 'Отворете вратата',
|
||||
leave = 'Излезте от апартамента',
|
||||
close_menu = '⬅ Затваряне на менюто',
|
||||
tennants = 'Наематели',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'bg' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,33 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Jste příliš daleko od zvonku',
|
||||
nobody_home = 'Nikdo není doma..',
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Přestěhovali jste se',
|
||||
changed_apart = 'Přestěhovali jste se',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Někdo je u dveří!',
|
||||
},
|
||||
text = {
|
||||
enter = 'Vstoupit do apartmánu',
|
||||
ring_doorbell = 'Zazvonit',
|
||||
logout = 'Odhlásit se z postavy',
|
||||
change_outfit = 'Převléknout se',
|
||||
open_stash = 'Otevřít skrýš',
|
||||
move_here = 'Přestěhovat se sem',
|
||||
open_door = 'Otevřít dveře',
|
||||
leave = 'Opustit apartmán',
|
||||
close_menu = '⬅ Uzavřít Menu',
|
||||
tennants = 'Nájemníci',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'cs' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,33 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Du er for langt væk fra dørklokken',
|
||||
nobody_home = 'Der er ingen hjemme..',
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Du modtog en lejlighed',
|
||||
changed_apart = 'Du flyttede lejlighed',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Nogen ringer på døren!',
|
||||
},
|
||||
text = {
|
||||
enter = 'Gå ind i lejlighed',
|
||||
ring_doorbell = 'Ring Dørklokken',
|
||||
logout = 'Log Ud',
|
||||
change_outfit = 'Outfits',
|
||||
open_stash = 'Åben Lager',
|
||||
move_here = 'Flyt Her',
|
||||
open_door = 'Åben Dør',
|
||||
leave = 'Forlad Lejlighed',
|
||||
close_menu = '⬅ Luk Menu',
|
||||
tennants = 'Lejere',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'da' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Du bist zu weit von der Türklingel entfernt',
|
||||
nobody_home = 'Es ist niemand zu Hause..',
|
||||
nobody_at_door = 'Es ist niemand an der Tür...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Du hast ein Apartment bekommen',
|
||||
changed_apart = 'Du bist umgezogen',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Jemand ist an der Tür!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Apartment Optionen',
|
||||
enter = 'Apartment betreten',
|
||||
ring_doorbell = 'Klingeln',
|
||||
logout = 'Ausloggen',
|
||||
change_outfit = 'Outfit wechseln',
|
||||
open_stash = 'Lager öffnen',
|
||||
move_here = 'Hierher umziehen',
|
||||
open_door = 'Tür öffnen',
|
||||
leave = 'Apartment verlassen',
|
||||
close_menu = '⬅ Menü schließen',
|
||||
tennants = 'Mieter',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'de' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,32 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'You are to far away from the Doorbell',
|
||||
nobody_home = 'There is nobody home..',
|
||||
nobody_at_door = 'There is nobody at the door...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'You got a apartment',
|
||||
changed_apart = 'You moved apartments',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Someone is at the door!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Apartment Options',
|
||||
enter = 'Enter Apartment',
|
||||
ring_doorbell = 'Ring Doorbell',
|
||||
logout = 'Logout Character',
|
||||
change_outfit = 'Change Outfit',
|
||||
open_stash = 'Open Stash',
|
||||
move_here = 'Move Here',
|
||||
open_door = 'Open Door',
|
||||
leave = 'Leave Apartment',
|
||||
close_menu = '⬅ Close Menu',
|
||||
tennants = 'Tennants',
|
||||
},
|
||||
}
|
||||
|
||||
Lang = Lang or Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true
|
||||
})
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Estás muy lejos del timbre',
|
||||
nobody_home = 'No hay nadie en casa..',
|
||||
nobody_at_door = 'No hay nadie en la puerta..'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Has obtenido un apartamento',
|
||||
changed_apart = 'Te has mudado de apartamento'
|
||||
},
|
||||
info = {
|
||||
at_the_door = '¡Hay alguien en la puerta!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Opciones de apartamento',
|
||||
enter = 'Entrar al apartamento',
|
||||
ring_doorbell = 'Tocar timbre',
|
||||
logout = 'Salir de personaje',
|
||||
change_outfit = 'Cambiar ropa',
|
||||
open_stash = 'Abrir escondite',
|
||||
move_here = 'Moverse aquí',
|
||||
open_door = 'Abrir puerta',
|
||||
leave = 'Salir del apartamento',
|
||||
close_menu = '⬅ Cerrar menú',
|
||||
tennants = 'Inquilinos',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'es' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,33 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Olet liian kaukana ovikellosta',
|
||||
nobody_home = 'Kukaan ei ole kotona..',
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Ostit asunnon',
|
||||
changed_apart = 'Vaihdoit asuntoa',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Joku koputtaa ovella!',
|
||||
},
|
||||
text = {
|
||||
enter = 'Astu sisään asuntoon',
|
||||
ring_doorbell = 'Soita ovikelloa',
|
||||
logout = 'Vaihda hahmoa',
|
||||
change_outfit = 'Vaihda vaatteita',
|
||||
open_stash = 'Avaa kaappi',
|
||||
move_here = 'Muuta tänne',
|
||||
open_door = 'Avaa ovi',
|
||||
leave = 'Poistu asunnosta',
|
||||
close_menu = '⬅ Sulje valikko',
|
||||
tennants = 'Vuokralaiset',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'fi' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Vous êtes trop loin de la sonnette',
|
||||
nobody_home = 'Il n\'y a personne à la maison..',
|
||||
nobody_at_door = 'Il n\'y a personne à la porte...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Vous avez un appartement',
|
||||
changed_apart = 'Vous avez changé d\'appartement',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Quelqu\'un est à la porte !',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Options d\'appartement',
|
||||
enter = 'Entrez dans l\'appartement',
|
||||
ring_doorbell = 'Sonnette de porte',
|
||||
logout = 'Déconnexion Personnage',
|
||||
change_outfit = 'Changez de tenue',
|
||||
open_stash = 'Ouvrir le coffre',
|
||||
move_here = 'Déplacez-vous ici',
|
||||
open_door = 'Ouvrir la porte',
|
||||
leave = 'Quitter l\'appartement',
|
||||
close_menu = '⬅ Fermer le menu',
|
||||
tennants = 'Locataire',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'fr' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,33 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'თქვენ ძალიან შორს ხართ კარის ზარისგან',
|
||||
nobody_home = 'სახლში არავინაა..',
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'შენ გაქვს ბინა',
|
||||
changed_apart = 'თქვენ გადაიტანეთ ბინები',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'ვიღაც კარებთან არის!',
|
||||
},
|
||||
text = {
|
||||
enter = 'ბინაში შესვლა',
|
||||
ring_doorbell = 'დარეკეთ კარზე',
|
||||
logout = 'გამოსვლის სიმბოლო',
|
||||
change_outfit = 'შეცვალეთ ტანსაცმელი',
|
||||
open_stash = 'გახსენით სეიფი',
|
||||
move_here = 'გადაადგილება აქ',
|
||||
open_door = 'Ღია კარი',
|
||||
leave = 'ბინის დატოვება',
|
||||
close_menu = '⬅ მენიუს დახურვა',
|
||||
tennants = 'მოიჯარეები',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'ge' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Je bent te ver van de deurbel',
|
||||
nobody_home = 'Er is niemand thuis..',
|
||||
nobody_at_door = 'Er is niemand aan de deur...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Je hebt een appartement',
|
||||
changed_apart = 'Je bent verhuisd naar appartement',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Er staat iemand voor de deur!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Apartement Opties',
|
||||
enter = 'Betreed appartement',
|
||||
ring_doorbell = 'Aanbellen',
|
||||
logout = 'Karakter Uitloggen',
|
||||
change_outfit = 'Verander Outfit',
|
||||
open_stash = 'Opbergruimte Openen',
|
||||
move_here = 'Verhuis naar hier',
|
||||
open_door = 'Deur Openen',
|
||||
leave = 'Appartement Verlaten',
|
||||
close_menu = '⬅ Menu Sluiten',
|
||||
tennants = 'Huurders',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'nl' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Você está muito longe do interfone',
|
||||
nobody_home = 'Não há ninguém em casa..',
|
||||
nobody_at_door = 'Não há ninguém na porta...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Você recebeu um apartamento',
|
||||
changed_apart = 'Você mudou de apartamento',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Alguém está na porta!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Opções do Apartamento',
|
||||
enter = 'Entrar no Apartamento',
|
||||
ring_doorbell = 'Tocar a Campainha',
|
||||
logout = 'Sair do Personagem',
|
||||
change_outfit = 'Trocar de Roupa',
|
||||
open_stash = 'Abrir Esconderijo',
|
||||
move_here = 'Mover para Cá',
|
||||
open_door = 'Abrir Porta',
|
||||
leave = 'Sair do Apartamento',
|
||||
close_menu = '⬅ Fechar Menu',
|
||||
tennants = 'Inquilinos',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'pt-br' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Estás demasiado longe da campainha',
|
||||
nobody_home = 'Não está ninguém em casa..',
|
||||
nobody_at_door = 'Ninguém á porta...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Adquiriste um apartamento',
|
||||
changed_apart = 'Mudaste-te para este apartamento',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Está alguém à porta!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Menu - Apartamento',
|
||||
enter = 'Entrar No Apartamento',
|
||||
ring_doorbell = 'Tocar À Campainha',
|
||||
logout = 'Sair Da Personagem',
|
||||
change_outfit = 'Mudar de Roupa',
|
||||
open_stash = 'Abrir Baú',
|
||||
move_here = 'Mudar Para Cá',
|
||||
open_door = 'Abrir Porta',
|
||||
leave = 'Sair Do Apartamento',
|
||||
close_menu = '⬅ Fechar Menu',
|
||||
tennants = 'Moradores',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'pt' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,35 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Du är för långt ifrån dörrklockan',
|
||||
nobody_home = 'Det är ingen hemma..',
|
||||
nobody_at_door = 'Det är ingen vid dörren...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Du fick en lägenhet',
|
||||
changed_apart = 'Du bytte lägenhet',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Någon är vid dörren!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Lägenhetsalternativ',
|
||||
enter = 'Gå in i lägenheten',
|
||||
ring_doorbell = 'Ring på dörrklockan',
|
||||
logout = 'Logga ut karaktär',
|
||||
change_outfit = 'Byt kläder',
|
||||
open_stash = 'Öppna förråd',
|
||||
move_here = 'Flytta hit',
|
||||
open_door = 'Öppna dörr',
|
||||
leave = 'Lämna lägenhet',
|
||||
close_menu = '⬅ Stäng meny',
|
||||
tennants = 'Hyresgäster',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'sv' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,33 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Kapı zilinden çok uzaktasın',
|
||||
nobody_home = 'Evde kimse yok..',
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Bir daire aldın',
|
||||
changed_apart = 'Daireni taşıdın',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Kapıda birisi var!',
|
||||
},
|
||||
text = {
|
||||
enter = 'Daireye Girin',
|
||||
ring_doorbell = 'Zili Çal',
|
||||
logout = 'Oturumu Kapat',
|
||||
change_outfit = 'Kıyafet Değiştir',
|
||||
open_stash = 'Zulayı Aç',
|
||||
move_here = 'Buraya Taşın',
|
||||
open_door = 'Kapıyı Aç',
|
||||
leave = 'Apartmandan Ayrıl',
|
||||
close_menu = '⬅ Menüyü Kapat',
|
||||
tennants = 'Kiracılar',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'tr' then
|
||||
Lang = Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true,
|
||||
fallbackLang = Lang,
|
||||
})
|
||||
end
|
||||
@ -1,34 +0,0 @@
|
||||
local Translations = {
|
||||
error = {
|
||||
to_far_from_door = 'Bạn đang ở xa chuông cửa',
|
||||
nobody_home = 'Không có ai ở nhà...',
|
||||
nobody_at_door = 'Không có ai ở cửa ...'
|
||||
},
|
||||
success = {
|
||||
receive_apart = 'Bạn có một căn hộ rồi',
|
||||
changed_apart = 'Bạn đã di chuyển căn hộ',
|
||||
},
|
||||
info = {
|
||||
at_the_door = 'Ai đó đang ở ngoài cửa!',
|
||||
},
|
||||
text = {
|
||||
options = '[E] Tùy chọn căn hộ',
|
||||
enter = 'Vào căn hộ',
|
||||
ring_doorbell = 'Nhấn chuông cửa',
|
||||
logout = 'Logout nhân vật',
|
||||
change_outfit = 'Thay đổi Outfits',
|
||||
open_stash = 'Mở Kho',
|
||||
move_here = 'Chuyển căn hộ đến đây',
|
||||
open_door = 'Mở cửa',
|
||||
leave = 'Rời căn hộ',
|
||||
close_menu = '⬅ Đóng',
|
||||
tennants = 'Tennants',
|
||||
},
|
||||
}
|
||||
|
||||
if GetConvar('qb_locale', 'en') == 'vn' then
|
||||
Lang = Lang or Locale:new({
|
||||
phrases = Translations,
|
||||
warnOnMissing = true
|
||||
})
|
||||
end
|
||||
@ -1,236 +0,0 @@
|
||||
local ApartmentObjects = {}
|
||||
local QBCore = exports['qb-core']:GetCoreObject()
|
||||
|
||||
-- Functions
|
||||
|
||||
local function CreateApartmentId(type)
|
||||
local UniqueFound = false
|
||||
local AparmentId = nil
|
||||
|
||||
while not UniqueFound do
|
||||
AparmentId = tostring(math.random(1, 9999))
|
||||
local result = MySQL.query.await('SELECT COUNT(*) as count FROM apartments WHERE name = ?', { tostring(type .. AparmentId) })
|
||||
if result[1].count == 0 then
|
||||
UniqueFound = true
|
||||
end
|
||||
end
|
||||
return AparmentId
|
||||
end
|
||||
|
||||
local function GetApartmentInfo(apartmentId)
|
||||
local retval = nil
|
||||
local result = MySQL.query.await('SELECT * FROM apartments WHERE name = ?', { apartmentId })
|
||||
if result[1] ~= nil then
|
||||
retval = result[1]
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
-- Events
|
||||
|
||||
RegisterNetEvent('qb-apartments:server:SetInsideMeta', function(house, insideId, bool, isVisiting)
|
||||
local src = source
|
||||
local Player = QBCore.Functions.GetPlayer(src)
|
||||
local insideMeta = Player.PlayerData.metadata['inside']
|
||||
|
||||
if bool then
|
||||
local routeId = insideId:gsub('[^%-%d]', '')
|
||||
if not isVisiting then
|
||||
insideMeta.apartment.apartmentType = house
|
||||
insideMeta.apartment.apartmentId = insideId
|
||||
insideMeta.house = nil
|
||||
Player.Functions.SetMetaData('inside', insideMeta)
|
||||
end
|
||||
QBCore.Functions.SetPlayerBucket(src, tonumber(routeId))
|
||||
else
|
||||
insideMeta.apartment.apartmentType = nil
|
||||
insideMeta.apartment.apartmentId = nil
|
||||
insideMeta.house = nil
|
||||
|
||||
|
||||
Player.Functions.SetMetaData('inside', insideMeta)
|
||||
QBCore.Functions.SetPlayerBucket(src, 0)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-apartments:returnBucket', function()
|
||||
local src = source
|
||||
SetPlayerRoutingBucket(src, 0)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:openStash', function(CurrentApartment)
|
||||
local src = source
|
||||
exports['qb-inventory']:OpenInventory(src, CurrentApartment)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:CreateApartment', function(type, label, firstSpawn)
|
||||
local src = source
|
||||
local Player = QBCore.Functions.GetPlayer(src)
|
||||
local num = CreateApartmentId(type)
|
||||
local apartmentId = tostring(type .. num)
|
||||
label = tostring(label .. ' ' .. num)
|
||||
MySQL.insert('INSERT INTO apartments (name, type, label, citizenid) VALUES (?, ?, ?, ?)', {
|
||||
apartmentId,
|
||||
type,
|
||||
label,
|
||||
Player.PlayerData.citizenid
|
||||
})
|
||||
TriggerClientEvent('QBCore:Notify', src, Lang:t('success.receive_apart') .. ' (' .. label .. ')')
|
||||
if firstSpawn then
|
||||
TriggerClientEvent('apartments:client:SpawnInApartment', src, apartmentId, type)
|
||||
end
|
||||
TriggerClientEvent('apartments:client:SetHomeBlip', src, type)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:UpdateApartment', function(type, label)
|
||||
local src = source
|
||||
local Player = QBCore.Functions.GetPlayer(src)
|
||||
MySQL.update('UPDATE apartments SET type = ?, label = ? WHERE citizenid = ?', { type, label, Player.PlayerData.citizenid })
|
||||
TriggerClientEvent('QBCore:Notify', src, Lang:t('success.changed_apart'))
|
||||
TriggerClientEvent('apartments:client:SetHomeBlip', src, type)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:RingDoor', function(apartmentId, apartment)
|
||||
local src = source
|
||||
if ApartmentObjects[apartment].apartments[apartmentId] ~= nil and next(ApartmentObjects[apartment].apartments[apartmentId].players) ~= nil then
|
||||
for k, _ in pairs(ApartmentObjects[apartment].apartments[apartmentId].players) do
|
||||
TriggerClientEvent('apartments:client:RingDoor', k, src)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:OpenDoor', function(target, apartmentId, apartment)
|
||||
local OtherPlayer = QBCore.Functions.GetPlayer(target)
|
||||
if OtherPlayer ~= nil then
|
||||
TriggerClientEvent('apartments:client:SpawnInApartment', OtherPlayer.PlayerData.source, apartmentId, apartment)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:AddObject', function(apartmentId, apartment, offset)
|
||||
local src = source
|
||||
local Player = QBCore.Functions.GetPlayer(src)
|
||||
if ApartmentObjects[apartment] ~= nil and ApartmentObjects[apartment].apartments ~= nil and ApartmentObjects[apartment].apartments[apartmentId] ~= nil then
|
||||
ApartmentObjects[apartment].apartments[apartmentId].players[src] = Player.PlayerData.citizenid
|
||||
else
|
||||
if ApartmentObjects[apartment] ~= nil and ApartmentObjects[apartment].apartments ~= nil then
|
||||
ApartmentObjects[apartment].apartments[apartmentId] = {}
|
||||
ApartmentObjects[apartment].apartments[apartmentId].offset = offset
|
||||
ApartmentObjects[apartment].apartments[apartmentId].players = {}
|
||||
ApartmentObjects[apartment].apartments[apartmentId].players[src] = Player.PlayerData.citizenid
|
||||
else
|
||||
ApartmentObjects[apartment] = {}
|
||||
ApartmentObjects[apartment].apartments = {}
|
||||
ApartmentObjects[apartment].apartments[apartmentId] = {}
|
||||
ApartmentObjects[apartment].apartments[apartmentId].offset = offset
|
||||
ApartmentObjects[apartment].apartments[apartmentId].players = {}
|
||||
ApartmentObjects[apartment].apartments[apartmentId].players[src] = Player.PlayerData.citizenid
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:RemoveObject', function(apartmentId, apartment)
|
||||
local src = source
|
||||
if ApartmentObjects[apartment].apartments[apartmentId].players ~= nil then
|
||||
ApartmentObjects[apartment].apartments[apartmentId].players[src] = nil
|
||||
if next(ApartmentObjects[apartment].apartments[apartmentId].players) == nil then
|
||||
ApartmentObjects[apartment].apartments[apartmentId] = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('apartments:server:setCurrentApartment', function(ap)
|
||||
local Player = QBCore.Functions.GetPlayer(source)
|
||||
|
||||
if not Player then return end
|
||||
|
||||
Player.Functions.SetMetaData('currentapartment', ap)
|
||||
end)
|
||||
|
||||
-- Callbacks
|
||||
|
||||
QBCore.Functions.CreateCallback('apartments:GetAvailableApartments', function(_, cb, apartment)
|
||||
local apartments = {}
|
||||
if ApartmentObjects ~= nil and ApartmentObjects[apartment] ~= nil and ApartmentObjects[apartment].apartments ~= nil then
|
||||
for k, _ in pairs(ApartmentObjects[apartment].apartments) do
|
||||
if (ApartmentObjects[apartment].apartments[k] ~= nil and next(ApartmentObjects[apartment].apartments[k].players) ~= nil) then
|
||||
local apartmentInfo = GetApartmentInfo(k)
|
||||
apartments[k] = apartmentInfo.label
|
||||
end
|
||||
end
|
||||
end
|
||||
cb(apartments)
|
||||
end)
|
||||
|
||||
QBCore.Functions.CreateCallback('apartments:GetApartmentOffset', function(_, cb, apartmentId)
|
||||
local retval = 0
|
||||
if ApartmentObjects ~= nil then
|
||||
for k, _ in pairs(ApartmentObjects) do
|
||||
if (ApartmentObjects[k].apartments[apartmentId] ~= nil and tonumber(ApartmentObjects[k].apartments[apartmentId].offset) ~= 0) then
|
||||
retval = tonumber(ApartmentObjects[k].apartments[apartmentId].offset)
|
||||
end
|
||||
end
|
||||
end
|
||||
cb(retval)
|
||||
end)
|
||||
|
||||
QBCore.Functions.CreateCallback('apartments:GetApartmentOffsetNewOffset', function(_, cb, apartment)
|
||||
local retval = Apartments.SpawnOffset
|
||||
if ApartmentObjects ~= nil and ApartmentObjects[apartment] ~= nil and ApartmentObjects[apartment].apartments ~= nil then
|
||||
for k, _ in pairs(ApartmentObjects[apartment].apartments) do
|
||||
if (ApartmentObjects[apartment].apartments[k] ~= nil) then
|
||||
retval = ApartmentObjects[apartment].apartments[k].offset + Apartments.SpawnOffset
|
||||
end
|
||||
end
|
||||
end
|
||||
cb(retval)
|
||||
end)
|
||||
|
||||
QBCore.Functions.CreateCallback('apartments:GetOwnedApartment', function(source, cb, cid)
|
||||
if cid ~= nil then
|
||||
local result = MySQL.query.await('SELECT * FROM apartments WHERE citizenid = ?', { cid })
|
||||
if result[1] ~= nil then
|
||||
return cb(result[1])
|
||||
end
|
||||
return cb(nil)
|
||||
else
|
||||
local src = source
|
||||
local Player = QBCore.Functions.GetPlayer(src)
|
||||
local result = MySQL.query.await('SELECT * FROM apartments WHERE citizenid = ?', { Player.PlayerData.citizenid })
|
||||
if result[1] ~= nil then
|
||||
return cb(result[1])
|
||||
end
|
||||
return cb(nil)
|
||||
end
|
||||
end)
|
||||
|
||||
QBCore.Functions.CreateCallback('apartments:IsOwner', function(source, cb, apartment)
|
||||
local src = source
|
||||
local Player = QBCore.Functions.GetPlayer(src)
|
||||
if Player ~= nil then
|
||||
local result = MySQL.query.await('SELECT * FROM apartments WHERE citizenid = ?', { Player.PlayerData.citizenid })
|
||||
if result[1] ~= nil then
|
||||
if result[1].type == apartment then
|
||||
cb(true)
|
||||
else
|
||||
cb(false)
|
||||
end
|
||||
else
|
||||
cb(false)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
QBCore.Functions.CreateCallback('apartments:GetOutfits', function(source, cb)
|
||||
local src = source
|
||||
local Player = QBCore.Functions.GetPlayer(src)
|
||||
|
||||
if Player then
|
||||
local result = MySQL.query.await('SELECT * FROM player_outfits WHERE citizenid = ?', { Player.PlayerData.citizenid })
|
||||
if result[1] ~= nil then
|
||||
cb(result)
|
||||
else
|
||||
cb(nil)
|
||||
end
|
||||
end
|
||||
end)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user