This commit is contained in:
MaddoScientisto 2026-04-12 14:45:08 +02:00
commit a9153546ae
56 changed files with 6731 additions and 258 deletions

View file

@ -38,7 +38,7 @@ applyTo: "**"
- Keep `crusader_decompilation_notes.md` updated after each verified batch. That file is now a short index — append new analysis to the appropriate file in `docs/` and add a row to the index table if a new file is created. - Keep `crusader_decompilation_notes.md` updated after each verified batch. That file is now a short index — append new analysis to the appropriate file in `docs/` and add a row to the index table if a new file is created.
- Keep `crusader_segment_coverage_ledger.csv` updated after each verified batch whenever a segment can be promoted or reclassified. - Keep `crusader_segment_coverage_ledger.csv` updated after each verified batch whenever a segment can be promoted or reclassified.
- Keep the progress section in `plan-mid.md` updated after each verified batch so the next pass can resume from the exact stopping point. - Keep the progress section in `plan-mid.md` updated after each verified batch so the next pass can resume from the exact stopping point.
- Keep `ghidra_mcp_wishlist.md` updated whenever the workflow hits a missing MCP capability and has to fall back to PyGhidra or another local-only path. - Keep `ghidra_mcp_wishlist.md` updated whenever the workflow hits a missing MCP capability and would otherwise tempt a fallback outside MCP.
- Each wishlist entry should be short and concrete: what MCP lacked, what command/script/tool had to replace it, and what a useful MCP endpoint or behavior would look like. - Each wishlist entry should be short and concrete: what MCP lacked, what command/script/tool had to replace it, and what a useful MCP endpoint or behavior would look like.
- Record raw-import addresses alongside original segment-relative offsets when porting names. - Record raw-import addresses alongside original segment-relative offsets when porting names.
- **Always use `rename_function_by_address`**`rename_function` (by name) fails with "must have required property 'old_name'" and is broken. Use `"function_address": "000c:XXXX"` format. - **Always use `rename_function_by_address`**`rename_function` (by name) fails with "must have required property 'old_name'" and is broken. Use `"function_address": "000c:XXXX"` format.
@ -56,18 +56,13 @@ applyTo: "**"
- Before running write endpoints such as `patch_bytes_and_reanalyze` or any PyGhidra byte-write script, verify that the selected program is the intended writable copy, not the reference executable. - Before running write endpoints such as `patch_bytes_and_reanalyze` or any PyGhidra byte-write script, verify that the selected program is the intended writable copy, not the reference executable.
- If the target program is not clearly a writable patch copy in `/Writable`, stop and ask the user before performing the byte write. - If the target program is not clearly a writable patch copy in `/Writable`, stop and ask the user before performing the byte write.
# PyGhidra Fallback # Python-Backed Ghidra Through MCP Only
- Use the local PyGhidra toolkit in `tools/pyghidra_crusader` when MCP is missing an operation such as function creation, deletion, or batched scripted edits. - Never use the offline/local PyGhidra CLI toolkit from this workspace.
- If Ghidra was started with Python enabled, prefer live MCP `run_readonly_script(...)` for one-off inspection first; drop to the local PyGhidra CLI only when the work needs write access or MCP still lacks the required operation. - Do not invoke `tools.pyghidra_crusader`, the local `.venv-pyghidra311` entrypoint, or any project-open workflow that competes with the live GUI lock.
- When PyGhidra is needed because MCP lacks a required operation, append a note to `ghidra_mcp_wishlist.md` in the same batch if the gap is not already documented. - Treat Python-backed Ghidra capabilities as MCP-only: use live `run_readonly_script(...)`, live write-capable MCP script endpoints, and other MCP operations exposed by the running Ghidra session.
- The workspace-local Python environment for this toolkit is `.venv-pyghidra311`, created from `C:\Users\Maddo\.pyenv\pyenv-win\versions\3.11.6\python.exe` and installed from the bundled Ghidra 12.0.4 offline packages. - If MCP lacks a needed Python-backed operation, record that gap in `ghidra_mcp_wishlist.md` instead of falling back to the offline/local toolkit.
- Default install dir for the toolkit is `I:\Apps\ghidra_12.0.4_PUBLIC`. - If the workflow needs the user to change Ghidra state for MCP access, use the ask-questions tool with a yes/no confirmation prompt instead of plain text. Ask the user to open the correct Ghidra program or make the correct tab active before MCP work when needed.
- Invoke the toolkit with `\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader ...` from the repo root.
- Rebuild or refresh that environment with `powershell -ExecutionPolicy Bypass -File .\tools\pyghidra_crusader\bootstrap_env.ps1` from the repo root when the local PyGhidra packages drift or a Ghidra upgrade lands.
- Keep PyGhidra batches small too: prefer one focused repair plan or 1-5 direct edits at a time.
- Write operations require the Ghidra project to open successfully. If `Crusader.lock` is present because the GUI owns the project, close Ghidra first or operate on a project copy.
- If the workflow needs the user to change Ghidra state, use the ask-questions tool with a yes/no confirmation prompt instead of plain text. Ask the user to close Ghidra before PyGhidra write commands, and ask the user to open the Ghidra project before MCP server commands. The prompt should briefly describe exactly what to do and instruct the user to answer `Yes` only after the action is complete.
# Current Verified Raw-Import Ports # Current Verified Raw-Import Ports

View file

@ -1,214 +1,43 @@
--- ---
name: pyghidra-ghidra-ops name: pyghidra-ghidra-ops
description: Local PyGhidra fallback workflow for Crusader Ghidra edits and queries description: MCP-only Python-backed Ghidra scripting workflow for Crusader edits and queries; use when live MCP Python/script capabilities are needed and never for the offline local CLI toolkit
--- ---
# PyGhidra Ghidra Ops # PyGhidra Ghidra Ops
Use this skill when Ghidra MCP is missing a needed operation and you need native CPython access to the Ghidra API for the local Crusader project. Use this skill when the live Ghidra MCP session needs Python-backed inspection or scripted edits. Do not use the offline local PyGhidra CLI from this workspace.
## Use Cases ## Use Cases
- Create or delete functions in `CRUSADER-RAW.EXE`. - Run live MCP readonly Python-backed inspection when decompiler or xref work needs scripted help.
- Apply small batched repairs driven by verified addresses. - Run live MCP write-capable scripted edits for small verified rename, comment, function-boundary, or datatype batches.
- Add comments or rename functions by address from a repeatable JSON plan. - Keep scripted Ghidra work inside the active GUI-backed MCP session so project locks do not matter.
- Decompile or disassemble functions without switching back to the MCP server.
- Query function metadata, search by name, and inspect xrefs from the same local CLI.
- Inspect project root files to confirm the program name/path before running edits.
## Workspace Defaults ## Workspace Defaults
- Ghidra install dir: `I:\Apps\ghidra_12.0.4_PUBLIC` - Active authority: the live Ghidra MCP session
- Ghidra project dir: repo root - Default target unless stated otherwise: `CRUSADER.EXE`
- Ghidra project name: `Crusader` - Python-backed operations must run through MCP endpoints exposed by the active Ghidra session
- Default program: `CRUSADER-RAW.EXE`
- Local Python env: `.venv-pyghidra311`
- CLI entrypoint: `.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader`
- Bootstrap script: `.\tools\pyghidra_crusader\bootstrap_env.ps1`
## Constraints ## Constraints
- Stay conservative. Use the same rename and batch-size rules as the main Ghidra workflow. - Stay conservative. Use the same rename and batch-size rules as the main Ghidra workflow.
- Prefer one focused plan or 1-5 direct edits at a time. - Prefer one focused plan or 1-5 direct edits at a time.
- If a live MCP session was started with Python enabled, use live `run_readonly_script(...)` for quick inspection before falling back to the local CLI; reserve the local PyGhidra path for write-side work or still-missing MCP capabilities. - Never fall back to the offline/local CLI path from this workspace.
- Write operations require the project to be openable for modification. If `Crusader.lock` is present because the GUI owns the project, close Ghidra first or work on a copy. - If MCP cannot do the needed Python-backed operation, document the gap in `ghidra_mcp_wishlist.md` rather than using the local toolkit.
- Keep `crusader_decompilation_notes.md` updated after verified repair batches. - Keep `crusader_decompilation_notes.md` updated after verified repair batches.
For 16-bit NE decompiler failures after prototype edits or function recreation, inspect direct callees before assuming the caller frame is corrupt. In this repo a broken caller (`1420:1499`) was only fixed after repairing a shared callee (`1000:42e2`) whose pointer-return prototype had decompiled with a hidden `__return_storage_ptr__` and poisoned the caller stack model. For 16-bit NE decompiler failures after prototype edits or function recreation, inspect direct callees before assuming the caller frame is corrupt. In this repo a broken caller (`1420:1499`) was only fixed after repairing a shared callee (`1000:42e2`) whose pointer-return prototype had decompiled with a hidden `__return_storage_ptr__` and poisoned the caller stack model.
Refresh the local PyGhidra environment when the bundled Ghidra version changes: ## MCP Usage Pattern
```powershell - Prefer standard MCP endpoints first for decompilation, disassembly, xrefs, renames, comments, function creation/deletion, and datatype work.
powershell -ExecutionPolicy Bypass -File .\tools\pyghidra_crusader\bootstrap_env.ps1 - Use live MCP Python/script endpoints only when the ordinary endpoint surface cannot express the needed operation.
``` - Keep script batches small and evidence-driven, just like ordinary MCP edit plans.
- When a live MCP Python/script batch succeeds, treat that as the canonical workflow; do not duplicate it through the local CLI.
## Commands
List root project files:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader project-files
```
Delete a bad function object:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader delete-function --entry 0007:5b6f
```
Create a repaired function with an explicit body:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader create-function \
--entry 0007:5a90 \
--name seg043_func_0090 \
--body-start 0007:5a90 \
--body-end 0007:5b79 \
--plate-comment "Recovered from standalone seg043 boundary scan"
```
Rename a function by entry address:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader rename-function --entry 0006:02cc --name entity_class_get_flag20
```
MCP-style read/query commands are also available from the same CLI:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-by-address --address 000a:48ff
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get_function_by_address --address 000a:48ff
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-containing --address 000a:4901
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader decompile-function-by-address --address 000a:48ff
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader disassemble-function --address 000a:48ff
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader read-region --start 000a:48ff --end 000a:4912
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader search-functions-by-name --query rng_
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-methods --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list_methods --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-strings --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-imports --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-exports --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-namespaces --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-segments --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-data-items --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-classes --limit 20
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-xrefs-to --address 000a:48ff
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-xrefs --name rng_next_modulo
```
All commands also support structured output for scripting:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader --format json get-function-by-address --address 000a:48ff
```
JSON output now uses a stable envelope:
```json
{
"schema_version": "1.0",
"command": "get-function-by-address",
"ok": true,
"schema": { "type": "object", "properties": { "name": { "type": "string" } } },
"data": {
"name": "rng_next_modulo",
"signature": "undefined rng_next_modulo()",
"entry": "000a:48ff",
"body_start": "000a:48ff",
"body_end": "000a:4912"
}
}
```
The CLI also accepts exact MCP-style underscore command aliases, so local automation can often swap MCP names directly with little or no translation.
For ad hoc investigation, prefer `run-script` over multiline `python -c` or pasted PowerShell here-strings. It avoids leaving the shared shell stuck in an unfinished string/block state:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader run-script --script .\pyghidra_plans\inspect_rng.py --read-only
```
Script globals available inside `run-script`:
```python
config
project
program
helpers["get_function"]
helpers["get_function_containing"]
helpers["decompile_function"]
helpers["disassemble_function"]
helpers["get_xrefs_to"]
helpers["get_xrefs_from"]
helpers["read_region_bytes"]
helpers["rename_function"]
helpers["set_comment"]
```
Write-side MCP-style aliases are available too:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader rename-function-by-address --entry 000a:48ff --name rng_next_modulo
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader set-decompiler-comment --address 000a:48ff --text "Returns RNG output modulo the requested bound."
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader set-disassembly-comment --address 000a:48ff --text "Modulo wrapper around rng_advance_state"
```
Apply a small JSON plan:
```json
{
"transaction": "Repair seg043 boundaries",
"remove_functions": [
"0007:5b6f"
],
"create_functions": [
{
"entry": "0007:5a90",
"name": "seg043_func_0090",
"body_start": "0007:5a90",
"body_end": "0007:5b79",
"comment": "Recovered from standalone seg043 boundary scan"
},
{
"entry": "0007:5b7a",
"name": "seg043_func_017a",
"body_start": "0007:5b7a",
"body_end": "0007:5c1b"
},
{
"entry": "0007:5c1c",
"name": "seg043_func_021c",
"body_start": "0007:5c1c",
"body_end": "0007:5c80"
}
],
"comments": [
{
"address": "0007:5b6f",
"text": "Old auto-created split overlaps the earlier seg043:0090..0179 routine.",
"type": "plate"
}
]
}
```
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader apply-plan --plan .\seg043_repair.json
```
Dry-run a plan before touching the project:
```powershell
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader apply-plan --plan .\seg043_repair.json --dry-run
```
## Implementation Notes ## Implementation Notes
- Address strings accept raw `SSSS:OOOO` form or plain integers such as `0x75a90`. - Address strings still accept raw `SSSS:OOOO` form or plain integers such as `0x75a90` when the underlying MCP endpoint supports them.
- The CLI tries a few root folder path variants when opening the program so it can tolerate minor project path differences. - Keep the active-program context in mind; if the wrong Ghidra tab is active, fix that through the live MCP workflow rather than opening a second offline project handle.
- Plan files support `remove_functions`, `rename_functions`, `create_functions`, `comments`, and `assert_functions`. - If a missing live endpoint or script capability blocks work, update `ghidra_mcp_wishlist.md` so the gap stays visible instead of reintroducing the local CLI fallback.
- `set-decompiler-comment` maps to a pre-comment and `set-disassembly-comment` maps to an EOL comment.
- Read/query commands open the program read-only; create/rename/comment/plan commands still require the project to be writable.
- `run-script --read-only` is the safest way to do one-off inspection without getting the shared PowerShell session stuck in a multiline Python string.
- `read-region` now reads bytes one address at a time instead of relying on a bulk `getBytes` path that produced misleading all-zero results in this project under PyGhidra.
- PyGhidra startup now suppresses the noisy local GhidraMCP `Module.manifest` warnings during normal CLI operation.

1
.gitignore vendored
View file

@ -44,3 +44,4 @@ bin/**
USECODE/REGRET/REGRET_USECODE_extracted/chunks/** USECODE/REGRET/REGRET_USECODE_extracted/chunks/**
exports/** exports/**
out/** out/**
binary/**

View file

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<FILE_INFO> <FILE_INFO>
<BASIC_INFO> <BASIC_INFO>
<STATE NAME="EXCLUSIVE" TYPE="boolean" VALUE="true" />
<STATE NAME="CHECKOUT_VERSION" TYPE="int" VALUE="1" />
<STATE NAME="CONTENT_TYPE" TYPE="string" VALUE="Program" /> <STATE NAME="CONTENT_TYPE" TYPE="string" VALUE="Program" />
<STATE NAME="PARENT" TYPE="string" VALUE="/psx/remorse" /> <STATE NAME="PARENT" TYPE="string" VALUE="/psx/remorse" />
<STATE NAME="FILE_ID" TYPE="string" VALUE="c0a86451c52a19721794868600" /> <STATE NAME="FILE_ID" TYPE="string" VALUE="c0a86451c52a19721794868600" />
<STATE NAME="FILE_TYPE" TYPE="int" VALUE="0" /> <STATE NAME="FILE_TYPE" TYPE="int" VALUE="0" />
<STATE NAME="LOCAL_CHECKOUT_VERSION" TYPE="int" VALUE="31" />
<STATE NAME="READ_ONLY" TYPE="boolean" VALUE="false" /> <STATE NAME="READ_ONLY" TYPE="boolean" VALUE="false" />
<STATE NAME="CHECKOUT_ID" TYPE="long" VALUE="2" />
<STATE NAME="NAME" TYPE="string" VALUE="SLUS_002.68" /> <STATE NAME="NAME" TYPE="string" VALUE="SLUS_002.68" />
</BASIC_INFO> </BASIC_INFO>
</FILE_INFO> </FILE_INFO>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -4,7 +4,8 @@
<SAVE_STATE> <SAVE_STATE>
<ARRAY NAME="EXPANDED_PATHS" TYPE="string"> <ARRAY NAME="EXPANDED_PATHS" TYPE="string">
<A VALUE="Crusader:regret:" /> <A VALUE="Crusader:regret:" />
<A VALUE="Crusader:Writable:" /> <A VALUE="Crusader:psx:" />
<A VALUE="Crusader:psx:remorse:" />
<A VALUE="Crusader:" /> <A VALUE="Crusader:" />
</ARRAY> </ARRAY>
<STATE NAME="SHOW_TABLE" TYPE="boolean" VALUE="false" /> <STATE NAME="SHOW_TABLE" TYPE="boolean" VALUE="false" />

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<FILE_INFO>
<BASIC_INFO>
<STATE NAME="CONTENT_TYPE" TYPE="string" VALUE="Program" />
<STATE NAME="PARENT" TYPE="string" VALUE="/psx/remorse" />
<STATE NAME="FILE_ID" TYPE="string" VALUE="c0a86451c52a19721794868600" />
<STATE NAME="FILE_TYPE" TYPE="int" VALUE="0" />
<STATE NAME="READ_ONLY" TYPE="boolean" VALUE="false" />
<STATE NAME="NAME" TYPE="string" VALUE="SLUS_002.68" />
</BASIC_INFO>
</FILE_INFO>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<CHECKOUT_LIST NEXT_ID="3">
<CHECKOUT ID="2" USER="Maddo" VERSION="1" TIME="1775898506591" PROJECT="Cirno-PC::K:/ghidra/Crusader_Decomp/Crusader" EXCLUSIVE="true" />
</CHECKOUT_LIST>

View file

@ -0,0 +1 @@
1;Maddo;1775898506582;

View file

@ -12,8 +12,11 @@ VERSION=1
00000001:CRUSADER-PATCHED.EXE:c0a86451f6e9206725659389900 00000001:CRUSADER-PATCHED.EXE:c0a86451f6e9206725659389900
/orig_cd /orig_cd
00000009:CRUSADER.EXE:c0a86451c28b202638339220200 00000009:CRUSADER.EXE:c0a86451c28b202638339220200
/psx
/psx/remorse
0000000b:SLUS_002.68:c0a86451c52a19721794868600
/regret /regret
00000007:ASYLUM.DLL:c0a86451c280202637798314100 00000007:ASYLUM.DLL:c0a86451c280202637798314100
00000008:REGRET.EXE:c0a86451c281202637836837200 00000008:REGRET.EXE:c0a86451c281202637836837200
NEXT-ID:b NEXT-ID:c
MD5:d41d8cd98f00b204e9800998ecf8427e MD5:d41d8cd98f00b204e9800998ecf8427e

File diff suppressed because one or more lines are too long

View file

@ -31,7 +31,7 @@
"30","code","0x5E000","0x5071","None","","","","crusader_ne_segments.csv" "30","code","0x5E000","0x5071","None","","","","crusader_ne_segments.csv"
"31","code","0x64000","0x6EE","Partial","StorageDataProcess queue create/run/release helpers","StorageDataProcess_Create; StorageDataProcess_Run; StorageDataProcess_Release; StorageDataProcess_RunAndTerminateProcs; storage_process_ref_list_create; storage_process_ref_list_append; storage_process_ref_list_terminate_item_matches; storage_process_ref_list_destroy","Still needs caller-side recovery beyond the verified collision producer and any stronger subsystem naming than the local hit/got-hit storage queue","plan-mid.md; docs/ne-hole-filling-priorities.md; docs/raw-0008-000c.md" "31","code","0x64000","0x6EE","Partial","StorageDataProcess queue create/run/release helpers","StorageDataProcess_Create; StorageDataProcess_Run; StorageDataProcess_Release; StorageDataProcess_RunAndTerminateProcs; storage_process_ref_list_create; storage_process_ref_list_append; storage_process_ref_list_terminate_item_matches; storage_process_ref_list_destroy","Still needs caller-side recovery beyond the verified collision producer and any stronger subsystem naming than the local hit/got-hit storage queue","plan-mid.md; docs/ne-hole-filling-priorities.md; docs/raw-0008-000c.md"
"32","code","0x64800","0x56A","Foothold","Item type / typename.dat loader helpers","ItemType_Init; ItemType_LoadTypeflagDat; ItemType_Load_TypenameDat; ItemType_GetTypenameRecordPtrAtIndex; ItemType_FindTypenameRecordIndex; Item_GetShapeFamilyOfShape","The segment is now clearly an item-type helper lane. The remaining gap is not local contract recovery but broader caller-side integration and any later higher-level item-family helpers that still sit outside this small loader-focused window.","plan-mid.md; docs/function-knowledge-roadmap.md" "32","code","0x64800","0x56A","Foothold","Item type / typename.dat loader helpers","ItemType_Init; ItemType_LoadTypeflagDat; ItemType_Load_TypenameDat; ItemType_GetTypenameRecordPtrAtIndex; ItemType_FindTypenameRecordIndex; Item_GetShapeFamilyOfShape","The segment is now clearly an item-type helper lane. The remaining gap is not local contract recovery but broader caller-side integration and any later higher-level item-family helpers that still sit outside this small loader-focused window.","plan-mid.md; docs/function-knowledge-roadmap.md"
"33","code","0x65000","0x10D7","Partial","NPC action / stand / pace / surrender / guard / loiter AI processes","NPCActionProcess_Create; NPCActionProcess_RunNoop; NPCActionProcess_Destroy; NPCActionProcess_VtableSlot10Noop; StandProcess_Run; StandProcess_Destroy; PaceProcess_CreateProcess; PaceProcess_Run; PaceProcess_Destroy; SurrenderProcess_CreateProcess; SurrenderProcess_Destroy; SurrenderProcess_Run; GuardProcess_CreateProcess; GuardProcess_Run; GuardProcess_Destroy; LoiterProcess_CreateProcess; LoiterProcess_Run; LoiterProcess_Destroy; LoiterProcess_VtableSlot10DispatchByShapeIfAlive; NPC_DoRandomIdleAnimTwiceIfNotBusy","The live AI-process lane now has run ownership, slot-1 destructor ownership, and the local slot-10 helper split grounded in the database. The remaining uncertainty in this immediate window is mostly higher-level behavior-policy meaning, especially what the slot-10/slot-11 transitions represent semantically beyond the now-named local function objects.","plan-mid.md; docs/function-knowledge-roadmap.md" "33","code","0x65000","0x10D7","Partial","NPC action / stand / pace / surrender / guard / loiter AI processes","NPCActionProcess_Create; NPCActionProcess_RunNoop; NPCActionProcess_Destroy; NPCActionProcess_VtableSlot10Noop; StandProcess_Run; StandProcess_Destroy; PaceProcess_CreateProcess; PaceProcess_Run; PaceProcess_Destroy; SurrenderProcess_CreateProcess; SurrenderProcess_Destroy; SurrenderProcess_Run; GuardProcess_CreateProcess; GuardProcess_Run; GuardProcess_Destroy; LoiterProcess_CreateProcess; LoiterProcess_Run; LoiterProcess_Destroy; LoiterProcess_VtableSlot10DispatchByShapeIfAlive; NPC_DoRandomIdleAnimTwiceIfNotBusy","The live AI-process lane now has owner-first Remorse class lifting in addition to run ownership, slot-1 destructor ownership, and the local slot-10 helper split. The remaining uncertainty in this immediate window is mostly higher-level behavior-policy meaning and whether the family is ready for a safe provisional datatype, especially what the slot-10/slot-11 transitions represent semantically beyond the now-named local function objects.","plan-mid.md; docs/function-knowledge-roadmap.md; docs/npc-action-process-class-layout.md"
"34","code","0x66600","0x253A","None","","","","crusader_ne_segments.csv" "34","code","0x66600","0x253A","None","","","","crusader_ne_segments.csv"
"35","code","0x69400","0xF67","None","","","","crusader_ne_segments.csv" "35","code","0x69400","0xF67","None","","","","crusader_ne_segments.csv"
"36","code","0x6A600","0x69F","None","","","","crusader_ne_segments.csv" "36","code","0x6A600","0x69F","None","","","","crusader_ne_segments.csv"

1 Segment Type FileOffset Length CoverageStatus KnownSubsystem KeyNamedFunctions Blockers NotesSource
31 30 code 0x5E000 0x5071 None crusader_ne_segments.csv
32 31 code 0x64000 0x6EE Partial StorageDataProcess queue create/run/release helpers StorageDataProcess_Create; StorageDataProcess_Run; StorageDataProcess_Release; StorageDataProcess_RunAndTerminateProcs; storage_process_ref_list_create; storage_process_ref_list_append; storage_process_ref_list_terminate_item_matches; storage_process_ref_list_destroy Still needs caller-side recovery beyond the verified collision producer and any stronger subsystem naming than the local hit/got-hit storage queue plan-mid.md; docs/ne-hole-filling-priorities.md; docs/raw-0008-000c.md
33 32 code 0x64800 0x56A Foothold Item type / typename.dat loader helpers ItemType_Init; ItemType_LoadTypeflagDat; ItemType_Load_TypenameDat; ItemType_GetTypenameRecordPtrAtIndex; ItemType_FindTypenameRecordIndex; Item_GetShapeFamilyOfShape The segment is now clearly an item-type helper lane. The remaining gap is not local contract recovery but broader caller-side integration and any later higher-level item-family helpers that still sit outside this small loader-focused window. plan-mid.md; docs/function-knowledge-roadmap.md
34 33 code 0x65000 0x10D7 Partial NPC action / stand / pace / surrender / guard / loiter AI processes NPCActionProcess_Create; NPCActionProcess_RunNoop; NPCActionProcess_Destroy; NPCActionProcess_VtableSlot10Noop; StandProcess_Run; StandProcess_Destroy; PaceProcess_CreateProcess; PaceProcess_Run; PaceProcess_Destroy; SurrenderProcess_CreateProcess; SurrenderProcess_Destroy; SurrenderProcess_Run; GuardProcess_CreateProcess; GuardProcess_Run; GuardProcess_Destroy; LoiterProcess_CreateProcess; LoiterProcess_Run; LoiterProcess_Destroy; LoiterProcess_VtableSlot10DispatchByShapeIfAlive; NPC_DoRandomIdleAnimTwiceIfNotBusy The live AI-process lane now has run ownership, slot-1 destructor ownership, and the local slot-10 helper split grounded in the database. The remaining uncertainty in this immediate window is mostly higher-level behavior-policy meaning, especially what the slot-10/slot-11 transitions represent semantically beyond the now-named local function objects. The live AI-process lane now has owner-first Remorse class lifting in addition to run ownership, slot-1 destructor ownership, and the local slot-10 helper split. The remaining uncertainty in this immediate window is mostly higher-level behavior-policy meaning and whether the family is ready for a safe provisional datatype, especially what the slot-10/slot-11 transitions represent semantically beyond the now-named local function objects. plan-mid.md; docs/function-knowledge-roadmap.md plan-mid.md; docs/function-knowledge-roadmap.md; docs/npc-action-process-class-layout.md
35 34 code 0x66600 0x253A None crusader_ne_segments.csv
36 35 code 0x69400 0xF67 None crusader_ne_segments.csv
37 36 code 0x6A600 0x69F None crusader_ne_segments.csv

View file

@ -0,0 +1,112 @@
# NPC Action Process Family Layout
## Purpose
This note captures the current safest class-lift state for the bounded seg033 NPC AI process family in live `CRUSADER.EXE`.
The goal is not to force a complete inheritance or process-layout model yet. The current goal is narrower:
- preserve the now-verified class ownership in Ghidra
- record which methods are strong enough to live under class owners today
- keep the remaining uncertainty explicit so later vtable and datatype work can resume without rediscovery
## Current Live Class Owners
The following class owners now exist live in `CRUSADER.EXE` under the `Remorse` namespace:
- `Remorse::NPCActionProcess`
- `Remorse::StandProcess`
- `Remorse::PaceProcess`
- `Remorse::SurrenderProcess`
- `Remorse::GuardProcess`
- `Remorse::LoiterProcess`
This is an owner-first lift only. No full instance struct, base-process overlay, or vtable datatype has been forced yet.
## Landed Method Batch
### Base family
| Live address | Current name | Why it is safe |
|---|---|---|
| `1100:0000` | `Remorse::NPCActionProcess::Create` | Shared family create entry already named and bounded in the local process lane. |
| `1100:1084` | `Remorse::NPCActionProcess::RunNoop` | Base run body is an intentional no-op and does not need wider semantics yet. |
| `1100:1089` | `Remorse::NPCActionProcess::Destroy` | Shared slot-1 destroy body is grounded by live `g_*ProcessFnPtr` ownership across the NPC action family. |
| `1100:0fe3` | `Remorse::NPCActionProcess::VtableSlot10Noop` | Shared slot-10 no-op body is structurally bounded even though final slot semantics remain open. |
### Derived families
| Live address | Current name | Why it is safe |
|---|---|---|
| `1100:02ed` | `Remorse::StandProcess::CreateProcess` | Direct family create wrapper in the same bounded seg033 lane. |
| `1100:0359` | `Remorse::StandProcess::Run` | Direct owned run body for the stand policy. |
| `1100:1036` | `Remorse::StandProcess::Destroy` | Slot-1 destroy entry grounded by live process-function-table ownership. |
| `1100:0383` | `Remorse::SurrenderProcess::CreateProcess` | Direct family create wrapper in the same bounded seg033 lane. |
| `1100:04ab` | `Remorse::SurrenderProcess::Run` | Direct owned run body for surrender behavior. |
| `1100:0437` | `Remorse::SurrenderProcess::Destroy` | Clear family destroy path; resets the surrender vtable root, clears the local NPC flag bit, and destroys two embedded dispatch-entry children. |
| `1100:0693` | `Remorse::PaceProcess::CreateProcess` | Direct family create wrapper in the same bounded seg033 lane. |
| `1100:0708` | `Remorse::PaceProcess::Run` | Direct owned run body for the pace policy. |
| `1100:0fe8` | `Remorse::PaceProcess::Destroy` | Slot-1 destroy entry grounded by live process-function-table ownership. |
| `1100:0984` | `Remorse::GuardProcess::CreateProcess` | Direct family create wrapper in the same bounded seg033 lane. |
| `1100:0a0e` | `Remorse::GuardProcess::Run` | Direct owned run body for the guard policy. |
| `1100:0f95` | `Remorse::GuardProcess::Destroy` | Slot-1 destroy entry grounded by live process-function-table ownership. |
| `1100:0afb` | `Remorse::LoiterProcess::CreateProcess` | Direct family create wrapper in the same bounded seg033 lane. |
| `1100:0bfa` | `Remorse::LoiterProcess::Run` | Direct owned run body for the loiter policy. |
| `1100:0d3e` | `Remorse::LoiterProcess::VtableSlot10DispatchByShapeIfAlive` | Loiter-only slot-10 override is behaviorally distinct enough to separate from the shared base slot-10 no-op. |
| `1100:0f47` | `Remorse::LoiterProcess::Destroy` | Slot-1 destroy entry grounded by live process-function-table ownership. |
## Shared Helper That Still Stays Free
`1100:0913` remains `NPC_DoRandomIdleAnimTwiceIfNotBusy` instead of being forced under one class owner.
Current evidence supports a helper role more strongly than a specific class-method role:
- the live caller map still shows only `Remorse::GuardProcess::Run` and `Remorse::LoiterProcess::Run`
- it gates on `NPC_IsBusy`
- it chooses between two idle-animation paths and dispatches `NPC_DoAnim` twice
That makes it strong enough to name and comment, but still weak for ownership under exactly one class.
## Ghidra Documentation Landed In-Session
The live database now also carries short decompiler comments on the main ownership and evidence anchors:
- `NPCActionProcess::Create`
- `NPCActionProcess::Destroy`
- `SurrenderProcess::CreateProcess`
- `SurrenderProcess::Destroy`
- `GuardProcess::CreateProcess`
- `GuardProcess::Run`
- `LoiterProcess::CreateProcess`
- `LoiterProcess::Run`
- `LoiterProcess::VtableSlot10DispatchByShapeIfAlive`
- `NPC_DoRandomIdleAnimTwiceIfNotBusy`
Those comments preserve the current rationale without pretending that the datatype and inheritance story is already closed.
## Current Safest Structural Read
The family now reads as:
1. one bounded `NPCActionProcess` base with a shared create entry, shared destroy entry, and at least one shared no-op virtual slot
2. multiple policy-specific derived families (`Stand`, `Pace`, `Surrender`, `Guard`, `Loiter`)
3. one shared guard/loiter-side idle helper that still belongs outside the current class owners
That is enough for owner-first navigation in Ghidra, but not yet enough for a full ABI-clean C++ inheritance model.
## Things Still Open
The main remaining gaps are now narrower than basic object identity:
- what slot `10` and the adjacent slot `11` mean semantically across the broader process family
- whether the current `NPCActionProcess::Create` should later split into a thinner base initializer plus a more explicit allocator/factory wrapper
- whether the shared slot-1 destroy body can support a first provisional base-process overlay without destabilizing other process families
- whether `StandProcess` and `PaceProcess` have any equally strong family-local helper methods in the same seg033 window that should move next
## Recommended Next Pass
The next pass on this family should stay conservative:
1. recover the process-function-table roots for the seg033 family explicitly enough to document slot order
2. inspect local caller/callee structure around `StandProcess::Run` and `PaceProcess::Run` for family-local helpers comparable to the guard/loiter idle helper
3. only then decide whether a provisional `/Remorse/NPCActionProcess` datatype is safe, or whether the family should remain owner-only for now

View file

@ -62,6 +62,14 @@ Representative maps after the extended pass:
So the first practical conclusion is straightforward: the exporter was the main blocker for most maps, and a constrained donor heuristic can replace a large fraction of placeholders with real graphics without touching the viewer runtime. So the first practical conclusion is straightforward: the exporter was the main blocker for most maps, and a constrained donor heuristic can replace a large fraction of placeholders with real graphics without touching the viewer runtime.
## No-Placeholder Cohort Pass
- A second follow-up pass replaced synthetic fallback atlas generation with cohort-aware donor resolution keyed by authored family plus raw `u5` lane before the older coarse type default is consulted.
- The exporter still prefers direct local `DAT_800758d8` payload matches first, but when a type bucket is mixed-role or otherwise unresolved it now resolves each cohort separately instead of collapsing the whole `map:type` bucket into one donor or one placeholder.
- Focused validation on `map 104` now rebuilds as `1002` real-art items with `0` fallback items, `1` atlas, and `136` shape definitions. Scene `sourceCounts` are now `52` `section0_dispatch_roots-art` plus `950` `section0_constructor_placements-art`.
- The scene JSON also now preserves `mappingSource` and optional `artCohort` on each `mapSource` row, so provisional cohort and emergency donor matches stay auditable after placeholders are removed.
- This is still not the final executable-proof binding rule. The focused cache is now placeholder-free, but many rows still resolve through `cohort-*` or `emergency-global-donor:*` provenance and should be treated as provisional PSX art recovery rather than as final family/resource closure.
## Remaining Unresolved Mass ## Remaining Unresolved Mass
- The cache is still not fully closed. `25,038` fallback items remain across `62` maps. - The cache is still not fully closed. `25,038` fallback items remain across `62` maps.
@ -70,12 +78,71 @@ So the first practical conclusion is straightforward: the exporter was the main
- `0x0042` remains the single largest unresolved type even after the heuristic pass. - `0x0042` remains the single largest unresolved type even after the heuristic pass.
- `map 104` is the most obvious current outlier: it still sits at `866` fallback items versus only `136` bundle-mapped items, so the next recovery pass should treat it as the best stress case rather than relying only on the now-mostly-readable early maps. - `map 104` is the most obvious current outlier: it still sits at `866` fallback items versus only `136` bundle-mapped items, so the next recovery pass should treat it as the best stress case rather than relying only on the now-mostly-readable early maps.
Those counts are now historical rather than current for the focused `map 104` export. They remain useful as the baseline that motivated the cohort pass, but the immediate blocker has shifted from “remove placeholders” to “replace provisional donor provenance with tighter executable-backed cohort/resource rules.”
## Practical Interpretation ## Practical Interpretation
- The donor heuristic is good enough to prove that many placeholders were caused by missing extraction-time inheritance logic. - The donor heuristic is good enough to prove that many placeholders were caused by missing extraction-time inheritance logic.
- It is not strong enough to count as the final executable-backed rule for the unresolved families. - It is not strong enough to count as the final executable-backed rule for the unresolved families.
- The remaining gap still sits where the earlier Ghidra work already pointed: somewhere between the constructor-side art/resource creation lane and the live post-spawn state/resource/frame reselection path. - The remaining gap still sits where the earlier Ghidra work already pointed: somewhere between the constructor-side art/resource creation lane and the live post-spawn state/resource/frame reselection path.
## Palette Lock-In Rule
- The PSX exporter must not treat `mode 1` bundle header palette index `+0x14` as the rendered 256-color selector.
- The current dump-grounded rule is narrower and stronger: the known-good visible family decodes against one contiguous 256-entry lookup table equivalent to live VRAM row `0xF0`, `x=0`.
- In extractor terms, that behaves like the first `16` adjacent `16-color` CLUTs flattened into one 256-entry palette. That is the rule now encoded in `map_renderer/src/lib/psx-cache.js` for `mode 1` bundles.
- The old bundle-header palette index is still preserved in exported scene metadata as `defaultPaletteIndex`, but it is diagnostic provenance, not the primary rendered selector.
- `mode 2` stays on the earlier per-bundle/per-usage palette-index path. This lock-in applies specifically to the current `mode 1` family proven by the VRAM dump.
- If a future pass proves a wider runtime CLUT-row formula, update this note and the exporter together in one change. Do not silently reintroduce `mode 1 -> bundle header palette index` as a fallback rule.
## Static Export Follow-Up
- The processed PSX catalog already carried `62` maps during this pass, so the "single map" symptom was not a cache-build enumeration failure.
- The immediate export-side blocker was config: `psx-remorse` was excluded from static export even though the prebuilt catalog/build-manager path already supports multi-map PSX scenes.
- The renderer config now includes `psx-remorse` in static export so full PSX exports can surface the full processed map set instead of dropping the version entirely.
## Live MCP Follow-Up (2026-04-12): Wall-Family Split Failure Mode
- Scope for this focused pass: live `SLUS_002.68` around the wall-heavy generic family band (`0x003e..0x004f`) and exporter donor-heavy bundles (`0x0008b48c`, `0x00085c40`).
- Constructor-side bind is now explicit and consistent across this family: both `psx_object_create_simple_record` (`0x800249f4`) and `psx_object_create_compound_record` (`0x80024eec`) read the per-type active art header from `DAT_800758d8[type]` before any lane-orientation updates.
- That means cross-family visual collapse is unlikely to be caused by a missing pre-constructor resource-bank split in this band; divergence happens after bind.
- Post-bind state/frame path is also explicit: `psx_object_select_state_script` (`0x800260e8`) installs selector state (`obj+0x9e` and script cursor `obj+0x90`), then `psx_object_advance_state_script` (`0x80025d68`) latches live frame token `obj+0x94` from the script stream.
- Draw submission consumes that latched token, not raw authored selector: stage-1 geometry (`0x80040d44`) and stage-2 geometry (`0x80040f78`) both query frame geometry from `obj+0x94`, and submitters read the same token in `psx_draw_main_visible_object` (`0x80041458`) / `psx_draw_special_visible_queue` (`0x80041144`).
- The stage-1/stage-2 route split matters materially: main-visible injects authored palette token only for type bands `>=0x003e`, while special-visible does not. If exporter cohorts collapse route outcome, families can appear as repeated art with wrong face/palette behavior.
- Practical exporter implication: the strongest missing discriminator is runtime route+latch state (effective selector/latch outcome), not a new type-level resource bank key.
Conservative live-artifact updates applied in Ghidra for this pass:
- `0x80046038`: comment clarifying `DAT_800758d8[type]` install timing and post-bind divergence expectation.
- `0x80026100`: comment clarifying pre-latch selector install and risk of authored-`u4`-only grouping.
- `0x80041554`: comment clarifying main-visible-only authored palette-token lane for `>=0x003e`.
- `0x80040f88`: comment clarifying stage-2 queue still consumes `obj+0x94` but has different route semantics.
## Live MCP Follow-Up (2026-04-12): CLUT Override Routing Closure
- Scope for this focused pass: world-object draw path and submitters at `0x80041458`, `0x80041144`, `0x80044bdc`, and `0x80044e9c`, with CLUT table symbols at `0x800a9f48` and `0x800a9f66`.
- `0x800a9f48` remains `psx_clut_table_by_resource_bank` and is both load-populated (`level_palette_upload_cluts`) and draw-consumed.
- `0x800a9f66` remains `psx_clut_override_table_by_palette_token` and is draw-consumed by both submitters.
- Main-visible (`0x80041458`) injects authored high-byte palette token into submit flags for type bands `>=0x003e`; special-visible (`0x80041144`) does not inject authored token and only forwards `obj_flags & 0x0002`.
- Submitter override gate is explicit in both submitters: override is used only when `(submit_flags & 0xfffffff0) != 0`.
- For this world-object path, token `0` behaves as "no override": with only low flag bits present, submitters fall through to default bank CLUT lookup.
- `psx_image_table_submit_frame` uses token as a key into `psx_clut_override_table_by_palette_token[token]` when override is active.
- `psx_sprite_resource_submit_frame` has two override lanes based on resource format field (`resource+4`):
- format `== 2`: token keys `psx_clut_override_table_by_palette_token[token]`.
- format `!= 2`: token is used as a row key into `psx_clut_table_by_resource_bank` (`token << 4` halfword index).
Exporter-facing implication from executable evidence:
- The current flattening bug matches a real semantic split in executable code: authored token injection is lane-dependent (main-visible only) and CLUT resolution is submitter/resource-format dependent.
- A single flattened token->CLUT mapping in exporter code will miscolor mixed cohorts where route lane or submitter/resource format differs.
Conservative live-artifact updates applied in Ghidra for this pass:
- `0x800415b0`: comment clarifying main-visible token injection source and band split.
- `0x800412d0`: comment clarifying special-visible non-injection behavior.
- `0x80044e10`: comment clarifying sprite submit override gate and token-0 fallthrough.
- `0x80044eb8`: comment clarifying image-table override keying behavior.
## Next Steps ## Next Steps
1. Trace the remaining high-volume band `0x0055..0x0063` in Ghidra with the same question used for `0x0042`: why does `DAT_800758d8` stay zero-sized while visible art still exists at runtime? 1. Trace the remaining high-volume band `0x0055..0x0063` in Ghidra with the same question used for `0x0042`: why does `DAT_800758d8` stay zero-sized while visible art still exists at runtime?

View file

@ -0,0 +1,230 @@
# PSX JL-9 In-Level Event Investigation
This note isolates one question from the broader JL-9 work on PlayStation `SLUS_002.68`:
- what natural in-level event writes `psx_debug_extra_channel_gate` and makes the later hidden `L0SR` + `R1 + Circle` path unlock `JL-9`?
## Current status
The downstream half is now closed.
- user validation proved that forcing `0x8006739d = 0x01`, then entering `L0SR`, then pressing `R1 + Circle` successfully adds `JL-9`
- therefore the remaining mystery is entirely on the natural gate-arm side
- `MFM4` is no longer the main mystery; it is only the strongest **natural prime candidate**
Current best interpretation:
- the more direct thing being bypassed by the manual poke is the in-level event itself
- natural `MFM4` failing means `MFM4` alone is not sufficient
- the missing event is most likely an uncommon or optional scripted/control event in a small late-level family
## Proven sink-side mechanics
### Gate byte
- `psx_debug_extra_channel_gate` = `0x8006739d`
- writer: `0x800232f0`
- reader: `0x8002fff4`
- storage is byte-wide (`sb` write, `lbu` read)
- late check is nonzero-based, not literal-value-based
### Natural gate-arm branch
Natural arm still converges on `psx_set_debug_extra_channel_gate` at `0x800230e4`.
The exact write branch is:
- tuple: `(slot=0x0f, arg1=0x0a, arg2=0x04)`
- hidden must still be off
- `psx_level_runtime_header_state` must be `3`
- only then does `0x800232f0` store `1` to `0x8006739d`
### Hidden trigger half
Later JL-9 unlock still requires:
- hidden passcode branch `L0SR` / `?0SR`
- input code `0x1e`
- practical mapping `R1 + Circle`
That later path reads `0x8006739d` at `0x8002fff4` and performs the extra `0x0d` unlock when nonzero.
## Proven dispatcher path
### Sink feeder
`0x800214ac..0x800215f8` is now promoted as:
- `psx_level_gate_slot_dispatch_from_action_record`
Recovered structure:
- dispatch slot byte comes from pointer at `record + 0x00`
- `arg1` byte comes from pointer at `record + 0x08`
- `arg2` byte comes from pointer at `record + 0x0c`
- handler call is indirect through `psx_level_gate_slot_handler_table[slot]` at `0x800640a0`
Important table entries:
- `psx_level_gate_slot_handler_table[0x0d] = 0x80022c6c`
- `psx_level_gate_slot_handler_table[0x0e] = 0x80022ea8`
- `psx_level_gate_slot_handler_table[0x0f] = 0x800230e4`
## Recovered slot-family behavior
### Slot `0x0d`
`0x80022c6c` is now named:
- `psx_control_event_slot0d_handler`
Recovered strong branch:
- `(0x0a,0x02)` => mission-complete passcode generation/display lane
### Slot `0x0e`
`0x80022ea8` is now named:
- `psx_control_event_slot0e_handler`
Recovered branches:
- `(0x0a,0x01)` => mission-complete passcode generation/display lane using quad index `0x0f`
- `(0x0a,0x02..0x04)` => mode/timer setup branches
- `(0x0a,0x06)` => selector/stage apply lane that force-applies selector `0x0f`
### Slot `0x0f`
`0x800230e4` remains:
- `psx_set_debug_extra_channel_gate`
Recovered `0x0a` cases:
- `1`
- `2`
- `3`
- `4`
- `0x2e`
The JL-9 natural arm branch is specifically:
- `(0x0a,0x04)`
## Host-level narrowing
The recovered gate family is still:
- `{54,55,56,57,58,82}`
Best current host ranking:
1. `54`
2. `55`
3. `56`
4. `57`
5. `82`
6. `58`
Why `54` remains the best anchor:
- `MFM4` is the only ordinary published code that statically satisfies the prime-side conditions
- selector `0x0f` maps to map/level `54`
- `DAT_80063e68[54] = 0x0f`
Why this still does not close the event:
- user-tested natural `MFM4` did **not** produce `JL-9`
- so level-family membership alone is not sufficient
- the missing event now looks optional, late, rare, or route-specific
## Upstream producer state
### What still exists as topology
The older behavior/subop chain still exists in tables:
- `psx_behavior_opcode_handler_table[54] = 0x80027ecc`
- `psx_behavior_subop_handler_table[49] = 0x800214ac`
`0x80027ecc` is now named conservatively:
- `psx_behavior_subopcode_dispatch`
### Why it is weaker now
The only currently proven caller into `psx_object_behavior_opcode_dispatch` is still range-limited:
- known caller path bounds `(opcode_word - 1) < 0x0a` at `0x80026710`
- that means the known active path can only reach indices `0..9`
- so `54 -> 49` is still valid topology, but it is no longer the best **active** explanation
Current practical reading:
- keep `54 -> 49` alive as structure
- deprioritize it as the leading runtime explanation until a second active caller/context is recovered
## Strongest event hypothesis now
The strongest remaining hypothesis is:
- the natural gate-arm is a sibling in the same late control-event family as mission-complete / transition handlers around `0x80022c6c..0x80023390`
- it is probably an uncommon or optional in-level scripted/control outcome
- it is more likely tied to a rare objective-path or late-event branch than to ordinary mission completion itself
Why this is stronger than the older theories:
- the sink-side tuple and slot family are concrete
- mission-complete siblings are already proven in adjacent handlers
- hard-clear / beat-the-game theory stayed weak
- direct forced-gate tests show the real unknown is the natural writer event, not the hidden code
## Ghidra changes applied in this event pass
### Functions / blocks
- `0x800204fc` -> `psx_show_mission_complete_congrats_text`
- `0x80020f7c` -> `psx_control_event_apply_level_channel_preset`
- `0x800214ac` -> `psx_level_gate_slot_dispatch_from_action_record`
- `0x80022c6c` -> `psx_control_event_slot0d_handler`
- `0x80022ea8` -> `psx_control_event_slot0e_handler`
- `0x80027ecc` -> `psx_behavior_subopcode_dispatch`
### Tables / data
- `0x80063e54` -> `psx_selector_to_map_id_table`
- `0x80063e68` -> `psx_map_id_to_gate_slot_table`
- `0x80063eac` -> `psx_map_progression_table`
- `0x80063610` -> `psx_behavior_subop_handler_table`
- `0x800640a0` -> `psx_level_gate_slot_handler_table`
- `0x800641ac` -> `psx_behavior_opcode_handler_table`
### Key comments added
- `0x800214bc`
- `0x800215bc`
- `0x800215cc`
- `0x800215dc`
- `0x800215e0`
- `0x800230e4`
- `0x800232f0`
- `0x80026710`
- `0x8002685c`
- `0x80027f0c`
- `0x80034d60`
## Best next code targets
1. Fully classify the control-event family around `0x80022c6c..0x80023390` as one switch/tuple cluster and align each sibling branch with concrete player-facing outcomes.
2. Recover one actual authored producer for `(slot=0x0f, arg1=0x0a, arg2=0x04)` by tracing the action-record inputs feeding `0x800215cc` / `0x800215e0`.
3. Find a second active caller/context into `psx_object_behavior_opcode_dispatch` or an alternate feeder into `0x800214ac` that can justify a high-index producer path in live gameplay.
## Deferred emulator experiments
Keep these queued for later:
1. Experiment 2: hard-clear / beat-the-game test
2. Experiment 4: compare `MFM4` against another header-state-`3` code under matched in-level actions
3. Experiment 5: hold map/event family constant and vary only the suspected scripted event
4. Experiment 6: ordering test (`event -> L0SR -> trigger` versus `L0SR -> event -> trigger`)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,778 @@
# PSX Map Storage Model Versus PC
## Scope
This note is a focused hypothesis document for how Crusader PSX stores map data on disk, how the executable turns that data into a renderable world, and how that model differs from the already better-understood PC build.
It is intentionally narrower than [map-rendering.md](map-rendering.md):
- this file concentrates on storage layout and platform differences
- the rendering note concentrates on the full runtime path from load to draw
The claims below are evidence-backed where possible and explicitly marked as current best read where they remain inferential.
## Bottom Line
Current best read:
1. The PSX build does not store a level as one direct item table equivalent to the PC map arrays.
2. A PSX level is a multi-section `LSET*.WDL` bundle that combines authored placement-like records, per-type runtime banks, palette/audio payloads, and at least one additional detached or compressed runtime-state lane.
3. The executable turns those authored rows into live objects early, then relies on per-type state-script and runtime reselection logic to decide what finally appears on screen.
4. The PC build appears much closer to a direct world-state model in memory: explicit map X/Y/Z arrays, entity tables, tile visibility grids, and item/shape sorting over stable world coordinates.
5. So the main platform difference is architectural: PSX level files look more like self-contained level bundles that feed an object-construction pipeline, while PC runtime/map handling looks more like direct world-state tables plus separately loaded shape/type metadata.
## 2026-04-12 Live Loader Ownership Deltas
Live MCP pass on active `SLUS_002.68` tightened loader-owned naming/comments in the map extraction lane:
- `0x80067838` renamed from `section_pack_source_80067838` to `psx_level_section_pack_base`.
- `0x800676d8` renamed from `level_clut_table_ptr` to `psx_level_clut_table_ptr`.
- Added decompiler evidence comments at `0x800398e8`, `0x80039af0`, `0x80039250`, `0x80024c60`, and `0x8003aba8`.
Extractor-relevant clarified schema in this pass:
- `psx_level_section_pack_base` is the contiguous section-pack base established by `wdl_resource_bundle_load_by_index`; subsequent header-offset adds derive `psx_type_policy_table_ptr`, `psx_control_opcode_stream_table`, and `psx_level_clut_table_ptr`.
- The compressed-state lane is explicit and size-stable: `psx_level_state_compressed_blob` (`0x8006b5d8`) inflates via `psx_lzss_unpack_into_level_buffer` into `psx_level_decompressed_state_buffer` (`0x8006769c`) with target size `0x3e00` before runtime-header apply and root-record dispatch.
- `psx_lzss_pack_level_buffer` is the save-side counterpart (caller `0x80049890`) and repacks the same level-state lane, confirming this blob family is persistent runtime substrate rather than a direct authored placement stream.
- `psx_load_type_state_banks` installs per-type runtime payload pointers into `psx_type_state_script_bank` / `psx_type_simple_component_bank` / `psx_type_companion_extents_bank`; constructors consume `psx_type_simple_component_bank[type]` at `0x80024c60` to seed object behavior program fields.
## 2026-04-12 Live Section-0 Descriptor Dispatch Deltas
Live MCP pass on active `SLUS_002.68` tightened section-0 record-family dispatch evidence for unresolved graphics-heavy types.
Ghidra artifacts updated in this pass:
- Renamed `0x800626f8` from `psx_descriptor_generic_3e_50` to `psx_descriptor_row_spawn_compound_main_visible_family`.
- Added explicit pointer labels for neighboring unresolved-family table entries:
- `0x8006323c` -> `psx_type_descriptor_ptr_0049`
- `0x8006326c` -> `psx_type_descriptor_ptr_0055`
- `0x800632a4` -> `psx_type_descriptor_ptr_0063`
- Added decompiler comments at `0x800626f8`, `0x80063220`, `0x800256b0`, and `0x800258cc` documenting row sharing and call convergence.
Schema clarification closed in this pass:
- Descriptor row `0x800626f8` is a 3-callback family row:
- slot0 `0x80013618` -> `psx_spawn_compound_record_advance_state_once`
- slot1 `0x80013688` -> `psx_object_refresh_main_visible_and_cleanup`
- slot2 `0x800254c8` -> `psx_object_release_to_free_list`
- Both section-0 authored families route into this same row via indirect descriptor dispatch:
- root-dispatch records (`psx_dispatch_section0_dispatch_roots`, `0x800256b0`)
- constructor-placement records (`psx_dispatch_section0_constructor_placements`, `0x800258cc`)
- Type `0x0042` pointer entry `0x80063220` resolves to this row, and neighboring unresolved-family entries in the requested band (`0x0049`, `0x0055..0x0063`) also resolve to the same row.
Practical consequence for exporter/runtime interpretation:
- There is currently no recovered type-unique constructor callback split between `0x0042`, `0x0049`, and `0x0055..0x0063` at section-0 descriptor-dispatch entry.
- These families should be treated as sharing a common spawn/update/release callback chassis, with visual divergence more likely coming from per-type banks (`psx_type_art_active_header_bank`, `psx_type_state_script_bank`, policy words, and live state latches) than from descriptor-row callback identity.
## 2026-04-12 Live Object Spawn To Visible Frame Closure
Live MCP pass on active `SLUS_002.68` closed the requested spawn/selector/transition lane for map-spawned objects, centered on:
- `psx_object_create_simple_record` (`0x800249f4`)
- `psx_object_create_compound_record` (`0x80024eec`)
- `psx_spawn_compound_record_advance_state_once` (`0x80013618`)
- `psx_spawn_simple_record_set_active_flag` (`0x8001372c`)
- `psx_object_select_state_from_transition_table` (`0x8001bca0`)
- `psx_object_advance_state_script` (`0x80025d68`)
- `psx_type42_transition_selector_tick` (`0x80018578`)
Renames/data labels applied in this pass:
- `0x8003a37c`: `FUN_8003a37c` -> `psx_queue_global_draw_tint_pulse_once`
- `0x80067544`: `DAT_80067544` -> `psx_global_draw_tint_pulse_phase`
- `0x80067614`: `DAT_80067614` -> `psx_global_draw_tint_pulse_mode`
Key clarified chain (authored row -> live frame/resource decision):
1. Root/transition spawn dispatch builds record payloads and calls object constructors (`0x80031500`, `0x80018164`).
2. Constructors copy authored lane/state word directly into `obj+0x1c` (`u5` lane) and seed selector/script cursor (`u4` lane) via `psx_object_select_state_script`.
3. `psx_object_select_state_script` installs selector at `obj+0x9e` and script cursor (`obj+0x8c/0x90`), but does not write final frame token.
4. `psx_object_select_state_from_transition_table` and `psx_type42_transition_selector_tick` can reseat selector and toggle low control bits (notably `obj+0x1c bit0x0002`) before latch.
5. `psx_object_advance_state_script` latches live frame/state token into `obj+0x94` from the active script stream (`obj+0x90`) and updates `obj+0x96` step/countdown.
6. Projection/draw consume `obj+0x94` directly as frame index token:
- projection: `psx_project_object_main_visible` (`0x80040d44`), `psx_project_object_special_visible_queue` (`0x80040f78`)
- geometry helpers: `psx_resource_frame_origin_x/y`, `psx_resource_frame_width/height`
- submit path: `psx_draw_main_visible_object` (`0x80041458`) chooses sprite-vs-image-table by bound resource kind (`obj+0x10`), then uses `obj+0x94` for per-frame lookup.
State/selector semantics tightened in this pass:
- `obj+0x1c` broad lane/routing bits come from authored constructor copy first; transition logic mainly toggles low control bits (`0x0002`, plus local masks), not full-word replacement.
- `obj+0x9e` is the current selector identity.
- `obj+0x94` is the final live frame/state token used by rendering geometry/resource frame queries.
- `obj+0x96` is step/countdown control between selector install and next frame-token latch.
Extractor/renderer implications:
- Do not treat authored selector bytes (`u4`) as final frame identity.
- Model a two-stage decision: selector install/reseat (`obj+0x9e`) then latch (`obj+0x94`) before projection.
- For unresolved map families (including type `0x0042`), replicate transition/reselection hooks (`psx_object_select_state_from_transition_table`, `psx_type42_transition_selector_tick`) before sampling visible frame.
- Keep `u5` as authored lane seed, but treat runtime low-bit toggles as post-spawn modifiers, not separate authored-family evidence.
## Evidence For The PSX Model
### 1. `LSET*.WDL` is a structured level bundle, not a single map table
Executable-backed evidence already shows the selected `LSET*.WDL` is treated as the live level-bundle format:
- `lset_level_bundle_load` builds `\LSETn\Lx.WDL` paths directly
- `wdl_resource_bundle_load_by_index` reads a fixed `0x38`-byte header whose first nine dwords act like section sizes
- the loader does not hand one raw placement blob to the renderer; it lays out multiple runtime destinations
The currently identified runtime destinations are:
- `psx_level_root_record_stream` (`DAT_800678f4`)
- `psx_section0_dispatch_root_records` (`DAT_80067720`)
- `psx_section0_constructor_placement_records` (`DAT_800678f0`)
- `psx_type_art_template_bank` (`DAT_800758d8`)
- `psx_type_simple_component_bank` (`DAT_800758d0`)
- `psx_type_state_script_bank` (`DAT_800758cc`)
- `psx_type_companion_extents_bank` (`DAT_800758d4`)
- `DAT_800675f8` per-type flags
- `psx_level_detached_blob` (`DAT_8006767c`)
- optional decompressed state into `psx_level_decompressed_state_buffer` (`DAT_8006769c`)
That alone already separates PSX from the naive "one file == one placement table" model.
### 2. The PSX level has at least two authored record families
The old viewer-side `region00/region01` names are no longer the best model. Live Ghidra work supports two authored families instead:
- `psx_section0_dispatch_root_records` at `DAT_80067720`
- `psx_section0_constructor_placement_records` at `DAT_800678f0`
Their roles differ:
- dispatch roots are generic runtime-object descriptors handled by per-type dispatchers
- constructor placements are tighter spawn inputs and already behave more like direct object-placement rows
This matters because it means the PSX map is not one homogeneous table of final world items. The file stores different authored row classes that the runtime interprets differently.
### 3. PSX level files also carry per-type runtime banks
`psx_load_type_state_banks` is the clearest storage-side evidence that part of the PSX level file is not map rows at all but type-local runtime support data.
Its current decompilation shows one serialized bank blob splitting into three parallel per-type lanes:
- state-script pointers into `DAT_800758cc`
- simple-component payload pointers into `DAT_800758d0`
- companion-extents pointers into `DAT_800758d4`
Separately, the template/art bank `DAT_800758d8` is assigned through its own late descriptor stream, not through the same small early-table hypothesis that was used in earlier viewer work.
So PSX storage combines:
- authored rows that say what kinds of things the level contains
- per-type banks that say how those things behave or present at runtime
That is a stronger sign of a bundle-plus-runtime-data model than of a direct item-grid serialization.
### 4. The PSX level contains a separate decompressed state lane
The current `FUN_8003b00c` read is also important. It is a sliding-window decompressor that inflates one source blob into `DAT_8006769c`.
Current best read:
- the decompressed size target is `0x3e00`
- this lane is separate from the late art bank and separate from the `psx_load_type_state_banks` blobs
- it therefore looks like additional runtime/state substrate, not just another view of the same authored placement rows
That makes the PSX storage model even less like a flat map table. The file appears to contain both authored placement-like records and a second prepacked runtime-state component that is unpacked during load.
### 5. PSX rendering does not consume the authored selector byte directly
The storage model is also constrained by the runtime draw path.
Current verified chain:
- constructors write authored coordinates into `obj+0x3c/+0x40/+0x44` as `16.16` fixed-point
- constructors preserve the original authored source-record pointer at `obj+0xa0`
- `psx_object_select_state_script` seeds the initial live script from `DAT_800758cc`
- `psx_object_advance_state_script` can advance that script and trigger sentinel-driven control flow
- some families can later reseat the active script from motion/heading logic
- final draw uses the current live state word, not just the original placement selector
That means part of what looks like "map appearance" on PSX is not stored directly as final art in the level rows. The level rows seed object creation, but runtime script/state logic still decides the final visible frame or variant in many cases.
## What The PSX `LSET*.WDL` Probably Contains
Current best file-level model for a retail level bundle:
1. Fixed top-level header.
2. Audio/SPU-related blob near the front.
3. One or more authored/map candidate regions.
4. Small control/index region.
5. Large late graphics bank region.
6. Detached and optionally decompressed runtime-state payloads referenced by the loader.
For `LSET1/L0.WDL`, the currently validated carve is:
- `audio_or_spu_blob` at `0x34 .. 0x7010`
- `post_audio_region_00` as a small table/directory block
- `post_audio_region_01` and `post_audio_region_02` as the strongest map/meta candidates
- `post_audio_region_04` as the strongest graphics bank candidate
For `LSET1/L1.WDL`, the same overall pattern reappears even though the raw row values do not line up one-to-one at the same offsets.
That cross-file repeatability is one of the main reasons the current PSX storage model is credible.
## How This Seems To Differ From The PC Build
## 1. PC evidence points to direct world-state tables
The PC notes show much more direct world-state storage in runtime memory:
- `0x7ded`: map X coordinate array
- `0x7df1`: map Y coordinate array
- `0x7df5`: map Z array
- `0x7df9`: entity state array
- `0x7e1e`: entity type table
The PC renderer and camera code also work directly against explicit map/tile/grid structures:
- a `6x5` tile-grid spatial index at `0x846a`
- dirty/render bitmasks for tiles
- camera/scroll globals
- direct world-to-screen transforms over stable world coordinates
That looks like a more exposed runtime world model: map entries, entity tables, and visibility structures are explicit and persistent as named tables rather than being mostly reconstructed from bundle-local banks on load.
## 2. PC rendering appears shape/item centric, not bundle/state-script centric
The current PC map renderer and notes are built around:
- `TYPEFLAG.DAT` shape metadata
- world-space item coordinates
- dependency-based item sorting
- direct world-to-screen projection with stable item footpads and shape offsets
The PC-side projection note is likewise straightforward:
$$
screen_x = \frac{x - y}{4}
$$
$$
screen_y = \frac{x + y}{8} - z
$$
That is not proof that the PC game stores levels in one trivial file format, but it does show that the practical renderer/world model is closer to direct item placement plus shape metadata than what the PSX executable is doing.
By contrast, PSX presentation currently depends on:
- authored placement rows
- constructor family choice
- per-type art bank
- per-type state-script bank
- live script advancement and possible reselection
- palette override bytes stored in the original authored record
- one of two world-facing render lanes
So the PSX runtime path is more layered even before final draw.
## 3. PC map state looks less self-contained inside one level asset bundle
Current PSX evidence suggests a single `LSET*.WDL` level bundle carries many classes of data together:
- placement-like authored rows
- late sprite/graphics bank
- type-specific runtime banks
- audio-related blob
- detached and decompressed runtime-state data
The PC notes, by comparison, point toward a more distributed asset model:
- map/world coordinates in explicit arrays
- shape/type metadata from files such as `TYPEFLAG.DAT`
- draw sorting and visibility handled over stable world entries
So the practical difference is not just "PlayStation uses WDL too". It is that the PSX `LSET` files appear to be denser, more self-contained level packages, while the PC game seems to expose a more table-oriented world model with auxiliary metadata coming from other resources.
## 4. PSX map appearance is more runtime-derived than PC map appearance
Current PSX evidence shows a stronger distinction between:
- what the level file stores directly
- what the live runtime derives after object creation
Examples:
- the authored selector byte is only the start of state resolution
- `DAT_800758d4` now reads as companion extents for collision/contact logic, not the missing direct art table
- heading-based or target-based reselection can overwrite the live state used for visible resource/frame choice
- some control-script paths can reroute camera behavior or queue deferred world changes after spawn
The PC evidence does not currently suggest the same degree of post-spawn reinterpretation for ordinary map storage. The DOS/Windows build still has rich runtime behavior, but the world representation looks closer to direct entries with stable positions, shape references, and tile-based visibility.
## What I Think The PSX Loader Is Really Doing
Current best reconstruction:
1. Open `SPEC_A.WDL` and selected `LSET*.WDL`.
2. Read the bundle header and section-size table.
3. Load palette/audio/front matter.
4. Materialize authored level streams into runtime pointers such as `DAT_800678f4`, `DAT_80067720`, and `DAT_800678f0`.
5. Load or override per-type banks into `DAT_800758cc/d0/d4`, plus the late art-template bank into `DAT_800758d8`.
6. Load an additional detached blob and optionally decompress a runtime-state payload into `DAT_8006769c`.
7. Dispatch the authored rows through constructor/dispatcher families to build live objects.
8. Let runtime state scripts, motion reselection, and palette override bytes determine final visible presentation.
That model fits more of the verified evidence than either of the older simplified ideas:
- "the first small record stream is the whole map"
- "there is one flat type-to-bundle table that directly explains all visible art"
## Confidence And Open Questions
High confidence:
- `LSET*.WDL` is a structured multi-section level bundle
- the PSX level uses at least two authored record families
- `DAT_800758d8/d0/cc/d4` are runtime per-type banks, not one homogeneous table
- there is a separate decompressed state lane at `DAT_8006769c`
- PSX visible art is not chosen from one flat authored selector byte alone
- PC runtime evidence exposes direct map coordinate arrays and a tile-grid visibility system
Medium confidence:
- `post_audio_region_01` and `post_audio_region_02` are the main authored/map metadata regions
- the PSX build is materially more self-contained per level bundle than the PC build
- the remaining placeholder-heavy PSX families fail mainly because the final live state-to-art bridge is still incomplete, not because the whole storage model is wrong
Open questions:
- exact semantics of `post_audio_region_01` versus `post_audio_region_02`
- exact on-disk mapping for every runtime destination loaded by the bundle loader
- exact role of `DAT_8006769c` in ordinary map logic versus specialized level/session state
- exact family-specific rules that map live script state to drawable resource/frame for unresolved types such as `0x0042` and `0x0049`
- how closely any PSX authored record family corresponds to a specific PC map file structure, if at all
## 2026-04-12 Live Bank-Role Clarification (SLUS_002.68)
This pass focused only on runtime-bank and art/resource install semantics in the live MCP session.
- `0x8003917c` renamed to `psx_install_type_state_script_component_extents_banks`
- `0x80045ffc` renamed to `psx_install_type_art_active_header_and_built_resource`
- The bank globals are now explicitly separated by role:
- `psx_type_art_active_header_bank` (`0x800758d8`) = current per-type art lane written by header installs and art install helper
- `psx_type_art_built_resource_bank` (`0x800758c8`) = resolved/reused built resource pointer lane (kind-specific)
- `psx_type_state_script_bank` (`0x800758cc`) = per-type state-script stream base
- `psx_type_simple_component_bank` (`0x800758d0`) = per-type simple behavior/component payload base
- `psx_type_companion_extents_bank` (`0x800758d4`) = per-type companion extents payload base
- `psx_type_policy_table_ptr` (`0x800675f8`) = per-level policy bit table consumed by interaction/order/draw logic
Loader-to-renderer chain tightened with comments at:
- art install loop callsites `0x800396cc` and `0x800399b4`
- state-bank install callsites `0x8003970c`, `0x800399f4`, and `0x80039ad0`
- active-header table write lane `0x8003977c`
- built resource + mirrored active lane commits `0x800460c8` and `0x800460d4`
- policy table install/read bridge `0x800398f0` and `0x80041604`
Practical exporter implication: per-type art binding should treat active-header and built-resource lanes as distinct install phases that can alias after commit, while piece loading should continue to source behavior/script/extents independently from the state-bank lanes rather than assuming one fused per-type blob.
## 2026-04-12 Live Marker/Control Runtime-Island Clarification (SLUS_002.68)
This pass focused on the post-load marker/control/runtime island around `psx_selector_to_map_id_table` (`0x80063e54`), `psx_map_id_to_gate_slot_table` (`0x80063e68`), and `psx_marker_channel_runtime_block` (`0x800675ec`).
Direct live outcomes:
- Confirmed post-load sequencing in `psx_level_post_load_runtime_reset` (`0x80039ef4`):
- restore or mode-action `8`, optional reciprocal selector/map gate check before mode-action `2`, always mode-action `4`, then `psx_marker_channel_runtime_state_snapshot`.
- The selector/map pair is a control-gating table pair, not art binding:
- `psx_selector_to_map_id_table` is consumed by passcode/apply and post-load checks.
- `psx_map_id_to_gate_slot_table` is consumed by post-load and section0/slot dispatch lanes (`0x8002153c/0x800215b4`, `0x80020fbc`, `0x8002f2a0`, `0x8002f7f4`).
- Confirmed runtime-block persistence semantics:
- `psx_marker_channel_runtime_state_snapshot` (`0x80031878`) packs mode/step/flags with sentinels.
- `psx_marker_channel_runtime_state_restore` (`0x80031a3c`) restores only on sentinel match, else falls back to reset helpers.
Live naming/comment deltas in this pass:
- `0x80030ed4` -> `psx_marker_channel_event_queue_reset`
- `0x80030cf0` -> `psx_marker_channel_event_queue_accumulate`
- `0x80030dfc` -> `psx_marker_channel_event_queue_remove_at`
- `0x80030ebc` -> `psx_marker_channel_event_queue_get_buffer_and_count`
- `0x80031738` -> `psx_marker_channel_get_condition_value`
- `0x80067798` -> `psx_marker_channel_event_queue_count`
- `0x8008f384` -> `psx_marker_channel_event_queue_entries`
- Added durability comments at `0x8002f190`, `0x800311c4`, `0x8002f7f4`, `0x80039fe8`, `0x80031878`, `0x80031a3c`, and `0x80031738`.
Practical exporter/runtimeDiagnostic implication:
- Treat this island as runtime control-state and channel gating, not direct map-art selection.
- If a diagnostic export wants stable post-load presentation behavior, include:
- reciprocal selector/map-gate state,
- runtime-block snapshot/restore validity,
- and queued packed marker-action state (event queue count and entries) because mode-action branches can repopulate and consume that queue around post-load transitions.
## Detailed Extraction Plan: PSX Map To PC-Like Render Input
The goal here is not to prove that the PSX data really is the same as the PC format. It is to extract a PSX level into a render-oriented structure that is similar enough to the PC pipeline to reuse the existing renderer architecture where that makes sense.
The safest target is therefore a two-layer export:
- a PC-like flattened render layer for practical drawing
- a reversible PSX evidence layer that preserves the original bundle-driven/runtime-driven structure
That keeps the render path usable without throwing away the information needed to correct mistakes later.
### Target Output Shape
The export should produce one per-map artifact with three conceptual parts.
### 1. Map header
Minimum fields:
- source file path such as `LSET1/L0.WDL`
- bundle header summary and section boundaries
- palette source metadata
- unresolved-confidence flags
- exporter version and evidence notes
### 2. PC-like render item list
This is the part meant to resemble the existing PC renderer input.
Each exported render item should ideally contain:
- stable item id
- `type`
- world `x/y/z`
- projected `screen_x/screen_y` or full screen rect when known
- bundle/frame reference or resolved art reference
- palette index or override when known
- render lane (`stage1` or `stage2` when known)
- approximate draw-order key or dependency metadata
- coarse shape/bounds for debugging and future occlusion work
This layer is the nearest PSX equivalent to the PC renderer's world item list.
### 3. PSX evidence layer
Each item in the flattened render list should still point back to the original PSX evidence used to build it.
Useful fields:
- source authored family: `section0_dispatch_roots` or `section0_constructor_placements`
- source file offset or runtime pointer equivalent
- raw row bytes or decoded raw words
- original selector byte
- chosen live script word if simulated
- resolved `DAT_800758cc/d0/d4/d8` bank references
- companion extents from `DAT_800758d4`
- original source-record bytes used for palette override reads
- whether the item is direct, inferred, or still provisional
Without this layer, the flattened format will become impossible to repair once new runtime evidence lands.
## Extraction Phases
### Phase 1: file-level bundle extraction
Input:
- selected `LSET*.WDL`
- any required shared companion bundle such as `SPEC_A.WDL`
Required outputs:
- section header table
- raw post-audio region boundaries
- detached blob candidates
- late graphics bank region
- candidate compressed-state source blob
Implementation notes:
- treat the file as a bundle with explicit section boundaries, not as one raw placement blob
- record all offsets and sizes in the export header even if the semantics are still provisional
- keep both raw offsets and normalized region-relative offsets
Success condition:
- the exporter can reproduce the known `L0.WDL` and `L1.WDL` boundary pattern consistently
### Phase 2: authored-row family extraction
Input:
- the regions currently believed to feed `DAT_80067720` and `DAT_800678f0`
Required outputs:
- decoded `section0_dispatch_roots`
- decoded `section0_constructor_placements`
- family-local row counts
- raw row byte snapshots for each record
Implementation notes:
- keep the two authored families separate all the way through export
- do not collapse them into a single inferred placement table yet
- include per-row type, authored selector, authored `x/y/z`, flags, and file offsets when available
Success condition:
- the row counts and type distributions are stable across multiple maps and match the current live viewer/export evidence
### Phase 3: runtime-bank extraction
Input:
- the bank blobs that feed `DAT_800758d8/d0/cc/d4`
Required outputs:
- per-type template/art descriptors from `DAT_800758d8`
- per-type simple payloads from `DAT_800758d0`
- per-type state-script descriptors from `DAT_800758cc`
- per-type companion extents from `DAT_800758d4`
Implementation notes:
- preserve the exact source region for each bank
- keep unresolved bank sub-splits explicit instead of silently normalizing them
- export the companion extents as runtime-bounds metadata, not as final art selectors
Success condition:
- a consumer can look up all currently known bank lanes by type from the exported metadata without rereading the raw bundle
### Phase 4: detached and decompressed state extraction
Input:
- detached blob source for `DAT_8006767c`
- compressed source that inflates into `DAT_8006769c`
Required outputs:
- raw detached blob dump
- decompressed `DAT_8006769c` equivalent
- metadata that identifies the source offset, compressed size, and output size
Implementation notes:
- treat this as a separate lane from authored rows and per-type banks
- do not flatten this into the item list unless a specific consumer proves how it maps to items
- keep checksums or hashes so later passes can confirm reproducibility
Success condition:
- the exporter can regenerate the same decompressed payload for repeated runs on the same map
### Phase 5: constructor-faithful object seeding
Input:
- authored-row families
- per-type banks
Required outputs:
- one provisional live object per constructor-backed authored row
- separate dispatch-root-backed provisional objects when the type path supports them
Minimum object fields:
- `type`
- authored `x/y/z`
- `world_x/world_y/world_z` in PSX fixed-point form when known
- original selector byte
- initial live script reference
- resource/template reference
- source authored family and source row offset
- palette override source bytes
Implementation notes:
- keep both raw authored coordinates and normalized runtime coordinates
- preserve the original source-record pointer semantics in metadata even though there is no literal RAM pointer in offline export
- mark whether an object came from direct constructor logic or from a more generic dispatch-root path
Success condition:
- the exported object model is rich enough to simulate initial state selection and later art/frame resolution without re-reading raw rows
### Phase 6: initial live-state simulation
Input:
- seeded objects
- `DAT_800758cc` state-script bank
Required outputs:
- initial live script word per object
- script-sentinel interpretation trace where available
- any immediate reselection or control-side effects that happen before the object becomes visible
Implementation notes:
- this phase should be deterministic and conservative
- only apply behavior that is currently proven from executable evidence
- keep a trace record that shows whether the chosen state came directly from the authored selector or from a later reselection rule
Success condition:
- for solved families, the chosen script word matches current executable-backed expectations
### Phase 7: art and palette resolution
Input:
- seeded live objects
- resolved live script words
- template bank, source-record bytes, and known palette override rules
Required outputs:
- chosen resource or bundle reference
- chosen frame index when known
- chosen palette index or override byte when known
- confidence label: `verified`, `family-rule`, or `provisional`
Implementation notes:
- use narrow family-specific rules only where current evidence supports them
- do not use `DAT_800758d4` as a fallback art selector
- preserve both default palette assumptions and authored override bytes so color work stays reversible
Success condition:
- the export produces a partially solved but clearly labeled art assignment rather than a misleadingly "complete" one built on weak heuristics
### Phase 8: projection into a PC-like item list
Input:
- resolved or provisional live objects
Required outputs:
- flattened render items with PC-like world coordinates and draw metadata
Projection rules already supported by current evidence:
- `screen_x = y - x`
- `screen_y = 2*z - (x + y)/2`
Implementation notes:
- keep stage-1 and stage-2 lane identity in metadata even if both are drawn into one final frame in the first renderer pass
- include screen rectangles when known because the runtime already caches them at `obj+0x20..+0x2e`
- include debug fields that show both raw PSX coordinates and PC-like renderer coordinates
Success condition:
- the flattened list can be fed through the existing renderer with only PSX-specific sort/projection differences layered on top
### Phase 9: render-order approximation
Input:
- flattened item list
- render lane metadata
- available bounds and screen rectangles
Required outputs:
- stable per-item sort keys or dependency edges
Implementation notes:
- do not assume simple `y` sorting is enough
- use whatever executable-backed visible-list ordering facts are currently known
- keep dependency metadata explicit so the renderer can later upgrade from scalar sort keys to graph ordering if needed
Success condition:
- the rendered scene is stable and debuggable even when some art remains provisional
### Phase 10: validation against the PC-like renderer contract
Input:
- final flattened item list
- evidence layer and raw extraction metadata
Required outputs:
- scene JSON or equivalent artifact ready for the renderer
- validation report listing which items are verified, inferred, or unresolved
Minimum validation checks:
- row counts are reproducible across runs
- extracted banks and decompressed blobs are reproducible across runs
- constructor-placement `z` values produce the expected multi-level output rather than a flat plane
- known solved art families map to the same bundle/frame choices as the current viewer evidence
- stage-1 versus stage-2 objects remain distinguishable in metadata
## Recommended Export Format
The closest useful analogue to the PC renderer input is a scene format with:
- `items`: flattened render items
- `mapSource`: original PSX authored-row metadata
- `stateLayers`: decoded per-type runtime banks
- `bundleInfo`: resolved art/palette references
- `evidence`: loader boundaries, decompression metadata, confidence flags, and unresolved notes
If the renderer needs something even closer to the PC item list, derive it as a second view rather than making it the only exported representation.
Good compromise:
- primary export: reversible PSX scene JSON
- secondary export: flattened PC-like render list derived from that scene JSON
## What "Similar To PC" Should Mean In Practice
The output should be similar to the PC format in renderer ergonomics, not in pretending the source data matches structurally.
That means:
- one row per drawable or potentially drawable object
- stable world coordinates
- stable art/frame/palette fields when known
- enough bounds and sort metadata for a normal scene renderer
- enough source metadata to trace every rendered item back to its PSX origin
It should not mean:
- forcing all PSX authored families into one fake item table
- discarding stage/lane distinctions
- flattening away state-script provenance
- treating unresolved art heuristics as solved data
## Short Practical Sequence
If this had to be implemented in the current codebase as the next extraction push, the order should be:
1. Stabilize section-boundary extraction for all `LSET` maps.
2. Export `section0_dispatch_roots` and `section0_constructor_placements` as separate row families with raw bytes.
3. Export `DAT_800758d8/d0/cc/d4` into `stateLayers` with exact source offsets.
4. Export the detached and decompressed state lanes separately.
5. Build a constructor-faithful provisional object list.
6. Simulate only the currently verified state-selection rules.
7. Resolve only the currently verified art and palette rules.
8. Flatten into a PC-like render list while keeping evidence links back to the PSX source.
9. Render with explicit unresolved markers instead of aggressive heuristics.
10. Iterate family by family until the provisional cases shrink.
## Practical Conclusion
If the goal is to reproduce PSX maps faithfully, the safest current approach is:
- treat `LSET*.WDL` as a bundle, not a flat placement file
- keep authored record families separate
- keep type banks separate from placement data
- keep the decompressed state lane separate from both
- model the runtime as object construction followed by script/state-driven presentation
- compare against PC at the level of architecture, not by forcing PSX rows into the PC item-table mental model
The strongest current difference is therefore architectural rather than cosmetic:
- PC looks like a more direct world/item/tile representation with auxiliary type metadata
- PSX looks like a bundled authored-record plus runtime-bank package that must be interpreted through constructors and state-script logic before the final map image exists

View file

@ -28,22 +28,53 @@
- One renderer-side mismatch is now closed: PSX sprites use authored `item.screen` rectangles, and the bounding/highlight overlay path now uses those same authored rectangles instead of recomputing a DOS-style wireframe from provisional `world` coordinates. - One renderer-side mismatch is now closed: PSX sprites use authored `item.screen` rectangles, and the bounding/highlight overlay path now uses those same authored rectangles instead of recomputing a DOS-style wireframe from provisional `world` coordinates.
- The executable now closes the last projection stage: authored object coordinates land in object fields `+0x3c/+0x40/+0x44` as `16.16` fixed-point values, and `FUN_80040d44` / `FUN_80040f78` project them with `screen_x = y - x` and `screen_y = 2*z - (x + y)/2` before writing the final screen rectangle at `+0x20..+0x2e`. - The executable now closes the last projection stage: authored object coordinates land in object fields `+0x3c/+0x40/+0x44` as `16.16` fixed-point values, and `FUN_80040d44` / `FUN_80040f78` project them with `screen_x = y - x` and `screen_y = 2*z - (x + y)/2` before writing the final screen rectangle at `+0x20..+0x2e`.
- Palette handling is partially grounded by runtime VRAM evidence, but the per-placement override rule is still missing. - Palette handling is partially grounded by runtime VRAM evidence, but the per-placement override rule is still missing.
- Palette override provenance is now tighter than that older summary: `psx_draw_main_visible_object` only applies authored override bytes for visible type bands `0x003e..0x00ab` and `>= 0x00ac`, and it reads them from different source-record words (`source+0x06` high byte for the earlier band, `source+0x0c` high byte for the later band). Types below `0x003e` do not take that override path in the main visible helper.
- The render-lane split matters for color too, not just visibility. `psx_draw_special_visible_queue` reuses the same frame/resource submitters as the main visible pass but does not apply the authored palette override byte at all, so any exporter rule that assumes one palette path for all world-facing objects is now too broad.
- The submitters themselves also separate the palette story by resource class. `psx_sprite_resource_submit_frame` and `psx_image_table_submit_frame` both consume the same high-byte override token, but they translate it through different CLUT lookup tables depending on the bound resource kind, so the remaining viewer bug is not just `which palette index` but `which palette table for which resource class and render lane`.
- The constructor/resource side is narrower now too. Both `psx_object_create_simple_record` and `psx_object_create_compound_record` resolve the drawable resource at spawn time from `DAT_800758d8` before the object enters the live update loop, so the remaining repeated-wall problem is no longer best framed as a missing late resource allocator. The open bridge is the family-specific post-spawn state/frame route plus the correct lane-aware palette policy.
- The scene/cache naming now uses executable-backed family names (`section0_dispatch_roots`, `section0_constructor_placements`) with the old `region00/region01` labels kept only as legacy aliases. - The scene/cache naming now uses executable-backed family names (`section0_dispatch_roots`, `section0_constructor_placements`) with the old `region00/region01` labels kept only as legacy aliases.
- The offline `FUN_8003b00c` path now exists in the renderer-local exporter and serializes one candidate on-disk compressed source plus the decoded `0x3e00` state buffer into the cache for each map. - The offline `FUN_8003b00c` path now exists in the renderer-local exporter and serializes one candidate on-disk compressed source plus the decoded `0x3e00` state buffer into the cache for each map.
- The type-to-art pass is still open. The exporter now scans parsed per-type template-bank payloads for bundle references, and it no longer promotes the disproven scan-order bundle fallback into visible map art. Unverified types stay on placeholders until the executable state/type path yields a real art binding. - The type-to-art pass is still open. The exporter now scans parsed per-type template-bank payloads for bundle references, and it no longer promotes the disproven scan-order bundle fallback into visible map art. Unverified types stay on placeholders until the executable state/type path yields a real art binding.
- That loader-shaped bank selection is now already paying off in the live cache: map `9` moved from `0` resolved bundle-mapped items to `111` after the template pass switched to the embedded late-section parse, even though unresolved root-dispatch families such as `0x0042` and `0x0049` still need the downstream state/variant path before they can stop using placeholders. - That loader-shaped bank selection is now already paying off in the live cache: map `9` moved from `0` resolved bundle-mapped items to `111` after the template pass switched to the embedded late-section parse, even though unresolved root-dispatch families such as `0x0042` and `0x0049` still need the downstream state/variant path before they can stop using placeholders.
- Current status: the PSX viewer output is still unreadable as a practical map. The exported placements and projection are good enough to sketch room massing, but most visible section-0 families still lack the executable's final state/variant-driven art binding and therefore remain on placeholders. - Current status: the focused PSX exporter is no longer placeholder-driven on `map 104`. Placements and projection remain structurally correct, and the current cache now resolves all `1002` records to actual PSX bundle art, but many of those bindings are still provisional donor selections rather than final executable-proved family/resource closure.
- A focused art-binding recovery pass is now landed in the cache builder too; see `docs/psx/art-binding-recovery.md` for the measured recovery note. The exporter now treats many zero-block `DAT_800758d8` constructor-placement types as inherited-art candidates instead of dropping directly to placeholders, first via same-map `DAT_800758cc` script-signature donors and then via a constrained nearest-donor fallback inside the current `0x003e..0x0064` constructor-placement family band. - A focused art-binding recovery pass is now landed in the cache builder too; see `docs/psx/art-binding-recovery.md` for the measured recovery note. The exporter now treats many zero-block `DAT_800758d8` constructor-placement types as inherited-art candidates instead of dropping directly to placeholders, first via same-map `DAT_800758cc` script-signature donors and then via a constrained nearest-donor fallback inside the current `0x003e..0x0064` constructor-placement family band.
- That heuristic materially improved the built cache even though it is still provisional rather than executable-proved. The latest rebuild moved the scene set from `58,262` fallback items / `1,714` bundle-mapped items down to `25,038` fallback items / `34,938` bundle-mapped items. Representative maps such as `0`, `9`, and `43` are now mostly real-art scenes, while `map 104` remains the clear outlier with `866` fallback items versus only `136` bundle-mapped items. - That heuristic materially improved the built cache even though it is still provisional rather than executable-proved. The latest rebuild moved the scene set from `58,262` fallback items / `1,714` bundle-mapped items down to `25,038` fallback items / `34,938` bundle-mapped items. Representative maps such as `0`, `9`, and `43` are now mostly real-art scenes, while `map 104` remains the clear outlier with `866` fallback items versus only `136` bundle-mapped items.
- The newest cache slice shows `map 104` is failing in a more specific way than “one unresolved type keeps using one bad wall.” The repeated `0x0042` wall cluster currently spans both `section0_constructor_placements` (`u5=0x0020`) and `section0_dispatch_roots` (`u5=0x0030`, plus smaller `0x0022` outliers), yet all of those records still collapse onto donor map `85` type `0x0040` bundle `0x0009d304` with palette `0`. That means the current donor recovery is over-merging at least two authored/runtime roles before the executable-backed family/resource split is proven.
- The viewer/exporter now has a stronger follow-up for that exact failure mode. Provisional donor matches in the unresolved generic family still do not apply as one coarse mixed-role `map:type` default, but the cache builder now resolves those rows per authored-family plus raw-`u5` cohort and emits actual PSX art instead of synthetic fallback atlases. Focused `map 104` validation now exports `52` `section0_dispatch_roots-art` items plus `950` `section0_constructor_placements-art` items with `0` fallback records, `1` atlas, and `136` shape definitions.
- That no-placeholder pass is intentionally provenance-heavy instead of pretending closure. Scene `mapSource` rows now preserve `mappingSource` and optional `artCohort`, so wrong-art cases can be audited as `cohort-*` or `emergency-global-donor:*` matches without reintroducing placeholders into the live renderer.
- The latest six-track Ghidra pass narrows that unresolved split further. `0x0042` does not have a unique descriptor entry; it shares the same generic descriptor object as the wider `0x003e..0x0050` band. For constructor-placement `0x0042`, the strongest current path is now explicit: compound spawn wrapper -> `psx_object_create_compound_record` -> `psx_object_advance_state_script` -> main-visible refresh/project -> stage-1 draw. For root-dispatch `0x0042`, the dominant `u5=0x0030` band and smaller `u5=0x0022` outliers currently still look like the same broad main-visible runtime role, with `0x0002` acting more like an orientation/extents variant than a separate lane.
- The same pass also closes the submitter side enough to constrain the next art-binding experiment. In both world-facing lanes, draw-time submitter choice is based on the bound resource header kind: `kind == 5` uses `psx_image_table_submit_frame`, otherwise the executable uses `psx_sprite_resource_submit_frame`. The HUD/overlay lane is now explicitly separated from that rule for viewer purposes: it can route by overlay-slot policy instead of normal world-object kind checks, and `psx_draw_clock_digits_overlay` is a fixed image-table user rather than evidence for map-object family binding.
- The newest six-track pass tightens the route-state side as well. Constructor and root-dispatch records both hand their authored lane/flags word into `obj+0x1c`; current best read is `0x0020 = broad world-visible route gate`, `0x0002 = orientation/extents-axis behavior`, and `0x0400 = stage-2 special-visible selector`. That means the current `u5=0x0030` versus `0x0022` split is still better treated as same-family main-visible routing with different orientation behavior, while the actual stage-1 versus stage-2 split should now be sampled through `obj+0x1c` bit `0x0400` instead of inferred from raw `u5` alone.
- The selector side is narrower now too. `psx_object_select_state_from_transition_table` (`DAT_80063b4c` row indexed by `type - 0x1e`) is now a concrete per-type selector source ahead of `psx_object_select_state_script`, and `psx_object_update_interaction_transition` shows one forced-selector path that drives helper objects to selector `3` unless they are already in `1` or `3`. So the next `0x0042` pass should correlate `type 0x0042` transition-table output with the later `obj+0x94` live script word instead of treating selector `3/4` only as a generic post-spawn mystery.
- The newest follow-up narrows `0x0042` selector recovery again. Type `0x0042` now has its own transition helper, `psx_type42_transition_selector_tick`, and that helper can dispatch low turning selectors `3/4` before the `+0x94`-style runtime latch copy. The current `DAT_80063b4c` row for type `0x0042` mostly yields higher script selectors, so selector `3/4` cases now look more like pre-latch turn/reseat outcomes than direct row literals.
- The exact descriptor slot is now closed too: `psx_type_descriptor_table[0x0042]` at `0x80063220` points to the shared row `0x800626f8`, whose create/update/release callbacks are `psx_spawn_compound_record_advance_state_once`, `psx_object_refresh_main_visible_and_cleanup`, and `psx_object_release_to_free_list`. Both section-0 constructor placements and root-dispatch rows still enter type `0x0042` through that same initial descriptor family.
- The per-type art/cache lane is narrower too. `DAT_800758d8` and `DAT_800758c8` no longer read as a simple immutable descriptor table plus separate cache; the active helper first seeds `DAT_800758d8[type]` with the incoming art header, then writes the built resource pointer back to both slots. So the remaining `0x0042` question is still runtime family/resource selection, but the notes should treat the pair as active per-type art state rather than as a permanently raw-versus-cached split.
- The newest follow-up tightens those two lanes again. The live names now reflect the stronger art/cache read: `DAT_800758d8 = psx_type_art_active_header_bank` and `DAT_800758c8 = psx_type_art_built_resource_bank`. On the selector side, `psx_type42_transition_selector_tick` now has one more concrete gate: it first checks `psx_object_is_within_view_margin`, then can remap a bucket from `psx_object_compute_heading_selector_to_focus` and dispatch selector `3/4` before the later latch copy. So the next runtime sample should include both the pre-latch dispatch state and the later `obj+0x94`-style latch, not just one or the other.
- The route-bit writer question also narrowed, but is not fully closed yet. The recovered anonymous islands now prove that `0x0400` is written in the wider nested runtime state machine (`psx_object_state_machine_dispatch_tick`) and that `psx_object_handle_control_pair_0a` mutates related policy state plus object-local bit `0x0200`, but this batch still did not pin a direct object-local `obj+0x1c |= 0x0400` writer for `0x0042`. That keeps the next concrete sample focused on runtime state plus object-local flags rather than assuming the stage-2 bit is only an authored `u5` literal.
- Step 2 is now landed on the viewer side as well. `map_renderer/src/lib/psx-cache.js` now exports a per-item `runtimeDiagnostic` payload in scene version `psx-runtime-record-probe-v10`, carrying the split the last Ghidra passes converged on: object-local route flags, selector seed/pre-latch hints, exporter-side latched-state candidate, nested-runtime placeholders, resource-kind hints, and a placeholder slot for the live `DAT_800675f8` policy word. The immediate next Ghidra work should now fill those channels for representative `map 104` `0x0042` items rather than redefining the channel model again.
- That first concrete `runtimeDiagnostic` pass is now done too. `item:25`/`35` (root `0x0022`), `item:30`/`31` (root `0x0030`), `item:85`/`86` (constructor `0x0030`), and control `item:53` now form the fixed `map 104` sample pack for future `0x0042` checks instead of broad family-level prose.
- The family bridge is explicit now: `psx_dispatch_section0_dispatch_roots` and `psx_dispatch_section0_constructor_placements` still converge through the same shared `0x0042` descriptor row into `psx_spawn_compound_record_advance_state_once`, then the shared constructor/state path. Family identity still matters as an exporter fence, but it is not a different descriptor family.
- `runtimeDiagnostic.objectLocalRouteFlags` is better grounded too. Constructors directly copy the authored lane word into `obj+0x1c`, so the exported `initialWord` values are real initial authored state. For the current sample pack, `0x0022` is authored `{ 0x0020, 0x0002 }`, while `0x0030` is authored `{ 0x0020, 0x0010 }`.
- `runtimeDiagnostic.selector` also stays justified as a separate channel from the later latch. Spawn-side selector seeding now has a named bridge through `psx_transition_spawn_and_seed_selector_from_record`, but `0x0042` still uses `psx_type42_transition_selector_tick` to emit selector `3/4` before the later latch copy.
- `runtimeDiagnostic.nestedRuntimeState` remains necessary too. The strongest recovered `0x0400` stage-selection write is still nested-state-side, and this pass still did not pin a direct object-local `obj+0x1c |= 0x0400` writer for `0x0042`.
- `runtimeDiagnostic.typePolicy` is narrower now as well. `DAT_800675f8` is now explicitly `psx_type_policy_table_ptr`, installed during level load and then read by type-indexed consumers, so the eventual numeric policy word should be treated as type-global within the loaded level rather than as a per-lane value.
- The next follow-up widened the same concrete sample pack in three practical directions: resource identity, frame-state flow, and explicit routing branches. The strongest current read is still that `map 104` `0x0042` is not yet split by a different descriptor family or an obvious different bind path; the remaining unresolved split is now mostly `bound resource identity` plus `live frame/state token` plus `object-local 0x0400 at route time`.
- The fixed `map 104` sample pack should therefore stay frozen for the next pass: root `0x0022` = `item:25/35`, root `0x0030` = `item:30/31`, constructor `0x0030` = `item:85/86`, control `0x0066` = `item:53`.
- The storage-side section family is tighter now too. The latest pass promotes `DAT_80067938` to `psx_ctor_placement_section_ptr`: a constructor-placement section installed during `wdl_resource_bundle_load_by_index` whose rows still read as six-halfword `0x0c`-stride records `[type, x, y, z, selector, u5]`. That strengthens the current read that map-object construction is fed from explicit section-pack row families rather than from one opaque monolith.
- Adjacent section-pack pointers are narrower as well. `DAT_80067838` is now at least a real level section base in the same install family rather than an unexplained scalar, `DAT_80067840` now reads as a `resource_bundle_ptr_table` with control/opcode-stream consumers, and `DAT_800676d8` now reads as `level_clut_table`, which keeps it on the palette/CLUT side instead of the missing placement/art side.
- The decompressed lane is tighter too. `DAT_8006769c` is now confirmed again as `psx_level_decompressed_state_buffer`, written during `wdl_resource_bundle_load_by_index`, passed to `psx_lzss_unpack_into_level_buffer`, and then reused by save/runtime snapshot code. Current best read is that it primarily seeds runtime-bank installs and broader session/runtime state rather than acting as one hidden flat item table.
- The `0x80063e54/0x80063e68/0x800675ec` control island is now promoted from background curiosity to an active blocker family. `0x800675ec` is now `psx_marker_channel_runtime_block`, with the strongest current field map at `+0x34` mode byte, `+0x6c` mode-step byte, and `+0x88/+0x8c` short fields that are still the most plausible presentation-adjacent members. That family still needs an explicit struct/table dump before it can be ruled in or out for map-facing `0x0042` divergence.
- Loader ownership is tighter too: `psx_load_type_state_banks` now reads as the `DAT_800758cc/d0/d4` installer only, `psx_stream_install_type_runtime_banks` is the packed-stream helper that can install all four banks including `DAT_800758d8`, and the `DAT_80067794` header block now reads as save/transition state through `psx_snapshot_level_runtime_header_block` / `psx_apply_level_runtime_header_block`, not as the missing `0x0042` art-binding source.
- The old fallback art binding is now positively disproven for map rendering, not just "still unverified": in the live cache, early `section0_dispatch_roots` types `0x0042` and `0x0049` repeatedly bind to portrait/talk-animation bundles (for example map `0` offsets `0x000B2970` and `0x000D84F4`), which confirms the section-0 dispatch rows are generic runtime-object descriptors whose visible art still depends on downstream per-type state/variant selection. - The old fallback art binding is now positively disproven for map rendering, not just "still unverified": in the live cache, early `section0_dispatch_roots` types `0x0042` and `0x0049` repeatedly bind to portrait/talk-animation bundles (for example map `0` offsets `0x000B2970` and `0x000D84F4`), which confirms the section-0 dispatch rows are generic runtime-object descriptors whose visible art still depends on downstream per-type state/variant selection.
- The executable-side type path is now clearer and named in the live PSX Ghidra database. `psx_object_create_simple_record` and `psx_object_create_compound_record` both index the same per-type banks rooted at `DAT_800758d8/d0/cc/d4`; `psx_object_select_state_script` selects an active state script from `DAT_800758cc`, `psx_object_advance_state_script` at `0x80025d68` interprets sentinel-driven script records, `psx_object_lookup_variant_entry` resolves a companion entry from `DAT_800758d4`, and `psx_reset_type_runtime_banks_from` at `0x80025ce8` is the nearby bank-reset helper that had been misnamed earlier. So the missing map-render rule is not one flat `type -> bundle` table but a multi-stage runtime selection path. - The executable-side type path is now clearer and named in the live PSX Ghidra database. `psx_object_create_simple_record` and `psx_object_create_compound_record` both index the same per-type banks rooted at `DAT_800758d8/d0/cc/d4`; `psx_object_select_state_script` selects an active state script from `DAT_800758cc`, `psx_object_advance_state_script` at `0x80025d68` interprets sentinel-driven script records, `psx_object_lookup_variant_entry` resolves a companion entry from `DAT_800758d4`, and `psx_reset_type_runtime_banks_from` at `0x80025ce8` is the nearby bank-reset helper that had been misnamed earlier. So the missing map-render rule is not one flat `type -> bundle` table but a multi-stage runtime selection path.
- The visible render pass is less opaque now too. `FUN_80041378` draws in three stages: the sorted visible-object list through `FUN_80041458`, a second special-visible list through `FUN_80041144`, and then HUD/overlay/icon primitives through `FUN_800416cc`. That means the remaining map-viewer gap is still mainly in world-object and special-object families, not in the HUD pass. - The visible render pass is less opaque now too. `FUN_80041378` draws in three stages: the sorted visible-object list through `FUN_80041458`, a second special-visible list through `FUN_80041144`, and then HUD/overlay/icon primitives through `FUN_800416cc`. That means the remaining map-viewer gap is still mainly in world-object and special-object families, not in the HUD pass.
- That draw tail is now named more tightly in Ghidra too: `FUN_800416cc` is now `psx_draw_hud_overlay_pass`, and its clock/timer child `FUN_8004214c` is now `psx_draw_clock_digits_overlay`.
- The next decompilation target is narrower now too: `FUN_8002906c` is the highest-value follow-up because it is a verified post-construction state reselection path that can overwrite the live script choice from `FUN_8003bc1c(obj) >> 2 & 0xf`, which is exactly the missing bridge between exported placement selectors and the art-facing `DAT_800758d4` variant lookup. - The next decompilation target is narrower now too: `FUN_8002906c` is the highest-value follow-up because it is a verified post-construction state reselection path that can overwrite the live script choice from `FUN_8003bc1c(obj) >> 2 & 0xf`, which is exactly the missing bridge between exported placement selectors and the art-facing `DAT_800758d4` variant lookup.
- The stage-2 path is now strong enough to affect renderer planning directly. `FUN_80040f78` is the queue-builder for the `FUN_80041144` pass: it projects an object just like the main `FUN_80040d44` path but appends it to `DAT_80078b70` / `DAT_80067472` instead of the main `DAT_8006ad5c` visible list. So a renderer that only models the stage-1 visible list will still miss a real world-facing object lane. - The stage-2 path is now strong enough to affect renderer planning directly. `FUN_80040f78` is the queue-builder for the `FUN_80041144` pass: it projects an object just like the main `FUN_80040d44` path but appends it to `DAT_80078b70` / `DAT_80067472` instead of the main `DAT_8006ad5c` visible list. So a renderer that only models the stage-1 visible list will still miss a real world-facing object lane.
- The broader lifecycle is readable now too. `psx_level_session_loop` is the outer level-session loop; `wdl_resource_bundle_load_by_index` performs the actual `SPEC_A.WDL` + selected `LSET*.WDL` load and root-record dispatch; `psx_world_frame_tick` is the normal per-frame world loop; and `FUN_80041378` is the top-level draw submission. - The broader lifecycle is readable now too. `psx_level_session_loop` is the outer level-session loop; `wdl_resource_bundle_load_by_index` performs the actual `SPEC_A.WDL` + selected `LSET*.WDL` load and root-record dispatch; `psx_world_frame_tick` is the normal per-frame world loop; and `FUN_80041378` is the top-level draw submission.
- The authored record passes now line up with the viewer model closely enough to use as the current executable ground truth: `psx_dispatch_section0_dispatch_roots` dispatches the `DAT_80067720` `0x18`-stride family plus the extra fixed-size entries near `DAT_80067658`, while `psx_dispatch_section0_constructor_placements` dispatches the `DAT_800678f0` `0x0c`-stride family. Those are the closest executable matches for the current `section0_dispatch_roots` and `section0_constructor_placements` viewer families. - The authored record passes now line up with the viewer model closely enough to use as the current executable ground truth: `psx_dispatch_section0_dispatch_roots` dispatches the `DAT_80067720` `0x18`-stride family plus the extra fixed-size entries near `DAT_80067658`, while `psx_dispatch_section0_constructor_placements` dispatches the `DAT_800678f0` `0x0c`-stride family. Those are the closest executable matches for the current `section0_dispatch_roots` and `section0_constructor_placements` viewer families.
- The live-object passes are separated too: `psx_run_live_object_type_updates` runs the per-type update callback over the linked live object list at `DAT_800675ac`, `psx_run_live_object_behavior_callbacks` runs the later per-object behavior callback stored on each object, and `FUN_80029de0` is the broad world/player motion integrator that sits between behavior updates and draw submission. - The live-object passes are separated too: `psx_run_live_object_type_updates` runs the per-type update callback over the linked live object list at `DAT_800675ac`, `psx_run_live_object_behavior_callbacks` runs the later per-object behavior callback stored on each object, and `psx_update_motion_and_nearby_interactions` is the broad world/player motion integrator that sits between behavior updates and draw submission.
- The cull/draw bridge is now explicit too: `FUN_800423b0` gates the two authored record-family dispatch passes, `FUN_80042424` gates already-instantiated live objects, and `FUN_80041458` draws from the final authored screen rectangle while sourcing palette overrides directly from the original record pointer at `obj+0xa0`. - The cull/draw bridge is now explicit too: `psx_authored_record_in_view_bounds` gates the two authored record-family dispatch passes, `psx_world_point_in_view_bounds` gates already-instantiated live objects, and `FUN_80041458` draws from the final authored screen rectangle while sourcing palette overrides directly from the original record pointer at `obj+0xa0`.
- Palette override provenance is tighter too: object field `+0xa0` is the original authored source-record pointer written by both constructors, so the current override path in `FUN_80041458` is reading authored record bytes directly rather than a hidden runtime side table. - Palette override provenance is tighter too: object field `+0xa0` is the original authored source-record pointer written by both constructors, so the current override path in `FUN_80041458` is reading authored record bytes directly rather than a hidden runtime side table.
- The cache builder now uses that evidence too: shared PSX reference art is keyed by `map:type:palette`, and scene export applies the authored palette byte when the source record exposes the proven override lane instead of always baking the bundle default palette. - The cache builder now uses that evidence too: shared PSX reference art is keyed by `map:type:palette`, and scene export applies the authored palette byte when the source record exposes the proven override lane instead of always baking the bundle default palette.
- One narrow renderer-side consequence is now verified in output, not just in notes: the cache builder now applies the executable-backed `0x0050` selector map (`0..3 -> frame 0..3`) as a temporary fallback, and retail map `9` now exports `type=80 state_selector=1 chosen_frame=1` instead of forcing frame `0`. - One narrow renderer-side consequence is now verified in output, not just in notes: the cache builder now applies the executable-backed `0x0050` selector map (`0..3 -> frame 0..3`) as a temporary fallback, and retail map `9` now exports `type=80 state_selector=1 chosen_frame=1` instead of forcing frame `0`.
@ -56,19 +87,23 @@
- The unresolved type path is more clearly dynamic than before: `psx_object_lookup_variant_entry` is called from both constructors and from `psx_object_advance_state_script`, so a root-dispatch family can change its visible companion bytes after script jumps instead of fixing them only at spawn time. - The unresolved type path is more clearly dynamic than before: `psx_object_lookup_variant_entry` is called from both constructors and from `psx_object_advance_state_script`, so a root-dispatch family can change its visible companion bytes after script jumps instead of fixing them only at spawn time.
- The built scene cache now tightens the remaining `0x0042` blocker too. The exported `state_selector` label is confirmed to be raw word `u4`, not a derived guess, while `lane` is raw word `u5`. A full cache scan found `3944` `type=0x0042` placeholders across `61` maps and showed selectors `0..4`, with real `3`/`4` cases on `map-4`, `map-5`, `map-8`, `map-45`, `map-69`, and `map-85`. - The built scene cache now tightens the remaining `0x0042` blocker too. The exported `state_selector` label is confirmed to be raw word `u4`, not a derived guess, while `lane` is raw word `u5`. A full cache scan found `3944` `type=0x0042` placeholders across `61` maps and showed selectors `0..4`, with real `3`/`4` cases on `map-4`, `map-5`, `map-8`, `map-45`, `map-69`, and `map-85`.
- That same cache scan shows lane and selector are not interchangeable. `0x0042` still clusters mostly on lanes `0x0020` and `0x0022`, but there are also `lane=0x0030` exports with `state_selector=0` (for example on `map-108`). So the next executable-backed pass should treat `u4` as the confirmed selector input and `u5` as a separate lane/class byte while tracing the post-advance `DAT_800758d4` variant lookup. - That same cache scan shows lane and selector are not interchangeable. `0x0042` still clusters mostly on lanes `0x0020` and `0x0022`, but there are also `lane=0x0030` exports with `state_selector=0` (for example on `map-108`). So the next executable-backed pass should treat `u4` as the confirmed selector input and `u5` as a separate lane/class byte while tracing the post-advance `DAT_800758d4` variant lookup.
- The next narrowing pass is now clearer too: the art-facing variant index is not the raw placement selector byte directly. `psx_object_select_state_script` stores the authored selector in `obj+0x9e`, but `psx_object_lookup_variant_entry` indexes `DAT_800758d4` with `obj+0x94`, the current script word. Two runtime paths (`FUN_80028c94` and `FUN_8002906c`) can reseat the active script from `FUN_8003bc1c(obj) >> 2 & 0xf`, so at least some live `0x0042` selectors `3` and `4` come from heading-based runtime reselection rather than from the original record alone. - The next narrowing pass is now clearer too: the art-facing variant index is not the raw placement selector byte directly. `psx_object_select_state_script` stores the authored selector in `obj+0x9e`, but `psx_object_lookup_variant_entry` indexes `DAT_800758d4` with `obj+0x94`, the current script word. Two runtime paths (`psx_object_reselect_state_from_target_vector` and `psx_type4_reselect_motion_state`) can reseat the active script from `psx_object_quantize_motion_heading16(obj) >> 2 & 0xf`, so at least some live `0x0042` selectors `3` and `4` come from heading-based runtime reselection rather than from the original record alone.
- The newly traced consumer side narrows that further. `psx_object_advance_state_script` sign-extends the `DAT_800758d4` lookup result into `obj+0x30/+0x34/+0x38`, and the verified downstream consumers of those fields are collision/contact helpers (`psx_object_test_overlap_3d`, `psx_object_update_contact_block_flags`, and the target-bounds reads inside the reselection helpers), not the visible draw path. So the practical exporter change is to serialize `DAT_800758d4` as signed companion-extents metadata and stop treating it as the leading candidate for the missing final art table. - The newly traced consumer side narrows that further. `psx_object_advance_state_script` sign-extends the `DAT_800758d4` lookup result into `obj+0x30/+0x34/+0x38`, and the verified downstream consumers of those fields are collision/contact helpers (`psx_object_test_overlap_3d`, `psx_object_update_contact_block_flags`, and the target-bounds reads inside the reselection helpers), not the visible draw path. So the practical exporter change is to serialize `DAT_800758d4` as signed companion-extents metadata and stop treating it as the leading candidate for the missing final art table.
- That exporter step is now landed in the viewer-side cache path too. `buildTypeCompanionExtentsMaps` decodes the exported `DAT_800758d4` layer as a `u32 count + packed 3-byte signed extents` table, scene `stateLayers` now preserve those decoded extents per type/state, and each exported PSX scene item and `mapSource` row now carries the resolved `companionExtents` tuple for its chosen live state when one is available. - That exporter step is now landed in the viewer-side cache path too. `buildTypeCompanionExtentsMaps` decodes the exported `DAT_800758d4` layer as a `u32 count + packed 3-byte signed extents` table, scene `stateLayers` now preserve those decoded extents per type/state, and each exported PSX scene item and `mapSource` row now carries the resolved `companionExtents` tuple for its chosen live state when one is available.
- That moves the remaining art problem to a more specific place: unresolved families still need a family-specific rule that explains how the live script word at `obj+0x94` interacts with the drawable resource at `obj+0x10` after post-spawn reselection. The next useful trace should therefore stay close to those family-specific presentation callers instead of spending more time on generic `DAT_800758d4` consumers. - That moves the remaining art problem to a more specific place: unresolved families still need a family-specific rule that explains how the live script word at `obj+0x94` interacts with the drawable resource at `obj+0x10` after post-spawn reselection. The next useful trace should therefore stay close to those family-specific presentation callers instead of spending more time on generic `DAT_800758d4` consumers.
- The latest dispatch-table pass narrows that again for `0x0042` and `0x0049`: they do not have unique per-type descriptor entries. Both sit inside the same generic descriptor cluster covering `0x003e..0x0050`, and their shared descriptor currently resolves to `psx_spawn_compound_record_advance_state_once`, `psx_object_refresh_main_visible_and_cleanup`, and the newly named `psx_object_release_to_free_list`. So the missing art rule is even less likely to be a special-case table fork and more likely to live downstream in generic object state/resource presentation. - The latest dispatch-table pass narrows that again for `0x0042` and `0x0049`: they do not have unique per-type descriptor entries. Both sit inside the same generic descriptor cluster covering `0x003e..0x0050`, and their shared descriptor currently resolves to `psx_spawn_compound_record_advance_state_once`, `psx_object_refresh_main_visible_and_cleanup`, and the newly named `psx_object_release_to_free_list`. So the missing art rule is even less likely to be a special-case table fork and more likely to live downstream in generic object state/resource presentation.
- That runtime bridge is now wider and better grounded than it was when `FUN_8002906c` first became the top target. The newly named cluster around it shows that type-4 delayed interactions can actively rewrite the live script after spawn: `psx_type4_update_delayed_interaction` (`0x80029c20`) seeds a forward-probe delay, `psx_type4_reselect_motion_state` (`0x8002906c`) either hands off to `FUN_80028c94` or recomputes heading from motion state before calling `psx_object_select_state_script`, `psx_object_update_nearby_interactions` (`0x80029478`) is the broader non-type-4 nearby-object sweep, `psx_object_test_overlap_3d` (`0x80028298`) is the box-overlap predicate, `psx_object_update_contact_block_flags` (`0x800289f0`) updates directional contact/block bits, and `psx_object_register_contact_pair` (`0x8002845c`) links both objects into the bilateral contact queue. - That runtime bridge is now wider and better grounded than it was when `psx_type4_reselect_motion_state` first became the top target. The newly named cluster around it shows that type-4 delayed interactions can actively rewrite the live script after spawn: `psx_type4_update_delayed_interaction` (`0x80029c20`) seeds a forward-probe delay, `psx_type4_reselect_motion_state` (`0x8002906c`) either hands off to `psx_object_reselect_state_from_target_vector` or recomputes heading from motion state before calling `psx_object_select_state_script`, `psx_object_update_nearby_interactions` (`0x80029478`) is the broader non-type-4 nearby-object sweep, `psx_object_test_overlap_3d` (`0x80028298`) is the box-overlap predicate, `psx_object_update_contact_block_flags` (`0x800289f0`) updates directional contact/block bits, and `psx_object_register_contact_pair` (`0x8002845c`) links both objects into the bilateral contact queue.
- The motion-heading piece is now named too. `psx_object_reselect_state_from_target_vector` (`0x80028c94`) writes a target-relative motion vector into `obj+0x60/+0x64/+0x68`, `psx_object_quantize_motion_heading16` (`0x8003bc1c`) wraps the current object motion vector, and `psx_quantize_vector_heading16` (`0x8003b980`) reduces that vector to one of 16 heading buckets before the caller maps it back to a `DAT_800758cc` script selector. Combined with the already-verified `psx_object_advance_state_script -> psx_object_lookup_variant_entry` path, this means the art-facing `DAT_800758d4` lookup is driven by the current script word at `obj+0x94`, not directly by the original placement selector stored at `obj+0x9e`. - The motion-heading piece is now named too. `psx_object_reselect_state_from_target_vector` (`0x80028c94`) writes a target-relative motion vector into `obj+0x60/+0x64/+0x68`, `psx_object_quantize_motion_heading16` (`0x8003bc1c`) wraps the current object motion vector, and `psx_quantize_vector_heading16` (`0x8003b980`) reduces that vector to one of 16 heading buckets before the caller maps it back to a `DAT_800758cc` script selector. Combined with the already-verified `psx_object_advance_state_script -> psx_object_lookup_variant_entry` path, this means the art-facing `DAT_800758d4` lookup is driven by the current script word at `obj+0x94`, not directly by the original placement selector stored at `obj+0x9e`.
- The local render-routing wrappers are named now too, which sharpens the practical exporter story. `psx_spawn_compound_record_advance_state_once` (`0x80013618`) and `psx_spawn_simple_record_set_active_flag` (`0x8001372c`) are constructor-side wrappers that immediately push new objects into specific live states, while `psx_object_refresh_main_visible_and_cleanup` (`0x80013688`) is a compact stage-1 projector/cleanup wrapper and `psx_object_advance_state_and_queue_special_visible` (`0x80013758`) is a compact stage-2 wrapper that advances script state and immediately queues through `psx_project_object_special_visible_queue`. So the executable already contains small dedicated routing helpers for “advance state, then main visible” versus “advance state, then special visible”, not just one monolithic render path. - The local render-routing wrappers are named now too, which sharpens the practical exporter story. `psx_spawn_compound_record_advance_state_once` (`0x80013618`) and `psx_spawn_simple_record_set_active_flag` (`0x8001372c`) are constructor-side wrappers that immediately push new objects into specific live states, while `psx_object_refresh_main_visible_and_cleanup` (`0x80013688`) is a compact stage-1 projector/cleanup wrapper and `psx_object_advance_state_and_queue_special_visible` (`0x80013758`) is a compact stage-2 wrapper that advances script state and immediately queues through `psx_project_object_special_visible_queue`. So the executable already contains small dedicated routing helpers for “advance state, then main visible” versus “advance state, then special visible”, not just one monolithic render path.
- The owner-level bridge is named now too: `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) integrates per-object motion, refreshes visibility flags, then advances script state and routes the object into either the stage-1 main visible list or the stage-2 special-visible queue. That means the runtime split relevant to the viewer is not a late draw-only distinction; it is already baked into the per-object motion/update step. - The owner-level bridge is named now too: `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) integrates per-object motion, refreshes visibility flags, then advances script state and routes the object into either the stage-1 main visible list or the stage-2 special-visible queue. That means the runtime split relevant to the viewer is not a late draw-only distinction; it is already baked into the per-object motion/update step.
- The drawable-resource side is much tighter now too. Both constructors resolve the per-type art bank and store the resulting drawable resource at `obj+0x10` before the object enters the live update loop; the current script word at `obj+0x94` is then passed straight through `psx_resource_frame_origin_x/y`, `psx_resource_frame_width/height`, and finally into `psx_sprite_resource_submit_frame` or `psx_image_table_submit_frame` during the stage-1 and stage-2 draw passes. So the renderer now has a much stronger executable-backed chain from type bank -> object resource pointer -> live frame index -> final primitive submission. - The drawable-resource side is much tighter now too. Both constructors resolve the per-type art bank and store the resulting drawable resource at `obj+0x10` before the object enters the live update loop; the current script word at `obj+0x94` is then passed straight through `psx_resource_frame_origin_x/y`, `psx_resource_frame_width/height`, and finally into `psx_sprite_resource_submit_frame` or `psx_image_table_submit_frame` during the stage-1 and stage-2 draw passes. So the renderer now has a much stronger executable-backed chain from type bank -> object resource pointer -> live frame index -> final primitive submission.
- The builder under that constructor-side handoff is named too: `psx_create_image_resource_from_descriptor` creates the drawable resource from the per-type art descriptor, using `image_resource_bind_vram_slot` for single-image type-4 resources and `image_bundle_load_to_vram` for multi-frame type-5 bundles. That means the remaining viewer gap is no longer “where does the object resource pointer come from”; it is the last live state/variant rule that determines which resource/frame combination unresolved families actually present. - The builder under that constructor-side handoff is named too: `psx_create_image_resource_from_descriptor` creates the drawable resource from the per-type art descriptor, using `image_resource_bind_vram_slot` for single-image type-4 resources and `image_bundle_load_to_vram` for multi-frame type-5 bundles. That means the remaining viewer gap is no longer “where does the object resource pointer come from”; it is the last live state/variant rule that determines which resource/frame combination unresolved families actually present.
- The stage-1 list machinery is named end to end as well: `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` are the concrete helpers behind the main visible-object pass that `psx_draw_world_visible_passes` submits before the stage-2 queue. - The stage-1 list machinery is named end to end as well: `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` are the concrete helpers behind the main visible-object pass that `psx_draw_world_visible_passes` submits before the stage-2 queue.
- Practical consequence for the viewer plan: the exported placement selector is now firmly only an input, not a final art choice. For unresolved families the next bridge must model at least part of the post-spawn interaction/reselection lane before a `type -> frame` rule can be trusted, otherwise the viewer will keep drawing structurally correct but still unreadable placeholder-heavy scenes. - The main-visible ordering rule is no longer safely approximated as a flat `screenY` sort. `psx_main_visible_list_get_sorted_slice` refreshes only the stage-1 list before `psx_draw_world_visible_passes`, and `psx_main_visible_list_sort_range` then walks dependency counters, class buckets, and world-extent tie-breakers before `psx_draw_main_visible_object` submits frames. A viewer that only projects coordinates and sorts by one screen axis will still miss part of the executable's coherence model even when art binding is otherwise correct.
- The level-load side is part of the same coherence chain too. `psx_level_session_loop` calls `wdl_resource_bundle_load_by_index` for the selected `SPEC_A.WDL`/`LSET*.WDL` pair, that load path applies `level_palette_header_apply`, and `level_palette_header_apply` immediately calls `level_palette_upload_cluts`. So the runtime CLUT tables consumed later by `psx_sprite_resource_submit_frame` and `psx_image_table_submit_frame` are installed during level load before any world-object draw routing begins.
- The remaining repeated-wall outlier is now tighter in cache evidence too. On `map 104`, the dominant bad cluster is still `type=0x0042`, and the current scene cache repeatedly shows it binding `cross-map-cc-signature-donor:85:0040` with `template_type=64`, `donor_type=64`, `bundle_offset=0x0009d304`, and `requested_palette_index=0 resolved_palette_index=0`. That means the current visible failure there is not primarily “wrong palette variant picked from a recovered family”; it is still the wrong runtime family/resource presentation rule for that unresolved root-dispatch band.
- The same evidence now sharpens the next repair rule too: keep authored family identity and raw `u5` class visible until the executable proves a shared resource path. `map 104` currently shows that matching only on donor template/signature is broad enough to merge `section0_constructor_placements` and `section0_dispatch_roots` into the same wall art even when their current runtime role is still unproven.
- Practical consequence for the viewer plan: the exported placement selector is now firmly only an input, not a final art choice. The immediate placeholder problem is removed for the focused exporter path, but the next bridge still must model post-spawn interaction/reselection well enough to replace `cohort-*` and `emergency-global-donor:*` provenance with tighter executable-backed family/resource/frame rules.
- `JL-9` already appears in recovered PSX weapon-name tables, but gameplay availability and sprite identity are not yet closed. - `JL-9` already appears in recovered PSX weapon-name tables, but gameplay availability and sprite identity are not yet closed.
## Success Criteria ## Success Criteria
@ -106,6 +141,8 @@ Tasks:
5. Recover the bundle/frame binding rule for map placements well enough to stop relying on broad candidate pairing. 5. Recover the bundle/frame binding rule for map placements well enough to stop relying on broad candidate pairing.
6. Recover the draw-order or layer rule used when multiple map records overlap. 6. Recover the draw-order or layer rule used when multiple map records overlap.
7. Validate the corrected multi-section schema on at least `L0.WDL` and `L1.WDL` so the decode is not overfit to one level. 7. Validate the corrected multi-section schema on at least `L0.WDL` and `L1.WDL` so the decode is not overfit to one level.
8. Dump the installed section-pack/runtime tables after level load for `map 104`, especially `psx_ctor_placement_section_ptr`, `psx_type_policy_table_ptr`, `level_clut_table`, and the `0x80063e54/0x80063e68/psx_marker_channel_runtime_block` family.
9. Correlate those live table rows against the fixed `map 104` `0x0042` sample pack (`item:25/30/31/35/85/86`) before widening any donor or family heuristics again.
Expected output: Expected output:
@ -182,13 +219,39 @@ Expected output:
3. Compare the rebuilt all-map exports against recognizable rooms and decide whether the remaining missing structure now lives mainly in the decoded `DAT_8006769c` buffer or in still-unrendered subordinate tables. 3. Compare the rebuilt all-map exports against recognizable rooms and decide whether the remaining missing structure now lives mainly in the decoded `DAT_8006769c` buffer or in still-unrendered subordinate tables.
4. Tighten the raw file mappings for the newly exported runtime-bank layers (`DAT_800758d8`, `DAT_800758d0`, `DAT_800758cc`, `DAT_800758d4`) so their current section selection is proven rather than heuristic. 4. Tighten the raw file mappings for the newly exported runtime-bank layers (`DAT_800758d8`, `DAT_800758d0`, `DAT_800758cc`, `DAT_800758d4`) so their current section selection is proven rather than heuristic.
Current delta: the bank split is stronger now. `DAT_800758d8` comes from its own late descriptor stream, while `DAT_800758d0/cc/d4` belong to the adjacent `psx_load_type_state_banks` blobs. The remaining open question is the exact sub-split inside those state-bank blobs, not whether they come from the decompressed level-state lane. Current delta: the bank split is stronger now. `DAT_800758d8` comes from its own late descriptor stream, while `DAT_800758d0/cc/d4` belong to the adjacent `psx_load_type_state_banks` blobs. The remaining open question is the exact sub-split inside those state-bank blobs, not whether they come from the decompressed level-state lane.
5. Recover an actual bundle/frame reference from the per-type template payloads or their consumers so the exporter can replace the now-disproven scan-order bundle fallback with a verified type-to-art rule. 5. Split the remaining art/palette problem by render lane and resource class before widening donor logic again. For each repeated-wall family, determine whether it reaches `psx_draw_main_visible_object` or `psx_draw_special_visible_queue`, whether its bound resource at `obj+0x10` is sprite-kind or image-table-kind, and whether the authored override byte should be interpreted through the main visible override path at all.
Current delta: the template bank selection is now stronger and already recovers real art for a first subset, but the still-missing families need the stage-1/stage-2 object draw path plus `DAT_800758cc/d4` state interpretation, not more HUD/overlay decoding. Current delta: `psx_draw_main_visible_object` now proves a banded override rule (`0x003e..0x00ab -> source+0x06 high byte`, `>=0x00ac -> source+0x0c high byte`), while `psx_draw_special_visible_queue` skips that authored override entirely.
Current delta: stage 2 is no longer hypothetical. The next renderer-improvement candidate is to expose/export the queued-object lane that feeds `FUN_80041144`, because the executable now clearly maintains it separately from the main visible list. Current delta: `psx_sprite_resource_submit_frame` and `psx_image_table_submit_frame` both accept the same high-byte override token but resolve it through different CLUT tables, so the exporter must stop treating palette selection as a type-only scalar.
Current delta: the unresolved families are also clearly dynamic now because `psx_object_lookup_variant_entry` reruns after `psx_object_advance_state_script`; the next verified art-binding pass should therefore sample post-jump state, not only constructor-time selectors. Current delta: the constructors already bind the drawable resource from `DAT_800758d8` before any later state transitions, so the remaining aliasing is not best attacked as a missing late bundle lookup.
6. Split section-0 placements into at least three executable-backed render classes: world-facing geometry/object placements, animated runtime-only objects, and clearly non-map-facing UI/talk assets such as the portrait bundles currently surfacing through fallback art matching. Current delta: the worst visible outlier on `map 104` is still `type=0x0042` repeatedly mapped to donor map `85` type `0x0040` bundle `0x0009d304` with palette `0`, so the next repair pass should treat it first as a wrong family/resource binding problem, not as a missing alternative palette decode.
7. Decode the `psx_object_advance_state_script` sentinel opcodes (`ffff`, `fffe`, `fffd`, `fffc`, `fffb`) well enough to tell when a placement loops, jumps into a subsidiary script, or fires a side-effect helper, because that state-machine branch is now the main discriminator between map-facing art and non-map runtime assets. Current delta: the current donor pass is broad enough to merge `section0_constructor_placements u5=0x0020` and `section0_dispatch_roots u5=0x0030/0x0022` onto that same donor wall, so the next exporter-safe experiment should keep family plus raw `u5` as a hard fence before broadening any cross-map donor reuse further.
Current delta: `fffe` is closed as an audio/effect dispatch through `FUN_8004061c`, `fffd` is the direct indexed jump, and `fffc/fffb` are now separated as the immediate subsidiary-switch versus scan-forward subsidiary-switch pair. Current delta: that hard fence is now implemented in the viewer cache builder for provisional generic-family donor matches. The cache now prefers honest placeholders over one shared wrong wall when a single type bucket mixes authored families or `u5` classes.
Current delta: the new subagent sweep says the next split should not be “invent a new per-type descriptor bucket for `0x0042`,” because `0x0042` still shares the generic `0x003e..0x0050` descriptor cluster. The next executable-backed discriminator should be runtime-bank content, state progression, resource kind, and lane/flag behavior instead.
6. Recover an actual family-specific state-to-frame rule from the post-spawn runtime path so the exporter can replace the still-provisional `state_selector -> chosen_frame` fallback with the live `obj+0x94` path used by the submitters.
Current delta: the unresolved families are clearly dynamic because `psx_object_lookup_variant_entry` reruns after `psx_object_advance_state_script`, `psx_object_reselect_state_from_target_vector`, and `psx_type4_reselect_motion_state`; the next verified art-binding pass should therefore sample post-jump state, not only constructor-time selectors.
Current delta: the executable's coherent-map path is now ordered as load-time CLUT install -> constructor-side resource bind -> per-frame motion/state advance -> stage-1 or stage-2 visibility routing -> stage-1 dependency sort -> frame submission. Any next exporter shortcut should be checked against that order before being treated as a stable map rule.
Current delta: draw-time submitter choice is now explicit too. `psx_draw_main_visible_object` and `psx_draw_special_visible_queue` both choose `psx_image_table_submit_frame` only when the bound resource header kind is `5`; otherwise they use `psx_sprite_resource_submit_frame`. So unresolved `0x0042` still needs runtime resource-kind evidence, not just lane or type labels.
Current delta: treat the HUD/overlay lane as a false lead for this specific repair. `psx_draw_hud_overlay_pass` can route through slot flags rather than the normal world-object kind split, and `psx_draw_clock_digits_overlay` is forced image-table presentation. Neither should be used to infer `0x0042` world-object art binding.
Current delta: the next executable-backed sample should be concrete, not another broad sweep. For `map 104`, trace at least one constructor-placement `type=0x0042 u5=0x0020` case and one root-dispatch `type=0x0042 u5=0x0030` case through `obj+0x10` resource kind, `obj+0x94` live script word, and final world-facing lane so the exporter can decide whether these are truly separate presentation families.
Current delta: the concrete sample pack is now fixed. Use `item:25`/`35` as the root `0x0022` control, `item:30`/`31` as the root `0x0030` control, and `item:85`/`86` as the constructor `0x0030` control before widening any art-binding experiment.
Current delta: include `obj+0x1c` in that concrete sample. The next trace should explicitly record whether bit `0x0400` is ever set for representative `map 104` `0x0042` objects, because that is now the strongest recovered stage-2 selector and is more decisive than raw `u5` by itself.
Current delta: include the per-type transition-table row in that same sample. For `type 0x0042`, inspect the `DAT_80063b4c` row used by `psx_object_select_state_from_transition_table` and compare its selector outputs against the observed `obj+0x9e` / `obj+0x94` values; that is now the clearest route to explaining selector `3/4` cases without reopening broad donor heuristics.
Current delta: do not stop at the latched `obj+0x94` value. For representative `0x0042` cases, sample the pre-latch selector dispatch path through `psx_type42_transition_selector_tick` as well; selector `3/4` can be selected there before the runtime latch copy, so a pure `obj+0x94` snapshot can still miss the active turn-state choice.
Current delta: treat `DAT_800675f8` as policy, not as the main route discriminator. `0x1000` is now best read as nearby-publication suppression after stage-2 routing, `0x0600` as stage-1 ordering class, and `0x2000` as main-visible semitrans policy. The next `0x0042` split still belongs first to `obj+0x1c`, pre-latch selector behavior, and bound resource kind.
Current delta: include the early-gate side in that same sample. `psx_type42_transition_selector_tick` now clearly checks `psx_object_is_within_view_margin` before the pre-latch turn/reseat dispatch, so the next runtime sample should log whether the object is even eligible to emit the transient selector before trying to explain a final frame choice.
Current delta: keep the `0x0400` question scoped correctly. The latest anonymous-island recovery proves wider runtime-state writes and related policy control, but not yet a direct object-local `obj+0x1c |= 0x0400` writer. So the next pass should explicitly separate object-local `obj+0x1c`, nested runtime state words, and global mode bits instead of folding them into one generic “route flag” bucket.
Current delta: treat `runtimeDiagnostic.typePolicy.word` as a level-global per-type value when you do capture it. The latest pass now closes the table role more tightly than before: `DAT_800675f8` is a level-loaded type-policy pointer, not a per-lane word source.
Current delta: use the new cache export as the comparison surface. The next batch should target concrete `mapSource.items[*].runtimeDiagnostic` channels in the generated PSX scene JSON and tie live Ghidra evidence back to those exact fields, especially for representative `map 104` `0x0042` placeholders.
Current delta: the next exporter/runtime correlation should add a stable bound-resource identity plus the live frame/state token used at submission if the current fields are not enough. The concrete `0x0042` sample pack now strongly suggests that `0x0022` versus `0x0030` is not itself the final `64x40` versus `64x64` split.
Current delta: the route branch itself is no longer vague. `psx_object_integrate_motion_and_route_visible` now has an explicit `obj+0x1c & 0x0400` stage-2 branch point commented in the live database, so the next runtime capture does not need to hunt for the branch location, only to determine whether the concrete sample items ever carry that bit at the decisive moment.
7. Split section-0 placements into at least four executable-backed presentation classes: main-visible objects with authored palette override, special-visible objects without that override, animated runtime-only objects, and clearly non-map-facing UI/talk assets such as the portrait bundles currently surfacing through fallback art matching.
8. Decode the `psx_object_advance_state_script` sentinel opcodes (`ffff`, `fffe`, `fffd`, `fffc`, `fffb`) well enough to tell when a placement loops, jumps into a subsidiary script, or fires a side-effect helper, because that state-machine branch is now the main discriminator between map-facing art and non-map runtime assets.
Current delta: `fffe` is closed as an audio/effect dispatch through `psx_script_dispatch_audio_event`, `fffd` is the direct indexed jump, and `fffc/fffb` are now separated as the immediate subsidiary-switch versus scan-forward subsidiary-switch pair.
9. Recheck the renderer-side palette export against those executable rules before another broad cache rebuild. In particular, verify that map-scene palette variants only use authored override bytes for the type bands and render lanes where `psx_draw_main_visible_object` actually consumes them, and keep special-visible families on their resource-default path until a stronger rule is proved.
Current delta: live function coverage is now measurable instead of anecdotal. The active `SLUS_002.68` session currently has `1274` local `0x800...` functions, `917` named and `357` still anonymous, which puts the practical local naming floor at `71.98%` and leaves `28.02%` still unresolved. The hottest remaining anonymous pages are currently `0x8002exxx`, `0x80030xxx`, and `0x80049xxx` with `21` unknowns each.
Current delta: one adjacent world/update family is now tighter too. The `0x80023c..2b1..` lane is no longer just a pile of anonymous helpers: `psx_object_run_control_opcode` now owns the local control-stream switch, case `1` is split into `psx_control_move_player_to_point` and `psx_control_move_object_to_point`, case `3` is `psx_control_wait_ticks`, cases `4/5` are now closed as `psx_control_configure_fixed_camera_anchor`, case `9` is `psx_control_set_facing_direction`, and case `2` is grounded as a deferred control-command queue (`psx_queue_deferred_control_command` -> `psx_flush_deferred_control_queue` -> `psx_apply_deferred_control_command` -> dispatch-root/live-object appliers). Case `8` is still not named at the wrapper level, but its direct callee is now closed as `psx_spawn_object_compound_effect_variant3`, which narrows the remaining uncertainty to the wrapper's timing and motion-side effects rather than the spawned effect itself.
8. In parallel with the map pass, trace the palette-override read path from the known draw helper caller and document which source field feeds the resolved CLUT. 8. In parallel with the map pass, trace the palette-override read path from the known draw helper caller and document which source field feeds the resolved CLUT.
9. Locate the `JL-9` weapon entry in the PSX executable tables and log its table index, surrounding weapon names, and all code/data xrefs. 9. Locate the `JL-9` weapon entry in the PSX executable tables and log its table index, surrounding weapon names, and all code/data xrefs.
10. Create a short follow-up note in `docs/psx/` after the batch rather than burying the result only in Ghidra comments. 10. Create a short follow-up note in `docs/psx/` after the batch rather than burying the result only in Ghidra comments.

View file

@ -5,6 +5,38 @@
- Target disc tree: `E:\emu\psx\Crusader - No Remorse` - 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. - Goal of this pass: identify the boot executable, separate likely code from content, and find the most practical first extraction routes for PS1 assets.
## Function Coverage Census
Current live census for the active Ghidra `SLUS_002.68` session:
- Raw function table: `1428` total, `1070` named, `358` still anonymous (`74.93%` named)
- Local executable code only (`0x800...` addresses): `1274` total, `917` named, `357` still anonymous (`71.98%` named, `28.02%` anonymous)
Method used:
- queried the live MCP `list_functions` endpoint against the active PSX program
- counted `FUN_` and `nullfn_` names as anonymous placeholders
- treated the `0x800...` address range as the practical coverage metric so imported/system helpers in other spaces do not distort the remaining-work count
Current hottest anonymous local windows by `0x1000` page are:
- `0x8002exxx`: `21`
- `0x80030xxx`: `21`
- `0x80049xxx`: `21`
- `0x80031xxx`: `17`
- `0x80040xxx`: `14`
- `0x8002bxxx`: `14`
- `0x8003axxx`: `13`
- `0x80020xxx`: `13`
- `0x80048xxx`: `13`
- `0x80046xxx`: `11`
Practical read:
- the PSX database is well past the early blind-mapping stage; roughly seven tenths of the local code already has non-placeholder names
- the remaining anonymous mass clusters in a few rendering, world-update, and resource-heavy windows rather than being evenly spread
- `0x80040xxx` remaining hot is still consistent with the current map-rendering blocker, because that window contains late projection and presentation helpers rather than only loader-side code
## Immediate Conclusions ## Immediate Conclusions
- `SYSTEM.CNF` is the disc boot file and points directly at `cdrom:\SLUS_002.68;1`. - `SYSTEM.CNF` is the disc boot file and points directly at `cdrom:\SLUS_002.68;1`.
@ -172,7 +204,7 @@ Current best read:
Executable-guided extraction status: 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. - `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`. - The same loader reads a small level header blob first, then a large SPU/audio blob, then dispatches the remaining level resource stream through `psx_stream_install_type_runtime_banks`.
- `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`. - `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. - `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. - 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.
@ -531,7 +563,7 @@ Current invalidation result:
- this direct `u0 -> bundle index` mapping is now considered invalid for real renderer output - 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 - 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 - executable-side tracing shows that art selection is type-driven through `DAT_800758cc/d0/d4/d8` resource tables loaded by `psx_stream_install_type_runtime_banks`, not by directly indexing the raw `post_audio_region_04` bundle scan
New loader/data evidence from this pass: New loader/data evidence from this pass:
@ -569,7 +601,7 @@ Current app compatibility notes:
Immediate implications for the next decode pass: 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 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` - the next priority is to replace the invalidated `u0 -> bundle index` hypothesis with a real type/resource lookup recovered from `psx_stream_install_type_runtime_banks`
- `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 - `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 - 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 - 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
@ -650,6 +682,8 @@ What the loader actually does:
- `type = record[+0x08]` - `type = record[+0x08]`
- `dispatch through PTR_PTR_80063118[type]` - `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`. - 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`.
- The current `0x0042` result now fits that same generic-object model more tightly than before: `psx_type_descriptor_table[0x0042]` at `0x80063220` points to the shared row `0x800626f8`, so both constructor-placement and root-dispatch `0x0042` still enter the same generic create/update/release family rather than a unique descriptor fork.
- The selector side is tighter too: type `0x0042` now has a dedicated transition helper, `psx_type42_transition_selector_tick`, that can dispatch low turning selectors `3/4` before the `+0x94`-style runtime latch copy. So unresolved `0x0042` presentation is no longer best framed as only a missing descriptor-table or raw-selector decode problem; part of the remaining gap is that the live turn/reseat path can be ahead of the currently latched script word.
Why the current export is incoherent: Why the current export is incoherent:
@ -659,7 +693,7 @@ Why the current export is incoherent:
New executable-backed evidence for the missing bulk content: 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` - `psx_stream_install_type_runtime_banks` 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 - `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 - 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 - 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
@ -698,7 +732,7 @@ Exporter status after the next renderer pass:
- 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 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 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 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. - the file-side header block now separates more cleanly too: `FUN_80039c40` allocates a `0x50` level runtime-header block at `DAT_80067794`, and `psx_apply_level_runtime_header_block` 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. - 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 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. - 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.
@ -733,13 +767,13 @@ Next decoded runtime layers from the constructor pass:
- `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. - `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. - 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: - Current strongest sentinel read:
- `0xfffe` dispatches `FUN_8004061c`, which is an audio/effect helper rather than a visible-frame selector. - `0xfffe` dispatches `psx_script_dispatch_audio_event`, 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. - `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. - `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. - `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: - 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. - `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. - `0xfffe` dispatches a side-effect helper (`psx_script_dispatch_audio_event`) using the following word as a parameter before advancing.
- `0xfffd` is the direct indexed jump control within the current script family. - `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. - `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. - 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.
@ -761,7 +795,7 @@ Next decoded runtime layers from the constructor pass:
- 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. - 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 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`. 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. Two runtime reselection paths now explain how those higher selectors can arise without contradicting the earlier three-script file read. `psx_object_reselect_state_from_target_vector` and `psx_type4_reselect_motion_state` both recompute the active script with `psx_object_select_state_script(obj, psx_object_quantize_motion_heading16(obj) >> 2 & 0xf)`, where `psx_object_quantize_motion_heading16` quantizes the object's current motion vector at `obj+0x60/+0x64` through `psx_quantize_vector_heading16` 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. 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. 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: The current draw split is clearer too. `FUN_80041378` is a three-stage render pass:
@ -778,7 +812,7 @@ Next decoded runtime layers from the constructor pass:
- 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`. - 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`). - 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. - 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. - One state-script sentinel is now functionally closed too: `0xfffe` dispatches `psx_script_dispatch_audio_event`, 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: - 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_8002d240` adds an object to the stage-1 `DAT_8006ad5c` visible-list array.
- `FUN_8002d35c` removes an object from that same array. - `FUN_8002d35c` removes an object from that same array.
@ -808,24 +842,36 @@ Additional constructor-backed coordinate grounding from the current pass:
Recovered per-level runtime-header lane: 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_80039c40` is now confirmed as a pure `0x50` allocator for `DAT_80067794`, and `psx_apply_level_runtime_header_block` 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. - `psx_apply_level_runtime_header_block` 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. - 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. - 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. - 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`. - `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 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 `psx_update_motion_and_nearby_interactions`, updates queued transient resources through the still-structural `FUN_8002aed0` queue-drain helper, 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: - 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_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. - `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: - 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_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. - `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. - One adjacent control family in the same world/update lane is now tighter too:
- 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. - `psx_object_run_control_opcode` (`0x80023c54`) executes one opcode from the object-local control stream at owner `+0x20`.
- `psx_control_move_player_to_point` (`0x80023efc`) is control opcode case `1` for the player path: it lazily seeds a target point from the opcode payload, steers toward it through the heading solver, and completes once the player reaches that point.
- `psx_control_move_object_to_point` (`0x80024070`) is the non-player version of the same case `1` path, using the object movement-state helper to keep facing and locomotion aligned while the object advances toward the opcode target.
- `psx_queue_deferred_control_command` (`0x800241f4`) appends deferred control-command entries into the small queue rooted at `DAT_8008f608` / `DAT_8008ef90` with count `DAT_80067730`.
- `psx_control_wait_ticks` (`0x80024290`) is control opcode case `3`: a one-shot timed wait gate that snapshots `DAT_80078a28` on first entry and completes only after the opcode's tick count has elapsed.
- `psx_control_configure_fixed_camera_anchor` (`0x800242f0`) is the shared control opcode case `4/5` helper: one branch projects an opcode-provided point into the shared camera basis and seeds a fixed anchor through `DAT_80078a2c`, while the other branch clears that anchor and restores normal camera-follow behavior.
- `psx_spawn_object_compound_effect_variant3` (`0x800184e8`) is the direct effect spawner used by control opcode case `8`: it builds a type-`2` compound record at the current object position, forces variant `3`, and triggers audio event `0x2c`.
- `psx_flush_deferred_control_queue` (`0x8002aed0`) drains that queue once per world tick, applying each entry through `psx_apply_deferred_control_command`.
- `psx_apply_deferred_control_command` fans one deferred entry out into both `psx_apply_deferred_control_to_dispatch_roots` and `psx_apply_deferred_control_to_live_objects`, which is the clearest current evidence that this queue is a small deferred world/control mutation lane rather than a render queue.
- `psx_control_set_facing_direction` (`0x80024438`) is control opcode case `9`: it forces an explicit facing token and immediately refreshes the player or object movement-state around that heading.
- The remaining neighboring helper at `0x800243b8` is still intentionally unnamed for now; it looks like a short delay gate around `psx_spawn_object_compound_effect_variant3`, but it still needs stronger subsystem evidence before it should get a behavioral name.
- `psx_update_motion_and_nearby_interactions` is the broad world-motion and nearby-interaction 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. `psx_authored_record_in_view_bounds` is the authored-record screen-space gate used by the two section-0 dispatch passes, while `psx_world_point_in_view_bounds` 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 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 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: - The next high-value executable target is now partly closed. `psx_type4_reselect_motion_state` (`0x8002906c`) is now named, 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_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_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_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.
@ -838,9 +884,9 @@ Recovered per-level runtime-header lane:
- The local post-advance render wrappers are also no longer anonymous labels: - 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_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_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_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 queues the object into the nearby-interaction active set through `psx_nearby_interaction_list_add`.
- `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. - `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. - 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 nearby-interaction enqueue step. 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. - 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: - 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_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.
@ -1042,6 +1088,8 @@ Per-bundle shipped inventory from the extracted disc tree:
### Passcodes and password-screen cheat status ### Passcodes and password-screen cheat status
Follow-up: the hidden passcode compare lane is now materially tighter in [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md). The recovered decoder at `0x8003ec8c` is table-driven rather than plain ASCII, and its special hidden rows now give the strongest current code-backed support for public PSX folklore that `L0SR` is the cheat-mode password candidate while `XXXX` routes into a separate hidden branch.
Current executable-backed passcode findings: Current executable-backed passcode findings:
- The mission-complete passcode display path at `80022cd4` and `80022f1c` synthesizes a `4`-character code from generated indexes. - The mission-complete passcode display path at `80022cd4` and `80022f1c` synthesizes a `4`-character code from generated indexes.
@ -1131,7 +1179,9 @@ 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` - 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 - 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 - `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 - the focused follow-up in [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now closes one important part of the question: `JL-9` is a real executable-backed final weapon-definition row rather than just a stray PSX-only string, and the strongest current acquisition route is a hidden passcode -> debug gate -> bulk weapon unlock path that likely reaches the extra late weapon channel
- that same focused follow-up now tightens the disambiguation further: the extra hidden-passcode/L0SR-adjacent non-PC weapon is now directly identified as `JL-9` (`0x0d`), while `JL-2` is the neighboring ordinary lane (`0x0c`)
- exact sprite identity and exact level placement for `JL-9` remain open and still need runtime or asset-side correlation
### Enemies ### Enemies
@ -1153,6 +1203,6 @@ Current safest read:
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. 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. 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. 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. 5. Recover the exact type IDs consumed by `psx_stream_install_type_runtime_banks` 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. 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. 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.

View file

@ -519,6 +519,58 @@ Features that are mostly just break-state/UI flags:
- `single step` calls `usecode_debugger_break_state_enable_single_step` - `single step` calls `usecode_debugger_break_state_enable_single_step`
- source-file open, goto-line, search, and breakpoint-table editing are mainly source-buffer/UI features once the debugger is open - source-file open, goto-line, search, and breakpoint-table editing are mainly source-buffer/UI features once the debugger is open
### 18. Retail Ghidra follow-up implied by the Regret mapping
The Regret pass is now strong enough to drive a specific retail `CRUSADER.EXE` naming batch rather than a generic `more seg109 cleanup` request.
The most important retail equivalents to promote explicitly in live Ghidra next are:
- `13a0:2882` = `usecode_debugger_build_menubar`
- `13a0:088f` = `usecode_debugger_source_pane_create`
- `13a0:0ae8` = `usecode_debugger_source_pane_init_view_from_break_state`
- `13a0:0ba7` = `usecode_debugger_source_pane_handle_command`
- `13a0:0f16` = `usecode_debugger_source_pane_handle_pointer_event`
- `13a0:1088` = `usecode_debugger_source_line_copy_for_display`
- `13a0:1118` = `usecode_debugger_source_pane_draw_visible_lines`
- `13a0:1413` = `usecode_debugger_source_pane_clamp_viewport`
- `13a0:15ac` = `usecode_debugger_source_pane_load_file`
- `13a0:16ee` = `usecode_debugger_watch_pane_create`
- `13a0:1791` = `usecode_debugger_watch_pane_draw`
- `13a0:193f` = `usecode_debugger_watch_pane_handle_click`
- `13a0:1c2c` = `usecode_debugger_translate_registered_event`
- `13a0:1dc6` = `usecode_debugger_forward_child_event`
- `13a0:2c2e` = `usecode_debugger_source_buffer_create_from_path`
- `13a0:2ca0` = `usecode_debugger_source_buffer_destroy`
- `13a0:2d14` = `usecode_debugger_source_buffer_open_from_path`
- `13a0:2e0a` = `usecode_debugger_source_buffer_load_text`
- `13a0:2f4f` = `usecode_debugger_source_buffer_split_lines_in_place`
- `13a0:301d` = `usecode_debugger_source_buffer_get_line_ptr`
Why this follows from the Regret result rather than from guesswork:
- the retail `000b:* -> 13a0:*` table already closes the same UI layer at the raw/reference level
- the Regret `1398:*` cleanup shows the same function ordering and subsystem boundaries in a build where the debugger bootstrap survived
- that makes the remaining retail seg109 backlog primarily a promotion/documentation task, not a fresh discovery task
The live retail rename pass also closed one useful correction: the original retail-first mirror list over-assigned `13a0:1791` and `13a0:193f` to the source pane. Current decompile evidence now makes the split cleaner: `13a0:16ee/1791/193f` form a watch-pane lane, while the source-pane lane retains the file-load, pointer-event, line-copy, and viewport helpers.
This is the right pre-patch documentation step because it turns the surviving retail debugger lane into named UI, event, and source-buffer surfaces instead of leaving the future patch window hidden among anonymous helpers.
### 19. Delivery implication after the patching stall
The Regret result changes what `practical next step` should mean.
The next useful proof is no longer `try another manual hex patch`. It is one of these two:
1. a runtime-only memory experiment that proves the create/store/open model on a clean executable
2. a reproducible scripted patch against a writable clone, driven by a verified byte plan rather than by hand edits
Why the Regret comparison pushes in that direction:
- it already tells us the missing retail behavior is a small bootstrap-and-vtable problem, not a whole missing subsystem
- it shows that the real stability question is whether live interpreter state is seeded correctly, not whether the source pane exists
- it therefore favors runtime proof and scripted reproducibility over one-off static patching attempts
Features that clearly depend on seeded current-entry runtime payload: Features that clearly depend on seeded current-entry runtime payload:
- `Inspect what?` - `Inspect what?`

View file

@ -38,6 +38,7 @@ Families that are only `callback-shaped` or `object-like` but still lack a safe
| `WatchEntityController` | Medium | Global controller/watch/camera object with explicit virtual dispatch and startup/display involvement | global object at `0x2bd8`, dispatch wrapper, create-global path, startup/display callsites | `0007:ba00 watch_entity_controller_create_global` delegates to `0007:ba45 watch_entity_controller_create`, stamps type `0x2c2b`, stores global object | no direct destructor identified in current notes | repeated dispatch through vtable slots `+0x24`, `+0x2c`, and `+0x30` | global-object ownership clearer than field layout; seed row at `0x2be4` into callback table | Worth inventorying now because it will benefit immediately from namespace/method grouping | | `WatchEntityController` | Medium | Global controller/watch/camera object with explicit virtual dispatch and startup/display involvement | global object at `0x2bd8`, dispatch wrapper, create-global path, startup/display callsites | `0007:ba00 watch_entity_controller_create_global` delegates to `0007:ba45 watch_entity_controller_create`, stamps type `0x2c2b`, stores global object | no direct destructor identified in current notes | repeated dispatch through vtable slots `+0x24`, `+0x2c`, and `+0x30` | global-object ownership clearer than field layout; seed row at `0x2be4` into callback table | Worth inventorying now because it will benefit immediately from namespace/method grouping |
| `EntityVmRuntime` | High | Main VM runtime object that owns owner-resource helper, cached slot/value state, and category-base setup | creation/load path is structurally stable and repeatedly cross-checked against extracted usecode evidence | `000d:44df entity_vm_runtime_init_from_path_if_configured`; `000d:4c99 entity_vm_runtime_create` | destroy path not fully named in the snippets here, but owner-resource destroy is known and runtime state/save-load consumers are well constrained | not a classic gameplay vtable family in the current notes, but method-style ownership and object fields are stable | object size and field zones strongly implied by `+0x10c/+0x10e`, `+0x117/+0x119`, `+0x1315/+0x1317` and related runtime state | Major lift target because VM readability is a blocker for recompilable source | | `EntityVmRuntime` | High | Main VM runtime object that owns owner-resource helper, cached slot/value state, and category-base setup | creation/load path is structurally stable and repeatedly cross-checked against extracted usecode evidence | `000d:44df entity_vm_runtime_init_from_path_if_configured`; `000d:4c99 entity_vm_runtime_create` | destroy path not fully named in the snippets here, but owner-resource destroy is known and runtime state/save-load consumers are well constrained | not a classic gameplay vtable family in the current notes, but method-style ownership and object fields are stable | object size and field zones strongly implied by `+0x10c/+0x10e`, `+0x117/+0x119`, `+0x1315/+0x1317` and related runtime state | Major lift target because VM readability is a blocker for recompilable source |
| `EntityVmOwnerResource` | High | File-backed helper owned by VM runtime that indexes source tables and materializes owner rows | helper object shape and per-entry loader contract are already tight | `000d:7000 entity_vm_runtime_owner_resource_create` allocates helper/object tables | paired destroy helper `000d:70fd entity_vm_runtime_owner_resource_destroy` is documented in related notes | helper method-table uses slots `+0x04` size-query and `+0x0c` materialization callback | helper-owned count `+0x14`, far-pointer table `+0x10`, paired word table `+0x18`, owner rows stride `0x0d` | One of the cleanest non-gameplay object families for typed struct work | | `EntityVmOwnerResource` | High | File-backed helper owned by VM runtime that indexes source tables and materializes owner rows | helper object shape and per-entry loader contract are already tight | `000d:7000 entity_vm_runtime_owner_resource_create` allocates helper/object tables | paired destroy helper `000d:70fd entity_vm_runtime_owner_resource_destroy` is documented in related notes | helper method-table uses slots `+0x04` size-query and `+0x0c` materialization callback | helper-owned count `+0x14`, far-pointer table `+0x10`, paired word table `+0x18`, owner rows stride `0x0d` | One of the cleanest non-gameplay object families for typed struct work |
| `NPCActionProcess` family | High | Bounded NPC AI process family with a shared base shell and derived stand/pace/surrender/guard/loiter policies | live process-table ownership now grounds the shared slot-1 destroy path, shared slot-10 no-op, and the bounded per-family create/run/destroy entries in seg033 | `1100:0000 NPCActionProcess_Create`, `1100:02ed StandProcess_CreateProcess`, `1100:0383 SurrenderProcess_CreateProcess`, `1100:0693 PaceProcess_CreateProcess`, `1100:0984 GuardProcess_CreateProcess`, `1100:0afb LoiterProcess_CreateProcess` | `1100:1089 NPCActionProcess_Destroy`, `1100:1036 StandProcess_Destroy`, `1100:0437 SurrenderProcess_Destroy`, `1100:0fe8 PaceProcess_Destroy`, `1100:0f95 GuardProcess_Destroy`, `1100:0f47 LoiterProcess_Destroy` | shared slot-10 base no-op at `1100:0fe3` plus loiter-only slot-10 override at `1100:0d3e`; broader slot semantics still open | current layout evidence is still thin, but `SurrenderProcess_Destroy` already proves family-local state plus two embedded dispatch-entry children | High navigation value for gameplay/NPC AI work even before a safe datatype pass |
| `EntityVmContext` | Medium | Per-slot/per-entity VM context object built from runtime and owner-resource data | create/setup/load helpers already have clear ownership, but broader dispatch semantics are still active work | `000d:46ec entity_vm_context_create_from_slot_index` and related masked-create wrappers | no single destroy method is highlighted in the current note set used here | context-side dispatch and busy-state updates through virtual or callback-like method surface at least on the context object | stable fields include `+0x32/+0x34`, `+0xd6/+0xd8`, `+0x102`, `+0x10c/+0x10e`, `+0x11b/+0x11d`, `+0x123` | Important for VM readability, but should follow runtime and owner-resource typing | | `EntityVmContext` | Medium | Per-slot/per-entity VM context object built from runtime and owner-resource data | create/setup/load helpers already have clear ownership, but broader dispatch semantics are still active work | `000d:46ec entity_vm_context_create_from_slot_index` and related masked-create wrappers | no single destroy method is highlighted in the current note set used here | context-side dispatch and busy-state updates through virtual or callback-like method surface at least on the context object | stable fields include `+0x32/+0x34`, `+0xd6/+0xd8`, `+0x102`, `+0x10c/+0x10e`, `+0x11b/+0x11d`, `+0x123` | Important for VM readability, but should follow runtime and owner-resource typing |
| `CacheBackendObject` | Medium | Small backend/cache loader object with DOS file-handle state and method table | constructor and callback roles are already explicit | `0009:5600 cache_backend_object_init` allocates `0x20` bytes and seeds method-table state | no explicit destructor named in current note slice | backend callback roles at `+0x34` and `+0x0c` are verified in cache lookup/load path | concrete `0x20`-byte size; fields at `+0x08`, `+0x0c`, `+0x10`, `+0x14`, `+0x16`, `+0x18`, `+0x1c` | Good contained family for early datatype work | | `CacheBackendObject` | Medium | Small backend/cache loader object with DOS file-handle state and method table | constructor and callback roles are already explicit | `0009:5600 cache_backend_object_init` allocates `0x20` bytes and seeds method-table state | no explicit destructor named in current note slice | backend callback roles at `+0x34` and `+0x0c` are verified in cache lookup/load path | concrete `0x20`-byte size; fields at `+0x08`, `+0x0c`, `+0x10`, `+0x14`, `+0x16`, `+0x18`, `+0x1c` | Good contained family for early datatype work |
| `PresentationCallbackBroker` | Low | Video/presentation-state callback broker rooted at `0x4588` | init/teardown/callback slot evidence is real, but subsystem naming remains intentionally conservative | `runtime_callback_object_init_once` family is documented, but not all constructor details are fully promoted here | `runtime_callback_object_teardown_once` and finalize path are explicit | vtable slots `+0x04`, `+0x08`, `+0x0c` all have live evidence | global state at `0x4588/0x458c/0x4590/0x4594/0x4595/0x45a6`; payload fields from caller objects at `+0x12d/+0x12f`, `+0x74f/+0x751` | Useful as a typed broker object later, but not a good first namespace/class pilot | | `PresentationCallbackBroker` | Low | Video/presentation-state callback broker rooted at `0x4588` | init/teardown/callback slot evidence is real, but subsystem naming remains intentionally conservative | `runtime_callback_object_init_once` family is documented, but not all constructor details are fully promoted here | `runtime_callback_object_teardown_once` and finalize path are explicit | vtable slots `+0x04`, `+0x08`, `+0x0c` all have live evidence | global state at `0x4588/0x458c/0x4590/0x4594/0x4595/0x45a6`; payload fields from caller objects at `+0x12d/+0x12f`, `+0x74f/+0x751` | Useful as a typed broker object later, but not a good first namespace/class pilot |
@ -50,13 +51,14 @@ If the goal is to make later class-authoring work fast and low-risk, the best or
1. `EntityDispatchEntryBase` 1. `EntityDispatchEntryBase`
2. `EntityDispatchEntryRuntimeState` 2. `EntityDispatchEntryRuntimeState`
3. `SpriteNode` 3. `SpriteNode`
4. `EntityVmOwnerResource` 4. `NPCActionProcess` family
5. `CacheBackendObject` 5. `EntityVmOwnerResource`
6. `WatchEntityController` 6. `CacheBackendObject`
7. `Entity` 7. `WatchEntityController`
8. `EntityVmRuntime` 8. `Entity`
9. `EntityVmContext` 9. `EntityVmRuntime`
10. `PresentationCallbackBroker` 10. `EntityVmContext`
11. `PresentationCallbackBroker`
This order prioritizes bounded families with visible constructors, derived variants, or explicit method tables before the larger gameplay and VM surfaces. This order prioritizes bounded families with visible constructors, derived variants, or explicit method tables before the larger gameplay and VM surfaces.

View file

@ -58,6 +58,7 @@ That set gives the high-level target, the current candidate families, the rebuil
- [docs/sprite-node-class-layout.md](docs/sprite-node-class-layout.md): `SpriteNode` destructor/event surface and candidate virtual-slot map. - [docs/sprite-node-class-layout.md](docs/sprite-node-class-layout.md): `SpriteNode` destructor/event surface and candidate virtual-slot map.
- [docs/entity-class-family-split.md](docs/entity-class-family-split.md): conservative split of the large `Entity` lane into base, projectile, debris, corpse/actor, and adjacent non-entity families. - [docs/entity-class-family-split.md](docs/entity-class-family-split.md): conservative split of the large `Entity` lane into base, projectile, debris, corpse/actor, and adjacent non-entity families.
- [docs/entity-vm-runtime-owner-resource-layout.md](docs/entity-vm-runtime-owner-resource-layout.md): current runtime/helper/context ownership model for the VM lane. - [docs/entity-vm-runtime-owner-resource-layout.md](docs/entity-vm-runtime-owner-resource-layout.md): current runtime/helper/context ownership model for the VM lane.
- [docs/npc-action-process-class-layout.md](docs/npc-action-process-class-layout.md): current owner-first class-lift state for the bounded seg033 NPC AI process family.
- [docs/presentation-callback-broker-layout.md](docs/presentation-callback-broker-layout.md): current object/lifecycle/vtable evidence for the `0x4588` presentation-state callback broker family. - [docs/presentation-callback-broker-layout.md](docs/presentation-callback-broker-layout.md): current object/lifecycle/vtable evidence for the `0x4588` presentation-state callback broker family.
- [docs/usecode-debugger-break-state-layout.md](docs/usecode-debugger-break-state-layout.md): current object/lifecycle/layout evidence for the dormant seg1408 debugger-state family. - [docs/usecode-debugger-break-state-layout.md](docs/usecode-debugger-break-state-layout.md): current object/lifecycle/layout evidence for the dormant seg1408 debugger-state family.
@ -134,9 +135,17 @@ Current authored `Remorse` classes in the active database are:
- `EntityVmRuntime` - `EntityVmRuntime`
- `EntityVmContext` - `EntityVmContext`
- `EntityVmSlotEntry` - `EntityVmSlotEntry`
- `NPCActionProcess`
- `StandProcess`
- `PaceProcess`
- `SurrenderProcess`
- `GuardProcess`
- `LoiterProcess`
The VM lane is still the furthest along in actual Ghidra authoring. Recent live batches added the bounded `EntityVmSlotEntry` class owner plus more owned `EntityVmRuntime` methods (`GetSlotChunkPtrAtOffset`, `ReleaseSlotChunkRef`, `TryUnloadSlotChunk`, `DebugDumpSlotMemory`, `ApplyToMatchingOwnerRows`) rather than stopping at free-function naming. The VM lane is still the furthest along in actual Ghidra authoring. Recent live batches added the bounded `EntityVmSlotEntry` class owner plus more owned `EntityVmRuntime` methods (`GetSlotChunkPtrAtOffset`, `ReleaseSlotChunkRef`, `TryUnloadSlotChunk`, `DebugDumpSlotMemory`, `ApplyToMatchingOwnerRows`) rather than stopping at free-function naming.
The new bounded NPC-family batch is intentionally lighter on datatypes than the VM and dispatch-entry work, but it is still real class lifting rather than mere renaming. The live database now has owner-first class shells for the seg033 AI-process family with `NPCActionProcess` as the shared base owner and `StandProcess`, `PaceProcess`, `SurrenderProcess`, `GuardProcess`, and `LoiterProcess` as derived behavior owners. The safest current stop point is still owner-first only: shared create/destroy/no-op virtual entries and the direct per-family create/run/destroy methods are lifted, while datatype and slot-order work remain open until the process-state layout and vtable roots are tighter.
The next planned pilot family is no longer purely preparatory either. `Remorse::EntityDispatchEntry` now exists as a real class owner in-session with a first provisional `/Remorse/EntityDispatchEntryBase` datatype covering the stable field block through `+0x18` and a matching `/Remorse/EntityDispatchEntryVtable` datatype exposing only the verified `+0x14` and `+0x28` callback slots. The first base-method batch has also landed from the old `0008:` note cluster after re-anchoring that range onto the live `11e0:` process-substrate segment: `InitBase`, `SetSourceType`, `SetEventTypeChecked`, `SetGroupId`, `Unlink`, and `IncrementGroupId` now live under the class owner with provenance comments preserved. The next planned pilot family is no longer purely preparatory either. `Remorse::EntityDispatchEntry` now exists as a real class owner in-session with a first provisional `/Remorse/EntityDispatchEntryBase` datatype covering the stable field block through `+0x18` and a matching `/Remorse/EntityDispatchEntryVtable` datatype exposing only the verified `+0x14` and `+0x28` callback slots. The first base-method batch has also landed from the old `0008:` note cluster after re-anchoring that range onto the live `11e0:` process-substrate segment: `InitBase`, `SetSourceType`, `SetEventTypeChecked`, `SetGroupId`, `Unlink`, and `IncrementGroupId` now live under the class owner with provenance comments preserved.
That family also has its first derived slice now. The old `000d:7e00/8078` runtime-state pair is re-anchored in the live `1440:` fade/palette cluster as `InitRuntimeState` and `ReleaseRuntimeState`, and `/Remorse/EntityDispatchEntryRuntimeState` now exists as a provisional overlay datatype with the recovered `+0x40..+0x4c` runtime-state tail fields. That is a meaningful pause point because the pilot family now has a class owner, a base datatype, a vtable shell, a first base-method batch, and one concrete derived/runtime-state batch rather than just one isolated constructor lane. That family also has its first derived slice now. The old `000d:7e00/8078` runtime-state pair is re-anchored in the live `1440:` fade/palette cluster as `InitRuntimeState` and `ReleaseRuntimeState`, and `/Remorse/EntityDispatchEntryRuntimeState` now exists as a provisional overlay datatype with the recovered `+0x40..+0x4c` runtime-state tail fields. That is a meaningful pause point because the pilot family now has a class owner, a base datatype, a vtable shell, a first base-method batch, and one concrete derived/runtime-state batch rather than just one isolated constructor lane.

View file

@ -141,6 +141,80 @@ Implication:
- but only after the debugger gump already exists and is registered - but only after the debugger gump already exists and is registered
- this does **not** give us a new no-patch retail entry path by itself - this does **not** give us a new no-patch retail entry path by itself
### 5. Retail still preserves a substantial debugger UI and command surface once the gump exists
The latest live `CRUSADER.EXE` decompile pass makes the surviving retail debugger capability map much clearer.
What retail can still do once a valid debugger object and gump already exist:
- `usecode_debugger_build_menubar` still builds the full hidden debugger menu bar with File, Run, Breakpoints, Search, and Data menus.
- `usecode_debugger_translate_registered_event` still translates the registered debugger/control event bundle into the local debugger command ids consumed by `usecode_debugger_handle_event`.
- `usecode_debugger_handle_event` still implements real debugger actions rather than decorative UI stubs: open file, run, break-next, single-step, go to line, watch, inspect, change global, search, search again, break to TDP, and watch clearing.
- `usecode_debugger_source_pane_draw_visible_lines` still clamps the source viewport, highlights the current line, scans the breakpoint table for visible marks, and draws the loaded source rows from the `.unk` buffer.
- `usecode_debugger_source_pane_handle_pointer_event` still converts pointer position into source line and column state and also drives scroll-style commands when the pointer is above or below the pane.
- the child-pane split is now clearer in retail than in the earlier first-pass mirror list: one pane is the loaded source view, while the sibling `13a0:16ee/1791/193f` lane is a separate watch-pane create/draw/click surface.
The live retail file-loading path is now also much clearer than before.
- `usecode_debugger_source_pane_load_file` resets local source-pane cursor state, destroys any previous source buffer, allocates a fresh source-buffer object from the requested path, updates the pane's line-count field, and refreshes the child widgets.
- `usecode_debugger_source_buffer_create_from_path` and `usecode_debugger_source_buffer_open_from_path` still build a dedicated far-memory file object, normalize the requested path, open the file, and pass it into the text-loader path.
- `usecode_debugger_source_buffer_load_text` still reads the whole file into far memory, rejects obviously non-text inputs, and hands the buffer to `usecode_debugger_source_buffer_split_lines_in_place`.
- `usecode_debugger_source_buffer_split_lines_in_place` still walks the loaded text, zero-terminates newline boundaries in place, and populates the per-line pointer table later consumed by draw, click, search, and goto-line logic.
- `usecode_debugger_source_buffer_get_line_ptr` is still the shared accessor used by source draw, pointer handling, find/find-next, and source-pane command handling.
Retail source navigation helpers are now closed well enough to describe the pane behavior directly.
- `usecode_debugger_set_line_selection` clamps the requested line against the loaded file range, clears transient cursor state, optionally updates the anchor line, and forces a redraw.
- `usecode_debugger_center_on_line` stores the target current line, computes a top-of-window line from the visible row count, and then delegates to `usecode_debugger_set_line_selection`.
- this means retail still preserves the ordinary source-browser workflow expected from the strings: load file, jump to line, center on current line, search through lines, and redraw the viewport around the active selection.
The watch-pane side is also more concrete now.
- `usecode_debugger_watch_pane_create` still allocates a real watch child gump and installs the watch-pane vtable.
- `usecode_debugger_watch_pane_draw` still iterates the shared 10-slot watch table at `1478:5580`, asks the break-state callback table to format each populated watch entry, and highlights the selected row.
- `usecode_debugger_watch_pane_handle_click` still converts pointer Y position into a watch-row index, updates the selected watch slot, and triggers a repaint.
- that is stronger evidence for `interactive watch list still survives` than the earlier weaker `watch-related strings still exist` wording.
Current safest interpretation:
- retail did not merely keep a few string tables or unused menu labels
- retail still keeps a functioning debugger front-end shell with real search, breakpoint, watch, inspect, and source-view logic
- the main missing piece is still how execution ever reaches that shell with valid live state
### 6. Retail callback and reachability limits are still the decisive difference from Regret
The same live pass also tightened the `what retail still cannot do` side.
Current direct-caller state in live `CRUSADER.EXE`:
- `usecode_debugger_open_for_current_unit` still has no recovered callers
- `usecode_debugger_open_modal` still has no recovered callers
- `Remorse::UsecodeDebuggerBreakState::Create` at `1408:0000` still has no recovered callers
- `usecode_debugger_handle_event` is still only reached through `usecode_debugger_translate_registered_event` and `usecode_debugger_forward_child_event`
Current callback state is still the key retail blocker too:
- `Remorse::UsecodeDebuggerBreakState::OnBreakTriggeredNoop` at `1408:046f` is still the live slot-0 callback
- `Remorse::UsecodeDebuggerBreakState::VtableSlot1ReturnZero` at `1408:0474` is still the live slot-1 callback
- unlike Regret, retail still has no recovered bootstrap that rewires the break-state object onto a live frontend-aware vtable
So the retail-versus-Regret split is now even sharper:
- both builds preserve a large debugger UI/event subsystem
- retail still lacks the recovered object bootstrap and live callback target that would naturally open the debugger on break
- Regret keeps both the writer/bootstrap path and the vtable upgrade that turns break-state callback slot `0` into a real `open_for_current_unit` launch path
That is why retail still reads as `functional debugger shell plus dormant break-state object`, while Regret reads as `same shell plus a surviving end-to-end open-on-break path`.
One practical refinement from the latest retail pass is that the shell is not merely a static menu/window skeleton.
- the file-open path still reaches a real far-memory source-buffer loader
- the search path still walks live source lines through `usecode_debugger_source_buffer_get_line_ptr`
- the goto-line path still updates line selection through the same source-pane helpers used by current-line centering
- the watch path still stores, formats, selects, clears, and redraws real watch rows
So the remaining barrier is still entry/bootstrap, not lack of interior debugger behavior after entry.
## What This Means For Usecode As An Entry Path ## What This Means For Usecode As An Entry Path
## Current Best Read ## Current Best Read
@ -261,13 +335,112 @@ Why this is now preferred over more retail patch fishing:
- if No Regret or JP No Remorse kept any surviving debugger bootstrap, it could collapse the retail problem from `invent a new path` to `port or mimic one missing write/call` - if No Regret or JP No Remorse kept any surviving debugger bootstrap, it could collapse the retail problem from `invent a new path` to `port or mimic one missing write/call`
- that is more likely to produce a truly minimal modification than another speculative retail patch chain - that is more likely to produce a truly minimal modification than another speculative retail patch chain
## Retail Ghidra Naming Backlog
The current note corpus now supports a tighter retail seg109 naming batch than the live authoring summary currently reflects.
These are the most important retail debugger-side helpers to promote explicitly in the active `CRUSADER.EXE` database before any new patch design work:
- `13a0:2882` = `usecode_debugger_build_menubar`
- `13a0:088f` = `usecode_debugger_source_pane_create`
- `13a0:0ae8` = `usecode_debugger_source_pane_init_view_from_break_state`
- `13a0:0ba7` = `usecode_debugger_source_pane_handle_command`
- `13a0:0f16` = `usecode_debugger_source_pane_handle_pointer_event`
- `13a0:1088` = `usecode_debugger_source_line_copy_for_display`
- `13a0:1118` = `usecode_debugger_source_pane_draw_visible_lines`
- `13a0:1413` = `usecode_debugger_source_pane_clamp_viewport`
- `13a0:15ac` = `usecode_debugger_source_pane_load_file`
- `13a0:16ee` = `usecode_debugger_watch_pane_create`
- `13a0:1791` = `usecode_debugger_watch_pane_draw`
- `13a0:193f` = `usecode_debugger_watch_pane_handle_click`
- `13a0:1c2c` = `usecode_debugger_translate_registered_event`
- `13a0:1dc6` = `usecode_debugger_forward_child_event`
- `13a0:2c2e` = `usecode_debugger_source_buffer_create_from_path`
- `13a0:2ca0` = `usecode_debugger_source_buffer_destroy`
- `13a0:2d14` = `usecode_debugger_source_buffer_open_from_path`
- `13a0:2e0a` = `usecode_debugger_source_buffer_load_text`
- `13a0:2f4f` = `usecode_debugger_source_buffer_split_lines_in_place`
- `13a0:301d` = `usecode_debugger_source_buffer_get_line_ptr`
Why this batch matters before more patching:
- it converts the remaining patch-target area from anonymous `FUN_13a0_xxxx` bodies into named UI, event, and source-buffer lanes
- it reduces the chance of patching the wrong helper when the debugger gump is already on-screen but still miswired
- it makes runtime-only experiments easier to reason about because the gump lifecycle, source loading, and event forwarding chain become legible in-session
The current strongest provenance for this retail batch is the combined retail `000b:* -> 13a0:*` table in `ne-segment1.md` plus the one-to-one structural match against the now-closed Regret `1398:*` family.
One live correction from the follow-up rename pass matters here: retail `13a0:16ee/1791/193f` reads as a watch-pane constructor/draw/click trio, while the source-pane lane remains centered on `13a0:088f/0ae8/0ba7/0f16/1088/1118/1413/15ac`. So the Regret-side structural match is still valuable, but the retail child-pane split is now sharper than the earlier first-pass list implied.
## Practical Alternatives To Manual Hex Patching
The current blocker is no longer `we do not know what to patch`. It is `the delivery path needs to stop depending on blind byte edits`.
### 1. Runtime-only proof via DOSBox-X debugger or equivalent live memory tooling
This is now the cleanest first confirmation path.
Use it to:
- keep the retail executable on disk unchanged
- patch or seed the debugger object only in live memory
- prove whether a create/store/open sequence is sufficient before committing to any permanent binary patch
What this should target first:
- seeding `1478:659c/659e` with a valid debugger-state object
- reusing the existing interpreter callback lane at `1418:049e..04b5`
- testing whether `13a0:020d` or the vtable callback path can open a stable debugger gump once state exists
This is especially attractive because it turns the current question from `did we edit the NE file correctly?` into `does the runtime model itself actually work?`
### 2. Scripted patch application to a writable clone, not manual hex editing
If a permanent retail patch is still wanted, the next step should be a reproducible patcher, not another manual byte-edit round.
That means:
- keep a dedicated writable clone of the executable
- store each patch as `address + expected old bytes + new bytes + reason`
- apply it through a scriptable patcher that validates the original bytes before writing
- regenerate the same patch on demand instead of hand-transcribing offsets each time
This can be done with PowerShell or Python against raw file offsets even if Ghidra export remains unreliable.
### 3. Use Ghidra only for analysis and verified byte plans
The current evidence does not support treating Ghidra export as the final patch-delivery mechanism for this lane.
What still works well:
- identify the correct callsite and byte budget in Ghidra
- annotate the patch rationale in-session
- test the control-flow hypothesis on a writable target
- then convert the verified result into an external patch manifest or launcher-side patcher
That keeps Ghidra in the role it is good at here: reverse-engineering and patch design, not blind final-binary distribution.
### 4. Keep `-u` as the low-risk data-side experiment surface
The `-u` override still does not solve the missing bootstrap, but it remains valuable for adjacent experiments that do not require byte writes.
Use it for:
- testing whether scripted monitor/camera/control families can get closer to debugger-adjacent compiled paths
- validating source-file and unit-name assumptions without touching the executable
- separating `data-side idea failed` from `patching workflow failed`
It should stay in the toolbox, but it should not be mistaken for a direct replacement for the missing retail bootstrap.
## Current Recommendation ## Current Recommendation
If the goal is the minimum modification that still has a realistic chance to work, the order should now be: If the goal is the minimum modification that still has a realistic chance to work, the order should now be:
1. Compare `REGRET.EXE` and JP `/ja/CRUSADER.EXE` for any surviving debugger bootstrap/writer. 1. Promote the retail seg109 naming backlog above so the remaining debugger lanes are explicit in Ghidra.
2. Keep `-u` / replacement `EUSECODE.FLX` as the preferred low-risk experiment surface for any script-side proxy ideas. 2. Use runtime-only memory seeding on a clean executable to prove or kill the bootstrap theory without committing file changes.
3. Do **not** resume broader retail executable patching unless the cross-build pass fails to yield a clearer bootstrap or the existing O/P family gets one clean runtime confirmation target. 3. Compare `REGRET.EXE` and JP `/ja/CRUSADER.EXE` for any surviving debugger bootstrap/writer that can replace a custom retail bootstrap.
4. Keep `-u` / replacement `EUSECODE.FLX` as the preferred low-risk experiment surface for any script-side proxy ideas.
5. Do **not** resume broader retail executable patching unless the runtime proof or cross-build pass yields one clear small patch plan that can be applied by script to a writable clone.
That ranking fits both the new live evidence and the user's practical constraint that complex retail patch attempts have already been unstable. That ranking fits both the new live evidence and the user's practical constraint that complex retail patch attempts have already been unstable.

View file

@ -15,6 +15,226 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
## Progress Snapshot ## Progress Snapshot
Latest verified batch: [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now includes a 2026-04-12 live MCP CLUT override routing closure pass on active `SLUS_002.68` centered on `0x80041458`, `0x80041144`, `0x80044bdc`, `0x80044e9c`, `0x800a9f48`, and `0x800a9f66`. Current best read is now exporter-critical and path-explicit: main-visible injects authored palette token while special-visible does not; submitter override gate is shared (`flags & 0xfffffff0`); and active override resolution diverges by submitter/resource-format lane (image-table and sprite format-2 use `psx_clut_override_table_by_palette_token[token]`, sprite non-format-2 uses token as a row key into `psx_clut_table_by_resource_bank`). Practical consequence is that token `0` is effectively no-override for this world-object path and exporter CLUT logic must branch by route lane plus submitter/resource format instead of flattening token handling. Live artifacts in this batch are targeted comments at `0x800415b0`, `0x800412d0`, `0x80044e10`, and `0x80044eb8`.
Latest verified batch: [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now includes a 2026-04-12 live MCP wall-family discriminator pass for the exporter regression where atlases repeat and wall faces collapse. Current best read is now split-explicit for `0x003e..0x004f`: constructor bind in `psx_object_create_simple_record`/`psx_object_create_compound_record` still converges on per-type `DAT_800758d8[type]`, while real divergence happens post-bind through selector install (`0x800260e8`), frame-token latch (`0x80025d68` -> `obj+0x94`), and stage-1 versus stage-2 route semantics (`0x80041458` vs `0x80041144`), including main-visible-only authored palette-token injection for `>=0x003e`. Immediate exporter consequence is to prioritize effective route/latch-state discrimination over inventing a new pre-constructor resource-bank split. Live artifacts in this batch are targeted comments at `0x80046038`, `0x80026100`, `0x80041554`, and `0x80040f88`.
Latest verified batch: [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now includes the 2026-04-12 VRAM-proof palette/export follow-up for `map_renderer/src/lib/psx-cache.js`, `src/config.js`, and `src/vue/controller/scene-presentation.js`. Current best read is now rule-explicit: `mode 1` PSX bundles should not trust bundle header palette index `+0x14` as the rendered selector, they should decode against one shared contiguous 256-entry CLUT equivalent to live VRAM row `0xF0`, `x=0`, with the old header value preserved only as diagnostic `defaultPaletteIndex`. The same batch also closes the static-export omission: the processed PSX catalog already contained `62` maps, so the "single map" symptom was export inclusion/config rather than cache enumeration.
Latest verified batch: [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now includes the 2026-04-12 focused no-placeholder exporter follow-up for `map 104` in `map_renderer/src/lib/psx-cache.js` plus renderer provenance surfacing in `src/vue/controller/scene-presentation.js`. Current best read is now exporter-explicit: mixed-role unresolved buckets no longer fall back to synthetic atlases, they resolve per authored-family + raw-`u5` cohort into actual PSX bundle art with preserved `mappingSource` / `artCohort` provenance. Focused validation rebuild now exports `1002` art items (`52` roots + `950` constructors), `0` fallback items, `1` atlas, and `136` shape definitions for scene fingerprint `3497e7f641856415`.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now includes the 2026-04-12 final live map-104 cohort closure pass on active writable `SLUS_002.68` anchored to scene fingerprint `3497e7f641856415` (`0x0042` roots `0x0022` items `25/35`, roots `0x0030` items `30/31`, constructor `0x0030` items `85/86`, and control `0x0066` item `53`) with focused create/update/draw/control inspection at `0x800249f4`, `0x80024eec`, `0x800131a8`, `0x80025d68`, `0x80041458`, `0x80041144`, `0x8002be6c`, `0x80013618`, and `0x80013688`. Current best read is now exporter-actionable: authored route seed split (`0x0022` vs `0x0030`) remains the strongest safe first-key divider; root-`0x0030` and constructor-`0x0030` cohorts still fail to diverge at creation and should not be split by origin alone; sampled `bit0x0400` and policy/runtime captures remain non-discriminating for this anchor set; and the immediate safe exporter change is route-seed bucketization for unresolved `0x0042` placeholders with conservative hold on deeper heuristics until runtime diagnostics sample concrete non-null values. Live artifacts in this batch are targeted decompiler comments at `0x800249f4`, `0x80024eec`, `0x800131a8`, `0x80025d68`, and `0x80041458`.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now includes a 2026-04-12 focused live runtime/control-island policy pass on active writable `SLUS_002.68` centered on `0x80063e54`, `0x80063e68`, `0x800675ec`, and `0x800675f8` with routing/draw/order consumers at `0x800131a8`, `0x80041458`, and `0x8002bf0c`. Current best read is now split-explicit: island tables are control/runtime gating structures and per-type policy modifies ordering/render/publication behavior, but stage-1 vs stage-2 visible lane choice still hinges primarily on object-local route bit `obj+0x1c & 0x0400` and submitter choice remains resource-kind based. Live artifacts in this batch are six conservative helper renames (`0x8002e598`, `0x8002e484`, `0x8002e498`, `0x8002e3e8`, `0x800308ac`, `0x800304c4`) plus targeted comments at `0x80039fd8`, `0x80013518`, `0x80013550`, `0x8004161c`, `0x8002bf2c`, and `0x80034d60`.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now includes a 2026-04-12 focused live loader/install pre-constructor closure pass on active writable `SLUS_002.68` centered on `0x80039444`, `0x8003917c`, `0x80045ffc`, `0x8002badc`, and `0x80040768`. Current best read is now install-boundary explicit: WDL load installs type art/state lanes and section-pack pointers before root dispatch; CLUT install is explicit in `level_palette_header_apply`; detached runtime-stream payload install is explicit in `psx_install_level_audio_runtime_stream_bundle`; and constructors therefore start with preinstalled art/state/policy/CLUT context instead of raw section-0 rows alone. Live artifacts in this batch are one conservative function rename (`0x80040768`) plus five targeted decompiler comments across loader/install entry points.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now includes a 2026-04-12 focused live draw-submission closure pass on active writable `SLUS_002.68` centered on `0x8004137c`, `0x80041458`, `0x80041144`, `0x80040d44`, `0x80044bdc`, and `0x80044e9c`. Current best read is now submission-explicit: both world-visible lanes select submitter by bound resource kind (`kind==5` image-table else sprite), both consume live frame token `obj+0x94`, and CLUT resolution converges on `psx_clut_table_by_resource_bank` with optional `psx_clut_override_table_by_palette_token[(flags>>8)]`. The strongest lane split is palette-token injection: main-visible ORs authored high-byte token (`source+0x06` or `+0x0c`) into submit flags, while special-visible does not, giving an executable-backed discriminator for unresolved placeholder-family export behavior.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now includes a 2026-04-12 focused live selector/transition pre-latch closure pass on active writable `SLUS_002.68` centered on `0x80018578`, `0x8001bca0`, `0x8001e6e8`, `0x800260e8`, and `0x80025d68` with direct row bytes from `0x80063c1c` and `0x80063d68`. Current best read is now stage-explicit: type-`0x0042` pre-latch reseat is early-gated by view margin and object lane bit `0x0020`; transition row lookup remains `DAT_80063a00` -> `DAT_80063b4c`; selector `3/4` effects route through `psx_object_select_state_script` install (`obj+0x9e`) before final live frame-token latch at `obj+0x94` in `psx_object_advance_state_script`. Live artifacts in this batch are five targeted decompiler comments at `0x80018578`, `0x8001bca0`, `0x8001e6e8`, `0x800260e8`, and `0x80025d68`, plus a new exporter implication section for map-104 placeholder-family splitting by pre-latch selector versus latched frame token.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now includes a 2026-04-12 focused live type-art install and constructor-binding closure pass on active writable `SLUS_002.68` centered on `0x80045ffc`, `0x800249f4`, `0x80024eec`, `0x80038f18`, and `0x80041458` with globals `0x800758d8/0x800758c8`. Current best read is now lane-explicit from load/install to draw: install writes active-header slot then resolves kind-4/5 resource and commits built-resource cache; constructors consume active-header by type and either reuse built-resource (kind 5) or build per-instance resource; draw uses ctor-bound `obj+0x10` resource and live `obj+0x94` frame token with submitter selected by resource kind. Live artifacts in this batch are five targeted decompiler comments at `0x80045ffc`, `0x800249f4`, `0x80024eec`, `0x80038f18`, and `0x80041458` to preserve exporter-relevant semantics for unresolved map-104 families.
Latest verified batch: [docs/psx/map-storage-model.md](docs/psx/map-storage-model.md) now includes a 2026-04-12 live section-0 authored-family descriptor-dispatch closure on active `SLUS_002.68` centered on `0x800256b0`, `0x800258cc`, and descriptor table lane `0x80063118/0x80063220/0x800626f8`. Current best read is now convergence-explicit: unresolved families `0x0042`, `0x0049`, and `0x0055..0x0063` share descriptor row `0x800626f8` (slot0 `0x80013618`, slot1 `0x80013688`, slot2 `0x800254c8`) for both root-dispatch and constructor-placement section-0 records, so type divergence should be pursued in per-type banks/policy/state lanes rather than in section-0 descriptor callback identity.
Latest verified batch: [docs/psx/map-storage-model.md](docs/psx/map-storage-model.md) now includes a 2026-04-12 live object-creation/state-selection closure pass on active `SLUS_002.68` centered on `psx_object_create_simple_record`, `psx_object_create_compound_record`, `psx_spawn_compound_record_advance_state_once`, `psx_spawn_simple_record_set_active_flag`, `psx_object_select_state_from_transition_table`, `psx_object_advance_state_script`, and `psx_type42_transition_selector_tick`. Current best read is now chain-explicit from authored row to visible frame: constructors copy authored `u5` into `obj+0x1c` and seed selector via `u4`; transition/reselection lanes can mutate selector/low control bits; `psx_object_advance_state_script` latches final live frame token into `obj+0x94`; projection/draw consume `obj+0x94` directly through `psx_resource_frame_*` helpers with submit path chosen by bound resource kind. Live artifacts in this batch are one helper rename (`0x8003a37c -> psx_queue_global_draw_tint_pulse_once`), two supporting data labels (`0x80067544`, `0x80067614`), and targeted decompiler comments across spawn/select/latch/projection entry points.
Latest verified batch: [docs/psx/map-storage-model.md](docs/psx/map-storage-model.md) now includes a 2026-04-12 live loader/storage ownership clarification pass on active `SLUS_002.68` centered on `wdl_resource_bundle_load_by_index` (`0x80039444`), `psx_lzss_unpack_into_level_buffer` (`0x8003b00c`), `psx_lzss_pack_level_buffer` (`0x8003aba8`), and `psx_load_type_state_banks` (`0x8003917c`). Current best read is now ownership-explicit in extractor terms: `0x80067838` is promoted to `psx_level_section_pack_base`, `0x800676d8` to `psx_level_clut_table_ptr`, the `0x3e00` compressed/decompressed lane (`0x8006b5d8` -> `0x8006769c`) is confirmed as persistent runtime substrate with save-side repack parity, and constructor consume-site `0x80024c60` now explicitly links loader-installed `psx_type_simple_component_bank[type]` payloads to live behavior program fields.
Latest verified batch: [docs/psx/map-storage-model.md](docs/psx/map-storage-model.md) now includes a 2026-04-12 live marker/control runtime-island clarification pass on active `SLUS_002.68` centered on `0x80063e54`, `0x80063e68`, `0x800675ec`, and post-load mode-action sequencing at `0x80039ef4`. Current best read is now control-explicit: the selector/map tables are post-load and slot-gating control structures (not direct art lanes), runtime block persistence is guarded by snapshot sentinels (`0x80031878`/`0x80031a3c`), and a previously anonymous bounded marker-action queue family (`0x80030cf0/0x80030dfc/0x80030ed4/0x80030ebc`, count at `0x80067798`) now has conservative live names/comments to preserve queue-mediated transition behavior.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now includes a 2026-04-12 live visibility-routing and draw-lane clarification pass on active `SLUS_002.68` centered on `psx_object_integrate_motion_and_route_visible`, `psx_main_visible_list_sort_range`, `psx_main_visible_order_graph_unlink_pair`, `psx_main_visible_order_graph_detach_object`, `psx_draw_main_visible_object`, `psx_draw_special_visible_queue`, `psx_sprite_resource_submit_frame`, and `psx_image_table_submit_frame`. Current best read is now rule-explicit: stage-1 routes through main-visible list/sort and stage-2 routes through the special-visible queue; ordering uses dependency-graph links with policy-bit influence (`0x0008`, `0x0600`); submitter choice is strictly resource-kind (`kind==5` image-table else sprite); main-visible applies authored palette-token overrides while special-visible does not; and submitters converge on CLUT selection through `psx_clut_table_by_resource_bank` with optional override via `psx_clut_override_table_by_palette_token[(flags>>8)]`.
Latest verified batch: [docs/psx/map-storage-model.md](docs/psx/map-storage-model.md) now includes a 2026-04-12 live runtime-bank/art-install clarification pass on active `SLUS_002.68` centered on `0x8003917c`, `0x80045ffc`, constructor consumers, and type-bank globals `0x800758c8/cc/d0/d4/d8` plus policy table pointer `0x800675f8`. Current best read is now role-split and loader-to-renderer explicit: state-bank installs seed scripts/components/extents; art installs resolve active-header and built-resource lanes with post-install alias behavior; and policy-table install at load bridges into world draw/order/interaction consumers. Live artifacts in this batch are two evidence-backed renames (`0x8003917c -> psx_install_type_state_script_component_extents_banks`, `0x80045ffc -> psx_install_type_art_active_header_and_built_resource`) plus targeted decompiler comments at `0x800396cc`, `0x800399b4`, `0x8003970c`, `0x800399f4`, `0x80039ad0`, `0x8003977c`, `0x800460c8`, `0x800460d4`, `0x800398f0`, and `0x80041604`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-12 live countdown-versus-slot frame-order closure centered on map-54 split `0x800208f0` and world-frame call order at `0x8002b830`. Current best read is now ordering-explicit: countdown terminal mode split/write runs before behavior and deferred-control lanes that can reach slot-`0x0f` arm sink `0x800232f0`, so boundary timing pressure is strongest, failure-side suppression is plausible, and plain countdown-success direct causality remains weakest. Live artifacts in this batch are conservative decompiler comments at `0x8002b830`, `0x80020900`, and `0x800232f0`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-12 live authored-producer reachability correction pass centered on `0x80026710`, `0x8002677c`, `0x80027ecc`, and `0x800214ac`. Current best read is now guard-corrected: `0x80026710` bounds record arg-count (`word0-1 < 0x0a`), not opcode value, so opcode `54 -> subop 49 -> sink 0x800214ac` remains a viable authored-program lane. Practical consequence is that strongest candidate producer context is again the type-state behavior record stream seeded from `psx_type_simple_component_bank[type]`, with tuple `(0x0f,0x0a,0x04)` classified as authored-static source data transported through optional runtime arg-index remap.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-12 live progression-latch timing pass centered on `0x80027548`, `psx_map_progression_table`, and map-54 boundary transitions. Current best read is now ordering-explicit: `0x80027548` stages `next_map` via `psx_level_session_set_next_map_id` into deferred latch `DAT_800678d0`, `current_map_id` only commits at session rollover (`0x80031edc`), and natural tuple miss risk is therefore a deferred preemption window (`54 -> 55` rollover before slot-`0x0f` `(0x0a,0x04)` emit), not a same-tick overwrite race.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-12 live synthesis-only conservative-stabilization pass for the natural in-level JL-9 event lane. Current best read is now wording-stable without overreach: callback entries `0x8002745c`/`0x80027548` stay classified as indirect callback-table targets (no direct callers), countdown tick `0x80020794` stays classified as world-frame control timing with explicit map-54 boundary split, and live comments now explicitly separate timing/context evidence from still-unproven direct tuple-production proof.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-12 live callback-table ownership pass on region `0x800641f0..0x80064220` for entries `0x8002745c` and `0x80027548`. Current best read is now owner-explicit: this region is part of `psx_behavior_opcode_handler_table` (`DAT_800641ac`) dispatched by `psx_object_behavior_opcode_dispatch` (`0x8002677c`), with exact indices `0x15` and `0x19`; selection is opcode-driven from behavior program words, while the only proven active caller lane still enforces `(opcode_word-1) < 0x0a` at `0x80026710`, so these entries remain topology/timing evidence rather than the strongest direct proven producer lane for slot-`0x0f` arm. Live artifacts in this batch are conservative decompiler comments at `0x8002685c`, `0x80026710`, `0x8002745c`, and `0x80027548`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-12 live slot-`0x0f` subcase taxonomy pass beyond tuple `(0x0a,0x04)` on active `SLUS_002.68`. Current best read is now family-explicit: surrounding `0x0f` branches cluster as transition/countdown/objective-state and message lanes (`0x0a/1..3`, `0x0a/0x2e`, `0x01/1`, `0x04/1`, `0x06/0x42`), while `(0x0a,0x04)` remains the only recovered eligibility-arm write to `psx_debug_extra_channel_gate` under non-hidden header-state-3 predicates. Live artifacts in this batch are one conservative rename (`0x80049014 -> psx_audio_cdxa_select_and_play_cue`) plus targeted decompiler comments at `0x80023154`, `0x800231b0`, `0x8002321c`, `0x80023334`, `0x80023390`, `0x800236d8`, and `0x800237cc`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live transition-callback provenance pass centered on `0x8002745c` and `0x80027548`. Current best read is now provenance-explicit and timing-aware: both entries are indirect callback-table targets from region `0x800641f0..0x80064220` (no direct caller xrefs), `0x8002745c` can carry or suppress slot-`0x0f` tuple opportunity depending on branch (`DAT_80078a14==0` path calls `0x80020f7c`, alternate path skips it), and `0x80027548` progression apply (`map_progression_table[current_map_id]`) makes natural tuple firing more timing-sensitive by advancing transition state before later control-event handling.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live producer-side authored-source closure pass centered on the action-record frame consumed by `psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`). Current best read is now loader-explicit: `psx_load_type_state_banks` (`0x800391f0`) installs `psx_type_simple_component_bank[type]` from level bundle type-state payloads, object constructors seed component `program_base/pc` from that bank (`0x80024c60/0x80024c88`), and behavior tick/dispatch (`0x80026740 -> 0x8002677c -> 0x80027ecc`) transports authored operands into the sink frame. Practical consequence is that tuple `(slot,arg1,arg2)=(0x0f,0x0a,0x04)` now looks authored-static at source context with runtime index-resolution as transport, while exact emitting type-row attribution remains open. Live artifacts added in this batch are conservative decompiler comments at `0x80039250`, `0x80024c60`, and `0x80026740`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live countdown-versus-slot-family structure check driven by the timed-segment failure clue. Current best read is now causality-tight: `psx_control_event_countdown_transition_tick` (`0x80020794`) and `psx_control_event_apply_countdown_step` (`0x800205e8`) are in the world-frame countdown lane, while natural JL-9 arm remains slot-dispatch lane (`0x800214ac -> table 0x800640a0 -> slot0f 0x800230e4 -> writer 0x800232f0`) with no recovered direct countdown-to-slot call edge; practical consequence is that timer behavior is likely related by shared control-state/timing pressure rather than being the same direct feeder branch.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live multi-map last-mission chain validation pass centered on `psx_map_progression_table`, selector-to-map mapping, and late transition helpers. Current best read is now split-map explicit: map `54` is still the strongest natural selector anchor (`0x0f -> 54`), while maps `55..58` are downstream transition phases in the same slot-`0x0f` gate family (`map_to_slot[54..58]=0x0f`) with contiguous progression (`54->55->56->57->58->0xff`), which better explains the JL-9 gate-host family without displacing `54` as the most reproducible natural host.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live synthesis pass driven by two new clues (`timed failure` and `multi-map last mission`) with fresh caller/xref evidence from active `SLUS_002.68`. Current best read is now ranking-explicit: optional scripted event inside the countdown/transition chain is strongest, countdown-branch timing miss second, map-to-map transition interaction third, and plain countdown-success branch weakest; practical consequence is that next runtime validation should prioritize tuple-emission observability (`slot 0x0f`, `0x0a/0x04`) at the map-54 boundary path (`0x800208f0`) with progression callback context (`0x80027548`).
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live alternate-feeder search centered on sink targets `0x800214ac` and `0x800230e4` with explicit control/deferred/callback lane checks. Current best read is now tighter in one key way: no new direct literal/table feeder into either sink was recovered beyond `0x800636d4 -> 0x800214ac` and `0x800640dc -> 0x800230e4`, so the strongest non-`54 -> 49` candidate is currently transition/control-driven (`psx_level_gate_slot05_handler` at `0x80022068`, `psx_control_event_slot0e_handler` case-6 at `0x80023074`, and helper callsite `0x8002748c` into `psx_control_event_apply_level_channel_preset`) rather than a newly proven behavior-opcode producer path.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live level-54 boundary clue pass for natural `MFM4` failures. Current best read is now map-specific and timing-tight: `psx_control_event_countdown_transition_tick` (`0x80020794`) has an explicit split at `current_map_id 0x36` (`<=54 -> mode 0x1a`, `>54 -> mode 0x1b`), and additional optional late control lanes (`psx_level_gate_slot05_handler` `(0x0a,0x28)` plus progression callback `0x80027548`) can advance state before slot `0x0f` `(0x0a,0x04)` arm writes `psx_debug_extra_channel_gate`. Practical consequence is that natural failure is now best explained as optional-path/timing miss around map-54 late transitions, not wrong-host selection.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live slot-handler sibling recovery pass centered on `psx_level_gate_slot_handler_table` (`0x800640a0`) with newly created table-entry functions at `0x800215fc`, `0x80021810`, `0x800219e4`, `0x80021fac`, `0x80022214`, `0x800222e8`, `0x800223cc`, `0x800226e0`, `0x800227ac`, `0x80022b50`, `0x80023854`, and `0x80023af0`, plus a control-pair rename at `0x80022940`. Current best read is now sibling-explicit across slot-family `0x0a..0x0f`: adjacent `0x0a` subcases are control/message/transition lanes, while slot `0x0f` case `(0x0a,0x04)` remains the only recovered sibling branch that arms `psx_debug_extra_channel_gate` at `0x800232f0`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live upstream-producer closure pass for `psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`). Current best read is now structure-explicit: the sink consumes a pointer-frame (`record+0/+4/+8/+c`) forwarded from `psx_behavior_subopcode_dispatch` (`0x80027ecc`) as `local_60+1`, and those pointer fields are produced in `psx_object_behavior_opcode_dispatch` by either direct pointer words or index-resolved slot pointers (`base + index*4`). Practical consequence is that tuple `(slot,arg1,arg2)=(0x0f,0x0a,0x04)` is now bounded as behavior-script-produced pointer data rather than a sink-local immediate constant, while the exact shipped authored producer instance remains an open runtime-trace target.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live event-only synthesis continuation pass for the natural gate-arm lane. Current best read is now slightly tighter but still conservative: `0x800230e4` is promoted to `psx_control_event_slot0f_handler` (slot-family role, not single-branch setter), slot-table entries `0x800640d4/0x800640d8/0x800640dc` are explicitly labeled as slot `0x0d/0x0e/0x0f` handler entries, and comments at `0x80064284` and `0x800636d4` now preserve the key split between structural topology (`54 -> 49 -> sink`) and still-unproven active reachability beyond the known `<0x0a` caller lane.
Latest verified batch created a dedicated event-only working note at [docs/psx/jl-9-in-level-event.md](docs/psx/jl-9-in-level-event.md). Current best read is now organized around the natural gate-arm problem specifically: sink dispatcher `0x800214ac..0x800215f8`, slot-family handlers `0x0d/0x0e/0x0f`, exact arm tuple `(slot 0x0f, arg1 0x0a, arg2 0x04)`, host family `{54,55,56,57,58,82}`, and the still-open upstream authored producer. Practical consequence is that future JL-9 event passes should resume from the dedicated event note instead of re-mixing event-only details into the broader passcode and inventory note.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 sink-feeder-only closure pass on live `SLUS_002.68` centered on `0x800214ac..0x800215f8` with explicit argument-byte loads at `0x800215cc/0x800215e0`. Current best read is now artifact-tight: `0x800214ac` is promoted as `psx_level_gate_slot_dispatch_from_action_record`; dispatch slot is loaded from `*(*(record+0))`; handler args are loaded from `*(*(record+8))` and `*(*(record+0xc))`; and the JL-9 gate-arm tuple is bounded to slot `0x0f` with `(arg1,arg2)=(0x0a,0x04)` into `psx_set_debug_extra_channel_gate` (`0x800230e4`) through `psx_level_gate_slot_handler_table[0x0f]` at `0x800640dc`. Upstream table topology is now preserved in live comments as `psx_behavior_opcode_handler_table[54]=0x80027ecc` and `psx_behavior_subop_handler_table[49]=0x800214ac`, while active reachability from the currently proven gameplay caller path remains bounded by `(opcode_word-1) < 0x0a` at `0x80026710`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 host-level closure pass dedicated to natural JL-9 event hosting across family `{54,55,56,57,58,82}` using live table bytes and late-transition caller checks. Current best read is now ranking-explicit: map `54` remains the strongest natural host because it is the reciprocal selector-anchored slot-`0x0f` entry (`0x0f -> 0x36`), while `55..57` stay plausible transition-phase hosts, `82` remains family-valid but weakly anchored for published-code replication, and `58` is now de-prioritized as a stable host because `psx_map_progression_table[58] = 0xff` marks terminal progression pressure; practical failure model for natural `MFM4` is now timing/event-miss (missing tuple `(0x0a,0x04)` before hidden phase), not passcode decode mismatch.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live caller/context reassessment around `0x80026690` / `0x8002677c` with a sibling control-runner check. Current best read is now split cleanly between active proof and topology: the only proven caller into `psx_object_behavior_opcode_dispatch` remains the `<0x0a` bounded lane at `0x80026710`, while `54 -> 49 -> 0x800214ac` remains structurally valid table linkage but should be deprioritized as an immediate active explanation until a second caller/context lane is recovered. The same batch also lands conservative live artifacts (`0x80027ecc` function creation/name and `0x80020f7c` rename with caller-bound comments).
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live MCP late-objective pass focused on mission-complete text handlers, transition siblings, and uncommon reward-branch outcomes in the slot `0x0d`/`0x0e`/`0x0f` control-event family. Current best read is now path-tight: slot `0x0e` case `0x0a/6` is a concrete late transition that applies selector `0x0f`, selector `0x0f` is the only sampled direct selector lane into channel `0x0f`, and natural JL-9 gate arm remains bounded to slot `0x0f` case `0x0a/4` (`0x800232f0`) under `hidden==0 && runtime_header_state==3`; practical consequence is that the remaining blocker is now specific authored event provenance inside that channel-`0x0f` family, not branch existence.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live closure pass dedicated to decoding the control-event 0x0a family around `0x80022c6c..0x80023390`. Current best read is now sibling-explicit: slot `0x0d` (`0x80022c6c`) and slot `0x0e` (`0x80022ea8`) are recovered as mission-complete/control-event siblings to slot `0x0f` (`0x800230e4`), and tuple `(0x0a,0x04)` remains the strongest natural pre-hidden gate-arm branch because it is the only recovered in-family case that writes `psx_debug_extra_channel_gate` under `hidden==0 && runtime_header_state==3`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 event-only synthesis pass for the natural JL-9 in-level gate-arm lane plus a conservative live Ghidra naming/comment sweep on still-raw central entities. Current best read stays intentionally split between what is proven and what is only topology: sink-side event evidence remains strong (`0x800214ac..0x800215f8` -> `slot 0x0f` -> `0x800230e4`, tuple branch `0x0a/0x04` at `0x800232f0`), while upstream `54 -> 49` is retained as structural table linkage pending proof of a second active caller/context beyond the known `(opcode_word-1) < 0x0a` guard at `0x80026710`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 active-caller recheck focused on natural gate-arm production into sink dispatcher `0x800214ac..0x800215f8`. Current best read is now priority-corrected: sink-side tuple closure remains strong (`slot 0x0f`, `arg1 0x0a`, `arg2 0x04` into `0x800230e4`), but the only proven gameplay caller lane into `psx_object_behavior_opcode_dispatch` remains bounded by `(opcode_word-1) < 0x0a` at `0x80026710`, so the earlier `54 -> 49` path should stay as table topology and be deprioritized as an immediate active explanation until a second caller/context lane is recovered.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 user experiment follow-up that materially weakens `MFM4` as the main mystery. Current best read is now experiment-backed: a natural `MFM4` trial did **not** yield `JL-9`, while `JFM4` plus manual gate byte `0x8006739d = 0x01`, then `L0SR`, then `R1 + Circle`, still produced `JL-9`. Practical consequence is that `MFM4` remains the best natural prime candidate on code grounds, but the dominant unresolved question is now the natural in-level gate-arm event rather than the passcode choice itself. Deferred user experiments to revisit later are `2`, `4`, `5`, and `6` from the current emulator test plan.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes user-validated downstream closure for the forced JL-9 test. Current best read is now causal-explicit: manual byte poke `0x8006739d = 0x01`, then `L0SR`, then `R1 + Circle` successfully produces `JL-9`, which proves the late hidden/input grant half is correct and that the gate byte alone is sufficient for the extra `0x0d` branch once hidden trigger runs. Practical consequence is that the more direct thing being bypassed is the natural in-level gate-arm event, while `MFM4` is now demoted from “lead candidate” to “best natural prime candidate” for recreating the same latch without manual memory edits.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` hard-clear-theory check and manual gate-poke closure. Current best read is now split cleanly: the "beat the game on hard, then enter `L0SR`" story remains weak-to-medium because completion/congratulation handlers were found but no direct bridge from endgame flow into `psx_debug_extra_channel_gate` was recovered; meanwhile the practical emulator route is now strong, because `psx_debug_extra_channel_gate` is confirmed as byte `0x8006739d`, read as nonzero at `0x8002fff4`, with no recovered clear in inspected session/load/menu paths, so manual `0x01` poke + hidden `L0SR` + `R1 + Circle` is now the best forced test for the extra `0x0d` lane.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` published-mission-code sweep against the user-supplied level-code table. Current best read is now candidate-explicit: ordinary decode rows are `i=0x00..0x0e`, ordinary selector return is `s=i+1`, and first-char difficulty only writes `psx_level_runtime_header_state`. Practical JL-9 consequence is that `MFM4` (Level 15 hard) is now the only strong published-code prime recovered so far, because ordinary row `i=0x0e` yields selector `s=0x0f`, maps through `DAT_80063e54[0x0f]=0x36`, and lands in gate family `DAT_80063e68[54]=0x0f` while also setting `runtime_header_state=3`; `LRTN/MRTN/PRTN` remain strong negatives because the `?RTN` family clears header state to `0`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` reachability-correction pass on the previously claimed step-2 producer chain. Current best read is now split cleanly between topology and proof: table topology still shows `DAT_800641ac[54] = 0x80027ecc` and `DAT_80063610[49] = 0x800214ac`, but static reachability currently proves only one caller to `psx_object_behavior_opcode_dispatch` (`0x80026740`) and that path bounds `(opcode_word-1) < 0x0a` before dispatch, so high-index entries are not yet proven active from the known path. Practical consequence is that sink-side gate-writer evidence remains strong (`0x800214ac..0x800215f8` -> `DAT_800640a0[0x0f] = 0x800230e4`, args `0x0a/0x04`), while the open upstream task is now narrowed to recovering a second proven caller/context that can feed high behavior-opcode indices.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` step-2 producer-path closure pass for the gate-arm dispatcher source. Current best read is now structurally explicit upstream: `psx_object_behavior_opcode_dispatch` table index `54` reaches handler `0x80027ecc`, which secondary-dispatches through `DAT_80063610[subop]`; sub-op `49` maps to `0x800214ac`, which then performs the level-gated slot dispatch into `DAT_800640a0[0x0f] = 0x800230e4` with arg bytes from record pointers (`0x800215cc/0x800215e0`). Practical consequence is that step-2 is now identified as an in-level behavior/control opcode event lane (`54 -> 49`), with the remaining open item narrowed to one concrete authored mission/object instance rather than the producer subsystem itself.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` NRTN/?RTN decode-to-apply closure over `0x8003ec8c`, `0x80034c14..0x80034ddc`, and `0x80021138`. Current best read is now delay-slot exact: special index `0x0f` clears `psx_level_runtime_header_state` but returns raw selector `0x10`, eval maps through `DAT_80063e54[0x10]=0x3f`, and caller therefore does not take the `s0==0` skip; apply executes selector-`0x10` case at `0x8002142c` (writes `DAT_800675e4=0x1d`, common helper `a0=0x36`). Practical JL-9 consequence remains strict: NRTN/?RTN does not itself satisfy gate-arm predicate `runtime_header_state==3` and cannot alone coexist with the required non-hidden gate-arm state.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a 2026-04-11 correction to the earlier Mission 16 / developer-office theory. Current best read is that the attractive `DAT_80063e54[0x0f] = 0x36 -> DAT_80063e68[0x36] = 0x0f` chain was the wrong table index for the runtime `?RTN` path; the live caller path uses returned selector `0x10`, maps through `DAT_80063e54[0x10] = 0x3f`, and only later executes selector-`0x10` apply logic with common-helper `a0 = 0x36`. Practical consequence is narrower: office-like content may still be involved, but current static evidence no longer proves that `NRTN/?RTN` places the session directly inside the slot-`0x0f` gate family.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` hypothesis check for the exact `NRTN -> in-level office event -> ?0SR/L0SR -> input 0x1e` recipe. Current best read is now classification-explicit: the two-phase structure remains code-backed (`pre-hidden gate-arm at 0x800232f0` then hidden/input trigger at `0x80013154..0x80013174`), dispatch evidence still supports a real in-level slot-`0x0f` gate-arm path via `0x800214ac..0x800215f8` into `0x800230e4`, but the exact player-visible office event and deterministic `NRTN` role are still not singularly closed by static evidence; practical status is therefore `plausible but unproven` rather than contradicted.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` clarification pass on what user-facing "step 2" actually means. Current best read is now much less misleading: the gate-arm write is not a second passcode-screen action but an in-level scripted/control dispatch routed through `0x800214ac..0x800215f8`, where `DAT_800640a0[0x0f] = 0x800230e4` (`psx_set_debug_extra_channel_gate`) and the exact writer tuple is `(slot 0x0f, param_2 0x0a, param_3 0x04)`; meanwhile hidden `?0SR` / `L0SR` still uses the shared passcode decoder but returns selector `0`, so it does not follow the ordinary nonzero mission/apply-load branch. Practical consequence is that the current JL-9 recipe is now explicitly `normal passcode prime -> in-level scripted gate-arm event -> hidden passcode -> R1+Circle`, with the remaining blocker narrowed to naming the concrete in-level event.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` step-2 caller narrowing pass on the gate-arm writer path (`0x800230e4` -> `0x800232f0`). Current best read is now dispatch-concrete: reachability is through an indirect level-gated dispatcher at `0x800214ac..0x800215f8` (`DAT_80063e68[current_level]` compare + `DAT_800640a0[a2]` handler table), with slot `0x0f` resolving to `psx_set_debug_extra_channel_gate`; handler arguments are loaded from action-record byte pointers; and the exact gate-arm tuple is tightened to `(dispatch index 0x0f, param_2 0x0a, param_3 0x04)`, narrowed to a small level-scripted family where `DAT_80063e68 == 0x0f` (`54..58,82`) rather than a single globally named menu callback.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` gate-survivability closure pass across passcode application, level-load/session loops, and menu-reset helpers. Current best read is now lifecycle-explicit: `psx_debug_extra_channel_gate` (`0x8006739d`) remains one-writer/one-reader (`0x800232f0` set, `0x8002fff4` read) with no recovered static clear in inspected load/menu paths; hidden decode (`0x8003ed28`) still returns selector `0` and skips normal apply/load branch checks around `0x80034d84`; and practical two-phase ordering remains `non-hidden gate arm first -> hidden/input trigger later`, with step 2 supported after a level load in the same running session.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` passcode-screen semantics closure for the normal-versus-hidden entry question and the JL-9 sequence wording check. Current best read is now user-flow explicit: normal and hidden passcodes both route through `psx_passcode_screen_eval_current_entry`/`psx_passcode_decode_to_mission_selector`; hidden `0x10` still sets `psx_hidden_passcode_flag` in that same decoder; and the unnamed caller block around `0x80034c14` has distinct immediate branch behavior for eval return `0` versus nonzero before transition/setup calls. Practical consequence is that the existing JL-9 sequence remains directionally right on two-phase logic (`pre-hidden gate arm` then hidden/input trigger) but is now marked operationally under-specified until one live trace pins the exact player-visible producer for `param_2==0x0a,param_3==4` into `psx_set_debug_extra_channel_gate`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` gate-arm caller-path closure around `psx_set_debug_extra_channel_gate` (`0x800230e4`) and the exact writer at `0x800232f0`. Current best read is now sequence-tight: the writer executes only in the `param_2==0x0a,param_3==4` branch; the store requires `psx_hidden_passcode_flag==0 && psx_level_runtime_header_state==3`; and the required `runtime_header_state==3` comes from the normal passcode first-char delta branch at `0x8003ed58` rather than special hidden/sibling passcode branches. Practical closure remains two-phase (`normal non-hidden state-3 prime -> gate-arm event -> hidden passcode -> input code 0x1e`) with one bounded remaining gap: naming the exact player-visible producer for the `0x0a/4` event inside the nearby undefined dispatch block.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` RP-16 startup/default follow-up that pushed on undefined nearby init stubs, startup mode-action dispatch callsites, selected-id writer closure, active-channel writes, and level mode/difficulty tables. Current best read is tighter and still negative for startup RP-16: `committed_selected_item_id` has only two recovered writers (`0x80039f68` reset-to-zero and `0x8002f170` table-commit sink), startup dispatch remains `8 -> (optional 2) -> 4` without a direct commit call, and scanned `channel_commit_row_selected_item_id` rows (`0x00..0x19`, table at `0x80064355` with selected byte at `+9`) contain no `sel=0x01`. Practical classification remains `RP-16 not a proven startup/default weapon` while preserving `0x01` as a real non-startup row.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` special-passcode priming closure pass over `?0SR/?RTN/?QQQ` versus `psx_level_runtime_header_state`, `psx_hidden_passcode_flag`, and `psx_debug_extra_channel_gate`. Current best read is now branch-tight: `?RTN` (`0x0f`) clears runtime-header state to `0`, `?0SR` (`0x10`) sets hidden flag to `1`, `?QQQ` (`0x11`) returns sentinel `0x12` without priming runtime-header state, and none of these specials can independently satisfy gate-arm (`hidden==0 && header_state==3`) at `0x800232f0`; practical JL-9 closure remains a two-phase flow where gate-arm is pre-hidden and hidden/input trigger occurs afterward.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a strict executable-only 2026-04-11 JL-9 validation lane over passcode decode, input-code mapping, gate write, grant call, and selected-id commit sink. Current best read is now narrowed to one concrete blocker for an exact user-facing recipe: code proves `hidden index 0x10 arm` + `input code 0x1e` + `gate read at 0x8002fff4` + late `unlock(0x0d)` and commit sink at `0x8002f168/0x8002f170`, but still does not independently map one deterministic player-visible action that drives the `0x800232f0` writer path (`psx_set_debug_extra_channel_gate`, `param_2='\\n' case 4`) under `hidden==0 && runtime_header_state==3`. Practical next closure is therefore one live caller-arg probe on `0x800230e4`/`0x800232f0`, not another broad sweep.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` lifecycle closure pass for `psx_debug_extra_channel_gate` (`0x8006739d`) and `psx_hidden_passcode_flag` (`0x80067454`) across session init, passcode decode/arm, and debug grant entry. Current best read is now explicit and condition-tight: hidden flag has bounded set/clear points (`0x8003ed28`/`0x8002bab8` set, `0x8002b9e4` timed clear), extra gate remains one-writer/one-reader with no recovered static clear (`0x800232f0` set under `hidden==0 && header_state==3`, `0x8002fff4` read for late `0x0d` unlock), and the strongest practical JL-9 sequence is now a two-phase A->B->input flow where gate-arm occurs pre-hidden and grant trigger occurs during hidden-mode timer.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` RP-16 startup/default-init closure pass over post-load reset, mission/loadout init, difficulty/mode-transition apply, and selected-id writer coverage. Current best read is now explicit for the startup question: `psx_level_post_load_runtime_reset` clears `committed_selected_item_id` to `0` before dispatching mode actions, `psx_weapon_channels_init_mode_loadout` and `psx_weapon_channels_apply_mode_transition_state` contain no fixed selected-id `0x01` seed, and recovered fixed-immediate commit callsites (`0x11`, `0x12`, and one context-specific `0x01`) occur in gameplay/control lanes rather than the named startup initializer path. Practical classification is now `RP-16 not proven startup/default weapon` while still remaining a real executable-backed row with unresolved non-startup acquisition role.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` input-side closure pass for the hidden/debug JL-9 trigger path. The active database now carries conservative input-map helper promotions (`0x80042ec4` -> `psx_input_map_install_profile`, `0x8002abb8` -> `psx_input_map_set_code_to_padmask`, `0x8002abe0` -> `psx_input_map_update_state_for_pad`, `0x8002adbc` -> `psx_input_map_get_code_and_edge`) plus new decompiler comments at `0x80042ec4`, `0x8002abe0`, and `0x80012c30`. Current best read is now explicit and executable-backed: `psx_object_update_runtime_input_modes` gates debug grant on `local_14 == 0x1e`; input code `0x1e` resolves through an exact-match 0x32-entry map; and that code maps to mask `0x2800` in every recovered profile branch, corresponding to the practical chord `R1 + Circle` under the active digital pad bit layout.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` section0-slot closure pass for JL-9 legit-path testing. Current best read is now decisively tighter: `psx_section0_dispatch_root_seed_marker_channel_table` seeds slot `0x14` as `(kind=3, channel=0x0d)`, `psx_section0_dispatch_root_apply_packed_channel_actions` dispatches kind `3` into `psx_weapon_channel_unlock_and_seed_markers(channel)`, and shipped PSX scene-cache root records contain authored bytes with `(byte & 0x3f)==0x14` across 22 maps (119 hits / 3298 scanned root records). Practical consequence is that non-debug shipped data can in fact drive the `unlock(0x0d)` lane; remaining uncertainty is route/timing reachability in ordinary play, not byte-path existence.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 live `SLUS_002.68` passcode decode closure pass centered on `psx_passcode_screen_eval_current_entry` (`0x80034e38`), `psx_passcode_decode_to_mission_selector` (`0x8003ec8c`), and table block `0x80064bbc/0x80064bd0/0x80064be4/0x80064bf8`. The pass closes the transform math (`entry[1..3]-0x1b` triplet match + first-char delta lane), recovers concrete table bytes from memory, and confirms special index behavior: `0x10` sets `psx_hidden_passcode_flag`, `0x0f` clears `psx_level_runtime_header_state`, and `0x11` returns sentinel `0x12`, with all three bypassing first-char validation. Current strongest string closure is hidden/debug `?0SR` (canonical `L0SR` when selector is `0`), plus `?RTN` and `?QQQ` sibling specials. Practical JL-9 consequence remains a staged hidden/debug gate path (`hidden flag` + `input code 0x1e` + `psx_debug_extra_channel_gate`) rather than an ordinary direct unlock lane.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` final-enable-sequence closure pass for the JL-9 hidden/debug lane at `0x8002ba9c`, `0x800232f0`, `0x8002fd90`, and `0x8002fff4`. Current best read is now explicit in executable terms: final `0x0d` unlock is gated by `psx_debug_extra_channel_gate` read at `0x8002fff4`; that gate is written only under `psx_hidden_passcode_flag==0 && psx_level_runtime_header_state==3` at `0x800232f0`; grant entry itself requires `psx_hidden_passcode_flag!=0` plus input code `0x1e` at `0x80013174`; and the strongest practical outcome is a two-phase hidden flow rather than a guaranteed one-action trigger.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` RP-16 classification pass around selected-id `0x01` with direct row/acquisition/HUD evidence. Current best read is tighter in four concrete ways: row `0x01` (`0x80064690`) is real structured weapon-definition data (`RP-16`) rather than invalid filler; primary shop direct-unlock progression still runs through `03..0c` and does not include `0x01`; the observed shop-side `0x01` table entry sits in the secondary `0x0a..0x0e` ammo-top-up branch (`0x8002e32c`) rather than primary unlock helper `0x8002e5f0`; and HUD short-label lookup for selected weapon id remains table-driven through `FUN_800455d4` (`0x80064e90` / `0x80064e9c`) with row-name identity still anchored by inline row bytes. Practical classification is now "real early row with legacy/startup/placeholder-like behavior" rather than "invalid/unused slot", pending one concrete non-debug acquisition writer.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` legitimate-acquisition verdict pass using emulator-grounded selected-id mapping (`0x8014577e`, `0x0c=JL-2`, `0x0d=JL-9`) as hard anchor. Current best read is now explicit by lane: normal loadout and shop direct unlock paths remain capped at `<=0x0c`; hidden/debug input gate still leads to the only recovered fixed-immediate `unlock(0x0d)` site (`0x80030004`); scripted packed-action dispatch remains the only plausible non-debug `0x0d` exception but is still authored-data dependent and unproven in shipped section0 content; practical classification therefore remains `JL-9 strongest hidden/debug-conditioned` rather than presently proven normal-flow unlock.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` broad acquisition-systems pass over default loadout/init, mission transition mode apply, pickups, shops, scripted packed awards, and debug grant paths for ids `0x0d` (`JL-9`) and `0x01` (`RP-16?`). Current best split is now explicit and source-backed: ordinary loadout and shop front path remain `<=0x0c` with no direct `0x0d`; debug grant still conditionally unlocks `0x0d` behind `psx_debug_extra_channel_gate`; scripted/pickup data-driven paths can still produce either id when authored; and `0x01` is not excluded from normal content because the shop channel map contains `0x01` in the alternate branch and low-id action commit paths also accept it.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a 2026-04-11 live `SLUS_002.68` reconciliation pass that incorporates the user-verified selected-weapon byte map at `0x8014577e` (`00..0d`) and tightens the old local-id-versus-row-id ambiguity. Current best model is explicitly two-domain: callers feed compact channel/local codes into `psx_apply_channel_effect_and_commit_selected_item_id` (`0x8002ef34`), then `0x8002f15c` resolves through `channel_commit_row_selected_item_id[(channel*10)+9]` into committed row-id domain (`00..0d`) before `0x8002f168` stores to the nested runtime `+0x1c` field family that includes the selected byte at `0x8014577e`. This corrects earlier shorthand that could imply direct caller-code equality with committed selected row id.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused 2026-04-11 live `SLUS_002.68` gate-closure pass for the exact JL-9 visibility preconditions requested around `psx_debug_extra_channel_gate`, `psx_hidden_passcode_flag`, and the selected-weapon watch at `0x8014577e`. Current best read is tighter in four practical ways: `DAT_8006739d` remains one-writer/one-reader (`0x800232f0` write, `0x8002fff4` read); `DAT_80067454` writer/reader roles are now bounded across decode/arm/reset/input handlers; unlock-capable channel-`0x0d` entry points are explicitly separated into debug bulk grant versus scripted packed-action dispatch; and normal-lane evidence still clusters at `<=0x0c` while non-debug shipped `0x0d` authoring remains unproven. The pass also landed one conservative live rename (`0x8002ba9c` -> `psx_hidden_passcode_arm_runtime_state`) plus durability comments at `0x8002bab8`, `0x80013154`, `0x8002fff4`, and `0x8002f170`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now captures the decisive starter-only RAM compare against the earlier all-weapons dump and retracts the previous `0x1456fc` inventory-list hypothesis. The compact `0x1456fc..0x145748` `0x0002..0x000b` sequence is unchanged across both dumps and is therefore not the live owned-weapon list; the dynamic lane starts at `0x14574c` and the strongest current field closure is now byte `0x14577e` (`0x0c` in the all-weapons dump versus `0x02` in the starter-only dump), which executable-side passes tie to selected/committed weapon row-id state inside the nested runtime `+0x1c` field family. The compare also keeps `0x67944` interesting as a changing watch (`0x0000000b` versus `0x00000001`) but downgrades it to unproven because no direct static xrefs were recovered for `0x80067944`. Current best read is now channel-state ownership plus dynamic runtime current-weapon fields, not a contiguous owned-id inventory array.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now captures a focused live `SLUS_002.68` ownership-model pass tied to the observed dump split (`0x801456fc..0x80145748` static, `0x8014574c..0x801457d0` changed). Current best read is tighter in four concrete ways: ownership query is channel-state based (`psx_marker_channel_mode_is_enabled` reads `psx_marker_channel_runtime_block[(channel*4)+0x34]`), per-channel ammo/step state is `+0x6c` (via `psx_marker_channel_get_mode_step_value` and channel add/unlock helpers), active selected channel is byte `+0x32`, and commit-to-weapon resolution remains table-driven (`channel_commit_row_table[(channel*10)+9]` at `0x8002f15c` into `idx*0x26` row access at `0x800315d8`) rather than a direct contiguous owned-id list. The live database also fixes a symbol collision by renaming `0x8002fd90` to `psx_debug_grant_weapon_channels_and_ammo`, preserving explicit post-gate extra unlock semantics at `0x8002fff4 -> channel 0x0d`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now adds a focused live `SLUS_002.68` runtime-ownership pass on RAM block `0x8014574c..0x801457d0` and closes the strongest current interpretation for watched byte `0x8014577e` (`file offset 0x14577e`). Current best read is now tighter in four concrete ways: `0x14577e` sits inside the nested player-runtime `+0x1c` field family reached through `DAT_800789f8 -> +0x8 -> +0x18 -> +0x1c`; that field family is written from `channel_commit_row_selected_item_id` in `psx_apply_channel_effect_and_commit_selected_item_id` and reset in `psx_set_debug_extra_channel_gate`; the surrounding block shape matches heap-resident per-object runtime state initialized by `psx_object_create_simple_record`; and the observed byte delta `0x0c` (all-weapons) versus `0x02` (starter-only) is therefore strongest as selected weapon row-id metadata, not primary ownership/ammo/HUD-cache storage.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now adds a focused live `SLUS_002.68` ownership/xref pass on the user-tracked block `0x80067938..0x80067958`, with direct closure on the `0x80067944` ambiguity. Current best read is now tighter in four concrete ways: `0x80067944` has no recovered static xrefs in this image under word/halfword/byte probes and is therefore not currently supported as selected-weapon-id or selected-row-id storage; `0x80067938` is reaffirmed as `psx_ctor_placement_section_ptr` installed by `wdl_resource_bundle_load_by_index` and consumed by `psx_apply_deferred_control_command` as constructor/deferred-control section state; nearby `0x8006793c/0x80067940/0x80067948` are input-mode dispatch state in `psx_object_update_runtime_input_modes`; and `0x80067954/0x80067958` are present/spec-upload draw-env state with one conservative helper promotion (`FUN_800461d0` -> `psx_draw_progress_overlay_and_swap_drawenv`).
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now closes the compact local-id versus weapon-row-id question for live `SLUS_002.68` as a table-driven conversion, not a standalone arithmetic helper. The new strongest chain is `caller channel/local code -> psx_apply_channel_effect_and_commit_selected_item_id(0x8002ef34) -> channel_commit_row_selected_item_id[(channel*10)+9] load at 0x8002f15c -> committed_selected_item_id -> idx*0x26 resolver at 0x800315d8 from base 0x8006466a`, with concrete JL mapping `local/channel 0x0b -> row 0x0c (0x80064832, JL-2)` and `local/channel 0x0c -> row 0x0d (0x80064858, JL-9)`. Three durable live comments were added at `0x8002f15c`, `0x800315d8`, and `0x8001d3fc` to preserve this conversion model for future RAM-dump reconciliation passes.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now closes the user-reported pointer delta near selected-weapon state watches (`0x80078248` in all-weapons versus `0x800782b8` in starter-only) as a render double-buffer artifact, not a weapon/inventory struct discriminator. Live `SLUS_002.68` decompiles now show `psx_platform_init` seeding both draw-env records, paired disp-env records at `+0x5c`, and `DAT_80067954` flipping between them in `psx_present_frame_and_flip`, `render_reset_draw_state`, and `FUN_800461d0`. Current practical consequence is tighter scope: this nearby pointer lane should no longer be treated as selected-weapon ownership state, and weapon identity work should stay on the channel-commit and weapon-row resolver path.
Latest verified batch: the earlier `0x1456fc` / `0x145744` / `0x67944` patch-candidate story is now superseded by the starter-only compare and should be treated as withdrawn. `0x1456fc..0x145748` is static across both dumps, so it is not the live owned-weapon list; byte `0x14577e` is now the strongest executable-backed dynamic selected-weapon row-id field, while `0x67944` remains only a changing watch without direct static-xref closure.
Latest verified batch: the earlier watched-value shorthand `JL-?=11` has now been superseded by direct emulator verification of the selected-weapon byte at `0x8014577e`: `0x0c = JL-2`, `0x0d = JL-9`, with the full `00..0d` row-id map now captured in [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md). This removes the older inference step for JL-2 versus JL-9 identity and shifts the remaining work entirely onto legitimate-acquisition proof and RP-16 classification.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now captures the current blocker much more precisely after another multi-lane `SLUS_002.68` pass. The strongest RAM-side lead has moved decisively to dense small-byte runtime clusters around file offsets `0x133000`, `0x133416`, and `0x1335d4`, with a weaker secondary candidate around `0x422c..0x4440`; by contrast, the earlier `DAT_80064355[(channel*10)+9]` field is now explicitly documented as not acting like a plain final JL row id in the sampled dump. On the code side, `0x8001E37C` is now promoted to `psx_handle_special_input_code`, which is the strongest current upstream helper for the hidden `0x1e` trigger range, while the ammo-side helper family is now tighter through newly named marker/runtime helpers. The remaining blocker is now singular and concrete: one executable-side inventory/HUD anchor is still needed to prove which of the current RAM candidate regions is the real live weapon-slot table.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now captures a third targeted six-agent `SLUS_002.68` cycle that sharpened both the RAM and input-code stories without yet fully closing them. The main negative result is now explicit: the candidate commit-table byte at `DAT_80064355[(channel*10)+9]` does not show plain `0x0c/0x0d` values in the sampled main-RAM dump rows, so that exact field is not behaving like a direct final JL row id in this capture. The strongest new positive lead is elsewhere in the same dump: denser table-like `0x0c/0x0d` clusters now stand out around file offsets `0x133000`, `0x133416`, and `0x1335d4`, which are better candidates for real live inventory/slot state and should be the next RAM target. The same pass also promoted `0x8001E37C` to `psx_handle_special_input_code`, tightening the upstream side of the hidden-passcode trigger path even though the exact `0x1e` -> button-chord mapping still remains open.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now captures another six-agent `SLUS_002.68` pass focused on the JL-2 ammo lane, the JL-9 debug trigger path, the 2 MiB main RAM dump, and direct inventory/HUD mapping helpers. The strongest new clarification is on the trigger story: executable evidence now closes `hidden passcode active -> gated input code 0x1e -> psx_debug_grant_weapon_channels_and_ammo -> extra 0x0d unlock behind psx_debug_extra_channel_gate`, but still does not prove that input code `0x1e` specifically means `R1 + Circle`. The same pass also tightened the RAM-dump read without rejecting it: `binary/Crusader - No Remorse Weapons Main Ram.bin` is still plausible main RAM and now has candidate compact slot-like records near file offsets `0xA0/0xB0/0xC0`, but the dump still needs one confirmed executable-side inventory/HUD anchor before it can decode the runtime weapon list. Live database cleanup in the same pass also added `0x800232f0` -> `psx_set_debug_extra_channel_gate` and reinforced the selected/equipped chain around `0x8002ef34`, `0x8002f15c`, `0x800315d8`, `0x8003d02c`, and `0x800424ac`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a fresh six-agent `SLUS_002.68` pass that added a second runtime-artifact lane instead of only rechecking the executable. The new distinction is now explicit: `binary/Crusader - No Remorse Memdump Weapons.bin` remains a VRAM/HUD artifact, while `binary/Crusader - No Remorse Weapons Main Ram.bin` is plausible 2 MiB PSX main RAM but still not self-decoding enough to resolve the selected `JL-?` slot as `JL-2` versus `JL-9` without the executable-side id/name chain. The same pass also tightened the live database around the selected/equipped path with fresh comments on `0x8002ef34`, `0x8002f15c`, `0x800315d8`, `0x8003d02c`, `0x800424ac`, and explicit extra-unlock naming at `0x80030004` -> `psx_weapon_channel_unlock_and_seed_markers`, while keeping `JL-2 AMMO` as the strongest current normal-lane lead.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) has now been normalized after a new six-agent `SLUS_002.68` pass instead of continuing to accumulate repeated follow-up narratives. The same batch adds two concrete deltas: the checked `binary/Crusader - No Remorse Memdump Weapons.bin` artifact now has a narrower runtime-artifact read as PSX VRAM with HUD/icon-atlas content but no direct slot-RAM proof for `JL-2` versus `JL-9`, and the next weapon-side unknown is now better framed as `JL-2` ammo/storage behavior because `JL-2 AMMO` exists at `0x800642b6` while `JL-9` still remains the stronger extra hidden/debug-conditioned lane. The live PSX database also gained a few more small durable artifacts in this pass, including the explicit helper name `psx_weapon_channel_unlock_and_seed_markers` at `0x80030004` plus confirming HUD/commit comments around `0x8002f15c`, `0x800323b0`, `0x80042690`, and the weapon-def base `0x8006466a`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a narrow 2026-04-11 follow-up on live `SLUS_002.68` that split symbolic cleanup from runtime-artifact checking. The live PSX database now carries evidence-backed JL-lane data names (`psx_hidden_passcode_flag`, `psx_debug_extra_channel_gate`, `psx_level_runtime_header_state`, `channel_commit_row_table`, `channel_commit_row_selected_item_id`, `committed_selected_item_id`, `psx_weapon_spawn_type`, `psx_weapon_spawn_audio_event_id`, and `psx_weapon_spawn_state_selector`), while the checked `binary/Crusader - No Remorse Memdump Weapons.bin` artifact is now explicitly reclassified as a full 1 MiB PSX VRAM dump rather than slot/inventory RAM. The same pass also sharpens the next pivot if the extra hidden/debug lane remains `JL-9`: `JL-2` now has direct `JL-2 AMMO` string evidence (`0x800642b6`) without a matching plain `JL-9 AMMO` peer, so the next useful unknown becomes JL-2's ammo/storage and normal-acquisition behavior rather than more blind JL-9 existence proof.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 MCP-only direct mapping proof pass for the requested chain on live `SLUS_002.68`: channel commit (`0x8002ef34`) -> committed item id load from `DAT_80064355[(channel*10)+9]` at `0x8002f15c` -> row-indexed weapon-def access with explicit `idx*0x26` stride at `0x800315d8/0x8003160c` from base `0x8006466a` -> concrete row split `0x0c=0x80064832 (JL-2)` vs `0x0d=0x80064858 (JL-9)`. The note now also includes row-byte diff evidence from MCP compare (`+0x24: 0x4b -> 0x0f`, name-field byte `0x32 -> 0x39`) and records that `0x800322b4..0x800325f8` remains only supporting UI-tail evidence (`0x0121 -> 0x013f`), not the primary commit/resolver path.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 MCP-only row-field consumer trace for `JL-2` (`0x80064832`) versus `JL-9` (`0x80064858`) on live `SLUS_002.68`. The new closure is lane-level: shared `+0x1c=0x18` still converges through `psx_weapon_def_apply_spawn_profile_by_index` (`0x8003d02c`) into the same type-indexed active art-header bind path in constructors (`0x800249f4` / `0x80024eec` via `DAT_800758d8`), while diverged `+0x24` (`0x4b` vs `0x0f`) remains consumed as a compact transition/state selector (returned from `0x8003d02c`, then used in callers like `0x80018bc4/0x80018bf8`) rather than a direct base-resource pointer. Current best answer is shared base-art lane with distinct selector/state lane, and runtime capture is still required only for exact final frame/resource-token closure.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 MCP-only four-lane acquisition comparison on live `SLUS_002.68` across normal loadout (`0x8002f814`), shop (`0x8003de68`), scripted grant (`0x800311f4..0x800313b4`), and debug grant (`0x8002fd90`). The strongest new discriminator is address-local and explicit: only debug grant performs the post-`0x0c` extra unlock call (`0x80030004 -> psx_weapon_channel_unlock_and_seed_markers(0x0d)`) behind `DAT_8006739d` read at `0x8002fff4`, while the L0SR-linked gate chain remains `0x8003ed28 (DAT_80067454=1)` -> `0x80013154/0x80013174` -> `0x8002fd90`. Current best conclusion remains that the extra post-L0SR non-PC weapon lane is `JL-9` (`0x80064858`, index `0x0d`) rather than `JL-2` (`0x80064832`, index `0x0c`), with three conservative new comments at `0x8003de68`, `0x80031344`, and `0x80030004`.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 MCP-only recheck of the hidden passcode to extra-unlock chain on live `SLUS_002.68`. The practical read is tighter: hidden decode branch at `0x8003ed28` sets `DAT_80067454`, runtime input gate at `0x80013154/0x80013174` reaches `psx_debug_grant_weapon_channels_and_ammo`, and the extra late unlock remains a second gate (`DAT_8006739d` at `0x8002fff4`) for index `0x0d`. Current direct conclusion remains that the extra late unlock lane maps to `JL-9` (index `0x0d`) rather than `JL-2` (`0x0c`), with the remaining closure target narrowed to one direct UI label-resolver chain plus full `DAT_8006739d` writer-context classification.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 MCP-only pass that tightened the unresolved `0x800323xx` lane and selected-id tail evidence in live `SLUS_002.68`. The lane is now anchored as render-mode UI state via `psx_render_mode_dispatch` (`0x800350a8`) and callback table `0x800648b4`, with `psx_ui_state_handler_322b4_color_cycle` driving `DAT_800678f8` from `0x80064880` and a terminal toggle `0x0121 -> 0x013f`, then consumed by `psx_hud_draw_selected_item_tile_bar` at `0x800424ac`. This batch also landed conservative naming/comments (`0x8003ddcc`, `0x800424ac`, and comments at `0x800323b0`, `0x800323e4`, `0x80042690`) and keeps the strongest current JL read as selected-id-backed (`JL-2 ~ 0x0121`, `JL-9 ~ 0x013f`) while direct channel-number equivalence remains a dedicated follow-up target.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a 2026-04-11 shipped-map weighting follow-up that keeps executable-side evidence authoritative and uses scene-cache evidence as support. Current best read tightens one practical decision: `JL-2` remains the stronger normal-lane shipped candidate (`0x0c`), while `JL-9` remains the stronger exceptional/debug-conditioned lane (`0x0d` behind `DAT_8006739d`). The pass also leaves concrete continuation anchors: current `DAT_8006739d` writer condition at `0x800232f0`, late-mode candidate range (`46..58` and `63`) from `0x80063e68`, and dense section0 cache targets (`map 46/49/63`) for direct placement correlation.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a focused live `SLUS_002.68` row-to-consumer pass that resolves the earlier JL-9 selector ambiguity. Current best read is tighter in three concrete ways: the candidate pair is corrected to `JL-2(+0x24=0x4b)` versus `JL-9(+0x24=0x0f)` while both keep `+0x23=0x0e`; both rows still converge through shared `+0x1c=0x18` into the same type-indexed base art bind path (`psx_weapon_def_apply_spawn_profile_by_index` -> object constructors -> `psx_type_art_active_header_bank[type]`); and the `+0x24` consumer lane is now evidenced as transition/spawn-state-facing rather than direct bundle-id-facing. Remaining closure stays narrow: runtime capture is still required to export final JL-9 frame/resource identity with certainty.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now adds a direct UI/state disambiguation pass for the hidden-passcode "all weapons received" outcome on live `SLUS_002.68`. The previously suspicious `0x800323xx` lane is now recovered as render-mode UI color/flash handling (`psx_render_mode_dispatch` callback table at `0x800648b4`, `0x80064880` halfword ramp into `DAT_800678f8`, consumed by `FUN_800424ac` tile-color writes), not weapon-name display/selection logic. The strongest identity proof now sits in the commit path: `psx_apply_channel_effect_and_commit_selected_item_id` commits `DAT_80064355[channel*10]`, channel `0x0d` row at `0x800643ce` stores item id `0x0d`, and weapon-def index `0x0d` is row `0x80064858` (`JL-9`) versus index `0x0c` row `0x80064832` (`JL-2`).
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a narrow 2026-04-11 reconfirmation pass over hidden passcode decode and late weapon unlock gating in live `SLUS_002.68`. The new evidence closes two practical points: `DAT_8006739d` usage in this image is now exhaustively bounded to one writer (`0x800232c0`) and one reader (`0x8002fd90`), and the extra conditional unlock call in `psx_debug_grant_weapon_channels_and_ammo` is now tied to index `0x0d` as the post-`0x0c` final JL lane, strengthening the JL-9-over-JL-2 conclusion for the hidden/debug route while keeping one explicit caveat that a single direct UI label resolver chain was not recovered in this pass. This batch also landed small durable artifacts in the live database: `FUN_8003ddcc` -> `psx_shop_entry_get_mode_gated_cost_by_slot`, new thin wrappers `psx_script_dispatch_packed_channel_actions_from_object` (`0x800203b0`) and `psx_script_dispatch_marker_mode_action_from_byte` (`0x800203ec`), and targeted decompiler comments at `0x8003de68`, `0x8002fff4`, and `0x80064858` to preserve the `0x0c` (`JL-2`) versus `0x0d` (`JL-9`) acquisition split.
Latest verified batch: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now consolidates the focused live `SLUS_002.68` JL-9 / hidden-passcode / debug-leftover sweep instead of leaving those results spread across subagent-only Ghidra edits. The live PSX database now has a tighter weapon-side helper family (`psx_weapon_marker_activate_by_index`, `psx_weapon_marker_add_with_cap_by_index`, `psx_weapon_def_get_u16_with_mode_gate`, `psx_weapon_def_apply_spawn_profile_by_index`, `psx_weapon_shop_try_apply_entry`, `psx_debug_grant_weapon_channels_and_ammo`, and the passcode decoder/generator pair), and the practical read is sharper in four ways: `JL-9` is now closed as a real final indexed PSX weapon-definition row rather than a stray string; the hidden passcode decoder is now directly recovered as transformed table logic instead of only inferred from folklore; the strongest current `how do you get JL-9` answer is a hidden passcode branch that sets `DAT_80067454` and then reaches the bulk weapon-unlock helper's extra late channel; and the remaining PSX gaps are now narrower again, concentrated in exact `channel -> JL-9` proof, exact sprite/frame/resource mapping, and exact level-placement correlation rather than in the broader existence question.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) and [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md) now capture the next six-agent PSX table-typing pass rather than another broad sweep. The live database now promotes `DAT_80067938` to `psx_ctor_placement_section_ptr`, `DAT_800676d8` to `level_clut_table_ptr`, `DAT_80067840` to `psx_control_opcode_stream_table`, and `DAT_80063e54/0x80063e68` to paired per-level selector/channel tables, while the earlier `DAT_800675ec` marker/control runtime block now has a stronger local field map centered on `+0x34`, `+0x6c`, and `+0x88/+0x8c`. Current best read is tighter in three practical ways: the constructor-placement section is now clearly a section-pack root with subordinate indexed control rows rather than one opaque blob; the CLUT and opcode-stream tables are now pushed off the shortlist of direct `0x0042` art discriminators and into palette/control support; and the real remaining structural blocker family is now the level-indexed `0x63e54/0x63e68/0x675ec` control island plus the exact installed rows fed from the decompressed/runtime-bank install chain.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a storage-mapping pass on live `SLUS_002.68` that stayed off broad draw-path analysis and traced ownership from WDL section installs through compressed-state unpack into runtime banks consumed by type `0x0042` object creation/presentation. The live database now also carries four small evidence-backed names in this lane (`FUN_8003b00c` -> `psx_lzss_unpack_into_level_buffer`, `FUN_8003aba8` -> `psx_lzss_pack_level_buffer`, `DAT_8006b5d8` -> `psx_level_state_compressed_blob`, `DAT_8006763c` -> `psx_level_heap_cursor`) plus targeted comments at `0x80039af0` and `0x800249f4`. Current best read is tighter in one practical way: the map-storage ownership chain is now concretely anchored from level-load section pointers to constructor/draw-time bank consumers, and the remaining blockers are narrowed to unresolved subordinate table schemas and per-item map-104 `0x0042` resource/state correlation.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now captures a focused PSX type-policy provenance pass around `DAT_800675f8` / `psx_type_policy_table_ptr` for `map 104` type `0x0042` and control type `0x0066`. The live `SLUS_002.68` database now adds one safe reader rename (`0x8001408c` -> `psx_update_nearest_policy80_contact_marker`) plus targeted comments at `0x800398f0`, `0x8002bf0c`, `0x80041604`, and `0x800140c8` that pin installation and major consumer roles. Current best read is tighter in three practical ways: pointer provenance is now explicit as level-load section-pack storage (single writer at `0x800398f0` in `wdl_resource_bundle_load_by_index`), consumer coverage now extends beyond the older `0x1000/0x0600/0x2000` set to additional policy bits (`0x0008`, `0x0010`, `0x0020`, `0x0080`, `0x0100`, `0x0800`, `0x4000`, `0x8000`), and the fixed map-104 sample rows (`25/30/31/35/85/86` and control `53`) still show `runtimeDiagnostic.typePolicy.word=null`, confirming concrete row-word capture remains a runtime export gap rather than an executable-static decode gap.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now captures a focused object-local route-bit provenance pass for fixed `map 104` `0x0042` sample items `25/30/31/35/85/86` at the decisive `obj+0x1c & 0x0400` branch in `psx_object_integrate_motion_and_route_visible`. Current best read is now stricter and evidence-backed: `psx_object_create_simple_record` (`0x80024b48`) and `psx_object_create_compound_record` (`0x80025040`) remain the strongest object-local writers because they copy authored `u5` directly into `obj+0x1c`; inspected named mutators in the same lane (`psx_type42_transition_selector_tick`, `psx_object_select_state_from_transition_table`, `psx_object_advance_state_script`, `psx_apply_deferred_control_to_live_objects`, `psx_object_handle_control_pair_0a`) change other route/control bits but did not reveal a direct object-local `0x0400` set/clear transform; and recovered `0x0400` writes remain nested/global (`psx_object_state_machine_dispatch_tick` nested runtime write at `0x8001a078`, global policy write in `psx_object_handle_control_pair_0a` at `0x80022a14`). The remaining gap stays narrow: a live capture is still required to close object-local `0x0400` provenance for this sample pack.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a focused frame-geometry pass on active `SLUS_002.68` following the selector/latch bridge into exact width/height/origin consumers. The live evidence chain is now explicit from `psx_object_select_state_script` (`obj+0x9e` install) and `psx_object_advance_state_script` (`obj+0x94` latch) into `psx_project_object_main_visible` / `psx_project_object_special_visible_queue`, then into `psx_resource_frame_origin_x/y` and `psx_resource_frame_width/height`. Current best read tightened one step further: for the fixed `map 104` `0x0042` sample pack, `64x40` versus `64x64` is most strongly a live frame-token (`obj+0x94`) outcome within a shared type resource lane, not a late presentation modifier; the remaining closure task is a compact live sample of bound-resource kind plus latched frame token for items `25/30/31/35/85/86`.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now captures a broad but concrete table-inventory pass against live `SLUS_002.68` focused on unresolved `map 104` / type `0x0042` storage and rendering coverage. The live database now adds six marker-channel/runtime-state helper names (`0x8002f190`, `0x8002f250`, `0x80031840`, `0x8003185c`, `0x80031878`, `0x80031a3c`) and targeted table comments on `0x80063e68`, `0x800675ec`, and `0x80063e54`. Current best read is tighter in two practical ways: the known descriptor/transition/runtime-bank tables are reaffirmed as shared/generic infrastructure rather than a missing unique `0x0042` fork, and the `0x80063e54/0x80063e68/0x800675ec` level-indexed control island is now promoted as a high-value unresolved table family worth explicit struct/table mapping in the next batch.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) and [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md) now capture another six-agent concrete `map 104` `0x0042` pass over the same fixed sample pack instead of broadening the search surface. The live PSX database now also names two selector-overlap probes, the runtime snapshot/release helpers, and a wider root marker/channel helper family. Current best read is tighter in three practical ways: the `64x64` versus `64x40` split still looks more like shared-resource / different-frame-state behavior than a different constructor bind path; the selector-to-frame bridge is now explicit through `obj+0x9e` install, `obj+0x94` latch, and the later frame-geometry consumers; and the stage-1 versus stage-2 branch point is now explicit in `psx_object_integrate_motion_and_route_visible`, even though the exact live `0x0400` provenance for this sample pack still needs capture. The next `map 104` pass should therefore stay on items `25/30/31/35/85/86` and capture bound resource identity plus live frame/state and route-bit state at the decisive branch instead of widening heuristics.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) and [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md) now capture the first concrete `map 104` `0x0042` `runtimeDiagnostic` follow-up against exported scene items rather than another generic six-track theory pass. The live PSX database now names the spawn-side selector bridge (`psx_transition_spawn_and_seed_selector_from_record`), a root-family decoder helper (`psx_section0_dispatch_root_find_marker_record_by_channel`), two nearby resource-upload helpers, the type-policy pointer (`psx_type_policy_table_ptr`), and the transition row tables (`psx_type_transition_mode_policy_rows`, `psx_type_transition_selector_rows`). Current best read is tighter in four practical ways: the root and constructor section-0 families now have explicit named entry points but still converge through the same shared `0x0042` descriptor row; constructors are now proven to seed `obj+0x1c` by directly copying the authored lane word, so exported `initialWord` values are meaningful authored state; the strongest recovered `0x0400` control is still nested-state-side rather than a direct object-local `0x0042` writer; and `DAT_800675f8` is now better modeled as a level-loaded per-type policy pointer, not a per-lane discriminator. The next `map 104` pass should therefore sample bound-resource identity plus live frame/state against the fixed item set (`item:25/30/31/35/85/86`) rather than widening donor logic or revisiting descriptor speculation.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md), [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md), and [map_renderer/src/lib/psx-cache.js](k:/ghidra/crusader_map_viewer/map_renderer/src/lib/psx-cache.js) now capture both sides of the next PSX narrowing step. The live Ghidra work stayed split across six non-overlapping `0x0042` tracks, and the viewer exporter now serializes the resulting channel model as a per-item `runtimeDiagnostic` payload in scene version `psx-runtime-record-probe-v10`. Current best read is now operationally tighter: the next `map 104` pass no longer needs to rediscover which fields matter, only to populate the exported channels for representative `0x0042` items with live evidence for pre-latch selector dispatch, latched state, object-local route flags, nested runtime state, resource kind, and the live type-policy word.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md), [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md), and [docs/psx/psx.md](docs/psx/psx.md) now capture another six-track live `SLUS_002.68` PSX batch with deliberately separate coverage areas instead of overlapping helper sweeps. Current best read is tighter in four ways: the per-type art/cache pair is now named by behavior (`DAT_800758d8 = psx_type_art_active_header_bank`, `DAT_800758c8 = psx_type_art_built_resource_bank`); `psx_type42_transition_selector_tick` now has an early eligibility gate through `psx_object_is_within_view_margin` before it can emit selector `3/4` ahead of the later latch copy; the root-dispatch-side `0x0042` mode gate still looks control-only rather than a distinct map-facing presentation family; and the anonymous control-island recovery now proves `0x0400` writes in the wider nested runtime state machine and related policy controls without yet pinning a direct object-local `obj+0x1c |= 0x0400` writer. The next `0x0042` discriminator is therefore narrower again: a concrete runtime sample now needs to separate pre-latch selector dispatch, latched `obj+0x94`, object-local `obj+0x1c`, nested runtime state words, and bound resource kind instead of treating them as one flat route/state bucket.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md), [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md), and [docs/psx/psx.md](docs/psx/psx.md) now capture the next focused live `SLUS_002.68` follow-up on unresolved `0x0042` rather than another broad render pass. Current best read is tighter in five ways: type `0x0042` is now pinned to exact descriptor-table slot `0x80063220 -> 0x800626f8`; both section-0 constructor placements and root-dispatch rows still enter that same generic create/update/release family; type `0x0042` now also has a dedicated transition helper `psx_type42_transition_selector_tick` that can dispatch low turning selectors `3/4` before the `+0x94`-style runtime latch copy; `obj+0x1c` writer behavior is clearer because constructors seed it from authored `u5` while `psx_object_select_state_from_transition_table` only mutates bit `0x0002`; and `DAT_800675f8` now reads as policy bits (`0x1000` nearby-publication suppression, `0x0600` stage-1 ordering class, `0x2000` semitrans policy) rather than as the missing main route discriminator. The remaining next-step discriminator is therefore narrower again: concrete `0x0042` samples need to correlate pre-latch selector dispatch, latched `obj+0x94`, `obj+0x1c` bit `0x0400`, and bound resource kind, not just raw `u5` or generic transition-row contents.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md), [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md), and [docs/psx/psx.md](docs/psx/psx.md) now capture one more six-track live `SLUS_002.68` PSX pass centered on unresolved `0x0042` routing. Current best read is narrower in four practical ways: constructor and root-dispatch records both hand authored flags into `obj+0x1c`; `0x0020` is still the broad world-visible gate, `0x0002` still reads as orientation/extents behavior rather than a lane split, and `0x0400` is now the strongest recovered stage-2 special-visible selector; `psx_object_select_state_from_transition_table` now gives a concrete per-type selector source ahead of `psx_object_select_state_script`; `psx_load_type_state_banks` now reads as the `DAT_800758cc/d0/d4` installer while `psx_stream_install_type_runtime_banks` is the packed-stream helper that can also install `DAT_800758d8`; and `DAT_80067794` is now tighter as save/transition runtime-header state via `psx_snapshot_level_runtime_header_block` / `psx_apply_level_runtime_header_block`, not as the missing `0x0042` art-binding source. The next executable-backed discriminator is therefore more specific again: sample representative `map 104` `0x0042` objects through `obj+0x1c`, `obj+0x10`, `obj+0x9e`, and `obj+0x94`, plus the `DAT_80063b4c` transition-table row for type `0x0042`, instead of spending another batch on raw `u5` or descriptor-table speculation.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) and [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md) now consolidate the full six-track live `SLUS_002.68` PSX sweep instead of leaving the result fragmented across individual helper passes. Current best read is narrower in a way that changes the next execution order: unresolved `0x0042` is no longer plausibly blocked on a missing unique per-type descriptor fork or on HUD/overlay presentation behavior. `0x0042` still shares the generic `0x003e..0x0050` descriptor cluster, constructor-placement `0x0042` still reads as a compound -> advance-state -> main-visible route inside that generic family, both world-facing lanes choose sprite versus image-table submitters from the bound resource header kind, and the HUD/overlay lane remains an explicit non-map-facing exception that can branch from overlay-slot policy instead of normal world-object kind checks. The next practical discriminator for viewer recovery is therefore representative runtime-bank/state/resource-kind evidence for `map 104` `0x0042` families, especially the split between constructor-placement `u5=0x0020` and root-dispatch `u5=0x0030/0x0022`, not more broad donor reuse or descriptor-table speculation.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a focused live `SLUS_002.68` world-frame/render-wrapper cleanup around `0x80031f0c`, `0x80031f9c`, `0x800320bc`, `0x80039dc4`, `0x8003977c`, and `0x800391f0`. The live PSX database now names `0x80031e0c` as `psx_lset_session_loop`, `0x80031f0c` as `psx_lset_world_frame_wrapper`, `0x80031f6c` as `psx_lset_session_teardown`, `0x800350a8` as `psx_render_mode_dispatch`, `0x80039ef4` as `psx_level_post_load_runtime_reset`, and `0x80044104` as `psx_present_frame_and_flip`, with direct technical comments set at the same loader/bank/runtime-header anchors. Current best read is tighter for reconstruction work: this lane now provides a clean evidence-backed chain from map-index bundle load and per-type bank install through runtime-header application to per-frame world visible draw and display flip, reducing ambiguity between storage decode and final presentation wrappers.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a focused live `SLUS_002.68` main-visible ordering/update cleanup around `0x8001263c`, `0x80012c30`, `0x8002d59c`, `0x8002d778`, `0x8002ca74`, and `0x8002e064`. The live PSX database now has the missing parent function object covering the `0x80012b44/60/c0/f0` lane (`psx_object_integrate_motion_and_route_visible`) plus explicit helper names/contracts for `psx_object_update_runtime_input_modes`, `psx_main_visible_list_swap_entries`, `psx_main_visible_order_graph_unlink_pair`, and `psx_main_visible_order_graph_detach_object`; direct technical decompiler comments were also applied to the update/rebucket/sort call sites and sort entry points. Current best read is tighter for exporter coherence: object update now cleanly resolves into state advance -> optional projection -> conditional rebucket -> optional nearby-interaction publish, while stage-1 draw submission remains refresh-gated and dependency-sorted before `psx_draw_world_visible_passes` consumes the slice.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now captures a focused live `SLUS_002.68` resource creation/submission cleanup around `0x80044434`, `0x800444e4`, `0x80044614`, `0x80044bdc`, `0x80044e9c`, and `0x80045ffc`. The live PSX database now also renames `0x800445c8` to `psx_overlay_slot_clone_bound_resource` and `0x80044fa4` to `psx_resource_release_image_vram_slot_if_needed`, with stale "verified by subagent pass" comments replaced by technical comments in the local HUD/overlay lane. Current best read is tighter for viewer art binding: kind `5` is the image-table submit path, kind `4` is the single-image bound sprite path, and the special-visible lane remains palette-override-distinct from main-visible despite sharing the same kind discriminator.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a focused live `SLUS_002.68` interaction/reselection cleanup around `psx_type4_reselect_motion_state` and `psx_object_update_nearby_interactions`. The live PSX database now names `0x80028050` as `psx_object_test_strict_nonoverlap_flag8_pair`, `0x800281d4` as `psx_object_test_strict_nonoverlap_flag8_subject`, `0x80028700` as `psx_object_adjust_param9c_by_view_side`, `0x800287bc` as `psx_object_update_param9c_from_contact_target`, `0x80028eb4` as `psx_object_apply_contact_push_bias`, and `0x8002923c` as `psx_object_spawn_type11_contact_proxy`, with concise technical decompiler comments added at each entry. Current best read is tighter for exporter work: post-spawn contact handling in this lane mutates runtime extents/width state (`+0x30..+0x38`, `+0x9c`) and can spawn type-`0x11` contact proxies in-flow, so unresolved-family art fallback logic should continue to model these as runtime interaction effects rather than as static constructor-placement fields.
Latest verified batch: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a focused live `SLUS_002.68` presentation-lane cleanup around the HUD/overlay pass and adjacent helpers. The live PSX database now names `0x80035cc0` as `psx_overlay_slot_create`, `0x80036000` as `psx_overlay_slot_release`, `0x80038114` as `psx_overlay_slot_step_color_fade`, and `0x800388a8` as `psx_hud_overlay_init_resources`, with direct technical decompiler comments replacing one earlier generic "verified by subagent pass" note at `psx_draw_hud_overlay_pass`. Current best read is tighter for viewer work: `psx_level_session_loop -> psx_hud_overlay_init_resources` seeds HUD resources during level-session startup, `psx_draw_world_visible_passes` still executes world stage-1 then stage-2 lanes before calling `psx_draw_hud_overlay_pass`, and the `psx_overlay_slot_*` table is a non-map-facing presentation queue layered after world routing rather than a source of authored map geometry. The practical implication is narrower false-match risk: unresolved map-family art fallback should not reuse evidence from this overlay slot lane, because these slots are UI/presentation primitives independent of section-0 map record dispatch and world-object projection.
Latest verified batch: [docs/regret-hidden-debugger-investigation.md](docs/regret-hidden-debugger-investigation.md) now also records the debugger-side cleanup pass after the first `.unk` loader/runtime split, the final exhaustive Regret-side caller sweep, and the first practical seeding/simulation model. The live `REGRET.EXE` database now has the source-pane constructor/pointer/draw/viewport methods, the source-buffer create/load/split/destroy chain, the breakpoint-table helpers, the current-entry push/pop helpers, the interpreter saved-farptr helpers (`13f0:0000/003c`), the interpreter-context create/init pair (`13f0:00e8/0244`), the shared slot-chunk accessor at `13f8:1d72`, and the named interpreter wrapper at `13f8:10da` that feeds `usecode_debugger_interpreter_hook`. The practical consequence is sharper than before: the remaining blocker is not `can we export readable source` or even `can compiled usecode carry line numbers`, because retail Remorse already can, and it is no longer `what else in Regret might still be debugger-related`. The remaining blocker is now narrowly `where inside the already-identified interpreter dispatcher/runtime path does Regret seed the current-entry stack, and how can that same engine-side path be reused or reproduced for stable RUN/step behavior`, with the strongest current simulation route now being a small in-process reuse of existing VM context data rather than blind external memory poking or offline-only source-file tricks. Latest verified batch: [docs/regret-hidden-debugger-investigation.md](docs/regret-hidden-debugger-investigation.md) now also records the debugger-side cleanup pass after the first `.unk` loader/runtime split, the final exhaustive Regret-side caller sweep, and the first practical seeding/simulation model. The live `REGRET.EXE` database now has the source-pane constructor/pointer/draw/viewport methods, the source-buffer create/load/split/destroy chain, the breakpoint-table helpers, the current-entry push/pop helpers, the interpreter saved-farptr helpers (`13f0:0000/003c`), the interpreter-context create/init pair (`13f0:00e8/0244`), the shared slot-chunk accessor at `13f8:1d72`, and the named interpreter wrapper at `13f8:10da` that feeds `usecode_debugger_interpreter_hook`. The practical consequence is sharper than before: the remaining blocker is not `can we export readable source` or even `can compiled usecode carry line numbers`, because retail Remorse already can, and it is no longer `what else in Regret might still be debugger-related`. The remaining blocker is now narrowly `where inside the already-identified interpreter dispatcher/runtime path does Regret seed the current-entry stack, and how can that same engine-side path be reused or reproduced for stable RUN/step behavior`, with the strongest current simulation route now being a small in-process reuse of existing VM context data rather than blind external memory poking or offline-only source-file tricks.
Latest verified batch: [docs/jp-remorse-hidden-debugger-investigation.md](docs/jp-remorse-hidden-debugger-investigation.md) now records the first debugger-focused comparison pass on `/ja/CRUSADER.EXE`. Current best read is narrower than the No Regret result but still decision-relevant: the JP Win32 build clearly retains broad executable cheat/debug machinery, yet live byte searches on the active image found no hits for the classic hidden usecode-debugger UI bundle (`Goto Line`, `Watch what?`, `Inspect what?`, `Global name`, `Search for`, `FILE NOT FOUND`, `Unable to open this file`, `Nothing to find`, `Not found`, `Done`) even though the same method still recovers positive-control strings like `JASSICA16`, `Immortality enabled.`, and `Cheats are now active.`. The practical consequence is that JP currently strengthens the `broad Win32 cheat/debug preservation` story, but not the `JP preserved the missing retail debugger bootstrap` theory; No Regret remains the stronger sibling-build anchor for the hidden-debugger unlock problem. Latest verified batch: [docs/jp-remorse-hidden-debugger-investigation.md](docs/jp-remorse-hidden-debugger-investigation.md) now records the first debugger-focused comparison pass on `/ja/CRUSADER.EXE`. Current best read is narrower than the No Regret result but still decision-relevant: the JP Win32 build clearly retains broad executable cheat/debug machinery, yet live byte searches on the active image found no hits for the classic hidden usecode-debugger UI bundle (`Goto Line`, `Watch what?`, `Inspect what?`, `Global name`, `Search for`, `FILE NOT FOUND`, `Unable to open this file`, `Nothing to find`, `Not found`, `Done`) even though the same method still recovers positive-control strings like `JASSICA16`, `Immortality enabled.`, and `Cheats are now active.`. The practical consequence is that JP currently strengthens the `broad Win32 cheat/debug preservation` story, but not the `JP preserved the missing retail debugger bootstrap` theory; No Regret remains the stronger sibling-build anchor for the hidden-debugger unlock problem.
@ -25,7 +245,7 @@ Latest verified batch: [docs/retail-debugger-entry-options.md](docs/retail-debug
Latest verified batch: [docs/startup-map-patch-file.md](docs/startup-map-patch-file.md) now closes the long-standing startup string `Using map patch file.` tightly enough to stop treating it as a vague debug/status artifact. Current best read is that `Init_Everything` prints that line only when `static\fixed.dat` exists, and the later fixed-map cache path then prefers the loaded `static\fixed.dat` archive handle over the base `fixed.dat` handle for map/fixed-object reads. The remaining uncertainty in this lane is now narrow: whether any later consumer does a finer-grained fallback/merge than the first recovered chooser, not what the startup line is referring to in the first place. Latest verified batch: [docs/startup-map-patch-file.md](docs/startup-map-patch-file.md) now closes the long-standing startup string `Using map patch file.` tightly enough to stop treating it as a vague debug/status artifact. Current best read is that `Init_Everything` prints that line only when `static\fixed.dat` exists, and the later fixed-map cache path then prefers the loaded `static\fixed.dat` archive handle over the base `fixed.dat` handle for map/fixed-object reads. The remaining uncertainty in this lane is now narrow: whether any later consumer does a finer-grained fallback/merge than the first recovered chooser, not what the startup line is referring to in the first place.
Latest verified batch: [docs/psx/psx.md](docs/psx/psx.md), [docs/psx/map-rendering.md](docs/psx/map-rendering.md), [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md), and [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now tighten the PSX render-side model another step in both Ghidra and the viewer exporter. The earlier `DAT_800758d4` consumer finding remains intact and is still wired into the viewer-side cache path as explicit `companionExtents` metadata, but the bigger practical change in this batch is the first measured art-binding recovery pass for the viewer exporter: the PSX cache builder now treats large zero-block `DAT_800758d8` constructor-placement bands as inherited-art candidates, first via same-map `DAT_800758cc` script-signature donors and then via a constrained nearest-donor fallback inside the current `0x003e..0x0064` family. That rebuild moved the scene set from `58,262` fallback items / `1,714` bundle-mapped items to `25,038` fallback items / `34,938` bundle-mapped items, making early representative maps such as `0`, `9`, and `43` mostly real-art while leaving `map 104` and the remaining `0x0042` / `0x0055..0x0063` constructor-placement band as the clearest unresolved outliers. The practical remaining gap is therefore narrower now: not "why are most PSX scenes placeholders" but "what executable-backed alias/resource rule explains the remaining zero-block constructor-placement families without leaning on donor heuristics." Latest verified batch: [docs/psx/psx.md](docs/psx/psx.md), [docs/psx/map-rendering.md](docs/psx/map-rendering.md), [docs/psx/map-viewer-plan.md](docs/psx/map-viewer-plan.md), and [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now tighten the PSX render-side model another step in both Ghidra and the viewer exporter. The earlier `DAT_800758d4` consumer finding remains intact and is still wired into the viewer-side cache path as explicit `companionExtents` metadata, but the bigger practical change in this batch is the first measured art-binding recovery pass for the viewer exporter: the PSX cache builder now treats large zero-block `DAT_800758d8` constructor-placement bands as inherited-art candidates, first via same-map `DAT_800758cc` script-signature donors and then via a constrained nearest-donor fallback inside the current `0x003e..0x0064` family. That rebuild moved the scene set from `58,262` fallback items / `1,714` bundle-mapped items to `25,038` fallback items / `34,938` bundle-mapped items, making early representative maps such as `0`, `9`, and `43` mostly real-art while leaving `map 104` and the remaining `0x0042` / `0x0055..0x0063` constructor-placement band as the clearest unresolved outliers. The newest live follow-up now sharpens the next branch enough to change execution order: `psx_draw_main_visible_object` applies authored palette overrides only for type bands `0x003e..0x00ab` (`source+0x06` high byte) and `>=0x00ac` (`source+0x0c` high byte), `psx_draw_special_visible_queue` skips those overrides entirely, and `psx_sprite_resource_submit_frame` versus `psx_image_table_submit_frame` resolve the same high-byte token through different CLUT tables. The latest coherence pass narrows that again in a way the exporter has to respect: `psx_level_session_loop` installs CLUT tables during level load via `wdl_resource_bundle_load_by_index -> level_palette_header_apply -> level_palette_upload_cluts`, both constructors bind the drawable resource at spawn from `DAT_800758d8`, `psx_object_integrate_motion_and_route_visible` advances state and routes objects into stage-1 versus stage-2 visibility, and `psx_main_visible_list_sort_range` dependency-sorts the stage-1 slice before draw submission. The newest six-track subagent sweep closes two more structural gaps: `0x0042` still shares the generic descriptor cluster used across `0x003e..0x0050` instead of owning a unique descriptor fork, and the root-dispatch `u5=0x0022` versus `u5=0x0030` split currently looks like a main-visible presentation/orientation variant rather than a separate draw lane. The viewer-side donor fence now reflects that narrower read: mixed-family and mixed-`u5` provisional donor merges for unresolved generic-family types are blocked, so `map 104` `0x0042` is back on explicit placeholders instead of one shared false wall. The practical remaining gap is therefore narrower now: not "why are most PSX scenes placeholders" and not even just "which palette index is right", but "which runtime-bank/state/resource-kind combination explains the remaining `0x0042` presentation families without reopening broad donor heuristics."
- Overall useful decompilation progress: about 59% - Overall useful decompilation progress: about 59%
- Reasonable uncertainty band: about 56% to 64% - Reasonable uncertainty band: about 56% to 64%
@ -77,6 +297,7 @@ Measured live naming floor for `CRUSADER.EXE` right now:
- The PSX lane is no longer just side inventory. Retail/pre-alpha bundle loading, mission-briefing/passcode structure, the reduced-content pre-alpha disc, and now the retail map object's last projection stage all have dedicated notes and enough stable naming to support future targeted passes. - The PSX lane is no longer just side inventory. Retail/pre-alpha bundle loading, mission-briefing/passcode structure, the reduced-content pre-alpha disc, and now the retail map object's last projection stage all have dedicated notes and enough stable naming to support future targeted passes.
- The Remorse class-lift preparation lane now has a usable document cluster: overall plan, candidate inventory, endpoint spec, ABI constraints, family notes for `EntityDispatchEntry` and `SpriteNode`, a conservative `Entity` family split, a VM runtime/owner-resource layout note, a compatibility-header draft, and one grouped resume index. - The Remorse class-lift preparation lane now has a usable document cluster: overall plan, candidate inventory, endpoint spec, ABI constraints, family notes for `EntityDispatchEntry` and `SpriteNode`, a conservative `Entity` family split, a VM runtime/owner-resource layout note, a compatibility-header draft, and one grouped resume index.
- The same class-lift prep lane is now more execution-ready: the `0x4588` broker family has its own focused object note, the toolchain story has a dedicated fingerprint-evidence note, and there is now a concrete first-batch class-authoring checklist ready for the first MCP-backed namespace/struct/vtable pass. - The same class-lift prep lane is now more execution-ready: the `0x4588` broker family has its own focused object note, the toolchain story has a dedicated fingerprint-evidence note, and there is now a concrete first-batch class-authoring checklist ready for the first MCP-backed namespace/struct/vtable pass.
- The live Remorse class-authoring lane now also has its first bounded NPC AI family lifted as class owners instead of flat process names: `Remorse::NPCActionProcess` plus `StandProcess`, `PaceProcess`, `SurrenderProcess`, `GuardProcess`, and `LoiterProcess` now exist in-session with their direct create/run/destroy surfaces moved under class ownership, while the shared guard/loiter idle helper remains a free function on purpose until a stronger single-class owner emerges.
- The live Remorse VM class-lift lane also recovered from a decompiler breakage in `Remorse::EntityVmRuntime::Create`: the root cause was a hidden-return-storage allocator helper signature at `1000:42e2`, `Create` now decompiles again, and the provisional `/Remorse/EntityVmSlotEntry` datatype now exists with the stable `+0x1e..+0x24` buffer-pair fields named. - The live Remorse VM class-lift lane also recovered from a decompiler breakage in `Remorse::EntityVmRuntime::Create`: the root cause was a hidden-return-storage allocator helper signature at `1000:42e2`, `Create` now decompiles again, and the provisional `/Remorse/EntityVmSlotEntry` datatype now exists with the stable `+0x1e..+0x24` buffer-pair fields named.
- The live Remorse VM class-lift lane is tighter again: the old `UsecodeProcess_*` context lifecycle bodies at `1420:0eec`, `1420:10b6`, `1420:10da`, `1420:1162`, `1420:118f`, and `1420:1278` now live under `Remorse::EntityVmContext::{CreateFromSlotIndex, FreeBuffer, SyncGlobalValueAndDispatch, Destroy, Save, Load}`, with short raw `000d:` provenance comments preserved on each entry. - The live Remorse VM class-lift lane is tighter again: the old `UsecodeProcess_*` context lifecycle bodies at `1420:0eec`, `1420:10b6`, `1420:10da`, `1420:1162`, `1420:118f`, and `1420:1278` now live under `Remorse::EntityVmContext::{CreateFromSlotIndex, FreeBuffer, SyncGlobalValueAndDispatch, Destroy, Save, Load}`, with short raw `000d:` provenance comments preserved on each entry.
- The same VM class-lift lane tightened one step further through local PyGhidra fallback once the live `run_write_script(...)` route still returned `404 No context found for request`: `/Remorse/EntityVmContext` is now a real datatype, `entity_vm_slot_entry_create_or_clear` and `InitSlotOwnerBuffers` now carry `EntityVmSlotEntry *`, `AcquireSlotForEntity` now returns `EntityVmSlotEntry *`, and `InitSlots` / `ReleaseSlots` now take direct `EntityVmRuntime * this`. - The same VM class-lift lane tightened one step further through local PyGhidra fallback once the live `run_write_script(...)` route still returned `404 No context found for request`: `/Remorse/EntityVmContext` is now a real datatype, `entity_vm_slot_entry_create_or_clear` and `InitSlotOwnerBuffers` now carry `EntityVmSlotEntry *`, `AcquireSlotForEntity` now returns `EntityVmSlotEntry *`, and `InitSlots` / `ReleaseSlots` now take direct `EntityVmRuntime * this`.
@ -141,7 +362,7 @@ Measured live naming floor for `CRUSADER.EXE` right now:
3. Promote ledger coverage from existing verified notes before broadening into fresh executable-wide sweeps. 3. Promote ledger coverage from existing verified notes before broadening into fresh executable-wide sweeps.
4. Use overlap repair only where it unlocks an active high-payoff lane. 4. Use overlap repair only where it unlocks an active high-payoff lane.
5. Use the map-renderer/tooling lane to validate shape ids, map placements, and viewer semantics before promoting additional static-object names in Ghidra. 5. Use the map-renderer/tooling lane to validate shape ids, map placements, and viewer semantics before promoting additional static-object names in Ghidra.
6. Keep the PSX lane focused on the final state/variant/art bridge now that the first post-spawn interaction/reselection cluster is named; avoid broad renderer-side heuristics that bypass those runtime paths. 6. Keep the PSX lane focused on the final lane-aware state/resource/frame/palette bridge now that the first post-spawn interaction/reselection cluster is named; avoid broad renderer-side heuristics that bypass those runtime paths or flatten main-visible and special-visible palette behavior together.
## Next Resume Point ## Next Resume Point
@ -160,13 +381,14 @@ The newest broad-sweep UI batch tightened three more local families without need
8. Tighten the seg006 masked-helper caller chains so the local state-selector/value family can be tied to concrete gameplay subsystems. 8. Tighten the seg006 masked-helper caller chains so the local state-selector/value family can be tied to concrete gameplay subsystems.
9. Classify the paired seg070 loops behind `entity_vm_runtime_owner_resource_create`, especially which temporary buffers and record schemas each family populates. 9. Classify the paired seg070 loops behind `entity_vm_runtime_owner_resource_create`, especially which temporary buffers and record schemas each family populates.
10. Stay on the Remorse VM class-lift batch while the repaired runtime lane is warm: use the now-recovered `CreateFromSlotIndex` caller pack to decide whether any remaining scalar positions deserve stronger typedefs, but keep the return semantically conservative until the base-process inheritance model is explicit enough to justify a prettier live return type. 10. Stay on the Remorse VM class-lift batch while the repaired runtime lane is warm: use the now-recovered `CreateFromSlotIndex` caller pack to decide whether any remaining scalar positions deserve stronger typedefs, but keep the return semantically conservative until the base-process inheritance model is explicit enough to justify a prettier live return type.
11. The current broader Remorse follow-up batch is now materially tighter: `WatchEntityController` is effectively re-identified as the live camera-process create lane, `DialogMenuObject` is the last compact family here without a safe live re-anchor, `PresentationCallbackBroker` now has install/teardown plus both slot `+0x08` and preserved slot `+0x0c` caller evidence, `CacheBackendObject` has its indexed entry writer, and `SpriteNode::Create` now looks like the shared compact node constructor for `GumpCreate_*` wrappers. The clearest next unresolved items are therefore: a safer live reanchor for `DialogMenuObject`, a decision on whether the camera-process lane should stay under the stronger live `Camera_*` naming or also receive a class-owner layer, deeper slot `+0x0c` payload classification in the broker lane, and higher-level subtype/layout work above the compact `SpriteNode` base. 11. Stay on the new Remorse NPC action-process class-lift lane while seg033 is warm: the owner-first move is done, so the next bounded pass should recover process-function-table slot order, inspect `StandProcess::Run` and `PaceProcess::Run` for equally strong family-local helpers, and decide whether a provisional `/Remorse/NPCActionProcess` datatype is safe or whether the family should remain owner-only for now.
12. In the local GhidraMCP upgrade lane, add support for dual POST body decoding (`application/json` plus form-urlencoded) and a constrained live write-side PyGhidra endpoint family so future custom-storage/type repairs can stay inside the active MCP session when Python is enabled. 12. The current broader Remorse follow-up batch is now materially tighter: `WatchEntityController` is effectively re-identified as the live camera-process create lane, `DialogMenuObject` is the last compact family here without a safe live re-anchor, `PresentationCallbackBroker` now has install/teardown plus both slot `+0x08` and preserved slot `+0x0c` caller evidence, `CacheBackendObject` has its indexed entry writer, and `SpriteNode::Create` now looks like the shared compact node constructor for `GumpCreate_*` wrappers. The clearest next unresolved items are therefore: a safer live reanchor for `DialogMenuObject`, a decision on whether the camera-process lane should stay under the stronger live `Camera_*` naming or also receive a class-owner layer, deeper slot `+0x0c` payload classification in the broker lane, and higher-level subtype/layout work above the compact `SpriteNode` base.
13. Promote additional ledger rows directly from already-verified docs and live comments, especially where segments already deserve `Foothold`, `Partial`, or `Deep`; the new seg029 step-aware sweep batch, seg031 queue-release batch, seg090 movement-helper batch, seg033 NPC-process foothold, and seg032 item-type foothold should be the immediate template. 13. In the local GhidraMCP upgrade lane, add support for dual POST body decoding (`application/json` plus form-urlencoded) and a constrained live write-side PyGhidra endpoint family so future custom-storage/type repairs can stay inside the active MCP session when Python is enabled.
14. Promote additional ledger rows directly from already-verified docs and live comments, especially where segments already deserve `Foothold`, `Partial`, or `Deep`; the new seg029 step-aware sweep batch, seg031 queue-release batch, seg090 movement-helper batch, seg033 NPC-process foothold, and seg032 item-type foothold should be the immediate template.
14. If the VM lane stalls, revisit `000e:ffb0` from the now-better-constrained video/audio caller windows and try to recover an adjacent non-overlapped helper before attempting broad boundary repair. 14. If the VM lane stalls, revisit `000e:ffb0` from the now-better-constrained video/audio caller windows and try to recover an adjacent non-overlapped helper before attempting broad boundary repair.
15. Continue the map-renderer cross-check lane by building one conservative shape-id/map-placement crosswalk from `shapedata_more_complete.txt`, extracted corpora, and authored scene evidence before promoting more trigger-heavy classes in NE. 15. Continue the map-renderer cross-check lane by building one conservative shape-id/map-placement crosswalk from `shapedata_more_complete.txt`, extracted corpora, and authored scene evidence before promoting more trigger-heavy classes in NE.
16. Keep the PSX pre-alpha lane alive as a secondary target: classify the `LoadExec` callers, test whether the stale `TALK1.XA` path is still reachable, and compare the shipped `LSET1` bundles against the retail extractor outputs. 16. Keep the PSX pre-alpha lane alive as a secondary target: classify the `LoadExec` callers, test whether the stale `TALK1.XA` path is still reachable, and compare the shipped `LSET1` bundles against the retail extractor outputs.
17. Continue the retail PSX state/art lane from the new art-binding recovery baseline: keep `DAT_800758d4` on the runtime-bounds side unless new family-specific evidence contradicts it, treat `map 104` plus the remaining `0x0042` / `0x0055..0x0063` zero-block constructor-placement band as the primary regression target, and trace the next family-specific callers around `psx_type4_reselect_motion_state`, `FUN_80028c94`, constructor-side resource creation, and the drawable-resource/frame submission lane until the remaining donor-based fallback logic can be replaced with an executable-backed alias/resource rule. 17. Continue the retail PSX state/art lane from the new art-binding recovery baseline: keep `DAT_800758d4` on the runtime-bounds side unless new family-specific evidence contradicts it, treat `map 104` plus the remaining `0x0042` / `0x0055..0x0063` zero-block constructor-placement band as the primary regression target, and trace the next family-specific callers around `psx_type4_reselect_motion_state`, `FUN_80028c94`, constructor-side resource creation, and the drawable-resource/frame submission lane until the remaining donor-based fallback logic can be replaced with an executable-backed lane-aware alias/resource/frame/palette rule. The first practical split to preserve is now explicit in the live database: main-visible objects can consume authored override bytes from different source offsets by type band, while the special-visible queue does not, and the next renderer-side check should keep the full coherence order intact: load-time CLUT install, spawn-time resource bind, frame-time state advance and routing, then stage-1 dependency sort before submission.
## Remaining Work To Reach A Reasonably Complete Decompilation State ## Remaining Work To Reach A Reasonably Complete Decompilation State

View file

@ -0,0 +1,142 @@
import sys
from collections import Counter, defaultdict
FN = r"binary/Crusader - No Remorse Weapons Main Ram.bin"
OFFSETS = [0x133000, 0x133416, 0x1335d4]
WINDOW_BEFORE = 0x100
WINDOW_AFTER = 0x200
def hexdump(buf, base):
lines = []
for i in range(0, len(buf), 16):
chunk = buf[i:i+16]
hexs = ' '.join(f"{b:02x}" for b in chunk)
ascii_ = ''.join((chr(b) if 32 <= b < 127 else '.') for b in chunk)
lines.append(f"{base+i:08x}: {hexs:<47} {ascii_}")
return '\n'.join(lines)
def analyze_region(buf, base):
print(f"\n-- Analysis for region base 0x{base:x}, length {len(buf):x} --")
ctr = Counter(buf)
print("Top byte frequencies:")
for b,c in ctr.most_common(12):
print(f" 0x{b:02x}: {c}")
# positions of 0x0c/0x0d
pos0c = [i for i,b in enumerate(buf) if b==0x0c]
pos0d = [i for i,b in enumerate(buf) if b==0x0d]
print(f"Count 0x0c: {len(pos0c)}, sample positions (rel): {pos0c[:12]}")
print(f"Count 0x0d: {len(pos0d)}, sample positions (rel): {pos0d[:12]}")
# stride detection via start-similarity
best = []
for stride in range(4,129):
n = len(buf)//stride
if n < 3:
continue
matches = 0
total = 0
for i in range(n-1):
a = buf[i*stride:i*stride+8]
b = buf[(i+1)*stride:(i+1)*stride+8]
total += 8
matches += sum(1 for x,y in zip(a,b) if x==y)
score = matches/total
best.append((score, stride, n))
best.sort(reverse=True)
print("Top candidate strides (score, stride, record_count):")
for s,stride,n in best[:8]:
print(f" {s:.3f}, {stride}, {n}")
if best:
top_stride = best[0][1]
print(f"\nSample records using stride {top_stride} (showing first 8 bytes of each record):")
n = len(buf)//top_stride
for i in range(min(n,12)):
rec = buf[i*top_stride:(i+1)*top_stride]
print(f" rec#{i:02d} @ {base + i*top_stride:08x}: {' '.join(f'{b:02x}' for b in rec[:12])}")
# look for small incrementing sequences at any fixed offset inside stride
def find_incrementing(offset_within, length=6):
vals = []
for i in range(0, (len(buf)-offset_within)//top_stride):
pos = i*top_stride + offset_within
vals.append(buf[pos])
# find runs of increasing or consistent values
if len(vals) < 3:
return None
return vals[:min(32,len(vals))]
# search offsets 0..min(32, stride-1)
inc_candidates = []
for off in range(0, min(32, top_stride)):
vals = []
nrecs = len(buf)//top_stride
for i in range(nrecs):
vals.append(buf[i*top_stride + off])
# measure monotonic segments
diffs = sum(1 for i in range(1,len(vals)) if vals[i] != vals[i-1])
if diffs > 0:
inc_candidates.append((diffs, off, vals[:16]))
inc_candidates.sort(reverse=True)
if inc_candidates:
print('\nTop changing offsets within stride (changes, offset, sample_values):')
for d,off,sample in inc_candidates[:8]:
print(f" {d}, {off}, {sample}")
if __name__ == '__main__':
try:
with open(FN, 'rb') as f:
data = f.read()
except FileNotFoundError:
print('ERROR: file not found:', FN)
sys.exit(2)
for off in OFFSETS:
start = max(0, off - WINDOW_BEFORE)
end = min(len(data), off + WINDOW_AFTER)
region = data[start:end]
print('\n' + '='*60)
print(f"Dump around 0x{off:08x} (file offsets 0x{start:08x}-0x{end:08x})")
print(hexdump(region, start))
analyze_region(region, start)
# unified larger window covering the three offsets
big_start = max(0, min(OFFSETS) - 0x200)
big_end = min(len(data), max(OFFSETS) + 0x300)
big = data[big_start:big_end]
print('\n' + '='*60)
print(f"Unified window 0x{big_start:08x}-0x{big_end:08x}, length {len(big):x}")
# run stride search on big window
ctr = Counter(big)
print('Unified top bytes:', ctr.most_common(12))
best = []
for stride in range(4,129):
n = len(big)//stride
if n < 4:
continue
matches = 0
total = 0
for i in range(n-1):
a = big[i*stride:i*stride+8]
b = big[(i+1)*stride:(i+1)*stride+8]
total += 8
matches += sum(1 for x,y in zip(a,b) if x==y)
score = matches/total
best.append((score, stride, n))
best.sort(reverse=True)
print('Unified top candidate strides (score, stride, n):')
for s,stride,n in best[:12]:
print(f" {s:.3f}, {stride}, {n}")
# show sample records for top unified stride
if best:
top = best[0][1]
print(f"\nUnified sample records with stride {top}:")
n = len(big)//top
for i in range(min(n,12)):
rec = big[i*top:(i+1)*top]
print(f" rec#{i:02d} @ {big_start + i*top:08x}: {' '.join(f'{b:02x}' for b in rec[:16])}")
print('\nDone')

View file

@ -0,0 +1,35 @@
#!/usr/bin/env python3
import os
fn = os.path.join('binary','Crusader - No Remorse Weapons Main Ram.bin')
if not os.path.exists(fn):
print('file missing', fn); raise SystemExit(2)
with open(fn,'rb') as f:
data = f.read()
size = len(data)
base = 0x6466A
stride = 0x26
def read_name(defOff):
end = defOff + stride
s = ''
for i in range(defOff, min(end, size)):
c = data[i]
if 32 <= c <= 126:
s += chr(c)
else:
if len(s) >= 2:
return s
s = ''
return s
for tableStart in (0x64355, 0x64340, 0x64330):
print(f'\nDumping table @0x{tableStart:X}')
rec = 10
for ch in range(40):
idxOff = tableStart + ch*rec + 9
if idxOff >= size:
break
sel = data[idxOff]
defOff = base + sel*stride
name = read_name(defOff) if defOff < size else ''
print(f'chan {ch:02d}: sel=0x{sel:02X} ({sel}) -> def@0x{defOff:X} -> {name} (idxOff=0x{idxOff:X})')

View file

@ -0,0 +1,208 @@
#!/usr/bin/env python3
import os
import sys
from collections import Counter
fn = os.path.join('binary','Crusader - No Remorse Weapons Main Ram.bin')
if not os.path.exists(fn):
print('ERROR: file not found:', fn)
sys.exit(2)
with open(fn,'rb') as f:
data = f.read()
size = len(data)
print(f'File: {fn} size=0x{size:X} ({size} bytes)')
# Try weapon table base/stride used earlier
base_candidates = [0x6466A, 0x64640, 0x64680, 0x64000]
stride = 0x26
def extract_weapon_table(base, stride, max_rows=128):
rows = []
for i in range(max_rows):
off = base + i*stride
if off >= size:
break
row = data[off:off+stride]
# extract ASCII-like name at start
name_bytes = bytearray()
for b in row:
if 32 <= b <= 126:
name_bytes.append(b)
else:
if len(name_bytes)>0:
break
name = name_bytes.decode('ascii',errors='replace')
rows.append((i, off, name))
return rows
found = None
for base in base_candidates:
rows = extract_weapon_table(base, stride, max_rows=64)
non_empty = [r for r in rows if r[2]]
if len(non_empty) >= 8:
found = (base, rows)
break
if not found:
# fallback: scan for repeated ASCII names with stride 0x26
print('Primary bases not successful; scanning for candidate bases...')
candidates = []
for b in range(0, min(size-0x26*8, 0x200000), 0x10):
rows = extract_weapon_table(b, stride, max_rows=12)
non_empty = sum(1 for r in rows if r[2])
if non_empty >= 6:
candidates.append((b, non_empty))
candidates.sort(key=lambda x:-x[1])
if candidates:
base = candidates[0][0]
print(f'Picked candidate base 0x{base:X} (hits={candidates[0][1]})')
found = (base, extract_weapon_table(base, stride, max_rows=128))
if not found:
print('ERROR: could not find weapon table automatically.')
sys.exit(3)
base, rows = found
print(f'Weapon table base=0x{base:X} stride=0x{stride:X} rows scanned={len(rows)}')
weapon_rows = [r for r in rows if r[2]]
for idx, off, name in weapon_rows:
print(f' idx {idx:02X} @0x{off:X} -> {name}')
max_index = max((r[0] for r in weapon_rows), default=-1)
print(f'Weapon rows discovered: {len(weapon_rows)} max idx {max_index}')
# build name map
name_map = {r[0]: r[2] for r in weapon_rows}
# Search for candidate commit tables of byte-sized indices
min_len = 12
scan_len = 24
candidates = []
limit = size - scan_len
for off in range(0, limit, 1):
window = data[off:off+scan_len]
valid = sum(1 for b in window if b <= max_index and b >= 0)
if valid >= int(scan_len*0.75):
# extend forward while valid fraction remains high
end = off+scan_len
while end < size:
b = data[end]
window_len = end-off+1
if b <= max_index:
end += 1
continue
# if occasional invalid, allow up to 25% invalid
win = data[off:end+1]
v = sum(1 for x in win if x <= max_index)
if v >= int(len(win)*0.75):
end += 1
continue
break
length = end-off
seq = list(data[off:off+min(64,length)])
# deduplicate nearby overlaps by only keeping when off is first in a run
if candidates and off < candidates[-1]['end'] + 4:
continue
candidates.append({'off':off,'end':end,'len':length,'sample':seq[:64]})
if len(candidates) >= 16:
break
print('\nCandidate byte-sized commit tables found:')
if not candidates:
print(' none')
else:
for c in candidates[:10]:
off = c['off']; l=c['len']
print(f' table @0x{off:X} len={l}')
# print first 24 entries mapped
n = min(24,l)
entries = list(data[off:off+n])
for i,v in enumerate(entries):
name = name_map.get(v,'')
print(f' ch {i:02} -> 0x{v:02X} {name}')
# check for 0x0C/0x0D
hits = [(i,v) for i,v in enumerate(entries) if v in (0x0C,0x0D)]
if hits:
for i,v in hits:
print(f' ** contains 0x{v:02X} at entry {i}')
# Also search for 16-bit big-endian indices sequences
print('\nScanning for 16-bit big-endian index sequences (min_len=12 entries)...')
be_candidates = []
min_entries = 12
for off in range(0, size-2*min_entries, 1):
# read min_entries big-endian 16-bit values
ok = True
vals = []
for i in range(min_entries):
idx = off + i*2
v = (data[idx]<<8) | data[idx+1]
if v > max_index:
ok = False
break
vals.append(v)
if ok:
be_candidates.append((off, vals[:min_entries]))
if len(be_candidates) >= 8:
break
if not be_candidates:
print(' none')
else:
for off,vals in be_candidates:
print(f' table @0x{off:X} (big-endian 16-bit entries) sample:')
for i,v in enumerate(vals):
print(f' ch {i:02} -> 0x{v:04X} {name_map.get(v,"")}')
# Summary: look for any channel mapping to 0x0C or 0x0D anywhere in file as single bytes
print('\nSummary scan for bytes 0x0C or 0x0D in likely index contexts:')
positions = []
for val in (0x0C,0x0D):
offs = [i for i,b in enumerate(data) if b==val]
# filter to positions where surrounding bytes align with many valid indices
filtered = []
for o in offs:
left = max(0,o-4); right = min(size,o+5)
win = data[left:right]
valid = sum(1 for b in win if b<=max_index)
if valid >= int(len(win)*0.7):
filtered.append(o)
print(f' byte 0x{val:02X}: total occurrences={len(offs)} filtered likely-context={len(filtered)}')
if filtered:
for o in filtered[:10]:
print(f' at 0x{o:X} (file offset)')
print('\nDone.')
# Extra: dump candidate channel commit tables at known offsets with more rows
def read_name_at(defOff):
end = defOff + stride - 1
s = ''
best = ''
for i in range(defOff, min(end+1, size)):
c = data[i]
if 32 <= c <= 126:
s += chr(c)
else:
if len(s) >= 2:
best = s
break
else:
s = ''
return best
for tableStart in (0x64355, 0x64340, 0x64330):
if tableStart >= size:
continue
rec = 10
rows = 40
print(f'\nChannel commit table @0x{tableStart:X} (rec={rec}) rows up to {rows}:')
for ch in range(rows):
idxOff = tableStart + ch*rec + 9
if idxOff >= size:
break
sel = data[idxOff]
defOff = base + sel*stride
name = read_name_at(defOff) if defOff < size else ''
print(f' chan {ch:02d}: sel=0x{sel:02X} ({sel}) -> def@0x{defOff:X} -> name: {name} (idxOff=0x{idxOff:X})')

8
tmp_dump_offsets.py Normal file
View file

@ -0,0 +1,8 @@
import os
fn=os.path.join('binary','Crusader - No Remorse Weapons Main Ram.bin')
for off in (0x133000,0x133416,0x1335d4):
with open(fn,'rb') as f:
f.seek(off)
b=f.read(128)
print(hex(off))
print(' '.join(f"{x:02X}" for x in b))

View file

@ -0,0 +1,8 @@
from PIL import Image
IN = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png"
OUT = r"K:\ghidra\Crusader_Decomp\binary\crop_weapon_row.png"
box = (80,44,360,92)
img = Image.open(IN)
crop = img.crop(box)
crop.save(OUT)
print('wrote', OUT)

27
tools/find_bytes.py Normal file
View file

@ -0,0 +1,27 @@
import argparse
def main():
p = argparse.ArgumentParser()
p.add_argument('--file', required=True)
p.add_argument('--byte', required=True, help='byte value in hex (e.g. 0x0c)')
p.add_argument('--base', type=lambda x: int(x,0), default=0x80000000)
p.add_argument('--start', type=lambda x: int(x,0), default=0x80064000)
p.add_argument('--end', type=lambda x: int(x,0), default=0x80064800)
args = p.parse_args()
bval = int(args.byte, 0)
with open(args.file, 'rb') as f:
f.seek(0,2)
size = f.tell()
s = args.start - args.base
e = args.end - args.base
s = max(0, s)
e = min(size, e)
f.seek(s)
data = f.read(e-s)
for i, b in enumerate(data):
if b == bval:
addr = args.base + s + i
print(hex(addr), hex(s+i))
if __name__ == '__main__':
main()

10
tools/find_cd_bytes2.py Normal file
View file

@ -0,0 +1,10 @@
import sys
fname = sys.argv[1]
start = int(sys.argv[2], 0)
length = int(sys.argv[3], 0)
with open(fname, 'rb') as f:
f.seek(start)
d = f.read(length)
for i, b in enumerate(d):
if b in (0x0c, 0x0d):
print(hex(0x80000000 + start + i), hex(start + i), hex(b))

45
tools/hexdump_region.py Normal file
View file

@ -0,0 +1,45 @@
import argparse
import sys
def hexdump(data, base=0, width=16):
for i in range(0, len(data), width):
chunk = data[i:i+width]
hex_bytes = ' '.join(f"{b:02x}" for b in chunk)
ascii_repr = ''.join((chr(b) if 32 <= b < 127 else '.') for b in chunk)
print(f"{base+i:08x}: {hex_bytes:<48} {ascii_repr}")
def main():
p = argparse.ArgumentParser()
p.add_argument('--file', required=True)
p.add_argument('--addr', required=True, help='PSX virtual address (e.g. 0x80064355)')
p.add_argument('--before', type=int, default=64)
p.add_argument('--after', type=int, default=256)
p.add_argument('--base', type=lambda x: int(x,0), default=0x80000000, help='PSX RAM base address used for dump offset')
args = p.parse_args()
addr = int(args.addr, 0)
base = args.base
offset = addr - base
if offset < 0:
print(f"Computed negative offset {offset} for addr {hex(addr)} base {hex(base)}", file=sys.stderr)
sys.exit(2)
try:
with open(args.file, 'rb') as f:
f.seek(0, 2)
size = f.tell()
if offset >= size:
print(f"Offset {offset:#x} beyond file size {size:#x}")
sys.exit(3)
start = max(0, offset - args.before)
f.seek(start)
data = f.read(args.before + args.after)
print(f"File: {args.file}")
print(f"PSX addr: {hex(addr)}, file offset: {hex(offset)}, dump start: {hex(start)}, len: {len(data)}")
hexdump(data, base=start)
except FileNotFoundError:
print(f"File not found: {args.file}")
sys.exit(4)
if __name__ == '__main__':
main()

107
tools/hud_icon_match.py Normal file
View file

@ -0,0 +1,107 @@
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
# regions
HUD_BOX = (80,44,360,92) # left,top,right,bot
VSTRIP_X0, VSTRIP_X1 = 956, 1023
import os, sys
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build RGB rows
rows = []
for y in range(H):
row = []
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.append((r,g,b))
rows.append(row)
# extract HUD crop
lx,ty,rx,by = HUD_BOX
w = rx-lx; h = by-ty
hud = [[rows[y][x] for x in range(lx,rx)] for y in range(ty,by)]
# build mask for HUD
hud_mask = [[(1 if any(ch!=0 for ch in hud[y][x]) else 0) for x in range(w)] for y in range(h)]
# extract vstrip area and find blobs
vx0,vx1 = VSTRIP_X0, VSTRIP_X1
vw = vx1-vx0+1
vrows = [[rows[y][x] for x in range(vx0,vx1+1)] for y in range(H)]
# mask and flood-fill
mask = [[1 if any(ch!=0 for ch in vrows[y][x]) else 0 for x in range(vw)] for y in range(H)]
visited = [[0]*vw for _ in range(H)]
from collections import deque
blobs = []
for y in range(H):
for x in range(vw):
if mask[y][x] and not visited[y][x]:
q=deque([(x,y)])
visited[y][x]=1
xs=[]; ys=[]
while q:
cx,cy=q.popleft()
xs.append(cx); ys.append(cy)
for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)):
nx,ny = cx+dx, cy+dy
if 0<=nx<vw and 0<=ny<H and mask[ny][nx] and not visited[ny][nx]:
visited[ny][nx]=1; q.append((nx,ny))
x0,x1 = min(xs), max(xs)
y0,y1 = min(ys), max(ys)
area = len(xs)
blobs.append((x0,y0,x1,y1,area))
# sort blobs by y (top to bottom)
blobs.sort(key=lambda b: b[1])
print('Found', len(blobs), 'blobs in vstrip')
for i,b in enumerate(blobs):
x0,y0,x1,y1,area = b
print(i, 'blob box (vstrip coords)=', (x0,y0,x1,y1), 'area=',area)
# extract blob images
def extract_from_rows(rr, x0,y0,x1,y1):
w = x1-x0+1; h = y1-y0+1
img = [[rr[y+y0][x+x0] for x in range(w)] for y in range(h)]
return img
blob_imgs = [extract_from_rows(vrows, *b[:4]) for b in blobs]
# template match each blob against hud with sliding window
import math
results = []
for bi, img in enumerate(blob_imgs):
bh = len(img); bw = len(img[0])
if bh<4 or bw<4: continue
best = (1e12, -1,-1)
# convert flattened arrays for speed
tmpl = [c for row in img for px in row for c in px]
for y in range(0, h-bh+1):
for x in range(0, w-bw+1):
ssd=0
for j in range(bh):
for i in range(bw):
r1,g1,b1 = img[j][i]
r2,g2,b2 = hud[j+y][i+x]
dr=r1-r2; dg=g1-g2; db=b1-b2
ssd += dr*dr + dg*dg + db*db
if ssd>best[0]: break
if ssd>best[0]: break
if ssd < best[0]: best = (ssd,x,y)
results.append((bi,bw,bh,best[0],best[1],best[2]))
# sort by score
results.sort(key=lambda x: x[3])
print('\nTop matches:')
for bi,bw,bh,ssd,x,y in results[:10]:
# compute HUD pixel coordinates and vram offsets
hud_x = lx + x; hud_y = ty + y
start_idx = hud_y*W + hud_x
end_idx = (hud_y+bh-1)*W + (hud_x + bw -1)
so = start_idx*2; eo = (end_idx+1)*2 -1
vb = blobs[bi]
v_x0 = vx0 + vb[0]; v_y0 = vb[1]; v_x1 = vx0 + vb[2]; v_y1 = vb[3]
print(f'blob#{bi} vbox=({v_x0},{v_y0})-({v_x1},{v_y1}) size={bw}x{bh} bestssd={ssd} hudpos=({hud_x},{hud_y}) bytes=0x{so:06x}-0x{eo:06x}')
# If results empty, report
if not results:
print('No matches found')

89
tools/hud_row_scan.py Normal file
View file

@ -0,0 +1,89 @@
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
TOP, BOT = 44, 92 # y range (inclusive start, exclusive end)
import os
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build mask for this row range
col_counts = [0]*W
for y in range(TOP, BOT):
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
if val & 0x7fff:
col_counts[x] += 1
# find runs where col_counts exceeds threshold
maxc = max(col_counts)
thr = max(3, int(maxc*0.2))
runs = []
inside=False
for x,c in enumerate(col_counts):
if c>thr and not inside:
sx = x; inside=True
if c<=thr and inside:
ex = x-1; inside=False; runs.append((sx,ex))
if inside: runs.append((sx,W-1))
print('max per-col count', maxc, 'threshold', thr)
print('runs:', runs)
# compute byte offsets and produce crops (no PIL)
import struct, zlib
OUT_DIR = r"K:\ghidra\Crusader_Decomp\binary\hud_candidates"
os.makedirs(OUT_DIR, exist_ok=True)
with open(IN_BIN,'rb') as f:
bin_data = f.read()
# build full rows RGB
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = bin_data[off] | (bin_data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5)&0x1F) << 3
r = ((val >>10)&0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# for each run, create a crop with padding
for i,(sx,ex) in enumerate(runs):
pad = 8
x0 = max(0, sx-pad)
x1 = min(W-1, ex+pad)
y0 = TOP; y1 = BOT-1
w = x1 - x0 + 1
h = y1 - y0 + 1
rawrows = []
for y in range(y0, y1+1):
rawrows.append(b"\x00" + rows[y][x0*3:(x0+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw,9)
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
import zlib
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
out = os.path.join(OUT_DIR, f'cand_{i}_{x0}_{y0}.png')
with open(out,'wb') as f:
f.write(png)
start_idx = y0*W + x0
end_idx = y1*W + x1
so = start_idx*2
eo = (end_idx+1)*2 -1
print(f'wrote {out} box=({x0},{y0})-({x1},{y1}) bytes=0x{so:06x}-0x{eo:06x} col_counts_max={max(col_counts[x0:x1+1])}')
# also print top columns with counts
top_cols = sorted(((c,x) for x,c in enumerate(col_counts)), reverse=True)[:30]
print('\nTop columns (count,x):')
for c,x in top_cols:
print(c,x)

52
tools/make_crop_no_pil.py Normal file
View file

@ -0,0 +1,52 @@
import struct, zlib, os
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_PNG = r"K:\ghidra\Crusader_Decomp\binary\crop_weapon_row_nopil.png"
W,H = 1024,512
# box: left,top,right,bot (exclusive right/bot matching PIL convention)
left,top,right,bot = 80,44,360,92
w = right - left
h = bot - top
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build rows
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# compose crop raw (PNG filter 0 per row)
rawrows = []
for y in range(top, top+h):
rawrows.append(b"\x00" + rows[y][left*3:(left+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw, level=9)
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
with open(OUT_PNG, 'wb') as f:
f.write(png)
# compute file offsets
start_idx = top*W + left
end_idx = (top+h-1)*W + (left + w -1)
so = start_idx * 2
eo = ((end_idx)+1)*2 -1
print('wrote', OUT_PNG)
print(f'pixel box=({left},{top})-({left+w-1},{top+h-1}) size={w}x{h}')
print(f'vram byte offsets: 0x{so:06x}-0x{eo:06x} (inclusive)')

View file

@ -0,0 +1,26 @@
import argparse
def main():
p = argparse.ArgumentParser()
p.add_argument('--file', required=True)
p.add_argument('--addr', required=True)
p.add_argument('--base', type=lambda x: int(x,0), default=0x80000000)
p.add_argument('--stride', type=int, default=10)
p.add_argument('--field_idx', type=int, default=9)
p.add_argument('--count', type=int, default=20)
args = p.parse_args()
addr = int(args.addr, 0)
offset = addr - args.base
with open(args.file, 'rb') as f:
for ch in range(args.count):
idx = offset + ch*args.stride + args.field_idx
f.seek(idx)
b = f.read(1)
if not b:
print(f"ch {ch:02d}: EOF")
break
print(f"ch {ch:02d}: addr {hex(addr + ch*args.stride + args.field_idx)} offset {hex(idx)} value {b[0]:02x} ({b[0]})")
if __name__ == '__main__':
main()

View file

@ -0,0 +1,57 @@
import struct, zlib, os
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_DIR = r"K:\ghidra\Crusader_Decomp\binary\vstrip_crops"
W,H = 1024,512
x0 = 956
x1 = 1023
w = x1 - x0 + 1
step_h = 48
import math
os.makedirs(OUT_DIR, exist_ok=True)
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build full RGB rows
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# produce crops
import struct, zlib
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
for i in range(0, H, step_h):
y0 = i
y1 = min(H-1, i+step_h-1)
h = y1 - y0 + 1
rawrows = []
for y in range(y0, y1+1):
rawrows.append(b"\x00" + rows[y][x0*3:(x0+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw,9)
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
out = os.path.join(OUT_DIR, f'vstrip_{y0:03d}_{y1:03d}.png')
with open(out,'wb') as f:
f.write(png)
start_idx = y0*W + x0
end_idx = y1*W + x1
so = start_idx*2
eo = (end_idx+1)*2 -1
print(out, f'box=({x0},{y0})-({x1},{y1}) bytes=0x{so:06x}-0x{eo:06x}')

60
tools/vram_analyze.py Normal file
View file

@ -0,0 +1,60 @@
from collections import deque
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
import os
with open(IN_PATH,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build mask of non-black pixels
mask = bytearray(W*H)
for i in range(count):
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
if r|g|b:
mask[i] = 1
# flood-fill connected components (4-neigh)
visited = bytearray(W*H)
bbs = []
for idx in range(W*H):
if mask[idx] and not visited[idx]:
q = deque([idx])
visited[idx]=1
xs = []
ys = []
while q:
v = q.popleft()
y = v // W
x = v % W
xs.append(x); ys.append(y)
# neighbors
for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)):
nx = x+dx; ny = y+dy
if 0<=nx<W and 0<=ny<H:
ni = ny*W+nx
if mask[ni] and not visited[ni]:
visited[ni]=1
q.append(ni)
x0,x1 = min(xs), max(xs)
y0,y1 = min(ys), max(ys)
area = len(xs)
start_idx = y0*W + x0
end_idx = y1*W + x1
start_off = start_idx*2
end_off = (end_idx+1)*2 - 1
bbs.append((x0,y0,x1,y1,area,start_off,end_off))
# sort by area descending
bbs.sort(key=lambda x: -x[4])
print('Found', len(bbs), 'components')
for i,(x0,y0,x1,y1,area,so,eo) in enumerate(bbs[:20]):
print(f'[{i}] box=({x0},{y0})-({x1},{y1}) area={area} bytes=0x{so:06x}-0x{eo:06x}')
# Save the top few as small BMP crops for inspection
from PIL import Image
img = Image.open(r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png")
for i,(x0,y0,x1,y1,area,so,eo) in enumerate(bbs[:12]):
crop = img.crop((x0,y0,x1+1,y1+1))
out = r"K:\ghidra\Crusader_Decomp\binary\crop_%02d.png"%i
crop.save(out)
print('wrote', out)

60
tools/vram_crop_grid.py Normal file
View file

@ -0,0 +1,60 @@
import struct, zlib, os
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
IN_PNG = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png"
W,H = 1024,512
# simple PNG crop writer: read full PNG rows from our previous generated PNG file
# read raw image bytes (we will load via simple approach using original binary->RGB conversion again)
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# make full RGB array
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# crop grid params
xs = list(range(0, W, 128))
ys = list(range(0, 192, 64))
out_dir = r"K:\ghidra\Crusader_Decomp\binary\crops_grid"
os.makedirs(out_dir, exist_ok=True)
import zlib, struct
for y0 in ys:
for x0 in xs:
w = min(256, W-x0)
h = min(128, H-y0)
# build raw rows top-to-bottom
rawrows = []
for y in range(y0, y0+h):
rawrows.append(b"\x00" + rows[y][x0*3:(x0+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw, level=9)
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
import zlib
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
out = os.path.join(out_dir, f'crop_{x0}_{y0}.png')
with open(out,'wb') as f:
f.write(png)
# compute file-offset range
start_idx = y0*W + x0
end_idx = (y0+h-1)*W + (x0+w-1)
so = start_idx*2
eo = (end_idx+1)*2 -1
print(out, f'box=({x0},{y0})-({x0+w-1},{y0+h-1}) bytes=0x{so:06x}-0x{eo:06x}')

View file

@ -0,0 +1,61 @@
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
with open(IN_PATH,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
mask = [0]*(W*H)
for i in range(count):
off = i*2
val = data[off] | (data[off+1]<<8)
if val & 0x7fff:
mask[i]=1
# row sums
row_sums = [sum(mask[y*W:(y+1)*W]) for y in range(H)]
col_sums = [sum(mask[x::W]) for x in range(W)]
# find row bands where row_sum > threshold
thr_row = max(10, int(max(row_sums)*0.05))
runs = []
inside = False
for y, s in enumerate(row_sums):
if s>thr_row and not inside:
start = y; inside=True
if s<=thr_row and inside:
end = y-1; inside=False; runs.append((start,end))
if inside: runs.append((start,H-1))
print('row runs (threshold=%d):'%thr_row, runs)
# print top runs with sums
for (s,e) in runs:
tot = sum(row_sums[s:e+1])
print('run',s,e,'rows=',e-s+1,'sum=',tot)
# find column runs similarly
thr_col = max(5, int(max(col_sums)*0.05))
cruns=[]; inside=False
for x,s in enumerate(col_sums):
if s>thr_col and not inside:
sx=x; inside=True
if s<=thr_col and inside:
ex=x-1; inside=False; cruns.append((sx,ex))
if inside: cruns.append((sx,W-1))
print('col runs (threshold=%d):'%thr_col, cruns[:10])
# Identify intersection boxes by combining top few row runs and col runs
candidates=[]
for (ry0,ry1) in runs:
for (cx0,cx1) in cruns:
# compute density
area = (ry1-ry0+1)*(cx1-cx0+1)
s = 0
for y in range(ry0,ry1+1):
s += sum(mask[y*W+cx0 : y*W+cx1+1])
if s > max(200, area*0.02):
candidates.append((cx0,ry0,cx1,ry1,s,area))
candidates.sort(key=lambda x:-x[4])
print('\nCandidates:')
for i,(x0,y0,x1,y1,s,area) in enumerate(candidates[:12]):
so = (y0*W + x0)*2
eo = ((y1*W + x1)+1)*2 -1
print(f'[{i}] box=({x0},{y0})-({x1},{y1}) area_pixels={area} nonzero={s} bytes=0x{so:06x}-0x{eo:06x}')
# print a few rows around top to locate HUD band
print('\nTop rows with nonzero counts (y:count)')
for y in range(0,160):
if row_sums[y]>0:
print(y, row_sums[y])

60
tools/vram_to_bmp.py Normal file
View file

@ -0,0 +1,60 @@
import struct, os
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_PATH = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.bmp"
W, H = 1024, 512
with open(IN_PATH, 'rb') as f:
data = f.read()
exp = W * H * 2
if len(data) < exp:
print(f"Warning: expected {exp} bytes, got {len(data)} bytes")
# read pixels (little-endian 16bpp)
count = min(len(data) // 2, W * H)
pixels = [None] * (W * H)
for i in range(count):
off = i * 2
val = data[off] | (data[off+1] << 8)
b = (val & 0x1F) << 3
g = ((val >> 5) & 0x1F) << 3
r = ((val >> 10) & 0x1F) << 3
pixels[i] = (r, g, b)
# fill remaining if any
for i in range(count, W*H):
pixels[i] = (0,0,0)
# BMP row padding
row_bytes_unpadded = 3 * W
row_size = (row_bytes_unpadded + 3) // 4 * 4
pixel_data = bytearray()
for y in range(H-1, -1, -1):
row_start = y * W
for x in range(W):
r,g,b = pixels[row_start + x]
pixel_data.extend(bytes((b, g, r)))
pad = row_size - row_bytes_unpadded
if pad:
pixel_data.extend(b"\x00" * pad)
# headers
bfType = b'BM'
bfSize = 14 + 40 + len(pixel_data)
bfReserved1 = 0
bfReserved2 = 0
bfOffBits = 14 + 40
bmp = bytearray()
bmp.extend(bfType)
bmp.extend(struct.pack('<IHHI', bfSize, bfReserved1, bfReserved2, bfOffBits))
# DIB header (BITMAPINFOHEADER)
biSize = 40
biWidth = W
biHeight = H
biPlanes = 1
biBitCount = 24
biCompression = 0
biSizeImage = len(pixel_data)
biXPelsPerMeter = 2835
biYPelsPerMeter = 2835
biClrUsed = 0
biClrImportant = 0
bmp.extend(struct.pack('<IIIHHIIIIII', biSize, biWidth, biHeight, biPlanes, biBitCount, biCompression, biSizeImage, biXPelsPerMeter, biYPelsPerMeter, biClrUsed, biClrImportant))
bmp.extend(pixel_data)
with open(OUT_PATH, 'wb') as f:
f.write(bmp)
print('Wrote', OUT_PATH, 'bytes=', len(bmp))

46
tools/vram_to_png.py Normal file
View file

@ -0,0 +1,46 @@
import struct, zlib
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_PATH = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png"
W, H = 1024, 512
with open(IN_PATH, 'rb') as f:
data = f.read()
exp = W * H * 2
if len(data) < exp:
print(f"Warning: expected {exp} bytes, got {len(data)} bytes")
count = min(len(data) // 2, W * H)
# build raw RGB rows top-to-bottom
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y * W + x
if i < count:
off = i * 2
val = data[off] | (data[off+1] << 8)
b = (val & 0x1F) << 3
g = ((val >> 5) & 0x1F) << 3
r = ((val >> 10) & 0x1F) << 3
else:
r = g = b = 0
row.extend([r, g, b])
rows.append(b"\x00" + bytes(row))
raw = b"".join(rows)
comp = zlib.compress(raw, level=9)
# PNG helpers
def chunk(ch_type, data):
out = struct.pack('>I', len(data)) + ch_type + data
import zlib
crc = zlib.crc32(ch_type + data) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
# IHDR
ihdr = struct.pack('>IIBBBBB', W, H, 8, 2, 0, 0, 0) # 8-bit, truecolor, no interlace
png += chunk(b'IHDR', ihdr)
# IDAT
png += chunk(b'IDAT', comp)
# IEND
png += chunk(b'IEND', b'')
with open(OUT_PATH, 'wb') as f:
f.write(png)
print('Wrote', OUT_PATH, 'size=', len(png))