Updated knowledge
This commit is contained in:
parent
5f22a8e2fd
commit
1ad746ba82
21 changed files with 882 additions and 9 deletions
241
docs/f7-overlays.md
Normal file
241
docs/f7-overlays.md
Normal file
|
|
@ -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)
|
||||
194
docs/map_1_spawners_targeted_investigation.md
Normal file
194
docs/map_1_spawners_targeted_investigation.md
Normal file
|
|
@ -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.
|
||||
187
docs/map_renderer/editor-item-animation.md
Normal file
187
docs/map_renderer/editor-item-animation.md
Normal file
|
|
@ -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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:<mapSourceIndex>` 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- **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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue