4.2 KiB
Data packs and resource packs
These are two completely separate, orthogonal systems. We deliberately keep them apart rather than collapsing them into a single generic "pack" concept. (The decision to have data packs register through the modding API instead of building a parallel system is documented in ADR-0007).
Resource packs
Resource packs are strictly client-side asset overlays. They contain textures, sounds, models, fonts, and language files, but absolutely zero logic.
A pack is just a directory tree that mirrors the structure of /assets/ and overrides files based on their path. When the renderer or asset loader looks for an asset ID, it resolves it against a stack of pack roots (starting from the base game, up through installed packs ordered by priority). The topmost hit wins. Ownership of this system sits entirely with the asset pipeline (currently in client, but it might move to a sibling assets crate if it grows). Pack authors never need to touch Lua.
A client's choice of resource packs is a purely local decision. The server has no say over them, and they are never checked during multiplayer modlist matching. The one exception to this is a server resource pack. A server can push a single cosmetic overlay (like a themed or total-conversion server) to connecting clients. This is a one-way push from server to client, applied squarely on top of the client's local stack. The server decides if it's optional (the client can decline and keep playing) or required (if the client declines or the fetch fails, the connection is rejected). Crucially, a server resource pack is still strictly assets/-only; it cannot contain data/ or scripts/, ensuring it can never accidentally affect authoritative gameplay state.
Data packs
Data packs handle declarative content definitions using JSON (though we might evaluate TOML or RON later). They define things like blocks, items, recipes, loot tables, biomes, and tags.
We intentionally did not build a parallel registration system for this. The loader simply parses the declarative files and calls the exact same Lua API that the engine and Lua mods use. This gives us one single source of truth:
data/blocks/stone.json → loader → blocks.register{ id = "stone", ... }
The loader logic belongs in scripting (or a sibling crate if it gets too large). Because they interface with the API, every data-pack schema is treated as a stable contract and versioned deliberately.
Declarative-first approach (ADR-0007): JSON and Lua are not meant to be interchangeable options. Anything that can be expressed purely as data (like the static fields of a block, item, recipe, loot table, biome, or tag) must be authored as data inside data/. We reserve Lua strictly for behavior, meaning logic that runs on an event or a tick. Because of this, a pure-data block needs absolutely zero Lua, and a data pack can register it entirely on its own. Only the behavioral half (if the block actually has any) comes from a Lua mod.
Our first-party engine content follows this exact same rule: we keep all pure-data core content in data/, and only behavioral systems live in scripts/. If a modder wants to avoid hand-authoring a massive amount of near-identical JSON files, they can use datagen. Datagen is code that emits data/ files at build time before the pack ships. The final generated files are the shipped artifact, while the generator code itself never runs at game load time.
Canonical load order
Later layers always override earlier ones:
base game (assets/scripts + assets/data)
→ data packs (adds/overrides declarative content)
→ Lua mods (has full API access)
→ resource packs (client-only asset overlays, loaded last so they dictate the final visuals)
Repo layout
/assets/
data/ # base-game declarative content
blocks/ items/ recipes/ ...
scripts/ # base-game Lua (behavior)
textures/ models/ sounds/ icons/ shaders/ # base-game assets
User-data layout
This is resolved at runtime using the directories (or dirs) crate:
<user-data>/
mods/ # Lua mods
datapacks/ # JSON content packs
resourcepacks/ # asset overlays (client only)