Files
AdubbzandAlessandro Suha ccec0abd2e PAL (#41)
* Name the discs by region and put every build input and output under a region directory

The disc images are rom/Dark Cloud (NTSC).iso and rom/Dark Cloud (PAL).iso;
each region's checksums and extracted files live under rom/ntsc and rom/pal.
scripts/build/region.py turns REGION (NTSC by default, or PAL) into the paths
every script and CMake use: config/<region> holds the split configuration,
symbol lists, fixups and linker script, splat writes asm/<region>, and CMake
builds into build/<region>. The overlay headers and pads are splat pad
segments, so no assets/ tree is written, and the unused config/reference_asm
and reference_asm.py are gone. overlay_private.sh places a private-repository
file still kept under rom/extracted/ in rom/ntsc/extracted/.


* Carry PR 40's PAL source differences; NTSC unchanged

The PAL prototype's small divergences sit inline under #ifdef PAL in 30
units; common.h gains the per-region screen geometry and GS Y offset,
clsmes.hpp the message window width and ClsMes size. The twenty PAL-only
functions stand as stubs at their retail positions, dataread's four
tree-building functions are NTSC-only, and PAL compiler-state pragma
blocks sit beside NTSC's. config/pal/splice.txt lists the 213 functions
whose PAL bytes are not yet expressed in C. Every unit compiles with
-DPAL; every NTSC object is byte-identical.

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>

* Generate the PAL split configuration from NTSC's

scripts/build/splat_config.py writes a release's symbol lists from its own
executable and carries NTSC's yaml, linker script and object fixups over to
another release by symbol and relocation pairing: the overlay entry points by
the retail name they note, _gp from .reginfo, @ORDER comments and the dun text
start, and function-local statics through the function that owns them. The
carried files are written once and maintained by hand; --check compares the
symbol lists and, on NTSC, proves the carry is the identity. region.paths()
names any release's files.

config/pal holds the PAL prototype's yaml, symbol lists, linker script, fixups
and expression-node overrides. NTSC unchanged.

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>

* Supply the PAL prototype's undecompiled functions from INCLUDE_ASM markers

Each PAL function not yet in C is `#ifdef PAL INCLUDE_ASM("asm/pal/nonmatchings/
<unit>", <symbol>)` beside its NTSC body (207 markers); include_asm.h and the
mwccgap marker path are back for PAL, with the name counter pinned where a
marker stands so later compiler constants keep their names. PAL-differing data
is C under #ifdef PAL in the owning units. datacheck.py reads a relocation's
target section from the REL entry's info field. docs/PAL.md describes the PAL
build. NTSC is byte-identical; PAL links with 120 functions and 2 data bytes
still to reconcile.

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>

* Carry PR 40's PAL data and layout; every PAL image verifies

The PAL linker script places EditGaijiTbl at 0x0025f02c, aligns _dun_end to
128, drops dngmessageman's small-data line (PAL's LanguageStr is in
hitvalue.cpp) and aligns editmapscript's .sbss to 16 over retail's unnamed
pad at 0x002a9f3c. title/op_c loads the Chara__3 entries NTSC does in
OpC_InitProcess2/5; battlemenu loads wepstatus and reloads DrawAallWeapon's
textures from BtlMenuExtBlock under PAL.

datacheck.py resolves retail's name__N spellings (Names), scores each voted
anchor on its own layout and resolves a pointer word by its exact name.

PAL: SCUS_971.11 3048 perfect / 154 asm, TITLE.BIN 126 / 44, DUN.BIN 52 / 9,
data perfect. NTSC unchanged.

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>

* PAL: 29 main-executable functions in C from PR 40; NTSC unchanged

battle_globals, battlemenu (9), memcard (2), menu_dungeon, shot_freefuncs,
clsmes, dataread (2), edit, editloop, fireomni, frame, mglib, texture (3),
stockitem, chararead, dungeonmap and spritetable express their PAL
differences under #ifdef PAL instead of INCLUDE_ASM markers; editloop's PAL
argument-flag indices follow. mglib.hpp declares PAL's mgTopVRAM and
mgZBufferAdr.

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>

* PAL: 10 title-overlay functions in C from PR 40; NTSC unchanged

op_a (2), op_b, op_c (2), op_d, opening, rushmovi (2) and title express their
PAL differences under #ifdef PAL. A string retail shares with a function
still supplied by a marker is reached through PAL_POOLED, so the compiled
function does not emit a second copy.

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>

* PAL: dun, gamepad, eastking, menu_draw and collisiondata functions in C from PR 40

dun/gameloop (2), gamepad (3), eastking (2), menu_draw (2) and collisiondata
express their PAL differences under #ifdef PAL; dun/gameloop takes PR 40's
PAL argument-flag block, InitEastKingEvent returns int under PAL, and the
PAL expression-node overrides carry PR 40's five entries. The PAL fixups
follow every unit's renumbered statics. All three PAL images verify with
158 functions still supplied by markers; NTSC unchanged.

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>

* Make decompile.sh's m2c context parse again

m2ctx.py spells a C++ template-id as a plain identifier, and now declares each
one it spells as an opaque struct, so CDataAlloc<1, 1690000> no longer breaks
m2c's C parser. decompile.sh generates a missing context with the container's
venv Python, which has libclang's bindings.


* Work on the PAL version

---------

Co-authored-by: Alessandro Suha <plarpoon@zandark.eu>
2026-09-30 21:49:16 +10:00

188 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Master a fresh ISO from the extracted disc, with the built files swapped in.
Every file comes from the extracted tree (rom/ntsc/extracted), except the
ones named on the command line, which are taken from the build instead. Because the image is mastered
rather than patched, a built file is under no obligation to be the same size
as the one it replaces -- it is simply laid down and the directory records
describe wherever it lands.
The layout is rebuilt from scratch, so files do not keep the sectors they
shipped on. The game opens its files by name through the CD filesystem, and
the offsets inside DATA.HED are relative to DATA.DAT rather than to the disc,
so nothing depends on the old sector numbers.
The naming and the volume identifiers are taken from the retail ISO rather
than guessed, so the new image describes itself the same way: ISO9660 names
carry their `;1` version.
## The path table has to be at sector 257
The console does not read the ISO9660 path table from where the volume
descriptor says it is. On DVD media it reads a fixed LBA 257, which is where
Sony's mastering tool always put it, and follows that table to the root
directory. Every general-purpose mastering tool instead packs the path table
in right after the descriptors, so the console reads sector 257, finds
nothing, and cannot open a single file -- not the executable, not even
SYSTEM.CNF. That is a silent failure: the disc mounts perfectly on a PC and in
PCSX2's own ISO reader, and the game simply dies moments after boot.
This was traced with a devel build of PCSX2 (IOP.cdvd tracing), which shows
the console reading sector 16 and then sector 257 and giving up:
CDRead: Reading Sector 0000016 <- the volume descriptor
CDRead: Reading Sector 0000257 <- where it expects the path table
open fail name SCUS_971.11;1
so after mastering, the path tables are moved to 257/259 and the descriptor is
repointed at them. Sectors 257-260 fall inside the area UDF reserves, which is
why the image is mastered with UDF and then has its UDF anchors cleared: the
console reads ISO9660 and ignores UDF entirely (verified by booting the retail
disc with its anchors zeroed), so the reservation is all UDF is used for here.
Usage:
make_iso.py --source <retail.iso> --tree <extracted dir> --out <built.iso>
[<replacement file> ...]
"""
import argparse
import os
import struct
import sys
import pycdlib
# pycdlib writes this revision; the retail disc predates it, but the PS2 reads
# the ISO9660 tree and both are present either way.
UDF_VERSION = '2.60'
def ident(pvd, name):
"""Read one of the PVD's identifier fields as text."""
value = getattr(pvd, name)
if hasattr(value, 'record'):
value = value.record()
if isinstance(value, (bytes, bytearray)):
value = value.decode('latin-1')
return value.rstrip(' \x00')
def strip_version(iso_path):
"""`/CONF/NET/NET.DB;1` -> `/CONF/NET/NET.DB`, the name as extracted."""
return iso_path.split(';')[0].rstrip('.')
def read_layout(source):
"""Return the volume identifiers and the directory/file lists of a disc."""
iso = pycdlib.PyCdlib()
iso.open(source)
try:
volume = {
'sys_ident': ident(iso.pvd, 'system_identifier'),
'vol_ident': ident(iso.pvd, 'volume_identifier'),
'app_ident_str': ident(iso.pvd, 'application_identifier'),
'pub_ident_str': ident(iso.pvd, 'publisher_identifier'),
'preparer_ident_str': ident(iso.pvd, 'preparer_identifier'),
}
# The UDF tree names the same objects in their original case, so index
# it by upper-cased path to pair each one with its ISO9660 name.
udf = {}
if iso.has_udf():
for dirname, dirs, files in iso.walk(udf_path='/'):
base = dirname.rstrip('/')
for entry in dirs + files:
path = f'{base}/{entry}'
udf[path.upper()] = path
directories, files = [], []
for dirname, subdirs, entries in iso.walk(iso_path='/'):
base = dirname.rstrip('/')
for entry in subdirs:
directories.append(f'{base}/{entry}')
for entry in entries:
files.append(f'{base}/{entry}')
return volume, directories, files, udf
finally:
iso.close()
# Where the console looks for the ISO9660 path tables, and where Sony's
# mastering tool put them.
PATH_TABLE_LBA = 257
PATH_TABLE_M_LBA = 259
SECTOR = 2048
def relocate_path_tables(path):
"""Move the path tables to the sectors the console reads them from."""
with open(path, 'r+b') as image:
image.seek(16 * SECTOR)
pvd = bytearray(image.read(SECTOR))
l_lba = struct.unpack_from('<I', pvd, 140)[0]
m_lba = struct.unpack_from('>I', pvd, 148)[0]
image.seek(l_lba * SECTOR)
l_table = image.read(SECTOR)
image.seek(m_lba * SECTOR)
m_table = image.read(SECTOR)
image.seek(PATH_TABLE_LBA * SECTOR)
image.write(l_table)
image.seek(PATH_TABLE_M_LBA * SECTOR)
image.write(m_table)
struct.pack_into('<I', pvd, 140, PATH_TABLE_LBA)
struct.pack_into('>I', pvd, 148, PATH_TABLE_M_LBA)
image.seek(16 * SECTOR)
image.write(bytes(pvd))
print(f' path tables moved to {PATH_TABLE_LBA}/{PATH_TABLE_M_LBA}')
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--source', required=True, help='the retail ISO, read for its layout')
ap.add_argument('--tree', required=True, help='directory the disc was extracted to')
ap.add_argument('--out', required=True)
ap.add_argument('files', nargs='*', help='built files to use instead of the extracted ones')
args = ap.parse_args()
replacements = {os.path.basename(f): f for f in args.files}
volume, directories, files, udf = read_layout(args.source)
on_disc = {os.path.basename(strip_version(p)) for p in files}
unknown = set(replacements) - on_disc
if unknown:
sys.exit(f'not present on the disc: {", ".join(sorted(unknown))}')
iso = pycdlib.PyCdlib()
iso.new(interchange_level=1, udf=UDF_VERSION, **volume)
# Parents before children, so every directory exists before it is filled.
for path in sorted(directories, key=lambda p: p.count('/')):
iso.add_directory(iso_path=path, udf_path=udf.get(path.upper()))
for iso_path in files:
name = strip_version(iso_path)
local = replacements.get(os.path.basename(name))
if not local:
local = os.path.join(args.tree, name.lstrip('/'))
if not os.path.isfile(local):
sys.exit(f'{local} is missing; extract the disc first.')
iso.add_file(local, iso_path=iso_path, udf_path=udf.get(name.upper()))
for name in sorted(replacements):
print(f' {name}: from {replacements[name]}')
os.makedirs(os.path.dirname(args.out) or '.', exist_ok=True)
iso.write(args.out)
iso.close()
relocate_path_tables(args.out)
print(f'Wrote {args.out} ({os.path.getsize(args.out)} bytes)')
if __name__ == '__main__':
main()