Files
institution/Wiki/core/Modder-API.md
T

239 lines
11 KiB
Markdown

# Modder API — building on Core
Core is a library. It has no XML, no defs, no UI — its entire purpose is to be *called*, by the other
Institution mods and by yours. This page is the public surface, with the one seam that matters most
(`Propensity.DeterrenceFactor`) explained in full.
> **This page is for modders.** Every other page in this wiki is written for players; this one is the
> technical surface for building on Core. If you just want to know how the mod plays, start at the
> [Home](Home.md) page and the [Propensity](Propensity.md) / [Criminal Record](Criminal-Record.md) /
> [Secured Context](Secured-Context.md) / [Treatment Engine](Treatment-Engine.md) pages instead.
Everything here is verified against the three source files in `Source/Core/`.
---
## The namespace is `Contraband`
Before anything else: **all of Core's types live in `namespace Contraband`**, not `Core` and not
`Institution`. This is historical — the engine was extracted from Institution: Contraband — and it is
not going to change under you, but it does mean your `using` looks slightly surprising:
```csharp
using Contraband; // Propensity, CriminalRecord, GameComponent_CriminalRecords,
// SecuredKind, SecuredContext, SecuredContexts,
// TreatmentProgram, TreatableConditionExtension
```
Reference the assembly `InstitutionCore.dll` (packageId `flan.institution.core`) and load your mod
after Core.
## The public surface at a glance
| Call | Returns | Creates state? | Use for |
|---|---|---|---|
| `Propensity.Nature(pawn)` | `float 0..1` | no | fixed trait-based disposition |
| `Propensity.Nurture(pawn)` | `float` (multiplier) | no | live circumstance multiplier |
| `Propensity.Would(pawn, baseChance, salt, cap=0.85)` | `bool` | no | the seeded yes/no for a behaviour |
| `Propensity.DeterrenceFactor` | `Func<Pawn,float>` field | — | the seam a justice layer fills |
| `GameComponent_CriminalRecords.For(pawn)` | `CriminalRecord` | **yes** (blank on first ask) | write paths |
| `GameComponent_CriminalRecords.PeekFor(pawn)` | `CriminalRecord?` | no | read paths |
| `SecuredContexts.Of(pawn)` | `SecuredContext?` | no | one pawn's hold kind |
| `SecuredContexts.OnMap(map)` | `IEnumerable<SecuredContext>` | no | every held/free pawn on a map |
| `TreatmentProgram.HasTreatableCondition(pawn)` | `bool` | no | is there a marked condition to work through? |
| `TreatmentProgram.ReduceConditions(pawn, strength)` | `void` | mutates hediffs | run one session's worth of treatment |
| `TreatmentProgram.FacilityQuality(pawn)` | `float 0.5..1.5` | no | scale a session by the room's quality |
| `TreatmentProgram.AdvanceRecovery(pawn, def, amount)` | `float` | **yes** (recovery hediff) | build a discharge / parole track |
All of these are `static` and null-safe. None throws on a null pawn; they return `0.05`/`1f`/`null`/
`false` as appropriate. See the [Propensity](Propensity.md), [Criminal Record](Criminal-Record.md),
and [Secured Context](Secured-Context.md) pages for the mechanics behind each.
---
## Asking "would this pawn?"
The idiomatic pattern for a new behaviour is: pick a **base rate**, pick a **stable salt** unique to
your question, and call `Would`. Do not re-implement the nature/nurture math — that is the whole point
of the shared engine.
```csharp
using Contraband;
// A unique, STABLE salt for this specific question. Any constant int works; keep it
// distinct from other questions so the rolls stay independent per behaviour.
private const int Salt_WouldStealMeds = 0x5EED01;
if (Propensity.Would(pawn, baseChance: 0.08f, salt: Salt_WouldStealMeds))
{
// this pawn, under these conditions, is the kind who does this
}
```
Because the roll is seeded on `pawn.thingIDNumber ^ salt`, the answer is a stable fact per pawn per
question and survives save/reload — so you can ask it repeatedly and get a consistent character, and
different salts give you independent facts about the same pawn. Pass a custom `cap` if `0.85` is the
wrong ceiling for your behaviour (e.g. a lower cap for something that should always keep a large
chance of not happening).
If you only need the ingredients — say, to display a disposition readout or gate a threshold — read
`Nature` and `Nurture` directly. Institution: Gangs, for instance, joins a pawn to a gang when
`Nature(pawn) * Nurture(pawn)` clears its own threshold, computed from these two calls rather than
from `Would`.
## Reading and writing records
Follow the one rule from the [Criminal Record](Criminal-Record.md) page — **write with `For`, read
with `PeekFor`** — so read paths never litter the save with blank records.
```csharp
using Contraband;
// WRITE: an event happened, so we need a record to write to.
CriminalRecord rec = GameComponent_CriminalRecords.For(pawn);
rec.contrabandMade++;
// READ: peek, and tolerate null (the pawn may have no record yet).
CriminalRecord existing = GameComponent_CriminalRecords.PeekFor(pawn);
if (existing != null && existing.reform < 0f)
{
// this pawn has been hardened
}
```
Core owns the storage and persistence; you own the meaning of your own events. Do not keep your own
per-pawn crime dictionary — that is exactly the fragmentation Core exists to prevent.
## Working across hold kinds
Reduce a pawn to its context and branch on the *predicate*, not the label:
```csharp
using Contraband;
SecuredContext? maybe = SecuredContexts.Of(pawn);
if (maybe is SecuredContext ctx && ctx.IsHeld)
{
// prisoner OR slave OR ward patient -- all covered, no special cases
}
// Sweep everyone the suite cares about on a map:
foreach (SecuredContext c in SecuredContexts.OnMap(map))
{
if (c.CanConceal) { /* consider hoarders too, not just the held */ }
}
```
`Of` returns `null` for anything the suite does not model (animals, mechs, the dead), so a `null`
check is your "not our business" branch.
## Running a treatment programme
Core carries the maths of "sustained attention in a secured facility reduces a marked condition and
builds a recovery track toward discharge" — the shared engine behind Ward's psychiatric care and
Justice's rehabilitation. Mark a hediff treatable with `TreatableConditionExtension`, then drive it
from your own job or interaction:
```csharp
using Contraband;
float strength = skill * TreatmentProgram.FacilityQuality(pawn) * share; // your own blend
TreatmentProgram.ReduceConditions(pawn, strength); // lower the condition
if (!TreatmentProgram.HasTreatableCondition(pawn))
{
TreatmentProgram.AdvanceRecovery(pawn, MyDefOf.Recovery, 0.15f * strength); // build discharge
}
```
You own the interaction mode, the job, the recovery hediff (give it a negative `severityPerDay` so it
decays without attention), and the discharge alert; Core owns only the numbers. Full formulas and the
consumer contract are on the **[Treatment Engine](Treatment-Engine.md)** page.
---
## The `DeterrenceFactor` seam
This is the most important extension point in Core, and the reason Core can remain a **dependency-free
leaf** while a colony-wide deterrence feedback loop runs *through* it.
### What it is
```csharp
// In Propensity:
public static System.Func<Pawn, float> DeterrenceFactor = _ => 1f;
```
A single static delegate. `Nurture` multiplies its result by `DeterrenceFactor(pawn)` as its last
step. Out of the box the delegate returns `1f` for every pawn — perfectly neutral — so **Core alone
behaves as if deterrence did not exist.**
### What it is *for*
Deterrence is a property of the whole colony's climate of order: a well-policed colony should deter
everyone a little; a lawless one should embolden them. That is a feedback loop — catching and
punishing *one* pawn changes the disposition of *the rest*. But deterrence itself is a justice-layer
concept, and Core must not know the justice layer exists.
The seam resolves the tension. Core defines *where* the colony's climate multiplies into disposition
(the last line of `Nurture`) without defining *what* that climate is. From the source:
> *"Deterrence lives in the Justice layer, so Core reads it through the seam above — neutral until
> Justice fills it. This one seam is what keeps Core a dependency-free leaf despite the deterrence
> feedback loop running through it."*
### How Institution: Justice fills it
When Institution: Justice loads, it assigns the delegate, pointing it at its own live deterrence
level. The shape it installs maps a `0..1` deterrence reading to a multiplier that straddles `1.0`:
```csharp
// Illustrative — this lives in Institution: Justice, not Core.
Propensity.DeterrenceFactor = pawn =>
{
float level = DeterrenceLevelFor(pawn.Map); // 0 = lawless, 1 = tightly policed
return Mathf.Lerp(1.3f, 0.7f, level); // lawless emboldens, order deters; neutral at 0.5
};
```
| Deterrence level | Factor | Effect on disposition |
|---|---:|---|
| `0.0` (lawless) | `1.3` | everyone ~30% more inclined |
| `0.5` (baseline) | `1.0` | neutral — a default colony sits exactly here |
| `1.0` (tightly policed) | `0.7` | everyone ~30% less inclined |
*(The `1.3 → 0.7` range, neutral at the `0.5` baseline, are Justice's numbers, documented here for context;
Core only guarantees the neutral `1f` default. Confirm against Institution: Justice's own source for
its current values.)*
### How to fill it yourself
If you are writing your own policing or morale layer and want it to feed disposition the same way,
just assign the delegate — ideally at game load, and ideally composing with whatever is already there
rather than clobbering it:
```csharp
using Contraband;
using UnityEngine;
// Compose: fold your factor into any existing one (e.g. Justice's) instead of overwriting.
var previous = Propensity.DeterrenceFactor;
Propensity.DeterrenceFactor = pawn => previous(pawn) * MyClimateFactorFor(pawn);
```
Two cautions. First, it is a single static field — if two mods both *assign* it (rather than compose),
the last one wins, so compose when you can. Second, keep your factor bounded and centred near `1.0`:
`Nurture` has no clamp on this term, so an extreme factor multiplies straight through into every
pawn's `Would` roll. Return `1f` when you have nothing to say, exactly as the default does.
### Why a delegate and not a dependency
The alternative — Core calling into Justice — would make Core depend on Justice, inverting the whole
topology and breaking Core's promise that it references *only the base game*. A single assignable
`Func<Pawn,float>`, neutral by default, lets the feedback loop run through Core while the arrow of
dependency still points only *at* Core. It is the smallest possible seam that buys the largest possible
decoupling, and it is why Core can sit at the bottom of the suite with zero dependencies of its own.
---
*Part of the **Institution** suite.*