Status: 2026-10-02. Format v1 is proven with Blood Gulch: a pack built by
mjolnir map pack, installed as the launcher will install it and registered
by mjolnir level register, plays CTF. Phases 1 to 3 are done: the hub takes
map packs with a review queue, and the launcher installs them with their
dependencies and registers them (apps/launcher/src-tauri/src/maps.rs). Of
Phase 4, map pack, map runtime and build_ce_runtime.sh are done; the
packs are not published yet. The tag editor's map publishing remains: see
Phases.
Converted Halo CE maps (ce_map_conversion.md) reach players the way texture swaps and tag edits do. Each map is baked ahead of time, uploaded through the mod hub, stored in R2, and installed by the launcher. Players can publish their own maps the same way.
Three rules shape everything below:
- No game-derived bytes in git. A baked map clones shipped tags (the canvas scenario, the donor BSP, object definitions) and carries CE's geometry, textures and sounds. These bytes live only in R2, behind the hub. The repository holds the tools, the code mods and the design.
- No Unreal Editor on a player's machine. Baking needs the editor today (the materials and sounds cook), so maps ship pre-baked. Player publishing waits on the Rust cook (Phase 5).
- Anything executable still ships only from signed CI. A map pack is
data: containers, JSON, textures and Megalo variants (bytecode the game's
own interpreter runs, sandboxed like a game type). The loader and lobby
that read it are code mods, released by
release-mods.ymlas before.
The pieces
| Piece | Kind | Published by | Holds |
|---|---|---|---|
A map, e.g. ce-blood-gulch | hub content, with a map listing | map author | the map's containers, level file, registration record |
mjolnir-ce-runtime | hub content | us | everything every converted map shares |
| MJOLNIRLevelLoader, MJOLNIRLobby | code mods | signed CI | the loader, the multiplayer menu, the game types (.mglo) |
A map pack
A .mjolnir archive (mjolnir_format.md) of type map:
mjolnir.json manifest: type "map", plus the map block below
signature.json added when the tag editor publishes it
content/
MJOLNIRMAP-<CODE>_P.utoc the scenario, its BSP, and the map's own world
MJOLNIRMAP-<CODE>_P.ucas
MJOLNIRMESH-<CODE>_P.utoc the terrain mesh
MJOLNIRMESH-<CODE>_P.ucas
MJOLNIRCOOK-<CODE>_P.utoc the map's cooked CE textures, lightmaps and
MJOLNIRCOOK-<CODE>_P.ucas ambient sounds
map/
level.json the level file the loader reads
registration.json the bake's registration record (below)
docs/README.md optional
Format rules (version 1):
- Every package a map adds lives under
/Game/MJOLNIR/Maps/<CODE>/, so two maps never claim the same package. The level file names only/Gamepaths: either the map's own, or the runtime pack's. - A map cooks into a chunk of its own,
10000 + int(CODE, 36)(Blood Gulch,BGL, is 24853). APrimaryAssetLabelat the map's root sets it, so the map's cook never mixes with the runtime pack's chunk 988. - No
.mglo, no loose textures and no code. Amap/preview.pngis Phase 6.
The manifest's map block:
{
"schema_version": 1,
"name": "Blood Gulch",
"version": "1.0.0",
"type": "map",
"map": {
"code": "BGL",
"title": "Blood Gulch",
"modes": ["slayer", "ctf"],
"size": "large",
"players": { "min": 4, "max": 16 },
"vehicles": true
},
"compat": { "min_build": "2026.06.26.1097863.1" },
"deps": [{ "slug": "mjolnir-ce-runtime", "range": "^1.0.0" }]
}
codeis the three-character scenario codename. It is unique across the hub, which rejects a second map claiming a code someone else's map holds.modeslists the game types the multiplayer menu offers. Each one must be a variant the loader ships (slayer,ctf, ...).size,players,vehiclesandoriginare optional catalog metadata, what players filter the hub's map list by (below). A release that leaves one out keeps the value the listing already has.originiscustom_editionororiginal;classicis set by moderators only. Field rules are in mjolnir_format.md.- The registration record is what
mjolnir level bake --standalonewrites beside its output as<CODE>.registration.json: the codename, the canvas scenario it was cloned from, the title, the description and the world path. The launcher builds the shared registration container from these records (below).
A map pack carries no .mglo. Game types are code-reviewed scripts the
loader ships, so a map can only name them. Custom game types per map are
Phase 6.
The CE runtime pack
This pack is published once by us and updated rarely. It holds everything
converted maps share. tools/level/build_ce_runtime.sh builds it:
| Container | Holds |
|---|---|
pakchunk988-MJOLNIRMAT-Windows | the CE material masters (/Game/MJOLNIR/CE/M_CE_*) and their shader library; the CTF flag's textures (/Game/MJOLNIR/CE/CTF); the announcer and event sounds (/Game/MJOLNIR/Sounds/Events) |
pakchunk990-MJOLNIRCTFMESH_P | the CE flag mesh (/Game/MJOLNIR/CTF/SM_CE_Flag) |
pakchunk990-MJOLNIRFLAG_P, -STAND_P, -MOTL_P | the CTF flag weapon, its stand, and the object-type-list override that adds the flag |
pakchunk994-MJOLNIRSPAWN_P, 993-MJOLNIRTELES_P, 992-MJOLNIRTELER_P, 991-MJOLNIRTELE2_P | the spawn point and the teleporter sender, receiver and 2-way |
pakchunk990-MJOLNIRHPACK_P, -HPSPOT_P, -HPMESH_P | the health pack, its spawn spot and its mesh |
pakchunk990-MJOLNIRSPARTAN_P | the Spartan model with its body material on the body damage section (shipped on the head section, every bullet hit was a headshot) |
pakchunk984-MJOLNIRUI-Windows | the multiplayer screens and HUD widgets (custom_ui.md) |
pakchunk985-MJOLNIRMENU_P | the main menu with its own MULTIPLAYER button (multiplayer_menu.md). It replaces the shipped menu, so it is rebuilt for every game update |
A map's ambient sounds are not shared. They cook into the map's own chunk,
under /Game/MJOLNIR/Maps/<CODE>/Sounds, so a map ships only the sounds it
plays.
Its containers are content containers, so they install like any content
mod. Its version moves when a shared piece changes; maps depend on a
compatible range. mjolnir map runtime <out dir> --version X --sign
packages the builder's output as the mjolnir-ce-runtime content mod.
Installing
Where things go
| What | Where |
|---|---|
| Map and runtime containers | Meteorite/Content/Paks, named by the launcher (pakchunk{9xx}-MJOLNIRHUB-<slug>-<n>_P), with stub .pak siblings |
Map data (map/) | Binaries/<Win64|WinGDK>/ue4ss/MJOLNIRMaps/<CODE>/ |
| The map list | ue4ss/MJOLNIRMaps/maps.json, generated |
| The registration container | Paks/pakchunk996-MJOLNIRREG_P.{utoc,ucas,pak}, generated |
A container that carries a shader library keeps its own chunk number in
the name the launcher gives it (pakchunk988-MJOLNIRHUB-…), not a load
order number. UE opens a mounted pak's shader library by the number it
reads off the file name (ShaderCodeLibrary.cpp, OnPakFileMounted), so
the CE material masters renamed to pakchunk9NN would mount without
their shaders.
Map data lives outside Mods/MJOLNIRLevelLoader. The launcher digests
each code mod's folder to detect tampering, so files written into the loader's
folder would make it read as "Modified". The loader reads
ue4ss/MJOLNIRMaps first and still falls back to its own levels/ folder,
so maps converted locally with --install keep working.
The registration container
The simulation builds its map list at boot from the cooked DT_Scenarios
table and the campaign's ScenarioList
(new_scenario_loading.md). So one container must
list every installed map. A map cannot ship its own copy: two maps would
claim the same chunks, and only one would win. The container is also a
modified copy of shipped data.
The launcher therefore builds it locally after every map install, uninstall,
enable or disable. The build is mjolnir level register:
- read every enabled map's
registration.json; - clone
DT_ScenariosandDA_FirstPlayableCampaignfrom the player's own game files, and add one row and one handle per map (blam_pack::scenario::register); - write
pakchunk996-MJOLNIRREG_Pandmaps.json.
The launcher links blam-pack for this, so it never runs the CLI. It
rebuilds only when the set of enabled maps or the game build changed since
the last rebuild, which it records in MJOLNIRMaps/.mjolnirhub-registered.json.
The game build is a fingerprint of the shipped containers' names, sizes and
times. Launching the game checks it, so a game update gets a new
registration before the game reads the old one. A rebuild takes under a
second. A registration built by hand (mjolnir level register, no record)
is left alone until the launcher has a map to register.
Map folders the launcher writes carry a .mjolnirhub marker. It removes
only those. Uninstalling UE4SS from the launcher also removes the
registration container it built.
Dependencies
Installing a map installs what it depends on first. Its manifest's deps
come from the hub (the CE runtime pack; its newest release, as the version
range is not enforced yet). The code mods are implied by type map:
MJOLNIRCore, MJOLNIRLevelLoader, MJOLNIRLobby and MJOLNIRHud. Each is
installed when missing, updated when the launcher installed it and nobody
edited it, and switched on in mods.txt. A map whose code another
installed map holds is refused.
Install multiplayer (the launcher's Multiplayer page,
hub::install_multiplayer) runs this for every official map (GET /maps?official=1), then materializes and registers once. Maps already at
their newest release are skipped, so running it again is an update.
Publishing
Our maps
tools/level/convert_ce_map.sh maps/bloodgulch.map BGL out/bgl
mjolnir map pack out/bgl --code BGL --version 1.0.0 --sign
map packcollects the conversion's containers (*-<CODE>_P), its level file and its registration record into the archive, and writes the manifest.--signsigns it with this machine's device key (the tag editor's, under DPAPI), which the hub requires.- The tag editor publishes it, as it publishes content mods: it signs the
archive with the publisher's device key, then creates or updates the hub
mod and uploads the release (
POST /mods,POST /mods/{slug}/releases,PUT /releases/{id}/archive,POST /releases/{id}/complete). - CI cannot bake, since it has no copy of the game, so maps are published from a maintainer's machine.
Players' maps
Players publish the same way. Until the Rust cook lands, they need the Unreal Editor and the MJOLNIRMaterials project, which in practice means us.
Review. A map release from a new author does not go live on a passing
scan. The release stays pending with a row in release_reviews until a
moderator approves it (/moderation/releases), because a map carries
game-derived bytes and names game types the loader will run. Moderators and
authors at trust level 2 or higher skip the queue. Content releases keep
auto-publishing.
On the hub
A map is a content mod. mods.type is a trust tier and stays content;
what makes a mod a map is its row in map_listings (migration 0012), keyed
by the mod and holding the code, title, game types, whether it is an
official classic, and the catalog metadata (migration 0017). GET /mods/{slug} reports a map pack as type: "map" with its map_code.
- The scanner, for a manifest of type
map:- requires the
mapblock, and only a map archive may carry one; - requires
map/level.jsonandmap/registration.json, as valid JSON; - warns about containers not named for the map (
-<CODE>_P); - rejects a
codeanother mod already holds (map_code_taken).
- requires the
- The archive limit is the same 50 MiB as any content release.
- A passing map release writes its
map_listingsrow; a held one (review) writes it too, but the listing stays out of the catalog until the mod is published. depsare recorded inrelease_deps.
The catalog
GET /api/v1/maps lists published maps with their newest release, cover
image, and metadata:
| Field | Values |
|---|---|
size | small, medium, large, or null |
players_min, players_max | 1 to 16, or null |
vehicles | true, false, or null |
origin | classic (the 19 converted stock maps), custom_edition, original, or null |
Migration 0017 fills these for the 19 classics. Authors set them in the
manifest, and the mod's owner, its authors and moderators change them with
PATCH /api/v1/maps/{code} (only moderators may set or change classic).
With no parameters, /maps returns every map (up to 500) in one list: the
official classics first, then by title. The launcher's Install
multiplayer (/maps?official=1) and the game's map list rely on that
form, so filters and paging are opt-in:
| Parameter | Effect |
|---|---|
official | 1 official maps only, 0 community maps only |
mode | maps offering this game type |
q | title, code or summary contains this (up to 80 characters) |
size, vehicles, origin | exact match; a map that never declared the field is left out |
players | maps whose player range includes this count; a map with no range is kept |
sort | newest, downloads, rating or title; absent keeps official first, then title |
limit, cursor | page size 1 to 100, and the next_cursor of the previous page |
The response is { maps, next_cursor }. next_cursor is null on the last
page. The website's /maps page is built on the same query. GET /mods
takes map=0 to leave map packs out (the website's /mods page does) and
map=1 to list only them; every mod carries cover_url and map_code for
its card.
Launcher links
The website hands off to the desktop launcher through the mjolnir://
scheme, which launcher 0.13.0 and newer register:
| Link | Opens |
|---|---|
mjolnir://map/<CODE> | the map, ready to install (map cards and map pages) |
mjolnir://mod/<slug> | a mod's page in the launcher (mod pages) |
mjolnir://join/<lobby id> | joins a listed game (/games, and the live strip on /matches) |
Phases
- Map data outside the loader. The loader and the lobby read
ue4ss/MJOLNIRMaps;mjolnir level registerbuilds the registration container andmaps.jsonfrom the records there. - Map packs.
mjolnir map pack; the tag editor's map publishing; the hub's map listings, scanner rules and review queue. - The launcher (done). It installs maps, resolves dependencies, and builds the registration container on every change.
- The CE runtime pack, and our maps. The shared containers move into the runtime pack, and each map's textures and sounds cook into a chunk of its own. Then we publish Blood Gulch, Gephyrophobia and the rest of the stock maps.
- The Rust cook. Textures and sounds are written as cooked packages by
rewriting shipped ones, as meshes already are
(ue_mesh_write.md).
mjolnir map build <ce.map>then converts a map end to end with no Unreal Editor, and players can publish their own. - Later. Custom game types per map (reviewed
.mgloin a map pack), map previews (map/preview.png) in the menu and on the hub, and collections ("CE classics").