Files
red-valley/resources/[framework]/[addons]/qs-advancedgarages/client/custom/target/qs-radialmenu.lua
2026-03-29 21:41:17 +03:00

484 lines
18 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
if Config.UseTarget ~= 'qs-radialmenu' then
return
end
-- ============================================================
-- State
-- ============================================================
local shellExitCoords = nil
local nearestGarage = nil
-- ============================================================
-- Self-contained proximity detection
-- Maintains nearestGarage independently of the escrowed code,
-- so garage items appear even when ClosestGarage is not set.
-- ============================================================
CreateThread(function()
while true do
local playerCoords = GetEntityCoords(cache.ped)
local closest, closestDist = nil, 10.0
for k, g in pairs(Config.Garages) do
if g.coords and g.coords.menuCoords then
local mc = g.coords.menuCoords
local d = #(playerCoords - vec3(mc.x, mc.y, mc.z))
if d < closestDist then
closest = k
closestDist = d
end
end
end
nearestGarage = closest
Wait(closest and 250 or 1000)
end
end)
local function getClosestGarage()
return ClosestGarage or nearestGarage
end
-- ============================================================
-- Lifecycle hooks (same interface as ox_target.lua)
-- The escrowed code calls these at key moments.
-- ============================================================
function InitZones() end
function InitShellGarages() end
---@param coords vector4
function InitShellExit(coords)
shellExitCoords = coords
end
function RemoveShellExit()
shellExitCoords = nil
end
-- ============================================================
-- Provider: getRadialItems export
-- Called synchronously by qs-radialmenu before building menu.
-- Returns a plain table of items — no cross-resource closures.
-- ============================================================
exports('getRadialItems', function()
local items = {}
local playerCoords = GetEntityCoords(cache.ped)
local garageName = getClosestGarage()
local garage = garageName and Config.Garages[garageName] or nil
local authorized = false
if garage then
local jobOk = type(CheckGarageAuthorization) ~= 'function' or CheckGarageAuthorization(garage.jobs, garage.gangs)
authorized = jobOk and (IsGarageOwner or garage.available or IsKeyHolder)
end
-- ── 1. Open Garage (on foot, authorized)
if garage and not cache.vehicle and authorized then
if garage.type ~= 'plane' or garage.isImpound then
table.insert(items, {
id = 'radialOpenMenu',
title = 'Open Garage',
icon = 'Car',
type = 'client',
event = 'qs-radialmenu:garages:openGarage',
shouldClose = true
})
end
end
-- ── 2. Enter Garage interior (on foot, authorized)
if garage and not cache.vehicle and authorized and not garage.isImpound then
local gt = garage.type
if gt == 'plane' or (gt == 'vehicle' and (Config.EnablePublicInteriors or not garage.available)) then
table.insert(items, {
id = 'radialEnterShell',
title = 'Enter Garage',
icon = 'Warehouse',
type = 'client',
event = 'qs-radialmenu:garages:enterShell',
shouldClose = true
})
end
end
-- ── 3. Buy Garage (no owner, authorized)
if garage and not garage.isImpound and (not garage.owner or garage.owner == '') then
local jobOk = type(CheckGarageAuthorization) ~= 'function' or CheckGarageAuthorization(garage.jobs, garage.gangs)
if jobOk then
table.insert(items, {
id = 'radialBuyGarage',
title = 'Buy Garage',
icon = 'Store',
type = 'client',
event = 'qs-radialmenu:garages:buyGarage',
shouldClose = true
})
end
end
-- ── 4. Store Vehicle regular garage (in vehicle)
if cache.vehicle and not CurrentShellGarage then
local canStore = false
if type(IsNearbyJobGarage) == 'function' and IsNearbyJobGarage() then
canStore = true
elseif garage and not garage.isImpound and authorized then
local sp = garage.coords.spawnCoords
local dst = #(playerCoords - vec3(sp.x, sp.y, sp.z))
if dst <= 50.0 then
canStore = true
end
end
if canStore then
table.insert(items, {
id = 'radialStoreVehicle',
title = 'Store Vehicle',
icon = 'Car',
type = 'client',
event = 'qs-radialmenu:garages:storeVehicle',
shouldClose = true
})
end
end
-- ── 5. Shell garage options
if CurrentShellGarage and existKey then
local shellData = type(ShellGarages) == 'table' and ShellGarages[CurrentShellGarage] or nil
if shellData and shellData.takeVehicle and shellData.takeVehicle.x then
if not cache.vehicle then
table.insert(items, {
id = 'radialEnterShellGarage',
title = 'Enter Garage',
icon = 'Warehouse',
type = 'client',
event = 'qs-radialmenu:garages:enterShellGarage',
shouldClose = true
})
else
table.insert(items, {
id = 'radialStoreShellVehicle',
title = 'Store Vehicle',
icon = 'Car',
type = 'client',
event = 'qs-radialmenu:garages:storeShellVehicle',
shouldClose = true
})
end
end
end
-- ── 6. Exit Garage (shell exit or VehicleShowRooms)
local showExit = false
if shellExitCoords then
showExit = true
end
if not showExit and Config.VehicleShowRooms then
for _, types in pairs(Config.VehicleShowRooms) do
for _, v in pairs(types) do
if v.entry and v.entry.x then
local dst = #(playerCoords - vec3(v.entry.x, v.entry.y, v.entry.z))
if dst <= 5.5 then
showExit = true
break
end
end
end
if showExit then break end
end
end
if showExit then
table.insert(items, {
id = 'radialExitGarage',
title = 'Exit',
icon = 'DoorOpen',
type = 'client',
event = 'qs-radialmenu:garages:exitGarage',
shouldClose = true
})
end
-- ── 7. Recover Vehicle
if Config.Recovery and Config.Recovery.coords then
for _, v in pairs(Config.Recovery.coords) do
local dst = #(playerCoords - vec3(v.x, v.y, v.z))
if dst <= 5.0 then
table.insert(items, {
id = 'radialRecoverVehicle',
title = 'Recover $' .. (Config.Recovery.price or 0),
icon = 'Car',
type = 'client',
event = 'qs-radialmenu:garages:recoverVehicle',
shouldClose = true
})
break
end
end
end
-- ── 8. Garage Management (owner only, on foot)
if IsGarageOwner and not cache.vehicle then
table.insert(items, {
id = 'radialGarageManagement',
title = 'Management',
icon = 'Settings',
type = 'client',
event = 'qs-radialmenu:garages:management',
shouldClose = true
})
end
-- ── 9. Impound Vehicle (allowed job, on foot)
if not cache.vehicle then
local job = type(GetJobName) == 'function' and GetJobName() or nil
if job and type(IsJobAllowed) == 'function' and IsJobAllowed(job, 'impound') then
table.insert(items, {
id = 'radialImpoundVehicle',
title = 'Impound Vehicle',
icon = 'Car',
type = 'client',
event = 'qs-radialmenu:garages:impoundVehicle',
shouldClose = true
})
end
end
-- ── 10. Job Garages
if Config.JobGarages then
for k, jGarage in pairs(Config.JobGarages) do
local access = type(CheckJob) == 'function' and CheckJob(jGarage.job, jGarage.grade) or false
if access then
local mc = jGarage.coords.menuCoords
local dst = #(playerCoords - vec3(mc.x, mc.y, mc.z))
if dst <= 15.0 then
if not cache.vehicle then
table.insert(items, {
id = 'radialOpenJobGarage_' .. k,
title = 'Open Garage',
icon = 'Warehouse',
type = 'client',
event = 'qs-radialmenu:garages:openJobGarage',
shouldClose = true
})
else
table.insert(items, {
id = 'radialStoreJobVehicle_' .. k,
title = 'Store Vehicle',
icon = 'Warehouse',
type = 'client',
event = 'qs-radialmenu:garages:storeJobVehicle',
shouldClose = true
})
end
end
end
end
end
return items
end)
-- ============================================================
-- Register as an item provider once qs-radialmenu is ready
-- ============================================================
CreateThread(function()
local attempts = 0
while attempts < 60 do
local ok = pcall(function()
exports['qs-radialmenu']:RegisterItemProvider(GetCurrentResourceName())
end)
if ok then return end
attempts = attempts + 1
Wait(500)
end
end)
-- ============================================================
-- Bridge events — call escrowed functions directly
-- (mirrors ox_target onSelect callbacks)
-- Use getClosestGarage() so bridge events also benefit from
-- the self-contained proximity fallback.
-- ============================================================
RegisterNetEvent('qs-radialmenu:garages:openGarage', function()
local gName = getClosestGarage()
if not gName then return end
local garage = Config.Garages[gName]
if not garage then return end
if type(OpenGarageMenu) == 'function' then
OpenGarageMenu(gName, garage.isImpound, nil, garage.type == 'boat')
end
end)
RegisterNetEvent('qs-radialmenu:garages:enterShell', function()
local gName = getClosestGarage()
if not gName then return end
local garage = Config.Garages[gName]
if not garage then return end
if type(GotoShellGarage) == 'function' then
GotoShellGarage(gName, garage.coords.spawnCoords, garage.shell)
end
end)
RegisterNetEvent('qs-radialmenu:garages:buyGarage', function()
local gName = getClosestGarage()
if not gName then return end
local garage = Config.Garages[gName]
if not garage then return end
TriggerServerEvent('advancedgarages:buyGarage', gName, garage.price)
end)
RegisterNetEvent('qs-radialmenu:garages:storeVehicle', function()
if type(StoreVehicle) ~= 'function' then return end
local jobGarage = type(IsNearbyJobGarage) == 'function' and IsNearbyJobGarage() or nil
if jobGarage then
StoreVehicle(jobGarage, true, cache.vehicle)
return
end
local gName = getClosestGarage()
StoreVehicle(gName, false, cache.vehicle)
end)
RegisterNetEvent('qs-radialmenu:garages:enterShellGarage', function()
if not CurrentShellGarage or not existKey then return end
local shellData = type(ShellGarages) == 'table' and ShellGarages[CurrentShellGarage] or nil
if not shellData or not shellData.takeVehicle then return end
nearbyGarageType = 'vehicle'
if type(GotoGarage) == 'function' then
GotoGarage(CurrentShellGarage, vec4(shellData.takeVehicle.x, shellData.takeVehicle.y, shellData.takeVehicle.z, shellData.takeVehicle.h), shellData.shell)
end
end)
RegisterNetEvent('qs-radialmenu:garages:storeShellVehicle', function()
if not CurrentShellGarage or not existKey then return end
nearbyGarageType = 'vehicle'
if type(SaveVehicle) == 'function' then
SaveVehicle(CurrentShellGarage, true)
end
end)
RegisterNetEvent('qs-radialmenu:garages:exitGarage', function()
if type(ExitGarage) == 'function' then
ExitGarage()
end
end)
RegisterNetEvent('qs-radialmenu:garages:recoverVehicle', function()
local vehicleList = lib.callback.await('advancedgarages:getRecoveryVehicles', false)
if not vehicleList or #vehicleList == 0 then
if type(Notification) == 'function' then
Notification(i18n.t('keyholders.empty_out'), 'info')
end
return
end
if type(OpenRecoveryMenu) == 'function' then
OpenRecoveryMenu(vehicleList)
end
end)
RegisterNetEvent('qs-radialmenu:garages:management', function()
TriggerEvent('advancedgarages:client:radialGarageManagement')
end)
RegisterNetEvent('qs-radialmenu:garages:impoundVehicle', function()
TriggerEvent('advancedgarages:client:radialImpoundVehicle')
end)
RegisterNetEvent('qs-radialmenu:garages:openJobGarage', function()
if not Config.JobGarages then return end
local playerCoords = GetEntityCoords(cache.ped)
for k, jGarage in pairs(Config.JobGarages) do
local access = type(CheckJob) == 'function' and CheckJob(jGarage.job, jGarage.grade) or false
if access then
local mc = jGarage.coords.menuCoords
local dst = #(playerCoords - vec3(mc.x, mc.y, mc.z))
if dst <= 15.0 then
local job = jGarage.job or jGarage.gang
local serverVehicles = lib.callback.await('advancedgarages:getJobVehicles', false, jGarage.name, job)
local vehicleList = serverVehicles or {}
local garageIsAvailable = lib.callback.await('advancedgarages:isGarageAvailable', false, k)
if not garageIsAvailable then
if type(Notification) == 'function' then Notification(i18n.t('garage_not_available'), 'error') end
return
end
for _, veh in pairs(vehicleList) do
veh.vehicle = json.encode(veh.vehicle)
end
if jGarage.vehicles then
for _, model in ipairs(jGarage.vehicles) do
local plate = tostring(job .. math.random(111, 999))
table.insert(vehicleList, {
id = #vehicleList + 1,
vehicle = json.encode({ model = model, plate = plate }),
plate = plate,
})
end
end
TriggerServerEvent('advancedgarages:setInJobGarage', k, true)
if type(OpenGarageMenu) == 'function' then
OpenGarageMenu(k, jGarage.isImpound, vehicleList)
end
return
end
end
end
end)
RegisterNetEvent('qs-radialmenu:garages:storeJobVehicle', function()
if not Config.JobGarages or type(StoreVehicle) ~= 'function' then return end
local playerCoords = GetEntityCoords(cache.ped)
for k, jGarage in pairs(Config.JobGarages) do
local access = type(CheckJob) == 'function' and CheckJob(jGarage.job, jGarage.grade) or false
if access then
local mc = jGarage.coords.menuCoords
local dst = #(playerCoords - vec3(mc.x, mc.y, mc.z))
if dst <= 15.0 then
StoreVehicle(jGarage, true)
return
end
end
end
end)
-- ============================================================
-- Cleanup
-- ============================================================
AddEventHandler('onResourceStop', function(resourceName)
if resourceName == 'qs-advancedgarages' then
pcall(function()
exports['qs-radialmenu']:UnregisterItemProvider(GetCurrentResourceName())
end)
end
end)
-- ============================================================
-- Marker thread (mirrors ox_target lines 333-352)
-- Uses getClosestGarage() for proximity fallback.
-- ============================================================
CreateThread(function()
local function checkStoreMarker()
local sleep = 500
local gName = getClosestGarage()
if not gName then return sleep end
local garage = Config.Garages[gName]
if not garage then return sleep end
if not IsGarageOwner and not garage.available and not IsKeyHolder then return sleep end
if type(CheckGarageAuthorization) == 'function' and not CheckGarageAuthorization(garage.jobs, garage.gangs) then return sleep end
if garage.isImpound then return sleep end
if cache.vehicle then
sleep = 0
if type(DrawMarkerZone) == 'function' then
DrawMarkerZone(garage.coords.spawnCoords.x, garage.coords.spawnCoords.y, garage.coords.spawnCoords.z)
end
end
return sleep
end
while true do
Wait(checkStoreMarker())
end
end)