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
+8
View File
@@ -14,6 +14,14 @@ The suite is the two halves of that: colonists commit crimes and a policing laye
Arrest is the hinge. Nothing hard-codes "prisoner" — every held-person system keys off a Arrest is the hinge. Nothing hard-codes "prisoner" — every held-person system keys off a
**secured context**: prisoner, ward patient, slave, or secure-area occupant. **secured context**: prisoner, ward patient, slave, or secure-area occupant.
## Documentation
The **complete suite wiki lives in this repo** — [**Wiki/Home.md**](Wiki/Home.md) is the front door:
the full gameplay loop, all five layers, and per-layer deep dives, aggregated in one place under
[`Wiki/`](Wiki/) (`core/`, `contraband/`, `justice/`, `gangs/`, `ward/`). Each layer repo carries its
own copy for a standalone install; [`Tools/sync-wiki.sh`](Tools/sync-wiki.sh) refreshes the
aggregated view here.
## The mods ## The mods
| Mod | What it is | Needs | Status | | Mod | What it is | Needs | Status |
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Consolidate every layer's Wiki/ into THIS repo's Wiki/, so the Institution page is the single place
# to read the whole suite's documentation. Each layer's pages land under Wiki/<layer>/ with their own
# internal links intact. The layer repos stay the canonical source (each ships its own wiki for a
# standalone install); re-run this whenever a layer's wiki changes to re-sync the aggregated copy.
#
# Wiki/Home.md (the suite index) is hand-written and lives at the top level -- this script never
# touches it, only the per-layer subfolders.
#
# Usage: Tools/sync-wiki.sh (assumes siblings at ../rimworld-<name>)
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
SIB="$(dirname "$HERE")"
# layer folder -> sibling repo
LAYERS=(
"core:rimworld-core"
"contraband:rimworld-contraband"
"justice:rimworld-justice"
"gangs:rimworld-gangs"
"ward:rimworld-ward"
)
echo "==> mirroring layer wikis into $HERE/Wiki/"
for entry in "${LAYERS[@]}"; do
layer="${entry%%:*}"
repo="${entry##*:}"
src="$SIB/$repo/Wiki"
dst="$HERE/Wiki/$layer"
if [[ -d "$src" ]]; then
rm -rf "$dst"
mkdir -p "$dst"
cp "$src"/*.md "$dst"/
echo " $layer: $(ls "$dst" | wc -l | tr -d ' ') pages"
else
echo " $layer: no Wiki/ in $repo -- skipped"
fi
done
echo "==> done. The Institution wiki now mirrors every layer under Wiki/<layer>/; Wiki/Home.md is the index."
+6 -6
View File
@@ -53,12 +53,12 @@ one off disables its systems in place without uninstalling anything.
| Layer | Toggle | What it adds | Wiki | | Layer | Toggle | What it adds | Wiki |
|---|---|---|---| |---|---|---|---|
| **Core** | *(always on — the engine)* | propensity, the criminal record, secured context, the shared **treatment engine**, the deterrence seam | [Core](https://git.onetick.ninja/flan/rimworld-core/wiki) | | **Core** | *(always on — the engine)* | propensity, the criminal record, secured context, the shared **treatment engine**, the deterrence seam | [Core](core/Home.md) |
| **Contraband** | Contraband | concealment & intake, improvised shivs, Prison-Architect tunnels, warden search, warden corruption | [Contraband](https://git.onetick.ninja/flan/rimworld-contraband/wiki) | | **Contraband** | Contraband | concealment & intake, improvised shivs, Prison-Architect tunnels, warden search, warden corruption | [Contraband](contraband/Home.md) |
| **Justice** | Justice | classification, deterrence feedback, discipline & reform, parole, regime (prisoner recreation) | [Justice](https://git.onetick.ninja/flan/rimworld-justice/wiki) | | **Justice** | Justice | classification, deterrence feedback, discipline & reform, parole, regime (prisoner recreation) | [Justice](justice/Home.md) |
| **Justice · Policing** | Policing | colony crime on the spectrum, witnesses, a constable, the weighed arrest, prison riots, recidivism alerts | [Policing](https://git.onetick.ninja/flan/rimworld-justice/wiki/Policing) | | **Justice · Policing** | Policing | colony crime on the spectrum, witnesses, a constable, the weighed arrest, prison riots, recidivism alerts | [Policing](justice/Policing.md) |
| **Gangs** | Gangs | gangs as contraband economies — joining, smuggling networks, rivalry, fights-as-crime | [Gangs](https://git.onetick.ninja/flan/rimworld-gangs/wiki) | | **Gangs** | Gangs | gangs as contraband economies — joining, smuggling networks, rivalry, fights-as-crime | [Gangs](gangs/Home.md) |
| **Ward** | Ward | psychiatric care — treatment, recovery, discharge, ward neglect, sedation | [Ward](https://git.onetick.ninja/flan/rimworld-ward/wiki) | | **Ward** | Ward | psychiatric care — treatment, recovery, discharge, ward neglect, sedation | [Ward](ward/Home.md) |
**Policing** has its own toggle inside Justice because colony crime is a large behavioural change — a **Policing** has its own toggle inside Justice because colony crime is a large behavioural change — a
player can run the rest of the corrections layer without their own colonists committing crimes. player can run the rest of the corrections layer without their own colonists committing crimes.
+128
View File
@@ -0,0 +1,128 @@
# Compatibility
*Contraband ships zero Harmony patches. It adds defs, subclasses a work-giver, mutates two duty
trees at startup, and polls. That is why it composes instead of colliding.*
## Requires: Institution: Core
Contraband declares a hard `modDependency` on **`flan.institution.core`** and loads after it. This
is not optional — after the split, the entire substrate Contraband reads lives in Core:
| Core provides | Contraband uses it for |
|---|---|
| `SecuredContext` / `SecuredContexts` / `CanConceal` / `IsHeld` | who counts as a concealer; who can be intaken, whittle, or dig |
| `Propensity.Would(pawn, base, salt)` | every disposition roll — intake (0.6), improvise (0.10), dig (0.08), all seeded and scaled by nature × nurture |
| `CriminalRecord` + `GameComponent_CriminalRecords` | `timesSearched`, `timesCaught`, `escapeAttempts`, `contrabandMade` — written by search and acquisition, read by priority |
Install Contraband without Core and it will not run. Everything else on this page is optional
interplay that degrades gracefully when the other mod is absent.
## Zero Harmony patches
Post-split, the mod's one Harmony patch (the regime that forces Joy for prisoners) moved to
**Institution: Justice**, so Contraband is Harmony-free again. Instead of patching, it:
- **Adds a new `WorkGiver`** (`WorkGiver_Warden_Search`) rather than patching a vanilla one.
- **Subclasses `WorkGiver_Warden`**, inheriting its behaviour rather than overriding it.
- **Mutates two `DutyDef` think trees at startup** via `StaticConstructorOnStartup` (not Harmony) —
an XML patch on a duty think tree silently no-ops in 1.6, so it is done in C#, deterministically.
- **Polls** every held pawn from a `MapComponent` for intake, improvisation, and tunnels — no hook
on the capture event is needed because the tick already visits every held pawn.
The net effect: the surfaces other prison mods touch are mostly untouched here, so conflicts are the
exception, not the rule.
## Foul Play — the carboy bridge
**Foul Play** (the "Piss Nuke" mod) stays its own mod with its own front door, and yet its **carboy
is a first-class contraband item.** This is the flagship demonstration of Contraband's *"items are
defs, subsystems are classes, mods are audiences"* principle, and it works through two seams:
1. **Cross-assembly worker resolution.** `CompProperties_Contraband`'s `useWorker` / `escapeWorker`
fields are plain `Type`s, and RimWorld resolves def `Type` fields with
`GenTypes.GetTypeInAnyAssembly` — searching **every loaded assembly**. So the carboy declares
`CompProperties_Contraband` and points `useWorker` at a class that lives in *Foul Play's* assembly
(e.g. a throw-the-carboy worker). Contraband never has to know that class exists.
2. **The opaque content tag.** A carboy is not just "a jar" — it is a jar *of* a fermented blend, and
that blend has to survive being concealed. Foul Play sets Contraband's two delegates:
```csharp
ContrabandUtility.ContentTagOf = thing => /* serialise the carboy's blend */;
ContrabandUtility.ApplyContent = (thing, tag) => /* pour that blend back in on redeem */;
```
When a carboy is intaken or smuggled, Contraband captures the tag blind; on redemption it hands
the tag back to Foul Play to restore the contents. **Contraband knows nothing about substances or
fermentation** — that framework lives entirely in Foul Play. If Foul Play is absent, both delegates
are null, no content is captured, and nothing breaks: a jar is just a jar. See
[Concealment and Intake](Concealment-and-Intake.md).
The dependency runs one way only: Foul Play bridges *to* Contraband (and to Core) if they are
present; Contraband takes **no** dependency on Foul Play.
## Institution: Justice — the record is the shared bus
Contraband and **Justice** never call each other. They meet on Core's `CriminalRecord`:
```
Contraband writes ──▶ CriminalRecord ──▶ Justice reads
timesSearched (in Core) Classification (security grade)
timesCaught Deterrence (colony-wide signal)
escapeAttempts Discipline / Parole (reform)
contrabandMade
```
- **Catching contraband feeds classification.** A prisoner repeatedly caught with shivs or foiled
mid-tunnel accrues `timesCaught` and `escapeAttempts`, which Justice's classification weighs into a
higher security grade.
- **And it feeds back.** Justice's deterrence signal flows *back* through Core's propensity seam
(`Nurture`), nudging every future intake / improvise / dig roll a Contraband pawn makes. A prison
that catches and disciplines becomes a prison where fewer prisoners bother trying — without either
mod referencing the other. Run Contraband alone and the deterrence factor is a neutral 1.0; the
record fields still route the warden.
## Institution: Gangs — the contraband economy
**Gangs** depends on Core, **Contraband**, and Justice, because a gang *is* a contraband economy:
- Gang smuggling moves contraband through the same public `Conceal` door intake and corruption use.
- A gang's outside members are the natural caller for the **reach-in corruption** primitive
(`Corruption.Smuggle`) — paying a bent warden to make a delivery. That primitive is exposed in
Contraband but has no in-repo trigger; Gangs is what drives it. See [Corruption](Corruption.md).
- Gang fights route to Justice's crime record, which then raises search priority here. The loop
closes through Core again.
## Prison mods the suite is tested against
| Mod | Interaction | Verdict |
|---|---|---|
| **Prison Commons** | postfixes `WorkGiver_Warden.ShouldSkip` on the **base** class; Contraband's search subclasses it and does not override, so searches respect prison-commons/allowed areas **for free** | works by inheritance |
| **Custom Prisoner Interactions** | patches only the *named* vanilla warden givers (`_Chat`, `_Convert`, `_Enslave`, `_ReleasePrisoner`); it cannot see a brand-new giver | no conflict |
| **Prisoner Realism** | its escapes are for "mood prisoners" mid-break; a tunnel breakout hands off to vanilla `PrisonBreakUtility.StartPrisonBreak`, which Prisoner Realism layers on. Tunnels **complement** it — they let ward patients and slaves who would never mood-break still, patiently, dig out | complementary |
| **Prison Labor** | prisoners working outside the cell are still held pawns and still tracked; Contraband adds no patches to labor jobs. A work assignment is simply another place a disposed prisoner might come by materials | no conflict |
| **Prisoner Recreation** | overlaps the *regime* feature — which now lives in **Institution: Justice**, not here, and is written to be idempotent with Prisoner Recreation | not Contraband's concern post-split |
## Load order
```
Ludeon.RimWorld
flan.institution.core ← required, loads before Contraband
flan.institution.contraband ← this mod
(Foul Play / Justice / Gangs — any order after Core; each bridges if present)
```
## The general rule
If a mod adds prisoner behaviour by **patching vanilla warden givers or the escape duty tree by
name**, it will not see Contraband's additions, and Contraband will not see its — they pass each
other. If a mod adds a *new* `WorkGiver` or duty node of its own, it stacks. The one place to watch is
another mod that *also* rewrites the same two duty think trees (`PrisonerEscape`,
`PrisonerAssaultColony`) wholesale; Contraband inserts its nodes at a specific anchor and logs a
warning if that anchor is missing, so a load-order or conflict problem is visible in the log rather
than silent.
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*
+230
View File
@@ -0,0 +1,230 @@
# Concealment and Intake
*How a hidden item exists without existing, how it gets onto a prisoner in the first place, and how
it becomes real again.*
This is the spine of the whole mod. Everything else — shivs, tunnels, searches, corruption — is a
different way to add to, or remove from, the private list this page describes.
## While it is concealed, there is no Thing
The single most important rule, and the source states it plainly:
> `MapComponent_Contraband` tracks what each pawn is hiding. **Concealed contraband is not a Thing.
> It is a secret.** It is not in the gear tab, there is no icon, and the player is told nothing —
> because an item you can see in a UI is not contraband, it is inventory, and there would be nothing
> for a warden to discover and no reason to ever order a search.
Concretely, a concealed item lives as a `ConcealedItem` row in a `List` on the map component. It
carries the def, a `progress` value, the material it ended up made of, an optional source it is
being harvested from, tunnel state if it is a pick, and an opaque `contentTag`. It holds **no
spawned object** anywhere in the world until one of two things happens:
| Moment | What happens | Method |
|---|---|---|
| The prisoner **uses** it | It materialises into their inventory as a real `Thing` | `TryRedeem` |
| A warden **finds** it | It is deleted from the list; it never becomes a `Thing` at all | `Confiscate` |
A corollary the search page leans on hard: **an empty search must still cost time.** If the
`Search` job only ever appeared when there was genuinely something to find, the mere *offer* of the
job would be the discovery. You have to be able to toss a clean cell and come up with nothing.
## Where a thing can hide: `ConcealSite`
Every contraband def declares where it can be hidden. This is not decoration — it decides **who can
ever find it**.
```csharp
[Flags] enum ConcealSite { None = 0, OnBody = 1, InCell = 2 }
```
| Site | Holds | Found by |
|---|---|---|
| `OnBody` | small: a shiv, pills, a phone | searching the **pawn** |
| `InCell` | bulky: a rifle, a carboy, a crude pick | tossing the **room** |
A warden turning over a prisoner and their cell reaches both (`OnBody | InCell`). The reason the
enum is split at all: a **constable** in Justice's now-shipping policing layer, frisking a colonist
named in a street crime, reaches only `OnBody`, so whatever is under their floorboards stays there
until somebody has grounds to search the *room*. The split between "who may search" and "what a
search reaches" is deliberate — see [Warden Search](Warden-Search.md).
## Three sources of contraband, and no list of items
At startup, `ContrabandUtility` scans every `ThingDef` and decides whether it is contraband. There
is no hardcoded list of item names — there are three recognisers:
1. **Explicit.** Any def carrying `CompProperties_Contraband`. This is the opt-in, and it is how a
mod plugs in its own `acquireWorker` / `useWorker` / `escapeWorker` behaviour.
2. **Every drug** (`ThingDef.IsDrug`) — inferred automatically.
3. **Every weapon** (`ThingDef.IsWeapon`) that is not `destroyOnDrop` — inferred from **mass**.
The inference (`ContrabandUtility.Infer`) fills in concealability and hiding place so a modder never
has to:
| Kind | Test | `concealability` | `hideIn` |
|---|---|---|---|
| Hard drug | `drugCategory == Hard` | **0.75** | `OnBody \| InCell` |
| Other drug | any other `IsDrug` | **0.5** | `OnBody \| InCell` |
| Light weapon | `Mass ≤ 2.0` | **0.7** | `OnBody \| InCell` |
| Heavy weapon | `2.0 < Mass ≤ 10.0` | **0.35** | `InCell` only |
| Very heavy weapon | `Mass > 10.0` | — | **not contraband** (a minigun hides nowhere) |
`concealability` runs 0..1 where **0 = a warden finds it every time and 1 = never**; higher is
harder to find. Drugs and inferred weapons get **no `acquireWorker`** (you cannot synthesise yayo in
a bare cell, and you cannot conjure a rifle) and **no `useWorker`** — because vanilla already knows
what to do with them. `JobGiver_SatisfyChemicalNeed` takes a drug; an armed prisoner *is* the
payload. Getting the item into the cell is the entire problem, and that is all these routes solve.
Nothing here mutates another mod's defs. It is a lookup keyed on the def, computed once, held in a
dictionary. Modded drugs and weapons are covered without ever being named.
## The poll: no Harmony
`MapComponent_Contraband` runs on a heartbeat. Every **250 ticks** (`CheckIntervalTicks`) it visits
every pawn who could be concealing something and does all the ongoing work: intake, tunnel progress,
and improvisation. If nothing on the map is self-acquirable and there are no escape tools registered,
it returns immediately and costs nothing.
Polling — rather than patching — is a deliberate architecture choice. **The tick already visits
every held pawn**, so intake needs no Harmony hook on the capture event: a pawn who is in custody but
not yet in the `intakeProcessed` set is, by definition, one who was newly taken. The whole mod ships
zero Harmony patches, and this is a large part of why.
### Who counts as a concealer
`ContrabandUtility.Concealers(map)` is **not** just prisoners. It delegates to Core's
secured-context predicate and yields every pawn whose belongings are anyone's business — prisoners,
slaves, **and free colonists**:
> A free colonist forbidden their drug by a drug policy or an ideoligion is precisely the pawn who
> keeps a private stash — and vanilla already denies them, with no valve.
Whether anyone may *search* a given concealer is a separate question with a separate answer (the
warden's authority reaches prisoners; a future cop's reaches colonists). Tracking is pawn-agnostic;
authority is not.
## Intake: the moment of capture
Vanilla never strips a captive *on capture* — a downed pawn keeps whatever they walked in with, and
the warden path just hauls them to a cell. Intake is Contraband's answer to that gap.
`RunIntake(pawn)` runs from the poll, on the first pass that sees a newly-held pawn:
1. **Held only.** The pawn must be humanlike and `IsHeld` (prisoner, slave, ward patient). A free
colonist is a concealer, but nobody frisks and processes *them* on the way into a cell, so their
carried gear is never intaken.
2. **Once.** The pawn's `thingIDNumber` is added to a `HashSet<int>`. The set stores an `int`, not a
pawn reference, so it holds no ghost of a captive who has since died. A second pass finds them
already processed and does nothing.
3. **Even while downed.** Intake runs *before* the "must be awake" gate. The stash a captive walked
in with is hidden the moment they are in custody, not when they wake — everything *after* intake
(whittling, digging) needs them awake, but hiding what is already on you does not.
4. **Only what can ride on a body.** `IsIntakeConcealable` accepts a thing only if its contraband
props include the `OnBody` flag. A shiv up a sleeve is intaken; a rifle or a suit of armour is
too bulky to palm and stays as ordinary strippable gear the player confiscates for free.
5. **Per-item disposition.** For each concealable thing the captive carries, the mod rolls whether
*this* pawn would hide *this* item:
```
Propensity.Would(pawn, 0.6, SaltIntake ^ item.thingIDNumber)
```
Base chance 0.6, then scaled by the pawn's nature × nurture (Core's `Propensity`). The roll is
**seeded per (pawn, item)** — a fixed fact, not a fresh coin each tick. The frightened cook
surrenders the knife; the disposed lifer palms it, and always would have.
When a pawn does hide an item, the real `Thing` is **destroyed** (equipment via
`DestroyEquipment`, inventory via `Destroy(Vanish)`) and a concealed secret is created in its place,
preserving what the item was made of (`stuff`) and — critically — its `contentTag`.
## The content tag: preserving contents without understanding them
A concealed vessel is not just "a jar" — it is a jar *of* something (hooch, piss, a fermented
blend), and that something has to survive being hidden and come back on redemption. Contraband
stores a single opaque string, `contentTag`, and **has no idea what it means**:
```csharp
// A bridge mod (Foul Play) fills these in. Null (no bridge) = items have no contents.
ContrabandUtility.ContentTagOf; // Func<Thing, string> — read on conceal
ContrabandUtility.ApplyContent; // Action<Thing, string> — applied on redeem
```
On concealment, `ContentTagOf(thing)` captures the tag (e.g. "a jar of a piss+hooch cocktail"). On
redemption, `ApplyContent(newThing, tag)` pours that content back into the freshly-made thing. This
is the exact seam **Foul Play** uses to keep a smuggled carboy's contents intact. Without a bridge,
both delegates are null, no content is captured, and nothing is lost — a shiv is just a shiv.
Fermentation itself lives in Foul Play, not here. A concealed vessel materialises **fresh** and
ferments afterward like any other, which suits the theme: fresh-brewed piss is weak, and time is
what makes it a weapon. (`concealedTick` records how long it was hidden, for any bridge that wants to
restore pre-aging — Contraband keeps the number but never acts on it.) See
[Compatibility](Compatibility.md).
## Redemption: the secret becomes a Thing
`TryRedeem(pawn, def)` is where a secret turns real:
1. Choose the material — the harvested `stuff`, or the def's default if it was made from stuff.
2. `ThingMaker.MakeThing(def, stuff)`.
3. If there was a `contentTag`, call `ApplyContent` to restore the contents.
4. `TryAdd` it to the pawn's inventory. If that fails, the thing is vanished and nothing is lost.
5. Remove the secret from the list.
The trigger for redemption is the breakout duty tree. `ThinkTreeInjection` runs once at startup and
mutates two duty defs directly (an XML patch on a `DutyDef` think tree silently no-ops in 1.6, so it
is done in C#):
| Duty | Injected | Where |
|---|---|---|
| `PrisonerEscape` | `JobGiver_ArmSelf`, then `JobGiver_UseContraband` | **after** `JobGiver_TakeCombatEnhancingDrug` |
| `PrisonerAssaultColony` | `JobGiver_ArmSelf`, then `JobGiver_UseContraband` | **before** `JobGiver_AIFightEnemies` |
`JobGiver_UseContraband` sits right beside `JobGiver_TakeCombatEnhancingDrug` — the one contraband
hook Ludeon wrote and then left reading an always-empty inventory. For each thing a pawn is hiding:
- **Inert contraband** (a drug, a stashed rifle — no `useWorker`): just materialise it and get out
of the way. Vanilla takes the drug via `JobGiver_SatisfyChemicalNeed`; a weapon in inventory is a
weapon. But a stashed **weapon** only comes out for someone who would actually use it
(`Disposition.WouldArmSelf`) — the same person who would not stop to pick a rifle off the floor
does not become a fighter because the knife was in their sock, and a pawn incapable of violence
never does at all. If they qualify, the weapon is moved from inventory into their equipment slot;
otherwise it stays hidden and they run. See [Improvised Weapons](Improvised-Weapons.md).
- **Active contraband** (a `useWorker` — e.g. Foul Play's throw-the-carboy): materialise it, then
hand off to the worker's `TryGiveJob`, which produces the actual use job.
## Confiscation and the door every route comes through
`Confiscate(pawn, def)` simply removes the secret. It **never becomes a Thing** — the warden found
it, and there is nothing to drop on the floor. (This is why searching is not a way for the colony to
*acquire* a prisoner's drugs; it is a way to *deny* them.)
At the other end, `Conceal(prisoner, def, ...)` is the public door **every** supply route comes
through — intake calls it, a bent warden calls it ([Corruption](Corruption.md)), and a smuggling
gangmate ([Institution: Gangs](Compatibility.md)) calls it. It hands a prisoner something already
finished and hidden, optionally with a `contentTag`. One door, many keys.
## Persistence
`MapComponent_Contraband.ExposeData` deep-saves the secret list, the per-pawn search cooldowns, and
the `intakeProcessed` set. On load it prunes any row whose pawn or def failed to resolve — prisoners
who died or left take their secrets with them, and a null key would throw on the next tick.
## Quick reference
| Constant | Value | Meaning |
|---|---|---|
| `CheckIntervalTicks` | 250 | poll cadence for intake / tunnels / improvisation |
| Intake base chance | 0.60 | per-item, before nature × nurture |
| Intake gate | `IsHeld` + `OnBody` | only held pawns, only body-hideable items |
| Hard-drug concealability | 0.75 | inferred |
| Other-drug concealability | 0.50 | inferred |
| Light-weapon concealability | 0.70 | `Mass ≤ 2.0`, hides on body |
| Heavy-weapon concealability | 0.35 | `2.0 < Mass ≤ 10.0`, `InCell` only |
| Unhideable mass | > 10.0 | not contraband |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*
+141
View File
@@ -0,0 +1,141 @@
# Corruption
*A search is only as thorough as the warden running it, and a warden is a person with their own
diligence and their own price.*
The counter to contraband is the [warden search](Warden-Search.md) — but a search is not a machine.
It is a human being with traits and a mood, and Contraband models that human being on **two axes,
both read off the same vanilla traits, mood, and relationships — no new stat to maintain**:
| Axis | Question | Effect on a search |
|---|---|---|
| **Diligence** | How hard do they look? | multiplies the find chance |
| **Corruption** | How bent are they? | a flat chance to look away — and a route *into* the prison |
This is *"what turns 'the game vs the player' into 'the player's own guards, each with an angle.'"*
The prisoner you can control. The guard you assigned to watch them, you cannot.
## Diligence — how hard they look
`WardenDisposition.Diligence(warden)` returns 0..1 and multiplies the whole find chance in
`JobDriver_SearchPawn`. A lax or miserable warden misses things a dutiful one would turn up.
```
d = 0.55 // baseline
d += 0.1 × degreeOf(Industriousness) // Lazy lowers, Industrious raises (per degree)
d += 0.15 if Abrasive // no qualms turning a cell over
d -= 0.20 if Kind // a gentle hand
d += 0.10 if Psychopath // does not care how it feels
d ×= Lerp(0.6, 1.1, mood) // a miserable warden phones it in
d = clamp01(d)
```
| Trait / factor | Δ Diligence | Reasoning |
|---|---|---|
| Industrious | +0.1 per degree | a hard worker searches hard |
| Lazy | −0.1 per degree | a lazy one frisks gently |
| Abrasive | +0.15 | happy to toss a cell |
| Psychopath | +0.10 | thoroughness without squeamishness |
| Kind | −0.20 | reluctant to be rough |
| Mood | ×0.6 (miserable) .. ×1.1 (content) | morale is thoroughness |
## Corruption — their price
`WardenDisposition.Corruption(warden)` returns 0..1. In a search, a bent warden gets a flat chance
to **look away** on each item they would otherwise have a shot at finding:
```
lookAwayChance = Corruption(warden) × 0.6 // rolled per hidden item, before the find roll
```
```
c = 0.04 // baseline — most guards are mostly honest
c += 0.40 if Greedy // has a price
c += 0.15 if Kind // bends the rules out of sympathy
c -= 0.10 if Ascetic // wants nothing, cannot be bought
c -= 0.05 if Abrasive
c ×= Lerp(1.6, 0.7, mood) // a desperate, miserable warden is more temptable
c = clamp01(c)
```
| Trait / factor | Δ Corruption | Reasoning |
|---|---|---|
| Greedy | +0.40 | the classic bent guard — everyone has a number |
| Kind | +0.15 | looks the other way out of sympathy, not money |
| Ascetic | −0.10 | wants nothing, so cannot be bought |
| Abrasive | −0.05 | too surly to do anyone a favour |
| Mood | ×1.6 (miserable) .. ×0.7 (content) | a happy guard is harder to tempt |
## The Kind warden is a double liability
Read the two formulas together and one trait jumps out. **Kind** *lowers* Diligence (−0.20) **and**
*raises* Corruption (+0.15). The gentle guard both frisks softly and bends the rules out of pity —
the worst warden you can put on a contraband detail, and the least obvious one, because "kind" reads
as a virtue everywhere else in the game. Meanwhile **mood** cuts the same way on both axes: a
miserable warden looks *less* hard and is *more* temptable. A depressed guard is a sieve at both
ends.
## Worked examples
Find chance for a hidden item is `baseFind × Diligence`, with a separate `lookAway = Corruption ×
0.6` chance to ignore it entirely. Assume a mid-skill warden whose raw find chance on a shiv is
around 0.30 before disposition:
| Warden | Diligence | Corruption | Look-away/item | Net on a 0.30 shiv |
|---|---|---|---|---|
| Neutral, content | ~0.61 | ~0.03 | ~2% | thorough-ish: ~0.18 find |
| Abrasive + Psychopath, content | ~0.88 | ~0.02 | ~1% | a nightmare for prisoners: ~0.26 find |
| Greedy, content | ~0.61 | ~0.31 | ~18% | looks capable, pockets bribes |
| Greedy, **miserable** | ~0.37 | ~0.62 | ~37% | ignores contraband over a third of the time |
| Kind + **miserable** | ~0.25 | ~0.27 | ~16% | a sieve at both ends: ~0.07 find |
(Numbers rounded; mood taken as content ≈ 100% or miserable ≈ 20%.) The takeaway is that **who you
assign to search matters as much as whether you search at all.** A greedy, miserable warden is not a
guard — he is a supply line you are paying to pretend otherwise.
## The bent warden as a supply route *in*
Corruption is not only "looks away." The `Corruption.Smuggle` primitive is the reaching-*in* half — a
guard who slips a prisoner contraband through the very same `Conceal` door a smuggler or a gangmate
uses:
```csharp
ThingDef Corruption.Smuggle(warden, prisoner, tracker)
// picks a random OnBody-hideable contraband and Conceal()s it onto the prisoner; returns the def
```
Only what can ride on a body gets slipped across a handshake (`OnBody`). The method is the **act
itself**; the caller decides *who* and *when*, gated on `WardenDisposition.Corruption`. In Contraband
on its own, this primitive is exposed but not yet wired to an in-game trigger — it is the seam
**Institution: Gangs** drives when a gang's outside members pay a bent warden to make a delivery, and
the thing a planned secure-zone checkpoint that frisks *guards* is meant to catch. Search-side
corruption (looking away) is fully live today; reach-in corruption is the hook the rest of the suite
plugs into. See [Compatibility](Compatibility.md).
## How to play with it
- **Vet your wardens.** A Greedy or Kind pawn on prison duty is a liability the trait tooltip won't
warn you about. Ascetic and Industrious make the best searchers; Psychopath and Abrasive are
brutally effective if you can stomach them.
- **Watch mood.** A prison-duty warden in a slump is failing at both jobs — searching *and* not
smuggling. Keep your guards content or rotate them out.
- **Redundancy beats brilliance.** Because each search is one shot per prisoner per day and even a
good warden misses a well-hidden shiv, a *second* diligent warden over time matters more than a
single perfect search.
## Quick reference
| Constant | Value | Meaning |
|---|---|---|
| Diligence baseline | 0.55 | before traits and mood |
| Corruption baseline | 0.04 | most guards are mostly honest |
| Diligence mood scale | ×0.6 .. ×1.1 | miserable → content |
| Corruption mood scale | ×1.6 .. ×0.7 | miserable → content |
| Look-away multiplier | ×0.6 | applied to Corruption, per item |
| Smuggle payload | `OnBody` contraband only | what fits in a handshake |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*
+130
View File
@@ -0,0 +1,130 @@
# Institution: Contraband
*Prisoners get hold of things they should not have. Wardens turn the cell over looking for them.*
This is the player-facing wiki for **Institution: Contraband**, a RimWorld 1.6 mod. It documents
every mechanic with the real numbers pulled from the source, explains why each one works the way it
does, and tells you how to play with them — and against them.
> **Note on scope.** Contraband was recently *split*. It is now one thing: the **physical smuggling
> loop** of a prison. The engine it used to carry — propensity, the criminal record, the
> secured-context predicate — now lives in **Institution: Core**, which this mod requires. The
> response systems (classification, deterrence, discipline, parole, regime) went to **Institution:
> Justice**, and the gang network to **Institution: Gangs**. If this page mentions "the record" or
> "a pawn's propensity," those are Core's; Contraband reads them.
## The one idea
The whole Institution suite rests on a single sentence:
> **Every pawn has a criminal propensity on a spectrum — nature (traits) × nurture (circumstance,
> mood, treatment) — and the colony can police it.**
Contraband is the half of that sentence you can *hold in your hand*. A propensity is abstract until
a disposed prisoner files a blade off his bunk, palms a twist of yayo on the way into a cell, or
starts digging. Contraband is where the spectrum becomes an object with edges.
## Vanilla built the demand and forgot the supply chain
RimWorld already ships a fully-built junkie. Raiders spawn addicted (`chemicalAddictionChance`,
`forcedAddictions`) and carrying combat drugs (`combatEnhancingDrugsChance`). You down one, capture
strips everything (`DropAndForbidEverything`), and now withdrawal grips them in the cell
(`Need_Chemical`, `Hediff_Addiction`). A prisoner already *seeks* a drug on their own —
`JobGiver_SatisfyChemicalNeed` is in the prisoner think tree — and already *ignores* the forbidden
flag inside their own cell (`ForbidUtility.CaresAboutForbidden`).
Put a drug within reach of an addicted prisoner and the base game makes them take it. Today. With no
new code. The consumer, the craving, the reach, the ignore-forbidden — all of it ships.
What does **not** ship is any answer to two questions:
| Vanilla has | Vanilla lacks |
|---|---|
| A prisoner who wants contraband | Any route for contraband to reach the cell |
| A warden with fifteen jobs | A sixteenth called *search* |
Search `Assembly-CSharp.dll` for `Contraband`, `Search`, `Confiscate`, `Smuggle`, `Frisk`,
`Shakedown` and you get **zero types**. The warden can chat, convert, feed, execute, enslave,
release, suppress — and cannot look in a pocket. Contraband builds exactly the missing two halves:
**supply** and **search**. It does not need to build the junkie, because Ludeon already did.
## Concealment is a secret, not an item
This is the load-bearing design decision, inherited from Foul Play (the Piss Nuke) and kept:
> **While it is concealed, there is no `Thing`.**
A hidden shiv is not in the gear tab. There is no icon. There is no stack on a shelf. The player is
told **nothing**. The item exists only as a row in `MapComponent_Contraband`'s private list, and it
becomes a real `Thing` at exactly two moments:
- when the prisoner **uses** it (the secret is "redeemed" into their inventory), and
- when a warden **finds** it in a search (it is confiscated and never becomes a thing at all).
Why go to the trouble? Because *an item you can see in a UI is not contraband* — it's inventory, and
there would be nothing to discover and no reason to ever order a search. The whole tension of the
system is that **you never know**. A warden you send to toss a cell might find a blade, might foil a
tunnel, or might waste twenty minutes on a prisoner who was hiding nothing — and that empty search
still has to cost, or the mere *offer* of the job would be the discovery.
Read the full lifecycle in **Concealment and Intake**.
## The four supply routes and the one counter
| Route | What it is | Page |
|---|---|---|
| **Intake** | A captive hides the small contraband they walked in with | [Concealment and Intake](Concealment-and-Intake.md) |
| **Improvise** | Whittle a shiv from the cell's own furniture | [Improvised Weapons](Improvised-Weapons.md) |
| **Tunnel** | Dig out under the perimeter with a filed pick | [Tunnels](Tunnels.md) |
| **A bent warden** | A greedy or over-kind guard smuggles *for* a prisoner | [Corruption](Corruption.md) |
| **The counter** | A prioritized cell toss that reads the tells | [Warden Search](Warden-Search.md) |
And because every guard is a person with their own diligence and their own price, the search is only
as good as who you send — see **Corruption**.
## How everything is contraband without a list
Contraband names no items. It recognises three sources at load and logs the count:
1. **Anything carrying `CompProperties_Contraband`** — the explicit opt-in, with pluggable behaviour.
2. **Every drug** (`ThingDef.IsDrug`) — vanilla's, Vanilla Expanded's, mods that don't exist yet.
Hard drugs conceal better (0.75 vs 0.5).
3. **Every weapon** (`ThingDef.IsWeapon`) — concealability and hiding place derived from **mass**. A
knife rides on the body; a heavy gun goes under the floor; a minigun (over 10 kg) hides nowhere.
It is a lookup, not a patch. Nothing mutates another mod's defs. Details in
[Concealment and Intake](Concealment-and-Intake.md) and [Compatibility](Compatibility.md).
## Index
- **[Concealment and Intake](Concealment-and-Intake.md)** — the secret model, `ConcealSite`, the
three sources, intake on capture, redemption, and the opaque content-tag bridge.
- **[Improvised Weapons](Improvised-Weapons.md)** — the shiv whittled from furniture, material
carry-through, the damage tell, and who reaches for it in a breakout.
- **[Tunnels](Tunnels.md)** — the Prison-Architect dig under the perimeter, which pawns dig, and how
a search uncovers the shaft.
- **[Warden Search](Warden-Search.md)** — the prioritized search, how tells and the record drive
priority, what a search does, and how it writes back.
- **[Corruption](Corruption.md)** — Diligence and Corruption from traits and mood, how they scale a
find, and the bent warden who looks away or smuggles in.
- **[Compatibility](Compatibility.md)** — the Core dependency, the Foul Play bridge, Justice/Gangs
interplay, and the prison mods the suite is tested against.
## The suite
Each mod stands alone as an install; together they form one system.
| Mod | Owns | Depends on |
|---|---|---|
| **Institution: Core** | propensity, criminal record, secured context | base game |
| **Institution: Contraband** (this) | concealment, intake, shivs, tunnels, search, corruption | Core |
| **Institution: Justice** | classification, deterrence, discipline, parole, regime | Core |
| **Institution: Gangs** | gangs as contraband economies | Core, Contraband, Justice |
| **Foul Play** | vessel + substance + throw framework; the Piss Nuke; bridges its carboy in as contraband | standalone |
| **Ward** | a ward prison mode + the suite's test harness | — |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*
+203
View File
@@ -0,0 +1,203 @@
# Improvised Weapons
*The acquisition route you cannot deny — because you are obliged to furnish the cell, and the
furniture is the raw material.*
You have to give a prisoner a bed. RimWorld will nag you until you do. That bed is a frame of wood,
or steel, or — if you built a nice prison — plasteel. Give a disposed prisoner a season of
unsupervised time with it and they will file a blade off it. **The prison's own furnishings are the
arsenal, and better furniture is worse.**
## The shiv
`CB_Shiv` is a crude stabbing weapon: *"A blade filed off a bed frame and wrapped in cloth for a
grip. Barely a weapon — but a barely-a-weapon in a cell you thought was empty is a dead guard."*
| Stat | Value |
|---|---|
| Tech level | Neolithic |
| Mass | 0.4 kg |
| `concealability` | **0.75** (hides very well — it rides on the body) |
| `hideIn` | `OnBody \| InCell` |
| Point (Stab) | power 7, cooldown 1.5 s |
| Handle (Blunt) | power 5, cooldown 1.6 s |
| `acquireWorker` | `AcquireWorker_Improvise` |
| `useWorker` | none — an armed prisoner *is* the payload |
It is **stuffable** (`stuffCategories`: Metallic, Woody, Stony), which is the entire point of the
harvest mechanic: a shiv's stats come from its material for free. A wood shiv is feeble; a plasteel
one is vicious. You never furnished a cell thinking of it as an armoury, but that is what it is.
## Who whittles: the disposition gate
`AcquireWorker_Improvise.CanAcquire` is not "any prisoner." Three things must all be true:
1. **Held.** The pawn is a prisoner, slave, or ward patient (`IsHeld`). Free colonists do not whittle
shivs in the commons — *yet* (the concealment layer already tracks them; only the authority to act
on them is missing).
2. **Disposed.** A seeded propensity roll:
```
Propensity.Would(pawn, 0.10, SaltImprovise)
```
Base chance **0.10**, scaled by Core's nature × nurture. It is seeded per pawn, so it is a *fixed
fact* about them — the mistreated, high-propensity lifer chews his bed apart; the frightened cook
never does, and never would have, no matter how many times you reload.
3. **Material in reach.** There is a workable source (see below).
The base is deliberately low. Most prisoners are not making weapons. When one is, that is a fact
about *who they are and how you keep them*, and the tell it leaves is meant to point you at exactly
that pawn.
## The source: what counts as raw material
`FindSource` looks for the nearest reachable, damageable, usefully-stuffed piece of furniture:
- **Their own assigned bed first** — it is right there in the cell.
- Otherwise the **closest artificial building within 12 tiles** that qualifies.
`IsWorkableSource` requires all of:
| Requirement | Why |
|---|---|
| Made of `Stuff` | you cannot whittle a blade out of nothing |
| `useHitPoints` | it has to be damageable |
| Stuff category is **Metallic, Woody, or Stony** | cloth and leather cannot be filed into a blade |
| Reachable at `Touch` through `Danger.Deadly` | they have to get to it |
This is also the counter-play. A cell furnished only in cloth and hydroponics offers nothing to file.
A steel bed offers a steel shiv. **What you build the cell out of decides whether it can become a
weapon, and how nasty that weapon is.**
## Harvest by damage: the tell *is* the mechanic
Each poll the pawn makes progress, `OnHarvestTick` chips at the source:
```
damage = max(1, round(source.MaxHitPoints × 0.012)) // Blunt, ~1.2% of max HP per worked poll
```
The damage is not a side effect — it is the point. A gnawed bed is **probable cause**. The same
poll that advances the shiv damages the furniture, and that visible wear is what
[Warden Search](Warden-Search.md) reads to send a guard to the *right* prisoner first: a badly
damaged bed shoots a prisoner to the top of the search priority list. Whittling a shiv is loud, in
the only sense that matters — it leaves a mark you can act on.
Because the source takes real damage, a determined prisoner can **reduce a piece of furniture to
wreckage and move on to the next**: if the source is destroyed before the shiv is finished,
`FindSource` looks for another, and if there is none, the route **stalls** (progress is kept, not
lost). A materially poor cell is a real defence — the prisoner cannot make progress with nothing to
file.
## How long, and the material it becomes
Progress uses the default rate — roughly **1.5 unsupervised days** of active work — jittered ±25%
each poll so no two shivs finish on the same schedule:
```
progressPerPoll = 250 / (1.5 × 60000) × Rand.Range(0.75, 1.25)
```
When progress reaches 1, the shiv is finished, **silently** — the player is told nothing. Two things
happen:
- **Material carry-through.** `item.stuff = source.Stuff`. A wood bed yields a wood shiv, a plasteel
bunk a plasteel one. When the shiv is later redeemed into a real `Thing`, it is made of exactly
that material, and its combat stats follow.
- **The record.** `CriminalRecord.contrabandMade` is incremented (Core's record). That number feeds
the warden's future suspicion of this pawn and, if you run **Institution: Justice**, their security
classification.
## The shiv comes out in a break — and who reaches for it
A hidden shiv is inert until a breakout. When a prisoner enters the `PrisonerEscape` or
`PrisonerAssaultColony` duty, `JobGiver_UseContraband` offers to materialise what they are hiding —
but a **weapon** only comes out for someone who would actually use it. That test is
`Disposition.WouldArmSelf`, and it is the same gate `JobGiver_ArmSelf` uses to decide whether an
escapee even bothers to pick a weapon off the floor.
This matters because **vanilla escapees never arm themselves at all.**
`JobGiver_PickUpOpportunisticWeapon` exists and is simply absent from the escape duty tree, so a
base-game prisoner walks out unarmed, always. Arming is genuinely new behaviour, so it is kept rare
enough that when it happens you recognise *who* did it.
### `WouldArmSelf`
First a **hard gate**: a pawn who `WorkTagIsDisabled(Violent)` never arms, ever — a pacifist who
picks up a rifle is not a rare event, it is a bug. Otherwise:
```
chance = ArmDisposition(pawn) × ArmCircumstance(pawn)
armed = WouldSeeded(pawn, min(chance, 0.85), SaltArm) // seeded — a fixed fact per pawn
```
**Disposition** — who they are, fixed, nothing about today changes it:
```
ArmDisposition = 0.10 (base) × trait × skill
```
| Factor | Value |
|---|---|
| Bloodlust | ×2.5 (highest applicable trait only — not a stack) |
| Psychopath | ×2.0 |
| Brawler | ×1.8 |
| Wimp | ×0.3 |
| Kind | ×0.4 |
| skill | `0.5 + max(Shooting, Melee)/20 × 1.5` → range **0.5 .. 2.0** |
*Being captured in a raid does not make someone a fighter* — plenty of prisoners are cooks and
conscripts. A weapon is only worth grabbing if you know what to do with it, hence the skill term.
**Circumstance** — what is happening right now, live, not seeded:
| Condition | Effect | Reasoning |
|---|---|---|
| Health < 50% | ×0.4 | badly hurt people run or crawl; they don't seek a fight |
| ≥ 2 other prisoners escaping within 12 tiles | ×2.0 | a crowd emboldens; one man slips out, six men riot |
| An armed colonist within 20 tiles | ×1.8 | now a weapon is not bravado, it's the plan |
### Which weapon they reach for
`JobGiver_ArmSelf` prefers, in order:
1. **Their own dropped weapon** — the gun they carried when you downed them, still lying where it
fell within 45 tiles. Vanilla remembers it in `Pawn_MindState.droppedWeapon` and then never uses
the field for prisoners. *"A man walking back to the spot where his own rifle fell is a better
story than a man rummaging through your stockpile."* A prisoner does not care that you marked it
forbidden.
2. **Anything to hand** within 18 tiles — but only for a pawn over a higher disposition floor
(`WouldSeeded(pawn, 0.35, …)`). Rummaging a stockpile is a further step than grabbing your own gun
back.
A concealed shiv slots into this the natural way: a prisoner who has been sitting on a blade and who
is the sort to use it does not leave it in his pocket. `JobGiver_UseContraband` redeems it and moves
it straight from inventory into the equipment slot. The pawn who would *not* arm keeps it hidden and
runs — and the shiv survives to be found in a later search, or used in a later break.
## Not only shivs: the crude pick
`AcquireWorker_Improvise` is shared. The **crude pick** (`CB_DiggingTool`) is filed off furniture
exactly the same way — same disposition gate, same damage tell — but it carries an `escapeWorker`
instead of weapon stats, and its purpose is a tunnel rather than a fight. See [Tunnels](Tunnels.md).
## Quick reference
| Constant | Value | Meaning |
|---|---|---|
| Improvise base chance | 0.10 | before nature × nurture; seeded per pawn |
| Damage per worked poll | ~1.2% of source max HP | Blunt; the visible tell |
| Source search radius | 12 tiles | own bed first, then nearest workable furniture |
| Time to finish a shiv | ~1.5 active days | jittered ±25% per poll |
| Workable stuff | Metallic / Woody / Stony | cloth and leather cannot be whittled |
| Shiv `concealability` | 0.75 | hard to find on the body |
| Arm chance cap | 0.85 | after disposition × circumstance |
| Own-weapon reach | 45 tiles | their dropped weapon |
| Any-weapon reach | 18 tiles | scavenge floor 0.35 disposition |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*
+178
View File
@@ -0,0 +1,178 @@
# Tunnels
*The Prison-Architect dig: a disposed pawn goes down, not through — under the ground and everything
built on it, out past the last wall you were relying on.*
A locked door stops a prisoner who wants to walk out. It does nothing at all about a prisoner who
digs. This is the escape that ignores your perimeter, because it does not cross it — it goes beneath
it. It is slow, it is quiet, and it works for pawns who would never join a mood-driven riot.
## The crude pick
The dig needs a tool. `CB_DiggingTool` is a crude pick, filed off the cell's own furniture exactly
the way a shiv is (`AcquireWorker_Improvise` — same disposition gate, same damage-to-furniture
tell; see [Improvised Weapons](Improvised-Weapons.md)). Where the shiv is a weapon, this is a *way
out*: it carries no combat stats and is never equipped.
| Stat | Value |
|---|---|
| Description | *"Useless as a weapon and useless above ground — but pointed downward, patiently, it is a way out that no lock and no guard can see."* |
| Max HP | 60 |
| Mass | 1.2 kg |
| `concealability` | **0.5** |
| `hideIn` | `InCell` only (too bulky to ride on a body) |
| `acquireWorker` | `AcquireWorker_Improvise` |
| `escapeWorker` | `EscapeTunnelWorker` |
| Deterioration | 2.0 |
Because it hides `InCell` only, a warden turning over the *room* can find the pick itself — and, far
more easily, the hole it is digging.
## A second secret riding on the first
A tunnel is *"the third thing a contraband item can be, after 'made' and 'used in a break': a thing
you dig your way OUT with, quietly, over days."* It is deliberately **not a Job**. There is no
supervised task a warden could walk in on and interrupt — only a secret that grows and a wall that
eventually gives. Progress is driven from `MapComponent_Contraband`'s poll (`TickTunnels`), the same
250-tick heartbeat that makes contraband, so *the same search that finds a shiv can find the tunnel*.
The pick must first be **made** (or smuggled). Only once it is finished (`Ready`) does the dig begin,
as a second, longer secret carried on the same `ConcealedItem` (`escapeProgress`, `escapeWall`).
## Who digs
`EscapeTunnelWorker.WouldDig` gates on three things:
1. **Humanlike and held** — prisoner, slave, or ward patient (`IsHeld`, via Core's secured context).
2. **The disposition to dig for weeks:**
```
Propensity.Would(holder, 0.08, SaltTunnel)
```
Base chance **0.08** — deliberately below the shiv's 0.10. *"Fewer pawns will dig for weeks than
will palm a shiv."* Seeded per pawn, so it is a fixed fact about them.
3. **A viable plan exists** — there has to be somewhere to surface (see below).
This is the entire reason tunnels live in Contraband and not in a prisoner-only escape mod:
> It works for prisoners **and ward patients** alike. Prisoner Realism's escapes are for "mood
> prisoners" mid-break bashing a door, and a committed ward patient is never that pawn. A patient
> with a filed-down pick and the disposition to use it can still, slowly, dig out — and nothing else
> in the ecosystem lets them.
## Planning the dig: aim for the wilderness
The target is **not** the cell wall. *"You do not go through the wall, you go under it."* `TryPlan`
finds the **nearest wilderness** — the first standable cell, scanning radially outward from the
holder up to **60 tiles** (`MaxSearchRadius`), whose room *touches the map edge* and is not the room
they are held in.
Two consequences fall straight out of that:
- **Depth is your defence, and a single wall is not.** A tunnel from a deep bunker is a long dig; a
tunnel from a shack against the map edge is a short one. Wrapping one more wall around a cell does
almost nothing — the dig already ignores intervening walls entirely.
- **A fully enclosed, map-locked base with no reachable edge has nowhere to surface.** `TryPlan`
fails, `ProgressPerCheck` returns 0, and the tunnel **stalls** — progress is kept, not lost. Seal
the map and the shaft simply waits.
### How long
Dig time scales with how deep the cell sits:
```
requiredDays = clamp(distanceToWilderness / 6, 3, 15) // CellsPerDay = 6
progressPerPoll = 250 / (requiredDays × 60000) × Rand.Range(0.75, 1.25)
```
| | Days |
|---|---|
| Minimum (shallow cell near the edge) | **3** |
| Maximum (deep bunker) | **15** |
| Per day of digging | ~6 cells of depth |
The floor of 3 days keeps even the shortest tunnel a *real threat window* you have time to catch; the
ceiling of 15 keeps the deepest from being effectively forever.
## The tell: spoil
A dig has to go somewhere, and the dirt is where you notice it. Each dig tick, with a 50% seeded
chance (`OnDigTick`), the worker drops one tile of `Filth_Dirt` at the holder's position:
> A dirt floor swallows it; a paved cell shows it — exactly where a player might notice.
It is cheap and occasional on purpose: enough that a paved cell slowly accumulates a suspicious mess,
not so much that it spams filth or litters a clean cell every tick. If you floor your cells, spoil is
a genuine visual cue that someone is digging.
## Finding a tunnel in a search
A tunnel is *"a second, far less hideable secret."* The pick has `concealability` 0.5, but the
**shaft** gives itself away in proportion to how far along it is. When a warden search reaches the
`InCell` site, `ExtraDiscoverChance` is added on top of the tool's normal find chance:
```
extraChance = clamp01(escapeProgress) × 0.4 // up to +0.40 at a nearly-finished tunnel
```
So a routine cell toss that finds a shiv can also catch a dig in progress — *"the same guard patrol
that finds a shiv finds the hole"* — and the deeper the shaft, the harder it is to miss. A foiled
dig is logged the same as one that ran: on discovery, `CriminalRecord.escapeAttempts` is
incremented, which raises this pawn's future search priority sharply (a known tunneller is the
prisoner a warden checks first — see [Warden Search](Warden-Search.md)). The full find math is on the
[Warden Search](Warden-Search.md) page.
## Breakout
When `escapeProgress` reaches 1, `OnBreakout` fires:
1. **Re-plan the exit** so it is correct even if the base changed during the weeks of digging.
2. **Breach the outer wall or fence** the tunnel comes up beside (`LastContainmentOnLine` — the
*outermost* impassable edifice or fence between the cell and the surfacing point, walking inward
from the exit). It is destroyed with `KillFinalize` — a visible hole the colony must repair, and
proof of how they got out. An open perimeter needs no breach.
3. **Record the attempt** (`escapeAttempts++`) — it happened whether or not they get clear of the
map.
4. **Surface beyond the perimeter.** They dug all the way out, so they emerge *outside*, not in the
yard: the pawn is teleported to the exit cell, with a little spoil dirt to mark where they came
up.
5. **Hand off to vanilla** (`PrisonBreakUtility.StartPrisonBreak`). Now that they are loose and
outside, the base game's prison-break flow — and anything layered on it, e.g. **Prisoner
Realism** — takes it from there. A pawn already outside who no longer qualifies as an in-prison
escapee simply flees on their own; the breach and the emergence are the real outcome regardless.
The player gets one message: *"{pawn} has tunnelled out under the perimeter and escaped."* The tunnel
and the pick are spent together — the `ConcealedItem` is removed.
## How to defend against it
| Defence | Effect |
|---|---|
| **Floor the cells** | spoil dirt becomes visible; you can *see* a dig |
| **Search known offenders** | a logged escape attempt is the heaviest single term in search priority |
| **Deny the pick's raw material** | no workable furniture, no pick — same starve as the shiv |
| **Depth** | a deeper cell is a longer dig and a wider window to catch it |
| **Seal the map** | no reachable edge means the tunnel stalls indefinitely |
Note what does **not** help much: adding perimeter walls. The dig goes under them and only breaches
the *last* one on the way out.
## Quick reference
| Constant | Value | Meaning |
|---|---|---|
| Dig base chance | 0.08 | before nature × nurture; seeded per pawn |
| Wilderness search radius | 60 tiles | how far out it looks for a surfacing point |
| Cells per day | 6 | dig speed vs. cell depth |
| Min / max dig time | 3 / 15 days | clamps on `distance / 6` |
| Spoil chance per dig tick | 50% | one tile of `Filth_Dirt` |
| Extra find chance | up to +0.40 | scales with `escapeProgress` |
| Pick `concealability` | 0.5 | `InCell` only |
| On discovery / breakout | `escapeAttempts++` | logged either way |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*
+198
View File
@@ -0,0 +1,198 @@
# Warden Search
*The sixteenth warden job vanilla never wrote. A guard turns the cell over, and might find nothing —
which is exactly why it has to cost.*
The warden can chat, convert, feed, execute, enslave, release, suppress, and eleven other things.
Searching a prisoner is not one of them, and no such verb exists anywhere in the base game.
Contraband adds it as a **new** `WorkGiver`, not a patch on a vanilla one, and builds it around a
single hard rule: **the search must be able to come up empty.**
## Authority vs. act
The design splits one job into two classes:
| Class | Question it answers | Notes |
|---|---|---|
| `WorkGiver_Warden_Search` | **Who** may be searched? | subclasses `WorkGiver_Warden` — a warden already has the run of the prison, so no new authority is invented |
| `JobDriver_SearchPawn` | **What** is a search? | takes any pawn, searches any pawn |
> The WorkGiver is the authority. This driver is just the act.
Why bother splitting them? Because the enforcement layer is meant to grow. A future **Institution:
Police** module adds a *second* authority — a cop who may stop and frisk a **free colonist** — as a
new WorkGiver that reuses the same driver with a **narrower `Reach`** (a street frisk gets what is
`OnBody`, not what is under the floorboards). That module is an *addition*, not a rewrite, precisely
because "who may search" and "what a search reaches" were never welded together.
## The WorkGiver: who gets searched
`Contraband_WardenSearch` is a `Warden` work-type giver:
| Field | Value | Why |
|---|---|---|
| `workType` | Warden | it is warden work |
| `priorityInType` | 40 | **below feeding** — a starving prisoner matters more than a hidden shiv |
| `requiredCapacities` | Manipulation, Sight | you search with your hands and eyes |
| `Prioritized` | **true** (in C#) | rank candidates by suspicion, not by distance |
`Prioritized` has to be set in code: in 1.6 it is a `WorkGiver_Scanner` property, **not** a
`WorkGiverDef` field, and an XML `<prioritized>` silently errors on load. That flag is what lets a
gnawed bed and a thick record send the warden to the *right* prisoner first instead of the nearest.
`JobOnThing` will offer a search only if all of these hold: the warden should take care of this
prisoner, the target is a `Pawn`, the per-prisoner cooldown is up (`CanSearchNow`), the prisoner is
**awake, not downed, and not in a mental state**, and the warden can reserve them.
Note what is **not** checked — and the source is emphatic about it:
> Whether the prisoner is actually hiding anything. **The warden does not know. Nobody knows until
> the cell is turned over.** If the job were only offered when there was something to find, the mere
> appearance of the job would BE the discovery, and searching would stop being a gamble the player
> pays for in warden-hours. An empty search has to be possible, and has to cost.
### The cooldown
`MapComponent_Contraband` won't let the same prisoner be turned over more than **once per day**:
```
SearchCooldownTicks = 60000 // one in-game day
```
You cannot brute-force a well-hidden shiv by re-searching the same prisoner ten times in an hour.
Every search is one shot, and then that prisoner is off the list for a day.
## Priority: the search reads the tells
`GetPriority` is where suspicion becomes routing. Everyone is worth a routine toss; the tells push a
prisoner up the queue:
```
priority = 4 // baseline — everyone gets frisked
+ 12 × bedDamageFraction // chewed furniture = probable cause
+ min(8, timesCaught×2 + contrabandMade×0.5 + escapeAttempts×3) // the record
```
| Term | Weight | Meaning |
|---|---|---|
| Base | 4 | a routine toss for every prisoner |
| Gnawed bed | up to +12 | `1 − HP/maxHP` of their owned bed — a badly damaged bed shoots to the top |
| `timesCaught` | ×2 | already caught with contraband before |
| `contrabandMade` | ×0.5 | has finished contraband in the past |
| `escapeAttempts` | ×3 | **the heaviest term** — a known tunneller is checked first, every time |
| (record cap) | +8 max | the record cannot completely swamp fresh probable cause |
The record is read with `PeekFor` (a read-only lookup that never creates a record). The base of 4 for
everyone is not an accident — **innocent prisoners still get frisked**, because the search must be
able to come up empty or its mere offer would be the discovery.
**Worked example.** A prisoner whose bed sits at 40% HP (`missing = 0.6`), with 2 prior catches and
1 foiled tunnel:
```
priority = 4 + 12×0.6 + min(8, 2×2 + 0 + 1×3)
= 4 + 7.2 + min(8, 7)
= 4 + 7.2 + 7 = 18.2
```
versus **4** for a clean prisoner with an intact bed. The warden walks past the quiet one and goes
straight for the one who has been busy. This closes the loop [Improvised Weapons](Improvised-Weapons.md)
opens: whittling a shiv chews the bed, the damage is probable cause, and the warden's *suspicion* —
not the player's eye — sends them to search.
## The act: what a search does
`JobDriver_SearchPawn` walks the searcher to the subject, runs a **900-tick** wait toil with a
progress bar, then resolves. Resolution always does two things, hit or miss:
1. `MarkSearched(subject)` — starts the daily cooldown.
2. `timesSearched++` on the record — *"classification and the guard's future suspicion read how often
this pawn has been turned over."*
Then it gathers everything the subject is hiding that this search can **reach** (`Reach` defaults to
`OnBody | InCell`; a subclass can narrow it). If nothing is reachable, the job ends here — the time
was still spent, and the player is told nothing.
### Find math, per hidden item
For each reachable secret:
```
conceal = item.concealability // 0 = always found, 1 = never
chance = clamp( (0.35 + 0.03 × socialSkill) × (1 − conceal) × 2 , 0.02 , 0.95 )
if item is a tunnel in progress:
chance = clamp( chance + tunnelExtra , 0.02 , 0.98 ) // tunnelExtra up to +0.40
chance ×= WardenDisposition.Diligence(searcher) // the guard's thoroughness
if Rand.Chance( WardenDisposition.Corruption(searcher) × 0.6 ):
continue // the warden saw it and said nothing — looked away
found = Rand.Chance(chance)
```
Reading it in plain terms:
- **Skill raises the ceiling; concealability lowers it.** `BaseFindChance` is 0.35; each level of the
warden's **Social** skill adds 0.03. The `(1 − conceal) × 2` factor pivots at concealability 0.5
(neutral, ×1.0): a drug at 0.5 is average, a shiv at 0.75 is halved (×0.5), a heavy weapon at 0.35
is boosted (×1.3).
- **A good warden still misses a well-hidden shiv more often than not.** Worked: a level-8 Social
warden searching for a 0.75-concealability shiv gets `(0.35 + 0.24) × 0.5 = 0.295` *before*
Diligence scales it down further. Well under a coin-flip.
- **A tunnel is a different story.** The pick hides at 0.5, but a nearly-finished shaft adds up to
+0.40 and the cap rises to 0.98 — the deeper the dig, the harder it is to miss. A routine cell toss
catches a dig in progress.
- **The warden's own thoroughness and price** enter last: `Diligence` multiplies the whole chance
down for a lax, kind, or miserable guard, and `Corruption` gives a bent guard a flat chance to look
the other way even on an item they would otherwise have found. Both are on the
[Corruption](Corruption.md) page.
### On a find
```
Confiscate(subject, item.def) // it never becomes a Thing — nothing drops on the floor
timesCaught++ // raises future suspicion, feeds classification
if it was a tunnel: escapeAttempts++ // a foiled dig is a logged attempt, same as one that ran
```
and the player gets a message — *"{warden} searched {prisoner} and found hidden {item}"*, or the
tunnel variant. If nothing is found the player is told **nothing**: they do not learn there *was*
something, only that the warden's time was spent.
## Writing back to the record
Every outcome updates Core's `CriminalRecord`, which is what makes the search a *loop* rather than a
one-off dice roll:
| Field | Written when | Read by |
|---|---|---|
| `timesSearched` | every search, hit or miss | priority, suspicion, classification |
| `timesCaught` | a find | priority (×2), classification |
| `escapeAttempts` | a foiled tunnel | priority (×3, heaviest), classification |
If you run **Institution: Justice**, those same fields drive a prisoner's security classification and
feed the deterrence signal — a prison that catches contraband becomes a prison that classifies its
troublemakers correctly and deters the next attempt. If you run Contraband alone, the fields still
route the warden. See [Compatibility](Compatibility.md).
## Quick reference
| Constant | Value | Meaning |
|---|---|---|
| Base find chance | 0.35 | before skill and concealability |
| Per Social level | +0.03 | skill raises the ceiling |
| Find chance clamp | 0.02 .. 0.95 | (0.98 for a tunnel) |
| Search duration | 900 ticks | one wait toil, hit or miss |
| Search cooldown | 60000 ticks (1 day) | per prisoner |
| Priority base | 4 | every prisoner is worth a routine toss |
| Gnawed-bed weight | up to +12 | `1 − HP/maxHP` |
| Record weight | up to +8 | `timesCaught×2 + contrabandMade×0.5 + escapeAttempts×3` |
| Default `Reach` | `OnBody \| InCell` | narrowed by a future street-frisk subclass |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*
+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).*
+115
View File
@@ -0,0 +1,115 @@
# Affiliations and segregation
*Gangs grow out of who pawns **already are** — and the counter-play is to keep the wrong pawns apart.*
A gang in this mod is never invented from nowhere. It crystallises along the bonds a colony already
contains: shared faction, shared faith, or real friendship. This page covers the formation rule — what
`SharesAffiliation` checks and why the *same room* requirement matters — and then the segregation
counter-play that turns those same bonds against the network.
## The formation rule
On each 5000-tick check, the gang component walks the eligible `crew` (every spawned humanlike that
currently meets the `Nature × Nurture ≥ 0.9` join bar) and tries to pair each unaffiliated member with
a mate:
```
mate = a pawn q in crew such that:
q != p
AND q.GetRoom() == p.GetRoom() -- same room, right now
AND SharesAffiliation(p, q) -- a bond they already have
if mate exists: Enlist(p, mate)
```
Two conditions, both required: they must be **in the same room** at the moment of the check, **and**
they must **share an affiliation**. A disposed pawn does not fall in with a stranger across the map; it
falls in with someone it is standing next to *and* already connected to.
`Enlist` then binds them: if either already runs with a gang, the other joins that gang; otherwise a
fresh gang id is minted. So gangs accrete — a new member pairing with an existing member is absorbed
into the existing crew rather than starting a rival one.
### What counts as an affiliation
```
SharesAffiliation(a, b) is true if ANY of:
a.Faction != null AND a.Faction == b.Faction -- same faction
Ideology active AND a.Ideo == b.Ideo (both non-null) -- same ideoligion
a.relations.OpinionOf(b) >= 20 -- a genuine friendship
```
| Bond | Condition | Notes |
|---|---|---|
| **Same faction** | `a.Faction == b.Faction` (non-null) | your colonists share one; captured raiders share theirs |
| **Same ideoligion** | `a.Ideo == b.Ideo`, only if the Ideology DLC is active | guarded by `ModsConfig.IdeologyActive` |
| **Friendship** | `OpinionOf(b) >= 20` | a real positive relationship, not mere acquaintance |
The design intent, from the source:
> Gangs grow out of who pawns ALREADY are, not out of nowhere — so a prisoner's gang on the outside is
> their old faction/friends, and rival factions run as rival gangs.
This is why the system feels coherent rather than arbitrary. A crew is a faction bloc, a congregation,
or a friend group — social lines the colony *already has*. Gangs mesh with them instead of stamping
random groupings over the top.
## Rival factions become rival gangs
Follow the rule to its conclusion. Two captured raiders from **different** hostile factions each share
an affiliation with *their own* side but not with each other. Housed in the same wing, each pairs up
along its own faction line — and now you have **two gangs**. By the definition on the
[Rivalry and Fights](Rivalry-and-Fights.md) page, members of two different gangs are automatically
**rivals**, and rival-gang violence between them is booked as a crime.
So the affiliation rule and the rivalry rule are two ends of one idea: **who bands together** and **who
is opposed** both derive from pre-existing loyalties. Mix hostile factions or clashing ideoligions in
one room and you have not made one big gang — you have made two rival ones and lit the fuse between
them. This is a housing decision with mechanical teeth.
## Segregation: the counter-play
Here is where the formation rule and the network rule meet, and where the rest of the suite earns its
keep. A gang can only *do* anything — resupply itself — if its members can **reach** each other.
`MoveWithin` requires `SameGang` **and**, for two co-located pawns, a walkable path
(`CanReach(..., Touch, Danger.Deadly)`). Break the path and you break the network:
```
MoveWithin(from, to):
...
if from.Spawned && to.Spawned && !from.CanReach(to, Touch, Deadly):
return false -- kept apart: the segregation counter-play
```
The source calls this out as the intended answer to the whole mod:
> The counter-play is the rest of the suite: Classification SEGREGATES rivals into different wings, and
> a network whose members cannot REACH each other is starved.
**Classification** lives in **Institution: Justice**. It grades pawns by risk and lets you sort them
into separate, walled wings. Two gangmates graded into different wings, with no walkable route between
them, fail the reach gate on every network pass. The gang still *exists* on the membership map — they
are still the same crew, still rivals of the other crew — but it can no longer pass a shiv from a
holder to a have-not. **A network that cannot reach itself is a network that cannot supply itself.**
This is the elegant part of the design: the very bonds that formed the gang (`SharesAffiliation`) tell
you *who to keep apart*, and the reach requirement (`CanReach`) makes keeping them apart actually
starve the supply chain. You do not disband a gang; you **partition the graph** until its edges carry
nothing.
## How to play it
- **Read affiliations before you house pawns.** Same-faction and same-ideoligion prisoners will band
together; hostile factions in one wing will form *rival* gangs and fight. Sort deliberately.
- **Segregate by Classification, not by hope.** Putting rivals in "different areas" is not enough — the
reach test cares about a *walkable path*. A shared corridor is a supply line. Use genuinely separate,
walled wings.
- **Starve, don't chase.** You will rarely delete a gang outright. The durable win is to keep its
members unable to reach one another so the network dries up, while keeping mood and deterrence high so
few pawns cross the join bar in the first place (see [Joining](Joining.md)).
- **Watch the outside valve.** Segregation starves *internal* resupply; a bent warden can still inject
new contraband from outside (see [Networks and Smuggling](Networks-and-Smuggling.md)). Partition the
wing *and* clean up your wardens for a network that genuinely goes dark.
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs (this). Each mod stands alone; together they are one system.*
*AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+89
View File
@@ -0,0 +1,89 @@
# Institution: Gangs
*Gangs as **contraband economies** — not mood. The social capstone of the **Institution** suite of
RimWorld 1.6 mods.*
---
## What Gangs is
Gangs turns the loose, disposed pawns in your colony — colonists, prisoners, slaves — into a
**network** that moves contraband. When disposition and circumstance line up, pawns who already share
a bond band together, and a member who holds a stash quietly resupplies a member who has none. A
search that turns up nothing on one prisoner has therefore *not* cleaned out the wing: the wing is a
supply chain, and you only searched one link.
That is the entire point, and it is a deliberately narrow one. Gangs does not do mood. It does not do
a "gang leader buff." It does the one thing nothing else in the ecosystem does — it makes contraband
**flow between pawns**, and then hands you the tools to cut the flow.
## The gap it fills (and the one it doesn't)
If you run **Prisoner Realism**, its *Ringleader* system already models a dominant prisoner spreading
unrest and mood contagion through a wing. That system is good, and Gangs does not touch it. Ringleader
owns *influence and mood*.
Gangs owns the thing Ringleader has no equivalent for: **the network as a logistics graph.** Contraband
physically changes hands along social lines. A stash on one pawn is a stash available to the whole
gang. The two systems are complementary — run both. Ringleader tells you *who stirs the pot*; Gangs
tells you *how the shivs get around*.
## Why it needs the whole suite
Gangs is the module built **last**, because it needs every other Institution mod already in place. It
is not a standalone feature; it is the suite working together, and it is honest about that in its
dependencies:
| It asks... | ...and the answer comes from |
|---|---|
| *Who is disposed enough to join?* | **Institution: Core** — `Nature × Nurture`, the shared propensity engine |
| *What do they move?* | **Institution: Contraband** — concealment, stashes, search, and the bent-warden supply route |
| *Where does a fight get booked, and how are rivals kept apart?* | **Institution: Justice** — the crime/deterrence loop, and Classification's segregation |
Remove any one of those and Gangs has nothing to stand on. It is the place where Core's spectrum,
Contraband's stashes, and Justice's policing all cash out at once.
## A healthy colony grows few gangs — or none
This is the thesis, and it is enforced in the numbers, not just the flavour. Membership is gated at
`Nature × Nurture ≥ 0.9` — a high bar on purpose. A disposed pawn who is **well-kept and
well-policed** does not band up: their nurture multiplier stays low, and deterrence pulls it lower
still. It takes a foul streak (nature) *and* a badly-run situation (nurture) at the same time.
So a gang problem is a **symptom**. It is the game telling you that a wing is mistreated, under-policed,
or both. The fix is never "fight the gang system" — it is to run a better prison. Feed them, give them
recreation, keep deterrence high, segregate rivals, and the networks starve on their own.
> A gang is a symptom of a badly-run colony, not furniture.
## The pages
- **[Joining](Joining.md)** — `WouldJoin = Nature × Nurture ≥ 0.9`: why the bar is high, why good
treatment and deterrence keep pawns *out*, and how the 5000-tick check works for every kind of pawn.
- **[Networks and Smuggling](Networks-and-Smuggling.md)** — `MoveWithin`: how a holder resupplies a
needy gangmate, the *same-gang + can-reach* rule, why this defeats a single search, and how a bent
warden refills a starved network from outside.
- **[Rivalry and Fights](Rivalry-and-Fights.md)** — `AreRivals` and `RecordFight`: how a gang fight is
booked as a crime through Justice, why inside-the-wire and out-on-the-street are the same offence,
and how it feeds deterrence.
- **[Affiliations and Segregation](Affiliations-and-Segregation.md)** — `SharesAffiliation` plus
*same room* as the formation rule, why rival factions become rival gangs, and how Classification's
segregation is the counter-play that starves a network which cannot reach itself.
## The suite
Part of the **Institution** suite of RimWorld 1.6 mods:
- **Institution: Core** — the propensity engine (`Nature × Nurture`), criminal records, secured context.
- **Institution: Contraband** — concealment, improvised shivs, tunnels, warden search and corruption.
- **Institution: Justice** — Classification, Deterrence, Discipline, Parole, Regime.
- **Institution: Gangs** — this mod: joining, networks/smuggling, rivalry/fights.
Sibling projects: **Foul Play** (the vessel/substance framework and the "Piss Nuke") and **Ward** (the
test harness and a ward/treatment prison mode).
Each mod stands alone as an install; together they form one system.
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs (this). Each mod stands alone; together they are one system.*
*AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+134
View File
@@ -0,0 +1,134 @@
# Joining a gang
*How Gangs decides who runs with a crew — and why a well-run colony keeps almost everyone out.*
Membership is not random, and it is not a mood event. A pawn joins a gang only when their **disposition
and their circumstances line up at once** — the same `Nature × Nurture` engine that drives every other
behaviour in the Institution suite. This page covers the exact test, why the bar is set where it is,
and how you keep pawns on the right side of it.
## The rule
```
WouldJoin(pawn) == Propensity.Nature(pawn) * Propensity.Nurture(pawn) >= JoinThreshold
JoinThreshold = 0.9
```
Two gates before the maths even runs:
| Guard | Effect |
|---|---|
| `pawn == null` | never joins |
| not `RaceProps.Humanlike` | never joins — animals and mechs are out |
Then it is one line: multiply the pawn's **Nature** by their **Nurture** and compare to **0.9**. There
is no dice roll here. `WouldJoin` is a deterministic threshold on the two propensity scores, so the
same pawn in the same situation always gives the same answer — join, or don't. (This is different from
Core's `Propensity.Would(...)`, which *is* a seeded random roll used for one-off acts like a piss
spree. Gang membership is a standing condition, so it uses the raw product.)
### The two halves
Both terms come from **Institution: Core** — see Core's own documentation for the full tables — but you
need the shape of them to understand the bar:
- **Nature** (`0..1`) is *who the pawn is*: a base of `0.05`, plus trait weights, clamped to `[0, 1]`.
Psychopath `+0.45`, Bloodlust `+0.35`, Kleptomaniac `+0.40`, Greedy `+0.25`, Abrasive `+0.15`;
and it goes **down** for Kind `−0.30` or Ascetic `−0.15`. This is fixed at pawn creation and barely
moves.
- **Nurture** (a multiplier, `~≥ 0.4`, base `1.0`) is *the situation you put them in*: a floored mood
multiplies it up hard (roughly `×2.5` under 0.20 mood, `×1.6` under 0.35), being held while
miserable adds more (`×1.4`), a hardened record (negative reform) raises it, and — critically —
**deterrence pulls it down** (`×Lerp(1.3, 0.7, deterrence)`, neutral `1.0` at baseline order, so a
high-deterrence colony scales nurture toward `0.7`). This is the half you control.
## Why the bar is high
`0.9` is a deliberately steep threshold. From the source, in its own words:
> A high bar, on purpose: gangs are a symptom of a badly-run colony, not furniture. A healthy Rimworld
> — content pawns, an orderly colony — grows few gangs, if any.
Because the two terms are **multiplied**, both have to be substantial for the product to clear `0.9`.
A nasty pawn in a happy, policed colony has a low nurture and stays out. A saintly pawn in a hellhole
has a near-zero nature and stays out. You only get a gang when a genuinely disposed pawn is *also*
neglected or unpoliced — foul streak **and** bad situation, together.
That is the design speaking through the arithmetic: a gang is never bad luck. It is feedback.
## Worked examples
All numbers below use Core's documented factors; the intermediate nurture figures are rounded to show
the shape of the decision.
| Pawn | Nature | Situation → Nurture | Product | Joins? |
|---|---|---|---|---|
| **Kind colonist**, ordinary life | `0.05 − 0.30 → 0.00` (clamped) | content, `≈ 1.0` | `0.00` | **No** — nature floors it |
| **Greedy prisoner**, content, policed | `0.30` | fed & high deterrence, `≈ 0.7` | `≈ 0.21` | **No** |
| **Greedy prisoner**, starved & neglected | `0.30` | mood floored + held, `≈ 3.5` | `≈ 1.05` | **Yes** |
| ...same pawn, but you raise deterrence | `0.30` | `× ≈ 0.7` → `≈ 2.45` | `≈ 0.74` | **No** — deterrence tipped them out |
| **Psychopath + Bloodlust**, mood floored | `≈ 0.85` | mood floored + held, `≈ 3.5` | `≈ 2.9` | **Yes** — nothing short of reform pulls this back |
The middle rows are the whole game of it. The **same greedy prisoner** joins or abstains purely on how
you run the wing. The Kind colonist and the near-maxed psychopath are the fixed poles: one essentially
cannot join, the other essentially always will if you neglect them. Everyone in between is a policy
choice.
The mod's own integration test asserts exactly these poles: a psychopath-plus-bloodlust pawn with a
floored mood returns `WouldJoin == true`, and a Kind colonist in ordinary circumstance returns
`WouldJoin == false`.
## It works for every pawn, everywhere
There is no "prisoners only" special case. The comment is explicit:
> Any pawn — colonist, prisoner, slave — can, wherever they are.
`WouldJoin` reads nothing about a pawn's secured status. A disposed **free colonist** can run with a
crew on the outside; a **prisoner** can run with theirs on the inside; a **slave** likewise. This is
what lets a gang span the wall — an outside member and an inside member of the same crew (see
[Networks and Smuggling](Networks-and-Smuggling.md)). Being held raises nurture (misery), but it is not
a requirement.
## The cadence: the 5000-tick check
Membership is re-evaluated on a fixed interval by the gang map component:
```
CheckInterval = 5000 ticks
MapComponentTick: run only when TicksGame % 5000 == 0
```
**5000 ticks** is about **2 in-game hours** (a RimWorld day is 60,000 ticks), or roughly **80 seconds**
of real time at normal (1×) speed. On each fire, the component:
1. Gathers `crew` — every spawned humanlike on the map for whom `WouldJoin` is currently true.
2. Pairs up unaffiliated members of `crew` who **share a room and a bond** into gangs (see
[Affiliations and Segregation](Affiliations-and-Segregation.md)).
3. Runs one round of network resupply inside each gang (see
[Networks and Smuggling](Networks-and-Smuggling.md)).
Because the check re-reads `WouldJoin` every time, membership is *live*: a pawn whose situation improves
past the point where the product drops back under `0.9` simply stops being eligible to form new bonds.
The bar is not a one-time gate at recruitment — it is a standing condition the colony is continuously
graded against.
## How to keep pawns out
You do not fight the gang system. You lower nurture, which lowers the product, which drops pawns below
`0.9`:
- **Feed them and give them recreation.** Mood is the biggest nurture multiplier by far. A wing above
0.35 mood loses the `×2.5`/`×1.6` spikes entirely.
- **Keep deterrence high** (Institution: Justice). Deterrence scales nurture down toward `0.7`; a
well-policed colony is *arithmetically* less gang-prone. This is the row in the table above where the
same greedy prisoner flips from *joins* to *abstains*.
- **Reform, don't just punish.** A hardened record (negative reform) *raises* nurture; discipline that
actually reforms (positive reform) lowers it. Harsh punishment that prisonizes a pawn makes the
gang problem worse, not better.
- **Accept the two poles.** A near-maxed psychopath will run with someone the moment you slip; a Kind
pawn essentially never will. Spend your attention on the middle.
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs (this). Each mod stands alone; together they are one system.*
*AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+120
View File
@@ -0,0 +1,120 @@
# Networks and smuggling
*The one thing nothing else models: contraband that flows **between** pawns. A gang is a supply chain.*
This is the core of the mod. Everything else — who joins, who fights, who is kept apart — exists to
serve or to break the network described here. A gang is not a mood aura; it is a **logistics graph**,
and its edges carry contraband.
## The move
The heart of it is one method, `MoveWithin(from, to)`: a gangmate who is holding a stash passes a piece
of it to a gangmate who has none. It returns `true` if something actually moved.
```
MoveWithin(from, to):
1. both non-null, and SameGang(from, to) -- else false
2. if both are Spawned: from must CanReach(to, -- the reach gate
PathEndMode.Touch, Danger.Deadly) -- else false
3. a Contraband tracker must exist on the map -- else false
4. 'from' must have at least one concealed item -- else false
5. take stash[0]:
tracker.Confiscate(from, item.def) -- leaves the supplier's hands
tracker.Conceal(to, item.def) -- arrives, hidden, in the customer's
return true
```
A few things worth reading carefully:
- **One item per move.** It takes `stash[0]` — the first concealed item on the supplier — and moves
exactly that. It is a redistribution, not a duplication: the item leaves `from` (`Confiscate`) and
arrives concealed on `to` (`Conceal`). The gang's total stash is unchanged; only *who holds it*
changes.
- **Same gang, always.** `SameGang` requires both pawns to hold the same non-zero gang id. There is no
smuggling to a stranger — the network only moves along membership.
- **The reach gate only applies to co-located pawns.** The `CanReach` check is guarded by
`from.Spawned && to.Spawned`. Two pawns both physically on the map must have a walkable path
(`Touch` range, willing to cross `Deadly` danger) between them. If a member is not spawned, the
reach check is skipped — which is what allows a gang to reach across the wall to a member who is off
the active map.
## Why this defeats a single search
Consider a wing of four gangmates and one shiv. You search Prisoner A and find nothing — clean. You
tick a box: *cell searched, no contraband.* But the shiv was on Prisoner C the whole time, and on the
next 5000-tick network pass it will move to whoever is out. Search C tomorrow and it may already be on
A again.
From the source comment, plainly:
> A gangmate holding a stash supplies one who has none, so a search that turns up nothing on one
> prisoner has not cleaned out the wing.
A single search is a snapshot of one node in a graph that reshuffles itself. This is the whole reason
contraband-as-a-network is worth modelling: **the unit of contraband is the wing, not the pawn.** To
clean it out you have to either search faster than it moves, or — far better — break the graph.
## The automatic resupply pass
You do not call `MoveWithin` by hand; the gang component does it on the 5000-tick check. After forming
gangs for the pass, it runs one resupply round **per gang**:
```
for each gang among the eligible crew:
holder = first member who IS hiding contraband
needy = first member who is NOT hiding contraband
if holder and needy both exist:
MoveWithin(holder, needy)
```
So on each interval, every gang that has both a haves-member and a have-not-member performs **one**
transfer, moving a single item from a holder to someone empty-handed. Over several ticks the effect is
that a gang tends to keep its members supplied and to spread a stash out — which is exactly what makes
a scattershot search miss it. (Only members who currently meet the join bar participate in this
automatic pass; the resupply loop draws from the same `crew` used for formation.)
## The cross-wall move
Because `WouldJoin` and `SameGang` care nothing about a pawn's secured status, a gang can have a
**free** member and a **held** member. If the free member is holding the stash, the network resupplies
*into* the prison — the outside man passes to the inside man. The mod's integration test builds exactly
this: a free psychopath colonist and a prisoner in one gang, the stash on the free member, and
`MoveWithin(free, prisoner)` succeeds — after which the prisoner is hiding contraband and the free
colonist is not. Your prison's contraband problem is not sealed inside your prison.
## Resupply from outside: the bent warden
`MoveWithin` only *redistributes* what a gang already has. So what happens when you finally search the
whole wing on the same day and strip every member clean — the graph has no more edges to carry? The
gang is starved. It stays starved until contraband **re-enters** from outside.
That external source is not part of Gangs; it is **Institution: Contraband's** corruption route. A
warden's honesty varies by personality (a Greedy warden has a price), and a bent warden can *smuggle
contraband in to a prisoner* — `Corruption.Smuggle(warden, prisoner, tracker)` plants a concealed item
directly. That single seeded item is then all the network needs: one holder, and the 5000-tick pass
spreads it back out across everyone who can reach.
So the two halves of the supply picture are:
| Mechanism | Owner | What it does |
|---|---|---|
| `MoveWithin` | **Gangs** | moves existing contraband *between* gangmates who can reach each other |
| `Corruption.Smuggle` | **Contraband** | injects *new* contraband from outside via a corruptible warden |
Cut both and a gang genuinely dries up. Cut only the redistribution and a bent warden re-seeds it; cut
only the warden and any stash you missed keeps circulating.
## How to break a network
- **Search the whole wing at once**, not one pawn at a time. A partial sweep is a snapshot the network
routes around.
- **Segregate rivals into different, unreachable wings** (Classification, in Institution: Justice). The
reach gate is the lever — a network whose members cannot walk to each other cannot pass anything.
This is the primary counter-play; see
[Affiliations and Segregation](Affiliations-and-Segregation.md).
- **Clean up your wardens.** If searches never seem to finish the job, you may have a Greedy warden
re-seeding the wing. Corruption is the resupply valve; personality is the fix.
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs (this). Each mod stands alone; together they are one system.*
*AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+116
View File
@@ -0,0 +1,116 @@
# Rivalry and fights
*Two gangs, one grudge. A gang fight is a **crime** — booked, attributed, and fed back into the colony's
climate of order.*
Gangs do not just supply themselves; they collide. Where the network is about who shares, rivalry is
about who is opposed — and, crucially, about turning gang violence into something the rest of the
suite can *see* and *police*.
## Who is a rival
```
AreRivals(a, b):
ga = GangOf(a); gb = GangOf(b)
return ga != 0 && gb != 0 && ga != gb
```
The definition is exactly as blunt as it reads: **two pawns in two different gangs are rivals.** Both
must actually be in a gang (a non-zero id), and the ids must differ. From the source:
> Two pawns of DIFFERENT gangs are rivals — inside the wire or out on the street.
Note what is *not* required. There is no separate "hostility" flag, no rival-declaration event, no
threshold to cross. The moment two crews exist, their members are rivals of one another. Rivalry is a
structural fact about the membership map, not a mood or a relationship value.
Two consequences fall straight out of that:
- **Non-members are nobody's rival.** A pawn with gang id `0` — everyone in a healthy colony — is never
a rival to anyone, because `AreRivals` requires both ids non-zero. No gangs, no rivalry.
- **Same gang, never rivals.** Members of one crew fail the `ga != gb` test. Within a gang there is no
rivalry to book; there is the network (see [Networks and Smuggling](Networks-and-Smuggling.md)).
Where do two *different* gangs come from in the first place? From the affiliations pawns already have —
rival factions and rival ideoligions form into separate crews. That is the subject of
[Affiliations and Segregation](Affiliations-and-Segregation.md); the short version is that gangs mesh
with the colony's existing social fault lines, so **rival factions run as rival gangs.**
## A fight is a crime
The payoff of tracking rivalry is `RecordFight`. When a rival-gang attack happens, it is not treated as
generic brawling — it is booked as a crime through the same Justice pipeline as any other offence.
```
RecordFight(attacker, victim):
if attacker == null || victim == null: return
if !AreRivals(attacker, victim): return -- only rival-gang violence counts
Justice.RecordCrime(attacker)
```
So the guard is precise: the two pawns must be **rivals** (different gangs) for anything to be recorded.
A scuffle between gangmates, or between two non-members, is not a *gang* fight and is not booked here.
When it *is* rival-gang violence, the whole event routes through one call — `Justice.RecordCrime`,
against the **attacker** — and that single call does three things at once (it lives in Institution:
Justice, but this is what Gangs is leaning on):
| Effect of `Justice.RecordCrime(attacker)` | Owner |
|---|---|
| `crimesCommitted` on the attacker's record goes up | Core's `CriminalRecord` |
| `lastCrimeTick` is stamped to now | Core's `CriminalRecord` |
| the colony's **deterrence** nudges **down** (a crime happened; order slipped) | Justice's `MapComponent_Deterrence` |
The mod's integration test confirms the loop end to end: it builds two rival gangs, records a fight,
and asserts both that the attacker and victim *are* rivals and that the attacker's `crimesCommitted`
went up as a result.
## Inside the wire and out on the street are the same offence
This is the design point the source is emphatic about:
> A gang fight is a CRIME — it goes on the attacker's record and moves the colony's climate through
> the same Justice loop as any other, so it can be investigated, attributed and policed. Rival-gang
> violence inside a prison and out on the street are the same offence.
There is no separate code path for a prison-yard shanking versus a colonists' brawl in the dining
room. `RecordFight` reads only `AreRivals` and calls `Justice.RecordCrime`. A free colonist who is a
gang member attacking a rival is booked identically to a prisoner doing the same in a cell block. The
gang system does not care which side of the wall the violence is on — only that it was between rivals.
This is what makes gang violence *legible* to the rest of the suite. A fight is not a one-off flavour
event that scrolls past in the log; it is a record entry with an attributed perpetrator and a timestamp,
which means it can be investigated, blamed on a specific pawn, and answered.
## How it feeds deterrence — and back into joining
Here is the loop that closes the whole suite:
1. A rival-gang fight fires `RecordFight` → `Justice.RecordCrime(attacker)`.
2. That nudges the colony's **deterrence down**. Order has visibly slipped.
3. Lower deterrence *raises* the nurture multiplier for **every** disposed pawn on the map
(deterrence scales nurture via `×Lerp(1.3, 0.7, deterrence)` — less deterrence, bigger multiplier).
4. Higher nurture pushes more pawns over the `Nature × Nurture ≥ 0.9` join bar (see
[Joining](Joining.md)).
5. More members → more rivals → more fights available to be booked.
Left unanswered, gang violence is self-reinforcing: each booked fight makes the next one likelier. The
brake is the other half of Justice — **punishment raises deterrence back up** (`RecordPunishment`
nudges it up), which lowers nurture, which drops borderline pawns back under the bar. The integration
test checks exactly this seesaw: a punishment raises the deterrence level, and a subsequent crime
lowers it again.
## How to play it
- **Respond to booked fights.** A gang fight is real feedback that deterrence has slipped. Punishing the
attributed attacker is not just retribution — it is the mechanical lever that pulls deterrence back
up and cools the whole map's propensity.
- **Don't manufacture two gangs where there was one.** Rivalry needs two different crews. Housing pawns
of hostile factions or clashing ideoligions together is what creates the second gang and the fights
between them — see the next page.
- **Read the record, not the moment.** Because every fight is attributed to a perpetrator, a thick
record of gang violence points you at *who* to segregate, discipline, or parole — the same tools the
rest of the suite already gives you.
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs (this). Each mod stands alone; together they are one system.*
*AI disclosure: developed with substantial assistance from Claude (Anthropic).*
+148
View File
@@ -0,0 +1,148 @@
# Classification
*The intelligence layer over the wings you already build by hand.*
You already build a minimum wing and a max block. You already put the psychopath behind steel and the
teenage pickpocket behind a wooden door. Classification does not replace those walls, cells and locks —
it answers the question vanilla never does: **who belongs where.** It reads the one shared
`CriminalRecord` every module writes to and turns it into a single verdict, so the crime system, the
search, the escape and the classifier all agree on how dangerous a pawn is, because they read the same
number.
Grade drives everything downstream: search frequency, privileges, escape risk, and — critically —
parole eligibility ([Parole](Parole.md) gates on grade).
> The isolation *toll* of a max/solitary placement is Prisoner Realism's (good) sim. Justice only
> decides the placement.
---
## The tiers
`SecurityGrade` is four bands, low to high — the tiers players already build, named:
| Grade | Risk score | Reading |
|---|---|---|
| **Minimum** | `< 0.35` | Barely a disposition, clean record. Open dorm, light watch. |
| **Medium** | `0.35 – 0.79` | A real record or a real nature. Proper cell, routine search. The parole ceiling. |
| **Maximum** | `0.80 – 1.49` | Repeat offender or proven flight risk. Steel, solitary door, frequent search. |
| **Supermax** | `≥ 1.5` | The headline cases. Escapes plus crimes plus a dark nature. Never let them near a wall. |
Note the ceiling at **Medium**: [Parole](Parole.md) will only consider a pawn graded Medium or below.
Grade is therefore not just a placement hint — it is the gate a pawn has to fall back through before
they can be released.
---
## The risk score
Risk is a `0..~3` number: baseline disposition plus everything on the record, minus reform earned.
```
risk = Nature(pawn) * 0.5 // who they are, before they have done anything
+ escapeAttempts * 0.35 // a proven flight risk is the headline
+ crimesCommitted * 0.20
+ timesCaught * 0.15
+ contrabandMade * 0.10
- max(0, reform) * 0.4 // genuine reform earns a downgrade
risk = max(0, risk) // never negative
```
| Term | Weight | Why this weight |
|---|---|---|
| `Nature` (0..1) | `× 0.5` | Disposition alone can carry a pawn to Medium (Nature 0.7+), but never past it on its own. Character is a suspicion, not a conviction. |
| `escapeAttempts` | `× 0.35` | The heaviest per-event term. A pawn who *runs* is the one who ends up armed in your base. One attempt is worth nearly two crimes. |
| `crimesCommitted` | `× 0.20` | The staple. Steady, cumulative. |
| `timesCaught` | `× 0.15` | Caught contraband is worse than merely suspected. |
| `contrabandMade` | `× 0.10` | The lightest term — making a shiv is common; escaping with one is not. |
| `reform` (if > 0) | `− 0.4` | The only term that can *lower* the grade. At most −0.4 (reform capped at 1.0). |
Two facts fall out of the numbers, and both matter for how you play:
- **`timesSearched` is not in the formula.** Searching a pawn does not make them more dangerous —
only *finding* something (`timesCaught`) does. Search freely; it costs the prisoner no grade.
- **Reform can shift at most 0.4 of risk.** At `reform = 1.0`, the downgrade is `−0.4`. So a pawn
whose record *without reform* already totals `≥ 1.2` can never fall to Medium (`< 0.8`) no matter
how thoroughly they reform — and therefore can **never be paroled**. A prolific offender is a
life sentence. You will hold them, work them, or execute them; you will not release them. This is a
deliberate property of the weights, not an accident.
---
## Worked example — grading a record
**Bram**, an Abrasive raider. Nature = `0.05 + 0.15 = 0.20`. He has three crimes, one escape attempt,
was caught twice, made two shivs, and has not been reformed (`reform = 0`).
```
risk = 0.20 * 0.5 = 0.100 (nature)
+ 1 * 0.35 = 0.350 (escape — the headline)
+ 3 * 0.20 = 0.600 (crimes)
+ 2 * 0.15 = 0.300 (caught)
+ 2 * 0.10 = 0.200 (contraband)
- 0 * 0.4 = 0.000 (no reform)
─────────
total = 1.550 → ≥ 1.5 → SUPERMAX
```
Now treat him. Say patient handling and good conduct lift him to `reform = 0.40`:
```
risk = 1.550 - (0.40 * 0.4) = 1.550 - 0.160 = 1.390 → MAXIMUM
```
He drops a band. But notice: his reform-less risk is `1.55`, well above `1.2`. Even at perfect
`reform = 1.0` he lands at `1.55 − 0.40 = 1.15` — still Maximum. **Bram is beyond parole for the rest
of his life.** His record decided that, not his disposition.
Contrast **Cass**, a Kind pickpocket. Nature = `clamp01(0.05 − 0.30) = 0`. One crime, caught once, no
escape, no contraband, `reform = 0`.
```
risk = 0 + 0 + (1*0.20) + (1*0.15) + 0 - 0 = 0.350 → MEDIUM (exactly on the line)
```
Give Cass the standard treatment to `reform = 0.30`:
```
risk = 0.350 - (0.30 * 0.4) = 0.350 - 0.120 = 0.230 → MINIMUM
```
Cass is now Minimum, and — being Medium-or-below with `reform ≥ 0.30` — **paroleable**. Same
institution, two futures, and the record told you which was which before you had to guess.
---
## How to play with grades
- **Segregate by grade, not by vibe.** Sort your cells to the four bands and put a pawn where their
score, not your hunch, says. The whole suite reads the record; you can too.
- **Escapes are the alarm.** One escape attempt (`+0.35`) will jump a middling pawn a band. If a pawn
crosses into Maximum after a break attempt, believe it — treat them as the flight risk the number
says they are.
- **A clean nature is not a clean record.** A Kind pawn who has escaped twice still grades Maximum;
disposition is only half the score.
- **Watch the parole ceiling.** If you intend to release someone, keep their reform-less risk under
`1.2` — mostly that means limiting how deep their record gets *before* you start treating them. A
pawn you let rack up escapes and catches has sentenced themselves.
- **Reform is the only lever that lowers a grade.** See [Discipline & Reform](Discipline-and-Reform.md)
for how to move it, and [Parole](Parole.md) for where the grade gate lands.
---
## For modders
```csharp
SecurityGrade grade = Classification.Grade(pawn); // the verdict
float score = Classification.Risk(pawn); // the raw 0..~3 number
```
Both are pure reads — `Risk` uses `GameComponent_CriminalRecords.PeekFor` and never creates a record.
`SecurityGrade` is an ordered enum (`Minimum < Medium < Maximum < Supermax`), so `grade <= Medium`
comparisons work as written. The type lives in the `Contraband` namespace (Justice was extracted from
Institution: Contraband and kept the shared namespace).
---
*Part of the **Institution** suite for RimWorld 1.6: Core · Contraband · Justice · Gangs. Developed
with substantial assistance from Claude (Anthropic).*
+168
View File
@@ -0,0 +1,168 @@
# Deterrence — the feedback loop
*The connection that turns the pipeline into a cycle.*
This is the module that makes policing **govern** rather than merely clean up. Everything else in
Justice reacts to one pawn: this reactor's *state* is the whole colony, and it feeds **back** into
every pawn's disposition. Crime that goes unanswered emboldens everyone a little; visible justice
deters everyone a little. The reason to punish is not just this prisoner — it is the message it sends
the rest, and here that message is a number they all read.
---
## The climate of order
Every map carries a `MapComponent_Deterrence` holding one value:
| | |
|---|---|
| **Range** | `0.0` (lawless) … `1.0` (iron) |
| **Baseline** | `0.5` — an ordinary colony |
| **Below baseline** | crime pays; dispositions run **hot** |
| **Above baseline** | order holds; dispositions run **cool** |
It is nudged by two events and, left alone, forgets.
### The nudges
| Event | Source | Nudge | Written by |
|---|---|---|---|
| A crime, unanswered | `Justice.RecordCrime(perp)` | **−0.08** | Justice, Gangs (fights) |
| Visible justice done | `Justice.RecordPunishment(map)` | **+0.12** | Justice (`Discipline.Punish`) |
Both clamp the result to `[0, 1]`. Note the asymmetry: **punishment (+0.12) outweighs crime (−0.08)
by 1.5×.** A colony that answers every offence does not merely break even — it ratchets *above*
baseline into "order holds" territory. This is the deterrence dividend, and it is deliberate: justice
seen to be done buys more order than the crime cost.
`RecordCrime` also writes the record — it increments `crimesCommitted` and stamps `lastCrimeTick` on
the perpetrator — so the same event that moves the climate is the event classification and parole
later see. `RecordPunishment` moves only the climate; the *record* side of punishment (the reform
shift) is [Discipline](Discipline-and-Reform.md)'s job.
### The drift
```
every 2500 ticks: level = MoveTowards(level, 0.5, 0.0015)
```
Memory fades. With nothing happening, the climate creeps back toward ordinary at **0.0015 per step,
one step every 2500 ticks** — which works out to:
```
drift per in-game day = (60000 / 2500) * 0.0015 = 24 * 0.0015 = 0.036 / day
```
So a single unanswered crime (`−0.08`) takes roughly `0.08 / 0.036 ≈ 2.2 days` to heal on its own —
or one punishment (`+0.12`) to over-answer instantly. An iron colony you stop maintaining slides back
to baseline over about two weeks; it does not stay stern for free.
---
## The feedback — how it reaches every pawn
This is the loop. The climate is read back into `Propensity.Nurture` (in Core) as a multiplier:
```
DeterrenceFactor(pawn) = Lerp(1.3, 0.7, level) // = 1.3 - 0.6 * level
Nurture(pawn) = ... * DeterrenceFactor(pawn)
```
| `level` | Factor | Effect on every pawn's disposition |
|---|---|---|
| 0.00 (lawless) | **1.300** | +30% hotter — crime pays, everyone feels it |
| 0.30 | 1.120 | +12% |
| **0.50 (baseline)** | **1.000** | **neutral — a default colony sits exactly here** |
| 0.70 | 0.880 | −12% |
| 0.90 | 0.760 | −24% |
| 1.00 (iron) | **0.700** | −30% cooler — order holds, everyone calms |
Two things to notice:
- **Baseline is the neutral point, by design.** A default colony sits at `0.5`, which is a factor of
exactly `1.0` — ordinary order neither inflames nor calms. That symmetry is deliberate. If baseline
ran hot (as an earlier tuning did), ordinary order would nudge propensity up, breed a little more
crime, drop order, and feed a slow runaway. Neutral-at-baseline means only a colony that lets order
*slide below* ordinary earns the hot multiplier, and only one that pushes *above* it earns the calm.
- **The swing is `0.7×` to `1.3×`, symmetric around `1.0`** — a `±30%` spread applied to *everyone's*
nurture at once. This is the multiplier that makes catching one pawn matter to the disposition of
the rest.
### JusticeBootstrap — reconnecting the loop across the mod boundary
Core keeps `Propensity.DeterrenceFactor` as a neutral seam — a `Func<Pawn,float>` that returns `1f`
until something fills it. Core alone never has to know Justice exists; that one seam is what lets Core
stay a dependency-free leaf while the deterrence loop runs *through* it.
At startup, `JusticeBootstrap` fills the seam:
```csharp
Propensity.DeterrenceFactor = pawn => {
var d = pawn?.Map?.GetComponent<MapComponent_Deterrence>();
return d != null ? Mathf.Lerp(1.3f, 0.7f, d.Level) : 1f; // neutral (1.0) at baseline 0.5
};
```
So **only when Justice is installed** does the climate bend disposition; without it, Core reads a flat
`1.0` and pawns are indifferent to order. This is the deterrence feedback loop, reconnected across the
mod boundary without Core ever depending on the justice layer. (If the pawn is off-map — caravan, in
transit — there is no map component, and the factor falls back to a neutral `1.0`.)
---
## Worked example — a day at the institution
The colony starts at baseline `0.5` (factor `1.05`). Over one day:
| Step | Event | `level` | Factor |
|---|---|---|---|
| start | — | 0.50 | 1.050 |
| 1 | Prisoner A shanks a guard (crime, unanswered) | 0.42 | 1.106 |
| 2 | Prisoner B foments trouble (crime, unanswered) | 0.34 | 1.162 |
| 3 | Warden punishes A (`RecordPunishment`) | 0.46 | 1.078 |
| 4 | Warden punishes B (`RecordPunishment`) | 0.58 | 0.994 |
| overnight | nothing happens, drift ≈ −0.036 toward 0.5 | 0.544 | 1.019 |
Two crimes cost `−0.16`; two punishments returned `+0.24`. The colony ends the day at `0.58` —
**above** where it started — because punishment out-answers crime. Every pawn's disposition dipped
hotter as the crimes landed (`1.05 → 1.16`) and then cooled below neutral once order was reasserted
(`0.994`). By morning the drift has begun erasing the gain, and if nothing keeps the pressure on, the
colony sinks back to its slightly-hot baseline over the next couple of weeks.
That arc — hot when crime pays, cool when justice is seen, forgetful when neither happens — is the
whole reason discipline is not inert. You are not punishing a pawn; you are setting the temperature of
the room.
---
## How to play with deterrence
- **Answer crime, visibly and promptly.** Each punishment is worth 1.5 crimes of order. A colony that
reliably punishes climbs *above* baseline and cools everyone; a colony that lets offences slide
sinks below and heats everyone — a spiral, because hotter pawns commit more crime.
- **Don't coast on a past crackdown.** The climate drifts back to baseline at `0.036/day`. Order is a
maintenance cost, not a one-time purchase.
- **Push past baseline if you want calm.** Neutral disposition needs `level ≈ 0.571`; ordinary
(`0.5`) still runs 5% hot. A steady rhythm of caught-and-punished offences is what holds you there.
- **A lawless spell is self-feeding.** At `level 0.3` every pawn is `1.19×` more disposed; more crime
follows, driving the level lower still. Break the cycle with punishments, not patience.
---
## For modders
```csharp
Justice.RecordCrime(perpetrator); // -0.08 climate, +1 crime, stamps lastCrimeTick
Justice.RecordPunishment(map); // +0.12 climate
float level = map.GetComponent<MapComponent_Deterrence>().Level; // raw 0..1
```
`RecordCrime` is the writer any module calls when a pawn does something the colony would police —
Institution: Gangs calls it on rival fights, for instance. `RecordPunishment` is called by
`Discipline.Punish`. The component and statics live in the `Contraband` namespace (shared across the
suite since Justice's extraction from Contraband).
---
*Part of the **Institution** suite for RimWorld 1.6: Core · Contraband · Justice · Gangs. Developed
with substantial assistance from Claude (Anthropic).*
+142
View File
@@ -0,0 +1,142 @@
# Discipline & Reform
*Punishment as an action, with a self-complete payoff — and the seat of institutionalization.*
Punishment in Justice does two things at once, on two different axes:
1. **It deters the colony.** Order seen to be done raises the climate of order for everyone —
`RecordPunishment`, `+0.12`. See [Deterrence](Deterrence.md).
2. **It reshapes the punished.** A harsh hand *prisonizes* — the pawn hardens, `reform` falls, their
disposition climbs for good. A corrective hand *rehabilitates* — `reform` rises, their disposition
cools for good.
Both effects are Justice's own, so discipline is never inert. Prisoner Realism owns the passive *toll*
of isolation (a good sim of how solitary *feels*); Justice owns the *order* it produces and the
persistent *shift* it leaves on the pawn. When both mods are present, PR's solitary deterioration
deepens the felt experience on top — it does not replace this.
---
## The two hands
```
Discipline.Punish(pawn, harsh):
Justice.RecordPunishment(pawn.Map) // +0.12 colony climate (Deterrence)
reform = Clamp(reform + (harsh ? -0.15 : +0.10), -1, +1) // the persistent shift
```
| Call | Reform delta | Meaning |
|---|---|---|
| `Punish(pawn, harsh: true)` | **−0.15** | Beatings, deprivation, the hard hand. Hardens — "prisonization." |
| `Punish(pawn, harsh: false)` | **+0.10** | Correction, structure, rewarded good conduct. Rehabilitates. |
`reform` lives on the shared record, clamped to `[−1, +1]`:
| `reform` | Reading |
|---|---|
| `+1.0` | Fully rehabilitated |
| `0.0` | Untouched — as they came in |
| `−1.0` | Fully hardened, institutionalized |
Note the asymmetry the other way from Deterrence: **harsh (−0.15) moves faster than gentle (+0.10).**
It takes three corrective acts to build `+0.30` of reform; two harsh acts to tear down `−0.30`. Damage
is quicker than repair — as it should be.
---
## How reform becomes disposition
Reform is not a status effect or a mood buff. It is read straight back into `Propensity.Nurture` (in
Core), where it multiplies a pawn's whole disposition:
```
if reform != 0:
Nurture *= Clamp(1 - reform * 0.5, 0.4, 2)
```
| `reform` | Nurture factor | Effect |
|---|---|---|
| −1.00 (hardened) | **1.500** | +50% more disposed — permanently |
| −0.30 | 1.150 | +15% |
| −0.15 (one harsh act) | 1.075 | +7.5% |
| 0.00 | 1.000 | untouched |
| +0.10 (one gentle act) | 0.950 | −5% |
| +0.30 (parole threshold) | 0.850 | −15% |
| +1.00 (rehabilitated) | **0.500** | −50% less disposed — permanently |
Given reform is clamped to `[−1, +1]`, the factor spans `0.5×` to `1.5×`; the `0.4`/`2` clamp is a
safety rail that reform alone never reaches. **This is the whole trick.** Institutionalization and
recidivism are not a parallel mechanic bolted on the side — they are *one number the propensity engine
already reads*. A hardened pawn is not flagged "recidivist"; they simply carry a `−reform` that makes
every "would they?" roll for the rest of their life come up hot. Discipline and Parole are what *move*
reform; Nurture only *reads* it — which is how institutionalization becomes Justice's without a second
system.
---
## Worked example — two sentences
Take a fresh prisoner, `reform = 0`, badly kept: mood `0.30`, held. From Core, before reform, their
Nurture is already `1 × 1.6 (mood) × 1.4 (held & unhappy) = 2.24`, and say the colony sits at baseline
so `DeterrenceFactor = 1.05`, giving Nurture `≈ 2.35`.
**The hard hand.** You beat them into line — three harsh punishments:
```
reform: 0 → -0.15 → -0.30 → -0.45
Nurture factor at reform -0.45 = 1 - (-0.45 * 0.5) = 1.225
```
Their disposition is now `2.35 × 1.225 ≈ 2.88` — **+22% hotter than when they arrived**, forever,
independent of mood. You have made them more criminal, not less. This is prisonization: the sentence
itself became the cause. (You did buy `+0.36` of colony climate along the way — order today, at the
cost of the pawn's tomorrow.)
**The corrective hand.** Instead you structure and reward — three gentle punishments *and* you fix
their conditions (recreation, decent cell) so mood recovers to `0.55`:
```
reform: 0 → +0.10 → +0.20 → +0.30
mood 0.55 → no mood multiplier, not held-&-unhappy → those factors drop out
Nurture factor at reform +0.30 = 1 - (0.30 * 0.5) = 0.85
```
Now Nurture is roughly `1 × 0.85 × 1.05 (deterrence) ≈ 0.89` — **below neutral.** The same pawn who
would have been `2.88` disposed is now `0.89`. And at `reform = 0.30` with a low enough grade they
have crossed the [Parole](Parole.md) threshold. Same institution, opposite outcomes — decided by which
hand you used.
---
## How to play with discipline
- **Harsh is a lever, not a punishment button.** It buys colony order *now* (`+0.12` climate) at the
price of the pawn's disposition *forever* (`−0.15` reform → hotter Nurture). Use it when you need the
deterrence and never intend to release the pawn — a Supermax lifer you are only ever going to hold.
- **Gentle is how anyone gets out.** Parole needs `reform ≥ 0.30`; only the corrective hand builds it.
Three gentle acts is the floor.
- **Damage compounds; repair is slow.** `−0.15` vs `+0.10` means a pawn you brutalize early is
expensive to bring back — and a hardened pawn commits more crime, which sinks the climate, which
makes *everyone* worse. Don't harden pawns you might want later.
- **Conditions and discipline stack.** Reform shifts disposition permanently; mood, neglect and
deterrence shift it live (see [Regime](Regime.md) and [Deterrence](Deterrence.md)). A reformed pawn
in a well-run wing is calm on two axes at once.
---
## For modders
```csharp
Discipline.Punish(pawn, harsh: true); // -0.15 reform, +0.12 climate
Discipline.Punish(pawn, harsh: false); // +0.10 reform, +0.12 climate
```
`Punish` uses `GameComponent_CriminalRecords.For` (creates the record if absent) and clamps `reform`
to `[−1, +1]`. It always fires `RecordPunishment` on the pawn's map, so *any* act of discipline moves
the colony climate regardless of which hand you use — the colony sees order done, whoever it was done
to. The class lives in the `Contraband` namespace (shared since the extraction from Contraband).
---
*Part of the **Institution** suite for RimWorld 1.6: Core · Contraband · Justice · Gangs. Developed
with substantial assistance from Claude (Anthropic).*
+96
View File
@@ -0,0 +1,96 @@
# Institution: Justice
*What the institution **does** about crime — the corrections and policing layer of the **Institution**
suite of RimWorld 1.6 mods.*
Vanilla RimWorld keeps a rap sheet no one reads. You capture a raider, warden them, and the game
promptly forgets they ever tried to knife a guard. Prisoners are interchangeable mood-boxes; a
"maximum security" wing is a decoration you build by hand and the sim never acknowledges. There is a
record, and there are no consequences.
**Justice turns the record into consequences, and closes the loop.** It grades every held pawn from
what they have actually done, gives the colony a *climate of order* that crime and punishment move,
feeds that climate back into every pawn's disposition, moves a *reform* score that decides whether a
sentence hardens or heals, gates release on it, and gives prisoners the recreation need vanilla flatly
denies them. The through-line is one sentence:
> **Catching and punishing one pawn changes the disposition of the rest.**
That is the difference between policing that *cleans up* and policing that **governs**. The reason to
punish an offender is not only this offender — it is the message it sends everyone else who is quietly
deciding, on a seeded roll, whether crime pays here.
---
## The one shared record
Justice never keeps its own per-pawn crime state. It reads and writes the single `CriminalRecord` that
**Institution: Core** holds for every pawn — the same record the search, the escape, the brewing, and
the gang systems write to. That is the whole point of the substrate: classification, deterrence,
discipline and parole all agree on how dangerous a pawn is, because they all read the same numbers.
| Field | What it counts | Written by |
|---|---|---|
| `crimesCommitted` | offences on the record | Justice (`RecordCrime`), Gangs (fights) |
| `escapeAttempts` | proven flight risk | Contraband (escape/tunnel) |
| `contrabandMade` | shivs, brews, tools made | Contraband |
| `timesSearched` | searches endured | Contraband (warden search) |
| `timesCaught` | searches that found something | Contraband |
| `lastCrimeTick` | when they last offended | Justice (`RecordCrime`) |
| `reform` | −1 hardened … +1 rehabilitated | Justice (`Discipline`, `Parole`) |
| `pardoned` | released after genuine reform | Justice (`Parole`) |
Justice authors the **crime / punishment / reform / pardon** columns; the counters of *what physically
happened* are fed by its siblings. It grades and gates over all of them.
---
## The five modules
| Module | What it does | Page |
|---|---|---|
| **Classification** | Grades held pawns `Minimum`..`Supermax` from a risk score over the record. | [Classification](Classification.md) |
| **Deterrence** | The colony's climate of order, moved by crime and punishment, drifting to baseline, read back into propensity. | [Deterrence](Deterrence.md) |
| **Discipline & Reform** | Punishment moves the reform score — harden vs. rehabilitate. The seat of institutionalization and recidivism. | [Discipline & Reform](Discipline-and-Reform.md) |
| **Parole** | Release when reform, grade, and pardon status line up. | [Parole](Parole.md) |
| **Regime** | Enables the prisoner recreation (Joy) need via a Harmony postfix, idempotent with Prisoner Recreation. | [Regime](Regime.md) |
Threaded through all five is one small piece of glue, **`JusticeBootstrap`**, which at load fills the
neutral deterrence seam Core leaves open — reconnecting the feedback loop across the mod boundary
without Core ever depending on Justice. See [Deterrence](Deterrence.md).
---
## Policing — the pre-arrest half (shipped)
Everything above is the **post-arrest** side — corrections. The **pre-arrest** side now ships too:
colonists commit crimes on the propensity spectrum, witnesses and a constable's work name the culprit,
and a weighed arrest can end in a cell or a resisted breakout — plus prison riots and a recidivism
alert. Deterrence models *the consequence of visible justice*; policing is the machinery that produces
crime to answer. It has its own settings toggle (on by default). Full detail in **[Policing](Policing.md)**.
---
## Requirements & load order
- **RimWorld 1.6**
- **Institution: Core** — required. The propensity/record engine Justice grades against.
- **Harmony** — required. Regime's prisoner-recreation patch is the suite's only Harmony-using code.
- Load **after** Core and Harmony.
Each mod in the suite stands alone as an install; together they form one system. Justice is useful
with Core alone, and richer with **Institution: Contraband** (which feeds the escape/search/contraband
counters) and **Institution: Gangs** (whose fights record crimes).
---
## The suite
Part of the **Institution** suite: **Core** (the propensity + record engine) · **Contraband**
(concealment, search, escape) · **Justice** (this mod) · **Gangs** (contraband economies). The
standalone **Foul Play** framework bridges into Core and Contraband when present.
---
*Part of the **Institution** suite for RimWorld 1.6: Core · Contraband · Justice · Gangs. Developed
with substantial assistance from Claude (Anthropic).*
+150
View File
@@ -0,0 +1,150 @@
# Parole
*Release, driven by what the pawn has become.*
Parole is the end of the arc, and it is deliberately a *decision*, not a mechanic that rebuilds the
game's release flow. It asks one question — **is this pawn ready?** — and answers it from two things
already on the record: the reform the sentence earned (or failed to earn), and the grade the record
warrants. Both are self-complete reads; nothing here re-simulates a pawn's future.
> The *return* outcome — a grateful ally, a vengeful raider — is Prisoner Realism's Recidivism, a good
> sim Justice does not rebuild. Justice's mirror is the parolee who **stays** and may reoffend,
> carrying their record into the colony (a Justice follow-up).
---
## The gate
```
CanRelease(pawn):
rec = record for pawn (peek, no create)
if rec == null: return true // nothing on record to hold them
if rec.pardoned: return false // already released
return rec.reform >= 0.30
&& Classification.Grade(pawn) <= SecurityGrade.Medium
```
Three conditions, all of which must hold:
| Condition | Threshold | Why |
|---|---|---|
| Not already pardoned | `pardoned == false` | Release is a one-way flag; you can't re-parole. |
| Genuinely reformed | `reform ≥ 0.30` | Proof the sentence *healed* rather than hardened. Three corrective acts minimum. |
| Low enough grade | `Grade ≤ Medium` (risk `< 0.8`) | Reform of attitude is not enough — the *record* must have cooled to something you'd let out. |
A pawn with **no record at all** is releasable trivially (there is nothing to hold them). Everyone
else has to earn all three.
The grade gate and the reform gate interact in a way worth internalizing. Because reform subtracts at
most `0.4` from risk ([Classification](Classification.md)), a pawn whose reform-*less* risk is `≥ 1.2`
can never reach Medium — so **no amount of reform will ever parole a prolific offender.** Parole is for
pawns whose record stayed shallow enough that treatment can pull them back under the ceiling. Let a
pawn rack up escapes and catches and you have quietly converted them into a lifer, whatever their
attitude.
---
## Release
```
Release(pawn):
record.pardoned = true
```
`Release` sets one flag: `pardoned`. Vanilla does the actual freeing; the flag is Justice's memory
that this pawn was let out after genuine reform, so reintegration can grant a clean slate and
`CanRelease` won't offer them twice. A modest faction-goodwill / relationship nudge for a well-handled
release is the immediate payoff, wired when the release job lands.
---
## The whole arc
The pawn moves through every Justice module in turn, and each one writes the record the next one reads:
| Stage | Module | What moves | Record touched |
|---|---|---|---|
| 1. Crime | [Deterrence](Deterrence.md) | Climate `−0.08`; the colony runs hot | `crimesCommitted++`, `lastCrimeTick` |
| 2. Caught & held | [Classification](Classification.md) | A grade is assigned from the record | reads all counters |
| 3. Discipline | [Discipline](Discipline-and-Reform.md) | `reform` moves ±; climate `+0.12` | `reform` |
| 4. Reform | [Discipline](Discipline-and-Reform.md) → Core | Disposition cools as reform rises | `reform` (read by Nurture) |
| 5. Regrade | [Classification](Classification.md) | Reform subtracts risk; grade falls | reads `reform` |
| 6. **Parole** | this page | Gate opens when reform ≥ 0.30 **and** grade ≤ Medium | reads `reform`, `pardoned`; sets `pardoned` |
It is a loop, not a line: the crime that opens stage 1 also cools the colony's climate, which the *next*
pawn's disposition reads — so one pawn's whole sentence is faintly present in every other pawn's odds.
---
## Worked example — a sentence that ends in release
**Cass**, a Kind pickpocket. Nature `= clamp01(0.05 − 0.30) = 0`. She arrives with one crime, caught
once, no escapes, no contraband, `reform = 0`.
```
Grade at intake:
risk = 0 + 0 + (1*0.20) + (1*0.15) + 0 - 0 = 0.35 → MEDIUM
CanRelease? reform 0 < 0.30 → NO
```
She is already Medium-or-below, so the *grade* gate is met — but her `reform` is `0`. You give her the
corrective hand three times (structure, rewarded conduct), and fix her cell so she isn't stewing:
```
reform: 0 → +0.10 → +0.20 → +0.30
Grade now: risk = 0.35 - (0.30 * 0.4) = 0.23 → MINIMUM
CanRelease? reform 0.30 ≥ 0.30 AND Minimum ≤ Medium AND not pardoned → YES
```
`Release(Cass)` sets `pardoned = true`. She walks. Contrast **Bram** from the Classification page,
whose reform-less risk of `1.55` keeps him at Maximum even at `reform = 1.0` — `CanRelease` returns
`false` for Bram forever, because the grade gate never opens. Two prisoners, the same treatment, and
the record decides which one you can ever let go.
---
## A note on record cleanup
A pawn whose record is `IsBlank` — no crimes, no escapes, no contraband, no catches, `reform == 0`,
not pardoned — is dropped on save/load (Core prunes blank and dead-pawn records). A *pardoned* pawn is
therefore **not** blank and their record persists: the pardon is remembered. This is why a released
parolee who stays and reoffends still carries their history — Justice does not forget that they were
once let out.
---
## Ideology-flavoured thresholds
Precepts layer on top of this baseline, not under it: an execution-favouring ideoligion paroles
rarely, a lenient one readily. `CanRelease` is the substrate-native baseline those precepts modulate —
the floor beneath the flavour, so the decision is coherent whether or not Ideology is installed.
---
## How to play with parole
- **Parole is earned, not granted.** You cannot release your way out of a deep record. Decide early
whether a pawn is a keep-or-release case and treat (gently) the ones you mean to free.
- **`reform ≥ 0.30` is three gentle acts, minimum.** Nothing faster reaches it. Start early.
- **Keep the record shallow if you want the option.** Every escape (`+0.35`) and catch (`+0.15`) you
let accumulate pushes a pawn toward the unparoleable side of the `1.2` line.
- **A parolee who stays is a Justice case, not a farewell.** They carry their record; if conditions
sour, their disposition (still reading their history) can put them back on it.
---
## For modders
```csharp
if (Parole.CanRelease(pawn)) // pure read: reform, grade, pardon
Parole.Release(pawn); // sets pardoned = true
```
`CanRelease` peeks (never creates a record) and treats a missing record as releasable. `Release` uses
`For` (creates if absent) and is idempotent — a second call is a no-op once `pardoned` is set. Both
live in the `Contraband` namespace (shared since Justice's extraction from Contraband).
---
*Part of the **Institution** suite for RimWorld 1.6: Core · Contraband · Justice · Gangs. Developed
with substantial assistance from Claude (Anthropic).*
+120
View File
@@ -0,0 +1,120 @@
# Policing — the pre-arrest half
*The other half of the loop. Where the rest of Justice deals with people the colony has already
caught, **policing** is what happens before the cell door: your own colonists commit crimes on the
propensity spectrum, the colony investigates, and — sometimes — arrests.*
It is a big behavioural change (your colonists become potential criminals), so it has **its own
toggle** in the mod settings, on by default. Turn it off to run the rest of Justice — classification,
deterrence, discipline, parole, regime — without colony crime.
---
## The loop
```
[crime] → [witnessed / investigated] → [weighed arrest (resist?)] → prison → … → parole → [recidivism]
```
Every stage reads the shared engine (`Propensity`, the `CriminalRecord`, the deterrence meter), so
policing is not a bolt-on — it feeds the same record classification grades on and discipline reforms.
## Crime — and why small colonies stay honest
Every ~40 seconds, each free colonist gets a seeded roll to offend. The base rate is low (`0.06`) and
multiplied hard by nature × nurture — almost nobody with an ordinary disposition ever does anything.
But it is *also* multiplied by **crime cover**:
| Free colonists | Crime cover |
|---|---|
| ≤ 3 | 0.15 |
| ~8 | ~0.6 |
| ≥ 13 | 1.0 |
In a tight 2–3 pawn colony everyone knows everyone and retribution is certain even without a jail, so
crime is **rare**; a larger, more anonymous colony offers cover. This is a deliberate inversion of the
naive "no enforcement → more crime": a small colony is kept honest by its own closeness, not by a
constable.
A crime is one of **theft**, **vandalism**, or **assault** — leaning on who the culprit is (a
kleptomaniac steals, a bloodlusty pawn assaults, a pyromaniac vandalizes).
## Consequences — the stakes
A crime leaves a mark the colony can *see* (all wrapped defensively — a consequence that finds no
target never breaks anything):
- **Theft** lifts a small stack of the colony's goods and the thief **pockets it**. RimWorld has no
per-person property (stockpiles are communal), so theft is theft from the common store — but the
culprit now physically carries what they took, a natural evidence trail a search can pick up.
- **Vandalism** damages a nearby colony building.
- **Assault** gives a nearby colonist a light, down-safe knock — and the culprit chooses their mark
**wisely**: someone they resent (motive), who is weak (little retribution), and unwatched (few
witnesses).
- **Relationships fray.** An assault victim thinks much worse of their attacker immediately (they saw
who did it); and when investigation *names* a culprit, the whole colony's opinion of them drops.
- The colony is **notified** a crime happened — culprit unknown.
## Witnesses and investigation
A crime is not equally solvable. Colonists who were near enough to **see** it are witnesses, and each
is a partial lead — a witnessed crime starts up to `0.8` of the way to solved before anyone lifts a
finger; an unseen one is a **cold case** worked from nothing.
Investigation then accrues evidence toward `1.0` (naming the culprit) two ways:
- **Ambient**, scaled by how many colonists the colony can spare (`~0.34` per check per unit of
effort — a few investigated windows).
- **The police work type.** Assign a colonist to *policing* and they walk to open **crime scenes** and
work them, adding a chunk of evidence scaled by their Intellectual + Social skill. A sharp constable
cracks cases fast; a colony that spares no one lets them go cold.
## The weighed arrest
A solved case does **not** mean an automatic arrest. Fellow-colonist cops will not gut the colony to
jail someone over a petty crime — the arrest is a cost/benefit (`ShouldArrest`):
```
arrest ⇔ colony can afford a prison AND severity ≥ 1.5 + value × 3
```
- **Severity** — the crime's kind (theft 1 · vandalism 1.5 · assault 2.5), escalated by the culprit's
record (a rap sheet and prior escapes compound).
- **Value** — 0..1 from the culprit's best skills. A star colonist raises the bar to ~4.5; an
expendable one leaves it at ~1.5.
- **Capacity** — **below ~5 colonists, or with no prison bed, the colony arrests no one.** A prisoner
plus a warden would cripple a small colony. Capacity ramps up as the colony grows.
So a petty crime by your only doctor → they walk. A murder → arrested regardless. And a crime that
paid, unanswered, erodes the climate of order a little more (crime emboldens).
## Resisting arrest
The disposed do not go quietly (`WouldResist`, seeded nature × nurture):
- **Comply** → taken in cleanly (they become a prisoner of the colony — imprisonment is direct, and it
survives guest-management mods like Hospitality).
- **Resist** → the violent turn **berserk**, the rest **flee**. A botched arrest becomes a crisis the
colony must handle the hard way, and openly defying arrest emboldens the lawless.
- **Subdued** → a resister the colony beats down is then **imprisoned** (auto-capture) — the resist
path resolves to a cell once the colony wins the confrontation. One who flees the map gets away.
## Prison riots
When the climate of order **collapses** (deterrence at the floor) in a colony holding prisoners, a
neglected prison boils over into a **riot**: disposed prisoners turn violent together, and the unrest
shatters order further. A well-run colony (high deterrence) never sees one — a riot is the thing a
badly-run institution *earns*. It ties the deterrence meter, the propensity engine, and the prison
population into one emergent event.
## Recidivism, made visible
The loop closes on itself: a pawn hardened in prison (negative reform) runs a higher propensity and
reoffends. When investigation names a culprit the colony had **paroled**, it says so out loud — the
recidivism loop turning in the open, the payoff for everything classification, discipline, and parole
did upstream.
---
Part of **Institution: Justice**, in the Institution suite. AI disclosure: developed with substantial
assistance from Claude (Anthropic).
+131
View File
@@ -0,0 +1,131 @@
# Regime
*The daily life of the held — and the one lever vanilla withholds.*
Most of what a prison regime needs, vanilla already gives you. The schedule is the Restrict tab. The
mood-driven disposition already flows through `Propensity.Nurture` — a neglected prisoner runs hotter
with no new machinery. So Regime does not rebuild any of that. It adds the single thing vanilla flatly
refuses to model: **recreation.**
---
## What vanilla does, and what Regime flips
Vanilla denies prisoners the **Joy** (recreation) need outright — the need is flagged `colonistsOnly`
and `neverOnPrisoner`, so it simply never appears on a prisoner. A caged pawn has no recreation bar,
cannot take joy from anything, and never suffers for its absence.
Regime flips exactly that one bit on, via a small Harmony postfix — the same approach the good, popular
**Prisoner Recreation** mod takes:
```csharp
[HarmonyPatch(typeof(Pawn_NeedsTracker), "ShouldHaveNeed")]
static class Patch_PrisonerRecreation
{
static void Postfix(NeedDef nd, Pawn ___pawn, ref bool __result)
{
if (__result || nd?.defName != "Joy") return; // only touch Joy, only when vanilla said no
if (___pawn?.RaceProps?.Humanlike == true
&& ___pawn.IsPrisonerOfColony)
__result = true; // humanlike prisoners get the need
}
}
```
Three guards, in order:
1. **`__result` already true** — vanilla or another patch already granted the need. Do nothing. This
is what makes the patch *additive*: it only ever turns a `false` into `true`, never the reverse.
2. **`nd.defName != "Joy"`** — this patch is about recreation and nothing else. Every other need is
left exactly as vanilla decided.
3. **humanlike prisoner-of-colony** — animals, guests, slaves and colonists are untouched; only a
humanlike prisoner of *your* colony gains the need.
That is the entire mechanism. Enabling recreation is trivial — once the need exists, vanilla does all
the rest (joy sources, the recreation bar, the mood effects of a full or empty one). The *value* is
not the plumbing; it is what an empty bar now costs.
---
## Why a need you can neglect is the point
Giving prisoners recreation is not a mercy toggle — it is a **lever with two ends**, and the down end
is the one that matters to the suite. Once a prisoner *has* the Joy need, they can be **starved** of
it, and vanilla's own machinery then does the work:
```
no recreation → Joy falls → mood falls → Propensity.Nurture rises → higher propensity
```
Every one of those arrows already exists. Regime only opens the first door; mood is wired to
disposition in Core, so a prisoner who gets no yard time stews, their mood sinks, and their Nurture —
and with it their odds on every "would they?" roll — climbs. From [Deterrence](Deterrence.md) and
[Discipline & Reform](Discipline-and-Reform.md) you already know Nurture is the multiplier the whole
system keys off; Regime is what lets *neglect* feed it.
Concretely, in Core's `Nurture`:
| Prisoner condition | Nurture contribution |
|---|---|
| Mood `< 0.20` (recreation-starved, at the floor of despair) | `× 2.5` |
| Mood `< 0.35` (badly kept) | `× 1.6` |
| Held **and** mood `< 0.40` (a badly run cell) | additional `× 1.4` |
A prisoner you give no recreation is a prisoner you are pushing up those brackets. Regime turns "I
didn't bother building a rec room" from a non-event into a disposition cost you pay on every roll.
---
## Idempotent with Prisoner Recreation
If you also run the **Prisoner Recreation** mod, nothing breaks. Both mods do the same thing — force
`ShouldHaveNeed` to return `true` for prisoner Joy — and because Regime's postfix only acts when
`__result` is still `false`, the two are simply *two postfixes that force the same `true`*. Whichever
runs first grants the need; the second sees it already granted and returns immediately. Running both is
harmless. Running either alone is sufficient. There is no double-need, no conflict, no load-order
sensitivity between them.
Regime announces itself at startup so you can confirm it loaded:
```
[Institution: Justice] Regime: prisoner recreation enabled.
```
---
## Content on top
Yard time, safety needs, and visitation are *content* on this foundation, not new machinery. Each of
them is just another thing that moves a prisoner's mood — and mood is already wired to disposition — so
they need only defs and jobs, not new systems. When they land, they will make a well-run regime feel
richer; the mechanical spine is already here in this one postfix.
---
## How to play with Regime
- **Build the rec room.** A prisoner with recreation settles; the mood brackets above stay off, and
their disposition sits near baseline. A cheap horseshoe pin or chess table earns its keep in crimes
that don't happen.
- **Neglect is a choice with a number.** Leaving prisoners with nothing to do isn't neutral — it drives
mood into the `×1.6` and `×2.5` Nurture brackets, and a hotter prisoner reoffends, which cools your
colony's climate of order, which heats *everyone*. The rec room is deterrence you build once.
- **Recreation and reform stack.** Regime cools disposition *live* (through mood); reform cools it
*permanently* (through [Discipline & Reform](Discipline-and-Reform.md)). A reformed prisoner in a
well-run wing is calm on both axes — which is exactly the pawn you can eventually [parole](Parole.md).
- **You don't need Prisoner Recreation too** — but if you have it, keep it. They coexist.
---
## For modders
Harmony ID `flan.institution.justice`, a single postfix on `Pawn_NeedsTracker.ShouldHaveNeed`. It is
the suite's *only* Harmony-using code — Contraband dropped its Harmony dependency after Justice was
split out. The patch class lives in the `Contraband` namespace (shared across the suite since the
extraction). The postfix is strictly additive (`false → true` only), so it is safe to stack with any
other mod that grants prisoner needs.
---
*Part of the **Institution** suite for RimWorld 1.6: Core · Contraband · Justice · Gangs. Developed
with substantial assistance from Claude (Anthropic).*
+316
View File
@@ -0,0 +1,316 @@
# Commitment
How involuntary psychiatric commitment actually works in play: how a colonist becomes a ward patient,
what the treatment station does, how a room turns into a ward, who does the counselling, what the
treatment buys, and what happens if you commit someone and then forget about them.
Every def and class name below is the real one from the mod source. The `Ward_` prefix marks a
def; the C# class names are the workers behind them.
---
## The loop in one paragraph
You **arrest** a colonist (or otherwise take a prisoner) and set their interaction mode to
**psychiatric care**. You build a **treatment station** in their cell, which turns the room into a
**psychiatric ward**. A warden walks over, sits, and **counsels** them; each session adds a little
`Ward_UnderTreatment` severity, which **lowers their mental-break threshold**. That severity
**decays** at 0.15/day, so the effect fades unless wardens keep attending. Counselling also gives a
small mood lift (`Ward_Counselled`). Neglect a committed patient — leave them with no active treatment
hediff — and once a day they gain `Ward_Neglected`, a mood *penalty* that drags them back toward the
break that got them committed. Treat them and they stabilise; forget them and they don't. That
feedback loop is the entire mod.
---
## Step 1 — Becoming a ward patient
Commitment reuses vanilla's arrest → prisoner transition. You don't invent a new pawn state; you take
a prisoner (arrest your own colonist, or a raider) and then flip on a **non-exclusive interaction
mode**.
### `Ward_PsychiatricCare` — the interaction mode
| Field | Value | Why |
|---|---|---|
| Def type | `PrisonerInteractionModeDef` | A Def, not the compiled `GuestStatus` enum — that's the whole point |
| `defName` | `Ward_PsychiatricCare` | |
| `label` | "psychiatric care" | Shown in the prisoner's interaction dropdown |
| `listOrder` | `150` | Sits between vanilla's `ReduceResistance` (100) and `Release` (200) |
| `isNonExclusiveInteraction` | `true` | **Stacks** with recruit/convert/work instead of stealing the radio button |
| `mustBeAwake` | `false` | You can flag a downed or sleeping patient for care; the *work giver* decides when to actually treat |
| `allowOnWildMan` | `false` | A wild man isn't a commitment case |
| `allowInClassicIdeoMode` | `true` | Available without Ideology's ideoligion system |
**Non-exclusive is load-bearing.** Vanilla's recruit/convert/enslave/release/execute modes are
mutually exclusive — you pick one from a radio group. Psychiatric care is modelled on vanilla's own
*Bloodfeed* and *Study* modes, which are toggles layered *on top* of the exclusive choice. So a
patient can be set to **recruit AND psychiatric care at once**: you counsel the sad colonist toward
stability while also chipping at their resistance. It also means Ward doesn't have to ship a
cross-product of `workAndPsychiatricCare` variants to coexist with *Prison Labor*'s work modes — both
just toggle on.
The harness proves this on a live pawn: after `ToggleNonExclusiveInteraction(Ward_PsychiatricCare,
true)` and then `SetExclusiveInteraction(AttemptRecruit)`, **both** modes report enabled
(`live.stacksWithRecruit = True`).
---
## Step 2 — The treatment station
Flagging a patient for care does nothing on its own. Treatment happens *at a station, in the
patient's own room*. Without one, a patient flagged for care in an ordinary cell simply goes
untreated — which is the point of the neglect thought (Step 6).
### `Ward_TreatmentStation` — the building
> *"A desk, a chair bolted to the floor, and a locked cabinet of sedatives. A warden set to
> psychiatric care will use it to counsel patients held in this room."*
| Field | Value | Why |
|---|---|---|
| `defName` | `Ward_TreatmentStation` | |
| Parent | `BuildingBase` | Ordinary passable furniture |
| Cost | **40 Steel + 2 Industrial medicine** | Deliberately cheap |
| `WorkToBuild` | `1600` | A quick build |
| `MaxHitPoints` | `120` | |
| `researchPrerequisites` | `MedicineProduction` | The one research gate |
| `size` | `(2,1)` | A two-tile desk |
| `graphicClass` | `Graphic_Single` | One texture, not a rotation set — nothing to keep in sync |
| `designationCategory` | `Misc` | |
| `passability` | `PassThroughOnly`, `pathCost 60` | Pawns squeeze past it; it doesn't wall a cell |
| `Flammability` | `1.0` | It burns |
**It's cheap on purpose.** The real cost of running a ward is not the 40 steel. It is the **warden
hours** you spend counselling and the **labour you give up** by not putting the patient to work. The
building is just the gate that says "this room is set up to treat people."
---
## Step 3 — The room becomes a ward
Once a treatment station stands in a room that already holds prisoner beds, the room's *role* changes
from prison cell to psychiatric ward.
### `Ward_PsychWard` — the room role
| Field | Value |
|---|---|
| Def type | `RoomRoleDef` (`workerClass` is a public field — no patch needed) |
| `defName` | `Ward_PsychWard` |
| `label` | "psychiatric ward" |
| `workerClass` | `Ward.RoomRoleWorker_PsychWard` |
| `relatedStats` | Impressiveness, Cleanliness, Space |
Vanilla already ships `PrisonCell`, `PrisonBarracks` and `Hospital` on exactly this rail; the ward is
the fourth sibling. The scoring worker, `RoomRoleWorker_PsychWard.GetScore`, is where the care goes:
```
score = 0 if stations == 0 OR prisonerBeds == 0
score = 1_000_000 × prisonerBeds otherwise
```
Two design decisions are baked into that formula:
- **Gated on a station.** The score is **zero** unless the room actually contains a
`Ward_TreatmentStation` *and* at least one prisoner bed. This is deliberate: without the gate, any
ordinary prison cell full of prisoner beds would start scoring as a ward and quietly steal the
`PrisonCell` role from vanilla — a compatibility bug dressed up as a feature. Build the station and
it's a ward; don't and your cells keep their vanilla role, untouched.
- **1e6, not a hard-coded constant.** Vanilla's room roles score in the `~1e5` range. The ward must
*outscore* `PrisonCell` for the same room or it would read as a cell block. Multiplying by
1,000,000 clears that range without pinning to a magic number a future RimWorld patch might move.
(The harness's `FakeDLC` fixture deliberately adds a greedy competing role at `1e5 × beds`, and the
ward still wins the room.)
**Crucially, it is still a prison cell.** Re-labelling the role does not release anyone. `Room.isPrisonCell`
is a cached field set only when the room's shape changes — never derived from the role — so a ward is
a prison cell *and* a ward simultaneously. Containment, food delivery, and prison breaks all keep
working. This is the pair of claims the harness holds together: `room.roleIsWard = True` **and**
`room.stillPrisonCellAsWard = True`.
---
## Step 4 — The warden does the work
### `Ward_WardenPsychiatricCare` — the work giver
| Field | Value | Why |
|---|---|---|
| `defName` | `Ward_WardenPsychiatricCare` | |
| `giverClass` | `Ward.WorkGiver_Warden_PsychiatricCare` | A **new** class extending `WorkGiver_Warden` |
| `workType` | `Warden` | Assigned like any other warden work |
| `verb` / `gerund` | "treat" / "treating" | |
| `priorityInType` | `70` | Above chat (60), below feeding — an untreated patient deteriorates, a bored one doesn't |
| `requiredCapacities` | Talking, Hearing | A mute or deaf warden can't counsel |
The class extends `WorkGiver_Warden` and its `JobOnThing` refuses the job unless **all** of these
hold:
1. The pawn is a prisoner of the colony being taken care of (`ShouldTakeCareOfPrisoner`).
2. `IsInteractionEnabled(Ward_PsychiatricCare)` — asked, not "is this *the* mode," because the mode is
non-exclusive and the patient may also be set to recruit or forced labour.
3. The patient is **awake, not in a mental state, and not downed** — counselling someone mid-break or
unconscious isn't counselling.
4. The warden can reserve the patient.
5. A `Ward_TreatmentStation` exists **in the patient's own room** (not `PsychologicallyOutdoors`), is
reservable, and isn't forbidden. You can't treat someone through a wall from the station two
blocks over.
**Being a new class is the entire compatibility story** (see The Compat Harness for the proof):
- *Custom Prisoner Interactions* prefixes/postfixes the **named** vanilla warden givers (`_Chat`,
`_Convert`, `_Enslave`, `_ReleasePrisoner`). It literally cannot see `WorkGiver_Warden_PsychiatricCare`,
so it can't break it.
- *Prison Commons* postfixes `WorkGiver_Warden.ShouldSkip` on the **base** class. Ward's giver
inherits that method and doesn't override it, so Ward respects prison-commons areas **for free**,
without Ward knowing Prison Commons exists.
### `Ward_ProvidePsychiatricCare` — the job
| Field | Value |
|---|---|
| `defName` | `Ward_ProvidePsychiatricCare` |
| `driverClass` | `Ward.JobDriver_PsychiatricCare` |
| `casualInterruptible` | `false` |
> Note: the `JobDef` and the `PrisonerInteractionModeDef` deliberately have **different** defNames.
> `[DefOf]` binds fields to defs by name, so two defs sharing one name couldn't both be reached from
> the `WardDefOf` class.
The driver, `JobDriver_PsychiatricCare`, walks the warden to the patient and runs a counselling toil:
- **Session length:** `SessionTicks = 1200` — about **20 in-game minutes**.
- The wait toil uses `activeSkill = Social` and shows a progress bar; it fails out if the patient
despawns, is forbidden, falls asleep, or enters a mental state partway through.
- On completion it applies treatment, grants the counselled mood memory, fires a `DeepTalk` social
interaction (so the relationship builds and the *next* session lands harder), and awards **60 Social
XP** to the warden.
---
## Step 5 — What treatment buys: `Ward_UnderTreatment`
Each completed session adds severity to a single hediff. The severity is **skill-scaled**:
```
gain = 0.20 + (wardenSocialLevel / 20) × 0.30 → range 0.20 .. 0.50 per session
```
| Warden Social skill | Severity gained per session |
|---|---|
| 0 | 0.20 |
| 5 | 0.275 |
| 10 | 0.35 |
| 15 | 0.425 |
| 20 | 0.50 |
The floor is deliberate: even an unskilled warden buys **0.20** per session. A bad ward is *neglect*,
not *zero* — a clumsy counsellor is still worth something. Severity is capped at `maxSeverity = 1.0`.
### The hediff
| Field | Value | Why |
|---|---|---|
| `defName` | `Ward_UnderTreatment` | |
| `hediffClass` | `HediffWithComps` | |
| `label` | "under psychiatric care" | |
| `isBad` | `false` | It's a good hediff; won't be treated as an injury |
| `scenarioCanAdd` | `false` | Can't be granted by scenario editor |
| `maxSeverity` | `1.0` | |
| **Decay** | `severityPerDay = −0.15` | Roughly a week from a full course back to nothing, unattended |
| **Effect** | `MentalBreakThreshold −0.10` (stage 0) | A *negative* offset means the pawn breaks at a **lower** mood — i.e. **breaks less often** |
**Decay is the mechanic.** Because severity bleeds off at 0.15/day, a patient treated once and then
ignored slides right back. Sustained treatment keeps the hediff topped up and the break threshold
suppressed; stopping lets it fade. Treatment is a *programme*, not a one-time cure. (The
`MentalBreakThreshold −0.10` value is a first-guess, not yet playtested — see the "unplaytested"
caveats in the README.)
---
## Step 6 — The other half: neglect
A ward you don't staff is not a neutral place to keep someone. It is a *worse* one. Without a cost,
"arrest the sad colonist and park them" would be a free way to remove a problem pawn from play. It
isn't free.
### `MapComponent_WardNeglect`
Once per **in-game day** (`CheckIntervalTicks = 60000`), this component scans every spawned prisoner
of the colony. For each prisoner flagged for `Ward_PsychiatricCare` who does **not** currently have a
`Ward_UnderTreatment` hediff, it grants the `Ward_Neglected` thought.
The absence of the treatment hediff is the signal *by design*: because the hediff decays on its own,
its absence means **no warden has been near this patient in days** — not merely that they aren't being
counselled at this exact instant.
### The two thoughts
| Thought | Mood | Duration | Stack | Meaning |
|---|---|---|---|---|
| `Ward_Counselled` | **+6** | 2 days | up to 3 | "Someone sat with me and listened. It helped, a little." |
| `Ward_Neglected` | **−8** | 3 days | up to 4 | "They locked me in here for my own good and then forgot about me." |
Neglect is asymmetric — a −8 penalty stacking four deep (−32) badly outweighs three counselling lifts
(+18). That's intentional: a neglected patient's mood falls, which raises their break risk, which is
exactly the crisis you committed them to avoid. The ward can *manufacture* the break it was built to
prevent. Staff it, or don't build it.
> **Known rough edge (from the README's "Open" list):** neglect currently keys off "no
> `Ward_UnderTreatment` hediff." A patient treated once a week technically dodges the neglect thought
> on the single day the hediff expires. A real last-treated timestamp would be cleaner. Documented,
> not yet fixed.
---
## How it stacks with recruitment (and everything else)
Because `Ward_PsychiatricCare` is non-exclusive, it layers onto whatever else you've set:
| You want to… | Set | Result |
|---|---|---|
| Just stabilise a broken colonist | Psychiatric care | Wardens counsel; break threshold drops |
| Talk a captured raider down *and* recruit them | Psychiatric care **+** Attempt recruit | Both run — counselling lifts mood while resistance is chipped |
| Convert *and* treat | Psychiatric care **+** convert | Both toggle on |
| Work a patient *and* treat them | Psychiatric care **+** Prison Labor's work mode | Both apply. Working a fragile patient is bleak — and it's your call to make |
The last row is the darkest option the mod exposes, and it exposes it on purpose: Ward doesn't
forbid working a patient, it just makes the trade-off visible.
Because a ward patient is a genuine vanilla prisoner, everything the wider Institution suite does to
prisoners applies to them unchanged — a committed patient can be classified, disciplined, paroled,
searched, and can conceal or improvise contraband. Ward writes no integration code for any of that;
the patient satisfies "held pawn" and the rest follows. See **Home** for the suite map and **The
Compat Harness** for how all of it is verified in one running game.
---
## Quick reference — every Ward def
| defName | Type | Class (if any) | Key numbers |
|---|---|---|---|
| `Ward_PsychiatricCare` | PrisonerInteractionModeDef | — | non-exclusive; listOrder 150 |
| `Ward_TreatmentStation` | ThingDef | `Building` | 40 steel + 2 medicine; WorkToBuild 1600; MedicineProduction research |
| `Ward_PsychWard` | RoomRoleDef | `RoomRoleWorker_PsychWard` | score = 1e6 × prisonerBeds (station-gated) |
| `Ward_WardenPsychiatricCare` | WorkGiverDef | `WorkGiver_Warden_PsychiatricCare` | priority 70; Talking + Hearing |
| `Ward_ProvidePsychiatricCare` | JobDef | `JobDriver_PsychiatricCare` | 1200-tick session; +60 Social XP |
| `Ward_UnderTreatment` | HediffDef | `HediffWithComps` | +0.20–0.50/session; −0.15/day decay; −0.10 break threshold |
| `Ward_Counselled` | ThoughtDef | `Thought_Memory` | +6 mood, 2 days, stack 3 |
| `Ward_Neglected` | ThoughtDef | `Thought_Memory` | −8 mood, 3 days, stack 4 |
| — | MapComponent | `MapComponent_WardNeglect` | daily scan (60000 ticks) |
---
## Part of the Institution suite
Institution: Ward is one mod in the **Institution** suite of RimWorld 1.6 mods — Ward, plus
**Institution: Core**, **Institution: Contraband**, **Institution: Justice**, **Institution: Gangs**,
and **Foul Play**. Each stands alone; together they interlock. A committed patient rides the same
prisoner rail the rest of the suite polices, so a psychiatric ward is also a secure context where a
patient can conceal and improvise contraband exactly as a prisoner can.
## AI disclosure
This mod was developed with substantial assistance from Claude (Anthropic), including the
compatibility analysis, the def and C# implementation, and the test harness.
+118
View File
@@ -0,0 +1,118 @@
# Institution: Ward
**Involuntary psychiatric commitment for RimWorld 1.6 — the third pawn state, neither free nor guilty.**
> **The psychiatric-care layer of the [Institution](https://git.onetick.ninja/flan/rimworld-institution)
> suite.** Ward runs on Core's shared treatment engine and ships bundled in the single Institution
> install with a toggle of its own; it still installs standalone (needing only Institution: Core and
> Harmony). Once a separate sister mod, now folded in.
RimWorld gives a person exactly two places to stand: **colonist**, or **prisoner**. There is no
third state for someone who is neither free nor guilty. Out of the box you cannot commit anyone. A
colonist breaks, wanders off, punches a wall, recovers on their own, and goes back to hauling steel
as though nothing happened. Mental breaks are episodic and amnesiac — nothing persists, nothing is
*done* about them.
Ward adds the missing state, and the room to put it in. A colonist you arrest can be set to
**psychiatric care**. Wardens counsel them at a **treatment station**. A room with prisoner beds
and a treatment station is a **psychiatric ward**, not a prison cell. Treatment lowers the patient's
mental-break threshold while it lasts — and it decays, so it has to be *sustained*. A ward you don't
staff is worse than no ward at all.
---
## The third state, without a fourth enum slot
The reason "commit a colonist" isn't a stock feature is a single compiled fact. `RimWorld.GuestStatus`
is a three-value enum:
```
Guest = 0
Prisoner = 1
Slave = 2
```
A mod **cannot add a fourth value** to a compiled enum. That one constraint drives every design
decision in Ward — and in the rest of the ecosystem. It is why *Hospitality* carries dozens of
Harmony patches to maintain a pawn who is in your colony but is not your colonist: the enum has no
slot for one, so it runs a shadow guest system and defends it by patching bed validity, allowed
areas, the work JobGiver and the mental-state handler.
**Ward doesn't pay that tax.** A committed pawn *is* a prisoner, in vanilla's own sense of the word —
arresting your own colonist already performs the colonist→prisoner transition. Ward only adds a new
**mode** to hold them under, and `PrisonerInteractionModeDef` is a **Def, not an enum**. Adding one
is XML plus, at most, a worker class. This is not a trick; it is the established pattern. *Prison
Labor* already ships seven custom interaction modes of its own.
So the *state itself* costs **zero patches** — it is all defs:
| Extension point | What Ward adds | Patches needed |
|---|---|---|
| `PrisonerInteractionModeDef` | `Ward_PsychiatricCare` (non-exclusive) | 0 |
| `RoomRoleDef` (`workerClass` is public) | `Ward_PsychWard` | 0 |
| `WorkGiverDef` (`giverClass`) | `Ward_WardenPsychiatricCare` | 0 |
| `JobDef`, `HediffDef`, `ThoughtDef`, `ThingDef` | treatment, recovery, neglect, the station | 0 |
Ward *does* use Harmony where a patch is the cleaner tool (a patient's inspect line, and more), so it
is not a zero-patch mod. Its compatibility is about **which** methods it avoids: it touches none of
the contested methods the popular psych/prison mods fight over, and never prefixes
`MentalStateHandler.TryStartMentalState`. The full argument — which mods it was verified against and
why it can't collide with them — is on **The Compat Harness** page.
---
## Riding the prisoner rail (why the suite cares)
The design choice that makes Ward *cheap* also makes it *interesting* inside the wider Institution
suite. A committed patient is a genuine vanilla prisoner. That means they are a **secure context** in
exactly the sense the rest of the suite understands: a pawn who is held, searchable, and capable of
concealing and improvising contraband. A patient in a psych ward can hoard a shiv or brew something
foul in a smuggled vessel for **free**, because the contraband, search, corruption and gang systems
were written against "held pawn," and a ward patient satisfies that predicate without Ward writing a
line of integration code.
The ward is a prison cell that happens to also be a ward. `Room.isPrisonCell` is a cached field
written only when the room's shape changes — it is **not** derived from the room's role — so
re-labelling a cell as a ward cannot break containment, food delivery, or prison breaks. The compat
harness asserts exactly this: a built ward is both `roleIsWard = True` **and** `stillPrisonCellAsWard
= True`. The two claims pull against each other, and both have to hold.
---
## Wiki index
| Page | What's on it |
|---|---|
| **Home** (this page) | What Ward is, the third-state problem, the suite |
| **Commitment** | How commitment works in play: the mode, station, room role, work giver, job, hediff, thoughts, neglect, and how it stacks with recruitment — with every real def name and number |
| **The Compat Harness** | The in-game test tool this repo carries: booting real headless RimWorld with the whole suite + the most-subscribed Workshop mods (~24 active), the dependency-ordered build, the `WARDTEST` / `PNTEST` / `CBTEST` / `FPBRIDGE` assertion families, and how to run it |
---
## Part of the Institution suite
A set of RimWorld 1.6 mods about what an institution does to the people inside it. Each mod stands
alone as an install; together they interlock into one system — colonists and prisoners drift toward
crime on a spectrum of nature × nurture, and a policing/justice layer discovers, catches, punishes,
reforms, or paroles them. Ward's psychiatric ward is a secure context where a committed patient can
conceal and improvise contraband exactly as a prisoner can, because they ride the same prisoner rail.
| Mod | What it is |
|---|---|
| **Institution: Ward** (this) | Involuntary psychiatric commitment — the third pawn state, neither free nor guilty. Also carries the suite's in-game compat harness. |
| **Institution: Core** | The shared engine — propensity (nature × nurture), the criminal record, secured context. |
| **Institution: Contraband** | The physical smuggling loop — conceal, improvise shivs, dig Prison-Architect tunnels, warden search, warden corruption. |
| **Institution: Justice** | Corrections & policing — classification, deterrence, discipline, parole, regime. |
| **Institution: Gangs** | Gangs as contraband economies — joining, smuggling networks, rivalry, fights-as-crime. Also the home of the suite's cross-module `CBTEST` integration test. |
| **Foul Play** | The vessel + substance + throw framework, and the "Piss Nuke" flagship; bridges to Core + Contraband when both are present. |
Reference sibling mods by name — the mods are separate repositories and cross-repo links break.
---
## AI disclosure
This mod was developed with substantial assistance from Claude (Anthropic), including the
compatibility analysis, the def and C# implementation, and the test harness. This disclosure lives
in the repository (About.xml, README, and here); it is deliberately not repeated as commit-message
attribution.
+285
View File
@@ -0,0 +1,285 @@
# The Compat Harness
`Tools/run-compat-test.sh` is not a unit test. It is a **documented tool** that boots a *real,
headless RimWorld* with the entire Institution suite and the most-subscribed relevant Workshop mods
loaded **at once**, plays the game at maximum speed for a few thousand ticks, and greps the log for
assertions that the mods it built actually work — and don't silently eat each other.
Ward carries this harness for the whole suite because Ward is where the compatibility argument lives:
Ward's central claim is "I add defs, I patch nothing, so I can't collide with your mod list," and a
claim like that can only be *believed* from static analysis. It has to be *run* to be proven.
---
## Why a running game, not static analysis
Static analysis got the design most of the way. Dumping the Harmony attributes out of every mod
assembly shows **where** two mods want to patch the same vanilla method. What it cannot show is
whether that collision actually **bites**:
- Two mods can both postfix a method and be completely fine.
- Or one mod's **prefix returns `false`**, which short-circuits the original method *and every other
prefix* — silently eating another mod's patch. **Nothing crashes.** A feature just quietly stops
working.
The canonical example the harness guards: *Hospitality* prefixes
`MentalStateHandler.TryStartMentalState`. If Ward ever added a second prefix there that returned
`false`, Hospitality's would silently never run, and there would be no error to notice. So the harness
doesn't read code — it interrogates the **live Harmony patch table inside a running RimWorld** and
asserts who owns each contested method.
The same failure mode, in a different shape, threatens Foul Play: the "Piss Nuke" psychotic-spree
feature is delivered by **XPath PatchOperations** into `MentalStateNonCritical` (and the escape/duty
defs). If another mod reshapes those defs, the XPath silently stops matching, the feature dies, and —
again — nothing errors. Only booting the whole stack and watching the spree actually happen catches
it.
---
## What it loads (~24 active mods)
The full-stack run activates the base game + three DLC, two libraries, the popular psych/prison
Workshop mods, and the whole Institution suite — **24 active mods** by default, **25** with the
`FakeDLC` fixture switched on.
| Group | Mods |
|---|---|
| Base + DLC | RimWorld Core, Royalty, Ideology, Biotech |
| Libraries | Harmony, HugsLib |
| Guest/prisoner rail | Hospitality, Prison Labor, Locks, Prison Commons, Custom Prisoner Interactions, Prisoner Realism, Prisoner Recreation |
| Mental health | Psychology (unofficial), Rim Disorders, More Mental Breaks, Restraints |
| The big neighbour | Dubs Bad Hygiene (632k subscribers — the one Foul Play deliberately does *not* duplicate) |
| Institution suite | Core, Contraband, Justice, Gangs, Foul Play, **Ward** |
| Optional fixture | FakeDLC (stands in for Anomaly/Odyssey; only with `FAKE_DLC=1`) |
The Workshop mods are read from a persistent, ID-keyed cache (default
`/home/dev/rimworld-ref/mods-cache`, keyed by Workshop file id). The cache lives on the home
partition, not `/tmp`, because a scratchpad reap once took the whole stack down with it. Rebuild it
from an installed run with `Tools/rebuild-mods-cache.sh`.
Two sharp compat targets in that list are worth calling out, because they are exactly the ones that
*could* break Ward:
- **Prisoner Realism** postfixes `Verse.Room.IsPrisonCell` — and Ward introduces a room role that can
outrank `PrisonCell`. Statically, `isPrisonCell` is a cached field written only by
`Notify_RoomShapeChanged` and is **not** derived from the role, so Ward can't break it. The harness
proves that at runtime, on a room it builds and re-roles live.
- **Hospitality** owns the whole "a pawn in the colony who is not a colonist" surface, including that
`TryStartMentalState` prefix. Ward rides the *prisoner* rail; Hospitality rides the *guest* rail;
the harness asserts they never touch.
---
## The dependency-ordered build
The Institution suite is split across separate assemblies with real cross-references, so they must
**build** and **load** in dependency order or the references won't resolve. Core is the dependency-free
leaf everything else reads.
### Build order (what the script compiles, and why)
| # | Built | Depends on (to compile) | SelfTest? | Notes |
|---|---|---|---|---|
| 1 | **Institution: Core** | base game only | no | The propensity / criminal-record / secured-context engine. Built even in Core-only, because everything below references its DLL. |
| 2 | **Institution: Justice** | Core | no | Classification, deterrence, discipline, parole, regime. Built even in Core-only (the bridge references it). |
| 3 | **Institution: Contraband** | Core | no | Concealment, search, tunnels. Builds **clean** now — see "Where CBTEST lives" below. |
| 4 | **Foul Play** (+ bridge) | Core, Contraband, its own DLL | **yes** (`PNTEST`) | The vessel/substance/throw framework and the Piss Nuke flagship. Its optional Contraband **bridge** is compiled against the Core/Contraband/FoulPlay DLLs. |
| 5 | **Institution: Gangs** | Core, Contraband, Justice | **yes** (`CBTEST`) | The social capstone — the only assembly that references *every* other Institution mod, which is why the cross-module integration harness lives here. |
| 6 | **Institution: Ward** | base game only (harness: + Harmony) | **separate assembly** (`WARDTEST`) | Ship `Ward.dll` carries no harness (Harmony ships as a normal dependency; the harness does not). The `WARDTEST` harness is its own project, `Source/Ward.CompatTest` → `Ward.CompatTest.dll`, built to a scratch dir and dropped beside the ship DLL at test time. |
### Load order (the `activeMods` list)
Harmony first (it says so itself), then the base game + DLC, then HugsLib, then the Workshop mods,
then the Institution suite **in dependency order**, and **Ward dead last**:
```
Core → Contraband → Justice → Foul Play → Gangs → Ward
```
Contraband and Justice each need only Core (their relative order doesn't matter). Foul Play's bridge
needs Contraband **active**, so Foul Play loads after it. Gangs is the capstone and needs Core +
Contraband + Justice. **Ward loads last on purpose** — its Harmony conflict report reads the live
patch table, so every other mod must have had its chance to patch *before* Ward looks.
The script self-checks the config, not just the mod: every Workshop mod copied into `Mods/` has its
real `packageId` cross-checked against `activeMods`, because copying a mod in without activating it
would silently shrink the stack and still pass. It also lints every def XML first (`Tools/lint-xml.py`) —
a malformed comment makes RimWorld drop a def *silently*, and that's not worth a 15-minute headless
run to discover.
---
## Where CBTEST lives now (Gangs, SelfTest)
The suite's cross-module integration test — the `CBTEST` family, which drives the Prison-Architect
tunnel dig→search→breach loop and the full Core/Justice/Gangs interaction — used to live in
Contraband. It **moved into Gangs**, because Gangs is the one assembly that already references every
Institution mod (Core, Contraband, Justice). Consequences:
- **Contraband now builds clean** — no `SelfTest`, no test code in the shipped `Contraband.dll`.
- **Gangs** is built with `-p:SelfTest=true`, which compiles its integration `GameComponent` in. That
build leaves test code in `Assemblies/InstitutionGangs.dll`, so the Gangs repo must be rebuilt
clean before its shipped DLL is committed.
- **Foul Play** is likewise built `-p:SelfTest=true` for its own `PNTEST` harness, so its DLL must
be rebuilt clean before it is shipped.
- **Ward** no longer works that way. Its harness is a **separate assembly**
(`Source/Ward.CompatTest` → `Ward.CompatTest.dll`), never compiled into `Ward.dll`. There is no
`SelfTest` flag on the ship project and nothing to "rebuild clean" — a build of `Ward.csproj` is
physically incapable of emitting the harness or a Harmony reference. `run-compat-test.sh` builds
the harness to a scratch dir and installs `Ward.CompatTest.dll` beside the clean ship DLL, where
RimWorld loads both. A CI guard additionally fails any push whose committed `Ward.dll` carries
`HarmonyLib`/`CompatTest` symbols. (Gangs and Foul Play still rely on the rebuild-clean
discipline; Ward removed that failure mode outright after a SelfTest `Ward.dll` shipped by
accident.)
---
## The assertion families
Everything is written to the log with a family tag, and the verdict section greps for each one. A run
with no `WARDTEST` lines at all is a hard failure ("NO ASSERTIONS RAN").
| Tag | Owner | What it proves |
|---|---|---|
| **`WARDTEST`** | Ward (`Source/Ward.CompatTest/CompatTest.cs`) | Ward loaded, patches no *contested* method, and works on a live prisoner |
| **`PNTEST`** | Foul Play self-test | The Piss Nuke / vessel / substance / ferment content works under the stack |
| **`FPBRIDGE`** | Foul Play ↔ Contraband bridge | The optional bridge loaded (and *only* when Contraband is present) and merged the two frameworks |
| **`CBTEST`** | Gangs self-test | The whole Institution loop — tunnel, search, classification, discipline, parole, corruption, gangs, regime — runs end-to-end on a live map |
| *(`PNCOMBAT`)* | Foul Play | Grepped alongside PNTEST for the combat/throw path |
### `WARDTEST` — Ward's own checks
Split between a `[StaticConstructorOnStartup]` (static def checks, "did Ward *load*") and a
`GameComponent` that runs at tick 60+ (the live checks, because the static-ctor order between mods is
undefined and Contraband injects its escape JobGivers from its *own* static ctor):
- **Def load:** `def.interactionMode` (and `.nonExclusive`), `def.roomRole` (and `.workerResolves`),
`def.workGiver` (and `.classResolves`), `def.job`, `def.hediff`, `def.station`,
`station.graphicLoaded` (a missing texture is a magenta box and a log line, not a crash — so it's
checked on purpose).
- **Vanilla intact:** `vanilla.prisonerModes.intact` — `MaintainOnly`, `AttemptRecruit`,
`ReduceResistance`, `Release`, `Execution` all still exist (a defName collision would let the game
boot while quietly removing your ability to execute or release a prisoner).
- **The central claim:** `ward.patchesNoContestedMethod` — the harness enumerates every vanilla method
where a psych/prison mod *could* collide (`SetGuestStatus`, `IsValidBedFor`, `InAllowedArea`,
`TryStartMentalState`, `MentalBreaker.TryDoRandomMoodCausedMentalBreak`, `WorkGiver_Warden.ShouldSkip`,
`WorkGiver_Warden_Chat.JobOnThing`, …), reads the live Harmony patch owners, and asserts **none of
them is owned by Ward.** The moment Ward grows a Harmony patch on a contested method, this fails.
- **The specific one:** `ward.noPrefixOn.TryStartMentalState` — Ward must never add a *prefix* there,
or it silently eats Hospitality's.
- **Live usability:** `live.isPrisoner`, `live.psychCareEnabled`, `live.stacksWithRecruit` — a real
prisoner is generated, flagged for care via vanilla's *own* public API, and confirmed to stack with
`AttemptRecruit`, with Prison Labor / Custom Prisoner Interactions / Hospitality all loaded and
patching the guest tracker.
- **The built ward:** the component builds an actual walled, roofed room with a prisoner bed and a
station on the live map, forces a region/room rebuild, and asserts `room.proper`,
`room.plainCellNotStolen` (a bare cell keeps its vanilla role), `room.roleIsWard`, and — the
load-bearing one — `room.stillPrisonCellAsWard`.
- **Escape-tree order (`escape.armBeforeExit`, `escape.useContrabandBeforeExit`):** Ward also verifies
that Contraband's escape JobGivers land *before* `JobGiver_GotoTravelDestination` in the
`PrisonerEscape` think tree. A think node that *landed* is not a think node that can *run* — the tree
takes the first node that returns a job, and the travel-destination node returns one essentially
always, so anything appended after it is dead code that would pass a mere presence test. The harness
asserts **position**, because pre-order index is evaluation order.
- Terminated by `WARDTEST DONE`.
### `PNTEST` / `FPBRIDGE` — Foul Play
The Foul Play self-test proves the flagship content survives the stack: the carboy weapon def loads
(`def.weapon`), the spree think-tree XPath still matches (`thinktree.patched`), and the generic-vessel
refactor holds — runtime substance, label-shows-contents, the eight-substance catalogue, drink
consequences, mix ratios, pour capacity, water-douses-fire, blend round-trip through save/load,
fermentation dilution and spoilage, "only 100% ripe piss reads as a nuke," any pawn filling a vessel,
furniture dip/pour, and the autonomous cocktail on fill. The `FPBRIDGE` family fires only when
Contraband is present, and proves the optional bridge wired the carboy's brew-from-body worker, merged
the two propensity engines, and wired the content-tag delegates so a concealed vessel keeps its
substance.
### `CBTEST` — the suite integration test (in Gangs)
When Contraband is active, the Gangs self-test drives the entire loop on a live map and every one of
these must report `True`: `escape.registered`, `escape.foundWillingDigger`, `escape.digRatePositive`,
`escape.tickAdvancesTunnel`, `escape.recordsAttempt`, `escape.breachDestroysWall`,
`escape.emergedOutside`, `escape.toolConsumed`, `intake.concealsOnBodyContraband`,
`content.tagSurvivesRedeem`, `corruption.dispositionVaries`, `classification.gradeReflectsRecord`,
`prisonization.reshapesNurture`, `justice.deterrenceFeedback`, `discipline.hardens`,
`parole.releaseDecision`, `corruption.bentWardenSmuggles`, `gangs.smugglesAcrossWall`,
`gangs.fightIsCrime`, `gangs.membershipByDisposition`, `regime.prisonerRecreation`. Any `CBTEST … =
False`, or a `CBTEST HARNESS THREW`, fails the run.
---
## How to run it
```bash
# Full stack: ~24 active mods, all DLC, the popular Workshop mods.
Tools/run-compat-test.sh /path/to/rimworld-install [/path/to/workshop-mods]
```
The second argument (the Workshop-mod cache) defaults to `/home/dev/rimworld-ref/mods-cache` or
`$RIMWORLD_MODS_CACHE`. The Institution sibling source dirs default to `$HOME/rimworld-<mod>` and can
be overridden with `INSTITUTIONCORE`, `INSTITUTIONJUSTICE`, `CONTRABAND`, `FOULPLAY`,
`INSTITUTIONGANGS`.
### The two modes — they fail in opposite directions
| Env var | What it does | Why it exists |
|---|---|---|
| `CORE_ONLY=1` | No DLC, no Workshop mods, no Contraband/Justice/Gangs — just **Foul Play + Ward on a bare Core install** | Coexisting with 24 mods proves nothing about the player who owns no DLC. A def that accidentally referenced a DLC thing would resolve happily in the full run and break for everyone else. This is the opposite test. |
| `FAKE_DLC=1` | Loads `Tools/fixtures/FakeDLC` — a *hostile* mod that does to Ward's surface everything an unknown DLC could: competing prisoner interaction modes, a competing room role (`~1e5 × beds`), and pokes the very think-tree/duty defs the Piss Nuke XPaths depend on | You own Royalty/Ideology/Biotech but **cannot** load Anomaly/Odyssey without owning them (a DLC's defs can't be downloaded otherwise). The fixture stands in for what can't be loaded, and it loads *before* the suite so its patches land first. |
`CORE_ONLY` is not achievable by editing `ModsConfig` (RimWorld re-activates any expansion whose
`Data/` folder is present) or by symlinks (Unity resolves the executable's real path back into the
original install). The script builds a **hard-link copy** of the game with the DLC `Data/` folders
deleted — the binary genuinely lives in a DLC-free tree, so its real path resolves there, at no disk
cost. In Core-only, `CB_ACTIVE=0` drops Contraband/Justice/Gangs from the load order too, so the pass
tests Foul Play + Ward genuinely standalone: the Contraband bridge must **not** load, yet the piss
content must still work. The verdict section asserts both directions — and a Core-only run that
quietly still had DLC loaded (which happened, twice, during development) is caught by grepping the
`loaded mods` line for `royalty|ideology|biotech`.
### Watching a run
```bash
# Stream a run to a stable log on the home partition (survives /tmp reaps):
FAKE_DLC=1 Tools/harness-stream.sh /path/to/rimworld-install
# Follow it live from anywhere on the box (-F survives the log being recreated each run):
Tools/watch-harness.sh
```
Internally the run boots via `timeout 900 xvfb-run … RimWorldLinux -logfile … -quicktest`, forces
`TimeSpeed.Ultrafast` from the `GameComponent` (same per-tick logic, just no idle real-time between
ticks), and calls `Root.Shutdown()` once every assertion is written (~tick 8000; a 12000-tick watchdog
is the safety cap). The 900-second `timeout` is a backstop, not the normal exit.
---
## Requirements — and why it's local, not CI
| Requirement | Why |
|---|---|
| A **licensed RimWorld install** with the `RimWorldLinux` executable | The harness boots the *real* game engine. There is no headless stub — the whole value is that vanilla's own room grid, think trees, and mental-state handler run for real. |
| **The Workshop mods**, in the ID-keyed cache | Fetched with `DepotDownloader -app 294100 -pubfile <id>` — which needs a Steam account that *owns* RimWorld. The mods can't be redistributed. |
| **xvfb** + Mesa software GL (`libgl1-mesa-dri`) | RimWorld is a Unity app that insists on a GL context even for `-quicktest`. `xvfb-run` gives it a headless framebuffer; `LIBGL_ALWAYS_SOFTWARE=1` / `llvmpipe` render on the CPU (slow, but no GPU needed). |
| **dotnet** | Builds all six Institution assemblies (net472 via Krafs reference assemblies — no game install needed to *compile*, only to *run*). |
Every one of those — a licensed game binary, non-redistributable Workshop content, a Steam login, a
GL-hungry Unity process — is a reason this cannot live in ordinary CI. It is a **local, on-demand
integration test**: you run it before a release, on a box that owns the game, and read the verdict.
The last line of a good run is `ALL COMPAT ASSERTIONS PASSED`.
---
## Part of the Institution suite
The harness in this repo builds and tests the whole suite — **Institution: Core**, **Institution:
Contraband**, **Institution: Justice**, **Institution: Gangs**, **Foul Play**, and **Institution:
Ward** — in dependency order, alongside the most-subscribed psych/prison Workshop mods, in one running
game. Ward carries it because Ward's design *is* a compatibility claim, and a claim like that is worth
nothing until it's run.
## AI disclosure
This mod was developed with substantial assistance from Claude (Anthropic), including the
compatibility analysis, the def and C# implementation, and the test harness.