Per the updated project policy, AI/Claude attribution lives in exactly one NOTICE file per repo and nowhere else -- removed it from the About description, README, wiki, and roadmap.
8.1 KiB
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
reformclears 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
pardonedmarks.
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 ornull. Call this on a read path that should not leave a trail. The archetypal caller isPropensity.Nurture: it wants to readreformif a record exists, but merely asking about a pawn's disposition must not conjure a criminal file for an innocent. IfNurtureusedFor, 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 withPeekFor. The only reasonNurturecan 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:
- 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.
- Null values — corruption guard.
- Blank records — anything
IsBlankis 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.