local frozenPlayers = {} local function isAdmin(source) return source == 0 or IsPlayerAceAllowed(source, 'devx.admin') end local function cleanReason(value) if type(value) ~= 'string' then return 'Keine Begründung angegeben.' end value = value:gsub('^%s+', ''):gsub('%s+$', '') if value == '' then return 'Keine Begründung angegeben.' end return value:sub(1, 255) end local function playerRows() local rows = {} for _, id in ipairs(GetPlayers()) do local source = tonumber(id) local player = exports.devx_core:GetPlayer(source) rows[#rows + 1] = { source = source, connectionName = GetPlayerName(source) or ('Spieler %s'):format(source), fullName = player and player.fullName or 'Kein Charakter geladen', characterId = player and player.characterId or nil, phoneNumber = player and player.phoneNumber or nil, cash = player and player.cash or 0, bank = player and player.bank or 0, rpXp = player and player.rpXp or 0, frozen = frozenPlayers[source] == true } end table.sort(rows, function(a, b) return a.source < b.source end) return rows end DevXRegisterCallback('admin:bootstrap', function(source) if not isAdmin(source) then return { ok = false, error = 'Keine Administratorberechtigung.' } end local weapons = {} for name, label in pairs(DevXConfig.AdminWeapons or {}) do weapons[#weapons + 1] = { name = name, label = label } end table.sort(weapons, function(a, b) return a.label < b.label end) return { ok = true, players = playerRows(), weapons = weapons } end) DevXRegisterCallback('admin:action', function(source, payload) if not isAdmin(source) then return { ok = false, error = 'Keine Administratorberechtigung.' } end local action = tostring(payload.action or '') local target = tonumber(payload.target) local targetPlayer = target and exports.devx_core:GetPlayer(target) or nil if action == 'refresh' then return { ok = true, players = playerRows() } end if not target or not GetPlayerName(target) then return { ok = false, error = 'Spieler ist nicht mehr online.' } end if action == 'setcash' then local amount = math.floor(tonumber(payload.amount) or -1) if amount < 0 or amount > DevXConfig.MaxMoneyOperation or not targetPlayer then return { ok = false, error = 'Ungültiger Bargeldbetrag.' } end if not exports.devx_core:SetCash(target, amount, ('Admin %s'):format(source)) then return { ok = false, error = 'Bargeld konnte nicht geändert werden.' } end elseif action == 'setbank' then local amount = math.floor(tonumber(payload.amount) or -1) if amount < 0 or amount > DevXConfig.MaxMoneyOperation or not targetPlayer then return { ok = false, error = 'Ungültiger Bankbetrag.' } end local success, errorMessage = exports.devx_banking:SetPlayerBalance(target, amount, ('Admin %s'):format(source)) if not success then return { ok = false, error = errorMessage } end elseif action == 'giveweapon' then local weapon = tostring(payload.weapon or ''):upper() if not DevXConfig.AdminWeapons[weapon] then return { ok = false, error = 'Waffe ist nicht freigegeben.' } end TriggerClientEvent('devx_admin:client:giveWeapon', target, weapon, 250) elseif action == 'heal' then TriggerClientEvent('devx_admin:client:heal', target) elseif action == 'freeze' then frozenPlayers[target] = not frozenPlayers[target] TriggerClientEvent('devx_admin:client:freeze', target, frozenPlayers[target]) elseif action == 'goto' then local ped = GetPlayerPed(target) if ped <= 0 then return { ok = false, error = 'Zielposition ist nicht verfügbar.' } end local coords = GetEntityCoords(ped) TriggerClientEvent('devx_admin:client:teleport', source, { x = coords.x, y = coords.y, z = coords.z + 1.0 }) elseif action == 'bring' then local ped = GetPlayerPed(source) if ped <= 0 then return { ok = false, error = 'Adminposition ist nicht verfügbar.' } end local coords = GetEntityCoords(ped) TriggerClientEvent('devx_admin:client:teleport', target, { x = coords.x, y = coords.y, z = coords.z + 1.0 }) elseif action == 'setphone' then local number = tostring(payload.phoneNumber or ''):gsub('%s+', '') if not targetPlayer or not number:match('^%d%d%d%-%d%d%d%d+$') or #number > 16 then return { ok = false, error = 'Telefonnummer muss wie 555-1234 aussehen.' } end local duplicate = MySQL.scalar.await('SELECT id FROM devx_characters WHERE phone_number = ? AND id <> ? LIMIT 1', { number, targetPlayer.characterId }) if duplicate then return { ok = false, error = 'Telefonnummer ist bereits vergeben.' } end MySQL.update.await('UPDATE devx_characters SET phone_number = ? WHERE id = ?', { number, targetPlayer.characterId }) targetPlayer.phoneNumber = number TriggerClientEvent('devx_core:client:playerData', target, targetPlayer) elseif action == 'kick' then local reason = cleanReason(payload.reason) DropPlayer(target, ('Du wurdest von einem Administrator getrennt.\nGrund: %s'):format(reason)) elseif action == 'ban' then local reason = cleanReason(payload.reason) local minutes = math.max(0, math.floor(tonumber(payload.minutes) or 0)) local license = exports.devx_core:GetLicense(target) if not license then return { ok = false, error = 'Spielerlizenz konnte nicht ermittelt werden.' } end local expiresAt = nil if minutes > 0 then expiresAt = os.date('!%Y-%m-%d %H:%M:%S', os.time() + minutes * 60) end MySQL.insert.await([[ INSERT INTO devx_bans (license, player_name, reason, banned_by, expires_at) VALUES (?, ?, ?, ?, ?) ]], { license, targetPlayer and targetPlayer.fullName or GetPlayerName(target), reason, GetPlayerName(source) or ('Admin %s'):format(source), expiresAt }) DropPlayer(target, ('Du wurdest von DevX-City gesperrt.\nGrund: %s'):format(reason)) else return { ok = false, error = 'Unbekannte Adminaktion.' } end return { ok = true, players = playerRows() } end) AddEventHandler('playerDropped', function() frozenPlayers[source] = nil end)