Skip to content

Commit 60a59a5

Browse files
authored
Merge pull request #49 from MHFU-ModKit/docs/71
docs: READMEs, CONTRIBUTING and format references
2 parents e03deb4 + 231a7f7 commit 60a59a5

35 files changed

Lines changed: 1238 additions & 108 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Contributing
2+
3+
## Layout
4+
5+
One [uv](https://docs.astral.sh/uv/) workspace: every directory under `packages/` (libraries) and
6+
`apps/` (the studio, the HUD, the Blender extension) is a member. `framework/` is the PRX in C++
7+
with its Lua host, `ports/` the port manifests, `ppsspp/` the patched emulator, and
8+
`docs/formats/` the file-format references.
9+
10+
## Setup
11+
12+
```bash
13+
uv sync # every package, with the test and lint tools
14+
uv sync --no-group qt # the same without the Qt test plugin, for a machine with no display
15+
uv run pre-commit install # the checks below on every commit
16+
```
17+
18+
The hooks run ruff (lint and format), `mypy --strict` over every package, shellcheck, and
19+
[REUSE](https://reuse.software/), plus two of this repository's own:
20+
21+
- **Comment style.** Comments say what and why: no emoji, dates, retractions, "Phase N" or
22+
issue numbers.
23+
- **No raw game addresses.** Addresses come from `packages/mhfu/src/mhfu/addresses.toml`, which
24+
generates the C header and the Lua table; Python reads it directly. A value that only looks
25+
like an address ends its line with a `noaddr` comment.
26+
27+
`uv run pre-commit run --all-files` runs them all by hand.
28+
29+
## Tests
30+
31+
`uv run pytest` runs everything this machine can. A test that needs something it lacks skips:
32+
33+
| Needs | Set |
34+
|---|---|
35+
| Extracted MHFU or MHP3rd | `MHFU_DATA`, `MHP3RD_DATA`: each extraction's `data_files` directory (`mhp-formats extract`) |
36+
| A real PPSSPP | `PPSSPP_BINARY` (or `PPSSPP_CONTAINER`) and `PPSSPP_GAME` |
37+
| The framework's host builds | a `c++` on `PATH` |
38+
| The studio's Qt tests | the `qt` group and a Qt that loads; `MHFU_UI_DISPLAY=1` for the GL window ones |
39+
40+
CI runs pre-commit, pytest on Python 3.11 and 3.14 without the `qt` group, and
41+
lua-language-server over the Lua against `framework/lua/meta/mhfu.d.lua`. Run these locally, since
42+
CI cannot: the Qt tests (`apps/studio/docker/run.sh` gives them a Linux GL), the Blender tests
43+
(`uv run apps/blender/build.py test`), the tests against a real PPSSPP, and the PRX build
44+
(`make -C framework`).
45+
46+
## Commits
47+
48+
[Conventional Commits](https://www.conventionalcommits.org/): `type(scope): lowercase summary`,
49+
where the scope is the package or app (`mhp-formats`, `mhfu`, `mhfu-port`, `ppsspp-debug`,
50+
`studio`, `hud`, `blender`, `framework`, `ppsspp`) or `repo`.
51+
52+
## No game data
53+
54+
Nothing extracted, dumped, disassembled or rendered from the games is committed: no ISO, no
55+
extracted files or PACs, no textures or renders, no memory dumps, no disassembly listings or
56+
dumped tables. Format references describe layouts in words and tables. `workspace/` is ignored
57+
by git for your extractions.
58+
59+
## Licences
60+
61+
Every new file carries an SPDX header matching its area:
62+
63+
| Area | Licence |
64+
|---|---|
65+
| `packages/mhp-formats`, `packages/mhfu-port`, `apps/studio`, `apps/blender` | GPL-3.0-or-later |
66+
| `ppsspp/patches` | GPL-2.0-or-later |
67+
| everything else | MIT |
68+
69+
```python
70+
# SPDX-License-Identifier: MIT
71+
# SPDX-FileCopyrightText: 2026 Your Name
72+
```
73+
74+
The REUSE hook checks it.

‎README.md‎

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,64 @@
77
Libraries, a runtime mod framework and tools for modding Monster Hunter Freedom Unite (PSP, EU
88
release ULES01213), and for porting monsters to it from Monster Hunter Portable 3rd.
99

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

1413
## Layout
1514

1615
| Path | Contents |
1716
|---|---|
18-
| `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 |
19-
| `framework/` | The PRX plugin and its Lua host |
20-
| `apps/` | The studio (monster and map editor), the HUD, the Blender extension |
21-
| `ports/` | Manifests of the monsters ported from MHP3rd: the Brute Tigrex and the Zinogre |
22-
| `ppsspp/` | PPSSPP patched for automation, its build, and a headless container to run it in |
23-
| `examples/` | Example mods |
24-
| `docs/` | Guides and format references |
17+
| [`packages/mhp-formats`](packages/mhp-formats) | The games' file formats, and extracting a game from its ISO |
18+
| [`packages/mhfu`](packages/mhfu) | The game: its address map (every language's is generated from it), live structs, automation, code analysis |
19+
| [`packages/mhfu-port`](packages/mhfu-port) | The porter: MHP3rd monsters into MHFU |
20+
| [`packages/ppsspp-debug`](packages/ppsspp-debug) | A client for PPSSPP's debugger |
21+
| [`packages/modkit-testing`](packages/modkit-testing) | The pytest fixtures the packages share |
22+
| [`framework`](framework) | The PRX plugin that runs mods in the game, its Lua host, and example mods in `framework/lua/examples/` |
23+
| [`apps/studio`](apps/studio) | The studio: ported monsters and map edits |
24+
| [`apps/hud`](apps/hud) | A live HUD next to PPSSPP |
25+
| [`apps/blender`](apps/blender) | The Blender extension for monster models |
26+
| [`ports`](ports) | Manifests of the monsters ported from MHP3rd: the Brute Tigrex and the Zinogre |
27+
| [`ppsspp`](ppsspp) | PPSSPP patched for automation, its build, and a headless container to run it in |
28+
| [`docs/formats`](docs/formats/README.md) | The file-format references |
2529

26-
## Development
30+
## Getting started
2731

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

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

36-
Tests that read game files take them from `MHFU_DATA` and `MHP3RD_DATA`, each a directory of
37-
extracted `DATA.BIN` files, and skip when the variable is unset; tests against a running PPSSPP
38-
take `PPSSPP_BINARY` and `PPSSPP_GAME` the same way (`ppsspp/build.sh` builds the patched one).
39-
Nothing from the games is committed here: no ISO, no extracted files, no memory dumps.
43+
The extraction is the game data the tools read, from your own copy of the game; `workspace/` is
44+
ignored by git, and the studio finds it there without `MHFU_DATA`. An MHP3rd extraction, for
45+
porting, goes to `workspace/extracted_mhp3` with `MHP3RD_DATA` (see
46+
[`mhp-formats`](packages/mhp-formats/README.md#extracting-a-game)). Then:
47+
48+
- the PRX plugin and how to install it: [`framework`](framework/README.md#build)
49+
- the Blender extension: [`apps/blender`](apps/blender/README.md#build)
50+
- PPSSPP with the debugger patches, or in Docker: [`ppsspp`](ppsspp/README.md#build)
51+
52+
Each package's own README has its commands. `uv sync --no-group qt` leaves out the Qt test
53+
plugin, for a machine without a display.
54+
55+
### Wheels
56+
57+
```bash
58+
uv build --all-packages --wheel # every package's wheel into dist/
59+
pip install --find-links dist mhfu-studio # elsewhere; its modkit dependencies come from dist/
60+
```
61+
62+
`uv build --package NAME` builds one. The packages are not on PyPI.
63+
64+
## Contributing
65+
66+
See [CONTRIBUTING.md](CONTRIBUTING.md). Nothing from the games is committed here: no ISO, no
67+
extracted files, no memory dumps, no disassembly.
4068

4169
## Licence
4270

‎apps/blender/README.md‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,28 @@
33
A Blender 4.2+ extension for MHFU monster models: import a PAC, edit it, export it as a PAC and
44
push it into the running game. It reads and writes through `mhp-formats` and `mhfu-port`.
55

6+
## Build
7+
8+
From a clone of modkit:
9+
610
```bash
711
uv run apps/blender/build.py # apps/blender/dist/mhfu_blender-<version>.zip
812
uv run apps/blender/build.py test # build, install into a throwaway profile, run tests/ in Blender
913
```
1014

11-
`--blender PATH` (or `$BLENDER`) picks the Blender; the default is
12-
`/Applications/Blender.app` on macOS and `blender` on `PATH` elsewhere. Install the zip from
13-
Blender's Preferences > Get Extensions > Install from Disk.
15+
The build packs and validates the zip with Blender itself: `--blender PATH` (or `$BLENDER`) picks
16+
it; the default is the one in `/Applications/Blender.app` on macOS and `blender` on `PATH`
17+
elsewhere. Install the zip from Blender's Preferences > Get Extensions > Install from Disk.
18+
19+
The zip carries its dependencies as pure-Python wheels at their `uv.lock` versions, so one zip
20+
serves every OS. numpy is Blender's own; packages with no pure wheel (psutil, rabbitizer) are left
21+
out, so the extension must not import the live-game chain that needs them.
22+
23+
## Example
24+
25+
Import `file_06185.bin` from an extracted MHFU (the Tigrex model), move some vertices in Edit
26+
Mode, and press Push to Game in the MHFU tab: the PAC lands on the memory stick, and the report
27+
gives the line a Lua mod needs to load it into the running game.
1428

1529
## Import
1630

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

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

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

7688
Tests that need Blender take the `bpy` fixture and skip anywhere else; `build.py test` runs them
77-
inside Blender with pytest from a cached directory under `.cache/`, never inside Blender itself.
89+
inside Blender with pytest from a cached directory under `.cache/`.
90+
91+
## Status
92+
93+
Blender 4.2 or newer; tested on 4.2, 4.5 and 5.2. The Blender tests run locally only: CI has no
94+
Blender.
95+
96+
## Licence
7897

79-
Licensed GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
98+
GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
8099
[mhff](https://github.com/svanheulen/mhff).

‎apps/hud/README.md‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
A live HUD for MHFU next to PPSSPP: vitals, the quest map with the monsters on it, each
44
monster's AI cells and its species row, drawn in a pygame window. It needs a running PPSSPP
5-
with the remote debugger on (Settings > Tools > Developer Tools > Allow remote debugger).
5+
with its remote debugger on: Settings > Tools > Developer tools > Allow remote debugger.
6+
7+
## Install
8+
9+
In a clone of modkit, `uv sync`. For a wheel, see the root README's
10+
[Wheels](../../README.md#wheels).
11+
12+
## Example
613

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

3643
A `manifest.json` in `monsters/`, `items/` or `maps/` maps a slug to another file name. Map
3744
calibration you save lands in `${XDG_CONFIG_HOME:-~/.config}/mhfu-hud/calibration.json`.
45+
46+
## Status
47+
48+
Reads vitals, the map, the monsters and their AI cells live, and writes nothing unless you stage
49+
an edit in QUEST_PREP. Only the Snowy Mountains map is calibrated.
50+
51+
## Licence
52+
53+
MIT.

‎apps/hud/src/mhfu_hud/item_db.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Item id (a PLAYER_BAG slot's u16) -> name and icon slug, for the ids matched in game so far."""
1+
"""Item id (a PLAYER_BAG slot's u16) -> name and icon slug; only the ids listed are named."""
22

33
from __future__ import annotations
44

‎apps/studio/README.md‎

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,64 @@
11
# mhfu-studio
22

3-
One window for authoring MHFU mods offline: a ported monster (its clips, parts, hitboxes and
4-
moves) and map edits (mesh, collision, textures), with the commands behind both.
3+
One Qt window for authoring MHFU mods offline, with the commands behind it. Two workspaces:
4+
5+
- **Monster**: a ported monster's clips, labels, parts, hitboxes and moves, kept in its manifest
6+
in [`ports/`](../../ports), checked against the base monster's data from the game, and
7+
exported as a Lua module the framework loads (`<name>_hit.lua`).
8+
- **Map**: edits to a stage's mesh, collision and textures, kept as edit lists in a `map.toml`
9+
document and pushed into the running game through PPSSPP's debugger.
10+
11+
## Install
12+
13+
In a clone of modkit, `uv sync` installs it with Qt (PySide6) and the Qt test plugin;
14+
`uv sync --no-group qt` leaves out only the test plugin, for a machine without a display. For a
15+
wheel, see the root README's [Wheels](../../README.md#wheels).
16+
17+
The studio needs the extracted games. It takes each from its variable (`MHFU_DATA`,
18+
`MHP3RD_DATA`), else from the folder chosen under Setup on its start page, else it looks for
19+
`workspace/extracted` and `workspace/extracted_mhp3` in the working directory and up to four
20+
folders above it. Extract there
21+
([`mhp-formats`](../../packages/mhp-formats/README.md#extracting-a-game)) and nothing needs
22+
setting. Sending to the game also needs PPSSPP's memory stick (`MHFU_MEMSTICK`, else PPSSPP's
23+
default).
24+
25+
## Example
526

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

12-
Licensed GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
34+
| Command | Does |
35+
|---|---|
36+
| `studio open [PATH]` | The window, with PATH open; `--workspace NAME`, `--size WxH` |
37+
| `studio render selftest\|map\|monster` | A stage or a monster to PNG, without a window |
38+
| `studio port scene\|clips\|align\|check\|hit` | A port's contents, clip coverage and labels, timing against the host, checks, and its Lua module |
39+
| `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 |
40+
41+
`--help` on any of them says more.
42+
43+
## Tests
44+
45+
`uv run pytest apps/studio` runs offscreen; the Qt tests skip without the `qt` group, and the
46+
few that need a real GL window run with `MHFU_UI_DISPLAY=1`. `apps/studio/docker/run.sh` runs
47+
the tests, or any command, in a Linux container with Mesa's software GL, the renderer the render
48+
goldens are pinned to:
49+
50+
```bash
51+
apps/studio/docker/run.sh # the studio's tests
52+
MHFU_UI_DISPLAY=1 apps/studio/docker/run.sh # with Qt on Xvfb: the GL ones too
53+
apps/studio/docker/run.sh uv run studio render selftest -o out/
54+
```
55+
56+
## Status
57+
58+
Both workspaces work. New map geometry has to fit the primitives a vertex group already has
59+
(`studio map budget` says how much). The Qt tests run locally only: CI has no display.
60+
61+
## Licence
62+
63+
GPL-3.0-or-later: it is built on `mhp-formats`, which contains code derived from
1364
[mhff](https://github.com/svanheulen/mhff).

‎apps/studio/src/mhfu_studio/monster/actions.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
actions, what it hits with and whether the timing fits; and the moves as a mod declares them.
55
66
The game asks for the base monster's anim N and plays whatever sits in the port's slot N, by
7-
position, not meaning (`docs/MOD_PORTED_MONSTER.md` §1), unless a move paints its own clip on
8-
the action. No toolkit here: the Actions panel shows the rows.
7+
position, not meaning, unless a move paints its own clip on the action. No toolkit here: the
8+
Actions panel shows the rows.
99
"""
1010

1111
from __future__ import annotations

‎apps/studio/src/mhfu_studio/monster/panels/parts.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
" The damage row is which grid row scales a hit: a Tigrex wing is part 6 but row 5."
5252
)
5353
GRID_NOTE = (
54-
"Grid writes are proven in the game (every byte 0xFF gave 411-damage hits); hurtbox"
55-
" writes are still being tested."
54+
"Export writes the damage grid and the hurtboxes into the game. Grid writes change the damage"
55+
" a hit does; whether hurtbox writes move where hits land is unverified in game."
5656
)
5757

5858

‎apps/studio/src/mhfu_studio/monster/panels/timeline.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
"""The Timeline: the transport at the engine's rate, and the base monster's action's frames on
44
the clip.
55
6-
The RATE is the action's (2.0 and 2.4 both measured on one monster); the clip owns only its
7-
SPAN. A gate past the clip's last frame never runs: the strip runs on past the end, shaded, and
8-
draws it hollow there (at the far edge when it lies further out than a third of the clip)."""
6+
The RATE is the action's (one monster plays some actions at 2.0, others at 2.4); the clip owns
7+
only its SPAN. A gate past the clip's last frame never runs: the strip runs on past the end,
8+
shaded, and draws it hollow there (at the far edge when it lies further out than a third of the
9+
clip)."""
910

1011
from __future__ import annotations
1112

@@ -209,8 +210,8 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
209210
on=lambda on: act("in place", lambda: self._in_place(on))(),
210211
)
211212
self.speed = kit.number(
212-
tip="Clip frames played per game frame. The action sets it, not the clip: 2.0 and"
213-
" 2.4 were both measured on one monster.",
213+
tip="Clip frames played per game frame. The action sets it, not the clip: one"
214+
" monster plays some actions at 2.0, others at 2.4.",
214215
value=2.0,
215216
step=0.1,
216217
decimals=2,
@@ -222,7 +223,7 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
222223
presets = [
223224
kit.button(
224225
f"{s:.1f}",
225-
tip=f"Speed {s:g}, a rate measured in the game",
226+
tip=f"Speed {s:g}, a rate the game plays actions at",
226227
on=act("speed", partial(self._set, "speed", s)),
227228
)
228229
for s in OBSERVED_SPEEDS

‎apps/studio/src/mhfu_studio/monster/validate.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"HITBOX_UNCHECKED": "Nothing to change in your port: the base monster's data has no attacks.",
9696
"ATTACK_RECORD_UNKNOWN": MANIFEST + "delete this [[attack]].",
9797
"ATTACK_VOLUME_UNKNOWN": MANIFEST + "give the [[attack]] a hit group the base monster has.",
98-
"ATTACK_JOIN_INFERRED": "Nothing to change: only em75's attack table was measured.",
98+
"ATTACK_JOIN_INFERRED": "Nothing to change: only em75's attack join is traced in game.",
9999
}
100100

101101

0 commit comments

Comments
 (0)