-- ============================================================ -- mercyv-garage | server/main.lua (komplett neu) -- ============================================================ local ESX = nil local GaragesData = {} local SpawnedPlates = {} local ActiveJobPlates = {} -- [plate] = jobName local PersistSentTo = {} local PersistDone = false -- Zufälliges Kennzeichen für Job-Fahrzeuge (ABC 123 Format) local function GenerateJobPlate() local chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ' local plate = '' for i = 1, 3 do local idx = math.random(1, #chars) plate = plate .. chars:sub(idx, idx) end plate = plate .. ' ' for i = 1, 3 do plate = plate .. tostring(math.random(0, 9)) end return plate end -- ────────────────────────────────────────────────────────────── -- Framework -- ────────────────────────────────────────────────────────────── Citizen.CreateThread(function() if Config.Framework == "esx" then if Config.NewESX then ESX = exports['es_extended']:getSharedObject() else while ESX == nil do TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) Wait(0) end end end end) local function GetPlayer(src) if Config.Framework == "esx" then return ESX and ESX.GetPlayerFromId(src) end end local function GetIdentifier(src) local xp = GetPlayer(src) return xp and xp.identifier or nil end local function GetPlayerJob(src) local xp = GetPlayer(src) return xp and xp.job and xp.job.name or nil end local function GetPlayerMoney(src, t) local xp = GetPlayer(src) if not xp then return 0 end return t == 'money' and xp.getMoney() or xp.getAccount(t).money end local function RemovePlayerMoney(src, t, amount) local xp = GetPlayer(src) if not xp then return end if t == 'money' then xp.removeMoney(amount) else xp.removeAccountMoney(t, amount) end end local function vT() return Config.Framework == "esx" and "owned_vehicles" or "player_vehicles" end local function oC() return Config.Framework == "esx" and "owner" or "citizenid" end local function pC() return Config.Framework == "esx" and "vehicle" or "mods" end -- ────────────────────────────────────────────────────────────── -- Admin check -- ────────────────────────────────────────────────────────────── local function IsAdmin(src) if IsPlayerAceAllowed(src, Config.AdminAce) then return true end if Config.Framework == "esx" and ESX then local xp = ESX.GetPlayerFromId(src) if xp then local g = xp.getGroup() if g == "admin" or g == "superadmin" then return true end end end return false end -- ────────────────────────────────────────────────────────────── -- DB Row → Garage -- ────────────────────────────────────────────────────────────── local function DBRowToGarage(row) return { id=row.id, label=row.label, garage=row.type, access=row.access, gang=row.gang or "none", blip={ show=row.blip_show==1, blipName=row.label, blipType=row.blip_type, blipColour=row.blip_colour }, npc={ npcModel=row.npc_model, npc={x=row.npc_x,y=row.npc_y,z=row.npc_z,w=row.npc_heading} }, car={ spawncar={x=row.spawn_x,y=row.spawn_y,z=row.spawn_z,w=row.spawn_heading}, garage={x=row.park_x,y=row.park_y,z=row.park_z}, showcar={x=row.showcar_x or row.spawn_x,y=row.showcar_y or row.spawn_y, z=row.showcar_z or row.spawn_z,w=row.showcar_heading or row.spawn_heading}, }, camera={x=row.cam_x or row.spawn_x+5, y=row.cam_y or row.spawn_y+5, z=row.cam_z or row.spawn_z+1, rotationX=0, rotationY=0, rotationZ=row.cam_rot_z or -20}, -- Job-Fahrzeuge (aus mercyv_job_vehicles Tabelle, wird separat geladen) cars = {}, } end -- ────────────────────────────────────────────────────────────── -- Tabellen erstellen -- ────────────────────────────────────────────────────────────── local function addColIfMissing(tbl, col, def) local r = MySQL.query.await(string.format( "SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='%s' AND COLUMN_NAME='%s'",tbl,col)) if r and r[1] and r[1].cnt == 0 then pcall(function() MySQL.query.await(string.format("ALTER TABLE `%s` ADD COLUMN `%s` %s",tbl,col,def)) end) print(string.format('[mercyv-garage] Spalte `%s`.`%s` angelegt.',tbl,col)) end end local function EnsureTables() local tbl = vT() MySQL.query.await([[CREATE TABLE IF NOT EXISTS `mercyv_garages` ( `id` VARCHAR(80) NOT NULL, `label` VARCHAR(100) NOT NULL DEFAULT 'Garage', `type` VARCHAR(20) NOT NULL DEFAULT 'normal', `access` VARCHAR(50) NOT NULL DEFAULT 'none', `gang` VARCHAR(50) NOT NULL DEFAULT 'none', `blip_show` TINYINT(1) NOT NULL DEFAULT 1, `blip_type` INT NOT NULL DEFAULT 357, `blip_colour` INT NOT NULL DEFAULT 3, `npc_model` VARCHAR(100) NOT NULL DEFAULT 'a_m_m_prolhost_01', `npc_x` FLOAT NOT NULL DEFAULT 0, `npc_y` FLOAT NOT NULL DEFAULT 0, `npc_z` FLOAT NOT NULL DEFAULT 0, `npc_heading` FLOAT NOT NULL DEFAULT 0, `spawn_x` FLOAT NOT NULL DEFAULT 0, `spawn_y` FLOAT NOT NULL DEFAULT 0, `spawn_z` FLOAT NOT NULL DEFAULT 0, `spawn_heading` FLOAT NOT NULL DEFAULT 0, `park_x` FLOAT NOT NULL DEFAULT 0, `park_y` FLOAT NOT NULL DEFAULT 0, `park_z` FLOAT NOT NULL DEFAULT 0, `showcar_x` FLOAT DEFAULT NULL, `showcar_y` FLOAT DEFAULT NULL, `showcar_z` FLOAT DEFAULT NULL, `showcar_heading` FLOAT DEFAULT 0, `cam_x` FLOAT DEFAULT NULL, `cam_y` FLOAT DEFAULT NULL, `cam_z` FLOAT DEFAULT NULL, `cam_rot_z` FLOAT DEFAULT -20, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4]]) addColIfMissing(tbl, 'veh_class', 'TINYINT DEFAULT 0') addColIfMissing(tbl, 'last_coords', 'VARCHAR(255) DEFAULT NULL') addColIfMissing(tbl, 'favorite', "VARCHAR(10) DEFAULT '0'") local count = MySQL.query.await('SELECT COUNT(*) AS cnt FROM mercyv_garages') if count and count[1] and count[1].cnt == 0 then print('[mercyv-garage] Tabelle leer – trage Standard-Garagen ein...') SeedDefaultGarages() end print('[mercyv-garage] Tabellen OK.') end -- ────────────────────────────────────────────────────────────── -- Seed Default Garagen -- ────────────────────────────────────────────────────────────── local function SeedDefaultGarages() local garages = { {"Garage A","Garage A","normal","none",1,357,3,"a_m_m_prolhost_01",214.5806,-806.8969,30.8052,336.3445,233.7616,-802.9507,30.4636,71.0069,214.9846,-790.6285,30.8301,236.39,-779.89,30.67,161.68,234.57,-785.1,30.59,-20.0}, {"Garage B","Garage B","normal","none",1,357,3,"a_m_m_prolhost_01",275.95,-344.06,45.17,165.24,292.79,-332.22,44.92,161.25,271.68,-341.61,44.92,274.63,-330.28,44.70,164.27,273.08,-335.04,44.92,-20.0}, {"Garage D","Garage D","normal","none",1,357,3,"a_m_m_prolhost_01",68.35,13.85,69.21,167.77,73.24,11.78,68.85,155.92,73.24,11.78,68.85,59.35,24.31,69.73,245.08,64.78,22.19,69.54,70.0}, {"Garage E","Garage E","normal","none",1,357,3,"a_m_m_prolhost_01",363.48,296.86,103.50,244.81,367.98,296.57,103.42,345.36,367.98,296.57,103.42,376.33,288.82,103.20,69.26,371.18,290.66,103.31,-110.0}, {"Garage F","Garage F","normal","none",1,357,3,"a_m_m_prolhost_01",-1158.51,-740.67,19.89,41.16,-1169.03,-743.49,19.63,42.38,-1169.03,-743.49,19.63,-1145.2,-759.03,18.82,39.92,-1148.57,-754.86,18.97,-140.0}, {"Garage G","Garage G","normal","none",1,357,3,"a_m_m_prolhost_01",-795.33,-2023.8,9.17,66.37,-790.11,-2022.68,8.87,58.85,-791.39,-2030.26,8.87,-763.11,-2042.28,8.91,37.29,-766.54,-2037.82,8.9,-143.0}, {"Garage H","Garage H","normal","none",1,357,3,"a_m_m_prolhost_01",-468.87,-819.67,30.52,358.04,-472.16,-812.83,30.53,179.63,-453.49,-814.23,30.58,-472.02,-800.43,30.54,183.47,-472.16,-806.15,30.54,-3.0}, {"Garage I","Garage I","normal","none",1,357,3,"a_m_m_prolhost_01",1142.38,2661.28,38.16,92.19,1137.57,2674.86,38.25,1.08,1137.59,2653.02,38.0,1121.15,2665.03,38.02,266.97,1127.68,2664.84,38.02,88.0}, {"Garage J","Garage J","normal","none",1,357,3,"a_m_m_prolhost_01",83.51,6420.3,31.76,313.17,85.93,6426.8,31.34,38.93,79.68,6417.33,31.28,112.65,6396.47,31.31,42.5,107.37,6402.14,31.33,-138.0}, {"Garage K","Boot Garage","boat","none",1,356,3,"a_m_m_prolhost_01",-717.9,-1327.46,1.6,50.86,-718.05,-1334.24,-0.44,222.71,-718.03,-1334.21,1.0,-723.7,-1329.22,-0.11,229.03,-719.57,-1332.72,1.41,50.0}, {"Garage L","Flugzeug Garage","aircraft","none",1,359,3,"a_m_m_prolhost_01",-1251.69,-3399.94,13.94,59.19,-1246.91,-3355.14,13.95,330.68,-1246.91,-3355.14,13.95,-1273.01,-3402.28,13.94,331.01,-1268.42,-3394.32,13.94,-210.0}, {"Garage M","Garage M","normal","none",1,357,3,"a_m_m_prolhost_01",271.94,-1509.32,29.18,87.30,243.22,-1502.84,29.14,222.92,243.22,-1502.84,29.14,253.68,-1511.65,29.14,260.14,256.95,-1500.42,29.14,-200.0}, {"Garage N","Garage N","normal","none",1,357,3,"a_m_m_prolhost_01",-1134.78,2682.73,18.46,132.28,-1155.64,2665.05,18.09,223.0,-1141.49,2680.13,18.09,-1157.10,2672.85,18.09,175.18,-1145.82,2670.54,19.75,-280.0}, {"Garage T","Garage T","normal","none",1,357,3,"a_m_m_prolhost_01",302.3,-189.94,61.57,73.01,288.09,-194.55,61.57,249.13,301.1,-183.12,61.59,274.64,-189.47,61.57,252.0,279.92,-191.36,61.57,61.57}, {"Impound Garage","Abschlepphof","impound","none",1,68,3,"a_m_m_prolhost_01",406.88,-1625.23,29.29,229.89,408.0,-1645.66,29.29,228.92,408.0,-1645.66,29.29,401.4,-1639.93,29.29,230.79,406.0,-1643.48,29.29,50.29}, {"Impound Boat","Abschlepphof Boot","impoundboat","none",1,357,3,"a_m_m_prolhost_01",-769.64,-1425.65,1.60,230.0,-786.56,-1424.55,-0.51,133.50,-786.56,-1424.55,-0.51,-786.56,-1424.55,-0.51,133.50,-795.95,-1436.94,3.06,322.48}, {"Impound Plane","Abschlepphof Flugzeug","impoundplane","none",1,357,3,"a_m_m_prolhost_01",-1030.27,-3016.30,13.95,339.01,-979.81,-2995.32,13.95,69.51,-979.81,-2995.32,13.95,-984.89,-3012.54,13.95,61.43,-994.19,-3008.20,13.95,236.98}, {"police","Polizei Garage","jobgarage","police",0,357,3,"ig_solomon",457.60,-977.66,21.95,87.43,449.80,-971.60,21.45,177.42,449.42,-979.14,21.45,417.47,-974.65,21.45,177.18,428.37,-974.73,21.45,-270.0}, {"ambulance","Ambulanz Garage","jobgarage","ambulance",0,357,3,"ig_solomon",-286.81,-588.56,27.78,1.87,-286.29,-576.06,27.63,85.73,-285.72,-580.70,27.63,-305.70,-567.67,27.63,296.93,-311.61,-563.86,27.63,-110.0}, } local q = [[INSERT IGNORE INTO mercyv_garages (id,label,type,access,blip_show,blip_type,blip_colour,npc_model, npc_x,npc_y,npc_z,npc_heading,spawn_x,spawn_y,spawn_z,spawn_heading, park_x,park_y,park_z,showcar_x,showcar_y,showcar_z,showcar_heading, cam_x,cam_y,cam_z,cam_rot_z) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)]] for _,g in ipairs(garages) do MySQL.update.await(q,{g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15],g[16],g[17],g[18],g[19],g[20],g[21],g[22],g[23],g[24],g[25],g[26],g[27]}) end print('[mercyv-garage] Standard-Garagen eingetragen.') end -- ────────────────────────────────────────────────────────────── -- Garagen laden -- ────────────────────────────────────────────────────────────── local function LoadGarages() local result = MySQL.query.await('SELECT * FROM mercyv_garages ORDER BY id') GaragesData = {} if result then for _,row in ipairs(result) do GaragesData[row.id] = DBRowToGarage(row) end end -- Job-Fahrzeuge aus DB laden MySQL.query.await([[CREATE TABLE IF NOT EXISTS `mercyv_job_vehicles` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `garage_id` VARCHAR(80) NOT NULL, `model` VARCHAR(100) NOT NULL, `label` VARCHAR(100) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4]]) local jvResult = MySQL.query.await('SELECT * FROM mercyv_job_vehicles') if jvResult then for _, row in ipairs(jvResult) do if not Config.JobVehicles then Config.JobVehicles = {} end if not Config.JobVehicles[row.garage_id] then Config.JobVehicles[row.garage_id] = {} end -- Prüfen ob schon in config (config hat Vorrang beim ersten Start) local exists = false for _, existing in ipairs(Config.JobVehicles[row.garage_id]) do if existing.model == row.model then exists = true; break end end if not exists then table.insert(Config.JobVehicles[row.garage_id], { model = row.model, label = row.label or row.model, grade = 0, }) end end end -- Config.JobVehicles auch initial in DB schreiben (einmalig) if Config.JobVehicles then for garageId, vehicles in pairs(Config.JobVehicles) do for _, v in ipairs(vehicles) do local exists = MySQL.query.await( 'SELECT id FROM mercyv_job_vehicles WHERE garage_id = ? AND model = ?', { garageId, v.model }) if not exists or #exists == 0 then MySQL.insert('INSERT INTO mercyv_job_vehicles (garage_id, model, label) VALUES (?, ?, ?)', { garageId, v.model, v.label or v.model }) end end end end local cnt = 0; for _ in pairs(GaragesData) do cnt=cnt+1 end print(string.format('[mercyv-garage] %d Garagen geladen.', cnt)) end -- ────────────────────────────────────────────────────────────── -- Startup -- ────────────────────────────────────────────────────────────── AddEventHandler('onResourceStart', function(res) if res ~= GetCurrentResourceName() then return end Wait(1500) EnsureTables() -- Blip-Fix VOR LoadGarages damit die Daten korrekt sind MySQL.update.await("UPDATE mercyv_garages SET blip_show=1 WHERE type NOT IN ('jobgarage','impound','impoundboat','impoundplane')") LoadGarages() local tbl = vT() -- Fahrzeuge OHNE gespeicherte Position → zurück in Garage local noPos = MySQL.query.await(string.format( "SELECT COUNT(*) AS cnt FROM `%s` WHERE stored=0 AND (last_coords IS NULL OR last_coords='')", tbl)) if noPos and noPos[1] and noPos[1].cnt > 0 then -- Fahrzeuge ohne parking → Standard-Garage setzen MySQL.update.await(string.format( "UPDATE `%s` SET stored=1 WHERE stored=0 AND (last_coords IS NULL OR last_coords='')", tbl)) -- Fahrzeuge ohne parking-Eintrag → Garage A als Fallback MySQL.update.await(string.format( "UPDATE `%s` SET parking='Garage A' WHERE stored=1 AND (parking IS NULL OR parking='')", tbl)) print(string.format('[mercyv-garage] %d Fahrzeuge ohne Position → zurück in Garage.', noPos[1].cnt)) end -- Fahrzeuge MIT gespeicherter Position → werden nach Join gespawnt local withPos = MySQL.query.await(string.format( "SELECT COUNT(*) AS cnt FROM `%s` WHERE stored=0 AND last_coords IS NOT NULL AND last_coords!=''", tbl)) local spawnCount = withPos and withPos[1] and withPos[1].cnt or 0 print(string.format('[mercyv-garage] %d Fahrzeuge mit gespeicherter Position → werden gespawnt.', spawnCount)) SpawnedPlates = {} PersistSentTo = {} PersistDone = false end) -- ────────────────────────────────────────────────────────────── -- Broadcast -- ────────────────────────────────────────────────────────────── local function BroadcastGarages() TriggerClientEvent('mercyv-garage:syncGarages', -1, GaragesData) end RegisterNetEvent('mercyv-garage:requestGarages', function() TriggerClientEvent('mercyv-garage:syncGarages', source, GaragesData) end) RegisterNetEvent('mercyv-garage:clientReady', function() local src = source -- Persist-Lock SOFORT setzen (synchron, außerhalb des Threads) local shouldSpawn = false if not PersistDone then PersistDone = true shouldSpawn = true PersistSentTo[src] = true print(string.format('[mercyv-garage Persist] Client %d wird Fahrzeuge spawnen.', src)) end Citizen.CreateThread(function() Wait(500) TriggerClientEvent('mercyv-garage:syncGarages', src, GaragesData) Wait(200) TriggerClientEvent('mercyv-garage:setAdminStatus', src, IsAdmin(src)) Wait(200) if shouldSpawn then SendPersistList(src) end -- Schlüssel für bereits aktive Job-Fahrzeuge geben Wait(1000) GiveJobKeysToPlayer(src) end) end) RegisterNetEvent('mercyv-garage:checkAdminStatus', function() TriggerClientEvent('mercyv-garage:setAdminStatus', source, IsAdmin(source)) end) -- ────────────────────────────────────────────────────────────── -- Persist: Spawn-Liste an Client -- ────────────────────────────────────────────────────────────── function SendPersistList(src) -- Alle Fahrzeuge in der Welt löschen die stored=0 sind (Persist-Fahrzeuge) -- So entstehen keine Duplikate wenn Resource neu gestartet wird local tblClean = vT() local storedOut = MySQL.query.await(string.format( "SELECT plate FROM `%s` WHERE stored=0 AND last_coords IS NOT NULL AND last_coords!=''", tblClean)) if storedOut and #storedOut > 0 then local outPlates = {} for _, row in ipairs(storedOut) do outPlates[string.lower(string.gsub(row.plate or '', '%s+', ''))] = true end local allVehs = GetAllVehicles() local deleted = 0 for _, veh in ipairs(allVehs) do if DoesEntityExist(veh) then local vPlate = string.lower(string.gsub(GetVehicleNumberPlateText(veh) or '', '%s+', '')) if outPlates[vPlate] then DeleteEntity(veh) deleted = deleted + 1 end end end if deleted > 0 then print(string.format('[mercyv-garage Persist] %d alte Persist-Fahrzeuge gelöscht.', deleted)) end end SpawnedPlates = {} local tbl = vT() local props = pC() local results = MySQL.query.await(string.format( "SELECT plate, `%s` AS props, last_coords, veh_class FROM `%s` WHERE stored=0 AND last_coords IS NOT NULL AND last_coords!=''", props, tbl)) print(string.format('[mercyv-garage Persist] Fahrzeuge mit Koordinaten: %d', results and #results or 0)) if not results or #results == 0 then return end local toSpawn = {} for _,row in ipairs(results) do local np = string.lower(string.gsub(row.plate or '','%s+','')) if not SpawnedPlates[np] then local ok, coords = pcall(json.decode, row.last_coords) if ok and coords and coords.x then local modelname = '' if row.props then local okP,pd = pcall(json.decode, row.props) if okP and pd and pd.model then modelname = tostring(pd.model) end end table.insert(toSpawn, { plate=row.plate, modelname=modelname, props=row.props, vehClass=row.veh_class or 0, x=coords.x, y=coords.y, z=coords.z, heading=coords.w or 0, }) end end end if #toSpawn > 0 then print(string.format('[mercyv-garage Persist] Sende %d Fahrzeuge an Client %d', #toSpawn, src)) TriggerClientEvent('mercyv-garage:persistSpawn', src, toSpawn) end end RegisterNetEvent('mercyv-garage:persistMarkSpawned', function(plates) for _,p in ipairs(plates) do SpawnedPlates[p] = true end PersistDone = true -- Fahrzeuge sind in der Welt, kein weiterer Spawn nötig print('[mercyv-garage Persist] Persist abgeschlossen, keine weiteren Spawns.') end) -- ────────────────────────────────────────────────────────────── -- Koordinaten speichern -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:saveCoords', function(list) if type(list) ~= 'table' then return end local tbl = vT() for _,v in ipairs(list) do if v.plate and v.x then MySQL.update.await( string.format("UPDATE `%s` SET last_coords=? WHERE plate=? AND stored=0", tbl), { json.encode({x=math.floor(v.x*100)/100, y=math.floor(v.y*100)/100, z=math.floor(v.z*100)/100, w=math.floor((v.heading or 0)*100)/100}), v.plate }) end end end) RegisterNetEvent('mercyv-garage:requestOutsidePlates', function() local src = source local tbl = vT() local r = MySQL.query.await(string.format("SELECT plate FROM `%s` WHERE stored=0", tbl)) local plates = {} if r then for _,row in ipairs(r) do plates[#plates+1] = row.plate end end TriggerClientEvent('mercyv-garage:outsidePlates', src, plates) end) -- ────────────────────────────────────────────────────────────── -- Fahrzeuge abfragen (normale + Job-Garagen) -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:getVehicles', function(garageId) local src = source local g = GaragesData[garageId] if not g then return end -- Job-Garage: Job-Fahrzeuge aus Config zurückgeben if g.garage == 'jobgarage' then local playerJob = GetPlayerJob(src) if playerJob ~= g.access then Config.ServerNotification(src, Config.Notify.NO_ACCESS, "error") return end -- Job-Fahrzeuge: erst aus GaragesData (admin-gesetzt), dann Config.JobVehicles local jobCars = {} local jobList = (Config.JobVehicles and Config.JobVehicles[g.access]) or {} for i, car in ipairs(jobList) do table.insert(jobCars, { plate = car.model, props = nil, modelname= car.model, stored = 1, parking = garageId, favorite = 0, vehClass = 0, isJobVehicle = true, label = car.label or car.model, jobGrade = car.grade or 0, }) end TriggerClientEvent('mercyv-garage:receiveVehicles', src, jobCars, garageId) return end -- Normale Garage: persönliche Fahrzeuge local identifier = GetIdentifier(src) if not identifier then return end local tbl = vT(); local owner = oC(); local props = pC() local query = string.format( "SELECT plate, `%s` AS props, stored, parking, favorite, veh_class FROM `%s` WHERE `%s`=?", props, tbl, owner) local result = MySQL.query.await(query, {identifier}) local vehicles = {} if result then for _,row in ipairs(result) do local gt = g.garage if gt=='impound' or gt=='impoundboat' or gt=='impoundplane' then -- Impound: nur eingelagerte Fahrzeuge anzeigen die explizit diesem Impound zugewiesen sind if row.parking ~= garageId or row.stored ~= 1 then goto cont end end local modelname = '' if row.props then local ok,pd = pcall(json.decode, row.props) if ok and pd and pd.model then modelname = tostring(pd.model) end end table.insert(vehicles, { plate=row.plate, props=row.props, modelname=modelname, stored=row.stored or 1, parking=row.parking or '', favorite=row.favorite or 0, vehClass=row.veh_class or 0, }) ::cont:: end end TriggerClientEvent('mercyv-garage:receiveVehicles', src, vehicles, garageId) end) -- ────────────────────────────────────────────────────────────── -- Fahrzeug holen -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:takeOut', function(plate, garageId, vehClass) local src = source local g = GaragesData[garageId] if not g then return end -- Job-Fahrzeug if g.garage == 'jobgarage' then local playerJob = GetPlayerJob(src) if playerJob ~= g.access then Config.ServerNotification(src, Config.Notify.NO_ACCESS, "error"); return end -- Einfach spawnen, kein DB-Eintrag nötig -- Zufälliges Kennzeichen generieren local jobPlate = GenerateJobPlate() -- Job-Fahrzeug tracken ActiveJobPlates[jobPlate] = g.access TriggerClientEvent('mercyv-garage:doSpawn', src, { plate=jobPlate, props=nil, modelname=plate, garageId=garageId, isJobVehicle=true, jobAccess=g.access }) return end -- Impound-Gebühr if g.garage=='impound' or g.garage=='impoundboat' or g.garage=='impoundplane' then if GetPlayerMoney(src, Config.MoneyType) < Config.ImpoundPrice then Config.ServerNotification(src, Config.Notify.NO_MONEY, "error"); return end RemovePlayerMoney(src, Config.MoneyType, Config.ImpoundPrice) Config.ServerNotification(src, Config.Notify.IMPOUND_PAID, "success") end local identifier = GetIdentifier(src) if not identifier then return end local tbl=vT(); local owner=oC(); local props=pC() local result = MySQL.query.await( string.format("SELECT plate, `%s` AS props, stored FROM `%s` WHERE plate=? AND `%s`=?", props, tbl, owner), {plate, identifier}) if not result or not result[1] then Config.ServerNotification(src, Config.Notify.NOT_OWNED, "error"); return end local row = result[1] -- last_coords wird vom Client gesetzt sobald Fahrzeug sich bewegt -- NULL = noch keine Position gespeichert (Migration setzt es auf stored=1 bei Neustart) -- parking bleibt gesetzt (wichtig für Restart-Recovery) -- Impound-Filter prüft stored=1 UND parking=garageId → stored=0 wird nicht angezeigt MySQL.update.await( string.format("UPDATE `%s` SET stored=0, veh_class=?, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner), {vehClass or 0, plate, identifier}) SpawnedPlates[string.lower(string.gsub(plate,'%s+',''))] = true local modelname = '' if row.props then local ok,pd = pcall(json.decode, row.props) if ok and pd and pd.model then modelname = tostring(pd.model) end end TriggerClientEvent('mercyv-garage:doSpawn', src, { plate=plate, props=row.props, modelname=modelname, garageId=garageId }) end) -- ────────────────────────────────────────────────────────────── -- Einparken -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:parkIn', function(plate, garageId, propsJson, vehClass) local src = source local g = GaragesData[garageId] -- Job-Fahrzeuge werden einfach gelöscht (kein DB-Eintrag) if g and g.garage == 'jobgarage' then -- Job-Fahrzeug aus Tracking entfernen ActiveJobPlates[plate] = nil Config.ServerNotification(src, Config.Notify.PARKED_IN, "success"); return end local identifier = GetIdentifier(src) if not identifier then return end local tbl=vT(); local owner=oC(); local props=pC() local r = MySQL.query.await(string.format("SELECT plate FROM `%s` WHERE plate=? AND `%s`=?",tbl,owner),{plate,identifier}) if not r or not r[1] then Config.ServerNotification(src, Config.Notify.NOT_OWNED, "error"); return end MySQL.update.await( string.format("UPDATE `%s` SET stored=1, parking=?, `%s`=?, veh_class=?, last_coords=NULL WHERE plate=? AND `%s`=?",tbl,props,owner), {garageId, propsJson, vehClass or 0, plate, identifier}) SpawnedPlates[string.lower(string.gsub(plate,'%s+',''))] = nil Config.ServerNotification(src, Config.Notify.PARKED_IN, "success") end) -- ────────────────────────────────────────────────────────────── -- Favorit -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:setFavorite', function(plate, value) local src=source; local id=GetIdentifier(src) if not id then return end local tbl=vT(); local owner=oC() MySQL.update(string.format("UPDATE `%s` SET favorite=? WHERE plate=? AND `%s`=?",tbl,owner),{value,plate,id}) end) -- ────────────────────────────────────────────────────────────── -- Admin: Garagen-Liste -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:admin:requestGarages', function() local src=source local list={} for id,g in pairs(GaragesData) 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 TriggerClientEvent('mercyv-garage:admin:receiveGarages', src, list) end) RegisterNetEvent('mercyv-garage:admin:saveGarage', function(data) local src=source if not IsAdmin(src) then Config.ServerNotification(src,Config.Notify.ADMIN_NO_PERM,"error"); return end if not data or not data.id or data.id=="" then return end MySQL.update([[INSERT INTO mercyv_garages (id,label,type,access,gang,blip_show,blip_type,blip_colour,npc_model, npc_x,npc_y,npc_z,npc_heading,spawn_x,spawn_y,spawn_z,spawn_heading, park_x,park_y,park_z,showcar_x,showcar_y,showcar_z,showcar_heading, cam_x,cam_y,cam_z,cam_rot_z) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE label=VALUES(label),type=VALUES(type),access=VALUES(access),gang=VALUES(gang), blip_show=VALUES(blip_show),blip_type=VALUES(blip_type),blip_colour=VALUES(blip_colour), npc_model=VALUES(npc_model),npc_x=VALUES(npc_x),npc_y=VALUES(npc_y), npc_z=VALUES(npc_z),npc_heading=VALUES(npc_heading), spawn_x=VALUES(spawn_x),spawn_y=VALUES(spawn_y),spawn_z=VALUES(spawn_z),spawn_heading=VALUES(spawn_heading), park_x=VALUES(park_x),park_y=VALUES(park_y),park_z=VALUES(park_z), showcar_x=VALUES(showcar_x),showcar_y=VALUES(showcar_y),showcar_z=VALUES(showcar_z),showcar_heading=VALUES(showcar_heading), cam_x=VALUES(cam_x),cam_y=VALUES(cam_y),cam_z=VALUES(cam_z),cam_rot_z=VALUES(cam_rot_z)]], {data.id,data.label,data.type,data.access or "none",data.gang or "none", (data.type=='jobgarage' and (data.blip_show and 1 or 0) or 1),data.blip_type or 357,data.blip_colour or 3, data.npc_model or "a_m_m_prolhost_01", data.npc_x,data.npc_y,data.npc_z,data.npc_heading or 0, data.spawn_x,data.spawn_y,data.spawn_z,data.spawn_heading or 0, data.park_x,data.park_y,data.park_z, data.showcar_x,data.showcar_y,data.showcar_z,data.showcar_heading or 0, data.cam_x,data.cam_y,data.cam_z,data.cam_rot_z or -20}) GaragesData[data.id] = DBRowToGarage({ id=data.id,label=data.label,type=data.type,access=data.access or "none",gang=data.gang or "none", blip_show=(data.type=='jobgarage' and (data.blip_show and 1 or 0) or 1),blip_type=data.blip_type or 357,blip_colour=data.blip_colour or 3, npc_model=data.npc_model or "a_m_m_prolhost_01", npc_x=data.npc_x,npc_y=data.npc_y,npc_z=data.npc_z,npc_heading=data.npc_heading or 0, spawn_x=data.spawn_x,spawn_y=data.spawn_y,spawn_z=data.spawn_z,spawn_heading=data.spawn_heading or 0, park_x=data.park_x,park_y=data.park_y,park_z=data.park_z, showcar_x=data.showcar_x,showcar_y=data.showcar_y,showcar_z=data.showcar_z,showcar_heading=data.showcar_heading or 0, cam_x=data.cam_x,cam_y=data.cam_y,cam_z=data.cam_z,cam_rot_z=data.cam_rot_z or -20, }) -- Job-Fahrzeuge persistieren (DB + Config) if data.type == 'jobgarage' and data.job_vehicles then if not Config.JobVehicles then Config.JobVehicles = {} end Config.JobVehicles[data.access] = data.job_vehicles -- In DB speichern MySQL.update.await("DELETE FROM mercyv_job_vehicles WHERE garage_id = ?", { data.id }) for _, v in ipairs(data.job_vehicles) do MySQL.insert("INSERT INTO mercyv_job_vehicles (garage_id, model, label) VALUES (?, ?, ?)", { data.id, v.model, v.label or v.model }) end end BroadcastGarages() Config.ServerNotification(src, Config.Notify.ADMIN_SAVED, "success") end) RegisterNetEvent('mercyv-garage:admin:deleteGarage', function(garageId) local src=source if not IsAdmin(src) then Config.ServerNotification(src,Config.Notify.ADMIN_NO_PERM,"error"); return end MySQL.update("DELETE FROM mercyv_garages WHERE id=?",{garageId}) GaragesData[garageId]=nil BroadcastGarages() Config.ServerNotification(src,Config.Notify.ADMIN_DELETED,"success") end) -- ────────────────────────────────────────────────────────────── -- Schlüssel für aktive Job-Fahrzeuge an einen Spieler geben -- ────────────────────────────────────────────────────────────── function GiveJobKeysToPlayer(src) local xp = ESX and ESX.GetPlayerFromId(src) if not xp then return end local playerJob = xp.job and xp.job.name for plate, jobName in pairs(ActiveJobPlates) do if playerJob == jobName then local ks = Config.VehicleKeySystem if ks == 'jaksam' then TriggerClientEvent('vehicles_keys:selfGiveVehicleKeys', src, plate) elseif ks == 'qs-vehiclekeys' then TriggerClientEvent('qs-vehiclekeys:client:GiveKeys', src, plate) elseif ks == 'qb-vehiclekeys' then TriggerClientEvent('qb-vehiclekeys:client:AddKeys', src, plate) elseif ks == 'wasabi-carlock' then TriggerClientEvent('wasabi-carlock:client:giveKeys', src, plate) end print(string.format('[mercyv-garage] Nachträglicher Schlüssel für %s → Spieler %d (%s)', plate, src, playerJob)) end end end -- ────────────────────────────────────────────────────────────── -- Job-Fahrzeug: Schlüssel an alle Online-Spieler mit passendem Job -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:giveJobKeys', function(plate, jobName) if not plate or not jobName then return end ActiveJobPlates[plate] = jobName local players = GetPlayers() local count = 0 for _, pid in ipairs(players) do local src = tonumber(pid) local xp = ESX and ESX.GetPlayerFromId(src) if xp and xp.job and xp.job.name == jobName then local ks = Config.VehicleKeySystem if ks == 'jaksam' then -- jaksam erwartet das Kennzeichen ohne Leerzeichen und uppercase TriggerEvent('vehicles_keys:addKey', plate, src) TriggerClientEvent('vehicles_keys:selfGiveVehicleKeys', src, plate) elseif ks == 'qs-vehiclekeys' then TriggerClientEvent('qs-vehiclekeys:client:GiveKeys', src, plate) elseif ks == 'qb-vehiclekeys' then TriggerClientEvent('qb-vehiclekeys:client:AddKeys', src, plate) elseif ks == 'wasabi-carlock' then TriggerClientEvent('wasabi-carlock:client:giveKeys', src, plate) end count = count + 1 end end print(string.format('[mercyv-garage] Job-Schlüssel für %s an %d %s-Spieler verteilt.', plate, count, jobName)) end) -- ────────────────────────────────────────────────────────────── -- Job-Fahrzeug einparken -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:parkJobVehicle', function(plate) local src = source local xp = ESX and ESX.GetPlayerFromId(src) if not xp then return end local jobName = ActiveJobPlates[plate] if not jobName then return end -- Kein aktives Job-Fahrzeug -- Nur Spieler mit dem passenden Job dürfen einparken if xp.job and xp.job.name ~= jobName then Config.ServerNotification(src, Config.Notify.NO_ACCESS, "error") return end -- Aus Tracking entfernen ActiveJobPlates[plate] = nil Config.ServerNotification(src, Config.Notify.PARKED_IN, "success") print(string.format('[mercyv-garage] Job-Fahrzeug %s eingeparkt von %s', plate, xp.identifier)) end) -- ────────────────────────────────────────────────────────────── -- Fahrzeug zerstört → zurück in Garage -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:vehicleDestroyed', function(plate) local src = source local identifier = GetIdentifier(src) if not identifier or not plate then return end local tbl = vT() local owner = oC() -- Nur wenn das Fahrzeug dem Spieler gehört und draußen war local r = MySQL.query.await( string.format("SELECT plate, parking FROM `%s` WHERE plate=? AND `%s`=? AND stored=0", tbl, owner), { plate, identifier }) if r and r[1] then -- Zurück in die letzte Garage oder Garage A als Fallback local lastParking = r[1].parking if not lastParking or lastParking == '' then lastParking = 'Garage A' end MySQL.update.await( string.format("UPDATE `%s` SET stored=1, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner), { plate, identifier }) SpawnedPlates[string.lower(string.gsub(plate, '%s+', ''))] = nil Config.ServerNotification(src, "Dein Fahrzeug (" .. plate .. ") wurde eingelagert.", "error") print(string.format('[mercyv-garage] Fahrzeug %s zerstört → stored=1', plate)) end end) -- ────────────────────────────────────────────────────────────── -- /dv → Fahrzeug in Impound schicken -- ────────────────────────────────────────────────────────────── RegisterNetEvent('mercyv-garage:sendToImpound', function(plate) local src = source local identifier = GetIdentifier(src) if not identifier then return end local tbl = vT() local owner = oC() -- Impound-Garage ID finden local impoundId = nil for id, g in pairs(GaragesData) do if g.garage == 'impound' then impoundId = id break end end if not impoundId then -- Kein Impound konfiguriert → einfach stored=1 MySQL.update( string.format("UPDATE `%s` SET stored=1, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner), { plate, identifier }) return end MySQL.update( string.format("UPDATE `%s` SET stored=1, parking=?, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner), { impoundId, plate, identifier }) Config.ServerNotification(src, "Fahrzeug wurde abgeschleppt. Impound: " .. impoundId, "error") print(string.format('[mercyv-garage] /dv: %s von %s → Impound (%s)', plate, identifier, impoundId)) end) -- Reset PersistDone wenn der Spawn-Spieler disconnectet AddEventHandler('playerDropped', function(reason) local src = source if PersistSentTo[src] then PersistSentTo[src] = nil -- Wenn noch kein anderer Spieler gespawnt hat, Reset local anyOther = false for k,_ in pairs(PersistSentTo) do anyOther = true; break end if not anyOther then PersistDone = false print('[mercyv-garage Persist] Spawn-Spieler hat Server verlassen, Reset.') end end end) -- Job-Wechsel: neue Job-Schlüssel geben AddEventHandler('esx:setJob', function(src, job) Wait(500) GiveJobKeysToPlayer(src) end)