45 KiB
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, egg0x1e. - That startup egg is not read from
CRUSADER.CFGor another external mission map. - For Crusader teleport eggs, the destination number that gameplay matches against is the low byte of the item
qualityfield. - 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 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
- item_factory.cpp
- egg.cpp
- egg.h
- monster_egg.h
- monster_egg.cpp
- teleport_egg.h
- teleport_egg.cpp
- 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 eggCurrentMap::findDestination(id)to find a teleport egg target with matching low-byte quality
It also distinguishes two teleport egg roles:
frame != 1= active teleporter triggerframe == 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 & 0xFFis 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
- Remorse:
npcNumdoes not behave like a DTABLE row here.xRange = (npcNum >> 4) & 0x0fyRange = npcNum & 0x0f- Crusader multiplies those range nibbles by
64world units. - The runtime trigger check also uses a
+/-48Z 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:4770isitem:12473:fixed:17:0:47888:53256:96withmapNum = 37,quality = 4, andnpcNum = 64. quality & 0xff = 4resolves the usecode class to0x0904, which matches the extracted RemorseCHANGER::hatchbody.CHANGER::hatchreadseggNum = Egg.getEggId(arg_06)frommapNum, walks nearbyroofcandidates, compares each candidate's low quality byte against that egg id, and destroys the matching roof whenItem.getQLo(roof) == eggNum.- The same local decoded scene contains nearby roof placements (
shape:538, kindroof) whosequality & 0xff = 37, matching the egg id frommapNum.
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
MonsterEggmodel stores monster shape inquality & 0x7FFand activity in the low three bits ofmapNum - but exported No Remorse
0x024Fframe0monster eggs often havequality = 0and still carry non-zeronpcNumvalues that line up with useful DTABLE rows - concrete exported examples include Remorse map 69 (
npcNum = 2andnpcNum = 6) and Remorse map 2 (rawNpcNum = 5) - No Remorse frame
10x024Fentries also exist, but the checked cases were zeroed placeholders with no usefulnpcNum,mapNum, orqualitypayload - exported No Regret
0x024Fcurrently diverges earlier: its shape definition resolves tofamily: 0,kind: terrainrather thanfamily: 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
0x024Fframe0eggs comes fromnpcNum, because that field matches observed spawn identity better than the zero-heavyqualityfield 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:
0x024Fframe0monster eggs with a non-zeronpcNumget the same DTABLE-backed blue NPC preview as0x04D0spawners
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 produceegg.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 family8egg content. - Current renderer scene exports show both frame
1and some frame00x01DBplacements carrying small low-bytequalityvalues that line up with real teleport-destination eggs. - Verified No Remorse examples include map
13, where frame10x01DBitems carryquality & 0xFF = 14and the map also contains teleport-destination egg14, and map3, where one frame10x01DBcarries low-byte16and the map contains teleport-destination egg16. - Verified No Regret examples now include map
3frame-1matches (quality 4 -> destination egg 4,quality 2 -> destination egg 2,quality 9 -> destination egg 9) plus the remaining frame-0telepad helpers (quality 27 -> destination egg 27,quality 28 -> destination egg 28), map5(quality 78 -> destination egg 78), map10(quality 39 -> destination egg 39,quality 49 -> destination egg 49), map11(quality 19/20/21 -> destination eggs 19/20/21), and map15(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
0and frame10x01DBplacements as teleporter link sources for arrow rendering when they carry an integer destination id inquality - 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
0x01DBplacement 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 incrusader_disasm.txt.TELEPAD::gotHitreads the pad'squalityinto localtheQualwithItem::I_getQuality.quality == 0xFFis treated as inert and exits without a teleport.qualityin the low range1..0x1duses the actor's current map together with the quality value as the egg id, then spawnsTELEPAD::ordinal20to 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 0x1e0x1f -> map 0x45, egg 0x450x20 -> map 0x04, egg 0x100x21 -> map 0x03, egg 0x110x23 -> map 0x29, egg 0x1e0x64 -> map 0x05, egg 0x640x65 -> map 0x05, egg 0x650x82 -> map 0x19, egg 0x820x83 -> map 0x19, egg 0x830x84 -> map 0x19, egg 0x820x85 -> map 0x19, egg 0x830xd7/0xda/0xdb/0xdc -> map 0x05with 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
0x28and map0x29have extra hardcoded presentation/cutscene handling around the jump process, includingR01,B01,wec,1d, and14amovie names
So the current safest renderer claim is:
- frame
0and frame10x01DBplacements are useful cross-game teleporter-link sources when low-bytequalitymatches a real destination egg - but the full authored gameplay semantics of
TELEPADinclude 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)readsItem::I_getQLoandItem::I_getQHifrom the elevator object itself.- For same-map moves,
ELEVATOR::gotHitdispatchesELEVATOR::ordinal20with destination egg ids derived from the low quality byte. - The verified same-map cases are
QLo 1..0x0f -> egg 1..0x0fand the explicit special caseQLo 0x10 -> egg 4. QLo 0x11andQLo 0x12are cross-map special cases in the checked body, so the viewer does not currently draw same-map arrows for those values.ELEVATOR::ordinal20later confirms that the move itself is still aMainActor::I_teleportToEgg(...)hop using the caller-supplied egg id and map.
Scene evidence lines up with that lane in No Remorse map 1:
shape:542quality0x0101sits near destination egg2, which matches a source platform that teleports to egg1.shape:542quality0x0202sits near destination egg1, which matches the return platform that teleports to egg2.- the same authored pattern repeats for the other nearby elevator pairs, including
0x0105 -> egg 5from the far endpoint and the local return platform0x0206 -> egg 6beside destination egg5.
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
3destination egg102does not sit on the same currently verifiedELEVATOR(shape:542) orLIFT(shape:307) lanes. - Current scene exports for that map show no nearby
shape:542orshape:307objects at all. - A nearby editor/helper record
item:1056:fixed:1201:0:41758:33694:0carriesmapNum 102, but there is not yet enough executable-side evidence to promote that into a viewer arrow rule. - The map renderer therefore leaves egg
102unresolved 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 readsItem::I_getQLo(item), temporarily adjusts that low-quality byte by difficulty, dispatchesTRIGGER::ordinal20, then restores the originalQLo - for the other frames, it chooses between trigger lane
0and trigger lane1based on a difficulty threshold derived fromframe + 2 - when
Item::I_getMapArray(item)is nonzero, the same lane choice is made but with0x80added to the dispatched trigger selector
That gives the current best read of the family:
frame 0= difficulty gate that flips at difficulty level2frame 1= difficulty gate that flips at difficulty level3frame 2= special skill-routing form that rewritesQLoper 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,
0x04E3appears mostly asframe 2, thenframe 1, with relatively fewframe 0records (64in cached Remorse scenes and25in cached Regret scenes) frame 2payloads cluster strongly byQLo, which matches the recoveredItem::I_getQLo(...)accessframe 0payloads are much more heterogeneous, which fits a gate/controller role better than a simple numeric spawn id- many dominant
frame 2records reuse the samenpcNum/mapNumpairs such as208/134, while the low byte ofqualitystill 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
0x04E3asSKILLBOXinstead of genericEditor Object - show
qualitysplit intoQLoandQHi - for
frame 0andframe 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
npcNumandmapNumvisible, 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_20iterating nearbyshape=0x04B1items - 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::gotHitexplicitly scans nearby authored helper shapes and includes0x04B1in that control-side whitelist - that keeps
0x04B1in 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
0x04B1asframe 0 - cached Regret scenes use
0x04B1across a much wider frame spread (0through10in the current cache) - for Remorse
frame 0, the most common low-byte values are small channel-like numbers such as1,2,3,5,6,10,20,30,31,40,50, and60 - the full
qualityword often changes while the low byte stays on the same small channel number, which is exactly what the earlierItem.getQLo(...)trigger evidence would predict - the frequently repeated
npcNum/mapNumpairs like208/134do not line up with a useful NPC-row interpretation and should stay raw for now
Current best read:
0x04B1frame0is a command/link helper used by trigger-style controller logicquality & 0xFFis the strongest current candidate for the authored link or command idmapNumis not a destination map; its low bits are routing flags and its high bits contribute to the decoded target shapenpcNumis not a DTABLE row here; it is the low byte of the decoded target selector used byTRIGGER.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 chainQHi= subcommand selector in the low three bits, plus a small argument in the upper bitsmapNum & 0x03= command modemapNum & 0x04= route into the item-targeting TRIGGER lanes instead of the NPC-triggering sidemapNum & 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
0is a helper-dispatch lane: it walks nearby0x0476helpers with the sameQLoand forwards the upper-bit argument intoFREE.slot_30using the helper's packed payload fields - subcommand
1is a direct target-mutation lane whose exact effect depends on mode: the recovered paths can writeQHi, writeQLo, callequip, setframe, or run a timedTRIGGER.slot_22 -> DOOR.slot_21pulse on matched targets - subcommand
2is 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
4and5are verified link rewrites, adding to or subtracting from the activeQLobefore the controller continues scanning - subcommand
6is a create-and-drop lane: it again resolves payload through nearby0x0476helpers, creates the target item, copiesQ, 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
10contains0x04B1items with decoded target shape0x04D0, and several of those have nearby same-QLo0x04D0helper matches. - The same map also contains
0x04B1items resolving to decoded target shapes0x0476and0x04E3, each with local same-QLomatches. - Example authored records include
mapNum=134, npcNum=208 -> target shape 0x04D0,mapNum=135, npcNum=118 -> target shape 0x0476, andmapNum=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
0x04B1from genericEditor Objectto something likeTrigger Link ControllerorCmd Link - show
QLoandQHiseparately, withQLoemphasized - decode
mapNuminto phase lane, priority, targeting mode, and high-bit target-shape contribution instead of only showing it raw - decode
npcNumas the low byte of the target selector - show the derived target shape or sentinel family code directly in the tooltip
- list nearby same-
QLoexact-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-
QLohighlight/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
eventnumber - several branches dispatch
TRIGGER.slot_20 - other branches touch camera state, audio, NPC actions, doors, and nearby
NUMBERShelpers
Current best read:
0x0361is a generic scripted event controller/helperQLois the strongest authored linkage field currently visible in the recovered bodyQHiis used by at least some counter-style branches, so it is worth surfacing tooframe 0should 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
QLoandQHi - 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 byItem.getQLo(...) - for
event == 1, it scans nearbyshape=0x03A9items, again matched byQLo - matching items then dispatch into
STEAMBOX.slot_20orSTEAMBOX.slot_21
Current best read:
0x0500is a steam hazard/control relay, not a generic placeholder cubeQLois the authored channel/link idframematters on nearby controlled shapes more than on the controller itself
What helps in the editor:
- label it
STEAMBOX - show
QLo/QHiwithQLoemphasized 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 0scans nearbyshape=0x04D0helpers and targets theirframe 0state- nonzero frames gate on
Item.isOnScreen(arg_06)and nearby actor-family presence before scanning the same0x04D0family
Current best read:
0x0561is a local alarm-state driver- it is meant to flip or arm nearby
0x04D0controller/spawner objects frame 0is 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 0as the direct local alarm lane - keep
npcNum/mapNumraw 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_20with lane0,1,0x80, or0x81
Current best read:
0x0581is an alert/alarm trigger relay- the important authored split is
mapArray == 0versus nonzero - the second split is world alert state inactive versus active
What helps in the editor:
- label it
ALRMTRIG - show the
mapNum/mapArraybyte 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_23iterates nearbyshape=0x04F8items after the door damage path- it matches them by
Item.getQLo(deathBox) == Item.getQLo(arg_06) - it dispatches
TRIGGER.slot_20on lane0whenItem.getMapArray(deathBox) == 0 - otherwise it dispatches the
0x80-offset lane
Current best read:
0x04F8is a door-side helper that lets authored doors become destroyable and then forward into trigger logicQLois the local door/link key- the map-array byte chooses the normal trigger lane versus the
+0x80variant
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
0123onlyis a separate helper family from0x04E3 - the only strong recovered identity for
shape 0x04E3is stillSKILLBOX - treat
0123onlyas 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
5ticks before doing anything - it only runs the main lane while
Item::I_getMapArray(item) == 0 - for
frame == 2, it reads the currentQLoas a base skill/link id and dispatchesTRIGGER::ordinal20lane0through three difficulty lanes:diff1 -> QLo,diff2 -> QLo + 1,diff3+ -> QLo + 2 - for the non-
frame 2forms, it does not rewrite the link id; instead it compares difficulty againstframe + 2and dispatchesTRIGGER::ordinal20lane0below that threshold and lane1at or above it
That makes the current best read:
0x0120is a fast-area skill gate/controller, not a generic editor cubeframe 0flips at difficulty2(diff1 -> lane 0,diff2+ -> lane 1)frame 1flips at difficulty3(diff1/2 -> lane 0,diff3+ -> lane 1)frame 2is the explicit skill-routing form that remaps the downstream triggerQLoby+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.
FASTSKILcan point to nearby0x04B1controller helpers when they live in the same local trigger cluster- for
frame 0andframe 1, the relevant local link key is the source item's currentQLo - for
frame 2, the relevant local link keys areQLo,QLo + 1, andQLo + 2, matching the recovered difficulty lanes - one concrete Remorse map
13example is especially strong: aFASTSKILplacement and a0x04B1controller helper sit at the same coordinates(35390, 20894)with matching raw quality283/QLo 27
This still needs to stay conservative.
- broader scene sweeps do not justify treating every nearby
0x04B1as aFASTSKILtarget - the renderer should therefore stay on the existing local-distance plus explicit
QLolane rule instead of inventing a wider object-class relationship - the tooltip should explain the frame-specific trigger lane or
QLorewrite 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
0xACand dispatchesTRIGGER::ordinal20with lane0
Current best read:
0x00A1frame0is the idle/inactive visual state of a panel switch family- the important authored controller key is still downstream
QLo, because the spawnedTRIGGERpath uses that link namespace rather thanmapNumornpcNum - sampled Remorse scene caches show nearby same-
QLo0x04B1controller 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 toMainActor::I_hasKeycard(...) - if the switch is already in
frame 4, it routes straight intoTRIGGER::ordinal20lane0 - otherwise, with the right card and no alert-state block, it flips to
frame 4, plays the authorization SFX lane, then dispatchesTRIGGER::ordinal20lane0 - the denied path eventually dispatches
TRIGGER::ordinal20lane1
Current best read:
0x031Dframe0is a locked north/south keycard switchQLois the keycard id and the strongest local link field for controller arrows- same-map same-
QLo0x04B1matches 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
0xAFfor the early-map lane - it then dispatches
TRIGGER::ordinal20lane0
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 nearbyshape 0x03AAitems- it compares them by shared
QLo - depending on the active mission/global branch, it applies
ITEM::ordinal23orITEM::ordinal24to those matchingSPANELitems
Current best read:
0x03AAframe0is 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-
QLoBRO_BOOT -> SPANELmatches on Remorse maps9,10,11,21,23,160, and246, 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 == 0and the occupancy checks satisfied, it flips toframe 1, waits briefly, and dispatchesTRIGGER::ordinal20lane0 - once the actor is no longer satisfying the occupancy lane, it restores
frame 0and dispatchesTRIGGER::ordinal20lane1
Current best read:
0x0366frame0is an idle NPC-only trigger padQLois the author-selected NPC-group key, but the executable compares it against actor field0x63, not against the scene-exportnpcNumor 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 -> actorarrows in the renderer - a follow-up scene-cache sweep also failed to produce a convincing generic
NPC_ONLY -> 0x04B1helper pattern; shared-QLolocal 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
0scans nearby flame-family shapes0x043A,0x043B,0x050A, and0x0518 - event
1scans nearby flame editor/helper shapes0x0438and0x0439 - both lanes compare helper items by
Item::I_getQLo(...) == Item::I_getQLo(flamebox) - the event-
1lane 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 24at(61310,60830)has nearby helpershape 0x0438at(61886,60798)FLAMEBOX qlo 7at(60350,49406)has nearby helpershape 0x050Aat(59938,49294)FLAMEBOX qlo 21at(59166,5214)has nearbyshape 0x0439matches at(58814,5790)and(59166,5790)
Current best read:
0x0403frame0is a local flame controllerQLois the authored flame channel id- editor-helper arrows from
FLAMEBOXto nearby same-QLoflame 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::ordinal20scans nearbyshape 0x04E7items directly
The strong executable-side lane is in NPCDEATH::ordinal20.
- it iterates nearby
DEATHBOXitems - it compares the incoming death-link value against
Item::I_getQLo(deathBox) - if
Item::I_getMapArray(deathBox) == 0, it dispatchesTRIGGER::ordinal20lane0from the helper item - otherwise it reads
QHiandnpcNumfrom the helper, can create a keycard with frameQHi - 1, can override that keycard'sQLofrom helpernpcNum, and then dispatches the0x80trigger lane
That makes the current best editor-facing read:
0x04E7is anNPCDEATHhelper/controller, not just a raw death markerQLois the death-link match keyQHicarries drop/helper mode information, including the verified keycard lane for values1..4npcNumcan 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 NPCarrows - some maps do show local same-
QLo0x04B1helpers nearDEATHBOX, but the recovered executable path is still clearest asNPC death event -> DEATHBOX -> TRIGGER, and a broader follow-up sweep did not justify promotingDEATHBOX -> 0x04B1into 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()andBRO_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 0x03AAitems and compares them by sharedQLo - it applies
ITEM::ordinal23orITEM::ordinal24to those matchingSPANELitems, 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-
QLoBRO_BOOT -> SPANELmatches across Remorse maps9,10,11,21,23,160, and246 - one concrete Remorse map
10example hasSPANELitem4538at(21018, 13406)with a nearbyBRO_BOOTat(20968, 13784), both carryingQLo 50
Current best read:
0x04FEis a scripted helper tied to nearbySPANELitems, not a free-form generic editor object- frame
0and frame1are boot-sequence animation states, not independent target ids QLois the local authored linkage field for the helper-to-SPANELlane
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 -> SPANELarrow layer based on sharedQLoplus 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 0plays the switch SFX and dispatchesTRIGGER::ordinal20lane1- nonzero frames dispatch
TRIGGER::ordinal20lane0
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-
QLoBOX_EW frame 0 -> 0x04B1matches, including co-located pairs on Remorse map9 - the same broader sweep did not justify treating nonzero
BOX_EWframes as the same generic cmd-link source lane
Current best read:
0x0080is theBOX_EWswitch familyQLoremains the practical authored controller key for nearby helper scans- the renderer can safely expose
BOX_EW frame 0 -> nearby 0x04B1arrows when local distance andQLoboth 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, dispatchesTRIGGER::ordinal20lane0, and later dispatches lane1as the condition clears - the same body also loops across nearby elevator-family items and can call
ELEVATcontrol slots in that path
Current best read:
0x04CDis 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-linkswitch - the wider exported-scene sweep did not justify promoting a generic local
TRIGPAD -> 0x04B1arrow 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
0x033Aas tiny glyph-sized frames, typically10x16or12x16 - those items repeatedly cluster beside larger sibling shapes such as
0x0501,0x0502,0x0503,0x0505, and0x0507 - the same map-side checks do not make it look like another trigger or cmd-link helper family
Current best read:
0x033Ais 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 0x04D0helpers, using the recovered local alarm scan behaviorSTEAMBOX (0x0500) -> nearby steam-family targets, matched by sharedQLo0x04F8 -> nearby destroyable door-family targets, matched by sharedQLoand local placementBRO_BOOT (0x04FE) -> nearby SPANEL targets, matched by sharedQLoPANELNS (0x00A1),CARD_NS (0x031D), andSPANEL (0x03AA) -> nearby 0x04B1controller helpers when they share the same localQLoBOX_EW (0x0080) frame 0 -> nearby 0x04B1controller helpers when they share the same localQLoFLAMEBOX (0x0403) -> nearby flame helper shapes, matched by sharedQLoEVENT (0x0361)andSKILLBOX (0x04E3) -> nearby 0x04B1controller helpers when they share the same localQLoFASTSKIL (0x0120) -> nearby 0x04B1controller helpers on the same local trigger lanes, withframe 2also exposing the recoveredQLo + 1andQLo + 2difficulty variantsALRMTRIG (0x0581) -> nearby 0x04B1controller helpers by the same lane byte, as a weaker relay-style visualization rather than a claimed final object identity0x04B1 -> nearby exact decoded target shapeswhen the TRIGGER field split resolves to a concrete target shape and local same-QLocandidates 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 genericTRIGPAD -> 0x04B1helper ruleNUMBERS (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
-eggoverride still wins and routes throughTeleporter_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
7Crusader handling is still marked in ScummVM as partly suspicious because those records can also behave container-like 250 = weapons roomis a strong workflow convention, but this investigation did not add a new executable-side proof beyond the catalog evidence and known reverse-engineering notes