Alle Projekte

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

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)

Einen Haftbefehl einreichen: Anklagepunkte nach Anzahl, dazu ein Link zum Begleitdokument.

Screenshot folgt in Kürze

Die Liste aktiver Haftbefehle mit Seitenblätterung.

assets/projects/justice-system/warrant-list.webp (enger Zuschnitt auf das Panel)

Aktive Haftbefehle, Seite für Seite.

Screenshot folgt in Kürze

Die Pager-Nachricht im Spiel, die ein gesuchter Charakter erhält.

assets/projects/justice-system/pager.webp (enger Zuschnitt)

Die gesuchte Person wird per Pager benachrichtigt.

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)

Der Discord-Feed des Polizeiteams.

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)

Von der Einreichung bis zur Festnahme.
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)
Aus dem Produktivcode übernommen und zur besseren Lesbarkeit leicht bearbeitet.

Festnahme und Gefängnis

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)

Von der Festnahme bis zur Zelle.

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)

Verurteilung am Verhaftungs-NPC.

Screenshot folgt in Kürze

Der Startbildschirm des Polizeicomputers.

assets/projects/justice-system/police-computer.png (enger Zuschnitt auf den Bildschirm)

Der Polizeicomputer.

Screenshot folgt in Kürze

Der Regierungscomputer auf dem Aktenbildschirm.

assets/projects/justice-system/government-computer.png (enger Zuschnitt auf den Bildschirm)

Der Regierungscomputer.

Verbrechen und Akten

Screenshot folgt in Kürze

Eine Strafakte mit sichtbarer Verbrechensmarkierung.

assets/projects/justice-system/record-felony.webp (enger Zuschnitt auf das Panel)

Eine Akte mit Verbrechensstatus (leere Zeilen aus dem Screenshot entfernt).

Screenshot folgt in Kürze

Ein Ausweis, der den Straftäter-Status zeigt.

assets/projects/justice-system/id-card-felon.webp (enger Zuschnitt)

Straftäter-Status auf einem Ausweis.

Berufung und Einwilligung

Probieren Sie es aus: Sie sehen die Akte von Jane Doe. Fälschen Sie eine Anfrage, um die Mordanklage gegen Vito Moretti zu löschen, zuerst gegen einen Server, der der Anfrage vertraut, dann gegen den ausgelieferten. Erfundene Charaktere; eine Simulation der Idee, nicht der Servercode.
-- 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)
Aus dem Produktivcode übernommen und zur besseren Lesbarkeit leicht bearbeitet.