All work

Live

Forensics and crime scenes

A detective system for a police roleplay server: bodies that persist, evidence that holds up, and case files detectives can search. I inherited it half-built and broken, and turned it into a working system.

Role
Took over an unfinished plugin, then rebuilt, extended and maintained it
Stack
Lua (GLua), NutScript, MySQL
Context
Police and criminal roleplay on Diverge Networks
Status
Live, in use since December 2024

Screenshots and clips on this page: captured on a development server with test characters and staged data.

The problem

When I joined Diverge Networks, the server already had a forensics plugin, written by an earlier developer and never finished. Bodies duplicated or vanished, evidence didn’t survive a map change, and some fingerprints couldn’t be lifted at all. Police and criminal roleplay leaned on it anyway.

Over nearly two years and about 70 commits, I turned it into the system the roleplay needed: crime scenes that persist, evidence that holds up, and case files detectives can actually work from. I kept the original author’s credit in the code.

Bodies and crime scenes

Video coming soon

A detective at a test crime scene: inspecting the body, bagging a bullet casing, then lifting a fingerprint onto a card.

assets/projects/forensics/crime-scene.mp4 (20 to 30 seconds)

Working a scene.

Screenshot coming soon

The inspect-body panel on a test character, showing time of death, wounds and the weapons carried.

assets/projects/forensics/inspect-corpse.webp (tight crop to the panel)

What a body tells you.

Video (optional) coming soon

A criminal burning a body with gasoline, or zipping it into a body bag.

assets/projects/forensics/body-disposal.mp4 (about 15 seconds)

Getting rid of the evidence.

Evidence

Screenshot coming soon

A fingerprint card after lifting a print, with its edited backing card.

assets/projects/forensics/fingerprint-card.webp (tight crop)

A lifted print and its backing card.

Case files and the database

Screenshot coming soon

The forensics database with search results, filled with test data.

assets/projects/forensics/case-database.webp (tight crop to the panel)

Searching the database.

Screenshot coming soon

A detective case file with evidence attached, test data only.

assets/projects/forensics/case-file.webp (tight crop to the panel)

A case file with evidence attached.

Built for a live server

-- 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)
Adapted from production code, lightly edited for readability. Never enough to rebuild the system.

Still being untangled

Some of the original design is still underneath, and it still surfaces as the occasional odd bug. Each fix is a chance to replace another piece of it.