A Forge pack is a normal .mjolnir content release with a validated catalog
of namespaced building pieces. The Hub lists them in its /forge catalog; the
launcher downloads published containers and exposes their catalog to Forge. A map
declares which pieces it supports when its scenario is baked. A saved layout
records the pack releases and palette bindings it used. Packs can be
updated without stranding the maps and layouts built with
them, and the format leaves room for other kinds of content
than building pieces.
For a practical first contribution, see creating a community Forge pack. For placing existing pieces, use the Forge building guide. This page describes the release format, build pipeline and dependency checks.
| Reference pack | Current version | Contents |
|---|---|---|
| Foundry | 0.2.0 | 24 original pieces on a 2 m module, including portal frames/pads and warm/red/blue lights |
| Foundry Wide | 0.2.0 | 38 pieces on a 4 m module; the original 18 retain their exact geometry and catalog order, with 20 additions for larger floors, walls, ramps, junctions, glass, doors, a gravity lift and green teleporter housings |
| Roadworks | 0.2.0 | Ten road pieces with 16 m clear decks, 24/48 m straights, R40 curves and 48 m ramps rising 4 m; open and guarded variants |
Crossline 0.3 combines Foundry and Foundry Wide in a 96 × 160 m Red versus Blue arena and utility showcase. Raised decks remain 4.5 m high inside a 9 m perimeter. Its 300 authored objects include two stock Warthogs, eight team-owned starts and four neutral midfield starts; 351 native slots remain available. Original Foundry's lights retain their size. The Forge Canvas stays a blank 64 × 64 m deck using Foundry, while Roadworks Circuit 0.2 provides a roughly 731 m course on a 320 × 320 m recovery deck with three stock Warthogs.
Road connectors describe exact source assembly; current Forge placement does not automatically dock them. Separate namespaces, and the rule that a compatible update keeps every existing piece, keep new geometry from silently resizing old builds. Historical Roadworks 0.1 used 8 m lanes, and Crossline 0.2 used the 18-piece Wide 0.1 kit in a 48 × 80 m arena; those layouts need an explicit compatible rebuild to use the larger sets.
Design research explains the piece selection. The validation record records the tested artifacts, gameplay checks and remaining coverage. Full vehicle-route, surface-placement lifecycle and second-machine utility checks remain separate work; a representative live check is not an exhaustive review of every piece.
Release contract
mjolnir.json keeps type: "content" so existing archive download and signature
handling applies. Its optional forge_pack block identifies the catalog:
{
"schema_version": 1,
"name": "Foundry",
"version": "0.2.0",
"type": "content",
"summary": "Modular industrial construction pieces",
"forge_pack": {
"schema_version": 1,
"id": "foundry",
"blocks": [
{
"id": "floor_2x2",
"name": "Floor 2 x 2",
"tag": "objects\\mjolnir\\forge\\foundry\\floor_2x2",
"category": "floors",
"dimensions_m": [4, 4, 0.5],
"snap_m": 0.5
}
]
}
}
- The pack ID must equal the Hub mod slug: 2–64 lowercase letters, digits or hyphens, beginning with a letter or digit.
- Block IDs and categories are 1–64 lowercase letters, digits, hyphens or underscores, beginning with a letter or digit. IDs are unique within a pack. Names contain 1–80 characters and cannot be blank.
- Every tag is exactly
objects\mjolnir\forge\<pack-id>\<block-id>, whatever the entry's kind. For a block it names a crate (bloc) with its associated model, physics and collision tags. - A catalog contains 1–256 blocks. Dimensions and optional snap are finite, positive metres. They describe authoring metadata; they do not change engine collision or configure the editor's global snap setting.
- Release versions are exact SemVer, at most 96 bytes. Hub versions exclude
+buildmetadata. A published version's bytes never change; see updating a pack for what each kind of new version may change. - Optional
depsuse the existing{ "slug": "other-pack", "range": "^1.2.3" }format. The launcher supports Rust SemVer requirements such as^1.2.3,~1.2.3and>=1.2.3, <2.0.0; a bare complete version is an exact pin.
Entry kinds
Each catalog entry may name its kind. Without one it is a block, which is
all the format carries today. The names below are reserved so that packs can
carry other objects once the bake, the launcher and Forge handle them:
kind | Object tag class | Status |
|---|---|---|
block | crate (bloc) | Supported |
scenery | scen | Reserved |
weapon | weap | Reserved |
vehicle | vehi | Reserved |
equipment | eqip | Reserved |
biped | bipd | Reserved |
The Hub, the packer (mjolnir forge pack) and the launcher refuse an entry of
any kind they do not support. A launcher meeting a pack built for a newer one
says to update instead of installing it, so a weapon is never handed to Forge
as a building block. Forge's light, lift and material code also skips entries
that are not blocks.
An entry's tag path stays objects\mjolnir\forge\<pack-id>\<entry-id> for every
kind. The tags an object needs besides its own (projectiles, effects, sounds,
animations, a weapon's first-person model) belong below
objects\mjolnir\forge\<pack-id>\ too, and Unreal assets below
/Game/MJOLNIR/Forge/<pack-id>/. A pack replaces nothing outside its own
namespace. The Hub enforces this for catalog tags today; checking every package
in a pack's containers against it is follow-up work before packs carry kinds
with dependency tags.
Required features and new fields
Catalogs stay at schema_version 1 while they only gain optional fields;
2 is kept for a change older launchers must not read at all, and they refuse
it with a request to update.
A pack lists in requires any feature it needs beyond what schema 1 readers
understand, for example "requires": ["turrets"]. None are defined yet: the
Hub accepts only features it knows, and a launcher refuses a pack that needs
one it does not, asking for an update. A pack that uses no new feature stays
installable by every launcher. The rule for extending the format:
- A new field that changes what MJOLNIR does with a pack (a new utility, a new
surface profile, a field inside
light,liftordoor) comes with arequiresfeature name, added to the Hub, the packer and the launcher in the release that supports it. - A field that only describes the pack (a credit, a preview hint) needs none; older readers ignore unknown top-level and entry fields.
Updating a pack
Releases follow SemVer, and maps and saved layouts accept any later release in the caret line of the version they were built with:
| Line | Same line | New line |
|---|---|---|
| 1.0.0 and later | the same major version: 1.4.2 serves a map built with 1.2.0 | 2.0.0 |
| 0.x | the same minor version: 0.2.7 serves one built with 0.2.0 | 0.3.0 |
Within a line, a release must keep every entry of the earlier ones: the same ID and kind, dimensions and snap, origin, and the heights where pieces meet. It may fix an entry's look and collision within that shape, and add new entries. In 0.x the patch number carries both fixes and additions. The Hub refuses a release that drops an entry of an earlier release in its line, or changes one's kind, dimensions or snap; origin and joins are the author's to keep. Anything else starts a new line, or a new pack: Foundry Wide is its own pack so that its larger pieces could never resize a Foundry build.
Maps record ^<version> for each pack they were baked with. When the launcher
has to download a pack it takes the newest release of that line, and updating
an installed pack within the line is allowed while maps that use it are
enabled. A saved layout records the version
it was saved with and loads with it or any later release of the same line; the
palette entry must still be the saved tag path exactly. Two maps or layouts
that need different lines of one pack cannot be enabled together.
A block may also contain a data-only light object (omit it for ordinary
pieces; null is also treated as absent):
"light": {
"offset_m": [0, 0, 3.1],
"color_rgb": [1, 0.05, 0.03],
"intensity_cd": 70,
"radius_m": 8
}
Offsets are relative to the piece's authored origin, in the same right-handed Z-up metres as its geometry. Each component must be finite and within −100..100 m. Linear RGB components are within 0..1. Intensity is positive and at most 10,000 candelas; radius is positive and at most 100 m. Unknown light fields are rejected. Hub, packer and launcher validate the same bounds. Forge derives the actor class from the pack/block IDs, attaches a movable inverse-square point light to its root, and converts metres to Unreal centimetres with Y reflected. It does not execute pack code or accept an arbitrary actor path. Illumination is capped at 64 active fixtures, is currently shadowless, and has no exposure multiplier. Deleted/pooled actors and world changes remove their attached lights. See Forge utilities.
Optional lift, door and surfaces fields provide the additional bounded
features used by Wide 0.2. See the utility contract
for their schemas and runtime limits, and material authoring
for pack-owned glass/portal assets and shader-chunk reservations. A portal
surface is visual; native teleporter markers still perform travel.
The archive contains mjolnir.json, cooked .utoc/.ucas pairs under
content/, optional docs/README.md and changes.json, and author signatures
when signed. Source .blend, glTF and collision JSON files are authoring inputs,
not directly installable content. The launcher generates container .pak stubs.
The generated Foundry release uses original visual geometry and collision with
game-specific actor/tag templates and shipped material references; its cooked
outputs require the supported game installation.
The Hub validates the catalog during the normal upload/scan/publish flow and
stores it on the release. GET /api/v1/releases/{id}/forge-pack returns the
published catalog, release version, archive SHA-256 and dependencies. The mod
detail page displays piece names, dimensions and snap. Choosing the Forge
category alone does not put an unvalidated archive in the Forge catalog.
Publishing still uses the existing registered author-key requirements and
review rules; this work does not deploy the Hub or publish a release.
Build the Foundry content
Run these commands from the repository root. $paks is a developer's installed
game directory, and generated donors/cooked content remain local under ignored
build/ directories. The examples use the debug tool builds for iteration.
$paks = 'C:\Program Files (x86)\Steam\steamapps\common\Halo Campaign Evolved\Meteorite\Content\Paks'
$cli = 'target\debug\mjolnir.exe'
cargo build -p blam-cli
cargo build -p ue-asset --example mesh_rewrite --example forge_actor
cargo build -p blam-pack --example package_add
python forge-packs/foundry/generate.py
python forge-packs/foundry/validate.py
& 'C:\Program Files\Blender Foundation\Blender 5.2\blender.exe' `
--background --factory-startup `
--python forge-packs/foundry/build_blender.py -- --render
& $cli extract --paks $paks --group collision_model --filter crate_space_a `
--shipped-only --verify --out forge-packs/foundry/build/donors
cargo run -p blam-sbsp --example forge_collision -- `
forge-packs/foundry/source/geometry.json `
forge-packs/foundry/build/donors/objects/props/covenant/crate_space_a/crate_space_a.collision_model `
forge-packs/foundry/build/blam-collision
python tools/forge/build_collision.py forge-packs/foundry/source/geometry.json `
forge-packs/foundry/build/blam-physics --mjolnir $cli --paks $paks `
--payload-out forge-packs/foundry/build/blam-collision
python tools/forge/build_pack.py forge-packs/foundry --paks $paks
& $cli forge pack forge-packs/foundry/build/game/all/mjolnir.json `
--content forge-packs/foundry/build/game/all/content `
--readme forge-packs/foundry/README.md `
--changes forge-packs/foundry/build/game/all/changes.json `
--out forge-packs/foundry/build/foundry-0.2.0.mjolnir --sign
--sign uses the existing local author device key registered with the Hub.
--sign-seed <file> uses an explicitly supplied key instead. Omit signing for an
offline build; the Hub does not accept an unsigned public upload. The packer
validates the catalog, container pairing and basic IoStore structure before it
writes the archive. The Hub independently scans the submitted archive.
build_pack.py --only <block-id> produces a reduced diagnostic inventory in
build/game/<block-id>/; package that directory only for local tests. Do not
publish a reduced diagnostic build with the same version as the full set. Use
a fresh output directory when changing the set's inventory: these
builders retain prior outputs.
The visual cooker writes a namespaced mesh and synchronized actor per piece. Blender/glTF geometry is converted to the game's units; the actor restores each piece's authored transform after mesh normalization: Foundry uses a bottom-centre origin, while Roadworks uses its entry floor. Actor visual collision is disabled so the simulation owns collision. The collision cooker builds triangle collision BSPs/MOPPs and fixed Havok polyhedra from the same source hulls. Composite pieces keep separate hulls so doors and windows remain open. The pack builder then creates namespaced collision, physics, model and crate tags and packages their visual dependencies.
Add a pack to a map
Add the desired subset to the level JSON before a level bake --standalone
build. Forge pack palettes currently require a standalone map; canvas overrides
are rejected because they do not install the new package preloads. Keep the
pack's cooked containers available in the bake's input Paks view so tag
references and preload dependencies resolve.
{
"blam": {
"map_variant": true,
"forge_packs": [
{
"slug": "foundry",
"version": "0.2.0",
"blocks": [
{
"id": "floor_2x2",
"tag": "objects\\mjolnir\\forge\\foundry\\floor_2x2"
}
]
}
]
}
}
This is a fragment to merge with the existing level definition. The bake adds one custom Forge palette in the authored pack/block order. There is no global slot assignment: independent maps choose their own subset and order. At most 63 packs and 200 custom references are accepted, and the completed palette must fit the engine's 255-entry total including standard objects. The baker checks both limits. It also adds each custom crate to the scenario wrapper's preload list, dependency bundle and container package-store imports, preserving the donor wrapper. A declared crate absent from the input Paks fails the bake.
Package the newly baked map with the existing mjolnir map pack command.
It preserves blam.forge_packs in the archived level JSON and adds a ^0.2.0
dependency on each listed pack (see updating a pack).
Installing the map through the
launcher resolves those dependencies, selecting a compatible published release
rather than blindly installing each dependency's latest version. The launcher
validates the enabled profile's dependencies before mounting content. A first
live map installation can mount new Forge pack dependencies before the map;
replacing an already installed pack requires a restart.
Installing a pack does not add slots to an already baked map. Such a map
must be rebaked and repackaged with blam.forge_packs before those pieces can be
placed. There is no automatic rebake or arbitrary runtime palette injection in
this implementation. Keep pack/block ordering stable when updating a map if
its existing layouts should continue to load.
Saved layouts and downloads
The launcher writes generated catalog files beneath
ue4ss/MJOLNIRForgePacks/: installed.json for the editor's display metadata and
installed.txt for the native identity check. These represent enabled,
verified releases and include exact archive SHA-256 values. Forge labels match
full tag paths, so two packs may use the same block ID without ambiguity.
At most 4,096 catalog blocks may be enabled across a profile, matching the
native inventory reader's bound; each map still has its smaller palette limit.
Saving a layout writes both <MAP>.mvar and <MAP>.mvar.packs beneath
ue4ss/Mods/MJOLNIRForge/layouts/. Keep the pair together when copying or sharing
a layout. The sidecar starts with forge-packs 1; each following tab-separated
row contains:
flat-palette-index full-tag-path pack-slug saved-version archive-sha256
The archive SHA-256 records which bytes the layout was saved with, for diagnosing a report; it is not a load condition.
Only custom pieces actually used by the layout produce rows. Every new .mvar
contains an fpk1 marker, including saves with no custom pieces, so removing
its sidecar cannot make it appear to be a legacy layout. Old layouts without
the marker remain usable when they do not reference custom pack tags.
Before launch, the launcher reads saved-layout pack requirements. A pack
installed at the saved version or a later one in its line serves the layout
as it is; otherwise the launcher downloads the newest release of that line
through the normal verified Hub install flow and enables the dependency
closure. Layouts saved with different releases of one line need the later
one. Layouts or enabled maps that need different lines of one pack are
written to the launcher log, and the launcher still restores everything else;
move the affected layout pair aside or use compatible map/pack versions.
Orphan sidecars without a matching .mvar and files in layout history do not
trigger downloads.
Before the engine recreates saved objects, native Forge checks each recorded flat palette index against the current full tag path and the installed pack's version. Missing inventory, a different palette binding, a missing sidecar on a new save, or an installed release outside the saved one's line (or older than it) refuse the layout. These checks prevent a saved custom index from becoming an unrelated object after a profile or map change. They do not migrate a layout to a different palette.
Downloads happen at launcher installation/launch time. Opening an incompatible
layout while the game is already running reports the dependency problem and
requires a launcher restart; the native DLL does not access the network.
Standalone Hub publishing of .mvar layout pairs and automatic map rebaking
remain follow-up work.
Verification
The repository tests cover manifest/catalog rejection and Hub release visibility, exact dependency selection and a local HTTP download fixture, signature/archive round trips, IoStore bounds, and native identity mismatches. Useful focused commands are:
cargo test -p blam-cli forge
cargo test --manifest-path apps/launcher/src-tauri/Cargo.toml --lib
& native/forge/test-pack-bindings.ps1
python forge-packs/foundry/validate.py
python forge-packs/foundry-wide/validate.py
python forge-packs/roadworks/validate.py
Run the Hub's npm test and npm run typecheck from hub/ when changing that
component. Geometry validation and the collision builder's ray checks verify
authored shapes offline. Before publishing
a playable pack, verify in game that each piece renders at its declared size,
ramps and platforms support players, apertures stay open, placement/duplication
and save/reload work, and a clean launcher profile fetches the exact dependencies.