Files
BFM-decomp/docs/formats.md
T

35 KiB
Raw Blame History

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 US, all 8 .CD files; 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. Not the unpadded size: in SC01–SC07.CD every length is a multiple of 0x800 (398 of 398 entries); in MAIN.CD 48 of 49 are and one is not
  • 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).

Runtime reader (verified, src/800.c). The game does not parse these TOC headers at runtime; it builds its file table from LIST.CD (§2.3) and the ISO directory. LoaderInitFileTable (0x8001971C) resolves the ISO files with CdSearchFile, reads the first 0xE40 bytes of LIST.CD into a staging buffer at 0x80180000, then walks the 8 tables in order (MAIN, SC01 … SC07). For each table it takes that .CD file's disc position from the ISO directory (CdPosToInt), and for every entry with a non-zero start it writes {CdlLOC = .CD start + entry start; u32 length} into cdFileLocTable (0x800AE830), 8 bytes per sub-file, numbered across the 8 tables in order. Loads look entries up there: ResourceGetCdLoc (0x8001B788) maps a resource id through resourceIdMap to a cdFileLocTable row and returns its position.

Repacking. Positions and lengths reach the game only through LIST.CD and the ISO directory, so a sub-file that changes size or moves requires rebuilding the .CD file's own TOC (for tools), LIST.CD (what the game reads) and the ISO 9660 directory records (extent and size) of every .CD file that moved or grew. When patching a raw 2352-byte sector image directly, every rewritten sector also needs its EDC/ECC recomputed.

2.2 Roles of each .CD file

File Role Sub-file count Provenance
LIST.CD Concatenated TOCs of the other 8 .CDs (§2.3) — not itself a normal container; the game's runtime file table is built from it (§2.1) n/a verified-local US (§2.3)
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: the game builds its runtime file table from LIST.CD at boot instead of reading each archive's header sector (§2.1, LoaderInitFileTable). It is read into a staging buffer at 0x80180000 (only the first 0xE40 bytes) and resolved into cdFileLocTable (0x800AE830).

Verification status: verified-local US (2026-09-30). Recomputing the 8 trimmed TOCs from the US .CD files (MAIN, SC01 … SC07, in that order, each 8 + 8*N bytes, back to back with no alignment) gives 3,640 bytes (0xE38) that are identical to LIST.CD's first 3,640 bytes; the remaining bytes of the 4,096-byte file are zero. The loader reads the first 0xE40 bytes, which covers all 8 tables.

⚠️ 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 Game: not cleared (contents undefined, §4.4); write index starts at 1 (LzssDecodeSector state 1). brave.c: zero-filled global, 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:

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). The game does not zero its ring either. In LzssDecodeSector (0x80018730, src/800.c) state 1 sets only the write index (1), the flag mask and the first flag byte; the ring is the 1 KB at scratchpad 0x1F800000, which other engine code also uses as scratch (GTE matrix and rect work in src/800.c), so its contents at stream start are undefined. The game does not expect a zero-filled ring: a valid stream must never read a ring slot it has not itself written. All 138 US type-4 streams satisfy this (zero reads of an unwritten slot, verified-local 2026-09-30). Neither decoder is an oracle for a stream that breaks the rule.

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).
  • Terminator sector rule (game loader, verified src/800.c). The PAC header length is not used to find the end of a type-4 payload. CdReadSectorReadyCB feeds each 0x800-byte payload sector to LzssDecodeSector; when the decoder reaches the terminator it returns 0 and the rest of that sector is dropped. Unless the entry is the last in its chain, the loader then reads the next sector as a PAC header and aborts the load if it does not start with PAC\0. So the terminator (the sector holding its second byte) must fall in the entry's last payload sector; for an in-place replacement that keeps the layout, that is the same 2048-byte sector as in the retail file. A stream that ends a sector early makes the loader read payload as a header; one that runs a sector long decodes the next header as stream data. Example: SC01.CD sub-file 1 (0-based TOC index) starts with a type-4 entry whose payload spans 200 sectors, so a replacement stream, terminator included, must be 407,553–409,600 bytes. All 138 US type-4 streams end in their last payload sector (verified-local 2026-09-30).

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….