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.
18 KiB
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 outrankPrisonCell. Statically,isPrisonCellis a cached field written only byNotify_RoomShapeChangedand 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
TryStartMentalStateprefix. 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 shippedContraband.dll. - Gangs is built with
-p:SelfTest=true, which compiles its integrationGameComponentin. That build leaves test code inAssemblies/InstitutionGangs.dll, so the Gangs repo must be rebuilt clean before its shipped DLL is committed. - Foul Play is likewise built
-p:SelfTest=truefor its ownPNTESTharness, 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 intoWard.dll. There is noSelfTestflag on the ship project and nothing to "rebuild clean" — a build ofWard.csprojis physically incapable of emitting the harness or a Harmony reference.run-compat-test.shbuilds the harness to a scratch dir and installsWard.CompatTest.dllbeside the clean ship DLL, where RimWorld loads both. A CI guard additionally fails any push whose committedWard.dllcarriesHarmonyLib/CompatTestsymbols. (Gangs and Foul Play still rely on the rebuild-clean discipline; Ward removed that failure mode outright after a SelfTestWard.dllshipped 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,Executionall 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 withAttemptRecruit, 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 beforeJobGiver_GotoTravelDestinationin thePrisonerEscapethink 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
# 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
# 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.