Alle Projekte

Live

Forensik und Tatorte

Ein Ermittlungssystem für einen Polizei-Roleplay-Server: Leichen, die bestehen bleiben, Beweise, die standhalten, und Fallakten, die Ermittler durchsuchen können. Ich habe es halbfertig und kaputt übernommen und zu einem funktionierenden System ausgebaut.

Rolle
Ein unfertiges Plugin übernommen, dann neu aufgebaut, erweitert und gewartet
Stack
Lua (GLua), NutScript, MySQL
Kontext
Polizei- und Kriminellen-Roleplay bei Diverge Networks
Status
Live, seit Dezember 2024 im Einsatz

Screenshots und Clips auf dieser Seite: aufgenommen auf einem Entwicklungsserver mit Testcharakteren und gestellten Daten.

Das Problem

Als ich zu Diverge Networks kam, hatte der Server bereits ein Forensik-Plugin, das ein früherer Entwickler geschrieben und nie fertiggestellt hatte. Leichen wurden dupliziert oder verschwanden, Beweise überlebten keinen Map-Wechsel, und manche Fingerabdrücke ließen sich gar nicht sichern. Polizei- und Kriminellen-Roleplay stützten sich trotzdem darauf.

Über fast zwei Jahre und rund 70 Commits habe ich es zu dem System ausgebaut, das das Roleplay brauchte: Tatorte, die bestehen bleiben, Beweise, die standhalten, und Fallakten, mit denen Ermittler tatsächlich arbeiten können. Die Namensnennung des ursprünglichen Autors habe ich im Code beibehalten.

Leichen und Tatorte

Video folgt in Kürze

Ein Ermittler an einem Test-Tatort: Er untersucht die Leiche, tütet eine Patronenhülse ein und sichert dann einen Fingerabdruck auf einer Karte.

assets/projects/forensics/crime-scene.mp4 (20 bis 30 Sekunden)

Einen Tatort bearbeiten.

Screenshot folgt in Kürze

Das Panel zur Leichenuntersuchung bei einem Testcharakter, mit Todeszeit, Verletzungen und mitgeführten Waffen.

assets/projects/forensics/inspect-corpse.webp (enger Zuschnitt auf das Panel)

Was eine Leiche verrät.

Video (optional) folgt in Kürze

Ein Krimineller verbrennt eine Leiche mit Benzin oder packt sie in einen Leichensack.

assets/projects/forensics/body-disposal.mp4 (etwa 15 Sekunden)

Beweise beseitigen.

Beweise

Screenshot folgt in Kürze

Eine Fingerabdruckkarte nach dem Sichern eines Abdrucks, mit ihrer bearbeiteten Trägerkarte.

assets/projects/forensics/fingerprint-card.webp (enger Zuschnitt)

Ein gesicherter Abdruck und seine Trägerkarte.

Fallakten und die Datenbank

Screenshot folgt in Kürze

Die Forensik-Datenbank mit Suchergebnissen, gefüllt mit Testdaten.

assets/projects/forensics/case-database.webp (enger Zuschnitt auf das Panel)

Die Datenbank durchsuchen.

Screenshot folgt in Kürze

Eine Ermittler-Fallakte mit angehängten Beweisen, nur Testdaten.

assets/projects/forensics/case-file.webp (enger Zuschnitt auf das Panel)

Eine Fallakte mit angehängten Beweisen.

Für einen Live-Server gebaut

-- Step 1, on the server: the player uses a fingerprint card while looking at an item.
-- If there's a choice of prints, the server grants a one-time authorisation tied
-- to this card and this item, and sends the list to choose from.
onRun = function(card)
    local ply = card.player
    local target = ply:GetEyeTrace().Entity
    if (target:GetClass() ~= "nut_item") then return false end

    local prints = Forensics:GetValidFingerprints(target)   -- decayed prints already filtered out
    -- ... zero prints and single-print shortcuts omitted

    ply.authFingerprintCard = card.id
    ply.authFingerprintItem = target.nutItemID
    ply.authFingerprintCardTime = SysTime()
    -- ... send the choices to the client
    return false
end

-- Step 2: the client answers with the card and the index of the print it picked.
net.Receive("Forensics:FingerprintCard", function(_, ply)
    local card = nut.item.instances[net.ReadUInt(32)]
    local printIndex = net.ReadUInt(32)
    local target = ply:GetEyeTrace().Entity

    -- The authorisation must match this card and be under a minute old.
    if (not card or card.uniqueID ~= "fingerprintcard"
        or ply.authFingerprintCard ~= card.id
        or SysTime() - ply.authFingerprintCardTime > 60) then
        return
    end

    -- Still looking at the same item, and within arm's reach.
    if (not IsValid(target) or target:GetClass() ~= "nut_item"
        or target.nutItemID ~= ply.authFingerprintItem
        or target:GetPos():DistToSqr(ply:GetPos()) > 100 * 100) then
        return
    end

    -- The prints are looked up again on the server. The client only picks an index.
    local prints = Forensics:GetValidFingerprints(nut.item.instances[target.nutItemID])

    -- One use only.
    ply.authFingerprintCard = nil
    ply.authFingerprintItem = nil
    ply.authFingerprintCardTime = nil

    local chosen = prints[printIndex]
    if (not chosen) then return end

    -- The card must still be in this player's inventory.
    if (not ply:getChar():getInv():getItems()[card.id]) then return end

    ply:notify("You peel off a fingerprint and imprint it onto the card.")
    -- ... write the lifted print to the forensics database (omitted)
end)
Aus dem Produktivcode übernommen und zur besseren Lesbarkeit leicht bearbeitet. Nie genug, um das System nachzubauen.

Noch nicht ganz entwirrt

Ein Teil des ursprünglichen Designs liegt noch darunter und taucht gelegentlich als merkwürdiger Bug auf. Jeder Fix ist eine Gelegenheit, ein weiteres Stück davon zu ersetzen.