251 lines
9.9 KiB
Lua
251 lines
9.9 KiB
Lua
-- ═══════════════════════════════════════════════════════════════
|
||
-- MercyV Bike – Server
|
||
-- ═══════════════════════════════════════════════════════════════
|
||
|
||
local ESX = nil
|
||
|
||
Citizen.CreateThread(function()
|
||
if Config.NewESX then
|
||
ESX = exports['es_extended']:getSharedObject()
|
||
end
|
||
end)
|
||
|
||
local function GetIdentifier(src)
|
||
local xp = ESX and ESX.GetPlayerFromId(src)
|
||
return xp and xp.identifier or nil
|
||
end
|
||
|
||
-- Fahrzeug-Tabelle (ESX)
|
||
local function vT() return "owned_vehicles" end
|
||
local function oC() return "owner" end
|
||
local function pC() return "vehicle" end
|
||
|
||
-- Zufälliges Kennzeichen generieren
|
||
local function GeneratePlate()
|
||
local chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
||
local plate = ''
|
||
for i = 1, 3 do plate = plate .. chars:sub(math.random(1,#chars),math.random(1,#chars)):sub(1,1) end
|
||
plate = plate .. ' '
|
||
for i = 1, 3 do plate = plate .. tostring(math.random(0,9)) end
|
||
return plate
|
||
end
|
||
|
||
local function IsAdmin(src)
|
||
if IsPlayerAceAllowed(src, Config.AdminAce) then return true end
|
||
if 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
|
||
|
||
-- ── Tabelle erstellen ──────────────────────────────────────────
|
||
|
||
AddEventHandler('onResourceStart', function(res)
|
||
if res ~= GetCurrentResourceName() then return end
|
||
Wait(1000)
|
||
MySQL.query.await([[
|
||
CREATE TABLE IF NOT EXISTS `mercyv_bike_claims` (
|
||
`identifier` VARCHAR(120) NOT NULL,
|
||
`bike_model` VARCHAR(60) NOT NULL,
|
||
`claimed_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (`identifier`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||
]])
|
||
print('^2[mercyv-bike]^0 Tabelle bereit.')
|
||
end)
|
||
|
||
-- ── NPC-Position synchronisieren ──────────────────────────────
|
||
|
||
local NPCData = nil
|
||
local SpawnData = nil -- separate Spawn-Position für Fahrräder
|
||
|
||
AddEventHandler('onResourceStart', function(res)
|
||
if res ~= GetCurrentResourceName() then return end
|
||
Wait(1500)
|
||
-- Tabelle ZUERST anlegen
|
||
MySQL.query.await([[
|
||
CREATE TABLE IF NOT EXISTS `mercyv_bike_npc` (
|
||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||
`model` VARCHAR(100) DEFAULT 'a_m_m_beach_01',
|
||
`x` FLOAT DEFAULT 0,
|
||
`y` FLOAT DEFAULT 0,
|
||
`z` FLOAT DEFAULT 0,
|
||
`heading` FLOAT DEFAULT 0,
|
||
`spawn_x` FLOAT DEFAULT 0,
|
||
`spawn_y` FLOAT DEFAULT 0,
|
||
`spawn_z` FLOAT DEFAULT 0,
|
||
`spawn_heading` FLOAT DEFAULT 0
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||
]])
|
||
|
||
-- Migration: Spalten hinzufügen falls noch nicht vorhanden
|
||
for _, col in ipairs({'spawn_x','spawn_y','spawn_z','spawn_heading'}) do
|
||
local exists = MySQL.query.await(string.format(
|
||
"SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mercyv_bike_npc' AND COLUMN_NAME='%s'", col))
|
||
if exists and exists[1] and exists[1].cnt == 0 then
|
||
MySQL.query.await(string.format("ALTER TABLE `mercyv_bike_npc` ADD COLUMN `%s` FLOAT DEFAULT 0", col))
|
||
print('[mercyv-bike] Spalte ' .. col .. ' hinzugefügt.')
|
||
end
|
||
end
|
||
-- Dann lesen
|
||
local r = MySQL.query.await("SELECT * FROM mercyv_bike_npc LIMIT 1")
|
||
if r and r[1] then
|
||
NPCData = r[1]
|
||
if r[1].spawn_x and r[1].spawn_x ~= 0 then
|
||
SpawnData = { x = r[1].spawn_x, y = r[1].spawn_y, z = r[1].spawn_z, heading = r[1].spawn_heading or 0 }
|
||
end
|
||
else
|
||
NPCData = {
|
||
model = Config.NPC.model,
|
||
x = Config.NPC.x,
|
||
y = Config.NPC.y,
|
||
z = Config.NPC.z,
|
||
heading = Config.NPC.heading,
|
||
}
|
||
end
|
||
print('^2[mercyv-bike]^0 NPC-Daten geladen.')
|
||
end)
|
||
|
||
-- ── Client ready ───────────────────────────────────────────────
|
||
|
||
RegisterNetEvent('mercyv-bike:clientReady', function()
|
||
local src = source
|
||
Citizen.CreateThread(function()
|
||
Wait(500)
|
||
TriggerClientEvent('mercyv-bike:syncNPC', src, NPCData)
|
||
Wait(200)
|
||
TriggerClientEvent('mercyv-bike:setAdminStatus', src, IsAdmin(src))
|
||
end)
|
||
end)
|
||
|
||
RegisterNetEvent('mercyv-bike:checkAdmin', function()
|
||
TriggerClientEvent('mercyv-bike:setAdminStatus', source, IsAdmin(source))
|
||
end)
|
||
|
||
-- ── Fahrrad abholen ────────────────────────────────────────────
|
||
|
||
RegisterNetEvent('mercyv-bike:claim', function(bikeModel)
|
||
local src = source
|
||
local identifier = GetIdentifier(src)
|
||
if not identifier then return end
|
||
|
||
-- Prüfen ob bereits eines abgeholt
|
||
local existing = MySQL.query.await(
|
||
'SELECT identifier FROM mercyv_bike_claims WHERE identifier = ?',
|
||
{ identifier }
|
||
)
|
||
if existing and existing[1] then
|
||
Config.ServerNotification(src, Config.Notify.ALREADY_CLAIMED, "error")
|
||
return
|
||
end
|
||
|
||
-- Zufälliges Kennzeichen
|
||
local plate = GeneratePlate()
|
||
|
||
-- Als eigenes Fahrzeug in owned_vehicles eintragen
|
||
local tbl = vT(); local owner = oC(); local props = pC()
|
||
local modelHash = joaat and joaat(bikeModel) or 0
|
||
local skinData = json.encode({ model = GetHashKey(bikeModel), plate = plate })
|
||
|
||
-- Fahrzeug eintragen mit stored=0 (ist direkt in der Welt)
|
||
MySQL.insert(
|
||
string.format("INSERT INTO `%s` (plate, `%s`, `%s`, stored, parking, job, veh_class) VALUES (?, ?, ?, 0, 'Garage A', '', 13)", tbl, owner, props),
|
||
{ plate, identifier, skinData }
|
||
)
|
||
|
||
-- Claim eintragen
|
||
MySQL.insert(
|
||
'INSERT INTO mercyv_bike_claims (identifier, bike_model) VALUES (?, ?)',
|
||
{ identifier, bikeModel }
|
||
)
|
||
|
||
-- Spawn-Signal mit Kennzeichen
|
||
TriggerClientEvent('mercyv-bike:doSpawn', src, bikeModel, SpawnData, plate)
|
||
Config.ServerNotification(src, Config.Notify.CLAIMED, "success")
|
||
print(string.format('[mercyv-bike] %s hat %s erhalten (Kennzeichen: %s).', identifier, bikeModel, plate))
|
||
end)
|
||
|
||
-- ── Prüfen ob Spieler bereits eines hat ───────────────────────
|
||
|
||
RegisterNetEvent('mercyv-bike:checkClaim', function()
|
||
local src = source
|
||
local identifier = GetIdentifier(src)
|
||
if not identifier then
|
||
TriggerClientEvent('mercyv-bike:claimStatus', src, false)
|
||
return
|
||
end
|
||
local r = MySQL.query.await(
|
||
'SELECT bike_model FROM mercyv_bike_claims WHERE identifier = ?',
|
||
{ identifier }
|
||
)
|
||
TriggerClientEvent('mercyv-bike:claimStatus', src, r and r[1] ~= nil, r and r[1] and r[1].bike_model)
|
||
end)
|
||
|
||
-- ── Admin: NPC-Position speichern ─────────────────────────────
|
||
|
||
RegisterNetEvent('mercyv-bike:saveNPC', function(data)
|
||
local src = source
|
||
if not IsAdmin(src) then
|
||
Config.ServerNotification(src, Config.Notify.NO_ACCESS, "error")
|
||
return
|
||
end
|
||
|
||
NPCData = data
|
||
if data.spawn_x and data.spawn_x ~= 0 then
|
||
SpawnData = { x = data.spawn_x, y = data.spawn_y, z = data.spawn_z, heading = data.spawn_heading or 0 }
|
||
end
|
||
|
||
-- In DB speichern
|
||
local existing = MySQL.query.await('SELECT id FROM mercyv_bike_npc LIMIT 1')
|
||
local sx = data.spawn_x or 0
|
||
local sy = data.spawn_y or 0
|
||
local sz = data.spawn_z or 0
|
||
local sh = data.spawn_heading or 0
|
||
if existing and existing[1] then
|
||
MySQL.update('UPDATE mercyv_bike_npc SET model=?, x=?, y=?, z=?, heading=?, spawn_x=?, spawn_y=?, spawn_z=?, spawn_heading=? WHERE id=?',
|
||
{ data.model, data.x, data.y, data.z, data.heading, sx, sy, sz, sh, existing[1].id })
|
||
else
|
||
MySQL.insert('INSERT INTO mercyv_bike_npc (model, x, y, z, heading, spawn_x, spawn_y, spawn_z, spawn_heading) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||
{ data.model, data.x, data.y, data.z, data.heading, sx, sy, sz, sh })
|
||
end
|
||
|
||
TriggerClientEvent('mercyv-bike:syncNPC', -1, NPCData)
|
||
Config.ServerNotification(src, "NPC-Position gespeichert.", "success")
|
||
end)
|
||
|
||
-- ── Benachrichtigung ──────────────────────────────────────────
|
||
|
||
RegisterNetEvent('mercyv-bike:notify', function(msg, type)
|
||
-- Wird clientseitig behandelt
|
||
end)
|
||
|
||
-- ── Admin: Claim zurücksetzen ─────────────────────────────────
|
||
|
||
RegisterCommand('bikereset', function(src, args)
|
||
if not IsAdmin(src) then return end
|
||
local targetId = tonumber(args[1])
|
||
if not targetId then
|
||
print('[mercyv-bike] Verwendung: /bikereset [Spieler-ID]')
|
||
return
|
||
end
|
||
local xp = ESX and ESX.GetPlayerFromId(targetId)
|
||
if not xp then return end
|
||
MySQL.update('DELETE FROM mercyv_bike_claims WHERE identifier = ?', { xp.identifier })
|
||
Config.ServerNotification(src, 'Claim von Spieler ' .. targetId .. ' zurückgesetzt.', 'success')
|
||
Config.ServerNotification(targetId, 'Dein Fahrrad-Claim wurde zurückgesetzt.', 'info')
|
||
end, false)
|
||
|
||
RegisterNetEvent('mercyv-bike:deleteNPC', function()
|
||
local src = source
|
||
if not IsAdmin(src) then return end
|
||
MySQL.update('DELETE FROM mercyv_bike_npc')
|
||
NPCData = nil
|
||
SpawnData = nil
|
||
TriggerClientEvent('mercyv-bike:syncNPC', -1, nil)
|
||
Config.ServerNotification(src, "NPC gelöscht.", "success")
|
||
end)
|