ESX = exports['es_extended']:getSharedObject() -- In-memory caches local treasury = {} -- faction_id -> { money, black_money } local permissions = {} -- faction_id -> { perm_key -> min_grade } local spawnedVehicles = {} -- source -> { vehicle_net_id, faction_id } -- ===================== -- HELPERS -- ===================== local function GetIdentifier(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return nil end return xPlayer.getIdentifier() end local function GetPlayerName(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return 'Unbekannt' end return xPlayer.getName() end local function GetPlayerFaction(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return nil, nil end local job = xPlayer.getJob() if not job or not Config.Factions[job.name] then return nil, nil end return job.name, job.grade end local function HasPermission(factionId, playerGrade, permissionKey) local minGrade if permissions[factionId] and permissions[factionId][permissionKey] ~= nil then minGrade = permissions[factionId][permissionKey] else minGrade = Config.DefaultPermissions[permissionKey] or 99 end return playerGrade >= minGrade end local function GetPermissionValue(factionId, permissionKey) if permissions[factionId] and permissions[factionId][permissionKey] ~= nil then return permissions[factionId][permissionKey] end return Config.DefaultPermissions[permissionKey] or 99 end -- ===================== -- WEBHOOK -- ===================== local function SendWebhook(title, message, color) if not Config.WebhookURL or Config.WebhookURL == "" then return end local embed = { { ["color"] = color or 3447003, ["title"] = "**" .. title .. "**", ["description"] = message, ["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") }, } } PerformHttpRequest(Config.WebhookURL, function() end, 'POST', json.encode({username = "Fraktion Log", embeds = embed}), { ['Content-Type'] = 'application/json' }) end -- ===================== -- DATABASE SETUP -- ===================== MySQL.ready(function() MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_faction_treasury ( id INT AUTO_INCREMENT PRIMARY KEY, faction_id VARCHAR(50) NOT NULL UNIQUE, balance_money INT NOT NULL DEFAULT 0, balance_black_money INT NOT NULL DEFAULT 0, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP )]]) MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_faction_transactions ( id INT AUTO_INCREMENT PRIMARY KEY, faction_id VARCHAR(50) NOT NULL, player_identifier VARCHAR(60) NOT NULL, player_name VARCHAR(100) NOT NULL, type ENUM('deposit', 'withdraw') NOT NULL, account_type ENUM('money', 'black_money') NOT NULL, amount INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_faction (faction_id), INDEX idx_created (created_at) )]]) MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_faction_permissions ( id INT AUTO_INCREMENT PRIMARY KEY, faction_id VARCHAR(50) NOT NULL, permission_key VARCHAR(50) NOT NULL, min_grade INT NOT NULL DEFAULT 0, UNIQUE KEY uq_faction_perm (faction_id, permission_key) )]]) -- Ensure all config factions have treasury entries for factionId, _ in pairs(Config.Factions) do MySQL.query.await('INSERT IGNORE INTO mercyv_faction_treasury (faction_id) VALUES (?)', { factionId }) end -- Load treasury local tOk, treasuryRows = pcall(function() return MySQL.query.await('SELECT * FROM mercyv_faction_treasury') end) if tOk and treasuryRows then for _, row in ipairs(treasuryRows) do if row.faction_id then treasury[row.faction_id] = { money = row.balance_money or 0, black_money = row.balance_black_money or 0, } end end end -- Load permissions local permOk, permRows = pcall(function() return MySQL.query.await('SELECT * FROM mercyv_faction_permissions') end) if permOk and permRows then for _, row in ipairs(permRows) do if row.faction_id and row.permission_key then if not permissions[row.faction_id] then permissions[row.faction_id] = {} end permissions[row.faction_id][row.permission_key] = row.min_grade end end end print('[mercyv-fraks] System gestartet (' .. #(treasuryRows or {}) .. ' Fraktionen geladen)') end) -- ===================== -- GET FACTION MEMBERS -- ===================== local function GetFactionMembers(factionId) local members = {} local onlinePlayers = ESX.GetExtendedPlayers() -- Online-Spieler mit passendem Job local onlineIdentifiers = {} for _, xPlayer in pairs(onlinePlayers) do local job = xPlayer.getJob() if job and job.name == factionId then onlineIdentifiers[xPlayer.getIdentifier()] = true members[#members + 1] = { identifier = xPlayer.getIdentifier(), name = xPlayer.getName(), grade = job.grade, grade_label = job.grade_label or ('Rang ' .. job.grade), online = true, source = xPlayer.source, } end end -- Offline-Spieler aus DB local rows = MySQL.query.await('SELECT identifier, firstname, lastname, job_grade FROM users WHERE job = ?', { factionId }) if rows then for _, row in ipairs(rows) do if not onlineIdentifiers[row.identifier] then members[#members + 1] = { identifier = row.identifier, name = (row.firstname or '') .. ' ' .. (row.lastname or ''), grade = row.job_grade, grade_label = 'Rang ' .. row.job_grade, online = false, source = nil, } end end end return members end local function GetOnlineMemberCount(factionId) local count = 0 for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do local job = xPlayer.getJob() if job and job.name == factionId then count = count + 1 end end return count end local function GetTotalMemberCount(factionId) local result = MySQL.query.await('SELECT COUNT(*) as cnt FROM users WHERE job = ?', { factionId }) if result and result[1] then return result[1].cnt end return 0 end -- ===================== -- GET JOB GRADES -- ===================== local function GetJobGrades(factionId) local rows = MySQL.query.await('SELECT grade, label FROM job_grades WHERE job_name = ? ORDER BY grade ASC', { factionId }) if not rows then return {} end local grades = {} for _, row in ipairs(rows) do grades[#grades + 1] = { grade = row.grade, label = row.label } end return grades end local function GetLowestGrade(factionId) local grades = GetJobGrades(factionId) if #grades == 0 then return 0 end return grades[1].grade end local function IsValidGrade(factionId, grade) local grades = GetJobGrades(factionId) for _, g in ipairs(grades) do if g.grade == grade then return true end end return false end -- ===================== -- CALLBACKS -- ===================== ESX.RegisterServerCallback('mercyv-fraks:getFactionData', function(source, cb) local factionId, grade = GetPlayerFaction(source) if not factionId then return cb(nil) end local config = Config.Factions[factionId] local bal = treasury[factionId] or { money = 0, black_money = 0 } -- Build permissions for this player local playerPerms = {} for permKey, _ in pairs(Config.DefaultPermissions) do playerPerms[permKey] = HasPermission(factionId, grade, permKey) end cb({ factionId = factionId, factionLabel = config.label, factionImage = config.image or '', playerGrade = grade, grades = GetJobGrades(factionId), treasury = { money = bal.money, black_money = bal.black_money, }, totalMembers = GetTotalMemberCount(factionId), onlineMembers = GetOnlineMemberCount(factionId), permissions = playerPerms, permissionValues = GetAllPermissionValues(factionId), theme = config.theme or {}, vehicles = config.vehicles or {}, }) end) function GetAllPermissionValues(factionId) local values = {} for permKey, _ in pairs(Config.DefaultPermissions) do values[permKey] = GetPermissionValue(factionId, permKey) end return values end ESX.RegisterServerCallback('mercyv-fraks:getMembers', function(source, cb) local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'viewMembers') then return cb({}) end cb(GetFactionMembers(factionId)) end) ESX.RegisterServerCallback('mercyv-fraks:getTreasuryLog', function(source, cb) local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'viewTreasury') then return cb({}) end local rows = MySQL.query.await('SELECT * FROM mercyv_faction_transactions WHERE faction_id = ? ORDER BY created_at DESC LIMIT 50', { factionId }) cb(rows or {}) end) ESX.RegisterServerCallback('mercyv-fraks:getNearbyPlayers', function(source, cb) local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'inviteMember') then return cb({}) end local sourceCoords = GetEntityCoords(GetPlayerPed(source)) local nearby = {} for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do if xPlayer.source ~= source then local job = xPlayer.getJob() if not job or job.name ~= factionId then local targetCoords = GetEntityCoords(GetPlayerPed(xPlayer.source)) local dist = #(sourceCoords - targetCoords) if dist <= Config.InviteRadius then nearby[#nearby + 1] = { source = xPlayer.source, name = xPlayer.getName(), job = job and job.label or 'Arbeitslos', } end end end end cb(nearby) end) -- ===================== -- INVITE SYSTEM -- ===================== RegisterNetEvent('mercyv-fraks:inviteMember') AddEventHandler('mercyv-fraks:inviteMember', function(targetSource) local source = source local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'inviteMember') then return end -- Check member limit local config = Config.Factions[factionId] local maxMembers = config.maxMembers or Config.MaxMembersDefault if GetTotalMemberCount(factionId) >= maxMembers then TriggerClientEvent('mercyv-fraks:notify', source, 'Mitgliederlimit erreicht (' .. maxMembers .. ')', 'error') return end local targetPlayer = ESX.GetPlayerFromId(targetSource) if not targetPlayer then return end -- Send invite to target TriggerClientEvent('mercyv-fraks:receiveInvite', targetSource, { factionId = factionId, factionLabel = config.label, inviterName = GetPlayerName(source), inviterSource = source, }) TriggerClientEvent('mercyv-fraks:notify', source, 'Einladung gesendet an ' .. targetPlayer.getName(), 'info') end) RegisterNetEvent('mercyv-fraks:respondInvite') AddEventHandler('mercyv-fraks:respondInvite', function(accepted, factionId, inviterSource) local source = source local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end if not accepted then if inviterSource then TriggerClientEvent('mercyv-fraks:notify', inviterSource, xPlayer.getName() .. ' hat die Einladung abgelehnt', 'error') end return end local config = Config.Factions[factionId] if not config then return end -- Set job to lowest available grade local lowestGrade = GetLowestGrade(factionId) xPlayer.setJob(factionId, lowestGrade) TriggerClientEvent('mercyv-fraks:notify', source, 'Du bist ' .. config.label .. ' beigetreten!', 'success') if inviterSource then TriggerClientEvent('mercyv-fraks:notify', inviterSource, xPlayer.getName() .. ' ist beigetreten!', 'success') end SendWebhook("Mitglied Beigetreten", string.format( "Fraktion: **%s**\nSpieler: **%s** (%s)\nEingeladen von: **%s**", config.label, xPlayer.getName(), xPlayer.getIdentifier(), inviterSource and GetPlayerName(inviterSource) or 'Unbekannt' ), 3066993) end) -- ===================== -- KICK MEMBER -- ===================== RegisterNetEvent('mercyv-fraks:kickMember') AddEventHandler('mercyv-fraks:kickMember', function(targetIdentifier) local source = source local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'kickMember') then return end local config = Config.Factions[factionId] -- Find target (online or offline) local targetOnline = nil for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do if xPlayer.getIdentifier() == targetIdentifier then targetOnline = xPlayer break end end local targetName = 'Unbekannt' if targetOnline then -- Check can't kick same or higher grade local targetJob = targetOnline.getJob() if targetJob and targetJob.grade >= grade then TriggerClientEvent('mercyv-fraks:notify', source, 'Du kannst diesen Rang nicht entlassen', 'error') return end targetName = targetOnline.getName() targetOnline.setJob('unemployed', 0) TriggerClientEvent('mercyv-fraks:notify', targetOnline.source, 'Du wurdest aus ' .. config.label .. ' entlassen', 'error') else -- Offline kick via DB local rows = MySQL.query.await('SELECT firstname, lastname, job_grade FROM users WHERE identifier = ? AND job = ?', { targetIdentifier, factionId }) if not rows or #rows == 0 then TriggerClientEvent('mercyv-fraks:notify', source, 'Spieler nicht gefunden', 'error') return end if rows[1].job_grade >= grade then TriggerClientEvent('mercyv-fraks:notify', source, 'Du kannst diesen Rang nicht entlassen', 'error') return end targetName = (rows[1].firstname or '') .. ' ' .. (rows[1].lastname or '') MySQL.query('UPDATE users SET job = ?, job_grade = ? WHERE identifier = ?', { 'unemployed', 0, targetIdentifier }) end TriggerClientEvent('mercyv-fraks:notify', source, targetName .. ' wurde entlassen', 'success') SendWebhook("Mitglied Entlassen", string.format( "Fraktion: **%s**\nEntlassen: **%s** (%s)\nVon: **%s**", config.label, targetName, targetIdentifier, GetPlayerName(source) ), 15158332) end) -- ===================== -- CHANGE RANK -- ===================== RegisterNetEvent('mercyv-fraks:changeRank') AddEventHandler('mercyv-fraks:changeRank', function(targetIdentifier, newGrade) local source = source local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'changeRank') then return end newGrade = math.max(0, math.floor(tonumber(newGrade) or 0)) local config = Config.Factions[factionId] -- Can't set grade >= own grade if newGrade >= grade then TriggerClientEvent('mercyv-fraks:notify', source, 'Du kannst keinen Rang auf oder ueber deinem eigenen setzen', 'error') return end -- Validate grade exists in job_grades if not IsValidGrade(factionId, newGrade) then TriggerClientEvent('mercyv-fraks:notify', source, 'Ungueltiger Rang', 'error') return end local targetOnline = nil for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do if xPlayer.getIdentifier() == targetIdentifier then targetOnline = xPlayer break end end local targetName = 'Unbekannt' if targetOnline then local targetJob = targetOnline.getJob() if targetJob and targetJob.grade >= grade then TriggerClientEvent('mercyv-fraks:notify', source, 'Du kannst diesen Rang nicht aendern', 'error') return end targetName = targetOnline.getName() targetOnline.setJob(factionId, newGrade) TriggerClientEvent('mercyv-fraks:notify', targetOnline.source, 'Dein Rang wurde geaendert', 'info') else local rows = MySQL.query.await('SELECT firstname, lastname, job_grade FROM users WHERE identifier = ? AND job = ?', { targetIdentifier, factionId }) if not rows or #rows == 0 then TriggerClientEvent('mercyv-fraks:notify', source, 'Spieler nicht gefunden', 'error') return end if rows[1].job_grade >= grade then TriggerClientEvent('mercyv-fraks:notify', source, 'Du kannst diesen Rang nicht aendern', 'error') return end targetName = (rows[1].firstname or '') .. ' ' .. (rows[1].lastname or '') MySQL.query('UPDATE users SET job_grade = ? WHERE identifier = ? AND job = ?', { newGrade, targetIdentifier, factionId }) end TriggerClientEvent('mercyv-fraks:notify', source, 'Rang von ' .. targetName .. ' geaendert', 'success') SendWebhook("Rang Geaendert", string.format( "Fraktion: **%s**\nSpieler: **%s**\nNeuer Rang: **%d**\nVon: **%s**", config.label, targetName, newGrade, GetPlayerName(source) ), 3447003) end) -- ===================== -- TREASURY -- ===================== RegisterNetEvent('mercyv-fraks:depositTreasury') AddEventHandler('mercyv-fraks:depositTreasury', function(amount, accountType) local source = source local xPlayer = ESX.GetPlayerFromId(source) local factionId, grade = GetPlayerFaction(source) if not xPlayer or not factionId then return end if not HasPermission(factionId, grade, 'depositTreasury') then return end amount = math.max(1, math.floor(tonumber(amount) or 0)) accountType = (accountType == 'black_money') and 'black_money' or 'money' local playerMoney = xPlayer.getAccount(accountType).money if playerMoney < amount then TriggerClientEvent('mercyv-fraks:notify', source, 'Nicht genug Geld', 'error') return end xPlayer.removeAccountMoney(accountType, amount) local bal = treasury[factionId] or { money = 0, black_money = 0 } bal[accountType] = bal[accountType] + amount treasury[factionId] = bal local dbField = accountType == 'black_money' and 'balance_black_money' or 'balance_money' MySQL.query('UPDATE mercyv_faction_treasury SET ' .. dbField .. ' = ? WHERE faction_id = ?', { bal[accountType], factionId }) MySQL.query('INSERT INTO mercyv_faction_transactions (faction_id, player_identifier, player_name, type, account_type, amount) VALUES (?, ?, ?, ?, ?, ?)', { factionId, GetIdentifier(source), GetPlayerName(source), 'deposit', accountType, amount }) TriggerClientEvent('mercyv-fraks:notify', source, '$' .. amount .. ' eingezahlt', 'success') TriggerClientEvent('mercyv-fraks:updateTreasury', source, bal) SendWebhook("Kasse Einzahlung", string.format( "Fraktion: **%s**\nSpieler: **%s**\nBetrag: **$%d** (%s)\nNeuer Stand: **$%d** / **$%d**", Config.Factions[factionId].label, GetPlayerName(source), amount, accountType == 'black_money' and 'Schwarzgeld' or 'Bargeld', bal.money, bal.black_money ), 3066993) end) RegisterNetEvent('mercyv-fraks:withdrawTreasury') AddEventHandler('mercyv-fraks:withdrawTreasury', function(amount, accountType) local source = source local xPlayer = ESX.GetPlayerFromId(source) local factionId, grade = GetPlayerFaction(source) if not xPlayer or not factionId then return end if not HasPermission(factionId, grade, 'withdrawTreasury') then return end amount = math.max(1, math.floor(tonumber(amount) or 0)) accountType = (accountType == 'black_money') and 'black_money' or 'money' local bal = treasury[factionId] or { money = 0, black_money = 0 } if bal[accountType] < amount then TriggerClientEvent('mercyv-fraks:notify', source, 'Nicht genug in der Kasse', 'error') return end bal[accountType] = bal[accountType] - amount treasury[factionId] = bal xPlayer.addAccountMoney(accountType, amount) local dbField = accountType == 'black_money' and 'balance_black_money' or 'balance_money' MySQL.query('UPDATE mercyv_faction_treasury SET ' .. dbField .. ' = ? WHERE faction_id = ?', { bal[accountType], factionId }) MySQL.query('INSERT INTO mercyv_faction_transactions (faction_id, player_identifier, player_name, type, account_type, amount) VALUES (?, ?, ?, ?, ?, ?)', { factionId, GetIdentifier(source), GetPlayerName(source), 'withdraw', accountType, amount }) TriggerClientEvent('mercyv-fraks:notify', source, '$' .. amount .. ' ausgezahlt', 'success') TriggerClientEvent('mercyv-fraks:updateTreasury', source, bal) SendWebhook("Kasse Auszahlung", string.format( "Fraktion: **%s**\nSpieler: **%s**\nBetrag: **$%d** (%s)\nNeuer Stand: **$%d** / **$%d**", Config.Factions[factionId].label, GetPlayerName(source), amount, accountType == 'black_money' and 'Schwarzgeld' or 'Bargeld', bal.money, bal.black_money ), 15158332) end) -- ===================== -- STASH (codem-inventory) -- ===================== ESX.RegisterServerCallback('mercyv-fraks:canUseStash', function(source, cb) local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'useStash') then return cb(false) end cb(true) end) -- ===================== -- GARAGE SYSTEM (mit Jaksam Vehicle Keys) -- ===================== -- Callback: Garage-Daten fuer NUI ESX.RegisterServerCallback('mercyv-fraks:getGarageData', function(source, cb) local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'useGarage') then return cb(nil) end local config = Config.Factions[factionId] if not config then return cb(nil) end -- Nur Fahrzeuge die der Spieler mit seinem Rang nutzen darf local availableVehicles = {} for _, v in ipairs(config.vehicles or {}) do if grade >= (v.minGrade or 0) then availableVehicles[#availableVehicles + 1] = { model = v.model, label = v.label, minGrade = v.minGrade or 0, image = v.image or '', } end end cb({ vehicles = availableVehicles, theme = config.theme or {}, factionLabel = config.label, hasSpawned = spawnedVehicles[source] ~= nil, }) end) -- Ausparken (Fahrzeug spawnen + Schluessel geben) RegisterNetEvent('mercyv-fraks:garageSpawn') AddEventHandler('mercyv-fraks:garageSpawn', function(vehicleModel) local source = source local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'useGarage') then return end local config = Config.Factions[factionId] if not config or not config.vehicles then return end -- Bereits ein Fahrzeug ausgeparkt? if spawnedVehicles[source] then TriggerClientEvent('mercyv-fraks:notify', source, 'Du hast bereits ein Fahrzeug ausgeparkt', 'error') return end -- Fahrzeug in Config finden local vehConfig = nil for _, v in ipairs(config.vehicles) do if v.model == vehicleModel then vehConfig = v break end end if not vehConfig then TriggerClientEvent('mercyv-fraks:notify', source, 'Fahrzeug nicht gefunden', 'error') return end if grade < (vehConfig.minGrade or 0) then TriggerClientEvent('mercyv-fraks:notify', source, 'Rang nicht ausreichend', 'error') return end -- Farbe bestimmen: Fahrzeug-spezifisch oder Fraktions-Default local color = vehConfig.color or config.vehicleColor or nil -- Client soll Fahrzeug spawnen TriggerClientEvent('mercyv-fraks:doSpawnVehicle', source, vehicleModel, config.garageSpawn, color, vehConfig.livery) TriggerClientEvent('mercyv-fraks:notify', source, vehConfig.label .. ' wird ausgeparkt...', 'success') end) -- Fahrzeug wurde vom Client gespawnt -> Schluessel geben RegisterNetEvent('mercyv-fraks:vehicleSpawned') AddEventHandler('mercyv-fraks:vehicleSpawned', function(netId, plate) local source = source local factionId = GetPlayerFaction(source) if not factionId then return end spawnedVehicles[source] = { netId = netId, faction_id = factionId, plate = plate } -- Jaksam Vehicle Keys: Allen Online-Fraktionsmitgliedern Schluessel geben for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do local job = xPlayer.getJob() if job and job.name == factionId then pcall(function() exports['vehicles_keys']:giveVehicleKeysToPlayerId(xPlayer.source, plate, 'temporary') end) end end TriggerClientEvent('mercyv-fraks:notify', source, 'Schluessel an alle Mitglieder verteilt', 'success') end) -- Einparken (Fahrzeug despawnen + Schluessel entfernen) -- Jedes Fraktionsmitglied darf einparken, nicht nur der Spawner RegisterNetEvent('mercyv-fraks:garageStore') AddEventHandler('mercyv-fraks:garageStore', function() local source = source local factionId = GetPlayerFaction(source) if not factionId then return end -- Eigenes Fahrzeug oder Fahrzeug eines Fraktionsmitglieds finden local ownerSource = nil local vehData = nil if spawnedVehicles[source] then ownerSource = source vehData = spawnedVehicles[source] else for src, data in pairs(spawnedVehicles) do if data.faction_id == factionId then ownerSource = src vehData = data break end end end if not vehData then TriggerClientEvent('mercyv-fraks:notify', source, 'Kein Fraktionsfahrzeug ausgeparkt', 'error') return end -- Jaksam Vehicle Keys: Schluessel bei allen Fraktionsmitgliedern entfernen for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do local job = xPlayer.getJob() if job and job.name == vehData.faction_id then pcall(function() exports['vehicles_keys']:removeKeysFromPlayerId(xPlayer.source, vehData.plate) end) end end -- Fahrzeug loeschen (broadcast an alle Clients) TriggerClientEvent('mercyv-fraks:despawnVehicle', -1, vehData.netId) spawnedVehicles[ownerSource] = nil TriggerClientEvent('mercyv-fraks:notify', source, 'Fahrzeug eingeparkt, Schluessel abgegeben', 'success') end) -- Cleanup on disconnect AddEventHandler('playerDropped', function() local source = source if spawnedVehicles[source] then local vehData = spawnedVehicles[source] -- Schluessel bei allen Fraktionsmitgliedern entfernen for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do local job = xPlayer.getJob() if job and job.name == vehData.faction_id then pcall(function() exports['vehicles_keys']:removeKeysFromPlayerId(xPlayer.source, vehData.plate) end) end end -- Fahrzeug loeschen TriggerClientEvent('mercyv-fraks:despawnVehicle', -1, vehData.netId) spawnedVehicles[source] = nil end end) -- ===================== -- PERMISSIONS MANAGEMENT -- ===================== RegisterNetEvent('mercyv-fraks:updatePermission') AddEventHandler('mercyv-fraks:updatePermission', function(permKey, newMinGrade) local source = source local factionId, grade = GetPlayerFaction(source) if not factionId or not HasPermission(factionId, grade, 'managePermissions') then return end newMinGrade = math.max(0, math.floor(tonumber(newMinGrade) or 0)) if not Config.DefaultPermissions[permKey] then return end if not permissions[factionId] then permissions[factionId] = {} end permissions[factionId][permKey] = newMinGrade MySQL.query('INSERT INTO mercyv_faction_permissions (faction_id, permission_key, min_grade) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE min_grade = ?', { factionId, permKey, newMinGrade, newMinGrade }) TriggerClientEvent('mercyv-fraks:notify', source, 'Recht "' .. (Config.PermissionLabels[permKey] or permKey) .. '" aktualisiert', 'success') SendWebhook("Rechte Geaendert", string.format( "Fraktion: **%s**\nRecht: **%s**\nNeuer Min-Rang: **%d**\nVon: **%s**", Config.Factions[factionId].label, Config.PermissionLabels[permKey] or permKey, newMinGrade, GetPlayerName(source) ), 10181046) end) -- ===================== -- SYNC -- ===================== RegisterNetEvent('mercyv-fraks:requestSync') AddEventHandler('mercyv-fraks:requestSync', function() -- Nothing to sync globally, data is per-faction end)