239 lines
11 KiB
Markdown
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.*
|