local QBCore = exports['qb-core']:GetCoreObject() local nearbyMachine = nil local machineData = {} local isInteracting = false -- Function to draw 3D text in the world function DrawText3D(x, y, z, text) -- Calculate distance to reduce computation when far away local playerCoords = GetEntityCoords(PlayerPedId()) local dist = #(vector3(x, y, z) - playerCoords) if dist > 5.0 then return end -- Set text properties SetTextScale(0.35, 0.35) SetTextFont(4) SetTextProportional(1) SetTextColour(255, 255, 255, 215) SetTextOutline() SetTextEntry("STRING") SetTextCentre(1) AddTextComponentString(text) SetDrawOrigin(x, y, z, 0) DrawText(0.0, 0.0) local factor = (string.len(text)) / 370 DrawRect(0.0, 0.0+0.0125, 0.017+ factor, 0.03, 0, 0, 0, 75) ClearDrawOrigin() end -- Get machine data with optimized callback handling function GetMachineData(entity) local coords = GetEntityCoords(entity) local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) -- Return cached data if available and not expired local currentTime = GetGameTimer() if machineData[entityId] and (currentTime - machineData[entityId].lastCheck < 10000) then return machineData[entityId] end -- Initialize with default values if not machineData[entityId] then machineData[entityId] = { isRegistered = false, canManage = false, lastCheck = currentTime, checking = true } else machineData[entityId].checking = true machineData[entityId].lastCheck = currentTime end -- Single callback to get all machine data at once (more efficient) QBCore.Functions.TriggerCallback('vending:server:getMachineStatus', function(status) if status then machineData[entityId].isRegistered = status.exists machineData[entityId].canManage = status.canManage else machineData[entityId].isRegistered = false machineData[entityId].canManage = false end machineData[entityId].checking = false end, coords) -- Short wait for callback to complete local timeout = 0 while machineData[entityId].checking and timeout < 20 do -- Reduced timeout Wait(5) -- Shorter wait timeout = timeout + 1 end -- If timeout reached, set checking to false to avoid deadlock if timeout >= 20 then machineData[entityId].checking = false end return machineData[entityId] end -- Clear cache periodically CreateThread(function() while true do Wait(60000) -- Clear cache every minute local currentTime = GetGameTimer() for entityId, data in pairs(machineData) do if currentTime - data.lastCheck > 60000 then machineData[entityId] = nil end end end end) -- Main thread to detect nearby vending machines CreateThread(function() while true do local playerPed = PlayerPedId() local playerCoords = GetEntityCoords(playerPed) local wait = 1000 local foundMachine = false -- Check for nearby vending machines for _, propName in ipairs(Config.VendingProps) do local hash = GetHashKey(propName) local objects = GetGamePool('CObject') for _, obj in ipairs(objects) do if GetEntityModel(obj) == hash then local objCoords = GetEntityCoords(obj) local dist = #(playerCoords - objCoords) if dist < 2.0 then wait = 0 foundMachine = true nearbyMachine = obj -- Get machine data local data = GetMachineData(obj) -- Display appropriate text based on machine status local z = objCoords.z + 1.0 if data.isRegistered then if data.canManage then DrawText3D(objCoords.x, objCoords.y, z, "[E] Kaufen | [G] Verwalten") -- Handle key presses for management if IsControlJustPressed(0, 38) and not isInteracting then -- E key isInteracting = true TriggerEvent('vending:client:openBuyMenu', {entity = obj}) Wait(500) -- Prevent multiple triggers isInteracting = false elseif IsControlJustPressed(0, 47) and not isInteracting then -- G key isInteracting = true TriggerEvent('vending:client:openOwnerMenu', {entity = obj}) Wait(500) -- Prevent multiple triggers isInteracting = false end else DrawText3D(objCoords.x, objCoords.y, z, "[E] Kaufen | [G] Aufbrechen") -- Handle key presses for buying/robbery if IsControlJustPressed(0, 38) and not isInteracting then -- E key isInteracting = true TriggerEvent('vending:client:openBuyMenu', {entity = obj}) Wait(500) -- Prevent multiple triggers isInteracting = false elseif IsControlJustPressed(0, 47) and not isInteracting then -- G key isInteracting = true TriggerEvent('vending:client:startRobbery', {entity = obj}) Wait(500) -- Prevent multiple triggers isInteracting = false end end else DrawText3D(objCoords.x, objCoords.y, z, "[E] Automaten kaufen ($" .. Config.VendingMachinePrice .. ")") -- Handle key press for buying machine if IsControlJustPressed(0, 38) and not isInteracting then -- E key isInteracting = true TriggerEvent('vending:client:buyMachine', {entity = obj}) Wait(500) -- Prevent multiple triggers isInteracting = false end end break end end end if foundMachine then break end end Wait(wait) end end) -- Event to refresh machine data when a new machine is registered RegisterNetEvent('vending:client:refreshTargets', function() -- Clear cached data machineData = {} end) -- Buy vending machine RegisterNetEvent('vending:client:buyMachine', function(data) local entity = data.entity local coords = GetEntityCoords(entity) local model = GetEntityModel(entity) local prop = nil -- Find prop name for i = 1, #Config.VendingProps do if GetHashKey(Config.VendingProps[i]) == model then prop = Config.VendingProps[i] break end end if not prop then return end lib.registerContext({ id = 'vending_buy_confirm', title = 'Verkaufsautomat kaufen', options = { { title = 'Bestätigen', description = 'Automaten für $' .. Config.VendingMachinePrice .. ' kaufen', icon = 'fas fa-check', onSelect = function() TriggerServerEvent('vending:server:registerMachine', coords, prop) -- Clear cache for this machine local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) machineData[entityId] = nil end }, { title = 'Abbrechen', description = 'Kauf abbrechen', icon = 'fas fa-times' } } }) lib.showContext('vending_buy_confirm') end) -- Open buy menu with quantity selection RegisterNetEvent('vending:client:openBuyMenu', function(data) local entity = data.entity local coords = GetEntityCoords(entity) -- Fast check using cached data local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) if machineData[entityId] and not machineData[entityId].isRegistered then QBCore.Functions.Notify('Dieser Automat ist nicht registriert!', 'error') return end QBCore.Functions.TriggerCallback('vending:server:getStashItems', function(items) if #items == 0 then QBCore.Functions.Notify('Dieser Automat ist leer!', 'error') return end local options = {} for i = 1, #items do local item = items[i] if item.amount > 0 then local itemLabel = QBCore.Shared.Items[item.name] and QBCore.Shared.Items[item.name].label or item.name table.insert(options, { title = itemLabel, description = 'Preis: $' .. item.price .. ' | Verfügbar: ' .. item.amount, icon = 'fas fa-shopping-cart', onSelect = function() openQuantityDialog(coords, item.name, item.price, item.amount, itemLabel) end }) end end if #options == 0 then QBCore.Functions.Notify('Keine Artikel verfügbar!', 'error') return end lib.registerContext({ id = 'vending_buy_menu', title = 'Verkaufsautomat', options = options }) lib.showContext('vending_buy_menu') end, coords) end) -- Open quantity dialog for buying items function openQuantityDialog(coords, itemName, price, maxAmount, itemLabel) local input = lib.inputDialog('Menge auswählen', { { type = 'number', label = itemLabel .. ' - $' .. price .. ' pro Stück', description = 'Wie viele möchtest du kaufen? (Max: ' .. maxAmount .. ')', required = true, min = 1, max = maxAmount, default = 1 } }) if input and input[1] then local amount = tonumber(input[1]) if amount > 0 and amount <= maxAmount then TriggerServerEvent('vending:server:buyItem', coords, itemName, amount) else QBCore.Functions.Notify('Ungültige Menge!', 'error') end end end -- Open owner menu RegisterNetEvent('vending:client:openOwnerMenu', function(data) local entity = data.entity local coords = GetEntityCoords(entity) -- Fast check using cached data local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) if machineData[entityId] and not machineData[entityId].canManage then QBCore.Functions.Notify('Du hast keine Berechtigung diesen Automaten zu verwalten!', 'error') return end QBCore.Functions.TriggerCallback('vending:server:getMachineByCoords', function(machine) if not machine then QBCore.Functions.Notify('Automat nicht gefunden!', 'error') return end local options = { { title = 'Inventar verwalten', description = 'Items hinzufügen/entfernen', icon = 'fas fa-box', onSelect = function() TriggerServerEvent('vending:server:openStash', coords) end }, { title = 'Preise festlegen', description = 'Verkaufspreise für Items setzen', icon = 'fas fa-tags', onSelect = function() openPriceMenu(coords) end }, { title = 'Geld abheben', description = 'Verfügbar: $' .. machine.money, icon = 'fas fa-money-bill', onSelect = function() openWithdrawMenu(coords, machine.money) end }, { title = 'Statistiken', description = 'Verkaufsstatistiken anzeigen', icon = 'fas fa-chart-bar', onSelect = function() openStatsMenu(machine) end } } -- Add manager options only for owner if machine.isOwner then table.insert(options, { title = 'Verwalter', description = 'Verwalter hinzufügen/entfernen', icon = 'fas fa-users-cog', onSelect = function() openManagersMenu(coords) end }) -- Add sell option only for owner table.insert(options, { title = 'Automaten verkaufen', description = 'Verkaufe den Automaten für ' .. math.floor(Config.VendingMachinePrice * Config.SellBackPercentage / 100) .. '$', icon = 'fas fa-dollar-sign', onSelect = function() sellVendingMachine(coords, machine.id) end }) end lib.registerContext({ id = 'vending_owner_menu', title = 'Verkaufsautomat Verwaltung', options = options }) lib.showContext('vending_owner_menu') end, coords) end) -- Function to sell the vending machine function sellVendingMachine(coords, machineId) local input = lib.inputDialog('Automaten verkaufen', { { type = 'checkbox', label = 'Bestätigen', description = 'Du erhältst ' .. math.floor(Config.VendingMachinePrice * Config.SellBackPercentage / 100) .. '$ zurück. Diese Aktion kann nicht rückgängig gemacht werden!', required = true } }) if input and input[1] then TriggerServerEvent('vending:server:sellMachine', coords, machineId) -- Clear cache for this machine local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) machineData[entityId] = nil end end -- Open price menu function openPriceMenu(coords) -- Fast check using cached data local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) if machineData[entityId] and not machineData[entityId].canManage then QBCore.Functions.Notify('Du hast keine Berechtigung diesen Automaten zu verwalten!', 'error') return end QBCore.Functions.TriggerCallback('vending:server:getStashItems', function(items) if #items == 0 then QBCore.Functions.Notify('Keine Items im Automaten!', 'error') return end local options = {} for i = 1, #items do local item = items[i] local itemLabel = QBCore.Shared.Items[item.name] and QBCore.Shared.Items[item.name].label or item.name table.insert(options, { title = itemLabel, description = 'Aktueller Preis: $' .. item.price, icon = 'fas fa-tag', onSelect = function() setPriceForItem(coords, item.name, itemLabel) end }) end lib.registerContext({ id = 'vending_price_menu', title = 'Preise festlegen', menu = 'vending_owner_menu', options = options }) lib.showContext('vending_price_menu') end, coords) end -- Set price for specific item function setPriceForItem(coords, itemName, itemLabel) local input = lib.inputDialog('Preis festlegen', { { type = 'number', label = 'Preis für ' .. itemLabel, description = 'Neuen Verkaufspreis eingeben', required = true, min = 1, max = 10000 } }) if input and input[1] then TriggerServerEvent('vending:server:setItemPrice', coords, itemName, tonumber(input[1])) end end -- Open withdraw menu function openWithdrawMenu(coords, availableMoney) -- Fast check using cached data local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) if machineData[entityId] and not machineData[entityId].canManage then QBCore.Functions.Notify('Du hast keine Berechtigung diesen Automaten zu verwalten!', 'error') return end if availableMoney <= 0 then QBCore.Functions.Notify('Kein Geld im Automaten!', 'error') return end local input = lib.inputDialog('Geld abheben', { { type = 'number', label = 'Betrag (Verfügbar: $' .. availableMoney .. ')', description = 'Wie viel möchtest du abheben?', required = true, min = 1, max = availableMoney } }) if input and input[1] then TriggerServerEvent('vending:server:withdrawMoney', coords, tonumber(input[1])) end end -- Open stats menu function openStatsMenu(machine) lib.registerContext({ id = 'vending_stats_menu', title = 'Verkaufsstatistiken', menu = 'vending_owner_menu', options = { { title = 'Gesamteinnahmen', description = '$' .. machine.money, icon = 'fas fa-dollar-sign' }, { title = 'Automat ID', description = '#' .. machine.id, icon = 'fas fa-hashtag' }, { title = 'Standort', description = 'X: ' .. math.floor(machine.coords.x) .. ' Y: ' .. math.floor(machine.coords.y), icon = 'fas fa-map-marker-alt' } } }) lib.showContext('vending_stats_menu') end -- Open managers menu function openManagersMenu(coords) -- Fast check for owner status QBCore.Functions.TriggerCallback('vending:server:isOwner', function(isOwner) if not isOwner then QBCore.Functions.Notify('Nur der Besitzer kann Verwalter verwalten!', 'error') return end -- Get current managers QBCore.Functions.TriggerCallback('vending:server:getManagers', function(managers) local options = { { title = 'Verwalter hinzufügen', description = 'Neuen Verwalter hinzufügen', icon = 'fas fa-user-plus', onSelect = function() openAddManagerMenu(coords) end } } -- Add existing managers with remove option if #managers > 0 then for i = 1, #managers do local manager = managers[i] table.insert(options, { title = manager.name, description = manager.online and 'Online' or 'Offline', icon = manager.online and 'fas fa-circle text-success' or 'fas fa-circle text-danger', onSelect = function() lib.registerContext({ id = 'manager_options', title = 'Verwalter: ' .. manager.name, menu = 'managers_menu', options = { { title = 'Entfernen', description = 'Verwalter entfernen', icon = 'fas fa-user-minus', onSelect = function() TriggerServerEvent('vending:server:removeManager', coords, manager.citizenid) Wait(500) openManagersMenu(coords) -- Refresh the menu end } } }) lib.showContext('manager_options') end }) end else table.insert(options, { title = 'Keine Verwalter', description = 'Es sind keine Verwalter vorhanden', icon = 'fas fa-info-circle', disabled = true }) end lib.registerContext({ id = 'managers_menu', title = 'Verwalter verwalten', menu = 'vending_owner_menu', options = options }) lib.showContext('managers_menu') end, coords) end, coords) end -- Open add manager menu function openAddManagerMenu(coords) -- Fast check for owner status QBCore.Functions.TriggerCallback('vending:server:isOwner', function(isOwner) if not isOwner then QBCore.Functions.Notify('Nur der Besitzer kann Verwalter hinzufügen!', 'error') return end QBCore.Functions.TriggerCallback('vending:server:getOnlinePlayers', function(players) if #players == 0 then QBCore.Functions.Notify('Keine Spieler online!', 'error') return end local options = {} for i = 1, #players do local player = players[i] table.insert(options, { title = player.name, description = 'ID: ' .. player.id, icon = 'fas fa-user', onSelect = function() TriggerServerEvent('vending:server:addManager', coords, player.id) Wait(500) openManagersMenu(coords) -- Refresh the menu end }) end lib.registerContext({ id = 'add_manager_menu', title = 'Verwalter hinzufügen', menu = 'managers_menu', options = options }) lib.showContext('add_manager_menu') end) end, coords) end -- Robbery menu RegisterNetEvent('vending:client:startRobbery', function(data) local entity = data.entity local coords = GetEntityCoords(entity) -- Fast check using cached data local entityId = tostring(coords.x) .. tostring(coords.y) .. tostring(coords.z) if machineData[entityId] then if not machineData[entityId].isRegistered then QBCore.Functions.Notify('Dieser Automat ist nicht registriert!', 'error') return elseif machineData[entityId].canManage then QBCore.Functions.Notify('Du kannst deinen eigenen Automaten nicht aufbrechen!', 'error') return end end lib.registerContext({ id = 'vending_robbery_confirm', title = 'Verkaufsautomat aufbrechen', options = { { title = 'Aufbrechen', description = 'Versuche den Automaten aufzubrechen', icon = 'fas fa-mask', onSelect = function() TriggerServerEvent('vending:server:startRobbery', coords) end }, { title = 'Abbrechen', description = 'Aufbruch abbrechen', icon = 'fas fa-times' } } }) lib.showContext('vending_robbery_confirm') end) -- Start robbery animation and progress RegisterNetEvent('vending:client:startRobbery', function(coords) local playerPed = PlayerPedId() local robberyTime = 10000 -- 10 seconds -- Animation RequestAnimDict('anim@heists@fleeca_bank@drilling') while not HasAnimDictLoaded('anim@heists@fleeca_bank@drilling') do Wait(100) end TaskPlayAnim(playerPed, 'anim@heists@fleeca_bank@drilling', 'drill_straight_idle', 8.0, -8.0, -1, 1, 0, false, false, false) -- Progress bar if lib.progressBar then local success = lib.progressBar({ duration = robberyTime, label = 'Automat aufbrechen...', useWhileDead = false, canCancel = true, disable = { car = true, move = true, combat = true } }) ClearPedTasks(playerPed) TriggerServerEvent('vending:server:completeRobbery', coords, success) else -- Fallback without progress bar Wait(robberyTime) ClearPedTasks(playerPed) TriggerServerEvent('vending:server:completeRobbery', coords, true) end end) -- Police alert with ox_lib notification and blinking blip RegisterNetEvent('vending:client:policeAlert', function(alertData) -- Extract data local coords = alertData.coords local locationName = alertData.locationName -- Create a blinking blip local blip = AddBlipForCoord(coords.x, coords.y, coords.z) SetBlipSprite(blip, 161) -- Robbery icon SetBlipColour(blip, 1) -- Red color SetBlipScale(blip, 1.2) SetBlipAsShortRange(blip, false) -- Make the blip flash SetBlipFlashes(blip, true) SetBlipFlashInterval(blip, 200) -- Flash interval in milliseconds -- Set blip name BeginTextCommandSetBlipName("STRING") AddTextComponentString("Verkaufsautomat Aufbruch") EndTextCommandSetBlipName(blip) -- Create route to the robbery SetBlipRoute(blip, true) SetBlipRouteColour(blip, 1) -- Red route -- Show ox_lib notification if lib and lib.notify then lib.notify({ title = 'Verkaufsautomat Aufbruch', description = 'Ein Verkaufsautomat wird aufgebrochen bei ' .. locationName, type = 'error', icon = 'fas fa-mask', position = 'top-right', duration = 8000 }) else -- Fallback to QBCore notification if ox_lib is not available QBCore.Functions.Notify('Verkaufsautomat Aufbruch gemeldet: ' .. locationName, 'error', 8000) end -- Play alert sound PlaySound(-1, "Lose_1st", "GTAO_FM_Events_Soundset", 0, 0, 1) -- Remove blip after 5 minutes SetTimeout(300000, function() RemoveBlip(blip) end) end) -- Management menu (alternative opening method) RegisterNetEvent('vending:client:openManagement', function(machine) -- Fast check for management permissions QBCore.Functions.TriggerCallback('vending:server:canManage', function(canManage) if not canManage then QBCore.Functions.Notify('Du hast keine Berechtigung diesen Automaten zu verwalten!', 'error') return end lib.registerContext({ id = 'vending_management', title = 'Verkaufsautomat #' .. machine.id, options = { { title = 'Inventar öffnen', description = 'Items hinzufügen oder entfernen', icon = 'fas fa-box', onSelect = function() TriggerServerEvent('vending:server:openStash', machine.coords) end }, { title = 'Einnahmen: $' .. machine.money, description = 'Geld abheben', icon = 'fas fa-money-bill', onSelect = function() openWithdrawMenu(machine.coords, machine.money) end } } }) lib.showContext('vending_management') end, machine.coords) end) -- Debug command to check props RegisterCommand('checkvendingprops', function() local playerPed = PlayerPedId() local playerCoords = GetEntityCoords(playerPed) local foundProps = 0 for _, propName in ipairs(Config.VendingProps) do local hash = GetHashKey(propName) local objects = GetGamePool('CObject') print("Checking for prop: " .. propName .. " (Hash: " .. hash .. ")") for _, obj in ipairs(objects) do if GetEntityModel(obj) == hash then local objCoords = GetEntityCoords(obj) local dist = #(playerCoords - objCoords) if dist < 30.0 then foundProps = foundProps + 1 print("Found " .. propName .. " at distance: " .. dist) -- Add a temporary blip local blip = AddBlipForEntity(obj) SetBlipSprite(blip, 1) SetBlipColour(blip, 2) SetBlipScale(blip, 0.8) BeginTextCommandSetBlipName("STRING") AddTextComponentString(propName) EndTextCommandSetBlipName(blip) -- Remove blip after 10 seconds SetTimeout(10000, function() RemoveBlip(blip) end) end end end end QBCore.Functions.Notify('Found ' .. foundProps .. ' vending machines nearby', 'primary') end, false) -- Debug commands RegisterCommand('vendingdebug', function() local playerPed = PlayerPedId() local coords = GetEntityCoords(playerPed) QBCore.Functions.TriggerCallback('vending:server:getMachineByCoords', function(machine) if machine then print('Machine found:', json.encode(machine)) QBCore.Functions.Notify('Machine data logged to console', 'primary') else print('No machine found at current location') QBCore.Functions.Notify('No machine found here', 'error') end end, coords) end, false) -- Clear cache command for debugging RegisterCommand('clearvendingcache', function() machineData = {} QBCore.Functions.Notify('Vending machine cache cleared', 'success') end, false) -- Event handler for when player loads RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() -- Clear cache when player loads machineData = {} end) -- Event handler for when player unloads RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() -- Clear cache when player unloads machineData = {} end) -- Event handler for resource start AddEventHandler('onResourceStart', function(resourceName) if resourceName == GetCurrentResourceName() then -- Clear cache when resource starts machineData = {} end end) -- Event handler for resource stop AddEventHandler('onResourceStop', function(resourceName) if resourceName == GetCurrentResourceName() then -- Nothing to do here, but good to have for completeness end end)