Skip to content

feat(studio): the map workspace and the stage backend - #30

Merged
sp00ktober merged 3 commits into
mainfrom
pr-studio-map
Oct 1, 2026
Merged

sp00ktober merged 3 commits into
mainfrom
pr-studio-map

Conversation

@sp00ktober

@sp00ktober sp00ktober commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

For sp00ktober/MHFU-ModKit#67, 2 of 3. Stacks on #29.

  • Map workspace: the map editor's scene, edit session, document, renderer and panels, now on the shell.
    • The edit session rebuilds in process through stage/.
    • map.toml is written with tomli-w.
    • Ops are checked only by stage.ops.
    • The 1,668-line app.py is now one module per panel.
  • studio render map: views, a hunter view and contact sheets (supersedes sp00ktober/MHFU-ModKit#46).
  • Stage backend: moved in from the research repo's tools/, built on mhp-formats, mhfu.memory and ppsspp-debug watchpoints.
    • One op schema, in stage/ops.py.
    • Mesh, collision and texture writers.
    • The live push, with catch, hold and restore.
    • Commands: studio map inject|push|edit|exits|surfaces|budget|textures|verify.
  • Shared packages:
    • mhp-formats gains public file offsets (Pac.table, Stage.table, Collision.table, Hits.heads), Tri.to_bytes and a public Block.spans.
    • mhfu gains exit_fault, resident_files keeping only LOADED slots, and STAGE_SCRATCH.

Bugs from the issue:

  • Exit floors: of 555 exits in 226 stages, 532 land and 23 do not. There are no duplicate checks, and st126 is included.
    • Taking the floor nearest the destination fixes 4 multi-level exits the issue counted as not landing: st084→083, st142→144, st145→144, st218→220.
  • Pack winding: the stripper honours the face order. 274 triangles are now drawn the right way and 0 reversed; the old code drew all 274 reversed.
  • Materials: everything goes through Pmo.material. The copies of the rule are deleted. The old stage rule agreed with it on all 9,109 retail stage groups.
  • Vertex scale: 32768/128. Palettes are encoded with TmhImage.encode.

Found on the way:

  • Stripper first triangle: the stripper now rotates a strip's first triangle, so OBJ quads join. A 12-triangle box packs into 6 strips of 4 instead of 12.
    • Port models shrink with identical triangles and winding: Brute 1,156 → 1,042 primitives, Zinogre 1,732 → 1,576.
    • Four mhfu-port digests are updated.
  • Positions-only pack: it never bends a primitive it leaves.
  • sculpt: now a positions-only pack. It refuses where it would bend shared vertices, which the old one did silently.
  • replace: packs into the group's existing primitives. The old one wrote a new TRIANGLES primitive, which draws nothing in a quest area.
  • add and climb ops: the editor never emitted them, and they are now rejected with a finding.
  • Transforms and collision: a transform or move now carries its collision in the viewport and in export. The old inject already pushed it: 416 triangles in the village.
  • Collision planner:
    • Flat box colliders no longer crash it.
    • It skips deleted triangles.
    • Planning is vectorised: a gizmo commit in the village takes about 20 ms instead of 0.3 s.

Found in the live test, and fixed here:

  • Scratch overlap. STAGE_SCRATCH lay inside the framework's Lua slab, and the collision push wrote over memory it did not own after only a warning.
    • The framework now reserves the scratch on the emulator before any mod allocates. Its size, 64 KB, is in addresses.toml.
    • The push writes only zeros or its own last records, within that size.
  • Catch freeze. A catch whose slot went to another file stopped the CPU on every write until its deadline. It now ends at the first foreign write.

Test plan

  • uv run pytest with and without MHFU_DATA, pre-commit run --all-files, and the CI jobs in a Linux container
  • Parity against the research tools on 133 cases (st098, st139, st099, st107, st150, st063 and the village document): identical, or each difference classified as above
  • Renders: 16 of 17 cases are pixel-identical to the old editor with its decode bugs put back. The 17th (no MSAA) differs by 0.003%, the same as the old editor against itself.
  • The map golden passes on Mesa 22.3, 25.0 (exact hash), 25.2 and Metal. A shift test proves it fails a moved mesh, removed exits and deleted collision.
  • Real-window smoke, including an ImGuizmo drag
  • PPSSPP:
    • Village document: the mesh push was written and verified. The collision push lands in the reserved scratch, and a re-push takes its own previous records back out.
    • Snow-camp edit in a quest: the mesh push was written and verified. The catch lets go at the first foreign write; catching the re-read is Map inject: catch a quest area's PAC when it is read again #32.
    • Rebuilt Brute and Zinogre: both load through the redirect and spawn, and the Brute was live and not culled in the player's section. There is no image: this session cannot capture the screen.

🤖 Generated with Claude Code

@sp00ktober
sp00ktober changed the base branch from pr-studio-shell to main October 1, 2026 18:00
sp00ktober and others added 3 commits October 1, 2026 20:01
The map editor's scene, edit session, document, renderer and panels on the shell, and the
stage tools from the research repo's tools/ (edit, collision, textures, live push and catch)
on mhp-formats and mhfu.memory: `studio map inject|edit|push|exits|surfaces|budget|textures|
verify`, `studio render map`. Fixes on the way: pack winding (the stripper honours the face
order, and rotates a strip's first triangle so OBJ quads join), materials through
Pmo.material, the GE's vertex scale, palette encoding, exit floors on multi-level stages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The far backdrop was textured, and every renderer samples a plane that wide at the horizon its
own way. The synthetic backdrop is untextured; collision and markers get layer-alone images
checked on coverage across renderers, exact hashes on the pinned one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es the game

Found in the live test. STAGE_SCRATCH lay inside the framework's Lua slab, and the collision
push wrote over memory it did not own after a warning. The framework now reserves the scratch
on the emulator before any mod allocates (its size is in addresses.toml), and the push writes
only zeros or its own last records, within that size. A catch whose slot goes to another
file ends at the first foreign write instead of stopping the CPU on every write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sp00ktober
sp00ktober merged commit 4ad983b into main Oct 1, 2026
5 checks passed
@sp00ktober
sp00ktober deleted the pr-studio-map branch October 1, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant