Wiki: player-facing rewrite of every layer + add a scenario FAQ, linked from Home

This commit is contained in:
flan
2026-07-16 02:18:25 +00:00
parent ff703271ba
commit 8250dcb369
32 changed files with 1654 additions and 2510 deletions
+124 -153
View File
@@ -4,98 +4,91 @@
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.
different way to add to, or remove from, the private stash this page describes.
## While it is concealed, there is no Thing
## While it is concealed, there is no item
The single most important rule, and the source states it plainly:
The single most important rule:
> `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
> The prison quietly tracks what each pawn is hiding. **Concealed contraband is not an item on the
> map. 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 the 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:
A concealed item is only a hidden fact about a pawn: what it is, how far along it is, what material it
ended up made of, an optional source it is being whittled from, its tunnel state if it is a pick, and
whatever it holds inside. It has **no physical 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` |
| Moment | What happens |
|---|---|
| The prisoner **uses** it | It materialises into their inventory as a real item |
| A warden **finds** it | It is deleted; it never becomes an item at all |
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.
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`
## Where a thing can hide
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 }
```
Every piece of contraband can hide in one of two places, and this decides **who can ever find it**:
| 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** |
| **On the body** | small: a shiv, pills, a phone | searching the **pawn** |
| **In the cell** | 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).
A warden turning over a prisoner and their cell reaches both. The reason the two are split at all: a
**constable** in Justice's policing layer, frisking a colonist named in a street crime, only reaches
what is on the body, 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
## Three kinds 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:
There is no hardcoded list of contraband item names. At the start of a game, the mod looks at every
item in the game and decides, once, whether it counts as contraband, in one of three ways:
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**.
1. **Explicit.** Anything a mod has deliberately marked as contraband. This is the opt-in, and it is
how a mod plugs in its own behaviour for how the item is acquired, used, or used to escape.
2. **Every drug** — recognised automatically.
3. **Every weapon** that isn't destroyed when dropped — recognised from its **mass**.
The inference (`ContrabandUtility.Infer`) fills in concealability and hiding place so a modder never
For anything not marked explicitly, the mod fills in how well it hides and where, so a modder never
has to:
| Kind | Test | `concealability` | `hideIn` |
| Kind | Test | Concealability | Hides |
|---|---|---|---|
| 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) |
| Hard drug | drug rated Hard | **0.75** | on body or in cell |
| Other drug | any other drug | **0.5** | on body or in cell |
| Light weapon | mass ≤ 2.0 kg | **0.7** | on body or in cell |
| Heavy weapon | 2.0 kg < mass ≤ 10.0 kg | **0.35** | in cell only |
| Very heavy weapon | mass > 10.0 kg | — | **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.
**Concealability** runs 0 to 1, where **0 = a warden finds it every time and 1 = never**; higher is
harder to find. Drugs and inferred weapons get **no crafting route** (you cannot synthesise yayo in a
bare cell, and you cannot conjure a rifle) and **no special use behaviour** — because vanilla already
knows what to do with them. A prisoner takes a drug on their own; 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.
Nothing here edits another mod's items. It is a one-time reading of each item's own stats. Modded
drugs and weapons are covered without ever being named.
## The poll: no Harmony
## The heartbeat
`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.
The prison does its ongoing work on a heartbeat. Every **250 ticks** 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 can be self-made and no escape tools are in play, it does nothing 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.
Because the heartbeat already visits every held pawn, intake needs no special trigger on the capture
event: a pawn who is in custody but has not yet been processed is, by definition, one who was newly
taken.
### 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**:
The set of pawns being tracked is **not** just prisoners. It is every pawn whose belongings are
anyone's business — prisoners, slaves, ward patients, **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.
@@ -107,124 +100,102 @@ 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.
the warden just hauls them to a cell. Intake is Contraband's answer to that gap, and it runs on the
first heartbeat that sees a newly-held pawn:
`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
1. **Held only.** The pawn must be a person and in custody — prisoner, slave, or ward patient. A free
colonist is tracked, 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
2. **Once.** Each captive is processed a single time; a later pass finds them already done and moves
on. (It remembers by an ID, so it holds no ghost of a captive who has since died.)
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.
4. **Only what can ride on a body.** A thing can be intaken only if it is small enough to hide on the
body. 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:
*this* pawn would hide *this* item — a base chance of **0.6**, then scaled by the pawn's nature ×
nurture (Core's propensity). The roll is **fixed per (pawn, item)** — a settled fact, not a fresh
coin each tick. The frightened cook surrenders the knife; the disposed lifer palms it, and always
would have.
```
Propensity.Would(pawn, 0.6, SaltIntake ^ item.thingIDNumber)
```
When a pawn does hide an item, the real object is **removed from the game** and a hidden secret takes
its place, preserving what the item was made of and — critically — anything it holds inside.
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.
## Preserving contents without understanding them
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`.
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 when the item is used. Contraband stores
what a vessel contains as a single opaque label and **has no idea what it means**.
## The content tag: preserving contents without understanding them
When the item is concealed, that label is captured; when the item comes back out, the label is used
to pour the contents 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 mod, no content is captured and nothing is
lost — a shiv is just a shiv.
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
Fermentation itself lives in Foul Play, not here. A concealed vessel comes back out **fresh** and
ferments afterward like any other, which suits the theme: fresh-brewed piss is weak, and time is what
makes it a weapon. (The prison remembers how long the vessel was hidden, for any bridge mod that
wants to restore its aging, but it never acts on that number itself.) See
[Compatibility](Compatibility.md).
## Redemption: the secret becomes a Thing
## Redemption: the secret becomes real
`TryRedeem(pawn, def)` is where a secret turns real:
When a hidden item is finally used, it 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.
1. It is made from the material it was whittled from (or the item's default material, if it was never
made from a material).
2. Anything it held inside is poured back in.
3. It is placed into the pawn's inventory. If that somehow fails, nothing is dropped and nothing is
lost.
4. The secret is cleared from the stash.
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#):
The trigger for this is a breakout. When a prisoner enters an escape or assault-the-colony state, they
will arm themselves and then use whatever they are hiding — the same moment a base-game prisoner would
reach for a combat drug. The one contraband hook Ludeon wrote and then left reading an always-empty
inventory is exactly where this slots in. For each thing a pawn is hiding:
| Duty | Injected | Where |
|---|---|---|
| `PrisonerEscape` | `JobGiver_ArmSelf`, then `JobGiver_UseContraband` | **after** `JobGiver_TakeCombatEnhancingDrug` |
| `PrisonerAssaultColony` | `JobGiver_ArmSelf`, then `JobGiver_UseContraband` | **before** `JobGiver_AIFightEnemies` |
- **Inert contraband** (a drug, a stashed rifle — nothing special to "use"): just materialise it and
get out of the way. Vanilla takes the drug on its own; a weapon in inventory is a weapon. But a
stashed **weapon** only comes out for someone who would actually use it — 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 moves into
their hands; otherwise it stays hidden and they run. See [Improvised Weapons](Improvised-Weapons.md).
- **Active contraband** (something with a special use — e.g. Foul Play's throw-the-carboy):
materialise it, then hand off to that item's own use behaviour.
`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:
## Confiscation, and the door every route comes through
- **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.
When a warden finds contraband, it is simply removed. It **never becomes a real item** — 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.)
## Confiscation and the door every route comes through
At the other end, there is one single door that **every** supply route comes through — intake uses it,
a bent warden uses it ([Corruption](Corruption.md)), and a smuggling gangmate
([Institution: Gangs](Compatibility.md)) uses it. It hands a prisoner something already finished and
hidden, optionally with contents. One door, many keys.
`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.)
## Saving
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.
Concealed contraband is saved with your game — the whole stash, the per-pawn search cooldowns, and who
has already been intaken. On load, any secret whose pawn or item no longer exists is quietly dropped:
prisoners who died or left take their secrets with them.
## Quick reference
| Constant | Value | Meaning |
|---|---|---|
| `CheckIntervalTicks` | 250 | poll cadence for intake / tunnels / improvisation |
| Heartbeat | every 250 ticks | 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 |
| Intake gate | held + body-hideable | only pawns in custody, only items small enough to palm |
| Hard-drug concealability | 0.75 | inferred from being a hard drug |
| 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 |
| Light-weapon concealability | 0.70 | mass ≤ 2.0 kg, hides on body |
| Heavy-weapon concealability | 0.35 | 2.0–10.0 kg, in cell only |
| Unhideable mass | > 10.0 kg | not contraband |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
**