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.
166 lines
8.2 KiB
Markdown
166 lines
8.2 KiB
Markdown
# 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<Pawn, CriminalRecord>` 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. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
|