Files
nxnative-easy/docs/agents/01-extract.md
T
seedds 1fbbd1486a Bayonetta (010076F0049A2000, update 1.1 = v65536, PlatinumGames' engine in its Wii U form on SDK 3.6.0, as Bayonetta 2): game profile, no host change. Plays on the iPhone (IPA 8.8 GB, user). Same sdk and subsdk0 builds as Bayonetta 2, so its profile is Bayonetta 2's with its own build row and tables, and the CPK reader and the two NVN fixes made for Bayonetta 2 cover it. On the Mac from a fresh save: the boot, the opening movie (USM; Plus skips it), the title and its attract demo, a new game (PLAY, NEW GAME, the auto-save notice, Normal), the prologue on the falling clock tower, the graveyard cutscene (skipped), the touch-controls tips, the practice tutorial (punch, kick, combos, evade, Witch Time), the graveyard fight of Verse 1 with Witch Time, its autosave and the next loading screen's practice, with CRI ADX2 sound through one 6-channel AudioOut and no red line: 60 fps in play (frame time p50 16.65 ms, p95 17.1-17.5 ms; GPU 2-4 ms a frame on an M6), 30 in its cutscenes, where the game sets present interval 2; about 2.45 GB resident and 1.54 GB of Metal allocations, flat over 15 minutes. No virtual address memory (a 3.11 GB heap in the 4 GB layout, mem_used 102 MB). Files: nine CRI CPK archives (data00.cpk ... data03.cpk, data10.cpk ... data14.cpk, 4.25 GB) of PlatinumGames DAT archives, beside the USM movies and the ADX2 sound. Shaders: 458 BNSH files of one stage each (236 pixel, 222 vertex), the .vso / .pso members of shader.dat in data00.cpk, all built for the Mac and iOS. Profile: the 3D pad layout (the camera is on the right stick; the touch controls are handheld only), a prewarm list recorded on the Mac over the route above (513 lines). Checked: the NSP script packs it from the NSPs alone (Mac and iOS); the Python tool, gfx and iOS tests pass (test_tbdr_budget with DM_GAME set). README (games gallery, a Mac picture of the graveyard fight), AGENTS.md (59 -> 60), docs/development.md and docs/agents 01 / 03 / 04 say so
2026-10-25 11:03:12 +08:00

27 KiB
Raw Blame History

01 — Extracting an NSP

Keys

  • Needs the user's own prod.keys and the title key from the NSP's ticket. Keep both under the work folder, e.g. $W/build/keys/.switch/{prod,title}.keys, and run hactool with HOME pointed there so it never reads anything else:

    # $W/build/keys/ht — hactool with a private key home
    HOME="$(cd "$(dirname "$0")" && pwd)" exec "$(dirname "$0")/../../tools/hactool/hactool" --disablekeywarns "$@"
    
  • Dumped prod.keys files may contain malformed lines (seen: 33 *master_kek_source_* lines with the wrong length) that make hactool refuse the whole file. Drop lines whose value length is wrong for the key type; keep a backup of the original.

  • Title key: the NSP carries <rights id>.tik. For a common ticket the encrypted title key is the first 16 bytes of the title-key block at 0x180; append <rights id> = <key hex> to title.keys. Never print the value.

NSP → NCA → ExeFS / RomFS

ht -t pfs0 --outdir=$W/build/nsp game.nsp > $W/build/nsp.log         # NSP is a PFS0: NCAs + cnmt + tik/cert
ht -t nca $W/build/nsp/<id>.nca | head -40                             # identify the Program NCA (content type Program, has ExeFS + RomFS)
ht -t nca --listromfs $W/build/nsp/<program>.nca > romfs_list.txt     # quick look before extracting 3+ GB
ht -t nca --exefsdir=$W/data/v0/ExeFS --romfsdir=$W/data/v0/RomFS $W/build/nsp/<program>.nca
  • ExeFS holds main.npdm, rtld (usually), main, sdk and sometimes subsdk* modules (NSO). D2R and Mario both had only main + sdk worth loading.
  • Update NSPs: the update's Program NCA is a patch NCA; extract it with --basenca=<base program nca> so RomFS is the merged view. Keep base and update in separate directories (data/v0, data/v<ver>); the rewriter and the host take one ExeFS at a time.
  • Newer patch NCAs (seen: Mario 1.2.1, key generation 17) make hactool stop with Invalid BKTR layout!. dm/tools/nx_extract (C#, LibHac 0.19) handles them: nx_extract <prod.keys> <base.nsp> <update.nsp> <out dir> [<base RomFS dir>]. It reads the NCAs straight out of both NSPs (no 3+ GB intermediate NCA), imports the common tickets itself, prints the display version and module build IDs, and with a base RomFS dir writes identical files as APFS clones.
  • After an update, find every address the host or rewriter hard-codes for the old build (CUTS / OBS / FRAME_STEP in the profile's patches.py or dm/tools/sdk_patches.py, ->base + 0x… in the host) and carry it over with dm/tools/nso_fnmap.py <old.orig.bin> <new.orig.bin> 0x…: it matches functions with address bits masked and maps globals through their adrp references; check each result by disassembly (sdis.py). Also diff the imported SDK symbols: new imports that reach a system service need host implementations, or the real SDK aborts after connecting to sm:.
  • A cartridge dump (.xci) as the base game (Monster Hunter Rise): the card header's magic HEAD sits at 0x100, and its root HFS0 holds the update, logo, normal and secure partitions; the game's NCAs are in secure (here the 7 GB program NCA, control, legal information, HTML manual and the meta NCA). Cartridge NCAs carry their key in the key area, so there is no ticket. nx_extract opens either kind as the base game (OpenNsp), and the NSP script takes *.xci beside *.nsp; the update and DLC stay NSPs.
  • Decompressed NSO images for analysis: dm/tools/nso.py <nso> <out.bin> (lz4; use a venv that has lz4). dm/tools/nso2elf.py makes an ELF for Ghidra / REA / objdump.

A damaged NSP (an incomplete copy, a broken download) shows as a hash error while the RomFS is read. nx_extract lists every file whose data fails the NCA's hash check and exits with code 3; --allow-damaged (the NSP script's ALLOW_DAMAGED / --allow-damaged) writes them as read and only lists them. To check a whole NCA, its file name is the first 16 bytes of its SHA-256: Super Mario Galaxy's base program NCA hashed differently, with 12 MB runs of zeros near its end (147 MB of damaged RomFS: the two WebM prologue movies), while its update and the small NCAs matched their names.

Workspace layout used so far

The work folder $W sits in the game's NSP folder, beside the NSP script's <title>_build/, never in the home folder:

<NSP folder>/<title>-work/
  build/nsp/            NCAs + tik/cert + nsp.log
  data/v0/{ExeFS,RomFS} extracted base game (v<ver> for updates)
  probe/                decompressed images (main.orig.bin, sdk.orig.bin), small experiments
  elf/                  nso2elf output
  census/               survey tables (imports, svc, NVN slots)
  build/patched_v0/     rewriter output (what the host loads)
  shader/               shader package directory + map + facts produced by shader_split.py
  build/cache/metal_shader_cache/b-<chain version>/   translated metallibs
  build/gfx/dmsh_xlate/ the shader translator build
  ghidra/proj           persistent Ghidra project (analysis takes ~35 min for a 28 MB main)
  run/                  boot.sh, logs, saves, screenshots
  dbg/                  lldb scripts

Compressed RomFS content

  • Newer EPD titles store most files as *.zs (plain zstd, no dictionary in Mario; Zelda TotK needs ZsDic.pack dictionaries). Check for *dic* files before assuming plain zstd.
  • Older EPD titles (Super Mario 3D World + Bowser's Fury, a Wii U port) store almost everything as *.szs: Yaz0-compressed SARC. shader_split.py decompresses Yaz0 itself (yaz0, pure Python, about 20–45 MB/s per core; the whole 4.7 GB decompressed RomFS scans in about a minute). One of its SARCs (SystemData/LayoutShader.szs) is big-endian (BOM FE FF), which the SARC reader does not handle; it holds no shader container the game uses.
  • *.sarc / *.pack / *.blarc / *.bfarc are SARC archives; *.byml / *.bgyml are BYML (version 7 in Mario). A minimal reader for both is short enough to write inline when needed (see shader_split.py for SARC). Unity titles: dm/gfx/tools/unityfs.py reads UnityFS bundles, serialized files and objects by type tree (bundle_files, serialized_objects).
  • Capcom's RE Engine (Monster Hunter Rise) keeps everything in PAK archives: re_chunk_000.pak plus re_chunk_000.pak.patch_001.pak … patch_016 (19.6 GB in all). A PAK is KPKA, u8 version 4, u8 0, u16 features (0: a plain entry table), u32 count, u32 fingerprint, then one 0x30-byte entry per file: u32 murmur hashes of the lower- and upper-case UTF-16 path, u64 offset, u64 stored length, u64 length, u64 attributes (bits 0–3: 0 stored, 1 raw deflate, 2 zstd), u64 checksum. File names are only hashes. The game reads all of them as one view: a path in a later patch replaces the same hash in earlier PAKs (kpka_jobs in shader_split.py takes only the newest copy). The first bytes of each file name its format (MESH, TEX\0, SDF\0 shader definitions, BKHD / AKPK Wwise, USR\0, PFB\0, …).
  • Koei Tecmo's KTGL (Hyrule Warriors: Age of Calamity) keeps everything in RDB archives: asset/<name>.rdb is the index (_DRK0000 header, then one IDRK0000 record per file), and asset/<name>.rdb.bin* plus asset/data/0x<KTID>.file hold the data as a run of IDRK0000 entries: u64 entry length, u64 stored length, u64 length, u32 0, u32 file KTID, u32 type KTID, u32 flags, a few parameter words, then the stored data at the end of the entry. Entries follow each other directly, some after padding to 64 KB. Flag 0x100000 = zlib in chunks: u32 stored length + a zlib stream each, up to 16 KB inflated per chunk, a zero length ends. The first bytes of the inflated data name the format (GT1G textures, _M1G models, _S2G shaders, KTSR / TSRS / ASRS sound). The DLC folders add .rdb.bin3_0 files of the same layout.
  • A newer RDB (ROMANCE OF THE THREE KINGDOMS 8 REMAKE, KTGL on SDK 17.5.3): 0002/root.rdb and 0002/system.rdb are the indexes (_DRK0000, with a .rdx beside each), and the data is in 296 0002/0x<KTID>.fdata files (12.2 GB) plus 0002/data/0x<KTID>.file (and jaJP/, zhCN/ copies). An .fdata starts with PDRK0000 and u64 0x10, then the same IDRK0000 entries (the u32 after the lengths is 1 or 0, the padding 16 bytes). The compression is new: flag 0x400000 = chunks of u16 stored length, 8 bytes, then a zlib stream; with 0x100000 as well (flags 0x500000) the streams are LZ4 blocks. Each chunk inflates to at most 16 KB and a zero length ends. Of the 51,655 entries, 49,620 are LZ4, 2,026 zlib and 9 stored; all decode to the length in their header (shader_split.py idrk_data). The shaders are _S2G5000 / 6000 / 7000 (G2S) and _DOK0000 containers again: 59,214 GLSLC outputs beside 149 in main.
  • An update can carry more than one program (ROMANCE OF THE THREE KINGDOMS 8 REMAKE 1.1.3): a multi-program application has one Program and one Control NCA per program index, title IDs …0000, …0001, … Its update NSP holds the game's patch (…0000, 1.5 GB) and a whole second program (…0001, 34 MB, a full NCA, not a patch) with its own Control NCA; main starts it with nn::oe::ExecuteProgram(1, …). nx_extract used to take the first Program NCA it found, the second program, and failed with NCA section 1 does not exist layering it over the base game; it now takes the lowest title ID of the base NSP (program index 0) and the update's NCAs of that title ID. The host runs program 0 only (hle_oe.c logs the call).
  • Unreal Engine (Octopath Traveler 0, UE5: the paged global distance field shaders and the LowLevelTasks task system) keeps everything in pak files: Octopath_Traveler0/Content/Paks/pakchunk0-Switch.pak (1.5 GB, 159,757 files) next to loose CRIWARE .awb sound banks and one .usm movie. A version 11 pak has its footer in the last 0xdd bytes (encryption key GUID, encrypted-index flag, magic 0x5A6F12E1, version, index offset and size, SHA-1, five compression method names) and an AES-256 (ECB) encrypted index: mount point, entry count, path hash seed, path hash index, full directory index (a second encrypted block: directories, then file names with offsets into the encoded entries) and the encoded entries (one bit-packed u32 per file: block size, block count, encrypted, compression method, then offset and sizes). The key is in main: the one 32-byte window of its read-only or data segment that decrypts the index start to the mount point ../../../ (Octopath Traveler 0: main+0x57c7f70, found among 116,287 candidates in under a second). Never print it. Most files are Oodle-compressed in 64 KB blocks; the two shader code archives are stored. dm/gfx/tools/uepak.py reads all of this (AES through macOS CommonCrypto, no Python package) and lists a pak (uepak.py list <pak> <main NSO> <out tsv>).
  • Unreal Engine 4.27 with IoStore (DRAGON QUEST VII Reimagined: PhysX 3.4, so UE4; the source paths say UEngine/4.27-source) keeps its cooked packages in IoStore containers (DOLL/Content/Paks/pakchunk0-Switch.utoc / .ucas, 2.8 GB, and smaller chunks beside it) and everything else in pakchunk0-Switch.pak (1.5 GB, 3,972 files: configs, the asset registry, fonts, CRIWARE .awb banks, the two shader code archives); the other pakchunk*-Switch.pak files are 339-byte empty paks. Its version 11 footer is the same 0xdd bytes, but the magic sits 0xcc from the end, not 0xc8 as in Octopath Traveler 0's (five whole 32-byte compression method names, Zlib and Oodle); uepak.py takes either. The index is not encrypted. The update (v131072) is a delta over the base RomFS, so the merged extraction holds the whole game.
  • Unreal Engine 4.26.2 with IoStore (Pikmin 4 1.1.1: SDK MW+EpicGames+UnrealEngine-4_26_2 in main) keeps its cooked packages in Carrot4/Content/Paks/Carrot4-Switch.utoc / .ucas (7.5 GB, TOC version 2) and everything else in Carrot4-Switch.pak (3.5 GB, version 11 with the magic 0xcc from the end as DRAGON QUEST VII Reimagined's, 10,046 files, Zlib, index not encrypted): the configs, 1,294 Wwise .wem files, the WebM movies (stored) and the two shader code archives (stored, every shader an LZ4 block: uepak.py lz4_shaders). The update (v262144, 0.9 GB) is a delta over the base RomFS.
  • Unreal Engine 5.6.1 (FINAL FANTASY RESONANCE, Square Enix: SDK MW+EpicGames+UnrealEngine-5_6_1 in main) keeps everything in one IoStore container, FFRS/Content/Paks/FFRS-Switch.utoc / .ucas (6 GB, 63,910 chunks, Oodle in 64 KB blocks, not encrypted), beside FFRS-Switch.pak (2.9 GB, 2,363 files: configs, fonts, sound banks; version 11, index not encrypted, Oodle), a global.utoc holding only the script objects, and the RomFS's Manifest_UFSFiles_Switch.txt, which lists every file the engine sees, the shader archives among them. The pak has no shader code archive: the cooker converted both libraries into the container (EIoChunkType 8 ShaderCodeLibrary, one chunk each, the tables; type 9 ShaderCode, one chunk per shader group). A TOC (version 8) is a 144-byte header, the chunk IDs (12 bytes: hash, index, type), offsets and lengths (5 + 5 bytes big-endian), perfect hash seeds, the compression blocks (5-byte offset, 3-byte stored and raw sizes, method) and the method names; a library chunk's ID is CityHash64 of <library>-<format> lowercased in UTF-16 (global-sf_glsl_switch-sp_glsl_switch). Its tables and each group are Oodle-compressed, two layers, so uepak.py (Toc, io_archive) reads them through the game's own decoder in the Mac host's tool mode (OodleHost, DM_OODLE_SERVE; served before the SDK starts, no boot) and writes each library as a version 2 archive with every shader decoded: 3,542 + 70,702 shaders, 707 MB, in about 40 s. The pak stores block size 0 for a file of one block (Pak.entry).
  • Unreal Engine 4.18 (DRAGON QUEST XI S, Square Enix) writes legacy paks, version 4: JackGame/Content/Paks/pakchunk0-Switch.pak (6.6 GB, 264,844 files) and pakchunk1-Switch.pak (7.2 GB, 147,824 files), and the update adds *_0_P.pak patch paks, which the engine mounts over the others (a path in a _P pak wins). The footer is the last 0x2d bytes (encrypted index flag, magic, version, index offset and size, SHA-1) and the index is not encrypted: the mount point, the file count, then each file's name and its FPakEntry (offset, stored and raw size, compression flags, SHA-1, the blocks' start and end, which are file offsets before version 5, the encrypted flag and the block size, 64 KB). The compression flags are no standard value: 3, which is LZ5 2.0 (LZ5 v2, later renamed Lizard; main exports LZ5_*, LZ5_versionNumber = 20001, and FCompression::UncompressMemory calls LZ5_decompress_safe for flags 3, main+0x16f5c70). Every block starts with its level, 0x2d = 45 (LIZv1 with Huffman). The repository has no LZ5 decoder: the game's own runs in the Mac host's tool mode (hle_oodle.c), which uepak.py (Pak.read with a decode function) and the shader split use. It takes 512 KB of work memory from the game's malloc on every call, so the host serves it only after nninitStartup. Scanning both paks (13.8 GB) with 8 workers takes 28 s.
  • Capcom's MT Framework (The Great Ace Attorney Chronicles) keeps everything under nativeNXx64/: 447 ARC archives (*.arc, 17,910 files) beside loose .tex textures, Opus / ADPCM sound and H.264 .mp4 movies. An ARC is ARC\0, u16 version 7, u16 file count, then one 0x90-byte entry per file: the path (128 bytes, \-separated, no extension), u32 type hash, u32 stored length, u32 length in the low 29 bits (flags above), u32 offset. Each file is one zlib stream (stored when the two lengths are equal), and its first bytes name the format (TEX\0, MOD\0, MRL\0, GMD\0, SDL\0, SPK\0 the shader package, …). shader_split.py reads them (mtarc_members).
  • NintendoWare Bezel Engine (Big Brain Academy: Brain vs. Brain) keeps nearly everything in Bezel Engine archives: 603 .bea files (Archive/<Permanent|Temporary>~<name>.nx[.<language>].bea, plus the engine's own System.nx.bea at the RomFS root), 7,665 files in all, 2.6 GB unpacked from 0.38 GB. A .bea is a NintendoWare binary file with magic SCNE (version 0x10100, BOM FF FE): +0x20 u64 entry count, +0x28 u64 offset of an array of u64 entry offsets, +0x30 the name dictionary (_DIC), +0x40 the archive name. Each entry is a 0x30-byte ASST record: +0x10 u16 kind (2; 258 for the one sound project), u16 alignment shift, +0x14 u32 stored length, +0x18 u32 length, +0x20 u64 data offset, +0x28 u64 name offset (u16 length, then the path). Every entry is one zstd frame, even the two whose stored length equals their length. Inside: layouts (.lyt, SARCs with their own __ArchiveShader.bnsh), BNTX textures, .bfsha shader archives, models (.fmdb), MSBT text and fonts; the sound is loose stream/*.bfstm and the movies loose movie/Movie/*.mp4 (H.264). shader_split.py reads the archives itself (bea_members).
  • Game Freak's engine before Trinity (Pokémon Sword 1.3.2, SDK 7.3.2) keeps its models, textures, effects and maps in 7,699 GFPAK archives under bin/archive (7.9 GB of the 12.1 GB RomFS), beside loose message tables, battle data, Wwise banks and BNSH files. A GFPAK is GFLXPACK, u32 version 0x1000, u32 0, u32 file count, u32 folder count, u64 offset of the file table, u64 offset of the file hashes (FNV-1a 64 of each path; no names are stored), u64 offset of the folder table pointers; the file table holds 0x18 bytes per file {u16 level, u16 compression, u32 length, u32 stored length, u32 0, u64 data offset}, compression 0 stored, 1 zlib, 2 LZ4 (one block), 3 Oodle Kraken (pkNX). All 201,635 entries of Sword's are LZ4, so the split needs no Oodle (shader_split.py gfpak_members). Inside: BNTX textures, BNSH shader archives, models (.gfbmdl) and animations.
  • Game Freak's Trinity engine (Pokémon Legends: Z-A) keeps almost everything in one archive, arc/data.trpfs (4.4 GB, 13,181 packs, 238,546 files, 17.7 GB unpacked), indexed by arc/data.trpfd; both are FlatBuffers. The descriptor's root table holds the file hashes (u64, sorted), the pack names (arc/<the first file's path without its slashes>.trpak), one info table per file (its pack index) and one per pack (size, file count). The file system starts with ONEPACK\0 and the u64 offset of its own root table: the pack name hashes and the pack offsets. The name hash is not a plain FNV-1a 64 of the descriptor's name, so a pack is named through its first file's hash. A pack is a table of file hashes and file tables {1 compression i8, 2 level u8, 3 length u64, 4 data}; compression 3 is Oodle (every file but 15,528 small stored ones), and every Oodle stream is Mermaid (header 8C 0A, CC 0A for an uncompressed chunk). Oodle is linked into main and there is no Mermaid decoder with a licence fit for this repository, so the shader split runs the game's own OodleLZ_Decompress in the Mac host's tool mode (dm/host/hle_oodle.c, DM_OODLE_SERVE=1: requests on stdin, answers on stdout). The host finds it in main by the one function that references its name string (as for Unreal's archives), and calls it as the game does (main+0x1a9b7bc on 2.0.2: fuzz safe, no CRC check, thread phase 3). The shader split's full scan (every file decoded, 17.7 GB) takes 5 minutes with 12 workers on a 12-core Apple M6.
  • The older KTGL of Fire Emblem: Three Houses uses a LINKDATA archive instead: DATA0.bin is an index of 32-byte entries {u64 offset, u64 length, u64 stored length, u64 compressed} into DATA1.bin (6.9 GB), and patch1–patch4 hold loose overrides listed by patchN/INFO0.bin (0x120-byte entries ending in a rom:/patchN/… path). Its compressed entries and *.gz files are another chunked zlib: u32 chunk size (0x10000), u32 chunk count, u32 length, the stored length of each chunk, then the chunks at 0x80 alignment, each a u32 length + one zlib stream, or the plain bytes when the stored length equals the chunk's length (kt_gz in shader_split.py).
  • One archive of hashed names (Super Smash Bros. Ultimate 13.0.5): the RomFS is data.arc (17.6 GB) and 14 NRRs. Its header (magic 0xABCDEF9876543210) gives the offsets of the music, file and shared-file sections and of the file system table (one zstd frame of 50 MB); paths are Hash40s (CRC-32 of the path, its length in the top byte). A path leads to its current file through its info index, file info, info-to-data entry and file data, at file section + directory offset + 4 × its offset, zstd or stored (dm/gfx/tools/smash_arc.py documents the layout). The archive also keeps every earlier version of updated files, which no path leads to any more: 183,882 data blobs, 169,939 of them current (the 98 fighter NROs, 764 with their old versions, which the NRRs of older releases still list). A file's first bytes name its format: SSBH HBSS resources (RDHS shader databases, XFUN effect libraries, LDOM / HSEM / LTAM / MINA / LEKS models, meshes, materials, animations, skeletons), EFFN effects, SARC UI layouts, BNTX, NUS3 sound and the NROs (NRO0 at +0x10).
  • Next Level Games' engine (Luigi's Mansion 3) keeps everything but its Wwise banks in archives of its own: <name>.dict indexes <name>.data (init, global, Hotel/<floor>, … 98 of them). The .dict is 3,203 bytes in every archive: u32 magic 0xa9f32458, u8 4, u8 1, u8 compressed, u8 0, u32 the largest stored chunk, u8 chunk count (120), u8 record count (52), u8 name count (3), u8 0; then the records (0x18 bytes: u32 hash, u16 the top-level entry count of its table, u16, and one chunk index for each of 16 sections), the chunks (0x10 bytes: u32 offset, length, stored length (one zlib stream; 0 in an archive stored as is), u16 alignment as log2, u8 file, u8 GPU memory) and the file names .data, .debug, .nxpc. Only .data ships: the 13 even records are the game's variants (languages), the odd ones point into .nxpc. Record v's resource table is chunk v: 12-byte entries {u32 kind (bit 31: a list of w1 entries from entry w2; bits 27–30 the section; bits 22–26 the alignment as log2), u32 length, u32 offset in the section}. The game reads an archive in main+0x1ba00 (1.4.0).
  • An update to that engine (Luigi's Mansion 3 1.4.0) leaves .dict and .data as they were and adds <name>.patch: u64 a bitmask of the records it patches, u32 offset, stored and inflated length of its body (one zlib stream: 16 u32 section offsets, 16 u16 alignments, then each section's new data), then 0xd0 bytes per patched record {u32 offset, stored length and entry count of its new resource table (one zlib stream), u16 its top-level entry count, u16, u32 the new length of each of 16 sections, 16 u32 end and 16 u32 start of the old data that is overwritten before it is read}. After loading a section the game walks the new table and packs that section's entries from its start at their alignment, each from the old section (an offset below its length) or from the body (the rest, counted from the old length), and writes the new offset into the entry (main+0x1c8d0, main+0x1cbd0); it does this in place, keeping a copy of the old data in [start, end). shader_split.py nlg_patch_section does the same. The base game's init has 5,149 shaders, 1.4.0's 5,317; 46 of the 103 the title draws exist only in the patched sections.
  • PlatinumGames' engine (Bayonetta 3) keeps everything but its WebM movies in 72 PKZ archives in the RomFS root (14 GB; root_000.pkz, core_000.pkz, pl_000.pkz, st1_000.pkz, sound_000.pkz … sound_017.pkz, …). A PKZ is pkzl, u32 version 0x20000, u64 archive length, u32 file count, u32 entry table offset (0x20), u32 name table length, u32 alignment (0x1000), then one 0x20-byte entry per file: u32 name offset, u32 compression name offset (both into the name table that follows the entries), u64 length, u64 offset, u64 stored length. The compression is named per entry, OodleKraken or None here (11,760 and 5,159 entries); main knows ZStandard too (main+0xcbdf80 maps the three names to 0xc9, 0x65 and 0). Inside are DAT archives: DAT\0, u32 file count, then the offsets of the file offset table (u32 each), the extension table (4 bytes each), the name table (u32 name length, then one name of that length each), the file length table (u32 each) and a name hash map. Their members are the engine's own formats (.wmb models, .wtb / .wta textures, .bxm, .eff, …) and, in root_000.pkz's shader*.dat and autoshadereff.dat, its shader objects (04). There is no Kraken decoder fit for this repository, and this main reads its archives only through Oodle's streaming decoder (main+0xcbc698 creates one, main+0xcbc7f8 feeds it 256 KB at a time into a 10 MB window), so OodleLZ_Decompress was never linked in and its name string is not there: hle_oodle.c finds OodleLZDecoder_MemorySizeNeeded, Create, Destroy and DecodeSome instead (by the strings of Oodle's oodlelzcompressors.cpp and their order) and decodes each entry with them in DM_OODLE_SERVE. The shader split decodes every entry of every PKZ (8 workers, one Mac host each) in about 20 s; only root_000.pkz holds shaders.
  • The same engine in its Wii U form (Bayonetta 2, SDK 3.6.0) keeps everything but its USM movies and Wwise sound in six CRI CPK archives in the RomFS root (data000.cpk … data006.cpk, there is no data004.cpk; 9.5 GB, 6,091 files). A CPK is CPK , u32 0xff, u32 the length of the @UTF table at +0x10 that names TocOffset and ContentOffset; at TocOffset comes TOC , 12 bytes and another @UTF table with one row per file: DirName, FileName, FileSize (stored), ExtractSize, FileOffset (relative to the smaller of TocOffset and ContentOffset). An @UTF table is big-endian: @UTF, u32 length, then from +8 u16 version, u16 rows offset, u32 strings offset, u32 data offset, u32 table name, u16 column count, u16 row width, u32 row count, and one flag byte (storage 0x10 zero, 0x30 constant, 0x50 per row; type in the low nibble) and a name offset per column. Vanillaware's CPKs (Unicorn Overlord) store their @UTF tables masked: each byte XOR the low byte of a 16-bit value that starts at 0x655f and is multiplied by 0x4115 after every byte (cpk.py unmask); the split reads their 7,744 files, 6.5 GB unpacked, in 38 s and finds no shader there, so Unicorn Overlord's tables are unchanged. 5,693 of the files (6.9 GB stored, 12 GB unpacked) are CRILAYLA: CRILAYLA, u32 size, u32 compressed length, the compressed bits, then the output's first 0x100 bytes stored; the bits are read from the end backwards and the output written from its end backwards (dm/gfx/tools/cpk.py, about 29 MB/s in Python with the bits read from a string). Inside are PlatinumGames DAT archives, as in Bayonetta 3; the shaders are BNSH files (.vso / .pso members of shader.dat, shader2.dat and shadereff.dat in data000.cpk), which the split finds by decoding every entry of every CPK (shader_split.py cpk_jobs, about 2.5 minutes with 12 workers). Bayonetta (SDK 3.6.0) has the same layout in nine CPKs (data00.cpk … data03.cpk, data10.cpk … data14.cpk, 4.25 GB), its shaders in shader.dat of data00.cpk; cpk.py reads them as they are.