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

32 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 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 .CDs (§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:

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