This commit is contained in:
UIRP.Hetzner 2025-04-07 01:41:12 +00:00
parent 7cedbd835c
commit 7788fc731c
4085 changed files with 430192 additions and 115651 deletions

View File

@ -24,3 +24,7 @@ set "txAdmin-hideDefaultWarning" "false"
set "txAdmin-hideDefaultScheduledRestartWarning" "false" set "txAdmin-hideDefaultScheduledRestartWarning" "false"
### 2025-04-05 21:18:19.739 ### 2025-04-05 21:18:19.739
txaEvent "configChanged" "{}" 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).\"}"

View File

@ -1 +0,0 @@
polyzone_created_zones.txt

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
![PolyZone around the prison](https://i.imgur.com/InKNaoL.jpg)
## Download
Click [here](https://github.com/mkafrin/PolyZone/releases) to go to the releases page and download the latest release
## Using PolyZone in a Script
In order to use PolyZone in your script, you must _at least_ include PolyZone's client.lua directly in your __resource.lua or fxmanifest.lua. You can do that by using FiveM's @ syntax for importing resource files:
```lua
client_scripts {
'@PolyZone/client.lua',
'your_scripts_client.lua',
}
```
This will allow you to create PolyZones in your script, but will not import other zones, such as CircleZone, BoxZone, etc. All the other zones are extra, and require their own explicit imports. Here is a `client_scripts` value that will include all the zones. Note the relative order of these imports, as the ordering is necessary! Many zones rely on each other, for example EntityZone inherits from BoxZone, and all zones inherit from PolyZone (client.lua).
```lua
client_scripts {
'@PolyZone/client.lua',
'@PolyZone/BoxZone.lua',
'@PolyZone/EntityZone.lua',
'@PolyZone/CircleZone.lua',
'@PolyZone/ComboZone.lua',
'your_scripts_client.lua'
}
```
## Documentation
For additional information on how to use PolyZone, please take a look at the [wiki](https://github.com/mkafrin/PolyZone/wiki)
## Troubleshooting and Support
For help troubleshooting issues you've encountered (that aren't in the FAQ), or to suggest new features, use the [issues page](https://github.com/mkafrin/PolyZone/issues). Just a reminder though, I do this in my free time and so there is no guarantee an issue will be fixed or a feature will be added. In lieu of my limited time, I will prioritize issues and bugs over features.
## FAQ - Frequently Asked Questions
**I'm getting the error `attempt to index a nil value` when creating a zone, what's wrong?**
> Did you include all the necessary scripts in your \_\_resource.lua or fxmanifest.lua? Remember some zones require other zones, like EntityZone.lua requires BoxZone.lua and BoxZone.lua requires client.lua.
**I'm getting no errors, but I can't see my zone in the right place when I turn on debug drawing**
> If you are using them, is minZ and maxZ set correctly? Or if you are using a CircleZone with useZ=true, is your center's Z value correct? If using a PolyZone, did you manually select all your points, or use the creation script? If you did it manually, the ordering of the points could be causing issues. Are you using the correct option to enable debug drawing? For PolyZones, you can use `debugPoly` and `debugGrid`, but for other zones, `debugPoly` is the only one that works.
**Is PolyZone faster than a distance check?**
> There's a page in the wiki for that, [here](https://github.com/mkafrin/PolyZone/wiki/Is-PolyZone-faster-than-a-distance-check%3F).
## License
**Please see the LICENSE file. That file will always overrule anything mentioned in the README.md or wiki**

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'
}

View File

@ -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)

View File

@ -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 arent loaded by default. Ive added quite a lot of places to load, based on [Mikeehs 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 Trevors trailer is handled and added a Wiki entry.
- Added a way to open or close Zancudos 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 wouldnt 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 gangs name, missions, and members pictures
- Added CEO office organizations 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 Losts 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 Losts safehouse interior
14/06/2017
- Original release
</details>

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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...
```

View File

@ -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)

View File

@ -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"

View File

@ -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"
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -1,124 +0,0 @@
# MenuV | Standalone Menu for FiveM | NUI Menu
[![N|CoreV](https://i.imgur.com/iq1llQG.jpg)](https://github.com/ThymonA/menuv)
[![Issues](https://img.shields.io/github/issues/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv/issues)
[![License](https://img.shields.io/github/license/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv/blob/master/LICENSE)
[![Forks](https://img.shields.io/github/forks/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv)
[![Stars](https://img.shields.io/github/stars/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv)
[![Discord](https://img.shields.io/badge/discord-Tigo%239999-7289da?style=for-the-badge&logo=discord)](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
:-------|:--------
![MenuV Default](https://i.imgur.com/xGagIBm.png) | ![MenuV Native](https://i.imgur.com/KSkeiQm.png)
[Default Theme](https://i.imgur.com/xGagIBm.png) | [Native Theme](https://i.imgur.com/KSkeiQm.png)

View File

@ -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}`);
});
});

View File

@ -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

View File

@ -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'
}

View File

@ -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"
}
}

View File

@ -1 +0,0 @@
1.5-beta

View File

@ -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'
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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
}

View File

@ -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)
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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;
}
}
});

View File

@ -1,18 +0,0 @@
{
"compilerOptions": {
"outDir": "./",
"module": "es6",
"strict": true,
"moduleResolution": "node",
"target": "es6",
"allowJs": true,
"lib": [
"es2017",
"DOM"
]
},
"include": [
"./**/*"
],
"exclude": []
}

View File

@ -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);
}
});

View File

@ -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>

View File

@ -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

View File

@ -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"
}
}

View File

@ -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ü"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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'"
}
}

View File

@ -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"
}
}

View File

@ -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'"
}
}

View File

@ -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

View File

@ -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/'
}
};

View File

@ -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.

View File

@ -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)

View File

@ -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'
}

View File

@ -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>

View File

@ -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();
});

View File

@ -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);
}

View File

@ -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

View File

@ -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.

View File

@ -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]

View File

@ -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

View File

@ -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'

View File

@ -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
![Inside Apartment](https://i.imgur.com/mp3XL4Y.jpg)
![Inside Apartment](https://i.imgur.com/3DH9RFw.jpg)
![Enter Apartment](https://imgur.com/1giGyt1.png)
![Stash](https://imgur.com/t6crf4c.png)
![Saved Outfits](https://imgur.com/I0YLuQA.png)
![Log Out](https://imgur.com/q1Yx3nS.png)
## 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},
}
},
}
```

View File

@ -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

View File

@ -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
}
},
}

View File

@ -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',
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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ı',
move_here = 'Buraya Taşın',
open_door = 'Kapıyı',
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

View File

@ -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

View File

@ -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