mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
373418dc8d
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).
521 lines
32 KiB
Markdown
521 lines
32 KiB
Markdown
# 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…`.
|