MJOLNIR CLI cli-v0.2.0
mjolnir live: what the running game has loaded
A new live command reads the running game's own table of loaded tags and its string-id registry, and poke uses that table to find a tag instantly instead of sweeping memory.
The simulation module the game loads keeps two tables that answer questions
this tool used to answer by scanning the whole process: which tags are loaded
and where each one's data is, and which string id names the running game
knows. Both are reached from fixed addresses in the module — addresses that
belong to one build, so the module is hashed first and an unknown build is
refused with its hash rather than read at a guess.
Added
-
mjolnir live status— the build, where the simulation module sits, how
many tags are loaded, how many string ids are registered. -
mjolnir live tags [--group weap] [--filter text] [--tsv out]— every
loaded tag with its handle, group, path and root address. -
mjolnir live string-ids [--find name] [--out file.json]— the
registry of names the running game accepts in astring idfield. A name
the game has not registered makes it reject the whole tag, so this is the
list to check an edit against;defs/hce/string-ids.jsonis the dump for
the current build in mission A30. -
mjolnir new-tag --group <g> --from <donor> --to <path>— put a brand-new
tag package in front of the game, in a container that registers it itself.
The wrapper is built from scratch for every group: the preload list from
the references the body carries, the Unreal binding from the donor (or
--asset-reference), a model's variants from the donor. The new path can
be anything. Measured in game twice: a collision model loaded by name the
moment a model tag was repointed at it, and a new Blueprint-bound
projectile was fired by the assault rifle.--setedits the body on the
way;--install-testwrites the triplet straight into the Paks folder. -
mjolnir extract [--group g]... [--filter text] [--out dir] [--dry-run] [--verify] [--shipped-only]— write tags out of the containers as files
in the layout Blam tooling expects (objects/weapons/rifle/smg.weapon),
no Python and no Oodle DLL needed. Mod-aware: an installed override wins
its tag unless--shipped-only. -
mjolnir values --json— the value tree as JSON: every field with its
name, type, offset, size and value, blocks with their elements and counts. -
mjolnir script --extract <dir>— a mission's.hscsource files
written out, one folder per scenario. -
mjolnir texture export --out x.dds— the cooked pixel format with
every mip, no re-encoding, virtual textures untiled;.tifwrites an
uncompressed RGBA TIFF of one mip. PNG as before. -
mjolnir mesh export --asset SM_… --out x.glb— a static or skeletal
mesh as glTF binary: every LOD, a primitive per material slot, metres and
+Y up. A mesh that ships as Nanite cluster pages is decoded from them and
exported at full detail first, with the classic fallback LODs after it; the
command reports the page, cluster and triangle counts. An exact asset name
wins over a longer one that contains it. -
mjolnir level export --mission a30 --out <dir>— a mission's Unreal
geometry as glTF: one.glbper World Partition cell (and the persistent
level), every placed static mesh at its world transform, instanced
components expanded from their per-instance matrices, Blueprint actors'
meshes resolved through their templates, and amanifest.jsonof counts
and skips.--cellnarrows to cells whose id contains a string,
--naniteplaces the full-detail geometry,--hlodadds the
hierarchical-LOD proxies,--dry-runreports without writing. -
mjolnir ue get|set|roundtrip— read and edit the properties of any
cooked Unreal package, not just a tag.getlists an export's properties
aspath = valuerows (ScalarParameterValues[4].ParameterValue = 100);
set --field <path> --value <text>changes one or more and writes the
package into an override container, reading it back first;roundtrip
counts how many exports of the matching packages decode and re-encode
byte for byte. Numbers, bools, names, strings, enums, soft object paths
and the vector-like native structs can be set; object references cannot
(that is import-map surgery). Proven in game on the assault rifle's
first-person material: two emissive tints set to red read back as
80,0,0,1from the running game's material instance. -
mjolnir zen-roundtrip— parse and re-serialize every shipped tag
wrapper (byte-exact for all 12,330 shipped) and count the derivation rules a
from-scratch wrapper relies on: every bare wrapper rebuilds byte for byte
from group, path and payload length; every wrapper with properties decodes
and re-encodes byte-exact and rebuilds from its spec. -
Live mode on a build without a measured profile. When the tag
module's hash matches no profile,liveandpokederive one from four
anchors in the module's code — instruction shapes that refer to the tag
table, the segment table, the string-id registry and the builtin ids,
each with its displacement wildcarded and decoded at run time — instead
of falling back to the sweep. On CU4 the derived profile equals the
measured one, RVA for RVA.
Changed
-
setrefuses an unregistered string id. The game rejects a whole tag
over one string id its registry does not know (the weapon simply
vanishes), so a string-id value is checked against the registry as the
game held it in mission A30 (defs/hce/string-ids.json, shipped in the
tool) and refused unless--allow-unknown-string-idsays you know. -
The stub
.pakis written, not copied.new-tag,leveland
textureused to copy the smallest shipped.pakbeside a container so
the game would discover it. They now write the 339-byte empty archive
themselves (ue_iostore::pak::write_stub), byte-identical to the shipped
stubs apart from the seed and hash that already differ between those. -
pokefinds a tag through the table on a known build — exact, and no
scan — and reads the engine's segment table to reach fields inside block
elements. On a build without a profile it sweeps as before and says so.
Known limits
- Profiles exist for the current Steam build (
2026.08.11.1121610.2, CU4).
A game update needs its eight addresses measured and added; until then the
sweep is the path. - The tables exist only once a mission is loading; at the menu
livereports
that no mission is loaded. pokestill refuses any field the format stores in a trailing section — a
string id, adata, atag reference— because changing one resizes the
tag on disk. Two of those do have a resident form that can be written
(docs/tag_table_and_string_ids.md), and the tag editor writes them; the
command line does not yet.
Install
Windows
scoop bucket add mjolnir https://github.com/devnull9090/mjolnir-core
scoop install mjolnirmacOS and Linux (Homebrew)
brew tap devnull9090/core https://github.com/devnull9090/mjolnir-core
brew install mjolnirDebian, Ubuntu — sudo dpkg -i mjolnir_0.2.0_amd64.deb
Fedora, RHEL, CentOS, openSUSE — sudo rpm -i mjolnir-0.2.0-1.x86_64.rpm
Or take an archive below, unpack it anywhere, and put that folder on
your PATH. The Linux binary is statically linked, so it runs on any
distribution without installing anything alongside it.
Downloads
| File | SHA-256 |
|---|---|
| mjolnir-0.2.0-windows-x64.zip | 719e2320c3c917befc2be4b2cb7d102f696f22a9f21595c5bee4b28c4a98e27e |
| mjolnir-0.2.0-linux-x64.tar.gz | 1d81eb8955eab1e58234b1a55bb79d14fc110ae5a223a7c4bf5f9035276c46e0 |
| mjolnir-0.2.0-macos-universal.tar.gz | dddad7c228033d6d86c92bfa9c5dfd3dc8bab0bc37ebfde3169149e8b2cf5c60 |
The macOS build is a universal binary — the same file runs on Intel
and on Apple Silicon. .deb and .rpm checksums are in the
checksums file attached below.
Verify your download
(Get-FileHash .\mjolnir-*.zip -Algorithm SHA256).Hashshasum -a 256 mjolnir-*.tar.gzCompare the output with the checksum above. They must match exactly.