Guides

Making a custom level

The MJOLNIR level pipeline is this game's answer to the old Halo Editing Kit: author a level in Blender, place vehicles and weapons like Sapien's scenario editor, bake it like Tool, and play it in the real game. This guide walks the whole loop. The file format reference is level_format.md.

Converting a classic CE map? That no longer needs a canvas's geometry: tools/level/convert_ce_map.sh builds the map its own structure BSP and a standalone multiplayer scenario in one command. See ce_map_conversion.md.

What a custom level is (and isn't) — read this first

A v1 custom level is a map variant: it runs on top of one of the 13 shipped campaign maps (the canvas), whose world and collision stay loaded. That's not a stopgap aesthetic choice — it follows from how the game works:

  • The Blam simulation owns all gameplay collision, and it collides only with its own world: the canvas map's BSP plus Blam objects. Unreal geometry — cooked or runtime-spawned — is walk-through, drive-through and shoot-through, always (verified 2026-08-19, multiplayer_investigation_notes.md).
  • Solid structures are therefore built the way the original Forge community built them: from placed Blam objects (scenery and crates), which carry their own collision. The bake turns your structures into scenario-tag placements.
  • Unreal meshes still have a place — as decor: any of the game's ~12k static meshes, tinted engine shapes, skyline dressing. They render; they do not exist to the simulation.

Everything solid comes from the scenario override; everything cosmetic comes from a runtime mod reading your level file. One .level.json describes both.

Setup (once)

WhatHow
mjolnir CLIcargo build --release -p blam-cli (or a release build)
Blender 4.0+ addoninstall tools/blender/mjolnir_level (its README)
MJOLNIRLevelLoader modscripts\install-bridge.ps1 -Mods MJOLNIRLevelLoader
Mesh catalog (for the decor library)mjolnir mesh list --paks <Paks> --out mesh-catalog.json (~3 min)

<Paks> is <game>\Meteorite\Content\Paks; set HCE_PAKS once and drop the --paks flags.

1 — Author in Blender

Open Blender, sidebar (N) → MJOLNIR:

  1. Set the level slug, the canvas scenario (B40 has the fullest vehicle palette: warthog, ghost, scorpion, banshee, wraith, shade), and the origin — the UE-centimeter point on the canvas map where Blender's world origin lands. Build around your origin; relocating the level later is a one-field edit.
  2. Lay the level out in meters, Z-up, as usual. Give every gameplay object a Role in the object panel:
    • Player start ×2+ (co-op needs two)
    • Vehicle / Weapon / Equipment with a type (warthog, sniper_rifle, overshield, … — the full list is defs/level/palette-map.json, which also says what each canvas already carries)
    • Structure with a Blam tag path and group (scenery/crates) — your ramps, walls and cover; these are the solid ones
    • Decor mesh for visuals (use Place shipped mesh… to browse the catalog as real-size proxy boxes)
  3. Export .level.json.

Keep the playable area on the canvas map's real ground: the simulation's floor is the BSP, wherever your meshes are. Yaw-only rotations are the safe kind; compound rotations compose differently between the engines.

2 — Bake and install

mjolnir level validate my_level.level.json
mjolnir level bake my_level.level.json --install-test

To ship the level as a mission of its own instead of an override of the canvas mission, add --standalone <CODE> (three characters): the bake writes the scenario as a new tag package under /Solo/<CODE>/, registers the codename (a cooked DT_Scenarios row carrying the level's title and description, plus the campaign's ScenarioList handle — the simulation builds its map registry from those tables at boot, see new_scenario_loading.md), and installs the decor file as <CODE>.level.json. --bsp INDEX=PAYLOAD clones structure BSP INDEX of the canvas scenario under the codename too, with PAYLOAD as its body, so custom collision no longer overrides the shipped mission's BSP:

mjolnir level bake examples/levels/bloodgulch.level.json --standalone BGL     --bsp 8=<staging>/bloodgulch/collision/bsp_01_1_start.bin     --world <bare.umap> --install-test

--world <bare.umap> gives the map an Unreal world of its own (the MapKit's empty world, renamed under the codename) instead of running on the canvas mission's. That needs the MJOLNIRLevelLoader mod with its native half, native/mjolnir_map_registry.dll (built by native/map_registry/build.ps1, shipped by the mods release): the engine resolves a mission's world by its short name through the AssetRegistry, which only knows shipped packages, and the DLL answers for worlds found in the installed containers (new_scenario_loading.md, "The world gate"). Without it the launch bounces back to the menu; without --world the map runs on the canvas world and needs nothing native.

Undo is deleting the six pakchunk996-MJOLNIRREG-<CODE>_P.* and pakchunk997-MJOLNIRMAP-<CODE>_P.* files. Two standalone maps installed at once each ship their own DT_Scenarios override, so only one registers; a merged registration container is the open item.

bake clones shipped placements and re-points them (position, palette entry, a fresh unique id — never a novel string id, which the native parser rejects), grows palettes when a structure's tag isn't in them yet, rewrites the insertion-point-0 player starts, and writes the result through the same byte-exact container verification mjolnir pack uses. --install-test puts the three pakchunk998-MJOLNIRLEVEL-* files in Paks and copies the level file to the loader's levels/ directory.

To iterate on decor only, edit the level file in place and run mjolnir_level_reload in the in-game console — no restart. Scenario-side changes (starts, vehicles, weapons, structures) need a re-bake and a game restart.

To uninstall: delete the three pakchunk998-MJOLNIRLEVEL-* files.

3 — Play

Start the game and launch the canvas mission through the game's own menu (Campaign → mission select, or the debug map menu via mjolnir_debug_ui). Do not use mjolnir_mission for a cold start — on current builds it loads the world but never starts the simulation (see the 2026-08-19 investigation notes).

In-game console (~):

  • mjolnir_level_status — what the loader found and spawned
  • mjolnir_level_reload — re-read the level file, respawn decor
  • mjolnir_level_clear — remove spawned decor

Two-player: the second player joins through the normal invite co-op flow and spawns at your second player start.

Limits (v1, honest)

  • The canvas mission still runs underneath — its AI, objectives and music. blam.clear can empty squads/bipeds/weapons/vehicles, but the mission's scripts still reference them; script stubbing is the next planned piece. Until then, build away from the mission's hot spots or embrace the chaos.
  • Vehicle/weapon/equipment types must already be in the canvas scenario's palette (structures grow their palettes automatically; cross-map palette grafts are untested — the referenced BP assets may not be streamed for that map).
  • Rotation conventions beyond yaw are provisional until verified in-game.
  • New worlds (own BSP, own lighting) need Blam collision generation — tracked separately; this pipeline's map variants don't attempt it.

How it hangs together

Blender (+ mjolnir_level addon)
   └─ my_level.level.json
        ├─ mjolnir level bake ──► scnr override container (_P) ──► Paks
        │        solid: starts, vehicles, weapons, equipment, structures
        └─ copied verbatim ─────► ue4ss/Mods/MJOLNIRLevelLoader/levels/
                 cosmetic: decor meshes, spawned on world load