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.
This commit is contained in:
flan
2026-07-15 20:30:38 +00:00
parent 25eb3a77db
commit 3530100ed5
31 changed files with 4574 additions and 6 deletions
+165
View File
@@ -0,0 +1,165 @@
# 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).*