-- ===================================================================== -- Aether Bank — Serverlogik -- Alle Geldbewegungen und die PIN-Pruefung laufen ausschliesslich hier. -- Die PIN verlaesst den Server niemals. -- ===================================================================== local Core = exports['aether-core'] -- Wie viele Buchungen an die Oberflaeche geschickt werden local MAX_BUCHUNGEN = 60 -- Hoechstbetrag je Einzelvorgang (Schutz vor absurden Eingaben) local MAX_BETRAG = 10000000 -- Merkt sich, wer sich an einem Automaten bereits mit PIN angemeldet hat local angemeldet = {} -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Liefert die Charakter-ID eines Spielers --- @param quelle number Server-ID --- @return number|nil local function CharakterVon(quelle) return Core:HoleCharakterId(quelle) end --- Prueft, ob ein Betrag eine gueltige, positive Ganzzahl ist --- @param betrag any Eingabe --- @return number|nil Bereinigter Betrag oder nil local function PruefeBetrag(betrag) if type(betrag) ~= 'number' then return nil end betrag = math.floor(betrag) if betrag < 1 or betrag > MAX_BETRAG then return nil end return betrag end --- Ordnet einer Buchungsart Symbol und Kategorie zu (nur fuer die Anzeige) --- @param art string Buchungsart aus der Datenbank --- @return string Symbol, string Kategorie local function Darstellung(art) local tabelle = { deposit = { '💵', 'Einzahlung' }, withdraw = { '🏧', 'Auszahlung' }, transfer = { '↗', 'Transfer' }, salary = { '💼', 'Einkommen' }, fine = { '⚖️', 'Strafe' }, } local eintrag = tabelle[art] or { '•', 'Sonstiges' } return eintrag[1], eintrag[2] end --- Laedt die letzten Buchungen eines Charakters fuer die Anzeige --- @param charakterId number Charakter-ID --- @return table Liste von Buchungen local function HoleBuchungen(charakterId) local zeilen = MySQL.query.await([[ SELECT type, amount, balance, label, created_at FROM aether_transactions WHERE `character` = ? ORDER BY id DESC LIMIT ? ]], { charakterId, MAX_BUCHUNGEN }) or {} local liste = {} for _, zeile in ipairs(zeilen) do local icon, kategorie = Darstellung(zeile.type) liste[#liste + 1] = { titel = zeile.label ~= '' and zeile.label or kategorie, betrag = zeile.amount, saldo = zeile.balance, zeit = tostring(zeile.created_at):sub(1, 16), kat = kategorie, icon = icon, } end return liste end --- Baut den Datensatz, den die Oberflaeche anzeigt (ohne PIN!) --- @param quelle number Server-ID --- @return table|nil local function BaueKonto(quelle) local charakterId = CharakterVon(quelle) if not charakterId then return nil end local daten = Core:HoleSpielerInfo(quelle) if not daten then return nil end return { inhaber = (daten.vorname or '') .. ' ' .. (daten.nachname or ''), iban = daten.iban or '—', saldo = daten.konto or 0, bargeld = daten.bargeld or 0, transaktionen = HoleBuchungen(charakterId), } end --- Schickt den aktuellen Kontostand an die Oberflaeche --- @param quelle number Server-ID --- @param hinweis string|nil Rueckmeldung local function SendeKonto(quelle, hinweis) local konto = BaueKonto(quelle) if not konto then return end konto.hinweis = hinweis TriggerClientEvent('aether:bank:konto', quelle, konto) end --- Lehnt eine Aktion mit Begruendung ab local function Ablehnen(quelle, grund) TriggerClientEvent('aether:bank:fehler', quelle, grund) end -- --------------------------------------------------------------------- -- Automat oeffnen -- --------------------------------------------------------------------- RegisterNetEvent('aether:bank:oeffnen', function() local quelle = source if not CharakterVon(quelle) then return end angemeldet[quelle] = false -- jede Sitzung beginnt mit PIN-Eingabe TriggerClientEvent('aether:bank:zeige', quelle) end) -- --------------------------------------------------------------------- -- PIN pruefen -- --------------------------------------------------------------------- RegisterNetEvent('aether:bank:pinPruefen', function(pin) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId or type(pin) ~= 'string' then return end local gespeichert = MySQL.scalar.await( 'SELECT bank_pin FROM aether_characters WHERE id = ?', { charakterId }) if gespeichert ~= pin then angemeldet[quelle] = false TriggerClientEvent('aether:bank:pinErgebnis', quelle, false) return end angemeldet[quelle] = true TriggerClientEvent('aether:bank:pinErgebnis', quelle, true, BaueKonto(quelle)) end) -- --------------------------------------------------------------------- -- PIN aendern -- --------------------------------------------------------------------- RegisterNetEvent('aether:bank:pinAendern', function(alt, neu) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if type(alt) ~= 'string' or type(neu) ~= 'string' then return end -- Neue PIN muss genau vier Ziffern haben if not neu:match('^%d%d%d%d$') then Ablehnen(quelle, 'Die neue PIN muss aus vier Ziffern bestehen.') return end local gespeichert = MySQL.scalar.await( 'SELECT bank_pin FROM aether_characters WHERE id = ?', { charakterId }) if gespeichert ~= alt then Ablehnen(quelle, 'Die aktuelle PIN ist falsch.') return end MySQL.update.await('UPDATE aether_characters SET bank_pin = ? WHERE id = ?', { neu, charakterId }) TriggerClientEvent('aether:bank:erfolg', quelle, 'PIN geändert', 'Deine neue PIN ist ab sofort gültig.') end) -- --------------------------------------------------------------------- -- Auszahlen (Konto -> Bargeld) -- --------------------------------------------------------------------- RegisterNetEvent('aether:bank:auszahlen', function(betrag) local quelle = source if not angemeldet[quelle] then return end betrag = PruefeBetrag(betrag) if not betrag then Ablehnen(quelle, 'Ungültiger Betrag.') return end -- Erst abbuchen (prueft die Deckung), dann als Bargeld gutschreiben if not Core:EntferneGeld(quelle, 'konto', betrag, 'Barauszahlung Automat') then Ablehnen(quelle, 'Nicht genügend Guthaben.') return end Core:FuegeGeldHinzu(quelle, 'bargeld', betrag, 'Barauszahlung') SendeKonto(quelle) TriggerClientEvent('aether:bank:erfolg', quelle, 'Bitte entnehmen Sie Ihr Geld', betrag .. ' € wurden ausgezahlt.') end) -- --------------------------------------------------------------------- -- Einzahlen (Bargeld -> Konto) -- --------------------------------------------------------------------- RegisterNetEvent('aether:bank:einzahlen', function(betrag) local quelle = source if not angemeldet[quelle] then return end betrag = PruefeBetrag(betrag) if not betrag then Ablehnen(quelle, 'Ungültiger Betrag.') return end if not Core:EntferneGeld(quelle, 'bargeld', betrag, 'Bareinzahlung') then Ablehnen(quelle, 'Du hast nicht so viel Bargeld dabei.') return end Core:FuegeGeldHinzu(quelle, 'konto', betrag, 'Bareinzahlung Automat') SendeKonto(quelle) TriggerClientEvent('aether:bank:erfolg', quelle, 'Einzahlung erfolgreich', betrag .. ' € wurden gutgeschrieben.') end) -- --------------------------------------------------------------------- -- Ueberweisen (Konto -> fremdes Konto, auch offline) -- --------------------------------------------------------------------- RegisterNetEvent('aether:bank:ueberweisen', function(ziel, betrag, zweck) local quelle = source if not angemeldet[quelle] then return end local charakterId = CharakterVon(quelle) if not charakterId then return end betrag = PruefeBetrag(betrag) if not betrag then Ablehnen(quelle, 'Ungültiger Betrag.') return end if type(ziel) ~= 'string' or #ziel < 3 then Ablehnen(quelle, 'Bitte eine gültige IBAN angeben.') return end zweck = type(zweck) == 'string' and zweck:sub(1, 64) or '' -- Empfaenger ueber die IBAN suchen local empfaenger = MySQL.query.await([[ SELECT id, firstname, lastname, bank FROM aether_characters WHERE iban = ? AND deleted = 0 LIMIT 1 ]], { ziel }) if not empfaenger or not empfaenger[1] then Ablehnen(quelle, 'Kein Konto mit dieser IBAN gefunden.') return end local ziel_ = empfaenger[1] if ziel_.id == charakterId then Ablehnen(quelle, 'Du kannst dir nicht selbst überweisen.') return end local empfaengerName = ziel_.firstname .. ' ' .. ziel_.lastname local verwendung = zweck ~= '' and (' (' .. zweck .. ')') or '' -- Abbuchen (prueft die Deckung) if not Core:EntferneGeld(quelle, 'konto', betrag, 'Überweisung an ' .. empfaengerName .. verwendung) then Ablehnen(quelle, 'Nicht genügend Guthaben.') return end -- Gutschrift: bei Online-Spielern ueber den Core, sonst direkt in der Datenbank local zielQuelle = Core:HoleQuelleNachCharakter(ziel_.id) if zielQuelle then Core:FuegeGeldHinzu(zielQuelle, 'konto', betrag, 'Überweisung von Konto' .. verwendung) TriggerClientEvent('aether:bank:benachrichtigung', zielQuelle, ('Du hast %d € erhalten.'):format(betrag)) else local neuerStand = (ziel_.bank or 0) + betrag MySQL.update.await('UPDATE aether_characters SET bank = ? WHERE id = ?', { neuerStand, ziel_.id }) MySQL.insert('INSERT INTO aether_transactions (`character`, type, amount, balance, label) VALUES (?, ?, ?, ?, ?)', { ziel_.id, 'transfer', betrag, neuerStand, 'Überweisung erhalten' .. verwendung }) end Aether.Info(('Überweisung: Charakter #%d -> #%d, %d €'):format(charakterId, ziel_.id, betrag)) SendeKonto(quelle) TriggerClientEvent('aether:bank:erfolg', quelle, 'Überweisung ausgeführt', ('%d € an %s gesendet.'):format(betrag, empfaengerName)) end) -- --------------------------------------------------------------------- -- Aufraeumen -- --------------------------------------------------------------------- RegisterNetEvent('aether:bank:schliessen', function() angemeldet[source] = nil end) AddEventHandler('playerDropped', function() angemeldet[source] = nil end)