Crusader_Decomp/docs/raw-000e.md
MaddoScientisto de42fd1ea1 Add Crusader-specific USECODE data and documentation
- Introduced new file `vm_mask_ladder.tsv` containing detailed mappings for Crusader USECODE VM masks and their associated descriptors.
- Added comprehensive documentation in `scummvm-crusader-reference.md` outlining the structure, findings, and implications for reverse-engineering the Crusader engine within ScummVM.
- Created `usecode-roundtrip-ir.md` to document the plan for converting Crusader USECODE bytes into a human-readable format, detailing the container layout, event names, and intrinsic tables.
- Implemented a PowerShell script `temp_usecode_sample.ps1` for extracting and analyzing USECODE data from the Crusader FLX files, providing insights into class and event structures.
2026-03-22 17:26:39 +01:00

27 KiB
Raw Blame History

Raw 000e: Parser & RIFF/Animation Clusters

Content extracted from crusader_decompilation_notes.md. Covers the 000e: segment parser helper cluster and the RIFF/AVI animation streaming subsystem.


Raw 000e Parser Helper Cluster

A small helper cluster in the raw 000e: area implements a fixed-size CRLF record parser/table builder, likely used by startup/config or script-ish text data.

Newly renamed helpers

Address Name
000e:345e record_table_init
000e:34cc record_table_destroy
000e:35c6 record_table_release_buffer
000e:35ef record_table_next_slot
000e:3639 record_table_parse_buffer
000e:3798 record_parser_read_line
000e:38a0 record_parser_seek_next_marker
000e:38f8 record_parser_find_marker
000e:39cc record_parser_dispatch_at_directive

Behavior notes

  • record_table_init clears the table header and zeroes 300 words of inline storage.
  • record_table_parse_buffer walks a CRLF-separated text buffer, captures each line, splits around a marker helper path, and stores parsed entry state into 0x0c-byte records.
  • record_parser_read_line advances to the next CRLF-delimited line, rejects lines that start with @ or with non-identifier punctuation, and terminates the line in-place with 0.
  • record_parser_seek_next_marker updates the parser's current marker cursor at +0x18/+0x1a by calling record_parser_find_marker; returns 1 if another marker was found, 0 at end-of-data.
  • record_parser_find_marker scans forward until an @ marker or end-of-data; optionally consumes the remaining length from the parser state.
  • record_parser_dispatch_at_directive returns 0 unless the current substring begins with @; in the @ case, it advances by 7 bytes and dispatches through a FAR thunk (0000:ffff).

EUSECODE.FLX extraction notes

  • USECODE/EUSECODE.FLX does not look like a loadable code image or plain text script. It is now validated as an indexed binary container.
  • Current table model:
    • entry count at file offset 0x54
    • entry table at 0x80
    • 8-byte records: <u32 data_offset, u32 declared_size>
    • entry_count = 3074
    • table_end = 0x6090, which matches the first non-zero payload offset
    • 403 non-zero entries in the current file
  • tools/extract_eusecode_flx.py now parses the full validated table and emits all 403 non-zero entries under USECODE/EUSECODE_extracted/, including entry_index.tsv, descriptor_index.tsv, descriptor_neighborhoods.tsv, summary.json, per-chunk .bin, and .strings.txt sidecars.
  • The extractor now also carries the conservative owner-loaded class rule directly into machine-readable outputs: class_layout_index.tsv records object_index, class_id, the raw bytes-8..11 field, derived code_base_minus_one, and conservative_event_count, while class_event_index.tsv expands parsed classes into raw 6-byte event rows with slot numbers, ScummVM event-name hints for 0x00..0x1f, unresolved leading words, and raw code-offset dwords.
  • The generated reports now expose lightweight descriptor summaries (primary_label, field_names, field_tags) so the object lane can be searched by field grammar instead of only by raw names.
  • The extracted data now separates into at least two lanes:
    • text-heavy records that fit the 000e: CRLF parser model, such as DATALINK mission/objective text and TEXTFIL1 message banks
    • binary object/behavior descriptors whose sidecars expose object names and field names, such as EVENT, NPCTRIG, CRUZTRIG, TRIGPAD, JELYHACK, JELYH2, SPECIAL, SURCAMNS, and SURCAMEW
  • The descriptor lane also shows a repeatable tagged field trailer rather than raw trailing strings only. Current spot-checks show patterns like 69 xx 00 <name> and 24 xx 02 <name> immediately before field names in NPCTRIG, CRUZTRIG, TRIGPAD, SPECIAL, and SFXTRIG. This is strong evidence that the field names belong to compact per-field metadata records, not accidental string leakage.
  • The strongest currently stable tag readings are:
    • 69:0000 -> referent
    • 69:0A00 -> event on event-capable classes such as EVENT, NPCTRIG, COR_BOOT, REE_BOOT, SFXTRIG, FLAMEBOX, NOSTRIL, VAR_BOOT, and STEAMBOX
    • 24:FE02 / 24:FC02 / 24:FA02 on object-reference-like fields such as item, elev, door, source, dest, monster1, deadGuy, and related referent-style links
    • 24:0A02 -> eventTrigger on SURCAMNS / SURCAMEW
  • The tag report is not a full type system yet, but it is already enough to separate scalar/event slots from pointer-like object links in many descriptor classes.
  • Confirmed descriptor examples from the full index:
    • EVENT: referent,event,item,source,dest,door,counter,counter2,link,time,post1,post2,floor,flicMan
    • NPCTRIG: referent,event,item,item2,typeNpc
    • CRUZTRIG: referent,item,elev
    • TRIGPAD: referent,item,elev
    • JELYHACK: referent
    • JELYH2: referent
    • SURCAMNS / SURCAMEW: referent,textFile,monit,valueBox,passcode,link,code,screen,cameraEgg,trueRef,therma,eventTrigger,foundGun
  • Current immortality-lane status inside EUSECODE:
    • the trigger/object namespace now clearly includes JELYHACK, NPCTRIG, CRUZTRIG, and TRIGPAD
    • JELYHACK / JELYH2 sit in a local extraction neighborhood beside SPECIAL, TRIGPAD, DATALINK, HOFFMAN, REE_BOOT, SURCAMEW, and SFXTRIG, which looks more like a map/object grouping than random table order
    • that neighborhood does not make JELYHACK itself event-bearing, but it does place it immediately beside multiple event-capable or trigger-adjacent classes (REE_BOOT, SFXTRIG, SURCAMEW.eventTrigger)
    • no extracted chunk has yet been tied directly to event 0x410
    • one exact 0x410 collision in compiled code is now explained away: 000e:0953 pushes 0x410 into imported ASYLUM.27 from the animation audio-subframe path immediately after setting the local audio-completion byte at +0xef1. Since ASYLUM.DLL is the ASS_* audio/media library, treat this as a media ordinal/value collision rather than a second gameplay or USECODE event source.
    • the present best reading is that 0x410 is likely carried by data relationships between generic event-capable descriptors (EVENT, NPCTRIG, SFXTRIG, etc.) and map/object references rather than by a plain-text script line
  • The 000e: record parser helpers still matter, but they now appear to cover only the text-oriented subset rather than the entire FLX payload. The strongest concrete caller so far is the raw window at 000e:1b9f..1d49, where record_table_parse_buffer is invoked after setup of fields that match the known animation object layout (+0x117/+0x11b/+0x11f/+0x123, +0xeaf/+0xeb1, +0x10f/+0x111). That makes the currently verified 000e:3639 consumer part of the animation-object lane, not a clean standalone EUSECODE loader.
  • This shifts the current working model: treat record_table_parse_buffer as a text/metadata helper used by at least one animation/resource object, while the EUSECODE binary descriptor lane is more likely consumed by the 000d VM/object interpreter path.
  • That 000d path is now materially less anonymous:
    • the global runtime object at 0x6611 is now named entity_vm_runtime_create / entity_vm_runtime_init_slots / entity_vm_runtime_release_slots / entity_vm_runtime_destroy
    • it owns the 0x80-entry slot table and a retained owner/resource object at +0x1315/+0x1317
    • entity_vm_slot_index_from_entity and entity_vm_context_try_create_masked_for_entity show that gameplay entities are filtered through one owner-side slot-mask table before a context is created
    • entity_vm_context_try_create_masked_for_entity is now better constrained too: after the owner-side mask check succeeds, an immediate-flagged context result clears the caller output word while an object-backed result returns the created object's low word
    • entity_vm_context_create_from_slot_index then seeds one 0x6714 context from entity_vm_slot_load_value_plus_offset, while the large callers at 000d:208b and 000d:21ed continue by reading bytecode-like data from the seeded +0xd6/+0xd8 lane
  • The context lane now also has a separate referent-registry subsystem:
    • entity_vm_set_field_da_to_global writes the current referent id to 0x8c94 from context field +0xda and then enters the still-misaligned 000c:3350 body
    • entity_vm_referent_registry_init / entity_vm_referent_registry_destroy / entity_vm_referent_registry_alloc / entity_vm_referent_registry_release_by_id / entity_vm_referent_registry_free_node show that 0x8c8c/0x8c8e/0x8c90/0x8c94 implement one free-list-backed registry keyed by that current referent id
    • this is the first solid runtime mechanism showing how referent-only descriptors can still drive script state even when the actual event field lives in a separate neighboring descriptor
    • the registry now also has a named chain container layer: entity_vm_referent_chain_copy, entity_vm_referent_chain_append_unique_from, entity_vm_referent_chain_contains_entry, entity_vm_referent_chain_get_entry_data_at, and entity_vm_referent_chain_get_indirect_data show that one referent can own copied/deduplicated payload chains with either inline fixed-size payloads or indirect string-like payloads
  • That chain layer is now less one-sided than before:
    • entity_vm_referent_chain_remove_matching_from (000d:6a9a) removes entries from one chain when they match a second chain, using either inline compare or indirect string compare depending on the chain type byte
    • entity_vm_referent_chain_set_entry_data_at (000d:6cf6) updates the payload of the Nth chain entry in place, freeing old indirect payload storage first when needed
    • entity_vm_opcode_finish (000d:3350) is now identified as the common opcode epilogue that writes 0x8c94 from the current frame result and unwinds the temporary slot-array state before returning the opcode result
  • That makes the emerging human-readable script model less ad hoc. A plausible future IR is now: referent anchor -> payload chain(s) -> event-bearing attachment(s) rather than a flat list of isolated descriptor rows.
  • The opcode side now reinforces that IR too: at least one handler family around 000d:0988 can either append unique payload entries or remove matching ones before returning through the same epilogue, which is a better fit for a graph-editing/object-attachment VM than for a pure linear trigger list.
  • That 000d:0988 family is now classified more tightly at the opcode-id level:
    • opcode 0x19 = append unique indirect/string-like payload entries into the referent chain
    • opcode 0x1a = remove matching indirect/string-like payload entries from the referent chain
    • opcode 0x1b = remove matching inline/fixed-size payload entries from the referent chain
    • the same helper body also implies the missing sibling 0x18 as the inline/fixed-size append-unique form, because only 0x19/0x1a set the indirect compare flag while only 0x1a/0x1b take the removal path
  • The first concrete 000c to 000d bridge inside that lane remains entity_vm_set_value_from_slot_plus_offset at 000c:f95f: it calls entity_vm_slot_load_value_plus_offset, stores its return pair into object fields +0xd6/+0xd8, and sits immediately beside other entity_vm_* helpers in the 000c:f6b8..f9d9 mini-VM cluster. On the 000d side, entity_vm_slot_load_value_plus_offset wraps entity_vm_slot_load_value, but the old PUSH 0x410 suspicion at 000d:5290 is now rejected: that site reaches the seg091 fatal-report helper family at 000a:44fd, not live gameplay dispatch.
  • The two main 000d caller blocks beneath that bridge now have a first stable byte/value reading too:
    • internal block 000d:208b is the simple materialize-or-forward path: it creates one VM context from the caller's stream state, checks the returned object flags, and either writes the returned value pair straight to the caller output slot or forwards the created object's low word through the shared opcode epilogue
    • internal block 000d:21ed is the inline-payload path: it creates the same VM context, prepends the caller-owned blob into the backward-growing context buffer at +0x102, then consumes two bytes from the seeded +0xd6/+0xd8 lane as small shape/count metadata before building an entity_link closure matrix from the following caller-stream words and pushing back the non-0x0400 results
    • that is the first concrete evidence that the +0xd6/+0xd8 lane is not only carrying immediate event/value ids; it also carries compact metadata bytes that parameterize larger inline payloads copied from the caller stream
  • Current JELYHACK implication: because JELYHACK and JELYH2 still expose only referent, the most defensible model is now that they provide map/object identity into the referent-registry lane, while one adjacent event-capable record (REE_BOOT, SURCAMEW.eventTrigger, SFXTRIG.event, or another nearby generic EVENT/NPCTRIG) carries the actual event semantics that can eventually reach 0x410.
  • The immediate runtime-owner writer is now pinned down one step further too. entity_vm_runtime_create (000d:4c99) is the only verified writer of runtime +0x1315/+0x1317, and it does so by calling newly recovered entity_vm_runtime_owner_resource_create (000d:7000). That helper does not simply copy a caller-supplied owner table: it constructs one embedded seg069/070 helper object, queries the needed table size through vtable +0x04, allocates child +0x10/+0x12, then fills the 0x0d-stride per-slot producer records through vtable +0x0c. The paired release path is entity_vm_runtime_owner_resource_destroy (000d:70fd).
  • That narrows the owner/resource classification safely but still stops short of speculative source-format naming. The embedded helper goes through the same seg069/070 object lifecycle used by other file/resource-style helpers (0009:1c00 init, 0009:1800 destroy), so the most defensible current description is still runtime owner/resource helper rather than USECODE file loader or a descriptor-specific name.
  • The first gameplay-side mask families around entity_vm_context_try_create_masked_for_entity are also now explicit from instruction evidence:
    • local wrapper 0004:f033 passes slot mask 0x8000:0007
    • FUN_0004_f05c passes slot mask 0x2000:0015 and is reached from 0004:f2b3 after overlap/proximity checks plus entity byte +0x32 state toggling
    • FUN_0005_27a4 passes slot mask 0x0001:0000 and is reached from the 000c:a09e entity +0x5b bit-0x0004 branch
  • Those masks are enough to prove that the runtime is exposing multiple gameplay-side materialization lanes into the same owner/resource table, but they are not yet enough to tie one lane specifically to the JELYHACK/JELYH2 anchor pair instead of the neighboring event-bearing descriptors (REE_BOOT, SURCAMEW, SFXTRIG, or another local trigger record).
  • The extractor now emits a first graph-oriented view of that claim too: referent_anchor_event_graph.tsv groups referent-bearing rows with nearby event-bearing neighbors, and jelyhack_island_graph.md renders the JELYHACK / JELYH2 island as edges to local descriptors. On the current data, the strongest event-bearing neighbors in that island are REE_BOOT (event), SURCAMEW (eventTrigger), and SFXTRIG (event).
  • The new focused comparison report (jelyhack_descriptor_compare.tsv) makes one more structural point explicit: JELYHACK and JELYH2 have identical first 16 header words and the same lone referent field tag, while differing only in the label string and one small trailing wx[...] literal. That strengthens the reading that they are sibling referent-anchor classes rather than separate event-bearing behavior records.
  • The same comparison also helps separate anchor classes from event-bearing neighbors: REE_BOOT, SURCAMEW, and SFXTRIG all carry materially richer header/state patterns than JELYHACK / JELYH2, which is consistent with them holding actual trigger or attachment semantics beside the anchor-only classes.
  • The 000d:21ed callee chain is now tighter too. The nested call at 0008:7d27 is entity_link, which appends one entity id into another entity's word-list and, unless bit 0x0400 is set, also updates the reciprocal pair-link slots. So the 22bc..2433 opcode block is best understood as building a bidirectional entity-link closure matrix from streamed entity ids, not merely copying opaque words around.
  • Ghidra now carries that interpretation as a conservative disassembly comment at 000d:22bc, but not yet as a symbol rename, because the surrounding 000d:208b/21ed/22bc region is still mis-split into artificial function bodies.
  • The new EVENT-focused reports (event_island_graph.md, event_descriptor_compare.tsv) broaden the descriptor-side picture beyond the JELYHACK anchor case. The strongest second island is the compact local cluster at indices 186..195, where COR_BOOT, EVENT, and NPCTRIG all expose explicit 69:0A00 -> event tags while ROLL_NS, CRUZTRIG, NPC_ONLY, and VMAIL stay on the referent/link/text side.
  • That cluster looks structurally different from JELYHACK in a useful way: EVENT is the large hub payload (0x20AA) carrying source, dest, door, link, time, counter, post1, post2, floor, and flicMan, while COR_BOOT and NPCTRIG are smaller event-bearing satellites and the surrounding records (ROLL_NS, CRUZTRIG, NPC_ONLY, VMAIL) look like attached state/trigger/object descriptors rather than alternate event cores.
  • The first compare pass on that island is already informative. COR_BOOT, EVENT, CRUZTRIG, NPC_ONLY, and VMAIL share the same leading 0x00000000 dword class shape, NPCTRIG moves to a nearby 0x00000001 shape, and ROLL_NS is the obvious outlier with first dword 0x00000002 plus rider/time/cargo fields. So the present best reading is one three-node event-bearing core embedded inside a wider referent-neighbor island, not one flat run of equivalent trigger records.
  • The extractor now also emits a global event-family pass (event_family_index.tsv, event_family_summary.md), which turns the local island findings into a wider descriptor taxonomy. Current validated families are:
    • event-hub: EVENT
    • boot-event-core: AND_BOOT, BRO_BOOT, COR_BOOT, VAR_BOOT, REE_BOOT
    • npc-trigger: NPCTRIG
    • minimal-event-core: SFXTRIG
    • environmental-event: FLAMEBOX, NOSTRIL, STEAMBOX
    • callback-eventtrigger: SURCAMNS, SURCAMEW
  • That split matters because it is the first extractor-backed distinction between active event carriers and callback-only trigger holders. The 69:0A00 -> event classes now look like the active event-bearing core of the descriptor system, while the surveillance classes with 24:0A02 -> eventTrigger are better treated as callback/attachment endpoints rather than peer event hubs.
  • The extractor now emits a stronger script-facing bridge artifact too: runtime_descriptor_family_rankings.md / .tsv rank those descriptor families against the verified runtime lanes instead of only listing neighborhoods. Current best fit is EVENT as the strongest active-event payload lane, _BOOT cores and NPCTRIG as strong satellites, SFXTRIG / environmental classes as moderate active-event fits, JELYHACK / JELYH2 as the dedicated referent-anchor lane, and SURCAM* as structurally distinct callback/attachment holders.
  • That ranking is anchored by the current owner-loader evidence as well as the descriptor grammar: 000d:44df -> 000d:4c99 -> 000d:7000 supplies the slot-backed source, and raw seg070 windows 0009:67b6 / 0009:6916 now show the embedded helper walking object +0x10/+0x18 tables, formatting per-entry paths, and open/read/close-loading files before the 0x0d-stride owner records are materialized.
  • The next focused pass tightened the _BOOT lane too. boot_family_compare.tsv now shows that all five _BOOT event cores (AND_BOOT, BRO_BOOT, COR_BOOT, VAR_BOOT, REE_BOOT) share the same header skeleton and the same compact field shape (referent,event,counter,item). The meaningful differences are payload size and local neighborhood, not descriptor schema.
  • The new boot_frontier_graph.md makes the best early _BOOT frontier explicit: AND_BOOT and BRO_BOOT sit in one compact referent-heavy neighborhood (OFFWORK, GUARD, GDOOR_N, GDOOR_E, BIGCAN, CRUMORPH, GUARDSQ, CARD_NS, CARD_EW, EWALLEW/EWALLNS) and also point directly at each other as adjacent event-bearing siblings. So the present best reading is a reusable boot-event core template instantiated in several different local object islands, not a set of unrelated boot scripts.
  • The environmental hazard lane is now similarly constrained. environmental_family_compare.tsv shows that FLAMEBOX and STEAMBOX are close structural siblings with the same active-event backbone (referent,event,<hazard>,<hazard2>,direction,count) and matching 24:0A02 / 24:FC02 / 24:FE02 object-link pattern, while NOSTRIL is a smaller fire-specific variant that keeps the active event plus dual fire references and count fields but drops the direction/newType side.
  • Their neighborhoods are different enough to matter: environmental_event_graph.md shows FLAMEBOX embedded among vent/door/bridge/copy records, NOSTRIL among flame/pad/desk/blaster/keypad records, and STEAMBOX among bounce/hover/fade/steam/flame box records. So this looks like one hazard-event descriptor family reused across distinct local object islands rather than one single environmental mega-cluster.
  • The callback lane is tighter too. callback_trigger_compare.tsv confirms that SURCAMNS and SURCAMEW are effectively the same callback-trigger template: identical field set (referent,textFile,monit,valueBox,passcode,link,code,screen,cameraEgg,trueRef,therma,eventTrigger,foundGun) and identical tag grammar except for the therma slot offset (24:F102 vs 24:F602). That keeps the eventTrigger split credible as a true callback/attachment lane rather than only a spelling variation on active event carriers.
  • The first runtime-side follow-through on those descriptor gains is now a little tighter too. Instruction search around 000d:ebe3 confirms one fixed sequenced VM/opcode driver body, not just a vague constructor helper: it calls 000d:177c, 000d:1acb, 000d:0988, the internal 000d:22bc link-matrix block, then 000d:1d4a and 000d:2104 in order. The key negative result is just as useful: 000d:ec31 is only the internal CALL 000d:22bc site inside that body, not a standalone function entry.
  • Ghidra now carries that as a conservative disassembly comment at 000d:ebe3. That is still short of a safe rename, but it does promote the lane from “suspected constructor chain” to “verified ordered opcode/handler sequence,” which is the clearest current bridge from the descriptor-side event families back into the 000d VM/object runtime.

Raw 000e RIFF/Animation Cluster

The 000e: segment contains a RIFF/AVI streaming animation subsystem.

Animation object field map

Field offsets relative to the object base pointer:

Offset Field
+0xb0 active/valid flag
+0xb4+0xc2 constructor-initialized flags
+0xd4 alive sentinel (must be -1 for "alive")
+0xe4 paused flag (0 = running)
+0xeaf/+0xeb1 far pointer to current RIFF chunk
+0xedb animation frame stack depth counter (max 9)
+0xee1 frame data from current chunk +4
+0xeef current subframe index
+0x1b3 subframe count
+0xef1 audio completion flag
+0x11b ring buffer write pointer
+0x11f ring buffer read pointer
+0x117 ring buffer base
+0x123 ring buffer end (capacity boundary)
+0x102 resource pointer
+0xde entry index (multiplied by 0x30 to reach per-entry data at +0x1c7)

RIFF format notes

The game uses standard RIFF/IFF:

  • LIST magic: 0x5453494c = "LIST"
  • RIFF magic: 0x46464952 = "RIFF"
  • "movi" FourCC subchunk for animation frames
  • Audio frames tagged "01wb" (0x62773130)
  • Video frames handled through a separate path

Newly renamed functions

Address Name Evidence
000e:2a28 riff_find_chunk_by_type Walks RIFF LIST/RIFF chunk list; compares each node's FourCC at +8 vs param_2; returns pointer to matching chunk or NULL
000e:2104 animation_start Finds "movi" chunk via riff_find_chunk_by_type, inits ring buffer ptrs at +0x11b from +0x117 + duration, calls animation_advance_frame, loops anim_load_audio_frame and a second frame-loader thunk path per subframe
000e:12f4 animation_advance_frame Fixed-point 0x1000 timer arithmetic; checks +0xe4 (paused), advances ring buffer +0x11b/+0x11f/+0x117/+0x123; calls advance thunk
000e:103f animation_tick Guard wrapper: checks param_1+0xd4 != -1, then calls animation_advance_frame(param_1, 0)
000e:06f7 anim_load_audio_frame Checks chunk tag == 0x62773130 ("01wb" = audio stream 1); computes ring buffer free space; copies chunk payload via 0x0000:ffff thunk; increments subframe index at +0xeef; resets at subframe count +0x1b3
000e:053d anim_load_video_frame_wrapper Called once per subframe in animation_start immediately after anim_load_audio_frame; thin wrapper that forwards to 000e:ffb0

Unresolved callee

  • 000e:ffb0 remains unresolved (decompiles garbled due to overlapping instructions at 000f:0085/000f:0086). Current evidence from the animation_start loop suggests this path is the video-side subframe loader paired with anim_load_audio_frame.

Constructor pattern

All three constructor variants (000e:2777, 000e:2860, 000e:2969) follow the same layout:

  1. Call FUN_000e_e935 (allocator — produces garbled 11KB decompile, not renamed)
  2. Set fields +0xb4 through +0xc2 on the result
  3. Call 000d:ebe3 directly (confirmed CALL sites at 000e:283e, 000e:2931, 000e:29e4; multi-step chain initializer: calls 177c, 1acb, 0988, 22bc, 1d4a, 2104 in sequence)
  4. Call assert_alive_sentinel (assertion: checks +0xd4 != -1)
  5. Call func_0x000eec83

The chain at 000d:ebe3 steps through VM opcode handlers (000d:177c, 000d:1acb, 000d:0988) that operate on a bytecode VM object with stack pointer at +0xcc (decremented by 2 per push) and segment base at +0xce.

The constructor-side field setup before that sequencer is now slightly tighter too:

  • variants A and B both set +0xc0 = 1 before the direct 000d:ebe3 call and derive +0xc2 from DS:0x604e
  • variant C instead sets +0xc0 = 0, +0xc2 = 1, and +0x4c = 0x000d before the same sequencer call
  • these direct xrefs make 000d:ebe3 a constructor-side animation sequencer rather than a globally xref-dark dispatcher, but they still do not expose any new wrapper-level opcode number beyond the internal 0x19/0x1a/0x1b family already proven inside 000d:0988

Constructor variant renames

Address Name
000e:223d assert_alive_sentinel
000e:2777 animation_ctor_variant_a
000e:2860 animation_ctor_variant_b
000e:2969 animation_ctor_variant_c