aether-framework/resources/[aether]/aether-character/server/main.lua
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

222 lines
8.7 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =====================================================================
-- Aether Character — Serverlogik
-- Liefert die Charakterliste, legt Charaktere an, loescht sie und
-- uebergibt die Auswahl an den Core.
--
-- Grundsatz: Alles, was vom Client kommt, wird hier geprueft.
-- =====================================================================
local Core = exports['aether-core']
-- ---------------------------------------------------------------------
-- Hilfsfunktionen
-- ---------------------------------------------------------------------
--- Liest die Rockstar-Lizenz eines Spielers aus
--- @param quelle number Server-ID
--- @return string|nil
local function HoleLizenz(quelle)
local lizenz = GetPlayerIdentifierByType(quelle, 'license')
if not lizenz then return nil end
return lizenz:gsub('license:', '')
end
--- Wandelt ein Datum von TT.MM.JJJJ nach JJJJ-MM-TT (Datenbankformat)
--- @param text string Eingabe des Spielers
--- @return string|nil Datum im DB-Format oder nil bei ungueltiger Eingabe
local function DatumFuerDatenbank(text)
if type(text) ~= 'string' then return nil end
local tag, monat, jahr = text:match('^(%d%d)%.(%d%d)%.(%d%d%d%d)$')
if not tag then return nil end
tag, monat, jahr = tonumber(tag), tonumber(monat), tonumber(jahr)
-- Grobe Plausibilitaetspruefung
if monat < 1 or monat > 12 or tag < 1 or tag > 31 then return nil end
if jahr < 1900 or jahr > 2010 then return nil end
return ('%04d-%02d-%02d'):format(jahr, monat, tag)
end
--- Wandelt ein Datum von JJJJ-MM-TT nach TT.MM.JJJJ (Anzeige)
--- @param wert any Wert aus der Datenbank
--- @return string
local function DatumFuerAnzeige(wert)
local text = tostring(wert or '')
local jahr, monat, tag = text:match('^(%d%d%d%d)-(%d%d)-(%d%d)')
if not jahr then return '—' end
return ('%s.%s.%s'):format(tag, monat, jahr)
end
--- Kuerzt und saeubert einen Namen (nur Buchstaben, Leerzeichen, Bindestrich)
--- @param text any Eingabe
--- @return string|nil Bereinigter Name oder nil, wenn ungueltig
local function SaeubereName(text)
if type(text) ~= 'string' then return nil end
local name = text:gsub('^%s+', ''):gsub('%s+$', '')
if #name < 2 or #name > 32 then return nil end
-- Nur Buchstaben (inkl. Umlaute), Leerzeichen und Bindestriche zulassen
if name:find('[^%a%sÄÖÜäöüß%-]') then return nil end
return name
end
--- Baut die Charakterliste in dem Format, das die Oberflaeche erwartet
--- @param lizenz string Rockstar-Lizenz
--- @return table Liste fuer die NUI
local function BaueListe(lizenz)
local datensaetze = MySQL.query.await([[
SELECT id, firstname, lastname, gender, dob, nationality,
job, cash, bank, playtime, appearance, last_played
FROM aether_characters
WHERE license = ? AND deleted = 0
ORDER BY last_played DESC
]], { lizenz }) or {}
local liste = {}
for _, datensatz in ipairs(datensaetze) do
-- Aussehen aus JSON zurueckwandeln
local aussehen = nil
if datensatz.appearance then
local erfolg, entschluesselt = pcall(json.decode, datensatz.appearance)
if erfolg then aussehen = entschluesselt end
end
liste[#liste + 1] = {
id = datensatz.id,
vorname = datensatz.firstname,
nachname = datensatz.lastname,
geschlecht = datensatz.gender,
geburt = DatumFuerAnzeige(datensatz.dob),
herkunft = datensatz.nationality,
job = datensatz.job,
geld = (datensatz.cash or 0) + (datensatz.bank or 0),
stunden = math.floor((datensatz.playtime or 0) / 60),
zuletzt = tostring(datensatz.last_played or ''):sub(1, 16),
aussehen = aussehen,
}
end
return liste
end
--- Schickt die aktuelle Liste an einen Spieler
--- @param quelle number Server-ID
--- @param hinweis string|nil Kurze Rueckmeldung fuer die Oberflaeche
local function SendeListe(quelle, hinweis)
local lizenz = HoleLizenz(quelle)
if not lizenz then return end
TriggerClientEvent('aether:character:liste', quelle,
BaueListe(lizenz), Config.MaxCharaktere, hinweis)
end
-- ---------------------------------------------------------------------
-- Auswahl anzeigen (wird vom Core ausgeloest)
-- ---------------------------------------------------------------------
AddEventHandler('aether:server:charakterAuswahlNoetig', function(quelle)
local lizenz = HoleLizenz(quelle)
if not lizenz then return end
TriggerClientEvent('aether:character:zeigeAuswahl', quelle,
BaueListe(lizenz), Config.MaxCharaktere)
end)
-- ---------------------------------------------------------------------
-- Charakter waehlen
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:character:spielen', function(charakterId)
local quelle = source
local lizenz = HoleLizenz(quelle)
if not lizenz or type(charakterId) ~= 'number' then return end
-- Gehoert der Charakter wirklich diesem Spieler?
local treffer = MySQL.query.await(
'SELECT id FROM aether_characters WHERE id = ? AND license = ? AND deleted = 0',
{ charakterId, lizenz })
if not treffer or not treffer[1] then
TriggerClientEvent('aether:character:fehler', quelle, 'Dieser Charakter gehört dir nicht.')
return
end
-- Core laedt den Charakter und spawnt den Spieler
local erfolg = Core:LadeCharakter(quelle, charakterId)
if not erfolg then
TriggerClientEvent('aether:character:fehler', quelle, 'Charakter konnte nicht geladen werden.')
end
end)
-- ---------------------------------------------------------------------
-- Charakter erstellen
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:character:erstellen', function(daten)
local quelle = source
local lizenz = HoleLizenz(quelle)
if not lizenz or type(daten) ~= 'table' then return end
-- Slotgrenze pruefen
local vorhandene = MySQL.scalar.await(
'SELECT COUNT(*) FROM aether_characters WHERE license = ? AND deleted = 0', { lizenz })
if (vorhandene or 0) >= Config.MaxCharaktere then
TriggerClientEvent('aether:character:fehler', quelle,
('Du hast bereits %d Charaktere.'):format(Config.MaxCharaktere))
return
end
-- Namen pruefen
local vorname = SaeubereName(daten.vorname)
local nachname = SaeubereName(daten.nachname)
if not vorname or not nachname then
TriggerClientEvent('aether:character:fehler', quelle,
'Bitte gültige Vor- und Nachnamen angeben (nur Buchstaben, 2–32 Zeichen).')
return
end
-- Geburtsdatum pruefen
local geburt = DatumFuerDatenbank(daten.geburt)
if not geburt then
TriggerClientEvent('aether:character:fehler', quelle,
'Ungültiges Geburtsdatum. Format: TT.MM.JJJJ (Jahr 1900–2010).')
return
end
-- Geschlecht auf erlaubte Werte begrenzen
local geschlecht = daten.geschlecht
if geschlecht ~= 'männlich' and geschlecht ~= 'weiblich' and geschlecht ~= 'divers' then
geschlecht = 'männlich'
end
-- Charakter anlegen (eigene Abfrage, da `Aether.DB` im Core-State liegt)
local neueId = MySQL.insert.await([[
INSERT INTO aether_characters
(license, firstname, lastname, dob, gender, nationality, appearance,
cash, bank, iban, job, job_grade)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
]], {
lizenz, vorname, nachname, geburt, geschlecht,
tostring(daten.herkunft or 'Los Santos'):sub(1, 48),
type(daten.aussehen) == 'table' and json.encode(daten.aussehen) or nil,
Config.StartWerte.bargeld,
Config.StartWerte.konto,
Aether.ErzeugeIban(),
Config.StartWerte.job,
Config.StartWerte.jobRang,
})
Aether.Info(('Charakter #%d erstellt: %s %s'):format(neueId, vorname, nachname))
SendeListe(quelle, ('%s %s wurde erstellt.'):format(vorname, nachname))
end)
-- ---------------------------------------------------------------------
-- Charakter loeschen
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:character:loeschen', function(charakterId)
local quelle = source
local lizenz = HoleLizenz(quelle)
if not lizenz or type(charakterId) ~= 'number' then return end
-- Weich loeschen; die Lizenzpruefung in der Abfrage sichert das Eigentum ab
MySQL.update.await(
'UPDATE aether_characters SET deleted = 1 WHERE id = ? AND license = ?',
{ charakterId, lizenz })
Aether.Info(('Charakter #%d geloescht (Lizenz %s).'):format(charakterId, lizenz))
SendeListe(quelle, 'Charakter gelöscht.')
end)