12 KiB
Map 1 Targeted 0x04D0 Spawner Investigation
Scope
This note closes the specific map-1 question about suspicious 0x04D0 frame-paired monster spawners, especially the cases where nearby frame 0 and frame 1 records point at different DTABLE rows.
Primary evidence used in this pass:
- decompressed cache scene:
k:\ghidra\crusader_map_viewer\map_renderer\.cache\scene-cache\remorse\map-1\9ccaa5dabe08947e\scene.json - extracted pseudocode:
MONSTER.slot_0F enterFastArea,MONSTER.slot_0A equip,ITEM.slot_2D - ScummVM Crusader sources:
glob_egg.cpp,actor.cpp,item.cpp,remorse_intrinsics.h,convert_usecode_crusader.h - live Ghidra instruction windows around
10a0:3889,10a0:0c40, and10a0:3d3f
The exact coordinate examples mentioned at the start of the investigation do not appear in this cache hash, so the conclusions below are tied to the decompressed cache currently used by the renderer rather than to an older export or a different build.
Short Answer
- Map 1 really does contain many authored frame-
0/ frame-10x04D0pairs. - Those pairs are not well explained as duplicate placements or viewer noise.
- The nearby-pair match key is
QLo = quality & 0xff, notnpcNum. - Frame
0is the only state checked byMONSTER.enterFastArea. - In the exact extracted
MONSTER.enterFastAreabody, the automatic lane is taken whenmapNum bit 0x08is clear, not set. - Frame
1is skipped byMONSTER.enterFastAreaand remains the strongest practical preview candidate in the currently confirmed auto-enabled map-1 and map-248 pairs.
Verified Usecode Chain
The strongest current chain is:
MONSTER.slot_0F(enterFastArea) checksframe == 0and conditionally routes intoMONSTER.equip(...).MONSTER.slot_0A(equip) immediately returns unless the current item is stillframe == 0.ITEM.slot_2Dscans nearbyshape 0x04D0items, requires a partner withframe == 1, requires the sameintrinsic_00EA(...)value on both items, and then callsintrinsic_012F(partner_frame1, source_frame0).intrinsic_012Fis the retail create-NPC helper at10a0:3d3f(Actor::I_createActorCru/Item_Intrinsic12F_CreateNPC).
That is direct evidence that the frame pairing is authored behavior, not just a visual/editor convention.
ScummVM plus the matching retail disassembly split the arguments further:
- the second argument (
source_frame0) supplies the DTABLE row throughother->getNpcNum() - the first argument (
partner_frame1) supplies facing fromitem->getNpcNum() & 0x0f - the frame-1 partner also supplies the difficulty gate from
mapNum & 3, the weapon flag frommapNum & 4, the default-activity-1 byte fromquality >> 8, and the low-quality unk byte - the frame-0 source supplies the default-activity-0 byte from
quality >> 8and default-activity-2 frommapNum
That open-source call shape is still useful, but it is no longer strong enough on its own to settle authored spawn identity for every paired 0x04D0 case.
Important Corrections
0x00EA is getQLo, not getNpcNum
An older local parser table still labeled 0x00EA as Item::getNpcNum(void), but stronger sources overrule it:
- ScummVM intrinsic tables map
0x0EAtoItem::I_getQLo - local intrinsic dumps map
10a0:3889/Int0EAtoItem::I_getQLo - live disassembly at
10a0:3889masks to the low byte
So the pair-match test inside ITEM.slot_2D is a low-quality-byte comparison, not an NPC-row comparison.
mapNum bit 0x08 needed one more correction
The exact extracted MONSTER.slot_0F body still reads:
frame == 0is requiredif (!(a & 8)) spawn MONSTER.equip(...)frame == 1skips that hook
So the safe viewer interpretation stays:
- clear bit
0x08= automatic enter-area lane enabled - set bit
0x08= automatic lane blocked / dormant until another path signals it
Representative Map-1 Cache Pairs
These are concrete decompressed-cache examples from the active hash.
Observer / RoamingSusan pair
item:186at50784, 5888, 0: frame0,mapNum = 1,npcNum = 6,quality = 1315, previewObserveritem:187at the same location: frame1,mapNum = 10,npcNum = 11,quality = 1315, previewRoamingSusan
Both records share QLo = 35 even though their npcNum values differ.
Observer / ChemSuitGuy pair
item:635at37310, 23102, 0: frame0,mapNum = 8,npcNum = 6,quality = 1792, previewObserveritem:636at the same location: frame1,mapNum = 7,npcNum = 2,quality = 512, previewChemSuitGuy
Both records share QLo = 0.
Observer / Guard nearby pair
item:637at37830, 22978, 0: frame1,npcNum = 4,quality = 2048, previewGUARDitem:638at37846, 22986, 0: frame0,npcNum = 6,quality = 1280, previewObserver
These are not perfectly colocated, but they are close enough for the viewer's local-pair search and again share QLo = 0 rather than npcNum.
NPC 99 example
item:14406near59864, 35592, 96: frame0,npcNum = 99
This remains a useful outlier because it shows that odd npcNum values can exist directly on 0x04D0 records. Under the current create-NPC model, a frame-0 npcNum = 99 would mean DTABLE row 99 is the spawn-identity source if that record reaches Actor.createActorCru.
Current Best Model
The safest synthesis from cache, usecode, ScummVM, and live retail disassembly is:
npcNumstill matters because the create-NPC path is DTABLE-backed.- But the authored pairing between nearby frame-
0and frame-10x04D0records is not keyed bynpcNum. - The pairing key is the low quality byte.
- Frame
0still owns the verified automaticenterFastAreacontroller lane. - Frame
1is still the required partner state inITEM.slot_2D. - But confirmed auto-enabled pairs now fit better if the frame-
1preview is treated as the practical visible-NPC candidate while frame0stays the controller-side row.
That explains why map 1 can legitimately contain nearby pairs whose two records name different NPC rows. The pair is not necessarily saying "these are two copies of the same actor". It is more consistent with a split-role authored setup where one 0x04D0 record is the frame-0 controller and the other is the frame-1 paired preview/state row.
Practical Read Of The Map-1 Examples
Under the current model:
- the auto-enabled
Observer/RoamingSusanpair is better read asframe 0 controls the lane, but the practical visible-NPC candidate is RoamingSusan - blocked
Observer/ChemSuitGuyandObserver/GUARDpairs still fit a dormant-controller interpretation, so they are not equivalent evidence for the visible actor - the viewer should stop claiming that
frame 0 preview always equals the spawned NPC
So the surprising frame-1 labels are no longer best treated as mere noise or a purely control-side annotation. In the confirmed auto-enabled examples, they are the strongest current practical preview of what actually appears.
Map 248 Follow-Up
Map 248 adds a useful counterexample set because it still contains multiple 0x04D0 records previewing Observer.
Two especially useful stable-ID pairs from the decompressed cache are:
fixed:4161at54656, 56936, 0: frame1, previewObserver, paired withfixed:4162frame0,npcNum = 100,mapNum = 0, sameQLo = 63fixed:3391at59846, 45194, 0: frame1, previewObserver, paired withfixed:3390frame0,npcNum = 134,mapNum = 0, sameQLo = 9
Those frame-0 rows matter because the generated Remorse DTABLE used by the renderer currently exposes only rows 0..34. npcNum = 100 and npcNum = 134 do not resolve to known Remorse DTABLE rows at all, while the paired frame-1 rows resolve cleanly to Observer.
There are also several blocked frame-0 Observer rows in map 248 such as:
fixed:2061frame0, previewObserver,mapNum = 8, paired with frame-1ChemSuitGuyfixed:2067frame0, previewObserver,mapNum = 8, paired with frame-1GUARD
That split is important:
- the blocked frame-0
Observerrows fit the dormant-controller interpretation cleanly - the frame-1
Observerrows paired with auto-enabled frame-0 unknown rows are stronger candidates for the real in-mapObserverspawns the user reported
So map 248 does more than merely weaken a blanket claim that frame 0 preview always equals the spawned NPC. It gives a direct contradiction for the old viewer theory:
- the auto-enabled controller row can carry an unresolved out-of-range
npcNum - the paired frame-1 row can still resolve cleanly to the actually reported in-map NPC
Current safest read is narrower:
- frame pairing is definitely real
QLostill looks like the local match key- blocked frame-0 rows can clearly be dormant helpers
- auto-enabled confirmed pairs currently line up better with the frame-1 preview as the practical visible-NPC cue
- but the exact
which side supplies the final NPC row in all authored casesquestion still needs one more call-site-level close, because the open-source call signature and the practical map evidence are still not perfectly reconciled
Pair-Class Scan
After the initial targeted examples, a broader cache scan over the active Remorse map-1 and map-248 exports was used to test whether those examples were outliers or a repeatable authored pattern.
For nearest opposite-frame 0x04D0 candidates matched by shared QLo within the renderer's local pair distance:
- map 1 produced
55auto-enabled mismatched valid pairs,96auto-enabled unresolved-controller / resolved-pair cases, and21blocked mismatched valid pairs - map 248 produced
14auto-enabled mismatched valid pairs,78auto-enabled unresolved-controller / resolved-pair cases, and8blocked mismatched valid pairs
That matters because it rules out the previous "just a handful of weird examples" escape hatch.
- auto-enabled pairs with different resolved previews are common
- auto-enabled pairs where frame
0does not even resolve to a valid Remorse DTABLE preview but frame1does are also common - blocked mismatched pairs remain common too, but they fit the dormant-controller interpretation much better than the visible-NPC interpretation
So the current viewer behavior is now intentionally asymmetric:
- auto-enabled paired
0x04D0previews use a single blue carrier, favoring the practical frame-1preview side when that side resolves cleanly - blocked paired
0x04D0previews use a single red carrier on the dormant frame-0controller side
That single-carrier rule is what removes the misleading double Observer / RoamingSusan overlays without pretending the underlying create path is fully closed.
Viewer Implications
This pass justifies two concrete renderer behaviors:
- local pair arrows should match nearby opposite-frame
0x04D0items by sharedQLo - the spawner UI should treat clear
mapNum bit 0x08as auto-enter enabled and set bit0x08as dormant / blocked - paired
0x04D0previews should render once per pair rather than once per record, using blue for the active carrier and red for dormant controller previews
It does not yet justify a stronger claim that every nearby same-QLo pair is guaranteed to be the runtime pair actually chosen in every scenario. The current overlay should stay local and conservative.
Remaining Uncertainty
- The static
Actor.createActorCruargument reading and the practical map evidence are not fully reconciled yet. Map 1 and map 248 now strongly suggest the frame-1 preview is the better viewer-facing NPC cue for confirmed auto-enabled pairs, but the final call-site argument ownership still needs a tighter close in retail disassembly or runtime tracing. - The user's original coordinates likely came from a different scene export, cache hash, or build, and that mismatch should be kept explicit instead of silently normalized away.
npcNum = 99and other sparse/outlier rows still deserve a separate cleanup pass once more map-1 pairs are cataloged.