Live
Haftbefehle und Strafjustiz
Ein vernetztes Justizsystem für eine Polizei-Roleplay-Community: Haftbefehle, Strafregister, Verbrechen, Kaution und Berufung, mit serverseitigen Prüfungen bei jeder Aktion.
- Rolle
- Entwurf und Umsetzung
- Stack
- Lua (GLua), MySQL, Discord-Webhooks
- Kontext
- Polizei- und Kriminellen-Roleplay bei Diverge Networks
- Status
- Live: Verbrechen, Gefängnis und Kaution seit 2025, Haftbefehle seit 2026
Screenshots und Clips auf dieser Seite: aufgenommen auf einem Entwicklungsserver mit Testcharakteren und gestellten Daten.
Das Problem
Polizei-Roleplay lebt von seinem Papierkram. Der Server brauchte Haftbefehle, die Beamte ausstellen und vollstrecken können, Akten, die standhalten, und Konsequenzen, die sich durch das ganze Spiel ziehen, und das alles, ohne irgendetwas zu vertrauen, was der Spielclient eines Spielers sendet.
Haftbefehle
- Beamte stellen Haftbefehle über einen Polizeicomputer im Spiel aus, durchsuchen und verwalten sie, mit seitenweise geblätterten Listen aktiver Haftbefehle.
- Jede Aktion prüft die rangbasierten Berechtigungen auf dem Server, einschließlich des Zugriffs für Regierungsrollen, und Datenbankanfragen sind pro Spieler ratenbegrenzt.
- Eingereichte Anklagepunkte werden auf dem Server anhand der echten Anklageliste neu aufgebaut und auf zehn Punkte pro Haftbefehl begrenzt.
- Der Link zum Begleitdokument muss mit der erwarteten Domain beginnen, einschließlich des abschließenden Schrägstrichs, sodass täuschend ähnliche Domains abgelehnt werden.
- Ein aktiver Haftbefehl pro Person. Um einen Haftbefehl zu vollstrecken, muss der Verdächtige in Handschellen, gefesselt und im Gefängnisbereich sein, und der Server prüft vor dem Schließen erneut, dass der Haftbefehl zu ihm gehört.
- Ausstellen, Aufheben und Vollstrecken eines Haftbefehls lösen jeweils ein Event aus. Eine Pager-Nachricht an die gesuchte Person, ein Discord-Post für das Polizeiteam und eine Fahndungsmarkierung im Spiel sind getrennte Listener, sodass keiner die anderen zum Absturz bringen kann.
- Der Discord-Webhook wird zur Laufzeit über einen rangbeschränkten Admin-Befehl gesetzt und nie im Code gespeichert.
- Kennzeichenprüfungen sowie gestohlene Kennzeichen, die an gestohlene Fahrzeuge montiert werden können und nach 30 Minuten ablaufen.
Screenshot folgt in Kürze
Das Formular zum Einreichen eines Haftbefehls, ausgefüllt mit Testdaten.
assets/projects/justice-system/warrant-form.webp (enger Zuschnitt auf das Panel)
Screenshot folgt in Kürze
Die Liste aktiver Haftbefehle mit Seitenblätterung.
assets/projects/justice-system/warrant-list.webp (enger Zuschnitt auf das Panel)
Screenshot folgt in Kürze
Die Pager-Nachricht im Spiel, die ein gesuchter Charakter erhält.
assets/projects/justice-system/pager.webp (enger Zuschnitt)
Screenshot folgt in Kürze
Der Discord-Post zu einem neuen Haftbefehl, mit unkenntlich gemachten Server- und Kanalnamen.
assets/projects/justice-system/discord-embed.webp (Zuschnitt nur auf die Nachricht)
Video (optional) folgt in Kürze
Einen Haftbefehl einreichen, ihn in der Liste erscheinen sehen und dann den Verdächtigen über das Kontextmenü festnehmen.
assets/projects/justice-system/warrant-arrest.mp4 (20 bis 30 Sekunden)
net.Receive("WarrantsDatabase.IssueWarrant", function(_, ply)
-- Permission first, before anything is read from the request.
if (not PLUGIN:CanIssueWarrant(ply)) then return end -- rank-based, checked on the server
-- One database request per second per player.
if (ply.lastWarrantDBQuery and ply.lastWarrantDBQuery > SysTime() - 1) then
ply:notify("You must wait 1 second before querying the warrants database again.")
return
end
ply.lastWarrantDBQuery = SysTime()
local charID = net.ReadUInt(32)
local requested = pon.decode(net.ReadString())
local documentLink = net.ReadString()
-- Rebuild the charges against the server's own charge list.
-- Unknown charge IDs and non-numbers are rejected outright.
local allCharges = nut.plugin.list.police:GetCharges()
local charges, total = {}, 0
for chargeID, count in pairs(requested) do
assert(isnumber(count), "charge count is not a number")
assert(allCharges[chargeID], "not a valid charge ID")
if (count > 0) then
charges[chargeID] = count
total = total + count
end
end
if (total > 10) then
ply:notify("You cannot submit a warrant with more than a total of 10 charges.")
return
end
-- The trailing slash matters: it rejects lookalikes such as docs.google.com.evil.example
if (not documentLink:StartsWith("https://docs.google.com/")) then
ply:notify("Invalid Google Docs link.")
return
end
-- One active warrant per person.
nut.db.preparedCallDeferred("warrantsGetActiveByWantedCharID", charID):next(function(res)
if (res.results and res.results[1]) then
ply:notify("A warrant has already been issued for this individual.")
return
end
-- ... look up the wanted character's name, then issue the warrant (omitted)
end)
end)Festnahme und Gefängnis
- Beamte arbeiten einen Verdächtigen über das Kontextmenü ab: in Handschellen legen und fesseln und, innerhalb der Wache, sofort sein Strafregister aufrufen.
- Handschellen überstehen eine Trennung der Verbindung. Ausloggen war früher ein kostenloser Ausweg; jetzt bleiben die Handschellen an diesem Charakter, bis sie entfernt oder aufgebrochen werden.
- Der Verhaftungs-NPC listet jeden Anklagepunkt mit Gesetzescode und Höchststrafe auf, zeigt das theoretische Maximum und lässt Beamte wählen, ob jemand auf seinen Prozess warten oder per Deal (Plea Deal) verurteilt wird, wobei der Strafregler innerhalb der Grenzen bleibt.
- Am Empfang können Spieler ihr eigenes Strafregister prüfen, und es kann Kaution für eine andere Person gezahlt werden.
- Kaution. Gefangene können es ablehnen, auf Kaution freigekauft zu werden, was verhinderte, dass Kaution genutzt wurde, um Leute gegen ihren Willen aus dem Gefängnis zu holen.
- Die ursprüngliche Hülle des Polizeicomputers gab es schon vor mir; jeder Bildschirm und jede Schaltfläche darin stammt seither von mir. Der Regierungscomputer, der daraus entstand, ist komplett mein eigener. Das Gefängnissystem darunter habe ich Jahre früher begonnen und hier neu aufgebaut.
Video folgt in Kürze
Einen Verdächtigen in Handschellen legen, seine Akte im Kontextmenü prüfen und ihn dann am Verhaftungs-NPC verurteilen.
assets/projects/justice-system/arrest-to-cell.mp4 (20 bis 30 Sekunden)
Screenshot folgt in Kürze
Der Verhaftungs-NPC mit Anklagepunkten, theoretischem Maximum und Strafregler, nur Testdaten.
assets/projects/justice-system/arrest-npc.png (enger Zuschnitt auf das Panel)
Screenshot folgt in Kürze
Der Startbildschirm des Polizeicomputers.
assets/projects/justice-system/police-computer.png (enger Zuschnitt auf den Bildschirm)
Screenshot folgt in Kürze
Der Regierungscomputer auf dem Aktenbildschirm.
assets/projects/justice-system/government-computer.png (enger Zuschnitt auf den Bildschirm)
Verbrechen und Akten
- Jeder Anklagepunkt wegen eines Verbrechens macht einen Charakter zum Straftäter.
- Der Straftäter-Status wird auf Ausweisen im Spiel angezeigt.
- Shops können jeden Artikel auf alle, nur Straftäter oder nur Nicht-Straftäter beschränken, auf dem Server im Moment des Handels durchgesetzt.
- Das Staff-Team verwaltet die Anklageliste im Spiel, wobei jeder Anklagepunkt gegen ein festes Schema validiert wird.
- Die Aktenverwaltung wurde für das neue Datensystem neu geschrieben, auch für Charaktere, die offline sind.
Screenshot folgt in Kürze
Eine Strafakte mit sichtbarer Verbrechensmarkierung.
assets/projects/justice-system/record-felony.webp (enger Zuschnitt auf das Panel)
Screenshot folgt in Kürze
Ein Ausweis, der den Straftäter-Status zeigt.
assets/projects/justice-system/id-card-felon.webp (enger Zuschnitt)
Berufung und Einwilligung
- Berufung. Regierungscharaktere können den Verbrechensstatus löschen, aber nur bei Akten, die der Server ihnen gezeigt hat. Der Server führt eine Liste der Akten-IDs, die er an jeden Spieler gesendet hat, und lehnt alles andere ab, sodass eine geänderte ID in der Anfrage nichts bewirkt.
- Einwilligung. Permanenter Charaktertod als Alternative zu einer Haftstrafe erfordert die Einwilligung des Spielers innerhalb einer Frist. Eine Ablehnung schickt ihn stattdessen vor Gericht, und ein Verbindungsabbruch umgeht die Entscheidung nicht.
-- When a government character opens someone's record, the server remembers
-- exactly which record IDs it sent to that player, and whose they were.
nut.getCharDataOffline(citizenID, nil, {}, function(charData)
ply.allowedSetRecords = {}
if (charData.jailed) then
ply.allowedSetRecords[0] = { CharID = citizenID } -- 0 = the current sentence
end
for recordID in pairs(charData.jailRapSheet or {}) do
ply.allowedSetRecords[recordID] = { CharID = citizenID }
end
-- ... send the records to the player (omitted)
end)
-- The appeal itself. The client sends a record ID and the new felony status,
-- but never whose record it is: that comes from the server's own list.
net.Receive("GovtManageRecordsSetIsFelony", function(_, ply)
local char = ply:getChar()
if (not char) then return end
if (not nut.faction.indices[char:getFaction()].NYC or not char:getData("allowAppeals")) then return end
if (ply.lastFelonySet and ply.lastFelonySet > SysTime() - 1) then
ply:notify("You must wait 1 second before setting another felony.")
return
end
ply.lastFelonySet = SysTime()
local isFelony = net.ReadBool()
local recordID = net.ReadUInt(32)
-- Anything the server didn't send this player is rejected.
local record = ply.allowedSetRecords and ply.allowedSetRecords[recordID]
if (not record) then return end
nut.getCharDataOffline(record.CharID, nil, {}, function(charData)
local key = (recordID == 0) and "jailed" or "jailRapSheet"
local data = charData and table.Copy(charData[key])
local entry = data and ((recordID == 0) and data or data[recordID])
if (not entry) then
ply:notify("Something went wrong - contact a developer.")
return
end
entry.Felony = isFelony
nut.setCharData(record.CharID, key, data, function()
ply:notify("Successfully updated felony status for selected record.")
-- ... refresh the player's view and log the change to Discord (omitted)
end)
end)
end)