mirror of
https://github.com/odiecay/chronicle-ps4.git
synced 2026-10-17 13:01:16 +02:00
* 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>
188 lines
7.1 KiB
Python
Executable File
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()
|