Files
institution/Wiki/core/Criminal-Record.md
T
flan 3530100ed5 Aggregate the whole suite wiki onto the Institution page
Wiki/Home.md is the suite's front door; every layer's wiki now mirrors here
under Wiki/<layer>/ (core, contraband, justice, gangs, ward), so the whole
suite reads in one place with the index linking to the local pages. The
layer repos stay canonical -- Tools/sync-wiki.sh refreshes the aggregated
copy on demand. The README's main page points to it.
2026-07-15 20:30:38 +00:00

8.2 KiB
Raw Blame History

Criminal Record — the history of fact

If Propensity is suspicion — "this pawn seems dangerous" — the criminal record is fact: "this pawn has escaped twice and shanked a guard." The two are deliberately different kinds of thing. Propensity is computed fresh from who a pawn is and how they are treated; a record is a durable log of what they have actually done, written once when it happens and read forever after.

CriminalRecord is the single per-pawn history the whole suite writes to and reads from. Classification grades on it, parole gates on it, the deterrence loop reacts to it, and reintegration remembers it. Because there is exactly one record per pawn and everyone shares it, no two modules can disagree about a pawn's past.

Everything here is verified against Source/Core/CriminalRecord.cs.


The fields

CriminalRecord is a plain IExposable bag of counters plus two special values. Core defines the fields and the storage; it does not write most of them — the modules that own each event do.

Field Type Default What it records Who writes it Who reads it
crimesCommitted int 0 count of committed crimes Justice RecordCrime (also on gang fights) classification risk score, deterrence
escapeAttempts int 0 breakout attempts Contraband (escape/tunnel logic) classification risk score
contrabandMade int 0 items brewed / whittled Contraband (shivs, vessels) classification risk score
timesSearched int 0 how often searched Contraband (warden search) search prioritisation, audit
timesCaught int 0 searches that found something Contraband (warden search) classification risk score
lastCrimeTick int −1 game tick of the most recent crime Justice RecordCrime recency / cooldown checks
reform float 0 rehabilitation score (see below) Justice Discipline, Parole Propensity.Nurture, parole gate, reintegration
pardoned bool false granted a clean slate after genuine reform Justice Parole (on release) parole gate, reintegration

lastCrimeTick defaults to −1 — the sentinel for "never," distinct from tick 0. Everything else defaults to zero/false.

Note on ownership. Core is the vault, not the clerk. It hands out records and persists them; the write logic ("a crime just happened, bump the counter") lives in the module where the event occurs, above Core. The columns above name where that logic lives in the suite — Core itself only defines the fields.

reform — the pivot of the suite

Most fields are inert tallies. reform is the one that feeds back into behaviour, and it is worth its own section.

reform : float, nominally 0..1 but can go negative
    raised by good treatment and good conduct
    decays without either
    read by Propensity.Nurture, the parole gate, and reintegration

The source is blunt about its role: "it is what makes reform MEAN something." A record is where a sentence's outcome is stored. Three systems lean on that one float:

  • Propensity's Nurture multiplies disposition by Clamp(1 − reform×0.5, 0.4, 2) — so a genuinely rehabilitated pawn (reform > 0) is calmer for good, and a prisonized one (reform < 0) is inflamed for good. This is how institutionalization and recidivism enter the model without a separate mechanic bolted on.
  • Parole (Justice) gates release on it: a pawn is only releasable once reform clears a threshold. It is the number that says "this one is ready."
  • Reintegration reads it to decide when a clean slate is earned — which is what pardoned marks.

Although the field is documented as 0..1, Justice's Discipline can drive it negative (harsh punishment subtracts from it). That negative range is not a bug — it is the "hardened" end of the spectrum, and Nurture's clamp is written to expect it.

pardoned and IsBlank

pardoned flips to true exactly once, after genuine reform, so reintegration can grant a clean slate without erasing the history that earned it — the counters stay, but the pawn is marked forgiven.

IsBlank is a computed convenience:

public bool IsBlank => crimesCommitted == 0 && escapeAttempts == 0 && contrabandMade == 0
                       && timesCaught == 0 && reform == 0f && !pardoned;

A blank record is one nothing has ever been written to. Note what IsBlank omits: timesSearched and lastCrimeTick are not in the test. A pawn who was searched and found clean — searched but never caught, never a crime — is still "blank" and will be pruned. That is intentional: being checked is not a mark against you, only being found is.


For vs PeekFor — the distinction that matters

Records are handed out by GameComponent_CriminalRecords, and there are two ways to ask for one. The difference is not cosmetic.

// Creates a blank record on first ask. Use on a WRITE path.
public static CriminalRecord For(Pawn p);

// Returns null if no record exists yet. Use on a READ path that must not create.
public static CriminalRecord PeekFor(Pawn p);
  • For(pawn) guarantees a record — if the pawn has none, it makes a blank one and stores it. Call this when you are about to write something ("record a crime"), because you need a record to write to.
  • PeekFor(pawn) returns the existing record or null. Call this on a read path that should not leave a trail. The archetypal caller is Propensity.Nurture: it wants to read reform if a record exists, but merely asking about a pawn's disposition must not conjure a criminal file for an innocent. If Nurture used For, every mood check would stamp a record onto every pawn on the map.

Both are static and both are null-safe — a null pawn (or a game with no component yet) returns null rather than throwing.

The rule of thumb: write with For, read with PeekFor. The only reason Nurture can be called on all pawns constantly without littering the save with empty records is that it peeks.


Persistence

GameComponent_CriminalRecords is the single source of truth for the whole game. The design note is explicit: "modules never keep their own per-pawn crime state, they read and write here, which is what keeps them agreeing with each other."

It stores records in a Dictionary<Pawn, CriminalRecord> and serialises them with RimWorld's Scribe:

Scribe_Collections.Look(ref records, "records",
    LookMode.Reference, LookMode.Deep, ref tmpPawns, ref tmpRecords);

The keys save as references (a pawn is saved elsewhere; the record just points at them) and the values save deep (the record's fields are written inline). Each CriminalRecord.ExposeData scribes its own eight fields with their defaults, so a save omits any field still at its default.

Post-load cleanup

On PostLoadInit, the component prunes the dictionary:

records.RemoveAll(kv => kv.Key == null || kv.Value == null || kv.Value.IsBlank);

Three things get dropped:

  1. Null keys — a pawn who has been removed or garbage-collected. "Dead/removed pawns take their records with them; a null key would throw later." This is defensive: a stale key would crash a later lookup.
  2. Null values — corruption guard.
  3. Blank records — anything IsBlank is true for. There is no reason to persist a file that records nothing, and pruning them keeps the save from accumulating one empty record per pawn ever examined.

The upshot: records are cheap and self-cleaning. You can For(pawn) freely on a write path without worrying about bloat, because anything that never got a real mark written to it evaporates on the next load.

The singleton

The component keeps a private static instance set both in its constructor and re-set on PostLoadInit, which is what lets For and PeekFor be static entry points reachable from anywhere without threading a reference through every caller. If no game is loaded (no component), both return null safely.


Part of the Institution suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).