Chronicle¶
A world that remembers. Chronicle is an external social-simulation service for Skyrim SE/AE: it gives every named NPC beliefs with provenance and strength, lets rumors spread and mutate as they pass from person to person, tracks grudges and obligations from what actually happened, and feeds all of it back into the game as behavior the player can perceive and shape.
The north star: if the player assassinates the Jarl of Whiterun, that should cascade -- a succession contest driven by the court's real relationships, an economic ripple through dependent merchants, a rumor that mutates as it travels to Riften, guard patrols that shift as a consequence of the simulation, not a scripted quest branch.
View on GitHub Read the architecture
Try it yourself¶
The headless simulation runs on any machine with no Skyrim install -- clone it, run three commands, watch 358 scenario tests pass in under four seconds:
The dashboard, live against a real run¶

The map view of dashboard/, showing rumor-stage glyphs (unheard, heard,
repeated, dormant, forgotten) and NPC markers over a real Whiterun layout,
against the north-star-01 scenario run. Not a mockup -- a screenshot of
the actual debug UI running against real simulation output.
How it works¶
(The diagrams below also live on the GitHub Pages site, rendered without GitHub's Mermaid-in-README quirks — same content, cleaner rendering.)
Chronicle is two things talking to each other over plain HTTP: a small C++ plugin living inside the Skyrim process, and a Python simulation service running natively on the host. The plugin never simulates anything — it only reads/writes game state and relays events. All the actual social reasoning (who believes what, how a rumor mutates, when a grudge cools) happens outside the game entirely, which is what makes it possible to test, replay, and inspect the whole simulation without ever launching Skyrim.
%%{init: {'flowchart': {'subGraphTitleMargin': {'top': 18, 'bottom': 12}}}}%%
flowchart TB
ENGINE["<span style='color:#4a3f1a'>Skyrim Engine (base game)</span>"] <--> BRIDGE["<span style='color:#4a3f1a'>ChronicleBridge -- SKSE C++ plugin</span>"]
BRIDGE =="HTTP: events in / state out"==> LISTENER
BRIDGE --> POS
subgraph PYTHON["Outside Skyrim"]
LISTENER["<span style='color:#22254a'>listener.py (HTTP)</span>"] --> CORE["<span style='color:#22254a'>chronicle/ engine</span>"] --> LOG["<span style='color:#22254a'>Frame log (JSONL)</span>"]
end
LOG --> DASH["<span style='color:#3a2245'>dashboard (Vue) -- debug UI</span>"]
subgraph MECH["Game-side mechanisms"]
direction TB
POS["<span style='color:#4a3f1a'>Position Streamer -- live NPC coords</span>"] ~~~ HYD["<span style='color:#4a3f1a'>Hydration Poller -- writes grudge as rank</span>"] ~~~ AVOID["<span style='color:#4a3f1a'>Avoidance Poller -- flips AI-package flag</span>"] ~~~ VEND["<span style='color:#4a3f1a'>Vendor Price Hook -- marks up barter price</span>"] ~~~ EVID["<span style='color:#4a3f1a'>Evidence Poller -- spawns object from a belief</span>"]
end
MECH -- "writes back into game state" --> ENGINE
classDef ingame fill:#fdf6d8,stroke:#c9b458,color:#4a3f1a;
classDef host fill:#e6e9f7,stroke:#8892c9,color:#22254a;
classDef debug fill:#f3e6f7,stroke:#a888c9,color:#3a2245;
class ENGINE,BRIDGE,POS,HYD,AVOID,VEND,EVID ingame
class LISTENER,CORE,LOG host
class DASH debug
style PYTHON stroke-dasharray: 6 4,fill:none,stroke:#8892c9
style MECH stroke-dasharray: 6 4,fill:none,stroke:#c9b458
Node label colors are set inline in the diagram source (not via
classDef's own color:, which modern Mermaid doesn't reliably apply to
HTML-rendered labels) because Material renders each diagram inside a
closed shadow root -- page-level CSS, !important or not, structurally
cannot reach inside it. Verified by reading Material's own bundled JS
(attachShadow({mode:"closed"})) rather than guessing after the fact.
The mechanisms are all built and compiled, but none has been confirmed working against a live, running game yet — see Project status below.
the mod: Skyrim engine + the ChronicleBridge SKSE plugin (C++)
the service: native Python, outside the game
the dashboard: Vue debugging UI, reads the service's logs
Everything under chronicle/ never imports anything Skyrim-specific —
it would run the exact same way against a different game entirely. The
only place allowed to know Skyrim exists is adapters/skyrim/.
How a rumor spreads and mutates¶
Gossip travels only through sampled encounters (shared location + schedule overlap), never a broadcast. Each retelling can mutate one detail and always loses some confidence:
sequenceDiagram
participant World as Game event
participant A as NPC A (witness)
participant B as NPC B
participant C as NPC C
World->>A: crime witnessed / NPC death
Note right of A: forms a Claim + Belief (confidence + strength)
Note over A,B: encounter sampled: shared location, probability roll
A->>B: tells the claim (tell-probability gate)
Note right of B: hears it -- may mutate one slot
Note over B,C: later encounter, different location
B->>C: retells it -- confidence decays another hop
Note right of C: forms its own belief: weaker, possibly mutated
How a rumor ages: heard, repeated, dormant, forgotten¶
Dormant means ~45 game-days with no retelling; Forgotten fires
independently, whenever the underlying belief's gist strength decays
past its floor, whichever stage it happens to be in:
stateDiagram-v2
[*] --> Heard: first exposure
Heard --> Repeated: retells it
Repeated --> Repeated: retold again
Heard --> Dormant: goes quiet
Repeated --> Dormant: goes quiet
Dormant --> Repeated: retold again
Heard --> Forgotten: gist decays out
Repeated --> Forgotten: gist decays out
Dormant --> Forgotten: gist decays out
Forgotten --> [*]
How a grudge turns into visible avoidance¶
Grudges decay continuously rather than clearing instantly, so a fresh
harm and a genuinely-forgiven one behave differently even at the same
raw severity. Avoiding fires once decayed severity crosses a
threshold; Cooled fires once it decays below a separate, lower
forgiveness floor:
stateDiagram-v2
[*] --> NoGrudge
NoGrudge --> Grudge: harm occurs
Grudge --> Avoiding: crosses threshold
Avoiding --> Grudge: drops back down
Grudge --> Cooled: fully forgiven
Avoiding --> Cooled: fully forgiven
Cooled --> Grudge: new harm
Cooled --> [*]
Avoiding is what a live game session would show as visible behavior —
two NPCs breaking off their usual routine to keep apart — driven purely
by decayed grudge severity, no scripting involved.
Project status¶
Chronicle is a headless social-simulation engine with a live Skyrim bridge deployed — but no visible in-game effects confirmed yet.
- v0.1 headless sim: done.
docs/v0.1-spec.md's full ~20-rule budget is implemented and scenario-proven: the claim/variant/belief store with the rumor stage machine (chronicle/claims.py), the social-state store — relationships, grudges, obligations, observer-local reputation (chronicle/social.py) — and schedule-driven encounter sampling (chronicle/schedule.py,chronicle/propagate.py). No Skyrim installation required to build, run, or test any of it. Schedules/relationships are still hand-seeded for the v0.1 Whiterun cast (chronicle/fixtures/) rather than derived from a full math-tier simulation. - ChronicleBridge builds and deploys: done. 7 SKSE slices (C++,
CommonLibSSE-NG) — live position streaming, death events, hydration,
avoidance, vendor-markup (barter-menu price hook), a crime-witness
cascade, and diegetic evidence — compile clean as a whole tree. The
DLL and a real 171-pair patched ESP are deployed into a live dev
install (
~/Games/ChronicleDev, correct load order). - In-game validation: not started. The bridge has never been
launched against a live game save. Every write path (hydration,
avoidance, vendor-markup, evidence) is unit/scenario-tested on the
Python side but unverified in a running game — see
docs/design/chronicle-bridge-verification-runbook.md. - No write path has been confirmed to produce a visible in-game effect yet. Every "out" slice (hydration, avoidance, vendor-markup, evidence) is compiled and Python-tested, but none has run against a live game — only "in" (positions, deaths) has ever been observed working.
- Named-cast coverage: 19 of 28.
IdentityMap.cpp'skNamedCastresolves 19 of Whiterun's 28 live-captured NPCs to a Chronicle identity (grown from 1); the rest stream as generic fallbacks the landed rules can't act on.
What this means: you can clone and run the simulation + dashboard today with no Skyrim install. You cannot yet install it as a mod and see the world react. That's the next milestone.
| Milestone | What it means | Status |
|---|---|---|
| M0: Headless proof | Belief cascade (Jarl dies → rumors spread → grudges form), scenario-tested, no game required | Done |
| M1: Bridge compiles | All 7 ChronicleBridge slices build clean against CommonLibSSE-NG | Done |
| M2: Bridge deploys | DLL + patched ESP in a real MO2 install, listener wired, ready to launch | Done |
| M3: In-game validation | Launch the game, confirm each slice live via the verification runbook | Next |
| M4: Named-cast coverage | Resolve the remaining 9 of 28 Whiterun NPCs to Chronicle identities | Mostly done (19/28, 9 remaining — see docs/design/next-phases-2026-08.md §0c) |
| M5: Visible "out" direction | Sim state actually changes what the player sees | Blocked on M3 |
| M6: Player-shareable | Downloadable artifact, install instructions, save-safety guarantee | Blocked on M5 |
See adapters/skyrim/README.md for per-slice status and
docs/design/next-phases-2026-08.md for the current plan.
(This section is transcluded directly from the repository's own README -- one source of truth, always current with the codebase.)