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.
These mechanisms are all built, compiled, and now confirmed writing correctly against a real, running game — see Project status below for exactly what's verified and what's still open.
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, and every write path is now confirmed working against a real, running game.
- 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 deploy cleanly into a real MO2/Proton install and load correctly.
- In-game validation: confirmed for game state, at the data level.
A pytest harness drives every slice against a live, running game over
DevBench (
adapters/skyrim/livetest/), and 14 of its 16 checks now pass: a real death event lands in the run log with the correct identity, a Chronicle grudge becomes a real vanilla relationship rank, an avoidance pair's AI-package flag actually flips, a vendor's markup multiplier actually caches from a real barter-directed grudge, and an evidence object actually spawns and survives a cell reload. One bug is open and blocking exactly 2 of the 16 checks: the save/load persistence tests, because reloading a save via the test harness currently silently no-ops (investigated at length — not yet root-caused; seedocs/design/simple-modlist-milestone.md). - Not yet confirmed: literal player-visible behavior. The above verifies that ChronicleBridge's writes land correctly in game state — it does not yet verify what a player watching the screen would see change as a result (e.g., two NPCs actually walking apart in real time, not just the underlying flag being set). That's the honest gap left before M5.
- 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, and every ChronicleBridge write path is now proven to correctly mutate game state in a live save. Whether that state change is something a player would actually notice on screen is the next thing to verify.
| 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 | Every slice confirmed live via an automated test harness | Substantially done — 14/16 checks pass; save/load persistence is the one open bug |
| 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 | A player watching the screen actually perceives the sim's effect, not just the underlying state change | Next |
| 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.)