Wiki: player-facing rewrite of every layer + add a scenario FAQ, linked from Home
This commit is contained in:
+26
-271
@@ -1,281 +1,36 @@
|
||||
# The Compat Harness
|
||||
# Compatibility
|
||||
|
||||
`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 is built to sit alongside your existing mod list rather than elbow it aside. The trick is that
|
||||
Ward mostly **adds** things — a new "psychiatric care" prisoner mode, a new building, a new room role,
|
||||
new jobs — instead of rewriting how vanilla prisoners already behave. And a committed patient isn't a
|
||||
new kind of pawn: they're an ordinary vanilla prisoner with an extra "treat me" flag switched on. So
|
||||
any mod that already works with prisoners keeps working with a ward patient, and Ward stays out of its
|
||||
way.
|
||||
|
||||
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.
|
||||
Here's how Ward gets along with the popular psych and prison mods:
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| Mod | How it coexists with Ward |
|
||||
|---|---|
|
||||
| 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`) |
|
||||
| **Hospitality** | Hospitality manages *guests*; Ward manages *prisoners*. They ride two completely separate rails and never touch. |
|
||||
| **Prison Labor** | Psychiatric care is a toggle, so you can put a patient **to work *and* in treatment at the same time**. No conflict, no either/or. |
|
||||
| **Custom Prisoner Interactions** | That mod tweaks the built-in warden interactions (chat, convert, enslave, release). Ward's counselling is a *separate* job of its own, so the two don't overlap. |
|
||||
| **Prison Commons** | Ward's warden work automatically respects prison-commons areas — you get that for free, without either mod knowing about the other. |
|
||||
| **Psychology (unofficial) · Rim Disorders · More Mental Breaks** | These reshape the mental-break and mental-illness machinery. Ward doesn't touch that machinery — it just nudges the break *threshold* the way a mood effect would — so they all stack. |
|
||||
| **Prisoner Realism** | It changes how the game decides what counts as a prison cell. Ward's ward role sits on top of that without breaking containment — a ward is still a prison cell. |
|
||||
| **Locks · Restraints · Prisoner Recreation** | All fine. They touch prisoner behaviours Ward leaves alone. |
|
||||
| **Dubs Bad Hygiene** | No overlap at all. |
|
||||
| **Royalty · Ideology · Biotech** | Ward works with or without the DLC. Psychiatric care is available even if you don't run Ideology's ideoligions. |
|
||||
|
||||
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`.
|
||||
The general rule: because Ward *adds* rather than *overrides*, it should also coexist with prison and
|
||||
psych mods that aren't on this list. If you ever find one it genuinely fights, that's a bug worth
|
||||
reporting — Ward's whole design is built around not doing that.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
The Institution suite — **Institution: Core**, **Institution: Contraband**, **Institution: Justice**,
|
||||
**Institution: Gangs**, **Foul Play**, and **Institution: Ward** — is a set of RimWorld 1.6 mods about
|
||||
what an institution does to the people inside it. Each stands alone; together they interlock. 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 the rest of the suite polices.
|
||||
|
||||
Reference in New Issue
Block a user