2026-04-15 21:50:01 +02:00

1186 lines
46 KiB
Lua

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: immer verfügbar
if v.isJobVehicle then
v.stored = 1
-- Einparken: prüfen ob irgendein Fahrzeug vom gleichen Modell in der Nähe steht
-- das in ActiveJobVehicles getrackt ist
local nearbyJobVehicle = false
for np, rawPlate in pairs(ActiveJobVehicles) do
if nearbyPlates[np] then
-- Modell des nahen Fahrzeugs prüfen
for _, veh in ipairs(GetGamePool('CVehicle')) do
if DoesEntityExist(veh) and normPlate(GetVehicleNumberPlateText(veh)) == np then
local vehModel = GetEntityModel(veh)
local jobModel = GetHashKey(v.modelname or v.plate)
if vehModel == jobModel then
nearbyJobVehicle = true
v.nearbyJobPlate = rawPlate -- echtes Kennzeichen für Einparken
end
break
end
end
end
end
v.nearby = nearbyJobVehicle
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
print('[mercyv-garage] SpawnVehicle: Garage nicht gefunden: ' .. tostring(data.garageId))
if closeGarageAfter then CloseGarage() end
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,
}
elseif g.car and g.car.spawncar then
spawn = g.car.spawncar
else
-- Fallback: 5m vor Spieler spawnen
local pedCoords = GetEntityCoords(PlayerPedId())
local heading = GetEntityHeading(PlayerPedId())
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,
}
end
if not spawn then
print('[mercyv-garage] ERROR: Keine Spawn-Koordinaten für ' .. tostring(data.garageId))
if closeGarageAfter then CloseGarage() end
return
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)
-- NUI wurde schon durch close() geschlossen - nur Fokus sicherstellen
SetNuiFocus(false, false)
exports['hex_4_hud']:HideHud(false)
GarageIsOpen = false
CurrentGarage = nil
-- 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)
-- Random-Plate tracken für Einparken-Button
local np = normPlate(data.plate)
ActiveJobVehicles[np] = data.plate
-- Entity direkt tracken damit Einparken-Erkennung funktioniert
PersistVehicles[np] = { entity = vehicle, rawPlate = 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
local garageId = CurrentGarage -- Garage-ID sichern bevor CloseGarage nil setzt
TriggerServerEvent('mercyv-garage:takeOut', data.plate, garageId, 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
-- nearbyJobPlate enthält das echte (random) Kennzeichen
local realPlate = data.nearbyJobPlate or data.plate
local targetPlate = normPlate(realPlate)
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
local vehModel = GetEntityModel(foundVeh)
TaskLeaveVehicle(ped, foundVeh, 0)
Citizen.Wait(1000)
RemoveVehicleKeys(data.plate, vehModel, foundVeh)
SetEntityAsMissionEntity(foundVeh, true, true)
DeleteEntity(foundVeh)
ActiveJobVehicles[targetPlate] = nil
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)