diff --git a/Crusader.rep/idata/01/~00000015.db/change.data.gbf b/Crusader.rep/idata/01/~00000015.db/change.data.gbf index 7479ed8..26fb920 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 3b023fd..3bc6051 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.37.gbf b/Crusader.rep/idata/01/~00000015.db/db.38.gbf similarity index 99% rename from Crusader.rep/idata/01/~00000015.db/db.37.gbf rename to Crusader.rep/idata/01/~00000015.db/db.38.gbf index 2f389d2..9f0d50e 100644 Binary files a/Crusader.rep/idata/01/~00000015.db/db.37.gbf and b/Crusader.rep/idata/01/~00000015.db/db.38.gbf differ diff --git a/Crusader.rep/idata/01/~00000015.db/db.36.gbf b/Crusader.rep/idata/01/~00000015.db/db.39.gbf similarity index 99% rename from Crusader.rep/idata/01/~00000015.db/db.36.gbf rename to Crusader.rep/idata/01/~00000015.db/db.39.gbf index 9f619c5..d2e7c02 100644 Binary files a/Crusader.rep/idata/01/~00000015.db/db.36.gbf and b/Crusader.rep/idata/01/~00000015.db/db.39.gbf differ diff --git a/Crusader.rep/user/00/~00000008.db/db.29.gbf b/Crusader.rep/user/00/~00000008.db/db.31.gbf similarity index 99% rename from Crusader.rep/user/00/~00000008.db/db.29.gbf rename to Crusader.rep/user/00/~00000008.db/db.31.gbf index 8e9070a..99a64f8 100644 Binary files a/Crusader.rep/user/00/~00000008.db/db.29.gbf and b/Crusader.rep/user/00/~00000008.db/db.31.gbf differ diff --git a/Crusader.rep/user/00/~00000008.db/db.30.gbf b/Crusader.rep/user/00/~00000008.db/db.32.gbf similarity index 98% rename from Crusader.rep/user/00/~00000008.db/db.30.gbf rename to Crusader.rep/user/00/~00000008.db/db.32.gbf index 8426fb8..57a795c 100644 Binary files a/Crusader.rep/user/00/~00000008.db/db.30.gbf and b/Crusader.rep/user/00/~00000008.db/db.32.gbf differ diff --git a/_tmp_changer_ir_dump.py b/_tmp_changer_ir_dump.py new file mode 100644 index 0000000..c529cee --- /dev/null +++ b/_tmp_changer_ir_dump.py @@ -0,0 +1,33 @@ +from pathlib import Path +import json + +from tools.poc_crusader_usecode_parser import parse_ir + + +def dump_variant(label: str, extracted_root: str, class_name: str, slot: int) -> None: + ir = parse_ir(class_name, slot, extracted_root) + print(f"=== {label} {class_name} slot=0x{slot:02X} ===") + print(json.dumps(ir["class"], indent=2)) + print(json.dumps(ir["event"], indent=2)) + for op in ir["ops"]: + print( + json.dumps( + { + "offset": op["offset"], + "mnemonic": op["mnemonic"], + "operands": op["operands"], + "raw_bytes": op["raw_bytes"], + }, + separators=(",", ":"), + ) + ) + + +def main() -> None: + root = Path(__file__).resolve().parent + dump_variant("remorse", str(root / "USECODE" / "EUSECODE_extracted"), "CHANGER", 0x07) + dump_variant("regret", str(root / "USECODE" / "REGRET" / "REGRET_USECODE_extracted"), "CHANGER", 0x07) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/_tmp_map248_observers.js b/_tmp_map248_observers.js new file mode 100644 index 0000000..7709a8b --- /dev/null +++ b/_tmp_map248_observers.js @@ -0,0 +1,35 @@ +const fs = require('fs'); + +const path = 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-248/b27ea0d8d2a1a391/scene.json'; +const scene = JSON.parse(fs.readFileSync(path, 'utf8')); +const items = scene.items.filter((item) => item.shapeDefId === 'shape:1232'); + +function qlo(item) { + return item.quality & 0xff; +} + +function dist(a, b) { + return Math.hypot(a.world.x - b.world.x, a.world.y - b.world.y); +} + +for (const item of items.filter((entry) => entry.npcPreview?.name === 'Observer')) { + const pairs = items.filter((candidate) => candidate.id !== item.id && candidate.frame !== item.frame && qlo(candidate) === qlo(item) && dist(candidate, item) <= 128); + console.log(JSON.stringify({ + id: item.id, + src: item.mapSourceIndex, + frame: item.frame, + mapNum: item.mapNum, + npcNum: item.npcNum, + qlo: qlo(item), + world: item.world, + pairs: pairs.map((candidate) => ({ + id: candidate.id, + src: candidate.mapSourceIndex, + frame: candidate.frame, + mapNum: candidate.mapNum, + npcNum: candidate.npcNum, + name: candidate.npcPreview?.name || null, + qlo: qlo(candidate) + })) + })); +} diff --git a/_tmp_spawner_compare.js b/_tmp_spawner_compare.js new file mode 100644 index 0000000..d7eda20 --- /dev/null +++ b/_tmp_spawner_compare.js @@ -0,0 +1,35 @@ +const fs = require('fs'); + +function load(path) { + return JSON.parse(fs.readFileSync(path, 'utf8')); +} + +function dist(a, b) { + return Math.hypot(a.world.x - b.world.x, a.world.y - b.world.y); +} + +function qlo(item) { + return item.quality & 0xff; +} + +function isSpawner(item) { + return item.shapeDefId === 'shape:1232'; +} + +for (const [label, path] of [ + ['map1', 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-1/9ccaa5dabe08947e/scene.json'], + ['map248', 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-248/b27ea0d8d2a1a391/scene.json'] +]) { + const scene = load(path); + const items = scene.items.filter(isSpawner); + const interesting = items.filter((item) => item.npcPreview?.name === 'Observer' || item.npcPreview?.name === 'RoamingSusan'); + console.log(`\n### ${label} interesting spawners ${interesting.length}`); + for (const item of interesting) { + const pairs = items.filter((candidate) => candidate.id !== item.id && candidate.frame !== item.frame && qlo(candidate) === qlo(item) && dist(candidate, item) <= 128); + const pairText = pairs.map((candidate) => `${candidate.id} src=${candidate.mapSourceIndex} f=${candidate.frame} npc=${candidate.npcNum} ${candidate.npcPreview?.name || '?'} qlo=${qlo(candidate)} d=${dist(candidate, item).toFixed(1)} map=${candidate.mapNum}`).join(' || '); + console.log(`${item.id} src=${item.mapSourceIndex} f=${item.frame} npc=${item.npcNum} ${item.npcPreview?.name || '?'} qlo=${qlo(item)} map=${item.mapNum} world=${item.world.x},${item.world.y},${item.world.z}`); + if (pairText) { + console.log(` pairs: ${pairText}`); + } + } +} diff --git a/_tmp_spawner_truth_pass.js b/_tmp_spawner_truth_pass.js new file mode 100644 index 0000000..0742a7d --- /dev/null +++ b/_tmp_spawner_truth_pass.js @@ -0,0 +1,61 @@ +import fs from "node:fs"; + +const scenePaths = [ + ["map1", "k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-1/9ccaa5dabe08947e/scene.json"], + ["map248", "k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-248/b27ea0d8d2a1a391/scene.json"] +]; + +function distance(left, right) { + return Math.hypot(left.world.x - right.world.x, left.world.y - right.world.y); +} + +for (const [label, scenePath] of scenePaths) { + const scene = JSON.parse(fs.readFileSync(scenePath, "utf8")); + const spawners = scene.items.filter((item) => item.shapeDefId === "shape:1232"); + const rows = []; + + for (const item of spawners) { + if (item.frame !== 0) { + continue; + } + const qlo = item.quality & 0xff; + const pairCandidates = spawners + .filter((candidate) => candidate.id !== item.id && candidate.frame === 1 && ((candidate.quality & 0xff) === qlo) && distance(item, candidate) <= 512) + .sort((left, right) => distance(item, left) - distance(item, right)); + const pair = pairCandidates[0] ?? null; + if (!pair) { + continue; + } + + rows.push({ + controllerId: item.id, + pairId: pair.id, + auto: (item.mapNum & 0x08) === 0, + controllerNpc: item.npcPreview?.name ?? null, + controllerNpcNum: item.npcNum, + pairNpc: pair.npcPreview?.name ?? null, + pairNpcNum: pair.npcNum, + qlo, + distance: Math.round(distance(item, pair)), + pairCount: pairCandidates.length + }); + } + + console.log(`\n=== ${label} ===`); + const autoMismatched = rows.filter((row) => row.auto && row.controllerNpc && row.pairNpc && row.controllerNpc !== row.pairNpc); + const blockedMismatched = rows.filter((row) => !row.auto && row.controllerNpc && row.pairNpc && row.controllerNpc !== row.pairNpc); + const autoControllerMissingPairResolved = rows.filter((row) => row.auto && !row.controllerNpc && row.pairNpc); + console.log(`auto mismatched valid pairs: ${autoMismatched.length}`); + console.log(`blocked mismatched valid pairs: ${blockedMismatched.length}`); + console.log(`auto unresolved-controller / resolved-pair: ${autoControllerMissingPairResolved.length}`); + + for (const row of autoMismatched.slice(0, 12)) { + console.log(`AUTO ${row.controllerId} ${row.controllerNpcNum}:${row.controllerNpc} -> ${row.pairId} ${row.pairNpcNum}:${row.pairNpc} qlo=${row.qlo} d=${row.distance} c=${row.pairCount}`); + } + for (const row of autoControllerMissingPairResolved.slice(0, 12)) { + console.log(`AUTO-UNRESOLVED ${row.controllerId} ${row.controllerNpcNum}:${row.controllerNpc} -> ${row.pairId} ${row.pairNpcNum}:${row.pairNpc} qlo=${row.qlo} d=${row.distance} c=${row.pairCount}`); + } + for (const row of blockedMismatched.slice(0, 12)) { + console.log(`BLOCKED ${row.controllerId} ${row.controllerNpcNum}:${row.controllerNpc} -> ${row.pairId} ${row.pairNpcNum}:${row.pairNpc} qlo=${row.qlo} d=${row.distance} c=${row.pairCount}`); + } +} \ No newline at end of file diff --git a/crusader_decompilation_notes.md b/crusader_decompilation_notes.md index c8713a3..df96ea3 100644 --- a/crusader_decompilation_notes.md +++ b/crusader_decompilation_notes.md @@ -48,6 +48,8 @@ That same note now also separates `~` from `jassica16` more cleanly: `jassica16` The same `docs/ne-segment1.md` note now also has the first consolidated cheat/debug key matrix for the live NE target, including which paths need the broader Laurie/debug master gate (`1478:0844`), which ones need the full keyboard-cheat latch (`1478:6045`), and which ones depend on the extra post-`jassica16` latch (`1478:8c52`). That pass also expands the egg-hatcher explanation: `Ctrl+F7` is now documented as a live `EggHatcherProcess` range visualizer, with practical guidance on where to look for egg-trigger regions in gameplay. +Latest F7 overlay follow-up: new note [docs/f7-overlays.md](docs/f7-overlays.md) now separates the three F7-family debug overlays more rigorously at the geometry level. The main correction is on `Alt+F7`: instead of treating it as broad egg coverage, the current best read now follows the live `SnapProcess` path directly, where `Snap_AddSnapEgg` is only reached for shape `0x04fe` and `Snap_GetSnapEggRange` derives the overlay rectangle from that item's `QHi`, `mapNum`, and `npcNum` bytes. The same note also clarifies the viewer-facing rule for plain `F7`: use an origin-aligned infinite `0x200`-unit world lattice across the visible viewport, not a screen-centered patch. + ## Documentation Structure | File | Contents | @@ -55,6 +57,7 @@ The same `docs/ne-segment1.md` note now also has the first consolidated cheat/de | [docs/overview.md](docs/overview.md) | Binary overview, installed copy findings, address space layout, NE fixup placeholder, segment map, NE import details, next steps | | [docs/phar-lap-extender.md](docs/phar-lap-extender.md) | DOS extender architecture, named functions (entry, loading, memory, I/O, interrupts), key string references | | [docs/ne-segment1.md](docs/ne-segment1.md) | NE Segment 1 full analysis: cursor, input, entity system, shot lifecycle, combat, weapons, AI, player/HUD, destruction, entity constants, vtable index, cheat system | +| [docs/f7-overlays.md](docs/f7-overlays.md) | Focused note on the three cheat-gated F7 debug overlays: toggle sites, live consumers, recovered geometry math, what each overlay represents, and the current viewer-safe reproduction rules | | [docs/jp-remorse-windows9x-investigation.md](docs/jp-remorse-windows9x-investigation.md) | Focused note on the Japanese `/ja/CRUSADER.EXE` Windows-native claim: PE/Win32 image evidence, Win32 windowing, DirectDraw/DirectSound, registry config under `J1.21`, IME/DBCS clues, and the GetVersion-driven Win9x compatibility branch | | [docs/jp-remorse-cheats-and-launch-params.md](docs/jp-remorse-cheats-and-launch-params.md) | Focused note on surviving JP `/ja/CRUSADER.EXE` cheat/debug and startup-argument lanes: `-laurie`, `JASSICA16`, immortality, the recovered Win32 parser table, the live `-u` usecode override, and the current caution that JP `-warp` is only directly proven in mission-only form | | [docs/spanish-cheat-differences.md](docs/spanish-cheat-differences.md) | Focused comparison note for `/es/CRUSADER.EXE` versus the English build's known cheat/debug lanes: `-laurie`, broad cheat gate, gameplay-input gate, low-level keyboard latch, `Ctrl+Q`, Hack Mover, and the current status of the unresolved secret sequence | @@ -73,6 +76,7 @@ The same `docs/ne-segment1.md` note now also has the first consolidated cheat/de | [docs/map-rendering.md](docs/map-rendering.md) | Offline map-rendering lane: `FIXED.DAT`/`GLOB.FLX`/`SHAPES.FLX`/`GAMEPAL.PAL` format notes, current Python renderer, supported inputs, and fidelity gaps | | [docs/editor-object-visibility.md](docs/editor-object-visibility.md) | Focused note on retail editor-only map object hiding: the live `1198:02e4` `SI_EDITOR` early-out in the normal item paint path, the lack of a recovered retail visibility toggle, and the ScummVM/Pentagram cross-check that treats `show editor items` as an engine-side debug feature | | [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 | | [docs/first-mission-map-selection.md](docs/first-mission-map-selection.md) | Focused note on fresh-game startup map selection: No Remorse `Game_Start`, No Regret's early and later mission-start selectors, the separate embedded `-warp mission` table, and the split between code-selected startup and external `FIXED.DAT` map content | | [docs/regret-game-start.md](docs/regret-game-start.md) | Detailed `REGRET.EXE` startup-flow note: `Game_Start`, `Game_RunNewGameFlow`, newly named helpers, startup override globals, and the current best explanation for the duplicated map-1 selector | | [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 | diff --git a/docs/f7-overlays.md b/docs/f7-overlays.md new file mode 100644 index 0000000..dce405b --- /dev/null +++ b/docs/f7-overlays.md @@ -0,0 +1,241 @@ +# F7 Debug Overlays In Retail CRUSADER.EXE + +This note documents the three cheat-gated F7-family overlays in the live NE `CRUSADER.EXE` database and what each one represents on screen. + +The practical keyboard side is already summarized in [docs/ne-segment1.md](docs/ne-segment1.md). This file focuses on the overlay consumers themselves, the recovered geometry math, and what the map viewer can safely reproduce from static scene data. + +## Toggle points + +Inside `World_HandleKeyboardInput_13e8_14b4`, the three overlay hotkeys toggle three different globals before forcing a camera refresh through the active camera process: + +- `13e8:1a7c` toggles `1478:2bca` for plain `F7` +- `13e8:1a50` toggles `1478:2bc9` for `Alt+F7` +- `13e8:1a20` toggles `1478:0ee0` for `Ctrl+F7` + +All three are still gated by the broader cheat/debug latch at `1478:0844`, so they are part of the Laurie/debug family rather than always-on retail UI features. + +The live database and exported evidence agree on the three consumers: + +- `Camera_1180_15ef` handles the plain `F7` grid and the `Ctrl+F7` egg-hatcher overlay +- `Snap_1058_0814` handles the `Alt+F7` snap overlay +- `EggHatcher_1090_0921` handles the `Ctrl+F7` diamond outlines through the shared helper at `1180:1ce5` + +## Palette colors and flash timing + +The overlay colors are not hardcoded RGB values inside the overlay consumers. They are palette-indexed line draws that ride the shared `CycleProcess` palette animator in segment `1438`. + +Recovered retail evidence now closes the important pieces: + +- `CycleProcess_InitColorTables` (`1438:0480`) seeds the runtime color-cycle tables at `1478:6848..1478:6871` +- `CycleProcess_Update` (`1438:011b`) advances those rows by `+2` intensity units per update tick and writes them into palette entries `8..14` +- `Snap_1058_0814` pushes color index `0x09` +- `EggHatcher_1090_0921` pushes color index `0x0d` +- `Camera_1180_15ef` stores and uses color index `0x0e` for the plain `F7` grid lines + +So the current overlay-to-palette mapping is: + +- plain `F7` grid = palette color `14` +- `Alt+F7` snap overlay = palette color `9` +- `Ctrl+F7` egg-hatcher overlay = palette color `13` + +The recovered cycle rows explain the visible behavior: + +- color `9` is a blue ramp that rises `0 -> 63` in steps of `2` and wraps +- color `13` is a white ramp that rises `0 -> 63` in all three channels in steps of `2` and wraps +- color `14` is primarily a green ramp, but its row is flagged through the special `word-flag == 1` path, so each wrap injects an additional small random-looking RGB offset before the next ramp cycle + +For the viewer that means the correct match is not one static CSS color per overlay. The viewer should simulate the same discrete palette-cycle phase and then derive the on-screen RGB from the live palette index assigned to each overlay. + +The retail game is updating those colors in its normal engine/palette lane, not in a browser-style wall-clock timer. For the viewer it is acceptable to run the same phase logic at a slower cadence if that produces a closer visual impression on a modern display. + +## Plain F7: coarse world-cell grid + +### What it represents + +Plain `F7` is the simple debug background grid. It is not an object-family overlay and it is not tied to egg processes. + +Current best read from the live camera path is: + +- the grid is aligned to the world coordinate lattice +- each coarse cell is `0x200` world units wide in both world axes +- the visible shape is an isometric diamond because the game projects the world lattice into screen space + +That means the overlay is best understood as a coarse world-cell coordinate grid useful for orientation and spatial debugging. + +### Viewer reproduction rule + +The viewer should not anchor this grid to the middle of the screen. The right model is: + +- use the world origin-aligned `0x200 x 0x200` lattice +- project every visible coarse cell that intersects the current viewport +- let the lattice continue indefinitely across the visible world instead of only drawing a fixed `3 x 3` patch around the viewport center + +That is closer to the game-space meaning of the overlay than a screen-centered approximation. + +## Ctrl+F7: egg-hatcher trigger diamonds + +### What it represents + +`Ctrl+F7` is not a third generic grid. It visualizes the trigger footprint used by live `EggHatcherProcess` objects. + +The live control flow is: + +- `13e8:1a20` toggles `1478:0ee0` +- `Camera_1180_15ef` checks `1478:0ee0` +- eligible live `EggHatcherProcess` objects are routed through `EggHatcher_1090_0921` +- `EggHatcher_1090_0921` calls the shared diamond helper at `1180:1ce5` + +The process/runtime note in [docs/ne-segment1.md](docs/ne-segment1.md) already closes the gameplay meaning: for non-monster egg families, these processes watch an avatar footprint against egg-centered X/Y/Z trigger windows and fire hatch or unhatch behavior on boundary crossings. + +### Geometry source + +The recovered runtime uses the same egg range values that the process runner tests: + +- `Egg_GetXRange(itemno)` returns the high nibble of the egg-range byte +- `Egg_GetYRange(itemno)` returns the low nibble of the same byte +- each nibble expands in steps of `0x40` world units in the current viewer-side evidence model +- the vertical window is about `+/- 0x30` Z units + +For authored family-4 usecode-trigger eggs in the viewer, the packed range byte already exists in the decoded map item payload, so the static reproduction is defensible. + +### Why it can appear blank in game + +This overlay walks live `EggHatcherProcess` objects, not all possible egg-family map items. A blank `Ctrl+F7` result only means there is no currently eligible live egg-hatcher outline being drawn at that moment. + +## Alt+F7: SnapProcess rectangles, not generic egg coverage + +### The important correction + +The first viewer approximation treated `Alt+F7` as a broad snap-oriented egg-family overlay. The recovered runtime is narrower than that. + +The key proof comes from the object-entry path that feeds the snap process: + +- the live/extracted logic only calls `Snap_AddSnapEgg` when `g_snapProcess != 0` +- and when the entering item's shape number is exactly `0x04fe` + +The exported live note at `10a0:2c82` summarizes the same behavior directly: `when shape 0x4fe enters, we trigger the snap process`. + +So `Alt+F7` is not a generic "all eggs" overlay. Its runtime source is the SnapProcess egg list, and the recovered producer for that list is currently tied to shape `0x04FE` items. + +### Geometry source + +The geometry is recovered more tightly now too, and the important detail is that the helper is not drawing a symmetric world-centered diamond from `item_x,item_y` alone. + +The `Snap_1058_0814` consumer does this per active SnapProcess entry: + +- `qHiHiNibble = (Item_GetQHi(itemno) >> 4) & 0x0f` +- `qHiLoNibble = Item_GetQHi(itemno) & 0x0f` +- `mapSigned = sign_extend_8(Item_GetMapArray(itemno))` +- `npcSigned = sign_extend_8(Item_GetNPCNum(itemno))` + +Then it builds the helper inputs as: + +$$ +x_{helper} = item_x + qHiHiNibble \cdot 0x20 + mapSigned \cdot 0x20 +$$ + +$$ +y_{helper} = item_y + qHiLoNibble \cdot 0x20 + npcSigned \cdot 0x20 +$$ + +$$ +xRange_{helper} = qHiHiNibble \cdot 2 +$$ + +$$ +yRange_{helper} = qHiLoNibble \cdot 2 +$$ + +and finally calls the shared overlay primitive with color `0x09`: + +- `FUN_1180_1ce5(x_helper, y_helper, item_z, xRange_helper, yRange_helper, 9)` + +That helper works in projected screen space. Starting from the projected helper center, it emits a four-segment polygon with these deltas: + +- point `A` = center `+ ( yRange * 0x10, -yRange * 0x08 )` +- point `B` = point `A + ( -xRange * 0x10, -xRange * 0x08 )` +- point `C` = point `B + ( -yRange * 0x10, yRange * 0x08 )` +- close back to center + +So the game is not drawing a simple `min/max` world rectangle and then projecting it. It is feeding an offset center and doubled nibble step counts into a dedicated screen-space overlay helper. That is why the earlier viewer reconstruction read too small. + +In practical terms for the viewer: + +- use the `BRO_BOOT` / `0x04fe` item as the authored source row +- offset the helper center by the packed signed bytes and nibble terms exactly as above +- use doubled nibble counts as helper step counts +- reproduce the helper polygon itself rather than a centered approximation + +This is materially different from the `Ctrl+F7` egg-hatcher path: + +- `Ctrl+F7` is tied to live `EggHatcherProcess` trigger windows +- `Alt+F7` is tied to the SnapProcess list and the explicit `Snap_GetSnapEggRange` rectangle math for shape `0x04fe` + +### What the overlay represents + +Current safest read is: + +- `Alt+F7` visualizes snap-process coverage regions associated with the subset of runtime objects that are inserted into the SnapProcess egg list +- those regions are authored through the `0x04fe` item's own `QHi`, `mapNum`, and `npcNum` bytes rather than the generic family-4 egg-hatcher nibble layout +- this is related to, but not identical with, the family-4 egg trigger overlay shown by `Ctrl+F7` + +### Is this related to camera snapping? + +Probably related to the game's broader snap subsystem, but not proven as a pure camera-only feature. + +What is directly supported by the recovered retail evidence is: + +- the overlay consumer is `Snap_1058_0814`, not a camera helper +- the producer path adds only shape `0x04fe` (`BRO_BOOT`) items into the SnapProcess list +- an inline live note at `10a0:2c82` says that when shape `0x04fe` enters, it triggers the snap process +- another recovered note inside the SnapProcess body marks one related branch as `snap to this egg` + +So the defensible statement is: + +- yes, it is related to the engine's snap behavior +- no, the currently recovered evidence does not justify calling it only a camera-snap overlay + +The best present label is still `SnapProcess coverage` or `snap-region overlay`, not `camera snap grid`. The subsystem may influence camera behavior, controlled-NPC placement, or both, but the proven path is the SnapProcess path itself. + +### Why `BRO_BOOT` appears to own it + +The viewer-side tie to `BRO_BOOT` is not arbitrary. That shape tie is exactly what the retail producer currently shows. + +Recovered producer behavior: + +- when `g_snapProcess != 0` +- and a newly entering item has shape `0x04fe` +- the game calls `Snap_AddSnapEgg` + +That is why the static viewer reproduction binds Alt+F7 to `BRO_BOOT` items. It is not because `BRO_BOOT` is being used as a loose guess for a nearby effect; it is because the current live evidence says those are the items that seed the SnapProcess overlay list. + +## Viewer guidance + +The map viewer can reproduce these overlays with different confidence levels: + +- plain `F7`: strong static reproduction, because it is just a world-aligned coarse grid +- `Ctrl+F7`: strong static reproduction for authored family-4 trigger eggs, because the packed X/Y range bytes are already decoded in scene data +- `Alt+F7`: narrower static reproduction using only shape `0x04fe` items and the exact recovered SnapProcess helper inputs and helper polygon; broader egg-family approximations should be avoided + +Current viewer policy after the latest correction: + +- keep the palette colors matched to the recovered cycle-color indices +- allow the viewer-side animation cadence to be slower than retail if the result reads more like the in-game flash on modern displays +- prefer reproducing the helper polygon exactly over re-expressing it as a simplified centered world diamond + +## Evidence anchors + +- `13e8:1a20`, `13e8:1a50`, `13e8:1a7c`: hotkey toggles in `World_HandleKeyboardInput_13e8_14b4` +- `1058:0137`: `Snap_GetSnapEggRange` +- `1058:021b`: `Snap_AddSnapEgg` +- `1058:0814`: `Alt+F7` overlay consumer +- `1090:0921`: `Ctrl+F7` overlay consumer +- `1180:15ef`: camera-side overlay helper +- `1180:1ce5`: shared diamond helper +- `10a0:2c82`: live note that shape `0x04fe` entering triggers the snap process + +Cross-reference evidence also lives in: + +- [docs/ne-segment1.md](docs/ne-segment1.md) +- [docs/crusader-disasm-reference.md](docs/crusader-disasm-reference.md) +- [exports/CRUSADER.EXE.xml](exports/CRUSADER.EXE.xml) \ No newline at end of file diff --git a/docs/map_1_spawners_targeted_investigation.md b/docs/map_1_spawners_targeted_investigation.md new file mode 100644 index 0000000..224ac27 --- /dev/null +++ b/docs/map_1_spawners_targeted_investigation.md @@ -0,0 +1,194 @@ +# Map 1 Targeted `0x04D0` Spawner Investigation + +## Scope + +This note closes the specific map-1 question about suspicious `0x04D0` frame-paired monster spawners, especially the cases where nearby frame `0` and frame `1` records point at different DTABLE rows. + +Primary evidence used in this pass: + +- decompressed cache scene: `k:\ghidra\crusader_map_viewer\map_renderer\.cache\scene-cache\remorse\map-1\9ccaa5dabe08947e\scene.json` +- extracted pseudocode: `MONSTER.slot_0F enterFastArea`, `MONSTER.slot_0A equip`, `ITEM.slot_2D` +- ScummVM Crusader sources: `glob_egg.cpp`, `actor.cpp`, `item.cpp`, `remorse_intrinsics.h`, `convert_usecode_crusader.h` +- live Ghidra instruction windows around `10a0:3889`, `10a0:0c40`, and `10a0:3d3f` + +The exact coordinate examples mentioned at the start of the investigation do not appear in this cache hash, so the conclusions below are tied to the decompressed cache currently used by the renderer rather than to an older export or a different build. + +## Short Answer + +- Map 1 really does contain many authored frame-`0` / frame-`1` `0x04D0` pairs. +- Those pairs are not well explained as duplicate placements or viewer noise. +- The nearby-pair match key is `QLo = quality & 0xff`, not `npcNum`. +- Frame `0` is the only state checked by `MONSTER.enterFastArea`. +- In the exact extracted `MONSTER.enterFastArea` body, the automatic lane is taken when `mapNum bit 0x08` is clear, not set. +- Frame `1` is skipped by `MONSTER.enterFastArea` and remains the strongest practical preview candidate in the currently confirmed auto-enabled map-1 and map-248 pairs. + +## Verified Usecode Chain + +The strongest current chain is: + +1. `MONSTER.slot_0F` (`enterFastArea`) checks `frame == 0` and conditionally routes into `MONSTER.equip(...)`. +2. `MONSTER.slot_0A` (`equip`) immediately returns unless the current item is still `frame == 0`. +3. `ITEM.slot_2D` scans nearby `shape 0x04D0` items, requires a partner with `frame == 1`, requires the same `intrinsic_00EA(...)` value on both items, and then calls `intrinsic_012F(partner_frame1, source_frame0)`. +4. `intrinsic_012F` is the retail create-NPC helper at `10a0:3d3f` (`Actor::I_createActorCru` / `Item_Intrinsic12F_CreateNPC`). + +That is direct evidence that the frame pairing is authored behavior, not just a visual/editor convention. + +ScummVM plus the matching retail disassembly split the arguments further: + +- the second argument (`source_frame0`) supplies the DTABLE row through `other->getNpcNum()` +- the first argument (`partner_frame1`) supplies facing from `item->getNpcNum() & 0x0f` +- the frame-1 partner also supplies the difficulty gate from `mapNum & 3`, the weapon flag from `mapNum & 4`, the default-activity-1 byte from `quality >> 8`, and the low-quality unk byte +- the frame-0 source supplies the default-activity-0 byte from `quality >> 8` and default-activity-2 from `mapNum` + +That open-source call shape is still useful, but it is no longer strong enough on its own to settle authored spawn identity for every paired `0x04D0` case. + +## Important Corrections + +### `0x00EA` is `getQLo`, not `getNpcNum` + +An older local parser table still labeled `0x00EA` as `Item::getNpcNum(void)`, but stronger sources overrule it: + +- ScummVM intrinsic tables map `0x0EA` to `Item::I_getQLo` +- local intrinsic dumps map `10a0:3889` / `Int0EA` to `Item::I_getQLo` +- live disassembly at `10a0:3889` masks to the low byte + +So the pair-match test inside `ITEM.slot_2D` is a low-quality-byte comparison, not an NPC-row comparison. + +### `mapNum bit 0x08` needed one more correction + +The exact extracted `MONSTER.slot_0F` body still reads: + +- `frame == 0` is required +- `if (!(a & 8)) spawn MONSTER.equip(...)` +- `frame == 1` skips that hook + +So the safe viewer interpretation stays: + +- clear bit `0x08` = automatic enter-area lane enabled +- set bit `0x08` = automatic lane blocked / dormant until another path signals it + +## Representative Map-1 Cache Pairs + +These are concrete decompressed-cache examples from the active hash. + +### Observer / RoamingSusan pair + +- `item:186` at `50784, 5888, 0`: frame `0`, `mapNum = 1`, `npcNum = 6`, `quality = 1315`, preview `Observer` +- `item:187` at the same location: frame `1`, `mapNum = 10`, `npcNum = 11`, `quality = 1315`, preview `RoamingSusan` + +Both records share `QLo = 35` even though their `npcNum` values differ. + +### Observer / ChemSuitGuy pair + +- `item:635` at `37310, 23102, 0`: frame `0`, `mapNum = 8`, `npcNum = 6`, `quality = 1792`, preview `Observer` +- `item:636` at the same location: frame `1`, `mapNum = 7`, `npcNum = 2`, `quality = 512`, preview `ChemSuitGuy` + +Both records share `QLo = 0`. + +### Observer / Guard nearby pair + +- `item:637` at `37830, 22978, 0`: frame `1`, `npcNum = 4`, `quality = 2048`, preview `GUARD` +- `item:638` at `37846, 22986, 0`: frame `0`, `npcNum = 6`, `quality = 1280`, preview `Observer` + +These are not perfectly colocated, but they are close enough for the viewer's local-pair search and again share `QLo = 0` rather than `npcNum`. + +### NPC 99 example + +- `item:14406` near `59864, 35592, 96`: frame `0`, `npcNum = 99` + +This remains a useful outlier because it shows that odd `npcNum` values can exist directly on `0x04D0` records. Under the current create-NPC model, a frame-0 `npcNum = 99` would mean DTABLE row `99` is the spawn-identity source if that record reaches `Actor.createActorCru`. + +## Current Best Model + +The safest synthesis from cache, usecode, ScummVM, and live retail disassembly is: + +- `npcNum` still matters because the create-NPC path is DTABLE-backed. +- But the authored pairing between nearby frame-`0` and frame-`1` `0x04D0` records is not keyed by `npcNum`. +- The pairing key is the low quality byte. +- Frame `0` still owns the verified automatic `enterFastArea` controller lane. +- Frame `1` is still the required partner state in `ITEM.slot_2D`. +- But confirmed auto-enabled pairs now fit better if the frame-`1` preview is treated as the practical visible-NPC candidate while frame `0` stays the controller-side row. + +That explains why map 1 can legitimately contain nearby pairs whose two records name different NPC rows. The pair is not necessarily saying "these are two copies of the same actor". It is more consistent with a split-role authored setup where one `0x04D0` record is the frame-`0` controller and the other is the frame-`1` paired preview/state row. + +## Practical Read Of The Map-1 Examples + +Under the current model: + +- the auto-enabled `Observer` / `RoamingSusan` pair is better read as `frame 0 controls the lane, but the practical visible-NPC candidate is RoamingSusan` +- blocked `Observer` / `ChemSuitGuy` and `Observer` / `GUARD` pairs still fit a dormant-controller interpretation, so they are not equivalent evidence for the visible actor +- the viewer should stop claiming that `frame 0 preview always equals the spawned NPC` + +So the surprising frame-1 labels are no longer best treated as mere noise or a purely control-side annotation. In the confirmed auto-enabled examples, they are the strongest current practical preview of what actually appears. + +## Map 248 Follow-Up + +Map `248` adds a useful counterexample set because it still contains multiple `0x04D0` records previewing `Observer`. + +Two especially useful stable-ID pairs from the decompressed cache are: + +- `fixed:4161` at `54656, 56936, 0`: frame `1`, preview `Observer`, paired with `fixed:4162` frame `0`, `npcNum = 100`, `mapNum = 0`, same `QLo = 63` +- `fixed:3391` at `59846, 45194, 0`: frame `1`, preview `Observer`, paired with `fixed:3390` frame `0`, `npcNum = 134`, `mapNum = 0`, same `QLo = 9` + +Those frame-0 rows matter because the generated Remorse DTABLE used by the renderer currently exposes only rows `0..34`. `npcNum = 100` and `npcNum = 134` do not resolve to known Remorse DTABLE rows at all, while the paired frame-1 rows resolve cleanly to `Observer`. + +There are also several blocked frame-0 `Observer` rows in map 248 such as: + +- `fixed:2061` frame `0`, preview `Observer`, `mapNum = 8`, paired with frame-1 `ChemSuitGuy` +- `fixed:2067` frame `0`, preview `Observer`, `mapNum = 8`, paired with frame-1 `GUARD` + +That split is important: + +- the blocked frame-0 `Observer` rows fit the dormant-controller interpretation cleanly +- the frame-1 `Observer` rows paired with auto-enabled frame-0 unknown rows are stronger candidates for the real in-map `Observer` spawns the user reported + +So map 248 does more than merely weaken a blanket claim that `frame 0 preview always equals the spawned NPC`. It gives a direct contradiction for the old viewer theory: + +- the auto-enabled controller row can carry an unresolved out-of-range `npcNum` +- the paired frame-1 row can still resolve cleanly to the actually reported in-map NPC + +Current safest read is narrower: + +- frame pairing is definitely real +- `QLo` still looks like the local match key +- blocked frame-0 rows can clearly be dormant helpers +- auto-enabled confirmed pairs currently line up better with the frame-1 preview as the practical visible-NPC cue +- but the exact `which side supplies the final NPC row in all authored cases` question still needs one more call-site-level close, because the open-source call signature and the practical map evidence are still not perfectly reconciled + +## Pair-Class Scan + +After the initial targeted examples, a broader cache scan over the active Remorse map-1 and map-248 exports was used to test whether those examples were outliers or a repeatable authored pattern. + +For nearest opposite-frame `0x04D0` candidates matched by shared `QLo` within the renderer's local pair distance: + +- map 1 produced `55` auto-enabled mismatched valid pairs, `96` auto-enabled unresolved-controller / resolved-pair cases, and `21` blocked mismatched valid pairs +- map 248 produced `14` auto-enabled mismatched valid pairs, `78` auto-enabled unresolved-controller / resolved-pair cases, and `8` blocked mismatched valid pairs + +That matters because it rules out the previous "just a handful of weird examples" escape hatch. + +- auto-enabled pairs with different resolved previews are common +- auto-enabled pairs where frame `0` does not even resolve to a valid Remorse DTABLE preview but frame `1` does are also common +- blocked mismatched pairs remain common too, but they fit the dormant-controller interpretation much better than the visible-NPC interpretation + +So the current viewer behavior is now intentionally asymmetric: + +- auto-enabled paired `0x04D0` previews use a single blue carrier, favoring the practical frame-`1` preview side when that side resolves cleanly +- blocked paired `0x04D0` previews use a single red carrier on the dormant frame-`0` controller side + +That single-carrier rule is what removes the misleading double `Observer` / `RoamingSusan` overlays without pretending the underlying create path is fully closed. + +## Viewer Implications + +This pass justifies two concrete renderer behaviors: + +- local pair arrows should match nearby opposite-frame `0x04D0` items by shared `QLo` +- the spawner UI should treat clear `mapNum bit 0x08` as auto-enter enabled and set bit `0x08` as dormant / blocked +- paired `0x04D0` previews should render once per pair rather than once per record, using blue for the active carrier and red for dormant controller previews + +It does not yet justify a stronger claim that every nearby same-`QLo` pair is guaranteed to be the runtime pair actually chosen in every scenario. The current overlay should stay local and conservative. + +## Remaining Uncertainty + +- The static `Actor.createActorCru` argument reading and the practical map evidence are not fully reconciled yet. Map 1 and map 248 now strongly suggest the frame-1 preview is the better viewer-facing NPC cue for confirmed auto-enabled pairs, but the final call-site argument ownership still needs a tighter close in retail disassembly or runtime tracing. +- The user's original coordinates likely came from a different scene export, cache hash, or build, and that mismatch should be kept explicit instead of silently normalized away. +- `npcNum = 99` and other sparse/outlier rows still deserve a separate cleanup pass once more map-1 pairs are cataloged. diff --git a/docs/map_renderer/editor-item-animation.md b/docs/map_renderer/editor-item-animation.md new file mode 100644 index 0000000..afc0c77 --- /dev/null +++ b/docs/map_renderer/editor-item-animation.md @@ -0,0 +1,187 @@ +# Editor-Item Animation In Retail Crusader And The Viewer + +This note closes the current map-viewer animation lane for editor/helper objects that flash, cycle colors, or step through multiple frames. + +The key result is that two different animation systems overlap in this lane: + +- frame animation from Crusader `TYPEFLAG.DAT` +- palette-slot animation from the shared `CycleProcess` rows written into palette entries `8..14` + +The viewer now reproduces both, with one important caveat: + +- the frame-selection semantics are evidence-backed, but the exact retail wall-clock tick length is still not directly closed from the live DOS executable, so the viewer uses the recovered `animType/animData/animSpeed` rules with a viewer-side step cadence that stays in family with the already-recovered palette cycle + +## Short Answer + +The flashing editor walls and helper surfaces are not a separate editor-only special effect process. + +Current best model: + +1. many editor/helper shapes are tagged `SI_TRANSL` and use the low Crusader translucent xform slots +2. those low slots map heavily to source palette indices `8..14` +3. `CycleProcess_Update` advances those same palette rows globally +4. when editor/helper sprites use those slots, they inherit the flashing/cycling colors automatically +5. some shapes also have nonzero `animType`, so they change frame as well as color + +## Evidence Base + +### Live `CRUSADER.EXE` / Ghidra side + +- `CycleProcess_Update` at `1438:011b` advances the shared palette-cycle rows and writes them back through `SuperVGA_SetPaletteColor` at `1438:0366` +- the same recovered cycle rows already explained the F7-family overlay colors in [docs/f7-overlays.md](docs/f7-overlays.md) +- `Gamepal_InitXformpalDatStruct` / `Gamepal_LoadXformpalDat` / `Gamepal_ReadXformpal_1028_0348` at `1028:01db`, `1028:0250`, and `1028:0348` are the live xform-palette load path +- `ItemType_LoadTypeflagDat` / `Item_GetTypeflagData` at `10f8:0275` / `10f8:0336` are the live typeflag load/access anchors for the 9-byte Crusader `TYPEFLAG.DAT` rows +- `Item_PaintSprite` at `1198:02e4` still skips `SI_EDITOR` in the normal gameplay renderer, so most of this animation is normally only visible on debug/editor/helper lanes unless another overlay or engine-side viewer exposes those objects + +### Renderer-side direct pixel evidence + +The earlier translucency pass already established that representative editor/helper translucent shapes are dominated by source palette slots `8`, `9`, `10`, `11`, `13`, and sometimes `14`. + +Representative probe families: + +- invisible/editor walls `0x005A..0x0069` +- helper `0x00E9` +- `Zappy_Surface_*` `0x044D` / `0x044E` + +That evidence is recorded in [docs/map_renderer/translucency-xformpal.md](translucency-xformpal.md). + +### Open-source Crusader engine cross-check + +The missing frame-animation semantics are closed well enough by the Crusader-specific loaders in ScummVM and Pentagram: + +- Crusader `TYPEFLAG.DAT` is a 9-byte format +- byte `4` high nibble = `animType` +- byte `5` low nibble = `animData` +- byte `5` high nibble = `animSpeed` +- byte `6` bit `0` = `SI_EDITOR` + +The ScummVM Crusader item update path also preserves a concrete `animateItem()` switch for `animType` values `1..6`, which is enough to reproduce the frame-selection rules even though the exact DOS tick duration is still not directly timed in the live retail pass. + +## Retail Model + +### 1. Shared palette-cycle rows drive the flashing colors + +This part is already directly evidenced from the live NE executable. + +- `CycleProcess_InitColorTables` seeds the cycle rows +- `CycleProcess_Update` advances them +- the rows are written back into live palette entries `8..14` + +Those same rows already drive: + +- plain `F7` +- `Alt+F7` +- `Ctrl+F7` + +The editor/helper flashing lane reuses that same machinery rather than inventing a separate color animator. + +### 2. XFORMPAL makes the low slots matter for translucent editor/helper shapes + +The editor/helper walls and related helper surfaces are often translucent, and their source pixels disproportionately use the low slot family `8..14`. + +That means their apparent color is effectively delegated to the current shared cycle rows. + +In practice, the visible rule is: + +- if a translucent editor/helper sprite is built from those low slots, its hues will flash when the shared cycle rows change + +### 3. Some editor/helper shapes also have real frame animation + +Crusader typeflags also carry per-shape animation metadata. + +Closed field layout for the Crusader 9-byte row: + +- `animType` = byte `4` high nibble +- `animData` = byte `5` low nibble +- `animSpeed` = byte `5` high nibble + +Recovered/open-source animation behaviors used by the viewer: + +- `animType 1` / `3`: increment through frames; `animData` chooses unconditional, 50%, or block-loop behavior +- `animType 2`: random frame changes +- `animType 4`: random start, then run through frames +- `animType 5`: usecode-driven animation hook; viewer leaves this as static because the live script side is not reproduced here +- `animType 6`: loop from frame `1` while leaving frame `0` as a resting/sentinel state + +## Viewer Implementation + +### Metadata fixes + +The map renderer now carries all three Crusader animation fields through the exported shape definition: + +- `animType` +- `animData` +- `animSpeed` + +The tooltip trait list now shows all three when present. + +### Atlas/reference change + +Static scene exports previously only guaranteed the specific frame used by the authored map item. + +That is not enough for animated shapes. + +The reference build now expands visible shapes with nonzero `animType` to include every frame from the shape archive, so the client can step frames at render time instead of getting stuck on the authored start frame. + +### Palette-cycle reproduction + +The client now carries a compact rendering descriptor for the low xform-cycle slots: + +- source slot `8..14` +- the baked atlas RGBA that corresponds to the current XFORMPAL-remapped translucent output + +At render time, translucent editor/helper sprites are copied into a small sprite canvas and any pixel matching one of those slot RGBA values is recolored from the current shared cycle-row RGB for the same slot. + +That lets the viewer animate the same low-slot translucent editor/helper art without inflating the scene payload with raw pixel streams. + +### Frame animation reproduction + +The client now steps `animType` shapes using the recovered Crusader rules above. + +Current approximation boundary: + +- the semantic frame-advance rules are evidence-backed +- the exact retail wall-clock tick size is still not directly timed from live DOS execution +- the viewer therefore uses a stable viewer-side animation step that stays aligned with the already recovered palette-cycle cadence instead of pretending to know the exact original millisecond rate + +That is good enough to reproduce the visible behavior for map-viewer/editor purposes without overclaiming a closed retail timing constant. + +## Practical Scope In The Viewer + +The current viewer pass targets the objects the user actually asked about: + +- editor-tagged shapes +- helper/occluding editor geometry +- translucent editor/helper surfaces that inherit the low cycle slots + +The frame-animation support is intentionally a little broader than the pure color-cycle support, because `animType` is real shape metadata and some animated map shapes are not strictly editor-only. + +The palette-cycle recolor stays narrower and only applies to the translucent editor/helper lane where the evidence is strong. + +## What Is Closed Versus Still Open + +### Closed enough for the viewer + +- the shared flashing colors come from `CycleProcess` rows written into palette entries `8..14` +- translucent editor/helper sprites really do depend heavily on those same low slots +- Crusader `TYPEFLAG.DAT` carries real `animType/animData/animSpeed` fields in the 9-byte row +- the viewer can now reproduce both the frame-step and palette-cycle sides in a defensible way + +### Still open + +- the exact live DOS wall-clock interval behind the per-item `gametick` animation updates +- whether any remaining helper families need a different XFORMPAL table than the current viewer default for hue reproduction +- the full live usecode behavior behind `animType 5` shapes + +## Ghidra Notes Applied In This Batch + +This batch also adds concise live-DB comments to keep the provenance visible at the key shared anchors: + +- `10f8:0336` for the Crusader 9-byte typeflag animation/editor field layout +- `1438:011b` for the shared palette-cycle row reuse by both F7 overlays and translucent editor/helper colors + +## Related Notes + +- [docs/map_renderer/translucency-xformpal.md](translucency-xformpal.md) +- [docs/editor-object-visibility.md](../editor-object-visibility.md) +- [docs/f7-overlays.md](../f7-overlays.md) \ No newline at end of file diff --git a/docs/map_renderer/editor-object-survey.md b/docs/map_renderer/editor-object-survey.md index f7f41d1..3f989ee 100644 --- a/docs/map_renderer/editor-object-survey.md +++ b/docs/map_renderer/editor-object-survey.md @@ -24,6 +24,14 @@ This pass widened the renderer research beyond egg and NPC spawner objects and f - placeholder cubes and placeholder UI markers - auto-derived helper shapes tied to specific USECODE families like `WALLGUN` +## Focused Caution: Suspicious Map Objects Are Not Always Helpers + +- The map-13 jump-start follow-up around the rare jump-through wall found one especially suspicious nearby placement: `fixed:4767`, shape `0x0135`, frame `0`, at world `47966,53598,97` in the decoded retail cache. +- That object looks tempting as an editor/helper candidate when viewed only from map placement, but the decoded reference data says otherwise: `0x0135` is `shape:309`, a `terrain` item with dimensions `4 x 4 x 0` and traits `solid`, `fixed`, and `land`. +- The useful classification came from USECODE rather than from the exported editor/helper buckets. In the extracted corpus, class `0x0135` is `FFFLOOR`, an environmental hazard/controller family with live `gotHit`, `equip`, and `unequip` bodies. +- The nearby map-13 companion object is not an editor wall flag or a hidden collision override either. The closest local trigger on the same upper platform is the family-4 egg `fixed:4770` (`shape 17`, egg id `37`, subtype selector `QLo 4`), which currently resolves to `CHANGER`, not to a direct wall-solidity helper. +- Practical renderer implication: when a placement looks suspicious in map context, do not assume it belongs in the editor/helper bucket just because it sits beside editor markers. `0x0135` is a good counterexample: it is a gameplay-side environmental floor tile that only becomes legible once the USECODE class is identified. + ## Implemented UI Enrichment The tooltip now exposes generalized metadata for editor/helper objects instead of reserving extra detail almost entirely for NPC spawners: diff --git a/docs/map_renderer/egg-identification.md b/docs/map_renderer/egg-identification.md index ec182c3..33cdaa9 100644 --- a/docs/map_renderer/egg-identification.md +++ b/docs/map_renderer/egg-identification.md @@ -78,7 +78,7 @@ Current Crusader viewer work now closes one additional family-4 detail for the ` - `quality & 0xFF` is the subtype selector for the authored family-4 usecode class. - The runtime resolves that class as `0x0900 + QLo`. - The currently verified authored subtype sets are: - - Remorse: `QLo 0, 1, 2, 13` -> `TRIGEGG`, `ONCEEGG`, `FLOOR1`, `MISS1EGG` + - Remorse: `QLo 0, 1, 2, 4, 13` -> `TRIGEGG`, `ONCEEGG`, `FLOOR1`, `CHANGER`, `MISS1EGG` - Regret: `QLo 0, 1, 2, 5, 8, 10, 13, 24` -> `TRIGEGG`, `ONCEEGG`, `FLOOR1`, `MHATCHER`, `CHANGER`, `DOOREGG`, `MISS1`, `VIDEOEGG` - `npcNum` does not behave like a DTABLE row here. - `xRange = (npcNum >> 4) & 0x0f` @@ -88,6 +88,15 @@ Current Crusader viewer work now closes one additional family-4 detail for the ` That is why the renderer now treats `0x0011` as a proximity/usecode-trigger egg with a projected footprint overlay, a subtype-aware USECODE landing point, and only the narrower local-arrow rules that are actually justified by the recovered subtype body. +One checked Remorse example now makes the `CHANGER` subtype concrete. + +- Map 13 `fixed:4770` is `item:12473:fixed:17:0:47888:53256:96` with `mapNum = 37`, `quality = 4`, and `npcNum = 64`. +- `quality & 0xff = 4` resolves the usecode class to `0x0904`, which matches the extracted Remorse `CHANGER::hatch` body. +- `CHANGER::hatch` reads `eggNum = Egg.getEggId(arg_06)` from `mapNum`, walks nearby `roof` candidates, compares each candidate's low quality byte against that egg id, and destroys the matching roof when `Item.getQLo(roof) == eggNum`. +- The same local decoded scene contains nearby roof placements (`shape:538`, kind `roof`) whose `quality & 0xff = 37`, matching the egg id from `mapNum`. + +Current safest read for `CHANGER` is therefore `keyed roof-destruction trigger`, not generic collision override logic and not a Regret-only family-4 subtype. + ### Monster eggs ScummVM's monster egg accessor exposes: diff --git a/docs/map_renderer/npc-spawners.md b/docs/map_renderer/npc-spawners.md index e978e5d..625543a 100644 --- a/docs/map_renderer/npc-spawners.md +++ b/docs/map_renderer/npc-spawners.md @@ -68,10 +68,11 @@ Current conclusion: `0x024F` frame `0` monster eggs are not the same authoring f ### `0x04D0` Field Ambiguity Notes -- The strongest current activation control lives in `MONSTER.slot_0F enterFastArea`, not in DTABLE. That script only checks `0x04D0` objects when `frame == 0`, then blocks the automatic enter-area lane if `mapNum & 0x08` is set. +- The strongest current activation control lives in `MONSTER.slot_0F enterFastArea`, not in DTABLE. That script only checks `0x04D0` objects when `frame == 0`, then reaches the automatic enter-area lane when `(mapNum & 0x08) == 0`. - Current safest read: `frame 0` is the only state that participates in the automatic `enterFastArea` spawn path, while `frame 1` skips that hook and is therefore more likely to be used in paired or externally signaled setups. +- Confirmed map-1 and map-248 pairs now also show that `frame 0 controller` should not be collapsed into `visible spawned NPC`. In the strongest checked auto-enabled cases, the paired frame-1 record is the better practical preview of the NPC that actually appears. - `mapNum` is not consistently populated across `0x04D0` objects. Some records carry a non-zero map value and others leave it at `0`, so the field is probably contextual control data rather than a universal NPC selector. -- Within that contextual control data, bit `0x08` is now evidence-backed as an `auto-enter disabled` flag for the `MONSTER.enterFastArea` lane. +- Within that contextual control data, bit `0x08` is now evidence-backed as an `auto-enter blocked` flag for the `MONSTER.enterFastArea` lane. - `quality` is also not specific to the DTABLE row. For example, `quality = 1285` shows up on unrelated non-`0x04D0` shapes in the exported scenes, so that value should not be read as proof of a particular NPC identity. - `quality` low byte still does not look like the primary `spawn immediately vs wait` control. The current exported scripts do not use it in `MONSTER.enterFastArea`, although Regret `ALARMHAT` does compare nearby `0x04D0` `Item.getQLo(...)` values against difficulty lanes `0/1/2` before equipping those helpers. - DTABLE row `0` is named `Crusader`, but the open-source engine does not use DTABLE row `0` to bootstrap the player. ScummVM's `CruGame::startGame()` takes the main actor stats from `getNPCDataForShape(1)`, while the generic Crusader actor-creation path accepts `npcNum = 0` as an ordinary DTABLE index. @@ -164,6 +165,13 @@ For editable fixed-record `0x04D0` items, the inspector now also exposes two evi The side panel now also exposes a dedicated `Monster Spawners` audit list for `0x04D0` records, including a filter for the `auto-enter blocked` subset. Clicking an entry centers and pins that spawner so its raw fields and editable controls can be audited quickly. +Recent viewer follow-up tightened that audit lane further: + +- fixed-record `0x04D0` items now surface their stable `fixed:` ids prominently and provide copy buttons +- the tooltip keeps the explicit `☑ auto-enabled` versus `☒ dormant` state label, while the scene preview itself now carries the color signal instead of a separate on-map checkbox badge +- tooltip and list wording now separate the verified frame-0 control lane from the current practical frame-1 preview heuristic instead of asserting that frame 0 always supplies the final visible NPC +- paired `0x04D0` previews now use a single carrier per pair instead of drawing both records: blue for the currently active preview carrier, red for dormant controller previews + The viewport's `Show verified link arrows` overlay now draws two evidence-backed link families: - teleport eggs point from teleporter eggs to teleport destinations that share the same teleport ID @@ -180,7 +188,9 @@ When a valid DTABLE row is present, the viewport renders a semitransparent blue - `0x04D0` DTABLE-backed editor spawners - `0x024F` frame `0` Remorse monster eggs that carry a non-zero `npcNum` -The current renderer uses frame `0` for NPC previews by default, except for `Observer`, which is forced to frame `0x00F` because the earlier frames are blank/broken in the retail assets. +The current renderer still uses each resolved row's known preview frame, except for `Observer`, which is forced to frame `0x00F` because the earlier frames are blank/broken in the retail assets. For paired `0x04D0` rows, the UI now treats frame-1 previews as the stronger practical cue in confirmed auto-enabled examples, even though the frame-0 control path remains the verified automatic trigger lane. + +The stronger reason for that rule is no longer just a few hand-picked examples. A broader cache scan across the active Remorse map-1 and map-248 exports found many auto-enabled mismatched valid pairs plus many auto-enabled cases where the frame-0 controller row does not resolve to a valid Remorse DTABLE preview at all while the paired frame-1 row does. That is why the renderer now suppresses duplicate pair ghosts instead of tinting both sides. Representative exported scene pairs: diff --git a/docs/map_renderer/trigger-usecode-links.md b/docs/map_renderer/trigger-usecode-links.md index 530a77f..36cbfbf 100644 --- a/docs/map_renderer/trigger-usecode-links.md +++ b/docs/map_renderer/trigger-usecode-links.md @@ -65,14 +65,15 @@ That is why the viewer opens `TRIGGER.slot_20` for pinned `0x04B1` helpers inste - `quality & 0xFF` is the subtype selector for this family. - The runtime resolves the usecode class as `0x0900 + QLo`. - Current authored subtype sets are: - - Remorse: `0, 1, 2, 13` -> `TRIGEGG`, `ONCEEGG`, `FLOOR1`, `MISS1EGG` + - Remorse: `0, 1, 2, 4, 13` -> `TRIGEGG`, `ONCEEGG`, `FLOOR1`, `CHANGER`, `MISS1EGG` - Regret: `0, 1, 2, 5, 8, 10, 13, 24` -> `TRIGEGG`, `ONCEEGG`, `FLOOR1`, `MHATCHER`, `CHANGER`, `DOOREGG`, `MISS1`, `VIDEOEGG` - `npcNum` packs `xRange = high nibble` and `yRange = low nibble`. - Crusader multiplies each nibble by `64` world units and uses a `+/-48` Z window for the trigger test. - `TRIGEGG` and `ONCEEGG` route into `TRIGGER.slot_20` on hatch/unhatch, so the renderer now draws local arrows to nearby `0x04B1` helpers by shared `QLo`. - Regret `MHATCHER` scans nearby frame-0 `0x04D0` helpers whose `QLo` matches the egg id in `mapNum`, so the renderer now draws that local helper lane too. - Regret `DOOREGG` scans nearby family-1 door objects whose `QLo` matches the egg id in `mapNum`, so the renderer now exposes that local door lane. -- `FLOOR1`, `CHANGER`, `MISS1*`, and `VIDEOEGG` remain subtype-aware in the tooltip and USECODE target, but they do not yet justify a generic local-arrow rule. +- Map-13 Remorse `CHANGER` example `fixed:4770` now gives the subtype a concrete local read: egg id `37` (`mapNum`) sits beside roof tiles whose `QLo` is also `37`, matching the extracted `CHANGER::hatch` body that destroys nearby roofs keyed by egg id. +- `FLOOR1`, `CHANGER`, `MISS1*`, and `VIDEOEGG` remain subtype-aware in the tooltip and USECODE target, but they still do not justify a generic local-arrow rule. ### `0x04C9 TIMER` diff --git a/docs/usecode-equipment-system.md b/docs/usecode-equipment-system.md index fe3189d..4ef34fa 100644 --- a/docs/usecode-equipment-system.md +++ b/docs/usecode-equipment-system.md @@ -195,6 +195,34 @@ This body does not resemble inventory logic at all. It scans nearby family-`6` o So by the time we reach Crusader, `unequip` has clearly broadened into `deactivate / detach / reverse state / cleanup side effects`. +### Concrete map-side example: map 13 `fixed:4767` + +The recent map-13 wall-jump follow-up produced a useful grounded example of what `FFFLOOR` looks like in the exported scene cache. + +- Retail decoded scene entry `fixed:4767` is `item:12469:fixed:309:0:47966:53598:97`. +- Reference data identifies shape `0x0135` / `shape:309` as `terrain` with dimensions `4 x 4 x 0` and traits `solid`, `fixed`, and `land`. +- The extracted EUSECODE corpus identifies class `0x0135` as `FFFLOOR`, with live `gotHit`, `equip`, and `unequip` handlers. + +The important part is the behavior, not the raw shape flags: + +- `FFFLOOR::gotHit` loops while an actor remains on the tile and repeatedly calls `FREE.slot_20(pid, 8)` followed by `NPC.slot_2d(...)`. +- `NPC.slot_2d` is not a teleport or pure logic stub in this lane; it resolves into the normal actor hit/damage path and can end in `Item.receiveHit(...)`. +- `FFFLOOR::equip` cases `29` and `30` also toggle nearby `0x0135` tiles between frame `1` and frame `0`, and case `30` explicitly `touch`es those tiles after restoring frame `0`. + +This makes the safest current interpretation: + +- `FFFLOOR` is a gameplay-side environmental hazard / sensor floor family. +- The suspicious map-13 tile near the wall-jump setup is therefore better read as `trap or trigger floor on the same upper platform` than as `hidden collision override for the wall itself`. + +That same map-side follow-up also found a nearby family-4 egg on the same upper platform, `fixed:4770` (`shape 17`, egg id `37`, subtype selector `QLo 4`), which resolves to `CHANGER` in retail Remorse. + +That companion egg is now behaviorally useful too: + +- the extracted `CHANGER::hatch` body reads the egg id from `mapNum`, scans nearby `roof` items, compares each roof's low quality byte against that egg id, and destroys matching roofs +- the local decoded map-13 scene contains nearby roof placements (`shape:538`, kind `roof`) whose `quality & 0xff = 37`, matching the egg id from `fixed:4770` + +So the current best local read is `hazard/sensor floor plus keyed roof-destruction trigger cluster`, not `single suspicious floor tile beside a special-cased wall`. + ## What survived from the RPG ancestor The surviving part is not just the word choice. The system architecture survived too: diff --git a/docs/usecode-roundtrip-ir.md b/docs/usecode-roundtrip-ir.md index 994fbf2..13a8f4f 100644 --- a/docs/usecode-roundtrip-ir.md +++ b/docs/usecode-roundtrip-ir.md @@ -819,4 +819,27 @@ That gets to a reversible editor sooner than waiting for a full semantic VM reco - **Renderer Fixes:**: [src/lib/usecode-decompiler.js](k:/ghidra/crusader_map_viewer/map_renderer/src/lib/usecode-decompiler.js) now propagates enclosing exit labels through nested structured regions, lifts raw `foreach_list` / `foreach_slist` loops into structured `while (true)` bodies, and treats comment-prefixed cleanup-plus-return blocks such as `/* free_local_list */` + `return;` as real return targets for control-flow recovery. - **Readability Impact:**: The remorse cache file [TRIGGER/slot_20_slot_20.txt](k:/ghidra/crusader_map_viewer/map_renderer/.cache/usecode/remorse/EUSECODE/pseudocode/TRIGGER/slot_20_slot_20.txt) now renders as one structured function: the initial phase/setup lane is straight-line `if/else`, the middle search fan-out is structured nested conditionals, the nearby `0x04B1` scan is a real `for item in nearby_items(...)` loop, and the follow-up low-priority trigger worklist is a structured fixed-point `while (1)` loop rather than detached `block_XXXX` labels. - **Regression Coverage:**: [scripts/test-usecode-structuring.mjs](k:/ghidra/crusader_map_viewer/map_renderer/scripts/test-usecode-structuring.mjs) now covers three additional generic structuring cases and one real-data regression that decodes `STATIC/EUSECODE.FLX`, rebuilds the live `TRIGGER.slot_20` IR, and asserts that the rendered pseudocode no longer falls back to block labels or `goto block_...` jumps. -- **Binary / Ghidra Impact:**: This pass tightened renderer-side control-flow recovery only. It did not add a new compiled-side VM decode, so no new Ghidra rename or comment was applied in `CRUSADER.EXE` during this batch. \ No newline at end of file +- **Binary / Ghidra Impact:**: This pass tightened renderer-side control-flow recovery only. It did not add a new compiled-side VM decode, so no new Ghidra rename or comment was applied in `CRUSADER.EXE` during this batch. +- **Additional Root Cause Closed:**: `BLASTPAC.slot_01` still kept loose blocks after the earlier trigger pass because the full structurer treated `goto` edges that jumped exactly to the current region end label as unstructured rather than as normal join exits. That blocked both the `nearby_items(shape=0x053A, origin=global[0x003C])` selector loop body and the later target/crouch join chain from collapsing. +- **Additional VM Evidence:**: ScummVM's Crusader VM remains the strongest external semantics anchor for this lane: [uc_machine.cpp](k:/misc/scummvm/engines/ultima/ultima8/usecode/uc_machine.cpp) shows opcode `0x51` as a relative branch on false, opcode `0x73` as `loopnext` pushing a loop-valid flag and freeing the temporary list when exhausted, and opcodes `0x75` / `0x76` as real foreach iterators that keep the loop frame live until completion and then pop it before jumping to the exit target. +- **Additional Renderer Fix:**: [src/lib/usecode-decompiler.js](k:/ghidra/crusader_map_viewer/map_renderer/src/lib/usecode-decompiler.js) now also treats jumps to the current structured-region end label as exits, which lets selector-loop bodies and nested join-heavy `if/else` regions close cleanly without falling back to raw block labels. +- **Additional Readability Impact:**: The remorse cache file [BLASTPAC/slot_01_use.txt](k:/ghidra/crusader_map_viewer/map_renderer/.cache/usecode/remorse/EUSECODE/pseudocode/BLASTPAC/slot_01_use.txt) now renders as straight structured pseudocode: the `shape 0x053A` search is a real `for item in nearby_items(...)` loop, the inner retry lane stays a structured counted loop, and the later `target` / `InCrouch` path is one nested `if/else` tree rather than detached `block_0415`, `block_046e`, `block_05c5`, and `block_061d` islands. +- **Additional Regression Coverage:**: [scripts/test-usecode-structuring.mjs](k:/ghidra/crusader_map_viewer/map_renderer/scripts/test-usecode-structuring.mjs) now adds one focused synthetic `region-end goto` regression plus one real-data `BLASTPAC.slot_01` regression, and the current focused suite passes after regenerating the cache. +- **Current Binary / Ghidra State:**: The compiled-side anchor is still the existing `000d:ebe3` sequencer note, and this batch still did not recover a new compiled opcode handler. A matching live decompiler comment was added at `000d:ebe3` to record the ScummVM-backed loop/branch contract used by the current BLASTPAC/TRIGGER selector-loop recovery (`0x51` false-branch, `0x73` loopnext validity/free behavior, `0x75/0x76` foreach iteration contract). + +## **Recent Renderer Work (2026-04-01, list + selector follow-up)** + +- **List Opcode Evidence Closed:**: ScummVM's live Crusader VM in [uc_machine.cpp](k:/misc/scummvm/engines/ultima/ultima8/usecode/uc_machine.cpp) confirms opcode `0x0E` builds a new list from `count` stack values of `element_size`, and opcode `0x17` concatenates two list ids by appending the top list into the next list and pushing the combined result. +- **Renderer Fixes:**: [src/lib/usecode-decompiler.js](k:/ghidra/crusader_map_viewer/map_renderer/src/lib/usecode-decompiler.js) now lifts `create_list` into list literals such as `[item]` and `append_list` into list concatenation expressions instead of leaving raw comment placeholders. That closes the common temporary-worklist patterns in bridge/trigger/free scripts where the old output showed `/* create_list */` and `/* append_list */` immediately before an assignment. +- **False-Branch Fix:**: The same renderer pass now treats compound boolean expressions conservatively when inverting Crusader's `0x51` false-branch. For simple comparisons it still flips the operator directly, but for composed `&&` / `||` expressions it now emits a whole-expression negation rather than corrupting the leftmost compare. This fixes the broken `BRO_BOOT.slot_0F` entry test that previously rendered as `global[0x001f] != 2 || global[0x001f] == 3 ...` even though the bytecode is a plain OR-chain of equality compares. +- **Selector Readability:**: Long same-selector equality ladders that share one join target now render as `switch (...)` blocks when every branch is a simple equality case. The immediate real-data win is [BRO_BOOT/slot_0A_equip.txt](k:/ghidra/crusader_map_viewer/map_renderer/.cache/usecode/remorse/EUSECODE/pseudocode/BRO_BOOT/slot_0A_equip.txt), whose repeated `global[0x001f] == N` movie dispatch chain now decompiles as a switch instead of six `else if` arms. +- **BRO_BOOT Structuring Impact:**: With the compound-condition fix in place, [BRO_BOOT/slot_0F_enterFastArea.txt](k:/ghidra/crusader_map_viewer/map_renderer/.cache/usecode/remorse/EUSECODE/pseudocode/BRO_BOOT/slot_0F_enterFastArea.txt) is expected to collapse into one structured `if/else` around the two `SPANEL` scans plus the trailing infinite animation loop, instead of keeping `entry:` / `block_0454`-style fallbacks. +- **Regression Coverage:**: [scripts/test-usecode-structuring.mjs](k:/ghidra/crusader_map_viewer/map_renderer/scripts/test-usecode-structuring.mjs) now adds synthetic regressions for list-literal lifting, compound false-branch negation, and switch rendering, plus real-data regressions for `BRO_BOOT.slot_0A` and `BRO_BOOT.slot_0F`. + +## **Recent Renderer Work (2026-04-02, CHANGER selector close)** + +- **Root Cause Closed:**: `CHANGER.slot_07` in both Remorse and Regret was still rendering as `while (condition)` because the JS loop-selector decoder only recognized the older field-match selectors such as `nearby_items(shape=...)` and the opaque `selector_0x42(...)` fallback. The CHANGER bodies use a different selector family: `loopscr 0x24` plus `loopscr 0x4c`, with a hardcoded shape whitelist left on the stack, a computed search distance (`100 * 32`), and the egg item as origin. Because that selector family was not decoded, the renderer could not surface the roof-target scan clearly enough to decompile or visualize. +- **Renderer Fix:**: [src/lib/usecode-decompiler.js](k:/ghidra/crusader_map_viewer/map_renderer/src/lib/usecode-decompiler.js) now recognizes that stacked-shape whitelist selector and emits readable loops such as `for roof in nearby_items(shapes=[...], distance=(100 * 32), origin=arg_06)` instead of collapsing back to `while (condition)`. +- **Readability Impact:**: The cached Remorse and Regret `CHANGER.slot_07` pseudocode bodies now expose the actual nearby-roof selector inputs directly: the hardcoded roof-shape whitelist, the recovered `3200`-unit range, and the egg-origin scan. That makes the later `Item.getQLo(...) == eggId` destroy branch legible without a raw-byte fallback pass. +- **Editor Impact:**: The same selector close justified promoting Regret `QLo 8 -> CHANGER` from tooltip-only metadata into the map editor overlay. The viewer can now expose the same local roof-target lane for Regret that was already proven for Remorse, using the recovered Regret whitelist and the same `3200`-unit scan distance. +- **Regression Coverage:**: [scripts/test-usecode-structuring.mjs](k:/ghidra/crusader_map_viewer/map_renderer/scripts/test-usecode-structuring.mjs) now adds one synthetic regression for the `loopscr 0x24/0x4c` stacked-shape selector and one real-data regression for Regret `CHANGER.slot_07`, so future renderer changes fail if this selector family falls back to opaque loop output again. \ No newline at end of file diff --git a/plan-mid.md b/plan-mid.md index 3bad4df..3efd6cb 100644 --- a/plan-mid.md +++ b/plan-mid.md @@ -32,6 +32,7 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan - The latest owner-loaded range pass justified another small confidence bump too: the owner-resource child selector now matches extracted `class_id + 2` exactly, the class header/subentry math at `000d:5066/51fd/53b4` is closed against the extractor's raw headers and event rows, and the surviving immortality uncertainty has moved from `can the loader fit NPCTRIG arithmetic at all?` to the narrower `which class family is actually selected upstream?` question. - The PSX sprite-extraction side is also less speculative now: a dump-grounded pass proved the known-colored wall-console bundle `bundle_000A1B04` already exists verbatim in live VRAM at texture page `(1,1)`, and the corrected working color formula is the top-left live CLUT candidate from the atlas, namely the contiguous `256`-entry slice at GPU row `0xF0`, `x=0`; the same rule now produces plausible output across a wider `92`-bundle `mode 1` batch instead of only the single cabinet proof case. - The PSX executable-side catalog lane is tighter too: `SLUS_002.68` now has comment-backed proof that `wdl_resource_bundle_load_by_index` selects seven hardcoded `\LSETn\L` prefixes across thresholds `10/20/30/40/50/60`, the extracted disc currently ships `62` level bundles (`L0..L58`, `L62..L64`) with a real gap at `L59..L61`, the executable exposes only `15` plain-text `Mission Briefing ^Mission N` strings, and the mission-complete passcode path now has a closed `4`-character consonant/digit alphabet at `80063ef0` plus direct ammo/item/weapon name tables. The remaining PSX passcode gap is now narrower: public cheat-password candidates `XXXX` and `L0SR`/`L0SER` are not stored as plain ASCII in `SLUS_002.68`, so the compare path likely uses numeric or transformed validation instead of a flat string table. +- The F7-overlay lane is tighter again after the latest live/exported cross-check. New note `docs/f7-overlays.md` now separates the three cheat-gated overlays by their actual geometry source: plain `F7` is the coarse origin-aligned `0x200`-unit world lattice; `Ctrl+F7` is the egg-hatcher trigger-footprint overlay driven by `EggHatcher_1090_0921`; and `Alt+F7` is narrower than the earlier viewer approximation because the runtime only feeds `Snap_AddSnapEgg` from shape `0x04fe`, with `Snap_GetSnapEggRange` deriving each snap rectangle from that item's `QHi`, `mapNum`, and `npcNum` bytes. The practical viewer implication is also closed more cleanly now: do not center-snap the plain grid, and do not treat `Alt+F7` as generic egg-family coverage. - The new PSX pre-alpha comparison lane is also anchored now: `/psx/prealpha/SLUS_002.68` still carries direct `Crusader: No Remorse` branding, the same retail-style `wdl_resource_bundle_load_by_index` `\LSET1\L .. \LSET7\L` threshold ladder, and the same `15` mission-briefing/passcode shell, but the unpacked `Crusader 2 Pre-Pre Alpha` disc currently ships only `3` level bundles, `1` XA, and no `.STR` movies. The most interesting current mismatches are architectural leftovers that no longer match the disc literally, especially the missing-file `\AUDIO\TALK1.XA;1` path and the surviving `LoadExec` helper for `MENU.EXE` / `ENGINE.EXE` / `PSX.EXE`. - That closes one live top-priority section and justifies a small headline increase even though the remaining work is still breadth-heavy. @@ -63,6 +64,8 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan - That same startup lane is now tighter at the argument level too. Current best parser/control-flow read in `REGRET.EXE` is `-warp [x y z]`, with X/Y/Z carried as positional argv tokens after the mission number rather than as separate recovered switches. The corresponding runtime branch in `Game_RunNewGameFlow` is also clearer: nonnegative `-egg` overrides beat the coordinate path, while the real eggless-map workaround is `-warp ` plus `-mapoff` with `-egg` omitted so the game falls into direct `NPC_Teleport` instead of the teleporter-egg lookup. - The matching No Remorse cross-check is now closed too. Live `CRUSADER.EXE` `HandleCommandlineArgs` at `1048:0adc` uses the same positional `-warp [x y z]` parser shape, and `Game_Start` at `1020:029e` / `1020:02d0` uses the same runtime precedence: direct coordinates only win when the egg override is still negative, otherwise the code falls back to `Teleporter_CreateProcessDirect`. The parameter-only eggless-map workaround is therefore shared across both retail games, not Regret-specific. - The public map-renderer link lane is tighter again. Cross-game `0x01DB` support now covers both the earlier frame-`1` teleporter-light helpers and the remaining Regret map `3` frame-`0` telepad helper placements that carry destination ids `27/28` in `quality`. The same pass also adds the checked same-map `ELEVATOR` lane: frame-`0` `shape:542` sources now link to local teleport-destination eggs by verified `QLo` rules (`1..0x0f -> same egg id`, `0x10 -> egg 4`). Current best gap is still Regret map `3` egg `102`, which does not sit on the verified `shape:542` / `shape:307` elevator lanes yet. +- The map-13 wall-jump follow-up is tighter too. The suspicious nearby start tile `fixed:4767` is now closed as `FFFLOOR` (`shape 0x0135` / `shape:309`) rather than as an editor/helper collision override: decoded reference data still marks it as ordinary `terrain` (`4 x 4 x 0`, `solid`, `fixed`, `land`), but the extracted EUSECODE corpus shows `FFFLOOR::gotHit/equip/unequip` as an environmental hazard/sensor lane that toggles nearby same-shape floors and pushes standing actors through `NPC.slot_2d` into the normal hit/damage path. The nearest same-level trigger companion in the retail cache is family-4 egg `fixed:4770`, which currently resolves to `CHANGER`, so the local map-13 setup now reads better as a small scripted floor/trigger cluster than as proof that the rare jump-through wall has an authored per-instance non-solid flag. +- That same map-13 trigger companion is tighter now too. The nearby family-4 egg `fixed:4770` is no longer just a subtype label: retail Remorse clearly uses `QLo 4 -> CHANGER`, and the extracted `CHANGER::hatch` body plus the local decoded scene now line up as a keyed roof-destruction trigger (`mapNum` egg id `37`, nearby roof tiles with `QLo 37`). Current safest local read is `FFFLOOR hazard tile plus CHANGER roof-removal cluster` on the same upper platform, still not a direct wall-solidity override. - The editor-helper overlay lane is tighter too. A broader exported-scene sweep now shows that `BRO_BOOT` (`0x04FE`) really does form a repeatable local helper lane into nearby same-`QLo` `SPANEL` items, with concrete Remorse examples on maps `9`, `10`, `11`, `21`, `23`, `160`, and `246`, so the renderer now promotes `BRO_BOOT -> SPANEL` alongside the existing cmd-link, alarm, steam, door, and flame helper arrows. The same follow-up kept two tempting false positives out of the overlay: `NPC_ONLY -> 0x04B1` and `DEATHBOX -> 0x04B1` still read better as incidental local overlap than as a dedicated helper-source relationship. The latest tooltip pass also upgrades `0x04B1` from a mostly structural decode to concrete operation notes: helper dispatch via nearby `0x0476`, direct target mutation, timed pulses through `TRIGGER.slot_22` / `DOOR.slot_21`, verified link rewrites, and a create-and-drop lane. - The skill-controller lane is tighter too. Shape `0x0120` is now closed as `FASTSKIL`, distinct from `SKILLBOX`: `enterFastArea` waits briefly, only runs while map-array is clear, uses frame `0/1` as difficulty thresholds for `TRIGGER.slot_20` lane `0` versus `1`, and uses frame `2` as an explicit `QLo/+1/+2` difficulty router. The renderer now exposes that decode in tooltips and adds conservative local `FASTSKIL -> 0x04B1` helper arrows, with frame-`2` variants for the recovered `QLo + 1` and `QLo + 2` lanes. - The switch/pad clarification lane is tighter too. Shape `0x0080` now closes as `BOX_EW`, and sampled exported scenes are strong enough to promote a conservative `BOX_EW frame 0 -> nearby same-QLo 0x04B1` helper arrow rule. Shape `0x04CD` now closes as `TRIGPAD`, but its broader occupancy/elevator behavior and the negative scene sweep keep it metadata-only instead of promoting a generic cmd-link overlay. Shape `0x033A` now reads best as a tiny `NUMBERS` readout/display helper family clustered with nearby `0x0501/0x0502/0x0503/0x0505/0x0507` pieces, so it also stays label-only. @@ -75,10 +78,11 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan - The USECODE/VM tooling lane now also has a concrete near-term implementation path: a Pentagram-derived proof-of-concept parser can reuse opcode decoding while swapping in the locally verified owner-loaded class and slot arithmetic, with a hybrid Ghidra comment/bookmark import path instead of a premature custom processor module. - The USECODE tooling lane now also has a first full readable corpus export: `tools/export_usecode_pseudocode.py` writes `977` current pseudocode bodies into `USECODE/EUSECODE_extracted/pseudocode`, and the first focused read of that corpus now shows `JELYHACK::use` / `JELYH2::use` as tiny shared `set_info(0x0207) -> process_exclude -> return` stubs rather than hidden active event cores. - The USECODE tooling lane now also has two new follow-up notes grounded in the exported corpus: `docs/usecode-tool-improvement-plan.md` turns the Pentagram/`crusader-disasm` comparison into a concrete parser roadmap, and `docs/usecode-alarmhat-analysis.md` records the current best evidence-backed read of `ALARMHAT::equip` as a frame-driven local alarm-state controller that equips nearby `shape 0x04D0` helper objects in different modes. -- That same `0x04D0` lane is tighter again after the next follow-up: `MONSTER::enterFastArea` is now verified as the immediate-spawn gate for `shape 0x04D0`, with the automatic lane only reached when `frame == 0` and `(mapNum & 0x08) == 0`, while frame `1` skips that hook entirely and remains the best current candidate for paired or externally signaled setups. -- The public renderer follow-up now has a better audit surface for that same lane: the `Monster Spawners` panel lists `0x04D0` records directly, the tooltip editor writes the verified `frame`/`mapNum bit 0x08` controls back into exportable FIXED records, and the new arrow overlay is intentionally scoped to evidence-backed link families only. Exported-usecode corroboration now reaches beyond `ALARMHAT` too: `ITEM.slot_2D`, `FUSPAC.slot_01`, and `MISS8.slot_20` all show nearby `0x04D0` scans keyed by frame and/or `Item.getQLo(...)`, which strengthens the current low-quality-byte-as-local-signal-key model without promoting it into a universal object pointer. +- That same `0x04D0` lane is tighter again after the latest map-1/map-248 follow-up: decompressed cache evidence plus extracted `MONSTER.slot_0F`, `MONSTER.slot_0A`, and `ITEM.slot_2D` still show that frame-`0` / frame-`1` `0x04D0` pairs are authored on purpose and keyed locally by `Item.getQLo(...)`, but the old `frame 0 always equals the spawned NPC row` reading is no longer strong enough. Confirmed auto-enabled pairs now line up better if frame `0` is treated as the verified controller lane while frame `1` is treated as the practical visible-NPC cue. +- The public renderer follow-up now reflects that narrower stance: the `Monster Spawners` panel still lists `0x04D0` records directly, fixed-record spawners expose copyable stable ids, tooltip/list semantics distinguish the verified frame-0 control path from the current frame-1 practical-preview heuristic, and paired previews now render once per pair instead of twice. Current preview rule is `blue active carrier / red dormant controller`, with the tooltip keeping the explicit `☑/☒` state label and the scene overlay dropping the older on-map checkbox badge. Exported-usecode corroboration still reaches beyond `ALARMHAT` too: `ITEM.slot_2D`, `FUSPAC.slot_01`, and `MISS8.slot_20` all show nearby `0x04D0` scans keyed by frame and/or `Item.getQLo(...)`, which strengthens the low-quality-byte-as-local-signal-key model without promoting it into a universal object pointer. - The USECODE tooling lane now also has a broader equipment-event note: `docs/usecode-equipment-system.md` records live binary proof that `Item_Equip` / `Item_Unequip` are real generic usecode event dispatchers gated by owner-row capability masks (`0x400` / `0x800`), and that the exported corpus currently contains `77` `equip` bodies plus `50` `unequip` bodies spread across actor, turret, alarm, conveyor, camera, and hazard classes. Current best read is `surviving Ultima-style event vocabulary generalized into activation/setup/state-change semantics`, not yet `fully proven paper-doll RPG gear subsystem`. - The USECODE tooling lane now also has its first implemented readability follow-through from that improvement list: `tools/poc_crusader_usecode_parser.py` and `tools/export_usecode_pseudocode.py` now regenerate the full `977`-body corpus with one verified wrapper alias seed (`FREE.waitNTimerTicks` for `0A0C:0032`), class-name-aware target rendering (`FREE.slot_21`, `BLASTPAC.slot_20`, `TRIGGER.slot_20`, etc.), first-pass selector decoding that upgrades the simpler alarm/trigger `loopscr` runs into `for ... in nearby_items(shape=..., origin=...)` / `for ... in nearby_items(family=..., origin=...)` loops, and a second readable selector-family fallback that collapses raw `loopscr 0x42` runs into `selector_0x42(arg0=..., arg1=..., arg2=..., origin=...)` annotations or `for ... in selector_0x42(...)` loops where the control flow is simple enough. +- That same renderer lane is tighter again after the BLASTPAC follow-up: ScummVM `uc_machine.cpp` keeps the VM-side semantics anchored (`0x51` branch-on-false, `0x73` loopnext pushes a validity flag and frees exhausted search lists, `0x75/0x76` are real foreach iterators), and the map-viewer structurer now also treats jumps to the current region end label as structured exits. After regenerating the cache, `BLASTPAC.slot_01` no longer falls back to `block_0171` / `block_0415` style islands; the `shape 0x053A` scan is a real `for item in nearby_items(...)` loop and the later `target` / crouch lane is one nested `if/else` tree. The focused renderer regressions now cover both the synthetic region-end join case and the real `BLASTPAC.slot_01` body. - The USECODE/VM lane now also has a verified generic masked-context creation hub (`000d:463a`) plus two concrete sequencer-internal consumer blocks (`000d:208b`, `000d:21ed`) built directly on `entity_vm_context_create_from_slot_index`. - The USECODE/VM lane now also has first caller-role evidence outside the older seg021 wrapper island: the new seg004 callers keep masks `0x8000:0x0007` and `0x2000:0x0015` in gameplay-side materialization lanes, while the newly named seg006 helpers now separate one extra-word masked lane with a real local class-state transition fallback (`0x0008:0x0030`) from a guarded `0x0010:0x0008` materializer that simply returns `0` on miss after readiness checks. - The USECODE/VM lane now also has a wider verified higher-slot wrapper ladder: the `0005` island reaches slot ordinals `0x10..0x14`, slot `0x12` is a zero-extra-word lane, slots `0x11/0x13/0x14` carry extra-word payloads, and the current safest read is `slot-stable payload-shape taxonomy` rather than direct event-name promotion.