MJOLNIR CLI changelog
MJOLNIR CLIv0.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 a string id field. 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.json is 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. --set edits the body on the way; --install-test writes 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 .hsc source 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; .tif writes 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 .glb per 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 a manifest.json of counts and skips. --cell narrows to cells whose id contains a string, --nanite places the full-detail geometry, --hlod adds the hierarchical-LOD proxies, --dry-run reports without writing.

  • mjolnir ue get|set|roundtrip — read and edit the properties of any cooked Unreal package, not just a tag. get lists an export's properties as path = value rows (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,1 from 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, live and poke derive 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

  • set refuses 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-id says you know.

  • The stub .pak is written, not copied. new-tag, level and texture used to copy the smallest shipped .pak beside 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.

  • poke finds 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 live reports that no mission is loaded.
  • poke still refuses any field the format stores in a trailing section — a string id, a data, a tag 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.