796 lines
42 KiB
Lua
796 lines
42 KiB
Lua
-- ============================================================
|
||
-- mercyv-garage | server/main.lua (komplett neu)
|
||
-- ============================================================
|
||
|
||
local ESX = nil
|
||
local GaragesData = {}
|
||
local SpawnedPlates = {}
|
||
local ActiveJobPlates = {} -- [plate] = jobName, aktive Job-Fahrzeuge in der Welt
|
||
local PersistSentTo = {} -- [src] = true
|
||
local PersistDone = false -- true sobald erster Spieler alle Fahrzeuge gespawnt hat
|
||
|
||
-- ──────────────────────────────────────────────────────────────
|
||
-- 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
|
||
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
|
||
-- Job-Fahrzeug tracken
|
||
ActiveJobPlates[plate] = g.access
|
||
|
||
TriggerClientEvent('mercyv-garage:doSpawn', src, {
|
||
plate=plate, 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 in Config aktualisieren
|
||
if data.type == 'jobgarage' and data.job_vehicles then
|
||
if not Config.JobVehicles then Config.JobVehicles = {} end
|
||
Config.JobVehicles[data.access] = data.job_vehicles
|
||
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
|
||
-- Plate tracken damit nachträgliche Einlogger auch Schlüssel bekommen
|
||
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
|
||
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)
|