Add a reliable egg browser to the map renderer and clarify what the game means by an egg "ID".
## Short Answer
- Fresh-game startup is hardcoded to map `1`, egg `0x1e`.
- That startup egg is not read from `CRUSADER.CFG` or another external mission map.
- For Crusader teleport eggs, the destination number that gameplay matches against is the low byte of the item `quality` field.
- The broader egg families use different payload fields depending on egg type, so the renderer should not assume that every egg-family item uses the same number source.
## Fresh-Game Default Start Egg
The existing reverse-engineering note in [docs/first-mission-map-selection.md](k:/ghidra/Crusader_Decomp/docs/first-mission-map-selection.md) already establishes the startup path:
- normal new game calls `Teleporter_CreateProcessDirect(1, 0x1e, 1)`
- the controlling call site is in `Game_Start`
- this is a code-selected default, not a config-file mapping
That means the known first-map spawn uses teleport egg id `0x1e` on map `1`.
## ScummVM Crusader Egg Model
The ScummVM Crusader engine in `engines/ultima/ultima8` maps Crusader egg families as follows:
One structural detail matters here: generic trigger eggs and teleport eggs inherit from `Egg`, but `MonsterEgg` is its own `Item` subclass instead of an `Egg` subclass. That helps explain why Crusader monster eggs do not line up perfectly with the generic egg-field conventions.
## Which Field Holds The Number?
There is not one universal answer for every egg-family item.
### Teleport eggs
Teleport eggs are the important case for spawn/start locations.
- Crusader multiplies those range nibbles by `64` world units.
- The runtime trigger check also uses a `+/-48` Z window.
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.
There is an important Crusader-specific wrinkle for renderer work:
- the generic ScummVM `MonsterEgg` model stores monster shape in `quality & 0x7FF` and activity in the low three bits of `mapNum`
- but exported No Remorse `0x024F` frame `0` monster eggs often have `quality = 0` and still carry non-zero `npcNum` values that line up with useful DTABLE rows
- concrete exported examples include Remorse map 69 (`npcNum = 2` and `npcNum = 6`) and Remorse map 2 (`rawNpcNum = 5`)
- No Remorse frame `1``0x024F` entries also exist, but the checked cases were zeroed placeholders with no useful `npcNum`, `mapNum`, or `quality` payload
- exported No Regret `0x024F` currently diverges earlier: its shape definition resolves to `family: 0`, `kind: terrain` rather than `family: 7`, `kind: egg`, so the renderer should not assume that Remorse monster-egg semantics carry over unchanged
So the renderer keeps two interpretations side by side for family `7` eggs:
- the egg-family label remains `monster id = mapNum >> 3`, matching the engine intrinsic surface
- the NPC preview for evidence-backed `0x024F` frame `0` eggs comes from `npcNum`, because that field matches observed spawn identity better than the zero-heavy `quality` field in current Remorse exports
### Glob eggs
Glob eggs expand glob contents using the item `quality` value as the glob reference.
## Renderer Decision
The new map-renderer egg UI treats all egg-family items as eggs, but labels each one with the field that matches its ScummVM family behavior:
- family `8`: teleport id = `quality & 0xFF`
- family `4`: egg id = `mapNum`
- family `7`: monster id = `mapNum >> 3`
- family `3`: glob id = `quality`
That keeps the viewer useful for teleport/start-point work without flattening all egg families into one misleading scheme.
For the NPC preview overlay, the renderer now makes one additional evidence-backed exception:
-`0x024F` frame `0` monster eggs with a non-zero `npcNum` get the same DTABLE-backed blue NPC preview as `0x04D0` spawners
That exception is intentionally narrower than the generic egg browser labels. It reflects the currently verified Remorse data, not a blanket claim that every family `7` egg in every game uses `npcNum` as its authoritative spawn row.
## Current Cross-Game State Of `0x024F`
- No Remorse: exported shape definition resolves to `family: 7`, `kind: egg`, and scene items produce `egg.type = monster-spawn`
- No Regret: exported shape definition currently resolves to `family: 0`, `kind: terrain`, and scene items do not produce egg metadata
That is the strongest current reason to keep the new monster-egg preview support scoped to the evidence-backed Remorse path instead of enabling it globally for both games.
## Weapon Room `250`
The current renderer catalog data already contains egg-oriented notes that include `250` in the known egg-id lists for Crusader egg shapes, which is consistent with the user-observed convention that egg `250` is the weapons room/test room marker.
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.
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`.
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.
- Regret also ships a separate elevator family at `shape:400` (`0x0190`), which the current shape catalog leaves unnamed but the recovered usecode closes as `ELEVATOR` through `ELEVATOR::slot_20`'s local `nearby_items(shape=0x0190, origin=global[0x001e])` scan.
- In Regret `ELEVATOR::gotHit`, the gate is different from the Remorse `shape:542` body: the object must have `QLo >= 100`, and the generic local-elevator lane is `QLo < 0x00c8`.
- That generic Regret lane dispatches `ELEVATOR::slot_20` with the current map and the source object's `QLo` as the destination egg id, while `QHi` still carries the direction/state argument.
- Regret map `3` now has a concrete same-map source for destination egg `102`: `item:664:fixed:400:0:44030:9662:0` with `quality 614` (`QLo 102`, `QHi 2`).
- Current scene exports for that map still show no `shape:542` or `shape:307` objects at all near destination egg `102`, and the nearby `0x04B1` cmd-link helpers instead carry unrelated local keys such as `7`, `9`, `41`, and `60`.
So the old Regret map-`3` egg-`102` gap is now closed: it is a same-map Regret `shape:400` elevator destination, not an unresolved `shape:542`/`shape:307` case and not a `0x04B1` helper lane.
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
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
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.
- 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.
- 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
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 decompressed `.cache` scene sweep does show repeated nearby same-`QLo``0x04B1` cmd helpers in authored Regret maps, so the renderer can now safely expose cautious `NPC_ONLY -> cmd QLo` arrows while still keeping `NPC_ONLY -> actor` arrows out of the overlay
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`
-`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.
- shows the decoded id appropriate to that egg family
- can center the camera on a chosen egg and pin-select it
- can draw zoom-stable id labels over eggs in the viewport
## Remaining Uncertainty
Two things are still worth keeping in mind:
- family `7` Crusader handling is still marked in ScummVM as partly suspicious because those records can also behave container-like
-`250 = weapons room` is a strong workflow convention, but this investigation did not add a new executable-side proof beyond the catalog evidence and known reverse-engineering notes