local function normPlate(p) return string.lower(string.gsub(p or '', '%s+', '')) end -- ============================================================ -- mercyv-garage | client/main.lua -- Enthält integriertes Vehicle-Persist System -- ============================================================ local Framework = nil local Garages = {} local NpcEntities = {} local BlipEntities = {} local GarageIsOpen = false local CurrentGarage = nil local PreviewVeh = nil local GarageCam = nil -- Persist -- Persist: Cache für eigene Fahrzeuge die draußen sind local MyOutsidePlates = {} local PersistVehicles = {} -- [normPlate] = {entity=veh, rawPlate="XYZ 123"} local ActiveJobVehicles = {} -- [normPlate] = rawPlate, ausgeparkte Job-Fahrzeuge -- MyOutsidePlates: [normPlate] = rawPlate (Original aus DB) local function UpdateMyOutsidePlates(vehicles) MyOutsidePlates = {} for _, v in ipairs(vehicles) do if v.stored == 0 then MyOutsidePlates[normPlate(v.plate)] = v.plate -- raw plate speichern end end end local function AddToMyPlates(plate) MyOutsidePlates[normPlate(plate)] = plate end local function RemoveFromMyPlates(plate) MyOutsidePlates[normPlate(plate)] = nil end local OutsidePlates = {} -- [normPlate] = true local PropsQueue = {} -- [netId] = true (Props bereits angefordert) -- ────────────────────────────────────────────────────────────── -- Framework -- ────────────────────────────────────────────────────────────── Citizen.CreateThread(function() Framework = GetFrameworkObject() end) -- normPlate weiter oben definiert -- ────────────────────────────────────────────────────────────── -- Garagen empfangen & NPCs/Blips aktualisieren -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:syncGarages', function(data) Garages = data or {} local cnt = 0 for _ in pairs(Garages) do cnt = cnt + 1 end print(string.format('[mercyv-garage] syncGarages empfangen: %d Garagen', cnt)) SpawnAllNpcs() UpdateAllBlips() end) -- ────────────────────────────────────────────────────────────── -- Robuste Initialisierung: wartet bis Spieler wirklich ingame ist -- ────────────────────────────────────────────────────────────── local clientReadySent = false local persistReceived = false -- verhindert doppeltes Spawn local function SendClientReady() if clientReadySent then return end clientReadySent = true print('[mercyv-garage] Sende clientReady an Server...') TriggerServerEvent('mercyv-garage:clientReady') end -- Methode 1: via onClientResourceStart Citizen.CreateThread(function() while not NetworkIsPlayerActive(PlayerId()) do Citizen.Wait(500) end Citizen.Wait(2000) SendClientReady() -- Polling: solange Garagen leer sind, alle 5s nochmal anfragen Citizen.CreateThread(function() local attempts = 0 while true do Citizen.Wait(5000) local garageCount = 0 for _ in pairs(Garages) do garageCount = garageCount + 1 end if garageCount == 0 and attempts < 8 then attempts = attempts + 1 print('[mercyv-garage] Garagen noch leer, erneuter Versuch ' .. attempts) clientReadySent = false -- reset damit nochmal gesendet werden kann SendClientReady() else break end end end) end) -- Methode 2: ESX playerSpawned (Fallback, nur wenn Garagen noch leer) AddEventHandler('playerSpawned', function() Citizen.CreateThread(function() Citizen.Wait(3000) local garageCount = 0 for _ in pairs(Garages) do garageCount = garageCount + 1 end if garageCount == 0 then print('[mercyv-garage] playerSpawned Fallback: sende clientReady') clientReadySent = false SendClientReady() end -- Kein zweites clientReady wenn Garagen schon da sind end) end) -- ────────────────────────────────────────────────────────────── -- NPC Verwaltung -- ────────────────────────────────────────────────────────────── function SpawnAllNpcs() for id, ped in pairs(NpcEntities) do if DoesEntityExist(ped) then DeleteEntity(ped) end end NpcEntities = {} for id, g in pairs(Garages) do if g.npc and g.npc.npc then local n = g.npc.npc SpawnNpc(id, n.x, n.y, n.z, n.w, g.npc.npcModel or "a_m_m_prolhost_01") end end end function SpawnNpc(id, x, y, z, heading, model) local hash = GetHashKey(model) RequestModel(hash) local t = GetGameTimer() + 5000 while not HasModelLoaded(hash) do Citizen.Wait(100) if GetGameTimer() > t then return end end -- Z - 1.0 damit NPC auf dem Boden steht (nicht schwebt) local ped = CreatePed(4, hash, x, y, z - 1.0, heading or 0.0, false, true) -- Kurz warten dann auf Boden setzen Citizen.Wait(200) PlaceObjectOnGroundProperly(ped) SetEntityInvincible(ped, true) SetBlockingOfNonTemporaryEvents(ped, true) FreezeEntityPosition(ped, true) SetPedFleeAttributes(ped, 0, false) SetModelAsNoLongerNeeded(hash) NpcEntities[id] = ped end -- ────────────────────────────────────────────────────────────── -- Blip Verwaltung -- ────────────────────────────────────────────────────────────── function UpdateAllBlips() -- Alte Blips entfernen for _, blip in pairs(BlipEntities) do if DoesBlipExist(blip) then RemoveBlip(blip) end end BlipEntities = {} if not Config.Blip then print('[mercyv-garage] Blips deaktiviert (Config.Blip = false)') return end -- Spieler-Job für Job-Garagen local playerJob = nil if Framework and Config.Framework == 'esx' then local pd = Framework.GetPlayerData() playerJob = pd and pd.job and pd.job.name or nil end local cnt = 0 for id, g in pairs(Garages) do if not (g.npc and g.npc.npc) then goto skipBlip end local n = g.npc.npc -- Job-Garage: nur für passenden Job if g.garage == 'jobgarage' then if not playerJob or g.access == 'none' or playerJob ~= g.access then goto skipBlip end local blip = AddBlipForCoord(n.x, n.y, n.z) SetBlipSprite(blip, 357) SetBlipColour(blip, (g.blip and g.blip.blipColour) or 3) SetBlipScale(blip, 0.6) SetBlipAsShortRange(blip, true) SetBlipDisplay(blip, 6) -- Karte + Minimap, KEIN Legendeneintrag BeginTextCommandSetBlipName("STRING") AddTextComponentSubstringPlayerName(g.label or "Garage") EndTextCommandSetBlipName(blip) BlipEntities[id] = blip cnt = cnt + 1 goto skipBlip end -- Normale Garage: immer anzeigen (außer Impound hat eigenes Icon) if true then local blip = AddBlipForCoord(n.x, n.y, n.z) SetBlipSprite(blip, 357) SetBlipColour(blip, (g.blip and g.blip.blipColour) or 3) SetBlipScale(blip, 0.6) SetBlipAsShortRange(blip, true) SetBlipDisplay(blip, 6) -- Karte + Minimap, KEIN Legendeneintrag BeginTextCommandSetBlipName("STRING") AddTextComponentSubstringPlayerName(g.label or "Garage") EndTextCommandSetBlipName(blip) BlipEntities[id] = blip cnt = cnt + 1 end ::skipBlip:: end print(string.format('[mercyv-garage] %d Blips gesetzt.', cnt)) end -- ────────────────────────────────────────────────────────────── -- E-Interaktion NPC → Garage öffnen -- ────────────────────────────────────────────────────────────── Citizen.CreateThread(function() local hintShown = false local nearGarage = nil while true do -- Abstandsprüfung: gedrosselt (alle 300ms) local ped = PlayerPedId() local pos = GetEntityCoords(ped) nearGarage = nil for id, g in pairs(Garages) do if g.npc and g.npc.npc then local n = g.npc.npc local dist = #(pos - vector3(n.x, n.y, n.z)) if dist < 5.0 then nearGarage = id break end end end if nearGarage and not GarageIsOpen then if not hintShown then hintShown = true exports['hex_4_hud']:ShowHelpNotify("Garage öffnen", "E") end -- Jeden Frame auf E prüfen wenn in der Nähe Citizen.Wait(0) if IsControlJustPressed(0, 38) then OpenGarage(nearGarage) end else if hintShown then exports['hex_4_hud']:HideHelpNotify() hintShown = false end Citizen.Wait(300) -- Weit weg: selten prüfen end end end) -- ────────────────────────────────────────────────────────────── -- Einpark-Marker + Interaktion -- ────────────────────────────────────────────────────────────── Citizen.CreateThread(function() local hintShown = false local nearPark = nil while true do local sleep = 1500 local ped = PlayerPedId() local pos = GetEntityCoords(ped) nearPark = nil if not GarageIsOpen and IsPedInAnyVehicle(ped, false) then for id, g in pairs(Garages) do local t = g.garage if t ~= "impound" and t ~= "impoundboat" and t ~= "impoundplane" then if g.car and g.car.garage then local gp = g.car.garage local dist = #(pos - vector3(gp.x, gp.y, gp.z)) if dist < Config.ParkRadius * 2 then sleep = 50 if dist < Config.ParkRadius then nearPark = id end end end end end end if nearPark and not GarageIsOpen then if not hintShown then hintShown = true exports['hex_4_hud']:ShowHelpNotify("Fahrzeug einparken", "E") end if IsControlJustPressed(0, 38) then ParkVehicle(nearPark) end else if hintShown then exports['hex_4_hud']:HideHelpNotify() hintShown = false end end Citizen.Wait(sleep) end end) -- ────────────────────────────────────────────────────────────── -- Garage öffnen / schließen -- ────────────────────────────────────────────────────────────── function OpenGarage(garageId) if GarageIsOpen then return end local g = Garages[garageId] if not g then return end GarageIsOpen = true CurrentGarage = garageId exports['hex_4_hud']:HideHud(true) SetNuiFocus(true, true) TriggerServerEvent('mercyv-garage:getVehicles', garageId) end function CloseGarage() GarageIsOpen = false CurrentGarage = nil SetNuiFocus(false, false) exports['hex_4_hud']:HideHud(false) if PreviewVeh and DoesEntityExist(PreviewVeh) then DeleteEntity(PreviewVeh) PreviewVeh = nil end SendNUIMessage({ action = "CLOSE" }) end -- ────────────────────────────────────────────────────────────── -- Fahrzeuge vom Server → NUI senden -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:receiveVehicles', function(vehicles, garageId) local g = Garages[garageId] if not g then return end -- Outside-Plates aus Fahrzeug-Liste cachen UpdateMyOutsidePlates(vehicles) local nuiVehicles = {} for _, v in ipairs(vehicles) do local modelname = v.modelname or '' local modelHash = tonumber(modelname) local displayName if modelHash then displayName = GetDisplayNameFromVehicleModel(modelHash) else displayName = GetDisplayNameFromVehicleModel(modelname) end if not displayName or displayName == "CARNOTFOUND" or displayName == '' then displayName = v.isJobVehicle and (v.label or v.modelname or 'Dienstfahrzeug') or (modelname ~= '' and modelname or 'Fahrzeug') end table.insert(nuiVehicles, { plate = v.plate, props = v.props, modelname = v.modelname, stored = v.stored, parking = v.parking, favorite = v.favorite, vehClass = v.vehClass or 0, carname = displayName, carimage = displayName, }) end local playerName = '' if Framework and Config.Framework == 'esx' then local pd = Framework.GetPlayerData() playerName = pd and pd.name or '' end -- Fahrzeuge in 30m Umkreis scannen (für Einparken-Button) local nearbyPlates = {} local ped = PlayerPedId() local pedPos = GetEntityCoords(ped) for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) and not IsEntityDead(veh) then local vehPos = GetEntityCoords(veh) local dist = #(pedPos - vehPos) if dist <= 30.0 then local plate = normPlate(GetVehicleNumberPlateText(veh)) if plate ~= '' then nearbyPlates[plate] = true end end end end -- Fahrzeuge markieren die in der Nähe sind for _, v in ipairs(nuiVehicles) do v.nearby = nearbyPlates[normPlate(v.plate)] == true -- Job-Fahrzeuge: "draußen" wenn in ActiveJobVehicles if v.isJobVehicle then v.stored = ActiveJobVehicles[normPlate(v.plate)] and 0 or 1 v.nearby = nearbyPlates[normPlate(v.plate)] == true end end SendNUIMessage({ action = "OPEN", vehicles = nuiVehicles, garageId = garageId, garageType = g.garage, playerName = playerName, impound = Config.Impound, impoundPrice = Config.ImpoundPrice, }) end) -- ────────────────────────────────────────────────────────────── -- Fahrzeug einparken -- ────────────────────────────────────────────────────────────── function ParkVehicle(garageId) local ped = PlayerPedId() local vehicle = GetVehiclePedIsIn(ped, false) if not DoesEntityExist(vehicle) then return end local plate = string.gsub(GetVehicleNumberPlateText(vehicle), '%s+', '') local class = GetVehicleClass(vehicle) local g = Garages[garageId] if not g then return end local allowed = Config.AllowedClasses[g.garage] or {} if not allowed[class] then Config.ClientNotification(Config.Notify.WRONG_CLASS, "error") return end local props if Config.Framework == 'esx' and Framework then props = json.encode(Framework.Game.GetVehicleProperties(vehicle)) else props = json.encode({ model = GetEntityModel(vehicle), plate = plate }) end RemoveVehicleKeys(plate, GetEntityModel(vehicle), vehicle) TaskLeaveVehicle(ped, vehicle, 0) Citizen.Wait(1500) DeleteEntity(vehicle) -- Aus Persist-Liste entfernen OutsidePlates[normPlate(plate)] = nil RemoveFromMyPlates(plate) TriggerServerEvent('mercyv-garage:parkIn', plate, garageId, props, class) end -- ────────────────────────────────────────────────────────────── -- Fahrzeug spawnen (nach TakeOut) -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:doSpawn', function(data) SpawnVehicle(data, true) end) function SpawnVehicle(data, closeGarageAfter) local g = Garages[data.garageId] if not g then return end local gType = g.garage or 'normal' local spawn -- Bei Impound: Fahrzeug neben dem Spieler spawnen, nicht am Abschlepphof if gType == 'impound' or gType == 'impoundboat' or gType == 'impoundplane' then local pedCoords = GetEntityCoords(PlayerPedId()) local heading = GetEntityHeading(PlayerPedId()) -- 5m vor dem Spieler local rad = math.rad(heading) spawn = { x = pedCoords.x + math.sin(-rad) * 5.0, y = pedCoords.y + math.cos(-rad) * 5.0, z = pedCoords.z, w = heading, } else spawn = g.car.spawncar end local props = type(data.props) == 'string' and json.decode(data.props) or data.props local model = nil -- Job-Fahrzeug: Modell direkt aus modelname (Modellname als String) if data.isJobVehicle then model = GetHashKey(data.modelname or 'adder') else -- Normal: aus Props (Hash-Integer) if props and props.model then model = tonumber(props.model) or GetHashKey(tostring(props.model)) end if not model or model == 0 then local mn = data.modelname or '' if mn ~= '' then model = tonumber(mn) or GetHashKey(mn) end end if not model or model == 0 then model = GetHashKey('adder') end end RequestModel(model) local t = GetGameTimer() + 8000 while not HasModelLoaded(model) do Citizen.Wait(100) if GetGameTimer() > t then Config.ClientNotification("Fahrzeug konnte nicht geladen werden.", "error") return end end -- Spawn-Platz prüfen local nearVeh = GetClosestVehicle(spawn.x, spawn.y, spawn.z, 3.0, 0, 71) if DoesEntityExist(nearVeh) then Config.ClientNotification(Config.Notify.SPAWN_BLOCKED, "error") SetModelAsNoLongerNeeded(model) TriggerServerEvent('mercyv-garage:parkIn', data.plate, data.garageId, type(data.props) == 'string' and data.props or json.encode(props), 0) return end local vehicle = CreateVehicle(model, spawn.x, spawn.y, spawn.z, spawn.w or 0.0, true, false) local tw = GetGameTimer() + 3000 while not DoesEntityExist(vehicle) do if GetGameTimer() > tw then break end Citizen.Wait(100) end SetVehicleNumberPlateText(vehicle, data.plate) if props and Config.Framework == 'esx' and Framework then Framework.Game.SetVehicleProperties(vehicle, props) end Config.SetVehicleFuel(vehicle, props and props.fuelLevel or 90) SetModelAsNoLongerNeeded(model) SetVehicleEngineOn(vehicle, true, true, false) GiveVehicleKeys(data.plate, model, vehicle) -- Job-Fahrzeug: allen Spielern mit gleichem Job Schlüssel geben if data.isJobVehicle and data.jobAccess then TriggerServerEvent('mercyv-garage:giveJobKeys', data.plate, data.jobAccess) ActiveJobVehicles[normPlate(data.plate)] = data.plate end -- In Persist-Liste aufnehmen OutsidePlates[normPlate(data.plate)] = true AddToMyPlates(data.plate) -- Sofort Position speichern (raw plate für DB-Match) local rawPlateForSave = data.plate -- original aus DB Citizen.CreateThread(function() Citizen.Wait(800) for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) then if normPlate(GetVehicleNumberPlateText(veh)) == normPlate(rawPlateForSave) then local coords = GetEntityCoords(veh) TriggerServerEvent('mercyv-garage:saveCoords', {{ plate = rawPlateForSave, -- RAW plate x = coords.x, y = coords.y, z = coords.z, heading = GetEntityHeading(veh), }}) print('[mercyv-garage Persist] Initiale Position gespeichert: ' .. rawPlateForSave) break end end end end) if closeGarageAfter then CloseGarage() end Config.ClientNotification(Config.Notify.TOOK_OUT, "success") end -- ────────────────────────────────────────────────────────────── -- ══════════ INTEGRIERTES PERSIST SYSTEM ══════════ -- ────────────────────────────────────────────────────────────── -- Außen-Kennzeichen empfangen RegisterNetEvent('mercyv-garage:outsidePlates', function(plates) OutsidePlates = {} for _, p in ipairs(plates) do OutsidePlates[p] = true end end) -- Fahrzeuge nach Restart spawnen (Server schickt Liste beim Join) RegisterNetEvent('mercyv-garage:persistSpawn', function(list) if not list or #list == 0 then return end if persistReceived then print('[mercyv-garage Persist] Duplikat-Spawn ignoriert.') return end persistReceived = true print(string.format('[mercyv-garage Persist] persistSpawn empfangen: %d Fahrzeuge', #list)) Citizen.CreateThread(function() -- Warten bis Spieler wirklich in der Welt ist (Z > -50 = nicht im Ladescreen) local waitTimeout = GetGameTimer() + 30000 repeat Citizen.Wait(500) until GetEntityCoords(PlayerPedId()).z > -50.0 or GetGameTimer() > waitTimeout Citizen.Wait(1000) -- Extra-Puffer nach Spawn for _, v in ipairs(list) do OutsidePlates[v.plate] = true AddToMyPlates(v.plate) end local spawned = {} print(string.format('[mercyv-garage Persist] Spawne %d Fahrzeuge nach Restart...', #list)) for _, v in ipairs(list) do repeat -- Vorhandene Fahrzeuge mit diesem Kennzeichen löschen PersistVehicles[normPlate(v.plate)] = nil for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) and normPlate(GetVehicleNumberPlateText(veh)) == normPlate(v.plate) then SetEntityAsMissionEntity(veh, true, true) DeleteEntity(veh) end end Citizen.Wait(200) local modelHash = nil if v.modelname and v.modelname ~= '' then local asNum = tonumber(v.modelname) if asNum then modelHash = asNum -- direkt als Integer verwenden else modelHash = GetHashKey(v.modelname) -- Modellname → Hash end end -- Fallback: aus Props JSON lesen if (not modelHash or modelHash == 0) and v.props then local ok, pd = pcall(json.decode, v.props) if ok and pd and pd.model then modelHash = tonumber(pd.model) or GetHashKey(tostring(pd.model)) end end if not modelHash or modelHash == 0 then break end RequestModel(modelHash) local tModel = GetGameTimer() + 5000 while not HasModelLoaded(modelHash) do if GetGameTimer() > tModel then SetModelAsNoLongerNeeded(modelHash) break end Citizen.Wait(100) end if not HasModelLoaded(modelHash) then break end local veh = CreateVehicle(modelHash, v.x, v.y, v.z, v.heading, true, false) local tVeh = GetGameTimer() + 3000 while not DoesEntityExist(veh) do if GetGameTimer() > tVeh then break end Citizen.Wait(100) end if DoesEntityExist(veh) then SetVehicleNumberPlateText(veh, v.plate) SetEntityAsMissionEntity(veh, true, true) if v.props then local ok, pd = pcall(json.decode, v.props) if ok and pd then if Config.Framework == 'esx' and Framework then Framework.Game.SetVehicleProperties(veh, pd) end if pd.fuelLevel then Config.SetVehicleFuel(veh, pd.fuelLevel) end end end SetModelAsNoLongerNeeded(modelHash) table.insert(spawned, v.plate) print('[mercyv-garage Persist] Gespawnt: ' .. v.plate) end until true -- repeat...until true = einmaliger Block mit break-Unterstützung Citizen.Wait(150) end if #spawned > 0 then TriggerServerEvent('mercyv-garage:persistMarkSpawned', spawned) end end) end) -- ────────────────────────────────────────────────────────────── -- Koordinaten speichern (simpel: alle Fahrzeuge des Spielers im Pool) -- ────────────────────────────────────────────────────────────── Citizen.CreateThread(function() Citizen.Wait(5000) local lastSaved = {} while true do Citizen.Wait(Config.PersistSaveInterval or 1000) -- Alle Fahrzeuge im Pool prüfen local toSave = {} local checkedPlates = {} -- Methode 1: Direkt getrackte Persist-Entities (auch wenn weit weg) for normP, data in pairs(PersistVehicles) do if DoesEntityExist(data.entity) and not IsEntityDead(data.entity) then local coords = GetEntityCoords(data.entity) local last = lastSaved[normP] local moved = not last or math.abs(coords.x - last.x) > 0.5 or math.abs(coords.y - last.y) > 0.5 or math.abs(coords.z - last.z) > 0.5 if moved then table.insert(toSave, { plate = data.rawPlate, x = coords.x, y = coords.y, z = coords.z, heading = GetEntityHeading(data.entity), }) lastSaved[normP] = { x = coords.x, y = coords.y, z = coords.z } end checkedPlates[normP] = true else -- Entity existiert nicht mehr → aus Tracking entfernen PersistVehicles[normP] = nil end end -- Methode 2: GetGamePool für normal ausgeparkte Fahrzeuge for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) and not IsEntityDead(veh) then local plate = normPlate(GetVehicleNumberPlateText(veh)) if not checkedPlates[plate] then local rawPlate = MyOutsidePlates[plate] if rawPlate then local coords = GetEntityCoords(veh) local last = lastSaved[plate] local moved = not last or math.abs(coords.x - last.x) > 0.5 or math.abs(coords.y - last.y) > 0.5 or math.abs(coords.z - last.z) > 0.5 if moved then table.insert(toSave, { plate = rawPlate, x = coords.x, y = coords.y, z = coords.z, heading = GetEntityHeading(veh), }) lastSaved[plate] = { x = coords.x, y = coords.y, z = coords.z } end end end -- if not checkedPlates end end if #toSave > 0 then TriggerServerEvent('mercyv-garage:saveCoords', toSave) end end end) -- ────────────────────────────────────────────────────────────── -- NUI Callbacks -- ────────────────────────────────────────────────────────────── RegisterNUICallback('close', function(data, cb) CloseGarage() cb({}) end) -- Admin aus der Garage heraus öffnen RegisterNUICallback('openAdminFromGarage', function(data, cb) if PreviewVeh and DoesEntityExist(PreviewVeh) then DeleteEntity(PreviewVeh) PreviewVeh = nil end -- NUI bleibt offen, Admin-Panel anzeigen SendNUIMessage({ action = "OPEN_ADMIN" }) cb({}) end) RegisterNUICallback('takeOut', function(data, cb) if not CurrentGarage then cb({}); return end TriggerServerEvent('mercyv-garage:takeOut', data.plate, CurrentGarage, data.vehClass or 0) cb({}) end) RegisterNUICallback('previewVehicle', function(data, cb) if not CurrentGarage then cb({}); return end local g = Garages[CurrentGarage] if not g or not g.car or not g.car.showcar then cb({}); return end local sc = g.car.showcar local model = GetHashKey(data.modelname or '') if PreviewVeh and DoesEntityExist(PreviewVeh) then DeleteEntity(PreviewVeh); PreviewVeh = nil end if model == 0 then cb({}); return end RequestModel(model) Citizen.CreateThread(function() local t = GetGameTimer() + 5000 while not HasModelLoaded(model) do if GetGameTimer() > t then return end Citizen.Wait(50) end PreviewVeh = CreateVehicle(model, sc.x, sc.y, sc.z, sc.w or 0.0, false, false) SetEntityInvincible(PreviewVeh, true) FreezeEntityPosition(PreviewVeh, true) if data.props then local ok, pd = pcall(json.decode, data.props) if ok and pd and Config.Framework == 'esx' and Framework then Framework.Game.SetVehicleProperties(PreviewVeh, pd) end end SetModelAsNoLongerNeeded(model) end) cb({}) end) RegisterNUICallback('setFavorite', function(data, cb) TriggerServerEvent('mercyv-garage:setFavorite', data.plate, data.value) cb({}) end) RegisterNUICallback('adminSaveGarage', function(data, cb) TriggerServerEvent('mercyv-garage:admin:saveGarage', data) cb({}) end) RegisterNUICallback('adminDeleteGarage', function(data, cb) TriggerServerEvent('mercyv-garage:admin:deleteGarage', data.id) cb({}) end) RegisterNUICallback('capturePosition', function(data, cb) local ped = PlayerPedId() local c = GetEntityCoords(ped) cb({ x = c.x, y = c.y, z = c.z, w = GetEntityHeading(ped) }) end) -- Admin-Garagen empfangen und ans NUI schicken RegisterNetEvent('mercyv-garage:admin:receiveGarages', function(list) SendNUIMessage({ action = "ADMIN_GARAGES", garages = list }) end) RegisterNUICallback('getAdminGarages', function(data, cb) -- Sofort Client-Cache zurückgeben falls vorhanden local list = {} for id, g in pairs(Garages) do table.insert(list, { id = id, label = g.label or id, type = g.garage, access = g.access or "none", npc_model = g.npc and g.npc.npcModel or "a_m_m_prolhost_01", npc_x = g.npc and g.npc.npc and g.npc.npc.x or 0, npc_y = g.npc and g.npc.npc and g.npc.npc.y or 0, npc_z = g.npc and g.npc.npc and g.npc.npc.z or 0, npc_heading = g.npc and g.npc.npc and g.npc.npc.w or 0, spawn_x = g.car and g.car.spawncar and g.car.spawncar.x or 0, spawn_y = g.car and g.car.spawncar and g.car.spawncar.y or 0, spawn_z = g.car and g.car.spawncar and g.car.spawncar.z or 0, spawn_heading = g.car and g.car.spawncar and g.car.spawncar.w or 0, park_x = g.car and g.car.garage and g.car.garage.x or 0, park_y = g.car and g.car.garage and g.car.garage.y or 0, park_z = g.car and g.car.garage and g.car.garage.z or 0, showcar_x = g.car and g.car.showcar and g.car.showcar.x, showcar_y = g.car and g.car.showcar and g.car.showcar.y, showcar_z = g.car and g.car.showcar and g.car.showcar.z, showcar_heading = g.car and g.car.showcar and g.car.showcar.w or 0, cam_x = g.camera and g.camera.x, cam_y = g.camera and g.camera.y, cam_z = g.camera and g.camera.z, cam_rot_z = g.camera and g.camera.rotationZ or -20, blip_show = g.blip and g.blip.show and 1 or 0, blip_type = g.blip and g.blip.blipType or 357, blip_colour = g.blip and g.blip.blipColour or 3, }) end if #list > 0 then -- Cache vorhanden → sofort zurückgeben cb(list) else -- Cache leer → Server fragen, Antwort kommt via ADMIN_GARAGES message TriggerServerEvent('mercyv-garage:admin:requestGarages') cb({}) end end) RegisterNUICallback('teleportToGarage', function(data, cb) if data.x then SetEntityCoords(PlayerPedId(), data.x, data.y, data.z + 0.5, false, false, false, false) if data.heading then SetEntityHeading(PlayerPedId(), data.heading) end end cb({}) end) -- ────────────────────────────────────────────────────────────── -- WICHTIGSTE PERSIST-LOGIK: Position beim Verlassen des Fahrzeugs speichern -- Zuverlässiger als Timer — läuft immer wenn Spieler aussteigt -- ────────────────────────────────────────────────────────────── local lastVehicle = nil Citizen.CreateThread(function() while true do Citizen.Wait(0) local ped = PlayerPedId() local veh = GetVehiclePedIsIn(ped, false) if veh ~= 0 then -- Spieler ist in Fahrzeug lastVehicle = veh elseif lastVehicle and lastVehicle ~= 0 then -- Spieler gerade ausgestiegen if DoesEntityExist(lastVehicle) then local plate = GetVehicleNumberPlateText(lastVehicle) local rawPlate = MyOutsidePlates[normPlate(plate)] or (PersistVehicles[normPlate(plate)] and PersistVehicles[normPlate(plate)].rawPlate) if rawPlate then local coords = GetEntityCoords(lastVehicle) local heading = GetEntityHeading(lastVehicle) TriggerServerEvent('mercyv-garage:saveCoords', {{ plate = rawPlate, x = coords.x, y = coords.y, z = coords.z, heading = heading, }}) end end lastVehicle = nil end end end) -- OutsidePlates werden jetzt direkt aus den Fahrzeugdaten gecacht -- ────────────────────────────────────────────────────────────── -- Einparken über Panel-Button (Fahrzeug in 30m Umkreis) -- ────────────────────────────────────────────────────────────── -- Job-Fahrzeug einparken RegisterNUICallback('parkJobVehicle', function(data, cb) if not data.plate then cb({}); return end local targetPlate = normPlate(data.plate) local ped = PlayerPedId() local pedPos = GetEntityCoords(ped) local foundVeh = nil for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) then if normPlate(GetVehicleNumberPlateText(veh)) == targetPlate then local dist = #(pedPos - GetEntityCoords(veh)) if dist <= 35.0 then foundVeh = veh break end end end end if not foundVeh then Config.ClientNotification("Fahrzeug nicht in der Nähe.", "error") cb({}); return end TaskLeaveVehicle(ped, foundVeh, 0) Citizen.Wait(1000) DeleteEntity(foundVeh) ActiveJobVehicles[targetPlate] = nil RemoveVehicleKeys(data.plate, GetEntityModel(foundVeh), foundVeh) TriggerServerEvent('mercyv-garage:parkJobVehicle', data.plate) CloseGarage() cb({}) end) RegisterNUICallback('parkFromPanel', function(data, cb) if not CurrentGarage or not data.plate then cb({}); return end local targetPlate = normPlate(data.plate) local ped = PlayerPedId() local pedPos = GetEntityCoords(ped) local foundVeh = nil -- Fahrzeug mit diesem Kennzeichen in der Nähe suchen for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) then local plate = normPlate(GetVehicleNumberPlateText(veh)) if plate == targetPlate then local dist = #(pedPos - GetEntityCoords(veh)) if dist <= 35.0 then foundVeh = veh break end end end end if not foundVeh then Config.ClientNotification("Fahrzeug nicht in der Nähe gefunden.", "error") cb({}) return end local g = Garages[CurrentGarage] local class = GetVehicleClass(foundVeh) local allowed = Config.AllowedClasses[g and g.garage or "normal"] or {} if not allowed[class] then Config.ClientNotification(Config.Notify.WRONG_CLASS, "error") cb({}) return end local props if Config.Framework == 'esx' and Framework then props = json.encode(Framework.Game.GetVehicleProperties(foundVeh)) else props = json.encode({ model = GetEntityModel(foundVeh), plate = data.plate }) end RemoveVehicleKeys(data.plate, GetEntityModel(foundVeh), foundVeh) TaskLeaveVehicle(ped, foundVeh, 0) Citizen.Wait(1500) DeleteEntity(foundVeh) OutsidePlates[targetPlate] = nil RemoveFromMyPlates(targetPlate) TriggerServerEvent('mercyv-garage:parkIn', data.plate, CurrentGarage, props, class) CloseGarage() cb({}) end) -- ────────────────────────────────────────────────────────────── -- Fahrzeug-Zerstörungs-Detektor -- ────────────────────────────────────────────────────────────── Citizen.CreateThread(function() while true do Citizen.Wait(2000) -- Alle getracken Persist-Fahrzeuge auf Zerstörung prüfen for np, data in pairs(PersistVehicles) do if not DoesEntityExist(data.entity) or IsEntityDead(data.entity) then -- Fahrzeug zerstört oder verschwunden print('[mercyv-garage] Fahrzeug zerstört: ' .. tostring(data.rawPlate)) TriggerServerEvent('mercyv-garage:vehicleDestroyed', data.rawPlate) PersistVehicles[np] = nil RemoveFromMyPlates(data.rawPlate) end end -- Auch normal ausgeparkte Fahrzeuge überwachen (via MyOutsidePlates) for np, rawPlate in pairs(MyOutsidePlates) do -- Suche das Fahrzeug im Pool local found = false for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) and not IsEntityDead(veh) then if normPlate(GetVehicleNumberPlateText(veh)) == np then found = true break end end end -- Wenn nicht gefunden und nicht in PersistVehicles getrackt → wahrscheinlich zerstört if not found and not PersistVehicles[np] then -- Nur melden wenn Spieler in der Nähe der letzten bekannten Position war -- (verhindert false positives durch Streaming) -- Wir prüfen ob das Fahrzeug in GetGamePool je sichtbar war -- Konservativ: nur wenn Fahrzeug aus PersistVehicles stammte end end end end) -- ────────────────────────────────────────────────────────────── -- /dv Command: Fahrzeug löschen → kommt in Impound -- ────────────────────────────────────────────────────────────── RegisterCommand('dv', function() local ped = PlayerPedId() local veh = GetVehiclePedIsIn(ped, false) if not DoesEntityExist(veh) then -- Nächstes Fahrzeug in 5m suchen veh = GetClosestVehicle(GetEntityCoords(ped), 5.0, 0, 71) end if not DoesEntityExist(veh) then Config.ClientNotification("Kein Fahrzeug in der Nähe.", "error") return end local plate = GetVehicleNumberPlateText(veh) -- Fahrzeug löschen und ins Impound senden TaskLeaveVehicle(ped, veh, 0) Citizen.Wait(500) DeleteEntity(veh) -- Aus Tracking entfernen local np = normPlate(plate) PersistVehicles[np] = nil RemoveFromMyPlates(plate) -- Server: Fahrzeug → Impound TriggerServerEvent('mercyv-garage:sendToImpound', plate) Config.ClientNotification("Fahrzeug wurde abgeschleppt.", "info") end, false)