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).*
+88
View File
@@ -0,0 +1,88 @@
# Institution: Core — Wiki
*The shared foundation of the **Institution** suite: a set of RimWorld 1.6 mods about what an
institution does to the people inside it, and the crime, policing, and justice that put them there.*
Core is the engine every other Institution mod runs on. **On its own it changes nothing you can
see** — no items, no jobs, no UI, no XML. It ships one small assembly and is completely inert until
another Institution mod is installed on top of it. You install Core only because something else asks
for it.
This wiki documents what that engine actually computes, down to the exact numbers.
---
## The one idea
> **Every pawn has a criminal propensity on a spectrum — nature (traits) × nurture (circumstance,
> mood, treatment) — and the colony can police it.**
That single sentence is the whole suite. Colonists, prisoners, and slaves drift toward crime on a
continuous spectrum; a policing and justice layer discovers, catches, punishes, reforms, or paroles
them. Core is where the *spectrum* lives as code — before anyone is caught, before there is a record
to read, before there is a warden to search them.
## Why a shared engine
The propensity idea did not start abstract. It was written twice, concretely, in two different mods:
the piss spree's `WouldFoulPeople` (which drunk, miserable pawn starts a mess) and the breakout's
`WouldArmSelf` (which prisoner whittles a shiv when the door opens). Both asked the same real
question — *would **this** pawn do it?* — and both answered it their own way.
Core exists so they stop disagreeing. When the contraband system, the justice system, and the gang
system all read one disposition engine, they agree about who is dangerous instead of each computing
it from scratch and drifting apart. One pawn who is "the scary one" is the scary one to every module
at once. That coherence — not any single number — is the point of a shared substrate.
Core carries exactly the three things every Institution mod reads and none should own alone, and
nothing else:
| Pillar | Class | Answers | Page |
|---|---|---|---|
| **Propensity** | `Propensity` | *Would this pawn do it?* (nature × nurture, seeded) | [Propensity](Propensity.md) |
| **Criminal record** | `CriminalRecord` | *What has this pawn actually done?* (one per-pawn history) | [Criminal Record](Criminal-Record.md) |
| **Secured context** | `SecuredContexts` | *What kind of hold is this pawn under, and who may search them?* | [Secured Context](Secured-Context.md) |
The distinction between the first two is the spine of the whole design: **propensity is suspicion, a
record is fact.** Propensity says "this pawn *seems* dangerous"; a record says "this pawn *has*
escaped twice and shanked a guard." Keeping them separate is what lets the colony act on evidence
instead of on vibes.
## The pages of this wiki
- **[Propensity](Propensity.md)** — nature × nurture in full: the trait-weight table, the nurture
multiplier ladder, the `Would()` formula with worked examples, and why the roll is seeded per pawn.
- **[Criminal Record](Criminal-Record.md)** — every field, who writes and reads each, the
`For` vs `PeekFor` distinction, how it persists, and why `reform` is the pivot of the suite.
- **[Secured Context](Secured-Context.md)** — the Free / Prisoner / Slave kinds, `Of()` and
`OnMap()`, `IsHeld` vs `CanConceal`, and why the suite keys off this instead of "prisoner."
- **[Treatment Engine](Treatment-Engine.md)** — the shared rehabilitation maths: mark a condition
treatable, reduce it by skill × facility quality per session, and build a recovery track toward
discharge. Ward's psychiatric care and Justice's reform share this one engine.
- **[Modder API](Modder-API.md)** — the public surface any mod can call, and the one seam
(`Propensity.DeterrenceFactor`) that lets a justice layer feed back into disposition without Core
ever depending on it.
## The suite
Each mod stands alone as an install; together they form one system. Core is the leaf everything else
depends on.
| Mod | What it adds | Needs |
|---|---|---|
| **Institution: Core** (this) | the propensity / record / context engine | base game only |
| **Institution: Contraband** | concealment, improvised shivs, tunnels, warden search, warden corruption | Core |
| **Institution: Justice** | classification, deterrence, discipline, parole, regime, and **policing** (colony crime → weighed arrest) | Core, Harmony |
| **Institution: Gangs** | gangs as contraband economies: joining, smuggling, rivalry, fights-as-crime | Core, Contraband, Justice |
| **Institution: Ward** | psychiatric care — a prisoner treated, not punished, on Core's treatment engine | Core, Harmony |
| **Foul Play** | the vessel + substance + throw framework, home of the "Piss Nuke" | standalone; bridges to Core + Contraband |
## Requirements & load order
- **RimWorld 1.6.** Core references only the base game — no Harmony, no DLC, no other mod.
- Load Core **before** any other Institution mod (`loadAfter` Ludeon.RimWorld only).
- `packageId`: `flan.institution.core`.
---
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+233
View File
@@ -0,0 +1,233 @@
# Modder API — building on Core
Core is a library. It has no XML, no defs, no UI — its entire purpose is to be *called*, by the other
Institution mods and by yours. This page is the public surface, with the one seam that matters most
(`Propensity.DeterrenceFactor`) explained in full.
Everything here is verified against the three source files in `Source/Core/`.
---
## The namespace is `Contraband`
Before anything else: **all of Core's types live in `namespace Contraband`**, not `Core` and not
`Institution`. This is historical — the engine was extracted from Institution: Contraband — and it is
not going to change under you, but it does mean your `using` looks slightly surprising:
```csharp
using Contraband; // Propensity, CriminalRecord, GameComponent_CriminalRecords,
// SecuredKind, SecuredContext, SecuredContexts,
// TreatmentProgram, TreatableConditionExtension
```
Reference the assembly `InstitutionCore.dll` (packageId `flan.institution.core`) and load your mod
after Core.
## The public surface at a glance
| Call | Returns | Creates state? | Use for |
|---|---|---|---|
| `Propensity.Nature(pawn)` | `float 0..1` | no | fixed trait-based disposition |
| `Propensity.Nurture(pawn)` | `float` (multiplier) | no | live circumstance multiplier |
| `Propensity.Would(pawn, baseChance, salt, cap=0.85)` | `bool` | no | the seeded yes/no for a behaviour |
| `Propensity.DeterrenceFactor` | `Func<Pawn,float>` field | — | the seam a justice layer fills |
| `GameComponent_CriminalRecords.For(pawn)` | `CriminalRecord` | **yes** (blank on first ask) | write paths |
| `GameComponent_CriminalRecords.PeekFor(pawn)` | `CriminalRecord?` | no | read paths |
| `SecuredContexts.Of(pawn)` | `SecuredContext?` | no | one pawn's hold kind |
| `SecuredContexts.OnMap(map)` | `IEnumerable<SecuredContext>` | no | every held/free pawn on a map |
| `TreatmentProgram.HasTreatableCondition(pawn)` | `bool` | no | is there a marked condition to work through? |
| `TreatmentProgram.ReduceConditions(pawn, strength)` | `void` | mutates hediffs | run one session's worth of treatment |
| `TreatmentProgram.FacilityQuality(pawn)` | `float 0.5..1.5` | no | scale a session by the room's quality |
| `TreatmentProgram.AdvanceRecovery(pawn, def, amount)` | `float` | **yes** (recovery hediff) | build a discharge / parole track |
All of these are `static` and null-safe. None throws on a null pawn; they return `0.05`/`1f`/`null`/
`false` as appropriate. See the [Propensity](Propensity.md), [Criminal Record](Criminal-Record.md),
and [Secured Context](Secured-Context.md) pages for the mechanics behind each.
---
## Asking "would this pawn?"
The idiomatic pattern for a new behaviour is: pick a **base rate**, pick a **stable salt** unique to
your question, and call `Would`. Do not re-implement the nature/nurture math — that is the whole point
of the shared engine.
```csharp
using Contraband;
// A unique, STABLE salt for this specific question. Any constant int works; keep it
// distinct from other questions so the rolls stay independent per behaviour.
private const int Salt_WouldStealMeds = 0x5EED01;
if (Propensity.Would(pawn, baseChance: 0.08f, salt: Salt_WouldStealMeds))
{
// this pawn, under these conditions, is the kind who does this
}
```
Because the roll is seeded on `pawn.thingIDNumber ^ salt`, the answer is a stable fact per pawn per
question and survives save/reload — so you can ask it repeatedly and get a consistent character, and
different salts give you independent facts about the same pawn. Pass a custom `cap` if `0.85` is the
wrong ceiling for your behaviour (e.g. a lower cap for something that should always keep a large
chance of not happening).
If you only need the ingredients — say, to display a disposition readout or gate a threshold — read
`Nature` and `Nurture` directly. Institution: Gangs, for instance, joins a pawn to a gang when
`Nature(pawn) * Nurture(pawn)` clears its own threshold, computed from these two calls rather than
from `Would`.
## Reading and writing records
Follow the one rule from the [Criminal Record](Criminal-Record.md) page — **write with `For`, read
with `PeekFor`** — so read paths never litter the save with blank records.
```csharp
using Contraband;
// WRITE: an event happened, so we need a record to write to.
CriminalRecord rec = GameComponent_CriminalRecords.For(pawn);
rec.contrabandMade++;
// READ: peek, and tolerate null (the pawn may have no record yet).
CriminalRecord existing = GameComponent_CriminalRecords.PeekFor(pawn);
if (existing != null && existing.reform < 0f)
{
// this pawn has been hardened
}
```
Core owns the storage and persistence; you own the meaning of your own events. Do not keep your own
per-pawn crime dictionary — that is exactly the fragmentation Core exists to prevent.
## Working across hold kinds
Reduce a pawn to its context and branch on the *predicate*, not the label:
```csharp
using Contraband;
SecuredContext? maybe = SecuredContexts.Of(pawn);
if (maybe is SecuredContext ctx && ctx.IsHeld)
{
// prisoner OR slave OR ward patient -- all covered, no special cases
}
// Sweep everyone the suite cares about on a map:
foreach (SecuredContext c in SecuredContexts.OnMap(map))
{
if (c.CanConceal) { /* consider hoarders too, not just the held */ }
}
```
`Of` returns `null` for anything the suite does not model (animals, mechs, the dead), so a `null`
check is your "not our business" branch.
## Running a treatment programme
Core carries the maths of "sustained attention in a secured facility reduces a marked condition and
builds a recovery track toward discharge" — the shared engine behind Ward's psychiatric care and
Justice's rehabilitation. Mark a hediff treatable with `TreatableConditionExtension`, then drive it
from your own job or interaction:
```csharp
using Contraband;
float strength = skill * TreatmentProgram.FacilityQuality(pawn) * share; // your own blend
TreatmentProgram.ReduceConditions(pawn, strength); // lower the condition
if (!TreatmentProgram.HasTreatableCondition(pawn))
{
TreatmentProgram.AdvanceRecovery(pawn, MyDefOf.Recovery, 0.15f * strength); // build discharge
}
```
You own the interaction mode, the job, the recovery hediff (give it a negative `severityPerDay` so it
decays without attention), and the discharge alert; Core owns only the numbers. Full formulas and the
consumer contract are on the **[Treatment Engine](Treatment-Engine.md)** page.
---
## The `DeterrenceFactor` seam
This is the most important extension point in Core, and the reason Core can remain a **dependency-free
leaf** while a colony-wide deterrence feedback loop runs *through* it.
### What it is
```csharp
// In Propensity:
public static System.Func<Pawn, float> DeterrenceFactor = _ => 1f;
```
A single static delegate. `Nurture` multiplies its result by `DeterrenceFactor(pawn)` as its last
step. Out of the box the delegate returns `1f` for every pawn — perfectly neutral — so **Core alone
behaves as if deterrence did not exist.**
### What it is *for*
Deterrence is a property of the whole colony's climate of order: a well-policed colony should deter
everyone a little; a lawless one should embolden them. That is a feedback loop — catching and
punishing *one* pawn changes the disposition of *the rest*. But deterrence itself is a justice-layer
concept, and Core must not know the justice layer exists.
The seam resolves the tension. Core defines *where* the colony's climate multiplies into disposition
(the last line of `Nurture`) without defining *what* that climate is. From the source:
> *"Deterrence lives in the Justice layer, so Core reads it through the seam above — neutral until
> Justice fills it. This one seam is what keeps Core a dependency-free leaf despite the deterrence
> feedback loop running through it."*
### How Institution: Justice fills it
When Institution: Justice loads, it assigns the delegate, pointing it at its own live deterrence
level. The shape it installs maps a `0..1` deterrence reading to a multiplier that straddles `1.0`:
```csharp
// Illustrative — this lives in Institution: Justice, not Core.
Propensity.DeterrenceFactor = pawn =>
{
float level = DeterrenceLevelFor(pawn.Map); // 0 = lawless, 1 = tightly policed
return Mathf.Lerp(1.3f, 0.7f, level); // lawless emboldens, order deters; neutral at 0.5
};
```
| Deterrence level | Factor | Effect on disposition |
|---|---:|---|
| `0.0` (lawless) | `1.3` | everyone ~30% more inclined |
| `0.5` (baseline) | `1.0` | neutral — a default colony sits exactly here |
| `1.0` (tightly policed) | `0.7` | everyone ~30% less inclined |
*(The `1.3 → 0.7` range, neutral at the `0.5` baseline, are Justice's numbers, documented here for context;
Core only guarantees the neutral `1f` default. Confirm against Institution: Justice's own source for
its current values.)*
### How to fill it yourself
If you are writing your own policing or morale layer and want it to feed disposition the same way,
just assign the delegate — ideally at game load, and ideally composing with whatever is already there
rather than clobbering it:
```csharp
using Contraband;
using UnityEngine;
// Compose: fold your factor into any existing one (e.g. Justice's) instead of overwriting.
var previous = Propensity.DeterrenceFactor;
Propensity.DeterrenceFactor = pawn => previous(pawn) * MyClimateFactorFor(pawn);
```
Two cautions. First, it is a single static field — if two mods both *assign* it (rather than compose),
the last one wins, so compose when you can. Second, keep your factor bounded and centred near `1.0`:
`Nurture` has no clamp on this term, so an extreme factor multiplies straight through into every
pawn's `Would` roll. Return `1f` when you have nothing to say, exactly as the default does.
### Why a delegate and not a dependency
The alternative — Core calling into Justice — would make Core depend on Justice, inverting the whole
topology and breaking Core's promise that it references *only the base game*. A single assignable
`Func<Pawn,float>`, neutral by default, lets the feedback loop run through Core while the arrow of
dependency still points only *at* Core. It is the smallest possible seam that buys the largest possible
decoupling, and it is why Core can sit at the bottom of the suite with zero dependencies of its own.
---
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+265
View File
@@ -0,0 +1,265 @@
# Propensity — nature × nurture
`Propensity` is the suite's disposition engine. It answers one question and only one: *"would
**this** pawn do it?"* — never *"would a prisoner,"* never *"would a colonist."* Every behaviour in
the suite that hinges on a pawn's character routes through here, so the crime system, contraband
brewing, escape arming, and gang recruitment all read the same answer instead of each guessing.
The model is deliberately old-fashioned: **nature × nurture.**
- **Nature** is a fixed property of the person — their traits. Almost nobody is strongly disposed.
This does not change over a pawn's life.
- **Nurture** is what the colony has done *to* them — mood, mistreatment, unmet needs, the shadow a
sentence leaves. This is the half **you** control, and the half no other mod models.
Good conditions pull nurture down; neglect and cruelty push it up. A saint left to rot can cross the
line; a monster kept content and deterred may never act. The engine is built so both of those stories
are possible.
Everything on this page is verified against `Source/Core/Propensity.cs`.
---
## Nature — who they are
```
Nature(pawn) : float in [0, 1]
n = 0.05 // baseline — everyone has a little
for each listed trait the pawn has:
n += weight
return Clamp01(n) // never below 0, never above 1
```
The baseline is `0.05`. That is the floor of the human condition in this model: everyone is capable
of *something*, most people barely. From there, traits add or subtract.
### Trait weights
Traits are read **reflectively by defName** via `DefDatabase<TraitDef>.GetNamedSilentFail`. If a
trait's mod is not installed, that entry is silently skipped rather than throwing a hard reference —
which is how Core consumes Vanilla Traits Expanded's dark traits without *depending* on VTE.
| Trait | Weight | Source | Note |
|---|---:|---|---|
| Psychopath | **+0.45** | Vanilla | no empathy; the heaviest single input |
| Kleptomaniac | **+0.40** | Vanilla Traits Expanded | consumed, not rebuilt |
| Bloodlust | **+0.35** | Vanilla | enjoys violence |
| Pyromaniac | **+0.30** | Vanilla Traits Expanded | consumed, not rebuilt |
| Greedy | **+0.25** | Vanilla | wants more than their share |
| Abrasive | **+0.15** | Vanilla | friction with everyone |
| Ascetic | **−0.15** | Vanilla | wants little, takes little |
| Kind | **−0.30** | Vanilla | the strongest pull *down* |
The design note in the source is explicit: *"We CONSUME Vanilla Traits Expanded's
kleptomaniac/pyromaniac as inputs here rather than rebuild them; vanilla's own dark traits count
too."* Core does not add traits of its own — it reads the ones the ecosystem already has.
### Worked Nature values
Because weights simply add and then clamp, Nature is easy to read by hand:
| Pawn | Arithmetic | Nature |
|---|---|---:|
| Ordinary pawn (none of the above) | `0.05` | **0.05** |
| Kind pawn | `0.05 − 0.30 = −0.25` → clamp | **0.00** |
| A single Greedy trait | `0.05 + 0.25` | **0.30** |
| Psychopath | `0.05 + 0.45` | **0.50** |
| Psychopath **and** Kind | `0.05 + 0.45 − 0.30` | **0.20** |
| Greedy + Abrasive | `0.05 + 0.25 + 0.15` | **0.45** |
| Psychopath + Bloodlust + Kleptomaniac | `0.05 + 0.45 + 0.35 + 0.40 = 1.25` → clamp | **1.00** |
Two things fall out of this. First, the clamp is not decorative: a Kind pawn floors at exactly `0`
(nature can never make them *disposed*, only never disposed), and a stacked monster ceilings at `1`.
Second, traits genuinely net against each other — a Kind Psychopath is a real, middling `0.20`, not a
contradiction the engine has to resolve by fiat.
Most rolled pawns sit at or near the `0.05` floor. That is intended: *"Most pawns sit near the floor;
a rare few are strongly inclined."* A colony full of ordinary people is supposed to be mostly safe on
nature alone. What makes them dangerous is nurture.
---
## Nurture — what you have done to them
```
Nurture(pawn) : float (a multiplier, >= ~0.5 in normal play)
m = 1.0 // ordinary circumstance
mood = pawn.needs.mood.CurLevelPercentage (or 1.0 if none)
if mood < 0.20: m *= 2.5 // at the floor of despair
else if mood < 0.35: m *= 1.6 // badly kept
if IsHeld(pawn) and mood < 0.40: m *= 1.4 // held AND unhappy compounds
rec = criminal record (peek — does not create one)
if rec != null and rec.reform != 0:
m *= Clamp(1 - rec.reform * 0.5, 0.4, 2.0) // prisonization
m *= DeterrenceFactor(pawn) // colony climate of order (1.0 in Core alone)
return m
```
`1.0` is an ordinary pawn under ordinary conditions. The number rises as things go wrong and falls as
they go right. Each clause below is one lever.
### The multiplier ladder
| Clause | Factor | Applies when | Stacks? |
|---|---|---|---|
| **Despair** | ×2.5 | `mood < 0.20` | mutually exclusive with "badly kept" |
| **Badly kept** | ×1.6 | `0.20 ≤ mood < 0.35` | mutually exclusive with "despair" |
| **Held & unhappy** | ×1.4 | `IsHeld` **and** `mood < 0.40` | on top of the mood clause |
| **Prisonization** | ×`Clamp(1 − reform×0.5, 0.4, 2.0)` | `reform ≠ 0` | on top |
| **Deterrence** | ×`DeterrenceFactor(pawn)` | always (neutral `1.0` in Core alone) | on top |
The two mood clauses are an `if / else-if`: a pawn is either in despair *or* badly kept, never both.
The "held & unhappy" clause is separate and multiplies again — so a mistreated prisoner in despair
compounds `2.5 × 1.4 = 3.5` before anything else. The source calls that exactly what it is: *"a badly
run cell."*
### Prisonization — the reform lever
The `reform` clause deserves its own look, because it is where a sentence leaves a permanent mark.
```
reform factor = Clamp(1 - reform * 0.5, 0.4, 2.0)
```
| `reform` | Meaning | Factor |
|---:|---|---:|
| **+1.0** | fully rehabilitated | 0.50 |
| +0.5 | improving | 0.75 |
| 0 | untouched (clause skipped) | *1.00* |
| −0.5 | hardening | 1.25 |
| **−1.0** | prisonized | 1.50 |
| ≥ +1.2 | (over-reformed) | clamp floor **0.40** |
| ≤ −2.0 | (utterly broken) | clamp ceiling **2.00** |
`reform` is nominally documented as `0..1`, but the punishment machinery can and does drive it
**negative** — the source discusses `reform < 0` ("hardened, prisonized") in as many words. Within the
realistic range `[−1, +1]` the factor spans `0.5 … 1.5`; the `0.4 / 2.0` clamps only bite at extremes
outside that, catching a pawn who has been endlessly punished or endlessly rehabilitated.
The important design property: Core only ever **reads** `reform` here. Nothing in Core moves it. The
Justice layer's `Discipline` and `Parole` are what nudge it up or down — which means
*institutionalization and recidivism become the suite's without a parallel mechanic.* A pawn who was
broken by a brutal prison stays broken (nurture ×1.5) after release; a pawn genuinely reformed stays
calmer (×0.5) for good. The scar is carried by one float.
### Deterrence — the climate of order
The final `× DeterrenceFactor(pawn)` is the seam that lets the whole colony's climate feed back into
each pawn's disposition. **In Core alone it is neutral — a flat `1.0`** — because Core does not know
what deterrence is. When Institution: Justice is loaded, it fills this in: a well-policed colony pulls
the factor below `1` and deters everyone a little; a lawless one pushes it above `1` and emboldens
them. That is what makes catching and punishing *one* pawn matter to the disposition of *the rest*.
The full mechanics of the seam — and how to fill it from your own mod — are on the
[Modder API](Modder-API.md) page. For now: with Core installed by itself, this clause does nothing,
and that is correct.
---
## Would — the full roll
`Nature` and `Nurture` are ingredients. `Would` is the meal: the actual yes/no for a specific
behaviour at a specific base rate.
```
Would(pawn, baseChance, salt, cap = 0.85) : bool
chance = min(cap, baseChance * (0.1 + Nature(pawn) * 2) * Nurture(pawn))
return Rand.ChanceSeeded( Clamp01(chance), pawn.thingIDNumber ^ salt )
```
Three things are happening in that one line of arithmetic:
1. **`baseChance`** is the caller's dial — the base rate of *this* behaviour for an average pawn
(a rare act passes a small number, a common one a larger). Core does not decide it; the module
asking the question does.
2. **`(0.1 + Nature × 2)`** is the nature amplifier. It spans **×0.1** (a saint, Nature 0) to
**×2.1** (Nature 1). At the common `0.05` floor it is `×0.2`. So a floor pawn is one-fifth as
likely as the base rate; a maxed monster over twice as likely — *before* nurture.
3. **`× Nurture`** applies circumstance on top, and `min(cap, …)` caps the whole thing at `0.85` by
default. Nobody is ever a dead certainty; there is always slack. A caller who wants a harder or
softer ceiling passes their own `cap`.
### Worked example 1 — an ordinary colonist, content
Base rate `0.10`, an ordinary pawn (Nature `0.05`), good mood so Nurture `1.0`:
```
nature term = 0.1 + 0.05 * 2 = 0.2
chance = min(0.85, 0.10 * 0.2 * 1.0) = 0.02 → 2%
```
Two percent, and — critically — **seeded**. For this pawn and this question it is a fixed 2% coin
that either comes up or does not; it is not re-flipped every tick.
### Worked example 2 — a mistreated psychopath prisoner
A Psychopath + Bloodlust prisoner (Nature `0.85`), mood `0.15` (despair ×2.5), held and unhappy
(×1.4), `reform = −0.5` (hardening → ×1.25), Core-only so deterrence `1.0`. Base rate `0.10`:
```
nature term = 0.1 + 0.85 * 2 = 1.8
nurture = 2.5 * 1.4 * 1.25 = 4.375
chance = min(0.85, 0.10 * 1.8 * 4.375) = min(0.85, 0.7875) = 0.7875 → ~79%
```
Same base rate as example 1, same engine — but nature and a badly-run cell have turned a 2% pawn into
a near-certainty. The player did that, clause by clause.
### Worked example 3 — hitting the cap
A fully stacked monster (Nature clamps to `1.0`), in despair (×2.5) and held & unhappy (×1.4), in a
lawless colony where Justice has set deterrence to `1.4`. Base rate `0.10`:
```
nature term = 0.1 + 1.0 * 2 = 2.1
nurture = 2.5 * 1.4 * 1.4 = 4.9
raw chance = 0.10 * 2.1 * 4.9 = 1.029
chance = min(0.85, 1.029) = 0.85 → capped at 85%
```
The raw product blew past `1.0`; the cap reins it to `0.85`. Even here, a `0.15` sliver of "not
today" survives. That sliver is deliberate — it keeps the worst pawn a character with a bad day
coming, not a scripted event.
---
## The seeded roll — characters, not a dice cup
The last argument of `Would` is a **salt**, and the seed is:
```
seed = pawn.thingIDNumber ^ salt
```
`Rand.ChanceSeeded` turns that seed into a *deterministic* pass/fail. This is the single most
important design decision on this page, and it is worth being precise about why.
**Save/reload stability.** Because the seed is derived from the pawn's stable `thingIDNumber` and a
fixed salt — not from the live RNG stream — asking "would they?" gives the same answer before and
after a save/reload, as long as the inputs (traits, mood, reform) are the same. You cannot scum a
reload to re-roll a pawn into a different person. *"WHO they are does not shift under them on a
save/reload."*
**A character, not a per-tick lottery.** A naive implementation would flip a fresh coin every tick,
so any pawn eventually does anything if you wait long enough. Seeding instead makes "would this pawn
pocket a shiv?" a *fixed fact* about that pawn under those conditions — *"what makes them a character,
not a dice cup."*
**Live circumstance still bites.** Seeding freezes the *identity*, not the *situation*. Nurture is
recomputed live, so as mood collapses or reform hardens, the same pawn's `chance` climbs and the same
seeded coin can flip from "no" to "yes." The pawn who would not have acted last month acts now — not
because the dice changed, but because you let their world get worse.
**Different questions, different salts.** Each behaviour passes its own salt, so the rolls are
independent. The source puts it plainly: *"a man who would pocket a shiv is not therefore a man who
would inform on his cellmate."* One pawn can be reliably one kind of trouble and reliably not another,
and that pattern is stable across the whole game.
---
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+153
View File
@@ -0,0 +1,153 @@
# Secured Context — the kind of hold a pawn is under
The suite has a rule that looks small and is load-bearing: **nothing keys off "is this a prisoner."**
It keys off `SecuredContext` — the *kind of hold* a pawn is under. That indirection is why a ward
patient, a slave, and (later) anyone inside a secure zone get the same concealment, search, schedule,
and needs machinery for free, without any module hard-coding a pawn status.
`SecuredContext` reduces a pawn to one of three kinds and the question "who, if anyone, may search
them." Everything downstream reads that, so a new module works across every context the day it is
written, and a new *context* works across every existing module the day it is added.
Verified against `Source/Core/SecuredContext.cs`.
---
## The three kinds
```csharp
public enum SecuredKind
{
Free, // a free colonist
Prisoner, // a prisoner of the colony
Slave, // a slave (Ideology)
}
```
| Kind | Who it is | May search them | Can conceal? | Held against their will? |
|---|---|---|---|---|
| **Free** | a free colonist | *no one* — until a secure-area or policing layer grants standing | **yes** — they can hoard | no |
| **Prisoner** | a prisoner of the colony | the **warden** | yes | **yes** |
| **Slave** | a slave (Ideology DLC) | the **overseer** | yes | **yes** |
Two subtleties fall out of this table:
- **Free colonists can still conceal.** A free colonist "can hoard contraband, but no one has
standing to search them until a secure-area or policing layer grants it." Freedom is not innocence —
it is only the absence of an authority permitted to check. That is why `CanConceal` (below) includes
the free.
- **Prisoner and Slave differ only in who holds the keys.** Both are "held"; the search authority is
the warden for one and the overseer for the other. Downstream modules that only care "is someone
entitled to search this pawn" read `IsHeld` and never branch on which.
## The struct
`SecuredContext` is a small `readonly struct` — the kind, the pawn, and two computed predicates.
```csharp
public readonly struct SecuredContext
{
public readonly SecuredKind Kind;
public readonly Pawn Pawn;
// Held against their will -- prisoner or slave. Excludes a free colonist.
public bool IsHeld => Kind == SecuredKind.Prisoner || Kind == SecuredKind.Slave;
// Anyone with something to hide -- includes free colonists (who may hoard).
public bool CanConceal => Pawn != null;
}
```
The two predicates split the world along two different axes, and picking the right one is the whole
skill of using this type:
| Predicate | True for | Ask it when you care about… |
|---|---|---|
| `IsHeld` | Prisoner, Slave | **custody** — who is under the colony's control, who can be disciplined, whose cell can be searched |
| `CanConceal` | Free, Prisoner, Slave (any real pawn) | **contraband** — who could be *hiding* something, regardless of status |
For example, [Propensity](Propensity.md)'s Nurture uses `IsHeld` in its "held & unhappy compounds"
clause — it only wants to pile the ×1.4 penalty on a pawn the colony actually holds and mistreats, not
on a grumpy free colonist. A warden-search feature, by contrast, would gate on `CanConceal` to decide
who is even worth checking, and then on standing to decide whether it is *allowed* to.
---
## `Of` — one pawn to a context
```csharp
public static SecuredContext? Of(Pawn p)
```
`Of` maps a pawn to a context, or **`null`** if the pawn has none. The order of the checks matters and
is worth reading literally:
```
if p is not humanlike, or p is dead -> null (not one of ours)
if p.IsPrisonerOfColony -> Prisoner
if p.IsSlaveOfColony -> Slave
if p.IsFreeColonist -> Free
otherwise -> null
```
- **The guard comes first.** Animals, mechs, and the dead are simply not the suite's business — they
return `null`, and every `OnMap` loop and downstream check skips them without a special case.
- **Prisoner and Slave are tested before Free.** A pawn who is somehow both a colonist and imprisoned
is classified by their *hold* first. The nullable return is the clean signal for "this pawn is
outside the model," so callers pattern-match `HasValue` rather than defaulting to some "Free-ish"
status.
Returning `SecuredContext?` (nullable) rather than a `Free` fallback is deliberate: there is a real
difference between "a free pawn we track" and "a pawn we do not model at all," and collapsing them
would let non-colony pawns leak into colony machinery.
## `OnMap` — every context on a map
```csharp
public static IEnumerable<SecuredContext> OnMap(Map map)
```
`OnMap` yields a context for every spawned pawn that has one — prisoners, slaves, and colonists,
skipping everything `Of` returns `null` for. It is a lazy `yield` iterator, so a caller can enumerate
"everyone the suite cares about on this map" in one loop:
```csharp
foreach (SecuredContext ctx in SecuredContexts.OnMap(map))
{
if (ctx.IsHeld) { /* consider only the held */ }
}
```
A `null` map yields nothing (an empty sequence), so callers do not need to null-check before looping.
---
## Why not just "prisoner"?
This is the design decision the whole page exists to justify, so it is worth stating plainly. If every
module checked `pawn.IsPrisonerOfColony` directly, then:
- adding **slaves** to a feature would mean editing every module;
- adding a **ward patient** mode would mean editing every module;
- adding a future **secure-zone** concept would mean editing every module.
By routing all of them through one predicate, a new hold-kind is added *once*, in Core, and every
existing module inherits it. The concealment, search, and needs machinery never has to learn what a
slave is — it only ever asked `IsHeld` and `CanConceal`.
### The deliberately-absent Ward kind
There is no `SecuredKind.WardPatient`, and its absence is intentional. From the source:
> *"a committed patient IS a prisoner in vanilla's sense (Ward rides the prisoner rail), so it is
> `SecuredKind.Prisoner` here and needs no special case. Contraband does not depend on Ward."*
Because Institution: Ward implements a committed patient *on top of* vanilla's prisoner rail, such a
pawn already returns `Prisoner` from `Of` — no new kind, no dependency from Core (or Contraband) onto
Ward, and every module treats a ward patient exactly as it treats any prisoner. The right amount of
new code to support Ward's hold semantics was zero, and that is the payoff of keying off the context
instead of the label.
---
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+159
View File
@@ -0,0 +1,159 @@
# The Treatment Engine — shared rehabilitation
*`TreatmentProgram` + `TreatableConditionExtension`, in `Source/Core/TreatmentProgram.cs`.*
Core carries one more thing every "fix a held pawn over time" system needs and none should own alone:
the maths of a **treatment programme**. Sustained attention in a secured facility reduces a marked
condition, the room's quality helps or hurts, and once there is nothing left to reduce a **recovery
track** builds toward a dischargeable state.
It exists for the same reason the propensity engine does. Two systems want the identical loop pointed
at different states — **Ward** treats the mental illness that got a pawn committed; **Justice** wants
to rehabilitate the disposition that got one imprisoned — and without a shared engine each would grow
its own copy and drift. Core owns **only the maths**. Which interaction mode enrols a pawn, which
hediffs and thoughts carry the flavour, which jobs run the sessions, and which alert fires on
discharge all stay in the consuming mod.
> The engine does something concrete when there is a real condition to work through, and is a
> **no-op** otherwise. A colony that marks nothing treatable never notices it exists.
---
## The two pieces
### `TreatableConditionExtension` — marking a condition
A `DefModExtension` you attach to a `HediffDef` to declare it something a programme can reduce.
```xml
<HediffDef Name="SomeDisorder">
<modExtensions>
<li Class="Contraband.TreatableConditionExtension">
<reductionPerSession>0.12</reductionPerSession>
</li>
</modExtensions>
</HediffDef>
```
| Field | Default | Meaning |
|---|---:|---|
| `reductionPerSession` | `0.12` | Severity removed per **completed** session, *before* the caller's skill / facility / share scale it. |
A full course is several sessions by design — the programme has to be **sustained**, not run once. The
extension carries no flavour; it says only *how fast this condition yields*. Ward attaches it to
mental illness (via a soft, conditional patch on Rim Disorders' depression, anxiety, PTSD, OCD);
anything a mod or a player marks is treated identically, so opting a hediff in is a one-line patch.
### `TreatmentProgram` — the maths
A static, null-safe class. It holds no state of its own — it reads and mutates hediffs on the pawn.
| Call | Returns | What it does |
|---|---|---|
| `TreatableConditions(pawn)` | `List<Hediff>` | every marked hediff on the pawn that still has severity |
| `HasTreatableCondition(pawn)` | `bool` | is there any condition left to work through? |
| `ReduceConditions(pawn, strength)` | `void` | lower every marked condition by `reductionPerSession × strength`; remove any that reach ~0 |
| `FacilityQuality(pawn)` | `float 0.5..1.5` | how much the pawn's room helps or hurts |
| `AdvanceRecovery(pawn, recoveryDef, amount)` | `float` | advance (creating if absent) a recovery hediff, clamped to `1.0`; returns the new severity |
| `RecoveryLevel(pawn, recoveryDef)` | `float 0..1` | current level of that recovery track, or `0` |
---
## The numbers
### Facility quality
```
room == null OR psychologically outdoors → 0.75 (a poor makeshift facility, not zero)
otherwise → Clamp(0.6 + impressiveness / 120, 0.5, 1.5)
```
| Room impressiveness | Factor |
|---|---:|
| 0 (bare) | `0.60` |
| ~48 (decent) | `1.00` |
| ≥108 (impressive) | `1.50` (capped) |
| outdoors / none | `0.75` |
The clamp is deliberate: a grim, filthy, cramped facility heals worse, a calm clean one better, but a
palace can't trivialise the labour the programme costs (1.5× ceiling), and even nowhere is a poor
makeshift room, not a hard zero.
### Reducing a condition
```
h.Severity -= reductionPerSession × strength // per marked hediff
if h.Severity <= 0.001 → remove it
```
`strength` is the **caller's** combined factor — skill × facility quality × this pawn's share of the
session. The engine does not define it; the consumer does, so a one-on-one session at full skill in a
good room reduces far more than a distracted share in a squalid one.
### Advancing recovery
```
recovery = pawn's hediff of recoveryDef (created at 0.001 if absent)
recovery.Severity = min(1.0, recovery.Severity + amount)
```
The recovery track is a plain `0..1` severity the consumer supplies the def for (Ward's
`Ward_Recovery`). Core only advances it; the consumer decides what its top stage *unlocks* (Ward: a
"ready for discharge" alert) and — crucially — gives the hediff a **negative `severityPerDay`** so it
**decays without sustained attention**. Recovery you stop maintaining slips back; that decay is what
makes the programme a loop rather than a one-time unlock.
---
## The consumer contract
Core draws a hard line at "only the maths". A consumer owns everything with flavour:
| Core owns (the maths) | The consumer owns (the flavour) |
|---|---|
| `TreatableConditions` / `HasTreatableCondition` | which hediffs are marked treatable |
| `ReduceConditions` | the job/interaction that runs a session |
| `FacilityQuality` | the room role that makes a "facility" |
| `AdvanceRecovery` / `RecoveryLevel` | the recovery hediff def + what its stable stage unlocks |
| — | the discharge alert, the thoughts, the decay rate |
The idiomatic session, drawn from Ward's `JobDriver_PsychiatricCare` (the reference consumer):
```csharp
using Contraband;
float skill = 0.7f * social + 0.3f * medicine; // the consumer's own blend
float quality = TreatmentProgram.FacilityQuality(patient);
float strength = (0.5f + skill / 20f * 0.5f) * quality * share; // 1.0 share for the primary patient
// 1. Reduce the actual condition first.
TreatmentProgram.ReduceConditions(patient, strength);
// 2. Only once nothing is left to treat does recovery advance -- fix the illness, then stabilise.
if (!TreatmentProgram.HasTreatableCondition(patient))
{
TreatmentProgram.AdvanceRecovery(patient, MyDefOf.RecoveryHediff, 0.15f * quality * share);
}
```
"Reduce the condition, *then* build recovery" is the shape Ward uses, but it is a convention, not a
rule Core enforces — a rehabilitation consumer with no clinical condition to clear (Justice's reform)
can advance a recovery track from the first session and read `RecoveryLevel` to gate a parole.
---
## The two consumers
- **[Institution: Ward](https://git.onetick.ninja/flan/rimworld-ward)** — psychiatric care. Marks
mental-illness hediffs treatable, runs warden counselling sessions, builds `Ward_Recovery` toward a
"ready for discharge" alert. The reference implementation.
- **Institution: Justice** — rehabilitation. Its `reform` score is the disposition axis punishment
moves; the recovery track is the natural home for a *sustained rehabilitation programme* that gates
parole on the same shared engine, so the two systems agree on "getting better" instead of each
inventing it.
The point, exactly as with propensity: one engine, two states, no drift.
---
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*