1158 lines
95 KiB
Markdown
1158 lines
95 KiB
Markdown
# 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:
|
|
|
|
```ini
|
|
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:
|
|
|
|
```text
|
|
\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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```powershell
|
|
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 now partially landed in the viewer/exporter too: the cache builder applies the executable-backed authored override byte when the source record exposes the proven `+0x06` / `+0x0c` lane, so the remaining blocker is the cases where the runtime first picks a different object/variant/state than the current export model
|
|
- 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 per-level file header is now structurally tighter too: after the initial `0x38` read, the loader treats the first nine dwords as contiguous section sizes, allocates a separate `0x50` runtime-header block at `DAT_80067794`, and later reads one more independently allocated compressed source blob before the optional `FUN_8003b00c(..., 0x3e00, 0x3e00)` inflate.
|
|
- The first runtime section is a top-level table at `DAT_800678f4` whose record stride is `0x18` bytes.
|
|
- The second runtime section at `DAT_80067720` is also `0x18`-stride, but it is not just a duplicate alias of the first pointer. The loader assigns the contiguous level sections in this order:
|
|
- `DAT_800678f4 = section[0]` root dispatch table
|
|
- `DAT_80067720 = section[1]` secondary `0x18`-stride control/placement table
|
|
- `DAT_800678f0 = section[2]`
|
|
- `DAT_80067938 = section[3]`
|
|
- `DAT_80067838 = section[5]`
|
|
- `DAT_800675f8 = section[6]`
|
|
- `DAT_8006754c = section[7]`
|
|
- `DAT_80067840 = section[8]`
|
|
- `DAT_800676d8 = section[9]` palette block loaded just before `level_palette_header_apply`
|
|
- The skipped header slot is not dead space. The loader allocates it separately as `DAT_8006767c`, then feeds it to `FUN_80040768`; that lane stays distinct from the contiguous section pack and from the later `DAT_8006b5d8 -> DAT_8006769c` decompression path.
|
|
- 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 now carries more than the parsed `DAT_800758d8` candidate section. In the current `psx-runtime-record-probe-v6` scene path, `map_renderer/src/lib/psx-cache.js` serializes `DAT_800758cc`, `DAT_800758d0`, `DAT_800758d4`, and the offline `FUN_8003b00c` decode candidate for `DAT_8006b5d8 -> DAT_8006769c` into `stateLayers`, and the scene writer preserves those layers in both scene metadata and `mapSource`.
|
|
- the new typed-section-16 discovery path is also broader than the earlier section-start probe: when no parsed-section candidate wins, the cache builder now falls back to an absolute-file scan, which is why the late compound-bank blobs can now land in the export even when their serialized source does not start exactly at a pre-labeled section boundary.
|
|
- the file-side header block now separates more cleanly too: `FUN_80039c40` allocates a `0x50` level runtime-header block at `DAT_80067794`, and `FUN_80039dc4` copies that block into globals such as `DAT_80078ab0`, `DAT_80078a88`, `DAT_80078a8c`, `DAT_80078a4c`, and `DAT_80067354` before calling `FUN_80042ec4`. So the first visible/root sections are not the only authoritative level metadata; the loader also applies a dedicated `0x50` per-level runtime header after the optional `0x3e00` decode succeeds.
|
|
- 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 raw-file provenance for those banks is tighter now too. Inside `wdl_resource_bundle_load_by_index`, `psx_load_type_state_banks` is called four times total: twice before the selected `LSET*.WDL` opens and twice again after the level-local section pack and palette/header lanes are loaded. The standalone `8`-byte descriptor-table read that assigns `DAT_800758d8` sits between the second pair of `psx_load_type_state_banks` calls. Current best read:
|
|
- one common/shared bank pair loads before the map-local WDL opens
|
|
- one map-local override pair loads after the level-local header and palette lanes
|
|
- `DAT_800758d8` definitely comes from its own late descriptor stream
|
|
- `DAT_800758d0` plus `DAT_800758cc/d4` are loaded through the adjacent `psx_load_type_state_banks` blobs rather than from the root section tables or the decompressed `0x3e00` state buffer
|
|
- 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".
|
|
- `psx_object_lookup_variant_entry` is not only a constructor-time helper. Its call graph now shows three direct consumers: `psx_object_create_simple_record`, `psx_object_create_compound_record`, and `psx_object_advance_state_script`. That means unresolved families cannot be modeled as one spawn-time `type -> variant` choice; the visible companion bytes can be recomputed after state-script control flow advances.
|
|
- 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` resolves a fresh subsidiary script base from the table rooted at `obj+0x88`, then immediately swaps both `obj+0x8c` and `obj+0x90` to that destination before continuing from the first record there.
|
|
- `0xfffb` also resolves a subsidiary script from the same table, but first walks the current script forward until it finds an in-band `0xfffd` marker and then uses that marker's selector word to choose 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` is the direct indexed jump control within the current script family.
|
|
- `0xfffc` and `0xfffb` are both subsidiary-script switches through the `DAT_800758cc` offset table rooted at `obj+0x88`, but `0xfffb` is specifically the scan-forward variant that consumes the next in-band `0xfffd` selector.
|
|
- because `psx_object_advance_state_script` calls `psx_object_lookup_variant_entry` after those control-flow steps, the visible art choice for unresolved types may depend on post-jump script state rather than on the placement selector byte alone.
|
|
- `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.
|
|
- The live viewer now keeps those heavyweight bank dumps out of the hot interactive scene state after load: `stateLayers` and `decodedRuntimeLayers` are preserved for exported scene JSON, but the runtime pan/zoom/editor path uses a trimmed scene object so normal navigation does not keep dragging the full PSX research blobs through the controller.
|
|
- The renderer-side state parser is also stronger than the earlier flat selector map. `buildTypeStateFrameMaps` now reads the exported `DAT_800758cc` layer and follows the currently verified control-flow sentinels (`0xfffe`, `0xfffd`, `0xfffc`, `0xfffb`) instead of just taking the first word of each selector entry. That does not fully close type `0x0042` or `0x0049`, but it does mean the viewer can now distinguish more fallback states from real script structure rather than from a blind `selector -> first frame` heuristic.
|
|
- The live cache now tightens the selector provenance too. In exported PSX scene items, the `state_selector=...` label is written directly from raw word `u4`, while the trailing `lane=...` label is raw word `u5`; this is not a post-hoc heuristic. A full scan of the built `psx-remorse` scene cache found `3944` visible `type=0x0042` placeholders across `61` maps, and the observed selectors track `u4` exactly (`0 -> 0x0000`, `1 -> 0x0001`, `2 -> 0x0002`, `3 -> 0x0003`, `4 -> 0x0004`).
|
|
- The executable-side handoff is narrower than that label might suggest. `psx_object_select_state_script` stores the authored selector byte in `obj+0x9e` and uses it only to choose the initial `DAT_800758cc` script base at `obj+0x8c/0x90`; `psx_object_lookup_variant_entry` does not index `DAT_800758d4` by `obj+0x9e`. It indexes by `obj+0x94`, which `psx_object_advance_state_script` refreshes from the current script entry after control-flow handling.
|
|
- 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.
|
|
- Current status note: even with the recovered placement/projection path and the first subset of real bundle-backed types, the live PSX map output is still unreadable as a practical map because most section-0 placements still miss the executable's final state/variant-driven art binding and therefore collapse back to placeholders.
|
|
- 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. The built scene cache shows that this is still not the whole art-facing discriminator: `type=0x0042` placeholders now appear with selectors `0..4`, and the higher selectors `3` and `4` are real exported cases rather than parser noise. Verified maps with `0x0042` selectors above `2` include `map-4`, `map-5`, `map-8`, `map-45`, `map-69`, and `map-85`.
|
|
Two runtime reselection paths now explain how those higher selectors can arise without contradicting the earlier three-script file read. `FUN_80028c94` and `FUN_8002906c` both recompute the active script with `psx_object_select_state_script(obj, FUN_8003bc1c(obj) >> 2 & 0xf)`, where `FUN_8003bc1c` quantizes the object's current motion vector at `obj+0x60/+0x64` through `FUN_8003b980` into a 16-way heading bucket. So cache-visible `0x0042` selectors `3` and `4` can come from runtime heading/state reselection, not only from the original placement byte.
|
|
That cache sweep also separates selector from lane more clearly than before. `0x0042` appears heavily on lanes `0x0020` and `0x0022`, and there are also map-local `lane=0x0030` cases (for example large clusters on `map-108`) that still export `state_selector=0`. So the unresolved bridge is narrower now: the visible-art rule cannot be modeled as just `u5` or just the initial `DAT_800758cc` selector parse. The remaining unknown is the downstream interaction between `u4`/`obj+0x9e`, the active state-script pointer at `obj+0x8c/0x90`, and the `DAT_800758d4` companion lookup that reruns after state-script advancement.
|
|
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.
|
|
|
|
Additional constructor-backed coordinate grounding from the current pass:
|
|
|
|
- the current constructor split is now precise enough to state both authored row layouts at once:
|
|
- `psx_object_create_compound_record` reads `type` from `record+0x00`, `x/y` from `u16` words at `+0x02/+0x04`, authored elevation from byte `+0x06`, flags from `+0x0a`, and the initial state selector from byte `+0x08`.
|
|
- `psx_object_create_simple_record` reads `type` from `record+0x04`, `x/y` from `u16` words at `+0x08/+0x0a`, authored elevation from byte `+0x0c`, and the initial state selector from byte `+0x0e`.
|
|
- both constructors write the original authored source-record pointer to object field `+0xa0`, which keeps the later palette-override and state-selection paths tied to raw placement bytes rather than to a hidden runtime copy.
|
|
- both constructors also resolve the same per-type banks before any render pass runs:
|
|
- `DAT_800758d8` -> art/template descriptor
|
|
- `DAT_800758cc` -> state-script offset table
|
|
- `DAT_800758d4` -> companion variant lookup
|
|
- `DAT_800758d0` -> simple-record-only component payload
|
|
- this strengthens the viewer/exporter rule again: the two visible section-0 row families are constructor inputs, not already-final render primitives. Any direct `type -> bundle/frame` export path still has to respect the later state-script and variant lookups.
|
|
|
|
Recovered per-level runtime-header lane:
|
|
|
|
- `FUN_80039c40` is now confirmed as a pure `0x50` allocator for `DAT_80067794`, and `FUN_80039dc4` is the matching applier for that block.
|
|
- `FUN_80039dc4` copies fixed fields from `DAT_80067794` into the active level globals, including camera/runtime anchor values and several per-level mode bytes, then calls `FUN_80042ec4` to refresh dependent runtime state.
|
|
- The downstream call graph narrows that lane further: `psx_apply_level_runtime_header_block` is the only loader-side caller that feeds those values from WDL data, while `FUN_80042ec4` is also reused by `psx_input_device_init`, `memory_card_menu_tick`, and one additional front-end/system path. Current safest read: `DAT_80067794` is shared per-level runtime mode or presentation state rather than hidden bulk map geometry.
|
|
- Practical exporter consequence: keep the `DAT_80067794` fields as a first-class raw metadata lane, but do not treat them as a missing placement stream. They are more likely to affect camera/runtime modes, screen-space behavior, or level-global toggles than to supply extra map cells directly.
|
|
- The higher-level level lifecycle is now readable too. `psx_level_session_loop` is the outer level-session loop: it loads the selected WDL through `wdl_resource_bundle_load_by_index`, applies shared overlay/resource setup through `FUN_800388a8`, resets a small per-level step-flag block with `FUN_8003a498`, and then runs `psx_world_frame_tick` as the per-frame world loop until the current level session exits.
|
|
- `wdl_resource_bundle_load_by_index` is now mapped tightly enough for viewer work. Its effective order is: load `SPEC_A.WDL` and shared type art/state banks; open the selected `LSET*.WDL`; read the `0x38` section-size header; lay out the contiguous per-level section pack at `DAT_800678f4`, `DAT_80067720`, `DAT_800678f0`, `DAT_80067938`, `DAT_80067838`, `DAT_800675f8`, `DAT_8006754c`, `DAT_80067840`, and `DAT_800676d8`; load the detached `DAT_8006767c` blob; optionally inflate `DAT_8006b5d8` into `DAT_8006769c`; apply the runtime header at `DAT_80067794`; then dispatch the `0x18`-stride root records at `DAT_800678f4` through the per-type function table in `PTR_PTR_80063118`.
|
|
- The per-frame world loop in `psx_world_frame_tick` is now split clearly enough for renderer planning. In the normal in-level branch it ticks existing live objects through `psx_run_live_object_type_updates`, instantiates or refreshes nearby authored records through `psx_dispatch_section0_dispatch_roots` and `psx_dispatch_section0_constructor_placements`, runs per-object behavior callbacks through `psx_run_live_object_behavior_callbacks`, integrates world/player motion and active-object state through `FUN_80029de0`, updates queued transient resources through `FUN_8002aed0`, and only then submits the draw pass through `FUN_80041378`.
|
|
- The two authored record-family passes now line up directly with the viewer exporter model:
|
|
- `psx_dispatch_section0_dispatch_roots` walks the `DAT_80067720` `0x18`-stride family plus the fixed-size entries at `DAT_80067658`, culls them to roughly a `+/-0x140` neighborhood around the current focus object, and dispatches their per-type handlers. This is the closest executable match for the current `section0_dispatch_roots` viewer family.
|
|
- `psx_dispatch_section0_constructor_placements` walks the `DAT_800678f0` `0x0c`-stride family with the same neighborhood cull and per-type dispatch. This is the closest executable match for the current `section0_constructor_placements` viewer family.
|
|
- The already-instantiated-object passes are separated too:
|
|
- `psx_run_live_object_type_updates` iterates the linked live object list at `DAT_800675ac` and calls the per-type update callback (`type_vtable+8`) for active in-world objects.
|
|
- `psx_run_live_object_behavior_callbacks` then runs each live object's callback stored at `obj+0x98` / `obj[0x26]`, which is the later object-specific behavior/update pass.
|
|
- `FUN_80029de0` is the broad world-motion and player-state integrator that sits between behavior updates and draw submission. For viewer purposes, this is the runtime bridge between authored map placement and the motion/state values that later feed heading-based state reselection and projection.
|
|
- The cull-to-draw bridge is now closed too. `FUN_800423b0` is the authored-record screen-space gate used by the two section-0 dispatch passes, while `FUN_80042424` is the corresponding gate for already-instantiated live objects. Both use the same isometric camera basis around `DAT_800678d4`, which means the viewer can treat the record-family export as feeding the same projection space as the later live object list instead of as a separate map coordinate model.
|
|
- The main world-object draw helper is now grounded more tightly as well. `FUN_80041458` builds the final sprite primitive from the authored screen rectangle at `obj+0x20..+0x2e`, then ORs in a palette override read from the original source-record pointer at `obj+0xa0`: for types `0x003e..0x00ab` it uses the high byte of source word `+0x06`, and for types `>= 0x00ac` it uses the high byte of source word `+0x0c`. That means the remaining viewer mismatch is not where the override comes from, but when the runtime chooses a different object/variant/state before draw.
|
|
- The stage split is tighter too. `psx_project_object_special_visible_queue` (`0x80040f78`) feeds a distinct world-facing stage-2 queue, and `FUN_80041144` consumes that queue with the same projected screen rectangle fields and the same resource-specific draw helpers used by the stage-1 visible list. So the unreadable output is not explained by one missing HUD lane; the dominant gap is still the unresolved final art-binding path, with the stage-2 queue as a secondary world-object lane the viewer must eventually model.
|
|
- The next high-value executable target is now partly closed. `FUN_8002906c` is now renamed `psx_type4_reselect_motion_state`, and the surrounding interaction cluster is finally concrete enough to describe instead of leaving it as a black box:
|
|
- `psx_type4_update_delayed_interaction` (`0x80029c20`) is the type-4-only delayed wrapper. It probes ahead, stores the hit object at the controller-local `+0x38` slot, seeds a countdown from distance and speed, and dispatches to `psx_type4_reselect_motion_state` when that delay matures.
|
|
- `psx_type4_reselect_motion_state` (`0x8002906c`) is the post-construction reselection path for those delayed type-4 interactions. Depending on target flags it either hands off to the older `psx_object_reselect_state_from_target_vector` (`0x80028c94`) helper or flips the object's motion components against the target bounds, then reseats the live state script through `psx_object_select_state_script(obj, psx_object_quantize_motion_heading16(obj) >> 2 & 0xf)` before registering bilateral contact.
|
|
- `psx_object_update_nearby_interactions` (`0x80029478`) is the broad nearby-object sweep that feeds most of the non-type-4 collision and interaction bookkeeping. It walks the active object set, culls locally, performs overlap checks, updates directional contact/block flags, and registers contact pairs.
|
|
- `psx_object_test_overlap_3d` (`0x80028298`) is the box-overlap predicate used by that sweep, and `psx_object_update_contact_block_flags` (`0x800289f0`) is the direction-sensitive flag updater that writes the contact/block bits on the active object.
|
|
- `psx_object_register_contact_pair` (`0x8002845c`) is the bilateral contact queue helper used by both the broad sweep and the type-4 delayed path, which means the interaction lane is no longer speculative glue around the art/state system. It is a verified runtime bridge that can directly reseat the live script before the later `DAT_800758d4` companion lookup.
|
|
- The motion-heading side of that bridge is now closed one step further too:
|
|
- `psx_object_reselect_state_from_target_vector` (`0x80028c94`) is the older target-relative reselection helper used by `psx_type4_reselect_motion_state`. It builds a motion vector either from a target-relative offset or from a normalized target-to-player vector, writes that vector to `obj+0x60/+0x64/+0x68`, and reseats the live state script from the resulting heading bucket.
|
|
- `psx_object_quantize_motion_heading16` (`0x8003bc1c`) is the thin wrapper that feeds the current object motion vector into `psx_quantize_vector_heading16`.
|
|
- `psx_quantize_vector_heading16` (`0x8003b980`) is the actual 16-way heading quantizer. It classifies the current x/y vector against the threshold table at `DAT_80064990`, which is why the runtime reselection callers consistently reduce its result with `>> 2 & 0xf` before indexing the `DAT_800758cc` script table.
|
|
- The local post-advance render wrappers are also no longer anonymous labels:
|
|
- `psx_spawn_compound_record_advance_state_once` (`0x80013618`) creates one compound-record object, forces its script countdown to `1`, immediately runs `psx_object_advance_state_script`, and then marks the object with `obj+0x1e |= 0x20`. This is the cleanest currently recovered example of a constructor wrapper that intentionally advances into a non-initial live state before the object joins the normal update/render flow.
|
|
- `psx_spawn_simple_record_set_active_flag` (`0x8001372c`) is the simpler sibling for the simple-record constructor: create the object, then immediately set the low active flag in `obj+0x1e`.
|
|
- `psx_object_refresh_main_visible_and_cleanup` (`0x80013688`) is the compact stage-1 handoff wrapper. When the object still has a drawable resource and the `0x20` flag is set, it feeds the object through `psx_project_object_main_visible`; if the object is not in the `obj+0x1c & 1` hold state but does carry `obj+0x1e & 0x10`, it then runs the usual `FUN_80027f80` cleanup/follow-up path.
|
|
- `psx_object_advance_state_and_queue_special_visible` (`0x80013758`) is the compact stage-2 handoff wrapper. If the object still has a drawable resource, it advances the active script and immediately queues the object through `psx_project_object_special_visible_queue`, then applies a small sentinel cleanup block that clears world coordinates and selected flags for specific type/selector cases.
|
|
- The owner above those wrappers is now named too: `psx_object_integrate_motion_and_route_visible` (`0x800131a8`). It is the per-object bridge between movement state and rendering: it integrates position/velocity fields, refreshes the local visible/on-screen flags, handles the controlled-object side path, then advances the live state script and routes the object either into `psx_project_object_special_visible_queue` for the type-4/special-visible branch or into `psx_project_object_main_visible` for the normal drawable branch before the usual `FUN_80027f80` cleanup. This is the clearest recovered owner-level proof that state advancement and render-lane routing belong to the same runtime step.
|
|
- Those wrappers matter because they close one more gap between `psx_object_advance_state_script` and the render split. The stage-1/stage-2 divergence is not only visible in larger caller bodies such as the `0x80013524` / `0x80013564` branches; it also exists as small dedicated wrappers that either project through the main visible list after state work or advance-and-queue directly into the special-visible pass. That makes the renderer problem look even less like a missing flat table and more like a true runtime pipeline with multiple post-script routing paths.
|
|
- The render-side leaf chain is now close to end-to-end:
|
|
- `psx_project_object_main_visible` and `psx_project_object_special_visible_queue` both use the current script word at `obj+0x94` as the frame selector they pass into the frame-metric helpers.
|
|
- `psx_resource_frame_origin_x` (`0x8004513c`), `psx_resource_frame_origin_y` (`0x800451d0`), `psx_resource_frame_width` (`0x80045014`), and `psx_resource_frame_height` (`0x800450a8`) read per-frame rectangle metadata from the current drawable resource at `obj+0x10` using that same live frame index.
|
|
- `psx_draw_world_visible_passes` (`0x80041378`) is the top-level world-facing draw orchestrator: stage 1 draws the sorted main visible list through `psx_draw_main_visible_object` (`0x80041458`), stage 2 draws the queued special-visible list through `psx_draw_special_visible_queue` (`0x80041144`), and stage 3 is the non-world HUD/overlay pass.
|
|
- `psx_draw_main_visible_object` and `psx_draw_special_visible_queue` then dispatch to one of two final frame submitters depending on resource kind: `psx_sprite_resource_submit_frame` (`0x80044bdc`) handles the streamed/VRAM-backed sprite resource path, while `psx_image_table_submit_frame` (`0x80044e9c`) handles the table-backed image resource path. Both receive the live frame index from `obj+0x94`, not the original authored selector.
|
|
- The stage-1 visible list manager is now named end to end too: `psx_main_visible_list_add`, `psx_main_visible_list_remove`, `psx_main_visible_list_rebucket_object`, `psx_main_visible_list_refresh_from_live_chain`, `psx_main_visible_list_sort_range`, and `psx_main_visible_list_get_sorted_slice` cover membership, rebucketing, refresh, dependency/tie-break sorting, and final slice retrieval for the main world-object draw pass.
|
|
- The constructor/resource side closes the remaining resource-pointer handoff. `psx_create_image_resource_from_descriptor` is now confirmed as the builder used by both constructors: type-4 descriptors bind a single indexed image resource through `image_resource_bind_vram_slot`, while type-5 descriptors allocate and upload a multi-frame bundle through `image_bundle_load_to_vram`. Both `psx_object_create_simple_record` and `psx_object_create_compound_record` resolve the per-type art bank before any draw pass runs, store the resulting drawable resource on the object at `obj+0x10`, store the per-type variant bank at `obj+0x84`, store the per-type state-script bank at `obj+0x88`, and only then call `psx_object_select_state_script`. So the current strongest end-to-end model is now:
|
|
- authored record `type` -> per-type art/state/variant banks
|
|
- constructor seeds `obj+0x10` drawable resource plus `obj+0x88/0x84`
|
|
- `psx_object_select_state_script` seeds the initial script from `DAT_800758cc`
|
|
- runtime reselection / `psx_object_advance_state_script` updates the live script word at `obj+0x94`
|
|
- `psx_object_lookup_variant_entry` maps that live script word through `DAT_800758d4`
|
|
- the projectors and final draw submitters use the same live `obj+0x94` frame index against the drawable resource at `obj+0x10`
|
|
- The newly traced consumer side changes the export diagnosis in an important way. `psx_object_advance_state_script` does not treat the `DAT_800758d4` lookup result as a resource id or replacement frame index; it sign-extends the returned three bytes into `obj+0x30/+0x34/+0x38`, and the verified downstream consumers are all in the overlap/contact lane. `psx_object_test_overlap_3d` uses those fields as box extents against `obj+0x54/+0x58/+0x5c`, `psx_object_update_contact_block_flags` uses the same extents while updating directional block bits, and the reselection helpers read target-object `+0x30/+0x34/+0x38` as target bounds. By contrast, the visible projectors and both stage-1/stage-2 draw helpers still only use `obj+0x10` plus live `obj+0x94` for visible presentation.
|
|
- The renderer/exporter path now preserves that distinction too. The PSX cache builder decodes `DAT_800758d4` as a `count + packed signed xyz-extents` table, carries the decoded per-state tuples in the exported state layer, and writes the resolved `companionExtents` tuple onto each scene item and `mapSource` row for the chosen live state when a matching entry exists. So the viewer now keeps the newly verified bounds evidence explicitly instead of flattening it back into the old placeholder-art bucket.
|
|
- That is not yet the full remaining answer for unresolved placeholder-heavy families, but the missing piece is narrower again now. The `DAT_800758d4` bytes are no longer the best candidate for the final art table; they are acting like per-state companion extents used by collision/contact logic. So the remaining gap sits inside the family-specific live resource/frame presentation path after script reselection, not in projection, placement decoding, draw-lane discovery, or generic `DAT_800758d4` consumption.
|
|
- The latest dispatch-table trace also removes one remaining false lead for `0x0042` and `0x0049`. Their type-table slots do not point to unique family handlers; both currently resolve to the same generic descriptor used across `0x003e..0x0050`, whose callback slots are `psx_spawn_compound_record_advance_state_once`, `psx_object_refresh_main_visible_and_cleanup`, and `psx_object_release_to_free_list`. So the unresolved art rule for those types is not hiding in a dedicated per-type descriptor fork; it still appears to live later in generic object state/resource handling.
|
|
- The state/art split is therefore even sharper than the earlier constructor notes implied. `psx_object_select_state_script` only chooses the current script base from `DAT_800758cc` and stores the authored selector at `obj+0x9e`, but both `psx_object_reselect_state_from_target_vector` and `psx_type4_reselect_motion_state` can later overwrite that live script choice from runtime motion state. `psx_object_advance_state_script` then refreshes `obj+0x94` from the current script word and reruns `psx_object_lookup_variant_entry`, and that lookup indexes `DAT_800758d4` by `obj+0x94`, not by the original `obj+0x9e` selector byte.
|
|
- That changes the exporter diagnosis in a useful way. The unreadable map output is still real, but the remaining gap is narrower than before: it is not just that `DAT_800758cc` exists, but that runtime interaction and heading-state paths can overwrite the live script after spawn. Any renderer-safe state-to-art rule for unresolved families such as `0x0042` has to account for those post-spawn reselection paths instead of assuming the authored selector byte is final.
|
|
- that means the PSX level load now has four distinct evidence-backed layers instead of the earlier two-way split:
|
|
- root dispatch records at `DAT_800678f4`
|
|
- secondary `0x18`-stride records at `DAT_80067720`
|
|
- per-type runtime banks at `DAT_800758d8/d0/cc/d4`
|
|
- a dedicated `0x50` level runtime-header block at `DAT_80067794`
|
|
- plus the separately loaded compressed source at `DAT_8006767c` and its optional `DAT_8006b5d8 -> DAT_8006769c` decoded `0x3e00` state buffer
|
|
|
|
## 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 is no longer wholly unresolved in the viewer path, but the `>= 0x00ac` source-base semantics and the runtime's later variant reselection still leave some color choices provisional
|
|
|
|
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.
|
|
|
|
## Recommended Ghidra Import Candidates
|
|
|
|
### 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
|
|
|
|
2. `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`:
|
|
|
|
```text
|
|
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`:
|
|
|
|
```text
|
|
^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.
|