-- ===================================================================== -- 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)