Aggregate the whole suite wiki onto the Institution page
Wiki/Home.md is the suite's front door; every layer's wiki now mirrors here under Wiki/<layer>/ (core, contraband, justice, gangs, ward), so the whole suite reads in one place with the index linking to the local pages. The layer repos stay canonical -- Tools/sync-wiki.sh refreshes the aggregated copy on demand. The README's main page points to it.
This commit is contained in:
@@ -0,0 +1,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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user