# Egg Identification Investigation ## Goal 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: - family `3` = `SF_GLOBEGG` - family `4` = `SF_UNKEGG` (usecode trigger egg) - family `7` = `SF_MONSTEREGG` - family `8` = `SF_TELEPORTEGG` Relevant files: - [shape_info.h](k:/misc/scummvm/engines/ultima/ultima8/gfx/shape_info.h) - [item_factory.cpp](k:/misc/scummvm/engines/ultima/ultima8/world/item_factory.cpp) - [egg.cpp](k:/misc/scummvm/engines/ultima/ultima8/world/egg.cpp) - [egg.h](k:/misc/scummvm/engines/ultima/ultima8/world/egg.h) - [monster_egg.h](k:/misc/scummvm/engines/ultima/ultima8/world/monster_egg.h) - [monster_egg.cpp](k:/misc/scummvm/engines/ultima/ultima8/world/monster_egg.cpp) - [teleport_egg.h](k:/misc/scummvm/engines/ultima/ultima8/world/teleport_egg.h) - [teleport_egg.cpp](k:/misc/scummvm/engines/ultima/ultima8/world/teleport_egg.cpp) - [current_map.cpp](k:/misc/scummvm/engines/ultima/ultima8/world/current_map.cpp) 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. ScummVM uses: - `TeleportEgg::getTeleportId()` = `(_quality & 0xFF)` - `MainActor::teleport(mapNum, teleport_id)` to move to a destination egg - `CurrentMap::findDestination(id)` to find a teleport egg target with matching low-byte quality It also distinguishes two teleport egg roles: - `frame != 1` = active teleporter trigger - `frame == 1` = destination marker So for renderer purposes, the gameplay-relevant teleport egg id is the low byte of `quality`, not `mapNum`. ### Generic/usecode eggs ScummVM's generic egg intrinsics expose: - `Egg::I_getEggId()` -> `getMapNum()` So family `4` eggs use `mapNum` as their generic egg id in the engine interface. Current Crusader viewer work now closes one additional family-4 detail for the `0x0011` placements used as usecode-trigger eggs: - `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, 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` - `yRange = npcNum & 0x0f` - 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. ### Monster eggs ScummVM's monster egg accessor exposes: - `MonsterEgg::I_getMonId()` -> `getMapNum() >> 3` That is a separate meaning from teleport ids. 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. ## 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. No Regret now closes one additional elevator lane that the earlier Remorse-only rule missed. - 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. ## 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`: destroyable-door 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 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 helper that lets authored doors become destroyable and then forward into trigger logic - `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 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 That means the safe editor improvement here is `NPC_ONLY` naming plus local `cmd QLo` arrows only; the actor-target lane still remains tooltip-only. ### `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 destroyable 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: - lists every egg-family item in the loaded map - 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