Proposal: Addon Pack Format #3

Open
opened 2026-10-21 17:13:33 +00:00 by muse-lantern · 2 comments
Member

Status: PROPOSAL — open for team input. This describes the addon format as designed in discussion (Zach + Lantern, 2026-10-21). It is not settled spec yet; the point of this issue is to get eyes on it before the loader is rebuilt around it. A loader v1 exists on the basic-menu branch (PR #2) but predates this design and will need rework to match whatever lands here.

Terminology

Content packages are called addons, not mods. Addons extend the game — adding new content is the primary design intent of the system. Alterations (an addon that changes how the game plays) are 100% acceptable, but extension is the intent, and the format is shaped by that.

Two tiers

Think Unreal: pak mods vs blueprint mods. Or Minecraft: datapacks vs mods.

Zip addon (data tier) PCK addon (full tier)
Container .zip, hand-makable .pck, built with Godot pack export or the pack tool
Can contain asset/ + data/ — no code asset/ + data/ + script/
Intended for Tweaks (damage, loot tables, capacity, speed) and new items built on existing frameworks — new weapons, props, equipment. The easy entry point. New frameworks, new systems, new mechanics — code included
Trust Data + assets only; nothing executable Scripts run with the game's full trust — treat like installing a program

An author should use the weakest tier that does the job.

Pack layout and keyed references

Inside a pack, everything lives under {asset,data,script}/<modid>/.... The base game's own modid is deadair.

Every file is referenced by a key: <modid>.<tier>.<path segments, dotted, no extension>.

coolpistols.zip
├── addon.json
├── asset/coolpistols/
│   ├── models/pm_custom.glb
│   ├── textures/pm_custom.png
│   ├── sounds/pm_shot.wav
│   └── icon.png
└── data/
    ├── coolpistols/
    │   ├── items/pm_custom.json      → coolpistols.data.items.pm_custom
    │   ├── weapons/pm_custom.json    → coolpistols.data.weapons.pm_custom
    │   └── grips/pm_custom.json      → coolpistols.data.grips.pm_custom
    └── deadair/
        └── loot/starter_cache.json   ← merge patch into a BASE table

Files reference each other by key only, never by raw path:

// data/coolpistols/items/pm_custom.json
{ "name": "PM Custom", "kind": "weapon",
  "weapon": "coolpistols.data.weapons.pm_custom",
  "model":  "coolpistols.asset.models.pm_custom",
  "magazine": "deadair.data.items.mag_9x18_8" }

// data/coolpistols/weapons/pm_custom.json
{ "framework": "deadair.data.frameworks.pistol",
  "damage": 42, "capacity": 8, "ammo_tags": ["9x18"], "fire_modes": ["semi"] }

Why keyed

Save files store keys ("def": "coolpistols.data.items.pm_custom"). On load, the first segment names the addon: if coolpistols isn't loaded, that content is inert, never deleted, and the game can report missing addons by name. It reactivates if the addon returns. (Extends the save-format rule in game-planning #28.)

Manifest

Every pack, both tiers, carries addon.json at its root:

{ "id": "coolpistols", "name": "Cool Pistols", "version": "1.0.0",
  "author": "Example Author", "tier": "zip", "format": 1,
  "description": "...", "depends": [] }

format is the addon-format version the pack targets; the loader warns/refuses on mismatch instead of loading garbage. id must match the <modid> used in the pack's folders.

Load order and precedence

  • The load order is an explicit list, top-down, first match wins, and it is user-configurable.
  • The base game sits at the bottom (lowest priority) by default — but the order can be changed, including moving the base game up.
  • Assets resolve whole-file: the first pack in the order that provides a key wins.
  • Data files deep-merge by key in load order, so two tweak packs touching different stats of the same item compose. Same-key conflicts: earlier in the list wins, and the loader logs the conflict — never silent.
  • A data file placed in another modid's namespace is a deliberate patch on that addon/base content (see the data/deadair/loot/... example above, which declares "_merge": true).

Runtime asset loading

Zip-tier assets must work in exported builds, where the engine cannot import raw files at runtime. So game code never loads moddable assets directly — it goes through a single AssetLoader that resolves a key in load order:

  • PCK tier: cooked resources via the normal resource loader.
  • Zip tier: runtime loaders — .glb/.gltf via GLTFDocument (skeletons/skins included), .png/.jpg via Image → ImageTexture, .wav from bytes. (Ogg/mp3 support is thinner; the zip spec starts with wav.)
  • Base game is the final fallback for every key.

Item-definition design lives elsewhere

What an item definition contains — materials slot arrays, grip tables, anatomy points, feed systems — is feature design, not packaging format, and has moved to #4 (Feature: Data-Driven Item Definitions). This proposal covers only how packs are laid out, referenced, ordered, and merged.

The blend → pack tool

Authoring happens in Blender; a build tool converts a .blend into a valid addon pack:

  • Exports the model GLB.
  • Reads a posed da_rig reference hand in the file and computes every finger bone's pos/rot relative to the wrist → writes the grips table.
  • Harvests material slot names → scaffolds the item JSON materials array.
  • Scaffolds addon.json and the pack tree; outputs .zip (or .pck for the script tier).
  • The posed reference hand is required only if the pack wants authored grips; without it the tool builds the pack with no grips file and the procedural default applies.

This is the same tool intended to build the official release asset pack from the private assets repo — first-party and community packs come out identical in shape.

Questions for reviewers

  1. Two tiers vs one: is the zip/pck split worth the extra spec, or should everything be pck?
  2. Key format: <modid>.<tier>.<dotted path> — readable and save-friendly, but verbose. Acceptable?
  3. Load order in the player's hands (including moving the base game up) — power we want to expose, or a footgun to lock down?
  4. Deep-merge for data: which file types should merge, and which should stay replace-only?
  5. Zip-tier audio limited to wav initially — fine?
  6. Should script/ in the pck tier get a defined registration hook only (no arbitrary autoloads), or full freedom?

An example pack built to this proposal is attached to this issue (coolpistols.zip — adds a pistol on the existing framework; greybox placeholder model, the structure is the point). Its key files are quoted above.

**Status: PROPOSAL — open for team input.** This describes the addon format as designed in discussion (Zach + Lantern, 2026-10-21). It is not settled spec yet; the point of this issue is to get eyes on it before the loader is rebuilt around it. A loader v1 exists on the `basic-menu` branch (PR #2) but predates this design and will need rework to match whatever lands here. ## Terminology Content packages are called **addons**, not mods. Addons *extend* the game — adding new content is the primary design intent of the system. Alterations (an addon that changes how the game plays) are 100% acceptable, but extension is the intent, and the format is shaped by that. ## Two tiers Think Unreal: **pak mods vs blueprint mods**. Or Minecraft: **datapacks vs mods**. | | **Zip addon** (data tier) | **PCK addon** (full tier) | |---|---|---| | Container | `.zip`, hand-makable | `.pck`, built with Godot pack export or the pack tool | | Can contain | `asset/` + `data/` — no code | `asset/` + `data/` + `script/` | | Intended for | Tweaks (damage, loot tables, capacity, speed) **and new items built on existing frameworks** — new weapons, props, equipment. The easy entry point. | New frameworks, new systems, new mechanics — code included | | Trust | Data + assets only; nothing executable | Scripts run with the game's full trust — treat like installing a program | An author should use the weakest tier that does the job. ## Pack layout and keyed references Inside a pack, everything lives under `{asset,data,script}/<modid>/...`. The base game's own modid is `deadair`. Every file is referenced by a **key**: `<modid>.<tier>.<path segments, dotted, no extension>`. ``` coolpistols.zip ├── addon.json ├── asset/coolpistols/ │ ├── models/pm_custom.glb │ ├── textures/pm_custom.png │ ├── sounds/pm_shot.wav │ └── icon.png └── data/ ├── coolpistols/ │ ├── items/pm_custom.json → coolpistols.data.items.pm_custom │ ├── weapons/pm_custom.json → coolpistols.data.weapons.pm_custom │ └── grips/pm_custom.json → coolpistols.data.grips.pm_custom └── deadair/ └── loot/starter_cache.json ← merge patch into a BASE table ``` Files reference each other by key only, never by raw path: ```json // data/coolpistols/items/pm_custom.json { "name": "PM Custom", "kind": "weapon", "weapon": "coolpistols.data.weapons.pm_custom", "model": "coolpistols.asset.models.pm_custom", "magazine": "deadair.data.items.mag_9x18_8" } // data/coolpistols/weapons/pm_custom.json { "framework": "deadair.data.frameworks.pistol", "damage": 42, "capacity": 8, "ammo_tags": ["9x18"], "fire_modes": ["semi"] } ``` ### Why keyed Save files store keys (`"def": "coolpistols.data.items.pm_custom"`). On load, the first segment names the addon: if `coolpistols` isn't loaded, that content is **inert, never deleted**, and the game can report missing addons *by name*. It reactivates if the addon returns. (Extends the save-format rule in game-planning #28.) ## Manifest Every pack, both tiers, carries `addon.json` at its root: ```json { "id": "coolpistols", "name": "Cool Pistols", "version": "1.0.0", "author": "Example Author", "tier": "zip", "format": 1, "description": "...", "depends": [] } ``` `format` is the addon-format version the pack targets; the loader warns/refuses on mismatch instead of loading garbage. `id` must match the `<modid>` used in the pack's folders. ## Load order and precedence - The load order is an **explicit list, top-down, first match wins**, and it is user-configurable. - The **base game sits at the bottom (lowest priority) by default** — but the order can be changed, including moving the base game up. - **Assets** resolve whole-file: the first pack in the order that provides a key wins. - **Data files deep-merge by key** in load order, so two tweak packs touching different stats of the same item compose. Same-key conflicts: earlier in the list wins, and the loader **logs the conflict** — never silent. - A data file placed in *another* modid's namespace is a deliberate patch on that addon/base content (see the `data/deadair/loot/...` example above, which declares `"_merge": true`). ## Runtime asset loading Zip-tier assets must work in exported builds, where the engine cannot import raw files at runtime. So game code never loads moddable assets directly — it goes through a single **AssetLoader** that resolves a key in load order: - **PCK tier:** cooked resources via the normal resource loader. - **Zip tier:** runtime loaders — `.glb`/`.gltf` via `GLTFDocument` (skeletons/skins included), `.png`/`.jpg` via `Image` → `ImageTexture`, `.wav` from bytes. (Ogg/mp3 support is thinner; the zip spec starts with wav.) - **Base game** is the final fallback for every key. ## Item-definition design lives elsewhere What an item definition *contains* — materials slot arrays, grip tables, anatomy points, feed systems — is feature design, not packaging format, and has moved to **#4 (Feature: Data-Driven Item Definitions)**. This proposal covers only how packs are laid out, referenced, ordered, and merged. ## The blend → pack tool Authoring happens in Blender; a build tool converts a `.blend` into a valid addon pack: - Exports the model GLB. - Reads a **posed `da_rig` reference hand** in the file and computes every finger bone's pos/rot relative to the wrist → writes the grips table. - Harvests material slot names → scaffolds the item JSON `materials` array. - Scaffolds `addon.json` and the pack tree; outputs `.zip` (or `.pck` for the script tier). - The posed reference hand is required **only if the pack wants authored grips**; without it the tool builds the pack with no grips file and the procedural default applies. This is the same tool intended to build the official release asset pack from the private assets repo — first-party and community packs come out identical in shape. ## Questions for reviewers 1. Two tiers vs one: is the zip/pck split worth the extra spec, or should everything be pck? 2. Key format: `<modid>.<tier>.<dotted path>` — readable and save-friendly, but verbose. Acceptable? 3. Load order in the player's hands (including moving the base game up) — power we want to expose, or a footgun to lock down? 4. Deep-merge for data: which file types should merge, and which should stay replace-only? 5. Zip-tier audio limited to wav initially — fine? 6. Should `script/` in the pck tier get a defined registration hook only (no arbitrary autoloads), or full freedom? An example pack built to this proposal is **attached to this issue** (`coolpistols.zip` — adds a pistol on the existing framework; greybox placeholder model, the structure is the point). Its key files are quoted above.
Author
Member

Migration note: the item-definition design that accumulated here — the anatomy addendum, gizmo point authoring, point shapes/config-in-name, and the feed systems (comments 562–564) — has moved to #4: Feature: Data-Driven Item Definitions — Anatomy, Points, Grips, Feed Systems, at Zach's direction: it's feature design, not packaging format. The Materials and Grips sections of the proposal above are likewise replaced with a pointer to #4. This issue keeps the packaging format only: tiers, layout, keyed references, manifest, load order, merging, and runtime asset loading.

**Migration note:** the item-definition design that accumulated here — the anatomy addendum, gizmo point authoring, point shapes/config-in-name, and the feed systems (comments 562–564) — has moved to **#4: Feature: Data-Driven Item Definitions — Anatomy, Points, Grips, Feed Systems**, at Zach's direction: it's feature design, not packaging format. The Materials and Grips sections of the proposal above are likewise replaced with a pointer to #4. This issue keeps the packaging format only: tiers, layout, keyed references, manifest, load order, merging, and runtime asset loading.

Two things the current loader can't deliver for this proposal. Both are worth settling before the loader is rebuilt around it.

1. The zip tier's "nothing executable" guarantee can't come from mounting.

AddonLoader mounts every pack, zips included, with ProjectSettings.load_resource_pack(path, true) (src/core/addon_loader.gd:69). Once a zip is mounted, every script, scene and resource in it is reachable through load():

  • a .gd at a new path;
  • a script at an existing path, replacing a base script that hasn't been loaded yet (in an exported build that means shipping the .gdc or .gd.remap file the export writes, rather than the raw .gd);
  • a .tscn or .tres carrying a built-in script or pointing at one. Today, for example, a zip can add a descriptor to data/levels/ whose scene points at a scene inside the zip, and src/core/level_loader.gd:33 loads it.

Any scene or resource the game loads by key could therefore run the addon's code. The table's "Data + assets only; nothing executable" holds only if the zip tier is never mounted.

The data tier needs its own reader:

  • Open the zip with ZIPReader.
  • Read addon.json and data/**.json as text.
  • Build assets with the runtime loaders the proposal already lists: GLTFDocument for glTF, Image to ImageTexture for images, WAV from bytes.
  • Refuse anything else: .gd, .gdc, .tscn, .tres, .res, .scn, .remap, and anything without an allow-listed extension.

This also answers review question 6 partly by construction: only the .pck tier can bring code, so registration rules only need to cover .pck.

docs/addons.md is honest about the current loader ("addons run with the game's full trust", docs/addons.md:75). The gap is only between that and this proposal's tier table.

2. Imported-asset overrides don't work through zips today.

The docs' arms example (a raw player_arms.glb in a zip) is never read, because load() follows the base game's .import remap. That's being fixed on the docs and test side in PR #18.

It's another reason the AssetLoader in this proposal is load-bearing rather than optional: until it exists, a hand-made zip of raw files can't replace a model, texture or sound at all. Only a pack that carries the engine's own imported files, in practice a pack exported by Godot, can do that. What a zip of raw files can override is content the game reads as plain files, like level JSON.

A small ask for whoever implements the AssetLoader: route ArmsView's model through it, since it currently calls load(MODEL_PATH) directly (src/player/arms_view.gd:117).

Two things the current loader can't deliver for this proposal. Both are worth settling before the loader is rebuilt around it. **1. The zip tier's "nothing executable" guarantee can't come from mounting.** `AddonLoader` mounts every pack, zips included, with `ProjectSettings.load_resource_pack(path, true)` (`src/core/addon_loader.gd:69`). Once a zip is mounted, every script, scene and resource in it is reachable through `load()`: - a `.gd` at a new path; - a script at an existing path, replacing a base script that hasn't been loaded yet (in an exported build that means shipping the `.gdc` or `.gd.remap` file the export writes, rather than the raw `.gd`); - a `.tscn` or `.tres` carrying a built-in script or pointing at one. Today, for example, a zip can add a descriptor to `data/levels/` whose `scene` points at a scene inside the zip, and `src/core/level_loader.gd:33` loads it. Any scene or resource the game loads by key could therefore run the addon's code. The table's "Data + assets only; nothing executable" holds only if the zip tier is **never mounted**. The data tier needs its own reader: - Open the zip with `ZIPReader`. - Read `addon.json` and `data/**.json` as text. - Build assets with the runtime loaders the proposal already lists: `GLTFDocument` for glTF, `Image` to `ImageTexture` for images, WAV from bytes. - Refuse anything else: `.gd`, `.gdc`, `.tscn`, `.tres`, `.res`, `.scn`, `.remap`, and anything without an allow-listed extension. This also answers review question 6 partly by construction: only the `.pck` tier can bring code, so registration rules only need to cover `.pck`. `docs/addons.md` is honest about the current loader ("addons run with the game's full trust", `docs/addons.md:75`). The gap is only between that and this proposal's tier table. **2. Imported-asset overrides don't work through zips today.** The docs' arms example (a raw `player_arms.glb` in a zip) is never read, because `load()` follows the base game's `.import` remap. That's being fixed on the docs and test side in PR #18. It's another reason the AssetLoader in this proposal is load-bearing rather than optional: until it exists, a hand-made zip of raw files can't replace a model, texture or sound at all. Only a pack that carries the engine's own imported files, in practice a pack exported by Godot, can do that. What a zip of raw files can override is content the game reads as plain files, like level JSON. A small ask for whoever implements the AssetLoader: route `ArmsView`'s model through it, since it currently calls `load(MODEL_PATH)` directly (`src/player/arms_view.gd:117`).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dead-air/engine#3
No description provided.