End-to-end XPP/PSARC tools for packed inFAMOUS 1 PS3 profiles, with BCUS98119 as the primary established render/decomp authority and NPUA80480 as a separately validated deployment target:
- extract textures to PNG
- encode PNGs back into XPP (same format the game already reads)
- derive lower-memory 1x/2x XPPs losslessly from existing 2x/4x packs
- strictly validate every descriptor and compare candidate texture residency with retail
- rebuild install PSARCs with selected XPP replacements
- extract and rebuild a complete install1/install2 profile with a byte audit
- generate exact texture hash indexes for scene-coverage tracing
- export static mesh sections to GLB
- export one exact packed character record as a bounded diagnostic GLB
- correlate exact packed character streams with a topology-paired runtime draw
Python 3.10+, standard library only. This repo does not include game files.
The game keeps reading XPP. PNG is the edit format. This tool does not make the executable load PNG.
xpp-tool is the neutral command name. The original if1-tex entry point is
kept as a fully compatible alias, including all existing commands and options.
Durable diagnostic commands and supporting modules are registered in
TOOL-INVENTORY.md.
if1-texor if1-tex ui, or if1-tex-ui. Drop an .xpp on the window, or click the big box and pick one.
Three buttons:
- Get the pictures out (PNG)
- Make them HD and pack a new
.xppthe game can still read - Save the 3D model (GLB)
Same jobs as the commands below. The window is the easy front. The CLI is for scripts.
If the desktop toolkit is missing, if1-tex ui --web opens a local page instead.
xpp-tool ui opens the same window; both command aliases accept the complete
command set below. GTK is optional, not a Python package dependency. The desktop
launcher is provided in packaging/if1-tex.desktop.
HD packing increases texture memory requirements; it is not a gameplay-safety claim. Use the retail comparison and profile preflight below before deployment.
Textures use 0x70-byte descriptors (chunk 0x03100000) plus a texel heap
(chunk 0x0D800000). Descriptor +0x40 is a payload-relative mip-chain
address. Header +0x28 is the payload's file offset.
file_offset = header[+0x28] + desc[+0x40]
heap_offset = desc[+0x40] − texel_chunk.offset
next_addr − this_addr == align128(chain_bytes) × faces
The explicit mip count at descriptor +0x2c and the embedded mip byte at
+0x45 must agree. The writer keeps them synchronized, preserves retail
texture order and opaque heap gaps, updates the owning segment and chunk spans,
and moves the untouched final link segment without rewriting it.
Formats: DXT1 0x86, DXT3 0x87, DXT5 0x88, X8R8G8B8 0x85,
R5G6B5 0x84, R6G5B5 0x8F, HILO8 0x95.
Static meshes: rigid sections with positions/UVs. Joint-local pieces (helicopter rotors) are placed at rest pose. Skinned/character packages have no static sections and are refused.
git clone https://github.com/deucebucket/infamous-xpp-textures.git
cd infamous-xpp-textures
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
xpp-tool --help
if1-tex --help # compatibility alias
if1-tex ui # window (also: if1-tex-ui, or either CLI with no arguments)pip install -e ".[dev]"
python3 -m pytest -qif1-tex list --xpp /path/to/package.xpp
if1-tex extract --xpp /path/to/package.xpp --outdir ./out
if1-tex extract --psarc /path/to/USRDIR/data.psarc --entry /some.xpp --outdir ./out
if1-tex extract-all --xpp-dir /path/to/xpp-folder --outdir ./out
if1-tex verify --xpp /path/to/package.xppverify now fails on every malformed or unresolved descriptor, explicit versus
embedded mip-count disagreement, incomplete 128-byte heap layout, overlap, or
out-of-bounds texel chain. It never silently drops a bad descriptor from the
reported total.
--level, --index, --max, --outdir work on extract as before.
Replace one or more 2D textures, then rewrite the heap and descriptors:
if1-tex pack --xpp /path/to/package.xpp --out ./edited.xpp --replace 0=./out/package.0.mip0.pngAll PNGs produced by extract (STEM.N.mip0.png):
if1-tex pack --xpp /path/to/package.xpp --out ./edited.xpp --from-dir ./outNearest-neighbor upscale every 2D texture, rebuild mips, rewrite addresses:
if1-tex pack --xpp /path/to/package.xpp --out ./edited.xpp --scale 4--scale implies a size change. Cubemaps are left alone (the packer only
replaces 2D textures). Extra unused texel-heap chunks are preserved; if textures
actually occupy more than one heap, packing refuses instead of dropping texels.
A final chain may omit alignment padding, but never required texel bytes.
Payload-relative addresses, synchronized mip counts, retail texture order,
opaque heap gaps, and the untouched final link segment are preserved.
Explicit replacements are strict by default: invalid image sizes/data and cubemap
replacements are refused. For the original window's best-effort batch behavior,
use pack --fit-replacements: oversized power-of-two images are halved to fit
4096, and unfittable images or cubemap slots are left retail. The window uses
this opt-in mode. Scaling already skips cubemaps. Neither mode permits missing
texel bytes, invalid descriptors, or multi-heap texture allocations.
Round-trip check:
if1-tex extract --xpp ./edited.xpp --outdir ./check
if1-tex verify --xpp ./edited.xppIf a 4x XPP already contains complete mip chains, derive a true 2x package by dropping only its largest mip. This copies the remaining BCn bytes exactly and does not recompress them:
if1-tex derive \
--retail ./retail/male_base_Zeke.xpp \
--source ./generated-4x/male_base_Zeke.xpp \
--target-scale 2 \
--out ./mod-xpp/male_base_Zeke.xppUse --target-scale 1 for enhanced texels at retail dimensions. Mixed 2x/4x
sources are supported. --include-index, --exclude-index, and
--max-upscaled build selective profiles for the game's fixed PS3-era texture
budget.
A global true-2x corpus can be structurally correct and still exceed the retail game's resident-memory budget. Prefer a hybrid: enhanced 1x globally, then promote selected visible packages to true 2x after gameplay testing.
inFAMOUS does not override its install data with loose XPPs. Rebuild the owner's retail install PSARC with the selected packages:
if1-tex psarc-pack \
--psarc ./retail/infamous1.psarc_s \
--xpp-dir ./mod-xpp \
--out ./mod/infamous1.psarc_sThe archive writer retains entry order, manifest bytes, uppercase-path MD5s,
and every entry not selected for replacement. The same mod directory can be
used for both install archives; packages belonging to the other archive are
reported as ignored. An explicit --include list is strict and fails if an
included name is absent. Keep protected retail archives outside the game
directory, never swap a live archive while RPCS3 is running, and use separate
retail and HD launch profiles.
xpp-tool profile-oracle \
--left-install1 ./bcus/install1/infamous1.psarc_s \
--left-install2 ./bcus/install2/infamous2.psarc_s \
--right-install1 ./npua/install1/infamous1.psarc_s \
--right-install2 ./npua/install2/infamous2.psarc_s \
--left-label BCUS98119 \
--right-label NPUA80480 \
--json-out ./oracle.jsonThe oracle emits aggregate archive contracts, package counts, case-insensitive
full-name and basename overlap, duplicate-routing counts, and exact shared
package byte identity. It never emits input paths, package names, payloads, or
payload hashes. --catalog-only skips slow payload hashing and explicitly
withholds byte-identity claims.
RR — Really Readable rundown: matching names only prove that two filing cabinets have labels in common. Matching sizes still do not prove the files are the same. Even byte-identical retail packages do not automatically prove that a modified package is safe in both games. Build or validate replacements against the exact target pair, then prove the target scene in runtime. The oracle always keeps direct cross-build replacement transfer unauthorized.
Do not copy a modified package wholesale when the oracle says the source and target package bytes differ. Keep the target retail XPP as the base and transfer only the texture edits whose original retail identities match uniquely:
xpp-tool texture-rebase \
--source-retail ./source/retail.xpp \
--source-candidate ./source/hd.xpp \
--target-psarc ./target/install1/infamous1.psarc_s \
--target-entry /matching-package.xpp \
--out ./target/rebased-hd.xpp \
--jsonThe identity is the exact original retail texture chain plus its format, face
count, dimensions, and mip topology. Descriptor numbers are deliberately not
used as cross-build identities. The command detects both resized textures and
same-size pixel edits, refuses missing or ambiguous matches, rejects cubemap
edits, verifies every unselected target texture remained exact, validates the
rebuilt target, and publishes the output atomically. --include-index can limit
the transfer to explicitly changed source records. --allow-zero-change
produces an exact target-retail control only when no edit is selected.
An already extracted target may be supplied with --target-retail instead of
the --target-psarc/--target-entry pair.
Without --include-index, every changed chain transfers, including same-size
re-encodes; inspect source_changed_records before building the profile.
RR — Really Readable rundown: think of the source retail texture as a fingerprint card. The tool finds exactly one matching original fingerprint in the target build, then puts the edited picture into that target-owned slot. If there are zero or several matching slots, it stops. This proves a careful offline conversion, not that the target game has loaded or displayed it; profile validation and scene-specific runtime proof still follow.
xpp-tool profile-extract \
--install1 ./retail/install1/infamous1.psarc_s \
--install2 ./retail/install2/infamous2.psarc_s \
--outdir ./workspaceThis writes every XPP/XPPS under workspace/xpp/install1 or
workspace/xpp/install2, preserving its manifest path. workspace.json
records both source hashes plus the name, size, and hash of every PSARC entry.
The output directory must not already exist; an interrupted extraction never
publishes a partial workspace. The slot directories preserve packages that use
the same basename in both archives; neither copy overwrites the other.
Use the existing extract, pack, derive, and verify commands. Put only
the finished replacement .xpp/.xpps files in one replacement directory.
Nested folders are allowed. A flat basename keeps the legacy workflow when it
has exactly one owner across the retail pair. When a basename exists in both
archives, preserve the extracted install1/ or install2/ prefix:
xpp-tool extract --xpp ./workspace/xpp/install1/textures/A21.xpp --outdir ./png
xpp-tool pack \
--xpp ./workspace/xpp/install1/textures/A21.xpp \
--from-dir ./png \
--out ./replacements/A21.xpp
xpp-tool verify --xpp ./replacements/A21.xpp
mkdir -p ./replacements/install1/textures
cp ./workspace/xpp/install1/textures/A21.xpp \
./replacements/install1/textures/A21.xppAn exact manifest-relative path wins inside an explicit slot. A slot-local basename shortcut is accepted only when unique in that archive. The tool fails before staging if a flat name has several owners, a file names the wrong slot, two inputs resolve to one retail target, or an inexact name is still ambiguous.
Compare one candidate with the exact retail XPP it replaces:
xpp-tool validate \
--retail ./workspace/xpp/install1/textures/A21.xpp \
--candidate ./replacements/A21.xpp \
--known-startup-pass-extra 608288 \
--known-startup-fail-extra 2705440Or route and validate the whole replacement set without spending time building either PSARC:
xpp-tool profile-validate \
--install1 ./retail/install1/infamous1.psarc_s \
--install2 ./retail/install2/infamous2.psarc_s \
--xpp-dir ./replacements \
--jsonThe report includes every descriptor, promoted-record count, exact raw and
128-byte-padded texture-chain growth, package growth, and per-record changes.
Optional observed pass/fail bounds are explicitly labeled startup-path only.
A pack can pass the opening simply because its promoted textures were not used;
scene coverage is always required before calling a texture gameplay-safe.
--fail-on-budget exits 2 (or refuses profile-build) at or above the supplied
observed startup-fail bound.
Generate a full JSON identity map and a compact hash allowlist from any strictly valid XPP:
xpp-tool runtime-index \
--xpp ./replacements/A21.xpp \
--label A21-candidate \
--json-out ./trace/A21.json \
--allowlist-out ./trace/A21.sha256The index covers each complete descriptor upload, face chain, individual mip, and leading mip prefix. Prefixes matter because the RSX path can omit BCn tail mips smaller than 4x4 while retaining the encoded bytes that precede them. The allowlist is consumed by the opt-in texture observer in the private RPCS3 research fork, so unrelated render targets and animated buffers never fill the log.
RR — Really Readable rundown: the tool makes fingerprints of the encoded texture bytes. A matching runtime fingerprint proves those exact bytes reached the observed GPU-upload boundary. No match means either the scene never asked for them or the game rearranged/copied the bytes first; it does not prove which one by itself. Pair the hash result with upload dimensions and a scene where the texture is visibly present.
Build one or more explicit host-GPU replacement records without enlarging the XPP the PS3 game sees:
xpp-tool runtime-bundle \
--retail ./retail/male_base_Zeke.xpp \
--candidate ./true2x/male_base_Zeke.xpp \
--index 1 \
--outdir ./trace/zeke-one-2xruntime-bundle strictly compares the two XPPs, binds the retail upload hash
and exact source shape to the candidate hash and larger shape, writes only the
selected encoded mip prefix, and publishes the bundle atomically. The private
RPCS3 fork refuses a row if any hash, shape, format, byte count, or file is
wrong. RR: the game still budgets a retail texture; only RPCS3's host GPU image
becomes larger. This is experimental and must be A/B tested in a scene proven
to call the selected retail identity.
xpp-tool profile-build \
--install1 ./retail/install1/infamous1.psarc_s \
--install2 ./retail/install2/infamous2.psarc_s \
--xpp-dir ./replacements \
--outdir ./profileprofile-build resolves each replacement to one exact archive/manifest owner,
rejects unknown or ambiguous inputs, performs the same strict retail comparison
on every XPP, and only then builds both outputs in a hidden staging directory.
It then reopens both archives and checks:
- PSARC version, compression, block size, flags, entry count, name digests, and exact manifest bytes/order;
- every replacement payload against the supplied XPP bytes;
- every unchanged payload against the retail source bytes;
- source and output sizes and SHA-256 hashes.
Only a completely verified pair is renamed to profile/. profile.json
contains the audit counts and hashes. The resulting flat
infamous1.psarc_s/infamous2.psarc_s directory can be selected directly in
the universal inFAMOUS Mod Manager's matching packed-profile controls.
if1-tex mesh-list --xpp /path/to/package.xpp
if1-tex mesh-list --xpp /path/to/package.xpp --oids oid-names.csv --contact-out ./heli.contact.json
if1-tex mesh-export --xpp /path/to/package.xpp --output ./heli.glb --contact ./heli.contact.json --pbrEvery static section (rotors, chassis, …) in one GLB:
if1-tex mesh-export --xpp /path/to/package.xpp --output ./prop.glbOne piece at a time (for painting a single mesh):
if1-tex mesh-export --xpp /path/to/package.xpp --output ./pieces --eachAssemble like the helicopter (one intact object, joints placed, Blender-ready GLB):
if1-tex mesh-export --xpp ./wf_helicopter_transport.xpp \
--output ./heli.glb --assemble recipeunique-largest keeps the biggest piece per oid (usually the intact hull, not the wreck). recipe uses the measured intact transport heli. --record-offset is still the explicit list.
Open the GLB in Blender. That is the viewer. There is no separate XPP viewport.
Compile an edited GLB back into an XPP the game can read (same vertex/triangle counts):
if1-tex mesh-compile --xpp ./original.xpp --glb ./edited.glb --out ./compiled.xppThat also writes compiled.glb so you can check the compile in Blender.
Remaster look (derived PBR — not in the game file). Pass the 4× albedo folder if you have one:
if1-tex mesh-export --xpp /path/to/package.xpp --output ./prop.glb \
--pbr --hd-dir ./textures_4x --maps-dir ./pbrOnly some pieces:
if1-tex mesh-export --xpp /path/to/package.xpp --output ./heli.glb \
--record-offset 0x1450 --record-offset 0x14b0 --record-offset 0x1510--texture some.png embeds that PNG. If omitted, the tool decodes a 2D texture
from the same package.
Character packages print that there are no static sections and exit non-zero.
xpp-tool inspect --xpp /path/to/package.xppInspect package structure, names, textures, static sections and Edge metadata
without claiming that skinned geometry can be compiled. OID names can be supplied
to mesh-list using --oids; contact reports support static mesh selection.
Assembly recipes, derived PBR maps and GLB compilation belong to the static
mesh path above, not the evidence-gated character pipeline below. Legacy extract,
mesh and UI writers may overwrite their destinations; use separate output paths.
Compare two owned builds before trusting descriptor numbers or internal locations:
xpp-tool character-oracle \
--left-xpp ./build-a/male_base_Zeke.xpp \
--right-xpp ./build-b/male_base_Zeke.xpp \
--left-label build-a \
--right-label build-b \
--json-out ./zeke-cross-build-oracle.jsonVersion 2.9 matches texture descriptors by their complete encoded-content identity and structural shape, never by list position. It separately matches character geometry contracts after removing only location fields, then reports location-delta histograms instead of raw payload bytes. Missing, changed, or ambiguous members fail closed; an existing output is never overwritten. A positive report may prove that one exact observer allowlist applies to both builds, while still keeping cross-build repacking, model export, and injection unauthorized.
RR — Really Readable rundown: two editions can contain the same Zeke textures but shuffle every numbered slot. The old slot number is therefore a shelf address, not the texture's identity. This oracle fingerprints the picture and its complete mip layout, finds its one matching shelf in the other edition, and does the same for the packed geometry contracts. A complete match gives us a universal lookup map. It does not yet tell Blender which packed numbers are positions, UVs, bones, or weights; that still needs one complete decoded retail vertex-stream proof.
character-component-ledger now has an explicit v2 mode for the case where the
same exact retail source record appears in more than one runtime page or pose:
timeout 60s xpp-tool character-component-ledger \
--title-id infamous-1 \
--build-id bcus98119-v0100 \
--candidate-id zeke \
--group-cross-page-source-records \
--material-report ./page2-head.json PAGE2_HEAD_SHA256 \
--material-report ./page1-five-page-head-union.json HEAD_UNION_SHA256 \
--visual-receipts ./zeke-visual-receipts.json \
--visual-receipts-sha256 VISUAL_RECEIPTS_SHA256 \
--output ./zeke-source-component-ledger.jsonThe grouping key is not a name or a visual guess. XPP hash and size, record offset, vertex count, retail triangle/index identity, and exact UV identity must all match. Position payloads are allowed to differ because two runtime pages may contain different poses; every page, event, draw, and position hash remains in its own observation. A render attaches only if that exact page+record has admitted material evidence.
RR — Really Readable rundown: page one and page two showed the same retail Zeke head in different poses. The old checklist used the page number in the part ID, so keeping both would make one head look like two separate inventory items. Version 2.41 gives the retail record one source-component ID and hangs both page-specific observations beneath it. Nothing is thrown away, and a different record cannot sneak into the group merely because its texture is also named Zeke.
The first real v2 ledger contains five source components / six material
observations / eight published renders. Hair is 290/294. Head record 536280
retains both its page-two 174/404 pose and page-one 212/404 pose under one
component. Jacket, packs, and the already-complete 24/24 jacket-detail record
remain separate. Normal and reversed inputs are byte-identical at 38,655 bytes,
SHA-256 2f74d3e4db3d51256053c39626ceea1a1fb1c5f9375b7bbd63e6b17552cd1c40.
Operator card: the new stable inventory ID is
xpp-tool.character-component-ledger.v2. The existing command without
--group-cross-page-source-records keeps v1 output exactly; a real compatibility
run reproduces the prior 30,923-byte ledger byte-for-byte. Existing file/count,
hash-pin, payload-free, atomic-new-output, offline, and single-process bounds
remain. This inventories components and poses; it does not align parts, fill
unresolved faces, recover rigging, create 4x/PBR, repack RPCS3, or import into
the native decomp.
character-material-coverage-export now accepts the exact representation that
the coverage-union tool already validates for a compatible four-map anchor:
the component's texture_names remains the deliberately displayed C/N pair,
while compatible_full_range_texture_names preserves the complete sorted
A/C/N/S shader-bound set. The older exporter incorrectly compared all four
anchor maps with the display-only pair and rejected valid evidence.
The repair remains strict. The compatible list is required whenever the anchor
binds extra maps; it must contain two through eight unique, sorted names and
include every display and anchor identity. A missing, incomplete, duplicated,
unsorted, oversized, or changed list fails closed. All four images remain
embedded in the private GLB, but only C/N are display-assigned. A/S stay
explicitly unassigned because their authored PBR meaning has not been proved.
RR — Really Readable rundown: the game handed this head draw four texture maps, but our honest preview uses only the two maps whose display roles we have proved. The union receipt correctly said both things. The exporter used to mistake that deliberate distinction for corruption. Version 2.40 understands the distinction without loosening identity checks or inventing shine. Five real observations now produce an editable Zeke head/neck component with 212 / 404 triangle occurrences textured and 192 left orange. Orange is an audit warning for material assignment, not a claim that those triangles are missing from the mesh.
Two source-tree runs and a fresh isolated 2.40.0-wheel run produce the same
493,956-byte GLB at SHA-256
59d7e3b477bc5f2c28fc935874d52d39b33e2d182cd1af90544aaa73bb05131f
and the same 6,574-byte report at SHA-256
2bf265a8e9684c39a09afc035725029aa33cb50465ff62c92c76e6bf4f080978.
The immediate Blender 5.2.0 unlit -58-degree audit is 1,671,003 bytes at
SHA-256 66c2092628b912972adbfda2cae6271871a138e230988f920b0f9daf6028d68f.
It proves a three-dimensional head/neck component, not a complete character:
hair, eyes, teeth, glasses, body, rig/skin, 4x textures, authored PBR, RPCS3
repacking, and native-decomp import remain separate work.
Operator card: this extends stable ID
xpp-tool.character-material-coverage-export.v1; it is not a one-off command.
Existing bounded inputs, immutable evidence, deterministic output, atomic
publication, no-overwrite behavior, and offline/single-process limits remain.
character-material-coverage-union now admits a full-range draw that binds
more textures than the canonical display anchor when the display material is
still exactly the same. This is the safe bridge for a runtime pass that binds
Zeke_Hair_A/C/N/S while the final diagnostic GLB deliberately displays only
the proved C/N pair:
timeout 60s xpp-tool character-material-coverage-export \
--xpp /path/to/owned/male_base_Zeke.xpp \
--xpp-sha256 RETAIL_XPP_SHA256 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--record-offset 533752 \
--anchor-lineage /path/to/page2-anchor-lineage.json \
--anchor-lineage-sha256 ANCHOR_LINEAGE_SHA256 \
--observation /path/to/page2-material.json PAGE2_SHA /path/to/page2-v4 /path/to/page1-keys.tsv \
--observation /path/to/page5-four-map-material.json PAGE5_SHA /path/to/page5-v4 /path/to/page4-keys.tsv \
--partial-observation /path/to/page1-lineage.json PAGE1_LINEAGE_SHA /path/to/page1-v3 - /path/to/source-census.json SOURCE_CENSUS_SHA /path/to/character-census.json CHARACTER_CENSUS_SHA \
--output-glb /new/path/zeke-hair-290.glb \
--output-report /new/path/zeke-hair-290.jsonThe assignment contract is strict. Display-assigned and unassigned suffix lists must be present together, bounded, unique, disjoint, and cover every texture in the pass. The declared shader-bound texture count must equal the report. Every display texture must match the anchor by descriptor, name, suffix, dimensions, decoded pixels, and runtime prefix. Repeated extra textures must also keep one identity. Only the generated PNG container hash may differ because exporting the same decoded image beside two images versus four can change that container; the anchor owns the PNG embedded in the final GLB.
RR — Really Readable rundown: the game used the same brown hair color/normal
maps but had two extra maps connected during this draw. The older checker saw
“two maps here, four maps there” and stopped, even though the actual displayed
hair pixels matched. Version 2.39 checks the maps by their real decoded pixels
and runtime identity, keeps the known-good C/N display material, and records
A/S only as unassigned evidence. That draw safely fills two of the six
orange holes: Zeke hair is now 290 / 294, with four orange faces left. The
matte/even-brown appearance is locked; no shine, PBR role, or missing face is
guessed.
Two installed-wheel runs in the pinned release environment produce the same
179,204-byte GLB at SHA-256
a18546b7dcf6db48e54affa0acdb5c045f074fa369c1e3df8386a321b2e78745
and the same 5,430-byte report at SHA-256
3cbdff5d85ab857c881e3b9d2bf73fc6ba7fc0addf481ae219be3344c5cd731e.
The immediate Blender 5.2.0 unlit -58-degree audit is 1,753,289 bytes at
SHA-256 fd4dc943c38f1dd697eddf787c9f8756fe0af8dc48656adecb35cdc930f0f56e.
Operator card: this extends stable IDs
xpp-tool.character-material-coverage-union.v1 and
xpp-tool.character-material-coverage-export.v1; it does not create a new
one-off command. Existing input, bundle, process, report, GLB, atomic
publication, and no-overwrite bounds remain. This proves two additional
material assignments only when the runtime supplies them. It does not prove
the final four faces, original normals/tangents, a complete head/body, rigging,
4x textures, authored PBR, an RPCS3 repack round trip, or native-decomp import.
Locate unresolved faces inside one canonical strict material GLB without serializing the mesh or guessing a camera direction:
timeout 60s xpp-tool character-material-gap-locator \
--material-glb /path/to/strict-material.glb \
--material-glb-sha256 GLB_SHA256 \
--material-report /path/to/strict-material.json \
--material-report-sha256 REPORT_SHA256 \
--output /new/path/material-gap-location.jsonThe command revalidates the exact GLB and report identities, strict observed- union schema, one observed and one unresolved triangle primitive, shared position/UV accessors, and every vertex/index bound. It emits only aggregate counts and measurements: connected-component sizes, adjacency to proved faces, diagnostic position/UV bounds and normalized centroids, area fractions, and dominant diagnostic triangle orientations. Raw indices, vertices, UV rows, textures, paths, and other payload lists stay private.
RR — Really Readable rundown: orange audit faces can mean two very different things. They could be a missing detachable chunk, or they could be tiny holes inside a mesh we already have. The first real run proves Zeke's six unresolved hair faces are two patches of three triangles. Their 10 vertices are all already used by surrounding proved hair, every unresolved face shares an edge with proved hair, and the six faces total about 1.263% of the diagnostic surface area and 0.917% of the UV triangle area. This is not a missing hair clump or another dimension. It is a material/pass gap embedded in the existing complete topology. A different camera may help us inspect it, but the next capture must seek a different game material pass, animation state, or LOD; camera rotation alone does not make the game issue another index draw.
Operator card: stable ID xpp-tool.character-material-gap-locator.v1. The
command is offline and single-process. GLB input is capped at 64 MiB; material
report and output are capped at 256 KiB; vertices and triangles are each capped
at 1,048,576. Inputs must be regular non-symlink files with explicit SHA-256
pins. Output must be a new path and is atomically published without overwrite.
Diagnostic coordinates are not proved object/world space, dominant directions
are not camera directions, and UV location does not assign a material. The
command does not close the gap, recover retail normals/tangents or rigging,
make 4x/PBR maps, repack an RPCS3 mod, or import into the native decomp.
Map one already checked material-coverage union onto the matching character topology in another owned build:
timeout 60s xpp-tool character-material-gap-oracle \
--left-xpp /path/to/source-build/character.xpp \
--right-xpp /path/to/target-build/character.xpp \
--coverage-union /path/to/source-material-coverage-union.json \
--left-label source-build \
--right-label target-build \
--json-out /new/path/material-gap-oracle.jsonThe command validates the union schema, source XPP identity, selected geometry record, retail index hash, triangle counts, and coverage arithmetic. It then uses the existing location-independent character oracle to require one unique target contract and an identical retail index stream. Record offsets and texture descriptor numbers are never assumed portable.
RR — Really Readable rundown: imagine painting 288 of 294 exact roof tiles and leaving six marked tiles unpainted. A second edition of the building can move the paint cans to different shelf numbers while keeping every roof tile identical. This oracle proves whether those same six tile identities exist in the second edition. It does not pretend the second edition showed us which paint belongs on them. A positive result therefore means “same topology gap,” not “new material evidence” or “finished character.”
Operator card: stable ID xpp-tool.character-material-gap-oracle.v1. The
command is offline and single-process. Each XPP is capped at 64 MiB; the source
coverage receipt and output are each capped at 256 KiB. Inputs must be regular
non-symlink files. Output must be a new path and is atomically published without
overwrite. The report contains hashes, counts, labels, and safe component
metadata only—no mesh, texture, index, path, or other payload bytes. A target
runtime material observation, material binding, descriptor-index portability,
full character, rig, 4x texture set, authored PBR, RPCS3 round trip, and native
decomp import all remain separate gates.
xpp-tool character-report \
--xpp /path/to/male_base_Zeke.xpp \
--external /path/to/owned/Fallout76/character.nif \
--json-out ./character-compatibility.jsoncharacter-report is the first fail-closed stage of the rigged-model pipeline.
On an inFAMOUS character XPP it proves paired geometry-heap envelopes, exact
big-endian triangle lists, descriptor-local vertex counts, and the byte extents
of three MSB-first packed streams. On a Fallout 4/76 NIF it validates the
header, block and string tables, block extents, footer roots, skin instances,
bone-data counts, and every referenced NiNode before reporting hashes and
counts. Game data is read locally and is never added to the repository.
The intended full chain is:
- validate the owned source NIF;
- normalize it as skinned glTF 2.0;
- map it onto a preserved retail inFAMOUS character template;
- rebuild XPP streams, pointers, chunks, fixups, and material references;
- prove an export/import semantic round trip; and
- validate animation and visibility in a controlled game scene.
The current command intentionally reports injection_authorized: false.
Triangle topology is proved, but target positions, normals, UVs, weights,
mesh-local joint palettes, hierarchy bindings, inverse-bind direction,
materials, wrapper/LOD ownership, and runtime visibility are not all decoded.
It will not create a plausible-looking but structurally unsafe character pack.
RR — Really Readable rundown: the Fallout model and Zeke model both have a skeleton-shaped structure, but they speak different binary languages. The report is the translator's checklist. It proves which pages we can read, names the pages we still cannot read, and blocks conversion until every animation- critical field has an exact mapping. The next proof is one retail character vertex stream captured before and after the game's decoder; that tells us how to turn the packed XPP numbers back into real vertex values.
An RPCS3 RSX capture can be rejected or correlated before deeper analysis:
xpp-tool character-capture-report \
--xpp /path/to/male_base_Zeke.xpp \
--rrc /path/to/BCUS98119_capture.rrc.gz \
--json-out ./zeke-capture-match.jsonThis validates the capture container and serialized maps, hashes each captured guest-memory payload, and looks for exact copies of the proven XPP triangle-index streams. It emits only identities, sizes, addresses, and hashes. An exact hit binds one package topology record to live RSX memory; a miss only says that this frame did not provide an exact index-buffer match. Neither result assigns vertex semantics or proves model compatibility.
When an exact index block is referenced by a captured
NV4097_SET_BEGIN_END(0) command, the report also records that replay-command
index and every memory block attached to the same draw. Sibling blocks remain
explicitly unclassified-draw-sibling candidates: their guest address, size,
and SHA-256 narrow the decoded vertex search without labeling any block as a
position, normal, UV, joint, or weight stream prematurely. Captured bytes are
never written to the JSON report.
RR — Really Readable rundown: finding Zeke's exact triangle list in a capture is like finding his page number in a live book. Binding that page to a draw call tells us which small stack of other pages the GPU read at the same time. Those nearby pages are where the decoded vertices should be, but the tool does not guess which page means position or skin weight. The next proof compares each bounded sibling to the known 26-vertex record and accepts a semantic only when its stride, numeric range, and decode/encode round trip all agree.
For a complete indexed draw, version 2.0 also replays the relevant raw RSX
register writes. It reports the primitive and index ranges, index type and
location, and each enabled vertex attribute's number, component format,
stride, frequency, location, and address. A vertex attribute is structurally
bound only when one sibling memory block exactly matches the address and the
capture extent calculated from the live index range. Attribute numbers remain
semantic-free: even a float32 x3 attribute is not labeled POSITION until a
numeric comparison and round trip prove that meaning.
RR — Really Readable rundown: the first report put all pages used by Zeke's draw on the table. The RSX descriptor pass now reads the tabs on those pages: "three floats every 12 bytes," "four normalized bytes every 10 bytes," and so on. That reduces a pile of 13 mystery blocks to the exact five vertex arrays used by the 26-vertex piece. It still does not call a tab "position" or "weights" merely because the label looks familiar.
Version 2.1 takes the next bounded step for vertex arrays whose RSX numeric
format is already defined. It decodes big-endian float32, float16, and
unorm8 elements for the exact captured index span, re-encodes them into a
copy of the original strided payload, and requires byte-for-byte and SHA-256
identity. Reports contain only counts, formats, component minima/maxima, and
hashes. Captured bytes and per-vertex values remain local. cmp32 stays
explicitly unsupported because its component packing has not been proved.
RR — Really Readable rundown: we can now read three kinds of numbered vertex pages and put every byte back exactly where it came from. That proves the number reader is correct; it does not yet prove what the numbers mean. A three-float page may look like positions, but xpp-tool will not label it until those values are mathematically tied back to one packed XPP stream. Model export and injection therefore remain blocked.
Version 2.2 correlates those supported arrays back to XPP stream zero. It searches bound arrays that share a location and stride, requires their numeric component bytes to cover one complete record with no gaps or overlaps, rebuilds every record, and compares the result with the bounded stream-zero bytes in the matched XPP geometry heap. A match is accepted only when it is unique and the source contains multiple distinct records and byte values, avoiding an all-zero coincidence. Reports still contain only layout metadata and hashes.
RR — Really Readable rundown: two separate RSX tabs turned out to be one XPP page split at byte 4. For the visible 26-vertex Zeke piece, attribute 3 supplies four bytes and attribute 9 supplies six bytes; joining them makes each original 10-byte XPP record exactly. This proves how stream zero is carried to the GPU. It still does not prove whether either field means UVs, color, weights, joints, or something else. The remaining three compressed streams and model semantics must be solved before export or injection can be enabled.
Version 2.3 adds the remaining RSX numeric format observed in that draw:
cmp32. It follows RPCS3's maintained vertex-fetch contract: one big-endian
word stores signed 11-, 11-, and 10-bit components, the values are extended
through the RSX 16-bit scale and divided by 32767, and W is 1. Every decoded
word must rebuild exactly. The source contract is available in the
RPCS3 vertex fetch implementation.
RR — Really Readable rundown: the last two unread tabs each squeeze three signed numbers into one 32-bit word. xpp-tool now opens all five tabs in the captured Zeke draw and can close them again without changing a byte. That is complete numeric coverage, not complete model understanding: the tool still does not call the two packed tabs normals or tangents, and it still cannot skip the game's decompression, skinning, or output-compression stages. Export and injection remain blocked.
Version 2.7 can export one proven draw as a deliberately limited Blender GLB:
xpp-tool character-diagnostic-export \
--xpp /path/to/owned/male_base_Zeke.xpp \
--binding-report /path/to/owned/exact-draw-binding.json \
--attribute-payload /path/to/owned/attribute-0.bin \
--position-hypothesis-attribute 0 \
--output ./zeke-piece-diagnostic.glb \
--json-out ./zeke-piece-diagnostic.jsonThe command requires exactly one complete RSX draw binding, one exact XPP
topology match, and one explicitly selected float32x3 attribute. It verifies
the captured payload's exact size and SHA-256, bounds every indexed vertex,
rejects non-finite or fully degenerate geometry, recenters the result, and
writes the retail triangle list unchanged. Repeating the same export produces
the same GLB bytes.
This is an inspection aid, not a rigged-character converter. The GLB embeds a plain unlit diagnostic material and labels the selected attribute as an unproved position hypothesis. It contains no UVs, skin, skeleton, inverse bind matrices, retail material, or injection authorization. Captured payloads and game packages remain local and must not be committed.
RR — Really Readable rundown: we can now put one small, exact piece of the visible Zeke draw on Blender's workbench. The triangle connections are proved, and the candidate coordinates make a real, non-flat shape, but the game has not yet told us what body part it is or how it attaches to Zeke's bones. A Blender render proves the export is usable for inspection. It does not prove the full character, textures, animation, or a safe replacement pack. Those remain the next gates.
Version 2.8 adds a separate bridge for complete runtime topology bundles that have not yet been mapped back to one XPP record:
xpp-tool runtime-topology-diagnostic-export \
--bundle /path/to/owned/topology-census-output \
--event 1 \
--position-hypothesis-attribute 0 \
--output ./runtime-draw-candidate.glb \
--json-out ./runtime-draw-candidate.jsonThe command validates the complete bundle as one immutable set: the completion
totals, all event binding files, reconstructed descriptor hashes, exact index
and vertex payload hashes and sizes, and absence of missing, extra, or symlinked
files. The selected event must contain one bounded big-endian u16 triangle list
and one unambiguous zero-frequency float32x3 attribute. Every source index
must fall inside the captured vertex range. When that range begins after vertex
zero, the exporter subtracts the recorded first vertex to make GLB-local
indices while retaining the original range and index hashes as evidence.
Coordinates must be finite and produce at least one nondegenerate triangle.
Output is refused inside the input bundle.
RR — Really Readable rundown: this lets us quickly put a GPU draw on Blender's workbench before we know which XPP record produced it. That is useful for sorting nearby draws into likely hair, face, clothes, hands, or unrelated scene objects. The tool still labels every result “runtime-only and unowned.” Looking like Zeke is a clue, not identity proof; XPP correlation, UVs, textures, bones, and injection remain separate gates. Progress pieces can render quickly at normal resolution, while 4K is reserved for the assembled full character.
Version 2.10 accepts the stricter texture-bound capture format as a separate mode. The exact allowlist used by the private runtime capture is required again at validation time:
xpp-tool runtime-topology-diagnostic-export \
--bundle /path/to/owned/texture-bound-topology-output \
--texture-allowlist /path/to/exact-target-textures.sha256 \
--event 1 \
--position-hypothesis-attribute 0 \
--output ./texture-bound-draw-01.glb \
--json-out ./texture-bound-draw-01.jsonThe validator proves that the listed texture identity was associated with an
enabled fragment-texture address when RPCS3 captured the draw. It also proves
the exact local payload set, capture-key calculation, bounds, and triangle
topology. It deliberately records shader_reference_proven=0: an enabled slot
is not yet proof that the active shader sampled it, and a texture-bound draw is
not automatically a uniquely owned Zeke body part. Visual inspection and XPP
correlation remain required.
RR — Really Readable rundown: the old search recognized a piece only when its runtime index list exactly matched the retail XPP index list. The new route can also ask, “Which bounded draw had this exact known Zeke texture switched on?” That is a stronger clue for reordered or alternate-LOD geometry, but still a clue. Each partial can be exported and rendered immediately at quick normal resolution. The 3840×2160 treatment starts when head, hair, body/skin, clothes, and gear are assembled into the fully textured character; 4× source textures and a 4K output canvas remain separate concepts.
Version 2.11 raises only the bounded texture-identity input envelope from 64 hashes / 16 KiB to 512 hashes / 40 KiB. A real surface-only cross-build oracle produced 501 unique whole-chain, face, mip, and mip-prefix identities because large textures may reach RPCS3 as partial uploads rather than one whole XPP descriptor. Strict lowercase SHA-256, duplicate, symlink, byte-size, and count validation is unchanged.
RR — Really Readable rundown: the first live texture-bound draw matched a 1×1 texture because its entire file is one tiny upload. The 28 larger Zeke descriptor hashes produced zero runtime matches even while Zeke was visible. That means the next search needs the smaller pieces of those textures—their mips and prefixes—not a larger guess. The new limit fits that exact 501-hash evidence set with 11 spare rows; it is not an unbounded memory increase.
Version 2.12 also accepts if1-texture-bound-topology-v2. Each captured draw
must include exactly one 8,708-byte RSX vertex-program image (including its
entry point) and one 8,192-byte transform-constant bank. Their filenames,
sizes, SHA-256 values, event numbers, binding rows, completion totals, and the
entire directory file set must reconcile before an export is written. The
older census-v1 and texture-bound-v1 formats remain accepted unchanged.
RR — Really Readable rundown: we now have real Zeke-material-bound geometry, but the pieces are stored before the GPU places and bends them. The v2 bundle preserves the small GPU “recipe and ingredient table” used for that placement: the vertex program says what math to perform, and the constant bank supplies matrices and other per-draw values. Capturing them does not magically prove which values are bones, but it gives the next analyzer exact, hash-bound input instead of forcing it to guess. A completed full Zeke render remains the gate for the first 3840×2160 image; partial progress can still publish immediately at quick resolution.
Version 2.13 decodes that v2 transform input without running the game again:
xpp-tool runtime-vertex-transform-census \
--bundle /path/to/owned/texture-bound-topology-v2 \
--texture-allowlist /path/to/exact-targets.sha256 \
--json-out /path/outside-the-bundle/vertex-transform-census.jsonThe command revalidates the entire bundle, walks only reachable RSX vertex instructions from the captured entry point, reconstructs all three encoded sources, and reports referenced vertex inputs, fixed constants, indexed constants, opcodes, output registers, and stable-versus-varying constant identities across shared programs. It refuses incomplete bundles and existing outputs. Raw program bytes, constant values, game paths, and game assets are not copied into the report.
RR — Really Readable rundown: this answers whether a draw uses a fixed stack of GPU values or an address-indexed array that could be compatible with a bone palette. It still does not name any value a matrix or bone. The next gate must replay the actual shader arithmetic and prove which output places vertices on screen before those pieces can be assembled honestly.
Version 2.14 performs that next bounded arithmetic replay for an explicit event set:
xpp-tool runtime-position-replay-export \
--bundle /path/to/owned/texture-bound-topology-v2 \
--texture-allowlist /path/to/exact-targets.sha256 \
--events 1,2,3 \
--projection-event 4 \
--output /path/outside-the-bundle/cluster.glb \
--json-out /path/outside-the-bundle/cluster.jsonIt symbolically executes only the supported straight-line affine path from RSX attribute zero to output zero. The selected projection constants must invert cleanly and decompose at least two captured program outputs before any GLB is written. Relative positions are preserved in one pre-projection frame; the combined result is recentered only for inspection.
RR — Really Readable rundown: the tool is taking the exact GPU placement math and undoing the final camera projection so nearby pieces can finally be viewed together. A colorful multi-piece result is still a diagnostic: an enabled Zeke texture slot does not prove the shader sampled that texture or that every draw belongs to Zeke. The render is evidence for geometry placement, not yet a full textured character.
Version 2.15 adds the missing fragment-side filter. A complete
if1-texture-bound-topology-v3 bundle includes one bounded, hash-bound RSX
fragment program per draw. The validator independently walks its exact
instruction extent, decodes texture opcodes and sampler numbers, reconciles the
derived sampler mask with RPCS3's captured mask, and requires every target
texture slot to appear in that mask:
xpp-tool runtime-fragment-sampler-census \
--bundle /path/to/owned/texture-bound-topology-v3 \
--texture-allowlist /path/to/exact-targets.sha256 \
--json-out /path/outside-the-bundle/fragment-samplers.jsonThis proves a static reference from the captured fragment program to the exact sampler slot carrying the target texture. It does not prove that a branch ran, that the texture owns the draw, or that a UV/material semantic is understood. The v3 filter exists specifically to exclude enabled-but-unused texture slots before geometry is assembled. The existing vertex-transform census and position replay accept both v2 and v3 bundles.
Version 2.16 adds the bounded fallback needed when captured vertex shaders use one combined world/view/projection transform that cannot be split honestly into a shared camera matrix plus per-draw model matrices:
xpp-tool runtime-screen-position-replay-export \
--bundle /path/to/owned/texture-bound-topology-v3 \
--texture-allowlist /path/to/exact-targets.sha256 \
--events 1,2,3 \
--output /path/outside-the-bundle/screen-cluster.glb \
--json-out /path/outside-the-bundle/screen-cluster.jsonThe command replays the exact affine path from captured attribute zero to clip output zero, performs the checked homogeneous divide, and preserves each draw's normalized screen position and depth. It writes neutral per-event colors so a render can be compared directly with the exact gameplay screenshot.
RR — Really Readable rundown: the GPU did not hand us one file called “Zeke.” It handed us separate draw calls, including his glasses and the book he was holding. This export puts those pieces back where they appeared on the captured screen, making it practical to classify them one by one. It does not turn the pieces into a body, prove that a draw belongs to Zeke, recover world space, or add UVs, textures, bones, weights, inverse binds, or editable game materials. A genuinely moddable character still requires those later gates and a verified reverse import/repack path.
Version 2.17 pages past the 16-draw capture ceiling without changing what a capture key means. First, generate the exact exclusion manifest from the complete base v3 bundle:
xpp-tool runtime-capture-key-exclusion \
--bundle /path/to/owned/page-1-v3 \
--texture-allowlist /path/to/exact-targets.sha256 \
--output /path/outside-the-bundle/page-1-keys.tsv \
--json-out /path/outside-the-bundle/page-1-keys.jsonThe private observer consumes that TSV and writes a v4 bundle. Every v4-aware
validator command requires the same exact file through
--capture-key-exclusion; a different byte sequence, count, or captured-key
overlap fails closed. After page 2 validates, combine the two page selections:
xpp-tool runtime-screen-position-page-merge \
--page-bundle /path/to/owned/page-1-v3 \
--page-events 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 \
--page-capture-key-exclusion - \
--page-bundle /path/to/owned/page-2-v4 \
--page-events 1,2,3 \
--page-capture-key-exclusion /path/to/page-1-keys.tsv \
--texture-allowlist /path/to/exact-targets.sha256 \
--output /path/outside-the-bundles/pages-1-and-2.glb \
--json-out /path/outside-the-bundles/pages-1-and-2.jsonRR — Really Readable rundown: page 1 is a tray that filled up after 16 GPU draw calls. The TSV is the exact list of pieces already on that tray. Page 2 tells the observer to ignore only those exact pieces, then catches the next unique ones. The merge tool checks that every later tray excludes the complete earlier set and that no piece appears twice before placing all selected pieces back at their captured screen coordinates. A new page may reveal Zeke's missing shoes, but it is not called footwear until its rendered shape visibly agrees with the foreground screenshot.
Operator card: both commands are offline and single-process. Inputs must be complete caller-owned, non-symlink bundles and exact external manifests; bundles remain read-only. Outputs must be new paths outside every input bundle and are never overwritten. A page contains at most 16 draws, an exclusion contains at most 256 unique lowercase SHA-256 keys and 16,705 bytes, and a merge contains 2 through 17 pages, at most 1,048,576 selected vertices, and at most 3,145,728 selected indices. Repeated runs over identical bytes are deterministic. The output is diagnostic NDC geometry only: it does not prove character ownership, feet, UVs, textures, world transforms, bones, weights, materials, PBR, or a reverse-import path.
Version 2.18 classifies reuse across the complete page chain before another runtime capture is requested:
xpp-tool runtime-page-family-census \
--page-bundle /path/to/owned/page-1-v3 \
--page-capture-key-exclusion - \
--page-bundle /path/to/owned/page-2-v4 \
--page-capture-key-exclusion /path/to/page-1-keys.tsv \
--texture-allowlist /path/to/exact-targets.sha256 \
--json-out /path/outside-the-bundles/page-families.jsonThe census compares four deliberately separate levels. Exact geometry bytes mean the index and ordered vertex-stream bytes repeat. Exact vertex-stream bytes allow the index selection to change. A stable-layout partial-stream candidate requires the same ordered layout, target texture/vertex-program identity, and at least one exact stream while another stream changes. A weak target-texture/vertex-program match is counted but never promoted to a component family. Many-to-one and one-to-many strong groups remain explicitly ambiguous.
RR — Really Readable rundown: a new capture key is a new ticket at the GPU door, not automatically a new body part. Animation, a different triangle subset, or another render pass can issue a different ticket for geometry we already saw. This report sorts strong repeated-family evidence from weak lookalikes so the next capture can move to a view that actually shows missing geometry instead of filling another tray with the same seated pose.
Operator card: this command is offline, payload-free, deterministic, and single-process. It accepts 2 through 17 distinct bundles, at most 16 events per page and 34,816 cross-page comparisons. The JSON is bounded to 256 KiB, must be written to a new path outside every immutable bundle, and contains no raw payload bytes. The tiers are correlation evidence only: same source component, new geometry, Zeke ownership, completeness, UVs, materials, bones, weights, PBR, and reverse import all remain unproved.
Version 2.19 ties those runtime families back to the editable XPP source without guessing a body-part name:
xpp-tool runtime-xpp-source-census \
--xpp /path/to/owned/male_base_Zeke.xpp \
--page-bundle /path/to/owned/page-1-v3 \
--page-capture-key-exclusion - \
--page-bundle /path/to/owned/page-2-v4 \
--page-capture-key-exclusion /path/to/page-1-keys.tsv \
--texture-allowlist /path/to/exact-targets.sha256 \
--json-out /path/outside-the-inputs/xpp-source-census.jsonThe command first requires complete topology coverage for the supplied XPP. For each bounded runtime block, it applies that block's captured stride and range start/count to every bounded XPP character stream-zero record and accepts only one exact byte match. This preserves distinct layouts instead of forcing every component through one record width. A full index-list match is reported separately. Zero matches remain unbound; multiple matches remain ambiguous. Neither is promoted by size, vertex count, appearance, or texture identity.
RR — Really Readable rundown: the GPU gives us Zeke as batches of triangles, not as one Blender file. This check asks whether a runtime batch contains an exact strip of bytes from one specific editable XPP record. A unique answer is a real bridge back to the game package. It still does not tell us whether that record is a shoe, jacket, glasses, book, or hair, and it does not decode UVs or bones. Those names and meanings need separate proof.
Operator card: the command is offline and single-process. The XPP is capped at 64 MiB and an observed record stride is capped at 64 bytes; the page/event/comparison bounds are inherited from the strict page validator; the JSON is capped at 256 KiB. Inputs must be regular immutable files/directories, output must be a new path outside every bundle and different from the XPP, and no raw source or runtime payload bytes enter the report.
A safe partial-range source receipt can now join an existing strict material
union without pretending that the capture contains every source vertex. Build
the partial lineage with the existing character-uv-texture-binding command,
then pass it to either union or export alongside at least one regular full-range
material observation:
xpp-tool character-material-coverage-export \
--xpp ./retail/male_base_Zeke.xpp \
--xpp-sha256 XPP_SHA256 \
--texture-allowlist ./zeke-surface-identities.sha256 \
--record-offset 533752 \
--anchor-lineage ./page2-full-range-lineage.json \
--anchor-lineage-sha256 ANCHOR_LINEAGE_SHA256 \
--observation ./page2-material.json REPORT_SHA ./page2-bundle ./page1-keys.tsv \
--observation ./page3-material.json REPORT_SHA ./page3-bundle ./page1-plus-page2-keys.tsv \
--partial-observation \
./page1-partial-lineage.json PARTIAL_LINEAGE_SHA \
./page1-bundle - \
./three-page-source-census.json SOURCE_CENSUS_SHA \
./character-census.json CHARACTER_CENSUS_SHA \
--output-glb ./zeke-hair-288.glb \
--output-report ./zeke-hair-288.jsonRR — Really Readable rundown: the page-one tray has 183 of the retail hair
record's 184 vertex rows. That sounds incomplete, but its triangle tickets use
only rows 0 through 182; the absent last row is never called. Version 2.36 does
not take that on faith. It reopens the actual index and UV bytes, checks every
triangle against retail, checks every index against the exact captured range,
replays the vertex/fragment shader identity, and independently matches every
named Zeke_Hair_A/C/N/S descriptor and mip prefix against the pinned character
census. Only then may the draw contribute material-covered faces.
The first owned result combines page one (276 faces), page two (11 new),
and page three (1 new) into 288 / 294 strict retail-material triangles.
The deterministic GLB remains 179,204 bytes, SHA-256
f11dc2be73ccba0aaad2576b76ae8e904c7302e8e44f3a6aa18c1cbc81705e3b.
The six unproved triangles remain a separate orange audit primitive. The exact
four-map names are retained as compatible pass evidence; the full-range C/N
anchor still owns the exported display material, and A/S are not invented
as PBR roles.
Operator card: this extends stable IDs
xpp-tool.character-uv-texture-binding.v1,
xpp-tool.character-material-coverage-union.v1, and
xpp-tool.character-material-coverage-export.v1. Every partial observation is
the eight-value tuple shown above: pinned lineage, immutable bundle and optional
exclusion, pinned source census, and pinned character census. One through 16
total observations are accepted, but at least one regular full-range material
export is mandatory as the texture/UV/export anchor. The command is offline and
single-process; existing 64 MiB XPP/GLB, 1 MiB authority, inherited bundle, and
256 KiB report bounds remain. Inputs are immutable regular files/directories;
outputs are atomic, new-only, outside every bundle, and never overwritten.
This proves a strict material assignment for 288 hair triangles. It does not prove the remaining six, a complete hair/head/body assembly, original normals/tangents, bones/weights, authored PBR, 4× textures, RPCS3 round trip, or native-decomp import. The approved matte/even-brown render remains the visual baseline; this capability changes evidence coverage, not its lighting recipe.
runtime-xpp-source-census now answers one more question before a useful old
draw is discarded: does its exact runtime triangle multiset exist inside the
pinned retail record, and do all referenced vertex indices stay inside the
exact source byte slice that was captured?
The first three-page owned run validates all 45 uniquely source-bound events and rejects zero. Zeke hair record 533752 has six observations but only three distinct index payloads. Page one events 15/16 carry 276 retail triangle occurrences and reference vertices 0 through 182, entirely inside their exact 183-of-184 captured vertex slice. Page two adds 11 new occurrences; page three adds one more. Their retail-ordered topology union is therefore 288 / 294, leaving 6 topology occurrences unobserved.
RR — Really Readable rundown: the older draw was missing one unused seat in its vertex table, not necessarily a visible piece of hair. This validator checked every triangle ticket from that draw against the retail hair record and then checked that none of those tickets asks for the uncaptured last vertex. It is safe topology evidence and cuts the search space in half. It is not yet a strict material export: the canonical textured GLB remains 282 / 294 until the page-one UV/shader/texture lineage is admitted through the material union. Nothing about the approved matte hair appearance changes.
Operator card: stable ID xpp-tool.runtime-xpp-source-census.v1, extended in
2.35.0 and maintained in
src/infamous_xpp_textures/source_correlation.py. The existing offline,
single-process command accepts one regular non-symlink XPP capped at 64 MiB,
one exact allowlist, and the inherited chain of at most 17 immutable v3/v4
pages with their exact exclusions. For every unique source-bound event it
reopens the checksum-validated index payload, requires a triangle list,
requires every triangle occurrence to be a retail multiset member, and
requires every referenced vertex to remain inside the exact mapped range.
Rejected index evidence remains explicit and cannot enter a union. The
deterministic payload-free JSON is capped at 256 KiB, written only to a new path
outside all inputs, and includes per-event plus per-record covered/unobserved
counts and hashes. It does not prove UVs, named material roles, full character,
rigging, 4×, authored PBR, RPCS3 round trip, or native-decomp import.
Attach one or more completed cross-material pass censuses when rebuilding the canonical character ledger:
xpp-tool character-component-ledger \
--title-id infamous-1 \
--build-id bcus98119-v0100 \
--candidate-id zeke \
--material-report ./zeke-hair-union.json \
--material-report-sha256 UNION_REPORT_SHA256 \
--material-report ./zeke-jacket.json \
--material-report-sha256 JACKET_REPORT_SHA256 \
--visual-receipts ./zeke-render-receipts.json \
--visual-receipts-sha256 RENDER_RECEIPTS_SHA256 \
--material-pass-census ./zeke-hair-pass-census.json \
--material-pass-census-sha256 PASS_CENSUS_SHA256 \
--output ./zeke-component-ledger.jsonRepeat the material-report pair for every proved component and the pass-census pair for every exact cross-pass receipt. A census is admitted only when its XPP, source record, vertex/triangle topology, retail index, texture family, covered and unobserved counts, and both union hashes match material evidence already in the same ledger. Its internal observations, pass groups, pairwise relationships, canonical IDs, and nonclaims are independently validated as well.
RR — Really Readable rundown: the standalone census told us Zeke hair has three
real material/shader passes but only two geometry selections. Version 2.34 puts
that fact in Zeke's permanent record. The ledger still treats the page-two and
page-three animated position captures separately, then links the pass receipt
to the matching source component. That is like filing three paint recipes with
one body panel instead of manufacturing three copies of the panel. Nothing is
lost, and a future exporter can see the A/C/N/S possibility without mistaking
it for the twelve still-unobserved faces.
The first updated ledger is 44,794 bytes and byte-identical under reversed material-report and equivalent census inputs. It retains five components, five render receipts, and one accepted matte hair baseline while adding one census, three pass signatures, and the exact 282/12 hair split. It does not infer PBR roles or compositing order, merge poses, assign missing faces, finish Zeke, upscale textures, rig the character, repack RPCS3 content, or import into the native decomp. This gate creates no render, so no image is withheld.
One character piece can be drawn several times with different shaders and texture sets. Those passes may reveal new faces, or they may cover the exact same faces for a different material stage. Compare them before spending another foreground capture:
xpp-tool character-material-pass-census \
--xpp ./retail/male_base_Zeke.xpp \
--xpp-sha256 XPP_SHA256 \
--texture-allowlist ./surface-identities.sha256 \
--record-offset 533752 \
--observation ./page2-hair.json REPORT_SHA ./page2-bundle ./page1-keys.tsv \
--observation ./page3-hair-four-map.json REPORT_SHA ./page3-bundle ./page1-plus-page2-keys.tsv \
--observation ./page3-hair-two-map.json REPORT_SHA ./page3-bundle ./page1-plus-page2-keys.tsv \
--output ./zeke-hair-pass-census.jsonRepeat --observation REPORT REPORT_SHA256 BUNDLE EXCLUSION_OR_DASH two
through 32 times. - is accepted only when that bundle does not require a
capture-key exclusion. Every report must be the strict observed-only result
for one draw. The command reopens the exact runtime index payload and refuses
authority drift, duplicate evidence, out-of-retail triangles, oversized output,
symlink output, and overwrite.
The report gives every exact pass signature, every pairwise triangle-multiset relationship, and one retail-ordered any-pass union. It serializes hashes and counts, not vertex, index, shader, or texture payload bytes.
RR — Really Readable rundown: Zeke's hair is genuinely drawn in multiple ways.
The page-three four-map A/C/N/S pass and its C/N pass use different fragment
programs and texture bindings, but both touch the exact same 275 triangles. They
are two coats of paint over the same boards, not extra boards. Page two and page
three each contribute seven faces the other draw missed, so together we still
have 282 of 294 proved hair triangles and the same 12 unknown faces. We must
preserve all three material passes for eventual faithful reconstruction, but a
new capture is worth doing only when it exposes a genuinely different draw,
pose, or state. Repeating this four-map pass cannot close those twelve.
This command does not decide that A means ambient/alpha, S means specular,
or how the passes composite. It does not render, upscale, rig, assemble full
Zeke, repack an RPCS3 mod, or import into the native decomp. The approved matte,
even-brown hair render remains the locked appearance regression baseline.
character-material-coverage-export closes the gap between a payload-free
coverage receipt and a usable private Blender asset. It fully reruns the 2.31
union validation, retains the exact covered triangle multiset in memory, selects
one checksum-pinned lineage as the position/UV/texture anchor, and writes a
strict GLB plus a payload-free receipt. It does not recover triangle bytes from
the public union JSON and never paints unresolved faces by inference.
timeout 60s xpp-tool character-material-coverage-export \
--xpp /path/to/owned/male_base_Zeke.xpp \
--xpp-sha256 RETAIL_XPP_SHA256 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--record-offset 533752 \
--anchor-lineage /path/to/page3-hair-lineage.json \
--anchor-lineage-sha256 PAGE3_LINEAGE_SHA256 \
--observation /path/to/page2-hair.json PAGE2_REPORT_SHA256 /path/to/page2-v4 /path/to/page1-keys.tsv \
--observation /path/to/page3-hair.json PAGE3_REPORT_SHA256 /path/to/page3-v4 /path/to/page1-plus-page2-keys.tsv \
--output-glb /new/path/zeke-hair-union.glb \
--output-report /new/path/zeke-hair-union.jsonThe first owned run exports Zeke hair record 533752 as one deterministic
179,204-byte GLB, SHA-256
e4199e6e8e31635bbe7624164bed04b665224771e3a73851599d68b1fc534879.
Its two compatible observations prove the retail Zeke_Hair_C/N material on
282 / 294 triangle occurrences; the remaining 12 stay in a separate
orange diagnostic primitive. The 5,423-byte receipt repeats byte-identically at
SHA-256 45bcea0b007be9b164b03168c7582806310ccf4195b58d3bfba892ece3f4566e.
RR — Really Readable rundown: the old union tool could say, “these two game views cover 282 hair faces together,” but Blender still received only one view's 275-face material assignment. This bridge carries the exact combined face list into the editable GLB. It keeps the approved matte texture and makes only the twelve still-unproved faces orange. It is a better hair component, not a complete hairstyle, head, character, rig, 4× texture set, PBR material, or working game mod.
Operator card: stable ID
xpp-tool.character-material-coverage-export.v1. Inputs are one regular
non-symlink retail XPP capped at 64 MiB; its canonical SHA-256; one exact
allowlist; one record offset; one through 16 checksum-pinned strict material
reports with immutable bounded v3/v4 bundles and required exclusions; and one
checksum-pinned anchor lineage that must identify exactly one accepted
observation. The command is offline, single-process, deterministic, and
new-only. GLB output is capped at 64 MiB and the receipt at 256 KiB; destinations
must differ and remain outside every input/bundle. Duplicate, conflicting,
ambiguous-anchor, out-of-retail, hash-drift, dishonest-count, over-bound, symlink,
and overwrite cases fail closed. Public reports contain hashes/counts only; the
GLB and game-derived payloads remain private operator assets.
Two wheel builds with pinned SOURCE_DATE_EPOCH=1787484000 are byte-identical.
The exact wheel size and SHA-256 live in CHANGELOG.md and
TOOL-INVENTORY.md, outside the wheel's README metadata, so the artifact never
contains a self-referential hash claim.
character-material-coverage-union answers whether two or more runtime views
actually expose different faces of the same editable retail record. It pins each
strict observed-only material report, reopens its immutable runtime bundle,
rechecks the exact index payload and page exclusion, and unions triangle
occurrences as a multiset against the retail XPP topology.
timeout 60s xpp-tool character-material-coverage-union \
--xpp /path/to/owned/male_base_Zeke.xpp \
--xpp-sha256 RETAIL_XPP_SHA256 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--record-offset 534628 \
--observation /path/to/page1-material.json PAGE1_REPORT_SHA256 /path/to/page1-v3 - \
--observation /path/to/page2-material.json PAGE2_REPORT_SHA256 /path/to/page2-v4 /path/to/page1-keys.tsv \
--output /new/path/zeke-jacket-coverage-union.jsonThe first owned run gives a useful negative answer. Main jacket coverage moves from 492 to only 493 / 1,002 triangles; the head becomes 185 / 404; the packs remain 170 / 302; and the small jacket detail is the expected complete duplicate control at 24 / 24. Repeating the same ordinary pose is therefore not a path to the missing surfaces. The next capture must deliberately change animation, camera, clothing/state, or occlusion before it earns another material observation.
RR — Really Readable rundown: a screenshot can look perfect while still showing only the same front-facing triangles the last screenshot showed. This tool lays the two exact face lists on top of each other. It proved the second view added one jacket triangle, some head triangles, and no new pack triangles. That is why we are keeping the accepted render as the visual baseline while changing the capture strategy instead of changing its good-looking material or lighting.
Operator card: stable ID xpp-tool.character-material-coverage-union.v1.
Inputs are one retail XPP capped at 64 MiB, one exact texture allowlist, and one
through 16 pinned material reports capped at 1 MiB each with their immutable
v3/v4 bundles and required v4 exclusion manifests. The tool is offline,
single-process, deterministic, input-order independent, and payload-free. It
rejects duplicate observations, source/UV/texture-family conflicts, dishonest
coverage, index payloads outside retail topology, symlinks, over-bound inputs,
and overwrite attempts. Output is capped at 256 KiB and contains counts and
hashes, never model, texture, shader, index, or game payload bytes. Full
character, original normals/tangents, rig/skin, 4×, authored PBR, RPCS3 round
trip, and native import remain separate gates.
character-component-ledger is the durable bridge between individual material
exports and the all-asset completion inventory. It consumes repeatable exact
infamous-character-material-export reports plus an optional checksum-pinned,
payload-free visual receipt manifest. A friendly texture name is not the
identity: each component is keyed by title, build, candidate, runtime page, and
source record offset. Multiple runtime events may therefore remain explicit
observations of one source component without becoming duplicate work.
timeout 60s xpp-tool character-component-ledger \
--title-id infamous-1 \
--build-id bcus98119-v0100 \
--candidate-id zeke \
--material-report /path/to/hair-material.json \
--material-report-sha256 HAIR_REPORT_SHA256 \
--material-report /path/to/head-material.json \
--material-report-sha256 HEAD_REPORT_SHA256 \
--visual-receipts /path/to/zeke-render-receipts.json \
--visual-receipts-sha256 RENDER_RECEIPTS_SHA256 \
--output /new/path/zeke-component-ledger.jsonThe first owned run reconciles five distinct Zeke records: hair 533752,
jacket 534628, packs 535048, head 536280, and jacket detail
536488. All five already have exact source record, runtime topology, UV,
retail texture binding, material GLB, and published-render receipts. Only jacket
detail has complete observed material coverage (24 / 24 triangles). Hair is
275 / 294; jacket is 492 / 1,002; packs are 167 / 302; and head is
174 / 404. Those four remain selected for missing material-draw evidence,
not for redundant re-export. Three final-code outputs are byte-identical at
28,843 bytes, SHA-256
755fb441c735671697953141074e92bce357049addb1bb831388cfccb76e6046.
RR — Really Readable rundown: this is Zeke's parts checklist. It says, “we have this exact hair record, this exact head record, these two separate jacket-family records, and this exact packs record.” It also says what each part still lacks. The approved matte/unlit hair image is now a protected visual baseline: future work should not reintroduce fake shine, patchiness, or split-tone lighting. But a good-looking picture cannot fill nineteen unproved hair faces, add feet, join the body, create bones, prove 4× textures, invent PBR, or prove a working RPCS3 mod. Those gates stay separate and false.
Operator card: stable ID xpp-tool.character-component-ledger.v1. The command
is offline and single-process. It accepts at most 256 material observations,
128 components, and 256 render receipts. Each material report is capped at 1
MiB, the visual receipt manifest at 256 KiB, and output at 1 MiB. Every input is
a regular non-symlink file with an exact SHA-256 pin; duplicate paths/content,
contradictory immutable geometry, mismatched texture families, dishonest
triangle coverage, unknown render targets, and unsupported schemas fail closed.
Output contains counts, names, and hashes—not model, texture, shader, or game
payload bytes—and is atomically published only to a new path. Full character,
rig/skin, 4×, authored PBR, RPCS3 round trip, and native-decomp import remain
false until independent evidence closes each gate.
character-material-candidate-census replaces the repeated manual step of
copying page/event/record triples into separate lineage commands. It reads one
checksum-pinned source census, selects every full-source-range event on one
exact runtime page, removes only explicit completed EVENT:RECORD_OFFSET
identities, and runs the maintained shader-lineage proof against every remaining
candidate.
timeout 60s xpp-tool character-material-candidate-census \
--bundle /path/to/owned/page-2-v4 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--capture-key-exclusion /path/to/page-1-keys.tsv \
--page 2 \
--source-census /path/to/xpp-source-census.json \
--source-census-sha256 SOURCE_CENSUS_SHA256 \
--character-census /path/to/character-census.json \
--character-census-sha256 CHARACTER_CENSUS_SHA256 \
--character-side left \
--exclude-candidate 5:536488 \
--exclude-candidate 15:533752 \
--exclude-candidate 16:533752 \
--output /new/path/page2-material-candidates.jsonThe first final-code run sees six eligible page-two full-range events. Three
are already completed and excluded by their exact event/record identities. The
remaining three all pass: event 3 / record 534628 / Zeke_Jacket, event 11 /
record 536280 / Zeke_Head, and event 13 / record 535048 / Zeke_Packs.
Two final reports repeat byte-for-byte at 5,901 bytes, SHA-256
6a5b5c88924aa81103ad3c87f259e75d4d43f94ce80150efaf9e1b719b87692b.
The report includes each accepted lineage-report SHA-256, UV input shape,
fragment coordinate, family, and sampler/name identities; a failed candidate
would remain present with its exact bounded rejection reason.
RR — Really Readable rundown: the old process could accidentally skip a valid component or redo one we had already finished because the list lived in shell commands and notes. The permanent census asks the source evidence for the whole eligible page, subtracts only exact completed identities, and gives every new candidate a pass or fail receipt. A pass means “safe to write the full lineage and render this component next.” It does not mean the component is the whole character, fully material-covered, rigged, PBR, 4×, or ready to inject.
Operator card: one page, at most 16 candidates, at most 272 source event rows, 2 MiB per pinned JSON authority, inherited 64 MiB bundle/payload bounds, one process, no runtime or network, and a 512 KiB payload-free JSON output. Inputs must remain immutable; the output must be a new path outside the bundle and all authorities. Duplicate/unknown completion exclusions and an empty remaining set fail closed. Repeated identical inputs produce identical bytes; occupied output refuses overwrite.
character-material-export now preserves every texture selected by one exact
shader-lineage family instead of requiring exactly two. A lineage must still
contain unique samplers, unique short alphanumeric name suffixes, one bounded
family, and required C plus N descriptors. Two through eight images are
accepted; every encoded runtime-prefix identity is rechecked against the retail
XPP before decoding.
timeout 60s xpp-tool character-material-export \
--xpp /path/to/owned/male_base_Zeke.xpp \
--bundle /path/to/owned/page-2-v4 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--capture-key-exclusion /path/to/page-1-keys.tsv \
--lineage /path/to/zeke-jacket-event5-binding.json \
--lineage-sha256 LINEAGE_SHA256 \
--material-coverage-mode observed-only \
--output-glb /new/path/zeke-jacket-event5.glb \
--output-report /new/path/zeke-jacket-event5.jsonThe first owned run exports record 536488 as 26 vertices / 24 triangles,
one shader-proved UV layer, generated inspection normals, and four exact retail
images in sampler order:
- sampler 0:
Zeke_Jacket_N.psd— display-wired as the name-derived normal; - sampler 1:
Zeke_Jacket_A.psd— embedded, role unassigned; - sampler 2:
Zeke_Jacket_S.psd— embedded, role unassigned; - sampler 3:
Zeke_Jacket_C.psd— display-wired as name-derived base color.
The runtime and retail index lists are identical, so all 24/24 triangles have
exact material coverage. Two final GLBs repeat at 513,796 bytes, SHA-256
bd364627c4bd2e57cb7088320859d1839e5b67fa4d3d91acd487646ea83e3c47;
their payload-free reports repeat at SHA-256
a208e1335c10ea5fb27585ede1499af26f075ddce57bc491d91ab9ce5815f09e.
RR — Really Readable rundown: Blender should receive every image the captured
shader selected, but receiving an image is not the same as knowing its modern
PBR meaning. The exporter therefore keeps all four retail maps inside the GLB,
uses only the already-supported C/N display convention, and writes explicit
null display roles for A and S. Nothing silently turns those letters into
alpha, specular, roughness, or metalness.
Three immediate unlit views show two small, widely separated textured islands,
including a legible EMPIRE CITY detail. That proves the UVs address coherent
retail imagery. It does not prove the human-readable name or whole-character
placement of the geometry, and it does not make this record a full jacket.
Operator card: stable ID xpp-tool.character-material-export.v1, maintained in
xpp-tool 2.28.0. The 64 MiB XPP/GLB and 256 KiB report bounds, immutable authority
pins, complete topology/material partition, deterministic atomic pair publication,
and no-overwrite behavior remain unchanged. The GLB is private/operator-owned;
the public repo and wiki retain only source, payload-free facts, and review PNGs.
character-uv-texture-binding now distinguishes two byte counts that look
similar but are not interchangeable: bytes in the original packed XPP vertex
stream and bytes in a renderer's padded host upload. A three-component
half-float row occupies six packed source bytes even when a host-side upload
later expands it to eight. The source-binding path applies the same packed rule
to three-component unorm8 rows; other formats retain their conservative sizing
until independently proved.
The command still refuses gaps, overlaps, non-finite float rows, and ambiguous descriptor ordering. It changed the source-storage width calculation; it did not weaken the existing complete-tiling proof.
timeout 60s xpp-tool character-uv-texture-binding \
--bundle /path/to/owned/page-2-v4 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--capture-key-exclusion /path/to/page-1-keys.tsv \
--page 2 --event 5 --record-offset 536488 \
--source-census /path/to/source-census.json \
--source-census-sha256 SOURCE_CENSUS_SHA256 \
--character-census /path/to/character-census.json \
--character-census-sha256 CHARACTER_CENSUS_SHA256 \
--character-side left \
--output /new/path/zeke-jacket-event5-binding.jsonThe first owned 2.27 run proves one exact 26-vertex / 24-triangle component:
- source record
536488, stream zero, 26 rows × 10 bytes; - one valid complete layout: four-byte attribute 3 at byte 0, then six-byte half3 attribute 9 at byte 4;
- attribute 9 XY feeds vertex output 7 and fragment
TEX0; its third stored component is consistently-1.0, but this gate assigns no semantic to it; - samplers 0–3 select unique named
Zeke_Jacket_N,A,S, andCdescriptors; - the runtime index list exactly equals the complete retail component: all 72 indices / 24 triangles are covered, with no hair-style unobserved remainder.
RR — Really Readable rundown: the earlier tool tried to measure the original game bytes with the ruler RPCS3 uses after preparing them for the PC GPU. That made a real ten-byte row appear impossibly twelve bytes wide. We now use the packed-game-data ruler for the XPP and keep the padded-renderer ruler in its own lane. The result is a second real wire from Zeke triangles to UVs to named jacket textures, not a texture chosen because it looked close.
This does not yet tell us whether the 24-triangle piece is a collar, jacket detail, book-adjacent prop, or another sub-piece. It creates no render and does not claim a full jacket or full Zeke. The next bounded gate extends the material exporter from a two-texture hair pair to this four-texture family while keeping retail name suffixes separate from authored PBR meanings; any progress render is published as soon as it exists.
Operator card: stable ID xpp-tool.character-uv-texture-binding.v1, introduced
in 2.25 and maintained in xpp-tool 2.28.0. Inputs, bounds, payload-free output, SHA-256 pins, atomic
new-only publication, and no-runtime/no-network behavior remain unchanged from
2.25. Packed source widths are now explicit and regression-tested; full
character, PBR/4x, RPCS3 repack, and native-decomp import remain separate gates.
character-material-export turns one checksum-pinned 2.25 lineage into a
deterministic, inspectable GLB. It rereads the immutable runtime bundle and
retail XPP, verifies the full retail topology and both encoded texture-prefix
identities, decodes the retail color/normal mip-zero images, and writes the
proved half-float rows as TEXCOORD_0.
timeout 60s xpp-tool character-material-export \
--xpp /path/to/owned/male_base_Zeke.xpp \
--bundle /path/to/owned/page-2-v4 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--capture-key-exclusion /path/to/page-1-keys.tsv \
--lineage /path/to/zeke-hair-binding.json \
--lineage-sha256 LINEAGE_SHA256 \
--output-glb /new/path/zeke-hair-retail-material.glb \
--output-report /new/path/zeke-hair-retail-material.jsonThe default --material-coverage-mode observed-only is the audit-safe export.
It proves the captured retail material on 275 of 294 triangles and writes
the remaining 19 material-unobserved triangles as a separate orange,
unlit diagnostic primitive. It repeats at 178,024 bytes, SHA-256
1a2a3eaa8229a2c870b99793996dd948db108397c42f83db5360f0ca018f0b68;
its payload-free receipt repeats at SHA-256
81e24c77402524c7d7739dfdec4d627158df99eb9b7afaaf46b515f004f3944f.
For a clean progress image, add
--material-coverage-mode preview-full-record. That preview applies the
observed material to all 294 retail triangles without orange clay, but both
the GLB metadata and receipt keep full_topology_material_coverage=false and
unobserved_material_preview_extrapolated=true. It repeats at 177,084 bytes,
SHA-256
39d8773b4deecaff5ebcf6cabae3b5e5b19b14a6030f47d3b3e56f95c0e71f6b;
its receipt repeats at SHA-256
d1261f53a968e3720b805796ac50db39e84e5ef7e8b3b6c88f8e7651e7e928a8.
Both forms contain 184 vertices, the full 294-triangle retail topology, one
proved UV layer, and two embedded retail PNGs.
RR — Really Readable rundown: this is the first time one recovered Zeke piece contains the edit-facing trio in one file: triangles, UV coordinates, and the exact retail color/normal images the captured shaders selected. The runtime draw proves where that material belongs for 275 triangles. It does not yet prove the other 19, so the strict file shows them plainly and the clean file labels their material as a preview extrapolation. Both are useful for Blender today, a retail reverse packer next, and a native importer later.
It is still a diagnostic hair component. Vertex attribute 0 remains an
explicitly unproved position semantic; the exported normals are generated from
triangles for inspection, not recovered retail normals; and C/N material
roles follow retail names rather than a proved native PBR contract. The first
published views were also artificially shiny because the old review renderer
replaced the imported material with metallic 0.18 / roughness 0.48; those
views are rejected historical evidence. The maintained renderer now has an
explicit imported-material-preservation mode, and the current clean preview is
matte. The command preserves the exact 275/19 evidence and leaves coordinate
convention, diagnostic position meaning, alpha/material behavior, and the
missing 19-face material assignment as separate gates instead of adjusting the
asset by eye.
The delivery paths remain independent. Near term, this canonical component is input to a validated retail XPP/PSARC round trip in RPCS3. Long term, the same mesh/UV/material manifest can feed a native decomp importer once its asset runtime and renderer exist. The native infrastructure is the larger up-front gate; each later native mod should need less retail-container archaeology.
Operator card: stable ID xpp-tool.character-material-export.v1. Inputs are
immutable and checksum-pinned through the lineage/bundle contracts; the XPP is
capped at 64 MiB, GLB at 64 MiB, and JSON receipt at 256 KiB. The command is
offline, single-process, new-only, and publishes the GLB/report pair together.
The report contains hashes, counts, names, bounds, and gate states—not texture,
vertex, index, shader, or game payload bytes. A render is published whenever it
exists and never gates the evidence/tool release.
character-uv-texture-binding closes one exact material-binding chain at a
time. It consumes an immutable v3/v4 draw bundle plus checksum-pinned source
and character censuses, follows the fragment sampler back through the vertex
shader at component level, and reconciles the selected vertex input with the
exact XPP source stream and named texture descriptors.
timeout 60s xpp-tool character-uv-texture-binding \
--bundle /path/to/owned/page-2-v4 \
--texture-allowlist /path/to/zeke-surface-identities.sha256 \
--capture-key-exclusion /path/to/page-1-keys.tsv \
--page 2 --event 16 --record-offset 533752 \
--source-census /path/to/source-census.json \
--source-census-sha256 SOURCE_CENSUS_SHA256 \
--character-census /path/to/character-census.json \
--character-census-sha256 CHARACTER_CENSUS_SHA256 \
--character-side left \
--output /new/path/zeke-hair-uv-texture-binding.jsonThe first owned run proves this chain for the 184-vertex hair record:
- exact XPP record
533752, stream zero, 184 rows × 8 bytes; - one valid complete packed layout, with attribute 3 at byte 0 and two-component half-float attribute 9 at byte 4;
- vertex attribute 9 XY feeds vertex output register 7, which RPCS3 maps to
TEX0; - both target fragment
TXBinstructions read thatTEX0coordinate; - sampler 0 resolves uniquely to the 174,760-byte mip prefix of
Zeke_Hair_N.psd, while sampler 2 resolves uniquely to the 87,376-byte mip prefix ofZeke_Hair_C.psd; - observed UV bounds are U
0.0051002502–0.9941406250and V0.0287017822–0.7763671875.
RR — Really Readable rundown: before this, we had the correct hair triangles
and the correct hair textures sitting beside each other, but no proved wire
between them. This command follows the wire the game actually uses: two packed
numbers enter vertex attribute 9, the vertex shader passes their X and Y into
TEX0, and the fragment shader uses TEX0 to sample the named hair color and
normal textures. That is why this is real progress toward a texturable Blender
piece rather than another visual guess.
The old capture did not directly store each descriptor's byte offset. The
command says so. It reconstructs byte 4 because the two descriptors exactly
tile the eight-byte stride and only one ordering has finite float components;
the reversed order decodes the leading 0xff bytes as non-finite half floats.
This is a deterministic, fail-closed reconstruction—not a claim that the byte
offset came directly from the capture log.
This gate does not create a render. One hair mesh is not full Zeke, the C/N
name suffixes are not presented as native PS3 PBR metadata, and 4x/PBR,
remaining material slots, Blender assembly, RPCS3 repack, and native-decomp
import stay separate. The next gate writes the proved half-float rows as
TEXCOORD_0, attaches retail color/normal materials, and publishes the first
correctly textured progress render immediately.
Operator card: stable ID xpp-tool.character-uv-texture-binding.v1. The command
is offline and single-process; each JSON authority and the output are capped at
2 MiB and 256 KiB respectively, the bundle inherits the strict 16-event / 64
MiB payload contract, every external authority is SHA-256 pinned, and output is
new-only with atomic publication. It serializes hashes, counts, names, bounded
numeric summaries, and lineage tokens—not shader, vertex, index, texture, or
game bytes. The same report feeds the near-term RPCS3 material/repack path and
the later native-decomp asset importer; neither delivery path is promoted by
this evidence alone.
asset-completion-inventory is the permanent gate before any corpus-wide
character or item batch. It consumes four exact, checksum-pinned authorities:
the decomp graphics/assets tally, the retail static-GLB manifest, a metadata-only
gallery snapshot, and one character/item census selected as the first unfinished
batch.
xpp-tool asset-completion-inventory \
--decomp-tally /path/to/GRAPHICS-ASSETS-TALLY.md \
--decomp-tally-sha256 TALLY_SHA256 \
--static-glb-manifest /path/to/MANIFEST.json \
--static-glb-manifest-sha256 GLB_MANIFEST_SHA256 \
--gallery-snapshot /path/to/gallery-drive-snapshot.json \
--gallery-snapshot-sha256 GALLERY_SNAPSHOT_SHA256 \
--character-census /path/to/character-census.json \
--character-census-sha256 CENSUS_SHA256 \
--candidate-id zeke \
--output /new/path/canonical-asset-inventory.jsonThe first owned run reconciles 57 successful retail static GLB exports, 19 unique 8K asset renders, 1 gameplay screenshot, 1 duplicate Drive file entry, 15,437 corrected texture records, and 0 character renders. Nine render subjects join one static GLB by the deliberately narrow normalized identity rule; ten remain unresolved instead of being guessed. The resulting inventory has 68 records: 0 fully complete, 58 partial, and 10 unknown. It skips only the work already proved: 57 retail static exports and 19 existing renders.
The first unfinished batch is Zeke because the input census—not a filename-only search—proves the same multipart target in both builds, 31 named cross-build texture identities, and 16 packed geometry contracts per build while every complete-model and delivery gate remains false. The short exit is an editable, correctly assembled asset that round-trips through retail XPP/PSARC in RPCS3. The long exit uses the same canonical record in the native decomp. Native import does not block the emulator path.
RR — Really Readable rundown: this is the project checklist that stops us from doing finished work twice or calling a pretty picture a finished mod. A GLB says “we exported this exact static model once.” An 8K PNG says “this exact picture exists.” Neither says the object is correctly aligned, fully textured, safely repackable, accepted by RPCS3, or loadable by the future native engine. For the native decomp, mod authoring should become easier after its asset loader and renderer exist because we control that loader and can use this stable manifest; the hard wait is building that native runtime, not repacking each later mod.
Inputs are regular non-symlink files and must match their SHA-256 pins. Counts, hashes, safe relative GLB paths, duplicate claims, candidate identity, and source reconciliation fail closed. Private contact paths and payload bytes are never serialized. Output is deterministic, capped at 4 MiB, and published only to a new path. A partial render or turntable may publish immediately and never blocks other evidence, but it cannot change a completion gate by itself.
character-asset-census is the permanent profile-wide discovery primitive for
complete character/item extraction. It does not assume one filename is the
whole asset and it does not hard-code Zeke. One invocation audits a target XPP
across two complete extracted profiles and their exact ordinal OID manifests:
xpp-tool character-asset-census \
--left-profile /path/to/bcus-profile \
--right-profile /path/to/npua-profile \
--left-workspace-sha256 LEFT_WORKSPACE_SHA256 \
--right-workspace-sha256 RIGHT_WORKSPACE_SHA256 \
--left-oid-manifest /path/to/bcus/oid_manifest.txt \
--right-oid-manifest /path/to/npua/oid_manifest.txt \
--left-oid-manifest-sha256 LEFT_OID_SHA256 \
--right-oid-manifest-sha256 RIGHT_OID_SHA256 \
--left-target xpp/install1/male_base_Zeke.xpp \
--right-target xpp/install1/male_base_Zeke.xpp \
--anchor male_base_Zeke.xml --name-token zeke \
--output /new/path/zeke-asset-census.jsonThe command verifies every workspace entry against its declared byte count and SHA-256 while streaming the complete package corpus. It records:
- exact descriptor OID-to-name bindings and texture-family groupings;
- aligned manifest-OID references separated by XPP chunk type;
- proved packed character topology counts without inventing per-piece names;
- exact descriptor sharing, substantial mip/prefix sharing, and tiny shared-mip counts across both profiles;
- location-independent cross-build descriptor mapping without trusting index;
- separate false gates for piece completeness, orientation, alignment, UVs, materials, LOD/state/flavor selection, Blender completion, RPCS3 mod round trip, and native-decomp import.
The first owned audit read 5,038 packages / 3,769,944,736 declared bytes and 31,557 texture descriptors. BCUS and NPUA each contain 31 target textures and 16 packed geometry contracts. All 31 texture identities match uniquely, but all 31 descriptor positions are reordered. No substantial target texture is shared by another package and no substantial partial mip/prefix match was found. The only exact external sharing is the same 8-byte, 1×1 utility texture in five other packages per build.
RR — Really Readable rundown: Zeke really is built from many named parts and texture sheets. His XPP directly names jacket, pants, head, hair, packs, a combined hands/feet/collar/glasses family, an eye, glasses lookup strip, and comic-book texture. The package also references named objects such as head, jacket, hair, shoes, glasses, straps, clasps, firearm, and book. The full scan does not find another package supplying his substantial texture bytes. The remaining blocker is the hard one: assigning each of the 16 packed mesh records to the correct named object and then proving which material/UV binding selects which named texture family. Names prove the multipart inventory; they do not by themselves assemble or texture the Blender model.
The command is a reusable per-asset primitive. Corpus batching must first consume a verified completion inventory so already finished assets are skipped. The short product goal is a validated Blender edit that round-trips into RPCS3; the same canonical asset manifest is retained for the later native decomp.
Bounds: two profiles of at most 4,096 packages / 8 GiB each; individual package 256 MiB; workspace and OID manifest 16 MiB each; manifest anchor window at most 512 rows per side; detailed match count 20,000; JSON report 2 MiB; new output only. Inputs are regular non-symlink files and every package is hash checked.
The same permanent character-source-runtime-correlate command now reports two
narrower transform classes for every numeric family in addition to its
unrestricted affine metrics:
- proper similarity: rotation + translation + one positive uniform scale;
- mirrored similarity: one fixed reflection followed by the same proper rotation/translation/uniform-scale fit.
The implementation uses a bounded standard-library symmetric-eigen/quaternion solver. It needs no NumPy, Blender, emulator, network, or game runtime, and it does not serialize the fitted rotation, translation, scale, or vertex values.
On the independently proved 184-vertex hair record,
scale-offset-unsigned preserves the near-exact source/runtime relationship
under a proper transform (R² 0.9999999971, normalized RMSE
0.0000091454). The next proper candidate falls to R² 0.3620036761, a
margin of 0.6379963210. The 26-vertex visible fragment independently ranks
the same family first (R² 0.9912978824, normalized RMSE 0.0222147720),
ahead by 0.0192858807 R-squared.
RR — Really Readable rundown: the earlier free affine fit could stretch each
axis independently, so every formula could be made to look equally good. The
new test ties the axes together: it allows the kind of rotation, movement, and
single scale expected for a rigid component, but it does not allow arbitrary
shape warping. Under that stricter rule, one formula wins on both records and
is almost exact on proved hair. That makes scale-offset-unsigned the strongest
decoder candidate so far. It is still a candidate—not a promoted semantic—until
the executed retail arithmetic or another equally direct authority proves it.
Operator card: the stable inventory ID remains
xpp-tool.character-source-runtime-correlate.v1; the invocation and bounds are
unchanged from 2.21. Reports are now schema version 2 and preserve all 2.21
unrestricted-affine fields while adding proper/mirrored family fits, rankings,
and margins. Numeric-family selection, position meaning, object space,
ownership, UV, material, rigging, full-character, PBR, and injection gates stay
false. This metric-only update creates no render and does not gate publication
of any other evidence.
The source/runtime decoder probe is now a permanent, payload-free command:
timeout 60s xpp-tool character-source-runtime-correlate \
--xpp /path/to/owned/male_base_Zeke.xpp \
--record-offset 536488 \
--runtime-index ./exact-index.bin \
--runtime-index-sha256 EXACT_INDEX_SHA256 \
--runtime-positions ./exact-float32x3.bin \
--runtime-positions-sha256 EXACT_POSITIONS_SHA256 \
--runtime-byte-order big \
--runtime-first-row 0 \
--output ./source-runtime-correlation.jsonThe command proves the selected runtime index bytes are exactly the chosen XPP record's triangle list, hashes the complete runtime position buffer, records an explicit contiguous row window, and compares every eligible packed three-component stream with those rows. Each result reports source rank, R-squared, RMSE, normalized RMSE, maximum point residual, and cross-stream ranking. It does not write source or runtime vertex values into the JSON.
Two independent owned records now select stream 1. The 184-vertex hair record
is an effectively exact affine match (R² 0.9999999972, normalized RMSE
0.0000089894); the 26-vertex visible fragment is a strong but imperfect match
(R² 0.9943533660, normalized RMSE 0.0178946912). Both source streams have
full three-axis rank.
RR — Really Readable rundown: the flat-looking side image was a thin captured draw viewed after the game had transformed it, not proof that Zeke is stored in one dimension. This new test walks backward to the packed XPP. The hair result shows that one packed stream contains a real three-axis shape which maps almost perfectly to what the GPU used. That is a major bridge toward an editable model, but it is not yet the whole character: we still must prove the canonical scale/offset formula, coordinate space, UV stream, material bindings, bones, weights, assembly, and reverse import.
Operator card: stable ID
xpp-tool.character-source-runtime-correlate.v1. The command is offline and
single-process; XPP input is capped at 64 MiB, each runtime payload at 16 MiB,
and JSON at 256 KiB. Inputs must be immutable regular non-symlink files with
exact SHA-256 pins. Output must be a new path and atomic publication preserves
a concurrent writer. Numeric families that differ only by affine scale/offset
remain intentionally indistinguishable, so all semantic, ownership, UV,
material, rigging, full-character, PBR, and injection gates remain false.
The packed-source visual probe is now a permanent command instead of a disposable analysis script:
timeout 60s xpp-tool character-source-diagnostic-export \
--xpp /path/to/owned/male_base_Zeke.xpp \
--record-offset 536488 \
--stream-index 1 \
--numeric-family endpoint-unsigned \
--output ./zeke-record-536488-source.glb \
--json-out ./zeke-record-536488-source.jsonThe command selects exactly one proved character record and one of its three
descriptor-backed packed streams. It verifies the stream and triangle-list
hashes, unpacks the exact MSB-first integers, applies only the explicitly named
numeric hypothesis, requires finite nondegenerate geometry, and writes a
deterministic GLB with the retail topology unchanged. A PSARC source is also
accepted through --psarc ARCHIVE --entry /path/to/package.xpp.
RR — Really Readable rundown: this is a permanent magnifying glass for packed character records. It lets us repeat the same source-space test on Zeke's glasses, book, clothing, or any later character record without rebuilding a throwaway script. The triangle connections and selected source bytes are proved. The formula used to turn packed integers into coordinates is still a hypothesis, so a recognizable shape is evidence—not yet an editable position stream. UVs, textures, bones, weights, a skeleton, full-body completeness, PBR, and reverse injection all remain separate gates.
Operator card: stable ID xpp-tool.character-source-diagnostic-export.v1.
Python 3.10+ is the only tool dependency; the command is offline and
single-process. The XPP payload and GLB are each capped at 64 MiB. The copyable
operator command uses a 60-second wall-clock bound. Inputs stay read-only;
outputs must be new, distinct paths and are never overwritten. Exit status is
zero only after the GLB is fully published and the deterministic JSON report
is emitted; validation, identity, bounds, nonfinite values, degenerate
topology, aliasing, and overwrite attempts fail nonzero. Repeated runs with the
same input bytes and options produce byte-identical GLBs and JSON. The command
does not prove position semantics, ownership, completeness, rigging,
materials, PBR, Blender readiness beyond diagnostic import, or safe game
injection.
profile-extractthe protected retail install pair.extractthe package.- Edit or upscale the PNGs (or use
--scale). packto a new.xpp.verifyandextractthe new file.- Build and byte-audit the complete install pair with
profile-build. - Run
profile-validatebefore the expensive PSARC build. - Test startup and a scene that actually uses every promoted texture before increasing 2x residency.
Do not commit or distribute game files or transformed textures. The tool and presets can be distributed; each owner builds the mod from their own dump.
No GPU. No Blender. No CUDA. Python 3.10+ and the standard library.
| Job | How | Hardware |
|---|---|---|
| Extract PNG | decode DXT on CPU | any |
HD pack (--scale 2/4) |
nearest-neighbor resize on CPU, rebuild mips, rewrite the XPP | any (a 2048² pack is a few seconds) |
| Derived PBR | Sobel normals + roughness/metal from the albedo, on CPU | any |
| Static mesh → GLB | read float3 + UVs | any |
| Window | GTK 3 if present, otherwise if1-tex ui --web |
any |
HD is not AI upscale and not GPU. It is integer nearest-neighbor so the game still sees DXT the same way. If someone later wants a nicer upscaler they edit the PNGs in whatever they have and --from-dir pack; the tool does not require that.
PBR maps are invented from the color texture. Infamous 1 does not store metalness/roughness. The GLB remaster is for looking at the mesh. The game still reads DXT in the XPP.
Settings in the window: HD scale (auto / 2× / 4×), assemble mode (unique-largest / first / all / heli recipe), remaster PBR on/off.
CC0 1.0. The code is public domain. The game is not.