Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Contributing

## Layout

One [uv](https://docs.astral.sh/uv/) workspace: every directory under `packages/` (libraries) and
`apps/` (the studio, the HUD, the Blender extension) is a member. `framework/` is the PRX in C++
with its Lua host, `ports/` the port manifests, `ppsspp/` the patched emulator, and
`docs/formats/` the file-format references.

## Setup

```bash
uv sync # every package, with the test and lint tools
uv sync --no-group qt # the same without the Qt test plugin, for a machine with no display
uv run pre-commit install # the checks below on every commit
```

The hooks run ruff (lint and format), `mypy --strict` over every package, shellcheck, and
[REUSE](https://reuse.software/), plus two of this repository's own:

- **Comment style.** Comments say what and why: no emoji, dates, retractions, "Phase N" or
issue numbers.
- **No raw game addresses.** Addresses come from `packages/mhfu/src/mhfu/addresses.toml`, which
generates the C header and the Lua table; Python reads it directly. A value that only looks
like an address ends its line with a `noaddr` comment.

`uv run pre-commit run --all-files` runs them all by hand.

## Tests

`uv run pytest` runs everything this machine can. A test that needs something it lacks skips:

| Needs | Set |
|---|---|
| Extracted MHFU or MHP3rd | `MHFU_DATA`, `MHP3RD_DATA`: each extraction's `data_files` directory (`mhp-formats extract`) |
| A real PPSSPP | `PPSSPP_BINARY` (or `PPSSPP_CONTAINER`) and `PPSSPP_GAME` |
| The framework's host builds | a `c++` on `PATH` |
| The studio's Qt tests | the `qt` group and a Qt that loads; `MHFU_UI_DISPLAY=1` for the GL window ones |

CI runs pre-commit, pytest on Python 3.11 and 3.14 without the `qt` group, and
lua-language-server over the Lua against `framework/lua/meta/mhfu.d.lua`. Run these locally, since
CI cannot: the Qt tests (`apps/studio/docker/run.sh` gives them a Linux GL), the Blender tests
(`uv run apps/blender/build.py test`), the tests against a real PPSSPP, and the PRX build
(`make -C framework`).

## Commits

[Conventional Commits](https://www.conventionalcommits.org/): `type(scope): lowercase summary`,
where the scope is the package or app (`mhp-formats`, `mhfu`, `mhfu-port`, `ppsspp-debug`,
`studio`, `hud`, `blender`, `framework`, `ppsspp`) or `repo`.

## No game data

Nothing extracted, dumped, disassembled or rendered from the games is committed: no ISO, no
extracted files or PACs, no textures or renders, no memory dumps, no disassembly listings or
dumped tables. Format references describe layouts in words and tables. `workspace/` is ignored
by git for your extractions.

## Licences

Every new file carries an SPDX header matching its area:

| Area | Licence |
|---|---|
| `packages/mhp-formats`, `packages/mhfu-port`, `apps/studio`, `apps/blender` | GPL-3.0-or-later |
| `ppsspp/patches` | GPL-2.0-or-later |
| everything else | MIT |

```python
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2026 Your Name
```

The REUSE hook checks it.
66 changes: 47 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,64 @@
Libraries, a runtime mod framework and tools for modding Monster Hunter Freedom Unite (PSP, EU
release ULES01213), and for porting monsters to it from Monster Hunter Portable 3rd.

> **Under construction.** This repository replaces the six
> [MHFU-ModKit](https://github.com/MHFU-ModKit) repositories, which stay the current release
> until it is done.
This repository replaces the six former MHFU-ModKit repositories (framework, example-mods,
monster-editor, hud, blender-addon, formats), which are archived.

## Layout

| Path | Contents |
|---|---|
| `packages/` | Python libraries: the debugger client, the file formats, the game (`mhfu`, which also holds the address map every language is generated from), the porter |
| `framework/` | The PRX plugin and its Lua host |
| `apps/` | The studio (monster and map editor), the HUD, the Blender extension |
| `ports/` | Manifests of the monsters ported from MHP3rd: the Brute Tigrex and the Zinogre |
| `ppsspp/` | PPSSPP patched for automation, its build, and a headless container to run it in |
| `examples/` | Example mods |
| `docs/` | Guides and format references |
| [`packages/mhp-formats`](packages/mhp-formats) | The games' file formats, and extracting a game from its ISO |
| [`packages/mhfu`](packages/mhfu) | The game: its address map (every language's is generated from it), live structs, automation, code analysis |
| [`packages/mhfu-port`](packages/mhfu-port) | The porter: MHP3rd monsters into MHFU |
| [`packages/ppsspp-debug`](packages/ppsspp-debug) | A client for PPSSPP's debugger |
| [`packages/modkit-testing`](packages/modkit-testing) | The pytest fixtures the packages share |
| [`framework`](framework) | The PRX plugin that runs mods in the game, its Lua host, and example mods in `framework/lua/examples/` |
| [`apps/studio`](apps/studio) | The studio: ported monsters and map edits |
| [`apps/hud`](apps/hud) | A live HUD next to PPSSPP |
| [`apps/blender`](apps/blender) | The Blender extension for monster models |
| [`ports`](ports) | Manifests of the monsters ported from MHP3rd: the Brute Tigrex and the Zinogre |
| [`ppsspp`](ppsspp) | PPSSPP patched for automation, its build, and a headless container to run it in |
| [`docs/formats`](docs/formats/README.md) | The file-format references |

## Development
## Getting started

Needs [uv](https://docs.astral.sh/uv/).
Needs git and [uv](https://docs.astral.sh/uv/). Nothing is prebuilt: every part builds from
this repository.

```bash
uv sync # every package, plus pytest and pre-commit
uv run pre-commit install # ruff, licence headers, comment style on each commit
uv run pytest
git clone https://github.com/MHFU-ModKit/modkit
cd modkit
uv sync # every package and app, with the test tools
uv run mhp-formats extract mhfu.iso workspace/extracted
export MHFU_DATA=$PWD/workspace/extracted/data_files
```

Tests that read game files take them from `MHFU_DATA` and `MHP3RD_DATA`, each a directory of
extracted `DATA.BIN` files, and skip when the variable is unset; tests against a running PPSSPP
take `PPSSPP_BINARY` and `PPSSPP_GAME` the same way (`ppsspp/build.sh` builds the patched one).
Nothing from the games is committed here: no ISO, no extracted files, no memory dumps.
The extraction is the game data the tools read, from your own copy of the game; `workspace/` is
ignored by git, and the studio finds it there without `MHFU_DATA`. An MHP3rd extraction, for
porting, goes to `workspace/extracted_mhp3` with `MHP3RD_DATA` (see
[`mhp-formats`](packages/mhp-formats/README.md#extracting-a-game)). Then:

- the PRX plugin and how to install it: [`framework`](framework/README.md#build)
- the Blender extension: [`apps/blender`](apps/blender/README.md#build)
- PPSSPP with the debugger patches, or in Docker: [`ppsspp`](ppsspp/README.md#build)

Each package's own README has its commands. `uv sync --no-group qt` leaves out the Qt test
plugin, for a machine without a display.

### Wheels

```bash
uv build --all-packages --wheel # every package's wheel into dist/
pip install --find-links dist mhfu-studio # elsewhere; its modkit dependencies come from dist/
```

`uv build --package NAME` builds one. The packages are not on PyPI.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). Nothing from the games is committed here: no ISO, no
extracted files, no memory dumps, no disassembly.

## Licence

Expand Down
41 changes: 30 additions & 11 deletions apps/blender/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@
A Blender 4.2+ extension for MHFU monster models: import a PAC, edit it, export it as a PAC and
push it into the running game. It reads and writes through `mhp-formats` and `mhfu-port`.

## Build

From a clone of modkit:

```bash
uv run apps/blender/build.py # apps/blender/dist/mhfu_blender-<version>.zip
uv run apps/blender/build.py test # build, install into a throwaway profile, run tests/ in Blender
```

`--blender PATH` (or `$BLENDER`) picks the Blender; the default is
`/Applications/Blender.app` on macOS and `blender` on `PATH` elsewhere. Install the zip from
Blender's Preferences > Get Extensions > Install from Disk.
The build packs and validates the zip with Blender itself: `--blender PATH` (or `$BLENDER`) picks
it; the default is the one in `/Applications/Blender.app` on macOS and `blender` on `PATH`
elsewhere. Install the zip from Blender's Preferences > Get Extensions > Install from Disk.

The zip carries its dependencies as pure-Python wheels at their `uv.lock` versions, so one zip
serves every OS. numpy is Blender's own; packages with no pure wheel (psutil, rabbitizer) are left
out, so the extension must not import the live-game chain that needs them.

## Example

Import `file_06185.bin` from an extracted MHFU (the Tigrex model), move some vertices in Edit
Mode, and press Push to Game in the MHFU tab: the PAC lands on the memory stick, and the report
gives the line a Lua mod needs to load it into the running game.

## Import

Expand Down Expand Up @@ -44,9 +58,9 @@ File > Export > MHFU PAC (.bin), or Export PAC in the MHFU tab, writes the selec
model PAC. An untouched monster comes out byte for byte.

- **Groups.** A group whose vertex count and faces are unchanged is written in place: positions,
UVs, the normals you changed, and weights over the joints its palette already holds. The PAC keeps its size. Adding or deleting vertices or faces, or weighting a vertex to a
joint the group has no palette slot for, rebuilds that group (8 joints at most) and the PAC
grows. A group whose object is deleted draws nothing; a vertex with no weight is refused.
UVs, the normals you changed, and weights over the joints its palette already holds. The PAC
keeps its size. Adding or deleting vertices or faces, or weighting a vertex to a joint the
group has no palette slot for, rebuilds that group (8 joints at most) and the PAC grows. A group whose object is deleted draws nothing; a vertex with no weight is refused.
- **Objects.** Moving a mesh object in Object Mode moves its vertices in the game. Modifiers and
shape keys are not applied. A PMO vertex has one UV, so a vertex on a UV seam is split into one
per UV, which rebuilds its group.
Expand All @@ -69,12 +83,17 @@ replaces the file in place (`file_NNNNN.bin`), a larger one takes the relocate p
(`file_NNNNN_grown.bin`). A Lua mod loads it; the report gives the call. The memory stick is the
extension's preference, else `$MHFU_MEMSTICK`, else PPSSPP's default folder.

The zip carries its dependencies as pure-Python wheels at their `uv.lock` versions, so one zip
serves every OS. numpy is Blender's own; packages with no pure wheel (psutil, rabbitizer) are left
out, so the extension must not import the live-game chain that needs them.
## Tests

Tests that need Blender take the `bpy` fixture and skip anywhere else; `build.py test` runs them
inside Blender with pytest from a cached directory under `.cache/`, never inside Blender itself.
inside Blender with pytest from a cached directory under `.cache/`.

## Status

Blender 4.2 or newer; tested on 4.2, 4.5 and 5.2. The Blender tests run locally only: CI has no
Blender.

## Licence

Licensed GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
[mhff](https://github.com/svanheulen/mhff).
18 changes: 17 additions & 1 deletion apps/hud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

A live HUD for MHFU next to PPSSPP: vitals, the quest map with the monsters on it, each
monster's AI cells and its species row, drawn in a pygame window. It needs a running PPSSPP
with the remote debugger on (Settings > Tools > Developer Tools > Allow remote debugger).
with its remote debugger on: Settings > Tools > Developer tools > Allow remote debugger.

## Install

In a clone of modkit, `uv sync`. For a wheel, see the root README's
[Wheels](../../README.md#wheels).

## Example

```bash
uv run hud # finds the PPSSPP on this machine and waits for it
Expand Down Expand Up @@ -35,3 +42,12 @@ backgrounds/village.png the village background (.jpg works too)

A `manifest.json` in `monsters/`, `items/` or `maps/` maps a slug to another file name. Map
calibration you save lands in `${XDG_CONFIG_HOME:-~/.config}/mhfu-hud/calibration.json`.

## Status

Reads vitals, the map, the monsters and their AI cells live, and writes nothing unless you stage
an edit in QUEST_PREP. Only the Snowy Mountains map is calibrated.

## Licence

MIT.
2 changes: 1 addition & 1 deletion apps/hud/src/mhfu_hud/item_db.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Item id (a PLAYER_BAG slot's u16) -> name and icon slug, for the ids matched in game so far."""
"""Item id (a PLAYER_BAG slot's u16) -> name and icon slug; only the ids listed are named."""

from __future__ import annotations

Expand Down
63 changes: 57 additions & 6 deletions apps/studio/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,64 @@
# mhfu-studio

One window for authoring MHFU mods offline: a ported monster (its clips, parts, hitboxes and
moves) and map edits (mesh, collision, textures), with the commands behind both.
One Qt window for authoring MHFU mods offline, with the commands behind it. Two workspaces:

- **Monster**: a ported monster's clips, labels, parts, hitboxes and moves, kept in its manifest
in [`ports/`](../../ports), checked against the base monster's data from the game, and
exported as a Lua module the framework loads (`<name>_hit.lua`).
- **Map**: edits to a stage's mesh, collision and textures, kept as edit lists in a `map.toml`
document and pushed into the running game through PPSSPP's debugger.

## Install

In a clone of modkit, `uv sync` installs it with Qt (PySide6) and the Qt test plugin;
`uv sync --no-group qt` leaves out only the test plugin, for a machine without a display. For a
wheel, see the root README's [Wheels](../../README.md#wheels).

The studio needs the extracted games. It takes each from its variable (`MHFU_DATA`,
`MHP3RD_DATA`), else from the folder chosen under Setup on its start page, else it looks for
`workspace/extracted` and `workspace/extracted_mhp3` in the working directory and up to four
folders above it. Extract there
([`mhp-formats`](../../packages/mhp-formats/README.md#extracting-a-game)) and nothing needs
setting. Sending to the game also needs PPSSPP's memory stick (`MHFU_MEMSTICK`, else PPSSPP's
default).

## Example

```bash
uv run studio # the window
uv run studio open PATH # the window with PATH open; also --workspace NAME, --size WxH
uv run studio --help # the commands: open, render, port, map
uv run studio # the window
uv run studio open ports/zinogre.toml # the window on a port
uv run studio port check ports/zinogre.toml # the same checks, on the command line
uv run studio port hit ports/zinogre.toml --deploy # zinogre_hit.lua onto the memory stick
```

Licensed GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
| Command | Does |
|---|---|
| `studio open [PATH]` | The window, with PATH open; `--workspace NAME`, `--size WxH` |
| `studio render selftest\|map\|monster` | A stage or a monster to PNG, without a window |
| `studio port scene\|clips\|align\|check\|hit` | A port's contents, clip coverage and labels, timing against the host, checks, and its Lua module |
| `studio map inject\|edit\|push\|exits\|surfaces\|budget\|textures\|verify` | A map document into the running game, one edit list offline or live, and what a stage holds |

`--help` on any of them says more.

## Tests

`uv run pytest apps/studio` runs offscreen; the Qt tests skip without the `qt` group, and the
few that need a real GL window run with `MHFU_UI_DISPLAY=1`. `apps/studio/docker/run.sh` runs
the tests, or any command, in a Linux container with Mesa's software GL, the renderer the render
goldens are pinned to:

```bash
apps/studio/docker/run.sh # the studio's tests
MHFU_UI_DISPLAY=1 apps/studio/docker/run.sh # with Qt on Xvfb: the GL ones too
apps/studio/docker/run.sh uv run studio render selftest -o out/
```

## Status

Both workspaces work. New map geometry has to fit the primitives a vertex group already has
(`studio map budget` says how much). The Qt tests run locally only: CI has no display.

## Licence

GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
[mhff](https://github.com/svanheulen/mhff).
4 changes: 2 additions & 2 deletions apps/studio/src/mhfu_studio/monster/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
actions, what it hits with and whether the timing fits; and the moves as a mod declares them.

The game asks for the base monster's anim N and plays whatever sits in the port's slot N, by
position, not meaning (`docs/MOD_PORTED_MONSTER.md` §1), unless a move paints its own clip on
the action. No toolkit here: the Actions panel shows the rows.
position, not meaning, unless a move paints its own clip on the action. No toolkit here: the
Actions panel shows the rows.
"""

from __future__ import annotations
Expand Down
4 changes: 2 additions & 2 deletions apps/studio/src/mhfu_studio/monster/panels/parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
" The damage row is which grid row scales a hit: a Tigrex wing is part 6 but row 5."
)
GRID_NOTE = (
"Grid writes are proven in the game (every byte 0xFF gave 411-damage hits); hurtbox"
" writes are still being tested."
"Export writes the damage grid and the hurtboxes into the game. Grid writes change the damage"
" a hit does; whether hurtbox writes move where hits land is unverified in game."
)


Expand Down
13 changes: 7 additions & 6 deletions apps/studio/src/mhfu_studio/monster/panels/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"""The Timeline: the transport at the engine's rate, and the base monster's action's frames on
the clip.

The RATE is the action's (2.0 and 2.4 both measured on one monster); the clip owns only its
SPAN. A gate past the clip's last frame never runs: the strip runs on past the end, shaded, and
draws it hollow there (at the far edge when it lies further out than a third of the clip)."""
The RATE is the action's (one monster plays some actions at 2.0, others at 2.4); the clip owns
only its SPAN. A gate past the clip's last frame never runs: the strip runs on past the end,
shaded, and draws it hollow there (at the far edge when it lies further out than a third of the
clip)."""

from __future__ import annotations

Expand Down Expand Up @@ -209,8 +210,8 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
on=lambda on: act("in place", lambda: self._in_place(on))(),
)
self.speed = kit.number(
tip="Clip frames played per game frame. The action sets it, not the clip: 2.0 and"
" 2.4 were both measured on one monster.",
tip="Clip frames played per game frame. The action sets it, not the clip: one"
" monster plays some actions at 2.0, others at 2.4.",
value=2.0,
step=0.1,
decimals=2,
Expand All @@ -222,7 +223,7 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
presets = [
kit.button(
f"{s:.1f}",
tip=f"Speed {s:g}, a rate measured in the game",
tip=f"Speed {s:g}, a rate the game plays actions at",
on=act("speed", partial(self._set, "speed", s)),
)
for s in OBSERVED_SPEEDS
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/mhfu_studio/monster/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"HITBOX_UNCHECKED": "Nothing to change in your port: the base monster's data has no attacks.",
"ATTACK_RECORD_UNKNOWN": MANIFEST + "delete this [[attack]].",
"ATTACK_VOLUME_UNKNOWN": MANIFEST + "give the [[attack]] a hit group the base monster has.",
"ATTACK_JOIN_INFERRED": "Nothing to change: only em75's attack table was measured.",
"ATTACK_JOIN_INFERRED": "Nothing to change: only em75's attack join is traced in game.",
}


Expand Down
Loading
Loading