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:
Executable
+100
@@ -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
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+82
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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".
|
||||
@@ -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]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
@@ -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`.
|
||||
Executable
+231
@@ -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())
|
||||
Reference in New Issue
Block a user