# Criminal Record — the history of fact If [Propensity](Propensity.md) 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](Propensity.md)'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: ```csharp 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. ```csharp // 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` and serialises them with RimWorld's Scribe: ```csharp 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: ```csharp 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.*