Files
BFM-decomp/docs/formats.md
T
Drew T 373418dc8d docs(phase-12): T5 — save trace findings (memcard path + directory confirmed; deep data deferred)
Live save-to-slot-1 trace via the PCSX-Redux web API confirmed the memcard path
bu00:BASLUS-00726MUSASHI (libmcrd) + the directory/title-frame staging in low RAM. The
save-DATA serialization couldn't be isolated from a live before/after diff (running-game
churn; the one big changed region @0x8007CD28 is an unrelated UI list). Clean route = the
.mcd file / a paused capture -> deferred as Gen3-repack territory, NOT needed for the
SaveLoadRoutine byte-match (code-level). SaveLoadRoutine stays the documented hard-defer
(1139-ins multi-entry, Gen1 precedent, P9).
2026-06-16 14:33:54 -06:00

521 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# On-Disc File Formats — Brave Fencer Musashi (SLUS-00726 USA)
> **Evolvable reference layer.** This file may be updated freely as RE progresses; note each
> change in the active phase log (`CURRENT_PHASE.md`). It is the canonical specification of
> the disc image layout and the game's container/compression formats (.CD, PAC, LZSS, SQV).
> Every entry is tagged with its provenance; anything not byte-verified against the local
> US dump is explicitly marked **UNVERIFIED** or **TBD**. RAM addresses are owned by
> `docs/memory-map.md` (authoritative); they are cross-referenced here, never redefined.
**Primary sources reconciled in this file:**
| Source | What it provides | Trust level |
|---|---|---|
| Local dump `Brave Fencer Musashi (USA)\` (4×.bin + .cue) | All `verified-local` facts (re-parsed 2026-06-10) | Ground truth |
| `tools/brave-CUE/brave.c` + `brave.txt` (CUE, 2012-2013, GPLv3) | Reference extractor: .CD TOC, PAC header, LZSS decoder | Authoritative for tool semantics; **diverges from game semantics on LZSS termination (§4.4)** |
| AiroRom wiki MIPS listing (pleonex) | Game-side LZSS decompressor, byte-verified vs local US EXE | Verified (see memory-map §2.1) |
| jywjyw/bravefencer-hack (Java repacker, 2018) + `doc/note.md` | LIST.CD structure, PAC details, working recompressor, overlay map | **JP (SLPS-01490)** unless noted — re-verify for US |
| romhacking.net thread 15730 (Vehek, Dark Fulgore, Auryn, 2013-2014) | LZSS termination/bias discoveries, US datapoints | Reported, partially re-verified |
---
## 1. Disc layout
### 1.1 BIN/CUE — 4 tracks
From the local `.cue` and track binaries (verified-local):
| Track | Mode | File size (bytes) | Sectors (2352 B raw) | Content |
|---|---|---|---|---|
| 01 | MODE2/2352 | 364,846,944 | 155,122 | Data (ISO9660 volume `MUSASHI`) |
| 02 | AUDIO (CD-DA) | 10,678,080 | 4,540 | `ST01_13A.DA` (see §1.4); INDEX 00 at 00:00:00, INDEX 01 at 00:02:00 |
| 03 | AUDIO (CD-DA) | 7,328,832 | 3,116 | `ST01_13B.DA`; same index layout |
| 04 | AUDIO (CD-DA) | 33,167,904 | 14,102 | `DUMMY_DA.DA`; same index layout |
Volume space size in the PVD = 176,880 sectors = 155,122 + 4,540 + 3,116 + 14,102 exactly
(the ISO volume spans all four tracks) (verified-local).
### 1.2 Track 1 sector geometry
- Raw sectors are 2352 bytes (MODE2). **User data (2048 bytes) begins at raw-sector offset
+24** (12-byte sync + 4-byte header + 8-byte Mode 2 Form 1 subheader).
- Byte offset of LBA *n*'s user data within `(Track 1).bin` = `n * 2352 + 24`.
- PVD at LBA 16; root directory record at LBA 22 (2048 bytes) (verified-local).
### 1.3 ISO root inventory — 27 files, no directories
Re-parsed from the raw image 2026-06-10 (verified-local). All names carry the `;1` version
suffix on disc; LBA = 2048-byte-sector index from track start; sizes are exact byte lengths
from the directory records.
| File | LBA | Size (bytes) | Notes |
|---|---|---|---|
| SYSTEM.CNF | 23 | 68 | Boot config (§1.5) |
| SLUS_007.26 | 24 | 413,696 | PS-X EXE (§1.6) |
| MUSA.ID | 226 | 9 | Disc-ID check file; contents `MUSASHI\r\n` |
| LIST.CD | 227 | 4,096 | Concatenated .CD TOCs (§2.3) |
| MAIN.CD | 229 | 5,988,352 | Resident/global archive, 49 sub-files (§2.2) |
| LOGOA.STR | 3,153 | 3,080,192 | FMV (out of scope, §7) |
| LOGOB.STR | 4,657 | 3,080,192 | FMV |
| SC01.CD | 6,161 | 20,078,592 | Chapter 1 scenario archive |
| ST01.STR | 15,965 | 12,795,904 | FMV |
| OPEN.STR | 22,213 | 22,724,608 | FMV |
| SC02.CD | 33,309 | 22,126,592 | Chapter 2 scenario archive |
| ST02.STR | 44,113 | 21,430,272 | FMV |
| SC03.CD | 54,577 | 64,569,344 | Chapter 3 scenario archive (largest) |
| ST03.STR | 86,105 | 8,028,160 | FMV |
| SHOPS01.STR | 90,025 | 7,536,640 | FMV |
| OUT.STR | 93,705 | 1,867,776 | FMV |
| SC04.CD | 94,617 | 19,425,280 | Chapter 4 scenario archive |
| ST04.STR | 104,102 | 17,825,792 | FMV |
| SC05.CD | 112,806 | 16,498,688 | Chapter 5 scenario archive |
| ST05.STR | 120,862 | 9,240,576 | FMV |
| SC06.CD | 125,374 | 23,928,832 | Chapter 6 scenario archive |
| ST06.STR | 137,058 | 18,612,224 | FMV |
| SC07.CD | 146,146 | 12,734,464 | Chapter 7 scenario archive |
| END.STR | 152,364 | 5,341,184 | FMV |
| ST01_13A.DA | 155,272 | 8,990,720 | CD-DA pointer → track 2 (§1.4) |
| ST01_13B.DA | 159,812 | 6,074,368 | CD-DA pointer → track 3 |
| DUMMY_DA.DA | 162,928 | 28,573,696 | CD-DA pointer → track 4 (padding audio) |
Observations (verified-local):
- **12 STR files** (not 13 as one earlier planning summary said — that was a miscount;
this table is the corrected, re-verified inventory). Tally: 12 STR + 3 `.DA` + 12 others
(SYSTEM.CNF, SLUS_007.26, MUSA.ID, LIST.CD, MAIN.CD, SC01–SC07.CD) = 27.
- STR movies are interleaved between the chapter archives in ascending LBA order
(seek-distance optimization).
- All `.CD` sizes are exact multiples of 2048.
- Layout is contiguous: e.g. SLUS_007.26 occupies LBA 24–225 (202 sectors), MUSA.ID
follows at 226; MAIN.CD ends exactly where LOGOA.STR begins.
### 1.4 `.DA` files = directory entries pointing into the CD-DA tracks
Verified by arithmetic in-session (2026-06-10): each `.DA` extent LBA equals the first
INDEX 01 sector of an audio track, and each size equals `(track sectors − 150 pregap) × 2048`
(the directory records use 2048 B/sector accounting even though the underlying sectors are
2352-byte audio):
| File | Extent LBA | Track start (cumulative) | + 150 pregap | Size check |
|---|---|---|---|---|
| ST01_13A.DA | 155,272 | track 2 @ 155,122 | = 155,272 ✓ | (4,540−150)×2048 = 8,990,720 ✓ |
| ST01_13B.DA | 159,812 | track 3 @ 159,662 | = 159,812 ✓ | (3,116−150)×2048 = 6,074,368 ✓ |
| DUMMY_DA.DA | 162,928 | track 4 @ 162,778 | = 162,928 ✓ | (14,102−150)×2048 = 28,573,696 ✓ |
So the `.DA` files contain no data inside track 1; they are the ISO-level handles the game
uses to seek CD-DA audio. They are **out of scope** for the extraction pipeline (§7).
### 1.5 SYSTEM.CNF (68 bytes, verified-local)
```
BOOT = cdrom:\SLUS_007.26;1
TCB = 4
EVENT = 16
STACK = 801ffff0
```
### 1.6 SLUS_007.26 — PS-X EXE header (verified-local)
| Field | Value | Notes |
|---|---|---|
| magic | `PS-X EXE` | |
| `pc0` | 0x80010000 | Entry point |
| `gp0` | 0x00000000 | No $gp in header ⇒ likely −G0; confirm in Phase 6 |
| `t_addr` | 0x80010000 | Image spans 0x80010000–0x80074800 |
| `t_size` | 0x64800 | File = 0x800 header + 0x64800 text = 0x65000 = 413,696 bytes ✓ |
| `sp` | 0x801FFFF0 | Matches SYSTEM.CNF STACK |
The EXE is tiny (~400 KB) relative to a ~340 MB game: the bulk of code/script lives inside
the `.CD` archives as LZSS-compressed PAC type-4 overlays (§3, §4; memory-map §4).
### 1.7 The `\DEBUG.BIN;1` ghost reference
The EXE's CD-path string table (`CdPathTable`, base **0x80062C24**, 23 entries, 0x30-byte
stride — see memory-map §2.2) contains the string **`\DEBUG.BIN;1` at 0x80063044**
(verified-local), adjacent to `\MUSA.ID;1` at 0x80063014. **DEBUG.BIN does not exist on the
retail ISO** (see §1.3). A surviving code path therefore references a debug loader file that
was stripped from the disc — a prime RE anchor (xref the string to find the debug loader;
relates to the TCRF L3 debug menu, memory-map §5).
Also note: the table covers LIST.CD, MAIN.CD, SC01–SC07.CD and ten of the STR movies, but
`LOGOA.STR`/`LOGOB.STR` and the three `.DA` files are absent from it — referenced elsewhere
(**TBD**, memory-map open question #14).
---
## 2. .CD container format
### 2.1 TOC structure (verified-local for MAIN.CD; brave.c is the reference parser)
A `.CD` file begins with a table of contents at offset 0:
| Offset | Type | Meaning |
|---|---|---|
| +0x00 | u32 LE | `count` — number of sub-files (MAIN.CD: 0x31 = 49) |
| +0x04 | u32 LE | zero |
| +0x08 + 8*i | u32 LE | sub-file *i* start, **in 2048-byte sectors relative to the start of the .CD file** |
| +0x0C + 8*i | u32 LE | sub-file *i* length in bytes (unpadded) |
- **The first field of each entry is NOT a disc LBA.** `brave.txt` calls it "LBA", but the
code in `brave.c::Extract()` proves it is file-relative (`buffer + entry_sector * 0x800`).
Do not add the `.CD` file's own disc LBA when splitting.
- The TOC and every sub-file are sector-aligned (0x800), zero-padded, and contiguous; the
first sub-file starts at sector 1 (offset 0x800).
- MAIN.CD and SC01–SC07.CD use this identical format — one code path in every known tool,
no per-file special-casing (brave.c; confirmed by jywjyw's `Conf.java` rebuilding all 8
with the same `CdRebuilder`).
### 2.2 Roles of each .CD file
| File | Role | Sub-file count | Provenance |
|---|---|---|---|
| LIST.CD | Concatenated TOCs of the other 8 `.CD`s (§2.3) — **not itself a normal container** | n/a | jywjyw (JP) + partial local check |
| MAIN.CD | Global/resident archive: loader-resident script blob (`FILE_010` PAC entry index 1, type 1), fonts, shared assets | **49** (0x31, verified-local US; same count in JP) | local + jywjyw |
| SC01.CD | Chapter 1 scenario archive (per-location PAC chains: scripts/overlays, graphics, VAB audio, SQV music) | **86** (0x56, **verified-local US 2026-06-13**; == JP) | local + jywjyw |
| SC02.CD | Chapter 2 scenario archive | **43** (**verified-local US 2026-06-13** — corrects the "45" Auryn hearsay) | local |
| SC03.CD | Chapter 3 scenario archive | **140** (verified-local US 2026-06-13) | local |
| SC04.CD | Chapter 4 scenario archive | **31** (verified-local US 2026-06-13) | local |
| SC05.CD | Chapter 5 scenario archive | **30** (verified-local US 2026-06-13) | local |
| SC06.CD | Chapter 6 scenario archive | **39** (verified-local US 2026-06-13) | local |
| SC07.CD | Chapter 7 scenario archive | **29** (verified-local US 2026-06-13) | local |
Sub-files inside a `.CD` are either PAC chains (§3) — identified by the `PAC\0` magic — or
non-PAC files, which per `brave.txt` are SQV (MIDI) archives (§5). Anything without either
signature is unknown (**TBD**).
JP reference sizes (jywjyw `Conf.CD_JAPAN_SIZE`, for cross-version diffing only — the US
sizes are in §1.3): MAIN 6,205,440; SC01 20,615,168; SC02 6,580,224; SC03 78,168,064;
SC04 20,764,672; SC05 16,502,784; SC06 23,908,352; SC07 12,730,368.
### 2.3 LIST.CD (4,096 bytes = 0x1000)
Per `brave.txt`: "contiene las cabeceras de todos los archivos CD" (contains the headers of
all the CD archives). The exact structure, per jywjyw's `ListCdWriter.java` +
`CdHeader.trimBuild()` (JP repacker source):
- LIST.CD = the 8 TOCs of MAIN.CD, SC01.CD … SC07.CD **in that order**, each **trimmed** to
exactly `8 + 8*N` bytes (u32 count + u32 zero + N entries — i.e. the TOC without its
sector padding), **concatenated back-to-back with no alignment between tables**, written
into a fixed 0x1000-byte (2-sector) buffer.
- Purpose (jywjyw `note.md`): loaded into RAM at game start so the engine can locate any
sub-file in any `.CD` without re-reading each archive's header sector. US RAM cache
location **TBD** (memory-map §3.2 `listCd_ramCache`, open question #3).
**Verification status:** locally confirmed only that LIST.CD *begins with* an exact copy of
MAIN.CD's TOC (first bytes identical to MAIN.CD sector 0). The full
trimmed-concatenation layout (SC01–SC07 portions, their order, the no-alignment packing) is
from the JP repacker and is **UNVERIFIED against the US disc** — verify by recomputing the
8 trimmed TOCs from the US `.CD` files and diffing against LIST.CD (cheap Phase-2 check).
⚠️ CUE's BRAVE tool does **not** special-case LIST.CD. Because LIST.CD *starts* with a valid
TOC, running BRAVE on it parses MAIN.CD's table and then reads garbage sector offsets inside
a 4 KB file. Never feed LIST.CD to the extractor; treat it as derived data (regenerable from
the other 8 TOCs).
---
## 3. PAC archive format
A `.CD` sub-file whose first u32 is `0x00434150` (`"PAC\0"` LE) is a **PAC chain**: a
sequence of entries, each with its own 0x800-byte header block immediately followed by its
payload, repeated until an entry's last-flag is set.
### 3.1 Entry header (0x800-byte block; fields per brave.c/brave.txt + jywjyw PacHeader.java)
| Offset | Type | Meaning | Status |
|---|---|---|---|
| +0x00 | u32 LE | Magic `0x00434150` = `"PAC\0"` | verified (tool + local) |
| +0x04 | u8 | **Type** (0–8), selects payload interpretation (§3.2) | verified |
| +0x05 | u8 | **Last-entry flag**: 1 = last entry in chain, 0 = more follow | verified |
| +0x06 | u16 | Zero | verified |
| +0x08 | u32 LE | **Unknown** — partly characterized (Phase 2, 2026-06-13). Across all 1189 US PAC entries it is **non-zero only for type 0 (301/301 graphics) and type 6 (29/29)**; it is **0 for every type-4 entry**, so it is **NOT a decompressed-size hint** (the original F2 guess is refuted). Likely a type-specific parameter (type-0 graphics dimensions/format; type-6 unknown). jywjyw preserves it verbatim. Decode meaning via the loader (memory-map #7) | type-0/6 param (F2) |
| +0x0C | u32 LE | **Length including this 0x800 header** — payload = `len − 0x800`, payload starts at entry+0x800 | verified |
| +0x10 … +0x7FF | — | Not interpreted by any known tool. jywjyw preserves the *entire* 0x800 header verbatim, implying it may contain meaningful non-zero bytes | **UNVERIFIED** |
### 3.2 Type table
Sources: `brave.txt` (CUE) + jywjyw `note.md`.
| Type | Payload | Notes |
|---|---|---|
| 0 | Graphics | Mostly 32×32 image blocks (8bpp = 64×32, 4bpp = 128×32 per jywjyw) |
| 1 | Graphics / **uncompressed script** | The memory-resident MAIN.CD `FILE_010` entry "1.1" and the shop-dialog "2.1" scripts are type 1 |
| 2 | VAB header (`.VH`) | PsyQ sound bank header |
| 3 | VAB body (`.VB`) | PsyQ sound bank sample data |
| 4 | **LZSS-compressed data** (§4) | Location scripts **containing embedded MIPS code** = the game's overlays. The only compressed type |
| 5 | — | Unused per CUE ("---") |
| 6 | Unknown | **TBD**. jywjyw notes all of a type-6 entry's data fits within its first 0x800 bytes |
| 7 | Unknown | **TBD** |
| 8 | Graphics | |
Only type 4 is compressed; everything else is stored raw. ("All .CD files are
LZSS-compressed" — an early planning claim — is **wrong**; compression is a per-PAC-entry
type flag, with no magic and no size heuristic.)
**Observed US type distribution (Phase 2, all 1189 PAC entries, 2026-06-13):**
type 0 = 301, type 1 = 166, type 2 = 201, type 3 = 201, type 4 = 138, type 6 = 29,
type 7 = **139**, type 8 = 14; **type 5 = 0 (absent, consistent with "unused")**. Note types
2 and 3 are equal counts (201 each) — VAB header/body pairs. **Type 7 is common (139
entries), not rare**, and type 6 (29) — both remain uninterpreted (F1), extracted raw.
### 3.3 Chaining (brave.c::UnPAC, verbatim semantics)
```
pos = 0
do:
type = buf[pos+0x04]; last = buf[pos+0x05]
len = u32le(buf + pos + 0x0C) # includes the 0x800 header
payload = buf[pos+0x800 : pos+len] # len − 0x800 bytes
pos += (len + 0x7FF) & ~0x7FF # next entry at sector alignment
while not last
```
Entries are sector-aligned, zero-padded between payload end and the next 0x800 boundary.
---
## 4. LZSS variant (PAC type-4 payloads) — CRITICAL SECTION
Two authoritative implementations exist and they **disagree on termination** (§4.4):
the game's own MIPS decompressor (US EXE **0x80018730**, byte-verified against the AiroRom
listing — memory-map §2.1) and CUE's `brave.c::Decode()` (tool reference). Everything below
is reconciled from both plus Vehek's findings in romhacking thread 15730.
### 4.1 Parameters
| Parameter | Value | Notes |
|---|---|---|
| `POS_BITS` | 10 | Position field width |
| `N` (ring size) | 1024 bytes (`1 << 10`) | Game keeps the ring in **scratchpad 0x1F800000** (memory-map §3.1) |
| `THRESHOLD` | 1 (CUE's naming) | jywjyw's encoder calls it 2 — naming difference only; both agree real match length = 6-bit field + 2 |
| Max match `F` | 65 bytes (`(1 << 6) + 1` + the loop's `<=`) | Copy lengths range **2..65** |
| Ring initial state | Zero-filled, write index `r = 0` | Encoder-side origin is index **1** (§4.3, §4.6) |
| Decompressed size | **No size field anywhere** | CUE grows the output buffer dynamically (start 128 KB, +64 KB steps); the game decodes until the terminator |
### 4.2 Flag engine (LSB-first with 0xFF00 sentinel)
From `brave.c`, verbatim:
```c
if (!((flags >>= 1) & 0xFF00)) flags = 0xFF00 | encoded[enc_size++];
if (flags & 1) { /* literal */ } else { /* match */ }
```
- A flag byte is loaded into the low 8 bits with sentinel `0xFF00` ORed above it; each step
right-shifts once and tests bit 0. After 8 shifts the sentinel is exhausted
(`& 0xFF00 == 0`) and the next flag byte is fetched. Net effect: **flag bits are consumed
LSB-first**, one flag byte governs the next 8 tokens.
- The game's MIPS routine implements the same order with a mask walking 0x01→0x80 against a
held token byte (AiroRom listing) — equivalent semantics, verified.
- Flag bit **1 = literal**, **0 = match reference**.
### 4.3 Literal and match paths
**Literal (flag bit = 1):** read 1 input byte → append to output **and** write to
`ring[r]`; `r = (r + 1) & 0x3FF`.
**Match (flag bit = 0):** read u16 **little-endian** `code`, then:
```
pos = code & 0x3FF // low 10 bits — ABSOLUTE ring index (NOT a relative offset!)
len = (code >> 10) + 1 // THRESHOLD = 1
for i in 0 ..= len: // len+1 iterations => 2..65 output bytes
b = ring[(pos - 1 + i) & 0x3FF] // note the −1 bias
output(b); ring[r] = b; r = (r + 1) & 0x3FF
```
- **Absolute index, not distance:** the 10-bit `pos` addresses the ring directly. Generic
LZSS tools (relative-distance decoders) will NOT work on this format.
- **The −1 bias:** stored positions are ring-index+1, because the game's *encoder* starts
writing at ring index 1 (Vehek: "the starting position in the buffer appears to be 1").
An off-by-one here corrupts every decompressed overlay. Validate any new implementation
by diffing a decompressed type-4 blob against a live emulator RAM dump at the overlay
load address (memory-map §4, open question #1).
- The ring write pointer `r` advances on **every** output byte (literals and match bytes
alike) and is independent of `pos`.
### 4.4 Termination — GAME vs TOOL discrepancy (load-bearing)
| Implementation | Stop condition | Consequence |
|---|---|---|
| **Game** (MIPS @ 0x80018730, US-verified; Vehek Aug 2014) | A match code whose **low 10 bits == 0** (`pos == 0`) terminates the stream. Streams typically end with bytes `00 00`. The PAC header length field has **no effect** on the game's decompressed size (Vehek-tested) | `pos == 0` is reserved ⇒ legal references are `pos` 1..1023, i.e. biased start indices 0..1022 |
| **CUE's brave.c** | No terminator check; stops when the input length (`PAC len − 0x800`) is consumed | Trusts the header length; never sees `pos == 0` specially |
**Project rule:** our extractor must implement **game semantics — stop on `pos == 0`** —
with a **length sanity cross-check**: after hitting the terminator, verify input consumption
is consistent with the PAC length field (`len − 0x800`, allowing trailing padding), and warn
on any stream that exhausts its input without a terminator or overruns it.
**Verified Phase 2 (2026-06-13, F6 RESOLVED):** all **138** US type-4 streams decode cleanly
under game semantics — every one hits a `pos == 0` terminator with the length cross-check
satisfied (zero warnings across the whole disc). Cross-checked byte-for-byte against
`brave.c`: our output is a **strict prefix** of brave's for all 138, and brave appends
**exactly 2 bytes** to each — it decodes the terminating `00 00` as a match (code 0 ⇒
pos 0, len 1 ⇒ emits `ring[1023], ring[0]` = `00 00`) instead of stopping. So game and tool
semantics **coincide on the real payload**; the only divergence is brave's 2-byte over-decode
of the terminator/padding. Our game-semantics output is the correct one. No retail stream was
found where the two diverge *within* the payload.
Additional tool caveat: `brave.c`'s `ring[]` is a global that is never re-zeroed between
files (only `r` resets), so CUE's tool is not a strict oracle for streams that reference
ring positions not yet written; the game expects a zero-filled ring at stream start.
### 4.5 Game-side streaming state machine (resumable, sector-fed)
The game's decompressor consumes input in **0x800-byte CD-sector chunks** and is a
**resumable 5-state machine** so decompression can suspend mid-token at a sector boundary.
All addresses US-verified; full ledger in memory-map §2.1/§3.1:
| Item | Address (US) |
|---|---|
| Decoder entry | 0x80018730 (`LzssDecodeSector`); returns at 0x80018910 |
| State jump table | 0x80072A30 (`00000000 FFFFFFFF` then 5 state pointers) |
| State variable | 0x800C7D24 (runtime BSS) |
| Persistent state block | 0x800747A0 mask, 0x800747A4 token byte, 0x800747AC output ptr, 0x800747B0 ring index, 0x800747B4 partial 16-bit match code |
| Ring buffer | 0x1F800000 (scratchpad, 1024 B) |
| Sector staging buffer | JP 0x80078BD0–0x800793D0 — **JP-only, US equivalent TBD** (memory-map §3.2) |
States: 0 finish, 1 init token ring, 2 check token bit, 3 process match, 4 next token bit.
The partial-code slot exists precisely because a 16-bit match code can straddle a sector
boundary.
### 4.6 Recompression — NOT byte-stable (encoder constraints)
- LZSS parsing is ambiguous (literal-vs-match and match-length choices); jywjyw's working
binary-search-tree encoder explicitly notes "recompressed 0.4 script not equals original".
**No recompressor is expected to reproduce Square's original bitstream.**
- ⇒ **Compressed type-4 payloads are opaque binary for build verification.** Only the
*decompressed* overlays can be matched; the matching build treats compressed blobs as
binary data. Repacking/injection is a **Gen3 parking-lot concern**.
- Any future encoder MUST: (a) **never emit a `pos == 0` reference mid-stream** — stock
Okumura-style LZSS output does, and the game treats it as end-of-stream (Vehek's failed
2013 attempt); (b) terminate the stream with a `pos == 0` pair; (c) start its ring origin
at index 1 (jywjyw's `Compresser.java` does exactly this — it also documents the encoded
byte order: byte0 = `pos & 0xFF`, byte1 = `(len-2) << 2 | pos >> 8`).
---
## 5. SQV format (sequenced music archives)
`.CD` sub-files that are not PAC chains; signature `".sqv"`. Source: `brave.txt` (CUE) —
**not yet locally re-verified against extracted US files (UNVERIFIED)**.
| Item | Spec |
|---|---|
| Magic | 4 bytes `".sqv"` at +0x00 |
| Header u32s | Up to **3** u32 offsets to `MThd` (Standard MIDI File) sub-files. Each value is an offset **to which 4 must be added** (+4 bias); a value of **0 means the archive holds a single MThd** (no further offsets) |
| Header alignment | 16 bytes, zero-padded |
| Member alignment | 4 bytes, zero-padded |
| VAB | A VAB file at **fixed offset 0x7000**, after the MThd data |
Pairs with PAC types 2/3 (VAB header/body) for the game's sound banks. **Driver — semi-custom
(Phase 12, partial):** the resident engine holds a **thin Square-custom sound layer over PsyQ
libsnd**. `DsMix` (0x800D1BD8) is a custom 2-line wrapper (`{ FUN_800d1bf8(); return 1; }` — ignores
its arg; NOT the stock libsnd `DsMix` — ghidra_psx_ldr's PsyQ-4.7 signature hit there was coincidental,
Phase-12 T1). The resident's sound cluster wraps/calls the (EXE-resident) PsyQ libsnd SEQ/VAB
primitives; the SQV→libsnd bridge is the custom glue (its **bytes** were matched in the Phase-12 harvest,
but the deep MThd→SEQ + VAB-load *playback semantics* are not exhaustively traced — deferred to Gen3 repack
work, and not needed for the byte-match). Verdict: **semi-custom** = Square wrappers/glue over stock PsyQ
libsnd sequencing.
## 6. Save / memory-card format (Q#5 — Phase 12, partial)
`SaveLoadRoutine` @ `0x8002B154` (EXE) is a multi-entry save/load handler blob driving the **linked
PsyQ libmcrd** primitives (`0x8005Fxxx`–`0x80061xxx`, e.g. `0x800603BC` heavily, `0x8006023C`,
`0x80060AE0`, `0x80061114`). Its config/dispatch data is `saveHeaderTemplate` @ `0x80072DF0`:
| Off | Bytes | Meaning |
|---|---|---|
| +0x00 | `82 67 82 85 82 92 82 8F` + pad | 4-char SJIS label (`Hero`?) |
| +0x0C | `"BASLUS-00726MUSASHI"` | **memcard filename** (PS1 `BA` + region-product + name) |
| +0x20 | SJIS `BRAVE FENCER MUSASHI` | **save title** (BIOS memcard-manager display) |
| +0x44 | `2A 00 00 00` | count/flag (0x2A = 42) |
| +0x54 | `0x8002B154, 0x8002B1AC, 0x8002BEA4, …` (a run of `0x8002Bxxx` pointers) | **handler dispatch table** — addresses INTERIOR to the SaveLoadRoutine blob; the save/load/validate sub-operations |
So the Gen1 "multi-entry blob" note **is** this dispatch table: the header holds a vector of sub-handler
addresses (the 3 known entries + more), each performing one memcard step via libmcrd.
**VERIFIED (static, Phase 12):** the header layout above; SaveLoadRoutine calls the linked libmcrd objects.
**TBD (needs Ghidra-on-EXE + a Drew-operated PCSX-Redux save trace + PsyQ-memcard research, R17):** the
**save-DATA block** serialization — almost certainly the Phase-3 player/progress state (`0x80078Exx`:
gold/HP/BP/day/hour/flags) + a **checksum**; capture the live memcard buffer from a real save to pin the
field order + the checksum algorithm — and matching the tractable handler entries. `dumps/ram_savescreen.bin`
(save screen, overlay flushed) holds the live `saveHeaderTemplate` for cross-check.
**Runtime-confirmed (Phase 12 trace — live save to card slot 1):** the memcard path is
`bu00:BASLUS-00726MUSASHI` (PsyQ device path `bu00:` = card unit 0 / slot 1, via libmcrd); the
title-frame + a directory list of 32-byte records stage in low RAM (~`0x80004E00`). The
**save-DATA serialization** (which game-state region is written + the checksum) was NOT isolated
from a live before/after RAM diff — the running game churns the diff (491 changed runs; the one
big region @`0x8007CD28` is an unrelated UI list of `"XXX,"`-tagged records, not save data), and
BFM appears to serialize from the live game-state rather than a findable staging copy. The clean
route to the byte-exact block is the **`.mcd` memcard file** (or a paused-state capture).
**Deferred** as Gen3-repack territory — **not needed for the `SaveLoadRoutine` byte-match** (that
is code-level: matched from the disassembly like any other function, when attempted; the 1139-ins
multi-entry blob remains the documented hard-defer per the Gen1 precedent, P9).
---
## 6. Extractor implementation notes (Phase 2 pipeline)
1. **Naming collision in CUE's tool — do not copy.** `brave.c` saves sub-files as
`<file>.dir/FILE_%03d` (fine) but names PAC chain entries `%03d` **by type byte**
(`sprintf(name, "%s.dir/%03d", filename, num)` where `num` = type). Two same-type entries
in one chain **silently overwrite each other** on extraction. **Our extractor uses
`{index}.{type}` naming** (jywjyw's `CdSplitter` convention: `0.4`, `1.1`, `2.1` …) so
every entry survives and the type remains visible.
2. **Decode rule:** decompress **only** type-4 entries, using game termination semantics
with the length cross-check (§4.4). Save all other types raw. Save non-PAC sub-files
as-is (SQV etc.).
3. **Never run the pipeline on LIST.CD** (§2.3); instead verify LIST.CD by *regenerating*
the trimmed-TOC concatenation from the 8 archives and diffing.
4. **Round-trip / repack reference (Gen3):** `github.com/jywjyw/bravefencer-hack`
(Java/Maven, 2018) — `CdHeader.java` (TOC rebuild: count, entries from sector 1,
`addr += paddedLen/0x800`), `PacHeader.java` (full 0x800 header incl. unknown bytes),
`hack/Compresser.java` (game-compatible LZSS encoder; LGPL 2.1 header, adapted from
JSword), `hack/CdRebuilder.java` (rebuilds all 8 .CDs, preserves original PAC headers via
`headers.bin`, recomputes length fields, pads to 0x800, warns if a rebuilt .CD outgrows
the original), `hack/ListCdWriter.java` (LIST.CD regeneration), `dump/Uncompresser.java`
+ `dump/CdSplitter.java` (decoder/splitter with the `{index}.{type}` naming). Its
`doc/note.md` is the best third-party memory/format document (JP-based).
5. **Cross-validation — DONE (Phase 2, `tools/bfm_extract/crosscheck.py`).** brave is built
from source on WSL via `tools/brave-CUE/posix_shim.h` (the GPL upstream `brave.c`/
`common.inc` stay byte-for-byte unmodified; `brave.exe` is a Windows PE, unused on Linux).
The harness maps brave's type-only names back to our `{index}.{type}` by re-walking each
chain (skipping "shadowed" same-type entries brave overwrites), then byte-compares.
Result on `--sample all`: **1484 raw payloads identical, 138/138 type-4** byte-identical up
to our `pos==0` terminator (brave's 2-byte over-decode logged as the expected F6
divergence; see §4.4).
6. **Licensing:** `brave.c` is GPLv3 (study/port is fine; note license if vendoring);
`Compresser.java` carries LGPL 2.1.
## 7. Out of scope (for now)
| Format | Status |
|---|---|
| `.STR` (12 files) | Standard PS1 STR movie streams (Mode 2 Form 2 interleaved video/XA). No game-specific container suspected (**UNVERIFIED** — assumed standard). Not part of the extraction/matching pipeline |
| `.DA` (3 files) | CD-DA track handles (§1.4). **Phase 2 (user scope change 2026-06-13):** now extracted as raw 2352-byte/sector audio from tracks 2-4 — ST01_13A.DA=Track 2 (10,325,280 B), ST01_13B.DA=Track 3 (6,976,032 B), DUMMY_DA.DA=Track 4 (32,815,104 B). Their 2048-based ISO sizes differ from the raw audio by design. Out of scope for *matching* (audio), but extracted for completeness |
## 8. Open questions specific to formats
| # | Question | Attack |
|---|---|---|
| F1 | PAC types 6/7 semantics (CUE "???", jywjyw "??") | **Phase 2:** both present in US (type 6 = 29, type 7 = **139** entries), extracted raw; semantics still TBD → Ghidra PAC-header parser in the loader |
| F2 | PAC header +0x08 u32 meaning | **Phase 2: size-hint hypothesis REFUTED** — +0x08 == 0 for all 138 type-4; non-zero only for type 0 (301/301) and type 6 (29/29). Remaining: decode the type-0/6 meaning via the loader |
| F3 | PAC header bytes +0x10..+0x7FF — meaningful content? | Hex-survey across all extracted headers (jywjyw preserves them) — **not yet done** |
| F4 | LIST.CD full layout on the US disc (only the MAIN.CD-TOC prefix is locally verified) | **Deferred** — extractor excludes LIST.CD (§2.3); regenerating the 8 trimmed TOCs and diffing is a cheap follow-up |
| F5 | ~~US sub-file counts for SC01–SC07~~ — **RESOLVED (Phase 2)** | MAIN=49, SC01=86, **SC02=43** (not the hearsay 45), SC03=140, SC04=31, SC05=30, SC06=39, SC07=29 — read from each US TOC (§2.2) |
| F6 | ~~Do any retail type-4 streams violate the length/terminator cross-check?~~ — **RESOLVED (Phase 2): NO** | All 138 decode clean (0 warnings) and are byte-identical to brave up to our `pos==0` terminator (§4.4) |
| F7 | Are `.sqv` details (3-offset cap, +4 bias, VAB at 0x7000) correct for US files? | **Phase 2:** 98 SQV sub-files detected by `.sqv` magic and extracted raw; header details still UNVERIFIED (Gen3 parse) |
| F8 | Where are LOGOA/LOGOB.STR and the .DA files referenced, if not in `CdPathTable`? | memory-map open question #14 |
> **Phase 2 extraction summary (2026-06-13).** One command (`tools/bfm_extract/extract.py`)
> extracts the full disc: 24 Track-1 files verbatim + 3 `.DA` files as raw CD-DA audio from
> tracks 2-4 (each `(Track N).bin` = 150-sector INDEX 00→01 pregap + audio). The 8 `.CD`
> archives split into **447 sub-files → 1189 PAC entries** (`{index}.{type}` naming, no
> collisions) → **138 type-4 LZSS payloads** decoded under game semantics. A deterministic
> JSON-Lines manifest (`manifest.jsonl` + `manifest.sha1`, 1801 artifacts) is the
> reproducibility contract; `--verify` re-checks it. Cross-validated byte-for-byte against
> CUE's `brave` (built from source via `tools/brave-CUE/posix_shim.h`): **1484 raw payloads
> identical, 138/138 type-4 F6-equivalent** (ours = the game-correct prefix). EXE round-trips
> to SHA1 `143dbb89…`.