Skip to content

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:

git clone https://github.com/ByteBard97/Chronicle.git
cd Chronicle
uv sync && make test

The dashboard, live against a real run

Chronicle dashboard map view: real Whiterun geography with rumor-stage overlay glyphs, live NPC markers, and a scrubbable timeline

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; see docs/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's kNamedCast resolves 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.)