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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ __pycache__/
.ruff_cache/
build/
dist/
.env
*.gen.h
*.gen.lua

Expand Down
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ repos:
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
exclude: \.patch$ # a blank line of context is one space
- id: trailing-whitespace
exclude: \.patch$ # a blank line of context is one space

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.9
Expand All @@ -17,6 +19,11 @@ repos:
args: [--fix]
- id: ruff-format

- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck

- repo: https://github.com/fsfe/reuse-tool
rev: v6.2.0
hooks:
Expand Down
117 changes: 117 additions & 0 deletions LICENSES/GPL-2.0-or-later.txt

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ release ULES01213), and for porting monsters to it from Monster Hunter Portable
| `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 |
| `ppsspp/` | PPSSPP patched for automation, its build, and a headless container to run it in |
| `examples/` | Example mods |
| `docs/` | Guides and format references |

Expand All @@ -33,13 +34,14 @@ uv run pytest

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. Nothing from the games is committed here:
no ISO, no extracted files, no memory dumps.
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.

## Licence

MIT, except the file-format library and everything that bundles it, which are GPL-3.0-or-later
because they contain code derived from [mhff](https://github.com/svanheulen/mhff). Every file's
licence is machine-readable ([REUSE](https://reuse.software/); check with `reuse lint`).
because they contain code derived from [mhff](https://github.com/svanheulen/mhff), and the
PPSSPP patches, which are GPL-2.0-or-later like PPSSPP. Every file's licence is machine-readable
([REUSE](https://reuse.software/); check with `reuse lint`).

Not affiliated with or endorsed by Capcom. Monster Hunter is a trademark of Capcom Co., Ltd.
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ version = 1
path = "**"
SPDX-FileCopyrightText = "2026 sp00ktober"
SPDX-License-Identifier = "MIT"

# patches to PPSSPP carry its licence; the last matching annotation wins
[[annotations]]
path = "ppsspp/patches/**"
SPDX-FileCopyrightText = ["2026 sp00ktober", "PPSSPP Project"]
SPDX-License-Identifier = "GPL-2.0-or-later"
23 changes: 16 additions & 7 deletions packages/ppsspp-debug/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ PPSSPP opens the debugger at startup only with `RemoteDebuggerOnStartup = True`
| PPSSPP (1.20.4) | The client |
|---|---|
| Sends its log only to the newest debugger connection, and stops it for all when any connection closes | Log streams get a connection of their own, opened when the stream starts |
| Does not say why the CPU stopped | Matches a breakpoint stop by its pc; counts as a watchpoint hit any stop this client did not ask for |
| Answers `cpu.stepping` and `cpu.resume` with a broadcast, not a reply; pausing a paused CPU answers nothing | `pause()` and `resume()` wait for the broadcast, and are fine to call twice |
| Does not say why the CPU stopped | Matches a breakpoint stop by its pc, and counts as a watchpoint hit any stop this client did not ask for; uses the reason where PPSSPP gives one |
| Reports a hit of a breakpoint that does not stop only as a log line | Parses it; takes `cpu.breakpoint.hit` events instead from a PPSSPP that sends them |
| Answers `cpu.stepping` and `cpu.resume` with a broadcast, not a reply; pausing a paused CPU answers nothing | `pause()` and `resume()` wait for the broadcast, and are fine to call twice; `resume()` also takes a new stop as its answer |
| Stops and restarts the CPU for every memory request while the game runs (about 10 ms each) | `paused()` makes each request about 0.3 ms; one `read()` of a whole struct costs one stop |
| Keeps breakpoints armed after the script that set them has died | `breakpoint()` and `watchpoint()` remove theirs when the block ends, however it ends, and let the game run if a hit arrived that nobody took |
| Reports errors as events, and an unknown event as an error | Raises `DebuggerError`, or `Unsupported` for an event this PPSSPP does not have |
Expand All @@ -58,14 +59,16 @@ with LocalEmulator("/path/to/PPSSPPSDL", "/path/to/game.iso") as emu:
ppsspp.wait_for_game()
```

`DockerEmulator(container)` drives PPSSPP in the modkit's headless container through its
`ppsspp-ctl`, and relaunches it when it dies right after reporting ready. A `state=` launch
skips the cold boot, so PRX plugins do not load.
`DockerEmulator()` drives PPSSPP in the modkit's headless container
([`ppsspp/`](../../ppsspp)) through its `ppsspp-ctl`, relaunches it when it dies right after
reporting ready, and captures its display with `screenshot()`. A `state=` launch skips the cold
boot, so PRX plugins do not load.

## Patched PPSSPP

`save_state()`, `load_state()` and `speed()` need the PPSSPP patches that ship with the modkit;
stock PPSSPP raises `Unsupported`.
The modkit's PPSSPP build ([`ppsspp/`](../../ppsspp)) fixes the first two rows of the table.
`save_state()` and `load_state()` need it; `speed()` and `set_speed()` need it or a PPSSPP newer
than 1.20.4. Stock 1.20.4 raises `Unsupported` for them.

`screenshot()` depends on the GPU backend: PPSSPP 1.20.4 with Vulkan answers "Could not
download output" whether the game runs or is stopped.
Expand All @@ -75,9 +78,15 @@ download output" whether the game runs or is stopped.
`ppsspp_debug.testing.FakePPSSPP` is a debugger server with PPSSPP's quirks, for testing code
that uses this client without an emulator.

`FakePPSSPP(patched=True)` behaves like the modkit's build, `FakePPSSPP(hit_events=True)` like a
PPSSPP that sends `cpu.breakpoint.hit`.

The live tests run against a real PPSSPP when `PPSSPP_BINARY` and `PPSSPP_GAME` are set, and
print what they measure with `-s`:

```bash
PPSSPP_BINARY=/path/to/PPSSPPSDL PPSSPP_GAME=/path/to/game.iso uv run pytest packages/ppsspp-debug -s
```

`PPSSPP_CONTAINER=ppsspp` in place of `PPSSPP_BINARY` runs them in the modkit's container, with
`PPSSPP_GAME` a path inside it.
2 changes: 2 additions & 0 deletions packages/ppsspp-debug/src/ppsspp_debug/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
RawEvent,
Resumed,
Screenshot,
Speed,
Stepping,
Watchpoint,
parse_hit,
Expand All @@ -55,6 +56,7 @@
"RawEvent",
"Resumed",
"Screenshot",
"Speed",
"Stepping",
"Stream",
"SyncStream",
Expand Down
Loading
Loading