Wiki: player-facing rewrite of every layer + add a scenario FAQ, linked from Home

This commit is contained in:
flan
2026-07-16 02:18:25 +00:00
parent ff703271ba
commit 8250dcb369
32 changed files with 1654 additions and 2510 deletions
+65 -128
View File
@@ -2,163 +2,100 @@
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.
kinds of thing. Propensity is computed fresh from who a pawn is and how they're treated; a record is a
durable log of what they've 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`.
There is exactly **one criminal record per pawn**, and the whole suite shares it. Classification grades
on it, parole gates on it, the deterrence loop reacts to it, and reintegration remembers it. Because
everyone reads the same record, no two parts of the suite can disagree about a pawn's past.
---
## The fields
## What a record remembers
`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.
A record is a small set of counters plus two special marks. Core provides the record and keeps it
safe; the individual events are logged by whichever mod they happen in.
| 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 |
| What it tracks | Starts at | Meaning | Tracked by | Used by |
|---|---:|---|---|---|
| Crimes committed | 0 | count of committed crimes | Justice (and gang fights) | classification risk score, deterrence |
| Escape attempts | 0 | breakout attempts | Contraband | classification risk score |
| Contraband made | 0 | items brewed / whittled | Contraband | classification risk score |
| Times searched | 0 | how often searched | Contraband (warden search) | search prioritisation |
| Times caught | 0 | searches that found something | Contraband (warden search) | classification risk score |
| Last crime | never | when the most recent crime happened | Justice | recency / cooldown checks |
| Reform | 0 | rehabilitation score (see below) | Justice (discipline, parole) | **Propensity's Nurture**, parole gate, reintegration |
| Pardoned | no | granted a clean slate after genuine reform | Justice (on release) | parole gate, reintegration |
`lastCrimeTick` defaults to `−1` — the sentinel for "never," distinct from tick `0`. Everything else
defaults to zero/false.
"Last crime" starts at *never* — a distinct value from "at the very first moment of the game."
Everything else starts at zero / no.
> **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.
> **Who logs what.** Core is the *vault*, not the *clerk*. It hands out records and keeps them; the
> actual "a crime just happened, add one" logging lives in the mod where the event occurs. The columns
> above name which mod does that — Core itself only holds the fields.
## `reform` — the pivot of the suite
## 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.
Most of a record is inert tallies. **Reform** is the one entry that feeds back into behaviour, and it's
worth its own section. It's raised by good treatment and good conduct, decays without either, and can
be driven negative by harsh punishment. A record is where a sentence's *outcome* is stored, and three
systems lean on that one number:
```
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
```
- **[Propensity](Propensity.md)'s Nurture** scales a pawn's disposition by their reform score — so a
genuinely rehabilitated pawn (reform above 0) is calmer *for good*, and a prisonized one (reform
below 0) is inflamed *for good*. This is how institutionalization and recidivism enter the game
without a separate mechanic bolted on.
- **Parole** (Justice) gates release on it: a pawn is only releasable once reform clears a threshold.
It's the number that says "this one is ready."
- **Reintegration** reads it to decide when a clean slate is earned — which is what the *pardoned*
mark records.
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:
The negative range isn't a bug — it's the "hardened" end of the spectrum, and Nurture is written to
expect it.
- **[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.
## Pardoned and blank records
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.
The **pardoned** mark flips on 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.
## `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.
A record counts as **blank** when nothing meaningful has ever been written to it: no crimes, no escape
attempts, no contraband, never caught, reform still 0, not pardoned. Note what blankness *ignores*:
times searched and the last-crime time aren't part of the test. A pawn who was searched and found clean
— searched but never caught, never a crime — still counts as blank. That's intentional: being
*checked* is not a mark against you, only being *found* is.
---
## `For` vs `PeekFor` — the distinction that matters
## Records are created only when needed
Records are handed out by `GameComponent_CriminalRecords`, and there are two ways to ask for one. The
difference is not cosmetic.
Records are made lazily. A pawn only gets a record the first time something is actually **written** to
it — a crime, a search that found something, a reform change. Simply **reading** a pawn's disposition
never creates one.
```csharp
// Creates a blank record on first ask. Use on a WRITE path.
public static CriminalRecord For(Pawn p);
That distinction matters because Propensity's Nurture reads a pawn's reform score constantly, on every
pawn on the map, as part of judging their mood. If merely checking disposition created a record, every
mood check would stamp a criminal file onto every innocent pawn on the map. Because reading only peeks
— and tolerates a pawn having no record at all — the innocent stay off the books.
// 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.
The rule the whole suite follows: **something happened → a record is created and written; just looking
→ no record is created.**
---
## 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."*
Records save with your game and reload with it, and there's exactly one shared store for the whole
colony — no mod keeps its own private copy, which is what keeps them all agreeing with each other.
It stores records in a `Dictionary<Pawn, CriminalRecord>` and serialises them with RimWorld's Scribe:
On load, that store cleans itself up. Three kinds of record get dropped:
```csharp
Scribe_Collections.Look(ref records, "records",
LookMode.Reference, LookMode.Deep, ref tmpPawns, ref tmpRecords);
```
1. **Records of pawns who are gone** — dead or removed. They take their records with them.
2. **Corrupt entries** — a guard against a broken save.
3. **Blank records** — anything still blank. There's no reason to keep a file that records nothing.
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.
The upshot: **records are cheap and self-cleaning.** Anything that never got a real mark written to it
simply evaporates on the next load, so the save never bloats with one empty file per pawn ever glanced
at.
---