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;roundtripcounts 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 as80,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,levelandtextureused 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 — astring 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.