Furthered knowledge

This commit is contained in:
MaddoScientisto 2026-03-31 00:35:13 +02:00
commit eea47d884e
10 changed files with 699 additions and 0 deletions

69
_tmp_scene_link_scan.ps1 Normal file
View file

@ -0,0 +1,69 @@
Set-Location 'k:\ghidra\crusader_map_viewer\map_renderer'
function Get-Shape([object]$item) {
if ($null -eq $item -or $null -eq $item.shapeDefId) { return $null }
if ($item.shapeDefId -match '^shape:(\d+)$') { return [int]$Matches[1] }
return $null
}
function Get-Qlo([object]$item) {
if ($null -eq $item -or $null -eq $item.quality) { return $null }
return ([int]$item.quality) -band 0xff
}
function Get-Distance([object]$a, [object]$b) {
$dx = [double]$a.world.x - [double]$b.world.x
$dy = [double]$a.world.y - [double]$b.world.y
return [math]::Sqrt(($dx * $dx) + ($dy * $dy))
}
$pairs = @(
@{ Name = 'BRO_BOOT->SPANEL'; Source = 0x04fe; Target = 0x03aa; Distance = 768 },
@{ Name = 'NPC_ONLY->CMD_LINK'; Source = 0x0366; Target = 0x04b1; Distance = 768 },
@{ Name = 'DEATHBOX->CMD_LINK'; Source = 0x04e7; Target = 0x04b1; Distance = 768 },
@{ Name = 'NPC_ONLY->TRIGGERISH'; Source = 0x0366; Target = 0x0361; Distance = 768 }
)
$sceneFiles = Get-ChildItem '.\site\data\maps' -Recurse -Filter 'scene.json' | Sort-Object FullName
$lines = New-Object System.Collections.Generic.List[string]
foreach ($pair in $pairs) {
$matchedSources = 0
$sourceCount = 0
$linkCount = 0
$examples = New-Object System.Collections.Generic.List[string]
foreach ($file in $sceneFiles) {
$scene = Get-Content $file.FullName -Raw | ConvertFrom-Json
$items = @($scene.items)
$sources = @($items | Where-Object { (Get-Shape $_) -eq $pair.Source })
if ($sources.Count -eq 0) { continue }
$targets = @($items | Where-Object { (Get-Shape $_) -eq $pair.Target })
foreach ($source in $sources) {
$sourceCount += 1
$qlo = Get-Qlo $source
if ($null -eq $qlo) { continue }
$matches = @($targets | Where-Object {
(Get-Qlo $_) -eq $qlo -and (Get-Distance $source $_) -le $pair.Distance
})
if ($matches.Count -gt 0) {
$matchedSources += 1
$linkCount += $matches.Count
if ($examples.Count -lt 6) {
$examples.Add(('{0}/{1}: source={2} qlo={3} targetIds={4}' -f $file.Directory.Parent.Name, $file.Directory.Name, $source.id, $qlo, (($matches | ForEach-Object { $_.id }) -join ',')))
}
}
}
}
$rate = if ($sourceCount -gt 0) { [math]::Round(($matchedSources / $sourceCount) * 100, 1) } else { 0 }
$lines.Add(('PAIR {0}' -f $pair.Name))
$lines.Add(('sources={0} matched={1} rate={2}% links={3}' -f $sourceCount, $matchedSources, $rate, $linkCount))
foreach ($example in $examples) {
$lines.Add($example)
}
$lines.Add('')
}
$lines | Set-Content 'k:\ghidra\Crusader_Decomp\_tmp_scene_link_scan.txt'

View file

@ -128,6 +128,632 @@ The current renderer catalog data already contains egg-oriented notes that inclu
Those catalog references are evidence for the workflow and UI, but the renderer should still treat them as conventions layered on top of the raw egg-family decoding above.
## Non-Egg Teleporter Carriers
Not every authored teleporter in the exported maps is itself an egg-family item.
- Both retail games use shape `0x01DB` (`TELEPORTER_LIGHTS`) as a non-egg teleporter helper even though the exported shape definition is terrain-like rather than family `8` egg content.
- Current renderer scene exports show both frame `1` and some frame `0` `0x01DB` placements carrying small low-byte `quality` values that line up with real teleport-destination eggs.
- Verified No Remorse examples include map `13`, where frame `1` `0x01DB` items carry `quality & 0xFF = 14` and the map also contains teleport-destination egg `14`, and map `3`, where one frame `1` `0x01DB` carries low-byte `16` and the map contains teleport-destination egg `16`.
- Verified No Regret examples now include map `3` frame-`1` matches (`quality 4 -> destination egg 4`, `quality 2 -> destination egg 2`, `quality 9 -> destination egg 9`) plus the remaining frame-`0` telepad helpers (`quality 27 -> destination egg 27`, `quality 28 -> destination egg 28`), map `5` (`quality 78 -> destination egg 78`), map `10` (`quality 39 -> destination egg 39`, `quality 49 -> destination egg 49`), map `11` (`quality 19/20/21 -> destination eggs 19/20/21`), and map `15` (`quality 22 -> destination egg 22`).
That evidence is strong enough for link-arrow visualization, but it is intentionally narrower than the family `8` egg rule above:
- the renderer now treats frame `0` and frame `1` `0x01DB` placements as teleporter link sources for arrow rendering when they carry an integer destination id in `quality`
- the link key is the low byte of `quality`
- the destination side still comes from real teleport-destination eggs
- this does not yet claim that every `0x01DB` placement or every non-egg teleporter helper in Crusader uses the same schema
## TELEPAD Hardcoding
The current best executable-side explanation for `0x01DB` behavior comes from the checked Crusader usecode disassembly corpus, not from the generic ScummVM egg model.
- `Usecode class 475 (01DB TELEPAD)` is a real authored telepad class in `crusader_disasm.txt`.
- `TELEPAD::gotHit` reads the pad's `quality` into local `theQual` with `Item::I_getQuality`.
- `quality == 0xFF` is treated as inert and exits without a teleport.
- `quality` in the low range `1..0x1d` uses the actor's current map together with the quality value as the egg id, then spawns `TELEPAD::ordinal20` to perform the move.
That means the simple viewer rule for `0x01DB` is not arbitrary UI inference. It matches a real quality-driven telepad dispatch lane.
The same `TELEPAD::gotHit` body also contains explicit hardcoded remaps and special cases rather than one universal `quality -> same-map egg` rule:
- `0x1e -> map 0x28, egg 0x1e`
- `0x1f -> map 0x45, egg 0x45`
- `0x20 -> map 0x04, egg 0x10`
- `0x21 -> map 0x03, egg 0x11`
- `0x23 -> map 0x29, egg 0x1e`
- `0x64 -> map 0x05, egg 0x64`
- `0x65 -> map 0x05, egg 0x65`
- `0x82 -> map 0x19, egg 0x82`
- `0x83 -> map 0x19, egg 0x83`
- `0x84 -> map 0x19, egg 0x82`
- `0x85 -> map 0x19, egg 0x83`
- `0xd7/0xda/0xdb/0xdc -> map 0x05` with matching egg ids
`TELEPAD::ordinal20` then confirms the actual move mechanism:
- the normal lane calls `MainActor::I_teleportToEgg(...)`
- same-map and cross-map forms both appear
- map `0x28` and map `0x29` have extra hardcoded presentation/cutscene handling around the jump process, including `R01`, `B01`, `wec`, `1d`, and `14a` movie names
So the current safest renderer claim is:
- frame `0` and frame `1` `0x01DB` placements are useful cross-game teleporter-link sources when low-byte `quality` matches a real destination egg
- but the full authored gameplay semantics of `TELEPAD` include a hardcoded quality switch with special map-specific behavior, not just generic destination-egg matching
## Elevator Helpers
The checked usecode corpus also closes one separate same-map elevator lane used by visible elevator car objects rather than by egg-family source triggers.
- `Usecode class 542 (021E ELEVATOR)` reads `Item::I_getQLo` and `Item::I_getQHi` from the elevator object itself.
- For same-map moves, `ELEVATOR::gotHit` dispatches `ELEVATOR::ordinal20` with destination egg ids derived from the low quality byte.
- The verified same-map cases are `QLo 1..0x0f -> egg 1..0x0f` and the explicit special case `QLo 0x10 -> egg 4`.
- `QLo 0x11` and `QLo 0x12` are cross-map special cases in the checked body, so the viewer does not currently draw same-map arrows for those values.
- `ELEVATOR::ordinal20` later confirms that the move itself is still a `MainActor::I_teleportToEgg(...)` hop using the caller-supplied egg id and map.
Scene evidence lines up with that lane in No Remorse map `1`:
- `shape:542` quality `0x0101` sits near destination egg `2`, which matches a source platform that teleports to egg `1`.
- `shape:542` quality `0x0202` sits near destination egg `1`, which matches the return platform that teleports to egg `2`.
- the same authored pattern repeats for the other nearby elevator pairs, including `0x0105 -> egg 5` from the far endpoint and the local return platform `0x0206 -> egg 6` beside destination egg `5`.
So the public renderer now treats same-map `shape:542` frame-`0` placements as elevator-link sources when their checked `QLo` values map to a local destination egg id.
## Current Elevator Gap
- No Regret map `3` destination egg `102` does not sit on the same currently verified `ELEVATOR` (`shape:542`) or `LIFT` (`shape:307`) lanes.
- Current scene exports for that map show no nearby `shape:542` or `shape:307` objects at all.
- A nearby editor/helper record `item:1056:fixed:1201:0:41758:33694:0` carries `mapNum 102`, but there is not yet enough executable-side evidence to promote that into a viewer arrow rule.
- The map renderer therefore leaves egg `102` unresolved for now instead of hardcoding a speculative elevator source pattern.
## Adjacent Editor Objects: `0x04E3` And `0x04B1`
These two shapes came up during egg-browser work because they sit near teleporter/elevator/trap authoring patterns, but the current evidence says neither one should be folded into the egg model.
### `0x04E3` frame `0`: `SKILLBOX`
The recovered Crusader usecode corpus has a direct class label here:
- `Usecode class 1251 (04E3 SKILLBOX)`
- the recovered handler is `SKILLBOX::func0A(sint16)`
That body is difficulty-sensitive rather than egg-like.
- it reads `Game::I_getDifficultyLevel()`
- it branches on the object's `frame`
- for `frame == 2`, it reads `Item::I_getQLo(item)`, temporarily adjusts that low-quality byte by difficulty, dispatches `TRIGGER::ordinal20`, then restores the original `QLo`
- for the other frames, it chooses between trigger lane `0` and trigger lane `1` based on a difficulty threshold derived from `frame + 2`
- when `Item::I_getMapArray(item)` is nonzero, the same lane choice is made but with `0x80` added to the dispatched trigger selector
That gives the current best read of the family:
- `frame 0` = difficulty gate that flips at difficulty level `2`
- `frame 1` = difficulty gate that flips at difficulty level `3`
- `frame 2` = special skill-routing form that rewrites `QLo` per difficulty before dispatch
For the specific target in this pass, `0x04E3` frame `0` is therefore best read as a hidden difficulty gate or skill-route selector, not as a teleporter egg, generic egg id carrier, or NPC spawner.
Scene-cache evidence supports that split.
- across cached maps, `0x04E3` appears mostly as `frame 2`, then `frame 1`, with relatively few `frame 0` records (`64` in cached Remorse scenes and `25` in cached Regret scenes)
- `frame 2` payloads cluster strongly by `QLo`, which matches the recovered `Item::I_getQLo(...)` access
- `frame 0` payloads are much more heterogeneous, which fits a gate/controller role better than a simple numeric spawn id
- many dominant `frame 2` records reuse the same `npcNum/mapNum` pairs such as `208/134`, while the low byte of `quality` still varies meaningfully; that makes the low byte look like the authored control value and the other bytes look more like secondary metadata
What helps in the editor:
- label `0x04E3` as `SKILLBOX` instead of generic `Editor Object`
- show `quality` split into `QLo` and `QHi`
- for `frame 0` and `frame 1`, show a small derived note describing the difficulty switch point
- for `frame 2`, show the resolved difficulty lanes explicitly: `diff1 -> QLo`, `diff2 -> QLo + 1`, `diff3+ -> QLo + 2`
- keep `npcNum` and `mapNum` visible, but treat them as raw helper metadata rather than as DTABLE or egg ids
### `0x04B1` frame `0`: trigger/link controller
`0x04B1` still does not have as clean a recovered class label as `SKILLBOX`, but the current usecode evidence already points in one direction.
- an earlier trigger pass found `TRIGGER.slot_20` iterating nearby `shape=0x04B1` items
- that trigger lane compares `Item.getQLo(item)` against a base link id
- it then branches on `Item.getMapNum(item)` flag bits before dispatching additional trigger logic
This pass also re-confirmed that `0x04B1` is grouped with controller/helper shapes rather than with actor payload objects.
- recovered control code such as `ELEVATOR::gotHit` explicitly scans nearby authored helper shapes and includes `0x04B1` in that control-side whitelist
- that keeps `0x04B1` in the same ecosystem as trigger/elevator helper markers rather than DTABLE-driven spawn objects
The currently recovered `cmd` text is not yet a stable class name. It appears as a local variable name in recovered usecode exports, which fits a command/controller interpretation, but that is still weaker evidence than the `SKILLBOX` label above.
Scene-cache evidence strengthens the controller model.
- cached Remorse scenes overwhelmingly use `0x04B1` as `frame 0`
- cached Regret scenes use `0x04B1` across a much wider frame spread (`0` through `10` in the current cache)
- for Remorse `frame 0`, the most common low-byte values are small channel-like numbers such as `1`, `2`, `3`, `5`, `6`, `10`, `20`, `30`, `31`, `40`, `50`, and `60`
- the full `quality` word often changes while the low byte stays on the same small channel number, which is exactly what the earlier `Item.getQLo(...)` trigger evidence would predict
- the frequently repeated `npcNum/mapNum` pairs like `208/134` do not line up with a useful NPC-row interpretation and should stay raw for now
Current best read:
- `0x04B1` frame `0` is a command/link helper used by trigger-style controller logic
- `quality & 0xFF` is the strongest current candidate for the authored link or command id
- `mapNum` is not a destination map; its low bits are routing flags and its high bits contribute to the decoded target shape
- `npcNum` is not a DTABLE row here; it is the low byte of the decoded target selector used by `TRIGGER.slot_21`
The recovered TRIGGER body now gives a more concrete field split for `0x04B1` itself:
- `QLo` = current link id matched against the upstream trigger chain
- `QHi` = subcommand selector in the low three bits, plus a small argument in the upper bits
- `mapNum & 0x03` = command mode
- `mapNum & 0x04` = route into the item-targeting TRIGGER lanes instead of the NPC-triggering side
- `mapNum & 0x08` = phase gate (`set -> phase 0 / 0x80`, `clear -> phase 1 / 0x81`)
- `mapNum & 0x10` = low-priority/deferred execution bucket
- `((mapNum & 0xE0) * 8) + npcNum` = decoded target search shape or sentinel target code
The executable-side read is now concrete enough to say more than "it probably triggers something": several `QHi` subcommands are visibly specific.
- subcommand `0` is a helper-dispatch lane: it walks nearby `0x0476` helpers with the same `QLo` and forwards the upper-bit argument into `FREE.slot_30` using the helper's packed payload fields
- subcommand `1` is a direct target-mutation lane whose exact effect depends on mode: the recovered paths can write `QHi`, write `QLo`, call `equip`, set `frame`, or run a timed `TRIGGER.slot_22 -> DOOR.slot_21` pulse on matched targets
- subcommand `2` is no longer just a placeholder in the editor readout: in the direct item-targeting body it resolves to a frame-set lane using the upper-bit argument as the frame value
- subcommands `4` and `5` are verified link rewrites, adding to or subtracting from the active `QLo` before the controller continues scanning
- subcommand `6` is a create-and-drop lane: it again resolves payload through nearby `0x0476` helpers, creates the target item, copies `Q`, moves it to the helper coordinates, and unequips/drops it
The strongest new practical result is that some authored `0x04B1` records can now be resolved to concrete nearby target shapes instead of staying purely abstract.
- Remorse map `10` contains `0x04B1` items with decoded target shape `0x04D0`, and several of those have nearby same-`QLo` `0x04D0` helper matches.
- The same map also contains `0x04B1` items resolving to decoded target shapes `0x0476` and `0x04E3`, each with local same-`QLo` matches.
- Example authored records include `mapNum=134, npcNum=208 -> target shape 0x04D0`, `mapNum=135, npcNum=118 -> target shape 0x0476`, and `mapNum=134, npcNum=227 -> target shape 0x04E3`.
That still does not mean every `0x04B1` record is fully solved. Some target codes still decode to shapes that need more map-side correlation, and the NPC-triggering versus item-targeting paths do not use every subcommand in exactly the same way. But the object is now clearly more than a generic relay: it is a compact local trigger program that encodes phase, priority, target domain, target shape, link rewrites, and several concrete operation lanes in the standard item fields.
What helps in the editor:
- relabel `0x04B1` from generic `Editor Object` to something like `Trigger Link Controller` or `Cmd Link`
- show `QLo` and `QHi` separately, with `QLo` emphasized
- decode `mapNum` into phase lane, priority, targeting mode, and high-bit target-shape contribution instead of only showing it raw
- decode `npcNum` as the low byte of the target selector
- show the derived target shape or sentinel family code directly in the tooltip
- list nearby same-`QLo` exact-shape candidates when the decoded target shape can be resolved in the current map
- do not attach DTABLE/NPC preview logic to this family
- keep a same-`QLo` highlight/filter action, because that remains the strongest local linkage field even after the target-shape decode
The practical egg-browser outcome is that both shapes are adjacent to egg workflows, but neither one should currently be decoded as an egg-family object. `0x04E3` looks like a difficulty/skill gate, and `0x04B1` looks like a trigger-link controller.
## More Editor Helpers
The next helper batch follows the same pattern: these are controller-side authored objects that sit near eggs, doors, hazards, or alarm setups, but they should stay in their own helper bucket instead of being collapsed into one generic editor-object schema.
### `0x0361` frame `0`: `EVENT`
This one now has a direct recovered class label too.
- `Usecode class 865 (0361 EVENT)`
- recovered body: `EVENT::equip`
`EVENT::equip` is a large event multiplexer rather than a single-purpose marker.
- it immediately reads `link = Item.getQLo(arg_06)`
- it branches on the incoming `event` number
- several branches dispatch `TRIGGER.slot_20`
- other branches touch camera state, audio, NPC actions, doors, and nearby `NUMBERS` helpers
Current best read:
- `0x0361` is a generic scripted event controller/helper
- `QLo` is the strongest authored linkage field currently visible in the recovered body
- `QHi` is used by at least some counter-style branches, so it is worth surfacing too
- `frame 0` should be treated as one authored placement of that broader event family, not as a special egg/NPC record
What helps in the editor:
- override placeholder-style catalog names with `EVENT`
- emphasize `QLo` and `QHi`
- describe it as a generic event controller, not a visible prop
### `0x0500` frame `0`: `STEAMBOX`
This helper also has a direct class label.
- `Usecode class 1280 (0500 STEAMBOX)`
- recovered body: `STEAMBOX::equip`
The recovered handler has two visible controller lanes.
- for `event == 0`, it scans nearby steam-family shapes and matches them by `Item.getQLo(...)`
- for `event == 1`, it scans nearby `shape=0x03A9` items, again matched by `QLo`
- matching items then dispatch into `STEAMBOX.slot_20` or `STEAMBOX.slot_21`
Current best read:
- `0x0500` is a steam hazard/control relay, not a generic placeholder cube
- `QLo` is the authored channel/link id
- `frame` matters on nearby controlled shapes more than on the controller itself
What helps in the editor:
- label it `STEAMBOX`
- show `QLo/QHi` with `QLo` emphasized as the steam channel
- keep the role text focused on nearby steam/hazard linkage
### `0x0561` frame `0`: `ALARMHAT`
This family was already partially documented in the dedicated alarm note, and the extracted body matches that earlier read.
- `Usecode class 1377 (0561 ALARMHAT)`
- recovered body: `ALARMHAT::equip`
The visible structure is local alarm-state control.
- `frame 0` scans nearby `shape=0x04D0` helpers and targets their `frame 0` state
- nonzero frames gate on `Item.isOnScreen(arg_06)` and nearby actor-family presence before scanning the same `0x04D0` family
Current best read:
- `0x0561` is a local alarm-state driver
- it is meant to flip or arm nearby `0x04D0` controller/spawner objects
- `frame 0` is the simple always-check local alarm form; nonzero frames add extra activation conditions
What helps in the editor:
- label it `ALARMHAT`
- add a frame note calling out `frame 0` as the direct local alarm lane
- keep `npcNum`/`mapNum` raw instead of forcing a DTABLE interpretation onto this helper itself
### `0x0581` frame `0`: `ALRMTRIG`
This one is compact and comparatively clean.
- `Usecode class 1409 (0581 ALRMTRIG)`
- recovered body: `ALRMTRIG::equip`
The whole handler is an alert-state relay.
- it checks `Item.getMapArray(arg_06)`
- it checks `World.getAlertActive()`
- it dispatches `TRIGGER.slot_20` with lane `0`, `1`, `0x80`, or `0x81`
Current best read:
- `0x0581` is an alert/alarm trigger relay
- the important authored split is `mapArray == 0` versus nonzero
- the second split is world alert state inactive versus active
What helps in the editor:
- label it `ALRMTRIG`
- show the `mapNum`/`mapArray` byte in hex because it behaves like a flag lane selector here
- describe the four resulting trigger lanes explicitly
### `0x04F8` frame `0`: door death helper
This shape still lacks a clean No Remorse class label in the recovered tables, but the door-side behavior is already concrete enough to expose in the editor.
- `DOOR.slot_23` iterates nearby `shape=0x04F8` items after the door damage/crush path
- it matches them by `Item.getQLo(deathBox) == Item.getQLo(arg_06)`
- it dispatches `TRIGGER.slot_20` on lane `0` when `Item.getMapArray(deathBox) == 0`
- otherwise it dispatches the `0x80`-offset lane
Current best read:
- `0x04F8` is a door-side death or crush trigger helper
- `QLo` is the local door/link key
- the map-array byte chooses the normal trigger lane versus the `+0x80` variant
That is strong enough to support an editor-facing role hint even though the final class/name provenance is still incomplete.
### `shape_04e3` / `0123only`
The workspace did not yield an exact `0123only` string hit in the current source, catalogs, or extracted usecode artifacts.
Current safest read:
- there is no evidence yet that `0123only` is a separate helper family from `0x04E3`
- the only strong recovered identity for `shape 0x04E3` is still `SKILLBOX`
- treat `0123only` as an external nickname or pending label until a real source hit turns up
So the editor integration should continue to show `SKILLBOX` for `0x04E3` and avoid promoting `0123only` to a canonical name.
### `0x0120`: `FASTSKIL`
This shape is a second skill-family controller, but it is not the same object as `SKILLBOX`.
- `Usecode class 288 (0120 FASTSKIL)`
- recovered handler: `FASTSKIL::enterFastArea()`
The recovered body is short enough to read directly.
- it waits `5` ticks before doing anything
- it only runs the main lane while `Item::I_getMapArray(item) == 0`
- for `frame == 2`, it reads the current `QLo` as a base skill/link id and dispatches `TRIGGER::ordinal20` lane `0` through three difficulty lanes: `diff1 -> QLo`, `diff2 -> QLo + 1`, `diff3+ -> QLo + 2`
- for the non-`frame 2` forms, it does not rewrite the link id; instead it compares difficulty against `frame + 2` and dispatches `TRIGGER::ordinal20` lane `0` below that threshold and lane `1` at or above it
That makes the current best read:
- `0x0120` is a fast-area skill gate/controller, not a generic editor cube
- `frame 0` flips at difficulty `2` (`diff1 -> lane 0`, `diff2+ -> lane 1`)
- `frame 1` flips at difficulty `3` (`diff1/2 -> lane 0`, `diff3+ -> lane 1`)
- `frame 2` is the explicit skill-routing form that remaps the downstream trigger `QLo` by `+0/+1/+2`
For editor arrows, the strongest conservative rule is the same local controller rule already used for other trigger sources, but with the frame-`2` rewrite made explicit.
- `FASTSKIL` can point to nearby `0x04B1` controller helpers when they live in the same local trigger cluster
- for `frame 0` and `frame 1`, the relevant local link key is the source item's current `QLo`
- for `frame 2`, the relevant local link keys are `QLo`, `QLo + 1`, and `QLo + 2`, matching the recovered difficulty lanes
- one concrete Remorse map `13` example is especially strong: a `FASTSKIL` placement and a `0x04B1` controller helper sit at the same coordinates `(35390, 20894)` with matching raw quality `283` / `QLo 27`
This still needs to stay conservative.
- broader scene sweeps do not justify treating every nearby `0x04B1` as a `FASTSKIL` target
- the renderer should therefore stay on the existing local-distance plus explicit `QLo` lane rule instead of inventing a wider object-class relationship
- the tooltip should explain the frame-specific trigger lane or `QLo` rewrite directly so the object reads as a trigger program rather than as another anonymous skill box
## Switch And Helper Follow-Up
This pass tightened several of the still-generic switch/helper shapes that sit next to egg workflows.
### `0x00A1` frame `0`: `PANELNS`
This one now has a direct recovered class label.
- `Usecode class 161 (00A1 PANELNS)`
- recovered handler: `PANELNS::use()`
The body is small and consistent with a plain directional wall panel switch.
- if `frame == 0`, the handler returns immediately
- if `Item::I_getMapArray(item) != 0`, the handler also exits without firing
- otherwise it plays SFX `0xAC` and dispatches `TRIGGER::ordinal20` with lane `0`
Current best read:
- `0x00A1` frame `0` is the idle/inactive visual state of a panel switch family
- the important authored controller key is still downstream `QLo`, because the spawned `TRIGGER` path uses that link namespace rather than `mapNum` or `npcNum`
- sampled Remorse scene caches show nearby same-`QLo` `0x04B1` controller helpers often enough to support editor arrows as a local-assistance layer
### `0x031D` frame `0`: `CARD_NS`
This one also closes cleanly from the recovered usecode corpus.
- `Usecode class 797 (031D CARD_NS)`
- recovered handler: `CARD_NS::use()`
`CARD_NS::use()` is only a thin wrapper. It immediately spawns `SWITCH::ordinal21` on the source item.
`SWITCH::ordinal21` is the more useful body for editor work.
- it reads `Item::I_getQLo(item)` and passes that to `MainActor::I_hasKeycard(...)`
- if the switch is already in `frame 4`, it routes straight into `TRIGGER::ordinal20` lane `0`
- otherwise, with the right card and no alert-state block, it flips to `frame 4`, plays the authorization SFX lane, then dispatches `TRIGGER::ordinal20` lane `0`
- the denied path eventually dispatches `TRIGGER::ordinal20` lane `1`
Current best read:
- `0x031D` frame `0` is a locked north/south keycard switch
- `QLo` is the keycard id and the strongest local link field for controller arrows
- same-map same-`QLo` `0x04B1` matches appear in sampled Remorse scenes, so the renderer can expose them as editor-helper arrows without claiming that the switch talks directly to those helpers in one hardcoded object-only lane
### `0x03AA` frame `0`: `SPANEL`
This shape now has both a direct class label and a second helper-side link path.
- `Usecode class 938 (03AA SPANEL)`
- recovered handler: `SPANEL::use()`
The direct switch body is simple.
- it exits when `mapArray != 0`
- otherwise it conditionally plays SFX `0xAF` for the early-map lane
- it then dispatches `TRIGGER::ordinal20` lane `0`
That closes the same switch-style controller story as `PANELNS`, with `QLo` remaining the practical local link key through the downstream `TRIGGER` chain.
`SPANEL` also appears on the target side of another helper family.
- `BRO_BOOT::enterFastArea()` scans nearby `shape 0x03AA` items
- it compares them by shared `QLo`
- depending on the active mission/global branch, it applies `ITEM::ordinal23` or `ITEM::ordinal24` to those matching `SPANEL` items
Current best read:
- `0x03AA` frame `0` is a switch panel source in the trigger/controller ecosystem
- it is also a helper target for `BRO_BOOT`
- broader exported-scene sweeps now show repeated local same-`QLo` `BRO_BOOT -> SPANEL` matches on Remorse maps `9`, `10`, `11`, `21`, `23`, `160`, and `246`, so the renderer can now expose that helper lane conservatively too
### `0x0366` frame `0`: `NPC_ONLY`
The earlier placeholder label is now closed.
- `Usecode class 870 (0366 NPC_ONLY)`
- recovered handler: `NPC_ONLY::gotHit(uword, word)`
The body is a gated trigger pad rather than a generic editor helper.
- it rejects the hit unless the incoming actor/reference resolves to an NPC-like source
- it reads `Actor::I_GetNPCDataField0x63_00B(actor)`
- it reads `Item::I_getQLo(item)` from the pad and requires those values to match
- with `mapArray == 0` and the occupancy checks satisfied, it flips to `frame 1`, waits briefly, and dispatches `TRIGGER::ordinal20` lane `0`
- once the actor is no longer satisfying the occupancy lane, it restores `frame 0` and dispatches `TRIGGER::ordinal20` lane `1`
Current best read:
- `0x0366` frame `0` is an idle NPC-only trigger pad
- `QLo` is the author-selected NPC-group key, but the executable compares it against actor field `0x63`, not against the scene-export `npcNum` or a simple DTABLE row
- current scene exports do not expose that actor-side field directly, so there is not yet enough evidence to draw trustworthy `NPC_ONLY -> actor` arrows in the renderer
- a follow-up scene-cache sweep also failed to produce a convincing generic `NPC_ONLY -> 0x04B1` helper pattern; shared-`QLo` local matches look incidental rather than like a dedicated authored link lane
That means the safe editor improvement here is naming and field emphasis, not a target-arrow rule yet.
### `0x0403` frame `0`: `FLAMEBOX`
This shape now has a direct class label and a usable local arrow rule.
- `Usecode class 1027 (0403 FLAMEBOX)`
- recovered handler: `FLAMEBOX::equip(sint16)`
The recovered body has two nearby-helper lanes keyed by shared `QLo`.
- event `0` scans nearby flame-family shapes `0x043A`, `0x043B`, `0x050A`, and `0x0518`
- event `1` scans nearby flame editor/helper shapes `0x0438` and `0x0439`
- both lanes compare helper items by `Item::I_getQLo(...) == Item::I_getQLo(flamebox)`
- the event-`1` lane can replace the helper marker with an animated flame actor and then spawns the flame process
Scene-cache checks on Remorse map `1` line up with that lane strongly enough to expose in the editor.
- `FLAMEBOX qlo 24` at `(61310,60830)` has nearby helper `shape 0x0438` at `(61886,60798)`
- `FLAMEBOX qlo 7` at `(60350,49406)` has nearby helper `shape 0x050A` at `(59938,49294)`
- `FLAMEBOX qlo 21` at `(59166,5214)` has nearby `shape 0x0439` matches at `(58814,5790)` and `(59166,5790)`
Current best read:
- `0x0403` frame `0` is a local flame controller
- `QLo` is the authored flame channel id
- editor-helper arrows from `FLAMEBOX` to nearby same-`QLo` flame helper shapes are evidence-backed and now justified
### `0x04E7` frame `0`: `DEATHBOX`
The existing `npc death` nickname was directionally right but incomplete. The recovered class name and caller path now narrow the object down.
- `Usecode class 1255 (04E7 DEATHBOX)`
- recovered helper body: `DEATHBOX::func0A(sint16)`
- recovered caller path: `NPCDEATH::ordinal20` scans nearby `shape 0x04E7` items directly
The strong executable-side lane is in `NPCDEATH::ordinal20`.
- it iterates nearby `DEATHBOX` items
- it compares the incoming death-link value against `Item::I_getQLo(deathBox)`
- if `Item::I_getMapArray(deathBox) == 0`, it dispatches `TRIGGER::ordinal20` lane `0` from the helper item
- otherwise it reads `QHi` and `npcNum` from the helper, can create a keycard with frame `QHi - 1`, can override that keycard's `QLo` from helper `npcNum`, and then dispatches the `0x80` trigger lane
That makes the current best editor-facing read:
- `0x04E7` is an `NPCDEATH` helper/controller, not just a raw death marker
- `QLo` is the death-link match key
- `QHi` carries drop/helper mode information, including the verified keycard lane for values `1..4`
- `npcNum` can act as a secondary payload for the spawned keycard helper path
Current limitation:
- the static scene export does not yet expose the originating NPC death-link values directly enough to draw reliable `DEATHBOX -> source NPC` arrows
- some maps do show local same-`QLo` `0x04B1` helpers near `DEATHBOX`, but the recovered executable path is still clearest as `NPC death event -> DEATHBOX -> TRIGGER`, and a broader follow-up sweep did not justify promoting `DEATHBOX -> 0x04B1` into a generic arrow rule
### `0x04FE` frame `0` / frame `1`: `BRO_BOOT`
This shape is no longer mysterious, even though its local authored targets are not yet consistently visible in sampled scenes.
- recovered class label: `BRO_BOOT`
- recovered handlers include `BRO_BOOT::enterFastArea()` and `BRO_BOOT::leaveFastArea()`
`BRO_BOOT::enterFastArea()` does three important things.
- it branches on a mission/global state lane (`global [001F 01]` in the recovered output)
- it scans nearby `shape 0x03AA` items and compares them by shared `QLo`
- it applies `ITEM::ordinal23` or `ITEM::ordinal24` to those matching `SPANEL` items, then runs its own boot-style frame animation loop
The broader scene cache now supports that link well enough for a renderer overlay.
- follow-up exported-scene sweeps found repeated local same-`QLo` `BRO_BOOT -> SPANEL` matches across Remorse maps `9`, `10`, `11`, `21`, `23`, `160`, and `246`
- one concrete Remorse map `10` example has `SPANEL` item `4538` at `(21018, 13406)` with a nearby `BRO_BOOT` at `(20968, 13784)`, both carrying `QLo 50`
Current best read:
- `0x04FE` is a scripted helper tied to nearby `SPANEL` items, not a free-form generic editor object
- frame `0` and frame `1` are boot-sequence animation states, not independent target ids
- `QLo` is the local authored linkage field for the helper-to-`SPANEL` lane
Current renderer stance:
- this is strong enough to relabel the family as `BRO_BOOT`
- the exported-scene sweep is now broad enough to justify a generic local `BRO_BOOT -> SPANEL` arrow layer based on shared `QLo` plus local distance
### `0x0080` frame `0`: `BOX_EW`
This one closes as another switch-family trigger source.
- `Usecode class 128 (0080 BOX_EW)`
- recovered handler: `BOX_EW::use()`
The recovered body is compact but useful.
- if `mapArray != 0`, it exits without firing
- `frame 0` plays the switch SFX and dispatches `TRIGGER::ordinal20` lane `1`
- nonzero frames dispatch `TRIGGER::ordinal20` lane `0`
The map-side correlation is strong enough to promote a conservative helper-arrow rule, but only for the idle switch state.
- sampled exported scenes show repeated local same-`QLo` `BOX_EW frame 0 -> 0x04B1` matches, including co-located pairs on Remorse map `9`
- the same broader sweep did not justify treating nonzero `BOX_EW` frames as the same generic cmd-link source lane
Current best read:
- `0x0080` is the `BOX_EW` switch family
- `QLo` remains the practical authored controller key for nearby helper scans
- the renderer can safely expose `BOX_EW frame 0 -> nearby 0x04B1` arrows when local distance and `QLo` both match
### `0x04CD` frame `0`: `TRIGPAD`
This family now has a direct class label, but not a new helper-arrow rule.
- `Usecode class 1229 (04CD TRIGPAD)`
- recovered handler: `TRIGPAD::gotHit(...)`
The recovered body behaves like a heavier floor trigger.
- it gates on occupancy and surface checks before the pad arms
- with `mapArray == 0`, it waits briefly, dispatches `TRIGGER::ordinal20` lane `0`, and later dispatches lane `1` as the condition clears
- the same body also loops across nearby elevator-family items and can call `ELEVAT` control slots in that path
Current best read:
- `0x04CD` is a real trigger-pad class, not a generic editor placeholder
- it belongs in the trigger/helper ecosystem, but its executable behavior is broader than a simple `QLo -> cmd-link` switch
- the wider exported-scene sweep did not justify promoting a generic local `TRIGPAD -> 0x04B1` arrow rule, so the safe editor improvement is labeling plus tooltip decoding only
### `0x033A` frame `0`: `NUMBERS`
This family still does not have a cleaner recovered usecode class label than the catalog-facing name, but the scene role is much less ambiguous now.
- exported scenes show `0x033A` as tiny glyph-sized frames, typically `10x16` or `12x16`
- those items repeatedly cluster beside larger sibling shapes such as `0x0501`, `0x0502`, `0x0503`, `0x0505`, and `0x0507`
- the same map-side checks do not make it look like another trigger or cmd-link helper family
Current best read:
- `0x033A` is best treated as a number/readout display helper family
- the renderer should label it clearly so editor users can spot display clusters
- current evidence does not justify helper arrows from `NUMBERS`
## Editor Helper Arrow Overlay
The renderer now exposes a separate nested toggle under `Show editor-only elements` called `Show editor helper arrows`.
That overlay is intentionally narrower than the existing verified teleport/elevator arrow layer. It only draws local helper relationships that have direct usecode support and an evidence-backed authored key:
- `ALARMHAT (0x0561) -> nearby 0x04D0` helpers, using the recovered local alarm scan behavior
- `STEAMBOX (0x0500) -> nearby steam-family targets`, matched by shared `QLo`
- `0x04F8 -> nearby door-family targets`, matched by shared `QLo` and local placement
- `BRO_BOOT (0x04FE) -> nearby SPANEL targets`, matched by shared `QLo`
- `PANELNS (0x00A1)`, `CARD_NS (0x031D)`, and `SPANEL (0x03AA) -> nearby 0x04B1` controller helpers when they share the same local `QLo`
- `BOX_EW (0x0080) frame 0 -> nearby 0x04B1` controller helpers when they share the same local `QLo`
- `FLAMEBOX (0x0403) -> nearby flame helper shapes`, matched by shared `QLo`
- `EVENT (0x0361)` and `SKILLBOX (0x04E3) -> nearby 0x04B1` controller helpers when they share the same local `QLo`
- `FASTSKIL (0x0120) -> nearby 0x04B1` controller helpers on the same local trigger lanes, with `frame 2` also exposing the recovered `QLo + 1` and `QLo + 2` difficulty variants
- `ALRMTRIG (0x0581) -> nearby 0x04B1` controller helpers by the same lane byte, as a weaker relay-style visualization rather than a claimed final object identity
- `0x04B1 -> nearby exact decoded target shapes` when the TRIGGER field split resolves to a concrete target shape and local same-`QLo` candidates exist
Two newly decoded families stay intentionally out of the arrow layer for now.
- `TRIGPAD (0x04CD)` is now named and decoded in tooltips, but the broader scene sweep did not justify a generic `TRIGPAD -> 0x04B1` helper rule
- `NUMBERS (0x033A)` looks like a display/readout marker family rather than a trigger/controller source, so it stays label-only
The important constraint is that these arrows are editor-assistance overlays, not canonical gameplay graphs. They are meant to expose likely local controller lanes without pretending that every helper object participates in one global id namespace.
## Live Ghidra Notes
The live `CRUSADER.EXE` Ghidra database now also has comment-backed notes at `1020:029e` and `1020:02d0` documenting the startup teleporter precedence that matters for map/egg overrides:
- if the X override remains `-1`, startup stays on the teleporter/egg path
- even when X/Y/Z coordinates are present, a nonnegative `-egg` override still wins and routes through `Teleporter_CreateProcessDirect`
That executable-side note is adjacent to the broader teleporter system rather than to `0x01DB` specifically, but it closes one important runtime rule for interpreting teleport-id driven startup behavior.
## Outcome For The Feature
The egg browser added to the renderer now:

View file

@ -62,6 +62,10 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
- That same warp-table lane is now exact across both retail DOS executables too. Byte checks against `CRUSADER.EXE` and `REGRET.EXE` now show matching 17-word `-warp mission` base-map tables (`0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,40`) at `1478:0488` and `1480:075c`, each followed by a `0,0` terminator. The public map renderer now also has a dedicated mission-table extractor and generated JSON cache, so scene metadata no longer has to treat mission/base-map usage as an unknown ownership question.
- That same startup lane is now tighter at the argument level too. Current best parser/control-flow read in `REGRET.EXE` is `-warp <mission> [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 <mission> <x> <y> <z>` 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 <mission> [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 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.
- The command-line lane is tighter around `-u` now too. In live non-Japanese `CRUSADER.EXE`, the parser case at `1048:0a46` copies the following token into `1478:065a`, and the renamed `startup_apply_u_override_if_present` at `1420:0cdf` later consumes that buffer to resolve/load an alternate usecode/EUSECODE source into `1478:6611/6613`, mark `1478:6615`, and rebuild the cumulative slot-base words at `1478:8c7c..8c82`. Current best read is `real retail startup usecode override`, not `JP-only` and not `dead string-table residue`; the paired consequence is that the older CRUSADER-side `-setver` attribution should now be treated as reopened until its exact retail consumer is isolated directly.
- That same `-u` lane is now tighter at the runtime-scope level too. The follow-up note `docs/usecode-startup-override.md` now records that retail `-u` appears to replace the single live usecode root at `1478:6611/6613`, not add a sidecar table: `startup_apply_u_override_if_present` overwrites that root directly, rebuilds the cumulative slot-base words, and later consumers including `Usecode_ItemCallEvent`, `UsecodeProcess_CreateProcess`, `Interpreter_NextUsecodeOp`, and `Item_GetDamaged` all read the same replacement root. Current safest tooling implication is `runtime swap for the existing Crusader usecode VM`, which makes `-u` a potentially important future validation hook for round-tripped/custom usecode archives once the accepted source format is nailed down.
- The same `-u` lane is tighter at the token-shape level now too. Live `1420:0cdf` does not use the copied argv token as an arbitrary final filename; it treats `1478:065a` as the `Filespec_GetFullPath` path component while loading the fixed mutable filename template `eusecode.flx` from `1478:07a0` through `1478:06d6/06d8` and forcing the first byte to `'e'` before both the existence probe and the final load call. Current safest read is therefore `path/root override for standard EUSECODE archive naming`, not `free-form filename override`. The stock bootstrap side is also better scoped: `1478:6611/6613` starts zero in the live NE image and the only currently recovered explicit writer there is the `-u` helper, so the normal non-`-u` seed remains only cross-referenced through the verified raw-side VM bootstrap note rather than fully live-NE-closed.