Wiki: player-facing rewrite of every layer + add a scenario FAQ, linked from Home
This commit is contained in:
+65
-128
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user