-- ===================================================================== -- aether-police / Modul: Strafenkatalog & Anzeigen -- -- Der Katalog ist ein NACHSCHLAGEWERK. Er berechnet einen Vorschlag — -- entschieden wird von einem Beamten. Deshalb speichert jede Anzeige -- beides getrennt: -- haft_empfohlen / geld_empfohlen = Vorschlag des Katalogs -- haft_final / geld_final = Entscheidung des Beamten -- Weicht der Beamte ab, ist eine Begründung Pflicht. -- ===================================================================== Kern.RegistriereModul({ id = 'anzeige', name = 'Anzeigen', icon = '⚖', reihenfolge = 4, recht = 'anzeige.erstellen', beschreibung = 'Strafenkatalog und Anzeigenerfassung', }) -- --------------------------------------------------------------------- -- Grundbestand des Katalogs -- Wird nur angelegt, wenn die Tabelle leer ist. Danach pflegt ihn die -- Administration — hier wird nichts mehr überschrieben. -- --------------------------------------------------------------------- local GRUNDKATALOG = { -- kennung, kategorie, bezeichnung, paragraph, haft (Min), geld, hinweis { 'v1', 'Verkehr', 'Überhöhte Geschwindigkeit', '§ 3 StVO', 0, 450, 'Bei erheblicher Überschreitung Fahrverbot prüfen.' }, { 'v2', 'Verkehr', 'Fahren ohne Fahrerlaubnis', '§ 21 StVG', 10, 1200, 'Führerscheinstatus im MDT prüfen.' }, { 'v3', 'Verkehr', 'Unerlaubtes Entfernen vom Unfallort', '§ 142 StGB', 30, 5000, nil }, { 'v4', 'Verkehr', 'Trunkenheit im Verkehr', '§ 316 StGB', 20, 3500, 'Bei Gefährdung § 315c prüfen.' }, { 'v5', 'Verkehr', 'Verbotenes Kraftfahrzeugrennen', '§ 315d StGB', 45, 9000, nil }, { 'e1', 'Eigentum', 'Diebstahl (geringwertig)', '§ 242 StGB', 15, 800, nil }, { 'e2', 'Eigentum', 'Einbruchdiebstahl', '§ 243 StGB', 45, 6000, nil }, { 'e3', 'Eigentum', 'Sachbeschädigung', '§ 303 StGB', 10, 1500, nil }, { 'e4', 'Eigentum', 'Hehlerei', '§ 259 StGB', 25, 4000, nil }, { 'g1', 'Gewalt', 'Körperverletzung', '§ 223 StGB', 45, 8000, nil }, { 'g2', 'Gewalt', 'Gefährliche Körperverletzung', '§ 224 StGB', 90, 15000, nil }, { 'g3', 'Gewalt', 'Raub', '§ 249 StGB', 120, 25000, nil }, { 'g4', 'Gewalt', 'Widerstand gegen Vollstreckungsbeamte', '§ 113 StGB', 30, 4000, nil }, { 'g5', 'Gewalt', 'Bedrohung', '§ 241 StGB', 20, 2500, nil }, { 'g6', 'Gewalt', 'Freiheitsberaubung', '§ 239 StGB', 60, 9000, nil }, { 'w1', 'Waffen', 'Unerlaubter Waffenbesitz', '§ 52 WaffG', 60, 10000, 'Waffenschein im MDT prüfen — ggf. Einziehung dokumentieren.' }, { 'w2', 'Waffen', 'Führen einer Waffe ohne Erlaubnis', '§ 52 WaffG', 30, 5000, nil }, { 'b1', 'Betäubungsmittel', 'Besitz von Betäubungsmitteln', '§ 29 BtMG', 25, 3000, nil }, { 'b2', 'Betäubungsmittel', 'Handel mit Betäubungsmitteln', '§ 29a BtMG', 110, 20000, nil }, { 's1', 'Sonstige', 'Hausfriedensbruch', '§ 123 StGB', 15, 1500, nil }, { 's2', 'Sonstige', 'Beleidigung', '§ 185 StGB', 0, 800, nil }, { 's3', 'Sonstige', 'Falsche Verdächtigung', '§ 164 StGB', 30, 4500, nil }, { 's4', 'Sonstige', 'Urkundenfälschung', '§ 267 StGB', 40, 6000, nil }, } CreateThread(function() Wait(3000) local anzahl = MySQL.scalar.await('SELECT COUNT(*) FROM police_katalog') or 0 if anzahl > 0 then return end for reihenfolge, eintrag in ipairs(GRUNDKATALOG) do MySQL.insert.await([[ INSERT INTO police_katalog (kennung, kategorie, bezeichnung, paragraph, haft, geld, hinweis, sortierung) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ]], { eintrag[1], eintrag[2], eintrag[3], eintrag[4], eintrag[5], eintrag[6], eintrag[7], reihenfolge }) end Aether.Info(('Strafenkatalog mit %d Einträgen angelegt.'):format(#GRUNDKATALOG)) end) -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Lädt den aktiven Katalog, nach Kategorien gruppiert --- @return table local function LadeKatalog() local eintraege = MySQL.query.await([[ SELECT id, kennung, kategorie, bezeichnung, paragraph, haft, geld, hinweis FROM police_katalog WHERE aktiv = 1 ORDER BY sortierung ASC, id ASC ]]) or {} local gruppen, reihenfolge = {}, {} for _, eintrag in ipairs(eintraege) do if not gruppen[eintrag.kategorie] then gruppen[eintrag.kategorie] = {} reihenfolge[#reihenfolge + 1] = eintrag.kategorie end table.insert(gruppen[eintrag.kategorie], eintrag) end local liste = {} for _, kategorie in ipairs(reihenfolge) do liste[#liste + 1] = { kategorie = kategorie, delikte = gruppen[kategorie] } end return liste end --- Rechnet den Vorschlag für eine Auswahl von Delikten aus. --- Der Katalog ist die einzige Quelle — der Client schickt nur Kennungen. --- @param kennungen table Liste von Katalog-Kennungen --- @return table|nil { delikte, haft, geld }, string|nil Fehler local function BerechneVorschlag(kennungen) if type(kennungen) ~= 'table' or #kennungen == 0 then return nil, 'Bitte mindestens ein Delikt auswählen.' end if #kennungen > 25 then return nil, 'Zu viele Delikte auf einmal.' end local delikte, haft, geld = {}, 0, 0 for _, kennung in ipairs(kennungen) do local eintrag = MySQL.query.await( 'SELECT kennung, bezeichnung, paragraph, haft, geld FROM police_katalog WHERE kennung = ? AND aktiv = 1', { tostring(kennung) }) if not eintrag or not eintrag[1] then return nil, ('Unbekanntes Delikt: %s'):format(tostring(kennung)) end local delikt = eintrag[1] delikte[#delikte + 1] = { kennung = delikt.kennung, name = delikt.bezeichnung, paragraph = delikt.paragraph, haft = delikt.haft, geld = delikt.geld, } haft = haft + (delikt.haft or 0) geld = geld + (delikt.geld or 0) end return { delikte = delikte, haft = haft, geld = geld } end -- ===================================================================== -- KATALOG ABRUFEN -- ===================================================================== Kern.RegistriereAktion('anzeige', 'katalog', nil, function(quelle, beamter) return { katalog = LadeKatalog(), rechte = { bestaetigen = Rechte.Hat(beamter, 'anzeige.bestaetigen'), abweichen = Rechte.Hat(beamter, 'anzeige.abweichen'), bearbeiten = Rechte.Hat(beamter, 'katalog.bearbeiten'), }, } end) --- Berechnet den Vorschlag zu einer Auswahl — reine Vorschau, speichert nichts Kern.RegistriereAktion('anzeige', 'vorschlag', nil, function(quelle, beamter, daten) local vorschlag, fehler = BerechneVorschlag(daten.delikte) if not vorschlag then return nil, fehler end -- Zusätzliche Hinweise der gewählten Delikte mitliefern local hinweise = {} for _, kennung in ipairs(daten.delikte) do local hinweis = MySQL.scalar.await( 'SELECT hinweis FROM police_katalog WHERE kennung = ? AND aktiv = 1', { kennung }) if hinweis and hinweis ~= '' then hinweise[#hinweise + 1] = hinweis end end return { delikte = vorschlag.delikte, haft = vorschlag.haft, geld = vorschlag.geld, hinweise = hinweise, } end) -- ===================================================================== -- ANZEIGE ERFASSEN -- -- Erzeugt einen ENTWURF. Nichts wird eingezogen, niemand kommt in Haft. -- ===================================================================== Kern.RegistriereAktion('anzeige', 'erstellen', 'anzeige.erstellen', function(quelle, beamter, daten) local beschuldigter = tonumber(daten.beschuldigter) if not beschuldigter then return nil, 'Kein Beschuldigter ausgewählt.' end local person = MySQL.scalar.await( 'SELECT id FROM aether_characters WHERE id = ? AND deleted = 0', { beschuldigter }) if not person then return nil, 'Diese Person existiert nicht.' end local vorschlag, fehler = BerechneVorschlag(daten.delikte) if not vorschlag then return nil, fehler end -- Werte des Beamten übernehmen; fehlen sie, gilt der Vorschlag local haftFinal = daten.haft ~= nil and math.max(0, math.min(1440, math.floor(tonumber(daten.haft) or 0))) or vorschlag.haft local geldFinal = daten.geld ~= nil and math.max(0, math.min(10000000, math.floor(tonumber(daten.geld) or 0))) or vorschlag.geld local weichtAb = (haftFinal ~= vorschlag.haft) or (geldFinal ~= vorschlag.geld) local begruendung = type(daten.begruendung) == 'string' and daten.begruendung:gsub('^%s+', ''):gsub('%s+$', '') or '' -- Abweichen ist ein eigenes Recht und verlangt eine Begründung if weichtAb then if not Rechte.Hat(beamter, 'anzeige.abweichen') then return nil, 'Dir fehlt die Berechtigung, vom Katalogvorschlag abzuweichen.' end if #begruendung < 5 then return nil, 'Bei Abweichung vom Vorschlag ist eine Begründung erforderlich.' end end local anzeigeId = MySQL.insert.await([[ INSERT INTO police_anzeige (akte, beschuldigter, delikte, haft_empfohlen, geld_empfohlen, haft_final, geld_final, begruendung, status, erstellt_von) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'entwurf', ?) ]], { tonumber(daten.akte), beschuldigter, json.encode(vorschlag.delikte), vorschlag.haft, vorschlag.geld, haftFinal, geldFinal, begruendung ~= '' and begruendung or nil, beamter.charakterId, }) Protokoll.Schreibe(beamter, 'anzeige', 'Anzeige als Entwurf erfasst', { typ = 'person', id = beschuldigter }, { anzeige = anzeigeId, haft = haftFinal, geld = geldFinal, abweichung = weichtAb }, begruendung ~= '' and begruendung or nil) Kern.Loese('anzeige.erstellt', anzeigeId, beamter) return { id = anzeigeId, entwurf = true } end) -- ===================================================================== -- BESTÄTIGEN -- -- Erst hier wird die Entscheidung des Beamten ausgeführt: Das Bußgeld -- wird eingezogen. Eine Haft entsteht dadurch NICHT — die Einweisung -- in Gewahrsam ist ein eigener, bewusster Schritt. -- ===================================================================== Kern.RegistriereAktion('anzeige', 'bestaetigen', 'anzeige.bestaetigen', function(quelle, beamter, daten) local anzeigeId = tonumber(daten.id) if not anzeigeId then return nil, 'Ungültige Kennung.' end local anzeige = MySQL.query.await('SELECT * FROM police_anzeige WHERE id = ?', { anzeigeId }) if not anzeige or not anzeige[1] then return nil, 'Anzeige nicht gefunden.' end local eintrag = anzeige[1] if eintrag.status ~= 'entwurf' then return nil, 'Diese Anzeige wurde bereits abschließend bearbeitet.' end -- Vier-Augen-Prinzip: Wer erfasst hat, darf nur mit dem Recht -- „bestaetigen" auch selbst bestätigen — das ist hier gegeben, -- wird aber protokolliert. local selbstbestaetigung = eintrag.erstellt_von == beamter.charakterId -- Bußgeld einziehen (erst Bargeld, dann Konto) local Core = exports['aether-core'] local eingezogen = 0 if eintrag.geld_final > 0 then local zielQuelle = Core:HoleQuelleNachCharakter(eintrag.beschuldigter) local bezahlt = false if zielQuelle then bezahlt = Core:EntferneGeld(zielQuelle, 'bargeld', eintrag.geld_final, 'Bußgeld') if not bezahlt then bezahlt = Core:EntferneGeld(zielQuelle, 'konto', eintrag.geld_final, 'Bußgeld') end else -- Offline: direkt vom Konto, auch ins Minus (Forderung bleibt bestehen) MySQL.update.await('UPDATE aether_characters SET bank = bank - ? WHERE id = ?', { eintrag.geld_final, eintrag.beschuldigter }) bezahlt = true end if bezahlt then eingezogen = eintrag.geld_final MySQL.update('UPDATE aether_treasury SET betrag = betrag + ? WHERE id = 1', { eintrag.geld_final }) end end MySQL.update.await([[ UPDATE police_anzeige SET status = 'bestaetigt', bestaetigt_von = ?, bestaetigt_am = NOW() WHERE id = ? ]], { beamter.charakterId, anzeigeId }) Protokoll.Schreibe(beamter, 'anzeige', 'Anzeige bestätigt', { typ = 'person', id = eintrag.beschuldigter }, { anzeige = anzeigeId, eingezogen = eingezogen, selbstbestaetigung = selbstbestaetigung }) -- Erinnerung, falls eine Haft festgelegt wurde — aber NICHT ausgeführt if eintrag.haft_final > 0 then Hinweise.Erstelle({ charakterId = beamter.charakterId }, { kategorie = 'gewahrsam', prioritaet = 'normal', titel = 'Haftzeit festgelegt — Einweisung offen', text = ('Die Anzeige sieht %d Minuten vor. Die Einweisung in Gewahrsam ist ein eigener Schritt.') :format(eintrag.haft_final), zielTyp = 'person', zielId = eintrag.beschuldigter, }) end Kern.Loese('anzeige.bestaetigt', anzeigeId, beamter) return { ok = true, eingezogen = eingezogen, haft = eintrag.haft_final, beschuldigter = eintrag.beschuldigter, } end) --- Entwurf verwerfen Kern.RegistriereAktion('anzeige', 'verwerfen', 'anzeige.erstellen', function(quelle, beamter, daten) local anzeigeId = tonumber(daten.id) if not anzeigeId then return nil, 'Ungültige Kennung.' end local anzeige = MySQL.query.await( 'SELECT erstellt_von, status, beschuldigter FROM police_anzeige WHERE id = ?', { anzeigeId }) if not anzeige or not anzeige[1] then return nil, 'Anzeige nicht gefunden.' end if anzeige[1].status ~= 'entwurf' then return nil, 'Nur Entwürfe können verworfen werden.' end -- Fremde Entwürfe darf nur verwerfen, wer bestätigen darf if anzeige[1].erstellt_von ~= beamter.charakterId and not Rechte.Hat(beamter, 'anzeige.bestaetigen') then return nil, 'Du kannst nur eigene Entwürfe verwerfen.' end local grund = type(daten.grund) == 'string' and daten.grund:sub(1, 255) or nil MySQL.update.await("UPDATE police_anzeige SET status = 'verworfen' WHERE id = ?", { anzeigeId }) Protokoll.Schreibe(beamter, 'anzeige', 'Anzeige verworfen', { typ = 'person', id = anzeige[1].beschuldigter }, { anzeige = anzeigeId }, grund) return { ok = true } end) -- ===================================================================== -- ÜBERSICHT UND EINZELANSICHT -- ===================================================================== Kern.RegistriereAktion('anzeige', 'liste', nil, function(quelle, beamter, daten) local bedingungen, werte = { '1=1' }, {} if daten.status and daten.status ~= '' then bedingungen[#bedingungen + 1] = 'a.status = ?' werte[#werte + 1] = daten.status end if daten.nurEigene then bedingungen[#bedingungen + 1] = 'a.erstellt_von = ?' werte[#werte + 1] = beamter.charakterId end local anzeigen = MySQL.query.await(([[ SELECT a.id, a.beschuldigter, a.delikte, a.haft_empfohlen, a.geld_empfohlen, a.haft_final, a.geld_final, a.begruendung, a.status, a.erstellt_am, a.bestaetigt_am, b.firstname AS b_vorname, b.lastname AS b_nachname, e.firstname AS e_vorname, e.lastname AS e_nachname FROM police_anzeige a LEFT JOIN aether_characters b ON b.id = a.beschuldigter LEFT JOIN aether_characters e ON e.id = a.erstellt_von WHERE %s ORDER BY CASE WHEN a.status = 'entwurf' THEN 0 ELSE 1 END, a.id DESC LIMIT 60 ]]):format(table.concat(bedingungen, ' AND ')), werte) or {} for _, anzeige in ipairs(anzeigen) do local erfolg, liste = pcall(json.decode, anzeige.delikte) anzeige.delikte = (erfolg and type(liste) == 'table') and liste or {} -- Weicht die Entscheidung vom Vorschlag ab? anzeige.abweichung = (anzeige.haft_final ~= anzeige.haft_empfohlen) or (anzeige.geld_final ~= anzeige.geld_empfohlen) end return { anzeigen = anzeigen, rechte = { bestaetigen = Rechte.Hat(beamter, 'anzeige.bestaetigen'), abweichen = Rechte.Hat(beamter, 'anzeige.abweichen'), }, } end) -- ===================================================================== -- KATALOGPFLEGE (Administration) -- ===================================================================== Kern.RegistriereAktion('anzeige', 'katalogSpeichern', 'katalog.bearbeiten', function(quelle, beamter, daten) local kennung = type(daten.kennung) == 'string' and daten.kennung:gsub('%s', '') or '' local bezeichnung = type(daten.bezeichnung) == 'string' and daten.bezeichnung:gsub('^%s+', ''):gsub('%s+$', '') or '' if #kennung < 1 or #kennung > 24 then return nil, 'Ungültige Kennung.' end if #bezeichnung < 3 then return nil, 'Bitte eine Bezeichnung angeben.' end local haft = math.max(0, math.min(1440, math.floor(tonumber(daten.haft) or 0))) local geld = math.max(0, math.min(10000000, math.floor(tonumber(daten.geld) or 0))) MySQL.query.await([[ INSERT INTO police_katalog (kennung, kategorie, bezeichnung, paragraph, haft, geld, hinweis, aktiv) VALUES (?, ?, ?, ?, ?, ?, ?, 1) ON DUPLICATE KEY UPDATE kategorie = VALUES(kategorie), bezeichnung = VALUES(bezeichnung), paragraph = VALUES(paragraph), haft = VALUES(haft), geld = VALUES(geld), hinweis = VALUES(hinweis), aktiv = 1 ]], { kennung, type(daten.kategorie) == 'string' and daten.kategorie:sub(1, 48) or 'Sonstige', bezeichnung, type(daten.paragraph) == 'string' and daten.paragraph:sub(1, 48) or nil, haft, geld, type(daten.hinweis) == 'string' and daten.hinweis:sub(1, 512) or nil, }) Protokoll.Schreibe(beamter, 'anzeige', 'Katalogeintrag gespeichert', { typ = 'katalog', id = kennung }, { haft = haft, geld = geld }) return { ok = true } end) Kern.RegistriereAktion('anzeige', 'katalogDeaktivieren', 'katalog.bearbeiten', function(quelle, beamter, daten) local kennung = tostring(daten.kennung or '') if kennung == '' then return nil, 'Ungültige Kennung.' end -- Nicht löschen, damit alte Anzeigen nachvollziehbar bleiben MySQL.update.await('UPDATE police_katalog SET aktiv = 0 WHERE kennung = ?', { kennung }) Protokoll.Schreibe(beamter, 'anzeige', 'Katalogeintrag deaktiviert', { typ = 'katalog', id = kennung }) return { ok = true } end)