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.

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'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. 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.)