synvael/docs/adr/0007-declarative-content-via-modding-api.md
Serkyo cae434d227
Some checks are pending
CI / Rust Check & Lint (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Lua Lint & Format (push) Waiting to run
CI / LFS Pointer Guard (push) Waiting to run
docs(workspace): rewrite the subsystem notes and ADRs
2026-08-06 22:52:57 +02:00

35 lines
3.7 KiB
Markdown

# 0007. Declarative content loads through the modding API
- **Status:** Accepted
- **Date:** 2026-06-27
## Context
We can define content declaratively in data files (like JSON) rather than writing it out in Lua. This is perfect for things like blocks, items, recipes, loot tables, biomes, and tags.
A naive way to build this would be to give data packs their own dedicated registration code path straight into the engine's registries. However, that creates two parallel ways to register the exact same content. Over time, they inevitably drift apart, and we end up having to maintain double the surface area just to keep everything correct.
Note that this decision is specifically about **data packs** (declarative content). This is completely distinct from **resource packs**, which are just client-side asset overlays that carry no logic whatsoever. The two systems are orthogonal and we must not merge them into a single generic "pack" concept.
## Decision
The data-pack loader parses the declarative files and then **calls the exact same Lua API** that the engine and Lua mods use. There is only one registration path:
`data/blocks/stone.json` → loader → `blocks.register{ id = "stone", ... }`.
We will not build a parallel registration system. The loader logic belongs in the `scripting` crate (or a sibling crate if it gets too large). Because they flow through the same pipeline, first-party content is free to use either JSON or Lua, depending on whichever fits best. We treat every data-pack schema as a completely stable contract, versioned just as deliberately as the Lua API itself.
**Amendment (declarative-first):** The choice between JSON and Lua is not a free-for-all. Anything that can be expressed as raw data (like the static fields of a block, item, recipe, loot table, biome, or tag) must be authored as data inside the `data/` directory. Lua is strictly reserved for *behavior* (logic that actually runs on an event or a tick).
This means a purely static block needs absolutely zero Lua. A data pack can register a block, item, or other primitive entirely on its own, as long as it's purely declarative. The moment that primitive needs behavior, the behavior half must come from a Lua mod. To save modders from hand-authoring massive volumes of near-identical files, they can use **datagen**. Datagen is code that emits `data/` files at build time on the author's machine before the pack ships. The actual output of datagen (not the generator code itself) is the shipped artifact, and the generator never runs at game load time. This perfectly preserves our single runtime load path.
The canonical load order (where later layers override earlier ones) is:
Base game → Data packs → Lua mods → Resource packs (resource packs go last so the client visuals always win).
## Consequences
- We maintain one single source of truth for all registration. Declarative content and scripted content simply cannot diverge in behavior because they funnel into the exact same API.
- The declarative-first rule means data is the default and code is the exception. If a new primitive is added, it gets a data schema first. A Lua-only registration path is a red flag that the schema is missing something. The base game aggressively dogfoods the datapack path, keeping pure-data `core` content in `data/` and strictly placing behavioral systems in `scripts/`.
- Accepting a schema is a major, long-lived commitment because data packs in the wild will immediately depend on it.
- Resource packs remain entirely client-side, require zero server involvement, and are kept conceptually isolated from data packs.
- You can find the full subsystem details (load order, repo layout, user-data layout, and resolution semantics) in [`docs/packs.md`](../packs.md).