1187 lines
46 KiB
Lua
1187 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,
|
|
modelname = v.modelname or '',
|
|
})
|
|
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)
|