-- ===================================================================== -- aether-admin / Modul: Spieler -- -- Die Liste der Online-Spieler und alle Eingriffe an ihnen. Nichts -- davon vertraut der Oberfläche: jeder Aufruf ist serverseitig -- rechtegeprüft, Zielspieler werden über ihre echte Server-ID -- aufgelöst, und alles Eingreifende landet im Prüfprotokoll. -- ===================================================================== Admin = Admin or {} Admin.Live = Admin.Live or { pos = {}, god = {}, frozen = {}, muted = {} } -- Grobe Ortsangabe aus Koordinaten (Annäherung, rein informativ) local ORTE = { { name = 'Legion Square', x = 195.0, y = -930.0 }, { name = 'Mission Row', x = 428.0, y = -984.0 }, { name = 'Vinewood', x = 300.0, y = 200.0 }, { name = 'Vespucci', x = -1230.0, y = -1500.0 }, { name = 'Del Perro', x = -1600.0, y = -300.0 }, { name = 'Sandy Shores', x = 1960.0, y = 3740.0 }, { name = 'Paleto Bay', x = -100.0, y = 6330.0 }, { name = 'LS Airport', x = -1030.0, y = -2730.0 }, { name = 'Grove Street', x = 90.0, y = -1930.0 }, } --- Nächstgelegener benannter Ort zu einer Position --- @param pos table|nil { x, y } --- @return string local function OrtVon(pos) if not pos or not pos.x then return '—' end local beste, abstand = '—', math.huge for _, ort in ipairs(ORTE) do local d = (ort.x - pos.x) ^ 2 + (ort.y - pos.y) ^ 2 if d < abstand then abstand, beste = d, ort.name end end return beste end --- Verwarnungen eines Benutzers zählen --- @param lizenz string|nil --- @return number local function WarnAnzahl(lizenz) if not lizenz then return 0 end return tonumber(MySQL.scalar.await( 'SELECT COUNT(*) FROM aether_admin_warns WHERE license = ?', { lizenz })) or 0 end --- Baut den Anzeigedatensatz eines Online-Spielers --- @param serverId number --- @param daten table Spielerdaten aus aether-core --- @return table local function SpielerZeile(serverId, daten) local lizenz = Admin.Auth.Lizenz(serverId) return { id = serverId, name = (daten.vorname or '?') .. ' ' .. (daten.nachname or ''), charakterId = daten.charakterId, job = daten.job or 'unemployed', jobRang = daten.jobRang or 0, fraktion = daten.fraktion, cash = math.floor(daten.bargeld or 0), bank = math.floor(daten.konto or 0), ping = GetPlayerPing(serverId) or 0, region = OrtVon(daten.position), leben = daten.leben or 200, istTot = daten.istTot and true or false, warns = WarnAnzahl(lizenz), god = Admin.Live.god[serverId] and true or false, frozen = Admin.Live.frozen[serverId] and true or false, muted = Admin.Live.muted[serverId] and true or false, } end --- Sammelt alle Online-Spieler --- @return table Liste function Admin.SpielerListe() local alle = exports['aether-core']:HoleAlleSpieler() or {} local liste = {} for serverId, daten in pairs(alle) do liste[#liste + 1] = SpielerZeile(tonumber(serverId), daten) end table.sort(liste, function(a, b) return a.id < b.id end) return liste end -- ===================================================================== -- LISTE & DETAILS -- ===================================================================== Admin.Kern.Registriere('spieler', 'liste', function(handelnder, daten) return { spieler = Admin.SpielerListe(), rechte = Admin.Kern.RechteAbbild(handelnder.quelle), jobs = { 'unemployed','police','ambulance','mechanic','taxi','trucker','lawyer','reporter','barkeeper' }, } end) Admin.Kern.Registriere('spieler', 'details', function(handelnder, daten) local serverId = tonumber(daten.id) if not serverId then return nil, 'Ungültige Spieler-ID.' end local info = exports['aether-core']:HoleSpielerInfo(serverId) if not info then return nil, 'Spieler ist nicht (mehr) online.' end local lizenz = Admin.Auth.Lizenz(serverId) local warns = lizenz and MySQL.query.await([[ SELECT id, grund, admin_name, erstellt_am FROM aether_admin_warns WHERE license = ? ORDER BY id DESC LIMIT 25 ]], { lizenz }) or {} local notizen = lizenz and MySQL.query.await([[ SELECT id, notiz, admin_name, erstellt_am FROM aether_admin_notes WHERE license = ? ORDER BY id DESC LIMIT 25 ]], { lizenz }) or {} return { spieler = SpielerZeile(serverId, info), lizenz = lizenz, gruppe = Admin.Auth.Gruppe(serverId), spielzeit = info.spielzeit, warns = warns, notizen = notizen, rechte = Admin.Kern.RechteAbbild(handelnder.quelle), jobs = { 'unemployed','police','ambulance','mechanic','taxi','trucker','lawyer','reporter','barkeeper' }, } end) -- ===================================================================== -- TELEPORT & ANSICHT -- ===================================================================== --- Auflösung: Position eines Spielers (aus dem Live-Cache) local function PosVon(serverId) return Admin.Live.pos[serverId] end Admin.Kern.Registriere('spieler', 'goto', function(handelnder, daten) local ziel = tonumber(daten.id) local pos = PosVon(ziel) if not pos then return nil, 'Position des Ziels ist noch nicht bekannt.' end TriggerClientEvent('aether-admin:client:teleport', handelnder.quelle, pos) Admin.Log.Schreibe(handelnder, 'Zu Spieler teleportiert', GetPlayerName(ziel)) return { ok = true } end) Admin.Kern.Registriere('spieler', 'bring', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local pos = PosVon(handelnder.quelle) if not pos then return nil, 'Deine eigene Position ist noch nicht bekannt.' end TriggerClientEvent('aether-admin:client:teleport', ziel, pos) Admin.Log.Schreibe(handelnder, 'Spieler hergeholt', GetPlayerName(ziel)) return { ok = true } end) Admin.Kern.Registriere('spieler', 'spectate', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end TriggerClientEvent('aether-admin:client:spectate', handelnder.quelle, { ziel = ziel, pos = PosVon(ziel), }) Admin.Log.Schreibe(handelnder, 'Beobachtet', GetPlayerName(ziel)) return { ok = true } end) -- ===================================================================== -- ZUSTAND -- ===================================================================== --- Kleiner Baukasten für einfache „an Ziel weiterreichen"-Aktionen --- @param aktion string Registrierter Aktionsname --- @param event string Client-Event am Ziel --- @param protokoll string Protokolltext local function ZielAktion(aktion, event, protokoll) Admin.Kern.Registriere('spieler', aktion, function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end TriggerClientEvent(event, ziel) Admin.Log.Schreibe(handelnder, protokoll, GetPlayerName(ziel)) return { ok = true } end) end ZielAktion('heal', 'aether-admin:client:heal', 'Spieler geheilt') ZielAktion('revive', 'aether-admin:client:revive', 'Spieler wiederbelebt') ZielAktion('armor', 'aether-admin:client:armor', 'Rüstung gegeben') --- Umschalt-Aktion mit gemerktem Zustand (Freeze/God/Mute) --- @param aktion string --- @param feld string Schlüssel in Admin.Live --- @param event string|nil Client-Event am Ziel (nil = nur serverseitig, z. B. Mute) --- @param nomen string Für das Protokoll local function UmschaltAktion(aktion, feld, event, nomen) Admin.Kern.Registriere('spieler', aktion, function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local neu = not Admin.Live[feld][ziel] Admin.Live[feld][ziel] = neu or nil if event then TriggerClientEvent(event, ziel, neu) end Admin.Log.Schreibe(handelnder, nomen .. (neu and ' an' or ' aus'), GetPlayerName(ziel)) return { ok = true, aktiv = neu } end) end UmschaltAktion('freeze', 'frozen', 'aether-admin:client:freeze', 'Einfrieren') UmschaltAktion('god', 'god', 'aether-admin:client:god', 'Godmode') UmschaltAktion('mute', 'muted', nil, 'Stummschaltung') -- ===================================================================== -- GEBEN & SETZEN -- ===================================================================== Admin.Kern.Registriere('spieler', 'geld', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local konto = daten.konto == 'cash' and 'cash' or 'bank' local betrag = math.floor(tonumber(daten.betrag) or 0) if betrag == 0 then return nil, 'Bitte einen Betrag angeben.' end local ok if betrag > 0 then ok = exports['aether-core']:FuegeGeldHinzu(ziel, konto, betrag, 'Admin: ' .. handelnder.name) else ok = exports['aether-core']:EntferneGeld(ziel, konto, -betrag, 'Admin: ' .. handelnder.name) end if not ok then return nil, 'Nicht möglich (Deckung?).' end Admin.Log.Schreibe(handelnder, 'Geld ' .. (betrag > 0 and '+' or '') .. betrag, GetPlayerName(ziel), konto) return { ok = true } end) Admin.Kern.Registriere('spieler', 'item', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local item = tostring(daten.item or '') local menge = math.max(1, math.floor(tonumber(daten.menge) or 1)) if item == '' then return nil, 'Kein Item gewählt.' end -- Falls ein Inventar-Resource läuft, dorthin geben; sonst nur vermerken local ok = pcall(function() return exports['aether-inventory']:GebeItem(ziel, item, menge) end) Admin.Log.Schreibe(handelnder, 'Item gegeben', GetPlayerName(ziel), menge .. '× ' .. item) return { ok = true, zugestellt = ok } end) Admin.Kern.Registriere('spieler', 'fahrzeug', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local modell = tostring(daten.modell or ''):gsub('%s', '') if modell == '' then return nil, 'Kein Modell gewählt.' end TriggerClientEvent('aether-admin:client:fahrzeug', ziel, modell) Admin.Log.Schreibe(handelnder, 'Fahrzeug gespawnt', GetPlayerName(ziel), modell) return { ok = true } end) Admin.Kern.Registriere('spieler', 'job', function(handelnder, daten) local ziel = tonumber(daten.id) local charakterId = tonumber(daten.charakterId) if not charakterId then return nil, 'Charakter unbekannt.' end local job = tostring(daten.job or 'unemployed') local fraktion = daten.fraktion ~= '' and tostring(daten.fraktion) or nil MySQL.update.await( 'UPDATE aether_characters SET job = ?, faction = ? WHERE id = ?', { job, fraktion, charakterId }) -- Falls online, den Client informieren (greift spätestens beim Nachladen) if ziel and GetPlayerName(ziel) then TriggerClientEvent('aether-admin:client:hinweis', ziel, 'Deine Tätigkeit wurde vom Team angepasst: ' .. job) end Admin.Log.Schreibe(handelnder, 'Job/Fraktion gesetzt', GetPlayerName(ziel) or ('Charakter #' .. charakterId), job .. (fraktion and (' / ' .. fraktion) or '')) return { ok = true } end) -- ===================================================================== -- MODERATION AM SPIELER -- ===================================================================== Admin.Kern.Registriere('spieler', 'warn', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local grund = tostring(daten.grund or ''):sub(1, 255) if #grund < 2 then return nil, 'Bitte einen Grund angeben.' end local lizenz = Admin.Auth.Lizenz(ziel) local charakterId = exports['aether-core']:HoleCharakterId(ziel) MySQL.insert.await([[ INSERT INTO aether_admin_warns (license, `character`, name, grund, admin_license, admin_name) VALUES (?, ?, ?, ?, ?, ?) ]], { lizenz, charakterId, GetPlayerName(ziel), grund, handelnder.lizenz, handelnder.name }) TriggerClientEvent('aether-admin:client:hinweis', ziel, '⚠️ Verwarnung: ' .. grund) Admin.Log.Schreibe(handelnder, 'Verwarnung', GetPlayerName(ziel), grund) return { ok = true } end) Admin.Kern.Registriere('spieler', 'notiz', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local text = tostring(daten.text or ''):sub(1, 1000) if #text < 2 then return nil, 'Bitte einen Text eingeben.' end MySQL.insert.await([[ INSERT INTO aether_admin_notes (license, name, notiz, admin_name) VALUES (?, ?, ?, ?) ]], { Admin.Auth.Lizenz(ziel), GetPlayerName(ziel), text, handelnder.name }) Admin.Log.Schreibe(handelnder, 'Notiz', GetPlayerName(ziel)) return { ok = true } end) Admin.Kern.Registriere('spieler', 'kick', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local grund = tostring(daten.grund or 'Kein Grund angegeben'):sub(1, 255) local name = GetPlayerName(ziel) DropPlayer(ziel, 'Vom Team gekickt: ' .. grund) Admin.Log.Schreibe(handelnder, 'Kick', name, grund) return { ok = true } end) Admin.Kern.Registriere('spieler', 'ban', function(handelnder, daten) local ziel = tonumber(daten.id) if not GetPlayerName(ziel) then return nil, 'Ziel ist offline.' end local grund = tostring(daten.grund or 'Regelbruch'):sub(1, 255) local dauer = tostring(daten.dauer or 'permanent') local name = GetPlayerName(ziel) local lizenz = Admin.Auth.Lizenz(ziel) -- Permanente Bans darf nur, wer 'ban' als owner-Recht hat — hier reicht das -- Aktionsrecht; die Rechte-Matrix regelt, ob überhaupt gebannt werden darf. local bis = Admin.BerechneBanEnde(dauer) Admin.DB.Sperre(lizenz, name, grund, bis, handelnder.lizenz, handelnder.name) DropPlayer(ziel, 'Gebannt: ' .. grund) Admin.Log.Schreibe(handelnder, 'Ban (' .. dauer .. ')', name, grund) return { ok = true } end) --- Rechnet eine Dauer-Angabe in ein DATETIME um (oder nil = permanent) --- @param dauer string --- @return string|nil function Admin.BerechneBanEnde(dauer) local tage = ({ ['1 Tag'] = 1, ['3 Tage'] = 3, ['7 Tage'] = 7, ['30 Tage'] = 30 })[dauer] if not tage then return nil end return os.date('%Y-%m-%d %H:%M:%S', os.time() + tage * 86400) end