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:
@@ -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).*
|
||||
Reference in New Issue
Block a user