Per the updated project policy, AI/Claude attribution lives in exactly one NOTICE file per repo and nowhere else -- removed it from the About description, README, wiki, and roadmap.
231 lines
13 KiB
Markdown
231 lines
13 KiB
Markdown
# Concealment and Intake
|
||
|
||
*How a hidden item exists without existing, how it gets onto a prisoner in the first place, and how
|
||
it becomes real again.*
|
||
|
||
This is the spine of the whole mod. Everything else — shivs, tunnels, searches, corruption — is a
|
||
different way to add to, or remove from, the private list this page describes.
|
||
|
||
## While it is concealed, there is no Thing
|
||
|
||
The single most important rule, and the source states it plainly:
|
||
|
||
> `MapComponent_Contraband` tracks what each pawn is hiding. **Concealed contraband is not a Thing.
|
||
> It is a secret.** It is not in the gear tab, there is no icon, and the player is told nothing —
|
||
> because an item you can see in a UI is not contraband, it is inventory, and there would be nothing
|
||
> for a warden to discover and no reason to ever order a search.
|
||
|
||
Concretely, a concealed item lives as a `ConcealedItem` row in a `List` on the map component. It
|
||
carries the def, a `progress` value, the material it ended up made of, an optional source it is
|
||
being harvested from, tunnel state if it is a pick, and an opaque `contentTag`. It holds **no
|
||
spawned object** anywhere in the world until one of two things happens:
|
||
|
||
| Moment | What happens | Method |
|
||
|---|---|---|
|
||
| The prisoner **uses** it | It materialises into their inventory as a real `Thing` | `TryRedeem` |
|
||
| A warden **finds** it | It is deleted from the list; it never becomes a `Thing` at all | `Confiscate` |
|
||
|
||
A corollary the search page leans on hard: **an empty search must still cost time.** If the
|
||
`Search` job only ever appeared when there was genuinely something to find, the mere *offer* of the
|
||
job would be the discovery. You have to be able to toss a clean cell and come up with nothing.
|
||
|
||
## Where a thing can hide: `ConcealSite`
|
||
|
||
Every contraband def declares where it can be hidden. This is not decoration — it decides **who can
|
||
ever find it**.
|
||
|
||
```csharp
|
||
[Flags] enum ConcealSite { None = 0, OnBody = 1, InCell = 2 }
|
||
```
|
||
|
||
| Site | Holds | Found by |
|
||
|---|---|---|
|
||
| `OnBody` | small: a shiv, pills, a phone | searching the **pawn** |
|
||
| `InCell` | bulky: a rifle, a carboy, a crude pick | tossing the **room** |
|
||
|
||
A warden turning over a prisoner and their cell reaches both (`OnBody | InCell`). The reason the
|
||
enum is split at all: a **constable** in Justice's now-shipping policing layer, frisking a colonist
|
||
named in a street crime, reaches only `OnBody`, so whatever is under their floorboards stays there
|
||
until somebody has grounds to search the *room*. The split between "who may search" and "what a
|
||
search reaches" is deliberate — see [Warden Search](Warden-Search.md).
|
||
|
||
## Three sources of contraband, and no list of items
|
||
|
||
At startup, `ContrabandUtility` scans every `ThingDef` and decides whether it is contraband. There
|
||
is no hardcoded list of item names — there are three recognisers:
|
||
|
||
1. **Explicit.** Any def carrying `CompProperties_Contraband`. This is the opt-in, and it is how a
|
||
mod plugs in its own `acquireWorker` / `useWorker` / `escapeWorker` behaviour.
|
||
2. **Every drug** (`ThingDef.IsDrug`) — inferred automatically.
|
||
3. **Every weapon** (`ThingDef.IsWeapon`) that is not `destroyOnDrop` — inferred from **mass**.
|
||
|
||
The inference (`ContrabandUtility.Infer`) fills in concealability and hiding place so a modder never
|
||
has to:
|
||
|
||
| Kind | Test | `concealability` | `hideIn` |
|
||
|---|---|---|---|
|
||
| Hard drug | `drugCategory == Hard` | **0.75** | `OnBody \| InCell` |
|
||
| Other drug | any other `IsDrug` | **0.5** | `OnBody \| InCell` |
|
||
| Light weapon | `Mass ≤ 2.0` | **0.7** | `OnBody \| InCell` |
|
||
| Heavy weapon | `2.0 < Mass ≤ 10.0` | **0.35** | `InCell` only |
|
||
| Very heavy weapon | `Mass > 10.0` | — | **not contraband** (a minigun hides nowhere) |
|
||
|
||
`concealability` runs 0..1 where **0 = a warden finds it every time and 1 = never**; higher is
|
||
harder to find. Drugs and inferred weapons get **no `acquireWorker`** (you cannot synthesise yayo in
|
||
a bare cell, and you cannot conjure a rifle) and **no `useWorker`** — because vanilla already knows
|
||
what to do with them. `JobGiver_SatisfyChemicalNeed` takes a drug; an armed prisoner *is* the
|
||
payload. Getting the item into the cell is the entire problem, and that is all these routes solve.
|
||
|
||
Nothing here mutates another mod's defs. It is a lookup keyed on the def, computed once, held in a
|
||
dictionary. Modded drugs and weapons are covered without ever being named.
|
||
|
||
## The poll: no Harmony
|
||
|
||
`MapComponent_Contraband` runs on a heartbeat. Every **250 ticks** (`CheckIntervalTicks`) it visits
|
||
every pawn who could be concealing something and does all the ongoing work: intake, tunnel progress,
|
||
and improvisation. If nothing on the map is self-acquirable and there are no escape tools registered,
|
||
it returns immediately and costs nothing.
|
||
|
||
Polling — rather than patching — is a deliberate architecture choice. **The tick already visits
|
||
every held pawn**, so intake needs no Harmony hook on the capture event: a pawn who is in custody but
|
||
not yet in the `intakeProcessed` set is, by definition, one who was newly taken. The whole mod ships
|
||
zero Harmony patches, and this is a large part of why.
|
||
|
||
### Who counts as a concealer
|
||
|
||
`ContrabandUtility.Concealers(map)` is **not** just prisoners. It delegates to Core's
|
||
secured-context predicate and yields every pawn whose belongings are anyone's business — prisoners,
|
||
slaves, **and free colonists**:
|
||
|
||
> A free colonist forbidden their drug by a drug policy or an ideoligion is precisely the pawn who
|
||
> keeps a private stash — and vanilla already denies them, with no valve.
|
||
|
||
Whether anyone may *search* a given concealer is a separate question with a separate answer (the
|
||
warden's authority reaches prisoners; a future cop's reaches colonists). Tracking is pawn-agnostic;
|
||
authority is not.
|
||
|
||
## Intake: the moment of capture
|
||
|
||
Vanilla never strips a captive *on capture* — a downed pawn keeps whatever they walked in with, and
|
||
the warden path just hauls them to a cell. Intake is Contraband's answer to that gap.
|
||
|
||
`RunIntake(pawn)` runs from the poll, on the first pass that sees a newly-held pawn:
|
||
|
||
1. **Held only.** The pawn must be humanlike and `IsHeld` (prisoner, slave, ward patient). A free
|
||
colonist is a concealer, but nobody frisks and processes *them* on the way into a cell, so their
|
||
carried gear is never intaken.
|
||
2. **Once.** The pawn's `thingIDNumber` is added to a `HashSet<int>`. The set stores an `int`, not a
|
||
pawn reference, so it holds no ghost of a captive who has since died. A second pass finds them
|
||
already processed and does nothing.
|
||
3. **Even while downed.** Intake runs *before* the "must be awake" gate. The stash a captive walked
|
||
in with is hidden the moment they are in custody, not when they wake — everything *after* intake
|
||
(whittling, digging) needs them awake, but hiding what is already on you does not.
|
||
4. **Only what can ride on a body.** `IsIntakeConcealable` accepts a thing only if its contraband
|
||
props include the `OnBody` flag. A shiv up a sleeve is intaken; a rifle or a suit of armour is
|
||
too bulky to palm and stays as ordinary strippable gear the player confiscates for free.
|
||
5. **Per-item disposition.** For each concealable thing the captive carries, the mod rolls whether
|
||
*this* pawn would hide *this* item:
|
||
|
||
```
|
||
Propensity.Would(pawn, 0.6, SaltIntake ^ item.thingIDNumber)
|
||
```
|
||
|
||
Base chance 0.6, then scaled by the pawn's nature × nurture (Core's `Propensity`). The roll is
|
||
**seeded per (pawn, item)** — a fixed fact, not a fresh coin each tick. The frightened cook
|
||
surrenders the knife; the disposed lifer palms it, and always would have.
|
||
|
||
When a pawn does hide an item, the real `Thing` is **destroyed** (equipment via
|
||
`DestroyEquipment`, inventory via `Destroy(Vanish)`) and a concealed secret is created in its place,
|
||
preserving what the item was made of (`stuff`) and — critically — its `contentTag`.
|
||
|
||
## The content tag: preserving contents without understanding them
|
||
|
||
A concealed vessel is not just "a jar" — it is a jar *of* something (hooch, piss, a fermented
|
||
blend), and that something has to survive being hidden and come back on redemption. Contraband
|
||
stores a single opaque string, `contentTag`, and **has no idea what it means**:
|
||
|
||
```csharp
|
||
// A bridge mod (Foul Play) fills these in. Null (no bridge) = items have no contents.
|
||
ContrabandUtility.ContentTagOf; // Func<Thing, string> — read on conceal
|
||
ContrabandUtility.ApplyContent; // Action<Thing, string> — applied on redeem
|
||
```
|
||
|
||
On concealment, `ContentTagOf(thing)` captures the tag (e.g. "a jar of a piss+hooch cocktail"). On
|
||
redemption, `ApplyContent(newThing, tag)` pours that content back into the freshly-made thing. This
|
||
is the exact seam **Foul Play** uses to keep a smuggled carboy's contents intact. Without a bridge,
|
||
both delegates are null, no content is captured, and nothing is lost — a shiv is just a shiv.
|
||
|
||
Fermentation itself lives in Foul Play, not here. A concealed vessel materialises **fresh** and
|
||
ferments afterward like any other, which suits the theme: fresh-brewed piss is weak, and time is
|
||
what makes it a weapon. (`concealedTick` records how long it was hidden, for any bridge that wants to
|
||
restore pre-aging — Contraband keeps the number but never acts on it.) See
|
||
[Compatibility](Compatibility.md).
|
||
|
||
## Redemption: the secret becomes a Thing
|
||
|
||
`TryRedeem(pawn, def)` is where a secret turns real:
|
||
|
||
1. Choose the material — the harvested `stuff`, or the def's default if it was made from stuff.
|
||
2. `ThingMaker.MakeThing(def, stuff)`.
|
||
3. If there was a `contentTag`, call `ApplyContent` to restore the contents.
|
||
4. `TryAdd` it to the pawn's inventory. If that fails, the thing is vanished and nothing is lost.
|
||
5. Remove the secret from the list.
|
||
|
||
The trigger for redemption is the breakout duty tree. `ThinkTreeInjection` runs once at startup and
|
||
mutates two duty defs directly (an XML patch on a `DutyDef` think tree silently no-ops in 1.6, so it
|
||
is done in C#):
|
||
|
||
| Duty | Injected | Where |
|
||
|---|---|---|
|
||
| `PrisonerEscape` | `JobGiver_ArmSelf`, then `JobGiver_UseContraband` | **after** `JobGiver_TakeCombatEnhancingDrug` |
|
||
| `PrisonerAssaultColony` | `JobGiver_ArmSelf`, then `JobGiver_UseContraband` | **before** `JobGiver_AIFightEnemies` |
|
||
|
||
`JobGiver_UseContraband` sits right beside `JobGiver_TakeCombatEnhancingDrug` — the one contraband
|
||
hook Ludeon wrote and then left reading an always-empty inventory. For each thing a pawn is hiding:
|
||
|
||
- **Inert contraband** (a drug, a stashed rifle — no `useWorker`): just materialise it and get out
|
||
of the way. Vanilla takes the drug via `JobGiver_SatisfyChemicalNeed`; a weapon in inventory is a
|
||
weapon. But a stashed **weapon** only comes out for someone who would actually use it
|
||
(`Disposition.WouldArmSelf`) — the same person who would not stop to pick a rifle off the floor
|
||
does not become a fighter because the knife was in their sock, and a pawn incapable of violence
|
||
never does at all. If they qualify, the weapon is moved from inventory into their equipment slot;
|
||
otherwise it stays hidden and they run. See [Improvised Weapons](Improvised-Weapons.md).
|
||
- **Active contraband** (a `useWorker` — e.g. Foul Play's throw-the-carboy): materialise it, then
|
||
hand off to the worker's `TryGiveJob`, which produces the actual use job.
|
||
|
||
## Confiscation and the door every route comes through
|
||
|
||
`Confiscate(pawn, def)` simply removes the secret. It **never becomes a Thing** — the warden found
|
||
it, and there is nothing to drop on the floor. (This is why searching is not a way for the colony to
|
||
*acquire* a prisoner's drugs; it is a way to *deny* them.)
|
||
|
||
At the other end, `Conceal(prisoner, def, ...)` is the public door **every** supply route comes
|
||
through — intake calls it, a bent warden calls it ([Corruption](Corruption.md)), and a smuggling
|
||
gangmate ([Institution: Gangs](Compatibility.md)) calls it. It hands a prisoner something already
|
||
finished and hidden, optionally with a `contentTag`. One door, many keys.
|
||
|
||
## Persistence
|
||
|
||
`MapComponent_Contraband.ExposeData` deep-saves the secret list, the per-pawn search cooldowns, and
|
||
the `intakeProcessed` set. On load it prunes any row whose pawn or def failed to resolve — prisoners
|
||
who died or left take their secrets with them, and a null key would throw on the next tick.
|
||
|
||
## Quick reference
|
||
|
||
| Constant | Value | Meaning |
|
||
|---|---|---|
|
||
| `CheckIntervalTicks` | 250 | poll cadence for intake / tunnels / improvisation |
|
||
| Intake base chance | 0.60 | per-item, before nature × nurture |
|
||
| Intake gate | `IsHeld` + `OnBody` | only held pawns, only body-hideable items |
|
||
| Hard-drug concealability | 0.75 | inferred |
|
||
| Other-drug concealability | 0.50 | inferred |
|
||
| Light-weapon concealability | 0.70 | `Mass ≤ 2.0`, hides on body |
|
||
| Heavy-weapon concealability | 0.35 | `2.0 < Mass ≤ 10.0`, `InCell` only |
|
||
| Unhideable mass | > 10.0 | not contraband |
|
||
|
||
---
|
||
|
||
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
|
||
Ward. Each stands alone; together they interlock.*
|
||
**
|