diff --git a/.github/instructions/ghidra.instructions.md b/.github/instructions/ghidra.instructions.md
index bbfadf5..4e871d3 100644
--- a/.github/instructions/ghidra.instructions.md
+++ b/.github/instructions/ghidra.instructions.md
@@ -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_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 `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.
- 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.
@@ -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.
- 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.
-- 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.
-- 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.
-- 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.
-- Default install dir for the toolkit is `I:\Apps\ghidra_12.0.4_PUBLIC`.
-- 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.
+- Never use the offline/local PyGhidra CLI toolkit from this workspace.
+- Do not invoke `tools.pyghidra_crusader`, the local `.venv-pyghidra311` entrypoint, or any project-open workflow that competes with the live GUI lock.
+- 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.
+- 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.
+- 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.
# Current Verified Raw-Import Ports
diff --git a/.github/skills/pyghidra-ghidra-ops/SKILL.md b/.github/skills/pyghidra-ghidra-ops/SKILL.md
index ba807e5..d27f02c 100644
--- a/.github/skills/pyghidra-ghidra-ops/SKILL.md
+++ b/.github/skills/pyghidra-ghidra-ops/SKILL.md
@@ -1,214 +1,43 @@
---
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
-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
-- Create or delete functions in `CRUSADER-RAW.EXE`.
-- Apply small batched repairs driven by verified addresses.
-- Add comments or rename functions by address from a repeatable JSON plan.
-- 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.
+- Run live MCP readonly Python-backed inspection when decompiler or xref work needs scripted help.
+- Run live MCP write-capable scripted edits for small verified rename, comment, function-boundary, or datatype batches.
+- Keep scripted Ghidra work inside the active GUI-backed MCP session so project locks do not matter.
## Workspace Defaults
-- Ghidra install dir: `I:\Apps\ghidra_12.0.4_PUBLIC`
-- Ghidra project dir: repo root
-- Ghidra project name: `Crusader`
-- 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`
+- Active authority: the live Ghidra MCP session
+- Default target unless stated otherwise: `CRUSADER.EXE`
+- Python-backed operations must run through MCP endpoints exposed by the active Ghidra session
## Constraints
- 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.
-- 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.
-- 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.
+- Never fall back to the offline/local CLI path from this workspace.
+- 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.
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
-powershell -ExecutionPolicy Bypass -File .\tools\pyghidra_crusader\bootstrap_env.ps1
-```
-
-## 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
-```
+- Prefer standard MCP endpoints first for decompilation, disassembly, xrefs, renames, comments, function creation/deletion, and datatype work.
+- 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.
## Implementation Notes
-- Address strings accept raw `SSSS:OOOO` form or plain integers such as `0x75a90`.
-- The CLI tries a few root folder path variants when opening the program so it can tolerate minor project path differences.
-- Plan files support `remove_functions`, `rename_functions`, `create_functions`, `comments`, and `assert_functions`.
-- `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.
\ No newline at end of file
+- Address strings still accept raw `SSSS:OOOO` form or plain integers such as `0x75a90` when the underlying MCP endpoint supports them.
+- 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.
+- 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.
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index fca1b05..f1d1d90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,4 @@ bin/**
USECODE/REGRET/REGRET_USECODE_extracted/chunks/**
exports/**
out/**
+binary/**
\ No newline at end of file
diff --git a/Crusader.rep/idata/01/0000001b.prp b/Crusader.rep/idata/01/0000001b.prp
index 0a924f8..dd8cb78 100644
--- a/Crusader.rep/idata/01/0000001b.prp
+++ b/Crusader.rep/idata/01/0000001b.prp
@@ -1,11 +1,15 @@
+
+
+
+
diff --git a/Crusader.rep/idata/01/~00000015.db/change.data.gbf b/Crusader.rep/idata/01/~00000015.db/change.data.gbf
index 2c4d5c0..56f2dba 100644
Binary files a/Crusader.rep/idata/01/~00000015.db/change.data.gbf and b/Crusader.rep/idata/01/~00000015.db/change.data.gbf differ
diff --git a/Crusader.rep/idata/01/~00000015.db/change.map.gbf b/Crusader.rep/idata/01/~00000015.db/change.map.gbf
index e4bb70d..9f37095 100644
Binary files a/Crusader.rep/idata/01/~00000015.db/change.map.gbf and b/Crusader.rep/idata/01/~00000015.db/change.map.gbf differ
diff --git a/Crusader.rep/idata/01/~00000015.db/db.123.gbf b/Crusader.rep/idata/01/~00000015.db/db.126.gbf
similarity index 99%
rename from Crusader.rep/idata/01/~00000015.db/db.123.gbf
rename to Crusader.rep/idata/01/~00000015.db/db.126.gbf
index dfe0dd1..2ebb760 100644
Binary files a/Crusader.rep/idata/01/~00000015.db/db.123.gbf and b/Crusader.rep/idata/01/~00000015.db/db.126.gbf differ
diff --git a/Crusader.rep/idata/01/~00000015.db/db.122.gbf b/Crusader.rep/idata/01/~00000015.db/db.127.gbf
similarity index 99%
rename from Crusader.rep/idata/01/~00000015.db/db.122.gbf
rename to Crusader.rep/idata/01/~00000015.db/db.127.gbf
index 1276595..b90fa4b 100644
Binary files a/Crusader.rep/idata/01/~00000015.db/db.122.gbf and b/Crusader.rep/idata/01/~00000015.db/db.127.gbf differ
diff --git a/Crusader.rep/idata/01/~0000001b.db/change.data.gbf b/Crusader.rep/idata/01/~0000001b.db/change.data.gbf
new file mode 100644
index 0000000..7730ed0
Binary files /dev/null and b/Crusader.rep/idata/01/~0000001b.db/change.data.gbf differ
diff --git a/Crusader.rep/idata/01/~0000001b.db/change.map.gbf b/Crusader.rep/idata/01/~0000001b.db/change.map.gbf
new file mode 100644
index 0000000..3cc354d
Binary files /dev/null and b/Crusader.rep/idata/01/~0000001b.db/change.map.gbf differ
diff --git a/Crusader.rep/idata/01/~0000001b.db/db.25.gbf b/Crusader.rep/idata/01/~0000001b.db/db.60.gbf
similarity index 88%
rename from Crusader.rep/idata/01/~0000001b.db/db.25.gbf
rename to Crusader.rep/idata/01/~0000001b.db/db.60.gbf
index ec93c58..d4d51c0 100644
Binary files a/Crusader.rep/idata/01/~0000001b.db/db.25.gbf and b/Crusader.rep/idata/01/~0000001b.db/db.60.gbf differ
diff --git a/Crusader.rep/idata/01/~0000001b.db/db.61.gbf b/Crusader.rep/idata/01/~0000001b.db/db.61.gbf
new file mode 100644
index 0000000..088a77a
Binary files /dev/null and b/Crusader.rep/idata/01/~0000001b.db/db.61.gbf differ
diff --git a/Crusader.rep/projectState b/Crusader.rep/projectState
index a42aba5..1aea38c 100644
--- a/Crusader.rep/projectState
+++ b/Crusader.rep/projectState
@@ -4,7 +4,8 @@
-
+
+
diff --git a/Crusader.rep/user/00/~00000008.db/db.56.gbf b/Crusader.rep/user/00/~00000008.db/db.57.gbf
similarity index 99%
rename from Crusader.rep/user/00/~00000008.db/db.56.gbf
rename to Crusader.rep/user/00/~00000008.db/db.57.gbf
index 724ea4e..0958511 100644
Binary files a/Crusader.rep/user/00/~00000008.db/db.56.gbf and b/Crusader.rep/user/00/~00000008.db/db.57.gbf differ
diff --git a/Crusader.rep/user/00/~00000008.db/db.55.gbf b/Crusader.rep/user/00/~00000008.db/db.58.gbf
similarity index 99%
rename from Crusader.rep/user/00/~00000008.db/db.55.gbf
rename to Crusader.rep/user/00/~00000008.db/db.58.gbf
index 128dcd0..871aa54 100644
Binary files a/Crusader.rep/user/00/~00000008.db/db.55.gbf and b/Crusader.rep/user/00/~00000008.db/db.58.gbf differ
diff --git a/Crusader.rep/user/00/~0000000a.db/db.10.gbf b/Crusader.rep/user/00/~0000000a.db/db.12.gbf
similarity index 99%
rename from Crusader.rep/user/00/~0000000a.db/db.10.gbf
rename to Crusader.rep/user/00/~0000000a.db/db.12.gbf
index 067abe3..fbe5216 100644
Binary files a/Crusader.rep/user/00/~0000000a.db/db.10.gbf and b/Crusader.rep/user/00/~0000000a.db/db.12.gbf differ
diff --git a/Crusader.rep/user/00/~0000000d.db/db.6.gbf b/Crusader.rep/user/00/~0000000d.db/db.8.gbf
similarity index 99%
rename from Crusader.rep/user/00/~0000000d.db/db.6.gbf
rename to Crusader.rep/user/00/~0000000d.db/db.8.gbf
index 78ee86e..5f7bfdb 100644
Binary files a/Crusader.rep/user/00/~0000000d.db/db.6.gbf and b/Crusader.rep/user/00/~0000000d.db/db.8.gbf differ
diff --git a/Crusader.rep/user/00/~0000000d.db/db.7.gbf b/Crusader.rep/user/00/~0000000d.db/db.9.gbf
similarity index 99%
rename from Crusader.rep/user/00/~0000000d.db/db.7.gbf
rename to Crusader.rep/user/00/~0000000d.db/db.9.gbf
index 4a45395..a86e3c0 100644
Binary files a/Crusader.rep/user/00/~0000000d.db/db.7.gbf and b/Crusader.rep/user/00/~0000000d.db/db.9.gbf differ
diff --git a/Crusader.rep/versioned/00/0000000b.prp b/Crusader.rep/versioned/00/0000000b.prp
new file mode 100644
index 0000000..0a924f8
--- /dev/null
+++ b/Crusader.rep/versioned/00/0000000b.prp
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Crusader.rep/versioned/00/~0000000b.db/checkout.dat b/Crusader.rep/versioned/00/~0000000b.db/checkout.dat
new file mode 100644
index 0000000..b1d2722
--- /dev/null
+++ b/Crusader.rep/versioned/00/~0000000b.db/checkout.dat
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Crusader.rep/idata/01/~0000001b.db/db.26.gbf b/Crusader.rep/versioned/00/~0000000b.db/db.1.gbf
similarity index 98%
rename from Crusader.rep/idata/01/~0000001b.db/db.26.gbf
rename to Crusader.rep/versioned/00/~0000000b.db/db.1.gbf
index dcb6437..899b839 100644
Binary files a/Crusader.rep/idata/01/~0000001b.db/db.26.gbf and b/Crusader.rep/versioned/00/~0000000b.db/db.1.gbf differ
diff --git a/Crusader.rep/versioned/00/~0000000b.db/history.dat b/Crusader.rep/versioned/00/~0000000b.db/history.dat
new file mode 100644
index 0000000..e560ff8
--- /dev/null
+++ b/Crusader.rep/versioned/00/~0000000b.db/history.dat
@@ -0,0 +1 @@
+1;Maddo;1775898506582;
diff --git a/Crusader.rep/versioned/~index.dat b/Crusader.rep/versioned/~index.dat
index be7e570..9cf8deb 100644
--- a/Crusader.rep/versioned/~index.dat
+++ b/Crusader.rep/versioned/~index.dat
@@ -12,8 +12,11 @@ VERSION=1
00000001:CRUSADER-PATCHED.EXE:c0a86451f6e9206725659389900
/orig_cd
00000009:CRUSADER.EXE:c0a86451c28b202638339220200
+/psx
+/psx/remorse
+ 0000000b:SLUS_002.68:c0a86451c52a19721794868600
/regret
00000007:ASYLUM.DLL:c0a86451c280202637798314100
00000008:REGRET.EXE:c0a86451c281202637836837200
-NEXT-ID:b
+NEXT-ID:c
MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/crusader_decompilation_notes.md b/crusader_decompilation_notes.md
index b57216d..eb61409 100644
--- a/crusader_decompilation_notes.md
+++ b/crusader_decompilation_notes.md
@@ -4,7 +4,97 @@ This file is an index. Detailed notes have been split into the `docs/` folder by
Active live analysis target is now `CRUSADER.EXE`. Existing `CRUSADER-RAW.EXE` notes remain in scope as cross-reference evidence and should be cited alongside live NE addresses when they support a rename, variable role, or behavior claim.
-Recent verified PSX map-viewer 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 the new [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now record both the latest executable-backed renderer findings and the first measured recovery pass against placeholder-heavy PSX scenes. Current best read is that the cache builder still exports executable-named section-0 visible families (`section0_dispatch_roots`, `section0_constructor_placements`), runtime/state layers for `DAT_800758d8`, `DAT_800758d0`, `DAT_800758cc`, `DAT_800758d4`, and one offline `FUN_8003b00c` decode candidate for `DAT_8006b5d8 -> DAT_8006769c`, but it now also treats large zero-block `DAT_800758d8` constructor-placement bands as inherited-art candidates before falling back to placeholders. That donor-based recovery path moved the built cache from `58,262` fallback items / `1,714` bundle-mapped items down to `25,038` fallback items / `34,938` bundle-mapped items, making maps such as `0`, `9`, and `43` mostly real-art while leaving `map 104` as the clearest remaining outlier. The newest pass does not change the earlier `DAT_800758d4` conclusion: `psx_object_advance_state_script` still sign-extends those three bytes into `obj+0x30/+0x34/+0x38` for overlap/contact-style consumers rather than the draw path, and the unresolved blocker still sits later in the live state-to-resource/frame bridge for the remaining constructor-placement families.
+Recent verified PSX CLUT override-routing follow-up: [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now records a 2026-04-12 live MCP closure pass on `0x80041458`, `0x80041144`, `0x80044bdc`, `0x80044e9c`, `0x800a9f48`, and `0x800a9f66`. Current best read is now exporter-critical and executable-backed: main-visible injects authored high-byte palette token while special-visible does not, override selection is gated by `flags & 0xfffffff0`, active override resolution diverges by submitter/resource-format lane, and token `0` is effectively no-override in the world-object draw path.
+
+Recent verified PSX palette/export follow-up: [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now records the 2026-04-12 lock-in for the prior VRAM-dump `mode 1` palette proof. Current best read is now export-explicit: `mode 1` bundles should render against a shared contiguous 256-entry CLUT equivalent to live row `0xF0`, `x=0`, while the bundle header palette index stays diagnostic only as `defaultPaletteIndex`. The same follow-up also records that the processed PSX catalog already carried `62` maps, so the user-visible "single map" issue was export inclusion rather than cache enumeration.
+
+Recent verified PSX no-placeholder exporter follow-up: [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) now records the 2026-04-12 focused `map 104` cache pass that removes synthetic fallback atlases from the exporter path. Current best read is now provenance-explicit: unresolved mixed-role buckets are resolved per authored-family plus raw-`u5` cohort into actual PSX bundle art, and scene `mapSource` rows now preserve `mappingSource` plus optional `artCohort` so provisional donor matches remain auditable. Focused validation now exports `1002` art items, `0` fallback items, `1` atlas, and `136` shape definitions for scene fingerprint `3497e7f641856415`.
+
+Recent verified PSX map-104 cohort follow-up: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a 2026-04-12 final live pass on active writable `SLUS_002.68` anchored to scene fingerprint `3497e7f641856415` and fixed cohorts (root `0x0022` items `25/35`, root `0x0030` items `30/31`, constructor `0x0030` items `85/86`, control `0x0066` item `53`). Current best read is now cohort-safe and exporter-ready: constructors preserve authored route seed into `obj+0x1c`, stage-2 still requires object-local `0x0400`, draw submitter remains resource-kind based, and order-policy bits (`0x0600`) remain ordering-only; practical immediate change is to keep unresolved map-104 fallback partitioned by exact `record_u5` seed family (`0x0022` vs `0x0030`) while runtime `typePolicy`/resource-kind fields remain unsampled.
+
+Recent verified PSX runtime/control-island follow-up: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a 2026-04-12 live policy/island pass on active writable `SLUS_002.68` centered on `0x80063e54`, `0x80063e68`, `0x800675ec`, and `0x800675f8`. Current best read is now discriminator-explicit: the island is post-load control/runtime gating and policy bits influence ordering/render/publication behavior, but stage-1 versus stage-2 world-visible lane choice still depends primarily on object-local route bit `obj+0x1c & 0x0400` and submitter path remains resource-kind based. Practical exporter consequence is to keep island/policy capture as secondary modifiers while prioritizing resource-kind, latched frame token, and route bit for unresolved map-104 cohort splitting.
+
+Recent verified PSX selector/transition follow-up: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now records a 2026-04-12 live pre-latch closure pass on active writable `SLUS_002.68` centered on `0x80018578`, `0x8001bca0`, `0x8001e6e8`, `0x800260e8`, and `0x80025d68`, with direct type-`0x0042` row bytes recovered from `0x80063c1c` and `0x80063d68`. Current best read is now stage-explicit: selector reseat (`3/4` and peers) is early-gated by view margin and object lane bit `0x0020`, selector install writes `obj+0x9e` and script cursor, and final visible frame choice still latches at `obj+0x94`; practical exporter consequence is to keep pre-latch selector and latched frame token as separate channels for unresolved map-104 placeholder families.
+
+Recent verified PSX descriptor-dispatch follow-up: [docs/psx/map-storage-model.md](docs/psx/map-storage-model.md) now records a 2026-04-12 live section-0 authored-family closure on active `SLUS_002.68` around `psx_dispatch_section0_dispatch_roots` (`0x800256b0`), `psx_dispatch_section0_constructor_placements` (`0x800258cc`), and descriptor table lane `0x80063118` / `0x80063220` / `0x800626f8`. Current best read is now dispatch-explicit: unresolved families `0x0042`, `0x0049`, and `0x0055..0x0063` converge on the same descriptor row (`0x800626f8`) and therefore share one callback chassis (compound-create + first state advance, main-visible refresh, release) at section-0 entry. Practical consequence is that exporter/model divergence for these families should be attributed to per-type bank/policy/state lanes rather than to a type-unique descriptor-row callback fork.
+
+Recent verified PSX progression-latch follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a live timing pass on `0x80027548` and `psx_level_session_set_next_map_id` (`0x8002ba84`). Current best read is now rollover-explicit: progression callback writes deferred next-map latch `DAT_800678d0`, `current_map_id` switches only at level-session rollover (`0x80031edc`), and natural `54 -> 55` miss risk is therefore a deferred preemption window (transition exit before slot-`0x0f` tuple `(0x0a,0x04)` emit), not an immediate same-tick overwrite race.
+
+Recent verified PSX transition-callback follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a live provenance pass on callbacks `0x8002745c` and `0x80027548`. Current best read is now callback-table explicit: both entries are indirect targets from the pointer region around `0x800641f0..0x80064220` with no direct caller xrefs, `0x8002745c` has a branch that can carry slot-family tuple opportunity via `0x80020f7c` and an alternate branch that skips it, and `0x80027548` progression apply increases natural slot-`0x0f` tuple timing sensitivity by advancing transition state before later control-event handling.
+
+Recent verified PSX countdown-vs-slot follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a live structure check driven by the timed-segment clue. Current best read is now causality-tight: countdown logic (`0x8002b738 -> 0x80020794 -> 0x800205e8`) and natural JL-9 arm logic (`0x800214ac -> 0x800640a0[0x0f] -> 0x800230e4 -> 0x800232f0`) are adjacent lanes sharing control-state/timing helpers, with no recovered direct countdown-to-slot call edge in static flow.
+
+Recent verified PSX slot-handler follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a live sibling-recovery pass on `psx_level_gate_slot_handler_table` (`0x800640a0`) with previously raw table-entry handlers defined and named 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 for the `0x0a` family: slot `0x0a/0x0b/0x0c` and slot `0x0d/0x0e` subcases are control/message/transition lanes, while slot `0x0f` case `(0x0a,0x04)` remains the only recovered sibling branch that writes `psx_debug_extra_channel_gate` at `0x800232f0`.
+
+Recent verified PSX event-only synthesis continuation: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a conservative live Ghidra naming/comment durability pass around the natural gate-arm control-event core. Current best read is now semantics-tight but still cautious: `0x800230e4` is promoted to `psx_control_event_slot0f_handler` (slot-family role), slot-entry labels for `0x0d/0x0e/0x0f` are now explicit at `0x800640d4/0x800640d8/0x800640dc`, and upstream `54 -> 49 -> sink` remains preserved as structural topology rather than promoted active path until a second caller lane beyond the known `<0x0a` bound is recovered.
+
+Recent verified PSX experiment follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records two more user emulator trials. Current best read is now sharper: a natural `MFM4` run did not yield `JL-9`, while `JFM4` plus manual gate byte `0x8006739d = 0x01`, then hidden `L0SR`, then `R1 + Circle`, still did. Practical consequence is that `MFM4` remains only the best natural prime candidate, while the natural in-level gate-arm event is now the dominant unresolved part of the mystery. Deferred follow-up experiments to revisit later are the current plan's `2`, `4`, `5`, and `6`.
+
+Recent verified PSX event-only decoding pass: [docs/psx/jl-9-in-level-event.md](docs/psx/jl-9-in-level-event.md) now collects the natural gate-arm work into a dedicated note. Current best read is now event-centered instead of passcode-centered: the sink dispatcher at `0x800214ac..0x800215f8`, slot-family handlers `0x0d/0x0e/0x0f`, and the exact JL-9 arm tuple `(slot 0x0f, arg1 0x0a, arg2 0x04)` are separated from the broader JL-9 chain, while the remaining upstream uncertainty is explicitly narrowed to one authored in-level producer rather than the hidden-code half.
+
+Recent verified PSX forced-test closure: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records user emulator confirmation that manual byte poke `0x8006739d = 0x01`, then hidden `L0SR`, then `R1 + Circle` successfully adds `JL-9`. Current best read is now causal-explicit: this proves the downstream hidden/input grant half and shows that the more direct thing being bypassed is the natural in-level gate-arm event rather than the passcode layer itself. Practical consequence is that `MFM4` is now best treated as the strongest natural prime candidate, not a required part of the forced memory-edit test.
+
+Recent verified PSX hard-clear and manual-test follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` check of the "beat the game on hard, then enter `L0SR`" theory plus a practical emulator poke closure. Current best read is now split: the hard-clear story remains weak because completion-text flows were found but no direct bridge from ending handlers into `psx_debug_extra_channel_gate` was recovered; meanwhile the manual test is now strong because `psx_debug_extra_channel_gate` is confirmed as single byte `0x8006739d`, read as nonzero in the late JL-9 branch, with no recovered clear in inspected menu/session paths. Practical consequence is that the best forced test is now `0x8006739d = 0x01`, then hidden `L0SR`, then `R1 + Circle`.
+
+Recent verified PSX published-code sweep: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` pass against the user-supplied mission-code table. Current best read is now candidate-explicit: `MFM4` (Level 15 hard) is the only strong published ordinary prime recovered so far because ordinary row `i=0x0e` returns selector `0x0f`, maps to current-level `54`, lands in gate family `DAT_80063e68[54]=0x0f`, and also sets `psx_level_runtime_header_state=3`. Practical consequence is narrower and more useful: if a real published-code JL-9 route exists, `MFM4` is now the lead prime, but it still requires the missing in-level gate-arm event before hidden `L0SR` plus `R1 + Circle`.
+
+Recent verified PSX JL-9 step-2 clarification: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now makes the old JL-9 recipe less misleading. Current best read is now explicit that step 2 is not a second passcode-screen action but an in-level scripted/control dispatch through `0x800214ac..0x800215f8`, where handler slot `0x0f` resolves to `psx_set_debug_extra_channel_gate` and the exact writer tuple is `(param_2==0x0a, param_3==0x04)`. The same follow-up also tightens hidden-passcode semantics: `?0SR` / `L0SR` still routes through the shared passcode evaluator/decoder, but its selector-`0` branch does not take the normal mission/apply-load path. Practical consequence is that the current JL-9 model is now `normal passcode prime -> in-level scripted gate-arm event -> hidden passcode -> R1+Circle`, with the remaining blocker narrowed to the concrete in-level event label.
+
+Recent verified PSX passcode-screen semantics follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` closure pass on user-facing step ordering for normal versus hidden passcodes. Current best read is now explicit for screen behavior: both normal and hidden entries route through the same evaluator/decoder path (`0x80034e38` -> `0x8003ec8c`), hidden `0x10` sets `psx_hidden_passcode_flag` in that shared decoder (`0x8003ed28`), and the unnamed caller block around `0x80034c14` has divergent immediate behavior for eval return `0` versus nonzero before transition setup. 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 should be treated as operationally under-specified until the exact player-visible producer for `param_2==0x0a,param_3==4` is directly traced.
+
+Recent verified PSX RP-16 startup/default hardening follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` pass that pushed on undefined nearby init stubs, startup mode-action callsites, selected-id writer closure, active-channel writes, and level mode/difficulty table resolution. Current best read is now stronger and still negative for startup RP-16: `committed_selected_item_id` has only two recovered writers (`0x80039f68` reset and `0x8002f170` table commit sink), startup dispatch stays `8 -> (optional 2) -> 4` without a direct commit call, and scanned `channel_commit_row_selected_item_id` rows (`0x00..0x19`, selected byte `+9`) contain no `0x01` selected-id entry. Practical classification remains `RP-16 not proven startup/default weapon` while preserving row `0x01` as real non-startup data.
+
+Recent verified PSX JL-9 sequence follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` special-passcode priming closure pass on `?0SR/?RTN/?QQQ` against `psx_level_runtime_header_state`, `psx_hidden_passcode_flag`, and `psx_debug_extra_channel_gate`. Current best read is now explicit: `?RTN` clears header state to `0`, `?0SR` sets hidden flag to `1`, `?QQQ` returns sentinel `0x12` without priming header state, and none of the recovered specials alone satisfy gate-arm (`hidden==0 && header_state==3`) at `0x800232f0`, so strongest JL-9 model remains two-phase (pre-hidden gate arm, then hidden/input trigger).
+
+Recent verified PSX RP-16 startup/default follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` startup-path closure pass over `psx_level_post_load_runtime_reset` (`0x80039ef4`), `psx_weapon_channels_init_mode_loadout` (`0x8002f814`), and `psx_weapon_channels_apply_mode_transition_state` (`0x8002f278`). Current best read is now explicit for the startup question: selected-id state is reset to `0` before init dispatch, no fixed selected-id `0x01` seed appears in the named loadout/mode-transition initializers, and fixed-immediate commit sites with `0x11/0x12/(contextual 0x01)` currently sit in gameplay/control lanes rather than fresh-start init. Practical classification is now `RP-16 not proven startup/default weapon` while still remaining a real row with unresolved non-startup acquisition role.
+
+Recent verified PSX JL-9 gate follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` final-enable-sequence closure pass around `0x8002ba9c`, `0x800232f0`, `0x8002fd90`, and `0x8002fff4`. Current best read is now explicit at instruction level: the extra `0x0d` unlock (`JL-9` lane) is gated by `psx_debug_extra_channel_gate` read at `0x8002fff4`, that gate is written only when `psx_hidden_passcode_flag==0 && psx_level_runtime_header_state==3` at `0x800232f0`, and the grant helper entry still requires `psx_hidden_passcode_flag!=0` plus input code `0x1e` at `0x80013174`, making a two-phase hidden flow the strongest practical model.
+
+Recent verified PSX RP-16 follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` row/acquisition pass for selected-id `0x01`. Current best read is now explicit: `RP-16` row `0x01` is real populated weapon-definition data (`0x80064690`), direct shop unlock progression still uses `03..0c` in the primary unlock branch, and the only observed shop-side `0x01` occurrence is in the secondary `0x0a..0x0e` ammo-top-up branch rather than a direct unlock call. This tightens `RP-16` away from "invalid slot" and toward a real early row with legacy/startup/placeholder-like behavior in the current image, pending one concrete non-debug normal acquisition writer.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused live `SLUS_002.68` legitimate-acquisition closure pass with emulator-grounded selected-id mapping (`0x8014577e`, `0x0c=JL-2`, `0x0d=JL-9`). Current best read is tighter by lane: normal loadout and shop direct unlocks remain capped at `<=0x0c`, hidden/debug gating still provides the only recovered fixed-immediate `unlock(0x0d)` site (`0x80030004`), and scripted packed-action dispatch remains the only plausible non-debug `0x0d` exception but is still data-dependent and unproven without a concrete shipped section0 marker/action row.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now incorporates the user-verified selected-weapon byte mapping at `0x8014577e` (`00..0d`) and corrects the earlier local-id shorthand into a stricter two-domain model: caller-side compact channel/local codes feed `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` writes the nested runtime `+0x1c` field.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records the starter-only RAM compare that retracts the earlier `0x1456fc` inventory-list interpretation. The attractive `0x1456fc..0x145748` `0x0002..0x000b` sequence is unchanged across the all-weapons and starter-only dumps, so it is not the live owned-weapon list; the dynamic region begins at `0x14574c`, and the strongest current executable-backed field closure is now byte `0x14577e` (`0x0c` all-weapons vs `0x02` starter-only) as selected/committed weapon row-id state inside a nested runtime block. The separate watch at `0x67944` still changes, but no direct static xrefs for `0x80067944` were recovered in the current image.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now incorporates the stronger emulator-grounded selected-weapon byte mapping at `0x8014577e`: `00` none, `01` `RP-16`, `02` `RP-22`, `03` `RP-32`, `04` `SG-A1`, `05` `AC-88`, `06` `PA-31`, `07` `EM-4`, `08` `PL-1`, `09` `UV-9`, `0A` `GL-303`, `0B` `AR-7`, `0C` `JL-2`, `0D` `JL-9`. This replaces the older `JL-?=11` inference and shifts the remaining open questions to legitimate JL-9 acquisition and RP-16 role/acquisition.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records the RAM-search blocker in a narrower form. The current best runtime-state lead is no longer the nearby commit-table neighborhood but denser small-byte clusters around file offsets `0x133000`, `0x133416`, and `0x1335d4`, with a weaker secondary candidate near `0x422c..0x4440`; the sampled `DAT_80064355[(channel*10)+9]` field does not behave like a plain final JL row id in this dump. The same pass also promotes `psx_handle_special_input_code` as the strongest upstream helper for the special `0x1e` trigger range while still leaving the exact controller-chord mapping open.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records the current RAM-search pivot more accurately. The earlier nearby commit-table neighborhood remains useful for the executable-side channel chain, but in the sampled main-RAM dump it does not show plain final JL ids at the tested byte field; the stronger current live-state candidates have moved to denser `0x0c/0x0d` table-like clusters around file offsets `0x133000`, `0x133416`, and `0x1335d4`. The same pass also names `psx_handle_special_input_code` as the strongest current upstream helper for the hidden `0x1e` input-code lane, while still stopping short of a closed `R1 + Circle` proof.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a six-agent pass that tightened both the JL-9 trigger story and the main-RAM angle. Current best read is: the code closes the path `hidden passcode active -> gated input code 0x1e -> psx_debug_grant_weapon_channels_and_ammo -> extra 0x0d unlock`, but still does not statically prove the exact folklore button chord; the 2 MiB `binary/Crusader - No Remorse Weapons Main Ram.bin` artifact is still plausible main RAM and now has candidate compact slot-like records, but it still needs one executable-side inventory/HUD anchor before it can decode the runtime weapon list cleanly.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now distinguishes the two checked dump artifacts instead of treating them as one generic memory lead. Current best read is: `binary/Crusader - No Remorse Memdump Weapons.bin` is still VRAM/HUD-side evidence, `binary/Crusader - No Remorse Weapons Main Ram.bin` is plausible main RAM but still not self-identifying enough to close the selected `JL-?` slot without the executable-side id resolver, and `JL-2 AMMO` remains the strongest next normal-lane clue while `JL-9` stays the stronger extra hidden/debug-conditioned lane.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) was normalized after a fresh six-agent `SLUS_002.68` pass so the note now reads as one fact-first summary instead of stacked dated rechecks. The strongest new practical deltas in that pass are: the checked `binary/Crusader - No Remorse Memdump Weapons.bin` artifact is still useful only as PSX VRAM/HUD evidence rather than direct slot RAM, and `JL-2` now becomes the clearer next unknown because `JL-2 AMMO` is present as a direct UI string while `JL-9` remains the stronger extra hidden/debug-conditioned lane.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now records a narrow live `SLUS_002.68` cleanup pass that promoted several previously documented JL-lane globals from raw `DAT_` placeholders to evidence-backed live symbols (`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`, `psx_weapon_spawn_state_selector`) and reclassified the checked `binary/Crusader - No Remorse Memdump Weapons.bin` artifact as a full 1 MiB PSX VRAM dump rather than weapon-slot RAM. The same note now also makes the next pivot explicit: if the extra hidden/debug lane remains `JL-9`, then `JL-2` becomes the next main unknown because `JL-2 AMMO` exists as a direct UI string while no matching plain `JL-9 AMMO` string has yet been recovered.
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes a focused four-lane MCP-only 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 explicit in disassembly: only debug grant performs the post-`0x0c` extra unlock (`0x80030004 -> ...unlock_and_seed_markers(0x0d)`) behind `DAT_8006739d` read at `0x8002fff4`, while the L0SR-linked chain still enters via `0x8003ed28 (DAT_80067454=1)` and `0x80013174`. Current best conclusion remains `JL-9` (`0x80064858`, index `0x0d`) as the extra post-L0SR non-PC lane over `JL-2` (`0x80064832`, index `0x0c`).
+
+Recent verified PSX JL follow-up: [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) now includes an MCP-only row-field consumer trace on live `SLUS_002.68` for `JL-2` (`0x80064832`) versus `JL-9` (`0x80064858`). Current best read is stable and narrower: both rows still share base type-art lane `+0x1c=0x18` into the type-indexed active art-header path (`DAT_800758d8` via object constructors), while `+0x24` diverges (`0x4b` vs `0x0f`) as a transition/state selector lane rather than a direct base-resource pointer. Runtime capture remains required only for exact final frame/resource-token closure.
+
+Recent verified PSX concrete-sample follow-up: [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 pass over the fixed `map 104` `0x0042` sample pack instead of broadening the search surface. The live PSX database now also names `psx_transition_selector_probe_nearby_overlap`, `psx_transition_selector_probe_marker_overlap`, `psx_snapshot_active_object_runtime_rows`, `psx_release_all_active_objects_and_reset_type_runtime_banks`, and a wider root marker/channel helper family. Current best read is tighter in three practical ways: the `64x64` versus `64x40` `0x0042` split still reads 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 at `psx_object_integrate_motion_and_route_visible`, even though the exact live `0x0400` provenance for the fixed 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.
+
+Recent verified PSX concrete-sample follow-up: [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 `map 104` `0x0042` pass grounded directly in exported `runtimeDiagnostic` scene items rather than generic family hypotheses. The live PSX database now names the spawn-side selector bridge `psx_transition_spawn_and_seed_selector_from_record`, the root-family decoder helper `psx_section0_dispatch_root_find_marker_record_by_channel`, the nearby resource-upload helpers `psx_upload_spec_wdl_image_pair_to_vram` and `psx_restore_display_draw_env_after_spec_upload`, the level-loaded policy pointer `psx_type_policy_table_ptr`, and the selector/policy row tables `psx_type_transition_mode_policy_rows` / `psx_type_transition_selector_rows`. Current best read is tighter in four viewer-facing ways: root and constructor section-0 families now have explicit named entry points but still converge through the same shared `0x0042` descriptor row; constructors directly copy the authored lane word into `obj+0x1c`, so exported `initialWord` values are real authored state; the strongest recovered `0x0400` stage-selection write is still nested-state-side rather than a direct object-local `0x0042` writer; and `DAT_800675f8` now reads as a level-loaded per-type policy pointer rather than a per-lane discriminator. The next `map 104` runtime pass should therefore stay on the fixed sample pack (`item:25/30/31/35/85/86`) and sample bound-resource identity plus live frame/state instead of broadening donor heuristics again.
+
+Recent verified PSX exporter follow-up: [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 the viewer-side half of the current `0x0042` narrowing step. The PSX cache builder now emits scene version `psx-runtime-record-probe-v10` with a per-item `runtimeDiagnostic` payload that mirrors the current Ghidra-side channel split: object-local route flags, selector seed/pre-latch hint, exporter-side latched-state candidate, nested-runtime placeholders, resource-kind hints, and a placeholder slot for the live `DAT_800675f8` word. The next `map 104` runtime pass should therefore work against those exported channels directly instead of redefining the channel model again.
+
+Recent verified PSX separate-batch follow-up: [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 focused live `SLUS_002.68` round where six sub-passes covered non-overlapping areas: constructor-placement sample path, root-dispatch/mode gate, pre-latch `0x0042` transition path, anonymous route-bit writer islands, the `DAT_800675f8` policy lane, and the per-type art/cache lane. Current best read is tighter in three practical ways: the art/cache pair now reads as `psx_type_art_active_header_bank` plus `psx_type_art_built_resource_bank`; `psx_type42_transition_selector_tick` now clearly gates on `psx_object_is_within_view_margin` before emitting pre-latch selector `3/4`; and the anonymous island recovery proves wider runtime-state `0x0400` writes and related policy control without yet proving a direct object-local `obj+0x1c |= 0x0400` writer for `0x0042`. The next runtime sample should therefore log object-local flags, nested runtime state, pre-latch selector, latched state word, and bound resource kind as separate channels instead of collapsing them into one route/state summary.
+
+Recent verified PSX static-discriminator follow-up: [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` pass on unresolved `0x0042`. Current best read is tighter in four direct viewer-facing ways: type `0x0042` is now pinned to exact descriptor-table slot `0x80063220 -> 0x800626f8`; type `0x0042` also has a dedicated transition helper `psx_type42_transition_selector_tick` that can dispatch low selectors `3/4` before the `+0x94`-style runtime latch copy; constructors still seed `obj+0x1c` from authored `u5` while the named transition-table path only mutates bit `0x0002`; and `DAT_800675f8` now reads as policy bits for nearby-publication, stage-1 ordering, and semitrans draw policy rather than as the main missing route split. The next useful `0x0042` sample should therefore correlate pre-latch selector dispatch, latched `obj+0x94`, `obj+0x1c` bit `0x0400`, and resource kind on representative map-facing cases instead of widening generic family heuristics again.
+
+Recent verified PSX route/state follow-up: [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` batch around unresolved `0x0042` routing rather than another broad helper sweep. Current best read is tighter in three ways that matter directly to the viewer: constructor and root-dispatch records both hand authored flags into `obj+0x1c`, where `0x0020` stays the broad world-visible gate, `0x0002` stays orientation/extents behavior, and `0x0400` is now the strongest recovered stage-2 selector; `psx_object_select_state_from_transition_table` now exposes a concrete per-type selector source through `DAT_80063b4c` ahead of `psx_object_select_state_script`; and the loader side is narrower because `psx_load_type_state_banks` owns `DAT_800758cc/d0/d4`, `psx_stream_install_type_runtime_banks` is the packed-stream all-bank installer, and `DAT_80067794` now reads as save/transition runtime-header state rather than the missing `0x0042` art-binding lane. The next viewer-facing sample should therefore correlate `type 0x0042` transition-table outputs with representative `map 104` `obj+0x1c` / `obj+0x10` / `obj+0x94` runtime values.
+
+Recent verified PSX six-track consolidation: [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 latest live `SLUS_002.68` Ghidra sweep across descriptor callbacks, interaction/reselection, world-frame wrappers, visible-list ordering, resource submission, and HUD/overlay presentation. Current best read is narrower than before in three practical ways: `0x0042` still shares the generic `0x003e..0x0050` descriptor cluster instead of owning a special descriptor fork, constructor-placement `0x0042` still reads as a compound/main-visible route inside that shared family, and both world-facing draw lanes choose sprite versus image-table submission from the bound resource header kind while the HUD/overlay lane remains a non-map-facing exception. The next viewer-facing recovery step should therefore sample representative `map 104` `0x0042` runtime-bank/state/resource-kind combinations rather than widening donor reuse or looking for a hidden descriptor-table split.
+
+Recent verified PSX interaction/reselection cleanup: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now captures a focused live `SLUS_002.68` pass around `psx_type4_reselect_motion_state` / `psx_object_update_nearby_interactions` and nearby helpers. The PSX database now names `0x80028050 = psx_object_test_strict_nonoverlap_flag8_pair`, `0x800281d4 = psx_object_test_strict_nonoverlap_flag8_subject`, `0x80028700 = psx_object_adjust_param9c_by_view_side`, `0x800287bc = psx_object_update_param9c_from_contact_target`, `0x80028eb4 = psx_object_apply_contact_push_bias`, and `0x8002923c = psx_object_spawn_type11_contact_proxy`, with concise technical comments at each entry. Current best read is that this lane materially mutates runtime interaction state post-spawn (`+0x30..+0x38`, `+0x9c`) and can spawn type-`0x11` contact proxies in-flow, so exporter fallback logic should continue treating it as runtime behavior, not static authored placement metadata.
+
+Recent verified PSX presentation-lane cleanup: [docs/psx/map-rendering.md](docs/psx/map-rendering.md) now captures a focused live `SLUS_002.68` pass around the HUD/overlay neighborhood. The PSX database now names `0x80035cc0 = psx_overlay_slot_create`, `0x80036000 = psx_overlay_slot_release`, `0x80038114 = psx_overlay_slot_step_color_fade`, and `0x800388a8 = psx_hud_overlay_init_resources`, and replaces the earlier generic in-database "verified by subagent pass" note on `psx_draw_hud_overlay_pass` with direct technical call-chain and table-behavior comments. Current best read remains that this helper cluster is a non-map-facing presentation lane layered after world stage-1/stage-2 draw submission; for viewer recovery it should be treated as a false-match guardrail, not as map-art placement evidence.
+
+Recent verified PSX map-viewer 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), [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md), and the new [docs/psx/map-storage-model.md](docs/psx/map-storage-model.md) now record both the latest executable-backed renderer findings and a focused evidence-backed write-up for how PSX level storage differs from the PC build. Current best read is that the cache builder still exports executable-named section-0 visible families (`section0_dispatch_roots`, `section0_constructor_placements`), runtime/state layers for `DAT_800758d8`, `DAT_800758d0`, `DAT_800758cc`, `DAT_800758d4`, and one offline `FUN_8003b00c` decode candidate for `DAT_8006b5d8 -> DAT_8006769c`, but it now also treats large zero-block `DAT_800758d8` constructor-placement bands as inherited-art candidates before falling back to placeholders. That donor-based recovery path moved the built cache from `58,262` fallback items / `1,714` bundle-mapped items down to `25,038` fallback items / `34,938` bundle-mapped items, making maps such as `0`, `9`, and `43` mostly real-art while leaving `map 104` as the clearest remaining outlier. The newest pass does not change the earlier `DAT_800758d4` conclusion: `psx_object_advance_state_script` still sign-extends those three bytes into `obj+0x30/+0x34/+0x38` for overlap/contact-style consumers rather than the draw path, and the unresolved blocker still sits later in the live state-to-resource/frame bridge for the remaining constructor-placement families. Follow-up live MCP passes now also tighten the per-frame interaction/control side in Ghidra: `psx_script_dispatch_audio_event` closes the `0xfffe` script sentinel as an audio/sequence side-effect opcode, `psx_heading16_lookup_unit_vector` closes the heading-token vector table used during target-vector reselection, `psx_authored_record_in_view_bounds` now closes the authored-record cull gate alongside `psx_world_point_in_view_bounds`, the old post-projection `FUN_80027f80` cleanup is now identified as the nearby-interaction active-set lane (`psx_reset_nearby_interaction_list`, `psx_nearby_interaction_list_add`, `psx_nearby_interaction_list_remove`, `psx_update_motion_and_nearby_interactions`) rather than a hidden render-only path, and the adjacent `0x80023c..2b1..` block is now grounded as a control-script lane (`psx_object_run_control_opcode`, `psx_control_move_player_to_point`, `psx_control_move_object_to_point`, `psx_control_wait_ticks`, `psx_control_configure_fixed_camera_anchor`, `psx_control_set_facing_direction`, `psx_queue_deferred_control_command`, `psx_flush_deferred_control_queue`, `psx_apply_deferred_control_command`, `psx_apply_deferred_control_to_dispatch_roots`, `psx_apply_deferred_control_to_live_objects`), with case `8` now narrowed further through direct callee naming as `psx_spawn_object_compound_effect_variant3`. The same pass also refreshes the live PSX naming census from MCP `list_functions`: `1274` local `0x800...` functions with `917` named and `357` still anonymous, for a current local naming floor of `71.98%`.
Recent verified combat-data batch: [docs/combat-dat.md](docs/combat-dat.md) now documents the shipped `COMBAT.DAT` archive end to end. Current best read is that all local Remorse/Regret variants ship the same `14`-record combat-tactic archive, each record contains a `16`-byte name plus four block offsets and bytecode, and the tactic VM is now grounded both in the live `CRUSADER.EXE` helpers (`Attack_SetupForTacticNo`, `Attack_SetupForBlockNo`, `NPC_Get/SetNPCTacticNo`) and in ScummVM's readable Crusader attack-process implementation. The new note also promotes the per-tactic human-readable catalog, including the midpoint-pressure, marker-shuttle, step-out-shoot, and stationary-chaos families.
@@ -32,13 +122,15 @@ Recent verified localized-build batch: [docs/spanish-cheat-differences.md](docs/
Recent startup fixed-map patch batch: [docs/startup-map-patch-file.md](docs/startup-map-patch-file.md) now records the current evidence-backed read of the retail `Using map patch file.` startup line. Current best read is that `Init_Everything` at `1048:039b` prints that line only if `static\fixed.dat` exists, and the later fixed-map loader path treats that file as a preferred alternate `FIXED.DAT` source by loading it into `DAT_1478_1064` and choosing that handle over the base archive handle when present. The safest current wording is therefore `alternate fixed-map archive selected at startup`, not `the -u usecode override` and not a proven per-record merge overlay.
+Recent Remorse NPC class-lift batch: new note [docs/npc-action-process-class-layout.md](docs/npc-action-process-class-layout.md) now records the first owner-first lift of the bounded seg033 NPC AI process family in live `CRUSADER.EXE`. Current best read is that this lane now has real class ownership rather than only flat function names: `Remorse::NPCActionProcess` owns the shared create/destroy/no-op surface, `Remorse::{StandProcess,PaceProcess,SurrenderProcess,GuardProcess,LoiterProcess}` now own their direct create/run/destroy methods, and the guard/loiter-only helper `NPC_DoRandomIdleAnimTwiceIfNotBusy` remains intentionally outside those class owners until stronger single-class ownership evidence appears. The remaining open work in this lane is now datatype and slot semantics, not basic object identity.
+
New roadmap note: [docs/function-knowledge-roadmap.md](docs/function-knowledge-roadmap.md) now turns the current decompilation state into a concrete path toward broad function coverage, with explicit completion criteria, lane priorities, and batch rules. The same note now also records the latest applied process-family rename work: `1100:0437` = `SurrenderProcess_Destroy`, `1100:0913` = `NPC_DoRandomIdleAnimTwiceIfNotBusy`, `1100:0d3e` = `LoiterProcess_VtableSlot10DispatchByShapeIfAlive`, `1100:0fe8` = `PaceProcess_Destroy`, `1100:0f95` = `GuardProcess_Destroy`, `1100:0f47` = `LoiterProcess_Destroy`, `1100:1036` = `StandProcess_Destroy`, `1100:1084` = `NPCActionProcess_RunNoop`, `1100:1089` = `NPCActionProcess_Destroy`, `1100:0fe3` = `NPCActionProcess_VtableSlot10Noop`, `1128:1e14` = `CruHealer_Destroy`, `1128:1fbe` = `BatteryChargerProcess_Destroy`, `1128:22ca` = `DeathSilenceProcess_Destroy`, `1110:0f19` = `PathfinderProcess_Destroy`, `1090:0aaf` = `TeleporterProcess_Destroy`, `1090:0a60` = `EggHatcherProcess_Destroy`, `1020:087e` = `MapJumpProcess_Destroy`, `1028:06bd` = `FadeProcess1_Destroy`, `1030:03cc` = `AnimProcess_Destroy`, `1058:08fc` = `SnapProcess_Destroy`, `10a0:4437` = `ItemProcess_Destroy`, `1138:0819` = `SuperSpriteProcess_Destroy`, `1150:32d3` = `OneFrameDelayProc_Destroy`, `1180:1e0a` = `CameraProcess_Destroy`, `11b8:0293` = `KeyDaemonProcess_Destroy`, `11b8:04c5` = `KeyboardProcess_Destroy`, `11c0:06df` = `AccWaitProcess_Destroy`, `11c0:0748` = `SystemTimerProcess_RunNoop`, `11c0:074d` = `SystemTimerProcess_Destroy`, `11c8:03fd` = `BiosProcess_Destroy`, `13b8:012e` = `CustomWaitProcess_Destroy`, `1430:0363` = `DumbTimerProcess_Destroy`, `1438:0557` = `CycleProcess_Destroy`, `1440:0f67` = `FadeProcAlt_Destroy`, `1468:4322` = `MyTimerProcess_Destroy`, `1468:0494` = `VideoPlayer_Destroy`, `1468:03e3` = `VideoPlayer_InitializePlayback`, `1468:2f7c` = `VideoPlayer_OpenMediaFiles`, `1468:32cb` = `VideoPlayer_AllocPlaybackBuffers`, `1468:3904` = `VideoPlayer_OpenMoviListAndPrimeStreams`, `1468:0483` = `VideoPlayer_StopAndDestroyWrapper`, `1468:431d` = `VideoPlayerProcess_VtableSlot11Noop`, `1468:001a` = `File_Exists`, `1468:03b4` = `VideoPlayer_FormatErrorMessage`, `1468:17b0` = `VideoPlayer_AdvanceChunkCursor`, `1468:1d3d` = `VideoPlayer_AdvanceChunkCursorWrapper`, `1468:1ef7` = `VideoPlayer_LoadAudioChunk`, `1468:1929` = `VideoPlayer_LoadVideoChunk`, `1468:1a92` = `VideoPlayer_BlitDecodedFrame`, `1030:0428` = `GameTimeProcess_Destroy`, `1030:03c7` = `AnimProcess_RunNoop`, `1048:0d3e` = `Process1048_0000_RunNoop`, `1048:0d43` = `Process1048_0000_Destroy`, `1050:051f` = `SavegameSlot_GetLabelPtr`, `1050:0532` = `SavegameSlot_SetLabel`, `1050:057e` = `File_CloseAndMaybeFree`, `10c0:00b9` = `MapJumpProcess_VtableSlot10AdvanceItemFind`, `10e8:4192` = `AnimPrimitiveProcessSomethingElse_Destroy`, `10f8:0120` = `ItemScript_AppendBytes`, `10f8:0161` = `ItemTypeflagRecord_ResetDefaults`, `1150:2f20` = `AnimPrimitiveProcessFamily_VtableSlot11CallSlot3`, `1188:0057` = `Process1188_0000_RunOnTimerDelta`, `1188:0979` = `Process1188_0000_Destroy`, `11c0:0483` = `WaitProcessFamily_VtableSlot10DispatchByPair`, `11c0:0691` = `WaitProcess_Destroy`, `11c0:02bf` = `AccWaitProcess_VtableSlot10DispatchByAnimation`, `1138:0444` = `SpriteProcess_Destroy`, `12e0:0151` = `ASS_StoreInitCallbackState`, `13c8:03f5` = `MainMenu_Destroy`, `13c8:04ee` = `MainMenu_DrawCornerDecorations`, `13c8:06a4` = `MainMenu_HandleButtonClick`, `13c8:06cd` = `MainMenu_HandleKey`, `13c8:082e` = `MainMenu_ActivateSelection`, `13c8:0ce7` = `MainMenuOptionButtonGump_Create`, `13c8:0dc0` = `MainMenuOptionButtonGump_HandlePointerEvent`, `13c8:0e2d` = `MainMenuOptionButtonGump_SelectPeer`, `13c8:0e94` = `MainMenuOptionButtonGump_Draw`, `13c8:0ece` = `MainMenuOptionsPanel_Create`, `13d0:0000` = `SavegameNameField_MapInputChar`, `13d0:0226` = `SavegameMenu_Destroy`, `13d0:02cb` = `SavegameMenu_HandleKey`, `13d0:03dd` = `SavegameMenu_HandleSlotAction`, `13d0:058c` = `SavegameSlot_DrawCornerDecorations`, `13d0:074e` = `SavegameSlotGump_Create`, `13d0:0841` = `SavegameSlotGump_Destroy`, `13d0:08a8` = `SavegameNameField_HandleKey`, `13d0:0b0a` = `SavegameSlot_HandleClick`, `13d0:0b89` = `SavegameSlot_BeginEditOrActivate`, `13d0:0cd5` = `SavegameNameField_Draw`, `13d0:0e18` = `SavegameSlot_Select`, `11c8:03c9` = `BiosProcess_VtableSlot10DosRealFarCall`, `1108:2259` = `AttackProcess_VtableSlot10DispatchByClip`, `1030:0183` = `AnimProcess_VtableSlot10DispatchByPort`, `1300:0d4e` = `BaseCameraProcess_VtableSlot10SetViewportRect`, `1300:0d76` = `BaseCameraProcess_VtableSlot11FreeBuffer`, `13b8:021a` = `CustomWaitProcess_VtableSlot11ArmAndRun`, `1440:03a0` = `FadeProcess2_VtableSlot10BlendTowardTargetPalette`, `1448:08fd` = `FlicPlayProcess_Destroy`, `1448:3290` = `FlicWaitProcess_Destroy`, `11f8:00a4` = `MusicPlayerProcess_RunNoop`, `11f8:035e` = `MusicPlayerProcess_Destroy`, `11f8:028e` = `Music_RestorePreviousTrackFromStack`, `11f8:02bf` = `Music_LoadStateAndReplayCurrentTrack`, `11f8:0311` = `Music_SaveState`, `12e0:0267` = `AssProcess_Destroy`, `1448:00eb` = `FlicWaitProcess_VtableSlot10TickAndMaybeAdvance`, `11f8:00a9` = `MusicPlayerProcess_VtableSlot10Noop`, `12e0:00ed` = `AssProcess_VtableSlot5ClearCreatedFlag`, `12e0:0105` = `AssProcess_VtableSlot6SetCreatedFlag`, `1020:08cd` = `Process_VtableSlot4Noop`, `1020:08d2` = `Process_VtableSlot8Noop`, `1028:0724` = `Process_VtableSlot9ReturnZero`, `1468:0114` = `MyTimerProcess_VtableSlot10IncrementCounterOnTick`, `11f0:02b9` = `StdIntHandlerProcess_Destroy`, `12f8:0530` = `GumpShared_DestroyNoop`, `12f8:0544` = `KeyboardInputHandler_DestroyNoop`, `12f8:0553` = `GumpShared_VtableSlot10Noop`, `12f8:0578` = `KeyboardInputHandler_VtableSlot10Noop`, `12f8:057d` = `KeyboardInputHandler_VtableSlot11Noop`, `1308:0616` = `ButtonGump_Destroy`, `13c0:04ee` = `KeypadGump_Destroy`, `13c0:0a94` = `KeypadButtonGump_Destroy`, `13e8:3aae` = `HelpGump_Destroy`, `13e8:3ba5` = `HelpGump_RefreshPage`, `13e8:3d53` = `HelpGump_HandleAdvanceAction`, `13e8:3d99` = `HelpGump_HandleNavigationKey`, `13e8:3ec8` = `HelpGump_RunAmbientSfxTick`, `13e8:3fd8` = `RunCreditsProcess_Destroy`, `13f8:01e9` = `QuickSaveLoadExitGump_Destroy`, `13f8:0510` = `Gump13f80383_Destroy`, `13f8:058c` = `Gump13f80383_Draw`, `13f8:05c6` = `Gump13f80383_VtableSlot10Noop`, and `13f8:05cb` = `Gump13f80383_VtableSlot11Noop`. That work is now live in Ghidra, and it also confirms the practical write-path rule for future work: read-only MCP analysis can stay on the live GUI session, while live write-capable scripts can land small verified rename/comment batches when the simpler edit-plan route refuses to commit.
Latest broad-sweep correction batch: several previously over-specific gump no-op names were generalized after direct table evidence showed they are shared across multiple gump families, not keyboard-only handlers. The live database now carries `12f8:0535 = GumpShared_VtableSlot3Noop`, `12f8:0544 = GumpShared_VtableSlot7Noop`, `12f8:0549 = GumpShared_VtableSlot8Noop`, `12f8:054e = GumpShared_VtableSlot9Noop`, `12f8:0578 = GumpShared_VtableSlot16Noop`, and `12f8:057d = GumpShared_VtableSlot17Noop` from direct `g_helpGumpFnPtr` / `g_gump13f80383FnPtr` slot reuse at `1478:6241` and `1478:6346`.
Latest broad-sweep UI follow-up: the same UI-heavy lane is now tighter in three more local families without requiring deeper subsystem claims. The live database now carries `12f8:02e4 = GumpShared_DestroyCommon`, `13f8:0237 = QuickSaveLoadExitGump_HandleChildButtonEvent`, `13f8:0299 = QuickSaveLoadExitGump_HandleKey`, `13f8:0349 = QuickSaveLoadExitGump_DrawLabel`, `13f8:0383 = QuickSaveLoadExitGump_Create`, `13c8:2f37 = MainMenuOptionsPanelButtonGump_Create`, `13c8:2fca = MainMenuOptionsPanelButtonGump_DrawLabel`, `13c8:3004 = MainMenuOptionsPanelButtonGump_Select`, `13c8:3030 = MainMenuOptionsPanelButtonGump_Deselect`, `13c8:1759 = MainMenuOptionsMenu_Destroy`, `13c8:17c5 = MainMenuOptionsMenu_Create`, `13c8:1e62 = MainMenuOptionsMenu_GetOptionRect`, `13c8:2975 = MainMenuOptionsMenu_HandleChildButtonEvent`, `13c8:29b3 = MainMenuOptionsMenu_HandleKey`, `13c8:2b16 = MainMenuOptionsMenu_DrawTitle`, and `13c8:2c56 = MainMenuOptionsMenuButtonGump_DrawLabel`. Current best read is that `12f8:02e4` is the shared gump base destroy path used by multiple UI families, the `13f8:` mini-cluster is the quick save/load/exit modal's constructor-plus-local input/draw surface, the `13c8:2f37..3030` cluster is the options-panel-specific button wrapper layered over the generic `1308:` button-gump create path, and the separate `13c8:1759..2c56` lane is now clearly the main options-menu create/destroy/input surface because its teardown path saves the current options back to config before the shared gump cleanup.
-Recent retail debugger-entry follow-up: [docs/retail-debugger-entry-options.md](docs/retail-debugger-entry-options.md) now consolidates the hidden-debugger entry question with the newer live Ghidra evidence instead of leaving it split across the older `-debug` and patch-attempt notes. Current best read is now tighter in three ways: first, fresh data-use recovery still finds reads but no writer for the debugger-state global at `1478:659c/659e`; second, fresh decompiles of `usecode_debugger_open_for_current_unit`, `usecode_debugger_open_modal`, `usecode_debugger_gump_create`, and `usecode_debugger_handle_event` confirm that the debugger UI and event bundle are real but only meaningful after a valid break-state object/gump already exists; and third, the retail `-u` override remains the lowest-risk non-EXE experiment surface but still does not currently show a script-visible way to construct the seg1408 break-state object or write the required global pointer. The resulting priority order is now explicit: prefer a focused No Regret / JP No Remorse bootstrap comparison first, keep `-u` plus a replacement `EUSECODE.FLX` as the least invasive indirect experiment surface second, and treat the current interpreter-callsite-retarget patch family as the smallest structurally defensible retail EXE path only if cross-build comparison fails to reveal a smaller missing bootstrap.
+Recent retail debugger-entry follow-up: [docs/retail-debugger-entry-options.md](docs/retail-debugger-entry-options.md) now consolidates the hidden-debugger entry question with the newer live Ghidra evidence instead of leaving it split across the older `-debug` and patch-attempt notes. Current best read is now tighter in seven ways: first, fresh data-use recovery still finds reads but no writer for the debugger-state global at `1478:659c/659e`; second, fresh decompiles of `usecode_debugger_open_for_current_unit`, `usecode_debugger_open_modal`, `usecode_debugger_gump_create`, and `usecode_debugger_handle_event` confirm that the debugger UI and event bundle are real but only meaningful after a valid break-state object/gump already exists; third, the retail seg109 naming backlog is now partly landed live in `CRUSADER.EXE`; fourth, the follow-up live pass sharpened the retail child-pane split by identifying `13a0:16ee/1791/193f` as a watch-pane create/draw/click trio and `13a0:0ae8` as the source-pane line-state initializer from break-state current-line data; fifth, the latest MCP decompiles now make retail's surviving debugger surface explicit once the gump exists, including real file-open, run/step, go-to-line, watch, inspect, change-global, search, breakpoint-display, and pointer-to-source navigation logic; sixth, the same pass now closes the retail source-buffer lane far enough to describe the real file-ingestion path end to end, from source-buffer allocation/open through whole-file text load, in-place line splitting, and shared line-pointer lookup; and seventh, the decisive retail gap versus Regret remains unchanged and now better evidenced live: no caller to `1408:0000 Create`, no caller to the two open wrappers, and retail callback slot `0` still lands on the no-op `1408:046f` stub instead of a live frontend callback.
Recent No Regret debugger follow-up: [docs/regret-hidden-debugger-investigation.md](docs/regret-hidden-debugger-investigation.md) now also records the debugger-side cleanup pass after the first source-loader/runtime split, the final deep caller-recovery closure in the upstream Regret VM lane, and the first practical seeding model. The live `REGRET.EXE` database now has names and comments not just for the breakpoint/current-entry helpers, but also for the source-pane constructor/pointer/draw/viewport methods, the full source-buffer create/load/split/destroy chain, the interpreter saved-farptr helpers at `13f0:0000/003c`, the interpreter-context create/init pair at `13f0:00e8/0244`, and the shared slot-chunk accessor at `13f8:1d72`. The practical model is tighter in four ways: first, the broader `usecode_debugger_handle_event` map now shows explicit line-search, goto-line, and breakpoint-clear interactions over that loaded source buffer; second, the compiled-usecode question is no longer ambiguous, because retail Remorse already carries parsed `LINE_NUMBER` ops while Regret currently does not; third, the remaining debugger-seeding uncertainty is now narrowly bounded inside the already-identified interpreter dispatcher path rather than in some still-unmapped Regret-side subsystem; fourth, the seeding record itself now reads as a serialization of existing live interpreter pointers, which means a future bring-up can probably reuse in-process VM state with a small patch instead of depending on any hypothetical external preprocessor. It would improve source correlation, but even a future Regret-focused line-number injector would still not replace the missing interpreter-seeded current-entry stack needed for stable `RUN` / step behavior.
@@ -127,6 +219,7 @@ Latest F7 overlay follow-up: new note [docs/f7-overlays.md](docs/f7-overlays.md)
| [docs/entity-class-family-split.md](docs/entity-class-family-split.md) | Focused working note on the large seg001 `Entity` lane: shared base-layout evidence, conservative split into projectile, debris, corpse/remnant, and adjacent non-entity families, and the recommended promotion order for later class lifting |
| [docs/entity-dispatch-entry-class-layout.md](docs/entity-dispatch-entry-class-layout.md) | Focused working note for the `EntityDispatchEntry` family: base versus derived split, stable field groups, constructor and release surfaces, candidate method map, and conservative future Ghidra modeling order |
| [docs/entity-vm-runtime-owner-resource-layout.md](docs/entity-vm-runtime-owner-resource-layout.md) | Focused working note for the VM runtime lane: `EntityVmRuntime`, `EntityVmOwnerResource`, and `EntityVmContext` ownership, stable layout claims, masked-create helpers, and the safest current class-lift order |
+| [docs/npc-action-process-class-layout.md](docs/npc-action-process-class-layout.md) | Focused working note for the seg033 NPC AI process family: current owner-first class lift, direct per-family create/run/destroy ownership, the shared guard/loiter idle helper, and the remaining slot/datatype gaps |
| [docs/presentation-callback-broker-layout.md](docs/presentation-callback-broker-layout.md) | Focused working note for the `0x4588` callback-object lane: install/teardown lifecycle, global state cluster, provisional vtable slots, payload-pair evidence, and conservative class-lift guidance |
| [docs/map_renderer/trigger-usecode-links.md](docs/map_renderer/trigger-usecode-links.md) | Evidence-backed map-viewer note for editor/controller shapes that now expose direct USECODE navigation, including the stable class/event targets and the special `TRIGGER.slot_20` handling for `0x04B1` cmd helpers |
| [docs/map_1_spawners_targeted_investigation.md](docs/map_1_spawners_targeted_investigation.md) | Focused map-1 note on suspicious `0x04D0` frame-paired spawners: decompressed-cache examples, the recovered `MONSTER -> ITEM.slot_2D -> create NPC` chain, QLo-based pairing, and the corrected `mapNum bit 0x08` enter-area interpretation |
@@ -135,6 +228,7 @@ Latest F7 overlay follow-up: new note [docs/f7-overlays.md](docs/f7-overlays.md)
| [docs/remorse-rebuild-abi-notes.md](docs/remorse-rebuild-abi-notes.md) | Working note for rebuild constraints: segmented-memory model, far-call provenance, runtime/toolchain evidence, ABI guardrails, and the split between original-style executable reconstruction and a behaviorally equivalent port |
| [docs/command-line-parameters.md](docs/command-line-parameters.md) | Consolidated startup/debug argument reference for the retail Crusader executables: live retail `-u` usecode override, the current `-setver` caution, `-debug`, `-asylum`, `-warp`, `-skill`, `-mapoff`, `-egg`, `-demo`, the `-laurie` cross-reference, and the evidence-backed direct-coordinate warp syntax/limits |
| [docs/psx/psx.md](docs/psx/psx.md) | PlayStation `SLUS_002.68` and disc-resource note: boot/load layout, `LSET`/menu WDL structure, executable-backed map inventory, passcode alphabet/display path, recovered PSX ammo/item/weapon tables, and current unresolved enemy/password-compare gaps |
+| [docs/psx/jl-9-investigation.md](docs/psx/jl-9-investigation.md) | Focused PSX `SLUS_002.68` note on the `JL-9` weapon: recovered row/index evidence, the hidden transformed passcode branch that sets the debug gate, the bulk weapon-unlock helper's extra late channel, current art/level-placement status, and the remaining proof gaps |
| [docs/psx/map-rendering.md](docs/psx/map-rendering.md) | Detailed PlayStation map-rendering architecture note: `LSET*.WDL` storage model, constructor record layouts, runtime banks, state/variant/art bridge, stage-1 versus stage-2 render lanes, visible-list sorting, final primitive submission, and the current best recipe for reconstructing PSX maps in the viewer |
| [docs/psx/art-binding-recovery.md](docs/psx/art-binding-recovery.md) | Focused PSX viewer recovery note: extraction-versus-render diagnosis, zero-block `DAT_800758d8` constructor-family donor heuristics, measured fallback reduction, representative map stats, and the remaining outlier families/maps |
| [docs/psx/prealpha.md](docs/psx/prealpha.md) | PlayStation pre-pre alpha `/psx/prealpha/SLUS_002.68` comparison note: reduced disc inventory, retained retail-style `LSET` loader, surviving No Remorse branding, stale `TALK1.XA` and `LoadExec` leftovers, and the current read that this build is closer to an unfinished No Remorse PSX branch than to a visibly rebranded sequel executable |
diff --git a/crusader_segment_coverage_ledger.csv b/crusader_segment_coverage_ledger.csv
index 0d9a02f..1fa776e 100644
--- a/crusader_segment_coverage_ledger.csv
+++ b/crusader_segment_coverage_ledger.csv
@@ -31,7 +31,7 @@
"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"
"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"
"35","code","0x69400","0xF67","None","","","","crusader_ne_segments.csv"
"36","code","0x6A600","0x69F","None","","","","crusader_ne_segments.csv"
diff --git a/docs/npc-action-process-class-layout.md b/docs/npc-action-process-class-layout.md
new file mode 100644
index 0000000..351d9ce
--- /dev/null
+++ b/docs/npc-action-process-class-layout.md
@@ -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
\ No newline at end of file
diff --git a/docs/psx/art-binding-recovery.md b/docs/psx/art-binding-recovery.md
index 606f055..971a941 100644
--- a/docs/psx/art-binding-recovery.md
+++ b/docs/psx/art-binding-recovery.md
@@ -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.
+## 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
- 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.
- `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
- 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.
- 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
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?
diff --git a/docs/psx/jl-9-in-level-event.md b/docs/psx/jl-9-in-level-event.md
new file mode 100644
index 0000000..f7276ff
--- /dev/null
+++ b/docs/psx/jl-9-in-level-event.md
@@ -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`)
\ No newline at end of file
diff --git a/docs/psx/jl-9-investigation.md b/docs/psx/jl-9-investigation.md
new file mode 100644
index 0000000..c43dc9b
--- /dev/null
+++ b/docs/psx/jl-9-investigation.md
@@ -0,0 +1,2459 @@
+# PSX JL-9 / Hidden Passcode Investigation
+
+This note is the compact fact-first summary for the active PlayStation `SLUS_002.68` investigation around `JL-2`, `JL-9`, hidden passcodes, the extra post-cheat weapon lane, the checked VRAM dump, and the checked 2 MiB main RAM dump.
+
+Natural in-level gate-arm event work now has its own companion note: [docs/psx/jl-9-in-level-event.md](k:\ghidra\Crusader_Decomp\docs\psx\jl-9-in-level-event.md).
+
+## Executive summary
+
+## Authored producer reachability correction pass (2026-04-12 live MCP)
+
+This pass pushed farther on the authored producer side across
+`psx_load_type_state_banks -> psx_object_create_simple_record -> psx_run_object_behavior_program_tick -> psx_object_behavior_opcode_dispatch -> psx_behavior_subopcode_dispatch -> psx_level_gate_slot_dispatch_from_action_record`.
+
+### Strongest producer-context update
+
+1. Loader/constructor provenance remains closed:
+ - `psx_load_type_state_banks` (`0x8003917c`) installs `psx_type_simple_component_bank[type]` from level/LSET payload.
+ - `psx_object_create_simple_record` (`0x800249f4`) seeds component `program_base/pc` from that bank.
+2. Behavior tick guard was revalidated at instruction level:
+ - `0x80026710` checks `(record_word0 - 1) < 0x0a`, which is arg-count bound.
+ - opcode is loaded separately from `record_word1` (`lw a0,0x4(a3)`), so opcode `54` is not excluded by this guard.
+3. Subdispatch linkage is therefore viable as authored-program context (not guard-blocked):
+ - opcode table entry `54` at `0x80064284` points to `psx_behavior_subopcode_dispatch` (`0x80027ecc`),
+ - subop table entry `49` at `0x800636d4` points to `psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`).
+
+### Best current candidate producer context
+
+Strongest current candidate is still the **type-state behavior program record lane**:
+
+1. simple-record object instance in late final-mission family (`map_id_to_gate_slot == 0x0f`)
+2. component program emits opcode `54`
+3. subop `49` frame resolves to sink `0x800214ac`
+4. sink record bytes resolve to tuple `(slot,arg1,arg2)=(0x0f,0x0a,0x04)`
+
+This is now stronger than the old "high opcode not proven active" read, because that old read depended on misinterpreting `0x80026710` as opcode clamp.
+
+### Authored-static vs runtime-remapped read (updated)
+
+Current strongest classification remains:
+
+1. **authored-static source lane** at the behavior program record level,
+2. with **optional runtime remap transport** via arg-mask/index resolution in `psx_object_behavior_opcode_dispatch`.
+
+Reason: arg resolver can pass direct pointers to record words (static bytes) or map arg words through slot-index tables (`base + index*4`) when mask bits are set. Exact mask choice of the emitting shipped record remains open.
+
+### Live Ghidra changes applied in this pass
+
+Decompiler comments updated:
+
+- `0x80026710` (arg-count guard correction; opcode still independent)
+- `0x8002685c` (opcode table index semantics + entry 54 viability)
+- `0x80027f0c` (subop dispatch viability depends on authored records, not guard exclusion)
+
+No renames were applied in this pass.
+
+## Synthesis-only conservative-stabilization pass (2026-04-12 live MCP)
+
+This pass was synthesis-only: no new lane expansion, only decision cleanup on what to stabilize in live naming/comments from the callback/countdown/progression clue set.
+
+### Updated concise natural-event synthesis
+
+1. Natural JL-9 arm remains a timing-sensitive in-level event lane, not a passcode-decoder-only effect.
+2. `0x8002745c` and `0x80027548` remain indirect callback-table targets (no recovered direct callers), so they are transition/progression context, not standalone proven tuple emitters.
+3. `0x80020794` remains world-frame countdown/control flow with map-54 boundary split (`<=54 -> 0x1a`, `>54 -> 0x1b`) and no recovered direct edge into slot-dispatch sink `0x800214ac`.
+4. Best current miss model is unchanged: late transition/progression can consume the active window before slot-`0x0f` tuple `(0x0a,0x04)` reaches the arm write.
+
+### Live Ghidra changes applied in this pass
+
+Decompiler comments were normalized to conservative evidence wording:
+
+- `0x8002745c` (`psx_control_callback_apply_level_preset_or_resume`): indirect callback-table target + branch split; explicitly marked not a standalone proven direct slot-`0x0f` feeder.
+- `0x80027548` (`psx_control_callback_apply_progression_target_level`): indirect callback-table target + progression-state timing lane; explicitly marked as non-proof of tuple emission by itself.
+- `0x80020794` (`psx_control_event_countdown_transition_tick`): world-frame countdown/control lane + explicit no-direct-edge statement to sink `0x800214ac`.
+
+No new renames were applied in this pass.
+
+### Names intentionally left conservative
+
+1. No tuple-specific rename for `0x8002745c` (insufficient direct feeder proof).
+2. No deterministic gate-arm rename for `0x80027548` (supports timing influence, not guaranteed tuple production).
+3. `0x800230e4` remains `psx_control_event_slot0f_handler` (slot-family scope retained; no narrowing to a single branch writer name).
+
+## Progression latch timing pass (2026-04-12 live MCP)
+
+This pass revisited `psx_control_callback_apply_progression_target_level` (`0x80027548`) to decide whether map `54 -> 55` progression can preempt natural slot-`0x0f` tuple arm `(0x0a,0x04)`.
+
+### Strongest new timing implication
+
+1. `0x80027548` reads `psx_map_progression_table[current_map_id]` and writes it through `psx_level_session_set_next_map_id` (`0x8002ba84`, renamed this pass).
+2. `psx_level_session_set_next_map_id` only stages deferred next-map latch `DAT_800678d0`; it does not immediately change `current_map_id`.
+3. `psx_level_session_loop` commits `current_map_id <- DAT_800678d0` at rollover (`0x80031edc`) after inner world-frame loop exit.
+4. Practical consequence: this is not a same-tick overwrite race, but it still creates a preemption window if transition logic exits the map-54 loop before slot-`0x0f` tuple branch `(0x0a,0x04)` is emitted.
+
+### 54->55 miss-window read (updated)
+
+1. `54 -> 55` remains the most likely natural miss window.
+2. The key mechanism is deferred rollover ordering, not direct tuple clobber.
+3. Countdown/control map split at `0x800208f0` (`<=54 -> 0x1a`, `>54 -> 0x1b`) plus staged progression target explains why natural tuple opportunity can disappear at the boundary.
+
+### Live Ghidra artifacts applied in this pass
+
+Renames:
+
+- `0x8002ba84` -> `psx_level_session_set_next_map_id`
+
+Decompiler comments:
+
+- `0x80027560`
+- `0x8002ba84`
+- `0x80031edc`
+- `0x800208f0`
+
+## Countdown-vs-slot frame-order closure pass (2026-04-12 live MCP)
+
+This pass targeted one question only: whether the map-54 countdown split around `0x800208f0` can plausibly suppress or precede natural slot-`0x0f` arm tuple `(0x0a,0x04)`.
+
+### Strongest causality clue recovered
+
+Recovered world-frame order is now explicit in disassembly/decompile:
+
+1. `0x8002b830`: `psx_control_event_countdown_transition_tick`
+2. `0x8002b844`: `psx_run_live_object_behavior_callbacks`
+3. `0x8002b864`: `psx_update_motion_and_nearby_interactions`
+4. `0x8002b86c`: `psx_flush_deferred_control_queue`
+
+So countdown runs earlier in frame than the later behavior/deferred-control lanes that can eventually reach slot dispatch and slot-`0x0f` arm sink `0x800232f0`.
+
+### Map-54 split relevance and branch read
+
+1. At `0x800208f0`, countdown terminal handoff selects mode by map boundary:
+ - `current_map_id <= 54` -> `0x1a`
+ - `current_map_id > 54` -> `0x1b`
+2. At `0x80020900`, countdown immediately applies `psx_control_set_mode_and_reset_runtime_flags`, writing shared control globals (`DAT_800675e4`, `DAT_80067379`).
+3. Slot-`0x0f` arm branch `(0x0a,0x04)` in `psx_control_event_slot0f_handler` also writes the same control-mode family before/with gate write.
+
+### Updated relevance ranking
+
+1. **Boundary timing is most relevant** (strongest): early countdown split+mode write can narrow later slot-event windows in the same/next frame.
+2. **Failure-side suppression is plausible but secondary**: no direct countdown->slot call edge was recovered, but shared mode-state writes can still preempt tuple opportunity.
+3. **Pure countdown-success branch is weakest**: no recovered success-path edge deterministically forcing tuple `(0x0f,0x0a,0x04)`.
+
+### Live Ghidra artifacts applied in this pass
+
+No renames in this pass.
+
+Decompiler comments added:
+
+- `0x8002b830`: frame-order anchor (countdown before behavior/deferred lanes)
+- `0x80020900`: boundary handoff writes shared mode/state before later slot lanes
+- `0x800232f0`: slot-`0x0f` arm sink is later lane than countdown tick
+
+## Slot-0x0f subcase taxonomy pass (2026-04-12 live MCP)
+
+This pass classified `psx_control_event_slot0f_handler` (`0x800230e4`) subcases beyond tuple `(0x0a,0x04)` to narrow natural JL-9 semantics without overfitting a single folklore path.
+
+### Compact taxonomy (slot `0x0f` family)
+
+1. `param2=0x0a, param3=0x01/0x02/0x03`
+ - **Transition/objective-state setup** (not direct reward grant).
+ - Evidence: each branch writes shared control mode/timer state (`DAT_80067340/44`), plays a CD-XA cue (`0x1a/0x1b/0x1d` via `0x80049014`), and clears per-event runtime latches.
+ - `param3=0x03` additionally sets `DAT_80067354=1`.
+
+2. `param2=0x0a, param3=0x2e`
+ - **Opcode-stream transition/reset** lane.
+ - Evidence: switches opcode stream index (`psx_control_assign_opcode_stream_by_index(...,1)`) and performs the same latch-clear pattern.
+
+3. `param2=0x01, param3=0x01`
+ - **Countdown/objective-pressure arm** lane.
+ - Evidence: writes `DAT_8006734d/4e/4f`, which are read by `psx_control_event_countdown_transition_tick` and `psx_draw_clock_digits_overlay`.
+
+4. `param2=0x04, param3=0x01`
+ - **Per-level message/objective-notice** lane.
+ - Evidence: loads text from `DAT_8006754c + index`, sets timer/control globals, and drives shared countdown-style object iteration side effects.
+
+5. `param2=0x06, param3=0x42`
+ - **Type-gated control transition** lane.
+ - Evidence: writes only global mode latches (`DAT_800675e4`, `DAT_80067379`, `DAT_80067350`) and reset helpers; no constructor/resource-bind call.
+
+6. `param2=0x0a, param3=0x04` (anchor case)
+ - **Eligibility arm latch** for hidden follow-up, not immediate reward.
+ - Evidence: only this subcase writes `psx_debug_extra_channel_gate` (`0x800232f0`) under `psx_hidden_passcode_flag==0 && psx_level_runtime_header_state==3`.
+
+### Implication for `(0x0a,0x04)`
+
+The surrounding slot-`0x0f` family is dominated by transition/countdown/objective-state control lanes and messaging, not direct reward payout handlers. That makes `(0x0a,0x04)` best interpreted as a **late objective-state eligibility arm** inside that control family, rather than a standalone reward/failure terminal event.
+
+### Live Ghidra artifacts applied in this pass
+
+Renames:
+
+- `0x80049014` -> `psx_audio_cdxa_select_and_play_cue`
+
+Decompiler comments added:
+
+- `0x80023154` (slot0f `0x0a/1` transition setup)
+- `0x800231b0` (slot0f `0x0a/2` countdown/objective-state setup)
+- `0x8002321c` (slot0f `0x0a/3` sibling objective-state branch)
+- `0x80023334` (slot0f `0x0a/0x2e` opcode-stream reset lane)
+- `0x80023390` (slot0f `0x01/1` countdown-overlay/objective-pressure lane)
+- `0x800236d8` (slot0f `0x04/1` level-message objective cue lane)
+- `0x800237cc` (slot0f `0x06/0x42` type-gated control transition lane)
+
+## Transition callback provenance pass (2026-04-11 live MCP)
+
+This pass traced the newly recovered transition callbacks at `0x8002745c` and `0x80027548` to determine provenance, state effects, and impact on the slot-`0x0f` tuple lane.
+
+### Provenance (best current)
+
+1. Neither callback has a recovered direct caller xref (`get_callers` returns none for both entries).
+2. Both are data-referenced from a contiguous function-pointer region near `0x800641f0..0x80064220`:
+ - `0x80064200 -> 0x8002745c`
+ - `0x80064210 -> 0x80027548`
+3. Current best classification is therefore **indirect callback-table dispatch** from an unresolved transition/control dispatcher, not direct static callsites.
+
+### State effects recovered
+
+1. `0x8002745c` (`psx_control_callback_apply_level_preset_or_resume`):
+ - if `DAT_80078a14 == 0`, it calls `psx_control_event_apply_level_channel_preset` (`0x80020f7c`) then `psx_control_reset_runtime_flag_67780`.
+ - else it takes an alternate resume lane (`FUN_80044074`) and sets `DAT_800673c4 = 1`.
+2. `0x80027548` (`psx_control_callback_apply_progression_target_level`):
+ - computes `next = psx_map_progression_table[current_map_id]` and applies it via `FUN_8002ba84`.
+
+### Slot-0x0f tuple carry/suppress read
+
+1. `0x8002745c` can **carry** tuple opportunity when it takes the `DAT_80078a14 == 0` branch because that branch executes `0x80020f7c` (the known level/channel apply helper in this late control lane).
+2. The alternate branch in `0x8002745c` can **suppress or defer** tuple opportunity for that tick because it skips `0x80020f7c` and takes resume/state handling instead.
+3. `0x80027548` increases **timing sensitivity**: it advances map progression before later control-event handling, so slot-`0x0f` tuple `(0x0a,0x04)` only carries if emitted after/within the progressed state window; otherwise progression can outrun the arm branch.
+4. For map `54 -> 55`, this is not a simple family exit (both are still slot-`0x0f` family), but it is a real state-transition step that can change event ordering and reduce deterministic tuple timing.
+
+### Live Ghidra artifacts applied in this pass
+
+Comments added:
+
+- `0x8002745c`: table-dispatch provenance + carry/suppress branch semantics.
+- `0x80027548`: progression callback provenance + timing-sensitive tuple implication.
+- `0x80064200`: callback-table-region note for `0x8002745c` entry.
+- `0x80064210`: callback-table-region note for `0x80027548` entry.
+
+No additional renames were applied in this pass (existing callback names remained conservative and evidence-backed).
+
+## Multi-map last-mission chain check (2026-04-11 live MCP)
+
+This pass tested the specific hunch that the last mission is split across multiple maps and that this split explains why the JL-9 gate-arm family is level-grouped (`54/55/56/57/58`) instead of single-map.
+
+### Fresh table evidence (live bytes)
+
+Recovered directly from active `SLUS_002.68` memory tables:
+
+1. Selector anchor into the family:
+ - `psx_selector_to_map_id_table[0x0f]` at `0x80063e63` is `0x36` (map `54`).
+2. Gate-slot family closure for the target chain:
+ - `psx_map_id_to_gate_slot_table[54..58]` at `0x80063e9e..0x80063ea2` is `0x0f,0x0f,0x0f,0x0f,0x0f`.
+3. Progression chain closure:
+ - `psx_map_progression_table[54..58]` at `0x80063ee2..0x80063ee6` is `0x37,0x38,0x39,0x3a,0xff` (`55,56,57,58,terminal`).
+
+Net result: this is a concrete contiguous progression chain that stays inside one gate-slot family (`0x0f`) until terminal progression marker `0xff`.
+
+### Control/transition helper evidence for split-flow behavior
+
+1. `psx_control_event_countdown_transition_tick` (`0x80020794`) has an explicit map boundary at `0x800208f0`:
+ - `current_map_id <= 54` uses mode `0x1a`
+ - `current_map_id > 54` uses mode `0x1b`
+2. `psx_control_event_slot0e_handler` has a late feeder branch at `0x80023074` that writes selector `0x0f` then calls `psx_control_event_apply_level_channel_preset`.
+3. `psx_control_event_apply_level_channel_preset` reads both:
+ - `psx_map_progression_table[current_map_id]` (`0x80020fa4`)
+ - `psx_map_id_to_gate_slot_table[current_map_id]` (`0x80020fbc`)
+
+This supports a staged final-mission flow where late transition/control logic can traverse `54..58` while remaining in the same slot-`0x0f` gate family.
+
+### Best reconstruction (current)
+
+1. Normal passcode selector path anchors entry at map `54` (`selector 0x0f -> map 54`).
+2. Late control/transition logic advances through progression chain `54 -> 55 -> 56 -> 57 -> 58`.
+3. Across that chain, gate dispatch stays in slot family `0x0f`.
+4. Terminal progression marker at map `58` (`0xff`) indicates end-of-chain behavior rather than another ordinary progression handoff.
+
+### JL-9 implication update
+
+This strengthens, not weakens, the `level 54` natural-host read:
+
+1. `54` remains the only member with direct selector anchor from the recovered normal passcode path.
+2. `55..58` are now better interpreted as downstream split-phase transition maps in the same family.
+3. The family-wide slot-`0x0f` mapping now looks like deliberate multi-map mission staging, which explains why JL-9 gate logic is host-family scoped while still leaving `54` as the strongest reproducible natural entry.
+
+### Live Ghidra artifacts applied in this pass
+
+Disassembly comments added:
+
+- `0x80063e63` (selector `0x0f` -> map `54` anchor)
+- `0x80063e9e` (map `54` slot-`0x0f` family start)
+- `0x80063ea2` (map `58` slot-`0x0f` terminal-edge note)
+- `0x80063ee2` (progression `54 -> 55` chain start)
+- `0x80063ee6` (progression `58 -> 0xff` terminal marker)
+- `0x800208f0` (map-54 boundary split `0x1a/0x1b`)
+- `0x80023074` (slot-`0x0e` late feeder back into selector `0x0f` family)
+- `0x80020fa4` (progression-table read and `54..58` staged flow note)
+
+## Level-54 boundary clue pass (2026-04-11 live MCP)
+
+This pass focused on one narrow question: why natural `MFM4` can still miss even when map `54` remains the best host anchor.
+
+### Strongest level-54-specific clue
+
+`psx_control_event_countdown_transition_tick` (`0x80020794`) has an explicit map boundary at `0x36` (54):
+
+- at terminal countdown handoff, it chooses control mode code `0x1a` when `current_map_id <= 0x36`
+- and chooses `0x1b` when `current_map_id > 0x36`
+
+This is a concrete level-54 split in late control flow, not a generic family-level observation.
+
+Supporting progression/control evidence recovered in the same pass:
+
+- `psx_control_callback_apply_progression_target_level` (`0x80027548`) applies `psx_map_progression_table[current_map_id]` via `FUN_8002ba84`
+- for map `54`, that progression target is `55`
+- `psx_level_gate_slot05_handler` (`0x80021fac`) has tuple branch `(param2=0x0a,param3=0x28)` that calls `psx_control_event_apply_level_channel_preset` and reset helpers, giving an optional late state-advance lane before slot `0x0f` `(0x0a,0x04)` arm may fire
+
+### Practical interpretation for natural MFM4 failures
+
+Current best read is now sharper:
+
+1. `MFM4` still correctly primes the strongest known host (`selector 0x0f -> map 54`, `runtime_header_state=3`).
+2. Failure is most likely a timing/optional-path miss inside late control-event progression around the map-54 boundary split, not wrong-host decode.
+3. The rare miss model is: a transition/control branch (including map-54-specific `0x1a` lane and optional slot05 branching) can advance or reroute state before slot `0x0f` `(0x0a,0x04)` writes `psx_debug_extra_channel_gate`.
+
+### Live Ghidra artifacts applied in this pass
+
+Renames:
+
+- `0x80020794` -> `psx_control_event_countdown_transition_tick`
+- `0x800205e8` -> `psx_control_event_apply_countdown_step`
+- `0x80020d54` -> `psx_control_set_mode_and_reset_runtime_flags`
+- created `0x8002745c` -> `psx_control_callback_apply_level_preset_or_resume`
+- created `0x80027548` -> `psx_control_callback_apply_progression_target_level`
+
+Comments:
+
+- `0x800208f0`: level-54 boundary (`<=54 -> 0x1a`, `>54 -> 0x1b`) and timing implication
+- `0x80022068`: slot05 `(0x0a,0x28)` optional late-event/preset lane note
+- `0x80027560`: progression callback note (`next = map_progression_table[current_map_id]`)
+
+## Slot-handler sibling recovery around table 0x800640a0 (2026-04-11 live pass)
+
+Focused scope for this pass was the still-raw slot-handler siblings referenced by
+`psx_level_gate_slot_handler_table` (`0x800640a0`), with emphasis on whether any
+`0x0a` sibling branch competes with or narrows tuple `(0x0a,0x04)` in slot `0x0f`.
+
+### Recovered sibling handler roles (table entries)
+
+Newly created and named from table boundaries (address range split by next table entry):
+
+- `0x800215fc` -> `psx_level_gate_slot01_handler`
+- `0x80021810` -> `psx_level_gate_slot02_handler`
+- `0x800219e4` -> `psx_level_gate_slot03_handler`
+- `0x80021fac` -> `psx_level_gate_slot05_handler`
+- `0x80022214` -> `psx_level_gate_slot06_handler`
+- `0x800222e8` -> `psx_level_gate_slot07_handler`
+- `0x800223cc` -> `psx_level_gate_slot08_handler`
+- `0x800226e0` -> `psx_level_gate_slot09_handler`
+- `0x800227ac` -> `psx_level_gate_slot0a_handler`
+- `0x80022b50` -> `psx_level_gate_slot0c_handler`
+- `0x80023854` -> `psx_level_gate_slot00_handler`
+- `0x80023af0` -> `psx_level_gate_slot10_return_true`
+
+Existing slot siblings kept/updated:
+
+- `0x80022c6c` = `psx_control_event_slot0d_handler`
+- `0x80022ea8` = `psx_control_event_slot0e_handler`
+- `0x800230e4` = `psx_control_event_slot0f_handler`
+- `0x80022940` renamed to `psx_level_gate_slot0b_control_pair_handler`
+ - table entry still points to `0x8002293c` (first prologue instruction), now preserved via comment.
+
+### New interpretation of the 0x0a subcase family
+
+Current strongest narrowing is now sibling-explicit across slots `0x0a..0x0f`:
+
+1. Slot `0x0a` (`0x800227ac`) has `(0x0a,0x01..0x03)` control/timer branches and does not write `psx_debug_extra_channel_gate`.
+2. Slot `0x0b` (`0x80022940`) `(0x0a,0x02)` sets policy bit `DAT_80078a88 |= 0x0400`, but is not a gate-byte writer.
+3. Slot `0x0c` (`0x80022b50`) `0x0a` family is control/message gated by `DAT_80078a88 & 0x200`, with no recovered gate-byte write.
+4. Slot `0x0d` (`0x80022c6c`) `(0x0a,0x02)` is mission-complete passcode text lane (`quad index 0x0e`).
+5. Slot `0x0e` (`0x80022ea8`) `(0x0a,0x01)` is mission-complete passcode text lane (`quad index 0x0f`); `(0x0a,0x06)` is late selector/apply transition.
+6. Slot `0x0f` (`0x800230e4`) `(0x0a,0x04)` remains the only recovered sibling in this family that can arm `psx_debug_extra_channel_gate` (write at `0x800232f0`) under non-hidden + header-state-3 predicates.
+
+So `(0x0a,0x04)` is now narrower than before: it is not just “one 0x0a case in slot0f”, it is the only recovered 0x0a-family sibling across adjacent slot handlers that actually sets the JL-9 gate byte.
+
+## 0x0a control-family semantic closure (2026-04-11 live pass)
+
+This pass was restricted to the late control family around:
+
+- `0x80022c6c` (`psx_control_event_slot0d_handler`)
+- `0x80022ea8` (`psx_control_event_slot0e_handler`)
+- `0x800230e4` (renamed this pass to `psx_control_event_slot0f_handler`)
+- `0x80020f7c` (`psx_control_event_apply_level_channel_preset`)
+
+### Best semantic classification for `(0x0a,0x04)`
+
+Current best read is **control-event eligibility arm (pre-hidden gate latch)**, not a reward payload by itself.
+
+Evidence:
+
+1. `(0x0a,0x04)` is in slot `0x0f` handler and writes `psx_debug_extra_channel_gate` only under:
+ - `psx_hidden_passcode_flag == 0`
+ - `psx_level_runtime_header_state == 3`
+2. The write is a single byte latch (`sb`) at `0x800232f0`, then consumed later by the hidden/debug grant lane at `0x8002fff4`.
+3. Sibling `0x0a` cases in slot `0x0d`/`0x0e` are mission-complete text/transition/control setup branches, which places `(0x0a,0x04)` inside a broader control progression family rather than a standalone "grant now" event.
+
+### Sibling-case comparison table (`param_2 == 0x0a`)
+
+| Handler slot | Address | Subcase (`param_3`) | Strongest semantics | Direct effect |
+|---|---|---|---|---|
+| `0x0d` | `0x80022c6c` | `0x02` | mission-complete passcode text branch | generates encoded quad index `0x0e`, writes congratulations text |
+| `0x0e` | `0x80022ea8` | `0x01` | mission-complete passcode text branch | generates encoded quad index `0x0f`, writes congratulations text |
+| `0x0e` | `0x80022ea8` | `0x02..0x04` | transition/setup control branches | mode/timer/runtime control setup |
+| `0x0e` | `0x80022ea8` | `0x06` | late selector/channel transition | sets selector `0x0f`, calls `psx_control_event_apply_level_channel_preset` and `psx_passcode_apply_mission_selector_and_stage` |
+| `0x0f` | `0x800230e4` | `0x01..0x03` | sibling control/event setup | runtime/control side effects without gate-byte arm |
+| `0x0f` | `0x800230e4` | `0x04` | **pre-hidden eligibility arm latch** | conditional write `psx_debug_extra_channel_gate = 1` at `0x800232f0` |
+
+Net: `(0x0a,0x04)` is best classified as a **control-arm/preset-eligibility latch** for the later hidden completion path, not as direct reward completion.
+
+## Sink-side feeder closure (2026-04-11 live pass)
+
+This pass was restricted to the sink-side feeder and argument sourcing around `0x800214ac..0x800215f8`.
+
+### Recovered action-record dispatch structure
+
+Recovered function body is now promoted in live Ghidra as:
+
+- `0x800214ac` -> `psx_level_gate_slot_dispatch_from_action_record`
+
+Observed layout used by this dispatcher:
+
+- `record+0x00` -> pointer to slot byte (`slot = *(*(record+0x00))`)
+- `record+0x08` -> pointer to arg1 byte (`arg1 = *(*(record+0x08))`)
+- `record+0x0c` -> pointer to arg2 byte (`arg2 = *(*(record+0x0c))`)
+
+Dispatch mechanics:
+
+- level gate compare uses `psx_map_id_to_gate_slot_table[current_map_id]` (`0x80063e68`)
+- indirect call uses `psx_level_gate_slot_handler_table[slot]` (`0x800640a0`)
+- slot `0x0f` entry points to `0x800230e4` (`psx_set_debug_extra_channel_gate`) at table address `0x800640dc`
+
+JL-9 gate-arm tuple remains explicit and unchanged:
+
+- slot `0x0f`
+- `arg1 = 0x0a`
+- `arg2 = 0x04`
+
+which reaches the gate-byte write branch at `0x800232f0`.
+
+### Upstream producer status (what is proven)
+
+Proven table topology into the sink feeder:
+
+- `psx_behavior_opcode_handler_table[54] = 0x80027ecc` (`psx_behavior_subopcode_dispatch`) at `0x80064284`
+- `psx_behavior_subop_handler_table[49] = 0x800214ac` (`psx_level_gate_slot_dispatch_from_action_record`) at `0x800636d4`
+
+Current proof boundary remains the same:
+
+- known gameplay caller lane into `psx_object_behavior_opcode_dispatch` still enforces `(opcode_word-1) < 0x0a` at `0x80026710`
+- therefore `54 -> 49 -> 0x800214ac` is retained as proven table topology, not yet proven active on the currently recovered caller path
+
+## Host-level family closure for natural JL-9 gate-arm (2026-04-11 live pass)
+
+Scope of this pass was restricted to the host-side family `{54,55,56,57,58,82}` with direct MCP evidence for:
+
+- selector mapping (`psx_selector_to_map_id_table`, `0x80063e54`)
+- level/map gate-slot mapping (`psx_map_id_to_gate_slot_table`, `0x80063e68`)
+- progression/completion transitions (`psx_map_progression_table`, `0x80063eac`, plus `FUN_80020f7c` callers)
+
+### Exact mapping closure used for ranking
+
+1. Published normal-code selector closure still anchors on selector `0x0f`:
+ - `psx_selector_to_map_id_table[0x0f] = 0x36` (map id `54`)
+2. Slot-family closure for natural gate-arm dispatch:
+ - `psx_map_id_to_gate_slot_table[54] = 0x0f`
+ - `psx_map_id_to_gate_slot_table[55] = 0x0f`
+ - `psx_map_id_to_gate_slot_table[56] = 0x0f`
+ - `psx_map_id_to_gate_slot_table[57] = 0x0f`
+ - `psx_map_id_to_gate_slot_table[58] = 0x0f`
+ - `psx_map_id_to_gate_slot_table[82] = 0x0f`
+3. Reciprocal passcode-validation behavior in caller flow around `0x80034d60..0x80034d7c` remains strongest for map `54`:
+ - map id `54` round-trips through `slot 0x0f -> map 54` by reverse lookup
+ - sibling family members (`55..58,82`) share slot `0x0f` but do not have the same direct selector round-trip anchor in recovered normal passcode tables
+
+### Late objective/completion hosting evidence
+
+`FUN_80020f7c` remains the strongest late transition hub in this lane. Live callers include:
+
+- `0x80022068`
+- `0x80022e58`
+- `0x80023080`
+- `0x8002748c`
+
+and the helper itself reads both:
+
+- `psx_map_progression_table[current_map_id]` (`0x80020fa4`)
+- `psx_map_id_to_gate_slot_table[current_map_id]` (`0x80020fbc`)
+
+Progression bytes for the host family are now explicit:
+
+- map `54 -> 0x37` (`55`)
+- map `55 -> 0x38` (`56`)
+- map `56 -> 0x39` (`57`)
+- map `57 -> 0x3a` (`58`)
+- map `58 -> 0xff` (terminal marker in this table)
+- map `82 -> 0x53` (`83`)
+
+This keeps a completion-timing explanation live: transition-heavy late events can move execution across the same slot-`0x0f` family, but not all members are equally stable for deterministic player replication.
+
+### Ranked host levels/maps (current best)
+
+1. **Map/level 54** (best host)
+ - only family member directly anchored by recovered normal selector path (`0x0f -> 54`)
+ - satisfies both prime-side and slot-family-side constraints in one static route
+2. **Map/level 55**
+ - immediate progression successor of 54 and still in slot-`0x0f` family
+ - plausible if gate-arm event is late-transition-bound rather than early-in-map
+3. **Map/level 56**
+ - same slot family, but one transition further away from direct passcode anchor
+4. **Map/level 57**
+ - same slot family, lower reproducibility due to additional transition depth
+5. **Map/level 82**
+ - valid slot-family host (`0x0f`) but weak for this specific user scenario because no direct published selector anchor was recovered
+6. **Map/level 58**
+ - slot-family valid, but progression entry marks terminal (`0xff`) and is therefore most likely to race/exit through completion paths rather than offer stable pre-hidden gate-arm timing
+
+### Best current explanation for failed natural MFM4
+
+After the failed natural `MFM4` trial, the best-supported explanation remains event-host/timing failure, not decode failure:
+
+1. `MFM4` still fits the strongest static prime (`runtime_header_state==3`, selector family leading to map `54`).
+2. The missing piece is still the in-level/control dispatch that must reach slot `0x0f` with tuple `(0x0a,0x04)` before hidden-mode trigger.
+3. Completion/transition hubs (`FUN_80020f7c` callers) can move state through family members and potentially past the required gate-arm moment.
+4. So a natural run can fail even with correct passcode if:
+ - the specific tuple event never fired,
+ - it fired outside the required hidden-flag polarity window,
+ - or progression/completion moved the run into a less stable timing state before hidden-input trigger.
+
+Net result for this pass: **map 54 remains the best host candidate**, but `MFM4` alone is not expected to be sufficient without the concrete in-level event timing.
+
+## Late-objective / mission-complete clue pass (2026-04-11, live MCP)
+
+Focused this pass only on concrete late-objective and reward-event clues that can naturally arm the JL-9 gate path (`psx_debug_extra_channel_gate`), with emphasis on mission-complete text handlers, transition siblings, and uncommon branch outcomes.
+
+### Hard evidence (high confidence)
+
+1. Mission-complete congratulations strings are anchored at:
+ - `0x80063f10`: congrats template with next-passcode tail.
+ - `0x80063f74`: shorter congrats variant.
+2. Two adjacent control-event handlers are concrete mission-complete/passcode writers:
+ - `0x80022c6c` (`DAT_800640a0[0x0d]`): slot-`0x0d` branch generates encoded passcode index `0x0e` and writes congrats text.
+ - `0x80022ea8` (`DAT_800640a0[0x0e]`): slot-`0x0e` branch generates encoded passcode index `0x0f` and writes congrats text.
+3. Slot-`0x0e` has a concrete late-transition branch that force-loads selector `0x0f`:
+ - at `0x80023040` (`param2=0x0a,param3=6` branch), code sets `DAT_80078a8c=0x0f` and calls `psx_passcode_apply_mission_selector_and_stage`.
+4. Selector/channel mapping remains decisive for natural gate-arm routing:
+ - `selector 0x0f -> level 0x36 (54) -> DAT_80063e68[level]=0x0f`.
+ - in sampled selector range, this is the only selector that maps directly into channel family `0x0f`.
+5. Natural gate-arm write remains bounded to slot-`0x0f` case `(param2=0x0a,param3=4)`:
+ - write at `0x800232f0` sets `DAT_8006739d` only if `psx_hidden_passcode_flag==0` and `psx_level_runtime_header_state==3`.
+
+### Text-display siblings and uncommon branch clues
+
+1. Slot-`0x0f` text-display sibling at `0x800236dc` (`param2=4,param3=1`) calls `ui_message_set_active_text(DAT_8006754c + idx)`.
+ - This is resource-table driven, distinct from hardcoded congratulations templates.
+ - It is a strong clue for optional scripted outcomes/messages tied to level-authored text resources.
+2. Slot-`0x0e` uncommon outcomes include:
+ - `param3=5`: writes `DAT_80078a10=2` (state-only branch).
+ - `param3=6`: direct selector apply to `0x0f` (late transition behavior, not ordinary text-only flow).
+
+### Strongest natural late-objective candidate events from this pass
+
+1. `slot 0x0e` mission-complete transition branch (`0x80022ea8`, `param2=0x0a,param3=6`) as the clearest upstream feeder into selector/channel `0x0f` family.
+2. `slot 0x0f` event branch (`0x800230e4`, `param2=0x0a,param3=4`) as the proven natural JL-9 gate-arm writer under non-hidden + runtime-state-3 predicates.
+3. `slot 0x0f` text-resource branch (`0x800236dc`, `param2=4,param3=1`) as the strongest sibling clue for uncommon/optional scripted objective messaging that likely co-occurs with late transition states.
+
+### What is still speculation
+
+1. Exact player-visible authored event sequence inside the level (the concrete map script moment) that emits `slot 0x0f` with `(0x0a,4)` remains unclosed.
+2. Any claim that one specific congratulation screen alone implies gate-arm is still speculative; current proof is branch-level and table-level, not full authored mission-script closure.
+
+## 0x0a family case-map closure around natural JL-9 gate arm (2026-04-11 live pass)
+
+This pass was restricted to the control-event family around `0x80022c6c..0x80023390` with live MCP decompile/disassembly and conservative in-database naming.
+
+### Recovered sibling handlers and table roles
+
+1. Slot-handler table bytes at `0x800640d4/0x800640d8/0x800640dc` confirm sibling entries:
+ - slot `0x0d` -> `0x80022c6c`
+ - slot `0x0e` -> `0x80022ea8`
+ - slot `0x0f` -> `0x800230e4` (`psx_set_debug_extra_channel_gate`)
+2. Sink dispatch block `0x800214ac..0x800215f8` remains the slot-gated tuple sink:
+ - compare slot byte with `DAT_80063e68[current_level]`
+ - if equal, call `DAT_800640a0[slot]` with tuple `(param_2,param_3)` loaded from record-byte pointers.
+
+### Concrete 0x0a family mapping (best current)
+
+For these three sibling handlers, `param_2 == 0x0a` is the family selector and `param_3` is the subcase index.
+
+1. Slot `0x0d` handler (`0x80022c6c`):
+ - `(0x0a,0x02)`: generates passcode quad index `0x0e`, writes encoded chars to `0x80063f6e..0x80063f71`, shows congratulations text.
+2. Slot `0x0e` handler (`0x80022ea8`):
+ - `(0x0a,0x01)`: same mission-complete passcode lane but quad index `0x0f`.
+ - `(0x0a,0x02..0x04)`: mode/timer setup branches.
+ - `(0x0a,0x06)`: applies mission selector and stage.
+3. Slot `0x0f` handler (`0x800230e4`):
+ - `(0x0a,0x01..0x04,0x2e)` switch family.
+ - critical branch `(0x0a,0x04)` at `0x800232f0`: sets `psx_debug_extra_channel_gate=1` only when `psx_hidden_passcode_flag==0` and `psx_level_runtime_header_state==3`.
+
+### Interpretation of tuple (0x0a,0x04)
+
+Strongest conservative interpretation is unchanged but now better bounded: this is the natural pre-hidden arm event branch in the slot-`0x0f` family, sibling to mission-complete/control subcases in slots `0x0d/0x0e/0x0f`, and it is the direct writer path for the later hidden debug grant extra-lane check.
+
+Practical consequence:
+
+1. `(0x0a,0x04)` is not an isolated special; it belongs to a coherent control-event sibling family.
+2. Its placement next to mission-complete style tuple branches strengthens the read that the missing natural trigger is an in-level authored control-event emission, not a separate passcode-screen operation.
+
+## Natural in-level event synthesis and conservative naming sweep (2026-04-11 live pass)
+
+This pass was restricted to the natural in-level gate-arm event lane for JL-9, not the broader hidden/debug chain.
+
+### Event-only synthesis (current best conservative read)
+
+1. Natural arm still centers on in-level control/event dispatch into the slot-gated sink block at `0x800214ac..0x800215f8`.
+2. Inside that sink, current-level family is checked against the slot index and then dispatches through slot-handler table entry `slot 0x0f -> 0x800230e4` (`psx_set_debug_extra_channel_gate`).
+3. Actual gate write remains constrained to tuple branch `(param_2 == 0x0a, param_3 == 0x04)` at `0x800232f0`, plus existing non-hidden/header-state predicates.
+4. Upstream behavior-opcode topology (`opcode 54 -> subop 49 -> sink`) is still structurally supported by table links, but active reachability from the only proven gameplay caller lane remains unproven because the recovered guard at `0x80026710` bounds the known lane to `(opcode_word-1) < 0x0a`.
+
+### Live Ghidra conservative rename/comment sweep applied
+
+Renamed data/labels (only still-raw central entities):
+
+- `0x800640a0`: `PTR_LAB_800640a0` -> `psx_level_gate_slot_handler_table`
+- `0x800641ac`: `PTR_LAB_800641ac` -> `psx_behavior_opcode_handler_table`
+- `0x80063610`: `PTR_LAB_80063610` -> `psx_behavior_subop_handler_table`
+- `0x800214ac`: `LAB_800214ac` -> `psx_level_gate_slot_dispatch_block_800214ac`
+- `0x80027ecc`: `LAB_80027ecc` -> `psx_behavior_subop_dispatch_block_80027ecc`
+
+Added disassembly comments (evidence/uncertainty preserving):
+
+- `0x800215dc`: level-family compare + slot-table indirect-call behavior (slot `0x0f` sink relevance).
+- `0x800232f0`: exact gate-arm tuple and predicate reminder.
+- `0x8002685c`: opcode-table dispatch note plus proven caller-lane bound reminder.
+- `0x80027f0c`: sub-op table dispatch note with explicit reachability uncertainty.
+- `0x800214ac`: event-sink dispatch-role note.
+
+### Unresolved on purpose
+
+- Exact player-visible authored in-level event that naturally emits the `(0x0f, 0x0a, 0x04)` sink tuple.
+- Proof of an additional active caller/context that can reach high behavior-opcode entries beyond the known `< 0x0a` lane.
+- Any stronger semantic name for `0x80027ecc` than conservative sub-op dispatch block labeling.
+
+## Event-only synthesis continuation (2026-04-11 live pass)
+
+Scope of this continuation pass was intentionally narrow: keep sink-side natural-event naming durable without promoting unproven upstream producer semantics.
+
+### Updated concise synthesis (event-only)
+
+1. Natural JL-9 pre-hidden arm remains a slot-family control-event branch, not a passcode-screen branch.
+2. Sink path is unchanged and still strongest: `psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`) validates current-level family, then dispatches through `psx_level_gate_slot_handler_table[slot]`.
+3. Slot `0x0f` resolves to `psx_control_event_slot0f_handler` (`0x800230e4`), where tuple `(0x0a,0x04)` at `0x800232f0` conditionally writes `psx_debug_extra_channel_gate` only under `hidden==0 && runtime_header_state==3`.
+4. Upstream `opcode 54 -> subop 49 -> sink` remains table-valid topology, but active gameplay reachability is still unproven past the known caller lane bounded by `(opcode_word-1) < 0x0a`.
+
+### Exact live Ghidra artifacts changed in this continuation pass
+
+Renamed function:
+
+- `0x800230e4`: `psx_set_debug_extra_channel_gate` -> `psx_control_event_slot0f_handler`
+
+Renamed table-entry labels:
+
+- `0x800640d4` -> `psx_level_gate_slot_handler_slot0d_entry`
+- `0x800640d8` -> `psx_level_gate_slot_handler_slot0e_entry`
+- `0x800640dc` -> `psx_level_gate_slot_handler_slot0f_entry`
+
+Added/updated comments:
+
+- `0x800230e4`: slot-`0x0f` family role comment; gate write classified as one subcase.
+- `0x800232f0`: exact natural gate-arm tuple + predicate comment.
+- `0x800640dc`: slot-`0x0f` table-entry comment with tuple anchor.
+- `0x80064284`: opcode-table entry `54` comment preserving topology-vs-reachability split.
+- `0x800636d4`: subop-table entry `49` comment preserving topology-vs-reachability split.
+
+### Deliberately unresolved / refused names
+
+1. Refused rename: `psx_behavior_subopcode_dispatch` (`0x80027ecc`) to any stronger event-producer name.
+ - Why refused: no direct recovered caller currently proves active high-index opcode reachability on gameplay path.
+2. Refused rename: `psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`) to mission-specific or JL-9-specific wording.
+ - Why refused: sink dispatch behavior is generic slot-family machinery beyond JL-9.
+
+## Published mission code sweep closure (2026-04-11 live pass)
+
+Focused live pass on the user-supplied ordinary PSX mission code table was used to answer one narrow question: does any published non-hidden mission code already satisfy the static JL-9 preconditions, or do they all fail before the hidden/debug stage?
+
+### Exact decode-space closure
+
+1. Ordinary passcode rows are decode indices `i = 0x00..0x0e`.
+2. Special passcodes are the next three rows:
+ - `i = 0x0f` => `?RTN` family
+ - `i = 0x10` => `?0SR` family
+ - `i = 0x11` => `?QQQ` family
+3. For ordinary rows, selector space is offset by one:
+ - ordinary decoded row `i` returns selector `s = i + 1`
+4. First-character difficulty only affects `psx_level_runtime_header_state`; it does **not** choose the selector/current-level lane.
+
+### Strongest published-code candidate
+
+`MFM4` is now the strongest ordinary published JL-9 setup candidate.
+
+Reasoning:
+
+1. Suffix `FM4` matches ordinary decode row `i = 0x0e`.
+2. First char `M` yields accepted delta `3`, so normal decode writes `psx_level_runtime_header_state = 3`.
+3. Ordinary row `i = 0x0e` returns selector `s = 0x0f`.
+4. Live table bytes then map:
+ - `DAT_80063e54[0x0f] = 0x36` (`54` decimal)
+ - `DAT_80063e68[54] = 0x0f`
+5. Therefore `MFM4` is the only currently recovered published mission code that statically satisfies both known preconditions at once:
+ - non-hidden prime with `runtime_header_state == 3`
+ - current-level family that can reach slot-`0x0f` dispatch
+
+### Strong negatives from the same sweep
+
+- `LRTN` / `MRTN` / `PRTN` are not ordinary mission primes; they are the special `?RTN` family and clear `psx_level_runtime_header_state` to `0`.
+- the older contradiction in this investigation came from mixing decode-row space (`i`) with returned selector space (`s`); `MFM4` works because ordinary `i = 0x0e` still returns selector `0x0f`.
+- no published mission code currently closes the full JL-9 route by itself; even `MFM4` still needs the in-level gate-arm event and then the later hidden/input trigger.
+- the user-supplied level-11 strings containing `O` should be treated cautiously as written because `O` is not present in the recovered passcode alphabet (`BCDFGHJKLMNPQRSTVWXZ0123456789`).
+
+### Practical consequence
+
+Current best actionable static route is now:
+
+1. Enter `MFM4` as the ordinary prime.
+2. In the same running session, hit the in-level scripted/control event that emits dispatch tuple `(0x0f,0x0a,0x04)` and arms `psx_debug_extra_channel_gate`.
+3. Enter hidden `L0SR` / `?0SR`.
+4. Press `R1 + Circle`.
+
+This is still not a fully closed player recipe because step 2 remains the missing concrete event, but it is now the strongest evidence-backed published-code path rather than a generic “some normal code with state 3” placeholder.
+
+## Hard-clear theory check and manual gate-poke closure (2026-04-11 live pass)
+
+Two follow-up questions were tested after `MFM4` was isolated as the strongest published prime candidate:
+
+1. is the missing in-level trigger actually tied to beating the game, especially on hard?
+2. if not, is manual arming of the known gate byte enough to make hidden `L0SR` plus the trigger input unlock `JL-9`?
+
+### Hard-clear theory verdict
+
+Current evidence keeps the "beat the game on hard, then enter `L0SR`" theory in the weak-to-medium bucket, not the lead explanation.
+
+What was actually recovered:
+
+1. congratulation/completion text paths do exist near unnamed code around `0x800204fc`, `0x80022d20`, and `0x80022f68`.
+2. those paths are plausible mission-complete / transition / UI handlers, but no direct write to `psx_debug_extra_channel_gate` (`0x8006739d`) was recovered from them.
+3. no better persistent JL-9-specific latch was recovered than the already-known gate byte itself.
+4. the strongest executable-backed model still points to an in-level scripted/event dispatch lane, not a post-credits reward lane.
+
+So the current read is narrower than folklore but not fully closed:
+
+- `MFM4` plausibly matters because it is the only published prime that sets `runtime_header_state = 3` and reaches the slot-`0x0f` gate family.
+- but beating the final mission on hard is **not yet proven** to be the missing arm event.
+- the unresolved gap remains the same concrete in-level producer for dispatch tuple `(0x0f,0x0a,0x04)`.
+
+### Manual gate-poke closure
+
+The practical emulator test is now strong enough to state directly.
+
+1. `psx_debug_extra_channel_gate` is runtime byte `0x8006739d`.
+2. writer at `0x800232f0` uses `sb`, so storage is byte-wide.
+3. reader at `0x8002fff4` uses `lbu` then branches on zero, so the gate is checked as **nonzero**, not as one exact magic literal.
+4. no second direct writer or clear for `0x8006739d` has been recovered in the inspected session/load/menu paths.
+
+### Practical manual test sequence
+
+The strongest current poke test is:
+
+1. set byte `0x8006739d = 0x01`
+2. enter hidden `L0SR` / `?0SR`
+3. press `R1 + Circle`
+
+Expected result if the trigger path executes normally:
+
+- hidden input path reaches `psx_debug_grant_weapon_channels_and_ammo`
+- late read at `0x8002fff4` sees nonzero gate byte
+- extra unlock path at `0x80030004` runs for channel/index `0x0d` (`JL-9` lane)
+
+### Practical caveats
+
+- the poke bypasses the natural writer-side predicates (`hidden==0`, `runtime_header_state==3`), so it does **not** validate the true in-level event by itself.
+- hidden mode still has to be active when the input trigger is pressed.
+- if the emulator uses uncached/physical mirrors, the same byte may appear as `0x0006739d` depending on the UI, but the logical KSEG0 runtime address is `0x8006739d`.
+- if a manual test fails with gate byte set, the next most likely cause is that hidden mode timed out or the input chord did not decode to `0x1e`.
+
+### User-validated downstream closure
+
+User emulator verification now confirms the downstream half directly:
+
+1. set main-memory byte `0x8006739d = 0x01`
+2. enter `L0SR`
+3. start gameplay and press `R1 + Circle`
+4. result: `JL-9` appears in inventory next to `JL-2`
+
+Practical meaning of that success:
+
+- this validates the late hidden/input grant half of the model, not the natural writer path.
+- the poke directly pre-satisfies the nonzero check at `0x8002fff4`, so the test does **not** prove that `MFM4` or the natural in-level gate-arm event occurred.
+- among those two upstream elements, the more direct thing bypassed is the in-level trigger itself, because the poke replaces the writer result (`psx_debug_extra_channel_gate = 1`) rather than recreating the natural writer predicates that would normally produce it.
+- `MFM4` is therefore best read now as the strongest **natural prime candidate**, not as a required part of the forced test.
+
+### User experiment follow-up (2026-04-11)
+
+Additional emulator trials now tighten the natural-versus-forced split further.
+
+1. Natural `MFM4` trial:
+ - user entered `MFM4`, allowed the level to load, then later returned to menu and attempted the hidden/input phase.
+ - result: no `JL-9`; only ordinary `JL-2` was present.
+ - practical meaning: current evidence no longer supports `MFM4` by itself as a sufficient natural route.
+2. Forced-gate control with non-hard final-level code:
+ - user entered `JFM4` (final map on easy), returned to menu, set `0x8006739d = 0x01`, entered `L0SR`, started a level, and pressed `R1 + Circle`.
+ - result: `JL-9` still appeared.
+ - practical meaning: the forced downstream route does not depend on `MFM4` specifically; manual gate arm is enough even when the prime code is not the hard-difficulty candidate.
+
+Current best interpretation after these trials:
+
+- `MFM4` remains the strongest **natural** prime candidate because it matches the writer-side predicates statically.
+- but it is no longer the lead bottleneck for the overall mystery.
+- the missing natural in-level event is now the dominant unknown.
+
+### Deferred user experiments
+
+Keep these queued for later follow-up:
+
+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: strict ordering test (`event -> L0SR -> trigger` versus `L0SR -> event -> trigger`)
+
+## NRTN / ?RTN passcode path closure (2026-04-11 live pass)
+
+Focused live MCP pass on active `SLUS_002.68` for:
+
+- `psx_passcode_decode_to_mission_selector` (`0x8003ec8c`)
+- `psx_passcode_apply_mission_selector_and_stage` (`0x80021138`)
+- caller block around `0x80034c14..0x80034ddc`
+
+### Exact decode and selector behavior
+
+1. The special `?RTN` row is decode index `i=0x0f` in `psx_passcode_decode_to_mission_selector`.
+2. At `0x8003ed10`, this branch clears `psx_level_runtime_header_state` (`DAT_80068ab0`) to `0`.
+3. Function return is **not** `0` on this branch.
+ - Due to delay-slot flow (`0x8003ecac`, `0x8003ed04`), `v0` remains `0x10` on return.
+4. `psx_passcode_screen_eval_current_entry` stores raw decode return `0x10` into `DAT_80068a8c` and maps through `DAT_80063e54[0x10]`.
+ - From live table bytes, `DAT_80063e54[0x10] = 0x3f`.
+5. In caller flow (`0x80034d84`), `s0` is this mapped value (`0x3f`), so `beq s0,zero` does not fire.
+
+### Exact apply/load behavior for NRTN / ?RTN lane
+
+1. Because `s0 != 0`, caller reaches `jal 0x80021138` at `0x80034dcc`.
+2. `psx_passcode_apply_mission_selector_and_stage` reads raw selector from `DAT_80068a8c`; for this path it is `0x10`.
+3. Switch case for raw selector `0x10` is `0x8002142c` (index `0x0f` after `selector-1`).
+ - writes `DAT_800675e4 = 0x1d`
+ - sets mode byte `DAT_80067379 = 2`
+ - calls common apply helper with `a0 = 0x36`
+4. This is consistent with the commonly described office-loading lane, but the exact user-facing mission label remains conservatively treated as "developer-office candidate" until runtime label capture in the same session.
+
+### JL-9 gate-arm compatibility answer
+
+- NRTN / ?RTN cannot itself satisfy gate-arm precondition `psx_level_runtime_header_state == 3`.
+- It explicitly forces that state to `0` in decoder (`0x8003ed10`).
+- Therefore it does **not** coexist with the required non-hidden gate-arm predicate at `0x800232c0..0x800232f0` (`hidden==0 && runtime_header_state==3`) unless another later path re-primes header state to `3`.
+
+Confidence:
+
+- high (`~0.95`) on decode return/write/apply control flow above (direct disassembly evidence)
+- medium-high (`~0.78`) on exact player-visible "developer office" naming for `a0=0x36` without paired runtime UI/string capture
+
+## NRTN -> Office Event -> ?0SR/L0SR -> 0x1e hypothesis check (2026-04-11 live pass)
+
+Target hypothesis tested against live `SLUS_002.68` structure:
+
+1. enter `NRTN` to load dev office
+2. let an in-level office event arm `psx_debug_extra_channel_gate`
+3. enter hidden `?0SR`/`L0SR`
+4. press input code `0x1e` (`R1 + Circle`)
+
+Verdict: **structurally plausible but currently unproven as an exact deterministic player script**.
+
+Exact reasons from live code evidence:
+
+- `psx_debug_extra_channel_gate` remains one-writer/one-reader in this image:
+ - write at `0x800232f0` in `psx_set_debug_extra_channel_gate`
+ - read at `0x8002fff4` in `psx_debug_grant_weapon_channels_and_ammo`
+- gate-arm writer requires `hidden_flag==0` and `runtime_header_state==3` at the write site (`0x800232c0..0x800232f0`), so arming is structurally pre-hidden.
+- hidden path (`?0SR`/`L0SR`, special index `0x10`) sets `psx_hidden_passcode_flag` at `0x8003ed28` and returns selector `0`, with selector-`0` branch skip around `0x80034d84`.
+- grant trigger path requires hidden mode and input `0x1e` (`0x80013154..0x80013174`), so trigger is structurally post-hidden.
+- in-level gate-arm routing is real and dispatch-based (`0x800214ac..0x800215f8`): it compares against `DAT_80063e68[current_level]`, then indirect-calls `DAT_800640a0[index]`; slot `0x0f` resolves to `0x800230e4` and uses byte args from the action record.
+
+Why this is not yet fully proven:
+
+- static evidence still does not name one exact player-visible "office event" that deterministically emits the required writer tuple (`slot 0x0f`, `param_2==0x0a`, `param_3==0x04`) in dev office specifically.
+- special-passcode sibling behavior (`?RTN` lane clears `runtime_header_state` in decoder) means the exact `NRTN` practical role still needs one runtime trace confirmation in the same session, not another folklore inference.
+
+Confidence split for this exact hypothesis:
+
+- high (`0.89`) that the two-phase shape (pre-hidden arm, post-hidden trigger) is structurally correct.
+- medium (`0.67`) that the concrete user recipe starting specifically from `NRTN` is correct as stated.
+- overall classification for the exact `NRTN -> office event -> ?0SR/L0SR -> 0x1e` claim: **currently unproven, plausible**.
+
+Latest live MCP ownership pass on active `SLUS_002.68` now tightens the storage model around your dump split (`0x801456fc..0x80145748` static across dumps, `0x8014574c..0x801457d0` changed):
+
+- code-side ownership is channel-state based, not a direct contiguous owned-id array:
+ - enable/query bit is byte `psx_marker_channel_runtime_block[(channel*4)+0x34]`
+ - per-channel step/ammo state is byte `psx_marker_channel_runtime_block[(channel*4)+0x6c]`
+ - active selected channel is byte `psx_marker_channel_runtime_block+0x32`
+- this lines up with the dump boundary: if runtime block base is near `0x80145718`, then `+0x34` is exactly `0x8014574c` (start of the changed range), and `+0x6c/+0x84/+0x8c` also sit inside the changed range.
+- commit/resolver path remains table-driven (`channel*10` rows), not contiguous-id-list driven:
+ - `psx_apply_channel_effect_and_commit_selected_item_id` loads `channel_commit_row_table[(channel*10)+9]` at `0x8002f15c`
+ - that committed id then resolves weapon rows via `idx*0x26` from `0x8006466a`
+- debug bulk grant is now explicitly separated from single-channel unlock helper in the live database:
+ - `0x8002fd90` renamed to `psx_debug_grant_weapon_channels_and_ammo`
+ - function bulk-calls channel unlocks `0x04..0x0c` and conditionally `0x0d` behind `psx_debug_extra_channel_gate` read at `0x8002fff4`
+
+Current best read is stable on four points:
+
+- `JL-9` is a real executable-backed weapon-definition row in the PSX build, not a stray string.
+- `JL-9` is now also user-verified in emulator memory editing as a working selectable weapon with unique sprite/behavior, not just a table-resident leftover.
+- the extra hidden/debug-conditioned late weapon lane maps to `JL-9`, not `JL-2`.
+- step 2 of the old JL-9 recipe is now much clearer: it is not a second passcode-screen action, but an indirect in-level scripted handler dispatch that reaches `psx_set_debug_extra_channel_gate` through slot `0x0f` with byte args `0x0a,0x04`.
+- hidden `?0SR` / `L0SR` also no longer looks like a level-loading code; the hidden branch returns selector `0` in the shared passcode-screen caller and skips the normal mission/apply-load path.
+- the checked `binary/Crusader - No Remorse Memdump Weapons.bin` artifact is a PSX VRAM dump and does not directly expose weapon-slot RAM.
+- the checked `binary/Crusader - No Remorse Weapons Main Ram.bin` artifact is plausible main RAM, but it still does not by itself distinguish the selected `JL-?` slot as `JL-2` versus `JL-9`.
+- the current JL-9 debug-path read is now tighter in two separate ways: input code `0x1e` is now closed to pad mask `0x2800` and high-confidence `R1 + Circle`, while the remaining recipe gap is the exact pre-hidden player action that reaches the `param_2==0x0a,param_3==4` gate-arm writer path.
+
+## What step 2 actually means (2026-04-11 clarification)
+
+The earlier wording for step 2 was too abstract. Current live code evidence says:
+
+1. Step 2 is **not** "enter something else on the passcode screen before the level loads."
+2. Step 2 is **not** the hidden passcode itself.
+3. Step 2 is an **in-level scripted/control event** that indirectly dispatches handler slot `0x0f`, which is `psx_set_debug_extra_channel_gate`.
+4. The actual gate-arm write only happens when that handler is called with byte args `0x0a` and `0x04`.
+
+So the practical model is now:
+
+- step 1: use a normal passcode path that leaves `runtime_header_state == 3`
+- level loads / control flow continues into gameplay
+- step 2: some scripted in-level event fires and reaches the slot-`0x0f` handler with args `0x0a,0x04`, arming `psx_debug_extra_channel_gate`
+- step 3: use the hidden passcode path (`?0SR` / `L0SR`) to set hidden mode without taking the ordinary level-load branch
+- trigger: press `R1 + Circle` (`0x1e`) while hidden is active so debug grant runs and consumes the already-latched extra gate
+
+The remaining unknown is therefore not "what button do I press for step 2?" but "what concrete in-level scripted event emits slot `0x0f` with args `0x0a,0x04`?"
+
+## Gate survivability across load/menu transitions (2026-04-11 live pass)
+
+Scope of this pass was the exact user question: does `psx_debug_extra_channel_gate` (`0x8006739d`) survive level loads, passcode transitions, main-menu returns, and hidden-passcode activation in one running session.
+
+Direct executable evidence:
+
+1. `psx_debug_extra_channel_gate` still has exactly one recovered writer and one recovered reader in this image.
+ - writer: `0x800232f0` in `psx_set_debug_extra_channel_gate` (`0x800230e4`)
+ - reader: `0x8002fff4` in `psx_debug_grant_weapon_channels_and_ammo` (`0x8002fd90`)
+ - a direct neighborhood write scan over `0x80067380..0x800673b0` found many writes to nearby state bytes (`0x80067384`, `0x8006738c`, `0x8006739c`, `0x800673a0`, `0x800673a4`), but no additional write to `0x8006739d`.
+
+2. Level-load/session loop logic does not recover a static clear of `0x8006739d`.
+ - `psx_level_session_loop` (`0x8002b8ec`) loads WDL bundles and resets several per-session flags, and it does clear `psx_hidden_passcode_flag` on timer expiry at `0x8002b9e4`.
+ - no direct store to `0x8006739d` appears in this loop.
+
+3. Main-menu/UI reset helpers seen in this lane do not recover a static clear of `0x8006739d`.
+ - `FUN_800350e4` and `FUN_800352d4` reset menu/passcode state (`DAT_80067384`, selector bytes, control flags, runtime-header staging), but no write to `0x8006739d` is recovered.
+
+4. Hidden passcode activation and normal passcode application diverge exactly where expected.
+ - decoder special index `0x10` sets `psx_hidden_passcode_flag=1` in `psx_passcode_decode_to_mission_selector` at `0x8003ed28` and returns selector `0`.
+ - in the passcode caller block around `0x80034d84`, selector `0` skips the apply/load branches (`psx_passcode_apply_selector_to_mode_0x0d`, `psx_passcode_apply_selector_to_mode_0x15`, `psx_passcode_apply_mission_selector_and_stage`) because of the explicit `beq s0,zero,...` branch.
+ - practical consequence: hidden-code entry does not itself force the same normal apply/load branch used by nonzero mission selectors.
+
+5. Gate-arm and grant-entry still require opposite hidden-flag states.
+ - gate arm at `0x800232f0` requires `psx_hidden_passcode_flag==0` and `psx_level_runtime_header_state==3`.
+ - debug grant entry at `0x80013154..0x80013174` requires `psx_hidden_passcode_flag!=0` and input code `0x1e`.
+
+Answer to the two-phase feasibility question:
+
+- In one running session, the two-phase recipe remains feasible.
+- Strongest current ordering is still: arm gate first in non-hidden state, then activate hidden mode and trigger input code `0x1e`.
+- Based on current static evidence, step 2 (hidden activation + trigger) is **after** the gate-arm event and can happen after a level load in the same process/session; no static clear of `0x8006739d` was recovered across the inspected load/menu transition helpers.
+- Step 2 **before** gate-arm is not consistent with the recovered predicates, because gate-arm explicitly requires hidden flag off.
+
+Confidence:
+
+- high (`~0.92`) on one-writer/one-reader gate model in this image
+- high (`~0.88`) on hidden-vs-nonhidden polarity constraint forcing a two-phase order
+- medium-high (`~0.76`) on practical persistence through all menu/load routes, because static evidence found no clear but an unresolved pointer-indirect/runtime write path is still theoretically possible
+
+## Passcode screen semantics closure: what step 1 and step 3 mean (2026-04-11 live pass)
+
+This closure pass targeted user-facing behavior, not just branch predicates: are normal and hidden passcodes entered on the same screen, do they immediately transition out, and is the current numbered sequence operationally safe.
+
+Direct code-backed findings:
+
+1. Normal and hidden passcodes are decoded through the same passcode-screen evaluator.
+ - `psx_passcode_screen_eval_current_entry` (`0x80034e38`) builds the same 4-byte candidate from current screen-entry bytes and always calls `psx_passcode_decode_to_mission_selector`.
+ - Hidden special decode (`index 0x10`, `?0SR/L0SR`) is set in that shared decoder at `0x8003ed28` (`psx_hidden_passcode_flag = 1`), not in a separate hidden-only input UI.
+
+2. The immediate passcode-screen control flow is value-sensitive in the unnamed caller block around `0x80034c14`.
+ - On the first eval site (`0x80034c14`), return `0` follows the reset/early-return branch (`0x80034c24..0x80034cdc`) rather than the acceptance/transition path.
+ - Nonzero eval values proceed through `0x80034d00` (store selected result) and then into transition/setup calls (`0x800380d8`, `0x8003a46c`) before additional per-level setup (`0x80034d2c` onward).
+
+3. User-facing meaning of the sequence steps is now tighter:
+ - Step 1 (normal passcode with delta=>state 3) means: in the same passcode-entry screen, enter a normal code that reaches the non-special first-char delta lane and writes `psx_level_runtime_header_state = 3` at `0x8003ed58`.
+ - Step 3 (hidden passcode) also means: use that same passcode-entry screen/evaluator path to hit special decode `0x10` (`0x8003ed28`), not a separate hidden-code menu.
+
+4. Operational risk in the old sequence wording:
+ - Because hidden decode and normal decode share this screen and the caller path has distinct zero/nonzero branches, a literal read of "do in-level step 2, then return later for step 3" is not yet proven as one deterministic minimal player script.
+ - Current conservative status is therefore: branch predicates are closed, but exact user-visible ordering remains partially unresolved until the `param_2==0x0a,param_3==4` producer is observed in one live trace.
+
+Conservative verdict on sequence correctness:
+
+- The old sequence is directionally correct about the two-phase logic (`pre-hidden gate arm` + `hidden input trigger`),
+- but it is operationally under-specified and should not be treated as a guaranteed one-try recipe without the remaining caller-context closure.
+
+Confidence:
+
+- high (`~0.90`) that hidden and normal passcodes are entered through the same screen/evaluator path
+- high (`~0.85`) that the caller block has immediate branch-divergent behavior for return `0` vs nonzero
+- medium (`~0.64`) on exact practical player-order script, pending direct producer closure for the `0x0a/4` gate-arm event
+
+## Gate-arm caller path closure around 0x800230e4 (2026-04-11 live pass)
+
+Scope of this pass was the exact writer condition at `0x800232f0` and the concrete upstream state flow that can make it true before hidden+input grant.
+
+Direct code facts now pinned:
+
+1. The gate write at `0x800232f0` is in `psx_set_debug_extra_channel_gate` (`0x800230e4`) and is only reachable in the local jump-table branch for `param_2==0x0a` and `param_3==4`.
+ - jump table at `0x80010550` maps entry `4` to block `0x800232a0`
+ - that block performs `FUN_8002ba90()` and `FUN_8002ba78()` then evaluates hidden/state guards and conditionally stores `1` to `DAT_8006739d`
+
+2. The exact store predicate is now explicit and comment-anchored in-session:
+ - hidden must still be off: `DAT_80067454 == 0` (`0x800232c0`, `0x800232d0` branch)
+ - runtime header must be `3`: `DAT_80078ab0 == 3` (`0x800232dc`, `0x800232e4` branch)
+ - only then store: `sb 1, DAT_8006739d` at `0x800232f0`
+
+3. `runtime_header_state==3` comes from normal (non-special) passcode decode math, not the hidden/special branches.
+ - `psx_passcode_decode_to_mission_selector` (`0x8003ec8c`) normal lane writes:
+ - `DAT_80078ab0 = code[0] - (DAT_80064bbc[idx] + 0x1b)` at `0x8003ed58`
+ - accepted only when result `<=3`
+ - special branches do not produce the required prime state:
+ - index `0x10` (`?0SR` / hidden) sets `DAT_80067454=1`
+ - index `0x0f` (`?RTN`) forces `DAT_80078ab0=0`
+
+4. The strongest player-facing prime model is now: valid non-hidden mission/passcode entry in the normal decode lane with first-char delta resolving to `3`.
+ - this yields the required precondition `DAT_80078ab0==3` while hidden is still `0`
+ - once the `param_2==0x0a,param_3==4` event path runs, `DAT_8006739d` can latch to `1`
+
+5. Hidden+input grant remains a later, separate phase with opposite hidden flag polarity.
+ - grant entry in `psx_object_update_runtime_input_modes` requires `DAT_80067454!=0` and input code `0x1e` at `0x80013154..0x80013174`
+ - grant helper reads `DAT_8006739d` at `0x8002fff4` and only then executes extra `unlock(0x0d)` at `0x80030004`
+
+Additional caller-path closure from later passes:
+
+6. The gate writer is reached through an indirect level-gated dispatcher, not a direct passcode callback.
+ - dispatcher block at `0x800214ac..0x800215f8`
+ - `0x800215bc` compares opcode index against `DAT_80063e68[current_level]`
+ - `0x800215dc` calls `DAT_800640a0[index]` via `jalr`
+ - slot `0x0f` in `DAT_800640a0` resolves to `0x800230e4` (`psx_set_debug_extra_channel_gate`)
+ - handler byte args are loaded from action-record pointers at `0x800215cc` and `0x800215e0`
+ - current best narrowed trigger tuple is therefore `(dispatch slot 0x0f, param_2 0x0a, param_3 0x04)` in a small level-scripted family, not a manual UI/menu callback
+
+Practical sequence now supported by executable evidence:
+
+1. Enter a normal valid passcode whose first-char delta lane sets `runtime_header_state` to `3` (hidden remains off).
+2. Let gameplay continue until the in-level scripted/control event fires that reaches slot `0x0f` / `psx_set_debug_extra_channel_gate` with `param_2==0x0a,param_3==4`, so `DAT_8006739d` is armed.
+3. Enter hidden passcode (`?0SR` / canonical `L0SR` when selector is `0`); current passcode-screen flow evidence says this shared decoder branch sets hidden flag but skips the normal nonzero mission/apply-load branch.
+4. Perform input code `0x1e` (project mapping currently `R1+Circle`) to run debug grant; extra `0x0d` unlock executes because gate is already set.
+
+Confidence:
+
+- high (`~0.91`) on gate write predicate and branch identity (`0x800232a0` path)
+- high (`~0.88`) that `runtime_header_state==3` prime comes from normal decode lane, not special hidden/sibling specials
+- medium (`~0.62`) on one exact user-visible action name for step 2 (`param_2==0x0a,param_3==4` producer), because this pass closed the argument/value lane and surrounding passcode state flow but did not yet recover a single named UI handler with clean function boundaries in the `0x80022e8..0x800230e0` block
+
+## Executable-only JL-9 validation lane (2026-04-11)
+
+Scope of this pass was strict: recover an exact user-facing JL-9 enable sequence from executable evidence only across passcode decode, input chord decode, gate write, grant call, and selected-id commit path.
+
+Recovered chain (all executable-backed):
+
+1. Hidden passcode decode arm:
+ - `psx_passcode_decode_to_mission_selector` (`0x8003ec8c`) sets `psx_hidden_passcode_flag` at `0x8003ed28` on special decode index `0x10`.
+ - index `0x10` bypasses first-character validation and uses only the `entry[1..3]` transformed triplet check (`-0x1b`) against `0x80064bd0/0x80064be4/0x80064bf8`.
+
+2. Input chord decode to grant trigger:
+ - `psx_object_update_runtime_input_modes` (`0x80012c30`) gates the grant path on `psx_hidden_passcode_flag!=0` and decoded input code `0x1e` (`0x80013154..0x80013174`).
+ - `psx_input_map_install_profile` (`0x80042ec4`) maps code `0x1e` to pad mask `0x2800` in all recovered profile branches, so the practical chord remains `R1 + Circle` under the project pad-bit model.
+
+3. Extra JL-9 gate write:
+ - `psx_set_debug_extra_channel_gate` (`0x800230e4`) writes `psx_debug_extra_channel_gate=1` at `0x800232f0` only when both conditions hold:
+ - `psx_hidden_passcode_flag == 0` (`0x800232c0` / `0x800232d0` branch)
+ - `psx_level_runtime_header_state == 3` (`0x800232dc..0x800232e4`)
+
+4. Grant call and final extra unlock:
+ - `psx_object_update_runtime_input_modes` calls `psx_debug_grant_weapon_channels_and_ammo` at `0x80013174`.
+ - grant helper reads gate at `0x8002fff4`; if nonzero, branch at `0x80030004` unlocks channel `0x0d` (JL-9 lane).
+
+5. Selected-id commit sink used by watched selected-byte lane:
+ - `psx_apply_channel_effect_and_commit_selected_item_id` loads committed row id from `channel_commit_row_selected_item_id[(channel*10)+9]` at `0x8002f15c` and stores to nested runtime `+0x1c` at `0x8002f168`, also mirroring to `committed_selected_item_id` at `0x8002f170`.
+ - this is the executable-backed row-id commit path that underpins the observed selected-byte `00..0d` behavior (`0x0d` for JL-9).
+
+Exact-sequence blocker from executable evidence only:
+
+- one link remains unclosed for a fully deterministic player-facing recipe: the concrete user-visible action/context that drives the `param_2=='\\n'` / `case 4` path in `psx_set_debug_extra_channel_gate` (`0x800230e4`) while hidden flag is still `0`.
+- without that mapping, code proves a two-phase ordering requirement but not one exact minimal button/menu script that always arms the gate before hidden grant input.
+
+Minimal next probe to close the blocker:
+
+- in one live trace, breakpoint/log `0x800232f0` and caller args into `0x800230e4` (especially `param_2` / `param_3`) while performing candidate player actions around passcode entry and mode transitions.
+- first observed user-facing action that reaches this writer under `hidden=0 && runtime_header_state=3` closes the final exact-step gap.
+
+Focused live follow-up on `0x80067944` and the surrounding `0x80067938..0x80067958` block now narrows one unresolved identity question:
+
+- `0x80067944` has no recovered static xrefs in this image under word, halfword, or byte probe passes (`get_data_uses` + operand scans), so it is currently not supported as a selected local weapon id or selected row id owner.
+- `0x80067938` is reaffirmed as `psx_ctor_placement_section_ptr`, installed in `wdl_resource_bundle_load_by_index` and consumed in `psx_apply_deferred_control_command` as constructor/deferred-control section state, not as weapon selection state.
+- nearby block ownership remains mixed: `0x8006793c/0x80067940/0x80067948` are consumed by `psx_object_update_runtime_input_modes` for input-mode dispatch helpers, while `0x80067954/0x80067958` are draw/disp environment flip/progress state used by present/spec-upload helpers.
+- one unnamed helper was promoted conservatively from direct behavior evidence: `FUN_800461d0` -> `psx_draw_progress_overlay_and_swap_drawenv`.
+
+The newest user-provided emulator verification replaces the earlier `JL-?=11` shorthand with a stronger selected-weapon mapping, while the starter-only compare still retracts one earlier storage claim:
+
+- verified selected-weapon byte at `0x8014577e` maps directly as row-id domain:
+ - `00` no weapon / invalid gun
+ - `01` `RP-16`
+ - `02` `RP-22`
+ - `03` `RP-32`
+ - `04` `SG-A1`
+ - `05` `AC-88`
+ - `06` `PA-31`
+ - `07` `EM-4`
+ - `08` `PL-1`
+ - `09` `UV-9`
+ - `0A` `GL-303`
+ - `0B` `AR-7`
+ - `0C` `JL-2`
+ - `0D` `JL-9`
+- the new starter-only RAM compare shows `0x1456fc..0x145748` is static across dumps and therefore is not the owned-weapon inventory list
+- the dynamic region is instead `0x14574c..0x1457d0`, with the strongest current field closure at `0x14577e`:
+ - all-weapons dump byte at `0x14577e`: `0c`
+ - starter-only dump byte at `0x14577e`: `02`
+ - current best read: selected weapon row-id byte inside a nested runtime state block, not a contiguous owned-id slot array
+- the separate watched field at file offset `0x67944` still changes (`0x0000000b` vs `0x00000001`), but live executable passes did not recover direct static xrefs for `0x80067944`, so it remains an unproven watch field rather than a safe patch target
+
+New correction from live MCP reconciliation with the user-verified selected-weapon byte mapping:
+
+- treat the verified byte mapping at `0x8014577e` as the selected-weapon **row-id domain** (`00..0d`: none/invalid through `JL-9`).
+- keep the argument domain at `0x8002ef34` separate: callers pass a compact channel/local code, then `0x8002f15c` converts through `channel_commit_row_selected_item_id[(channel*10)+9]` into the committed row id that is written to the nested runtime field at `+0x1c` (`0x8002f168`).
+- practical correction: prior shorthand that could read as "local id equals committed row id" is too loose; the robust model is now `caller channel/local code -> commit table -> committed row id (00..0d)`, and the user-verified byte at `0x8014577e` belongs to that committed row-id domain.
+- static xrefs cannot directly prove absolute runtime RAM addresses such as `0x8014577e`, so address closure remains runtime-evidence-backed plus commit-path-backed rather than static-xref-backed.
+
+The strongest remaining unknown is no longer whether `JL-9` exists. It is now split into two narrower questions:
+
+- what exact runtime conditions make the late `JL-9` unlock visible in normal play timing,
+- and what `JL-2` actually is as a normal ammo-using weapon, since `JL-2 AMMO` is present while no matching plain `JL-9 AMMO` string has been recovered.
+
+## Gate and hidden-flag lifecycle closure (2026-04-11 live pass)
+
+Scope of this pass was the exact lifecycle of `psx_debug_extra_channel_gate` (`0x8006739d`) and `psx_hidden_passcode_flag` (`0x80067454`) across session init, passcode entry, and debug grant.
+
+Recovered reference set is now tight:
+
+- `psx_debug_extra_channel_gate` (`0x8006739d`):
+ - writer: `0x800232f0` in `psx_set_debug_extra_channel_gate`
+ - reader: `0x8002fff4` in `psx_debug_grant_weapon_channels_and_ammo`
+ - recovered static clears/resets: none
+- `psx_hidden_passcode_flag` (`0x80067454`):
+ - sets: `0x8003ed28` (`psx_passcode_decode_to_mission_selector`, special index `0x10`), `0x8002bab8` (`psx_hidden_passcode_arm_runtime_state`)
+ - clear/reset: `0x8002b9e4` (`psx_level_session_loop`, timer-expiry branch)
+ - readers: `0x80013154` (`psx_object_update_runtime_input_modes`), `0x800232c0` (`psx_set_debug_extra_channel_gate`)
+
+State-machine facts now closed:
+
+1. Extra unlock gate arm condition is strict and opposite to the grant-entry hidden condition.
+ - At `0x800232c0..0x800232f0`, `psx_set_debug_extra_channel_gate` writes `psx_debug_extra_channel_gate=1` only when:
+ - `psx_hidden_passcode_flag == 0`
+ - `psx_level_runtime_header_state == 3`
+
+2. Debug grant entry requires hidden mode to be active.
+ - At `0x80013154..0x80013174`, `psx_object_update_runtime_input_modes` returns early unless `psx_hidden_passcode_flag != 0`.
+ - If active, decoded input code `0x1e` calls `psx_debug_grant_weapon_channels_and_ammo`.
+
+3. Extra JL-9 unlock check is a separate late latch.
+ - At `0x8002fff4`, grant helper reads `psx_debug_extra_channel_gate`.
+ - If nonzero, branch at `0x80030004` unlocks channel `0x0d` (JL-9 lane).
+
+4. Hidden flag has explicit timed clear behavior; extra gate currently does not.
+ - `psx_hidden_passcode_arm_runtime_state` seeds `DAT_800673cc=2000` and sets hidden flag at `0x8002bab8`.
+ - `psx_level_session_loop` clears hidden flag at `0x8002b9e4` when timer reaches zero.
+ - No recovered static clear path writes `0` to `psx_debug_extra_channel_gate` in this image.
+
+Practical persistence answer for the requested A -> B -> `R1+Circle` model:
+
+- Strongest executable-backed result is yes: the gate can persist long enough for a two-phase flow because we recovered one set and no static clear for `psx_debug_extra_channel_gate`, while hidden mode can be armed later and used within its own timer window.
+- This supports sequence shape:
+ - step A: satisfy the non-hidden gate-arm condition (`hidden=0`, header-state `3`) so `psx_debug_extra_channel_gate` becomes `1`
+ - step B: enter hidden passcode (`0x10` decode branch, canonical `L0SR` form when selector is `0`) to arm hidden mode
+ - trigger: press input code `0x1e` (practical mapping in this project remains `R1 + Circle`) before hidden timer expiry
+ - effect: debug grant path runs and late extra branch includes `unlock(0x0d)`
+
+Confidence:
+
+- high (`~0.93`) on writer/reader/clear sets listed above (direct xref and instruction evidence)
+- high (`~0.90`) on two-phase ordering requirement (opposite hidden-flag conditions between gate arm and grant entry)
+- medium-high (`~0.78`) on persistence duration beyond hidden timer window, because no static clear is recovered for the extra gate but pointer-indirect or un-recovered dynamic writes are still theoretically possible
+
+## RP-16 status closure (2026-04-11 pass)
+
+Scope of this pass was narrowed to user-observed selected-weapon id `0x01` and whether it should be treated as a real usable RP-16 lane, an invalid slot, an earlier variant, or a startup placeholder.
+
+Direct executable findings:
+
+1. Weapon-definition row is real and populated.
+ - row `0x01` at `0x80064690` decodes as `RP-16` and carries nonzero row fields (`+0x1c=0x01`, `+0x20=0x03e8`, `+0x24=0x06`), unlike pure null/blank filler.
+2. Primary shop acquire lane does not include row `0x01`.
+ - `psx_weapon_shop_try_apply_entry` front path (`param_1 < 10`) uses direct unlock helper `psx_weapon_channel_unlock_and_seed_markers` and shop table bytes `03 04 05 06 07 08 09 0a 0b 0c`.
+ - practical consequence: direct shop unlock progression reaches `JL-2` (`0x0c`) but excludes `RP-16` (`0x01`) and `RP-22` (`0x02`).
+3. `0x01` appears in shop lookup, but in secondary ammo branch, not direct unlock branch.
+ - shop table slot `10` byte is `0x01` (`0x80064b9a`), but this path enters the `0x0a..0x0e` branch that calls `0x8002e32c` (ammo top-up helper), not unlock helper `0x8002e5f0`.
+ - this supports "defined id in economy tables" but not "normal explicit acquisition of RP-16 weapon row" from this lane.
+4. Hidden/debug lane remains focused on late ids, not `0x01`.
+ - `psx_debug_grant_weapon_channels_and_ammo` still closes around normal progression plus extra `0x0d` gate behavior; no new fixed immediate `0x01`-specific unlock site was recovered in this pass.
+
+HUD/name path closure for RP-16 in this pass:
+
+- full weapon-name identity remains row-driven: row `0x01` inline bytes at `0x80064690+2` decode to `RP-16`.
+- HUD short-label rendering uses selected-id-indexed lookup bytes in `FUN_800455d4` from tables around `0x80064e90` and `0x80064e9c` (selected id minus one).
+- this supports RP-16 as a represented display id, even where normal-lane unlock evidence is weaker than row existence.
+
+Current classification for RP-16 (best supported):
+
+- **not invalid/empty**: row is concrete and populated.
+- **not currently proven as normal direct unlock lane**: primary shop/loadout unlock path evidence emphasizes `>=0x03` progression and caps at `0x0c` in the direct shop lane.
+- **strongest fit right now**: real defined early weapon row that behaves like a legacy/startup/placeholder-capable entry in this image, with table presence and UI representation but no newly recovered dedicated normal acquisition proof in this pass.
+
+Confidence:
+
+- high (`~0.89`) that row `0x01` is real structured weapon data (`RP-16`), not random padding.
+- high (`~0.84`) that direct shop unlock progression excludes `0x01` in the primary acquisition branch.
+- medium (`~0.61`) on final gameplay role label (`legacy/startup/placeholder`) until one concrete non-debug in-mission acquisition or initialization writer is recovered.
+
+## RP-16 startup/default-init closure (2026-04-11 pass)
+
+Focused live MCP pass on active `SLUS_002.68` to answer only this question: does RP-16 (`row/id 0x01`) get seeded as startup/default weapon via fresh-game init, difficulty-driven starts, mode transitions, or mission/loadout init?
+
+Direct startup/init findings:
+
+1. Post-load reset explicitly clears selected-id state before init dispatch.
+ - `psx_level_post_load_runtime_reset` writes `committed_selected_item_id = 0` at `0x80039f68`.
+ - from the same function, startup path then dispatches mode actions (`8`, optionally `2`, then `4`), not a fixed `0x01` commit.
+
+2. Mission/loadout init does not perform a fixed RP-16 selected-id write.
+ - `psx_weapon_channels_init_mode_loadout` (`0x8002f814`) is mode-table driven from `psx_level_channel_table_80063e68` and applies unlock/ammo helpers via fallthrough; no fixed immediate `0x01` selected-id commit is present.
+ - `psx_weapon_channels_apply_mode_transition_state` (`0x8002f278`) sets active channel state to `2` or `3` in the observed startup branch (`0x8002f468` / `0x8002f49c`), not channel/id `0x01`.
+
+3. The selected-id global has only two recovered writers in this image.
+ - writer A: reset-to-zero at `0x80039f68`.
+ - writer B: table-based commit sink in `psx_apply_channel_effect_and_commit_selected_item_id` at `0x8002f170`.
+ - no dedicated startup writer with a fixed immediate `0x01` was recovered.
+
+4. Fixed-immediate commit callsites found in this pass do not support RP-16-as-default.
+ - recovered immediate dispatches use `a0=0x11`, `a0=0x12`, and one context-specific `a0=0x01` action lane in unnamed gameplay/control handlers (`0x8001ede8`, `0x8001ef08`, `0x8001f068`, `0x80021930`, `0x80022624`), not the named startup/loadout reset path.
+ - this keeps low-id usage structurally possible outside startup, but does not convert RP-16 into a proven default-start weapon.
+
+5. Default/loadout table context remains non-startup evidence.
+ - shop/channel map bytes at `0x80064b90` still include `0x01` (`... 0c 01 05 04 ...`), but this remains economy/action-path evidence rather than a startup seed proof.
+
+Current startup/default verdict for RP-16:
+
+- **not proven startup/default weapon** in fresh-game init, difficulty/mode-transition apply, or mission/loadout init paths recovered in this pass.
+
+## JL-9 producer-side authored-source closure pass (2026-04-11 live MCP)
+
+Scope of this pass was limited to the producer side of the action-record pointer frame consumed by
+`psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`), with priority on finding one concrete
+upstream authored context for tuple `(slot 0x0f, arg1 0x0a, arg2 0x04)`.
+
+### Strongest producer-side clue recovered
+
+The strongest new clue is now loader/constructor explicit:
+
+1. `psx_load_type_state_banks` (`0x800391f0`) installs `psx_type_simple_component_bank[type]`
+ from the level bundle type-state blob (`SPEC_A.WDL`/`L*.WDL` path via
+ `wdl_resource_bundle_load_by_index` at `0x8003977c`).
+2. `psx_object_create_simple_record` seeds component `program_base` and `pc` from that exact bank
+ at `0x80024c60` and `0x80024c88`.
+3. `psx_run_object_behavior_program_tick` then executes words from that component stream and calls
+ `psx_object_behavior_opcode_dispatch` at `0x80026740`.
+4. `psx_object_behavior_opcode_dispatch` routes opcode `54` through
+ `psx_behavior_opcode_handler_table` (`0x800641ac`) to `psx_behavior_subopcode_dispatch`
+ (`0x80027ecc`), which then routes subop `49` through `psx_behavior_subop_handler_table`
+ (`0x80063610`) to sink `0x800214ac`.
+
+Practical implication: the pointer-frame lane feeding slot/arg bytes into `0x800214ac` is now best
+modeled as authored type-state behavior program content loaded per level, not sink-local constants.
+
+### Tuple classification update
+
+Current best classification for `(0x0f,0x0a,0x04)` is now:
+
+1. **authored-static at source context** (type-state behavior payload in level bundle),
+2. then **runtime-index resolved** in dispatcher frame construction when mask bits request
+ `base + index*4` slot-pointer mapping.
+
+So this tuple is not strongest as a pure runtime remap invention; runtime remap appears to be the
+transport mechanism over authored behavior-program operands.
+
+### Exact live Ghidra changes in this pass
+
+Decompiler comments added:
+
+- `0x80039250`: producer provenance note on `psx_type_simple_component_bank[type]` install from
+ level/LSET type-state blob.
+- `0x80024c60`: constructor note that component program base/pc come from
+ `psx_type_simple_component_bank[type]` (upstream authored source lane).
+- `0x80026740`: behavior-tick note that opcode/mask/args are read from component pc stream and feed
+ `psx_object_behavior_opcode_dispatch`.
+
+No function renames were applied in this pass.
+
+### Remaining open item (narrowed)
+
+Still open is one concrete **type row + map context** instance whose loaded behavior stream emits the
+exact `54 -> 49` producer record that resolves to `(slot 0x0f, arg1 0x0a, arg2 0x04)` at runtime.
+The unresolved part is now specific row attribution, not producer subsystem identity.
+- **still a real executable-backed row** with non-startup lane presence.
+- **best current label**: placeholder/legacy-capable early row with unresolved normal acquisition role, not a demonstrated startup default.
+
+Confidence (startup/default question only):
+
+- high (`~0.86`) that current named startup/loadout/mode-transition initializers do not hard-seed selected id `0x01`.
+- medium (`~0.63`) that no indirect startup-side table commit resolves to `0x01` in unseen/unnamed init stubs, because several nearby callsites still live in undefined function ranges.
+
+## RP-16 startup/default recheck (2026-04-11 live MCP follow-up)
+
+This follow-up pass pushed specifically on undefined nearby init stubs, startup selected-id writes, active-channel writes, and difficulty/mode tables to test any indirect RP-16 (`0x01`) seed route.
+
+Direct findings:
+
+1. `committed_selected_item_id` still has exactly two recovered writes in this image.
+ - reset write: `0x80039f68` in `psx_level_post_load_runtime_reset` (`=0`)
+ - commit sink write: `0x8002f170` in `psx_apply_channel_effect_and_commit_selected_item_id`
+ - no third startup/default writer was recovered.
+
+2. Startup mode-action dispatch remains bounded and non-committing.
+ - startup callsites dispatch `mode_action=8` then `2`/`4` (`0x80039fa4`, `0x8003a014`, `0x8003a01c`; additional observed stub callsite at `0x8003e6a0` dispatching `8`).
+ - these actions drive loadout/transition/seed tables but do not directly call the selected-id commit sink.
+
+3. Active-channel writes in startup lanes do not imply RP-16 commit.
+ - `psx_weapon_channels_apply_mode_transition_state` sets `psx_marker_channel_runtime_block+0x32` to `2` or `3` in the startup branch.
+ - loadout unlock/ammo helpers only initialize when `+0x32==0`, then write the channel argument used by that helper path; no startup helper immediate observed here seeds channel `0x01` as selected-id commit.
+
+4. Difficulty/mode table path still does not resolve to selected-id `0x01`.
+ - `psx_level_channel_table_80063e68` feeds `DAT_80078a8c` in startup (`0x8002f2a0`, `0x8002f868`, `0x8002f978`, `0x80039fe8`).
+ - table-driven startup channels resolve into commit-row selected bytes at `channel_commit_row_selected_item_id` (`0x80064355`, `+9` in each 10-byte row), and scanned rows `0x00..0x19` contain no `sel=0x01` byte.
+
+5. Undefined nearby commit callsites found in this pass do not establish startup-default RP-16.
+ - recovered no-function callsites into commit helper include `0x8001ede8` and `0x8001ef08` (`a0=0x11`), plus additional gameplay/control lanes from prior passes.
+ - these are outside the named startup reset/loadout transition chain.
+
+Conservative live Ghidra artifact updates from this pass:
+
+- rename: `0x8002fd80` -> `psx_marker_channel_set_mode6_only`
+- decompiler comments:
+ - `0x80039f68` (startup reset selected-id clear)
+ - `0x8002f170` (commit sink table-write semantics)
+ - `0x8002f2a0` (mode-table source and non-`0x01` implication)
+- disassembly comments:
+ - `0x80064355` (selected-id table semantics and no `0x01` in scanned rows)
+ - `0x8003e6a0` (observed startup/transition stub dispatching mode action `8`)
+
+Updated verdict after this follow-up:
+
+- startup/default RP-16 remains **ruled out in recovered startup/init code paths** for active `SLUS_002.68`.
+- RP-16 (`0x01`) remains a real row and remains reachable in non-startup contexts, but this pass found no evidence of startup/difficulty/mode-init seeding to selected-id `0x01`.
+
+## Legit Acquisition Closure (2026-04-11 pass)
+
+Scope of this pass was narrowed to legitimate `JL-9` acquisition paths versus hidden/debug leftovers, with emulator verification treated as ground truth for selected weapon id domain (`0x8014577e`, `0x0c=JL-2`, `0x0d=JL-9`).
+
+Direct executable findings by lane:
+
+1. Normal loadout lane (`psx_weapon_channels_init_mode_loadout`):
+ - fallthrough switch seeds baseline unlock/ammo progression and reaches ordinary channels, but does not perform a fixed immediate unlock of channel `0x0d`
+ - this lane supports normal progression to `JL-2` (`0x0c`) and earlier channels, not a direct hardcoded `JL-9` grant
+
+2. Shop lane (`psx_weapon_shop_try_apply_entry`):
+ - unlock front-path is gated by `param_1 < 10`
+ - `DAT_80064b90[0..9]` is `03 04 05 06 07 08 09 0a 0b 0c`
+ - practical result: direct shop unlock path is capped at `0x0c` (`JL-2`) and does not directly issue `0x0d` (`JL-9`)
+
+3. Scripted packed-action/pickup lane (`psx_section0_dispatch_root_apply_packed_channel_actions`):
+ - action type `3` dispatches `psx_weapon_channel_unlock_and_seed_markers(channel_byte)` from decoded triplet data
+ - triplet table is seeded at runtime by `psx_section0_dispatch_root_seed_marker_channel_table` from section0 marker records
+ - this means scripted non-debug `0x0d` is structurally possible only if authored level marker data actually supplies channel `0x0d`; this pass did not recover a shipped-map proof row that does so in normal play
+
+4. Hidden/debug lane (`psx_object_update_runtime_input_modes` -> `psx_debug_grant_weapon_channels_and_ammo`):
+ - `psx_hidden_passcode_flag` gate at `0x80013154` must be active before input code `0x1e` can call debug grant
+ - debug grant always unlocks through `0x0c`, and conditionally unlocks `0x0d` only when `psx_debug_extra_channel_gate` is nonzero
+ - this remains the only recovered fixed-immediate `unlock(0x0d)` call site (`0x80030004`) in the current executable
+
+Current verdict from executable evidence:
+
+- strongest supported path to `JL-9` remains hidden/debug-conditioned
+- strongest supported normal gameplay acquisition lanes close at `<= 0x0c` (`JL-2`)
+- non-debug scripted `0x0d` remains the only plausible legitimate exception, but is still unproven without a concrete shipped section0 marker/action row
+
+Confidence:
+
+- high (`~0.87`) that fixed-code normal/shop paths do not directly grant `JL-9`
+- high (`~0.90`) that hidden/debug path can grant `JL-9` via the `0x0d` conditional unlock
+- medium (`~0.52`) on whether shipped non-debug section0 data ever drives a legitimate scripted `0x0d` unlock
+
+## Executable-side condition closure (2026-04-11 pass)
+
+Focused live MCP pass on active `SLUS_002.68` to close the exact gate chain around your verified selected-weapon byte watch (`0x8014577e`) and the JL-9 late unlock lane.
+
+What this pass closes directly:
+
+1. `psx_debug_extra_channel_gate` (`0x8006739d`) is still a one-writer/one-reader gate in this image:
+ - writer: `psx_set_debug_extra_channel_gate` (`0x800232f0`)
+ - reader: `psx_debug_grant_weapon_channels_and_ammo` (`0x8002fd90`) at `0x8002fff4`
+2. `psx_hidden_passcode_flag` (`0x80067454`) writer/reader set is now explicit:
+ - writers: `psx_passcode_decode_to_mission_selector` (`0x8003ed28`), `psx_hidden_passcode_arm_runtime_state` (`0x8002ba9c`), and clear/reset points in `psx_level_session_loop` (`0x8002b9e4`)
+ - readers: `psx_set_debug_extra_channel_gate` (`0x800232c0`) and `psx_object_update_runtime_input_modes` (`0x80013154`)
+3. Unlock-capable call families for channel `0x0d` are now bounded:
+ - debug bulk grant: `psx_debug_grant_weapon_channels_and_ammo` (always includes `0x0c`, includes `0x0d` only when `psx_debug_extra_channel_gate != 0`)
+ - scripted packed actions: `psx_section0_dispatch_root_apply_packed_channel_actions` can call `psx_weapon_channel_unlock_and_seed_markers(channel)` for data-driven channel bytes
+ - normal loadout/shop paths call the same unlock helper but are mode/slot constrained; shop front path (`param_1 < 10`) maps channels `0x03..0x0c` and does not directly include `0x0d`
+4. Commit/read chain for visible selected weapon remains stable:
+ - commit: `psx_apply_channel_effect_and_commit_selected_item_id` -> `channel_commit_row_selected_item_id[(channel*10)+9]`
+ - sinks: nested player runtime field (`... + 0x1c`) and `committed_selected_item_id` (`0x80078a90`)
+ - consumers include spawn/HUD-adjacent lanes (`FUN_80014d04`, `FUN_80014eac`, `psx_spawn_contact_burst_simple_records`)
+
+`0x8014577e` closure status in this pass:
+
+- direct static xrefs from `get_data_uses(0x8014577e)` are still empty (expected for heap/runtime fields)
+- executable-side chain strongly supports that this watch sits in the same nested player runtime object family as the committed selected-id lane, but this pass did not recover one direct static instruction with absolute `0x8014577e`
+
+Practical classification after this pass:
+
+- strongest debug-only lane: hidden-passcode-gated input path that reaches `psx_debug_grant_weapon_channels_and_ammo`, then conditionally unlocks `0x0d` through `psx_debug_extra_channel_gate`
+- plausible shipped-reachable lane: section0/script packed-action dispatcher can grant channels by data (including potential `0x0d` if authored data uses it), but no new map/script row proving shipped non-debug `0x0d` authoring was recovered in this pass
+- strongest normal-lane evidence remains `<= 0x0c` via loadout/shop constraints and earlier commit-table/model work
+
+## Final JL-9 enable sequence closure (2026-04-11 pass)
+
+Focused live MCP pass on active `SLUS_002.68` against the exact target points:
+
+- `psx_hidden_passcode_arm_runtime_state` (`0x8002ba9c`)
+- `psx_set_debug_extra_channel_gate` write site (`0x800232f0`, inside function entry `0x800230e4`)
+- `psx_debug_grant_weapon_channels_and_ammo` (`0x8002fd90`)
+- extra gate read / extra unlock branch (`0x8002fff4` / `0x80030004`)
+
+### Direct gate facts now closed
+
+1. Final extra JL-9 unlock is definitely gated by `psx_debug_extra_channel_gate`:
+ - disassembly at `0x8002fff4` reads `lbu v0,0x9d(gp)` (`DAT_8006739d`)
+ - if nonzero, branch executes `jal 0x8002e5f0` with `a0=0x0d` at `0x80030004`
+ - this is the only extra post-`0x0c` unlock site in this helper and maps to channel/id `0x0d` (`JL-9` lane)
+
+2. The extra gate is written only under a strict precondition:
+ - writer instruction at `0x800232f0` stores `1` to `DAT_8006739d`
+ - immediate guards in the same block require:
+ - `psx_hidden_passcode_flag == 0` (`lw v1,0x7454(v1)` then `bne v1,zero,...`)
+ - `psx_level_runtime_header_state == 3` (`lw v1,-0x7550(v1)` then `bne v1,3,...`)
+ - practical write condition is therefore:
+ - set extra gate only when hidden-passcode flag is currently **off** and runtime-header state is `3`
+
+3. Calling the grant helper requires the opposite hidden-flag condition:
+ - in `psx_object_update_runtime_input_modes` (`0x80013154`), debug path returns early unless `psx_hidden_passcode_flag != 0`
+ - only then can decoded input code `0x1e` reach `psx_debug_grant_weapon_channels_and_ammo` (`call at `0x80013174`)
+
+4. Hidden-passcode flag lifecycle remains bounded and explicit:
+ - set in `psx_passcode_decode_to_mission_selector` (`0x8003ed28`, special index `0x10` path)
+ - armed/set in `psx_hidden_passcode_arm_runtime_state` (`0x8002ba9c`) with timer seed `DAT_800673cc=2000`
+ - cleared/reset in `psx_level_session_loop` (`0x8002b9e4`) when timed armed state expires
+
+### Exact precondition model for JL-9 extra unlock
+
+To execute the extra `0x0d` unlock inside `psx_debug_grant_weapon_channels_and_ammo`, executable evidence now requires both:
+
+- `psx_debug_extra_channel_gate != 0` at `0x8002fff4` (gate read)
+- hidden debug input path active (`psx_hidden_passcode_flag != 0` and input code `0x1e`) to enter the helper
+
+And the gate itself is only set in a prior state where:
+
+- `psx_hidden_passcode_flag == 0`
+- `psx_level_runtime_header_state == 3`
+
+### One hidden action or two?
+
+Current executable-side verdict is now:
+
+- strongest model is a **two-phase hidden flow** for guaranteed JL-9 extra unlock, because gate-write and grant-entry require opposite hidden-flag states (`==0` vs `!=0`)
+- a single hidden action could only be sufficient if one action simultaneously establishes both states across time (for example gate already latched from an earlier phase), which is not directly proven in this pass
+
+Safest practical sequence from code evidence alone:
+
+1. Reach the gate-write condition (`hidden flag off`, runtime-header state `3`) so `psx_debug_extra_channel_gate` is latched.
+2. Arm/reactivate hidden-passcode state (`psx_hidden_passcode_flag != 0`).
+3. Trigger decoded input code `0x1e` to call `psx_debug_grant_weapon_channels_and_ammo`.
+4. Helper runs normal grants through `0x0c`, then executes extra `0x0d` unlock because gate is already set.
+
+Confidence for this sequence shape: high (`~0.86`) from direct write/read disassembly and guarded call flow; medium (`~0.63`) on exact player-facing button/passcode choreography for each phase.
+
+## Special passcode priming closure (?0SR / ?RTN / ?QQQ) (2026-04-11 live pass)
+
+Scope of this pass was to answer one specific question: can one of the recovered special passcodes (`?0SR`, `?RTN`, `?QQQ`) by itself prime `psx_level_runtime_header_state` or `psx_debug_extra_channel_gate` so JL-9 enable is one-code, or does executable evidence still force a two-code flow.
+
+Direct decode/write facts (all from `psx_passcode_decode_to_mission_selector`, `0x8003ec8c`):
+
+1. `?RTN` branch (special index `0x0f`) writes `psx_level_runtime_header_state = 0` at `0x8003ed10`.
+2. `?0SR` branch (special index `0x10`) writes `psx_hidden_passcode_flag = 1` at `0x8003ed28`.
+3. `?QQQ` branch (special index `0x11`) returns sentinel `0x12` and does not write `psx_hidden_passcode_flag` or `psx_level_runtime_header_state` in that branch.
+4. All three special branches bypass the normal first-char delta calculation path that writes runtime-header state in the non-special lane (`0x8003ed3c..0x8003ed58`).
+
+Direct gate-arm facts (from `psx_set_debug_extra_channel_gate`, `0x800230e4` / store at `0x800232f0`):
+
+1. `psx_debug_extra_channel_gate` is set to `1` only when both are true at that branch:
+ - `psx_hidden_passcode_flag == 0` (`0x800232d0` guard)
+ - `psx_level_runtime_header_state == 3` (`0x800232e4` guard)
+2. This gate is read later at `0x8002fff4` in `psx_debug_grant_weapon_channels_and_ammo` for the extra `unlock(0x0d)` branch.
+
+Implication for one-code versus two-code:
+
+- None of `?0SR` / `?RTN` / `?QQQ` can by itself satisfy the gate-arm condition for JL-9 extra unlock:
+ - `?RTN` forces header state to `0`, not `3`.
+ - `?0SR` forces hidden flag to `1`, but gate arm requires hidden flag `0`.
+ - `?QQQ` does not set header state to `3` and does not arm the gate by itself.
+- So the strongest executable-backed result remains a two-phase flow where gate arm must come from a separate pre-hidden state, then hidden mode is armed for the input-triggered grant.
+
+Strongest exact passcode sequence currently provable:
+
+1. Prime phase: use a non-hidden passcode/state path that leaves `psx_hidden_passcode_flag==0` and reaches `psx_level_runtime_header_state==3` before the `0x800232f0` gate-arm check runs.
+2. Hidden phase: enter `?0SR` (canonical `L0SR` when selector is `0`) to set `psx_hidden_passcode_flag=1`.
+3. Trigger phase: execute input code `0x1e` to enter `psx_debug_grant_weapon_channels_and_ammo`; with gate latched, helper executes extra `unlock(0x0d)`.
+
+Practical status of the sibling specials in this model:
+
+- `?RTN`: anti-prime for JL-9 gate arm (explicitly clears runtime-header state).
+- `?QQQ`: neutral/side-effect-special in this context (no recovered direct gate-prime write).
+
+Confidence:
+
+- high (`~0.91`) that recovered special passcodes do not directly prime JL-9 gate arm by themselves (direct branch/store evidence)
+- high (`~0.88`) that the strongest current model is two-phase rather than one-code for guaranteed `0x0d` extra unlock
+- medium (`~0.66`) on which exact non-special prime code is used in real play, because this pass intentionally focused on the recovered special-code trio and gate predicates
+
+## Core proven facts
+
+### 1. Weapon table identity is direct
+
+- weapon-definition table base is `0x8006466a`
+- row stride is `0x26`
+- `JL-2` row is `0x80064832` (index `0x0c`)
+- `JL-9` row is `0x80064858` (index `0x0d`)
+
+Recovered visible order remains:
+
+1. `INVALID GUN`
+2. `RP-16`
+3. `RP-22`
+4. `RP-32`
+5. `SG-A1`
+6. `AC-88`
+7. `PA-31`
+8. `EM-4`
+9. `PL-1`
+10. `UV-9`
+11. `GL-303`
+12. `AR-7`
+13. `JL-2`
+14. `JL-9`
+
+The strongest stable row split is:
+
+- shared: `+0x1c = 0x18`, `+0x23 = 0x0e`
+- diverged: `JL-2 +0x24 = 0x4b`, `JL-9 +0x24 = 0x0f`
+
+### 1b. Compact local id to weapon row id conversion is table-driven
+
+The runtime values near selected/inventory state (`0x0001`, `0x0002`, `0x000b`, `0x000c`) now reconcile cleanly with the weapon table as follows:
+
+1. Callers pass a compact channel/local code to `psx_apply_channel_effect_and_commit_selected_item_id` (`0x8002ef34`) (for example at `0x8001d3fc`).
+2. `0x8002f15c` then loads the committed weapon id from `channel_commit_row_selected_item_id[(channel*10)+9]`.
+3. That committed id is consumed as the weapon-table index in `psx_weapon_def_get_u16_with_mode_gate` (`0x800315d8`), which resolves `idx*0x26` from base `0x8006466a`.
+
+This means the caller-side compact channel/local code to final selected weapon row-id step is implemented through channel-table lookup, not a dedicated global `+1` conversion helper.
+
+Concrete JL pair from this path:
+
+- compact local/channel `0x0b` resolves through table slot `+9` to committed row `0x0c` (`0x80064832`, `JL-2`)
+- compact local/channel `0x0c` resolves through table slot `+9` to committed row `0x0d` (`0x80064858`, `JL-9`)
+
+This matches the stronger user-verified selected-weapon byte mapping where `0x0c = JL-2` and `0x0d = JL-9` at `0x8014577e`.
+
+Correction for domain clarity:
+
+- the user-verified selected-weapon byte mapping (`00 none`, `01 RP-16`, `02 RP-22`, `03 RP-32`, `04 SG-A1`, `05 AC-88`, `06 PA-31`, `07 EM-4`, `08 PL-1`, `09 UV-9`, `0A GL-303`, `0B AR-7`, `0C JL-2`, `0D JL-9`) is row-id-domain and aligns with weapon table row identities.
+- this does not contradict the `0x8002ef34` call path: it clarifies that the call argument and the committed/stored selected id can be from different domains linked by the `channel_commit_row_selected_item_id` lookup.
+
+### 2. Extra late unlock maps to JL-9
+
+The strongest current chain is direct and no longer only inferential:
+
+1. `psx_apply_channel_effect_and_commit_selected_item_id` (`0x8002ef34`) commits the selected item id from `channel_commit_row_selected_item_id`.
+2. the channel `0x0d` row commits item id `0x0d`.
+3. weapon-definition helpers index rows as `0x8006466a + idx*0x26`.
+4. row `0x0d` is `0x80064858`, whose visible name bytes are `JL-9`.
+5. row `0x0c` is `0x80064832`, whose visible name bytes are `JL-2`.
+
+This closes the late extra lane as:
+
+- `0x0c` = `JL-2`
+- `0x0d` = `JL-9`
+
+## Upstream producer structure recovery for 0x800214ac (2026-04-11 live pass)
+
+Scope of this pass was restricted to upstream production of the action-record pointer bytes consumed by `psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`).
+
+### Best recovered producer structure (current strongest model)
+
+Recovered chain in live `SLUS_002.68`:
+
+1. `psx_behavior_subopcode_dispatch` (`0x80027ecc`) calls subop handler table entry with:
+ - context argument from `local_60[10]`
+ - action-record pointer frame from `local_60+1`
+2. `psx_level_gate_slot_dispatch_from_action_record` (`0x800214ac`) receives that frame as `record` and consumes:
+ - `record+0x00` pointer -> mode byte (`*(*(record+0x00))`)
+ - `record+0x04` pointer -> slot byte (`*(*(record+0x04))`)
+ - `record+0x08` pointer -> `arg1` byte (`*(*(record+0x08))`)
+ - `record+0x0c` pointer -> `arg2` byte (`*(*(record+0x0c))`)
+3. Upstream per-argument pointer production occurs in `psx_object_behavior_opcode_dispatch` (`0x8002677c`):
+ - if argument mask bit is clear, argument word is used as direct pointer
+ - if set, argument word is treated as slot index and resolved by `base + index*4`
+ - resolver base is object-local slot table (`*(object_state+0x18)`), except when mask `0x100` forces global base `DAT_800789e0`
+
+This makes the producer structure a pointer-vector frame (`local_60`) populated by opcode-dispatch argument resolution, then forwarded to subop handler `49` (`0x800214ac`) through `psx_behavior_subop_handler_table`.
+
+### Concrete authored-data clue (bounded but real)
+
+The sink tuple still closes as `(slot,arg1,arg2)=(0x0f,0x0a,0x04)` for the gate arm branch, but upstream formation is now better constrained:
+
+1. These values are not required to be hardcoded literals in executable code.
+2. They are produced as dereferenced bytes via pointer fields in the behavior action-record frame.
+3. Because each pointer field can be either direct or slot-index-resolved (`base + index*4`), authored behavior script data can feed this tuple either as direct byte pointers or as slot-table indices resolved at runtime.
+
+This is the strongest concrete authored-data clue recovered in this pass: the tuple is behavior-script produced pointer data, not a fixed immediate triple in the sink function.
+
+### Exact Ghidra changes applied in this pass
+
+1. Function rename:
+ - `0x800268a4`: `FUN_800268a4` -> `psx_behavior_arg_index_to_slot_ptr`
+2. Decompiler comments:
+ - `0x80026814`: argument resolver semantics (`index*4` pointer mapping and base selection)
+ - `0x80027f0c`: producer frame field map (`local_60[0..4]` plus context at `local_60[10]`)
+ - `0x800215cc`: sink-side field mapping (`record+4/+8/+0c` to slot/arg1/arg2)
+
+Conservative confidence split:
+
+- high (`~0.9`) on pointer-frame field mapping and resolver behavior (direct decompile evidence)
+- medium (`~0.65`) on exact authored object/mission row currently producing `(0x0f,0x0a,0x04)` in shipped gameplay without runtime trace
+
+### 3. Hidden passcode path reaches the JL-9 lane
+
+The strongest recovered control path is:
+
+1. `psx_passcode_decode_to_mission_selector` (`0x8003ec8c`) sets `psx_hidden_passcode_flag`
+2. `psx_object_update_runtime_input_modes` checks `psx_hidden_passcode_flag`
+3. on gated input code `0x1e`, it calls `psx_debug_grant_weapon_channels_and_ammo`
+4. that helper always unlocks through `0x0c` and conditionally unlocks `0x0d` when `psx_debug_extra_channel_gate != 0`
+
+This is the strongest current executable-backed explanation for how the extra PSX-only lane reaches `JL-9`.
+
+The newest narrow passes now sharpen the trigger story in a more useful way:
+
+- code now clearly supports `hidden passcode active -> gated input code 0x1e -> psx_debug_grant_weapon_channels_and_ammo -> extra 0x0d unlock behind psx_debug_extra_channel_gate`
+- input-map closure now ties `0x1e` to pad mask `0x2800`, which is high-confidence `R1 + Circle` under the active digital-pad bit layout
+- the remaining JL-9 trigger uncertainty is no longer the chord; it is the exact pre-hidden player-visible action that reaches the gate-arm writer path at `0x800232f0`
+
+### 4. JL-2 is now the stronger normal ammo-lane unknown
+
+The current practical split is:
+
+- `JL-2` sits in ordinary acquisition space (`0x0c`)
+- `JL-9` sits in the extra hidden/debug-conditioned late lane (`0x0d`)
+- the visible string `JL-2 AMMO` exists at `0x800642b6`
+- no matching plain `JL-9 AMMO` string has been recovered in the same string family
+
+That does not fully decode `JL-2`, but it does shift the next unknown toward `JL-2` rather than back toward `JL-9` existence proof.
+
+### 5. The checked dump is VRAM, not slot RAM
+
+The user-supplied `binary/Crusader - No Remorse Memdump Weapons.bin` is now best read as a PSX VRAM capture:
+
+- file size is exactly `0x100000` (`1,048,576`) bytes, matching PlayStation VRAM size
+- the dump contains VRAM-like repeated 16-bit pixel patterns rather than compact inventory rows
+- inspection found HUD and icon-atlas style regions, but not a direct weapon-slot RAM structure
+
+Current safest conclusion:
+
+- the dump is useful as a presentation artifact
+- it is not a direct source for weapon-slot storage values
+- it does not currently distinguish `JL-2` from `JL-9` with confidence
+
+### 6. The checked 2 MiB main RAM dump is plausible RAM, but still inconclusive
+
+The user-supplied `binary/Crusader - No Remorse Weapons Main Ram.bin` is not rejected as the wrong broad domain.
+
+Current safest read:
+
+- file size is `0x200000` (`2,097,152`) bytes, matching PlayStation main RAM size
+- no plain ASCII `JL-2`, `JL-9`, or `JL-` strings were recovered in the dump
+- no immediately self-identifying weapon-slot table was recovered from this pass alone
+
+So the main-RAM dump currently lands in a narrower state than the VRAM dump:
+
+- it is plausible runtime state memory,
+- but not yet a self-decoding proof of whether the selected `JL-?` weapon with `10` clips and `0` loaded bullets is `JL-2` or `JL-9`,
+- because the identity still appears to depend on numeric runtime ids that must be cross-referenced back to the executable-side weapon table and HUD/name resolver logic.
+
+The newest inventory-oriented pass did at least tighten where to look next inside that dump:
+
+- candidate compact 16-byte slot-like records were noticed very early in the file (for example around file offsets `0xA0`, `0xB0`, `0xC0`)
+- those records are not self-labeled and were not enough on their own to map `RP-32 ... JL-?`
+- the most useful next RAM step is now to map one confirmed executable-side inventory or HUD pointer onto the dump, instead of continuing a blind whole-image sweep
+
+The starter-only compare forces a narrower and more defensible RAM read:
+
+- file offset `0x1456fc` does still begin a compact sequence of 8-byte records whose first 16-bit field climbs `0x0002 .. 0x000b`
+- however, the same sequence is unchanged in the starter-only dump, so it cannot be the live owned-weapon inventory list
+- the dynamic block starts immediately after that static table, at `0x14574c`, and remains changed through at least `0x1457d0`
+- the strongest current field closure inside that block is:
+ - byte `0x14577e = 0x0c` in the all-weapons dump
+ - byte `0x14577e = 0x02` in the starter-only dump
+ - executable-side write path now supports this as selected/committed weapon row-id state inside a player/runtime state block
+- `0x67944` separately changes from `0x0000000b` to `0x00000001`, but this field still lacks direct static ownership in the current image and should not be treated as a closed selected-weapon home yet
+
+Current strongest practical consequence:
+
+- the earlier `0x145744 -> 0x000c` / `0x67944 -> 0x0000000c` patch recommendation is retracted
+- current evidence supports a channel-state ownership model plus a dynamic current-weapon row field at `0x14577e`, not a contiguous owned-id inventory list that can be safely patched in place from this evidence alone
+- the next RAM step should target ownership-state bytes/flags in the dynamic runtime block and the channel-runtime block model, not the static `0x1456fc` table
+
+The latest follow-up also narrows one false lead and one stronger lead inside the same dump:
+
+- the candidate commit-table field at `DAT_80064355[(channel*10)+9]` does not show plain `0x0c` / `0x0d` values in the sampled rows of this dump, so that exact byte is not acting like a direct final JL row id here
+- however, denser table-like clusters of `0x0c` / `0x0d` do appear much later in RAM, especially around file offsets `0x133000` and nearby `0x133416` / `0x1335d4`, which are now stronger candidates for real runtime weapon-slot or inventory-state structures than the earlier blind slot guess
+
+The newest pass tightened those late clusters one step further:
+
+- the `0x133000` neighborhood now looks more like a compact fixed-record runtime table than random data, with the strongest candidate stride landing near `0x40`
+- values in that area are dominated by small enum-like bytes such as `0x0d` and `0x0f`, which fit a runtime index/flag table better than text or raw pixel data
+- one additional pass also found an alternative clue region around `0x422c..0x4440` where triplet-like patterns such as `0x0c 0x0a 0x00` could plausibly encode a `JL-2 / 10 clips / 0 loaded` style state, but that interpretation is not yet strong enough to replace the `0x133000` cluster as the main RAM lead
+- the remaining blocker is now very specific: one executable-side HUD/inventory anchor still has to be tied to one of these RAM regions before the dump can be read as a confirmed runtime weapon list
+
+Current best practical consequence:
+
+- no ask-user re-dump was forced from this pass, because the file does look like the right broad memory domain,
+- but a stronger runtime closure still needs either a more targeted live memory capture around the player/inventory struct or a resolved id->name/UI chain inside the executable.
+
+## Evidence detail
+
+### Weapon table and row consumers
+
+- `psx_weapon_def_get_u16_with_mode_gate` (`0x800315d8`) computes `idx*0x26` and reads row fields from the `0x8006466a` family
+- `psx_weapon_def_apply_spawn_profile_by_index` (`0x8003d02c`) fans row fields into live-named globals:
+ - `psx_weapon_spawn_type`
+ - `psx_weapon_spawn_audio_event_id`
+ - `psx_weapon_spawn_state_selector`
+- current best art read stays narrow:
+ - `JL-2` and `JL-9` share base type/art lane via `+0x1c = 0x18`
+ - they diverge on selector/state lane via `+0x24 = 0x4b` vs `0x0f`
+
+### Channel commit and HUD presentation
+
+- `channel_commit_row_table` is the per-channel commit source table
+- `committed_selected_item_id` mirrors the committed item id after `0x8002f15c`
+- `psx_hud_draw_selected_item_tile_bar` (`0x800424ac`) is the strongest currently named HUD-side consumer lane
+- `psx_ui_color_cycle_state`, `psx_hud_selected_item_color`, `psx_hud_selected_tile_color_a`, and `psx_hud_selected_tile_color_b` now cover the nearby UI-tail state that was previously left as raw `DAT_` labels
+- latest pass also tightened the selected/equipped chain with fresh comments at `0x8002ef34`, `0x8002f15c`, `0x800315d8`, `0x8003d02c`, and `0x800424ac`, plus explicit extra-unlock naming at `0x80030004` -> `psx_weapon_channel_unlock_and_seed_markers`
+- latest pass also renamed the known writer for the extra late unlock gate: `0x800232f0` -> `psx_set_debug_extra_channel_gate`
+- the newest input-side pass also renamed `0x8001E37C` -> `psx_handle_special_input_code`, which is now the strongest current upstream candidate for consuming the special `0x1e` debug-trigger code range
+- the newest ammo-side pass also tightened the marker/runtime helper family with live names such as `psx_marker_channel_mode_is_enabled`, `psx_marker_channel_get_mode_step_value`, `psx_marker_channel_runtime_state_snapshot`, `psx_marker_channel_runtime_state_restore`, and `psx_marker_channel_dispatch_mode_action`, which are now the strongest nearby candidates for the actual ammo/count mutation lane even though the exact `clips` versus `loaded bullets` fields are still not closed
+
+- Evidence note: no alternate per-slot source was found — HUD item id is resolved via the channel commit table (DAT_80064355[(channel*10)+9]) -> committed_selected_item_id -> weapon-definition table at `0x8006466a` -> `psx_weapon_def_apply_spawn_profile_by_index` -> HUD draw.
+
+This lane helps explain what the HUD is drawing, but it is still weaker than the channel-commit chain for direct `JL-2` vs `JL-9` identity.
+
+Confirmed resolver chain (evidence-backed):
+
+- commit source: `DAT_80064355[(channel*10)+9]` is loaded at `0x8002f15c` and supplies the committed item id for a channel.
+- id -> row resolver: `psx_weapon_def_get_u16_with_mode_gate` (`0x800315d8`) computes `idx * 0x26` and indexes the weapon-definition table at `0x8006466a` to select the weapon row.
+- row -> display: `psx_weapon_def_apply_spawn_profile_by_index` (`0x8003d02c`) fans row fields into spawn/art/selectors consumed by HUD code; `psx_hud_draw_selected_item_tile_bar` (`0x800424ac`) consumes those selectors to draw the on-screen tile/art. Name/label bytes live in the weapon row and are therefore resolved implicitly by the `idx*0x26` table access rather than a separate string-lookup UI helper.
+
+ - `0x8003ec8c` -> `psx_passcode_decode_to_mission_selector`
+ - `0x80021138` -> `psx_passcode_apply_mission_selector_and_stage`
+- transformed compare tables:
+ - `0x80064bbc`
+ - `0x80064bd0`
+ - `0x80064be4`
+ - `0x80064bf8`
+- key hidden decoder behavior:
+ - hidden index `0x0f`: writes `psx_level_runtime_header_state = 0`, returns selector `0x10`
+ - hidden index `0x10`: writes `psx_hidden_passcode_flag = 1`, returns selector `0`
+ - hidden index `0x11`: returns selector `0x12`
+
+Current safest read is that the hidden compare path is real and transformed/table-driven, even though the exact folklore string mapping is still not fully closed at runtime.
+
+## Current interpretation
+
+### What is now solid
+
+- `JL-9` is real
+- `JL-9` is the extra late hidden/debug lane
+- `JL-2` is the neighboring ordinary lane and now has stronger explicit ammo-label evidence
+- the checked dump is VRAM, not direct slot RAM
+- the checked 2 MiB main RAM dump is plausible runtime RAM, but still needs executable-side id resolution before it can close `JL-2` vs `JL-9`
+
+### What is still open
+
+- exact runtime timing for `psx_hidden_passcode_flag` and `psx_debug_extra_channel_gate`
+- exact role of the denser `0x0c` / `0x0d` RAM clusters near `0x133000`, and whether one of them is the real live inventory/slot table
+- exact executable-side anchor that proves whether the main RAM lead is the `0x133000` cluster or the smaller `0x422c..0x4440` candidate region
+- direct UI label/display resolver that prints `JL-9` from committed runtime state in one instruction chain
+- exact `JL-2` ammo decrement/storage path
+- exact `JL-9` sprite/frame identity
+- exact shipped-map placement for either JL row
+
+## Input code `0x1e` button-chord closure (2026-04-11 pass)
+
+Focused live MCP pass on active `SLUS_002.68` to close the exact input-side decode path around:
+
+- `psx_object_update_runtime_input_modes` (`0x80012c30`)
+- `psx_input_map_get_code_and_edge` (`0x8002adbc`, renamed this pass)
+- `psx_input_map_update_state_for_pad` (`0x8002abe0`, renamed this pass)
+- `psx_input_map_install_profile` (`0x80042ec4`, renamed this pass)
+
+Direct closure:
+
+1. `local_14` in runtime input mode update is read from the input-map state table.
+ - `psx_object_update_runtime_input_modes` calls `psx_input_map_get_code_and_edge(1,&local_18,&local_14)`.
+ - `local_14 == 0x1e` is the exact compare that gates the debug grant helper when `psx_hidden_passcode_flag != 0`.
+2. Input code resolution is exact-match against a 0x32-entry mask table.
+ - `psx_input_map_update_state_for_pad` polls raw pad mask from `FUN_80048d04` and scans `0x80090d34 + pad*0xfc` for exact value equality.
+ - matched index is written to current-code state (`0x80090e24 + pad*0xfc`) and returned by `psx_input_map_get_code_and_edge`.
+3. Input code `0x1e` is mapped to pad mask `0x2800` in all recovered profile cases.
+ - `psx_input_map_install_profile` writes map entries with `psx_input_map_set_code_to_padmask(1, code, mask)`.
+ - every switch branch in this function writes code `0x1e -> 0x2800`.
+4. Using the same PSX digital mask legend implied by nearby known controls (`0x08` start, d-pad nibble patterns, shoulder/face combos), `0x2800` resolves to `R1 (0x0800) + Circle (0x2000)`.
+
+Practical trigger statement now supported:
+
+- after hidden passcode state is active, the JL-9 debug grant path is triggered by the decoded special input code `0x1e`, which maps to the controller chord `R1 + Circle`.
+- because map lookup is exact-match, extra simultaneously held buttons can produce a different code and miss the `0x1e` gate.
+
+Confidence:
+
+- high (`~0.93`) that `0x1e -> 0x2800` is the actual decode mapping in this executable.
+- high (`~0.88`) that `0x2800` corresponds to `R1 + Circle` under the active digital-pad bit layout.
+
+### Selected-weapon-adjacent pointer reclassification (2026-04-11)
+
+The user-reported pointer delta near the selected-weapon watch block is now closed as render-state, not inventory state:
+
+- all-weapons dump: pointer-like value `0x80078248`
+- starter-only dump: pointer-like value `0x800782b8`
+
+Live MCP decompile/xref evidence on `SLUS_002.68` shows these are the two double-buffer draw-environment records:
+
+- `psx_platform_init` (`0x80042b38`) initializes them with `SetDefDrawEnv((DRAWENV *)&DAT_80078248, ...)` and `SetDefDrawEnv((DRAWENV *)&DAT_800782b8, ...)`
+- the paired display environments are at `0x800782a4` and `0x80078314` (`+0x5c` from each draw-env base), matching a packed per-buffer draw/disp environment layout
+- `DAT_80067954` is the active draw-env pointer toggled each frame in:
+ - `psx_present_frame_and_flip` (`0x80044188`)
+ - `render_reset_draw_state` (`0x80042be8`)
+ - `FUN_800461d0` (`0x800461d0`)
+
+Practical consequence:
+
+- the observed `0x80078248 <-> 0x800782b8` change is a frame-buffer/render flip artifact
+- this pointer pair does not encode selected weapon id, HUD selected-item row, or inventory ownership state
+- weapon selection/inventory evidence remains stronger in the channel-commit and weapon-definition lanes (`DAT_80064355`, `committed_selected_item_id`, `0x8006466a + idx*0x26`)
+
+### Runtime block classification around `0x8014574c..0x801457d0` (2026-04-11)
+
+Current strongest executable-backed interpretation for the user-targeted RAM block:
+
+- this region is most likely a live player-object nested runtime-state block, not a HUD color cache and not the static channel-commit table
+- the watched byte at file offset `0x14577e` (RAM `0x8014577e`) is most strongly the active selected weapon row-id byte within the nested `+0x1c` runtime field family
+- this byte behaves like current-weapon metadata, not ownership-bitset storage and not direct ammo-count storage
+
+Concrete evidence chain:
+
+1. `psx_apply_channel_effect_and_commit_selected_item_id` writes the committed item id from `channel_commit_row_selected_item_id[(channel*10)]` into two sinks:
+ - global `committed_selected_item_id` (`0x80078a90`)
+ - nested player runtime field at `*( *( *(DAT_800789f8 + 8) + 0x18) + 0x1c )`
+2. `psx_set_debug_extra_channel_gate` clears that same nested `+0x1c` field in several mode-reset branches, alongside nearby nested fields (`+0x14`, `+0x16`, low byte of `+0x18`), consistent with runtime weapon-mode/state reset behavior.
+3. `psx_object_create_simple_record` initializes the nested block rooted at `*(obj_component + 0x18)` as a large runtime state area and stores back-pointer/object runtime linkage there, matching a heap-resident per-object state block.
+4. The dump delta at byte `0x14577e` (`0x0c` all-weapons vs `0x02` starter-only) fits this as a selected weapon row-id change rather than ownership-table cardinality or HUD palette animation.
+
+Interpretation confidence:
+
+- `0x14577e` as selected weapon row-id byte (`nested +0x1c` field family): high
+- broader `0x8014574c..0x801457d0` region as player nested runtime-state block: medium-high
+- region as primary inventory ownership table: low
+- region as primary ammo counters table: low
+- region as HUD-only cache: low
+
+### Starter-only compare correction (2026-04-11)
+
+The follow-up compare against `binary/Crusader - No Remorse RAM starter weapon only.bin` corrects one earlier overreach in this note.
+
+What changed:
+
+- `0x1456fc..0x145748` is identical in the all-weapons and starter-only dumps, even though the observed weapon inventory is very different
+- therefore that attractive `0x0002 .. 0x000b` 8-byte record sequence is a static table or static-adjacent runtime seed, not the live owned-weapon list
+- the real dynamic lane starts at `0x14574c` and includes:
+ - `0x14577e: 0x000c -> 0x0002`
+ - nearby count/flag-like deltas around `0x14575c..0x14578c`
+- the separate watched field at `0x67944` also changes cleanly (`0x0000000b -> 0x00000001`), but the current image still has no recovered static xrefs for `0x80067944`
+
+Current safest wording after that compare:
+
+- no contiguous owned-weapon id list has been recovered yet
+- the storage model is better explained by channel-state ownership plus one or more dynamic current-weapon fields in the runtime block
+- the earlier patch-target recommendation on `0x145744` should be considered withdrawn
+
+## Speculation and folklore
+
+Keep speculation bounded here rather than mixing it into the evidence chain.
+
+Current plausible but not fully closed reads:
+
+- `L0SR` remains the strongest current executable-backed cheat-mode candidate because it fits the recovered four-symbol hidden branch better than `L0SER`
+- `R1 + Circle when L0SR is active` is now the strongest executable-backed button-chord read for the gated input route; the remaining folklore-level uncertainty is about the exact pre-hidden gate-arm action, not the `0x1e` chord mapping itself
+- `XXXX` still plausibly maps to the hidden-pictures folklore because the decoder has a second hidden-special lane consistent with a transformed secret path
+- the VRAM dump likely contains the weapon HUD/icon atlas, but without labeled reference icons or a cleaner runtime capture it does not yet let us separate `JL-2` from `JL-9` visually
+- the main RAM dump likely contains the relevant runtime inventory state, but current evidence is split between a nearby commit-table neighborhood that does not show plain final JL ids and stronger later clusters around `0x133000` that still need one executable-side anchor to decode
+- the strongest current input-side static closure now turns the special `0x1e` code into pad mask `0x2800` and a high-confidence `R1 + Circle` interpretation, while still leaving the upstream pre-hidden gate-arm producer unresolved
+
+None of those points are needed for the current core conclusion that the extra late executable-backed lane is `JL-9`.
+
+## Best next steps
+
+1. Capture a runtime sample after the hidden passcode/debug gate is active and log which weapon channels are actually unlocked, especially `0x0d`.
+2. Decode the controller-button mapping that produces the gated input code reaching `psx_debug_grant_weapon_channels_and_ammo`, so the `L0SR` trigger path is either confirmed or corrected.
+3. Trace all reads and writes of `psx_debug_extra_channel_gate` to close the late extra-unlock precondition.
+4. Trace the direct `JL-2 AMMO` UI path and the underlying ammo decrement/storage lane.
+5. Decode the denser `0x0c` / `0x0d` RAM clusters around `0x133000` using one confirmed executable-side inventory/HUD anchor, instead of continuing whole-image sweeps.
+6. If that anchor still does not land cleanly, test the smaller `0x422c..0x4440` candidate region against the same runtime meaning before requesting another dump or capture.
+
+## Weapon acquisition systems vs id `0x0d` / `0x01` (2026-04-11 broad pass)
+
+Focused live MCP pass on active `SLUS_002.68` to classify which acquisition families can produce the verified selected ids `JL-9=0x0d` and `RP-16?=0x01`.
+
+### Systems analyzed
+
+- default loadout/init: `psx_weapon_channels_init_mode_loadout` (`0x8002f814`)
+- mission transition mode application: `psx_weapon_channels_apply_mode_transition_state` (`0x8002f278`)
+- pickups/action-coded grants: accepted-code dispatch lane at `0x8001d3fc`
+- shop path: `psx_weapon_shop_try_apply_entry` (`0x8003de68`) using `DAT_80064B90`
+- scripted awards: `psx_section0_dispatch_root_apply_packed_channel_actions` (`0x800311f4`)
+- debug grants: `psx_debug_grant_weapon_channels_and_ammo` (`0x8002fd90`)
+
+### Concrete results
+
+1. id `0x0d` (`JL-9`) remains gated from ordinary loadout/shop paths.
+ - loadout init has fixed unlock/ammo calls through `0x0c` and does not directly unlock `0x0d`
+ - mission transition state helper does not add a `0x0d` direct unlock path
+ - shop front path (`param<10`) maps through `DAT_80064B90[0..9] = 03 04 05 06 07 08 09 0a 0b 0c`, so no direct `0x0d` there
+
+2. id `0x0d` is directly produced by debug grant and is possible in data-driven scripted/pickup lanes.
+ - debug: `0x8002fff4` conditionally calls `psx_weapon_channel_unlock_and_seed_markers(0x0d)` only when `psx_debug_extra_channel_gate != 0`
+ - scripted awards: packed-action decoder (`0x80031250`) calls `unlock(channel)` from seeded `DAT_8008F120` rows (runtime-seeded from section0 markers)
+ - pickup/action-coded lane (`0x8001d3fc`) range-check path still commits accepted codes through `psx_apply_channel_effect_and_commit_selected_item_id(a0)`, including `0x0d`
+
+3. id `0x01` is not excluded and appears in non-debug systems.
+ - shop table byte dump confirms `DAT_80064B90 = 03 04 05 06 07 08 09 0a 0b 0c 01 05 04 03 02 07`
+ - while `0x01` is outside the first ten direct-unlock shop slots, it is still present in shop's alternate branch (`param_1=10`) via the same channel map
+ - pickup/action-coded commit path accepts low ids (including `0x01`) and routes them to commit
+ - scripted packed actions are data-driven and can target `0x01` when authored in section0 records
+
+### Exclusion read after this pass
+
+- `0x0d`: strongest current read is "not part of baseline ordinary progression," with explicit debug-gated production and only data-driven/scripted/pickup exceptions.
+- `0x01`: not excluded; it appears in normal non-debug content paths (shop mapping plus accepted low-id action path).
+
+### Notes on evidence quality
+
+- The packed-action LUT base (`0x8008F120`) is runtime-seeded and can be uninitialized in static reads; classification therefore uses the seeding/writer and decoder call behavior, not a static full table dump.
+- The core `0x0d` gate remains directly evidenced at `0x8002fff4` in live `SLUS_002.68`.
+
+## Appendix: key anchors
+
+### Weapon rows and helpers
+
+- `0x8006466a` weapon-definition table base
+- `0x80064832` `JL-2` row (`0x0c`)
+- `0x80064858` `JL-9` row (`0x0d`)
+- `0x800315d8` `psx_weapon_def_get_u16_with_mode_gate`
+
+- **Live field map (evidence):** `row+0x20` is read as a 16-bit row field (loaded at `0x8003160c`), `row+0x22` is a 1-byte mode/gate field (loaded at `0x8003163c`), and `row+0x24` contains a per-row state/selector used by HUD/ammo resolver (example: `JL-2` shows `0x4B`, `JL-9` shows `0x0F`). The nearby ASCII `JL-2 AMMO` at `0x800642b6` aligns with code reads from the `0x800642b2/0x800642b4` region, supporting a small adjacent string/table used for ammo text resolution.
+- `0x8003d02c` `psx_weapon_def_apply_spawn_profile_by_index`
+
+### Channel commit and HUD
+
+- `0x8002ef34` `psx_apply_channel_effect_and_commit_selected_item_id`
+- `0x8002f15c` committed item-id load from channel commit row
+- `0x800424ac` `psx_hud_draw_selected_item_tile_bar`
+- `0x800350a8` `psx_render_mode_dispatch`
+
+### Hidden passcode and extra unlock
+
+- `0x80034e38` `psx_passcode_screen_eval_current_entry`
+- `0x8003ec8c` `psx_passcode_decode_to_mission_selector`
+- `0x80013154` `psx_object_update_runtime_input_modes`
+- `0x8002fd90` `psx_debug_grant_weapon_channels_and_ammo`
+- `0x800232f0` `psx_set_debug_extra_channel_gate`
+- `0x80030004` extra `0x0d` unlock call site
+
+### Runtime dump artifacts
+
+- `binary/Crusader - No Remorse Memdump Weapons.bin` -> VRAM-sized HUD/presentation artifact
+- `binary/Crusader - No Remorse Weapons Main Ram.bin` -> 2 MiB plausible main RAM artifact, currently not self-decoding enough to close `JL-2` vs `JL-9`
+- stronger current unresolved RAM cluster candidates for live weapon-slot state: around file offsets `0x133000`, `0x133416`, and `0x1335d4`
+- secondary smaller RAM candidate for `JL-2 / 10 clips / 0 loaded` style state: around file offsets `0x422c..0x4440`
+
+### Live-named globals used in this note
+
+- `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`
+- `psx_weapon_spawn_state_selector`
+- `psx_ui_color_cycle_state`
+- `psx_hud_selected_item_color`
+- `psx_hud_selected_tile_color_a`
+- `psx_hud_selected_tile_color_b`
+
+## Hidden passcode decode closure (2026-04-11 pass)
+
+Focused live MCP pass on active `SLUS_002.68` against:
+
+- `psx_passcode_screen_eval_current_entry` (`0x80034e38`)
+- `psx_passcode_decode_to_mission_selector` (`0x8003ec8c`)
+- passcode tables at `0x80064bbc`, `0x80064bd0`, `0x80064be4`, `0x80064bf8`
+- helper `psx_passcode_generate_encoded_quad` (`0x8003ec30`)
+
+### Closed transform logic
+
+Decode path (`0x8003ec8c`) compares transformed input, not direct ASCII:
+
+1. `entry[1..3]` are normalized by `-0x1b` and matched against table triplets:
+ - `T1 = [0x80064bd0 + i]`
+ - `T2 = [0x80064be4 + i]`
+ - `T3 = [0x80064bf8 + i]`
+ - valid `i` range is `0..0x11` (`18` entries)
+2. If no `i` matches all three, decode fails.
+3. For ordinary entries (`i != 0x0f/0x10/0x11`):
+ - `delta = entry[0] - (0x1b + [0x80064bbc + i])`
+ - `psx_level_runtime_header_state = delta` (stored at `0x80068ab0`)
+ - accept only when `delta < 4`; return selector `i+1`
+
+Special branches:
+
+- `i == 0x0f`: clears `psx_level_runtime_header_state` and returns `0x10` (delay-slot flow preserves `v0=0x10`)
+- `i == 0x10`: sets `psx_hidden_passcode_flag = 1` (`0x80067454`) and returns `0`
+- `i == 0x11`: returns sentinel `0x12`
+
+Important closure: all three special branches bypass the first-character delta check, so their first character is effectively wildcarded by the decoder.
+
+### Recovered table values
+
+Read directly from `0x80064bb0..0x80064c20`:
+
+- `T0 (0x80064bbc)` = `01 03 0B 0E 0F 06 07 0A 09 12 01 02 03 03 06 08 08 12`
+- `T1 (0x80064bd0)` = `10 11 08 13 02 15 16 17 18 19 1A 1B 1C 04 03 0D 14 12`
+- `T2 (0x80064be4)` = `0B 0C 0D 0A 19 00 01 02 03 04 05 06 07 08 09 0F 0E 12`
+- `T3 (0x80064bf8)` = `02 0B 0C 03 0E 0F 10 11 12 13 14 15 16 17 18 0A 0D 12`
+
+Symbol alphabet (`0x80063ef0`) for passcode glyph indexes is:
+
+- `BCDFGHJKLMNPQRSTVWXZ0123456789`
+
+### Concrete recovered hidden/debug strings
+
+Generator (`0x8003ec30`) emits:
+
+- `out[0] = T0[i] + psx_level_runtime_header_state`
+- `out[1] = T1[i]`
+- `out[2] = T2[i]`
+- `out[3] = T3[i]`
+
+Mapped through `0x80063ef0`, special rows are:
+
+1. `i=0x10` (sets `psx_hidden_passcode_flag`): `?0SR`
+ - canonical when selector state is `0`: `L0SR`
+ - decoder confirms first character wildcard for this branch
+2. `i=0x0f` (clears selector state): `?RTN`
+ - canonical when selector state is `0`: `LRTN`
+ - first character wildcard for this branch
+3. `i=0x11` (sentinel return `0x12`): `?QQQ`
+ - canonical when selector state is `0`: `XQQQ`
+ - first character wildcard for this branch
+
+### Debug JL-9 path tie-in
+
+This pass tightens the two-phase gate model using direct branch conditions:
+
+1. Hidden branch (`i=0x10`) sets `psx_hidden_passcode_flag` (`0x80067454`) at `0x8003ed28`.
+2. Input-mode handler (`0x80013154`) only calls debug grant (`0x8002fd90`) on input code `0x1e` when hidden flag is nonzero.
+3. Extra JL-9 unlock (`0x80030004`, channel `0x0d`) still requires `psx_debug_extra_channel_gate != 0` at `0x8002fff4`.
+4. Gate write (`0x800232f0`) still requires hidden flag zero plus `psx_level_runtime_header_state == 3`.
+
+Current best practical read remains: extra `JL-9` requires a staged hidden/debug flow; no single direct ordinary-lane unlock for `0x0d` was recovered.
+
+## Shipped section0 slot-0x14 closure (2026-04-11 pass)
+
+Focused live MCP + scene-cache pass on active `SLUS_002.68` to answer one narrow question: can shipped non-debug authored section0/script bytes ever drive the `unlock(0x0d)` lane used by `JL-9`.
+
+### Executable closure
+
+1. Packed-action dispatch behavior is explicit at `psx_section0_dispatch_root_apply_packed_channel_actions` (`0x800311c4`):
+ - decoder uses `slot = (action_byte & 0x3f)` into seeded triplets `DAT_8008f120/121/124`
+ - triplet kind `3` path calls `psx_weapon_channel_unlock_and_seed_markers(channel)`
+2. Seed mapping is explicit at `psx_section0_dispatch_root_seed_marker_channel_table` (`0x8002f518`):
+ - `psx_section0_dispatch_root_find_marker_record_by_channel(0x14, 3, 0x0d)`
+ - therefore slot `0x14` maps to action-kind `3`, channel `0x0d`
+3. Live naming cleanup applied:
+ - `0x8002fd90` renamed to `psx_debug_grant_weapon_channels_and_ammo` to resolve the duplicate-name collision with `0x8002e5f0`
+
+### Shipped authored-data closure
+
+Cross-map scan of shipped PSX scene-cache root-dispatch records (`.cache/scene-cache/psx-remorse/map-*/.../scene.json`) parsed packed action dwords from record raw words and checked for bytes where `(byte & 0x3f) == 0x14`.
+
+Results:
+
+- scanned root-dispatch records: `3298`
+- slot-`0x14` authored-byte hits: `119`
+- maps with at least one slot-`0x14` hit: `22`
+- map ids with hits: `1,2,3,4,5,22,23,26,27,28,29,47,63,64,80,82,88,89,104,106,108,122`
+
+Representative shipped records:
+
+- map `1`, `item:21:psx-section0_dispatch_roots:left:8`, raw words `0045 1d3b 0d43 0014 0000 0020` (packed dword `0x00140d43`)
+- map `5`, `item:11:psx-section0_dispatch_roots:right:8`, raw words `004e 1b83 0943 0014 0000 0020` (packed dword `0x00140943`)
+- map `63`, slot-`0x14` hits present (`2`)
+- map `89`, slot-`0x14` hits present (`30`)
+
+### Practical verdict for JL-9 legit-path question
+
+- **Ruled in**: shipped non-debug section0/script authored data does include packed action bytes that resolve to slot `0x14`, and executable seed/dispatch logic maps slot `0x14` to `unlock(channel 0x0d)`.
+- **Not fully universal**: this does not prove every campaign route triggers those records during normal play timing, but it does prove the non-debug shipped-data lane exists and is executable-reachable in principle.
+
+Confidence:
+
+- high (`~0.93`) that slot `0x14` is an `unlock(0x0d)`-capable packed-action slot in this build
+- high (`~0.91`) that shipped non-debug section0 root records contain authored slot-`0x14` bytes
+- medium-high (`~0.77`) that this yields legitimate in-play JL-9 acquisition in at least some non-debug scenarios (remaining uncertainty is trigger/reachability timing, not byte-path existence)
+
+## Step-2 caller narrowing for gate-arm event (`0x800230e4`/`0x800232f0`) (2026-04-11 live pass)
+
+Goal of this pass was to close the immediate caller/event side for the gate writer branch (`param_2==0x0a`, `param_3==4`) inside `psx_set_debug_extra_channel_gate`.
+
+### Exact executable evidence recovered
+
+1. The gate writer remains exactly `sb v0,0x739d(at)` at `0x800232f0` in `psx_set_debug_extra_channel_gate` (`0x800230e4`) under local branch `param_2==0x0a` and case `param_3==4`.
+2. Direct caller xrefs stay empty because this lane is indirect-dispatch based.
+3. A concrete indirect dispatcher block was recovered at `0x800214ac..0x800215f8`:
+ - `0x800215bc`: compares dispatch index (`a2`) against per-level table byte `DAT_80063e68[current_level]`.
+ - `0x800215dc`: loads handler pointer from jump table `DAT_800640a0[a2]` and `jalr`s.
+ - `0x800215cc` + `0x800215e0`: handler arguments are loaded as bytes from pointer fields in the action record (`a1=*(*(record+8))`, `a2=*(*(record+0xc))`).
+4. Table slot proof:
+ - `DAT_800640a0[0x0f] = 0x800230e4` (the target handler).
+ - therefore the gate-arm branch requires this record-level triplet at dispatch time:
+ - dispatch index byte = `0x0f` (to pick handler `0x800230e4`),
+ - first argument byte = `0x0a`,
+ - second argument byte = `0x04`.
+5. Per-level gating proof from `DAT_80063e68`:
+ - indices where table byte is `0x0f`: `54,55,56,57,58,82`.
+ - outside those level indices, this exact `0x0f -> 0x800230e4` lane is not selected by the recovered level gate compare.
+
+### Inferred player-facing meaning (narrowed)
+
+- strongest read is now a late/level-scripted control-event lane, not a random global input callback: the branch is reached through a level-gated action dispatcher and a control-handler jump table.
+- practical narrow set: player-visible triggers tied to scripted/control progression events in level-index set `{54..58,82}` that emit dispatch triple `(0x0f,0x0a,0x04)`.
+- still not fully singular: static analysis in this pass did not recover one uniquely named UI/menu function boundary that can be asserted as the only producer of that exact triple.
+
+### Confidence update
+
+- high (`~0.90`) that call reachability into `0x800230e4` is through `0x800214ac..0x800215f8` + `DAT_800640a0` indirect dispatch.
+- high (`~0.87`) that branch trigger shape is exactly `(dispatch_index=0x0f, param_2=0x0a, param_3=0x04)`.
+- medium-high (`~0.76`) that player-facing source is a small late-level scripted/control action family (`54..58,82`), not a single global menu button.
+
+### Live Ghidra artifacts changed
+
+- conservative disassembly comments added:
+ - `0x800215bc` (level-gated dispatch compare against `DAT_80063e68[current_level]`)
+ - `0x800215cc` (argument-byte extraction from action record pointers)
+ - `0x800215dc` (jump-table dispatch through `DAT_800640a0`, slot `0x0f -> 0x800230e4`)
+
+## Mission 16 / developer office mapping correction (2026-04-11 live pass)
+
+An older interpretation in this note treated the `?RTN` / `NRTN` special branch as if it directly selected `DAT_80063e54[0x0f] = 0x36`, which would have put Mission 16 / dev-office content inside the `DAT_80063e68 == 0x0f` gate family.
+
+That interpretation is now superseded.
+
+### Correct runtime flow
+
+1. The `?RTN` special branch is decode index `i = 0x0f`, but it does **not** return `0x0f`.
+ - it clears `psx_level_runtime_header_state` at `0x8003ed10`
+ - due to the decoder's return flow, raw selector returned to caller is `0x10`
+2. `psx_passcode_screen_eval_current_entry` then maps through `DAT_80063e54[0x10]`, not `DAT_80063e54[0x0f]`.
+ - live table bytes now anchor this as `DAT_80063e54[0x10] = 0x3f`
+3. Caller state at `0x80034d84` therefore uses mapped `s0 = 0x3f`, not `0x36`.
+4. Apply path still reaches selector-`0x10` case at `0x8002142c`, which writes `DAT_800675e4 = 0x1d`, sets `DAT_80067379 = 2`, and calls the common helper with `a0 = 0x36`.
+
+### Practical consequence for the dev-office hypothesis
+
+- `a0 = 0x36` in the selector-`0x10` apply case may still be related to the user-observed developer-office lane, but that is **not** the same claim as `current_level_map_id == 0x36` or `DAT_80063e68[current_level] == 0x0f`.
+- so the previously attractive chain `?RTN -> map 54 -> gate family 0x0f` should be treated as withdrawn.
+- the surviving office hypothesis is weaker and narrower: `?RTN` / `NRTN` may still route into office-like content, but current static evidence does **not** prove that this places the session directly inside the slot-`0x0f` gate-arm family.
+
+### Confidence update
+
+- high (`~0.95`) that the correct caller mapping is `?RTN index 0x0f -> raw selector 0x10 -> DAT_80063e54[0x10] = 0x3f`
+- high (`~0.91`) that the older `DAT_80063e54[0x0f] = 0x36` interpretation was the wrong table index for the runtime `?RTN` path
+- medium (`~0.66`) that `a0 = 0x36` in the apply case still corresponds to the same player-facing developer-office concept without one runtime label capture
+
+### Live Ghidra artifacts changed
+
+- conservative comments added in later follow-up:
+ - `0x8003ed10`
+ - `0x80034d84`
+ - `0x8002142c`
+
+## Step-2 producer-path closure (control-opcode lane) (2026-04-11 live pass)
+
+Goal of this pass was to close the unresolved upstream producer for the gate-arm dispatcher block (`0x800214ac..0x800215f8`) by tracing backward from `0x800215cc` / `0x800215e0` into the parent script/control lane.
+
+### Exact executable evidence recovered
+
+1. `0x800215cc` / `0x800215e0` byte loads are inside unnamed control helper `0x800214ac`.
+ - it reads pointer arguments from `record+8` and `record+0xc` (`arg1=*(*(record+8))`, `arg2=*(*(record+0xc))`), then dispatches through `DAT_800640a0[slot]` after level gate compare at `0x800215bc`.
+2. `0x800214ac` is now proven as table entry `DAT_80063610[49]` (address `0x800636d4`).
+3. Upstream call chain for that entry is now explicit:
+ - `psx_object_behavior_opcode_dispatch` (`0x8002677c`) dispatches through `DAT_800641ac[opcode_index]`.
+ - table entry `DAT_800641ac[54] = 0x80027ecc`.
+ - `0x80027ecc` performs a second dispatch by loading sub-opcode from `*(*(a1+0))` and jumping through `DAT_80063610[subop]`.
+ - when `subop==49`, it reaches `0x800214ac`.
+4. Therefore the unresolved writer tuple path is no longer a free-floating callback; it is a **nested behavior/control opcode producer path**:
+ - behavior opcode index `54` -> secondary sub-opcode `49` -> `0x800214ac` -> slot table `DAT_800640a0`.
+5. The gate-arm writer tuple remains unchanged at the sink:
+ - slot `0x0f` selects `psx_set_debug_extra_channel_gate` (`0x800230e4`), and branch write requires args `0x0a`, `0x04`.
+
+### What this closes and what remains open
+
+- closed: where the action-record bytes come from structurally.
+ - they are produced from the object behavior/control stream argument pack used by opcode-54 handler `0x80027ecc` and its sub-dispatch into `DAT_80063610[49]`.
+- still open: one singular player-facing mission event label.
+ - static executable evidence now identifies the producer subsystem and dispatch indexes, but does not yet uniquely name one authored script instance as "the" office event.
+
+### Narrowed map/event set in this pass
+
+- executable gate compare lane remains constrained by `DAT_80063e68[current_level] == 0x0f` (current narrowed level-id family from prior pass: `{54..58,82}`).
+- cache-side cross-check in this pass:
+ - available scene-cache member from that family was map `82` only in current catalog snapshot.
+ - map `82` section0 root records showed no direct slot-`0x0f` packed-byte hits, which is consistent with this pass's new upstream finding that the writer can be produced by the **object control-opcode stream lane** (not only section0 root packed-action bytes).
+
+### Practical player-visible status
+
+- status is now stronger than "unknown source":
+ - this is a scripted in-level behavior/control opcode event lane (nested opcode `54 -> 49`), i.e., a real gameplay/script progression path rather than a menu-only artifact.
+- but still not fully singular:
+ - one concrete authored trigger instance (exact mission script/object instance) is still unresolved.
+
+### Confidence update
+
+- high (`~0.94`) that producer path is `behavior opcode 54 -> sub-op 49 -> 0x800214ac -> slot 0x0f handler 0x800230e4`.
+- high (`~0.90`) that action-byte arguments at `0x800215cc/0x800215e0` are sourced from opcode argument-pointer records, not direct UI input.
+- medium (`~0.74`) that a single map-82-like scripted control event family is the practical player-visible source, pending one final authored-instance bind.
+
+## Step-2 producer-path reachability correction (2026-04-11 live pass)
+
+This follow-up rechecked whether the previously claimed `opcode 54 -> sub-op 49 -> 0x800214ac` chain is currently proven reachable in active `SLUS_002.68`.
+
+### Direct executable evidence from live script/ref scans
+
+1. `DAT_800641ac` does contain high-index entries:
+ - `DAT_800641ac[49] = 0x80027d2c`
+ - `DAT_800641ac[54] = 0x80027ecc`
+2. `0x80027ecc` still dispatches through `DAT_80063610[subop]`, and `DAT_80063610[49] = 0x800214ac` remains true.
+3. But static reachability in this image is currently narrower than that table topology suggests:
+ - only recovered caller to `psx_object_behavior_opcode_dispatch` (`0x8002677c`) is at `0x80026740`
+ - that caller guards with `(opcode_word - 1) < 0x0a` at `0x80026710`
+ - therefore this known path only selects `DAT_800641ac[0..9]`
+4. `0x80027ecc` currently has only one recovered incoming reference, and it is data-only (`0x80064284` table slot), not a direct call/jump xref.
+5. `0x800214ac` likewise currently has data-only incoming evidence (`0x800636d4` table slot), not a direct call xref.
+
+### Practical correction
+
+- The earlier `54 -> 49` statement should now be treated as **table-topology evidence**, not yet as a proven active runtime producer path.
+- What remains high-confidence is the sink-side gate-writer shape:
+ - `0x800214ac..0x800215f8` is a real level-gated slot dispatcher.
+ - `DAT_800640a0[0x0f] = 0x800230e4` and sink args remain `0x0f,0x0a,0x04`.
+- The open upstream question is now more precise:
+ - recover a second proven caller path into `0x8002677c` (or alternate path into `0x800214ac`) that can actually feed high dispatch indices.
+
+### Live Ghidra artifacts changed
+
+- conservative disassembly comments added:
+ - `0x80026710` (known caller path bounds dispatch index to `< 0x0a`)
+ - `0x8002685c` (documents `DAT_800641ac[a0]` use and known caller-path bound)
+ - `0x80027f0c` (documents secondary `DAT_80063610[subop]` use under `0x80027ecc`)
+
+### Confidence update (corrected)
+
+- high (`~0.93`) that sink-side slot dispatch into `0x800230e4` via `DAT_800640a0[0x0f]` is real.
+- high (`~0.89`) that current static refs do **not yet** prove active reachability of `DAT_800641ac[54] -> 0x80027ecc` from the known caller path.
+- medium (`~0.71`) that high-index behavior-opcode producers still exist in runtime via currently un-recovered caller/context lanes.
+
+## Natural gate-arm active-caller recheck (2026-04-11 live pass)
+
+Scope of this pass was narrow and read-only: verify whether any currently proven gameplay caller path can really feed the sink-side dispatcher at `0x800214ac..0x800215f8` with the known tuple `(slot 0x0f, arg1 0x0a, arg2 0x04)`.
+
+### New direct evidence
+
+1. Sink-side facts remain unchanged and strong:
+ - `0x800214ac` performs level-gated indirect slot dispatch through `DAT_800640a0[a2]`.
+ - slot `0x0f` still resolves to `0x800230e4` (`psx_set_debug_extra_channel_gate`) from table entry address `0x800640dc`.
+ - sink arg bytes are still loaded as `a1=*(*(record+8))` and `a2=*(*(record+0xc))`.
+2. Upstream topology is still true but only as topology:
+ - `0x80027ecc` is still table-backed at `DAT_800641ac[54]` (`0x80064284`).
+ - `0x80027d2c` is still table-backed at `DAT_800641ac[49]` (`0x80064270`).
+ - `0x800214ac` is still table-backed at `DAT_80063610[49]` (`0x800636d4`).
+3. Proven active gameplay caller lane into behavior-op dispatch remains bounded:
+ - callers into `psx_run_object_behavior_program_tick` (`0x80026690`) include `psx_object_integrate_motion_and_route_visible` callsites (`0x80012668`, `0x800131d0`) plus two additional runtime callsites (`0x80011e78`, `0x80011f78`).
+ - the dispatch handoff at `0x80026740` is still guarded by `(opcode_word - 1) < 0x0a` at `0x80026710`.
+ - therefore this proven gameplay lane can only select `DAT_800641ac[0..9]`, not index `54`.
+4. No additional direct caller/xref path into `0x8002677c`, `0x80027ecc`, or `0x800214ac` was recovered in this pass.
+
+### Practical interpretation update
+
+- strongest **active** producer candidate is now the currently proven gameplay behavior-tick lane:
+ `psx_object_integrate_motion_and_route_visible -> psx_run_object_behavior_program_tick -> psx_object_behavior_opcode_dispatch`.
+- but that lane currently cannot feed the `54 -> 49 -> 0x800214ac` chain because of the `< 0x0a` guard.
+- so the older `54 -> 49` chain should stay in the model only as valid table topology, and be deprioritized as an immediate natural-event explanation until a second real caller/context is recovered.
+
+### New concrete static target
+
+Next high-value target is to recover the missing caller/context lane that reaches `psx_object_behavior_opcode_dispatch` with indices above `9`, or an alternate feeder that reaches `DAT_80063610[49]` directly.
+
+Most concrete first pivot for that is the mode/transition hub around `FUN_80020f7c` (`0x80020f7c`) and its active callers (`0x80022068`, `0x80022e58`, `0x80023080`, `0x8002748c`), because this is now the strongest live non-table-only control lane that already consumes level-family state (`DAT_80063e68`) and is visibly active in runtime transitions.
+
+### Confidence update
+
+- high (`~0.95`) that sink-side tuple shape and slot mapping remain correct.
+- high (`~0.92`) that the currently proven gameplay behavior-op lane is bounded to `0..9`.
+- medium-high (`~0.79`) that the natural gate-arm event still exists in an unrecovered alternate caller/context lane rather than in the currently proven `0..9` path.
+
+## Dispatcher caller/context reassessment around `0x80026690` / `0x8002677c` (2026-04-11 live pass)
+
+This pass was dedicated to finding an active context that can naturally feed the JL-9 sink dispatcher chain, with explicit focus on behavior dispatch around `0x80026690/0x8002677c` and sibling control-program runners.
+
+### Direct caller closure (behavior lane)
+
+1. `psx_object_behavior_opcode_dispatch` (`0x8002677c`) still has one recovered direct caller:
+ - `0x80026740` inside `psx_run_object_behavior_program_tick` (`0x80026690`).
+2. That caller still enforces `(opcode_word - 1) < 0x0a` at `0x80026710` before calling `0x8002677c`.
+3. Therefore the only proven active behavior lane currently dispatches indices `0..9`, not high indices such as `54`.
+
+### Secondary-dispatch topology (still valid, still not active-proven)
+
+1. `DAT_800641ac[54] = 0x80027ecc` remains true.
+2. `0x80027ecc` dispatches through `DAT_80063610[subop]`; `DAT_80063610[49] = 0x800214ac` remains true.
+3. `0x800214ac` remains the level-gated sink block that can route slot `0x0f` into `0x800230e4`.
+4. But no additional caller/xref path was recovered that proves runtime entry into `0x80027ecc` or `0x800214ac` from an active lane beyond table topology.
+
+### Strongest active context candidate now
+
+The strongest active context remains the object update loop:
+
+- `psx_object_integrate_motion_and_route_visible`
+ - calls `psx_run_object_behavior_program_tick` at `0x80012668` and `0x800131d0`
+ - also calls sibling `psx_object_run_control_opcode` at `0x80012ae0`
+
+This makes the behavior/control sibling pair the best live context family, but only the behavior half is currently connected to `0x8002677c`, and that half is still bounded to `< 0x0a`.
+
+### 54 -> 49 status update
+
+- keep `54 -> 49` as structurally correct table evidence.
+- retire it as an immediate active-caller explanation for now.
+- working priority should stay on recovering a second active caller/context into `0x8002677c` (or alternate proven feeder into `0x800214ac`).
+
+### Live Ghidra artifacts changed in this pass
+
+1. Created function:
+ - `0x80027ecc` -> `psx_behavior_subopcode_dispatch` (body `0x80027ecc..0x80027f34`).
+2. Renamed function:
+ - `0x80020f7c` -> `psx_control_event_apply_level_channel_preset`.
+3. Added decompiler comments:
+ - `0x80026710`: documents `< 0x0a` proven caller bound.
+ - `0x80027f0c`: documents secondary sub-op dispatch and unresolved active reachability of entry `49`.
+ - `0x80020f7c`: documents role as level-channel preset helper used by control-event slot handlers.
+
+## JL-9 natural-event synthesis from new clues (2026-04-11 live pass)
+
+Scope of this pass was narrow and evidence-only: integrate the two new clues (timed failure segment and "last mission is multi-map") into a ranked natural-arm model, then apply conservative live Ghidra artifacts only where call/xref evidence is explicit.
+
+### Ranked hypothesis list (current best)
+
+1. **Optional scripted event within countdown/transition chain** (strongest)
+ - slot-`0x0f` arm remains indirect-only (`0x800640dc -> 0x800230e4`) and tuple-specific (`0x0a,0x04`), which fits a conditional scripted branch better than a guaranteed branch.
+ - progression callback lane is table-driven (`0x80064210 -> 0x80027548`) and therefore naturally optional/contextual.
+
+2. **Countdown-failure branch timing miss**
+ - countdown tick is on active world-frame path (`0x8002b9a0 -> 0x8002b738 -> 0x80020794`).
+ - map boundary split at `0x800208f0` (`<=54 -> 0x1a`, `>54 -> 0x1b`) gives a concrete timed bifurcation where a natural run can miss tuple timing.
+
+3. **Map-to-map transition interaction**
+ - multi-map clue is code-backed via `psx_map_progression_table` reads in both preset and callback lanes (`0x80020fa4`, `0x80027560`).
+ - this supports "state advanced before arm tuple" as a real mechanism, but currently weaker than #1 because no single transition branch is proven to always suppress arm.
+
+4. **Countdown-success branch itself** (weakest of the four)
+ - no recovered direct evidence that plain success path alone arms `psx_debug_extra_channel_gate` without the tuple-specific slot `0x0f` branch.
+ - success/failure appears to gate mode flow, while arm remains a separate control-event tuple branch.
+
+### Strongest new reason to test one path first
+
+Most decisive next test target is the **optional scripted-event path inside the countdown/transition chain**.
+
+Reason:
+
+- this is the only ranked path simultaneously supported by all new evidence anchors in one chain:
+ - frame-timed countdown integration (`0x8002b738 -> 0x80020794`),
+ - map-54 bifurcation (`0x800208f0`),
+ - multi-map progression callbacks (`0x80020fa4`, `0x80027548`/`0x80027560`),
+ - and indirect slot-`0x0f` tuple arm (`0x800640dc -> 0x800230e4`, `(0x0a,0x04)`).
+
+So the best discriminating runtime probe is now: observe whether tuple `(0x0a,0x04)` is skipped when the countdown branch/preset path takes the map-boundary/progression route before slot-`0x0f` dispatch.
+
+### Live Ghidra artifacts changed in this pass
+
+No new function renames were applied in this pass (conservative threshold not met for additional naming).
+
+Decompiler comments added:
+
+- `0x80020794`: countdown tick call-chain note (`level_session_loop -> world_frame_tick -> countdown_transition_tick`).
+- `0x800208f0`: explicit map-54 boundary bifurcation note (`<=54 -> 0x1a`, `>54 -> 0x1b`).
+- `0x80020fa4`: progression-table read note with multi-map timing implication.
+- `0x80027548`: callback-table-driven progression note (`xref via 0x80064210`).
+- `0x800230e4`: indirect-only slot-table reachability reminder (`0x800640dc`), preserving tuple-branch specificity.
diff --git a/docs/psx/map-rendering.md b/docs/psx/map-rendering.md
index 091e69a..bc1beec 100644
--- a/docs/psx/map-rendering.md
+++ b/docs/psx/map-rendering.md
@@ -32,9 +32,962 @@ The current strongest model is:
- stage 2: queued special-visible list
6. The viewer can already reconstruct placement, projection, most resource loading, and much of the draw path from executable evidence.
7. The main remaining blocker is the last live state-to-art rule for unresolved families such as `0x0042` and `0x0049`. The map is still unreadable in practical terms because those families still fall back to placeholders.
+8. The current `map 104` repeated-wall regression is now clearly over-merged. The cache shows `type=0x0042` records from both authored section-0 families and multiple raw `u5` lane/class values still collapsing onto the same donor wall bundle, which is stronger evidence of a wrong runtime family/resource merge than of a missed palette variant.
+9. The latest six-track Ghidra pass closes one more structural question: `0x0042` is not special at descriptor time. It shares the same generic descriptor cluster as the wider `0x003e..0x0050` band, so the practical split the viewer still needs is downstream in state, flags, lane routing, and resource kind.
+10. A focused live cleanup pass on the update and ordering lane now closes the missing-function/object gap around `0x80012b44`: the parent routine is now explicitly modeled as `psx_object_integrate_motion_and_route_visible` (`0x8001263c..0x80012c2c`), with the local control helper named `psx_object_update_runtime_input_modes` (`0x80012c30`). The stage-1 ordering lane also now has explicit helper names/contracts for `psx_main_visible_list_swap_entries` (`0x8002e064`), `psx_main_visible_order_graph_unlink_pair` (`0x8002ca74`), and `psx_main_visible_order_graph_detach_object` (`0x8002c89c`), reducing ambiguity in rebucket/sort refresh behavior used by the exporter.
+11. The next concrete `map 104` `0x0042` pass now ties the exporter-side `runtimeDiagnostic` schema back to named live code instead of only to theory. 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, the strongest recovered `0x0400` stage-selection write is still nested-state-side rather than a direct object-local `0x0042` writer, and `DAT_800675f8` is now tighter as a level-loaded per-type policy pointer instead of a per-lane discriminator.
+12. A focused object-local route-bit provenance pass over the fixed `map 104` `0x0042` sample pack (`item:25/30/31/35/85/86`) now tightens the `obj+0x1c & 0x0400` branch model: `psx_object_create_simple_record` (`0x80024b48`) and `psx_object_create_compound_record` (`0x80025040`) remain the strongest concrete object-local writers because they copy authored `u5` directly into `obj+0x1c`; downstream named mutators (`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`) mutate other bits but do not introduce an object-local `0x0400` set/clear path; and recovered `0x0400` writers in this lane are still nested/global (`psx_object_state_machine_dispatch_tick` nested runtime word at `0x8001a078`, global policy word in `psx_object_handle_control_pair_0a` at `0x80022a14`).
+13. A focused visibility-routing/final-draw pass now closes the strongest remaining ambiguity between stage lanes and submitter rules. `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) now has an explicit stage split note (`type==4 || flags_1c&0x0400` routes to stage-2 special queue, else stage-1 main-visible), stage-1 order graph helpers are named/commented (`0x8002c89c`, `0x8002ca74`, `0x8002d778`, `0x8002e064`), draw pass ordering is pinned (`0x80041378`), submitter dispatch is pinned (`kind==5` image-table else sprite), and CLUT selection is now named through `psx_clut_table_by_resource_bank` (`0x800a9f48`) plus `psx_clut_override_table_by_palette_token` (`0x800a9f66`).
So the problem is no longer "how do PSX coordinates work" or "where do draw rectangles come from". The problem is now much narrower: the viewer still does not fully reproduce the executable's final runtime art-state resolution.
+## Final Live Map-104 Cohort Pass (2026-04-12)
+
+Pass objective:
+
+- close the final cohort split question for scene fingerprint `3497e7f641856415` on active writable `SLUS_002.68`
+- keep scope fixed to anchor groups already sampled in cache diagnostics:
+ - root `0x0022`: items `25/35`
+ - root `0x0030`: items `30/31`
+ - constructor `0x0030`: items `85/86`
+ - control `0x0066`: item `53`
+
+Functions inspected in this pass (create/update/draw/control path):
+
+- `psx_object_create_simple_record` (`0x800249f4`) - edited (comment)
+- `psx_object_create_compound_record` (`0x80024eec`) - edited (comment)
+- `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) - edited (comment)
+- `psx_object_advance_state_script` (`0x80025d68`) - edited (comment)
+- `psx_draw_main_visible_object` (`0x80041458`) - edited (comment)
+- `psx_draw_special_visible_queue` (`0x80041144`) - inspected (no new edit)
+- `psx_main_visible_order_compare_pair_for_graph` (`0x8002be6c`) - inspected (no new edit)
+- control-family callbacks (`0x80013618`, `0x80013688`) - inspected (no new edit)
+
+Live artifacts changed in this pass:
+
+- decompiler comments added/updated at:
+ - `0x800249f4`
+ - `0x80024eec`
+ - `0x800131a8`
+ - `0x80025d68`
+ - `0x80041458`
+
+Concrete cohort conclusions (what still differs vs still fails to differ):
+
+1. Still differs (strong): authored route seed in section-0 records remains the cleanest stable split for this sample set. Constructor and root create paths both copy authored route word directly into `obj+0x1c`, so root `0x0022` and `0x0030` are preserved at spawn instead of being synthesized later.
+2. Still fails to differ (strong): root `0x0030` (`30/31`) and constructor `0x0030` (`85/86`) are route-equivalent at creation (`obj+0x1c` seed), so this pair should not be split by constructor-vs-root family alone.
+3. Still fails to differ (current capture): stage split bit `obj+0x1c & 0x0400` remains clear in sampled anchors, so all cohorts continue through stage-1 main-visible rather than diverging by stage lane.
+4. Still fails to differ (current capture): no recovered per-cohort difference in submitter class at draw callsites yet; submitter remains bound-resource-kind based (`kind==5` image-table else sprite) and current anchors lack a proven mixed-kind split.
+5. Still differs (secondary but real): main-visible draw injects authored palette-token high byte while special-visible does not. This remains a lane behavior split, but because sampled anchors currently route main-visible, it is not yet the primary divider among these specific cohorts.
+
+Strongest evidence for the next exporter rule:
+
+- Use authored route seed (`u5` -> `obj+0x1c`) as the first unresolved-family splitter for map-104 `0x0042` placeholders, with `0x0022` and `0x0030` kept in separate fallback buckets until live capture proves convergence on resource kind plus latched frame token.
+
+Safe immediate renderer/exporter change suggested by this pass:
+
+- keep same-type unresolved placeholders separated by authored route seed for this fingerprint:
+ - bucket A: `0x0042` + route seed `0x0022`
+ - bucket B: `0x0042` + route seed `0x0030`
+- do not split `0x0030` bucket by root vs constructor origin alone.
+- do not promote `bit0x0400` or policy-word heuristics to primary keys for these anchors until runtime diagnostics actually sample non-null values.
+
+## Latest Loader/Install Pre-Constructor Pass (2026-04-12)
+
+Pass objective:
+
+- close the pre-constructor loader/install side for graphics-critical state on active writable `SLUS_002.68`
+- recover concrete semantics for WDL bundle load, CLUT install, detached runtime-stream install, and per-type bank population
+- apply only conservative live Ghidra edits where evidence is direct from decompile/disassembly
+
+Functions inspected (loader/install focus):
+
+- `wdl_resource_bundle_load_by_index` (`0x80039444`) - edited (comment)
+- `psx_install_type_state_script_component_extents_banks` (`0x8003917c`) - edited (comment)
+- `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`) - edited (comment)
+- `level_palette_header_apply` (`0x8002badc`) - edited (comment)
+- `FUN_80040768` -> `psx_install_level_audio_runtime_stream_bundle` (`0x80040768`) - edited (rename + comment)
+
+Live artifacts changed in this pass:
+
+- rename:
+ - `0x80040768`: `FUN_80040768` -> `psx_install_level_audio_runtime_stream_bundle`
+- decompiler comments added/updated at:
+ - `0x80039444`
+ - `0x8003917c`
+ - `0x80045ffc`
+ - `0x8002badc`
+ - `0x80040768`
+
+Recovered semantics (bundle/CLUT/runtime-bank install):
+
+1. `wdl_resource_bundle_load_by_index` performs a staged multi-section install before constructor dispatch: it reads per-level section sizes, installs type art/state lanes from two bundle passes, installs section-pack pointers (`policy`, `control opcode stream`, `CLUT table`), applies palette/CLUT header, installs detached runtime stream payload, optionally inflates persistent runtime-state blob, and only then dispatches root records.
+2. CLUT install is explicit in `level_palette_header_apply`: packed palette header fields are decoded, palette data is expanded, and CLUT blocks are uploaded through `level_palette_upload_cluts` while level CLUT mode/state globals are updated.
+3. Per-type state/runtime bank install is explicit in `psx_install_type_state_script_component_extents_banks`: it writes `psx_type_state_script_bank`, `psx_type_simple_component_bank`, and `psx_type_companion_extents_bank` by type row before constructors consume those lanes.
+4. Per-type art install is explicit in `psx_install_type_art_active_header_and_built_resource`: type art header slot is written, kind-4/5 resource is resolved/built, built-resource cache is committed, and the active slot is mirrored to resolved runtime resource for constructor-side binding.
+5. Detached runtime stream install is explicit in `psx_install_level_audio_runtime_stream_bundle`: it initializes SPU/sequence runtime from the detached blob header, maps 9 stream chunks, opens sequence/VAB handles, uploads stream payload to SPU RAM, and commits voice/channel defaults before object constructors run.
+
+Strongest evidence for what is available before constructors run:
+
+- per-type art lane is already installed (`psx_type_art_active_header_bank` / `psx_type_art_built_resource_bank`)
+- per-type behavior/state lanes are installed (`psx_type_state_script_bank`, `psx_type_simple_component_bank`, `psx_type_companion_extents_bank`)
+- type-policy pointer table and control-opcode stream table are installed from section-pack offsets
+- level CLUT table and expanded/uploaded CLUT data are installed via palette header apply path
+- detached runtime stream payload (audio sequence/bank runtime) is installed and SPU runtime is initialized
+
+Concrete exporter implication (avoid placeholder families):
+
+- export should model loader output as the authoritative pre-constructor state boundary, not only section-0 authored rows.
+- for unresolved visible families, placeholder fallback should be delayed until after reconstructing this install chain:
+ - type art active/built lanes
+ - type state/component/extents lanes
+ - type policy lane
+ - CLUT table + palette override readiness
+- practical consequence: resource-kind or frame-state donor heuristics that ignore preinstalled type/art/CLUT lanes will continue to collapse distinct runtime families into repeated placeholder walls.
+
+## Latest Draw Submission, Resource-Kind Dispatch, And Palette Token Pass (2026-04-12)
+
+Pass objective:
+
+- close the visible draw submission side with direct evidence from the two world-visible lanes
+- pin exactly how bound resource kind and frame token select submitter and CLUT path
+- document exporter-facing implications for unresolved placeholder families
+
+Functions inspected in this pass:
+
+- `psx_draw_world_visible_passes` (`0x8004137c`)
+- `psx_draw_main_visible_object` (`0x80041458`)
+- `psx_draw_special_visible_queue` (`0x80041144`)
+- `psx_project_object_main_visible` (`0x80040d44`)
+- `psx_sprite_resource_submit_frame` (`0x80044bdc`)
+- `psx_image_table_submit_frame` (`0x80044e9c`)
+
+Live artifacts changed in this pass:
+
+- decompiler comments added/updated at:
+ - `0x8004137c`
+ - `0x80041458`
+ - `0x80041144`
+ - `0x80040d44`
+ - `0x80044bdc`
+ - `0x80044e9c`
+
+Recovered semantics (draw submission and palette token):
+
+1. World draw order is fixed by `psx_draw_world_visible_passes`: stage-1 sorted main-visible draw first, then stage-2 special-visible queue, then HUD/overlay.
+2. Both visible world lanes use the same resource-kind dispatch at draw call sites:
+ - if `**(obj+0x10) == 5` -> `psx_image_table_submit_frame`
+ - else -> `psx_sprite_resource_submit_frame`
+3. Main-visible injects authored palette token into submit flags before dispatch:
+ - base flags include `obj_flags & 0x0002`
+ - for `type 0x003e..0x00ab`: token high byte from `source+0x06`
+ - for `type >= 0x00ac`: token high byte from `source+0x0c`
+4. Special-visible does not inject authored palette token high byte; it passes only orientation/flip bits (`obj_flags & 0x0002`) into submitters.
+5. Submitters converge on the same CLUT rule:
+ - default: `psx_clut_table_by_resource_bank[resource_bank]`
+ - override when high-byte token present: `psx_clut_override_table_by_palette_token[(submit_flags >> 8)]`
+6. Frame token bridge is explicit in projection and submit:
+ - `obj+0x94` is used by `psx_project_object_main_visible` frame-origin/size helpers
+ - same token is passed to sprite/image-table submitters as the per-object visible frame selector
+
+Strongest main-visible vs special-visible evidence split:
+
+- Main-visible (`0x80041458`) computes and ORs a high-byte palette token before submit.
+- Special-visible (`0x80041144`) performs the same kind dispatch but omits token injection.
+- Because submitters only apply CLUT override when nonzero high-byte token exists, this omission is the strongest executable-backed reason the two lanes can render with different palette behavior even for comparable resource kinds.
+
+Concrete exporter implication for remaining placeholder families:
+
+- Keep world visible lanes distinct in export/replay and diagnostics:
+ - lane `main-visible`: allow authored palette-token override into CLUT selection.
+ - lane `special-visible`: do not apply authored token override unless a separate stage-2 token source is recovered.
+- For unresolved families (`0x0042`, `0x0049`, `0x0055..0x0063`), include both fields in runtime diagnostics and donor-key matching:
+ - bound resource kind (`sprite` vs `image-table` path)
+ - latched frame token (`obj+0x94`)
+- Avoid cross-kind donor fallback (sprite <-> image-table) because submitter path and frame metadata semantics differ at callsite level.
+
+## Latest Selector/Transition Pre-Latch Pass (2026-04-12)
+
+Pass objective:
+
+- close pre-latch selector and transition-row semantics for unresolved visible families, centered on type `0x0042`
+- separate early gating and selector reseat (`obj+0x9e` path) from final frame latch (`obj+0x94` path)
+
+Functions inspected in this pass:
+
+- `psx_type42_transition_selector_tick` (`0x80018578`)
+- `psx_object_select_state_from_transition_table` (`0x8001bca0`)
+- `psx_object_is_within_view_margin` (`0x8001e6e8`)
+- `psx_object_select_state_script` (`0x800260e8`)
+- `psx_object_advance_state_script` (`0x80025d68`)
+
+Live artifacts changed in this pass:
+
+- decompiler comments added/updated at:
+ - `0x80018578`
+ - `0x8001bca0`
+ - `0x8001e6e8`
+ - `0x800260e8`
+ - `0x80025d68`
+
+Recovered selector/transition semantics (exact current read):
+
+1. Type-`0x0042` pre-latch reseat is explicitly gated before selector edits: `psx_type42_transition_selector_tick` first requires `psx_object_is_within_view_margin(obj, 0x14)` and object lane bit `obj+0x1c & 0x0020` before its turn/heading reseat path proceeds.
+2. Transition row selection is table-indexed and type-relative in `psx_object_select_state_from_transition_table`: `transition_code = DAT_80063a00[(type-0x1e)*0x0f + slot]`, then selector base from `DAT_80063b4c[(type-0x1e)*0x0f + transition_code]`.
+3. For type `0x0042`, the recovered rows are now byte-concrete from live memory:
+ - mode/policy row @ `0x80063c1c`: `2d 00 00 05 0a 14 0f 19 23 23 28 00 00 00 1e`
+ - selector row @ `0x80063d68`: `3c 00 00 00 50 0f 00 00 00 19 00 00 32 32 00`
+4. Selector `3/4` (and neighboring headings) feed art choice through the pre-latch path, not by directly writing the final frame token: turn/heading branches in `0x80018578` call `psx_object_select_state_script` with wrapped heading buckets (`& 7`) before latch.
+5. `psx_object_select_state_script` is confirmed as selector install only (`obj+0x9e`, `obj+0x8c/0x90` cursor); final visible token still latches later in `psx_object_advance_state_script` via write to `obj+0x94`.
+
+Strongest evidence for selector-to-visible-art linkage:
+
+- The same live chain now stays consistent across all inspected callsites: pre-latch selector reseat (`0x80018578` and `0x8001bca0`) -> selector install (`0x800260e8`) -> frame/state latch (`0x80025d68`, write to `obj+0x94`) -> projection/draw frame queries. This is the clearest current evidence that selector `3/4` effects are real but indirect: they bias the later latched frame token rather than bypassing it.
+
+Exporter implication for map `104` and remaining placeholder families:
+
+- Treat selector and latch as separate channels in export logic for unresolved families (`0x0042`, `0x0049`, `0x0055..0x0063`):
+ - `preLatchSelector`: from reseat/install lane (`obj+0x9e` path)
+ - `latchedFrameToken`: final draw-driving token (`obj+0x94`)
+- Do not collapse unresolved cases into one donor wall when pre-latch selectors differ but latches are unknown. For map `104`, this pass further supports splitting placeholder cohorts by selector/transition row behavior before any cross-family donor fallback.
+
+## Latest Type-Art Install And Constructor-Bind Pass (2026-04-12)
+
+Pass objective:
+
+- tighten the active-header/built-resource install lane around `0x800758d8` and `0x800758c8`
+- lock a conservative, evidence-backed install->constructor->draw chain for unresolved map-viewer families
+
+Functions inspected (focused set):
+
+- `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`)
+- `psx_object_create_simple_record` (`0x800249f4`)
+- `psx_object_create_compound_record` (`0x80024eec`)
+- `psx_stream_install_type_runtime_banks` (`0x80038f18`)
+- `psx_create_image_resource_from_descriptor` (`0x80044434`)
+- `psx_draw_main_visible_object` (`0x80041458`)
+
+Live artifacts changed in this pass:
+
+- decompiler comments added/updated at:
+ - `0x80045ffc`
+ - `0x800249f4`
+ - `0x80024eec`
+ - `0x80038f18`
+ - `0x80041458`
+
+Strongest install->draw evidence recovered:
+
+1. `psx_install_type_art_active_header_and_built_resource` first writes the incoming header pointer into `psx_type_art_active_header_bank[type]` (`0x800758d8` lane), then performs kind dispatch (`4` single-image bind, `5` bundle build/upload) and commits the resolved runtime resource pointer into `psx_type_art_built_resource_bank[type]` (`0x800758c8` lane).
+2. The same installer mirrors the active slot to the resolved runtime resource pointer after build/reuse, so constructor-time reads observe resolved per-type runtime resource state, not only raw descriptor headers.
+3. Both constructors (`simple` and `compound`) bind `obj+0x10` from the per-type lane: if active entry kind is `5`, they reuse `psx_type_art_built_resource_bank[type]`; otherwise they invoke `psx_create_image_resource_from_descriptor` and refresh cache state.
+4. `psx_stream_install_type_runtime_banks` seeds/clears type runtime bank slots and resets built-resource cache entries during stream install, preserving a clear ownership boundary between stream payload install and later resource realization.
+5. `psx_draw_main_visible_object` uses ctor-bound `obj+0x10` plus live frame token `obj+0x94`; submitter choice is resource-kind based (`kind==5` -> `psx_image_table_submit_frame`, else `psx_sprite_resource_submit_frame`).
+
+Conservative semantics confirmed in this pass:
+
+- `0x800758d8` is the active per-type art lane at install/constructor handoff time.
+- `0x800758c8` is the built-resource cache lane used for kind-5 reuse and refreshed through install/build paths.
+- Constructor binding and draw submission are directly connected through `obj+0x10` (resource) and `obj+0x94` (live frame token), so unresolved visible families should be debugged as runtime lane/state/resource issues rather than as missing top-level section-0 decode.
+
+Exporter-facing implication for unresolved map-104 families:
+
+- For unresolved `0x0042` bands, do not treat type-level art as a static `type -> bundle` table. The executable path is `type install` -> `active/built per-type lane` -> `ctor object bind` -> `live frame token` -> `kind-based submitter`.
+- Mixed-family donor heuristics should remain fenced unless they match this chain (especially resource kind parity and frame-state behavior), because map-104 failures are now better explained by wrong runtime resource-family merges than by missing section-0 placement decoding.
+
+## Latest Visibility Routing, Ordering, And Draw-Lane Pass (2026-04-12)
+
+Pass objective:
+
+- close final routing/order/submitter ambiguity around stage-1 versus stage-2 world draw lanes
+- keep edits conservative and evidence-backed in live `SLUS_002.68`
+
+Functions and helpers inspected in this pass:
+
+- routing/input lane:
+ - `psx_player_object_integrate_motion_and_route_visible` (`0x8001263c`, renamed this pass from duplicate generic name)
+ - `psx_object_update_runtime_input_modes` (`0x80012c30`)
+ - `psx_object_integrate_motion_and_route_visible` (`0x800131a8`)
+- stage-1 order graph lane:
+ - `psx_main_visible_order_graph_detach_object` (`0x8002c89c`)
+ - `psx_main_visible_order_graph_unlink_pair` (`0x8002ca74`)
+ - `psx_main_visible_list_sort_range` (`0x8002d778`)
+ - `psx_main_visible_list_swap_entries` (`0x8002e064`)
+- draw and submit lane:
+ - `psx_draw_world_visible_passes` (`0x80041378`)
+ - `psx_draw_special_visible_queue` (`0x80041144`)
+ - `psx_draw_main_visible_object` (`0x80041458`)
+ - `psx_sprite_resource_submit_frame` (`0x80044bdc`)
+ - `psx_image_table_submit_frame` (`0x80044e9c`)
+
+Live edits applied in this pass:
+
+- rename:
+ - `0x8001263c`: `psx_object_integrate_motion_and_route_visible` -> `psx_player_object_integrate_motion_and_route_visible`
+- data labels:
+ - `0x800a9f48` -> `psx_clut_table_by_resource_bank`
+ - `0x800a9f66` -> `psx_clut_override_table_by_palette_token`
+- decompiler comments:
+ - `0x800131a8`: explicit stage split (`type==4 || flags_1c&0x0400` -> stage-2 special queue)
+ - `0x8002d778`: stage-1 dependency sorter and policy-bit influence (`0x0008`, `0x0600`)
+ - `0x8002ca74`: directed order-edge unlink contract
+ - `0x8002c89c`: full order-graph detach contract
+ - `0x80041378`: fixed world draw pass sequence (stage-1, stage-2, then HUD)
+ - `0x80041458`: main-visible submitter and palette-token override rule
+ - `0x80041144`: stage-2 submitter rule parity, no authored palette-token override
+ - `0x80044bdc`: sprite submit upload/refresh and CLUT override behavior
+ - `0x80044e9c`: image-table submit metadata-only frame resolve and CLUT override behavior
+
+Strongest evidence recovered:
+
+1. Stage routing split in `0x800131a8` is explicit and branch-local: `type==4 || (obj+0x1c & 0x0400)` calls `psx_project_object_special_visible_queue`; the fallthrough path calls `psx_project_object_main_visible`.
+2. World pass submission order in `0x80041378` is explicit and fixed: iterate sorted main-visible slice -> draw special-visible queue -> draw HUD/overlay.
+3. Stage-1 ordering is not a plain z-sort: `0x8002d778` repeatedly resolves dependency counts and may call `psx_main_visible_order_graph_unlink_pair`; graph maintenance is handled by `0x8002ca74` and bulk detach by `0x8002c89c`.
+4. Submitter choice is lane-independent and strictly resource-kind based at both call sites (`0x80041458`, `0x80041144`): `kind==5` routes to `psx_image_table_submit_frame`; else `psx_sprite_resource_submit_frame`.
+5. Palette handling differs by lane: main-visible computes an authored high-byte token from source fields (`+0x06` for `0x003e..0x00ab`, `+0x0c` for `>=0x00ac`) and ORs it into submit flags, while special-visible currently passes only orientation-flip bit (no authored token injection).
+6. Both submitters converge on identical CLUT override semantics: default from `psx_clut_table_by_resource_bank`, optional override through `psx_clut_override_table_by_palette_token[(flags>>8)]` when high-byte token is present.
+
+Renderer implication for map-viewer quality:
+
+- stage-1/stage-2 should remain distinct queues through export and replay; merging them collapses ordering and palette behavior.
+- ordering must preserve graph constraints, not only depth heuristics.
+- frame submitter must be selected from bound resource kind (`5` image-table, otherwise sprite) independent of lane.
+- palette override behavior should be lane-aware: apply authored token override in main-visible only; keep stage-2 on baseline CLUT unless separate evidence introduces a stage-2 override source.
+
+## 2026-04-12 Exporter Follow-Through
+
+The renderer/cache builder now follows two of the strongest executable-backed constraints from the visibility/draw pass:
+
+1. Authored palette override bytes are only consumed for records whose authored route flags currently classify them as main-visible. If a record carries a palette token but routes to the special-visible lane, scene export now preserves that token only as diagnostics and keeps the rendered palette on the bundle-default/heuristic path.
+2. Cross-map donor reuse is now fenced by recovered active-header `payloadKind` from `DAT_800758d8`, so the exporter no longer treats image-table-backed and sprite-backed types as interchangeable donor candidates when borrowing unresolved art.
+
+That does not close the remaining unresolved families by itself, but it removes two broad sources of false positives:
+
+- stage-2 objects inheriting authored palette overrides that the executable does not inject
+- unresolved types borrowing donors from the wrong resource-kind family
+
+## Latest Runtime/Control-Island Policy Pass (2026-04-12)
+
+Pass objective:
+
+- test whether the runtime/control island around `0x80063e54` / `0x80063e68` / `0x800675ec` and per-type policy table `0x800675f8` can directly explain unresolved visible-art family splits on `map 104`
+- apply only conservative live edits for helpers where behavior is directly evidenced
+
+Functions inspected in this pass (focus set):
+
+- `psx_level_post_load_runtime_reset` (`0x80039ef4`) - edited (comment)
+- `psx_object_integrate_motion_and_route_visible` (`0x800131a8`, policy read at `0x8001353c`) - edited (comment)
+- `psx_draw_main_visible_object` (`0x80041458`, policy read at `0x80041604`) - edited (comment)
+- `psx_main_visible_order_compare_pair_for_graph` (`0x8002bf0c`) - edited (comment)
+- passcode/control state block around `0x80034d60` (table-pair control gate) - edited (disassembly comment)
+
+Conservative helper renames landed (runtime-marker island family):
+
+- `0x8002e598`: `FUN_8002e598` -> `psx_marker_channel_runtime_state_init_heap_block`
+- `0x8002e484`: `FUN_8002e484` -> `psx_marker_channel_get_mode2_meter_value`
+- `0x8002e498`: `FUN_8002e498` -> `psx_marker_channel_try_spend_mode2_meter`
+- `0x8002e3e8`: `FUN_8002e3e8` -> `psx_marker_channel_add_mode2_meter_and_queue_event`
+- `0x800308ac`: `FUN_800308ac` -> `psx_marker_channel_refresh_mode2_active_slot`
+- `0x800304c4`: `FUN_800304c4` -> `psx_marker_channel_cycle_active_slot_by_direction`
+
+Recovered island/policy interaction semantics (strongest current read):
+
+1. `0x80063e68` and `0x80063e54` act as a reciprocal control-gating pair in post-load/passcode transition flow: selected map id maps to slot via `0x80063e68`, then is validated by reverse map lookup via `0x80063e54` before mode-action side effects proceed.
+2. `0x800675ec` is the marker-channel runtime-state block pointer used by loadout/mode-action/reset and marker event helpers; this is a control/runtime lane, not a direct per-object art-resource lane.
+3. `0x800675f8` policy words are consumed in routing/order/draw as behavior bits by type:
+ - route-side read (`0x80013550`) uses `0x1000` for nearby-interaction publication behavior after lane route decision
+ - draw-side read (`0x8004161c`) uses `0x2000` for a render-state branch after submitter/lane are already chosen
+ - order-graph compare (`0x8002bf2c`) uses `0x0600` class behavior during pair ordering
+
+Strongest evidence for/against this being the visible-art family split:
+
+- Against direct lane-split causality (strong): stage-1 vs stage-2 world-visible lane choice is object-local `obj+0x1c & 0x0400` at `0x80013518`, not a direct read of `0x800675f8` or the `0x63e54/0x63e68/0x675ec` island.
+- Against direct submitter-split causality (strong): sprite vs image-table submitter selection is resource-kind based at submit sites, before/independent of policy bit tests.
+- For secondary visual influence (moderate): policy bits (`0x2000`, `0x0600`, `0x1000`) can still change render-state branch, draw ordering class, and interaction publication. These can alter final presentation behavior but are not currently the primary discriminator for unresolved family resource/lane identity.
+
+Concrete exporter implication for unresolved `map 104` cohorts:
+
+- keep island/policy diagnostics, but do not use them as first-key art-family split selectors:
+ - primary split keys remain bound resource kind, `obj+0x94` latched frame token, and route bit `obj+0x1c & 0x0400`
+ - island/policy words should be recorded as secondary modifiers (`orderingClass`, `drawPolicyBits`, `publishPolicyBits`) to explain same-resource visual divergence
+- practical fallback rule: if cohorts differ only by island/policy bits and not by resource kind/frame token/route lane, treat them as one art family with policy-variant rendering, not separate base-art families.
+
+## Latest Storage-Mapping Pass (2026-04-11)
+
+Pass objective:
+
+- stay on live PSX storage ownership and runtime-bank install chain for unresolved `map 104` type `0x0042`
+- avoid draw-path broadening and focus on subordinate table/blob ownership
+
+Functions and globals inspected in this pass:
+
+- loader and blob chain:
+ - `wdl_resource_bundle_load_by_index` (`0x80039444`)
+ - `psx_lzss_unpack_into_level_buffer` (`0x8003b00c`, renamed this pass)
+ - `psx_lzss_pack_level_buffer` (`0x8003aba8`, renamed this pass)
+ - `psx_load_type_state_banks` (`0x8003917c`)
+ - `psx_cache_type_art_descriptor_and_resource` (`0x80045ffc`)
+- section-0 authored dispatch:
+ - `psx_dispatch_section0_dispatch_roots` (`0x800256b0`)
+ - `psx_dispatch_section0_constructor_placements` (`0x800258cc`)
+ - type-`0x0042` descriptor row pointer/value anchors:
+ - `0x80063220` -> `0x800626f8`
+ - row callbacks include `psx_spawn_compound_record_advance_state_once` (`0x80013618`), `psx_spawn_simple_record_set_active_flag` (`0x8001372c`), and update/release slots
+- constructor/runtime-bank consumers:
+ - `psx_object_create_simple_record` (`0x800249f4`)
+ - `psx_object_create_compound_record` (`0x80024eec`)
+ - `psx_draw_main_visible_object` (`0x80041458`)
+ - `psx_family_wrapper_spawn_compound_pair_y_and_type42_mode_gate` (`0x800230e4`)
+- key storage globals:
+ - `psx_level_decompressed_state_buffer` (`0x8006769c`)
+ - `psx_level_state_compressed_blob` (`0x8006b5d8`, renamed this pass)
+ - `psx_level_heap_cursor` (`0x8006763c`, renamed this pass)
+ - `psx_section0_dispatch_root_records` (`0x80067720`)
+ - `psx_section0_constructor_placement_records` (`0x800678f0`)
+ - `DAT_80067938`, `DAT_80067838`, `DAT_80067840`, `DAT_800676d8`
+ - `psx_type_policy_table_ptr` (`0x800675f8`)
+ - `psx_type_art_active_header_bank` (`0x800758d8`)
+ - `psx_type_art_built_resource_bank` (`0x800758c8`)
+ - `psx_type_state_script_bank` (`0x800758cc`)
+ - `psx_type_simple_component_bank` (`0x800758d0`)
+ - `psx_type_companion_extents_bank` (`0x800758d4`)
+
+Applied live renames/comments in this pass:
+
+- renames:
+ - `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`
+- comments:
+ - at `0x80039af0`: compressed level-state blob loads into `psx_level_state_compressed_blob`, then unpacks into `psx_level_decompressed_state_buffer` before root/constructor dispatch
+ - at `0x800249f4`: constructor bind note for `psx_type_art_active_header_bank[type]` and `psx_type_art_built_resource_bank[type]`
+
+Recovered ownership chain (strongest current read):
+
+1. `wdl_resource_bundle_load_by_index` reads the level section header, allocates contiguous level heap storage, and installs section pointers (`psx_section0_dispatch_root_records`, `psx_section0_constructor_placement_records`, plus subordinate slices such as `DAT_80067938`, `DAT_80067838`, `psx_type_policy_table_ptr`, `DAT_80067840`, `DAT_800676d8`).
+2. The same loader reads compressed state into `psx_level_state_compressed_blob` and unpacks through `psx_lzss_unpack_into_level_buffer` into `psx_level_decompressed_state_buffer` (`DAT_8006769c`), then proceeds with root/constructor section dispatch setup.
+3. Type runtime banks are installed from subordinate bundle sections: `psx_load_type_state_banks` seeds `psx_type_state_script_bank`, `psx_type_simple_component_bank`, and `psx_type_companion_extents_bank`; descriptor/resource install paths seed `psx_type_art_active_header_bank` and `psx_type_art_built_resource_bank`.
+4. Section-0 authored records feed object construction via descriptor-table callback rows. For `0x0042`, both root and constructor families resolve through `0x80063220 -> 0x800626f8` and enter the same generic create/update/release family.
+5. During object creation and presentation, runtime art/state ownership is bank-driven, not direct-authored-draw-driven: constructors bind through `psx_type_art_active_header_bank[type]` and `psx_type_art_built_resource_bank[type]`, while draw/update logic also consumes `psx_type_policy_table_ptr[type]` policy bits.
+
+Storage nodes still unresolved (blocking full `map 104` `0x0042` closure):
+
+1. exact schemas and semantics for `DAT_80067938`, `DAT_80067838`, `DAT_80067840`, and `DAT_800676d8` (currently installed by offset in the section pack but weakly typed downstream)
+2. map-104-specific ownership/meaning of `psx_level_decompressed_state_buffer` payload slices after inflate (which words materially feed type-`0x0042` object family divergence)
+3. explicit per-item runtime correlation for sample set `25/30/31/35/85/86`: bound resource pointer identity, `resource->kind`, and latched frame/state channels against shared `0x0042` descriptor row
+4. final control interaction between section-installed policy (`psx_type_policy_table_ptr`), constructor-seeded `obj+0x1c` flags, and any deferred-control reads from subordinate tables before submit
+
+## Latest Object-Local 0x0400 Provenance Pass (2026-04-11)
+
+## Focused Type-Policy Provenance Pass (2026-04-11)
+
+Focused target:
+
+- `DAT_800675f8` / `psx_type_policy_table_ptr`
+- concrete map-104 relevance for type `0x0042` and control type `0x0066`
+- installation source, reader masks, and storage ownership limits
+
+Functions/addresses inspected in this pass:
+
+- writer/install path:
+ - `wdl_resource_bundle_load_by_index` (`0x80039444`), write at `0x800398f0`
+- key readers sampled directly:
+ - `psx_object_integrate_motion_and_route_visible` (`0x8001353c`) -> `& 0x1000`
+ - `psx_draw_main_visible_object` (`0x80041604`) -> `& 0x2000`
+ - `psx_main_visible_order_compare_pair_for_graph` (`0x8002bf0c`, `0x8002c174`, `0x8002bf5c`) -> `& 0x0600`
+ - `psx_object_state_machine_dispatch_tick` (`0x8001a280`, `0x8001aa44`) -> `& 0x0800`
+ - `psx_object_update_nearby_interactions` (`0x8002957c`, `0x80029970`, etc.) -> `& 0x0100`, `& 0x4000`, `& 0x0008`
+ - `psx_object_register_contact_pair` / related overlap helpers (`0x80028488` family) -> `& 0x0008`
+ - `psx_find_nearest_los_target_with_typeflag10_or20` (`0x800149cc`, `0x80014b14`) -> `& 0x0010`, `& 0x0020`
+ - `psx_update_nearest_policy80_contact_marker` (`0x8001408c`) -> `& 0x0080`
+
+Applied live symbol/comment updates (small and evidence-backed):
+
+- rename:
+ - `FUN_8001408c` -> `psx_update_nearest_policy80_contact_marker`
+- comments:
+ - `0x800398f0`: marks `psx_type_policy_table_ptr` install from level section pack (`DAT_80067838 + local_b4`)
+ - `0x8002bf0c`: marks `0x0600` class compare role in stage-1 ordering
+ - `0x80041604`: marks `0x2000` draw-time semitrans gate
+ - `0x800140c8`: marks `0x0080` policy gate before proximity trace selection
+
+Best evidence on provenance and storage ownership:
+
+1. `psx_type_policy_table_ptr` has exactly one recovered writer in the executable (`0x800398f0`) and is rebuilt during `wdl_resource_bundle_load_by_index`.
+2. The installed pointer is computed from the contiguous map bundle section pack (`DAT_80067838 + local_b4`), with subsequent slices derived by offset (`+ local_b0`, `+ local_ac`, `+ local_a8`).
+3. This establishes ownership as level-loaded runtime storage (LSET bundle section data), not immutable executable ROM data.
+4. Static executable-only recovery of concrete policy rows for type `0x0042` / `0x0066` remains blocked because the table contents are not authored in-place in the code image.
+5. Runtime-adjacent cache evidence is still incomplete for these rows: current map-104 sample rows (`25/30/31/35/85/86` for `0x0042`, `53` for `0x0066`) still export `runtimeDiagnostic.typePolicy.sampled=false` and `word=null`.
+
+Current interpretation impact for map-104 `0x0042` art split:
+
+- `psx_type_policy_table_ptr[type]` is now more strongly confirmed as shared per-type policy gating (interaction, ordering, and draw flags), not a per-lane constructor-family selector.
+- This table should stay secondary for explaining the `0x0042` `64x64` vs `64x40` split until concrete map-level row capture is available; primary discriminators remain resource identity, pre-latch/latch state flow, and route/state bits.
+
+Focused target:
+
+- route branch inside `psx_object_integrate_motion_and_route_visible`: `obj+0x1c & 0x0400`
+- fixed sample families: root `u5=0x0022`, root `u5=0x0030`, constructor `u5=0x0030`
+- fixed sample pack: `item:25/30/31/35/85/86`
+
+Concise provenance summary (inspected writer/preserve/transform chains):
+
+- Object-local writers (direct `obj+0x1c` stores):
+ - `psx_object_create_simple_record` (`0x80024b48`) — copies authored `u5` into `obj+0x1c` at creation (strong object-local seed).
+ - `psx_object_create_compound_record` (`0x80025040`) — same copy behavior for compound constructors.
+
+- Named mutators examined (mutate low control bits, none introduce direct `0x0400` set/clear):
+ - `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`
+
+- Strongest recovered non-object-local `0x0400` writers (nested/runtime or policy-driven):
+ - `psx_object_state_machine_dispatch_tick` (`0x8001a078`) — nested runtime-state write.
+ - `psx_object_handle_control_pair_0a` (`0x80022a14`) — global/policy-adjacent write affecting routing control words.
+
+Conclusion: constructors remain the primary object-local seed for `obj+0x1c` (including `0x0400` when authored), but the strongest dynamic `0x0400` writes seen so far are nested/runtime or policy-driven and not direct object-local transforms. A focused live runtime capture on the fixed sample pack is still required to conclusively separate authored `0x0400` from nested/policy-set `0x0400` during the `psx_object_integrate_motion_and_route_visible` branch.
+
+Addresses inspected in this pass:
+
+- route-side consumer and branch context: `0x8001263c..0x80012c2c`, `0x800131a8..0x80013614`
+- constructor/local writers: `0x80024b48`, `0x80025040`, `0x80031514`
+- transition/mutator lanes touching `obj+0x1c`: `0x8001918c`, `0x800191b0`, `0x800191f4`, `0x8001bde8`, `0x80025dd4`, `0x80025e88`, `0x8002b0c8`, `0x8002b174`, `0x80022b28`
+- non-object-local `0x0400` controls in same neighborhood: `0x8001a078`, `0x80022a14`
+
+Recovered chain for object-local `0x0400` in this sample lane:
+
+1. Section-0 authored row feeds constructor.
+2. Constructor copies authored `u5` directly into `obj+0x1c` (`psx_object_create_simple_record` / `psx_object_create_compound_record`).
+3. Later named mutators in the `0x0042` path rewrite low control bits (`0x0001/0x0002/0x0004/0x0020/0x0040/0x0080/0x0100/0x0200`) but no inspected object-local write site introduces a new `obj+0x1c |= 0x0400` or `obj+0x1c &= ~0x0400` transform.
+4. Route split still consumes `obj+0x1c & 0x0400` at `psx_object_integrate_motion_and_route_visible`.
+
+Current strongest read stays unchanged but tighter:
+
+- strongest object-local writer for `0x0400` presence/absence is still authored `u5` copied at object creation
+- strongest `0x0400` writer recovered in active runtime logic remains nested/global state, not object-local `obj+0x1c`
+
+Implication for `map 104` `0x0042` remains open but narrower:
+
+- yes, live capture is still needed to close the gap for `item:25/30/31/35/85/86` because static writer recovery still does not pin a direct object-local runtime transformer for bit `0x0400`
+
+## Latest Table Inventory Pass (2026-04-11)
+
+This pass stayed broad on table coverage but concrete on evidence, centered on unresolved `map 104` / type `0x0042` storage and render routing context.
+
+Functions/data inspected in this pass:
+
+- Descriptor and transition tables:
+ - `0x80063118` (`psx_type_descriptor_table` base)
+ - `0x80063220` (`psx_type_descriptor_ptr_0042`)
+ - `0x80063b4c` (`psx_type_transition_selector_rows`)
+- Type-policy and runtime-bank globals:
+ - `0x800675f8` (`psx_type_policy_table_ptr`)
+ - `0x800758c8` (`psx_type_art_built_resource_bank`)
+ - `0x800758d0` (`psx_type_simple_component_bank`)
+ - `0x800758d4` (`psx_type_companion_extents_bank`)
+ - `0x800758d8` (`psx_type_art_active_header_bank`)
+- Marker/runtime control island near post-load reset:
+ - `0x80063e68` (`DAT_80063e68`)
+ - `0x80063e54` (`DAT_80063e54`)
+ - `0x800675ec` (`DAT_800675ec`)
+ - `0x80067728` (current level index)
+- Key functions touched during inventory/decompile:
+ - `psx_object_create_simple_record`
+ - `psx_object_create_compound_record`
+ - `psx_dispatch_section0_dispatch_roots`
+ - `psx_dispatch_section0_constructor_placements`
+ - `psx_run_live_object_type_updates`
+ - `psx_object_select_state_from_transition_table`
+ - `psx_object_integrate_motion_and_route_visible`
+ - `psx_draw_main_visible_object`
+ - `psx_cache_type_art_descriptor_and_resource`
+ - `psx_level_post_load_runtime_reset`
+ - `psx_section0_dispatch_root_seed_marker_channel_table`
+
+Applied live symbol updates (small, evidence-backed):
+
+- `0x8002f190` -> `psx_marker_channel_dispatch_mode_action`
+- `0x8002f250` -> `psx_marker_channel_runtime_state_clear`
+- `0x80031840` -> `psx_marker_channel_mode_is_enabled`
+- `0x8003185c` -> `psx_marker_channel_get_mode_step_value`
+- `0x80031878` -> `psx_marker_channel_runtime_state_snapshot`
+- `0x80031a3c` -> `psx_marker_channel_runtime_state_restore`
+
+Applied targeted comments:
+
+- `0x80063e68`: per-level marker-channel profile seed byte indexed by current level index (`DAT_80067728`) and consumed by seed/apply/reset flow.
+- `0x800675ec`: pointer to `0x90`-byte marker-channel runtime state block shared by clear/snapshot/restore/accessor paths.
+- `0x80063e54`: per-level companion byte table read by post-load reset and adjacent control helper, likely paired with `DAT_80063e68` as profile/eligibility control.
+
+Concrete table-role clarifications from this pass:
+
+- Type `0x0042` still resolves through generic descriptor table fetch at `psx_type_descriptor_table[0x0042]` (no unique descriptor fork).
+- The transition selector row table at `0x80063b4c` remains narrow and state-chooser specific (only direct reads in `psx_object_select_state_from_transition_table`).
+- `psx_type_policy_table_ptr` (`0x800675f8`) remains a draw/routing policy source (including semitrans and ordering/publication gating) rather than a unique map-104 art discriminator.
+- Runtime-bank trio (`0x800758d0/0x800758d4/0x800758d8`) is confirmed as install/reset-backed loader products consumed by constructors and downstream draw/resource setup.
+- The `0x80063e54/0x80063e68/0x800675ec` cluster is now tighter as a level-indexed marker/control runtime island that can affect post-load state and channel behavior, and is therefore a legitimate remaining table family for map-104 type-`0x0042` divergence work.
+
+## Latest Table-Typing Pass (2026-04-11)
+
+This pass stayed focused on turning the newly named storage/control pointers into actual table roles instead of broadening back out into generic render theory.
+
+Applied live symbol updates in this pass:
+
+- `DAT_80067938` -> `psx_ctor_placement_section_ptr`
+- `DAT_800676d8` -> `level_clut_table_ptr`
+- `DAT_80067840` -> `psx_control_opcode_stream_table`
+- `DAT_80063e54` -> `psx_level_selector_table_80063e54`
+- `DAT_80063e68` -> `psx_level_channel_table_80063e68`
+
+And the `DAT_800675ec` rename from the previous batch now has a stronger local field map as `psx_marker_channel_runtime_block`.
+
+### `psx_ctor_placement_section_ptr` is a real section-pack root, but not the final row family by itself
+
+The top consumer pair (`wdl_resource_bundle_load_by_index` and `psx_apply_deferred_control_command`) now shows that `psx_ctor_placement_section_ptr` is installed as a section root and then used to derive subordinate bases such as `section_pack_source_80067838`.
+
+The strongest current consumer path is not the original six-halfword constructor row itself. Instead it uses a small u16 index table rooted near `psx_ctor_placement_section_ptr - 2`, multiplies the selected index by `8`, and then walks `8`-byte rows out of the derived `section_pack_source_80067838` base.
+
+So the safe current read is:
+
+- `psx_ctor_placement_section_ptr` is the constructor-placement section root installed at level load
+- some downstream control/deferred-command logic uses it as a header/index root for a different `8`-byte row family
+- no direct `type == 0x0042` comparison was recovered in this consumer pair yet
+
+That narrows the next storage question again: distinguish the original constructor-placement row family from the subordinate control/index rows derived from the same section root.
+
+### `level_clut_table_ptr` is palette/CLUT support, not the missing `0x0042` art table
+
+The current best evidence from `level_palette_expand_5bit_to_16color` and `level_palette_upload_cluts` is that `level_clut_table_ptr` is a halfword array whose low `5` bits are used as a CLUT index (`0..31`).
+
+That keeps this table on the level-palette side of the pipeline:
+
+- useful for explaining color/CLUT choice
+- not a strong candidate for the remaining `0x0042` shape/frame split by itself
+
+So map-104 `0x0042` should still treat `level_clut_table_ptr` as palette support rather than the missing world-object family discriminator.
+
+### `psx_control_opcode_stream_table` is control/state support, not a direct world-art selector
+
+The writer at `0x80039908` and the strongest reader at `psx_control_assign_opcode_stream_by_index` now tighten `DAT_80067840` into a real control/opcode-stream pointer table installed from the level bundle.
+
+Its current best role is:
+
+- primary: state/control opcode stream support for constructor/state-machine callers
+- secondary: indirect behavioral influence on presentation through control/state logic
+- not a direct art/frame lookup table for unresolved `0x0042`
+
+That means it still matters for map behavior, but it is not currently the best direct explanation for `64x40` versus `64x64` presentation.
+
+### `psx_marker_channel_runtime_block` is now narrowed to marker/control runtime state with two still-interesting short fields
+
+The latest field pass keeps most of this `0x90`-byte block on the marker/control side:
+
+- `+0x34` = mode byte
+- `+0x6c` = per-mode/per-step byte
+- `+0x88` / `+0x8c` = restored/snapshotted short fields still worth treating as the most presentation-adjacent members in this struct
+
+Snapshot/restore code now clearly reads and writes `+0x88/+0x8c` through global snapshot words before restoring them to the runtime block. That proves they are part of live runtime state, but not yet that they feed ordinary world-object draw.
+
+So this family stays important, but the open question is narrower again: do `+0x88/+0x8c` ever flow into actual render/presentation logic, or do they remain marker/control-only state.
+
+### `psx_level_selector_table_80063e54` and `psx_level_channel_table_80063e68` are per-level control tables
+
+The read windows in `psx_level_post_load_runtime_reset` and `psx_section0_dispatch_root_seed_marker_channel_table` now establish both addresses as per-level byte tables, not scalars.
+
+Current best read:
+
+- `psx_level_channel_table_80063e68` is used by section-0 marker/channel seeding and related dispatch control
+- `psx_level_selector_table_80063e54` is a paired per-level selector/index byte consumed in the same post-load/control island
+
+This keeps the whole `0x63e54/0x63e68/0x675ec` family grouped as one legitimate remaining blocker family for level-specific control/state behavior.
+
+### Install-chain consequence
+
+The install-helper pass also tightened the unpack flow one step further:
+
+- `psx_level_decompressed_state_buffer` is still the unpack destination and immediate source for runtime-bank installation
+- `psx_load_type_state_banks` remains the installer for `DAT_800758cc/d0/d4`
+- `psx_stream_install_type_runtime_banks` remains the packed-stream installer that can populate the art/built-resource side too
+- constructors then consume those installed banks directly when binding drawable resources
+
+So the remaining storage/render gap is now tighter than before:
+
+- not “what installs the runtime banks”
+- not “is `0x0042` hiding behind one unnamed unique descriptor fork”
+- but “which installed rows and control tables still explain the concrete map-104 `0x0042` family split after load”
+
+Highest-value unresolved tables/structures after this pass:
+
+1. `DAT_800675ec` struct layout (`0x90` bytes): fields at `+0x34` bit-flag rows, `+0x6c` step-value rows, and `+0x84..+0x8f` state words still need a named datatype.
+2. `DAT_80063e68` and `DAT_80063e54` exact schema (entry count/semantics) and whether they directly separate map-104 `0x0042` families or only gate marker/control channels.
+3. Type-`0x0042` runtime-bank interpretation at constructor and post-state-advance points: exact relationship between `DAT_800758d8[type]` active header, `DAT_800758c8[type]` built resource, and effective submitted frame group for sample items `25/30/31/35/85/86`.
+4. Per-type policy word capture for `0x0042` in live map-104 sample context (`psx_type_policy_table_ptr[0x0042]`) correlated with stage route (`obj+0x1c` / stage-1 vs stage-2) and bound resource kind.
+
+## Latest Live Cleanup (2026-04-11)
+
+Focused live `SLUS_002.68` cleanup tightened the world-frame/render wrapper lane around
+`0x80031f0c`, `0x80031f9c`, `0x800320bc`, `0x80039dc4`, `0x8003977c`, and `0x800391f0`.
+
+Applied in-database symbol cleanups:
+
+- `0x80031e0c` -> `psx_lset_session_loop`
+- `0x80031f0c` -> `psx_lset_world_frame_wrapper`
+- `0x80031f6c` -> `psx_lset_session_teardown`
+- `0x800350a8` -> `psx_render_mode_dispatch`
+- `0x80039ef4` -> `psx_level_post_load_runtime_reset`
+- `0x80044104` -> `psx_present_frame_and_flip`
+
+Applied direct technical comments at the same lane anchors:
+
+- `0x800320bc`: load-phase sync point between first runtime blob assignment and detached-blob load path.
+- `0x800391f0`: per-type state-script bank assignment path.
+- `0x8003977c`: per-type descriptor bank assignment path.
+- `0x80039dc4`: runtime-header application into active globals.
+
+Operationally this confirms a stable chain for map storage to frame presentation in this lane:
+
+1. `psx_lset_session_loop` drives map-index loads and frame loop control.
+2. `lset_level_bundle_load` loads runtime blobs and stream payload lanes.
+3. Per-type bank installs (`psx_load_type_state_banks` and descriptor-bank writes) seed runtime object resources.
+4. Runtime-header and post-load reset apply mission/level state before steady-state updates.
+5. `psx_lset_world_frame_wrapper` dispatches render mode, world visible draw, and `psx_present_frame_and_flip`.
+
+## Latest Concrete Map 104 0x0042 Pass (2026-04-11)
+
+This batch was anchored to concrete `map 104` scene-cache items from scene fingerprint `3497e7f641856415` rather than another broad helper sweep.
+
+Primary sample pack:
+
+- root lane `0x0030`: `item:30` / `item:31` (`raw words: 0042 0b1f 01bf ... 0030`)
+- root lane `0x0022`: `item:25` / `item:35` (`raw words: 0042 0bff 01a3 ... 0022`, `0042 0b1f 01df 0002 ... 0022`)
+- constructor lane `0x0030`: `item:85` / `item:86` (`raw words: 0042 0b1f/0b3f 019f ... 0030`)
+- control constructor sample: `item:53` (`raw words: 0066 0b13 0183 0000 0001 0020`)
+
+Main live symbol recovery in this pass:
+
+- `0x80017fe8` -> `psx_transition_spawn_and_seed_selector_from_record`
+- `0x80031044` -> `psx_section0_dispatch_root_find_marker_record_by_channel`
+- `0x800460fc` -> `psx_upload_spec_wdl_image_pair_to_vram`
+- `0x800463bc` -> `psx_restore_display_draw_env_after_spec_upload`
+- `DAT_800675f8` -> `psx_type_policy_table_ptr`
+- `DAT_80063a00` -> `psx_type_transition_mode_policy_rows`
+- `DAT_80063b4c` -> `psx_type_transition_selector_rows`
+
+### Family bridge is now explicit
+
+- `psx_dispatch_section0_dispatch_roots` is the root-family entry.
+- `psx_dispatch_section0_constructor_placements` is the constructor-family entry.
+- Both still converge through `psx_type_descriptor_table[0x0042] = 0x80063220 -> 0x800626f8`.
+- That shared row still enters `psx_spawn_compound_record_advance_state_once`, then the shared `psx_object_create_compound_record` / `psx_object_advance_state_script` path.
+
+This is stronger negative evidence against any remaining idea that root versus constructor family, or `0x019f` versus `0x01bf`, chooses a different descriptor family for `0x0042`. The practical split the viewer still needs remains later in state, flags, nested runtime state, and resource/frame choice.
+
+### Spawn-side selector seeding is tighter
+
+`psx_transition_spawn_and_seed_selector_from_record` now makes the spawn-side authored selector seed explicit before the later type-specific turn/reseat logic.
+
+- The spawn-side seed feeds `psx_object_select_state_from_transition_table`.
+- For `0x0042`, `psx_type42_transition_selector_tick` still remains the stronger explanation for live selector `3/4` cases because those low turn selectors can be emitted before the later latch copy.
+
+So exporter/runtime analysis still needs both channels:
+
+- pre-latch selector evidence
+- later latched `obj+0x94`-style state
+
+### Authored lane to `obj+0x1c` handoff is now direct
+
+The constructor-side route-flag story is now more concrete than before.
+
+- Constructors directly copy the authored lane/flags word into `obj+0x1c`.
+- Runtime logic then mostly mutates low bits such as `0x0002`; it is not inventing the initial `0x0022` versus `0x0030` split after the fact.
+
+For the current `map 104` sample pack this means:
+
+- lane `0x0022` starts as authored `{ 0x0020, 0x0002 }`
+- lane `0x0030` starts as authored `{ 0x0020, 0x0010 }`
+
+That is why `runtimeDiagnostic.objectLocalRouteFlags.initialWord` should be treated as meaningful initial authored state for these records, not only as a later runtime residue.
+
+### Nested `0x0400` still reads stronger than object-local `0x0400`
+
+This pass still did not recover a new direct object-local `obj+0x1c |= 0x0400` writer for `0x0042`.
+
+- The strongest recovered `0x0400` write remains nested-state-side in the wider runtime state machine.
+- `0x0042` continues to read and rewrite nested runtime-state words in the same neighborhood.
+
+So the current best read stays split:
+
+- object-local `obj+0x1c` is real authored route/orientation state
+- nested runtime state still carries the strongest recovered `0x0400` stage-selection signal
+
+### `DAT_800675f8` is tighter as a level-loaded type-policy pointer
+
+`DAT_800675f8` is now better modeled as `psx_type_policy_table_ptr`.
+
+- It is installed at level load.
+- Runtime readers then index it by `type_id << 1` and test the resulting halfword.
+- The currently strongest known masks remain:
+ - `0x1000`: nearby-interaction publication suppression
+ - `0x0600`: stage-1 ordering class
+ - `0x2000`: main-visible semitrans policy
+
+That means `runtimeDiagnostic.typePolicy.word` should currently be treated as type-global within a loaded level/session, not as a lane-specific candidate. The remaining gap is the live numeric `0x0042` word for the concrete `map 104` cases, not the table's general role.
+
+### Latest sample-pack follow-up: resource/frame, decode, and routing
+
+The next six-agent concrete pass widened coverage across the same `map 104` anchor pack instead of reopening generic theory.
+
+Additional live renames from this pass:
+
+- `0x80018330` -> `psx_transition_selector_probe_nearby_overlap`
+- `0x80018414` -> `psx_transition_selector_probe_marker_overlap`
+- `0x800261f4` -> `psx_snapshot_active_object_runtime_rows`
+- `0x80025b88` -> `psx_release_all_active_objects_and_reset_type_runtime_banks`
+- `0x8002f518` -> `psx_section0_dispatch_root_seed_marker_channel_table`
+- `0x80030ee0` -> `psx_section0_dispatch_root_lookup_marker_record_by_kind_channel`
+- `0x80030f60` -> `psx_section0_dispatch_root_lookup_marker_slot_by_kind_channel`
+- `0x80030fcc` -> `psx_section0_dispatch_root_get_marker_slot_triplet_by_index`
+- `0x800311c4` -> `psx_section0_dispatch_root_apply_packed_channel_actions`
+- `0x80031454` -> `psx_section0_dispatch_root_spawn_simple_from_marker_record`
+
+Three new practical conclusions matter for the viewer/exporter.
+
+First, the current `0x0042` `64x64` versus `64x40` split still looks more like a shared-resource / different-frame-state problem than a distinct pre-draw resource bind.
+
+- The constructor paths bind type-indexed active-header state before the lane word is copied.
+- Draw-time submitter/frame-geometry consumers still take the live frame token from `obj+0x94`.
+- So for the current `map 104` sample pack, lane `0x0022` versus `0x0030` is still not strong evidence of a different bound resource by itself.
+
+That keeps the strongest next exporter field recommendation the same in spirit but tighter in scope: preserve a stable bound-resource identity separately from the live frame/state token.
+
+Second, the frame-state bridge is now more explicit end to end.
+
+- `psx_object_select_state_script` installs selector identity into `obj+0x9e`, plus the script cursor and intermediate token state.
+- `psx_object_advance_state_script` later latches the live frame/state token into `obj+0x94`.
+- Both `psx_project_object_main_visible` and `psx_project_object_special_visible_queue` then consume `obj+0x94` for frame origin/width/height queries before submission.
+
+This sharpens the current exporter rule again: `obj+0x9e` and `obj+0x94` should not be collapsed into one selector field for `0x0042`.
+
+Third, the stage-1 versus stage-2 branch point is now locally explicit even though the exact live `0x0400` provenance for this sample pack is still not closed.
+
+- `psx_object_integrate_motion_and_route_visible` tests `obj+0x1c & 0x0400` at the decisive branch.
+- For `0x0042`, the type-`4` special case is irrelevant, so object-local `0x0400` remains the practical stage-2 discriminator.
+- The strongest recovered `0x0400` write is still nested-state-side rather than a direct type-`0x0042` object-local OR site.
+
+So the current best routing verdict for the fixed sample pack remains:
+
+- broadly stage-1/main-visible by default
+- stage-2 only when object-local `obj+0x1c` actually carries `0x0400` at the routing point
+
+The remaining gap is now very narrow: one live capture pass that samples bound resource identity, `obj+0x9e`, `obj+0x94`, `obj+0x1c`, and the final queue/list path for items `25/30/31/35/85/86`.
+
+### Focused frame-geometry pass: where `64x40` vs `64x64` is chosen (2026-04-11)
+
+This pass started from the now-stable selector/latch bridge and only followed width/height/origin consumers.
+
+Verified bridge and consumer addresses:
+
+- `0x800260e8` `psx_object_select_state_script`: installs selector to `obj+0x9e`
+- `0x80025d68` `psx_object_advance_state_script`: latches current frame/state into `obj+0x94`
+- `0x80040d44` `psx_project_object_main_visible`: reads `obj+0x94`, then queries frame origin/width/height from `obj+0x10`
+- `0x80040f78` `psx_project_object_special_visible_queue`: same `obj+0x94` frame-geometry query path
+- `0x8004513c` `psx_resource_frame_origin_x`, `0x800451d0` `psx_resource_frame_origin_y`
+- `0x80045014` `psx_resource_frame_width`, `0x800450a8` `psx_resource_frame_height`
+
+Decisive evidence from decompile:
+
+- The projector functions read frame token from `obj+0x94` before any frame-geometry call.
+- Width/height/origin helpers branch only on resource kind (`4` sprite-header frame records vs `5` image-table frame records) and use the caller-provided frame index.
+- No later projector/draw step rewrites width/height into a `64x40` vs `64x64` family split; later logic applies orientation flip (`obj+0x1c & 0x0002`) and routing/queue behavior, not a replacement size family.
+
+Conclusion for fixed `map 104` `0x0042` sample pack:
+
+- The split is strongest as a live frame-token outcome (`obj+0x94`) within a shared type resource lane, not as a late presentation modifier.
+- A resource-header-family branch still exists technically (`kind 4` vs `kind 5` access path), but for same-type `0x0042` objects this is type-level and upstream of per-object frame choice.
+
+Minimum runtime fields to preserve in exporter/viewer for faithful reproduction:
+
+- `obj+0x10` bound resource identity, including resource `kind` (`4` vs `5`)
+- `obj+0x94` live latched frame/state token (the geometry key)
+- `obj+0x9e` authored selector seed (kept separate from `obj+0x94`)
+- `obj+0x1c` route/orientation flags (`0x0002` flip behavior and `0x0400` stage route)
+
+Concrete sample-pack sanity check from current scene cache (`map 104`, fingerprint `3497e7f641856415`):
+
+- item `25` and `35` currently render `64x40` with route seed `0x0022`
+- item `30`, `31`, `85`, `86` currently render `64x64` with route seed `0x0030`
+
+This keeps the next unresolved step narrow: capture live bound-resource kind plus live `obj+0x94` for those items to close any remaining uncertainty about per-item frame-token divergence.
+
+## Final Live Map 104 Cohort Pass (2026-04-12)
+
+Pass scope:
+
+- active writable `SLUS_002.68`
+- scene fingerprint `3497e7f641856415`
+- fixed anchors:
+ - root `0x0022`: items `25/35`
+ - root `0x0030`: items `30/31`
+ - constructor `0x0030`: items `85/86`
+ - control `0x0066`: item `53`
+
+Functions inspected this pass (create/update/draw + ordering):
+
+- `psx_object_create_simple_record` (`0x80024b48`) - inspected, comment updated
+- `psx_object_create_compound_record` (`0x80025040`) - inspected, comment updated
+- `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) - inspected, comment updated
+- `psx_object_advance_state_script` (`0x80025d68`) - inspected, no edit this pass
+- `psx_draw_main_visible_object` (`0x80041458`) - inspected, no edit this pass
+- `psx_draw_special_visible_queue` (`0x80041144`) - inspected, no edit this pass
+- `psx_main_visible_order_compare_pair_for_graph` (`0x8002bf0c`) - inspected, comment updated
+
+Live artifacts changed in this pass:
+
+- decompiler comments added/updated:
+ - `0x80024b48`
+ - `0x80025040`
+ - `0x800131a8`
+ - `0x8002bf0c`
+
+Concrete cohort findings from scene-cache fields and executable behavior:
+
+1. Differences that are still real across the fixed map-104 cohorts:
+ - object-local route seed differs exactly as authored:
+ - root `0x0022` (`25/35`) -> `initialWord=0x0022` (`bit0x0002=1`, `bit0x0020=1`)
+ - root/constructor `0x0030` (`30/31/85/86`) -> `initialWord=0x0030` (`bit0x0002=0`, `bit0x0020=1`)
+ - control `0x0066` sample (`53`) -> `initialWord=0x0020`
+ - visible frame geometry differs in exported candidates:
+ - `25/35` -> `64x40`
+ - `30/31/85/86` -> `64x64`
+ - `53` -> `64x32`
+2. Differences that still fail to differentiate the anchor cohorts:
+ - stage-route discriminator `bit0x0400` remains clear in all sampled anchors (`routeOutcomeCandidate=main-visible`).
+ - pre-latch dispatch and latched-state runtime captures remain unsampled (`preLatchDispatchSampled=false`, `latchedState.sampled=false`).
+ - per-type policy word remains unsampled (`typePolicy.sampled=false`, `word=null`) for both type `0x0042` and control type `0x0066` anchors.
+ - per-type art-header kind remains unsampled (`resourceKind.sampled=false`), so the kind-4 versus kind-5 split is still unresolved at item granularity.
+
+Strongest executable evidence for the next exporter rule:
+
+- constructors directly preserve authored route/lane word into `obj+0x1c` (`0x80024b48`, `0x80025040`), so `0x0022` versus `0x0030` is a valid authored split signal.
+- routing to stage-2 still requires `type==4 || (obj+0x1c & 0x0400)` (`0x800131a8`), and the fixed anchors currently do not satisfy this.
+- draw submitter path is resource-kind based and shared by both world-visible lanes (`0x80041458`, `0x80041144`); stage-lane choice alone does not pick sprite versus image-table.
+- policy table class bits (`0x0600`) in `0x8002bf0c` are ordering-precedence modifiers, not resource-binding keys.
+
+Practical next exporter rule (highest confidence):
+
+- Keep map-104 `0x0042` cohort partitioning keyed first by authored `record_u5`/`obj+0x1c` seed families (`0x0022` vs `0x0030`) and keep control `0x0066` separate.
+- Do not use `typePolicy` or stage-route `0x0400` as primary split keys for these anchors until runtime sampling is populated.
+
+Safe immediate renderer/exporter change suggested by this evidence:
+
+- In scene export/fallback grouping for unresolved map-104 families, add/keep a hard cohort fence:
+ - `recordRouteFlags.initialWord` must match exactly (`0x0022` group separate from `0x0030`; `0x0066` control separate from `0x0042`).
+- Treat `typePolicy.word` and `resourceKind.activeHeaderKindCandidate` as optional tie-breakers only when sampled, never as required keys when null.
+- This is safe now because it only prevents cross-cohort over-merge (the repeated-wall failure mode) and does not invent new art mappings.
+
## Evidence Base
This model is grounded by a combination of static and runtime-adjacent evidence:
@@ -54,11 +1007,15 @@ Most important named functions and data anchors:
- `psx_object_advance_state_script`
- `psx_object_lookup_variant_entry`
- `psx_object_integrate_motion_and_route_visible`
+- `psx_object_update_runtime_input_modes`
- `psx_project_object_main_visible`
- `psx_project_object_special_visible_queue`
- `psx_draw_world_visible_passes`
- `psx_draw_main_visible_object`
- `psx_draw_special_visible_queue`
+- `psx_main_visible_list_swap_entries`
+- `psx_main_visible_order_graph_unlink_pair`
+- `psx_main_visible_order_graph_detach_object`
- `psx_level_root_record_stream` (`DAT_800678f4`)
- `psx_section0_dispatch_root_records` (`DAT_80067720`)
- `psx_section0_constructor_placement_records` (`DAT_800678f0`)
@@ -70,6 +1027,54 @@ Most important named functions and data anchors:
- `psx_level_decompressed_state_buffer` (`DAT_8006769c`)
- `psx_level_runtime_header_block` (`DAT_80067794`)
+## Resource-Kind Split (Live SLUS_002.68)
+
+The resource creation/submission lane is now explicit enough to treat as stable viewer/exporter evidence.
+
+### Creation and per-type cache
+
+- `psx_cache_type_art_descriptor_and_resource` (`0x80045ffc`) stores the per-type descriptor at `DAT_800758d8[type]` and materialized drawable resource at `DAT_800758c8[type]`.
+- Exact kind branch in this cache helper:
+ - `0x80046048`: `kind == 4` -> `psx_resource_bind_single_image_vram_slot` (`0x800444e4`)
+ - `0x80046054`: `kind == 5` -> allocate bundle wrapper and call `image_bundle_load_to_vram` (`0x80044614`)
+- `psx_create_image_resource_from_descriptor` (`0x80044434`) uses the same discriminator:
+ - `0x8004445c`: `kind == 4` branch
+ - `0x80044468`: `kind == 5` branch
+ - `0x8004447c`: call `psx_resource_bind_single_image_vram_slot`
+ - `0x800444ac`: call `image_bundle_load_to_vram`
+
+### Draw-time submitter selection
+
+- `psx_draw_main_visible_object`:
+ - `0x800415a8`: compare `resource->kind` against `5`
+ - `0x800415c0`: `kind == 5` -> `psx_image_table_submit_frame` (`0x80044e9c`)
+ - `0x800415e0`: otherwise -> `psx_sprite_resource_submit_frame` (`0x80044bdc`)
+- `psx_draw_special_visible_queue`:
+ - `0x800412c8`: compare `resource->kind` against `5`
+ - `0x800412dc`: `kind == 5` -> `psx_image_table_submit_frame`
+ - `0x800412f8`: otherwise -> `psx_sprite_resource_submit_frame`
+- `psx_draw_hud_overlay_pass` has the same split for slot-managed overlays:
+ - `0x80041b5c`: image-table submit path
+ - `0x80041b84`: sprite submit path
+
+### Overlay-lane exception
+
+The HUD/overlay lane now has one important exception to the otherwise clean world-object rule.
+
+- `psx_draw_main_visible_object` and `psx_draw_special_visible_queue` branch directly on `resource->kind == 5`.
+- `psx_draw_hud_overlay_pass` can route through image-table submission from an overlay-slot flag (`0x10`) rather than by treating the slot resource like a normal world-object kind check.
+- `psx_draw_clock_digits_overlay` is a fixed image-table user, not evidence for world-object family binding.
+
+So the viewer/exporter should reuse resource-kind logic for world-object lanes, but it should not mine the HUD/overlay lane as if it were one more map-facing art discriminator.
+
+### Runtime implication for viewer binding
+
+- Treat `resource kind == 5` as table-backed UV/frame metadata (`psx_image_table_submit_frame`) and `resource kind != 5` as streamed/decoded sprite uploads (`psx_sprite_resource_submit_frame`).
+- Keep lane semantics separate:
+ - main-visible can OR authored palette high-byte overrides into submit flags
+ - special-visible submits without that authored override OR path
+ - HUD/overlay has its own slot/table policy and should not be used as a donor for world-object map art binding.
+
## How Map Data Is Stored
### 1. `LSET*.WDL` is a multi-section bundle
@@ -94,6 +1099,12 @@ Current map-relevant runtime destinations:
- `DAT_8006b5d8 -> psx_level_decompressed_state_buffer` (`DAT_8006769c`): optional decompressed `0x3e00` runtime/state substrate
- `psx_level_runtime_header_block` (`DAT_80067794`): separate `0x50` level runtime-header block
+The loader/install ownership is now tighter too:
+
+- `psx_load_type_state_banks` installs the `DAT_800758cc/d0/d4` banks only.
+- `psx_stream_install_type_runtime_banks` is the packed-stream helper that can install all four per-type banks (`DAT_800758cc/d0/d4/d8`) together and clears the cached owner/resource slot at `DAT_800758c8`.
+- `psx_snapshot_level_runtime_header_block` and `psx_apply_level_runtime_header_block` make the `DAT_80067794` lane read as save or transition state, not as the missing per-type art-binding source for unresolved `0x0042` families.
+
### 2. The map is split across authored families, not one row type
The viewer's old `region00/region01` labels were a useful stepping stone, but the current executable-backed names are better:
@@ -121,7 +1132,42 @@ These families do different jobs.
- much closer to direct object spawn inputs
- already usable for a large part of the viewer export
-### 3. The late template bank matters
+### 3. The descriptor-table cluster for `0x003e..0x0050` is shared
+
+The newest Ghidra pass tightens the per-type descriptor story substantially.
+
+- `psx_type_descriptor_table` at `0x80063118` is a pointer table.
+- Every currently sampled type in the `0x003e..0x0050` band points to the same descriptor object at `0x800626f8`.
+- Type `0x0042` is now pinned exactly too: `psx_type_descriptor_table[0x0042]` at `0x80063220` points to `0x800626f8`.
+- That shared descriptor currently resolves to a small callback cluster that includes:
+ - `psx_spawn_compound_record_advance_state_once`
+ - `psx_object_refresh_main_visible_and_cleanup`
+ - `psx_object_release_to_free_list`
+ - `psx_spawn_simple_record_set_active_flag`
+ - `psx_object_advance_state_and_queue_special_visible`
+ - `psx_object_create_simple_record`
+ - `psx_object_integrate_motion_and_route_visible`
+
+This is important negative evidence. `0x0042` does not currently have a unique descriptor fork that would justify a type-only exporter key. If the viewer needs to distinguish `0x0042` from neighboring generic-family types, that distinction has to be recovered later from runtime-bank content, state progression, flags, lane routing, or resource-kind evidence.
+
+The row layout is now clearer too:
+
+- `descriptor + 0x00`: section-0 create/dispatch callback
+- `descriptor + 0x04`: per-object update callback copied into the live object
+- `descriptor + 0x08`: release callback
+- `descriptor + 0x0c`: descriptor flags
+
+The newest pass also tightens the strongest known constructor-placement route for `0x0042` itself:
+
+- `psx_spawn_compound_record_advance_state_once`
+- `psx_object_create_compound_record`
+- `psx_object_advance_state_script`
+- `psx_object_refresh_main_visible_and_cleanup`
+- stage-1 main-visible draw through `psx_draw_main_visible_object`
+
+That is useful negative evidence too. Constructor-placement `0x0042` currently reads as one compound/main-visible path inside the generic descriptor family, not as a special queue or a hidden presentation-only lane.
+
+### 4. The late template bank matters
The per-type art bank in `DAT_800758d8` is not taken from the earlier small-section heuristic.
@@ -133,6 +1179,19 @@ Current best read:
This is one of the strongest pieces of evidence that the viewer must respect executable loader structure rather than broad file-wide scans or first-match heuristics.
+The art-cache side is narrower now too. `psx_type_art_template_bank` and `psx_type_art_resource_cache_bank` no longer read as a simple immutable descriptor-versus-resource split:
+
+- the cache-build path first seeds `psx_type_art_template_bank[type]` with the incoming descriptor/header pointer
+- after resource construction, the helper writes the built resource pointer back to both `psx_type_art_resource_cache_bank[type]` and `psx_type_art_template_bank[type]`
+- constructor-side consumers then treat `psx_type_art_template_bank[type]` as the active art header used for kind discrimination, while `psx_type_art_resource_cache_bank[type]` acts as the reusable per-type built-resource cache
+
+That still does not create a unique `0x0042` branch, but it does mean exporter notes should treat the `DAT_800758d8` / `DAT_800758c8` pair as active per-type art state, not as one permanently raw descriptor table plus one completely separate cache.
+
+The current live database names now reflect that tighter read too:
+
+- `DAT_800758d8` = `psx_type_art_active_header_bank`
+- `DAT_800758c8` = `psx_type_art_built_resource_bank`
+
## Constructor Record Layouts
Two constructor families are now strong enough to describe directly.
@@ -164,6 +1223,7 @@ Both constructors:
- write authored coordinates into object fields `+0x3c/+0x40/+0x44` as `16.16` fixed-point
- preserve the original authored source-record pointer at `obj + 0xa0`
+- copy the authored lane/flags word into `obj + 0x1c`
- resolve the per-type art bank and seed a drawable resource pointer at `obj + 0x10`
- store the per-type variant bank at `obj + 0x84`
- store the per-type state-script bank at `obj + 0x88`
@@ -171,6 +1231,12 @@ Both constructors:
That preserved source-record pointer at `obj + 0xa0` is especially important because it closes the palette-override provenance: later draw code really is reading authored bytes directly.
+Current best read for the copied lane/flags word is:
+
+- bit `0x0020`: broad world-visible route gate
+- bit `0x0002`: orientation/extents-axis swap and projected horizontal-anchor flip, not a lane switch
+- bit `0x0400`: per-instance stage selector later consumed by visible-routing helpers
+
## Runtime Banks And Object Fields
The current best object-centric map/render model revolves around a small cluster of object fields.
@@ -185,6 +1251,7 @@ The current best object-centric map/render model revolves around a small cluster
### Important object fields
- `obj + 0x10`: current drawable resource pointer
+- `obj + 0x1c`: live route/flags word copied from the authored row and later consumed by visible-routing helpers
- `obj + 0x20..0x2e`: projected on-screen rectangle
- `obj + 0x3c/+0x40/+0x44`: fixed-point world `x/y/z`
- `obj + 0x54/+0x58/+0x5c`: next/target world position cluster used by motion/integration helpers
@@ -226,7 +1293,7 @@ But that is only the starting point.
Verified sentinel/control behavior now includes:
-- `0xfffe`: non-visible audio/effect helper dispatch
+- `0xfffe`: `psx_script_dispatch_audio_event`, a non-visible audio/sequence side-effect dispatch
- `0xfffd`: direct in-family jump
- `0xfffc`: immediate switch to subsidiary script-table entry
- `0xfffb`: scan-forward variant that consumes the next in-band `0xfffd` selector before switching
@@ -274,6 +1341,12 @@ Verified reselection path:
- `psx_object_reselect_state_from_target_vector`
- `psx_object_quantize_motion_heading16`
- `psx_quantize_vector_heading16`
+- `psx_object_select_state_from_transition_table`
+- `psx_type42_transition_selector_tick`
+
+The live reselection path is now slightly tighter than before:
+
+- `psx_heading16_lookup_unit_vector` is the table-backed heading-to-vector helper used by `psx_object_reselect_state_from_target_vector` when a target-side heading token is available
Verified interaction/reselection cluster:
@@ -284,10 +1357,52 @@ Verified interaction/reselection cluster:
- `psx_object_update_contact_block_flags`
- `psx_object_register_contact_pair`
+Latest live `SLUS_002.68` cleanup in this same cluster also closes six previously anonymous helpers that shape post-spawn interaction state:
+
+- `0x80028050` = `psx_object_test_strict_nonoverlap_flag8_pair`
+- `0x800281d4` = `psx_object_test_strict_nonoverlap_flag8_subject`
+- `0x80028700` = `psx_object_adjust_param9c_by_view_side`
+- `0x800287bc` = `psx_object_update_param9c_from_contact_target`
+- `0x80028eb4` = `psx_object_apply_contact_push_bias`
+- `0x8002923c` = `psx_object_spawn_type11_contact_proxy`
+- `0x8001ae9c` = `psx_object_update_interaction_transition`
+- `0x8001bca0` = `psx_object_select_state_from_transition_table`
+
+Current practical read for exporter-facing behavior:
+
+- `+0x30/+0x34/+0x38` remains the shared runtime companion-extents lane used by overlap/reselection checks.
+- `+0x9c` is actively rewritten after contact/reselection and camera-side scaling, so it should not be treated as spawn-static metadata.
+- contact-triggered type-`0x11` proxy spawning now has a direct helper anchor (`0x8002923c`) inside nearby-interaction flow, not just generic script-advance assumptions.
+- the transition-table lane is now explicit too: `psx_object_select_state_from_transition_table` uses the per-type table at `DAT_80063b4c` plus heading-bucket logic to choose selectors before `psx_object_select_state_script` reseats the active script.
+- `psx_object_update_interaction_transition` shows one concrete forced-selector path: spawned helper objects are pushed to selector `3` unless they are already in selector `1` or `3`.
+- type `0x0042` now has a dedicated transition/update helper too: `psx_type42_transition_selector_tick` computes and dispatches low turning selectors before the `+0x94`-style runtime latch copy.
+- for type `0x0042`, the `DAT_80063b4c` row itself mostly yields higher script selectors; the low selectors `3/4` are better explained by that dedicated pre-latch turn/reseat path than by the row alone.
+- the constructor-facing sample path now also closes one adjacent helper: `psx_spawn_compound_transition_effect_by_code` is a transition-effect constructor helper used in the same `0x0042` transition lane and confirms that selector install writes `obj + 0x9e` while the later state advance still owns the `obj + 0x94` latch.
+
These helpers prove that post-spawn interaction and motion state can reseat the active script from runtime heading, not only from the authored row.
That is why exported `0x0042` selectors `3` and `4` do not contradict an earlier three-script file read: some of those live selectors are runtime outcomes.
+The latest pass narrows that one step further for `0x0042`: selector `3/4` can be dispatched before the `+0x94`-style runtime latch is updated. So exporter logic that keys too literally off the current latched script word can still miss a just-selected turn state.
+
+The pre-latch path is now slightly tighter again:
+
+- `psx_type42_transition_selector_tick` uses `psx_object_is_within_view_margin` as an early gate.
+- it can take a heading bucket from `psx_object_compute_heading_selector_to_focus`, remap that bucket through mirrored-turn logic, and call `psx_object_select_state_script` before the latch copy.
+- only after that dispatch does the helper copy its transition result into the `+0x94`-style runtime latch word.
+
+The newest cache evidence also shows the remaining `0x0042` failure is not one homogeneous placement class. On `map 104`, the same donor binding currently spans:
+
+- `section0_constructor_placements` with `u5=0x0020`
+- `section0_dispatch_roots` with a large `u5=0x0030` band
+- smaller `section0_dispatch_roots` outliers with `u5=0x0022`
+
+In the current cache all of those still land on donor map `85` type `0x0040` bundle `0x0009d304` with palette `0`. That is useful as negative evidence: the exporter is currently flattening multiple authored/runtime roles into one wall-like resource before the executable-backed family split is proven.
+
+The viewer now carries a narrow safety rule derived from that evidence. Provisional donor matches in the unresolved generic family are rejected when one `map:type` bucket spans mixed authored families or mixed raw `u5` classes. That does not solve the missing runtime resource rule, but it does stop the exporter from presenting a single false wall field as if the executable had already proven it.
+
+The same six-track verification pass also narrows one part of the `u5` split. For root-dispatch `0x0042`, the smaller `u5=0x0022` cases do not currently read as a separate runtime lane from the dominant `u5=0x0030` band. Both still satisfy the same broad main-visible gating through bit `0x0020`; the visible difference is that `0x0022` additionally carries bit `0x0002`, which affects orientation/extents math and therefore looks more like a presentation variant than a separate queue or subsystem path. So the exporter should keep `u5` visible and distinct, but it should not assume every `0x0022` / `0x0030` split implies a different draw pass.
+
### 6. This is the current blocker
The map is still unreadable because the viewer still does not fully reproduce that last runtime bridge:
@@ -300,6 +1415,19 @@ The map is still unreadable because the viewer still does not fully reproduce th
Projection, placement decoding, list routing, and primitive submission are no longer the main unknowns.
+One exporter-side step is now in place to make the next runtime pass more concrete. The PSX scene builder now emits a per-item `runtimeDiagnostic` payload in scene version `psx-runtime-record-probe-v10`.
+
+Current diagnostic channels exported for each PSX item:
+
+- `objectLocalRouteFlags`: seeded from raw `u5`, with decoded `0x0002` / `0x0020` / `0x0200` / `0x0400` bits and a route-outcome candidate
+- `selector`: raw selector seed plus an explicit note when the type-`0x0042` pre-latch turn path may diverge from the later latch
+- `latchedState`: current exporter-side state/frame candidate used for scene art selection
+- `nestedRuntimeState`: explicit placeholder slots for the live nested runtime words that still need Ghidra-side sampling
+- `resourceKind`: per-type active-header/built-resource hints derived from the current art-bank decode
+- `typePolicy`: explicit placeholder slot for the live `DAT_800675f8` word
+
+That payload does not solve the remaining `0x0042` bridge by itself, but it gives the next Ghidra pass a stable schema to fill against concrete scene items instead of re-deriving which channels matter.
+
## Per-Frame World And Render Pipeline
### 1. Outer lifecycle
@@ -315,17 +1443,62 @@ The executable still operates on authored families each frame before or alongsid
- `psx_dispatch_section0_dispatch_roots`: dispatches the `DAT_80067720` family plus nearby fixed-size entries
- `psx_dispatch_section0_constructor_placements`: dispatches the `DAT_800678f0` constructor-placement family
+- `psx_authored_record_in_view_bounds`: shared screen-space cull gate for those authored record families before handler dispatch
These are the closest executable matches for the viewer's exported authored record families.
+That distinction matters directly for the current renderer bug. The active `map 104` cache regression is no longer just “many `0x0042` records choose the wrong wall art”; it is “records emitted from both of these authored families are currently being funneled into the same donor resource despite different raw `u5` classes.” So the next exporter repair should preserve family identity until runtime evidence proves a shared resource path.
+
### 3. Live-object update lane
- `psx_run_live_object_type_updates`: per-type live-object update callback pass
- `psx_run_live_object_behavior_callbacks`: later per-object behavior callback pass
- `psx_object_integrate_motion_and_route_visible`: integrates motion, updates visibility flags, advances script state, and routes the object to the appropriate render lane
+That lane now has two more verified helper clusters that matter for map reconstruction:
+
+- `psx_run_object_behavior_program_tick` and `psx_object_behavior_opcode_dispatch` service a small per-object timed opcode stream before or alongside the main motion/update path
+- `psx_world_point_in_view_bounds` is the shared world-space cull helper used both by `psx_object_integrate_motion_and_route_visible` and `psx_run_live_object_type_updates`
+- `psx_object_run_control_opcode`, `psx_control_move_player_to_point`, `psx_control_move_object_to_point`, `psx_control_wait_ticks`, `psx_control_configure_fixed_camera_anchor`, `psx_control_set_facing_direction`, `psx_queue_deferred_control_command`, `psx_flush_deferred_control_queue`, `psx_apply_deferred_control_command`, `psx_apply_deferred_control_to_dispatch_roots`, and `psx_apply_deferred_control_to_live_objects` form a small control-script lane that mutates both per-object motion state and deferred world control state during the world/update step rather than during final draw
+
+The old unnamed post-projection `FUN_80027f80` follow-up is now closed too. It is not a hidden render cleanup path. The live helpers are:
+
+- `psx_reset_nearby_interaction_list`
+- `psx_nearby_interaction_list_add`
+- `psx_nearby_interaction_list_remove`
+- `psx_update_motion_and_nearby_interactions`
+
+Current best read:
+
+- `psx_object_integrate_motion_and_route_visible` and `psx_object_refresh_main_visible_and_cleanup` enqueue eligible objects into a nearby-interaction active set after projection/state refresh
+- `psx_update_motion_and_nearby_interactions` consumes that active set before the next frame, running `psx_type4_update_delayed_interaction` for type `4` objects and `psx_object_update_nearby_interactions` for the broader collision/contact lane
+- `psx_control_move_player_to_point` and `psx_control_move_object_to_point` close control opcode case `1` as a move-to-point instruction rather than a render-side helper, `psx_control_wait_ticks` closes case `3` as a timed gate on `DAT_80078a28`, `psx_control_configure_fixed_camera_anchor` closes cases `4/5` as the fixed-camera-anchor configurator, and `psx_control_set_facing_direction` closes case `9` as an explicit heading override
+- control opcode case `8` is still not named at the wrapper level, but its direct callee is now grounded: `psx_spawn_object_compound_effect_variant3` creates a type-`2` compound effect at the current object position, so the wrapper currently reads as a short delay gate around that spawn plus a local motion-state change
+- the separate deferred control-command queue is world-facing control state, not a hidden presentation queue; it is flushed from the per-frame world loop before draw submission and can touch both section-0 dispatch rows and instantiated live objects
+- this queue is therefore part of runtime interaction maintenance, not a separate hidden art-routing pass
+
### 4. Stage 1 versus stage 2 is a real runtime split
+The split point is now explicit inside `psx_object_integrate_motion_and_route_visible`, not just inferable from later draw helpers.
+
+- `obj + 0x1c` bit `0x0020` keeps the object in the broad world-visible route class.
+- `obj + 0x1c` bit `0x0400` selects the stage-2 path: set routes to `psx_project_object_special_visible_queue`, clear falls through to `psx_project_object_main_visible`.
+- `obj + 0x1c` bit `0x0002` still reads as orientation/extents behavior only; it does not switch lanes by itself.
+- `DAT_800675f8[type]` bit `0x1000` gates the nearby-interaction publish call after projection, and bits `0x0600` feed the stage-1 order-graph comparator.
+
+The writer side is now tighter too:
+
+- constructors seed `obj + 0x1c` directly from the authored `u5` word
+- `psx_object_select_state_from_transition_table` can set or clear bit `0x0002`
+- the strongest recovered stage-2 consumer remains the route test itself; no equally strong local writer for bit `0x0400` has been confirmed yet in the named helper set, so current best read is still that `0x0400` is usually data-driven from authored state unless one of the remaining anonymous control islands proves otherwise
+
+The anonymous control islands now partially close that last caveat.
+
+- `psx_object_state_machine_dispatch_tick` contains a confirmed write of bit `0x0400`, but to the nested runtime state word, not directly to the object-local `obj + 0x1c` halfword.
+- `psx_object_handle_control_pair_0a` can set global policy bits in `DAT_80078a88`, clear the nested runtime `+0x1c` dword, and set object-local bit `0x0200`, but this pass still did not recover a direct object-local `obj + 0x1c |= 0x0400` write.
+
+So the current best read stays split: `0x0400` is definitely a real stage-selection concept in the wider runtime state machine, but a direct object-local `0x0042` writer is still not pinned in the named helper set.
+
Stage 1:
- projector: `psx_project_object_main_visible`
@@ -341,6 +1514,36 @@ Stage 2:
This is not a HUD-only split. Stage 2 is a real world-facing lane.
+The type-flag lane is now more concrete too:
+
+- `DAT_800675f8[type] & 0x1000`: suppresses nearby-interaction publication after the stage-2 route point
+- `DAT_800675f8[type] & 0x0600`: stage-1 ordering priority class used by the order graph and slice sorter
+- `DAT_800675f8[type] & 0x2000`: main-visible semitrans policy bit at draw time
+
+Those flags now read as ordering, interaction, and draw-policy modifiers. They do not currently overturn the stronger route split recovered from `obj + 0x1c`.
+
+The type-flag lane is also broader than the first pass suggested, but still policy-only:
+
+- bit `0x0800`: reduced or half-adjust behavior in the radius/param9c decay family
+- bits `0x0010` / `0x0020`: target-class selection used by nearby LOS-target search helpers
+- bit `0x0008`: flag-8 target-family inclusion gate in contact and decay helpers
+
+These sharpen neighboring gameplay semantics, but they still do not create a unique `0x0042` presentation lane by themselves.
+
+The renamed tail of the top-level draw pass is now clearer too:
+
+- `psx_draw_world_visible_passes` ends with `psx_draw_hud_overlay_pass` (`0x800416cc`), not a third world-object lane.
+- One concrete child of that pass is `psx_draw_clock_digits_overlay` (`0x8004214c`), which formats and submits the timer/clock digits through the image-table submitter.
+
+The adjacent presentation-only helper chain is now named in the live database:
+
+- `psx_level_session_loop` calls `psx_hud_overlay_init_resources` (`0x800388a8`) during level-session bring-up.
+- `psx_hud_overlay_init_resources` preloads fixed HUD resources and descriptor defaults used by the later overlay pass.
+- `psx_draw_hud_overlay_pass` consumes the slot table and calls `psx_overlay_slot_step_color_fade` (`0x80038114`) per active slot.
+- overlay/menu controllers allocate and retire those slots through `psx_overlay_slot_create` (`0x80035cc0`) and `psx_overlay_slot_release` (`0x80036000`).
+
+This chain is a non-map-facing presentation lane: it is layered after both world-visible passes and does not participate in authored map record dispatch, world projection, or stage-1/stage-2 world list routing.
+
### 5. Small wrapper helpers prove the split is intentional
Recovered wrappers:
@@ -418,6 +1621,14 @@ The sorter is important because it shows that draw order is not just screen-y or
Frame metric accessors:
- `psx_resource_frame_origin_x`
+
+The final submitter split is now explicit:
+
+- both `psx_draw_main_visible_object` and `psx_draw_special_visible_queue` choose the submitter from the bound resource header at `obj + 0x10`, not from type id alone
+- if `resource->kind == 5`, draw goes through `psx_image_table_submit_frame`
+- otherwise draw goes through `psx_sprite_resource_submit_frame`
+
+That means image-table-versus-sprite submission is a runtime resource-kind property, not a stable type-family label. For unresolved `0x0042`, this is one of the last meaningful missing facts: the decision point is now known exactly, even though the per-map resource-header kind still has to be sampled from runtime-loaded art-bank data.
- `psx_resource_frame_origin_y`
- `psx_resource_frame_width`
- `psx_resource_frame_height`
@@ -433,7 +1644,7 @@ The critical point is that both paths take the live frame index from `obj + 0x94
`psx_create_image_resource_from_descriptor`:
-- type-4 descriptors bind a single image resource through `image_resource_bind_vram_slot`
+- type-4 descriptors bind a single image resource through `psx_resource_bind_single_image_vram_slot`
- type-5 descriptors allocate and upload multi-frame bundles through `image_bundle_load_to_vram`
This is why the constructors can seed `obj + 0x10` early and then let later code only vary frame index and state.
diff --git a/docs/psx/map-storage-model.md b/docs/psx/map-storage-model.md
new file mode 100644
index 0000000..f09cd9f
--- /dev/null
+++ b/docs/psx/map-storage-model.md
@@ -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
\ No newline at end of file
diff --git a/docs/psx/map-viewer-plan.md b/docs/psx/map-viewer-plan.md
index 6eea73f..77b5fa4 100644
--- a/docs/psx/map-viewer-plan.md
+++ b/docs/psx/map-viewer-plan.md
@@ -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.
- 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 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 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.
- 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.
- 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 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.
+- 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 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 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 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 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: `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.
- 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`.
@@ -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 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.
-- 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.
- 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.
- 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 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 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 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.
## 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.
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.
+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:
@@ -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.
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.
-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.
-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: 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: 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.
-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.
-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: `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.
+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: `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: `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 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.
+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.
+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: 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.
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.
diff --git a/docs/psx/psx.md b/docs/psx/psx.md
index 262d391..868aaef 100644
--- a/docs/psx/psx.md
+++ b/docs/psx/psx.md
@@ -5,6 +5,38 @@
- 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.
+## 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
- `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:
- `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`.
- `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.
@@ -531,7 +563,7 @@ Current invalidation result:
- this direct `u0 -> bundle index` mapping is now considered invalid for real renderer output
- the produced scene repeats a small set of obviously wrong assets, including portrait/UI-like art, in places that do not make spatial sense for the map
-- executable-side tracing shows that art selection is type-driven through `DAT_800758cc/d0/d4/d8` resource tables loaded by `level_resource_stream_load`, not by directly indexing the raw `post_audio_region_04` bundle scan
+- 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:
@@ -569,7 +601,7 @@ Current app compatibility notes:
Immediate implications for the next decode pass:
- the public renderer integration path is now proven enough to use as a live debug target for PSX map-format work
-- the next priority is to replace the invalidated `u0 -> bundle index` hypothesis with a real type/resource lookup recovered from `level_resource_stream_load`
+- 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
- 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
@@ -650,6 +682,8 @@ What the loader actually does:
- `type = record[+0x08]`
- `dispatch through PTR_PTR_80063118[type]`
- Those dispatch handlers do not behave like a terrain-tile walker. They construct one runtime object or a tiny object cluster at a time through `FUN_800249f4`, `FUN_80024eec`, `FUN_8003c314`, `FUN_8003c714`, and `FUN_8003cc08`.
+- 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:
@@ -659,7 +693,7 @@ Why the current export is incoherent:
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
- 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
@@ -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 cache export now carries more than the parsed `DAT_800758d8` candidate section. In the current `psx-runtime-record-probe-v6` scene path, `map_renderer/src/lib/psx-cache.js` serializes `DAT_800758cc`, `DAT_800758d0`, `DAT_800758d4`, and the offline `FUN_8003b00c` decode candidate for `DAT_8006b5d8 -> DAT_8006769c` into `stateLayers`, and the scene writer preserves those layers in both scene metadata and `mapSource`.
- the new typed-section-16 discovery path is also broader than the earlier section-start probe: when no parsed-section candidate wins, the cache builder now falls back to an absolute-file scan, which is why the late compound-bank blobs can now land in the export even when their serialized source does not start exactly at a pre-labeled section boundary.
-- the file-side header block now separates more cleanly too: `FUN_80039c40` allocates a `0x50` level runtime-header block at `DAT_80067794`, and `FUN_80039dc4` copies that block into globals such as `DAT_80078ab0`, `DAT_80078a88`, `DAT_80078a8c`, `DAT_80078a4c`, and `DAT_80067354` before calling `FUN_80042ec4`. So the first visible/root sections are not the only authoritative level metadata; the loader also applies a dedicated `0x50` per-level runtime header after the optional `0x3e00` decode succeeds.
+- 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.
- 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.
@@ -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.
- The current renderer-side consequence is important: section-0 word `u4` is no longer treated as a verified sprite-frame index. It is now carried forward as a state-selector candidate in exported scene metadata until the `DAT_800758cc/d4` path is decoded far enough to pick the right animation frame from executable evidence.
- Current strongest sentinel read:
- - `0xfffe` dispatches `FUN_8004061c`, which is an audio/effect helper rather than a visible-frame selector.
+ - `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.
- `0xfffc` resolves a fresh subsidiary script base from the table rooted at `obj+0x88`, then immediately swaps both `obj+0x8c` and `obj+0x90` to that destination before continuing from the first record there.
- `0xfffb` also resolves a subsidiary script from the same table, but first walks the current script forward until it finds an in-band `0xfffd` marker and then uses that marker's selector word to choose the destination entry.
- Current best read of those sentinels:
- `0xffff` marks a terminal or restart control that re-anchors the script at `obj+0x8c` and raises object-state flags.
- - `0xfffe` dispatches a side-effect helper (`FUN_8004061c`) using the following word as a parameter before advancing.
+ - `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.
- `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.
@@ -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.
- The next loader-side correction is now verified in the live cache too: the effective late `LSET*.WDL` `DAT_800758d8` candidate is not the earlier small-section heuristic, but a large late section whose working descriptor stream begins at an embedded `+0x38` offset. On retail map `9` that correction alone lifts `bundleMappedItemCount` from `0` to `111`, which is enough to restore real bundle-backed art for a first subset of types without reintroducing the disproven scan-order fallback.
The still-unresolved root-dispatch families remain instructive rather than contradictory. `0x0042` and `0x0049` still stay on placeholders after the bank-selection fix, but the same pass now decodes their `DAT_800758cc` state rows more cleanly: type `0x0042` carries three selector-targeted scripts (`0`, `1`, `2`) that all terminate through `0xffff`, while type `0x0049` carries a single selector-`0` script. The built scene cache shows that this is still not the whole art-facing discriminator: `type=0x0042` placeholders now appear with selectors `0..4`, and the higher selectors `3` and `4` are real exported cases rather than parser noise. Verified maps with `0x0042` selectors above `2` include `map-4`, `map-5`, `map-8`, `map-45`, `map-69`, and `map-85`.
- Two runtime reselection paths now explain how those higher selectors can arise without contradicting the earlier three-script file read. `FUN_80028c94` and `FUN_8002906c` both recompute the active script with `psx_object_select_state_script(obj, FUN_8003bc1c(obj) >> 2 & 0xf)`, where `FUN_8003bc1c` quantizes the object's current motion vector at `obj+0x60/+0x64` through `FUN_8003b980` into a 16-way heading bucket. So cache-visible `0x0042` selectors `3` and `4` can come from runtime heading/state reselection, not only from the original placement byte.
+ 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.
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:
@@ -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`.
- Three of those sites then feed the main stage-1 projector path through `FUN_80040d44` (`0x80012b60`, `0x8001357c`, `0x800136d4`), while two feed the stage-2 queue-builder path through `FUN_80040f78` (`0x8001352c`, `0x80013780`).
- That exact `3` versus `2` split matters because it tightens the earlier claim: stage-2 membership is tied to a narrower runtime object/state branch after state advance, not to the decompressed substrate buffer alone and not to all state-advanced objects indiscriminately.
-- One state-script sentinel is now functionally closed too: `0xfffe` dispatches `FUN_8004061c`, which is an audio/effect helper rather than a visible-frame selector. That shrinks the unknown sentinel set for the remaining `DAT_800758cc` script work.
+- 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:
- `FUN_8002d240` adds an object to the stage-1 `DAT_8006ad5c` visible-list 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:
-- `FUN_80039c40` is now confirmed as a pure `0x50` allocator for `DAT_80067794`, and `FUN_80039dc4` is the matching applier for that block.
-- `FUN_80039dc4` copies fixed fields from `DAT_80067794` into the active level globals, including camera/runtime anchor values and several per-level mode bytes, then calls `FUN_80042ec4` to refresh dependent runtime state.
+- `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.
+- `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.
- Practical exporter consequence: keep the `DAT_80067794` fields as a first-class raw metadata lane, but do not treat them as a missing placement stream. They are more likely to affect camera/runtime modes, screen-space behavior, or level-global toggles than to supply extra map cells directly.
- The higher-level level lifecycle is now readable too. `psx_level_session_loop` is the outer level-session loop: it loads the selected WDL through `wdl_resource_bundle_load_by_index`, applies shared overlay/resource setup through `FUN_800388a8`, resets a small per-level step-flag block with `FUN_8003a498`, and then runs `psx_world_frame_tick` as the per-frame world loop until the current level session exits.
- `wdl_resource_bundle_load_by_index` is now mapped tightly enough for viewer work. Its effective order is: load `SPEC_A.WDL` and shared type art/state banks; open the selected `LSET*.WDL`; read the `0x38` section-size header; lay out the contiguous per-level section pack at `DAT_800678f4`, `DAT_80067720`, `DAT_800678f0`, `DAT_80067938`, `DAT_80067838`, `DAT_800675f8`, `DAT_8006754c`, `DAT_80067840`, and `DAT_800676d8`; load the detached `DAT_8006767c` blob; optionally inflate `DAT_8006b5d8` into `DAT_8006769c`; apply the runtime header at `DAT_80067794`; then dispatch the `0x18`-stride root records at `DAT_800678f4` through the per-type function table in `PTR_PTR_80063118`.
-- The per-frame world loop in `psx_world_frame_tick` is now split clearly enough for renderer planning. In the normal in-level branch it ticks existing live objects through `psx_run_live_object_type_updates`, instantiates or refreshes nearby authored records through `psx_dispatch_section0_dispatch_roots` and `psx_dispatch_section0_constructor_placements`, runs per-object behavior callbacks through `psx_run_live_object_behavior_callbacks`, integrates world/player motion and active-object state through `FUN_80029de0`, updates queued transient resources through `FUN_8002aed0`, and only then submits the draw pass through `FUN_80041378`.
+- The 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:
- `psx_dispatch_section0_dispatch_roots` walks the `DAT_80067720` `0x18`-stride family plus the fixed-size entries at `DAT_80067658`, culls them to roughly a `+/-0x140` neighborhood around the current focus object, and dispatches their per-type handlers. This is the closest executable match for the current `section0_dispatch_roots` viewer family.
- `psx_dispatch_section0_constructor_placements` walks the `DAT_800678f0` `0x0c`-stride family with the same neighborhood cull and per-type dispatch. This is the closest executable match for the current `section0_constructor_placements` viewer family.
- The already-instantiated-object passes are separated too:
- `psx_run_live_object_type_updates` iterates the linked live object list at `DAT_800675ac` and calls the per-type update callback (`type_vtable+8`) for active in-world objects.
- `psx_run_live_object_behavior_callbacks` then runs each live object's callback stored at `obj+0x98` / `obj[0x26]`, which is the later object-specific behavior/update pass.
-- `FUN_80029de0` is the broad world-motion and player-state integrator that sits between behavior updates and draw submission. For viewer purposes, this is the runtime bridge between authored map placement and the motion/state values that later feed heading-based state reselection and projection.
-- The cull-to-draw bridge is now closed too. `FUN_800423b0` is the authored-record screen-space gate used by the two section-0 dispatch passes, while `FUN_80042424` is the corresponding gate for already-instantiated live objects. Both use the same isometric camera basis around `DAT_800678d4`, which means the viewer can treat the record-family export as feeding the same projection space as the later live object list instead of as a separate map coordinate model.
+- One adjacent control family in the same world/update lane is now tighter too:
+ - `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 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_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.
@@ -838,9 +884,9 @@ Recovered per-level runtime-header lane:
- The local post-advance render wrappers are also no longer anonymous labels:
- `psx_spawn_compound_record_advance_state_once` (`0x80013618`) creates one compound-record object, forces its script countdown to `1`, immediately runs `psx_object_advance_state_script`, and then marks the object with `obj+0x1e |= 0x20`. This is the cleanest currently recovered example of a constructor wrapper that intentionally advances into a non-initial live state before the object joins the normal update/render flow.
- `psx_spawn_simple_record_set_active_flag` (`0x8001372c`) is the simpler sibling for the simple-record constructor: create the object, then immediately set the low active flag in `obj+0x1e`.
- - `psx_object_refresh_main_visible_and_cleanup` (`0x80013688`) is the compact stage-1 handoff wrapper. When the object still has a drawable resource and the `0x20` flag is set, it feeds the object through `psx_project_object_main_visible`; if the object is not in the `obj+0x1c & 1` hold state but does carry `obj+0x1e & 0x10`, it then runs the usual `FUN_80027f80` cleanup/follow-up path.
+ - `psx_object_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.
-- 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.
- 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.
@@ -1042,6 +1088,8 @@ Per-bundle shipped inventory from the extracted disc tree:
### 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:
- 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`
- that makes `JL-2` and `JL-9` strong PSX-only naming additions rather than inherited PC names
- `JL-2` is also the only one of the two with an explicit PSX ammo label (`JL-2 AMMO`) in the nearby executable text table, while no matching `JL-9 AMMO` string has been recovered
-- the extracted PSX `pickups_and_weapons` sprite category contains repeated weapon-pickup art across a large spread of maps, but this pass still does not have a defensible sprite-to-name mapping for specific `JL-2` or `JL-9` pickup appearances
+- 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
@@ -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.
3. Focus map decoding on `post_audio_region_01` and `post_audio_region_02`, starting with table structures, coordinate ranges, and repeated record widths.
4. Focus sprite/graphics decoding on `post_audio_region_04`, including more aggressive TIM validation and possible packed-image expansion.
-5. Recover the exact type IDs consumed by `level_resource_stream_load` so the sprite/image resource records can be labeled more precisely.
+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.
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.
diff --git a/docs/regret-hidden-debugger-investigation.md b/docs/regret-hidden-debugger-investigation.md
index 9f7c888..6c6e757 100644
--- a/docs/regret-hidden-debugger-investigation.md
+++ b/docs/regret-hidden-debugger-investigation.md
@@ -519,6 +519,58 @@ Features that are mostly just break-state/UI flags:
- `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
+### 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:
- `Inspect what?`
diff --git a/docs/remorse-class-candidate-inventory.md b/docs/remorse-class-candidate-inventory.md
index a68ea18..4858cca 100644
--- a/docs/remorse-class-candidate-inventory.md
+++ b/docs/remorse-class-candidate-inventory.md
@@ -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 |
| `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 |
+| `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 |
| `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 |
@@ -50,13 +51,14 @@ If the goal is to make later class-authoring work fast and low-risk, the best or
1. `EntityDispatchEntryBase`
2. `EntityDispatchEntryRuntimeState`
3. `SpriteNode`
-4. `EntityVmOwnerResource`
-5. `CacheBackendObject`
-6. `WatchEntityController`
-7. `Entity`
-8. `EntityVmRuntime`
-9. `EntityVmContext`
-10. `PresentationCallbackBroker`
+4. `NPCActionProcess` family
+5. `EntityVmOwnerResource`
+6. `CacheBackendObject`
+7. `WatchEntityController`
+8. `Entity`
+9. `EntityVmRuntime`
+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.
diff --git a/docs/remorse-class-lift-index.md b/docs/remorse-class-lift-index.md
index 457cbd5..025ce49 100644
--- a/docs/remorse-class-lift-index.md
+++ b/docs/remorse-class-lift-index.md
@@ -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/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/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/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`
- `EntityVmContext`
- `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 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.
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.
diff --git a/docs/retail-debugger-entry-options.md b/docs/retail-debugger-entry-options.md
index 5132d60..8dc480d 100644
--- a/docs/retail-debugger-entry-options.md
+++ b/docs/retail-debugger-entry-options.md
@@ -141,6 +141,80 @@ Implication:
- 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
+### 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
## 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`
- 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
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.
-2. Keep `-u` / replacement `EUSECODE.FLX` as the preferred low-risk experiment surface for any script-side proxy ideas.
-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.
+1. Promote the retail seg109 naming backlog above so the remaining debugger lanes are explicit in Ghidra.
+2. Use runtime-only memory seeding on a clean executable to prove or kill the bootstrap theory without committing file changes.
+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.
diff --git a/plan-mid.md b/plan-mid.md
index 2f10244..e270b5f 100644
--- a/plan-mid.md
+++ b/plan-mid.md
@@ -15,6 +15,226 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
## 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/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/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%
- 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 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 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 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`.
@@ -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.
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.
-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
@@ -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.
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.
-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.
-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.
-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.
+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. 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. 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.
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.
-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
diff --git a/scripts/analyze_weapons_table_region.py b/scripts/analyze_weapons_table_region.py
new file mode 100644
index 0000000..e7800b4
--- /dev/null
+++ b/scripts/analyze_weapons_table_region.py
@@ -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')
diff --git a/scripts/dump_channel_table_at.py b/scripts/dump_channel_table_at.py
new file mode 100644
index 0000000..9d5f133
--- /dev/null
+++ b/scripts/dump_channel_table_at.py
@@ -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})')
diff --git a/scripts/dump_weapon_commit_table.py b/scripts/dump_weapon_commit_table.py
new file mode 100644
index 0000000..a07fb11
--- /dev/null
+++ b/scripts/dump_weapon_commit_table.py
@@ -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})')
diff --git a/tmp_dump_offsets.py b/tmp_dump_offsets.py
new file mode 100644
index 0000000..eea2280
--- /dev/null
+++ b/tmp_dump_offsets.py
@@ -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))
diff --git a/tools/create_focused_crop.py b/tools/create_focused_crop.py
new file mode 100644
index 0000000..aa787dc
--- /dev/null
+++ b/tools/create_focused_crop.py
@@ -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)
diff --git a/tools/find_bytes.py b/tools/find_bytes.py
new file mode 100644
index 0000000..f4e07af
--- /dev/null
+++ b/tools/find_bytes.py
@@ -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()
diff --git a/tools/find_cd_bytes2.py b/tools/find_cd_bytes2.py
new file mode 100644
index 0000000..fc32a62
--- /dev/null
+++ b/tools/find_cd_bytes2.py
@@ -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))
diff --git a/tools/hexdump_region.py b/tools/hexdump_region.py
new file mode 100644
index 0000000..be74e13
--- /dev/null
+++ b/tools/hexdump_region.py
@@ -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()
diff --git a/tools/hud_icon_match.py b/tools/hud_icon_match.py
new file mode 100644
index 0000000..13bf5d1
--- /dev/null
+++ b/tools/hud_icon_match.py
@@ -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<=nxbest[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')
diff --git a/tools/hud_row_scan.py b/tools/hud_row_scan.py
new file mode 100644
index 0000000..1aa7a0c
--- /dev/null
+++ b/tools/hud_row_scan.py
@@ -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)
diff --git a/tools/make_crop_no_pil.py b/tools/make_crop_no_pil.py
new file mode 100644
index 0000000..519d0c2
--- /dev/null
+++ b/tools/make_crop_no_pil.py
@@ -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)')
diff --git a/tools/read_commit_table.py b/tools/read_commit_table.py
new file mode 100644
index 0000000..836d665
--- /dev/null
+++ b/tools/read_commit_table.py
@@ -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()
diff --git a/tools/vertical_strip_crops.py b/tools/vertical_strip_crops.py
new file mode 100644
index 0000000..6cd299a
--- /dev/null
+++ b/tools/vertical_strip_crops.py
@@ -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}')
diff --git a/tools/vram_analyze.py b/tools/vram_analyze.py
new file mode 100644
index 0000000..2d7a6ff
--- /dev/null
+++ b/tools/vram_analyze.py
@@ -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<=nxRGB 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}')
diff --git a/tools/vram_density_scan.py b/tools/vram_density_scan.py
new file mode 100644
index 0000000..2b84355
--- /dev/null
+++ b/tools/vram_density_scan.py
@@ -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])
diff --git a/tools/vram_to_bmp.py b/tools/vram_to_bmp.py
new file mode 100644
index 0000000..b301091
--- /dev/null
+++ b/tools/vram_to_bmp.py
@@ -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('> 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))