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 tombA 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.8542Viewing 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).
{
"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:
{
"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:
{
"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):
## 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.