-- ===================================================================== -- aether-police / Modul: Gewahrsamsverwaltung -- -- Dieses Modul enthält BEWUSST keinen Automatismus: -- * Niemand wird automatisch eingewiesen. -- * Keine Frist entlässt jemanden. -- * Abgelaufene Haftzeiten bleiben offen stehen, bis ein Beamter -- entscheidet — sichtbar an `entlassen_von` / `entlassen_am`. -- -- Das System führt die Frist mit und erinnert. Mehr nicht. -- ===================================================================== Kern.RegistriereModul({ id = 'gewahrsam', name = 'Gewahrsam', icon = '🔒', reihenfolge = 5, recht = 'gewahrsam.einweisen', beschreibung = 'Zellenbelegung und Fristen', }) local Core = exports['aether-core'] -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Sucht eine Zelle in der Konfiguration --- @param zellenId string|nil --- @return table|nil local function HoleZelle(zellenId) for _, zelle in ipairs(Polizei.Config.Gewahrsam.zellen) do if zelle.id == zellenId then return zelle end end return nil end --- Lädt alle offenen Gewahrsamsfälle --- @return table local function LadeOffene() local eintraege = MySQL.query.await([[ SELECT g.id, g.character, g.anzeige, g.haft_minuten, g.beginn, g.faellig_am, g.grund, g.zelle, g.eingewiesen_von, TIMESTAMPDIFF(SECOND, NOW(), g.faellig_am) AS rest_sekunden, c.firstname, c.lastname, c.dob, e.firstname AS e_vorname, e.lastname AS e_nachname, p.dienstnummer AS e_dienstnummer FROM police_gewahrsam g JOIN aether_characters c ON c.id = g.character LEFT JOIN aether_characters e ON e.id = g.eingewiesen_von LEFT JOIN police_personal p ON p.`character` = g.eingewiesen_von WHERE g.entlassen_am IS NULL ORDER BY g.faellig_am ASC ]]) or {} for _, eintrag in ipairs(eintraege) do eintrag.rest_sekunden = tonumber(eintrag.rest_sekunden) or 0 eintrag.abgelaufen = eintrag.rest_sekunden <= 0 -- Ist die Person gerade online? eintrag.online = Core:HoleQuelleNachCharakter(eintrag.character) ~= nil end return eintraege end --- Verbringt eine Person in eine Zelle (nur wenn sie online ist) --- @param charakterId number --- @param zelle table Zellendefinition local function VerbringeInZelle(charakterId, zelle) if not Polizei.Config.Gewahrsam.verbringen or not zelle then return end local zielQuelle = Core:HoleQuelleNachCharakter(charakterId) if not zielQuelle then return end TriggerClientEvent('aether:police:gewahrsamVerbringen', zielQuelle, { x = zelle.pos.x, y = zelle.pos.y, z = zelle.pos.z, w = zelle.pos.w, zelle = zelle.name, }) end -- ===================================================================== -- ÜBERSICHT -- ===================================================================== Kern.RegistriereAktion('gewahrsam', 'liste', nil, function(quelle, beamter) local offene = LadeOffene() -- Zellenbelegung ermitteln local belegt = {} for _, eintrag in ipairs(offene) do if eintrag.zelle then belegt[eintrag.zelle] = eintrag end end local zellen = {} for _, zelle in ipairs(Polizei.Config.Gewahrsam.zellen) do local insasse = belegt[zelle.id] zellen[#zellen + 1] = { id = zelle.id, name = zelle.name, frei = insasse == nil, insasse = insasse and { id = insasse.id, name = insasse.firstname .. ' ' .. insasse.lastname, rest = insasse.rest_sekunden, abgelaufen = insasse.abgelaufen, } or nil, } end -- Kürzlich Entlassene (nur zur Nachvollziehbarkeit) local verlauf = MySQL.query.await([[ SELECT g.id, g.haft_minuten, g.beginn, g.entlassen_am, g.entlass_grund, g.grund, c.firstname, c.lastname, e.firstname AS e_vorname, e.lastname AS e_nachname FROM police_gewahrsam g JOIN aether_characters c ON c.id = g.character LEFT JOIN aether_characters e ON e.id = g.entlassen_von WHERE g.entlassen_am IS NOT NULL ORDER BY g.entlassen_am DESC LIMIT 20 ]]) or {} return { offene = offene, zellen = zellen, verlauf = verlauf, maxDauer = Polizei.Config.Gewahrsam.maxDauer, verbringen = Polizei.Config.Gewahrsam.verbringen, rechte = { einweisen = Rechte.Hat(beamter, 'gewahrsam.einweisen'), entlassen = Rechte.Hat(beamter, 'gewahrsam.entlassen'), }, } end) -- ===================================================================== -- EINWEISEN -- -- Geschieht nur auf ausdrückliche Bestätigung eines Beamten. Es gibt -- keinen Weg, hier ohne dessen Zutun hineinzukommen. -- ===================================================================== Kern.RegistriereAktion('gewahrsam', 'einweisen', 'gewahrsam.einweisen', function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) if not charakterId then return nil, 'Keine Person ausgewählt.' end local person = MySQL.query.await( 'SELECT firstname, lastname FROM aether_characters WHERE id = ? AND deleted = 0', { charakterId }) if not person or not person[1] then return nil, 'Diese Person existiert nicht.' end -- Doppelte Einweisung verhindern local bereits = MySQL.scalar.await( 'SELECT id FROM police_gewahrsam WHERE `character` = ? AND entlassen_am IS NULL', { charakterId }) if bereits then return nil, 'Diese Person befindet sich bereits in Gewahrsam.' end -- Haftdauer: kommt ausschliesslich vom Beamten local dauer = math.floor(tonumber(daten.dauer) or 0) if dauer < 1 or dauer > Polizei.Config.Gewahrsam.maxDauer then return nil, ('Die Haftdauer muss zwischen 1 und %d Minuten liegen.') :format(Polizei.Config.Gewahrsam.maxDauer) end local grund = type(daten.grund) == 'string' and daten.grund:gsub('^%s+', ''):gsub('%s+$', '') or '' if #grund < 3 then return nil, 'Bitte einen Grund angeben.' end -- Zelle prüfen und Belegung sicherstellen local zelle = HoleZelle(daten.zelle) if daten.zelle and not zelle then return nil, 'Unbekannte Zelle.' end if zelle then local besetzt = MySQL.scalar.await( 'SELECT id FROM police_gewahrsam WHERE zelle = ? AND entlassen_am IS NULL', { zelle.id }) if besetzt then return nil, ('%s ist bereits belegt.'):format(zelle.name) end end local gewahrsamId = MySQL.insert.await([[ INSERT INTO police_gewahrsam (`character`, anzeige, haft_minuten, faellig_am, grund, zelle, eingewiesen_von) VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL ? MINUTE), ?, ?, ?) ]], { charakterId, tonumber(daten.anzeige), dauer, dauer, grund, zelle and zelle.id or nil, beamter.charakterId, }) -- Person in die Zelle bringen, sofern konfiguriert und online VerbringeInZelle(charakterId, zelle) Protokoll.Schreibe(beamter, 'gewahrsam', 'In Gewahrsam genommen', { typ = 'person', id = charakterId }, { gewahrsam = gewahrsamId, dauer = dauer, zelle = zelle and zelle.id }, grund) Hinweise.Erstelle(nil, { kategorie = 'gewahrsam', prioritaet = 'normal', titel = ('%s %s in Gewahrsam'):format(person[1].firstname, person[1].lastname), text = ('%d Minuten · %s · eingewiesen von %s') :format(dauer, grund, beamter.dienstnummer), zielTyp = 'gewahrsam', zielId = gewahrsamId, }) Kern.Loese('gewahrsam.eingewiesen', gewahrsamId, beamter) return { id = gewahrsamId } end) -- ===================================================================== -- ENTLASSEN -- -- Der einzige Weg aus dem Gewahrsam. Es gibt keinen Hintergrundprozess, -- der das übernimmt — auch nicht bei längst abgelaufener Frist. -- ===================================================================== Kern.RegistriereAktion('gewahrsam', 'entlassen', 'gewahrsam.entlassen', function(quelle, beamter, daten) local gewahrsamId = tonumber(daten.id) if not gewahrsamId then return nil, 'Ungültige Kennung.' end local eintrag = MySQL.query.await([[ SELECT g.*, c.firstname, c.lastname, TIMESTAMPDIFF(MINUTE, NOW(), g.faellig_am) AS rest FROM police_gewahrsam g JOIN aether_characters c ON c.id = g.character WHERE g.id = ? ]], { gewahrsamId }) if not eintrag or not eintrag[1] then return nil, 'Vorgang nicht gefunden.' end local g = eintrag[1] if g.entlassen_am then return nil, 'Diese Person wurde bereits entlassen.' end local grund = type(daten.grund) == 'string' and daten.grund:gsub('^%s+', ''):gsub('%s+$', '') or '' -- Vorzeitige Entlassung verlangt eine Begründung local rest = tonumber(g.rest) or 0 local vorzeitig = rest > 0 if vorzeitig and #grund < 3 then return nil, ('Die Frist läuft noch %d Minuten. Für eine vorzeitige Entlassung ist eine Begründung erforderlich.') :format(rest) end MySQL.update.await([[ UPDATE police_gewahrsam SET entlassen_von = ?, entlassen_am = NOW(), entlass_grund = ? WHERE id = ? ]], { beamter.charakterId, grund ~= '' and grund or nil, gewahrsamId }) -- Person an den Entlassungspunkt setzen, sofern online if Polizei.Config.Gewahrsam.verbringen then local zielQuelle = Core:HoleQuelleNachCharakter(g.character) if zielQuelle then local punkt = Polizei.Config.Gewahrsam.entlassung TriggerClientEvent('aether:police:gewahrsamEntlassen', zielQuelle, { x = punkt.x, y = punkt.y, z = punkt.z, w = punkt.w, }) end end Protokoll.Schreibe(beamter, 'gewahrsam', vorzeitig and 'Vorzeitig entlassen' or 'Entlassen', { typ = 'person', id = g.character }, { gewahrsam = gewahrsamId, restMinuten = rest }, grund ~= '' and grund or nil) Hinweise.Erstelle(nil, { kategorie = 'gewahrsam', prioritaet = 'niedrig', titel = ('%s %s entlassen'):format(g.firstname, g.lastname), text = ('Entlassen durch %s%s'):format(beamter.dienstnummer, vorzeitig and (' · vorzeitig: ' .. grund) or ''), }) Kern.Loese('gewahrsam.entlassen', gewahrsamId, beamter) return { ok = true, vorzeitig = vorzeitig } end) -- ===================================================================== -- HAFTZEIT ANPASSEN -- -- Auch das ist eine bewusste Entscheidung und wird protokolliert. -- ===================================================================== Kern.RegistriereAktion('gewahrsam', 'fristAendern', 'gewahrsam.entlassen', function(quelle, beamter, daten) local gewahrsamId = tonumber(daten.id) local minuten = math.floor(tonumber(daten.minuten) or 0) if not gewahrsamId then return nil, 'Ungültige Kennung.' end if minuten == 0 then return nil, 'Bitte eine Änderung angeben.' end if math.abs(minuten) > Polizei.Config.Gewahrsam.maxDauer then return nil, 'Die Änderung ist zu groß.' end local grund = type(daten.grund) == 'string' and daten.grund:sub(1, 255) or '' if #grund < 3 then return nil, 'Bitte einen Grund für die Änderung angeben.' end local eintrag = MySQL.query.await( 'SELECT `character`, entlassen_am FROM police_gewahrsam WHERE id = ?', { gewahrsamId }) if not eintrag or not eintrag[1] then return nil, 'Vorgang nicht gefunden.' end if eintrag[1].entlassen_am then return nil, 'Der Vorgang ist bereits abgeschlossen.' end MySQL.update.await([[ UPDATE police_gewahrsam SET faellig_am = DATE_ADD(faellig_am, INTERVAL ? MINUTE), haft_minuten = GREATEST(0, haft_minuten + ?) WHERE id = ? ]], { minuten, minuten, gewahrsamId }) Protokoll.Schreibe(beamter, 'gewahrsam', 'Haftzeit angepasst', { typ = 'person', id = eintrag[1].character }, { gewahrsam = gewahrsamId, aenderung = minuten }, grund) return { ok = true } end) -- ===================================================================== -- ZELLE WECHSELN -- ===================================================================== Kern.RegistriereAktion('gewahrsam', 'zelleWechseln', 'gewahrsam.einweisen', function(quelle, beamter, daten) local gewahrsamId = tonumber(daten.id) local zelle = HoleZelle(daten.zelle) if not gewahrsamId or not zelle then return nil, 'Unvollständige Angaben.' end local besetzt = MySQL.scalar.await( 'SELECT id FROM police_gewahrsam WHERE zelle = ? AND entlassen_am IS NULL AND id <> ?', { zelle.id, gewahrsamId }) if besetzt then return nil, ('%s ist bereits belegt.'):format(zelle.name) end local eintrag = MySQL.query.await( 'SELECT `character` FROM police_gewahrsam WHERE id = ? AND entlassen_am IS NULL', { gewahrsamId }) if not eintrag or not eintrag[1] then return nil, 'Vorgang nicht gefunden.' end MySQL.update.await('UPDATE police_gewahrsam SET zelle = ? WHERE id = ?', { zelle.id, gewahrsamId }) VerbringeInZelle(eintrag[1].character, zelle) Protokoll.Schreibe(beamter, 'gewahrsam', 'Zelle gewechselt', { typ = 'person', id = eintrag[1].character }, { zelle = zelle.id }) return { ok = true } end) -- ===================================================================== -- BEI ANMELDUNG PRÜFEN -- -- Wer während einer offenen Haft ausloggt, findet sich beim nächsten -- Anmelden wieder in der Zelle. Die Frist läuft derweil weiter, aber -- entlassen wird trotzdem nur durch einen Beamten. -- ===================================================================== AddEventHandler('aether:server:spielerGeladen', function(quelle, spieler) if not Polizei.Config.Gewahrsam.verbringen then return end local eintrag = MySQL.query.await([[ SELECT id, zelle, grund, TIMESTAMPDIFF(MINUTE, NOW(), faellig_am) AS rest FROM police_gewahrsam WHERE `character` = ? AND entlassen_am IS NULL LIMIT 1 ]], { spieler.charakterId }) if not eintrag or not eintrag[1] then return end local zelle = HoleZelle(eintrag[1].zelle) if not zelle then return end -- Kurz warten, bis der Spieler wirklich in der Welt ist Wait(3000) TriggerClientEvent('aether:police:gewahrsamVerbringen', quelle, { x = zelle.pos.x, y = zelle.pos.y, z = zelle.pos.z, w = zelle.pos.w, zelle = zelle.name, rest = tonumber(eintrag[1].rest) or 0, grund = eintrag[1].grund, }) end) -- ===================================================================== -- EXPORT — Auskunft für andere Module -- ===================================================================== --- Befindet sich eine Person in Gewahrsam? exports('IstInGewahrsam', function(charakterId) local treffer = MySQL.scalar.await( 'SELECT id FROM police_gewahrsam WHERE `character` = ? AND entlassen_am IS NULL', { charakterId }) return treffer ~= nil end)