Proposal: Addon Pack Format #3
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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-menubranch (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, hand-makable.pck, built with Godot pack export or the pack toolasset/+data/— no codeasset/+data/+script/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 isdeadair.Every file is referenced by a key:
<modid>.<tier>.<path segments, dotted, no extension>.Files reference each other by key only, never by raw path:
Why keyed
Save files store keys (
"def": "coolpistols.data.items.pm_custom"). On load, the first segment names the addon: ifcoolpistolsisn'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.jsonat its root:formatis the addon-format version the pack targets; the loader warns/refuses on mismatch instead of loading garbage.idmust match the<modid>used in the pack's folders.Load order and precedence
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:
.glb/.gltfviaGLTFDocument(skeletons/skins included),.png/.jpgviaImage→ImageTexture,.wavfrom bytes. (Ogg/mp3 support is thinner; the zip spec starts with wav.)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
.blendinto a valid addon pack:da_rigreference hand in the file and computes every finger bone's pos/rot relative to the wrist → writes the grips table.materialsarray.addon.jsonand the pack tree; outputs.zip(or.pckfor the script tier).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
<modid>.<tier>.<dotted path>— readable and save-friendly, but verbose. Acceptable?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.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.
AddonLoadermounts every pack, zips included, withProjectSettings.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 throughload():.gdat a new path;.gdcor.gd.remapfile the export writes, rather than the raw.gd);.tscnor.trescarrying a built-in script or pointing at one. Today, for example, a zip can add a descriptor todata/levels/whosescenepoints at a scene inside the zip, andsrc/core/level_loader.gd:33loads 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:
ZIPReader.addon.jsonanddata/**.jsonas text.GLTFDocumentfor glTF,ImagetoImageTexturefor images, WAV from bytes..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
.pcktier can bring code, so registration rules only need to cover.pck.docs/addons.mdis 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.glbin a zip) is never read, becauseload()follows the base game's.importremap. 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 callsload(MODEL_PATH)directly (src/player/arms_view.gd:117).