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)