Initialise native Rust port of Ratchet & Clank: Going Commando

Sets up the foundation for reimplementing the PS2 game in Rust rather than
emulating it. Nothing is implemented yet; this is the scaffold, the verified
target and the working agreements that keep a long project coherent.

Target disc, verified against a real image:

  SCUS_972.68 v1.01, USA Black Label, NTSC 60 Hz
  boot ELF 2,618,684 bytes, SHA-1 bacda860...d098a77, entry 0x00131AE8

The entry point independently matches the value recorded by the rac2-gc-decomp
community reference, which cross-confirms the identification. This is the exact
build the upstream research pins; Greatest Hits v2.00 is a different binary and
is not the target. NTSC matters beyond label compatibility: PAL discs run 50 Hz,
which would make chargeboots and other speed-sensitive movement run at 5/6
speed. Verifying the disc before writing movement code avoids discovering that
in Phase 5.

Contents:

- Five-crate workspace (formats, extract, game, video, engine) on Bevy 0.19.1,
  matching the version ReRAC uses so its Graphics Synthesizer and VU work stays
  portable. `cargo check --workspace` is clean.

- tools/identify-disc.py, which reproduces the identification against any
  disc image and distinguishes a truncated dump from a non-PS2 image.

- .githooks/pre-commit, refusing game data from entering git history: disc
  image extensions, asset directories, container signatures at offset 0, and
  files over 50 MiB. Verified against seven cases including a disc image
  renamed to look like source. Now that this pushes to a remote, this matters
  more than it would have locally.

- AGENTS.md, the operating agreements: no game data ever, reproduce the PS2
  float model rather than approximating it, bound every read, verify against
  the real disc.

- PROVENANCE.md, recording that this follows ReRAC (ISC) and learns from
  rac2-gc-decomp (MIT). Wrench is used as a written specification only and no
  GPLv3 code is copied, since that would relicense everything derived from it.
  RAC2Decomp is explicitly not used: its provenance is disputed in the upstream
  community reference, and it targets the wrong region.

- docs/roadmap.md, phased so the riskiest unknowns are retired early. The two
  that matter: overlay relocation transfers from ReRAC and makes 27 level
  overlays tractable, while VU1 microcode is binary and is the hardest single
  item. Phase 5 is a deliberately scoped Veldin vertical slice as the go/no-go.
This commit is contained in:
2026-10-09 15:42:10 +02:00
commit a02f79a6d1
20 changed files with 6880 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
#
# Pre-commit guard: refuse to commit game data, disc images, or anything
# unusually large into this repository.
#
# This repository is a from-scratch Rust reimplementation. It contains NO
# game assets, NO disc images, NO BIOS dumps and NO proprietary SDK binaries.
# The build takes your own legally obtained copy of the disc as input.
#
# Hooks live in .githooks/ and are activated by `git config core.hooksPath`.
# To bypass in an emergency: git commit --no-verify
set -uo pipefail
MAX_BYTES=$((50 * 1024 * 1024)) # 50 MiB
RED=$'\033[31m'; YEL=$'\033[33m'; RST=$'\033[0m'
errors=0
# Collect staged files (new/modified/renamed copies), NUL-separated so we
# survive spaces and odd characters in paths.
mapfile -d '' staged < <(
git diff --cached --name-only --diff-filter=ACMR -z 2>/dev/null
)
if [ ${#staged[@]} -eq 0 ]; then
exit 0
fi
for path in "${staged[@]}"; do
# Blobs that were removed from the worktree still exist in the object DB.
if ! [ -f "$path" ]; then
size=$(git cat-file -s ":$path" 2>/dev/null || echo 0)
else
size=$(stat -c%s "$path" 2>/dev/null || echo 0)
fi
# 1) Disc-image / asset extensions.
case "$path" in
*.iso|*.bin|*.img|*.cue|*.mdf|*.rom|*.rom1|*.rom2)
printf '%sBLOCKED%s %s\n' "$RED" "$RST" "$path"
printf ' looks like a disc image, BIOS dump or raw asset blob.\n'
errors=1
continue
;;
esac
# 2) Known extracted-asset directories.
case "/$path/" in
*/extracted/*|*/assets/*|*/iso/*|*/disc/*)
printf '%sBLOCKED%s %s\n' "$RED" "$RST" "$path"
printf ' lives in an asset directory that must stay untracked.\n'
errors=1
continue
;;
esac
# 3) Oversized files (catches archives and anything else we did not think of).
if [ "$size" -gt "$MAX_BYTES" ]; then
printf '%sBLOCKED%s %s (%s MiB, limit 50)\n' \
"$RED" "$RST" "$path" "$(( size / 1024 / 1024 ))"
errors=1
continue
fi
# 4) Content signature: ISO 9660 / WAD / CRI ADX container magic at offset 0.
# Catches disc images renamed to look like source. Compare the first five
# bytes as hex so binary bytes (NUL, 0x10, ...) cannot perturb the match.
# 2048 bytes is one PS2 logical sector: the smallest plausible asset.
if [ -f "$path" ] && [ "$size" -ge 2048 ]; then
sig=$(head -c 5 "$path" 2>/dev/null | xxd -p | tr -d '\n')
case "$sig" in
4344303031*|574144*|414458*)
printf '%sBLOCKED%s %s\n' "$RED" "$RST" "$path"
printf ' begins with a disc/asset container signature (%s).\n' "${sig:0:10}"
errors=1
continue
;;
esac
fi
done
if [ "$errors" -ne 0 ]; then
cat <<EOF
${RED}Commit aborted.${RST} This repository must never contain game data.
These files belong outside git (they are covered by .gitignore). Unstage them:
${YEL} git reset${RST}
and confirm with:
${YEL} git status${RST}
If something here is genuinely source code and larger than 50 MiB, that is
itself worth investigating before committing it.
EOF
exit 1
fi
exit 0
+38
View File
@@ -0,0 +1,38 @@
# ── Game data: NEVER commit. This is copyrighted material and it is huge. ──
# Your legally obtained disc image and anything extracted from it.
*.iso
*.bin
*.img
*.cue
*.mdf
*.raw
extracted/
assets/
iso/
disc/
data/iso/
.env.local
# ReRAC's own output layout, in case we adopt it.
extracted/
gs_ram/
# BIOS dumps (licensed separately, not ours to redistribute).
*.rom
*.rom1
*.rom2
bios/
# ── Vendored upstream (pinned by PROVENANCE.md, not tracked) ──
vendor/
# ── Rust build output ──
/target/
**/*.rs.bk
*.pdb
# ── Editor / OS noise ──
.idea/
.vscode/
*.swp
.DS_Store
+129
View File
@@ -0,0 +1,129 @@
# AGENTS.md — working agreements for this repository
Guidance for coding agents and for anyone picking this up cold. Read this
before changing anything.
## What this project is
A from-scratch native PC reimplementation of **Ratchet & Clank: Going Commando**
(PlayStation 2, 2003), written in Rust. The game is **reimplemented**, not
emulated and not translated: there is no MIPS interpreter and no CPU
emulator anywhere in this repository.
The goal is a build that plays the game the way the original does. "Close
enough" is not the bar — the point is that it feels like the game you remember.
## Hard rules
### 1. No game data in this repository. Ever.
Not the ISO, not extracted assets, not textures, not models, not audio, not
BIOS dumps, not SDK binaries. Not in a branch, not in a stash, not "just for a
test".
- Everything is covered by `.gitignore`.
- `.githooks/pre-commit` blocks disc images, asset directories, container
signatures and files over 50 MiB. It is activated via
`git config core.hooksPath .githooks` — **check that this is still set** in a
fresh clone, or run `git config core.hooksPath .githooks`.
- To undo the hook for a legitimate source change: `git commit --no-verify`.
Legitimate source changes should not need it.
- Game data is an **input**, supplied by the user from their own legally
obtained disc. Repositories that need the disc state its serial and hashes.
### 2. Reproduce the PS2 float model.
The Emotion Engine's FPU is not IEEE-754. It has **no denormals**, and rounding
and exception behaviour differ from x86 in ways that are visible in gameplay.
Gameplay and animation maths must model this deliberately rather than using
Rust's native `f32` and hoping. When porting a routine, find out how the
original computes it and reproduce that, including the odd cases — ReRAC's
reference implementation deliberately reproduces a "post-scale list bug" in the
moby animation evaluator because doing so is what makes the result match.
Do not "fix" something that looks like a bug until you have confirmed the
original does the same thing. Fidelity beats correctness.
### 3. Verify against the real disc, not against a guess.
A parser is correct when it round-trips your own disc's bytes, checked by hash.
Commit size-and-hash tables (never content) so a test can assert "this matches
retail" rather than "this matches whatever was on my disk".
### 4. Bound every read.
Asset data is untrusted input from a 2003 disc format. Parsers return typed
errors; they never panic and never read past the end of a buffer.
## Orientation
Read these before working in an unfamiliar area:
| Path | What it is |
|---|---|
| `docs/disc-identification.md` | Which disc we build against, verified |
| `docs/roadmap.md` | Phases, gates, and what is done |
| `docs/plan/` | One file per subsystem, written **as work proceeds** |
| `docs/formats/` | Format specs, written **as formats are reversed** |
| `PROVENANCE.md` | Upstream projects we build on, and their licences |
| `vendor/reRAC/` | Pinned reference checkout (not tracked) |
## Crates
| Crate | Responsibility |
|---|---|
| `gc-formats` | Parse asset formats into plain Rust values. No I/O beyond the caller's buffer. |
| `gc-extract` | Disc pipeline: identify, unpack, verify. |
| `gc-game` | Game logic: hero, weapons, classes, progression, saves. |
| `gc-video` | IPU/MPEG-2 decode. |
| `gc-engine` | Bevy app: GS, VU0/VU1, renderer, frame loop. |
The dependency direction is `gc-formats` ← `gc-extract`; `gc-formats` ←
`gc-game` ← `gc-engine`; `gc-video` ← `gc-game` ← `gc-engine`. `gc-formats`
depends on nothing of ours. Do not invert these — a format parser that can
reach into the engine cannot be tested in isolation.
## Conventions
- Rust 2021 edition, workspace inheritance for all shared config. Do not
hardcode a version in a member crate.
- `rustfmt` defaults. Clippy warnings are errors in spirit; do not silence them
without a comment explaining why.
- Error types: `thiserror` for library errors in `gc-formats` and `gc-game`,
`anyhow` at the binary/`xtask` boundary.
- Comments explain **why**, especially for anything that looks like a bug. The
`.githooks` and crate docs deliberately have few inline comments; match the
surrounding density rather than adding noise.
- Tests live beside the code (`#[cfg(test)] mod tests`) for unit-level checks,
and in `tests/` for golden-file checks against real disc data.
## Working rhythm
This is a long project. Keep it navigable:
1. Work in small, coherent commits with a substantive body explaining what was
non-obvious. Match the existing style in `git log`.
2. **Update `docs/roadmap.md`** when a phase advances or a gate is met.
3. **Write the `docs/plan/*.md` file for a subsystem when you start it**, and
update it as you learn. The reasoning is more valuable than the code; the
code is in git.
4. **Update this file** if you change a convention.
## When you get stuck
Reverse engineering means long stretches of not knowing. That is normal, not a
failure. Write down what you tried and what it ruled out in the relevant
`docs/plan/` file before moving on — the next attempt (which may be you, in
three months) needs the negative results as much as the positive ones.
If something is genuinely blocked, say so plainly and say what would unblock
it. Do not paper over a gap with an approximation that looks like it works.
## Legal
Ratchet & Clank is a trademark of Sony Interactive Entertainment, developed by
Insomniac Games. This is an unofficial fan project, not affiliated with or
endorsed by either. We redistribute no game content. Upstream projects we learn
from keep their own licences — see `PROVENANCE.md`, and keep those notices
intact when porting their code.
Generated
+5563
View File
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
[workspace]
resolver = "2"
members = [
"crates/gc-formats",
"crates/gc-extract",
"crates/gc-engine",
"crates/gc-game",
"crates/gc-video",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.85"
license = "MIT OR ISC"
publish = false
# The PS2's Emotion Engine is a MIPS R5900 with a 128-bit vector unit and a
# float model that differs from x86 in ways that matter for this game (no
# denormals, specific rounding). Every ported routine must reproduce that model
# rather than Rust's native f32, or the game will subtly not feel right.
# See docs/plan/float-model.md.
[workspace.dependencies]
gc-formats = { path = "crates/gc-formats" }
gc-extract = { path = "crates/gc-extract" }
gc-game = { path = "crates/gc-game" }
gc-video = { path = "crates/gc-video" }
anyhow = "1"
thiserror = "2"
bytemuck = { version = "1", features = ["derive", "min_const_generics"] }
sha1 = "0.10"
# Bevy 0.19.1 is the engine ReRAC uses; matching it keeps the GS/VU work portable.
#
# default-features = false avoids the cpal audio backend, which needs system
# ALSA development headers on Fedora (alsa-lib-devel). Audio decode lives in
# gc-video and is the last phase of the port, so the host audio backend is not
# needed to build. Re-enable "audio" and install alsa-lib-devel when it is.
bevy = { version = "0.19.1", default-features = false, features = [
"std",
"bevy_asset",
"bevy_color",
"bevy_core_pipeline",
"bevy_gizmos",
"bevy_log",
"bevy_mesh",
"bevy_pbr",
"bevy_render",
"bevy_shader",
"bevy_sprite",
"bevy_state",
"bevy_text",
"bevy_ui",
"bevy_window",
"bevy_winit",
"bevy_animation",
"bevy_gltf",
"png",
"ktx2",
"zlib",
"zstd_rust",
"tonemapping_luts",
"multi_threaded",
"x11",
"wayland",
"keyboard",
"mouse",
"gamepad",
] }
[profile.dev]
# Iterate fast on our own code; do not rebuild dependencies unoptimised.
opt-level = 1
[profile.dev.package."*"]
opt-level = 3
debug = false
[profile.release]
lto = "thin"
codegen-units = 1
+120
View File
@@ -0,0 +1,120 @@
# PROVENANCE
This project is built on the work of other people. Their licences apply to their
code, and those notices travel with anything derived from it.
## Summary
| Project | Role | Licence | Code copied into this repo? |
|---|---|---|---|
| [ReRAC](https://github.com/re-rac/rerac) | Native Rust/Bevy port of Ratchet & Clank (2002) — the architecture this follows | ISC | Not yet; derived from, attribute when used |
| [OpenRAC / rac2-gc-decomp](https://github.com/OpenRAC/rac2-gc-decomp) | Matching decompilation research for this game | MIT | No — consulted for documentation |
| [rac2-gc-decomp community reference](https://github.com/OpenRAC/rac2-gc-decomp/blob/RAC2/docs/COMMUNITY-ENGINE-REFERENCE.md) | Engine architecture notes, moby struct layout | MIT | Documentation only |
| [Wrench](https://github.com/chaoticgd/wrench) | Insomniac container format documentation | GPLv3 | **No.** Specification reference only |
| [RAC2Decomp](https://github.com/platypet2217-star/RAC2Decomp) | Functional decompilation, PAL | MIT | **Not used** — see below |
## Game content: none
No game assets, disc images, extracted data, BIOS dumps or proprietary
toolchains are or may be committed to this repository. The build takes the
user's own legally obtained disc as input. This is enforced by
`.githooks/pre-commit`.
Ratchet & Clank is a trademark of Sony Interactive Entertainment. This is an
unofficial fan project, not affiliated with or endorsed by Sony Interactive
Entertainment or Insomniac Games.
---
## ReRAC — ISC
The direct model for this project. ReRAC re-implements the *first* Ratchet &
Clank as a native Rust application on Bevy, reading assets from the player's own
disc. This project follows the same architecture for the second game.
What we take from ReRAC:
- **The architecture.** Crate split (`formats` / `extract` / `game` / `engine` /
`video`), Bevy 0.19.1, the PS2 float-model discipline, and the milestone
gating style.
- **Overlay relocation.** `rc-formats::level_overlay` implements
relocation-tolerant code-identity matching so one level's function can be
found on every other level. R&C2's 27 overlays have the same seven-section
layout (`.lit`, `.bss`, `.data`, `lvl.vtbl`, `lvl.camvtbl`, `lvl.sndvtbl`,
`.text`), so this machinery transfers directly. It is the single most
valuable thing we inherit.
- **Verified format specs.** `docs/formats/disc_layout.md` documents all four
R&C games' container layouts, derived from behavioural analysis.
- **Discipline worth copying.** Golden tests against committed snapshot hashes;
bit-exact CPU/GPU identity checks; documenting negative results.
ISC permits use, modification and redistribution with the notice intact. When
code is derived from ReRAC it must carry:
```
Copyright (c) 2026 ReRAC contributors
Licensed under the ISC License.
```
Pinned checkout: `vendor/rerac` at commit
`d00cb00ad4018f776556e72640dcba438d098a77` (2026-10-09). Not tracked by git —
clone it yourself; the commit is recorded so everyone uses the same one.
Note that ReRAC targets a *different game*. Its per-level documentation is a
useful hint about Insomniac's engine in general, but R&C2's spherical worlds
changed roughly 50,000 lines of gameplay code, so nothing about R&C1's
movement, camera or level geometry should be treated as directly applicable.
## OpenRAC / rac2-gc-decomp — MIT
Matching decompilation research for **this** game, targeting exactly our disc
(`SCUS_972.68` v1.01). It reconstructs the boot ELF and all 27 level overlays
byte-for-byte, and publishes verified struct layouts, symbol names and engine
documentation.
We use it as a **reference**, not as source. Its value is that it tells us what
the structures *are*, which saves a great deal of guessing.
Its matching progress is low (~1.6% of code in C) and byte-matching is not a
prerequisite for this port anyway — it is a verification technique. We are not
pursuing a byte-identical rebuild.
## Wrench — GPLv3, specification reference only
Community documentation and tooling for Insomniac's container formats. We use
the written format *specifications* as a reference for how the data is laid
out.
**No Wrench code is copied into this repository.** That is deliberate and
load-bearing: copying GPLv3 code would impose GPLv3 terms on everything derived
from it. Clean-room reimplementation from a written specification does not. If
this boundary is ever crossed, the licensing of this project changes and
`AGENTS.md` must be updated to say so.
## RAC2Decomp — not used
A functional (non-byte-matching) decompilation of the PAL release. It is
recorded here only so its absence is a decision rather than an oversight.
The `rac2-gc-decomp` community reference explicitly flags it as *"criticized by
community for questionable provenance and non-clean-room C code. Avoid
copying."* Given that we are building a reimplementation and its provenance is
disputed, we do not use it. It is also the wrong region: PAL, 50 Hz.
## Third-party libraries
Rust dependencies keep their own licences; see `Cargo.lock` and each crate's
`LICENSE` file. Notable ones:
- **Bevy** (MIT / Apache-2.0) — application framework and wgpu renderer.
- **wgpu / wgpu-hal / wgpu-types** (MIT / Apache-2.0) — GPU abstraction used for
the Graphics Synthesizer reimplementation.
- **bytemuck** (Zlib OR Apache-2.0 OR MIT) — zero-copy casts over asset
structures.
- **thiserror**, **anyhow** (MIT OR Apache-2.0) — error handling.
## When you add a dependency or port code
1. Record it in this file, with its licence and what you took.
2. Keep the upstream notice intact in any file containing derived code.
3. If it would change this project's licensing terms, stop and say so first.
+106
View File
@@ -0,0 +1,106 @@
# Going Commando
A native PC reimplementation of **Ratchet & Clank: Going Commando** (PlayStation
2, 2003), written in Rust.
The game is reimplemented, not emulated. There is no MIPS interpreter here and
no CPU emulator: the systems are ported to Rust so they run as ordinary native
code.
**Status: initial scaffold.** The disc is identified and verified, the
workspace builds, and nothing else is implemented yet. See
[`docs/roadmap.md`](docs/roadmap.md) for where things stand and what comes next.
## The target
| | |
|---|---|
| Game | Ratchet & Clank: Going Commando |
| Platform | PlayStation 2 |
| Disc | `SCUS_972.68` v1.01, USA Black Label |
| Video | NTSC, 60 Hz |
| Boot ELF SHA-1 | `bacda860cbd6723142e86db56f197e47e728970f` |
Verified against a real disc — see [`docs/disc-identification.md`](docs/disc-identification.md).
This is the build the reference projects pin; Greatest Hits v2.00 is a
different binary and is not the target.
## No game content
This repository contains **no game assets, no disc images, no BIOS dumps and no
proprietary toolchains**. You supply your own legally obtained copy of the disc
as an input, the way the upstream reference projects do.
A pre-commit hook enforces this, blocking disc images, asset directories,
container signatures and files over 50 MiB. If you clone elsewhere, enable it:
```sh
git config core.hooksPath .githooks
```
## Layout
```
crates/
gc-formats/ asset format parsers (pure, no I/O beyond the caller's buffer)
gc-extract/ disc pipeline: identify, unpack, verify
gc-game/ game logic: hero, weapons, moby classes, progression, saves
gc-video/ IPU / MPEG-2 decode
gc-engine/ Bevy app: Graphics Synthesizer, VU0/VU1, renderer, frame loop
docs/
disc-identification.md which disc we build against, verified
roadmap.md phases, gates, current status
plan/ one file per subsystem, written as work proceeds
formats/ format specs, written as formats are reversed
tools/
identify-disc.py identify any Going Commando disc image
vendor/
rerac/ pinned upstream reference checkout (not tracked)
```
## Building
Requires a Rust toolchain via [rustup](https://rustup.rs).
```sh
cargo check --workspace
cargo test --workspace
```
The host audio backend is disabled in `Cargo.toml` (see the comment there) so
that the workspace builds without system ALSA development headers. Audio decode
is the last phase of the port and lives in `gc-video`; re-enable it and install
`alsa-lib-devel` when that work starts.
## Identifying a disc
```sh
python3 tools/identify-disc.py <your-disc.iso>
```
Reports the serial, version and video mode from `SYSTEM.CNF`, hashes the boot
executable, and warns when the disc is not the supported build. Reads only a
few megabytes, so it is near-instant.
## How it will be verified
Visual and behavioural comparison against PCSX2, replaying recorded input and
comparing frames, level by level. The PS2 float model is reproduced rather than
approximated — see [`AGENTS.md`](AGENTS.md#2-reproduce-the-ps2-float-model).
## Credits and licensing
This project learns from and is built on the work of others. Licences are
recorded in [`PROVENANCE.md`](PROVENANCE.md) and notices must travel with any
code derived from them.
- **ReRAC** — a native Rust/Bevy port of *Ratchet & Clank* (2002), the first
game. ISC. The architecture this project follows.
- **OpenRAC / rac2-gc-decomp** — matching decompilation research for this
game. MIT. Source of verified struct layouts and engine documentation.
- **Wrench** — community documentation of Insomniac's container formats. GPL,
used as a specification reference only; no code is copied.
Ratchet & Clank is a trademark of Sony Interactive Entertainment. This is an
unofficial fan project, not affiliated with or endorsed by Sony Interactive
Entertainment or Insomniac Games.
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "gc-engine"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
description = "Bevy application: Graphics Synthesizer, VU0/VU1 and the renderer"
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
gc-formats.workspace = true
gc-game.workspace = true
bevy.workspace = true
+35
View File
@@ -0,0 +1,35 @@
//! The application: renderer, hardware models, and the frame loop.
//!
//! Reimplements the pieces of the PlayStation 2 that the game talks to, so the
//! game logic in `gc-game` can be written as ordinary Rust that draws and
//! sounds like the original.
//!
//! # Hardware being modelled
//!
//! * **Graphics Synthesizer (GS)** - fixed-function rasteriser. No shaders on
//! the real hardware: the GS does texture mapping, alpha and blend rules,
//! and z/stencil. Here that becomes wgpu plus explicit handling of the GS's
//! own rules (its `round(log2(z/n) + K)` mip selection, its blend and alpha
//! test behaviour, its alpha-0x80-is-1.0 convention).
//! * **VU0 / VU1** - vector units running microcode uploaded from the disc.
//! Skinning, lighting and transform setup live here. The microcode is
//! **binary** in the game's `.vutext` section, so it is reverse engineered
//! rather than parsed: this is the single hardest item in the project.
//! * **DMA and GIF/VIF packets** - the game streams geometry to the GS through
//! tag-split packet lists. Those packet formats are the input to rendering.
//! * **SPU2 / IOP** - audio and the disc interface, in `gc-video` and
//! `gc-extract` respectively.
//!
//! # Divergence from the first game
//!
//! R&C2's levels are spherical worlds, so camera, culling, fog and the sky are
//! all effectively rewritten compared with Ratchet & Clank (2002). Geometry
//! subsystems carry over in shape - `tfrag`, `tie`, `shrub`, `moby` are the
//! same four - but their behaviour on a sphere is new work.
//!
//! # Reference frame
//!
//! The port is validated against PCSX2 by replaying recorded input and
//! comparing frames. Without that loop, "looks close" and "is correct" drift
//! apart, and this game's difficulty and movement make that drift very
//! noticeable.
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "gc-extract"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
description = "Disc pipeline: identify, unpack and verify game data from a Going Commando disc image"
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
gc-formats.workspace = true
sha1.workspace = true
+29
View File
@@ -0,0 +1,29 @@
//! Disc pipeline for *Ratchet & Clank: Going Commando* (PS2).
//!
//! Turns a legally obtained disc image into verified, unpacked asset files that
//! the engine can read directly, and does nothing else.
//!
//! # Stages
//!
//! 1. **identify** - read `SYSTEM.CNF`, confirm serial/version/video mode and
//! hash the boot ELF. Reports clearly when a disc is not the supported
//! build. A Python reference implementation of this stage lives in
//! `tools/identify-disc.py`.
//! 2. **unpack** - read `RC2.HDR`, walk the table of contents, decompress WAD
//! streams and write per-level and global asset directories.
//! 3. **verify** - check every extracted file against a recorded size and
//! SHA-1 table, so a corrupted or mismatched dump cannot quietly produce a
//! subtly wrong game.
//!
//! # Output layout
//!
//! Extracted data is written outside the repository (see `.gitignore`). The
//! pre-commit hook in `.githooks/` refuses disc images, asset directories and
//! oversized files, so nothing here can end up in git history by accident.
//!
//! # Verification is not optional
//!
//! Every parser downstream is only as trustworthy as the bytes it was handed.
//! A hash table committed to the repo (sizes and hashes only, never content)
//! is what lets a test say "this parse matches the retail disc" rather than
//! "this parse matches whatever happened to be on my disk".
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "gc-formats"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
description = "Parsers for Ratchet & Clank: Going Commando (PS2) asset formats"
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
bytemuck.workspace = true
sha1.workspace = true
[dev-dependencies]
+38
View File
@@ -0,0 +1,38 @@
//! Asset format parsers for *Ratchet & Clank: Going Commando* (PS2).
//!
//! Target: `SCUS_972.68` v1.01, USA Black Label, NTSC 60 Hz. See
//! `docs/disc-identification.md`.
//!
//! This crate reads data structures out of the game and produces plain Rust
//! values. It performs no rendering, no simulation and no I/O beyond what the
//! caller hands it, which keeps every parser testable against a byte slice.
//!
//! # What belongs here
//!
//! * `iso9660` - the disc filesystem. R&C2 lists its assets as real files,
//! unlike R&C1, so enumeration works.
//! * `hdr` - `RC2.HDR`, the table of contents (408,444 bytes at the
//! disc root). The engine addresses assets by sector, and
//! this is where those sector numbers come from.
//! * `wad` - Insomniac's LZ77 container (`WAD` magic, 16-byte header).
//! Carries level core data, audio and video streams.
//! * `tfrag` - terrain fragments: LOD chains, strips, morph targets.
//! * `tie` - instanced environment geometry ("ties"), with fat vertices.
//! * `shrub` - instanced foliage and decorative meshes.
//! * `moby` - dynamic actors, including Ratchet, enemies and crates.
//! * `moby_anim` - skeletal animation: sequences, frames, blending.
//! * `collision` - the collision world: cells, packed vertices, faces.
//! * `occlusion` - the PVS grid used for visibility.
//! * `sky`, `volumes`, `textures` - sky shells, fog volumes, CSM1 textures.
//!
//! # Rules
//!
//! **Bound every read.** Malformed data must produce a typed error, never a
//! panic and never a read past the end of the buffer.
//!
//! **No game data in this repository.** Parsers are written against a copy of
//! your own legally obtained disc, and the extracted output stays outside git.
//!
//! **Verify against the real thing.** Every parser gets a test that loads
//! known-good bytes from your disc and checks the parsed result. See
//! `docs/formats/` for the specs being implemented.
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "gc-game"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
description = "Game logic: hero controller, weapons, moby classes, progression and saves"
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
gc-formats.workspace = true
gc-video.workspace = true
+38
View File
@@ -0,0 +1,38 @@
//! Game logic for *Ratchet & Clank: Going Commando* (PS2).
//!
//! The rules, systems and behaviour of the game, reimplemented from the
//! original code rather than wrapped around it. No emulation, no interpreter:
//! this is a Rust implementation that is meant to behave the way the game does.
//!
//! # What belongs here
//!
//! * `hero` - Ratchet's controller. Movement on a **spherical** planet,
//! which is what distinguishes this game: reaching for the
//! camera and the collision code changes gravity handling
//! across the whole world, and is the main reason the first
//! game's movement code does not transfer.
//! * `chargeboots`- hold and release timing, chained jumps and the dash.
//! * `weapons` - the wrench and the weapon set, plus the weapon modification
//! system (per-weapon bitmasks: lock-on, shock, acid, ...).
//! * `gadgets` - the Pyrocitor, Bomb Glove, Glove of Doom and friends.
//! * `moby_update`- the per-class update dispatch, driven by each level's
//! `lvl.vtbl` class table. Classes that are identical across
//! levels are shared; the rest are resolved through the
//! overlay relocator in `gc-formats`.
//! * `collision` - queries against the collision world.
//! * `progression`- bolts, the Gadgetron vendor, RPG upgrades and unlocks.
//! * `save_game` - the IFF chunk save format, and importing a Ratchet & Clank
//! (2002) save to unlock the legacy weapons for free.
//! * `memcard` - the 25-state memory-card finite state machine and its UI.
//!
//! # The float model
//!
//! The Emotion Engine's FPU is not IEEE-754 in every respect: it has no
//! denormals, and rounding differs in places. Gameplay code here reproduces
//! that model rather than using Rust's native `f32`, because the difference is
//! visible in the movement and the camera. See `docs/plan/float-model.md`.
//!
//! # Timing
//!
//! The game is 60 Hz (this disc is NTSC). Constants are expressed per tick at
//! that rate. PAL discs run 50 Hz and would need different timing throughout.
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "gc-video"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
description = "IPU/MPEG-2 video stream decoding for Ratchet & Clank: Going Commando"
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
+19
View File
@@ -0,0 +1,19 @@
//! Video decoding for *Ratchet & Clank: Going Commando* (PS2).
//!
//! R&C2 uses the Emotion Engine's **IPU** to stream MPEG-2 video, most visibly
//! on the title screen where Ratchet watches television. This crate decodes
//! those streams to raw frames for the host GPU to upload.
//!
//! # Note on scale
//!
//! The MPEG WADs on this disc total roughly 2.4 GB (about 1.0 GB, 1.3 GB and
//! 115 MB). Video is a data problem before it is a decoding problem, so the
//! pipeline needs to stream rather than hold a whole movie in memory.
//!
//! # Pipeline
//!
//! `bits.rs` -> `vlc.rs` -> `idct.rs` -> `mpeg2.rs`
//!
//! MPEG-2 decoding is a solved, well-specified problem, and a general-purpose
//! decoder will produce correct output. The PS2-specific part is only the
//! container: how the streams are stored and addressed on the disc.
+87
View File
@@ -0,0 +1,87 @@
# Disc identification — VERIFIED 2026-10-09
Your disc was identified while initialising this repository. It is the **best
case target**: the exact build that `OpenRAC/rac2-gc-decomp` pins.
## Result
| Field | Value |
|---|---|
| Title | Ratchet & Clank: Going Commando |
| Serial | `SCUS_972.68` |
| Version | **1.01** (Black Label) |
| Video mode | **NTSC** (60 Hz) |
| Region | USA |
| ISO 9660 volume | `RATCHETANDCLANK2` |
| Image size | 3,828,350,976 bytes |
| Publisher / Preparer | SONY / INSOMNIAC |
### Boot executable (`SCUS_972.68`)
| Property | Value |
|---|---|
| Size | 2,618,684 bytes |
| SHA-1 | `bacda860cbd6723142e86db56f197e47e728970f` |
| CRC-32 | `262705496` |
| Format | ELF32, little-endian, MIPS (EM_MIPS = 8) |
| Entry point | `0x00131AE8` |
The entry point `0x00131AE8` matches the value independently recorded by the
`rac2-gc-decomp` community reference for this target, which cross-confirms the
identification.
## Why this matters
**`VER = 1.01` + `VMODE = NTSC` is exactly what we want.**
- **v1.01 is the pinned target.** `rac2-gc-decomp` states plainly that
*Greatest Hits v2.00 is a different target — do not point a build at it.*
You have the Black Label original.
- **NTSC means 60 Hz.** This is the detail that decides the movement constants.
PAL discs report `VMODE = PAL` and run the game at 50 Hz, which makes the
chargeboots and other speed-sensitive movement run at 5/6 of their intended
speed. Building against NTSC timing constants is correct for this disc.
There is no ambiguity to resolve, so no disc-conversion work is required.
## Not used for identification
Redump's catalogue records the **first track** hash for this disc as
`f2cfaf0eb12b3c0d8fd6db7aa49ce032e8a6020b` (CRC-32 `3828350976`). The values
computed above are the **boot executable's** hash, which is a different thing:
a single file rather than a track. Both are stable identifiers, but they must
not be compared against each other. We identify by serial + `VER` + `VMODE` +
boot-ELF SHA-1, which is unambiguous.
## Disc layout (observed)
R&C2 lists its assets in the ISO 9660 filesystem, unlike R&C1. Confirmed
entries at the disc root:
```
SYSTEM.CNF 60 bytes
RC2.HDR 408,444 <- the table of contents, as a named file
SCUS_972.68 2,618,684 <- the boot ELF
IOPRP255.IMG 264,449
Z6TAIL.DUP 0
G/... <- global WADs (LEVEL*, AUDIO*, SCENE*, MPEG*, HUD, …)
```
`RC2.HDR` being present as a real file is convenient: the extractor can read
the table of contents directly instead of hunting for it at a fixed sector as
with R&C1. The `G/` WADs are listed in the filesystem, so enumeration works
too.
### Observation on MPEG WADs
The three MPEG WADs are large — roughly 1.0 GB, 1.3 GB and 115 MB. That is a
substantial fraction of the 3.8 GB disc and will matter for the video phase
(R&C2 uses IPU/FMV streams, notably the title screen). Worth factoring into the
plan: video is a large *data* problem before it is a decoding problem.
## Reproducing this check
`tools/identify-disc.py` performs the same identification against any
R&C2 disc image and prints the table above. It reads only the ISO 9660
structures, `SYSTEM.CNF` and the boot ELF (a few MB of seeks), so it runs in
well under a second on a local image.
+194
View File
@@ -0,0 +1,194 @@
# Roadmap
Status as of **2026-10-09**.
A from-scratch native Rust reimplementation of Ratchet & Clank: Going Commando
(PS2). Each phase ends in a **gate** — something checkable, not a feeling that
it is going well. Phases are ordered so that the riskiest unknowns are retired
early, while they are still cheap to retire.
## Where we are
- [x] Target disc identified and verified (`SCUS_972.68` v1.01, NTSC 60 Hz)
- [x] Workspace builds (`cargo check --workspace`)
- [x] Repository hygiene in place (pre-commit guard, `.gitignore`, licence
record)
- [ ] Nothing else implemented yet
The disc being the exact build the reference projects pin is the single
biggest favourable fact so far: no disc conversion, no regional timing work,
and no re-derivation of the reference baseline.
---
## Phase 0 — Foundation ✅
**Done.**
- Disc identified: `SCUS_972.68` v1.01, USA Black Label, NTSC 60 Hz. Boot ELF
SHA-1 `bacda860cbd6723142e86db56f197e47e728970f`, entry `0x00131AE8` — the
entry point independently matches the value recorded by `rac2-gc-decomp`,
which cross-confirms the identification.
- `tools/identify-disc.py` reproduces the identification against any disc image
and distinguishes a truncated dump from a non-PS2 image.
- Five-crate workspace scaffolding, Bevy 0.19.1 pinned, builds clean.
- `.githooks/pre-commit` blocks game data. Verified against seven cases:
`.iso`, renamed disc image, WAD container, ADX container, `extracted/`,
a 60 MiB file, and legitimate source.
- `AGENTS.md` and `PROVENANCE.md` record conventions and licences.
### Open from this phase
- `alsa-lib-devel` is not installed, so Bevy's host audio backend is disabled in
`Cargo.toml`. Audio is the last phase; revisit then.
- Bevy feature set is deliberately minimal. Re-add features as needed rather
than switching `default-features` back on wholesale.
---
## Phase 1 — Disc pipeline
**Goal:** turn the disc image into verified, unpacked asset files.
- [ ] `iso9660` reader (R&C2 lists assets as real files, unlike R&C1)
- [ ] `RC2.HDR` table of contents parser (408,444 bytes at disc root)
- [ ] WAD LZ77 decompression
- [ ] Unpack global WADs (`G/*.WAD`) and per-level WADs
- [ ] Commit a size+SHA-1 table per target file (hashes only, never content)
- [ ] `verify` stage
**Gate:** every extracted file matches the committed hash table, and the
decompressed size of every WAD stream matches its header.
**Note:** the three MPEG WADs total roughly 2.4 GB of the 3.8 GB disc. Video is
a data problem before it is a decoding problem — plan to stream.
---
## Phase 2 — Format parsers
**Goal:** every asset type parses into plain Rust values, tested against real
disc data.
- [ ] `tfrag` — terrain: LOD chains, strips, morph targets
- [ ] `tie` — instanced environment geometry, fat vertices
- [ ] `shrub` — foliage and decorative meshes
- [ ] `moby` — dynamic actors; `moby_anim` — skeletal animation
- [ ] `collision` — cells, packed vertices, faces
- [ ] `occlusion` — the PVS grid
- [ ] `sky`, `volumes`, `textures` (CSM1 CLUT + PS2 alpha)
- [ ] `level_overlay` — the seven-section parser and relocation from Phase 3
**Gate:** every parser has a golden test against bytes from the real disc, and
reports mismatched header counts as failures rather than warnings.
**Discipline:** bound every read. Typed errors, no panics, no reads past the
buffer.
---
## Phase 3 — Overlay system
**Goal:** make 27 level overlays tractable.
Every overlay carries the same seven sections (`.lit`, `.bss`, `.data`,
`lvl.vtbl`, `lvl.camvtbl`, `lvl.sndvtbl`, `.text`) and the same engine code,
linked at different addresses. ReRAC solves this by masking relocations — `j`/
`jal` targets, `lui` immediates of main-RAM addresses, the `%lo` that completes
them, and `$gp`-relative operands — then matching the masked words to find a
reference function's copy on another overlay.
- [ ] Port the masking and code-identity logic
- [ ] `Relocation::func` and `Relocation::data`
- [ ] `$gp` for this game (`0x001AEFF0`, against R&C1's `0x166c00`)
- [ ] Extend the level table from 19 to 27
**Gate:** every level-01 function's relocated copy is found on all 27 overlays;
cross-checked against an independent cluster hash.
**Why this is high-value:** it turns "27 overlays of unknown code" into a table
lookup, and it is the main thing inherited from ReRAC.
---
## Phase 4 — Engine core
**Goal:** draw a frame.
- [ ] Graphics Synthesizer in wgpu, with the GS's own rules: its
`round(log2(z/n) + K)` mip selection, blend and alpha-test behaviour,
alpha `0x80` meaning 1.0, CLAMP wrap
- [ ] VU0/VU1 skinning and lighting on the PS2 float model
- [ ] LOD/morph/cull per `TfragProc` and `TieProc`
- [ ] Occlusion culling, fog, shadows
- [ ] GIF/VIF packet stream interpretation
**Gate:** CPU and GPU paths agree on every vertex of a reference level — the
same identity check ReRAC runs.
**Risk:** VU1 microcode is **binary** in the game's `.vutext` section, so it is
reverse engineered rather than parsed. This is the hardest single item in the
project and the main threat to the timeline. Note that ReRAC's own moby VU1
analysis trips their disassembler on two opcodes.
---
## Phase 5 — Veldin vertical slice
**Goal:** one planet, playable start to finish.
- [ ] Ratchet's controller on a **spherical** planet
- [ ] Chargeboots: hold/release timing, chained jumps, dash
- [ ] Wrench and the camera
- [ ] Collision and the HUD
**Gate:** Veldin is completable and visually matches PCSX2 frame-for-frame under
replayed input.
**This is the go/no-go for the project.** If Veldin works, the other 26 levels
are content. If it does not, the reason is discovered while it is still cheap.
Spherical gravity is the thing most likely to break, because it invalidates
R&C1's movement and camera assumptions wholesale.
---
## Phase 6 — Remaining overlays and the shell
- [ ] Per-level moby class dispatch via `lvl.vtbl`
- [ ] Remaining 26 levels
- [ ] Planets, travel, the Gadgetron vendor, RPG upgrades
- [ ] Weapon modification bitmasks (lock-on, shock, acid, …)
- [ ] Minigames and challenges
- [ ] The 25-state memory-card FSM and its UI
- [ ] IFF chunk saves, including import of a R&C1 save to unlock legacy weapons
---
## Phase 7 — Audio and video
- [ ] 989snd v2 audio decode and playback (not CRI ADX — a different codec)
- [ ] SPU2 mixing, reverb zones, level sounds
- [ ] IPU/MPEG-2 video for the title screen and cutscenes
---
## Phase 8 — Fidelity pass
Differential testing against PCSX2 across every level:
- [ ] Recorded-input frame comparison, per planet
- [ ] Geometry, fog, blend, alpha, texture sampling
- [ ] Audio sync and 60 Hz timing
- [ ] Behavioural comparison: traversal routes, boss timing, RNG
---
## Principles
- **A gate is pass/fail, not a percentage.** "Looks fine" is not progress.
- **Negative results are written down.** A ruled-out approach saves the next
attempt as much as a working one.
- **The float model is not negotiable.** Approximate is not the same as
faithful, and the difference is visible in how the game feels.
- **Fidelity over tidiness.** Where the original does something that looks like
a bug, reproduce it and say why in a comment. See `AGENTS.md`.
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
Identify a Ratchet & Clank: Going Commando disc image.
Reads only the ISO 9660 structures, SYSTEM.CNF and the boot ELF, so it needs a
few megabytes of seeks rather than a full read. Reports the serial, version and
video mode, and warns when the disc is not the build our references target.
python3 tools/identify-disc.py <disc.iso>
python3 tools/identify-disc.py --verbose <disc.iso>
Only your own legally obtained disc is read. Nothing is written anywhere.
"""
from __future__ import annotations
import argparse
import hashlib
import struct
import sys
import zlib
from dataclasses import dataclass, field
from pathlib import Path
SECTOR = 2048
# The build OpenRAC/rac2-gc-decomp pins, and the one our reference material
# describes. Boot-ELF SHA-1 below is measured from a retail v1.01 image.
TARGET_SERIAL = "SCUS_972.68"
TARGET_VERSION = "1.01"
TARGET_ELF_SHA1 = "bacda860cbd6723142e86db56f197e47e728970f"
# Serial -> (game, edition) for the R&C discs we can recognise.
KNOWN_TITLES = {
"SCUS_972.68": ("Going Commando (USA)", "Black Label v1.01"),
"SCUS_972.68GH": ("Going Commando (USA)", "Greatest Hits v2.00 - DIFFERENT TARGET"),
"SCES_516.07": ("Going Commando (PAL)", "Locked and Loaded v1.00 - 50 Hz"),
"SCPS_150.56": ("Going Commando (JP)", "Gagaga! Ginga no Commando-ssu"),
"SCKA_200.11": ("Going Commando (KR)", ""),
"SCUS_971.99": ("Ratchet & Clank (USA)", "the first game, not Going Commando"),
"SCES_509.16": ("Ratchet & Clank (PAL)", "the first game, not Going Commando"),
}
class NotAnIso(Exception):
pass
@dataclass
class Iso:
"""Minimal read-only ISO 9660 view: enough to find the root directory."""
path: Path
volume_id: str = ""
root_lba: int = 0
root_size: int = 0
files: dict[str, tuple[int, int]] = field(default_factory=dict)
def read(self, lba: int, size: int) -> bytes:
with open(self.path, "rb") as fh:
fh.seek(lba * SECTOR)
data = fh.read(size)
if len(data) < size:
raise NotAnIso(
f"image is truncated: wanted {size} bytes at LBA {lba}, got {len(data)}"
)
return data
def open_iso(path: Path) -> Iso:
size = path.stat().st_size
with open(path, "rb") as fh:
magic = fh.read(SECTOR * 16 + 6)
# The CD001 signature sits at offset 0x8001 in the Primary Volume Descriptor.
if magic[0x8001:0x8006] != b"CD001":
# A 2352-byte raw dump has a different layout; we only support .iso.
head = magic[:5]
raise NotAnIso(
"not an ISO 9660 image (no CD001 signature at 0x8001); "
"only plain 2048-byte-sector .iso dumps are supported"
)
iso = Iso(path=path)
with open(path, "rb") as fh:
fh.seek(16 * SECTOR)
pvd = fh.read(SECTOR)
iso.volume_id = pvd[40:72].decode("latin1").strip()
root = pvd[156:190]
iso.root_lba = struct.unpack_from("<I", root, 2)[0]
iso.root_size = struct.unpack_from("<I", root, 10)[0]
fh.seek(iso.root_lba * SECTOR)
data = fh.read(iso.root_size)
off = 0
while off < len(data):
rec_len = data[off]
if rec_len == 0:
# Records never span a logical block; skip to the next one.
off = (off // SECTOR + 1) * SECTOR
continue
name_len = data[off + 32]
lba = struct.unpack_from("<I", data, off + 2)[0]
fsize = struct.unpack_from("<I", data, off + 10)[0]
name = data[off + 33:off + 33 + name_len].split(b";")[0].split(b"\x00")[0]
iso.files[name.decode("latin1")] = (lba, fsize)
off += rec_len
return iso
def cnf_value(cnf: str, key: str) -> str:
for line in cnf.splitlines():
if "=" not in line:
continue
k, v = line.split("=", 1)
if k.strip() == key:
return v.split(";")[0].strip()
return ""
def elf_summary(blob: bytes) -> str:
if len(blob) < 52 or blob[:4] != b"\x7fELF":
return "not an ELF"
bits = {1: "ELF32", 2: "ELF64"}.get(blob[4], "?")
endian = {1: "little", 2: "big"}.get(blob[5], "?")
machine = struct.unpack_from("<H", blob, 18)[0]
entry = struct.unpack_from("<I", blob, 24)[0]
machine_name = {8: "MIPS (EM_MIPS)", 40: "PowerPC", 20: "PPC"}.get(machine, str(machine))
return f"{bits} {endian}-endian, {machine_name}, entry 0x{entry:08X}"
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("iso", type=Path, help="path to your own disc image (.iso)")
ap.add_argument("--verbose", "-v", action="store_true", help="list root directory")
args = ap.parse_args()
if not args.iso.is_file():
print(f"error: {args.iso} is not a file", file=sys.stderr)
return 2
try:
iso = open_iso(args.iso)
except NotAnIso as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
print("Disc identification")
print("=" * 62)
print(f" file {args.iso.name}")
print(f" size {args.iso.stat().st_size:,} bytes")
print(f" volume id {iso.volume_id or '(none)'}")
if "SYSTEM.CNF" not in iso.files:
# A truncated dump still has a valid PVD, so distinguish "damaged"
# from "genuinely not a PS2 disc".
if iso.volume_id:
print(
f"\n SYSTEM.CNF is missing but the image declares volume id "
f"{iso.volume_id!r}, which means the dump is truncated or damaged.",
file=sys.stderr,
)
return 4
print("\n no SYSTEM.CNF: not a PlayStation 2 game disc", file=sys.stderr)
return 3
lba, size = iso.files["SYSTEM.CNF"]
cnf = iso.read(lba, size).decode("latin1")
boot = cnf_value(cnf, "BOOT2").replace("cdrom0:\\", "").replace("\\", "/")
serial = boot.rsplit("/", 1)[-1]
version = cnf_value(cnf, "VER")
vmode = cnf_value(cnf, "VMODE")
print(f" BOOT2 {boot}")
print(f" serial {serial}")
print(f" version {version or '(absent)'}")
print(f" video mode {vmode or '(absent)'}")
if serial in KNOWN_TITLES:
title, edition = KNOWN_TITLES[serial]
print(f" title {title}")
if edition:
print(f" edition {edition}")
if boot in iso.files:
blba, bsize = iso.files[boot]
elf = iso.read(blba, bsize)
sha1 = hashlib.sha1(elf).hexdigest()
print()
print("Boot executable")
print("-" * 62)
print(f" size {bsize:,} bytes")
print(f" SHA-1 {sha1}")
print(f" CRC-32 {zlib.crc32(elf)}")
print(f" format {elf_summary(elf)}")
else:
print("\n the boot executable named by SYSTEM.CNF is not in the root directory")
sha1 = None
if args.verbose:
print()
print("Root directory")
print("-" * 62)
for name in sorted(iso.files):
l, s = iso.files[name]
print(f" {name:24s} LBA {l:>8,} {s:>12,} bytes")
print()
print("=" * 62)
is_target = serial == TARGET_SERIAL and version == TARGET_VERSION
if is_target and sha1 == TARGET_ELF_SHA1:
print(" This is the build we target: USA Black Label v1.01, NTSC 60 Hz.")
print(" Expected SHA-1 of the boot executable matches.")
elif serial == TARGET_SERIAL:
print(f" Serial matches, but version is {version or 'unknown'}"
f" and we target {TARGET_VERSION}.")
print(" Greatest Hits v2.00 is a different build and will not match")
print(" the reference material. Everything else should still work.")
else:
print(f" Not the target build. We target {TARGET_SERIAL} v{TARGET_VERSION}.")
if vmode == "PAL":
print(" PAL disc: the game runs at 50 Hz, so movement runs at 5/6")
print(" speed. Plan timing constants accordingly.")
return 0
if __name__ == "__main__":
raise SystemExit(main())