Docs/Codex/Patches & diffs

Patches & diffs

Each game has a per-(game, branch) patch series. Entities ride a snapshot-on-change ledger: a row is written only when the entity's content_hash differs from the prior row on the same branch. Removals are tombstones (data IS NULL).

flowchart LR
   A["0.16.134<br/>row 12 written<br/>gear_score 50"]
   B["0.16.135<br/>no row written<br/>content_hash unchanged"]
   C["0.16.136<br/>row 287 written<br/>gear_score 55"]
   D["0.16.137<br/>row 901 written<br/>tombstone · data NULL"]
   A --> B --> C --> D
   classDef write fill:#0f1f0f,stroke:#16a34a,color:#86efac
   classDef skip  fill:#0f172a,stroke:#334155,color:#94a3b8
   classDef tomb  fill:#1f1208,stroke:#7c2d12,color:#fdba74
   class A,C write
   class B skip
   class D tomb

A typical Dark and Darker patch ships ~20k entity definitions but mutates only a few hundred. Snapshot-on-change writes those few hundred — not 20k × every patch.

Re-importing the same patch is always a no-op: added=0 changed=0 removed=0 unchanged=N.

Reading patch history

List patches on a branch:

The current head is in the manifest:

Point-in-time lookups

Add ?at_patch=<v> to read an entity as it was at a specific patch:

$hearth codex get dark-and-darker id.item.longsword_5001 --at-patch=0.16.134.8542

Viewing changes between patches

Field-level diffs are precomputed at import time into entity_diffs between adjacent patches on the same branch — queries are instant rather than reconstructing on demand. Each changed op is a flat list of { path, before, after } triples; removed and added carry full before / after snapshots. Paths are dotted and jq-compatible (e.g. data.primary.physical_damage.max).

jsonc
{
   "from":    { "version": "0.16.134+build.8542" },
   "to":      { "version": "0.16.135+build.8645" },
   "summary": { "added": 1, "removed": 1, "changed": 1, "unchanged": 4818 },
   "entries": [
      {
         "id":   "id.item.longsword_5001",
         "type": "item",
         "op":   "changed",
         "changes": [
            { "path": "data.gear_score",                  "before": 50, "after": 55 },
            { "path": "data.primary.physical_damage.max", "before": 22, "after": 25 }
         ]
      }
   ]
}

Whole-game delta:

Single-entity delta:

op is added, removed, changed, or unchanged. unchanged returns changes: [] — distinct from a 404, which means one of the patches doesn't exist.

Generating a changelog

The /diff endpoint returns the raw ledger. For a human-shaped summary (counts, names, grouped by type, optional markdown), use /changelog. The endpoint paginates by type to keep the wire size predictable even when a patch touches thousands of entities.

Overview

Bare /changelog returns just the top-line summary + per-type counts — bounded by the number of entity types (~70), so the payload stays small regardless of patch size:

jsonc
{
   "from":    { "version": "0.16.135.8645-2663" },
   "to":      { "version": "0.16.137.8664-2682" },
   "summary": { "added": 598, "removed": 114, "changed": 5587, "renamed": 117, "reicon": 16, "total": 6299 },
   "by_type": [
      { "type": "actor_status_effect", "added": 26, "removed":  0, "changed": 1010, "total": 1036 },
      { "type": "item",                "added": 11, "removed":  0, "changed":  139, "total":  150 },
      ...
   ]
}

With no from / to it diffs the latest imported patch against the one before it. Pass both explicitly for any range:

Drilling into one type

Pass ?type=<t> to load that type's entries inline as a flat section.entries[]. Each entry has op (added, removed, or changed) and the bits relevant to its op:

jsonc
{
   "summary": { ... },
   "by_type": [ ... ],
   "section": {
      "type":    "item",
      "added":   11,
      "removed":  0,
      "changed": 139,
      "total":   150,
      "limit":   100,
      "offset":    0,
      "next":    100,
      "entries": [
         { "id": "id.item.battle_worn_armor_fragment", "op": "added",   "name": "Battle-Worn Armor Fragment" },
         { "id": "id.item.zweihander_8001",            "op": "changed", "name": "Bloodthirst",
           "renamed": null, "icon_changed": false,
           "fields":  [ "data.abilities[6]", "data.abilities[7]", "links[14].rel" ] }
      ]
   }
}

Pagination

For heavy types (monster_effect, actor_status_effect can each have 1000+ entries), use ?limit= and ?cursor=:

The response carries next — the cursor for the following page (or null when no more entries remain). Re-issue the request with ?cursor=<next>:

limit defaults to 100 and caps at 500. Entries are sorted by op (added, then removed, then changed) and by name, stable across requests.

Markdown view

Add ?format=markdown to any of the above for a pasteable release-notes view. Each changed field is phrased as a plain-English sentence — numeric changes show direction, references are humanized, and codex-internal paths (links[*], _source.*) are hidden:

Sample (real diff, items section):

markdown
## Items  _(11 added, 0 removed, 139 changed)_

### Added
- **Battle-Worn Armor Fragment**
- **Brand of the Subservient**
- **Demon Wing Membrane**

### Changed
- **Bloodthirst**
  - Abilities #7 changed from Zweihander Riposte Attack 02 to Zweihander Unique Passive.
  - Abilities #8 changed from Zweihander Unique Passive to Zweihander Whirlwind.
  - Abilities #9 cleared (was Zweihander Whirlwind).
- **Castillon Dagger**
  - Abilities #7 set to Castillon Dagger Riposte Attack 02.

Every meaningful change is rendered — there's no per-entry cap. Codex-internal paths (links[*], _source.*, assets[*]) silently drop out because they're denormalized projections of the same data.* changes shown above; including them would double every bullet without adding information. The JSON response keeps the raw {path, before, after} triples for any tool that wants exact values.