Crusader_Decomp/docs/psx/psx.md
2026-04-07 00:15:44 +02:00

68 KiB

Crusader: No Remorse (PlayStation) Recon

Scope

  • Target disc tree: E:\emu\psx\Crusader - No Remorse
  • Goal of this pass: identify the boot executable, separate likely code from content, and find the most practical first extraction routes for PS1 assets.

Immediate Conclusions

  • SYSTEM.CNF is the disc boot file and points directly at cdrom:\SLUS_002.68;1.
  • SLUS_002.68 is the main game executable. It begins with a valid PS-X EXE header.
  • No other top-level file currently looks like a second normal PS1 executable.
  • Disc content is dominated by standard PS1 media (.STR, .XA) plus a large number of game-specific .WDL blobs.
  • FMV.BIN looks like movie-playback support data or a resource blob, not a bootable executable.
  • ZZZ.ZZZ looks much more like media/container data than code, and its size exactly matches MOVIES/FMV3.STR.

Disc Boot Evidence

SYSTEM.CNF contents:

BOOT = cdrom:\SLUS_002.68;1
TCB = 4
EVENT = 10
STACK = 801FFFFC

SLUS_002.68 header evidence:

  • Magic: PS-X EXE
  • Initial PC: 0x8004BD90
  • Load address: 0x80010000
  • Image size: 0x000A4800 (675,840 bytes)

This is the clearest Ghidra import candidate for code analysis.

Top-Level File Classification

Top-level items:

  • SLUS_002.68
  • SYSTEM.CNF
  • FMV.BIN
  • ZZZ.ZZZ
  • SPEC_A.WDL
  • LEGAL.SCR
  • LICENSEA.DAT
  • AUDIO/
  • MOVIES/
  • MENUS/
  • LSET1/ through LSET7/

Recursive extension summary from the extracted disc tree:

Extension Count Total bytes Current classification
.STR 33 415,027,744 PS1 movie streams
.XA 2 91,897,856 PS1 XA audio
.WDL 66 76,198,860 custom game asset/level blobs
.ZZZ 1 26,148,984 likely media/container data
.68 1 675,840 main PS1 executable
.SCR 1 153,600 data asset
.BIN 1 90,812 support/resource blob
.DAT 1 28,032 data asset
.CNF 1 70 boot config

File Family Findings

1. SLUS_002.68

  • This is the boot target from SYSTEM.CNF.
  • It has a normal PS1 executable header.
  • It should be the first import into Ghidra.
  • Current working assumption: this is the only primary native code binary on the disc.

2. AUDIO/*.XA

  • Files found:
    • AUDIO/MULTI8.XA
    • AUDIO/TALK1.XA
  • These are standard PS1 XA audio files.
  • MULTI8.XA is divisible by both 2352 and 2048, which is consistent with sector-oriented media data.
  • TALK1.XA is divisible by 2048 but not exactly by 2352.
  • Most practical extraction route: use standard PS1/XA tooling rather than custom RE first.

3. MOVIES/*.STR

  • Files found: FMV0.STR through FMV32.STR.
  • These are the strongest candidates for standard PS1 video streams.
  • The raw headers look consistent with sectorized PS1 stream data rather than executable code.
  • Most practical extraction route: treat them as PS1 STR video and run them through PS1 media tooling first.

4. FMV.BIN

This file does not look like a normal executable.

First bytes begin with:

\MOVIES\FMV%d.STR
MDEC_rest:bad option(%d)
MDEC_in_sync
MDEC_out_sync
DMA=(%d,%d), ADDR=(0x%08x->0x%08x)
FIFO=(%d,%d),BUSY=%d,DREQ=(%d,%d),RGB24=%d,STP=%d

Current best read:

  • FMV.BIN is movie-related support data, code tables, or debug/resource text for the MDEC/FMVs.
  • It clearly references the external movie path pattern \MOVIES\FMV%d.STR.
  • It is worth a secondary Ghidra import only if the goal is to understand the movie subsystem specifically.
  • It is not the disc boot executable.

5. ZZZ.ZZZ

Key findings:

  • Size: 26,148,984 bytes
  • That size exactly matches MOVIES/FMV3.STR.
  • The file begins with stream-like binary data rather than an executable header.
  • It also yielded movie-adjacent string evidence such as MDEC.

Current best read:

  • ZZZ.ZZZ is probably not code.
  • It is a strong candidate for either:
    • a renamed movie stream, or
    • a duplicate/alternate copy of FMV3.STR

Most practical next check:

  1. compare a few sectors or hashes against MOVIES/FMV3.STR
  2. try opening ZZZ.ZZZ directly in PS1 STR-capable tooling

6. LSET*/L*.WDL

These are the most important unknown asset family for content extraction.

Representative level sample: LSET1/L0.WDL

  • Size: 1,312,624 bytes
  • Header starts with structured values, not raw pixels:
0x00000034 0x00006FDC 0x0000376C 0x00001000
0x00000160 0x00000498 0x0000025C 0x00000FE0
0x00000070 0x00072EC4 0x00034B6C 0x00007448
0x0007407C 0x00010824 0x00000002 0x00000000
  • This does not behave like a flat framebuffer dump.
  • It looks more like a custom structured level/container blob with internal offsets, lengths, or section pointers.
  • Additional offset targets inside the file, such as 0x376C, 0x6FDC, and 0x10824, land on repeating structured records rather than code.

Strict TIM-style scan results for L0.WDL found plausible embedded PS1 image headers at:

  • 0xE7A84
  • 0x117DEC
  • 0x12CECC
  • 0x135F18
  • 0x1369F4
  • 0x136B38
  • 0x136C40

Current best read:

  • LSET*.WDL likely holds mixed level resources.
  • At least some of those resources may include standard embedded PS1 TIM-like image blocks.
  • These files are the strongest current target for a custom extractor.

Executable-guided extraction status:

  • lset_level_bundle_load in the imported PSX executable now confirms the executable builds \LSETn\Lx.WDL paths directly and treats those files as the live level-bundle format.
  • The same loader reads a small level header blob first, then a large SPU/audio blob, then dispatches the remaining level resource stream through level_resource_stream_load.
  • image_resource_bind_vram_slot and image_bundle_load_to_vram show that resource types 4 and 5 are image/sprite-oriented resources: they resolve VRAM placement and upload image data through LoadImage.
  • sprite_rle_decode_rows is now confirmed as the row-based decompressor used when a type-5 frame record has its compressed bit set.
  • Current consequence: sprite extraction now has a real executable-backed path, while map extraction has a reliable raw-carving path even though the full tile/object semantics are not decoded yet.

7. MENUS/*.WDL and SPEC_A.WDL

Representative menu sample: MENUS/M13.WDL

  • Size: 1,475,928 bytes
  • Starts with dense repeating values like:
0x9D499D29 0x9D299D29 0x9D4A9D4A 0xA14A9D4A
0x9D49A14A 0x9D299D29 0x9D4A9D4A 0xA14A9D4A

Representative special sample: SPEC_A.WDL

  • Size: 545,424 bytes
  • Starts with the same raw-looking pattern as M13.WDL.

Current best read:

  • These do not begin with obvious pointer tables.
  • They look more like raw or lightly wrapped image/screen asset data than like level containers.
  • M13.WDL had no strict TIM hits in the quick scan.
  • SPEC_A.WDL did have a few plausible stricter TIM-style hits at:
    • 0x449A8
    • 0x80ED8

So the .WDL family probably is not one single uniform format. Current evidence supports at least two subfamilies:

  • structured level/resource blobs (LSET*/*.WDL)
  • raw-looking menu/special screen blobs (MENUS/*.WDL, SPEC_A.WDL)

Executable-guided extraction status:

  • SPEC_A.WDL does not behave like the LSET*.WDL container family.
  • The executable-backed extractor work currently treats it as a raw blob with embedded image candidates rather than as a contiguous section table.
  • A validated carve on SPEC_A.WDL currently finds strict TIM-style hits at 0x1B5CC and 0x80ED8.

Executable-Backed Extraction Model

These findings are now grounded in both file inspection and the imported SLUS_002.68 executable.

Level bundles: LSET*/*.WDL

Validated current extraction model for LSET1/L0.WDL:

  • top-level header size: 0x34
  • immediately following blob: 0x6FDC bytes
  • post-audio resource area starts at: 0x7010
  • high-confidence internal boundaries recovered from the header and validated with the extractor:
    • 0x7448
    • 0x34B6C
    • 0x72EC4
    • 0x7407C

Current carved regions from L0.WDL:

Region Offset Size Current interpretation
audio_or_spu_blob 0x34 0x6FDC SPU/sequence data loaded by the audio init path
post_audio_region_00 0x7010 0x438 small table/directory block
post_audio_region_01 0x7448 0x2D724 strong map/placement candidate
post_audio_region_02 0x34B6C 0x3E358 strong map/placement candidate
post_audio_region_03 0x72EC4 0x11B8 small control/index block
post_audio_region_04 0x7407C 0xCC6F4 strongest current sprite/graphics bank candidate

Important consequence:

  • for map work, the best current extraction targets are post_audio_region_01 and post_audio_region_02
  • for sprite/graphics work, the best current extraction target is post_audio_region_04

Important correction from the next executable pass:

  • a previously suspected text-like block in the broader PSX resource system is now confirmed separately in executable analysis as a menu/prompt text resource, not map data
  • a heavily used level-side table is also now confirmed as a per-type flag/behavior table used by collision/order logic, not a raw map grid
  • so the late-level extraction focus stays on LSET*.WDL post-audio regions, not on every large runtime table seen in the executable

The validated strict TIM carver currently finds one confirmed embedded TIM block in L0.WDL at:

  • 0xBBA54

That hit lands inside post_audio_region_04, which supports treating the late large region as the current best graphics bank candidate.

The same structure now reproduces on LSET1/L1.WDL too:

  • header size: 0x34
  • audio blob: 0x3244
  • post-audio start: 0x3278
  • high-confidence boundaries: 0x6F48, 0x334D8, 0x602C4, 0x732D4
  • late graphics candidate region: 0x732D4 .. EOF
  • current strict TIM hit: 0xB4DC8

So the current working model is no longer based on just one level file: LSET bundles appear to share a stable pattern of:

  1. fixed 0x34 header
  2. SPU/audio blob
  3. several map/meta candidate regions
  4. one large late graphics-oriented region

What is still not stable yet:

  • the internal semantics of post_audio_region_01 and post_audio_region_02 are still unresolved
  • L0.WDL starts with rows that look structured when viewed as u16x6, but L1.WDL does not preserve the same obvious interpretation at the same region boundary
  • current safest reading is that these are still raw candidate map/meta payloads, not yet a decoded placement format

Menu / special blobs: SPEC_A.WDL, MENUS/*.WDL

These currently behave like raw image-oriented blobs, not like the structured LSET family.

Validated current extraction model for SPEC_A.WDL:

  • whole-file raw blob fallback works cleanly
  • strict TIM hits currently validate at:
    • 0x1B5CC
    • 0x80ED8

Representative secondary check on MENUS/M13.WDL:

  • whole-file raw blob fallback also works cleanly there
  • current strict TIM hit validates at:
    • 0x493EC

This keeps menus/special screens as a secondary image-carving problem instead of a map/container problem.

Working Extractor

Current extractor script:

  • tools/psx_extract_wdl.py

What it does right now:

  • recognizes the validated LSET*.WDL top-level layout
  • carves the audio blob and header-directed post-audio regions
  • scans the whole file for strict TIM blocks and extracts them
  • falls back to raw-blob carving for SPEC_A.WDL / menu-like files
  • emits an exploratory u16x6 CSV view for the first post-audio LSET candidate region so raw row patterns can be inspected without claiming final semantics
  • scans the large late LSET graphics region for type-5 sprite bundle headers
  • decodes row-RLE compressed sprite frames and writes raw frame payloads plus grayscale preview images
  • writes carved output under out/psx_wdl/<stem>/

Current practical usage:

c:/Users/Maddo/.PYENV/PYENV-WIN/versions/3.14.3/python.exe tools/psx_extract_wdl.py "E:/emu/psx/Crusader - No Remorse/LSET1/L0.WDL"
c:/Users/Maddo/.PYENV/PYENV-WIN/versions/3.14.3/python.exe tools/psx_extract_wdl.py "E:/emu/psx/Crusader - No Remorse/SPEC_A.WDL"

This is enough to start extracting:

  • raw map-candidate blocks from level bundles
  • strict TIM sprite/image blocks from both level and menu/special blobs
  • exploratory raw row exports for the first LSET post-audio candidate region
  • actual extracted sprite frames from at least some type-5 bundles inside the late LSET graphics region

Current caution:

  • the u16x6 export is only a raw inspection aid
  • L0.WDL gives structured-looking rows such as 0041,177B,0F7F,0000,0002,0020, but L1.WDL shows very different values at the same relative region
  • so this export should be treated as evidence-gathering for map decoding, not as a solved object-placement parser yet

Confirmed Sprite Extraction

The extractor now produces actual sprite-frame outputs from at least part of the late LSET graphics bank.

The late-graphics scan is now widened beyond the original first-32-bundle probe. Current L0.WDL extraction finds 159 candidate bundles in post_audio_region_04, and the larger-first overview now clearly includes not just floor/wall tiles but also several object/UI-like assets such as framed panels, cabinets, a portrait, a hand-shaped sprite, a bone, and other small pickup-like art.

Confirmed current example from LSET1/L0.WDL:

  • graphics-region-relative bundle offset: 0xE5B8
  • whole-file bundle offset: 0x82634
  • mode: 2 (current best read: 4bpp indexed)
  • frame count: 3
  • first extracted frame dimensions: 40 x 66
  • runtime default bundle palette index: 12

Confirmed output files:

  • raw frame bytes: out/psx_wdl/L0/sprite_bundles/bundle_0000E5B8/frame_000.bin
  • preview image: out/psx_wdl/L0/sprite_bundles/bundle_0000E5B8/frame_000.png
  • colored preview image: out/psx_wdl/L0/sprite_bundles/bundle_0000E5B8/frame_000_color.png
  • colored sprite atlas: out/psx_wdl/L0/sprite_bundles/bundle_0000E5B8/atlas_color.png
  • bundle metadata: out/psx_wdl/L0/sprite_bundles/bundle_0000E5B8/bundle.json
  • palette metadata: out/psx_wdl/L0/sprite_bundles/bundle_0000E5B8/palette.json

Current result:

  • this is no longer just a graphics-bank hypothesis
  • the workflow can now extract actual sprite/image frame payloads and render preview images from at least some LSET graphics bundles
  • the workflow can now also render colored previews and a proper per-bundle RGBA atlas using the bundle default palette index recovered from the PSX executable
  • widened grayscale overviews now confirm that the late graphics bank contains recognizable object and UI art, not just texture/noise candidates

Current palette model:

  • the executable-backed palette source for LSET*.WDL is the first 0x1000 bytes loaded into DAT_800676d8 by lset_level_bundle_load
  • level_palette_upload_cluts uploads that 0x1000 block as 8 x 16 raw 16-color CLUTs and then caches the raw CLUT handles in DAT_800a9f48
  • level_palette_expand_5bit_to_16color separately builds a second 0x1000 grayscale-expanded table, but DAT_800a9f48 is populated only from the first 8 raw rows
  • the bundle header field at +0x14 is the default palette-table index used when no override is supplied at draw time
  • the remaining blocker is not locating the raw CLUT block anymore; it is recovering the per-placement palette override metadata that can replace the bundle default during map/tile rendering

Current color blocker:

  • both main texture draw helpers (FUN_80044bdc and FUN_80044e9c) fall back to the bundle default palette index only when no override is present
  • the important caller path at FUN_80041458 ORs in a high-byte palette override from object/tile metadata pointed to by object field +0xa0
  • that +0xa0 pointer is now tighter too: both object constructors store the original authored source-record pointer there, so the override is not coming from a hidden runtime side table. For current solved families the draw helper reads the override straight from the authored record bytes:
    • type 0x003e..0x00ab: high byte of source word at record +0x06
    • type >= 0x00ac: high byte of source word at record +0x0c
  • that means standalone bundle previews can still be wrong even when the bundle parser and raw CLUT table are both correct
  • the extractor now emits wider u16x12 raw CSV views for post_audio_region_01 and post_audio_region_02 because the relevant override state appears to live beyond the first 6 words of those candidate placement records
  • the current top-ranked portrait bundle (bundle_00064478, default palette index 106) is a useful color-validation anchor because the grayscale frame is obviously correct while all raw-palette candidates remain visibly wrong
  • another important unresolved issue is the exact on-disk location of the second-stage runtime header after the initial 0x3520 front image block. The loader assembly proves the runtime sequence is: 0x3520 front block -> 12-byte header -> palette blob -> audio blob -> 4-byte stream count, but the raw file bytes at offset 0x3520 do not yet reconcile cleanly with those expected sizes.
  • cd_file_read itself does not transform or decompress bytes; it performs sector-based buffered CD reads. So the remaining palette-source problem is now narrowed to file-layout interpretation rather than hidden read-time decoding.

Runtime Dump Grounding: cabinet console bundle

The new RAM/VRAM dump pair was used to ground the known-colored cabinet console bundle against live runtime state instead of continuing static palette guessing.

Verified cabinet anchor:

  • bundle: out/psx_wdl/L0/sprite_bundles/bundle_000A1B04
  • mode: 1
  • frame 0: 56 x 68
  • default bundle palette index: 0

Verified live-texture result from binary/Crusader - No Remorse (USA) GPU RAM.bin:

  • the frame payload from bundle_000A1B04/frame_000.bin exists in live VRAM as one exact 8bpp texture match
  • exact match location: texel x=258, y=256
  • texture page: (1,1)
  • in-page offset: (2,0)
  • no flipped exact match was found

This is a strong confirmation that the current mode 1 pixel decode is correct. The remaining problem is CLUT selection, not texture extraction.

Verified CLUT result from the same dump:

  • the active CLUT band used by level_palette_upload_cluts still sits at rows 0xF0..0xF7
  • the important successful step was simpler than the later screen-match ranking pass: in live_vram_clut_atlas.png, the very first candidate at the top-left corner is the correct formula for this visible cabinet family
  • that top-left candidate is the contiguous 256-entry palette taken directly from live GPU row 0xF0 at x=0
  • in other words, current best read for this mode 1 family is: byte value -> direct index into the 256-word slice [row 0xF0, x 0..255]
  • equivalently, this behaves like 16 adjacent live 16-color CLUTs flattened into one 256-entry lookup table for the sprite byte stream
  • the later numeric ranking pass that preferred handle 64 / row 0xF4 was misleading for this case and should not be treated as the correct palette formula

Important consequence:

  • the dump-grounded success case is not bundle default row 0 from L0.WDL and not the later row 0xF4 ranking result
  • the working palette source is the live VRAM CLUT row 0xF0, x=0, treated as one contiguous 256-entry table
  • this means the current extractor problem for mode 1 bundles is better described as recover the runtime CLUT-row formula rather than pick one cached CLUT handle index
  • for the visible wall-console bundle, that runtime formula now has a concrete verified answer even though the higher-level metadata path that selects it is still unresolved

Wider decode result using the corrected formula:

  • a focused batch renderer was run over the detected mode 1 bundles in LSET1/L0.WDL post-audio graphics region 04
  • using the same live palette source row 0xF0 / x=0, the pass rendered 92 mode 1 bundles with plausible colored output instead of only the single cabinet proof case
  • the strongest batch proof is the generated overview:
    • out/psx_wdl/L0/mode1_live_clut_row_f0_x0/overview_live_row_f0_x0.png
  • per-bundle outputs and summary metadata now live under:
    • out/psx_wdl/L0/mode1_live_clut_row_f0_x0/
    • out/psx_wdl/L0/mode1_live_clut_row_f0_x0/summary.json
  • that wider pass now shows many object-like assets decoding plausibly under the same rule: cabinets, panels, tanks, wall fixtures, floor markers, weapons, pickups, and small machinery props

Generated runtime-grounded artifacts:

  • binary/psx_framebuffer_left.png
  • binary/psx_framebuffer_console_crop.png
  • out/psx_wdl/L0/sprite_bundles/bundle_000A1B04/live_vram_clut_atlas.png
  • out/psx_wdl/L0/sprite_bundles/bundle_000A1B04/live_vram_clut_top_matches.png
  • out/psx_wdl/L0/sprite_bundles/bundle_000A1B04/live_vram_clut_best.png
  • out/psx_wdl/L0/sprite_bundles/bundle_000A1B04/live_vram_clut_rank.txt
  • out/psx_wdl/L0/mode1_live_clut_row_f0_x0/overview_live_row_f0_x0.png
  • out/psx_wdl/L0/mode1_live_clut_row_f0_x0/summary.json

One important caveat from the dump-grounded pass:

  • none of the 8 raw 256-color palette blocks carved from LSET1/L0.WDL matched the live CLUT rows byte-for-byte in this dump
  • that means either the dump was captured from a different loaded level/resource set, or the active runtime palette source for this on-screen console is not the raw L0.WDL palette blob currently being tested

Palette follow-up note:

  • most extracted PSX sprite data is now structurally correct, but palette selection is still only partially solved
  • the current exporter should therefore be treated as good enough to continue map work, not as a final automatic color pipeline
  • we need a later pass to recover the runtime palette-selection rule well enough to assign the correct palette automatically for every bundle family instead of relying on the currently verified mode 1 rule plus heuristics

PSX Map Decode Plan

Current objective:

  • decode the PSX LSET*.WDL map/resource layout well enough to render PSX maps through the existing public map renderer pipeline instead of building a one-off viewer

Current working split:

  • post_audio_region_01 is now the first high-confidence map-placement candidate
  • post_audio_region_02 still looks more like compressed or mixed resource payload than directly renderable placement rows
  • post_audio_region_04 remains the late graphics bank and should be treated as the PSX art source for any eventual renderer integration

Immediate working hypothesis:

  • post_audio_region_01 is a fixed-row authored layout stream
  • the raw u16x12 view is already showing that each 24-byte row behaves like two adjacent 6-word records with similar field structure
  • the strongest early evidence is that neighboring left/right halves carry similar value ranges and repeat the same small control words in the tail fields, which is what we would expect from paired cell/object placements rather than opaque compressed data

Practical renderer goal:

  • adapt the PSX decode into the same broad source model the public renderer already uses for PC fixed maps: coordinates, shape/frame identity, and a few raw metadata bytes/words kept for inspection
  • do not block on fully naming every field before producing a first renderer-fed PSX map source

Planned work order:

  1. Lock down post_audio_region_01 row structure across more LSET files and confirm whether 24 bytes is the true authored row size.
  2. Separate the two half-rows into individual candidate placement records and track stable min/max ranges for each word position.
  3. Identify which words are likely coordinates by checking for bounded map-like ranges and local spatial continuity between neighboring rows.
  4. Identify which words are likely tile/object ids by checking whether the same values recur in ways that match repeated wall/floor/object motifs.
  5. Correlate the placement stream against post_audio_region_04 bundle offsets or bundle-local ids to recover the art linkage.
  6. Determine whether post_audio_region_02 is a secondary map layer, a lookup table for region 01, or a different compressed resource class entirely.
  7. Prototype a PSX map-source exporter that emits JSON in a renderer-friendly form even if some fields are still labeled as raw words.
  8. Add a PSX-specific loader path to the existing map renderer instead of creating a separate PSX viewer.
  9. Once the first map renders, iterate on field naming, layer semantics, and art binding rather than trying to solve the whole format up front.

Current evidence-backed next step:

  • the extractor now needs to keep emitting a paired-record export for post_audio_region_01 so the candidate row model can be checked quickly across multiple maps without reinterpreting the CSV by hand each time

Current renderer-compatibility result:

  • the old Python/site real-art probe remains useful as discarded negative evidence, but it is no longer the active viewer workflow
  • the active integration path now lives inside k:\ghidra\crusader_map_viewer\map_renderer and builds live data into .cache from STATIC_PSX
  • active renderer-local scripts:
    • src/build-psx-cache.js
    • src/lib/psx-cache.js
  • build entrypoint:
    • npm run build-psx-cache
  • current generated live-cache outputs:
    • k:\ghidra\crusader_map_viewer\map_renderer\.cache\psx\catalog.json
    • k:\ghidra\crusader_map_viewer\map_renderer\.cache\reference-data\psx-remorse\reference-data.json
    • per-map scene files under k:\ghidra\crusader_map_viewer\map_renderer\.cache\scene-cache\psx-remorse\map-*\<fingerprint>\scene.json
    • k:\ghidra\crusader_map_viewer\map_renderer\Catalogs\psx_shape_catalog_remorse.csv
  • current processed-cache characteristics from the verified build:
    • source: k:\ghidra\crusader_map_viewer\map_renderer\STATIC_PSX
    • scene format version: psx-region01-provisional-art-probe-v2
    • processed maps: 23
    • shared shape definitions: 313
    • shared atlases: 313
    • largest currently useful placement-heavy maps: LSET1/L0 (1050 items), LSET4/L33 (942 items), LSET5/L48 (851 items), LSET6/L51 (463 items), LSET7/L63 (315 items)

Current art-binding hypothesis used by this probe:

  • region-01 u0 is treated as a provisional direct bundle index into the extracted sprite_bundles/ set
  • region-01 u4 was originally treated as a provisional frame index within that bundle, but that interpretation is now considered wrong; the constructor chain instead points to u4 as a state/script selector candidate
  • this is evidence-backed enough to render real PSX art in the existing map renderer, but not strong enough yet to call the binding solved
  • the strongest negative check so far is that the region-01 u5 values (0x20, 0x22, 0x30) do not match the bundle default palette indexes, so the palette-selection/control path is still missing

Current invalidation result:

  • this direct u0 -> bundle index mapping is now considered invalid for real renderer output
  • the produced scene repeats a small set of obviously wrong assets, including portrait/UI-like art, in places that do not make spatial sense for the map
  • executable-side tracing shows that art selection is type-driven through DAT_800758cc/d0/d4/d8 resource tables loaded by level_resource_stream_load, not by directly indexing the raw post_audio_region_04 bundle scan

New loader/data evidence from this pass:

  • post_audio_region_00 now has dedicated extractor diagnostics:
    • out/psx_wdl/L0/post_audio_region_00_00007010_u16x6.csv
    • out/psx_wdl/L0/post_audio_region_00_00007010_u16x12.csv
    • out/psx_wdl/L0/post_audio_region_00_00007010_u32x5.csv
    • out/psx_wdl/L0/post_audio_region_00_00007010_stream_probe.json
  • the new raw probe confirms that post_audio_region_00 begins with a little-endian count value 0x20
  • after an initial short header/preamble, the bytes from about 0x3c onward look like tightly packed 12-byte records in the same broad shape family as the old candidate placement rows:
    • example bytes at 0x3c: 4a 00 03 16 e7 0e 00 00 01 00 20 00
    • little-endian words: 0x004A, 0x1603, 0x0EE7, 0x0000, 0x0001, 0x0020
  • that record family is a better next target than the invalidated direct bundle probe because it already exposes a small type-like word (0x004A) plus coordinate-like words without forcing an arbitrary raw-bundle index

What this renderer pass means now:

  • the live renderer can expose PSX as an optional game only after the processed cache exists; it is no longer tied to ad hoc site exports
  • the current active output is now a provisional real-art probe rather than a placeholder-only type/lane scene
  • the processed-cache path is now compatible with the existing shared reference-data pipeline and PC-style catalog grouping, which keeps PSX integration inside the normal viewer architecture instead of forking it
  • the old real-art probe is still valuable as negative evidence because it proved that direct raw bundle ordering produces obviously wrong scene content

New renderer-grounded improvement from this pass:

  • src/lib/psx-cache.js now scans post_audio_region_04 directly from STATIC_PSX, parses bundle headers in JavaScript, colorizes the extracted frames with the currently available default/heuristic palette path, and writes per-map bundle atlases into .cache/reference-data/psx-remorse
  • the live cache no longer uses only synthetic placeholder shapes for map 0; the current LSET1/L0.WDL scene references 49 real atlases and 62 real sprite frames under the still-provisional direct u0 -> bundle index hypothesis
  • extracted bundle origins are now sanitized on import so bad 0xFFFF offsets do not blow out the scene bounds; LSET1/L0.WDL is back to a sane 3896 x 8431 footprint instead of the broken 67k-pixel-wide intermediate result
  • PSX shape definitions now use a 1x1x1 footprint and the scene items synthesize viewer-compatible world.x/world.y/world.z from the final screen anchors; this keeps bounding-box and preview overlays aligned with the PSX art probe instead of projecting nonsense from the raw u1/u2/u3 words

Current app compatibility notes:

  • the public renderer app was updated so non-FIXED.DAT map sources do not advertise a bogus binary export path
  • for the PSX probe scene, Download Map Binary is intentionally disabled while Download PNG, Download Map JSON, and Download Atlas PNG remain available
  • the static app successfully loads the PSX LSET1/L0 Region 01 Art Probe catalog entry and currently fits it at about 8% zoom instead of the earlier collapsed 2% fit

Immediate implications for the next decode pass:

  • the public renderer integration path is now proven enough to use as a live debug target for PSX map-format work
  • the next priority is to replace the invalidated u0 -> bundle index hypothesis with a real type/resource lookup recovered from level_resource_stream_load
  • post_audio_region_00 is now a top-tier candidate for that work because its new diagnostics expose a count-prefixed preamble plus compact typed records that look more loader-compatible than the old region-01 art probe
  • the palette override path is still the main blocker to correct final color selection even when the bundle/frame choice is plausible
  • once the bundle key and palette control path are recovered, the same scene-export path can graduate from real-art probe to actual PSX map rendering

PSX Provisional Real-Art Probe

The live renderer now prefers a smaller loader-backed record family when it can normalize that family into structured placement rows, while still preserving the older dense region-01 probe as a fallback/debugging strategy.

What changed in this pass:

  • the temporary Python probe established the scene structure, but the active implementation is now renderer-local JavaScript rather than a standalone exporter
  • src/lib/psx-cache.js now reads STATIC_PSX, parses LSET*.WDL, prefers normalized post_audio_region_00 count-prefixed records when they pass the existing structured-candidate filter, falls back to post_audio_region_01 otherwise, scans post_audio_region_04 for sprite bundles, and emits per-map atlases built from the extracted PSX frame data
  • src/build-psx-cache.js writes the resulting processed data into the live cache tree:
    • k:\ghidra\crusader_map_viewer\map_renderer\.cache\psx\catalog.json
    • k:\ghidra\crusader_map_viewer\map_renderer\.cache\reference-data\psx-remorse\reference-data.json
    • per-map scenes under k:\ghidra\crusader_map_viewer\map_renderer\.cache\scene-cache\psx-remorse\...
    • k:\ghidra\crusader_map_viewer\map_renderer\Catalogs\psx_shape_catalog_remorse.csv
  • the viewer now detects psx-remorse from the processed manifest instead of from a fake PC-style source-file heuristic
  • scene items now keep the candidate PSX x/y words directly in world, use the executable-backed projection basis screen_x = y - x, screen_y = 2*z - (x + y)/2 with provisional z = 0, and keep 1x1x1 shape footprints so overlay boxes remain usable without pretending the old PC-style world export is solved

Current verified processed-cache result:

  • scene format version: psx-runtime-record-probe-v1
  • processed maps: 61
  • atlas-backed shapes: 1112
  • atlases: 1112
  • LSET1/L0.WDL preferred source family: post_audio_region_00
  • LSET1/L0.WDL rendered items from the preferred family: 59
  • LSET1/L0.WDL still has 1050 dense fallback post_audio_region_01 records preserved in scene metadata for comparison
  • LSET1/L0.WDL resolved real-art atlases for the preferred family: 18
  • LSET1/L0.WDL resolved sprite frames for the preferred family: 26
  • LSET1/L0.WDL unique u0 types in the preferred family: 18
  • lane split:
    • 0x0020: 26
    • 0x0022: 21
    • 0x0030: 12
  • LSET1/L0.WDL current scene bounds after the runtime-record pass: 1313 x 438
  • LSET1/L0.WDL currently resolves all 59 preferred-family records to real extracted bundles with 0 placeholder fallbacks, but still clamps 15 frame requests down to the highest available extracted frame index
  • one visible viewer mismatch is now separated from the remaining map-format problem: PSX sprites already draw from authored item.screen, but the old highlight/bounding overlay path was still recomputing DOS-style wireframes from provisional item.world; scene-presentation.js now falls back to authored screen rectangles for PSX items instead of drawing those incorrect projected boxes

Why this matters:

  • this is the first live viewer path that prefers a loader-compatible, count-prefixed record family instead of treating the huge dense region-01 stream as the only scene source
  • it keeps the strongest current working assumption narrower and more explicit:
    • normalized post_audio_region_00 rows are now the preferred placement family when they satisfy the same structural checks as the older region-01 records
    • post_audio_region_01 remains a dense fallback evidence source instead of being silently discarded
    • the art lookup is still unresolved and must be recovered from the real runtime resource tables rather than inferred from raw bundle ordering
  • it also moves the viewer one step closer to the executable model by applying the recovered PSX projection basis directly in the cache builder instead of plotting raw u1/u2 values on a pseudo-screen plane

Immediate next consequence:

  • the next map-format batch should treat the processed .cache runtime-record probe as the baseline renderer target and focus on proving exactly how the normalized post_audio_region_00 words line up with the constructor-fed x/y/z fields
  • the old dense region-01 path should stay available as evidence, but it should no longer be the default scene family unless the loader-backed family fails to normalize on a given map
  • that means the remaining visual corruption should now be treated primarily as a placement/schema problem again, not as a box-overlay problem; the next pass needs to recover the authoritative height lane and the exact constructor-fed field mapping instead of spending more time on DOS-style overlay math

PSX Map-System Correction

The current live viewer export was built on the wrong premise. The ~45..59 records currently exported per PSX map are not enough to represent a whole Crusader level, and executable tracing now shows why.

What the loader actually does:

  • wdl_resource_bundle_load_by_index reads the selected LSET*.WDL into multiple section pointers, not one flat placement stream.
  • The first runtime section is a top-level table at DAT_800678f4 whose record stride is 0x18 bytes.
  • The loader iterates that first section with:
    • for each 0x18-byte top-level record
    • type = record[+0x08]
    • dispatch through PTR_PTR_80063118[type]
  • Those dispatch handlers do not behave like a terrain-tile walker. They construct one runtime object or a tiny object cluster at a time through FUN_800249f4, FUN_80024eec, FUN_8003c314, FUN_8003c714, and FUN_8003cc08.

Why the current export is incoherent:

  • the current region00-first exporter is effectively treating that small top-level descriptor family as if it were the whole level
  • those records are only the root nodes of the level bundle's object/resource system
  • they are too few because the bulk level content lives elsewhere in the loaded bundle state

New executable-backed evidence for the missing bulk content:

  • level_resource_stream_load and FUN_8003917c populate the typed runtime resource tables rooted at DAT_800758cc/d0/d4/d8
  • DAT_80067720 is a small top-level 0x18 record list used by object/event-style helpers such as FUN_80031044 and FUN_8002b1a8; it is not a whole-map terrain stream
  • during bundle load, FUN_8003b00c(DAT_8006769c, &DAT_8006b5d8, 0x3e00, 0x3e00) inflates a separate compressed blob into a dedicated level buffer
  • that decompressed buffer is carried through save/load helpers (FUN_8003a0f4, FUN_80049890) independently of the tiny top-level descriptor list, which is exactly what a real map substrate would do
  • the two DAT_80067720 helpers are now clearer about role too:
    • FUN_80031044 scans the 0x18-stride rows for 0xAAAA-tagged entries and low-6-bit selector matches, then caches a pointer to the matched row payload
    • FUN_8002b1a8 mutates matching rows by type/id and flag bits in place
    • both behaviors fit a small event/marker/control list and do not look like whole-map geometry submission
  • the decompressed lane is more clearly persistent substrate/state than before:
    • FUN_8003a0f4 hands DAT_8006769c plus DAT_80067528 to the save helper path
    • FUN_80049890 repacks the DAT_8006b5d8 / 0x3e00 state lane into the 0x4000 memory-card save block
    • this strengthens the read that DAT_8006769c is the saved/restored map-state substrate while DAT_80067720 stays the tiny top-level control list

Current safest read:

  • the ~59 exported records are top-level WDL nodes, not the entire PSX map
  • the real PSX level is split across:
    • a small top-level descriptor stream
    • typed subordinate resource tables
    • at least one separate decompressed level-state blob
  • the viewer looks nonsensical because it is rendering only one small layer of that system and mistaking it for the full map

Immediate consequence for the exporter:

  • stop treating post_audio_region_00 as the default whole-map scene source
  • keep post_audio_region_00 and post_audio_region_01 as evidence sources, but pivot the next decode pass toward the multi-section WDL model recovered from the executable
  • the next map-export target must include the decompressed bundle state and/or the subordinate placement/tile resources behind the top-level 0x18 records, not just the root records themselves

Exporter status after the next renderer pass:

  • the earlier five-region post-audio carve was still wrong for visible-map recovery. The corrected loader-sized section probe shows that the first post-audio section already contains both the count-prefixed top-level descriptor rows and the dense 24-byte bulk placement rows that the flat maps were missing.
  • map_renderer/src/lib/psx-cache.js now recovers visible families from loader-sized post_audio_section_00 instead of treating the old guessed post_audio_region_01 carve as the default bulk source.
  • the exported scene metadata now records those visible families under executable-backed names instead of the old provisional labels:
    • section0_dispatch_roots for the top-level dispatch/root records
    • section0_constructor_placements for the dense constructor-fed placement records
  • a verified full rebuild now exports all 62 PSX maps with large scene volumes and non-flat z stats. LSET1/L0.WDL now emits 1189 items, LSET1/L1.WDL jumps from 53 items to 754, and the rebuilt catalog reports 62/62 maps with section0_dispatch_roots + section0_constructor_placements coverage and uniqueZCount > 1.
  • the renderer-side reference payload no longer emits one atlas per resolved PSX shape. The new packed-atlas pass reduces the shared PSX reference cache from the old 4032 one-shape atlases to 1925 shared packed atlases across the same 4032 shape definitions, and a spot-check on LSET1/L0.WDL now exports the map scene itself with atlasCount = 1 instead of a long per-bundle atlas list.
  • the cache export still carries the parsed DAT_800758d8 candidate section and an offline FUN_8003b00c decode candidate for the compressed source feeding DAT_8006b5d8 -> DAT_8006769c, but the generic raw-file DAT_800758cc/d0/d4 serialization is not currently landing in the live scene cache and should be treated as an open exporter gap rather than a closed layer.
  • this still does not mean the PSX map decode is fully solved: the viewer now has enough volume to represent whole-level candidates across the disc, but the remaining blocker is semantic decoding of the subordinate runtime banks and the separate decompressed 0x3e00 buffer, not record-count starvation.
  • the type-to-art path is only partially improved. The cache builder now scans the parsed per-type art-template payloads for bundle references, and the renderer no longer treats the disproven scan-order u0 -> bundle mapping as trustworthy visible art. Unverified types now stay on placeholder art instead of surfacing known-bad portrait/talk bundles as map geometry.
  • the scan-order fallback is now known to be wrong at the root, not merely incomplete. In the live .cache output, section0_dispatch_roots types 0x0042 and 0x0049 repeatedly bind to portrait/talk-animation bundles such as map 0 type 0042 -> offset 0x000B2970 and map 0 type 0049 -> offset 0x000D84F4, with the same failure pattern continuing through early maps. Those portrait bundles are useful negative evidence: they show the top-level dispatch rows are generic object/state descriptors, not a direct map-graphics stream that can be paired to bundle order.

Next decoded runtime layers from the constructor pass:

  • DAT_800758d8 is the per-type art/template bank, not the missing whole-map substrate. wdl_resource_bundle_load_by_index populates it from an 8-byte descriptor table, and both FUN_800249f4 and FUN_80024eec consume it before calling FUN_80044434 through the loader-side helper path.
  • DAT_800758d0 is a per-type companion/component bank for the simpler constructor family. FUN_800249f4 copies the resolved pointer from that bank into the local object payload at obj->8->[0,4], so this looks like a per-type component/template block rather than a top-level placement stream.
  • DAT_800758cc is a per-type offset-table bank for the compound constructor family. FUN_80024eec stores it at obj+0x88, and FUN_800260e8 later indexes it with the placement byte at record+0x08 to resolve a state/offset subrecord into obj+0x8c/0x90.
  • DAT_800758d4 is another per-type companion bank for the compound constructor family. FUN_80024eec stores it at obj+0x84, and FUN_8002841c queries it later using the object's +0x94 selector, so it behaves like a variant table or companion lookup rather than raw map geometry.
  • The key functions in that chain are now renamed in the live PSX Ghidra database:
    • FUN_800249f4 -> psx_object_create_simple_record
    • FUN_80024eec -> psx_object_create_compound_record
    • FUN_80025ce8 -> psx_reset_type_runtime_banks_from
    • FUN_80025d68 -> psx_object_advance_state_script
    • FUN_800260e8 -> psx_object_select_state_script
    • FUN_8002841c -> psx_object_lookup_variant_entry
    • FUN_8003917c -> psx_load_type_state_banks
    • FUN_80044434 -> psx_create_image_resource_from_descriptor
    • FUN_80045ffc -> psx_cache_type_art_descriptor
  • the constructor/runtime chain is now clearer too:
    • psx_reset_type_runtime_banks_from is a bank reset helper used during init/recycle paths; it clears DAT_800758c4/c8/cc/d0/d4/d8 from the requested type index upward and is not the state interpreter itself.
    • psx_object_create_simple_record and psx_object_create_compound_record are two placement constructors for different section-0 row layouts, but both index the same per-type runtime banks by type id before any final render-facing selection is made.
    • psx_create_image_resource_from_descriptor turns the DAT_800758d8 per-type descriptor into a renderable resource/header object; this is why DAT_800758d8 should be read as an art/template descriptor bank, not as a whole-map tile layer.
    • psx_object_select_state_script selects a state or animation subrecord from DAT_800758cc using a placement byte (record+0x08 in the compound family), storing the resolved script/state pointer at obj+0x8c/0x90 and the selector at obj+0x9e.
    • psx_object_advance_state_script then interprets the active state script with sentinel/control values such as 0xffff, 0xfffe, 0xfffd, 0xfffc, and 0xfffb, so the visible frame path is explicitly state-driven rather than just "type id -> one bundle".
    • The current renderer-side consequence is important: section-0 word u4 is no longer treated as a verified sprite-frame index. It is now carried forward as a state-selector candidate in exported scene metadata until the DAT_800758cc/d4 path is decoded far enough to pick the right animation frame from executable evidence.
    • Current strongest sentinel read:
      • 0xfffe dispatches FUN_8004061c, which is an audio/effect helper rather than a visible-frame selector.
      • 0xfffd is an in-script jump/re-anchor control that rewrites obj+0x90 relative to the current script base.
      • 0xfffc switches obj+0x8c/0x90 to another subsidiary script selected through the DAT_800758cc offset table.
      • 0xfffb also switches into a subsidiary script, but first scans forward to an in-script 0xfffd marker before choosing the destination entry.
    • Current best read of those sentinels:
      • 0xffff marks a terminal or restart control that re-anchors the script at obj+0x8c and raises object-state flags.
      • 0xfffe dispatches a side-effect helper (FUN_8004061c) using the following word as a parameter before advancing.
      • 0xfffd, 0xfffc, and 0xfffb switch into subsidiary scripts through the DAT_800758cc offset table rooted at obj+0x88.
    • psx_object_lookup_variant_entry finally uses obj+0x94 to look up a companion entry in DAT_800758d4, which means even after construction the art-facing choice is still mediated by per-type variant/state tables.
  • This means the next PSX layers are now at least structurally separated:
    • visible root descriptors (section0_dispatch_roots, legacy alias region00)
    • visible bulk placement candidates (section0_constructor_placements, legacy alias region01)
    • per-type art/template descriptors (DAT_800758d8)
    • per-type simple-object component blocks (DAT_800758d0)
    • per-type compound state-offset tables (DAT_800758cc)
    • per-type compound variant tables (DAT_800758d4)
    • the still-separate decompressed 0x3e00 level-state buffer (DAT_8006769c)
  • The current renderer pass now records those banks explicitly as exported scene/state layers, while still only rendering the first two as visible scene items.
  • Immediate map-viewer consequence: the current fallback art probe should be treated only as a diagnostic overlay for candidate bundle families. A workable renderer will need to recover the per-type DAT_800758d8 descriptor mapping and the downstream DAT_800758cc/d4 state+variant selection path before it can decide whether a section-0 placement should show world geometry, an animated object, or something non-map-facing like a portrait/talk asset.
  • The next loader-side correction is now verified in the live cache too: the effective late LSET*.WDL DAT_800758d8 candidate is not the earlier small-section heuristic, but a large late section whose working descriptor stream begins at an embedded +0x38 offset. On retail map 9 that correction alone lifts bundleMappedItemCount from 0 to 111, which is enough to restore real bundle-backed art for a first subset of types without reintroducing the disproven scan-order fallback. The still-unresolved root-dispatch families remain instructive rather than contradictory. 0x0042 and 0x0049 still stay on placeholders after the bank-selection fix, but the same pass now decodes their DAT_800758cc state rows more cleanly: type 0x0042 carries three selector-targeted scripts (0, 1, 2) that all terminate through 0xffff, while type 0x0049 carries a single selector-0 script. So the remaining blocker for those roots is no longer "find any late template bank at all"; it is the deeper DAT_800758cc/d4 state-to-visible-art bridge. A first renderer-safe bridge landed even with that exporter gap still open: the verified 0x0050 state-script mapping (selector 0..3 -> frame 0..3) is now applied as a narrow fallback in the cache builder, and the rebuilt live map-9 scene now shows type=80 state_selector=1 chosen_frame=1 instead of the old forced chosen_frame=0. Unresolved fallback placeholders are also now clamped to opacity=0.45 in live scene output so the still-missing families stop visually overpowering the recovered real art. This remains intentionally scoped: the fallback frame map only covers the one family with direct executable-backed frame evidence, and the opacity clamp is diagnostic relief rather than a decoding claim. The current draw split is clearer too. FUN_80041378 is a three-stage render pass:
    • stage 2: a second special-visible list drawn by FUN_80041144
    • stage 3: HUD/overlay/icon primitives from FUN_800416cc
  • That split matters for the map-viewer target: stages 1 and 2 remain relevant to missing world-facing content, while stage 3 is mostly front-end or overlay material and should not be mistaken for the missing half of the map.
  • Stage 2 is now materially better understood and is no longer just a read-side observation:
    • FUN_80040f78 is the queue-builder for that pass. It projects an object with the same fixed-point world-to-screen math as FUN_80040d44, writes the final screen rectangle to +0x20..+0x2e, then appends the object to DAT_80078b70 and increments DAT_80067472.
    • FUN_80041144 consumes that queue directly, iterating DAT_80078b70[0 .. DAT_80067472) and submitting sprite primitives through the same texture draw helpers as the main object pass.
    • FUN_80044fec resets the queue each frame by clearing DAT_80067472 after the top-level draw pass.
    • So the stage-2 list is not UI/HUD noise and not a duplicate of the main clipped visible list. It is a distinct world-facing queued-object lane, which is now a concrete candidate explanation for part of the still-missing map content in the viewer.
  • The immediate caller-side consequence matters too:
    • FUN_80040d44 remains the main clipped visible-list toggle, calling the stage-1 add/remove helpers when an object enters or leaves the screen.
    • The recovered post-state-advance updater family now splits into five visible call sites: 0x80012b44, 0x80013524, 0x80013564, 0x80013650, and 0x80013778 all call psx_object_advance_state_script.
    • Three of those sites then feed the main stage-1 projector path through FUN_80040d44 (0x80012b60, 0x8001357c, 0x800136d4), while two feed the stage-2 queue-builder path through FUN_80040f78 (0x8001352c, 0x80013780).
    • That exact 3 versus 2 split matters because it tightens the earlier claim: stage-2 membership is tied to a narrower runtime object/state branch after state advance, not to the decompressed substrate buffer alone and not to all state-advanced objects indiscriminately.
  • One state-script sentinel is now functionally closed too: 0xfffe dispatches FUN_8004061c, which is an audio/effect helper rather than a visible-frame selector. That shrinks the unknown sentinel set for the remaining DAT_800758cc script work.
  • The main visible-list helpers are now also separated cleanly enough to stop treating them as a blocker:
    • FUN_8002d240 adds an object to the stage-1 DAT_8006ad5c visible-list array.
    • FUN_8002d35c removes an object from that same array.
    • FUN_8002d59c returns the sorted slice that FUN_80041378 iterates for the stage-1 world-object pass.
    • FUN_8002d6f8 and FUN_8002d778 act as refresh/rebucket/sort helpers over that main list.
  • This is an important scope reduction for renderer work: the remaining missing world content is now less likely to be caused by misunderstanding the main stage-1 visibility array itself, and more likely to live in the separate stage-2 queued-object pass plus the still-unresolved DAT_800758cc/d4 state-to-art path.

Recovered next visible layer from the bulk placement family:

  • The structured section0_constructor_placements rows are no longer height-agnostic. The FUN_80024eec constructor reads its authored elevation from byte +0x06 of the input record, which corresponds to the low byte of the current exported u3 word for the accepted bulk-placement records.
  • That byte is not just random payload on the accepted rows. Under the corrected section-0 scan, the same ladder generalizes across the whole rebuilt catalog instead of only the earlier L0 subset. LSET1/L0.WDL still collapses to 11 distinct height values (0, 2, 4, 10, 12, 14, 18, 20, 22, 24, 26), and LSET1/L1.WDL now exposes 9 distinct levels with a z range of 0..32.
  • The PSX cache builder now uses that recovered z byte for section0_constructor_placements projection instead of forcing the whole bulk layer onto z = 0, while the top-level section0_dispatch_roots descriptor stream stays at z = 0 until its own constructor-backed height source is proven.
  • This is now the first PSX export pass in the viewer pipeline that produces visibly multi-layer whole-map candidates across the rebuilt retail catalog from executable-backed height data rather than from a single flattened candidate layer.

PSX Coordinate Model From Executable

The current coordinate problem is no longer a renderer-only guess. The executable now closes the last projection step well enough to treat PSX placement as its own map-space model instead of as a PC-style direct world export.

Key function evidence:

  • FUN_800249f4 and FUN_80024eec are constructor paths that load authored coordinates into object fields +0x3c, +0x40, and +0x44 as 16.16 fixed-point values.
  • For the first family, the source record shape is now strong enough to describe directly:
    • u16 word at record +0x08 -> object +0x3c as value << 16
    • u16 word at record +0x0a -> object +0x40 as value << 16
    • u8 byte at record +0x0c -> object +0x44 as value << 16
  • FUN_80040d44 and FUN_80040f78 are the projection helpers that turn those fixed-point object coordinates into the per-object screen rectangle stored at +0x20..+0x2e.
  • FUN_80041458 and FUN_80041144 then consume that already-built rectangle directly during draw submission; they do not derive screen position on the fly.

Recovered projection model:

  • +0x3e and +0x42 are not separate authored fields. They are the high 16-bit halves of the fixed-point x and y values stored at +0x3c and +0x40.
  • The runtime builds an intermediate screen anchor in fixed-point at +0x78/+0x7c from those world coordinates:
    • screen_anchor_x = y - x
    • screen_anchor_y = 2 * z - (x + y) / 2
  • FUN_80040d44 computes that anchor with the exact writes:
    • obj+0x78 = ((y_hi - x_hi) << 16)
    • obj+0x7c = (obj_z * 2) - ((x_hi + y_hi) << 15)
  • The projection helper then subtracts the current camera anchor from DAT_800678d4 + 0x3c/+0x40, subtracts sprite-frame origin/size metadata from FUN_8004513c, FUN_800451d0, FUN_80045014, and FUN_800450a8, and writes the final visible rectangle into +0x20..+0x2e.

What this means for the viewer:

  • the PSX map does not want the PC viewer's current synthetic world.x/world.y/world.z guess based directly on raw candidate words
  • the most defensible renderer-side export target is now the runtime's own projected anchor or the equivalent fixed-point world tuple that reproduces the same screen_anchor_x/screen_anchor_y formulas
  • any importer that treats the raw authored coordinates as if they were already PC-style isometric world coordinates will bunch objects together or smear them across the map because PSX uses a different projection basis
  • the current cache builder no longer synthesizes PC-style world coordinates from final screen anchors; it now keeps the candidate PSX x/y words directly in exported scene items and applies the runtime projection basis separately during anchor generation

Open parts that still matter:

  • this closes the final world-to-screen math, but it does not yet prove which raw post_audio_region_01 or post_audio_region_00 record family feeds each constructor path
  • it also does not close the type/resource lookup that selects the correct bundle/frame through DAT_800758cc/d0/d4/d8
  • palette override remains a separate unresolved control path layered on top of the now-understood projection math

Immediate consequence for the next pass:

  • the next executable-guided decode step should map candidate authored record words directly onto constructor inputs, not onto PC-style scene coordinates
  • once the correct record family is tied to FUN_800249f4 or FUN_80024eec, the renderer can export either:
    • the raw fixed-point PSX world tuple, plus a viewer-side reproduction of the runtime projection, or
    • the runtime-equivalent projected anchor/rectangle directly for debug rendering
  • the cache builder now uses the recovered projection basis and prefers the loader-backed record family, but the exact record-to-constructor link and the authoritative height lane still need proof before this can be called a solved map export

PSX Script / Usecode Equivalent

Current status:

  • there is no evidence yet that the PSX build carries the exact same external USECODE/EUSECODE.FLX style asset pipeline used by the DOS version
  • the current PSX executable-backed work has mostly exposed compiled resource loaders, animation/audio handlers, and image upload/decode paths rather than a separate obvious bytecode container

Current working question:

  • the likely PSX equivalent, if one exists, may be either:
    • compiled gameplay logic directly inside SLUS_002.68, or
    • a separate embedded event/script resource format inside the LSET/other disc blobs that is not yet isolated

Immediate plan:

  1. scan the PSX executable and current renamed function set for script/event-dispatch terminology or obvious VM-style control loops
  2. compare any candidate dispatch path against the DOS usecode model only at the behavioral level, not by assuming the asset format is shared
  3. keep this as a secondary track while map decoding takes priority

Practical Extraction Paths

Standard media first

The easiest wins are the standard PS1 media formats:

  • MOVIES/*.STR: treat as PS1 video streams
  • AUDIO/*.XA: treat as XA audio
  • ZZZ.ZZZ: try as a movie stream too, especially against FMV3.STR

This does not need custom reverse engineering first.

Custom .WDL extraction second

The .WDL files are the main custom-content frontier.

Current executable-backed extraction order:

  1. run tools/psx_extract_wdl.py over representative LSET*.WDL files
  2. treat post_audio_region_01 and post_audio_region_02 as the current best map-data extraction targets
  3. treat post_audio_region_04 as the current best sprite/graphics extraction target
  4. carve any strict TIM blocks first, because those now have executable support via the type 4 / type 5 image handlers
  5. separately carve SPEC_A.WDL / MENUS/*.WDL as raw image-oriented blobs

The level files and menu/special files should not be assumed to share one parser until that is proven.

Primary

  1. E:\emu\psx\Crusader - No Remorse\SLUS_002.68

Reason:

  • confirmed by SYSTEM.CNF
  • valid PS-X EXE
  • main native code image

Secondary, only if useful for subsystem RE

  1. E:\emu\psx\Crusader - No Remorse\FMV.BIN

Reason:

  • clearly tied to FMV playback
  • contains path and MDEC-related strings
  • could be worth importing as a raw binary/data blob if the movie subsystem becomes a target

Not primary code imports

These currently look like content, not executables:

  • E:\emu\psx\Crusader - No Remorse\ZZZ.ZZZ
  • E:\emu\psx\Crusader - No Remorse\SPEC_A.WDL
  • E:\emu\psx\Crusader - No Remorse\LSET1\L0.WDL
  • E:\emu\psx\Crusader - No Remorse\MENUS\M13.WDL

They may still be worth loading as raw binaries later for format RE, but they are not first-choice code imports.

Current Working Model

  • SLUS_002.68 = main PS1 executable
  • FMV.BIN = FMV helper/support blob
  • MOVIES/*.STR = standard movie streams
  • AUDIO/*.XA = standard XA audio
  • ZZZ.ZZZ = likely renamed or duplicated movie stream data
  • LSET*.WDL = structured level/resource containers
  • MENUS/*.WDL and SPEC_A.WDL = raw-looking screen/menu resource blobs, possibly with some embedded standard PS1 image content

Executable Catalog Findings

This batch focused on the imported SLUS_002.68 executable as a catalog source rather than on the raw WDL bundles alone.

Map inventory and mission-facing structure

Current executable-backed map findings:

  • wdl_resource_bundle_load_by_index now has a direct string-backed proof for the shipped folder layout. The loader copies one of seven hardcoded path prefixes \LSET1\L through \LSET7\L based on map-index thresholds 10, 20, 30, 40, 50, and 60, then formats the final .WDL path.
  • The extracted disc tree currently ships 62 level bundles total:
    • LSET1: L0 through L9
    • LSET2: L10 through L19
    • LSET3: L20 through L29
    • LSET4: L30 through L39
    • LSET5: L40 through L49
    • LSET6: L50 through L58
    • LSET7: L62 through L64
  • So the shipped PSX map-bundle range is L0..L64 with a real on-disc gap at L59..L61.
  • The executable also preserves only 15 plain-text Mission Briefing ^Mission N strings, for Mission 1 through Mission 15.

Current safest read:

  • the PSX disc contains 62 shipped map/resource bundles used by the LSET loader
  • the player-facing campaign/briefing flow exposed by the executable is 15 numbered missions
  • any extra bundle coverage beyond that mission-facing set is currently better treated as lower-level map/resource inventory, not automatically as 15 == all shipped WDLs

Per-bundle shipped inventory from the extracted disc tree:

Bundle range Folder Count Size range (bytes)
L0..L9 LSET1 10 987,932 .. 1,312,624
L10..L19 LSET2 10 1,107,380 .. 1,314,992
L20..L29 LSET3 10 904,384 .. 1,221,556
L30..L39 LSET4 10 1,104,316 .. 1,321,656
L40..L49 LSET5 10 1,120,084 .. 1,303,732
L50..L58 LSET6 9 1,012,956 .. 1,341,684
L62..L64 LSET7 3 965,072 .. 1,150,428

Passcodes and password-screen cheat status

Current executable-backed passcode findings:

  • The mission-complete passcode display path at 80022cd4 and 80022f1c synthesizes a 4-character code from generated indexes.
  • Those indexes are mapped through the hardcoded alphabet at 80063ef0:
BCDFGHJKLMNPQRSTVWXZ0123456789
  • The resulting 4 characters are written into the temporary display buffer at 80063f6e..80063f71, null-terminated at 80063f72, and shown through the completion message at 80063f10:
^Congratulations!^ You have completed your mission.^^The passcode for the next mission is:^   
  • So PSX mission passcodes are definitely real executable-generated 4-character values, not just external manual text.

Current best password-screen cheat list from public PSX references:

  • XXXX = hidden pictures
  • L0SR or L0SER = cheat-mode password reported by public sources; the conflicting transcription is almost certainly a 0 vs O issue and is not yet closed directly from the executable

Important executable-side caveat:

  • none of the known public PSX mission passwords checked in this pass (FWQP, HWQP, LRTN) appear as plain ASCII strings inside SLUS_002.68
  • the same is true for the public cheat-password candidates XXXX, L0SR, and L0SER
  • current safest read is therefore password entry and/or validation is numeric or transformed, not a plain embedded string table of passcodes
  • this pass closed the visible generation/display side, but it did not yet directly close the hidden cheat-password compare path

Weapons and items

The executable does preserve user-facing text tables for equipment.

Recovered ammo names:

  • INVALID AMMO
  • JL-2 AMMO
  • AR-7 AMMO
  • GL-303 AMMO
  • RP-22 AMMO
  • SG-A1 AMMO

Recovered item names:

  • NULL ITEM
  • INHIBITOR
  • CREDITS
  • SCI PLANS
  • BLAST PAC
  • DET PAC
  • DATA LINK
  • LAND MINE
  • SPIDER BOMB
  • MEDICAL KIT
  • ENERGY CUBE
  • FUSION PAC
  • CHEMICAL BATTERY
  • FISSION BATTERY
  • FUSION BATTERY
  • GRAVITON GENERATOR
  • IONIC GENERATOR
  • PLASMA GENERATOR

Recovered weapon names:

  • RP-16
  • RP-22
  • RP-32
  • SG-A1
  • AC-88
  • PA-31
  • EM-4
  • PL-1
  • UV-9
  • GL-303
  • AR-7
  • JL-2
  • JL-9

Current safest read:

  • these are real executable-backed display-name tables, not guessed carryovers from the DOS build
  • the PSX build still uses a recognizable Crusader equipment taxonomy even where some item labels differ from the more familiar DOS-side vocabulary

JL-2 / JL-9 follow-up:

  • neither JL-2 nor JL-9 appears in the known DOS Weapon_GetNameForShapeNo tables already extracted in this repo for retail Remorse or Regret; those tables stop at the older DOS weapon families such as BA-40, BA-41, PA-21, EM-4, SG-A1, RP-22, RP-32, AR-7, GL-303, PA-31, PL-1, AC-88, UV-9, and the Regret-only additions BK-16, LNR-81, XP-5
  • that makes JL-2 and JL-9 strong PSX-only naming additions rather than inherited PC names
  • JL-2 is also the only one of the two with an explicit PSX ammo label (JL-2 AMMO) in the nearby executable text table, while no matching JL-9 AMMO string has been recovered
  • the extracted PSX pickups_and_weapons sprite category contains repeated weapon-pickup art across a large spread of maps, but this pass still does not have a defensible sprite-to-name mapping for specific JL-2 or JL-9 pickup appearances

Enemies

This pass did not recover a comparable plain-text enemy-name table from SLUS_002.68.

What is closed:

  • the PSX executable has clean user-facing text for mission briefings, passcode UI, ammo, items, and weapons
  • the same executable does not expose an equally obvious plain-text enemy catalog in its main printable-string regions

Current safest read:

  • enemy identities in the PSX build are probably carried primarily as numeric resource/type ids, spawn tables, or script/resource references rather than as a direct display-name list
  • the next enemy-focused pass should start from enemy spawn/type dispatch or resource-stream type tables, not from more blind string hunting

Highest-Value Next Steps

  1. Run tools/psx_extract_wdl.py over more LSET*.WDL samples and compare whether the high-offset region pattern stays stable across level sets.
  2. Recover the password-entry validation path directly so the hidden PSX cheat-password compare logic can be proven from code instead of only cross-referenced from public password lists.
  3. Focus map decoding on post_audio_region_01 and post_audio_region_02, starting with table structures, coordinate ranges, and repeated record widths.
  4. Focus sprite/graphics decoding on post_audio_region_04, including more aggressive TIM validation and possible packed-image expansion.
  5. Recover the exact type IDs consumed by level_resource_stream_load so the sprite/image resource records can be labeled more precisely.
  6. Compare carved post_audio_region_04 image assets against on-screen level graphics to separate sprite sheets from tiles.
  7. Run the raw-blob fallback across MENUS/*.WDL to identify which menu files contain usable embedded TIM data and which are likely packed 15-bit images.