- Updated Ghidra instructions to emphasize keeping analysis batches small. - Added new binary files: `db.104.gbf`, `db.105.gbf`, and `db.27.gbf`. - Expanded decompilation notes for `cheat_code_check`, detailing its internal workings and verified cheat actions. - Revised segment coverage ledger to reflect new findings and promote segments from `Foothold` to `Partial`. - Enhanced `plan-mid.md` with updated estimates and focus areas for ongoing analysis.
203 KiB
Crusader: No Remorse - Decompilation Notes
Binary Overview
- Game: Crusader: No Remorse (Origin Systems, 1995)
- Platform: DOS (16-bit protected mode)
- DOS Extender: Phar Lap 286 DOS-Extender (RUN286)
- Executable Format: Bound
MZ -> NEexecutable with Phar Lap DOS-extender code - Entry Point:
10da:7c40
Installed Copy Findings
- No standalone
.EXPfile exists inF:\Apps\Crusader No Remorse. CRUSADER.EXEis the original game binary and contains a valid internalNEheader.- Outer DOS
MZheader points toe_lfanew = 0x36F70. - Internal header at
0x36F70starts withNEand describes 145 segments. - The NE segment table references data from the original file directly, so there is no separate embedded payload that needs to be carved out first.
CNRCEXP.EXEis a modern Win32 helper tool, not part of the original DOS execution path.
Raw Full-EXE Import Mapping
- A separate raw-binary import of the full executable (
crusader-raw.exe) is usable: Ghidra discovers thousands of functions across a single flatramblock. - Direct
file_offset -> flat_addressmapping from the standalone segment extracts is not reliable for porting names into that raw import. - The extracted
segNNN_*.binfiles matchCRUSADER_NE.EXE, but the raw full-EXE import must be mapped by verified byte signatures / known function bodies. - Verified segment bases in the raw full-EXE import:
seg001base =0x6E570(cursor_update_hoverat0006:e5d0, rel0x0060)seg021base =0x87170(entity_count_by_type_aat0008:7377, rel0x0207)
- Porting rule for these verified segments:
raw_full_exe_flat = verified_segment_base + standalone_segment_relative_offset
- Naming note:
seg001andseg021both contain a keyboard handler; in the full program database, the seg001 copy is namedseg001_input_keyboard_handlerto avoid a symbol collision with seg021input_keyboard_handler.
Address Space Layout in the Raw Import
Ghidra segment:offset SSSS:OOOO = flat address SSSS * 0x10000 + OOOO.
| Flat range | Content |
|---|---|
0x00000–0x36F6F |
Phar Lap 286 DOS extender (outer MZ stub code) |
0x36F70 |
NE header (145-segment game image begins here in file) |
0x6E570+ |
NE game segments at their Phar Lap linear load addresses |
Mapping rule (verified for seg001 and seg021):
runtime_flat_base = NE_segment_file_offset + 0x36F70
Example: seg004 at file 0x40A00 → runtime 0x77970 → Ghidra 0007:7970.
Functions at Ghidra 0003:XXXX / 0004:XXXX are Phar Lap extender code (flat < 0x40000 is below any game segment). Functions at 0006:E570+ are game NE segments.
0000:ffff — NE Fixup Placeholder (not a dispatcher)
unresolved_far_thunk_dispatch at 0000:ffff is NOT a runtime function. Every CALLF 0x0000:ffff in the original NE image is a different external or inter-segment call patched by the NE loader at runtime. The body at 0000:ffff is just fixup placeholder data, so decompiling it as a function is meaningless.
Repair status in CRUSADER-RAW.EXE:
- A PyGhidra repair pass now applies the verified NE relocation table directly to the raw-program bytes for literal internal
CALLF 9A ptr16:16sites, then re-disassembles each patched instruction. - Current verified batch results:
8851internal literalCALLFsites patched to their real segment:offset targets.2841far-pointer relocation entries skipped because they were not literalCALLFinstructions (data or other non-call uses).119import callsites annotated asNE IMPORT -> module.symbol.
- Remaining xrefs to
0000:ffffshould now mostly be import callsites or non-literal far-pointer cases rather than unresolved intra-game far calls.
Known call-site classifications (by argument pattern):
PUSH DS; PUSH imm_ordinal; CALLF— Phar Lap extender calling a runtime-imported procedure by ordinalPUSH ptr_seg; PUSH ptr_off; CALLF— inter-NE-segment function call (intra-game far call)- Multiple typed pushes then CALLF — external C runtime / game subsystem call with normal args
Latest Raw Full-EXE Porting Progress
- Newly ported and renamed into
CRUSADER-RAW.EXEfrom verifiedseg001mapping (base 0x6E570):0007:28ce=shot_entity_alloc(seg001 + 0x435e)0007:2a19=shot_entity_free(seg001 + 0x44a9)0007:2bc9=projectile_init_vector(seg001 + 0x4659)0007:3001=entity_fire_weapon(seg001 + 0x4a91)0007:3088=fire_weapon_from_cursor(seg001 + 0x4b18)0007:30e8=projectile_check_hit(seg001 + 0x4b78)0007:319e=projectile_step_update(seg001 + 0x4c2e)0007:3298=projectile_trace_ray(seg001 + 0x4d28)0007:371d=projectile_update_tick(seg001 + 0x51ad)0007:4009=projectile_apply_hit(seg001 + 0x5a99)
- Decompiler comments were added on key raw-import projectile functions to preserve provenance for later passes.
- Quick verification from current raw import:
entity_fire_weaponcurrently decompiles as a thin wrapper that callsprojectile_init_vector.fire_weapon_from_cursorstill decompiles poorly in the raw import, but disassembly shows it begins by pushing cursor sprite/state data from the0x27d6area, consistent with the existing seg001 notes.
Raw seg091 Boundary Recovery (init/context + RNG helpers)
- Conservative PyGhidra boundary repair created the missing seg091 functions in
CRUSADER-RAW.EXE:000a:44fd=seg091_func_00fd, body000a:44fd-000a:454c000a:454d=seg091_func_014d, body000a:454d-000a:45fd000a:48a0=rng_advance_state, body000a:48a0-000a:48e2000a:48ff=rng_next_modulo, body000a:48ff-000a:4912
- Additional adjacent helper identified directly in the raw import:
000a:48e3=rng_set_seed
- Verified current behavior from the raw import:
seg091_func_00fdshares runtime flag0x44a4withruntime_init_or_abort; if the flag is clear it sets it and dispatches through an unresolved far thunk, then falls into a second unresolved thunk path that Ghidra currently marks as non-returning.seg091_func_014dalso shares flag0x44a4; it checks an optional long argument against the global context/cookie at0x45a6, zeroes the pointed byte when the argument is null, then dispatches through an unresolved far thunk. Keep the positional name until caller-side analysis resolves the thunk target and full signature.rng_set_seedwrites the 32-bit RNG seed/state pair at0x4584:0x4586and forces the low word odd.rng_advance_stateupdates the same 32-bit state with a simple multiply/add step.rng_next_moduloadvances the RNG state and returns the result modulo the requested bound, or0when the bound is zero.
- Short decompiler comments were added in Ghidra at all five seg091 entries so the current evidence stays attached to the raw database.
Raw 0x4588 Runtime Callback Lifecycle Batch (direct MCP analysis)
- New conservative runtime-callback lifecycle renames (direct analysis):
000a:4913=runtime_callback_object_init_once000a:4a56=runtime_callback_object_teardown_once0009:b1c3=runtime_callback_object_phase_finalize
- Boundary repair applied with MCP edit-plan API:
- Rebuilt
000a:b988assprite_node_get_or_traversewith full body000a:b988-000a:bab5. - This repair absorbs both callback-state sync callsites at
000a:b9e5and000a:ba66that were previously in a no-function gap.
- Rebuilt
- Verified callback-object behavior from this pass:
runtime_callback_object_init_oncesets one-time guard0x4594, snapshots state words (0x458c/0x4590) viavideo_bios_state_snapshot, installs the object FAR pointer at0x4588, and ensures fallback buffer allocation at0x45a6.runtime_callback_object_teardown_oncesets one-time guard0x4595, clears0x4588, conditionally emits vtable+0x0ccallback when current/previous state differ, then calls vtable+0x04release path.runtime_callback_object_phase_finalizeinvokes vtable+0x08twice and sweeps table entries viaallocator_head_finalize_sweep.- Large caller
FUN_000d_9afdcontains both additional vtable+0x0ccallsites (000d:9d5eand000d:a3b7) and remains the best next target for concrete subsystem naming.
- Short decompiler comments were added at the three renamed lifecycle functions to preserve current evidence.
Raw 0x4588 Follow-up Batch (allocator/video helper clarification)
- New conservative helper renames from direct disassembly/decompile evidence:
0009:a961=allocator_head_finalize_sweep000a:4a1f=video_bios_state_snapshot
- Verified behavior anchors:
allocator_head_finalize_sweepperforms per-head chain compaction/finalize work over allocator table entries used byruntime_callback_object_phase_finalize.video_bios_state_snapshotexecutes BIOS video interrupts (INT 10hwithAX=4F03andAX=1130,BH=3) and returns packed state inDX:AX; callers store/compare this pair around callback emissions.
- Decompiler comments were updated so downstream analysis sees the new helper names directly.
Raw 0x4588 Follow-up Batch 2 (cleanup + mode-state wrapper)
- New conservative structural renames (direct decompile/disassembly evidence):
000a:4972=video_mode_set_and_record_state000d:9afd=entity_cleanup_resources_and_dispatch
- Verified behavior anchors:
video_mode_set_and_record_statestores requested mode/state to0x4590, handles VBE-style mode values (0x101/0x103/0x105) via helper checks, and falls back toINT 10hmode path for other values.entity_cleanup_resources_and_dispatchis a large teardown/finalize path for an entity-like object: it clears flags, frees multiple owned buffers/palette handles, performs conditional callback dispatch through0x4588vtable+0x0c, then destroys object word-list structures.
- Decompiler comments were added at both renamed addresses to preserve this provenance.
Raw 0x4588 Follow-up Batch 3 (cleanup-callee helper classification)
- New conservative helper renames from direct MCP decompile evidence:
0009:7853=palette_buffer_alloc_and_init_2560009:1c3a=file_handle_alloc_init_and_open0009:1d6a=file_handle_open_with_mode0009:8d7b=surface_release_internal0009:8e0a=surface_release_and_maybe_free000d:9231=sprite_redraw_global_if_active
- Verified behavior anchors:
palette_buffer_alloc_and_init_256ensures a caller-provided far buffer exists, allocates/initializes a0x100-entry palette/work block, and fills it from static table data.file_handle_alloc_init_and_openallocates a handle structure on demand, seeds sentinels, then delegates tofile_handle_open_with_mode.file_handle_open_with_modeperforms path/open initialization with optional pre-delete behavior and stores DOS open result metadata into the handle structure.surface_release_and_maybe_freewrapssurface_release_internaland conditionally frees memory when(flags & 1) != 0.sprite_redraw_global_if_activeredraws the global sprite/object pointer at0x4f38only when the global gate byte0x68e5is enabled.
entity_cleanup_resources_and_dispatchnow has direct named callees for file/surface/palette cleanup paths, reducing the remaining ambiguity to callback-object role naming and the000d:7e00event-dispatch constructor path.- Short decompiler comments were added at all six renamed helpers to preserve evidence provenance in-database.
Raw 0x4588 Follow-up Batch 4 (function-object recovery around 000d:7e00)
- Missing function objects recovered from direct disassembly boundaries:
000d:7e00-000d:8077created and renamed toentity_dispatch_entry_init_runtime_state000d:8078-000d:819frenamed toentity_dispatch_entry_release_runtime_state0003:a880-0003:a896created asFUN_0003_a880(arithmetic helper; decompiler currently simplifies it)0003:b8e2-0003:bb39created and renamed tofar_buffer_alloc_with_mode_flags
- Verified behavior anchors:
entity_dispatch_entry_init_runtime_stateis a constructor-side helper that initializes runtime fields (+0x41/+0x42/+0x44), clears and allocates paired work/palette buffers (+0x46/+0x48and+0x4a/+0x4c), applies event/setup calls through seg061 helpers, then finalizes activation.entity_dispatch_entry_release_runtime_stateis the destructor-side pair: it frees the same paired buffers, propagates active-state changes via global0x6828, and destroys embedded word-list members.far_buffer_alloc_with_mode_flagsis a low-level far-buffer utility that allocates/reuses a destination pointer and dispatches mode-dependent copy/fill behavior via an internal flag table.
- This resolves the previous
000d:7e00"missing function object" blocker and improves readability forentity_cleanup_resources_and_dispatchcallback/setup paths.
Raw 0x4588 Follow-up Batch 5 (seg061/064/076 helper stabilization)
- New conservative helper renames:
0009:6ec7=vga_palette_read0008:d3ba=timer_entity_enable_wrapper
- Additional evidence-preserving decompiler comments were added (without speculative renames) on:
0008:eb430008:ebe70008:eac80008:ec23
- Verified behavior anchors:
vga_palette_readmirrorsvga_palette_writeand reads DAC entries through ports0x3c7/0x3c9into a far palette buffer.timer_entity_enable_wrapperis a thin forwarder totimer_entity_enableand is widely used in lifecycle/setup paths.- The seg064 gate helpers (
0008:eb43/0008:ebe7/0008:ec23) control one-shot global flag transitions at0x3b72/0x3b73, then dispatch via unresolved thunk paths; names remain intentionally conservative pending stronger subsystem identity.
- Callback callsite clarification retained:
entity_cleanup_resources_and_dispatchvtable+0x0ccall at000d:9d5epasses object fields+0x12d/+0x12f.- Matching vtable
+0x0ccall at000d:a3b7passes object fields+0x74f/+0x751. - These pairs appear to be state/coordinate-like payloads for the runtime callback object at
0x4588.
Raw 0x4588 Follow-up Batch 6 (constructor lane naming + callback globals)
- New conservative helper renames:
0008:d27e=entity_set_update_period_and_reschedule0009:7905=palette_buffer_alloc_copy_from_source
- Verified behavior anchors:
entity_set_update_period_and_reschedulestores timing/update-period fields (+0x36/+0x38/+0x3a), clears deferred fields (+0x3c/+0x3e), then triggers timer recompute/reschedule helpers.palette_buffer_alloc_copy_from_sourceallocates/replaces destination palette buffer metadata and copies RGB triplets from a source far pointer (entry_count * 3bytes).
- Disassembly annotations added on both callback emit callsites so payload provenance remains attached in-database:
000d:9d5e-> vtable+0x0cpayload from+0x12d/+0x12f000d:a3b7-> vtable+0x0cpayload from+0x74f/+0x751
- Global data labels were promoted for the callback lane (where symbolization applies in decompiler views):
g_active_dispatch_entry_farptrat0x6828- callback-state/object globals at
0x4588/0x458c/0x4590/0x4594/0x4595/0x45a6 - dispatch callback-table pointer at
0x39ca
Raw 0007 Gameplay Helper Batch (entity/tile aux state)
- New conservative gameplay-side helper renames (direct analysis from field writes and call structure):
0007:85f6=entity_sync_tile_aux_state0007:8865=entity_sync_tile_aux_if_linked0007:8709=entity_mark_dirty_and_sync_tile_aux
- Current verified behavior:
entity_sync_tile_aux_statereads entity tile index at+0x4, toggles bit0x04in tile record+0x59based on entity byte+0x54, and copies entity word+0x55into tile record+0x0d.entity_sync_tile_aux_if_linkedonly performs the sync when entity link/pointer+0x50/+0x52is non-null.entity_mark_dirty_and_sync_tile_auxcalls the linked-sync helper, sets entity flag bit0x04at+0x42, then calls through0000:ffffwith args(SS:&tile_index, entity[+0x57])— annotated at0007:8666asentity_tile_type_notify(tile_index_ptr, type_byte).
- New entity field found this pass:
entity[+0x57](byte) = entity type/class byte (passed to tile-type notification; meaning not yet fully established — adjacent to named fields+0x54/+0x55)
Raw 0007 Gameplay Helper Batch (facing/direction)
- New gameplay helper rename (direct analysis):
0007:8bd9=entity_set_facing_direction
- Current verified behavior:
- Updates entity facing byte
+0x38using incoming direction/event code values (notably0x10/0x11/0x12) with parity-aware adjustment. - Uses entity flags at
+0x4dto select increment/decrement behavior for clockwise/counterclockwise facing updates. - Called from the large gameplay update state machine at
0007:5b9ainsideFUN_0007_5b6f.
- Updates entity facing byte
Raw 0007 Gameplay Helper Deep Dive: snap_entity_to_ground
- Function:
0007:2207=snap_entity_to_ground - Caller in gameplay flow:
spawn_entity_checked(0007:22de, call at0007:2366) - Purpose (high confidence): pre-spawn position adjustment for a small allow-list of entity types so they land on valid ground/height context before normal spawn allocation.
Variable replacement pass (applied in Ghidra)
param_1->entity_typelocal_48->snap_entity_type_tablelocal_34->snap_dispatch_seg_tablelocal_20->snap_dispatch_off_tablelocal_c->entity_type_cursorlocal_4->dispatch_index
What the function does structurally
- Copies three 10-entry static tables into stack-local scratch buffers:
- from
0x2910intosnap_dispatch_off_table - from
0x2924intosnap_dispatch_seg_table - from
0x2938intosnap_entity_type_table
- from
- Performs a linear scan across 10 entity IDs in
snap_entity_type_table. - If
entity_typematches an entry, it calls into the unresolved shared FAR thunk target (0000:ffff) with spawn coordinate-derived arguments. - If no table entry matches, it exits without modifying the request.
Why this is "snap to ground" behavior
- The only known caller (
spawn_entity_checked) gates this function behind:- global mode flag
*(char *)0x27fe != 0 - a hardcoded list of exactly 10 entity IDs (
0x31c,0x31f,0x320,0x321,0x322,0x323,0x324,0x325,0x326,0x327)
- global mode flag
- That caller prepares local spawn position values, calls
snap_entity_to_ground, then proceeds to spawn logic. This pattern strongly indicates pre-placement correction rather than generic AI or render logic. - The segment/offset companion tables strongly suggest per-entity handler dispatch metadata (or per-entity parameter blocks) used by the thunked path.
Current limitation in raw import
- The placeholder at
0000:ffffstill exists as a symbol, but the relevant internal literalCALLFsites are no longer the best source of truth: they have been patched in-place to their real NE targets. - For
snap_entity_to_ground, the formerly unresolved call at0007:2261now disassembles toCALLF 0004:e7bd, i.e.world_to_screen_coords. - Remaining
0000:ffffsightings in the raw import are now primarily import calls or non-literal far-pointer cases, not evidence that this gameplay helper still dispatches through a single shared runtime function.
Working pseudocode (behavioral)
void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
copy_10_words(local_off_table, DATA_2910);
copy_10_words(local_seg_table, DATA_2924);
copy_10_words(local_type_table, DATA_2938);
for (dispatch_index = 0; dispatch_index < 10; dispatch_index++) {
if (local_type_table[dispatch_index] == entity_type) {
// Through unresolved FAR thunk in raw import.
// Uses spawn position context to compute a ground-aligned placement.
call_thunk_with_spawn_context(spawn_x, spawn_y, ...);
}
}
}
Architectural Resolution: unresolved_far_thunk_dispatch / 0000:ffff
unresolved_far_thunk_dispatch is NOT a real dispatcher. It is the NE binary fixup placeholder.
- In a Phar Lap 286 NE executable, inter-segment and external far calls are stored in the binary as
CALLF 0x0000:ffff(or similar invalid sentinel values). - The Phar Lap NE loader patches each of these call sites to the real segment:offset at load time using the per-segment relocation records in the NE file.
- In Ghidra's raw import, those fixups are never applied. Every unresolved far call collapses to the same
0000:ffffstub, where the decompiler produces garbled output (it's reading fixup-chain data, not real instructions). - Each
CALLF 0x0000:ffffin the binary is a DIFFERENT call with a DIFFERENT actual target. Identifying the target requires either parsing the NE relocation table or cross-matching with the resolved standalone segment extracts.
Address layout in the raw import (flat_address = SSSS:OOOO where flat = SSSS * 0x10000 + OOOO):
0000:–0003:(flat <0x40000) = Phar Lap 286 DOS extender code (the outer MZ stub portion)0006:E570onwards = NE game segments (seg001+ at their Phar Lap-assigned linear addresses)- Mapping rule verified:
runtime_flat = NE_segment_file_offset + 0x36F70(the NE header offset in the EXE)
Decompiler comment added to 0000:ffff in Ghidra documenting this.
Next RE targets for snap_entity_to_ground
- The repaired call at
0007:2261now lands atworld_to_screen_coords(0004:e7bd), so the next step is to reinterpret the helper with the real callee in view rather than through the old0000:ffffplaceholder model.
Raw 0007 Gameplay Helper Follow-up: AI sweep + checked spawn path
- Additional gameplay-side annotation pass completed directly in
CRUSADER-RAW.EXE.
spawn_entity_checked (0007:22de) refinements
- Function signature was expanded to 7 arguments in Ghidra so stack arguments remain visible in decompile:
entity_type,spawn_flags_a,spawn_flags_b,spawn_flags_c,spawn_x,spawn_y,spawn_layer_arg
- Parameter/local naming pass applied for readability in decompiled output.
- New comments added at key control-flow points:
0007:22f8: allow-list gate for ground-snap mode (0x27fe != 0+ entity IDs0x31c..0x327subset)0007:2366: explicitsnap_entity_to_ground(entity_type, &spawn_x, &spawn_y, &spawn_layer)handoff0007:247e: fallback path that calls coreentity_spawnwith original arguments
- Current caveat:
- Decompiler still aliases the temporary y/layer scratch region imperfectly around the thunked call site, but disassembly confirms the call setup uses local
x/y/layertemporaries ([bp-6],[bp-8],[bp-9]) before spawn.
- Decompiler still aliases the temporary y/layer scratch region imperfectly around the thunked call site, but disassembly confirms the call setup uses local
entity_ai_update_loop (0007:0fb6) structural recovery
- Added disassembly + decompiler comments capturing stable behavior:
- Reads player entity FAR pointer from global
0x2de4. - Copies player world position fields (
+0x40,+0x42) into globals0x27e7/0x27e9(AI focus position cache used by downstream logic). - Iterates entity IDs from
2through255and dispatches per-entity processing through two sequential thunked calls per entity.
- Reads player entity FAR pointer from global
- After the NE far-call repair pass, the first call at
0007:101cnow disassembles and decompiles directly asentity_resolve_slot_ptr(0005:0466) instead ofCALLF 0000:ffff. - The repaired call chain now exposes several concrete helpers used by the sweep:
0005:42c8=entity_projected_bbox_overlaps_viewport— projects the entity slot viaworld_to_screen_coords, subtracts entity height from0x7df5[slot], derives sprite/flag context, and returnsbbox_overlap_testagainst the active viewport rectangle referenced from global0x4014.0005:3cf5=entity_class_has_flag2000— class-word flag test overentity_get_class_word(slot) & 0x2000.0005:ff2d=entity_class_get_flag8— returns bit0x08from entity-class detail byte0x7e1e[type*0x79 + 0x59].0006:1305=entity_class_get_word_02— raw accessor for word+0x02in the0x7e1eclass-detail record.0006:0ca4=entity_class_get_word_0a— raw accessor for word+0x0ain the same class-detail record.0006:11a1=entity_class_clear_flag8_and_dispatch— clears bit0x08in class-detail byte+0x59, then performs follow-up entity/type checks and callback dispatch. Name intentionally stays flag-centric until the downstream side effects are fully mapped.
- New disassembly comments added at both dispatch call sites:
0007:101c:entity_slot_fetch(SS:&entity_id)— first call; resolves entity slot/pointer from loop ID0007:1093:entity_tick_dispatch(SS:&entity_id, g_0x27c8)— second call; per-entity AI tick with global0x27c8mode/context word
- Global
0x27c8is now confirmed as the current targeted/current entity handle:entity_is_type_matchcompares against it directly, and both spawn helpersmap_find_spawn_point/enemy_spawn_at_positionsnapshot it before their thunked core paths.
Raw 0007 Gameplay Logic: animation / range / command globals
is_player_in_range (0007:0f79) — fully recoverable
- Prototype updated in Ghidra:
int is_player_in_range(int entity_x, int entity_y) - Reads player world position from
g_player_entity_farptr(0x2de4, fields+0x40(x) and+0x42(y)`. - Computes unsigned delta from AI focus globals
g_ai_focus_pos_x(0x27e7) /g_ai_focus_pos_y(0x27e9). - Returns 1 if player Y delta == 0 AND player X delta < 0xF0 (240 world units), else 0.
- Only confirmed caller so far:
0007:0bcb(in unanalyzed function region).
entity_animation_frame_update (0007:26e2) — fully decompiled
- Prototype updated:
void entity_animation_frame_update(int *entity_ptr) - Key globals read:
g_anim_tick_counter(0x3a00) — frame timing tick counter.g_anim_tick_overdrive_flag(0x3a02) — if set, forces max-advance (4 steps).g_speed_double_flag(0x27fd) — doubles speed_factor to 2 when set (fast game mode).
- Local variables renamed:
speed_factor(1 or 2) andadvance_steps(0–4, number of frame advances this tick). - Entity struct fields confirmed (relative to
entity_ptrasint*):[0x1b](byte+0x36) = frame_min (backward direction counter)[0x1c](byte+0x38) = frame_max[0x1d](byte+0x3a) = current_frame[0x1e](byte+0x3c) = loop_flag (0 = animation disabled)[0x1f](byte+0x3e) = reverse_direction_flag / double-speed flag+0x3f(word, byte-offset) = completion handle/sentinel (-1= none,0x2802= player entity)+0x00(far ptr) = vtable pointer
- New disassembly comments added at all three
CALLF 0x0000:ffffsites and the vtable indirect call:0007:27dc:entity_completion_callback(handle)— fires when loop wraps; skips player handle0007:27fd: vtable indirectentity->vtable[+8](entity, 0, 0)—on_loop_completevirtual method0007:281e:notify_frame_progress(handle, current_frame)— per-frame notification0007:2851:entity_sprite_advance(entity_far_ptr, advance_amount, 0)— core frame-advance call; advance_amount =entity[+0x3c] * (steps+1) * speed_factor
entity_command_dispatch (0007:0990) — partially decompiled
- Prototype:
void entity_command_dispatch(int entity_handle, int target_seg, int command_type, byte absolute_pos_flag) - When
absolute_pos_flag == 0: computes player-relative delta usingg_player_entity_farptrand stores result:- delta_x →
g_player_delta_x(0x27f5) - delta_y →
g_player_delta_y(0x27f7) - Clears cached origin globals
g_cmd_effect_origin_x(0x27f1) andg_cmd_effect_origin_y(0x27f3) after use.
- delta_x →
- Dispatches entity command through shared thunk; actual command table data not yet resolved.
- No incoming XREFs found in the raw import (likely called via table or vtable dispatch).
Enemy spawn helper cluster (0007:505d, 0007:5259, 0007:5275, 0007:5291)
- Existing raw names align with prior standalone seg001 notes:
0007:505d=map_find_spawn_point(seg001 + 0x6aed)0007:5259=enemy_spawn_with_target(seg001 + 0x6ce9)0007:5275=enemy_spawn_no_target(seg001 + 0x6d05)0007:5291=enemy_spawn_at_position(seg001 + 0x6d21)
- Current verified raw-import behavior:
enemy_spawn_with_targetis a thin wrapper overenemy_spawn_at_position(..., target_player_flag = 1).enemy_spawn_no_targetis the same wrapper but passestarget_player_flag = 0.map_find_spawn_pointandenemy_spawn_at_positionboth copy DS:0x27c8into locals before entering their unresolved thunk body, matching the standalone notes that treat0x27c8as the current targeted/current entity handle.
- Short decompiler comments were added in Ghidra on the raw spawn helpers to preserve this provenance.
Global map additions (renamed in Ghidra)
| Address | Name | Evidence |
|---|---|---|
0x27c8 |
g_current_entity_handle |
Compared directly by entity_is_type_match; also captured by entity_ai_update_loop, map_find_spawn_point, and enemy_spawn_at_position as the current targeted/current entity handle |
0x2de4 |
g_player_entity_farptr |
FAR ptr to player entity; +0x40/+0x42 are world X/Y |
0x27e7 |
g_ai_focus_pos_x |
Set by entity_ai_update_loop from player entity +0x40 |
0x27e9 |
g_ai_focus_pos_y |
Set by entity_ai_update_loop from player entity +0x42 |
0x27f1 |
g_cmd_effect_origin_x |
Cached effect origin X, cleared after delta in entity_command_dispatch |
0x27f3 |
g_cmd_effect_origin_y |
Cached effect origin Y |
0x27f5 |
g_player_delta_x |
Player X delta from last effect origin |
0x27f7 |
g_player_delta_y |
Player Y delta from last effect origin |
0x27fd |
g_speed_double_flag |
0 = normal, 1 = double speed animation |
0x27fe |
g_ground_snap_mode_flag |
Non-zero = ground-snap prepass active for placements |
0x27d0 |
g_entity_update_max_id |
Max entity ID used by entity_ai_update_loop sweep |
0x3a00 |
g_anim_tick_counter |
Animation tick counter for frame-advance step budget |
0x3a02 |
g_anim_tick_overdrive_flag |
0 = normal, non-zero = force max frame advance step |
0x2802 |
g_player_entity_handle |
Player entity handle (used as sentinel in animation completion checks) |
Raw 000e Parser Helper Cluster
- A small helper cluster in the raw
000e:area now appears to implement a fixed-size CRLF record parser/table builder, likely used by startup/config or script-ish text data. - Newly renamed helpers:
000e:345e=record_table_init000e:34cc=record_table_destroy000e:35c6=record_table_release_buffer000e:35ef=record_table_next_slot000e:3639=record_table_parse_buffer000e:3798=record_parser_read_line000e:38a0=record_parser_seek_next_marker000e:38f8=record_parser_find_marker000e:39cc=record_parser_dispatch_at_directive
- Current behavior read from raw-import decompilation/disassembly:
record_table_initclears the table header and zeroes 300 words of inline storage.record_table_parse_bufferwalks 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_lineadvances to the next CRLF-delimited line, rejects lines that start with@or with non-identifier punctuation, and terminates the line in-place with0.record_parser_seek_next_markerupdates the parser's current marker cursor at+0x18/+0x1aby callingrecord_parser_find_marker; returns 1 if another marker was found, 0 at end-of-data.record_parser_find_markerscans forward until an@marker or end-of-data; optionally consumes the remaining length from the parser state.record_parser_dispatch_at_directivereturns0unless the current substring begins with@; in the@case, it advances by 7 bytes and dispatches through a FAR thunk (0000:ffff).
Raw 000e RIFF/Animation Cluster
The 000e: segment contains a RIFF/AVI streaming animation subsystem. Animation objects have a confirmed field layout (offsets relative to the object base pointer).
Animation object field map:
+0xb0= active/valid flag+0xb4,+0xb6,+0xb8,+0xba,+0xbc,+0xbe,+0xc0,+0xc2= constructor-initialized flags+0xd4= alive sentinel (must be-1for "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= some entry index (multiplied by0x30to reach per-entry data at+0x1c7)
RIFF format notes: Game uses standard RIFF/IFF: LIST and RIFF header magic (0x5453494c = "LIST", 0x46464952 = "RIFF"), "movi" FourCC subchunk for frames. Audio frames tagged "01wb" (0x62773130), video frames in 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:ffb0remains unresolved (decompiles garbled due to overlapping instructions at000f:0085/000f:0086). Current evidence fromanimation_startloop suggests this path is the video-side subframe loader paired withanim_load_audio_frame.
Constructor pattern (000e:2777, 000e:2860, 000e:2969):
All three follow the same layout:
- Call
FUN_000e_e935(allocator — produces garbled 11KB decompile, not renamed) - Set fields
+0xb4through+0xc2on the result - Call
000d:ebe3(multi-step chain initializer: calls177c,1acb,0988,22bc,1d4a,2104in sequence) - Call
assert_alive_sentinel(assertion: checks+0xd4 != -1) - 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.
Constructor variant renames (direct analysis):
000e:223d=assert_alive_sentinel000e:2777=animation_ctor_variant_a000e:2860=animation_ctor_variant_b000e:2969=animation_ctor_variant_c
Segment Map
| Segment | Address Range | Purpose |
|---|---|---|
| CODE_0 | 1000:0000 - 1000:01ff |
Interrupt dispatch table / thunks |
| CODE_1 | 1020:0000 - 1020:0b9f |
Low-level interrupt handlers, mode switching |
| CODE_2 | 10da:0000 - 10da:25ef |
Main runtime — C library, I/O, formatting, entry point |
| CODE_3 | 1339:0000 - 1339:0c2f |
DOS/DPMI services — INT 21h/31h wrappers, interrupt vector mgmt, fast memcpy |
| CODE_4 | 13fc:0000 - 13fc:27af |
String data & runtime constants — error messages, format strings, Phar Lap ID |
| CODE_5 | 1677:0000 - 1677:0e8f |
EMS/XMS memory management — expanded memory handlers |
| CODE_6 | 1760:0000 - 1760:7ccd |
DOS Extender core — EXP loader, command-line parser, memory management, system init |
| DATA | 1760:7cd0 - 1760:7cdf |
Global data |
| HEADER | HEADER::0000 - HEADER::044f |
MZ/P2 file header |
Named Functions
Entry & Startup
| Address | Name | Description |
|---|---|---|
10da:7c40 |
entry |
Program entry point — checks CPU, parses command line, launches game |
10da:1816 |
main_init_and_run |
Main initialization — loads child EXP, sets up subsystems, runs game |
1760:1432 |
parse_cmdline_and_run |
Parses command-line args and invokes main_init_and_run |
1760:42fa |
init_dos_extender |
Initializes Phar Lap 286 DOS extender (CPU check, VCPI/DPMI setup) |
Executable Loading
| Address | Name | Description |
|---|---|---|
1760:2cdf |
load_exp_file |
Loads .EXP executable — opens file, reads headers, allocates memory |
1760:1dfc |
load_executable_image |
Parses P2/MZ headers, loads segments, creates LDT entries |
1760:24a6 |
apply_relocations |
Applies segment relocations to loaded executable |
1760:5eca |
exec_child_process |
Executes child process with command-line arguments |
1760:5fee |
exec_program_with_args |
Builds command line, locates and executes a program |
10da:1f7e |
load_and_run_child |
Wrapper: loads child EXP and initializes it |
System Services
| Address | Name | Description |
|---|---|---|
10da:2330 |
dos_exit |
Calls INT 21h AH=4Ch (terminate program) |
1760:42aa |
detect_cpu_type |
Detects CPU: 0=8086, 2=286, 3=386+ |
1339:04a6 |
dpmi_set_interrupt_vector |
INT 31h — DPMI set interrupt vector |
1339:06ca |
switch_to_real_mode |
Switches CPU from protected to real mode |
1339:06f2 |
switch_to_protected_mode |
Switches CPU from real to protected mode |
1339:0076 |
setup_interrupt_handlers |
Configures interrupt vectors via INT 21h |
1339:0a38 |
dos_int21h_wrapper |
Simple INT 21h call wrapper |
1339:0a82 |
dos_int21h_with_regs |
INT 21h call with register parameters |
10da:2360 |
get_flags_register |
Returns CPU FLAGS register |
10da:2363 |
set_flags_register |
Sets CPU FLAGS register |
Memory Management
| Address | Name | Description |
|---|---|---|
1677:0d12 |
cleanup_ems_memory |
Frees EMS (INT 67h) memory handles |
10da:14fc |
init_stack_fill_cc |
Fills stack with 0xCC (INT 3) for debugging/guard |
10da:1706 |
get_segment_base_addr |
Computes linear base address from segment descriptor |
Task Management
| Address | Name | Description |
|---|---|---|
10da:19ca |
task_switch_to_child |
Context switch to child process |
10da:1946 |
task_switch_from_child |
Context switch back from child process |
10da:1af4 |
call_termination_handler |
Calls registered termination callback |
I/O & Output
| Address | Name | Description |
|---|---|---|
10da:00d6 |
flush_output_buffer |
Flushes buffered output via function pointer |
10da:0132 |
putchar_buffered |
Writes character to buffer, flushes on newline |
10da:0808 |
memcopy_to_buffer |
Copies N bytes from source to destination buffer |
10da:178c |
print_error_message |
Formats and prints load error (references "not loaded: %s") |
10da:09e4 |
print_fatal_error |
Prints "Fatal Error" prefix + message |
10da:192a |
print_internal_error |
Prints "Internal Error" message |
Interrupt Management
| Address | Name | Description |
|---|---|---|
10da:1ec0 |
restore_interrupt_vectors |
Restores INT 2Fh and INT 67h vectors |
10da:2249 |
restore_int_2f_67 |
Restores INT 15h vector if saved |
1760:3d86 |
init_system_check |
Validates system (CPU, DOS version, VCPI/DPMI, memory) |
Utility
| Address | Name | Description |
|---|---|---|
10da:15ea |
check_ds_segment |
Returns true if DS == 0x10 (checks data segment selector) |
1760:3c9e |
nop_stub |
Always returns 0 (unused hook) |
Key String References
| Address | String | Context |
|---|---|---|
13fc:0016 |
$Id: comhighc.c 1.1 91/08/06... |
Phar Lap C runtime source ID |
13fc:0048 |
$Id: comutils.c 1.1 91/08/06... |
Phar Lap utility functions source ID |
13fc:0078 |
Serial Number |
DOS extender serial validation |
13fc:14ca |
Internal Error |
Error class prefix |
13fc:14da |
Fatal Error |
Fatal error class prefix |
13fc:156a-1628 |
File error messages | Not found, bad format, no memory, etc. |
1760:665c |
Copyright (C) 1986-93 Phar Lap Software, Inc. |
DOS extender copyright |
1760:73da |
-LDTSIZE 4096 -EXTHIGH D0_0000h -NI 18 -ISTKSIZE 3 |
Default extender config |
1760:76fc-7c5a |
Numbered error messages | System requirement errors (1000-2170) |
Architecture Notes
Correction: The Game Ships As A Bound NE Executable
Important: The installed copy does not contain a separate .EXP file. CRUSADER.EXE is a bound executable with an outer DOS MZ stub and an internal NE executable image. The Phar Lap loader/runtime code and the game's real segment layout are both described inside this same file.
The flow is:
entry→ checks DOS version, CPU typeinit_dos_extender→ sets up protected mode (VCPI/DPMI)load_exp_file→ opens the game's.EXPfileload_executable_image→ parses P2/MZ headers, creates segments, applies relocationstask_switch_to_child→ transfers control to the actual game code
For the installed retail copy, this means the currently loaded Ghidra program is only one interpretation of CRUSADER.EXE. The next import should target the NE layer of the same file, not a missing external .EXP.
NE Import Details
- File to import:
F:\Apps\Crusader No Remorse\CRUSADER.EXE - Outer DOS header:
MZ e_lfanew:0x36F70- Internal executable header:
NE - Segment count:
145 - Initial
CS:IP:0001:0000 - Initial
SS:SP:0091:2000
The currently analyzed protected-mode code at addresses like 10da:7c40 is consistent with the Phar Lap runtime/loader path. To reach the rest of the program, import CRUSADER.EXE again using an NE-aware loader or a workflow that starts from the internal NE header rather than the outer DOS stub.
Segment 1339: Fast Memory Operations
FUN_1339_02a8 contains an unrolled loop (Duff's device pattern with 57 iterations) — a hand-optimized fast memory fill/add routine, typical in DOS game graphics engines.
EMS Memory (Segment 1677)
The game uses EMS (Expanded Memory Specification) via INT 67h for additional memory beyond the 1MB real-mode limit. Functions in segment 1677 manage EMS page frames and handle allocation/deallocation.
NE Segment 1 Analysis — Game Logic Functions (seg001_code_off_37600_len_8400.bin)
This segment was imported as Raw Binary at base 0x0000, language x86:LE:16:Protected Mode.
All 35+ identified functions renamed and annotated in Ghidra.
Cursor Subsystem (0x0060–0x0d5f)
| Address | Name | Description |
|---|---|---|
0x0060 |
cursor_update_hover |
Hover update: if mouse active & entity set, calls cursor_set_target |
0x00e9 |
cursor_set_target |
Positions cursor on entity, updates sprite + direction visual |
0x0322 |
cursor_shutdown |
Frees cursor resources, resets state |
0x0398 |
cursor_animation_update |
Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6 |
0x050f |
cursor_draw_tick |
Per-frame cursor draw (calls cursor_animation_update if dirty) |
0x0c24 |
action_key_valid |
Returns 1 if action code (param_1) is a valid game action key |
0x0d5f |
cursor_direction_input |
Arrow-key input: rotates cursor angle, updates direction sprite |
Input Handling
| Address | Name | Description |
|---|---|---|
0x0526 |
input_keyboard_handler |
Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load |
Cursor State Data (at DS:0x27xx)
| Address | Field | Meaning |
|---|---|---|
0x27c4 |
cursor_sel1 | Selection counter 1 |
0x27c6 |
cursor_sel2 | Selection counter 2 |
0x27c8 |
current_entity | Handle to currently targeted entity |
0x27ca–0x27ce |
cursor_state | Cursor interaction state bytes |
0x27d0 |
cursor_entity_type | Current entity type index |
0x27d2 |
z_offset | Z-height offset for terrain adjustment |
0x27d4 |
cursor_angle | Rotation angle (0–359) |
0x27d6 |
cursor_sprite | Sprite handle for cursor visual |
0x27d8 |
cursor_dirty | Set when cursor needs redraw |
0x27d9 |
cursor_active | Master cursor enabled flag |
0x27da |
cursor_no_turn | Flag disabling cursor rotation |
0x27ed |
difficulty | Enemy accuracy divisor (used in projectile_init_vector) |
0x27fd |
hard_mode | Two-step mode (combat vs. explore) |
0x27fe |
move_mode | Movement phase flag |
0x27ff |
mouse_active | Mouse/input system active |
0x2800–0x2811 |
various | UI state: active sprite, facing byte, cur entity handle |
0x283f/0x2841 |
menu_obj_ptr | Active menu/dialog object far pointer |
0x2844 |
in_save | In-progress save game flag |
0x290e |
entity_count | Number of active entities |
0x2910–0x2947 |
snap_type_ids[10] | Entity types that snap-to-ground in snap_entity_to_ground |
Input / Action Dispatch
| Address | Name | Description |
|---|---|---|
0x2420 |
entity_command_dispatch |
Dispatches player commands to target entity; reads 0x27d0, 0x2de4, sends events 0x14/0xf, handles state machine 0x27ca/0x27cd/0x27ce |
0x279a |
cheat_code_check |
Checks entity byte+1 against a five-byte event-code table at 0x2833 (50 80 3e fd 27 00, counter 0x283d); on full match, toggles 0x844/0x6045, emits helper/event code 0x103, and takes one of two local success-side dispatch paths based on the new toggle state |
Follow-up: cheat_code_check internals
- Direct raw-EXE recovery now tightens the cheat path substantially:
cheat_code_checkinCRUSADER-RAW.EXEis0007:0d0a-0007:0e08.- It has exactly one direct caller in this build:
FUN_0007_04dcat0007:0511, which prepares a small local event record and then callscheat_code_checkbefore continuing normal input dispatch. - The byte compared on each call is
entity_or_event_record[+1], not a character fetched from a typed string buffer. - Ghidra-side cleanup is now applied inside the function too: the decompiler shows
input_event_record,input_event_offset,new_cheat_enabled, andcheat_status_display_root, plus a function comment describing the matched sequence and the cheat-toggle side effects.
- Variable and constant roles from the recovered body:
0x2833is a local byte table, not an ASCII string in the raw EXE. The first five bytes are50 80 3e fd 27, followed by a0x00terminator. The checker walks that table one byte at a time.0x283dis the current match index into that byte table. On a successful byte match it increments; on mismatch it resets to zero and immediately retries the current input byte against the first table byte, so overlapping prefixes still work.entity[+1]is the compared input/event token. Because the cheat table is non-ASCII bytes, this path is matching higher-level input/event codes rather than literal typed letters.0x844is the main cheat-enable flag. The success path toggles it by converting the current byte to a boolean and negating it (0 -> 1, non-zero ->0).0x6045is written with the same post-toggle value as0x844, so it is a mirrored or secondary cheat-state latch rather than an independent control.- Constant
0x103is pushed into the shared helper at000a:5276immediately after the toggle. Existing notes already tie that constant to event0x42f; the local meaning is “emit the cheat-toggle side effect” rather than part of the matcher itself. 0x8c52is forced to1on success before the side-effect path continues.
- Success-path structure:
- After a full table match, the code resets
0x283dto zero, sets0x8c52 = 1, toggles0x844and0x6045, and calls the shared0x103helper. - It then branches on the new cheat state. The
cheats-onpath uses local code pointerDS:0x287b; thecheats-offpath usesDS:0x2892. - Those values are not text strings and not vtable IDs. They land inside local helper code around
entity_registry_decrement(0007:286d) andentity_sprite_move_delta(0007:2884), then pass throughdisplay_null_check_dispatch(000b:1446) andsprite_node_get_or_traverse(000a:b988). The exact UI/presentation object built by that path is still open, but it is clearly a local success-side dispatch path rather than “spawn vtable 0x287b/0x2892”.
- After a full table match, the code resets
- Conservative conclusion:
cheat_code_checkis a compact stateful matcher over event-code bytes, not text input.- The interesting remaining question is what upstream input normalization turns user actions into the five-byte sequence
50 80 3e fd 27, and what exact presentation object or notification path the two success-side helper targets construct for cheats-on versus cheats-off.
Follow-up: cheat-enable sources and verified cheat-only actions
- Two independent cheat-enable sources are now verified in this build:
- The hidden input matcher in
cheat_code_checktoggles0x844and0x6045after matching the five-byte event-code table at0x2833. - The command-line parser at
0004:635c-0004:63b8recognizes the literal switch-laurieand directly sets0x844 = 1before taking a local notification path. This is the clearest readable cheat-enabler in the raw EXE.
- The hidden input matcher in
jassica16is still not directly visible in the raw EXE:- No literal
jassicastring is present in the current string table, while-laurieis present as plain text. - The matcher table remains the raw byte sequence
50 80 3e fd 27 00, so ifjassica16is a real player-facing input for this build it must be produced by an upstream normalization/compression step rather than stored as literal text. - That upstream producer is still open; current evidence does not justify claiming a direct byte-for-character correspondence.
- No literal
- A plain
F10cheat action is now verified in the low-level keyboard path:seg001_input_keyboard_handlerat0006:ec29handles input byte0x44and immediately returns unless cheats are enabled through0x6045.- That branch does not test a modifier bit before the cheat action, so the code currently supports “plain F10 when cheats are enabled” much more strongly than “Ctrl+F10”.
- The branch emits event
0x261, refreshes the active0x7e22entity/object lane, rebuilds or destroys several linked entities, and fires the follow-up event batch0x33d,0x33f,0x340,0x341,0x33ebefore re-enabling channels4,1, and0. - The exact gameplay-side names of those follow-up events are still open, but this is consistent with a substantial restore/reset path such as the reported full-heal/resurrect action. No separate god-mode latch has been found in this branch.
FUN_0007_04dcexposes a second cluster of cheat-only hotkeys once the same cheat gate is open:- Confirmed byte tests in the caller-side dispatch are
0x37,0x4a,0x4e,0x52,0x53,0x0f,0x24, plus the already-visible character events'9'and'R'. 0x37calls000c:8072, while0x4acalls the neighboring helper at000c:81c0.000c:8072cycles a small1..5selector, writes that choice into the per-entity0x7e1etable at field+0x15throughFUN_0006_162d, chooses one of the small sprite/state IDs0x2e,0x2f,0x24,0x25, and then callsentity_table_set_spriteat0007:14af.000c:81c0walks a broader0x0b..0x19selector range and writes the chosen value into the same per-entity table at field+0x19throughFUN_0006_1671.- Together these two helpers look like cheat/debug selector tooling tied to the current
0x7e22object lane, not to health, invulnerability, or the cheat-enable toggle itself. 0x4e,0x52, and0x53all route through the same object-method dispatch on the currently selected0x49fbentry after building a shared argument block withfunc_0x000b2e00, which again looks like debug/view tooling rather than a passive status flag.
- Confirmed byte tests in the caller-side dispatch are
- The UI/event layer also exposes multiple cheat-gated overlay or visualizer toggles behind internal event codes:
- Event
0x441reaches000c:8e16and toggles byte0xee0before refreshing the0x2bd8controller object. - Event
0x241reaches000c:8e46and toggles byte0x2bc9before the same refresh. - Event
0x141reaches000c:8e72and toggles byte0x2bcabefore the same refresh. - Event
0x410reaches000c:9703and toggles byte0x604f, then takes one of two display/notification paths. - Events
0x142and0x143also dispatch into large cheat-gated view-building paths at000c:9154and000c:92cd; they clearly redraw substantial display state, but their exact user-facing names remain open.
- Event
- Current bottom line on the folklore claims:
- “Cheats can be enabled with
-laurie” is directly verified. - “There is a hidden input-sequence cheat enabler” is directly verified, but its exact human-readable spelling is still unresolved at the binary level.
- “F10 performs a large cheat-only restore/reset action” is directly verified.
- “Ctrl+F10 enables god mode” is not supported by the current code path; the verified F10 branch does not require a modifier and no dedicated god-mode latch has been recovered yet.
- “Other cheat-only debug visualizers/tools exist” is directly verified, though several are still known only by internal event codes or selector helpers rather than final user-facing names.
- “Cheats can be enabled with
Menu / Event Callbacks
| Address | Name | Description |
|---|---|---|
0x2e53 |
cursor_event_notify_a |
Vtable thunk: forwards event to 0x27ca area handler |
0x2e96 |
cursor_event_notify_b |
Vtable thunk: forwards event to 0x27ca area handler (alt path) |
0x2ed9 |
menu_event_notify_a |
Vtable thunk: forwards event to 0x2843 (near menu object) |
0x2f0c |
menu_event_notify_b |
Vtable thunk: forwards event to 0x2843 (alt path) |
0x2ff3 |
stub_noop_2ff3 |
Empty stub, noop |
0x2ff8 |
entity_collision_callback_a |
Calls touch handler then func(entity+0x1e, seg, 2); opt: extra func if param_3&1 |
0x3046 |
set_active_menu |
Writes param_1/param_2 to 0x283f/0x2841 (active menu far pointer) |
0x3058 |
entity_collision_callback_b |
Same as entity_collision_callback_a (second vtable entry) |
Entity System (0x2401–0x5a50)
| Address | Name | Description |
|---|---|---|
0x2401 |
clear_cursor_selection |
Zeros 0x27c4/0x27c6 (selection counters) |
0x2899 |
cursor_switch_target_entity |
Switches cursor target: unloads old entity, loads new, re-registers |
0x29d8 |
get_z_offset |
Returns func() + *(0x27d2) = adjusted Z/height |
0x2a09 |
is_player_in_range |
Checks if entity is at player (0x2de4) X/Y +/-0xf0 range |
0x2a46 |
entity_ai_update_loop |
Loops entities 2–255, checks visibility, triggers fire/move |
0x2c36 |
ui_update_callback |
Calls cursor_state_clear then vtable[2] on menu object |
0x2c6b |
cursor_state_clear |
Clears cursor state bytes 0x27ca–0x27ce, clears entity flag bit1 |
0x2c92 |
dialog_spawn |
Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca |
0x2d47 |
entity_pick_handler |
Handles entity selection or save-game trigger (type 0x38d) |
0x2df9 |
clear_active_menu |
Zeros 0x283f/0x2841 (active menu far pointer) |
0x2e18 |
game_mode_init |
Initializes game mode state, resets sprite/cursor/menu state |
0x2f3f |
entity_table_set_sprite |
Reads 0x7df9+slot2; writes entity type table 0x7e1e[slot0x79+0x0d]=param_2, +0x10=0 |
0x3c97 |
snap_entity_to_ground |
If entity type in snap_type_ids[10], resets Z to 0xf0 and adjusts XY |
0x3d6e |
spawn_entity_checked |
Spawns entity with explosion pool limit check (0x84c0, 0x84c2) |
0x3f2f |
entity_spawn |
Allocates entity, vtable=0x29aa/0x39ca, positions it |
0x40d4 |
entity_remove |
Removes entity: destroys sprites, clears 0x2802/0x2804 if needed |
0x4172 |
entity_animation_frame_update |
Advances/retreats anim frame ([+0x1d]) toward target [+0x1c/0x1b] based on quality |
0x42f8 |
stub_noop_42f8 |
Empty stub, noop |
0x42fd |
entity_registry_decrement |
Calls cleanup func then decrements entity count at 0x290e |
0x4314 |
entity_sprite_move_delta |
Updates shot sprite handle (entity+0x3f) position by adding delta params |
0x4552 |
entity_set_position |
Sets entity+0x3e (type_handle), world_x/y (entity+0x45/47), base_x/y (entity+0x4f/51) |
0x452b |
shot_set_spawn_pos |
Calls entity_set_position then sets entity+0xbe = param_3 (extra spawn field) |
0x4591 |
entity_try_place |
entity_set_position with validation — position only set if placement succeeds |
0x5092 |
entity_deactivate |
Calls vtable[2] to deactivate, or finds in registry and removes |
0x5a50 |
entity_list_contains |
Checks if entity ptr exists in active entity list at 0x294c |
0x5b05 |
stub_noop_5b05 |
Empty stub, noop |
Entity Object Layout (NE Segment 1 entities)
| Offset | Field | Meaning |
|---|---|---|
+0x00 |
vtable_ptr | Vtable pointer (0x29aa for generic, 0x2a57 for debris) |
+0x02 |
slot_index | Entity slot index (used for registry at 0x39ca) |
+0x04 |
entity_type | Entity type ID |
+0x19/+0x1a |
flags | Entity flags (bit0=debris, bit1=cleared by cursor_state_clear, bit6=active, bit8=valid) |
+0x1b |
vel_x | X velocity (clamped ±0x20) |
+0x1c |
vel_y | Y velocity (clamped ±0x20) |
+0x1d |
vel_z | Z velocity (clamped ±0x10) |
+0x1e |
fire_handle | Weapon/fire handle |
+0x1f |
is_enemy | 1 if entity is an enemy type |
+0x20/+0x21 |
pos_frac_x/y | Fractional position (sub-tile) for movement |
+0x22 |
pos_frac_z | Fractional Z |
+0x36 |
weapon_type | Active weapon type ID |
+0x38 |
facing | Current facing direction (0–15) |
+0x3c |
sprite_handle | Sprite for this entity |
+0x3f |
shot_sprite | Sprite handle for active projectile (0xFFFF = none) |
+0x45/+0x47/+0x49 |
world_x/y/z | Current world position (integer) |
+0x4f/+0x51/+0x53 |
base_x/y/z | Base/spawn position |
+0x54/+0x56/+0x58 |
prev_x/y/z | Previous frame position |
+0x59 |
attack_active | Attack in progress flag |
+0x5a |
at_target | Reached target flag |
+0x5e–+0x65 |
delta_x/y/z/high | Per-step movement deltas (fixed point) |
+0x66/+0x68 |
step_active | Stepping active (1=yes, 0=off) |
+0x6a/+0x6c |
weapon_slot/dist | Weapon slot and total travel distance |
+0x6e |
delta_z | Alt Z delta |
+0x70 |
projectile_type | Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain) |
+0x72/+0x74/+0x76 |
target_x/y/z | Target position with deviation |
+0x77 |
target_entity | Target entity handle |
+0x79 |
secondary_pos | Secondary position struct pointer |
+0xad |
owner_entity | Owning entity handle |
+0xaf |
shot_owner_flags | Shot owner (entity/player) |
+0xb1 |
bounce_count | Bounce counter (used with homing, type 5) |
+0xb3 |
has_bounce | Has bounce trajectory active |
+0xbd |
actor_type | Actor type byte (used for direction table lookups) |
Shot Entity Lifecycle (0x435e–0x44a9)
| Address | Name | Description |
|---|---|---|
0x435e |
shot_entity_alloc |
Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7 |
0x44a9 |
shot_entity_free |
Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1 |
Projectile / Combat (0x4659–0x5a99)
| Address | Name | Description |
|---|---|---|
0x4659 |
projectile_init_vector |
Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536 |
0x4a91 |
entity_fire_weapon |
Fires weapon from entity using 0x129b/0x12ac direction offset tables |
0x4b18 |
fire_weapon_from_cursor |
Gets cursor angle sprites, fires projectile at cursor target |
0x4b78 |
projectile_check_hit |
Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity) |
0x4c2e |
projectile_step_update |
Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked |
0x4d28 |
projectile_trace_ray |
Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate |
0x51ad |
projectile_update_tick |
Full projectile tick: move, check reach target, bounce, call projectile_check_hit |
0x5a99 |
projectile_apply_hit |
Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner |
Weapon Type Table (0x2536)
- Each entry is 0x11 bytes (17), accessed as
weapon_type * 0x11 [0]= step divisor for distance calculation[0x19]= max range threshold (used in projectile_update_tick)
Direction Tables (0x129b / 0x12ac)
- Indexed by facing (0–15): dx offsets at 0x129b, dy offsets at 0x12ac
- Values are multiplied by distance (e.g.
*0x500) for projectile spawn offsets
Collision Detection (0x60c1–0x621e)
| Address | Name | Description |
|---|---|---|
0x60c1 |
aabb_overlaps_3d |
3D AABB overlap test — box layout [xmin,ymin,zmin,,,xmax,,ymax,,zmax] |
0x621e |
bbox_translate |
Translates a 3D bounding box by (dx, dy, dz) — both min and max points |
Enemy AI / Spawning (0x6aed–0x6d21)
| Address | Name | Description |
|---|---|---|
0x6aed |
map_find_spawn_point |
Finds map tile matching entity conditions; returns packed XYZ tile coords |
0x6bfc |
actor_find_in_view |
Finds actor visible in current view frustum (temp data at 0x7eca) |
0x6ce9 |
enemy_spawn_with_target |
Wrapper: spawns enemy with player as target (param5=1) |
0x6d05 |
enemy_spawn_no_target |
Wrapper: spawns enemy without targeting player (param5=0) |
0x6d21 |
enemy_spawn_at_position |
Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A) |
Player / HUD
| Address | Name | Description |
|---|---|---|
0x50ee |
player_position_update |
Updates player position from direction data; clamps to screen bounds |
0x6ff7 |
player_health_update_and_effect |
Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect |
Destruction / Death (0x7490–0x75ff)
| Address | Name | Description |
|---|---|---|
0x7490 |
debris_spawn |
Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list |
0x75ff |
entity_die |
Death handler: spawns 1–4 debris objects, picks best explosion direction |
Entity Type Constants (weapon_type/entity class)
| Value | Entity Class |
|---|---|
0x17 |
Robot/mech type A |
0x18 |
Robot/mech type B |
0x1 through 0x3c |
Various entity/weapon types |
0x3d |
Robot/mech type C |
0x3e |
Robot/mech type D |
0x2f5–0x2f7 |
Special movement entity |
0x595/0x597 |
Platform/elevator entities |
0x31c/0x322–0x327 |
Explosive/effect entities |
0x38d |
Save game trigger entity |
0x426 |
Spark/scatter sub-shot |
0x59a |
Player cursor/select indicator |
Entity Data Table at 0x7e1e
- Stride:
0x79bytes (121 bytes per entry) - Indexed by entity type (integer) or entity slot
+0x59offset = class-detail flags byte (entity_class_get_flag8returns bit0x08; other callers also clear bit0x10here during at-target facing updates)+0x5aoffset = flags byte (bit4 = special entity flag, bit3 = armor/shield flag)+0x68= targeting flag
Map / Resource Tables
| Address | Content |
|---|---|
0x2833 |
Cheat code input sequence (null-terminated) |
0x283d |
Cheat sequence match position counter |
0x7ded |
Map X coordinate array (2 bytes per entry) |
0x7df1 |
Map Y coordinate array (2 bytes per entry) |
0x7df5 |
Map Z array (1 byte per entry) |
0x7df9 |
Entity state array (2 bytes per slot) |
0x7e46 |
Player state block far pointer |
0x7e1e |
Entity type table (stride 0x79) |
Entity Vtable Index (NE Segment 1)
| Address | Entity Class |
|---|---|
0x28b5 |
Dialog/menu object vtable |
0x287b |
Cheat-spawned entity (cheat ON) vtable |
0x2892 |
Cheat-spawned entity (cheat OFF) vtable |
0x2969 |
Entity registry vtable (stored at 0x39ca+slot*4, not entity's own vtable) |
0x297e |
Shot/projectile entity vtable |
0x29aa |
Generic/AI entity vtable |
0x2a1a |
Corpse entity vtable (variant) |
0x2a33 |
Actor/corpse entity vtable |
0x2a57 |
Debris fragment entity vtable |
Next Steps
-
✅ NE Segment 1 imported and analyzed — all 58 identified functions renamed and annotated
-
Import additional NE segments — priority: segments 22, 30, 59, 86 (segment 21 complete)
-
Analyze additional segments — apply same decompile→rename→annotate workflow
-
Map file format loaders —
.FLX,.SHP,.MAP,.TNTresource formats -
Cross-reference entity type constants with game entities (robots, platforms, triggers)
-
Identify external segment calls — the
func_0x0000ffff()placeholders are all cross-segment calls; resolving them requires importing the referenced segments -
✅ NE Segment 1 imported and analyzed — all 58 identified functions renamed and annotated
-
✅ Raw 0007 segment analyzed — rendering, camera/scroll, save slot, and scroll region subsystems documented (~60 functions renamed and annotated)
-
Import additional NE segments — priority: segments 22, 30, 59, 86 (segment 21 complete)
-
Analyze raw 0007 draw helper cluster —
FUN_0007_03b4,FUN_0007_04b8,FUN_0007_04dc,FUN_0007_057f,FUN_0007_0614; called by sprite/draw list functions -
Analyze
FUN_0007_4cdf— large 15-case animation/movement dispatcher; overlapping instruction warnings; cases 0, 2, 3, 6, 9, 0xa, 0xe are clean -
Map file format loaders —
.FLX,.SHP,.MAP,.TNTresource formats -
Cross-reference entity type constants with game entities (robots, platforms, triggers)
-
Identify external segment calls — the
func_0x0000ffff()placeholders are all cross-segment calls; resolving them requires importing the referenced segments
Raw 0007 Rendering & Sprite Draw List Subsystem (new)
A complete sprite draw list and tile-based visibility system was recovered from the 0007:e000–0007:fe00 flat range (above seg001, separate segment loaded at 0x7E000+).
Isometric Coordinate Transform
| Address | Name | Evidence |
|---|---|---|
0007:be67 |
world_to_screen_isometric |
Classic 2:1 isometric formula: screen_x = (wx + sx) + (wy + sy)*2, screen_y = (wy + sy)*2 - (wx + sx). Scroll globals: 0x2bb7 (X), 0x2bb9 (Y). Output to *param_3, *param_4. |
0007:be9e |
world_to_screen_isometric_wrapper |
Thin wrapper — calls world_to_screen_isometric with args shifted by one |
Draw List Node Format
Sprite/draw node (size 0x18 = 24 bytes from pool, allocated by linked_list_pop_2cc3):
| Offset | Field | Notes |
|---|---|---|
+0x0 |
vtable | Function ptr for render callback (vtable[0] = draw) |
+0x8 |
dep_from_list | Near ptr to list of sprites that depend ON this |
+0xa |
dep_to_list | Near ptr to list of sprites this depends ON |
+0xc |
bbox_xmin | Screen bounding box (4 ints) |
+0xe |
bbox_ymin | |
+0x10 |
bbox_xmax | |
+0x12 |
bbox_ymax | |
+0x14 |
flags | bit 0=queued in draw list, bit 5=visible/rendered, bit 6=root sentinel, bit 7=must-redraw |
+0x15 |
z_depth | int; sort key for painter's algorithm. 1 = root sentinel |
+0x17 |
order_flag | if set, propagates must-redraw to dependent sprites |
+0x18 |
tile_index | sparse tile index (for dirty bitmask bit addressing) |
Draw List Functions
| Address | Name | Evidence |
|---|---|---|
0007:eb36 |
drawlist_pool_init |
Inits free pool: 1500 nodes × 0x2a (42) bytes from base 0x2cc7. Sets 0x2ccf = 1500 (count), 0x2cc3 = linked list head. Copies 0x2cc9 → 0x2cae. |
0007:eb12 |
linked_list_push_2cc3 |
LIFO push: node->next = head_2cc3; head = node; count_2ccf++. |
0007:eada |
linked_list_pop_2cc3 |
LIFO pop: dequeues node from 0x2cc3 head, decrements 0x2ccf count. |
0007:ebd9 |
linked_list_dequeue_headtail |
FIFO dequeue from head/tail pair list; calls FUN_0007_03ec after dequeue. |
0007:ec2c |
drawlist_enqueue |
FIFO enqueue: appends node to draw list (head if empty, tail always updated). |
0007:ec63 |
drawlist_remove_node |
Unlinks specific node from head or tail position; calls FUN_0007_03ec. |
0007:eca8 |
drawlist_process_and_render |
Two-stage render pass. Stage 1: drains 0x8442 (pending), viewport-tests each sprite, moves in-bounds to 0x8446 (visible). Stage 2: drains 0x8446, calls sprite vtable[0] to draw. Recursive re-run if 0x2cb2 defer flag set. |
0007:edfa |
drawlist_enqueue_sprite_children |
Enqueues all child sprites at node +8 linkage to draw pending list if not already queued or rendered. Skips if +0x15 z-depth == 1. |
0007:ef3d |
sprite_add_draw_dependency |
Links sprites A and B with z-order dependency. Allocates link node via FUN_0007_057f. Stores in A's +10 and B's +8. Sets must-redraw on B if A has +0x17 set. |
0007:ef9f |
sprite_enqueue_for_draw |
Null-guard; sets node +0x14 |= 1, enqueues to 0x8442. |
0007:efca |
sprite_invalidate_and_unlink |
Full sprite removal: re-enqueues all dependents, frees link nodes via FUN_0007_0614, clears +8/+10 lists, dequeues self from 0x8442. |
0007:f2a0 |
sprite_sort_by_depth_and_link |
Compares +0x15 z-depth of two sprites; calls sprite_add_draw_dependency in the correct order (lower z first). Handles equal-depth via thunk. |
0007:f654 |
drawlist_init |
Full draw system init: drawlist_pool_init, init full-screen viewport (0x844a..0x8450 = 0..639, 0..screen_h), allocate root sentinel node (vtable FUN_0000_2ce4, z-depth=1, flag=0x40), stored at 0x846a. |
0007:ea00 |
bbox_intersect |
In-place 2D rect intersection: max of mins, min of maxes. Input/output: 4-int array [xmin,ymin,xmax,ymax]. |
0007:ea6d |
bbox_union |
In-place 2D rect union: min of mins, max of maxes. |
0007:ee5a |
viewport_update_from_sprite_bounds |
Subtracts scroll offsets from sprite bbox; clips to screen rect (0x2ca6..0x2cac); calls bbox_intersect; stores updated viewport at 0x844a..0x8450; dispatches to render. |
Tile-Based Visibility System
The rendering system uses a 6×5 tile grid at DS:0x846a (30 entries × 2 bytes each =60 bytes). Tiles represent screen regions of approximately 128 screen pixels per tile. Each tile holds a near pointer to a linked list of sprite nodes overlapping that tile. Bitmasks track dirty and renderable tiles.
| Address | Name | Evidence |
|---|---|---|
0007:f9e2 |
drawlist_mark_dirty_tiles |
Converts bbox to tile grid coords (divide by 128, clamp to 6×5). Walks all sprites in overlapping tiles' spatial buckets; re-enqueues overlapping sprites. Sets bits in dirty bitmask 0x2cbb. |
0007:fb53 |
tile_visibility_update |
Iterates redraw bitmask (0x2cbb, up to 10240 tiles). For each dirty tile with a sprite, computes isometric screen position from map X/Y/Z. Checks sprite dimension bitfields at pbVar8+2..+3 against 640×480 viewport. Sets corresponding bit in render bitmask 0x2cb7. Clears dirty bitmask after processing. |
0007:fd98 |
tilemap_draw_if_dirty |
Guard: if render bitmask 0x2cb7 non-zero, calls thunk_FUN_0007_001d to trigger tilemap draw. |
Draw List / Viewport Globals
| Address | Name | Notes |
|---|---|---|
0x2bb7 |
g_scroll_offset_x |
World-to-screen X offset (scroll) |
0x2bb9 |
g_scroll_offset_y |
World-to-screen Y offset (scroll) |
0x2ca6..0x2cac |
g_screen_clip_rect |
Screen clip rectangle [xmin,ymin,xmax,ymax] |
0x2cae |
g_draw_segment |
DS segment for draw node far pointers |
0x2cb0 |
g_free_pool_seg |
Segment for node free pool far calls |
0x2cb2 |
g_render_defer_flag |
If set, defers current render pass |
0x2cb7 |
g_render_tile_bitmask |
Far ptr to bitmask of tiles ready to render |
0x2cbb |
g_dirty_tile_bitmask |
Far ptr to bitmask of dirty/changed tiles |
0x2cbf |
g_tile_origin_x |
Tile grid world origin X (for tile coordinate math) |
0x2cc1 |
g_tile_origin_y |
Tile grid world origin Y |
0x2cc3 |
g_free_pool_head |
Free node pool linked list head |
0x2ccf |
g_free_pool_count |
Free node pool remaining count (max 1500) |
0x8442 |
g_draw_pending_list |
Draw pending list head/tail (near ptrs) |
0x8446 |
g_draw_visible_list |
Draw visible list head/tail (near ptrs) |
0x844a..0x8450 |
g_viewport_rect |
Current viewport [xmin,ymin,xmax,ymax] |
0x846a |
g_tile_grid_base |
6×5 tile grid spatial index (near ptr per tile) |
Raw 0007 Map Scroll / Camera Subsystem (new)
A scroll/camera management cluster found in the 0007:bxxx–0007:dxxx range.
Entity State Transition Helper
| Address | Name | Evidence |
|---|---|---|
0007:5b6f |
internal block only (no function after repair) | Direct raw-analysis behavior remains useful as a local label: this block sets entity +0x3a = 1 (arrived flag), calls entity_set_facing_direction, clears bit 0x10 from entity type table 0x7e1e[type*0x79+0x59], then tail-calls onward. After the PyGhidra boundary repair, 0007:5b6f is no longer a function entry and should be treated only as an internal control-flow label inside the first repaired seg043 routine. |
seg043 Standalone Boundary Recovery
- Direct disassembly of
NE_segments/seg043_code_off_75A00_len_336F.binshows the first non-zero bytes at offset0x0090; offsets0x0000..0x008fare all zero in the standalone extract. - The first three clean 16-bit prologues in seg043 are at:
seg043:0090-> raw0007:5a90seg043:017a-> raw0007:5b7aseg043:021c-> raw0007:5c1c
- The first recovered standalone function spans
0x0090..0x0179, which means raw0007:5b6ffalls inside the tail of that routine and overlaps the true return at raw0007:5b79. - Repair status: applied in
CRUSADER-RAW.EXEvia the local PyGhidra toolkit. The bad function object at0007:5b6fwas removed, and three conservative replacement functions were created:0007:5a90=seg043_func_0090with body0007:5a90..0007:5b790007:5b7a=entity_set_at_target_update_facingwith body0007:5b7a..0007:5c1b0007:5c1c=seg043_func_021cwith body0007:5c1c..0007:5c80
- Follow-up re-decompilation now supports one real behavioral rename:
0007:5b7asets entity+0x3ato 1, callsentity_set_facing_direction, clears class-detail bit0x10at0x7e1e[type*0x79+0x59], then continues into downstream dispatch, so the repaired middle function has been renamedentity_set_at_target_update_facing. 0007:5a90now has a stronger structural read from standalone disassembly: it allocates an object when the incoming far pointer is null (literal0x98), runs a far setup helper using DS:0x4b48..0x4b4eand the second incoming far pointer, writes0x4c13at the object base, callsentity_set_at_target_update_facingwith the third incoming far pointer, then adjusts the nested object at+0x38using extents read from the object at+0x34before returning the object pointer.0007:5c1calso has a stronger structural read: it optionally calls a virtual method through[object->vtable + 0x4c]whenobject+0x44/+0x46is non-null, passes a local stack word throughentity_class_get_flag20, then dispatches one or two downstream far helpers usingobject+0x48, gated by a local status byte at[bp-0xe].0007:5a90and0007:5c1cremain intentionally positional because their current decompiles still collapse into unresolved thunk dispatches and do not yet support safe behavioral names.
Entity Class Flag Helper
| Address | Name | Evidence |
|---|---|---|
0006:02cc |
entity_class_get_flag20 |
Returns ((class_detail[type*0x79 + 0x59] & 0x20) >> 5). Conservative raw-analysis name; bit meaning still unknown, so the helper is named after the observed flag mask rather than a guessed behavior. |
Animation Start Frame Helper
| Address | Name | Evidence |
|---|---|---|
0007:71b2 |
entity_set_anim_start_frame_from_flags |
Reads entity +0x4b flags. If bit 1 set: uses type table +0x59 & 4 (attack active) to select last frame (+0x39 - 1), zero, or half-frame (+0x39 >> 1). Writes computed value to type table +0x10. If bit 2 set without bit 1: dispatch thunk. |
Combat Helper
| Address | Name | Evidence |
|---|---|---|
0007:894b |
entity_check_attack_flags_and_dispatch |
Guards on entity +0x4b bit 1 AND target object +5 bits 0x1c. If both set: dispatches thunk attack event. |
Vtable Dispatch Helpers
| Address | Name | Evidence |
|---|---|---|
0007:8920 |
entity_call_vtable_slot0c |
Calls (*param_1)[vtable+0xc](). |
0007:8cb8 |
entity_call_vtable_slot08 |
Calls (*param_1)[vtable+0x8](). |
0007:ccf1 |
entity_call_vtable_slot28 |
Calls (*param_1)[vtable+0x28](). |
Active Flag / Counter
| Address | Name | Evidence |
|---|---|---|
0007:8854 |
entity_set_active_flag |
Sets entity +0x40 = 1 (active); increments global 0x2800. |
Dispatch Table Lookup
| Address | Name | Evidence |
|---|---|---|
0007:8508 |
entity_table_lookup_and_dispatch |
Searches 1-entry table at 0x2b46 for (param_3, param_4) key pair; on match, calls the entry's function pointer at [2]. |
Scroll/Camera Functions
| Address | Name | Evidence |
|---|---|---|
0007:ba00 |
watch_entity_controller_create_global |
Thin global-create wrapper around watch_entity_controller_create; allocates default controller object and stores it at 0x2bd8. |
0007:ba13 |
watch_entity_controller_dispatch_if_present |
If 0x2bd8 is non-null, calls controller vtable slots +0x2c and +0x30. |
0007:ba45 |
watch_entity_controller_create |
Allocates/initializes a type 0x2c2b controller object, stores it at 0x2bd8, sets event type 0x0219, and installs callback-table entry 0x2be4 through 0x39ca. |
0007:bab5 |
entity_set_watch_ptr |
Legacy name still in place, but newer constructor evidence now shows 0x2bd8 is a controller object lane rather than just a raw watched-entity FAR pointer. |
0007:baea |
camera_update_and_check_player_scroll |
Calls watch entity vtable +0x24; if 0x2bd1 flag clear checks if player position (from g_player_entity_farptr+0x40) has moved > 32 units since 0x2be0; if so, updates 0x2be0 and conditionally dispatches scroll event via 0x45aa. |
0007:c6ba |
scroll_camera_set_state_params |
Stores word params to 0x8354, 0x8356, byte to 0x8358; dispatches. |
0007:cd56 |
dispatch_if_flag_2bd3_set |
Returns unless 0x2bd3 non-zero. |
0007:cfef |
dispatch_if_mode_flags_set |
Two-flag check: dispatches if 0x2bca or 0xee0 is non-zero. |
0007:d0f6 |
scroll_call_set_params_unless_blocked |
Calls scroll_camera_set_state_params only if 0x2bbb == 0. |
0007:d119 |
scroll_update_direction_tracking |
Guards on 0x2bd3. Calls scroll_call_set_params_unless_blocked. Compares direction bytes from 0x2cf4/0x2cf5 against cached 0x2bbd/0x2bbe; if changed, clears 0x2bbc. Dispatches. |
0007:d4a5 |
scroll_set_option_value |
Sets 0x2bc6 = param_1. |
0007:d4b0 |
scroll_set_params_default |
Unconditional call to scroll_camera_set_state_params. |
0007:d4d3 |
scroll_set_map_index_validated |
If param_1 in [0..250] and differs from 0x2bbf, updates 0x2bbf and clears 0x2bbc/0x2bbb. |
0007:d655 |
map_position_has_changed |
Compares map arrays 0x7ded/0x7df1/0x7df5 at index 0x2bc6 against cached 0x2bc1/0x2bc3/0x2bc5. Returns 1 if changed, 0 if same. |
0007:d6b1 |
scroll_clear_dirty_flags_and_dispatch |
Clears 0x2bbb = 0 and 0x2bbc = 0; dispatches. |
0007:de57 |
entity_check_player_range_and_update |
Reads player world position (g_player_entity_farptr+0x40); if moved > 59 units from entity+0x32 (cached pos), updates cache and calls scrollregion_find_and_dispatch. |
Scroll Region Table (0x835a)
8 entries × 0x19 (25) bytes = 200 bytes. Entry layout:
| Offset | Field | Notes |
|---|---|---|
+0x0/+0x2 |
key pair | Matched by scroll region key lookup |
+0x8 |
refcount | Reference count (incremented on match) |
+0xe |
count2 | Secondary counter |
+0x10 |
active | Non-zero when region is live |
+0x11/+0x13 |
x_start/x_end | Bounding X coordinates |
+0x15/+0x17 |
y_start/y_end | Bounding Y coordinates |
| Address | Name | Evidence |
|---|---|---|
0007:e194 |
scrollregion_process_active |
Iterates active scroll entries (key non-zero AND +0x10 != 0), reads bounding box fields +0x11/+0x13/+0x15/+0x17, dispatches with bounds args. |
0007:e214 |
scrollregion_find_and_dispatch |
Finds empty scroll entry (zero key) and dispatches. |
0007:e29c |
scrollregion_register |
Finds or allocates scroll region entry. Existing match: bump refcount / set active. New: init via scrollregion_entry_init. Special key 0x4ed: thunk dispatch instead. |
0007:e50f |
scrollregion_entry_init |
Null-guards param_1; zeroes output param_2, param_3, param_4; dispatches continuation. |
0007:e74a |
(unnamed) | Sets up call to walk 0x835a table (8 entries, stride 0x19, filter 0x968), with callback return label e763. |
Save Slot System (0x8337 + 0x2ba3)
10 save slots, each 0x400 (1024) bytes. Handle table at 0x8337 (10 words). Slot buffer base at FAR ptr 0x2ba3.
| Address | Name | Evidence |
|---|---|---|
0007:ac13 |
saveslot_table_clear |
Fills 0x8337..0x834b (10 words) with 0xFFFF (all slots empty). |
0007:acab |
saveslot_free_if_empty |
Scans slot 0x2ba3[param_1*0x400] for non-zero data; if empty, sets handle 0x8337[param_1] = 0xFFFF. |
0007:ad47 |
saveslot_find_index_by_id |
Linear scan of 10-word handle table 0x8337; returns index of matching word or -1. |
0007:ad79 |
(unnamed) | Finds a free (0xFFFF) slot index. Complement of saveslot_find_index_by_id. |
0007:afd4 |
saveslot_get_or_alloc |
Gets slot pointer: calls saveslot_find_index_by_id; if not found calls ad79 to get free slot; returns 0x2ba3 + slot * 0x400. Returns 0 if no free slot. |
0007:b02c |
saveslot_write_entry |
Navigates to slot_base[param_3 * 4]; dispatches thunk paths for write (existing, overwrite, new). |
0007:b0de |
saveslot_read_entry_flags |
Reads from slot entry far pointer at slot_base[param_3*4]; extracts 4-byte packed bitfield from +4..+7 in entry record into *param_1. Bit-by-bit extraction loop for 4 bytes. |
String & Memory Utilities
| Address | Name | Evidence |
|---|---|---|
0007:a96d |
entity_copy_string_truncated80 |
Strlen(param_3) ≤ 0x50 guard; copies string word-by-word from param_3 into param_2+8. |
0007:b813 |
memcpy_4words |
Copies 4 words (8 bytes) from param_2 to param_1. |
0007:b46d |
entity_dispatch_if_slot82e2_valid |
Guard: if *(int *)0x82e2 != -1, calls dispatch thunk. |
Linked List Utilities (draw pool + sprite)
| Address | Name | Evidence |
|---|---|---|
0007:ea00 |
bbox_intersect |
In-place rect intersection: [xmin,ymin,xmax,ymax] = max(mins) × min(maxes). |
0007:ea6d |
bbox_union |
In-place rect union: [xmin,ymin,xmax,ymax] = min(mins) × max(maxes). |
Scroll/Camera Globals
| Address | Name | Notes |
|---|---|---|
0x2bb7 |
g_scroll_offset_x |
Isometric scroll X — added to world_x in screen transform |
0x2bb9 |
g_scroll_offset_y |
Isometric scroll Y |
0x2bbb |
g_scroll_blocked |
If non-zero, blocks scroll_camera_set_state_params call |
0x2bbc |
g_scroll_dirty |
Scroll direction changed flag (cleared when direction tracks) |
0x2bbd |
g_scroll_dir_x |
Cached scroll direction X |
0x2bbe |
g_scroll_dir_y |
Cached scroll direction Y |
0x2bbf |
g_map_index |
Current map/level index [0..250] |
0x2bc1/0x2bc3/0x2bc5 |
g_map_entry_x/y/z |
Cached map entry X/Y/Z (vs. live map arrays) |
0x2bc6 |
g_map_slot_index |
Index into 0x7ded/0x7df1/0x7df5 arrays for current map slot |
0x2bca/0x2bc9 |
g_option_toggle_state |
UI option toggle state flags |
0x2bd1 |
g_scroll_block_flag |
Blocks camera update path if non-zero |
0x2bd3 |
g_scroll_active |
Non-zero = scroll system active |
0x2bd8 |
g_watch_entity_controller_farptr |
FAR ptr to the watch/camera controller object created by watch_entity_controller_create; older notes treating it as a raw entity pointer were too narrow |
0x2be0 |
g_player_scroll_pos |
Cached player world X+Y (ulong) for scroll threshold detection |
0x8354..0x8358 |
g_scroll_state_params |
Three scroll state params (word, word, byte) |
Raw 0008 Gameplay Dispatch Helper Batch (new)
Small conservative rename batch from direct field-write behavior in the 0008:ba00-0008:be05 cluster.
Newly renamed functions
| Address | Name | Evidence |
|---|---|---|
0008:ba00 |
entity_dispatch_entry_init |
Constructor-style init: optional alloc (0x32 bytes), vtable/list-link setup (0x3b06, 0x2d10, 0x3afe), zeroes state fields, seeds group from global active layer 0x39c9 via entity_set_group_id |
0008:bbb6 |
entity_set_source_type |
Writes entry word field +0x04 from incoming parameter, then dispatches through FAR thunk path |
0008:bc27 |
entity_set_event_type_checked |
Writes entry word field +0x06; when source field +0x04 is non-zero, validates old/new event transition, including special checks for 0xF0-0xF7 and upper bound <= 0x0FFF |
0008:bca8 |
entity_set_group_id |
Validates group id range 1..31, writes low 5-bit group in byte +0x08, decrements old per-group counter and increments new one via counter table pointed to by 0x39c5 |
0008:bd53 |
entity_dispatch_entry_unlink |
Clears bit 0x1000 in flags2 at +0x18 and zeroes the four link/state words at +0x0a..+0x10; used as the common unlink/reset tail in the local dispatch-entry pruning path |
0008:be05 |
entity_increment_group_id |
Computes ((entry+0x08)&0x1F)+1, validates against active-layer assumptions (0x39c9), then applies through entity_set_group_id |
Verified call/xref notes
entity_set_group_idis called from:entity_dispatch_entry_initat0008:bae4entity_increment_group_idat0008:be57
entity_set_source_typeis used from at least:FUN_0008_c92f(0008:c94d,0008:c96d)FUN_0008_ca18(0008:ca36,0008:ca56)
Gameplay relevance
- This cluster appears to manage core dispatch-entry metadata (
source_type,event_type, group/layer byte and counters) that feeds the seg021 scheduler/event system previously documented. - The field offsets match the current seg021 entity/dispatch layout notes (
+0x04,+0x06,+0x08), strengthening confidence in cross-function struct consistency. - Follow-up on the same cluster:
0008:bd79remains positional, but current decompiler evidence shows it compares an entry extent/position tuple against the player world position (g_player_entity_farptr + 0x40/+0x42), optionally fires the vtable callback at+0x28when flag0x100is armed, then callsentity_dispatch_entry_unlink.
Raw 0008 Pair-Sync Helper Batch (new)
Conservative directional rename batch from the 0008:c7f1-0008:cad7 cluster.
These functions are clearly paired and structurally symmetric, but final gameplay semantics are still partial due to FAR-thunk heavy internals.
Newly renamed functions
| Address | Name | Evidence |
|---|---|---|
0008:c7f1 |
entity_pair_update_link_slot_a |
Guards on entry flags (+0x16 must not include 0x4000), then dispatches through FAR thunk using entry local struct at +0x28 and partner-side key/id input |
0008:c890 |
entity_pair_update_link_slot_b |
Twin of entity_pair_update_link_slot_a with identical call shape and guard behavior; used in opposite order by pair-sync wrappers |
0008:c92f |
entity_pair_sync_a |
If either side has unset source_type (+0x04), copies from partner via entity_set_source_type; then calls link-slot helpers in A-order and ends in FAR thunk using first side +0x1e data |
0008:ca18 |
entity_pair_sync_b |
Mirror of entity_pair_sync_a with reversed side/order for helper calls and final thunk argument ordering |
0008:c9ee |
entity_pair_mark_and_sync_a |
Sets bit 0x10 in entry flags at +0x16, then calls entity_pair_sync_a |
0008:cad7 |
entity_pair_mark_and_sync_b |
Sets bit 0x10 in entry flags at +0x16, then calls entity_pair_sync_b |
Verified call/xref notes
entity_pair_sync_ais called fromentity_pair_mark_and_sync_a(0008:ca10).entity_pair_sync_bis called fromentity_pair_mark_and_sync_b(0008:caf9).- Shared helper use inside pair sync wrappers:
entity_pair_update_link_slot_bat0008:c981and0008:ca6aentity_pair_update_link_slot_aat0008:c995and0008:ca7e
Gameplay relevance
- This cluster likely handles directional two-entity relationship synchronization in the scheduler/entity-dispatch layer (source/type propagation plus paired link-slot updates).
- Offsets used here (
+0x04,+0x16,+0x1e,+0x28) align with the existing seg021 object-field and linker/list usage patterns, which increases confidence while preserving conservative naming.
Raw 0008 Flag-0x20 Target-State Helpers (new)
Two complementary helpers near the pair-sync cluster were renamed using strict field/bit behavior only.
| Address | Name | Evidence |
|---|---|---|
0008:cb2c |
entity_flag20_clear_and_update_target |
Clears bit 0x20 at entry flags +0x16; if non-null target args are provided, writes far-pointer target fields +0x12/+0x14; then calls shared refresh helper 0008:c01d |
0008:cb5c |
entity_flag20_set_and_init_target |
Sets bit 0x20 at entry flags +0x16; initializes target far-pointer fields +0x12/+0x14 only when currently zero; then calls shared refresh helper 0008:c01d |
Notes:
- Naming intentionally stays flag-centric because high-level gameplay meaning of bit
0x20is not yet fully resolved. - Both helpers share the same post-update refresh path (
0008:c01d), suggesting they are two state transitions in one target/link-management subsystem.
Raw 0008 Dispatch Refresh Pipeline (new)
Follow-up rename batch for the shared refresh node used by the new flag-0x20 helpers.
| Address | Name | Evidence |
|---|---|---|
0008:c01d |
entity_refresh_dispatch_state |
Early-exit when flags at +0x16 indicate dead (0x8) or already refreshed (0x4000); otherwise runs pre-clear, sets 0x4000, calls update vfunc path, then runs flag-conditioned handlers |
0008:bfb2 |
entity_clear_status_bits_from_flags |
Clears specific bits in status word at +0x32 based on state flags (+0x16:0x400, +0x18:0x40/0x80) |
0008:bf8e |
entity_call_update_vfunc14 |
Calls helper 0008:be6b, then dispatches entity vtable call at offset +0x14 |
0008:beee |
entity_run_flagged_handlers |
Executes handler calls gated by flags (+0x16:0x400/0x4, +0x18:0x40/0x80) and then dispatches via FAR thunk using entry slot/index (+0x2) |
Verified xref context:
entity_refresh_dispatch_stateis directly called from:entity_flag20_clear_and_update_target(0008:cb54)entity_flag20_set_and_init_target(0008:cb86)
Gameplay relevance:
- This establishes a concrete state pipeline for dispatch entries after target/link changes: flag-gated status clear -> mark refreshed (
0x4000) -> vtable update callback -> flag-conditioned subsystem handlers.
Raw 0008 Flag-0x100 and Constructor-Variant Batch (new)
Additional conservative renames from the 0008:d1a4-0008:d27d cluster.
| Address | Name | Evidence |
|---|---|---|
0008:d1a4 |
entity_set_flag100_in_flags2 |
Gate-checked setter: ORs bit 0x100 into entry word at +0x18 |
0008:d1dc |
entity_clear_flag100_in_flags2 |
Gate-checked clearer: ANDs entry word at +0x18 with 0xFEFF (clears bit 0x100) |
0008:cefb |
entity_dispatch_entry_ctor_vtbl_3ad2 |
Constructor variant: allocates if null, reinitializes via entity_dispatch_entry_init, sets vtable 0x3ad2, sets flag 0x100 at +0x16, and zeroes the extension words at +0x32/+0x34 |
0008:d214 |
entity_dispatch_entry_ctor_vtbl_3aa6 |
Constructor variant: allocates 0x40 bytes if null, reinitializes via 0008:cefb, sets vtable to 0x3aa6, sets flag 0x200 at +0x16, zeroes fields +0x38..+0x3e |
Notes:
- The
entity_set_flag100_in_flags2/entity_clear_flag100_in_flags2pair is a verified complementary toggle with identical gate logic (0x39a8/0x39f9/0x3991check path). - Constructor naming is intentionally vtable-centric (
0x3ad2,0x3aa6) until more direct gameplay semantics are recovered from their callback dispatch paths.
Raw 0008 Periodic/Counter Helpers (new)
Follow-up renames from the 0008:d313-0008:d47d cluster tied to the 0x3aa6 constructor branch.
| Address | Name | Evidence |
|---|---|---|
0008:d313 |
entity_periodic_accumulate_and_dispatch |
Adds global delta (0x39d0/0x39d2) into entry accumulator (+0x3c/+0x3e), wraps against period (+0x38/+0x3a), and on wrap invokes entry vtable callback at +0x28 with reentrancy guard bit 0x400 in +0x18 |
0008:d3e6 |
entity_set_flag2000_and_update_active_counters |
Atomic (CLI/PUSHF) set of bit 0x2000 in +0x16; if bit 0x400 is set, decrements global counter 0x39f6 and increments 0x39f4 |
0008:d433 |
entity_clear_flag2000_and_update_active_counters |
Atomic clear of bit 0x2000 in +0x16; if bit 0x400 is set, decrements 0x39f4 and increments 0x39f6 |
Gameplay relevance:
- This identifies a concrete periodic dispatch mechanism (accumulator+wrap callback) and a paired active/inactive counter transition path around flag
0x2000. - The
0x39f4/0x39f6counter swap strongly suggests global bookkeeping for a scheduler subset associated with these entries.
Raw 0008 Word-List Management Batch (new)
Verified helper cluster for entry-owned word-list storage (sentinel-terminated with 0x0408).
| Address | Name | Evidence |
|---|---|---|
0008:da00 |
entity_word_list_set_0408_terminated |
Rebuilds/replaces entry list from stack-provided words terminated by 0x0408; frees prior list pointer at +0x06/+0x08; allocates and populates new list |
0008:dba3 |
entity_word_list_free_existing |
Validates list pointer exists, then frees old list buffer referenced by +0x06/+0x08 |
0008:dbec |
entity_word_list_destroy |
Resets vtable to 0x2d10, frees list if present via entity_word_list_free_existing, and optionally frees object when destroy flag bit 1 is set |
0008:dc38 |
entity_word_list_ensure_contains |
Scans existing list for a given word; if missing, appends through entity_word_list_append_unique |
0008:dcab |
entity_word_list_append_unique |
Allocates larger list, copies existing words, appends new word plus 0x0408 terminator, frees old list, then rebuilds via entity_word_list_set_0408_terminated |
Notes:
- Entry fields used by this subsystem: count at
+0x02, list far pointer at+0x06/+0x08. - The explicit
0x0408terminator appears both in scanner/build logic and append path, making it a reliable list format marker.
Raw 0008 Word-List Access/Mutation Batch (new)
Follow-up renames extending the same list subsystem.
| Address | Name | Evidence |
|---|---|---|
0008:deea |
entity_word_list_get_at |
Bounds-checks index against count (+0x02) and returns word from list pointer (+0x06/+0x08, stride 2) |
0008:df1b |
entity_word_list_set_at |
Bounds-checks index then writes value into list element (+0x06/+0x08, stride 2) |
0008:dfa1 |
entity_word_list_find_unflagged_by_id10 |
Scans list and returns first value satisfying (value & 0x400)==0 and (value & 0x3ff)==requested_id; writes 0 when not found |
0008:ddaf |
entity_word_list_remove_value |
Removes matching value(s) by counting survivors, rebuilding compact storage for non-matching entries, freeing old list storage, and updating list state |
Notes:
entity_word_list_find_unflagged_by_id10implies list entries pack a 10-bit id plus flag bits (0x400observed).- This further supports that the
0008:da00..dfa1region is a compact encoded-ID list manager used by gameplay dispatch entries.
Raw Import Note: 0000:ffff Thunk Target (new)
Requested deep-dive on FUN_0000_ffff:
- Renamed to
unresolved_far_thunk_dispatch. - Current raw-import evidence indicates this is not valid local executable logic in this program view:
- Decompiler emits overlapping-instruction warnings and bad-control-flow warnings.
- Disassembly from
0000:ffffinto0001:xxxxis nonsensical/misaligned (mixed data/code artifacts). - The body is heavily shared as a call sink from many segments, consistent with unresolved inter-segment thunking in this import mode.
Practical interpretation:
- Treat calls to
unresolved_far_thunk_dispatchas unresolved external/indirect dispatch edges, not as meaningful function internals to recover in the raw flat import. - Semantic recovery should continue from call-site argument setup and local field effects (the workflow used in recent 0008 batches).
Raw 0008 Gate-Callback Wrapper Batch (new)
Conservative renames for callback wrappers sharing the same global gate condition.
| Address | Name | Evidence |
|---|---|---|
0008:d00e |
entity_gate_callback_wrapper_a |
Gate check on globals 0x39a8/0x39f9/0x3991; on pass dispatches callback through unresolved thunk using entry +0x2 and [0x3b32:0x3b34] + 0x32 |
0008:d05f |
entity_gate_callback_wrapper_b |
Same gate pattern; callback wrapper variant via unresolved thunk |
0008:d0b0 |
entity_gate_callback_wrapper_c |
Same gate pattern; passthrough-style callback wrapper |
0008:d0ed |
entity_gate_callback_wrapper_d |
Same gate pattern; passthrough-style callback wrapper |
0008:d12a |
entity_gate_callback_wrapper_e |
Same gate pattern; passthrough-style callback wrapper |
0008:d167 |
entity_gate_callback_wrapper_f |
Same gate pattern; passthrough-style callback wrapper |
0008:d3d2 |
entity_slot_callback_wrapper |
Thin wrapper: pushes entry slot/index (+0x2) and dispatches through unresolved thunk |
Notes:
- Names remain wrapper-oriented because target callbacks are unresolved in this raw-import model.
- These wrappers are now easier to track from call sites while preserving conservative semantics.
Additional Unresolved Thunk Stubs (new)
Follow-up thunk census after inspecting 0000:ffff behavior.
Confirmed trampoline-only stubs
All of the following are single-instruction wrappers (CALLF 0000:ffff) and were given unique labels:
| Address | New Name | Observed Caller(s) |
|---|---|---|
0004:2592 |
thunk_callf_0000_ffff_0004_2592 |
0004:262d (FUN_0004_2620) |
000b:f924 |
thunk_callf_0000_ffff_000b_f924 |
000b:0144 (FUN_000b_010b) |
000c:827d |
thunk_callf_0000_ffff_000c_827d |
000c:8985, 000c:8f96 (FUN_000c_88b4) |
000c:82f9 |
thunk_callf_0000_ffff_000c_82f9 |
000c:8a10, 000c:8f79, 000c:9052 |
000c:8356 |
thunk_callf_0000_ffff_000c_8356 |
000c:84a9 (FUN_000c_84a5) |
000c:e4f9 |
thunk_callf_0000_ffff_000c_e4f9 |
000c:e4f5 (FUN_000c_e4e0) |
Notes:
- This confirms
unresolved_far_thunk_dispatchis represented by multiple local trampoline copies in different segment regions. - Separating them by address improves call-graph navigation and makes subsystem-specific tracing less ambiguous.
Raw 000c State-Dispatch Helper Cluster (new)
After separating thunk stubs, a coherent local state/chain management cluster was lifted in 000c:ab32-000c:ac8f.
| Address | Name | Evidence |
|---|---|---|
000c:ab32 |
entity_state_tick_dispatch |
Core state tick helper using fields +0x38/+0x39/+0x3b/+0x3d/+0x5b; clears mode bit 0x100 when +0x38==0, may call cleanup helper 000c:ac55, calls 000c:7730(state,1), and conditionally advances chain |
000c:ab96 |
entity_state_reset_and_tick_dispatch |
Reset wrapper: zeroes +0x38 and +0x39 then calls entity_state_tick_dispatch |
000c:abb4 |
entity_state_advance_next_or_fallback_a |
Advance path A: when +0x49!=0, follows node-next pointers from +0x3b/+0x3d using offsets +2/+4; when exhausted, either clears active flag and re-dispatches, or falls back to backup pointer +0x41/+0x43 |
000c:ac8f |
entity_state_advance_next_or_fallback_b |
Advance path B: same structure as A but follows alternate node offsets +6/+8 and fallback pointer +0x45/+0x47 |
Notes:
- This cluster gives a concrete local interpretation for part of the large
000c:88b4control flow without relying on unresolved thunk internals. - Naming remains direction/path based (
a/b) where high-level gameplay meaning is still pending.
Raw 000c State-Flag Guard / Input Handler Batch (new)
Second sweep through 000c adjacent helpers — gated thunk wrappers and input/animation tick handlers.
| Address | Name | Evidence |
|---|---|---|
000c:9f74 |
entity_state_flag100_check_and_dispatch |
Init latch guard at [0x6053]; clears [0x8c55] on first call; checks [ptr+0x5b] bits 0x100 and 0x40; three-path thunk dispatch |
000c:a1ad |
entity_state_clear_flag40_and_dispatch |
Skips if [ptr+0x5b] has 0x180 bits set; if 0x40 set, clears it and calls far ptr at [0x5e82/0x5e84]; then dispatches twice with args (0x0b,0x10,0x1,0x0) (record/state-key pattern) |
000c:a74e |
entity_state_dispatch_if_flag_bit2 |
Tests [ptr+0x5b] bit 0x2; if set pushes extra arg + ptr and dispatches via thunk |
000c:84c3 |
entity_state_set_byte40_at_global_ptr |
Sets byte [DAT_0000_6828 ptr + 0x40] = 1 then calls thunk unconditionally; enables global entity flag |
000c:ac55 |
entity_state_fire_if_handle_valid |
Guard: fires thunk dispatch only when [0x6054] != -1; no-op otherwise |
000c:ac6d |
entity_state_fire_with_args_if_handle_valid |
3-arg variant: pushes [BP+0xe] (byte), [BP+0xc], [BP+0xa], handle [0x6054], then CALLF 0000:ffff |
000c:afa5 |
entity_state_check_field49_and_call_vfunc3c |
Checks field [ptr+0x49]: −1→reset to 0 return 1; 2→call vtable[0x3c] return 0; else thunk dispatch |
000c:b153 |
entity_state_animation_done_tick |
Checks [param_2+0x14+0xa] animation-complete flag; if zero increments field49 and calls entity_state_check_field49_and_call_vfunc3c; if set calls vtable[0x3c] |
000c:b199 |
entity_state_input_key_handler |
Full input dispatcher: ESC/x/X → vtable[0x3c] (cancel); Left/Right arrows 0x14b/0x148 → prev state; n/N/0x14d/0x150 → next state; e/E → set field47=1; - with counter → trigger at 4. Manages field47 and field49 |
000c:b2c3 |
stub_noop_000c_b2c3 |
Empty stub; returns immediately |
000c:b2c8 |
entity_state_dispatch_if_field49_eq4 |
Fires thunk only when [ptr+0x49]==4 |
000c:b349 |
entity_state_dispatch_if_far_ptr_nonzero_a |
Fires thunk if far-pointer args non-zero |
000c:b383 |
entity_state_set_field3f_and_dispatch |
If non-NULL: writes &DAT_0000_2d18 to [ptr+0x3f], then dispatches |
000c:b3d8 |
entity_state_dispatch_if_far_ptr_nonzero_b |
Same null-guard pattern as b349, variant b |
Patterns confirmed in this batch:
field49= state-sequence index; 0=reset, 2=vtable callback, 4=triggered endfield47= keystroke-combo counterfield3f= linked data pointer (event/record reference)- Global
[0x6054]= current entity handle;[0x6828]= another global entity far pointer - Bits in
[ptr+0x5b]:0x1=init,0x2=active/event,0x40=pending dispatch,0x100=flag100,0x180=skip-all mask
Raw 000c Palette Fade + Entity VM Cluster (new)
Two distinct subsystems identified in 000c:cdde-000c:f98b.
VGA Palette Fade (000c:cdde, 000c:ce57)
| Address | Name | Evidence |
|---|---|---|
000c:cdde |
palette_fade_step_down |
Writes (R−offset, G−offset, B−offset) clamped to 0 to VGA I/O 0x3c8/0x3c9; decrements [0x630d] by step [0x6316]; clears active at [0x630a] when black |
000c:ce57 |
palette_fade_step_up |
Same loop, adds offset, clamps at 63 (0x3f full VGA). Clears [0x630a] when fully bright |
Globals used: [0x6312]=start index, [0x6314]=count, [0x630e]=palette src ptr, [0x630d]=brightness offset, [0x6316]=step, [0x630a]=active flag.
Entity Mini-VM / Record-Player Context (000c:f6b8-000c:f98b)
| Address | Name | Evidence |
|---|---|---|
000c:f6b8 |
record_table_get_by_index |
Bounds check param < [0x8c88]; return word at [0x8c84 + param*4]. Table at 0x8c84 |
000c:f6e8 |
entity_vm_stack_init_with_data |
Init stack ptrs at [ptr+0xcc..+0xd4] pointing to self; max depth 199; copies optional initial data |
000c:f772 |
entity_vm_state_copy |
Copies 200 bytes (100 words from [src+4] to [dst+4]), then copies 4 words at +0xcc..+0xd2 |
000c:f7c7 |
entity_vm_stack_push_frame |
Push call-frame: saves ret offset at [ptr+0xd4], decrements [ptr+0xcc] by param_size, zeroes new frame |
000c:f844 |
entity_vm_context_setup |
Calls entity_vm_stack_init_with_data, then sets +0xd6..+0xe3 with position/dimension/state params |
000c:f600 |
entity_vm_pair_stack_push |
Push (word_a, word_b) onto 31-entry array at [ptr+0x80] (count); error if full |
000c:f63c |
entity_vm_pair_stack_pop |
Pop and return word from pair stack; error if empty |
000c:f94f |
entity_vm_counter_add |
[ptr+0xd6] += param_2; simple accumulator |
000c:f98b |
entity_vm_set_field_da_to_global |
Writes [param_2+0xda far-ptr + 2] into [0x8c94] |
Notes:
- Field offsets
+0xcc=VM stack ptr,+0xce/+0xd0=segment regs,+0xd2=base,+0xd4=frame depth,+0xd6=counter,+0xd8/+0xda/+0xdc=VM position/bounds. - The 200-byte body region at
[ptr+4..+0xcc]holds 100 words of script/state payload. - The pair-stack (field
+0x80) is separate — likely pass/return value stack for the mini-script.
Raw 000c Cursor Zone / Slot Array / String Queue Batch (new)
Three utility subsystems identified in 000c:e6d9-000c:eadd, plus companion slot array API.
Cursor / Directional Zone Classifier
| Address | Name | Evidence |
|---|---|---|
000c:e6d9 |
cursor_zone_quadrant_classify |
Splits screen by [0x63d6]/2 and [0x63d8]/2 vs bounds [0x8c6c..0x8c72]; returns directional code from 9-word table at 0x6401 |
Zone table layout (9 entries): NW/N/NE / W/Center/E / SW/S/SE based on horizontal threshold at 0x8c6c/0x8c70 and vertical at 0x8c6e/0x8c72.
Slot Array System (000c:ea53-000c:ee44)
A complete 29-slot menu/choice array with fixed stride 0x15 bytes, base at [ptr+0x67], count at [ptr+0x7a]:
| Address | Name | Evidence |
|---|---|---|
000c:ea53 |
entity_slot_count_update_and_notify |
Sets [ptr+0x72]=param-1; calls slot_array_get_current_entry and slot_array_find_and_dispatch; calls vtable[0]() when +0x75 flags set |
000c:eba5 |
slot_array_dispatch_matching |
Walks 0xb-stride array from [ptr+4]; calls thunk for each entry where [entry+9]==param_4 |
000c:ec30 |
slot_array_dispatch_if_nonempty |
Returns 0xffff if count < 1; else dispatches |
000c:ec9e |
slot_array_find_and_dispatch |
Searches 0xb-stride array for [entry+9]==param_4; calls thunk on first match |
000c:ecf5 |
slot_array_push_entry |
Copies named string to [base+0xc]; writes 6 param words at +0x12..0x20; increments count |
000c:edb0 |
slot_array_push_raw |
Copies 0x15-byte raw entry from param_2; increments count |
000c:edf7 |
slot_array_pop |
Decrements [ptr+0x7a]; asserts >= 0 |
000c:ee19 |
slot_array_init |
Sets [ptr+0x78]=0, [ptr+0x76]=0, [ptr+0x75]=1 (active flag) |
000c:ee32 |
slot_array_clear_flags |
Clears [ptr+0x74]=0, [ptr+0x75]=0 |
000c:ee44 |
slot_array_get_current_entry |
Returns ptr + [ptr+0x7a]*0x15 + 0x67 (current entry ptr); 0 if count <= 0 |
String Queue
| Address | Name | Evidence |
|---|---|---|
000c:eadd |
string_queue_push |
Appends string to 10-entry queue at [ptr+4]; count at [ptr+2]; sets [ptr+0xd]=param_4 |
Additional VM-Adjacent Helpers
| Address | Name | Evidence |
|---|---|---|
000c:f2e7 |
entity_call_vtable_entry_10_if_valid |
Null-guard: calls (*[ptr+8+0x10])() if param_1 non-null |
000c:f39f |
string_table_lookup |
Searches [0x65bc/0x65be] table by key string; returns matching words to out-params |
Raw 000c Cursor Nav Dispatcher / State Reset Batch
Cursor navigation subsystem in 000c:d3e9–000c:db68. Manages directional zone changes, mouse button reads, keyboard scancodes, and entity vtable dispatch for interactive UI cursors.
Cursor Navigation Fields (entity object offsets)
| Offset | Purpose |
|---|---|
+0x32 |
Current zone code (0–8) |
+0x33 |
Previous zone code |
+0x37–+0x3a |
Directional booleans: N/S/W/E |
+0x3f–+0x42 |
Mouse button flags |
+0x45 |
Last keyboard scancode |
+0x47 |
Navigation index |
Globals: [0x63da] = mouse button state, [0x63d6]/[0x63d8] = cursor X/Y, [0x638e] and [0x6346] = reference data tables.
Functions
| Address | Name | Evidence |
|---|---|---|
000c:dac1 |
cursor_nav_state_reset |
Zeros all directional/button flags; sets [+0x32/+0x33]=0xff, [+0x47]=0xffff |
Top-40 Most-Called Far-Call Targets (NE Fixup Resolution)
Named via systematic analysis of 11,692 NE relocation fixup entries. These are the functions most frequently called through the CALLF 0x0000:ffff thunk mechanism.
Tier 1: Top 20 (73+ callers)
| Rank | Address | Name | Calls | Description |
|---|---|---|---|---|
| 1 | 000a:44fd |
seg091_func_00fd |
331 | Recovered boundary. Shares init flag 0x44a4 with runtime_init_or_abort; thunk-heavy non-returning wrapper. |
| 2 | 0003:ac7e |
mem_alloc |
272 | Allocation wrapper → seg082:0000 (0009:a200) |
| 3 | 0008:dbec |
entity_word_list_destroy |
238 | Already named. Frees entity word-list buffer. |
| 4 | 0003:a751 |
mem_free |
207 | Free wrapper → seg082:007a (0009:a27a = mem_free_checked) |
| 5 | 0008:bb4f |
mem_alloc_far |
174 | Thin wrapper → mem_alloc |
| 6 | 0003:a897 |
far_memcpy |
165 | REP MOVSW + trailing MOVSB |
| 7 | 0005:088f |
entity_get_type_word |
130 | Returns type word from table 0x7df9 indexed by slot |
| 8 | 000b:358d |
sprite_tree_accumulate_pos |
122 | Recursively sums X/Y offsets (+0x21/+0x23) through linked child nodes (+0x19/+0x1b), copies 8-byte position block via far_memcpy |
| 9 | 0008:ce3d |
entity_call_two_vtables |
118 | Calls vtable[+4] at entity+0x1e and +0x28 |
| 10 | 0004:26cd |
nop_void_stub |
118 | Empty function, returns void |
| 11 | 0008:ce00 |
entity_call_two_vtables_base |
117 | Calls vtable[0] at entity+0x1e and +0x28 |
| 12 | 0008:bb8c |
entity_check_flag_0x4000 |
115 | Short-circuits if flag 0x4000 set at +0x16 |
| 13 | 0008:cda7 |
entity_free_both_word_lists |
115 | Frees word lists at entity+0x1e and +0x28 if optional pointers at +0x24/+0x26 and +0x2e/+0x30 non-null. Both call entity_word_list_free_existing. |
| 14 | 0004:26d2 |
nop_void_stub_b |
111 | Empty function, returns void |
| 15 | 000a:45fe |
runtime_init_or_abort |
108 | Reentrancy-guarded init. Flag at 0x44a4; flushes via FUN_000a_4a56, then calls crt_exit_wrapper(1). Hidden code gap 0x4616-0x4643. |
| 16 | 0004:3324 |
nop_return_zero |
95 | Returns 0 |
| 17 | 0009:c563 |
event_queue_push |
82 | Circular buffer enqueue. Ring index (+0xe) masked 0x3f, slot masked 0xfff8. Writes event type word + data byte pair. |
| 18 | 0005:c448 |
list_remove_and_free |
74 | Unlinks node from linked list via FUN_0005_c495, optionally calls mem_free if bit 0 of flags set |
| 19 | 000b:2e00 |
(no function in Ghidra) | 74 | Analysis gap at seg109:0000. Needs manual function creation. |
| 20 | 0009:1f12 |
dos_file_lseek |
73 | DOS LSEEK (INT 21h AH=42h) wrapper with error reporting to 0x867a |
Tier 2: Ranks 21-40 (56-73 callers)
| Rank | Address | Name | Calls | Description |
|---|---|---|---|---|
| 21 | 0009:3600 |
rotating_buffer_advance |
73 | Advances 5-slot circular counter at 0x3eb6, zeros pointer in table at 0x867c, dispatches via jump table |
| 22 | 0009:943a |
entity_rect_compare_and_dispatch |
68 | Compares bounding rectangles of two entities, dispatches based on flag bits 4/2/1 at +0x16 |
| 23 | 0009:1e61 |
dos_file_close |
65 | DOS file close (INT 21h), error reporting, sets handle to -1 |
| 24 | 0005:e252 |
(unnamed — unclear) | 65 | Copies 11 words from Phar Lap extender area (FUN_0000_12c6+5), then calls thunk. Interrupt/trampoline setup? |
| 25 | 0003:dbcc |
crt_format_string |
64 | MetaWare High C formatting wrapper. Calls FUN_0003_bb92 with runtime format dispatch table. |
| 26 | 0007:5a00 |
(no function in Ghidra) | 64 | High-traffic raw target at seg043:0000. Earlier debris_spawn / seg001 mapping was rejected after checking relocation labels. Still needs manual function creation and direct analysis. |
| 27 | 000a:4742 |
assert_buffer_valid |
63 | Validates handle: asserts param_2 == cookie at 0x45a6 and param_1 < limit at 0x87e0 |
| 28 | 0009:9216 |
entity_conditional_render_dispatch |
63 | Checks entity flag bits 4 and 1 at +0x16, dispatches to vtable[+0xc] or thunk |
| 29 | 0008:cb2c |
entity_flag20_clear_and_update_target |
61 | (already named) Clears flag bit 0x20, writes target +0x12/+0x14, calls refresh |
| 30 | 0008:cb5c |
entity_flag20_set_and_init_target |
61 | (already named) Sets flag bit 0x20, inits target if zero, calls refresh |
| 31 | 0007:7306 |
entity_create_stack_object |
58 | Allocates 0xCC bytes on stack, inits via object_init_zero_fields (0005:c400), calls thunk |
| 32 | 0007:8709 |
entity_mark_dirty_and_sync_tile_aux |
58 | (already named) Syncs tile aux, sets flag bit 0x04 at +0x42 |
| 33 | 0007:87c5 |
entity_set_flag20_from_field42 |
58 | Reads entity+0x42/+0x44, calls entity_flag20_set_and_init_target with those values |
| 34 | 0007:8508 |
entity_table_lookup_and_dispatch |
58 | (already named) Searches table at 0x2b46, dispatches via indirect jump |
| 35 | 0007:8920 |
entity_call_vtable_slot0c |
58 | (already named) Calls vtable entry at +0x0c |
| 36 | 000a:b988 |
sprite_node_get_or_traverse |
57 | If child pointer at +0x19/+0x1b non-null, traverses; otherwise returns leaf value |
| 37 | 0003:a98b |
crt_signed_div32 |
56 | Entry: adjusts near→far stack, sets CX=0 (signed quotient), jumps to crt_div32_impl |
| 38 | 000a:7b44 |
nop_return_void_a |
56 | Empty function (default vtable slot?) |
| 39 | 000a:7b49 |
nop_return_void_b |
56 | Empty function (default vtable slot?) |
| 40 | 000a:7b53 |
nop_return_void_c |
56 | Empty function (default vtable slot?) |
Supporting Functions Discovered
| Address | Name | Description |
|---|---|---|
000b:3a00 |
sprite_tree_sum_x_offset |
Recursive: sums field +0x21 through child chain +0x19/+0x1b |
000b:3a35 |
sprite_tree_sum_y_offset |
Recursive: sums field +0x23 through child chain +0x19/+0x1b |
0003:a845 |
crt_exit_wrapper |
Calls crt_exit_impl(param,0,0) |
0003:a7ee |
crt_exit_impl |
Full C exit: atexit handlers, stdio flush, MetaWare runtime cleanup |
0003:a9a8 |
crt_div32_impl |
32-bit division core. CX flags: bit0=unsigned, bit1=modulo, bit2=negate |
0005:c400 |
object_init_zero_fields |
Zeros fields +0x25, +0x29, +0x31, +0x32 of a struct. Returns pointer. |
000a:4440 |
joystick_read_axes_and_buttons |
Reads PC game port 0x201. Times axis responses, reads button nibble to 0x44a2 |
000b:3380 |
sprite_node_is_dirty |
Checks flags at obj+0x29 & 3 == 1 or 3 → returns bool |
000b:33a6 |
sprite_node_mark_dirty |
If not dirty, calls FUN_000b_3965 with mode=3 to invalidate |
Tier 3: Ranks 41-60 (42-56 callers)
| Rank | Address | Name | Calls | Description |
|---|---|---|---|---|
| 41 | 000a:7b58 |
nop_return_zero_b |
56 | Returns 0 (default vtable slot) |
| 42 | 000b:3ab2 |
sprite_node_dispatch_event |
56 | Large event dispatch: checks event type (2/4/8/0x100), updates global focus ptr at [0x4fd0:4fd2], dispatches via vtable methods [+0x14/+0x18/+0x20/+0x24] by event code. Switch table for 16 event types. |
| 43 | 000a:48ff |
rng_next_modulo |
55 | Advances seg091 RNG state and returns the result modulo the requested bound; returns 0 when bound is 0. |
| 44 | 000b:3362 |
sprite_tree_unwind_check |
55 | Validates SS == param_2 (stack segment guard), then decrements global counter at [0x4fd6] |
| 45 | 000b:40ee |
sprite_node_update_and_dispatch |
55 | If sprite_node_is_dirty returns false: marks dirty, calcs accumulated bounds via sprite_tree_get_accumulated_bounds (3ed8), then dispatches via thunk |
| 46 | 000a:7b5f |
vtable_stub_trampoline |
55 | Calls through fixup thunk (forwarder to another function) |
| 47 | 000a:7b78 |
nop_return_void_e |
55 | Empty function (default vtable slot) |
| 48 | 000a:7b7d |
nop_return_void_f |
55 | Empty function (default vtable slot) |
| 49 | 000a:7b4e |
nop_return_void_d |
54 | Empty function (default vtable slot) |
| 50 | 000b:330c |
sprite_tree_dispatch_wrapper |
52 | Pure thunk wrapper: calls through fixup |
| 51 | 0009:2034 |
dos_file_seek |
51 | INT 21h AH=42h (LSEEK). Takes file object ptr, extracts handle at obj+4, seeks to offset param. Error reporting to [0x867a]. |
| 52 | 0005:0466 |
entity_resolve_slot_ptr |
50 | (already named) |
| 53 | 0003:a880 |
(no function in Ghidra) | 49 | Analysis gap in CRT segment |
| 54 | 0006:170c |
tile_class_get_byte |
47 | Looks up class data: indexes into table at [0x7e1e] by (*param_1 * 0x79), returns byte at offset +0xc |
| 55 | 000b:4097 |
sprite_dispatch_with_event |
45 | Pushes event params + global [0x49c2:0x49c4], calls thunk |
| 56 | 0005:02c1 |
entity_is_type_match |
43 | Compares *param_1 against global at [0x27c8], returns 1 if equal, 0 otherwise |
| 57 | 0003:ad75 |
(no function in Ghidra) | 43 | Analysis gap in CRT segment |
| 58 | 000a:e709 |
render_dispatch_by_flag |
43 | Dispatches between two thunk paths based on boolean flag at stack+0x10 |
| 59 | 0003:d0ff |
crt_sprintf_wrapper |
42 | Calls FUN_0003_bb92 (format engine) with rearranged params and string constant at 0x67ac |
| 60 | 000b:326e |
sprite_node_destroy |
42 | Destructor: sets vtable ptr to 0x501a, clears global [0x4fd0:4fd2] if self, releases child nodes, calls mem_free via thunk |
Updated Analysis Gaps
0007:5a00 / 0007:5b6f reconciliation:
- The earlier standalone seg001 port hypothesis in this subrange was wrong.
- Relocation data places raw
0007:5a00atseg043:0000, and the already-named helper at0007:5b6fsits atseg043:016f. - Because of that segment placement, standalone seg001 names such as
debris_spawn(0x7490) andentity_die(0x75ff) should NOT be ported into this raw range. 0007:5b6fno longer exists as a function after the PyGhidra repair pass. Its old raw-analysis behavior now lines up with the repaired function0007:5b7a = entity_set_at_target_update_facing, so0007:5b6fshould be treated only as an internal control-flow location inside that function.- Additional resolved call targets inside the missing seg043 block were annotated in Ghidra from relocation data:
0007:5a8a->entity_set_event_type_checked0007:5a98->FUN_0008_cc01(timer-related flag/event helper; tests+0x16 & 0x2, sets+0x16 |= 0x800, copies event field+0x06to+0x22, checks0x1000, then conditionally dispatches)0007:5b36->entity_get_type_word0007:5b44->saveslot_read_entry_flags0007:5bb8->entity_is_type_match0007:5c49->entity_class_get_flag200007:5c8b->mem_alloc_far
- Current boundary state:
- The seg043 split has now been repaired in Ghidra. Verified temporary functions exist at raw
0007:5a90,0007:5b7a, and0007:5c1c. - The repaired middle function at
0007:5b7ahas now been promoted from a positional label toentity_set_at_target_update_facingbased on direct decompile/disassembly behavior. - The remaining repaired functions at
0007:5a90and0007:5c1cshould keep their positional names until a later pass resolves the thunk-heavy bodies more clearly. - The next pass on this region should continue re-decompiling
seg043_func_0090andseg043_func_021c, resolve the still-unknown far thunks they call, and replace the positional names only when their behavior is directly supported.
- The seg043 split has now been repaired in Ghidra. Verified temporary functions exist at raw
| Address | NE Segment | Callers | Notes |
|---|---|---|---|
000a:44fd |
seg091:00fd | 331 | Recovered as seg091_func_00fd; thunk-heavy init wrapper sharing flag 0x44a4. |
000b:2e00 |
seg109:0000 | 74 | Start of segment 109. |
0007:5a00 |
seg043:0000 | 64 | Start of segment 43. Earlier seg001 debris_spawn port was rejected; still needs manual function creation and direct analysis. |
000a:48ff |
seg091:04ff | 55 | Recovered as rng_next_modulo; bounded wrapper around seg091 RNG state advance. |
0003:a880 |
seg005:0880 | 49 | In CRT segment near far_memcpy. |
0003:ad75 |
seg005:0d75 | 43 | In CRT segment near mem_alloc. |
000a:454d |
seg091:014d | 32 | Recovered as seg091_func_014d; init/context helper using the 0x45a6 cookie/context global. |
Tier 4: Ranks 61-80 (29-42 callers)
| Rank | Address | Name | Calls | Description |
|---|---|---|---|---|
| 61 | 000b:30a5 |
sprite_tree_forward_wrapper |
42 | Pure thunk forwarder |
| 62 | 0008:bc27 |
entity_set_event_type_checked |
41 | (pre-existing name) Sets event code at +0x06 with range/timer checks |
| 63 | 0008:d214 |
entity_dispatch_entry_ctor_vtbl_3aa6 |
40 | (pre-existing name) Constructor: alloc 0x40, vtbl 3AA6, flag 0x200 |
| 64 | 0005:1565 |
entity_action_by_type_dispatch |
39 | Checks entity type against whitelist (0x432,0x5a0,0x1fd,0x1fe,0x8f,0x59f,0x2b3,0x2ca), dispatches by flags at [0xc76] and [0x85f] |
| 65 | 0008:4bba |
channel_slot_enable |
39 | Sets enable byte=1 in 5-slot table at 0x84ca (slot * 0xd stride) |
| 66 | 0009:6f5a |
vga_palette_write |
38 | Writes RGB triplets to VGA DAC (port 0x3C8/0x3C9). Range param_2..param_3 from palette data at *param_1 |
| 67 | 0009:8ef6 |
line_draw_dispatch |
38 | Compares abs(dx) vs abs(dy) to determine major axis, dispatches to appropriate line draw routine |
| 68 | 000a:7b30 |
nop_return_void_g |
38 | Empty function (default vtable slot) |
| 69 | 000a:7b3f |
nop_return_void_h |
38 | Empty function (default vtable slot) |
| 70 | 0009:6e7f |
palette_free_if_set |
35 | Frees existing palette data if ptr non-null, checks alignment |
| 71 | 000a:7b35 |
nop_return_void_i |
35 | Empty function (default vtable slot) |
| 72 | 0009:c433 |
event_queue_align_index |
34 | Returns param_1 & 0xFFF8 — aligns ring index to 8-byte event slot boundary |
| 73 | 0009:2156 |
dos_file_get_size |
33 | Saves file position, does INT 21h AH=42h AL=02 (seek to end), restores position. Returns file size in DX:AX |
| 74 | 000a:2c41 |
list_iterate_next |
33 | Linked list iterator: if *out==0 returns first from obj+2; else follows next at ptr+2/+4. Returns bool (has more) |
| 75 | 000a:454d |
seg091_func_014d |
32 | Recovered boundary. Shares flag 0x44a4; checks optional long argument against the 0x45a6 cookie/context global. |
| 76 | 000b:2446 |
sprite_clear_redraw_flag |
31 | Clears flag at obj+0x17e, then dispatches via thunk |
| 77 | 0005:1238 |
entity_get_class_word |
30 | Looks up table at [0x7e01] indexed by *param_1 * 2, returns word. Sister of entity_get_type_word (which uses [0x7df9]) |
| 78 | 000b:1446 |
display_null_check_dispatch |
30 | Null-checks far ptr params, dispatches to different thunks based on result |
| 79 | 000d:85da |
map_object_set_dirty_flag |
29 | Sets byte at global_obj[0x6828]+0x40 = 1 if global non-null, then calls thunk |
| 80 | 0005:1511 |
entity_destroy_trampoline |
29 | Pure thunk forwarder to entity destruction |
Deep Analysis: Coordinate Transform System
world_to_screen_coords at 0004:e7bd (NE seg018:07bd)
Signature:
void world_to_screen_coords(int world_x, int world_y, int *screen_x, int *screen_y)
Isometric Projection Math:
screen_x = (world_x - world_y) / 2 - camera_x // SAR 1 (signed divide)
screen_y = (world_x + world_y) / 4 - camera_y // SHR 2 (unsigned divide)
Camera globals: g_scroll_offset_x (DS:0x2bb7), g_scroll_offset_y (DS:0x2bb9).
Assembly detail:
SAR AX, 1for screen_x — signed arithmetic shift preserves sign for negative (world_x - world_y) differencesSHR AX, 2for screen_y — unsigned logical shift (sum world_x + world_y is always positive)- The 2:1 ratio (÷2 for X, ÷4 for Y) produces the classic 2:1 isometric diamond tile shape
Coordinate axes on screen:
- World X axis → lower-right on screen (+0.5 screen_x, +0.25 screen_y per world unit)
- World Y axis → lower-left on screen (-0.5 screen_x, +0.25 screen_y per world unit)
- Camera subtraction converts absolute world-space to viewport-relative screen coordinates
Callers (17 across 8 NE segments):
| Call site | NE Segment | Context |
|---|---|---|
0004:7d6f |
seg012 | Map/tile rendering |
0005:0305 |
seg021 | Entity system |
0005:432f |
seg021 | Entity placement |
0005:4457 |
seg021 | Entity placement |
0005:6f8f |
seg022 | Entity rendering |
0005:7263 |
seg022 | Entity rendering |
0007:2262 |
seg040 | snap_entity_to_ground — ground alignment |
0007:237d |
seg040 | Ground snap dispatch |
0007:cf4e |
seg049 | Entity positioning |
0007:d039 |
seg049 | Entity positioning |
0007:d43f |
seg049 | Entity positioning |
0007:d6fe |
seg049 | Entity positioning |
0008:3223 |
seg053 | Entity-to-screen render setup |
0008:32e7 |
seg053 | Entity-to-screen render setup |
0008:334b |
seg053 | Entity-to-screen render setup |
000b:858b |
seg115 | Sprite system |
000b:f100 |
seg120 | Sprite system |
Entity struct layout (from seg053 caller at 0008:31f6):
entity_array_base = far ptr at [DS:0x2cff]
entity_struct_size = 19 bytes (0x13)
entity.world_x = offset +0x0a (word)
entity.world_y = offset +0x0c (word)
Comparison: Two Coordinate Transform Functions
| Property | world_to_screen_coords (0004:e7bd) |
world_to_screen_isometric (0007:be67) |
|---|---|---|
| Input type | Fine-grained world units (entity positions) | Coarse tile-grid units (map rendering) |
| screen_x | (wx - wy) / 2 - cam_x |
(wx + sx) + (wy + sy) * 2 |
| screen_y | (wx + wy) / 4 - cam_y |
(wy + sy) * 2 - (wx + sx) |
| Camera handling | Subtracted after transform | Added before transform |
| Operations | Division (SAR/SHR) | Multiplication (SHL) |
| Aspect ratio | 2:1 (from /2 : /4) | 2:1 (from 1 : 2 multipliers) |
Both functions implement the same 2:1 isometric projection but at different coordinate scales. world_to_screen_coords divides down from fine world units while world_to_screen_isometric multiplies up from coarse tile units.
Adjacent Function: map_position_equal at 0004:e784
Compares two 5-byte map_position structs: { x:word, y:word, layer:byte }. Returns 1 (AL) if all three fields match, 0 otherwise. Located immediately before world_to_screen_coords in seg018.
Tier 5: Ranks 81-100 (25-29 callers)
| Rank | Address | Name | Calls | Description |
|---|---|---|---|---|
| 81 | 0009:1c00 |
dos_file_handle_init |
29 | Inits 6-byte file handle struct: dword=0, word+4=0xFFFF (invalid). Aborts on null ptr |
| 82 | 0008:75f3 |
entity_get_ptr |
29 | (pre-existing) Looks up entity far ptr from table at DS:0x39b0, indexed by id*4 |
| 83 | 0006:0208 |
entity_class_get_flag4 |
29 | Returns bit 2 of classinfo byte at [0x7e1e]+p10x79+0x13 → 0 or 1 |
| 84 | 000a:30d7 |
list_node_set_if_context |
29 | Sets node fields +2/+4 if params match context globals at 0x45a6/0x45a8 |
| 85 | 0009:c45f |
object_init_and_get_next |
29 | Calls object_init_zero_fields then returns *(result+2) — init+accessor combo |
| 86 | 0004:d7a0 |
object_deref_get_word4 |
28 | Dereferences far ptr chain: returns word at ((param_1)+4) |
| 87 | 000a:5276 |
debug_check_flag_45aa |
28 | If byte at DS:0x45aa non-zero, calls thunk (diagnostic/assert check) |
| 88 | 0003:d94f |
far_memset |
28 | Wrapper reordering params for CRT memset impl at 0003:d92b (odd-aligned, word-fill loop) |
| 89 | 000a:7b3a |
nop_return_void_j |
28 | Empty function (default vtable slot) |
| 90 | 0008:ca18 |
entity_pair_sync_b |
27 | (pre-existing) Pairwise sync wrapper direction B |
| 91 | 0008:bd20 |
entity_sprite_set_target_pos |
27 | (pre-existing) Sets flag 0x1000, copies player pos to entity +0x0a/+0x0c |
| 92 | 0009:3ceb |
buffer_release_and_dispatch |
27 | Frees far ptr at obj+0x3b if set, nulls it; conditionally dispatches on bit 0 |
| 93 | 0005:09b4 |
entity_get_flags_byte |
27 | Reads byte from [0x7dfd]+id, conditionally extends with classinfo byte at [0x7e1e]+id*0x79+0xf |
| 94 | 0005:0fbb |
entity_lookup_sprite_word |
27 | Returns word from [0x7e05]+p12 — sprite/visual index table |
| 95 | 0008:d27e |
entity_dispatch_trampoline_b |
26 | Pure forwarder thunk (CALLF thunk only) |
| 96 | 0005:0376 |
entity_resolve_base_type |
26 | Walks entity class hierarchy (bit 8 in [0x7e01]) via [0x7ded], returns base type from [0x7df1] |
| 97 | 000b:2492 |
sprite_redraw_if_needed |
26 | If redraw flag at +0x17e is clear, calls update routine + thunk |
| 98 | 0003:e4d3 |
dos_file_open_wrapper |
26 | Zeros output byte, delegates to file open impl at 0003:bb92 |
| 99 | 0005:033e |
entity_resolve_base_parent |
25 | Same hierarchy walk as entity_resolve_base_type but returns parent from [0x7ded] |
| 100 | 000a:87fd |
render_clip_rect_to_viewport |
25 | Clips 4 rect params to viewport bounds at [0x4014], sets dirty flag at 0x8a16, increments draw counter at 0x4716 |
| 101 | 0005:3cf5 |
entity_class_has_flag2000 |
25 | Returns (entity_get_class_word(slot) & 0x2000) != 0 |
| 102 | 0009:80db |
bbox_overlap_test |
25 | Boolean rectangle overlap test for two 4-word bbox structs; unlike bbox_intersect, it does not write back an intersection |
| 103 | 000d:cc00 |
entity_compute_proximity_or_visibility_bucket |
25 | Returns 0x40 if entity is null or projected bbox overlaps viewport; otherwise buckets world-distance from current reference entity (0x7e22) into 0x32/0x20/0x10/0x08 |
| 104 | 000d:d413 |
entity_refresh_recent_proximity_or_visibility_buckets |
24 | Recomputes bucket values for the last four active entries in 0x69ac and notifies backing handles via 000a:6343 when a bucket changes |
Raw 000d Proximity/Visibility Bucket Cluster (new)
Small conservative rename batch from the 000d:cc00-d413 region after the far-call repair exposed the viewport helper in live decompilation.
| Address | Name | Evidence |
|---|---|---|
000d:cc00 |
entity_compute_proximity_or_visibility_bucket |
Returns bucket 0x40 for null or on-screen entities (entity_projected_bbox_overlaps_viewport), else computes a distance bucket from the current reference entity at 0x7e22 with thresholds 0x17d, 0x281, 0x3c1 mapping to 0x32, 0x20, 0x10, 0x08 |
000d:d413 |
entity_refresh_recent_proximity_or_visibility_buckets |
Walks the last four active records in the 0x69ac array, recomputes the same bucket, stores it back to each entry, and calls 000a:6343 when the bucket changes |
000d:cdd0 |
tracked_entity_bucket_prune_invalid_entries |
Walks the 0x69ac array, validates backing handles through 000a:637a, and clears entry handles to 0xffff when the backing object is gone |
000d:cd62 |
tracked_entity_bucket_find_free_main_slot |
Finds the first free entry in the main portion of the 0x69ac array (0 .. count-4) |
000d:cd9a |
tracked_entity_bucket_find_free_aux_slot |
Finds the first free entry in the auxiliary tail portion of the 0x69ac array (count-4 .. count-1) |
Supporting caller notes:
000d:ce1epopulates one0x69acentry by reserving a free slot, computing the initial bucket throughentity_compute_proximity_or_visibility_bucket, storing both current and previous bucket fields, then allocating/linking the backing handle through000a:5f36.000d:d409is a thin wrapper that only callsentity_refresh_recent_proximity_or_visibility_buckets.000d:cfadis an update-or-allocate helper for(param_1,param_2)pairs: it tries to update an existing tracked entry through000a:606a, clears dead entries, and falls back to000d:ce1eallocation when no live match remains.000d:cec5is the auxiliary-slot allocator: it prunes invalid entries, usestracked_entity_bucket_find_free_aux_slot, tags the new entry with byte+0x0a = 1, and seeds its handle via000a:5f36(..., flag=1).000a:606a=tracked_entity_bucket_handle_update_or_alloc— updates the backing handle for an existing tracked bucket entry when possible, or falls back to allocation via000a:5f36if the handle has gone stale.000d:d350=tracked_entity_bucket_set_value— finds a tracked(entity_id, entity_ref)entry in0x69acand pushes a new bucket value into its backing handle through000a:6343.000d:d10b=tracked_entity_bucket_clear_ref_field— clears only the+0x02reference field for all matching entries.000d:d151=tracked_entity_bucket_remove_by_ref— marks matching entries' backing handles for removal and clears the local entry handle/reference fields.000d:d1b1=tracked_entity_bucket_remove_tagged_by_ref— same removal path, but only for entries whose byte+0x0atag is set.
Raw 000a Tracked-Handle Table (new)
The 0x4673 table now reads as the backing handle registry for the 0x69ac tracked-entry bucket subsystem.
That client layer sits on top of a separate generic cache manager rooted at 0x4688..0x46b7, so the current tracked_entity_* names should be read as client-side structure names rather than names for the cache internals themselves.
| Address | Name | Evidence |
|---|---|---|
000a:5f02 |
tracked_entity_handle_find_slot |
Linear scan over 12 entries in the 0x4673 table for a matching 32-bit handle id |
000a:602b |
tracked_entity_handle_is_live |
Returns true only when a handle exists in 0x4673 and its flag word at +0x0a does not have bit 0x0002 set |
000a:60eb |
tracked_entity_handle_mark_remove |
Sets bit 0x0002 in the handle-table flag word and dispatches through the unresolved cleanup path |
000a:612e |
tracked_entity_handle_mark_remove_all |
Iterates all 12 handle-table entries and marks each live handle for removal |
000a:6167 |
tracked_entity_handle_alloc_slot |
Allocates a slot in one of two ranges (0..7 or 8..11) depending on the aux flag; when full, wraps in a ring and evicts via tracked_entity_handle_mark_remove before reusing the slot |
000a:6228 |
tracked_entity_handle_prune_removed |
Reaps entries previously marked with bit 0x0002, clears dead slots, and refreshes high-index entries through 000a:6b2d |
000a:63bc |
tracked_entity_handle_find_by_entity |
Finds the first live handle-table entry whose key/entity word at +0x04 matches the requested entity id |
Current structural read of one 0x4673 entry (stride 0x0c):
+0x00= 32-bit handle id+0x04= key/entity id+0x06= class/group/source-style selector passed in from tracked-entry allocation+0x08= current bucket/value+0x0a= flags (bit0set by aux-slot allocation,bit1= pending removal)
Thin public wrappers on top of the tracked-handle client layer:
000a:5276=entity_bucket_track_default_main— gated by0x45aa; creates or refreshes a main-slot tracked handle with bucket0x40and selector0xff.000a:5294=entity_bucket_track_main— same path, but takes the bucket value as an argument for the main-slot range.000a:52d0=entity_bucket_track_default_aux— aux-slot variant with default bucket0x40.000a:52ee=entity_bucket_track_aux— aux-slot variant with explicit bucket argument.
Raw 000a Generic Cache Manager (new)
Follow-up analysis of 000a:6b2d and the 0x4688..0x46b7 globals shows that this region is a generic cache manager used by the tracked-handle layer, not part of the tracked-entity subsystem itself.
| Address | Name | Evidence |
|---|---|---|
000a:6b2d |
cache_lookup_or_load_entry_by_id |
Fast-paths the last id via 0x46af/0x46b1, otherwise searches 0x469d, evicts older cache slots until there is room under byte budget 0x46a5, allocates a block from the free-list, clears/initializes the payload, records the id, and dispatches through the loader interface at 0x468c |
000a:6a95 |
cache_release_entry_by_slot |
Releases a cached slot by index, clears any client references through 000a:62d8, frees its backing block through cache_free_block_by_slot, and marks the slot id in 0x469d as unused (0xffff) |
000a:6d07 |
cache_alloc_block_for_slot |
Allocates or splits a block from the free-list anchored at 0x4688, tags it with the owning cache slot index, and updates the in-use byte count at 0x46a9 |
000a:6f4d |
cache_free_block_by_slot |
Finds the free-list node for a cache slot, marks it free, subtracts its size from 0x46a9, and coalesces adjacent free blocks |
000a:67d9 |
cache_shutdown |
Tears down the generic cache manager: flushes/reset state, frees slot arrays at 0x4699/0x469d/0x46b3, frees the free-list container at 0x4688, and closes backing state at 0x4691 |
000a:6898 |
cache_set_loader_interface |
Installs the backend loader/callback interface pointer at 0x468c |
Current structural read of the cache globals:
0x4688= intrusive free-list / block-list head used bycache_alloc_block_for_slotandcache_free_block_by_slot0x468c= backend loader interface / callback table;+0x34returns the payload size for an id and+0x0cloads or binds a block after allocation0x4695= base pointer for the raw cache payload arena0x4699= per-slot payload-pointer table0x469d= per-slot cached id table (0xffff= unused)0x46a5= cache byte budget / arena capacity0x46a9= current bytes in use0x46af/0x46b1= one-entry fast-path cache of the last requested id and slot index0x46b3= per-slot block metadata pointer mirror used when releasing or refreshing slots
Follow-up: caller resolution for the public bucket wrappers
0005:3b34=tracked_entity_bucket_alloc_main_if_enabledand0005:3b53=tracked_entity_bucket_alloc_aux_if_enabled; these are the two thin gate wrappers that feed the0x69actracked-entry layer.0005:3b72=tracked_entity_bucket_remove_by_entity_and_ref_if_enabled, which forwards into000d:d086 = tracked_entity_bucket_remove_by_entity_and_refwhen0x45aais set.0006:d404is not a standalone function entry; it is the tail call inside the unnamed destructor block immediately before0006:d414. That block tears down a0x2774dispatch-entry object, pushes(entity_id = 0xdb, entity_ref = *0x7e22), and removes the matching aux tracked bucket entry.0006:d5aeis the analogous tail call immediately before0006:d5be. It tears down the sibling0x2750dispatch-entry object, pushes(entity_id = 0xa4, entity_ref = *0x7e22), and removes the matching aux tracked bucket entry.- The matching constructor sides are
0006:d370and0006:d51a: both allocate/init a dispatch entry, stamp source type8, seed a per-object field from the current reference entity at0x7e22, and then calltracked_entity_bucket_alloc_aux_if_enabled. 0007:cce8is the tail call at the end ofscroll_camera_set_state_params. After the camera scroll state is updated and the new screen-space origin is committed to0x2bb7/0x2bb9, it refreshes the recent proximity/visibility buckets through000d:d409when0x45aais enabled.
Follow-up: 0x45aa gate and cache loader installation
0x45aanow reads astracked_entity_bucket_system_enabled, not a one-off debug/test flag. It gates all three public wrapper helpers above and the camera-side refresh inscroll_camera_set_state_params.- The enable bit is set only by the unlabeled init block around
000a:51d0..5222: that code stores the incoming backend/interface pointer into0x45ab/0x45ad, installs it into0x468cviacache_set_loader_interface, allocates the tracked-handle table (000a:5e00) and the 32-entry bucket array (000d:cca3(0x20)), then sets0x45aa = 1. - The matching unlabeled shutdown block starts at
000a:5223: it checks0x45aa, tears down the tracked-handle table through000a:5e59, frees the bucket array through000d:ccec, and only then clears/uses the backend pointer state at0x45ab. 0x468cremains best named as a genericcache_loader_interface: the current verified evidence is a cache backend callback table (+0x34= size query,+0x0c= load/bind callback) shared by the tracked-entry service. The newly traced gameplay callers prove this service participates in camera/entity-interest updates, but they are not yet strong enough to justify an audio-specific or resource-specific subsystem rename.
Follow-up: init/shutdown entry points around 000a:51xx
000a:5186=tracked_entity_bucket_system_init.000a:538e=tracked_entity_bucket_system_init_if_configured; it only calls the init routine when config/feature gate0x89f4is non-zero.000a:5223=tracked_entity_bucket_system_shutdown.0x45ab/0x45adnow read astracked_entity_bucket_backend_object, a cached backend/interface object pointer used by init/shutdown in addition to the lower-levelcache_loader_interfacecallback table at0x468c.tracked_entity_bucket_system_initfirst allocates a rotating buffer via0009:3600, lazily createstracked_entity_bucket_backend_objectthrough0009:5600when absent, installs that object intocache_loader_interface, allocates the tracked handle table via the missing function entry at000a:5e00, allocates the 32-entry0x69acbucket array via000d:cca3(0x20), then setstracked_entity_bucket_system_enabled.tracked_entity_bucket_system_shutdownis called from the wider engine teardown routine at0004:621b; it tears down the tracked handle table, frees the0x69acbucket array, calls backend-object vtable slot+0x38with(3, backend_object), and clearstracked_entity_bucket_backend_object.
Follow-up: backend object constructor at 0009:5600
- The missing raw-import function entry at
0009:5600has now been recovered in-place ascache_backend_object_initwith body0009:5600-0009:57b9. - Current verified behavior is still structural, but stronger than before:
- Allocates a
0x20-byte object when the caller passes null. - Initializes embedded DOS file-handle state via
dos_file_handle_init(0009:1c00). - Seeds internal method-table / state fields at object offsets
+0x08,+0x0c,+0x10,+0x14,+0x16,+0x18, and+0x1c. - Dispatches through the object method table during construction and returns the object pointer later cached at
0x45ab/0x45ad.
- Allocates a
- This is enough to justify the structural
cache_backend_object_initname, but not yet enough to promote the backend object to a file-, audio-, or resource-specific subsystem name. - Verified callback roles inside
cache_lookup_or_load_entry_by_id:- backend vtable
+0x34= size query callback for a cache entry id (used before allocation/eviction). - backend vtable
+0x0c= load/bind callback that populates the newly allocated slot buffer for the requested id.
- backend vtable
Follow-up: missing function entry at 000a:5e00
- The missing raw-import function entry at
000a:5e00has now been recovered in-place astracked_entity_handle_table_initwith body000a:5e00-000a:5e58. - Verified behavior: if
0x4672is clear, it allocates0x90bytes at0x4673/0x4675, aborts throughruntime_init_or_aborton allocation failure, calls000a:577dand local helper000a:5e95, then sets0x4672 = 1. - Matching teardown helper promoted alongside it:
000a:5e59=tracked_entity_handle_table_shutdown. - Current structural names for that local state:
0x4672=tracked_entity_handle_table_active0x4673/0x4675=tracked_entity_handle_table
- This matches the existing client-layer interpretation:
0x4673holds 12 handle entries (12 * 0x0c = 0x90bytes), and000a:5e00is the table allocator/initializer used by the tracked bucket subsystem startup path.
Follow-up: missing function entry at 000a:6600
- The missing raw-import function entry at
000a:6600has now been recovered in-place ascache_initwith body000a:6600-000a:67d8. - Verified behavior from the repaired body:
- Stores the requested slot count in
0x46ad. - Allocates the per-slot payload-pointer table at
0x4699(count * 4) and aborts on failure. - Seeds each slot with allocator-returned pointers / zero low words before running local pointer normalization helpers (
0009:c496,0009:c400,0009:c6ae). - Queries/derives the cache arena size, subtracts
0x1000, and stores the byte budget in0x46a5. - Allocates the arena backing object at
0x4691, derives the payload base pointer0x4695, and aborts throughseg091_func_00fdon failure. - Allocates the per-slot block-metadata mirror at
0x46b3(count * 4) and per-slot cached-id table at0x469d(count * 2). - Allocates and initializes the free-list head object at
0x4688, then calls local helper000a:68aabefore returning.
- Stores the requested slot count in
Follow-up: cache reset / handle-table helpers
000a:68aa=cache_reset_runtime_state.- Verified role: shared cache reset/bootstrap helper called from
cache_init,cache_shutdown, and one wider external reset path. It allocates per-slot arena-header / metadata nodes, rebinds slot pointers to the arena base, clears the cached-id table, seeds the free-list head, and resets0x46a9(bytes in use) plus0x46af(last-id fast path). 000a:703e=cache_compact_arena_blocks.- Verified role: compacts live cache arena blocks into earlier free holes when
cache_alloc_block_for_slotcannot find a large-enough free block, updates per-slot payload pointers, and merges adjacent free-list headers afterward. 000a:5e95=tracked_entity_handle_table_clear_and_dispatch.- Verified role: when
tracked_entity_handle_table_activeis set, it zeroes the full0x90-byte handle table at0x4673, resets adjacent local state at0x4677/0x4679/0x467b, then dispatches through the remaining thunked follow-up path. 000a:5339=tracked_entity_handle_mark_remove_all_if_enabled.- Verified role: thin gate wrapper that only forwards to
tracked_entity_handle_mark_remove_allwhentracked_entity_bucket_system_enabledis set.
Follow-up: external reset paths using the cache/tracked-handle layer
- The unlabeled path around
0004:25a9now has enough local evidence to classify as an external reset sequence: it callscache_reset_runtime_state, thentracked_entity_handle_table_clear_and_dispatch, then continues through additional tracked-entry/cache-side refresh helpers (000d:cd22,000d:44b3,0006:ae66,0006:ae00, etc.). - The unlabeled path around
0004:eb80is a conditional tracked-bucket reset/update sequence: whentracked_entity_bucket_system_enabledis set, it callstracked_entity_handle_mark_remove_all_if_enabled, thentracked_entity_handle_table_clear_and_dispatch, thencache_compact_arena_blocks, before resuming its outer flow. - These caller sites strengthen the current interpretation that the
0x45aa/0x4673/0x4688..46b7layer is a shared runtime cache service used by gameplay/system reset flows, but they still do not expose a resource-specific subsystem name by themselves.
Follow-up: repaired seg004 reset-path function objects
0004:2592had been mis-modeled as a one-instruction thunk body. It has now been repaired to the full body0004:2592-25deand renamedruntime_cache_reset_sequence.- Current verified behavior for
runtime_cache_reset_sequence:- Calls
0008:7bfe. - Calls
game_mode_init(*(0x27c4)). - Calls an import-resolved follow-up site at
0004:25a4, now annotated asNE IMPORT -> ASYLUM.24. - Then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers, and resumes the wider runtime reset flow.
- Calls
- Known caller so far:
0004:262dinside the tiny wrapper at0004:2620, which sets byte+0x40on the object at0x6828before invoking the reset sequence. 0004:eb1fhad also been truncated. It has now been repaired to the full body0004:eb1f-eb9band renamedentity_dispatch_entry_ctor_0f3a_with_cache_reset.- Verified behavior for
entity_dispatch_entry_ctor_0f3a_with_cache_reset:- Allocates/initializes an entity dispatch entry when needed.
- Stamps entry type
0x0f3a. - Stores its two word payload fields from the incoming args.
- Runs local setup through the embedded helper at
0004:ebf4, which dispatchesentity_dispatch_reset_all(*0x7e22, 0x00f0)and, when the local flag plus global0x0ee1allow it, allocates a type0x0f5edispatch entry and passes it toentity_pair_sync_b. - When
tracked_entity_bucket_system_enabledis set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through0009:b1c3in phase0.
- The sibling at
0004:eb9cremains separate and valid; it builds the same0x0f3aentry type without the extra cache-reset tail, so the repaired0004:eb1fboundary stops cleanly at0004:eb9b.
Follow-up: new local helper classification around the repaired seg004 path
0004:ea00is now a real function object namedentity_dispatch_entry_alloc_type_0f5ewith body0004:ea00-0004:ea46.- Verified behavior for
entity_dispatch_entry_alloc_type_0f5e:- Reuses the incoming FAR pointer when non-null; otherwise allocates
0x33bytes throughmem_alloc_far. - Initializes the entry through
entity_dispatch_entry_init. - Stamps the entry type word at
+0x00to0x0f5ebefore returning it.
- Reuses the incoming FAR pointer when non-null; otherwise allocates
0009:b1c3is now renamedallocator_phase_finalize_passand remains intentionally allocator-scoped rather than subsystem-specific:- Both known call sites pass only phase bytes
0or1. - It forwards that byte twice to the object rooted at
0x4588through vtable slot+0x08. - It then sweeps the allocator head table at
0x8724up to the active head count at0x879c, callingallocator_head_finalize_sweepon each entry.
- Both known call sites pass only phase bytes
- That evidence is strong enough for the allocator-side rename, but not yet enough to promote
0x4588to a more specific subsystem name.
Follow-up: seg082 allocator cluster (0009:a229, 0009:af87, 0009:b06b, 0009:b1c3)
0009:a229is now verified as the public size-only wrapper around the seg082 allocator path.- Caller evidence:
saveslot_table_clearrequests0x2800bytes through0009:a229, stores the returned FAR pointer at0x2ba3/0x2ba5, then zeroes the result in0x400-byte chunks.- The wrapper lazily initializes the allocator on first use through
0009:bcb9, then callsallocator_try_alloc_from_head_table(size, default_tag, 0xff).
0009:bcb9is now annotated as the one-time lazy initializer for this path.- It parses an optional
-xtuning value from the PSP command line, clamps the derived percentage into0x14..0x50, then seeds local seg082 helpers before setting init flag0x4096 = 1.
- It parses an optional
- Table structure around
0x8724is tighter now:0x8724is an array of0x0c-byte allocator heads.0x879cis the active head count / table limit.- The per-node size/value encoding used under each head is manipulated through
0009:c628and0009:c6ae, which read/write a packed 32-bit quantity split acrossword + byte + bytefields.
0009:af87is now annotated as the free-space probe for the same cluster.- It walks the node chain rooted at
0x8724. - For each node, it accumulates
node_size - 9into a running total and tracks the largest single free block. - Known callers include
cache_initand the seg013 path at0004:833b, both of which use it to size subsequent allocation work.
- It walks the node chain rooted at
0009:b06bis now renamedallocator_try_alloc_from_head_table.- It validates the requested size, reserves a temporary work token through
0009:e15f, and scans the0x8724allocator head table in0x0c-byte entries via the local helper at0009:a336. - On a successful fit, it commits the result through
0009:e2b4, clears failure flag0x4098, and returns the allocated FAR pointer. - When a pass does not find a fit, it interleaves up to two finalize phases through
allocator_phase_finalize_pass(phase)before the final retry, then releases the work token through0009:e1f6.
- It validates the requested size, reserves a temporary work token through
- The formerly missing callee at
0009:a336has now been recovered in-place asallocator_head_try_alloc_blockwith body0009:a336-0009:a5d0.- It is the per-head first-fit allocator helper used by
allocator_try_alloc_from_head_tablewhile sweeping the0x8724allocator head table. - It normalizes the requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds the local
0x0anode header overhead, and enforces a minimum allocation size of0x10bytes. - It walks the node chain for one allocator head until it finds a free span large enough.
- On success it unlinks the chosen free node, either consumes it whole or splits off a remainder node when at least
0x10bytes remain, stores the owner/tag word, and returnspayload_ptr + 0x0a. - On failure for that head it returns
0, which matches the calling pattern inallocator_try_alloc_from_head_table.
- It is the per-head first-fit allocator helper used by
- Boundary follow-up from the same read-only scan:
- The adjacent missing body at
0009:a5d1has now also been recovered in-place asallocator_head_free_blockwith body0009:a5d1-0009:a960. - It is the per-head free helper paired with
allocator_head_try_alloc_block. - It rebuilds the node header from a payload pointer (
payload - 0x0a), validates the owner/tag word against the expected caller-supplied tag, reinserts the block into one allocator head, and coalesces with adjacent free neighbors when possible. - Its earlier branch targets
0009:a7a1and0009:a7b8are now confirmed to be internal labels, not separate function entries.
- The adjacent missing body at
0009:a961is now better constrained as the per-head finalize sweep used byallocator_phase_finalize_pass.- It walks one
0x8724head's node chain, skips odd-tagged spans, coalesces or rewrites eligible spans, and updates head/back-pointer links when deferred space needs to be merged back into the chain. - This strengthens the current interpretation that
allocator_phase_finalize_passis allocator-side callback/finalize glue rather than a cache-specific public API.
- It walks one
0009:b224is now namedallocator_free_block_by_ptr.- Current verified behavior: converts the payload pointer back through the local header helpers, scans the
0x8724head table for the owning range, dispatches toallocator_head_free_block, and aborts if no owning head is found. - Known wrappers
0009:a24fand0009:a27aare now clearly small checked entry points into this free-by-pointer path.
- Current verified behavior: converts the payload pointer back through the local header helpers, scans the
- With both recovered bodies in place, the seg082 cluster now has a verified alloc/free pair at the per-head level:
allocator_head_try_alloc_block(0009:a336)allocator_head_free_block(0009:a5d1)allocator_free_block_by_ptr(0009:b224)
allocator_phase_finalize_passnow has a corrected single-byte phase parameter in Ghidra; the object at0x4588is still left unnamed because its subsystem role is not yet strong enough.- This narrows the remaining ambiguity around
allocator_phase_finalize_pass: the unresolved part is now the role of the object at0x4588, not the local allocator mechanics around0x8724.ASYLUM.24is still not identified from the current evidence.
Follow-up: 0x4588 object-role evidence (Priority 1 start)
- A direct instruction scan found real uses of
0x4588/0x458aeven though normal static reference APIs were not materializing them. - Current verified behavior from those uses:
entity_conditional_render_dispatch(0009:9216) calls through the runtime-installed object at0x4588via vtable slot+0x0cwhen the entity flags allow the alternate path andparam_2 == 0.000a:4a56is a one-shot teardown/reset path for the same object: it checks a local once-flag at0x4595, clears0x4588when non-null, optionally performs a vtable+0x0ccallback when0x4590 != 0x458c, then calls vtable slot+0x04followed byFUN_0009_0d30().- The two callback sync sites inside
sprite_node_get_or_traverse(000a:b9e5and000a:ba66) only emit vtable+0x0cwhen the candidate two-word pair differs from the current pair, then immediately mirror that pair through000b:1e39using global sprite/object pointer0x4f38/0x4f3a. - A read-only data probe of
0x4588in the current database returned all zero bytes, so the object pointer is null-initialized statically and likely installed later at runtime.
- Conservative conclusion:
- The
0x4588object now looks like a runtime-installed callback / dispatch object that participates in conditional render or presentation-side flow and has an explicit teardown path. - That is enough for comments and ledger progress, but still not enough to safely rename
0009:b1c3or the global itself to a concrete subsystem name.
- The
Follow-up: 0x4588 install/clear windows from the no-function hit list
- A read-only PyGhidra instruction-window pass against an unlocked project clone confirmed that the planned no-function hit list is real code, not aligned data.
- New verified lifecycle evidence:
000a:4932and000a:4936store the same incoming dword into0x4590and0x458c, then000a:493estores the incoming FAR object pointer into0x4588.0004:5b8cand0004:5bbfboth clear0x4588immediately before the fatal/reporting-style seg091 call through000a:454d.0004:5ea7and0004:6430both clear0x4588and then immediately run the one-shot teardown path000a:4a56(1).000a:b9e5,000a:ba66,000d:9d5e, and000d:a3b7all push a two-word value pair followed by the0x4588FAR pointer and call the object's vtable slot+0x0c.entity_conditional_render_dispatchremains the only named caller found so far for the same slot, but it passes a single literal0x0101argument instead of a two-word pair.
- Conservative conclusion after the window pass:
0x4588is definitely a nullable runtime-installed FAR object with explicit install, clear, callback, and teardown transitions.- The unresolved part is now its concrete subsystem identity, not whether the object lifecycle is real.
- The best next cheap win is no longer broad instruction searching; it is caller-side recovery around the still-unbounded
000a:b9e5/000a:ba66and000d:9d5e/000d:a3b7windows.
Follow-up: seg138 caller-side dispatch-entry emission helper
FUN_000d_938cis now confirmed as a real caller-side helper with body000d:938c-000d:9583, and an evidence-preserving decompiler comment was added in Ghidra instead of forcing a speculative rename.- Current verified behavior from direct MCP decompile/disassembly:
- When the mode/global gate is not already in the
0x13:0x0008state and entity byte+0x33is clear, it allocates a scratch palette buffer, constructs a dispatch entry, sets type0x051e, and initializes runtime state throughentity_dispatch_entry_init_runtime_statewith entry kind0x3c. - Later in the same helper it constructs a second dispatch entry from the current palette globals at
0x4e4:0x4e6, again sets type0x051e, and initializes runtime state with entry kind0x14and active-state parameters(1,0,1). - Both created entries are polled until their runtime flag word clears bit
0x0002, after which the helper redraws the global sprite path, syncs display-state byte0x58efrom the entity when the global display object exists, callsFUN_0006_16e1, clearsg_active_dispatch_entry_farptr[+0x40], and finally dispatches through the input object's vtable slot+0x08.
- When the mode/global gate is not already in the
- Conservative conclusion:
- seg138 now has one more verified caller tying
entity_dispatch_entry_init_runtime_stateto palette/presentation-side emission work around entity cleanup and redraw flow. - This narrows the open question from "is the dispatch-entry lane real?" to "what exact presentation/event subsystem does this lane belong to?"
- seg138 now has one more verified caller tying
Follow-up: seg137 palette and dispatch-entry helper family
- A larger direct MCP rename batch now stabilizes a coherent seg137 palette helper family:
000d:82ea=dispatch_entry_create_black_palette_state_active000d:83be=dispatch_entry_create_grayscale_palette_state_active000d:85da=vga_palette_set_all_black000d:8653=vga_palette_set_all_white000d:86cc=vga_palette_set_all_rgb000d:875d=dispatch_entry_create_solid_palette_state_active000d:88b2=dispatch_entry_create_solid_palette_state000d:8a47=dispatch_entry_create_black_palette_state
- Current verified behavior from direct MCP decompile/disassembly:
vga_palette_set_all_blackcorrects the earlier overreach rename at000d:85da: it allocates a0x100-entry palette buffer filled with zero RGB triplets, writes it to VGA, and frees the scratch buffer. The previousmap_object_set_dirty_flagname was not supported by the recovered body.vga_palette_set_all_whiteis the same helper shape with all three RGB components initialized to0x3f, then written throughvga_palette_write.vga_palette_set_all_rgbtakes caller-supplied RGB bytes, replicates them across a0x100-entry palette buffer, writes the result to VGA, and frees the scratch palette.dispatch_entry_create_black_palette_state_activeanddispatch_entry_create_black_palette_stateboth build a runtime-state dispatch entry of type0x051efrom a black0x100-entry palette buffer; the_activeform first setsg_active_dispatch_entry_farptr[+0x40] = 1, while the quiet form does not.dispatch_entry_create_grayscale_palette_state_activereads the current VGA palette, normalizes each triplet by copying the first channel across all three RGB bytes, then builds a runtime-state dispatch entry from that grayscale palette while marking the active dispatch entry.dispatch_entry_create_solid_palette_state_activeanddispatch_entry_create_solid_palette_statevalidate0..0x3fRGB inputs, fill a scratch0x100-entry palette buffer with that solid color, and build the same0x051eruntime-state dispatch entry, again split into active-marking and quiet variants.
- Additional caller-side comments were added instead of speculative renames on:
000d:84f4(current-palette dispatch entry paired with a second object of type0x68bfthroughentity_pair_sync_b)000d:89c6(parameterized current-palette runtime-state wrapper with active-state flags)
- Conservative conclusion:
- seg137 is now materially beyond a foothold: it contains a coherent palette-write and palette-backed dispatch-entry emission family tied to the same runtime-state constructor lane.
- The remaining uncertainty is higher-level script/event meaning, especially the paired
0x68bfobject and the exact role of the0004:5ad4-5b6ecaller sequence, not the local palette-helper mechanics.
Follow-up: seg005 startup/display orchestration and seg136 active dispatch entry
- A new direct MCP recovery pass stabilized one high-value handoff path and its nearby active-dispatch helpers:
0004:60c0-0004:621arecovered asFUN_0004_60c0with a decompiler comment summarizing the orchestration flow.000d:7600-000d:760dcreated and renamed toactive_dispatch_entry_mark_enabled.000d:760erenamed toactive_dispatch_entry_mark_disabled.000d:761crenamed toactive_dispatch_entry_create_default.
- Current verified behavior from direct MCP decompile/disassembly:
FUN_0004_60c0is a recovered startup/display orchestration path: it performs broad setup calls, reads the live VGA palette, validates a caller-provided object through vtable slot+0x0c, drives the sprite/object lane through0x4f38, runs the seg137 palette and dispatch-entry helper family, creates the default active dispatch entry throughactive_dispatch_entry_create_default, programs mouse interrupt state via seg056INT 33hwrappers, then hands off into the still-unrecovered0004:1e00routine.- The old
0004:5ad4-5b6ecaller sequence is now confirmed as one internal sub-sequence within that larger recovered function rather than an isolated orphan. active_dispatch_entry_create_defaultallocates or reuses a0x42-byte dispatch entry, stamps type0x687f, installs a callback-table pointer through0x39ca, sets event type0x248, sets update period0x1e, marks the entry as the global active dispatch entry at0x6828, toggles the local+0x40state byte throughactive_dispatch_entry_mark_enabledandactive_dispatch_entry_mark_disabled, then enables the timer wrapper.active_dispatch_entry_mark_enabledandactive_dispatch_entry_mark_disabledare tiny helpers that set or clearg_active_dispatch_entry_farptr[+0x40]respectively.
- Conservative conclusion:
- seg005 now has its first strong high-value foothold in a startup/display handoff path, even though the downstream
0004:1e00target still needs recovery and naming. - seg136 now has a concrete active-dispatch-entry foothold rather than being empty ledger space.
- seg005 now has its first strong high-value foothold in a startup/display handoff path, even though the downstream
Follow-up: seg005 large runtime/display handoff body recovered at 0004:1e00
0004:1e00-0004:2420has now been recovered as a real function object in Ghidra asFUN_0004_1e00with an evidence-preserving comment, replacing the earlier no-function gap.- Current verified behavior from the recovered body:
- It begins by forcing an all-black palette through
vga_palette_set_all_black, then performs several startup/display setup calls before manipulating the active dispatch entry at0x6828. - It constructs two animation-side objects through
animation_ctor_variant_b(000e:2860) using DS-local descriptors at0x04aeand0x04b2, then waits on global words around0x8a94-0x8a98when the alternate startup flag path is active. - It conditionally calls
sprite_node_get_or_traverse(000a:b988) after a seg122 helper path, toggles seg064 gate helpers, and then enters a larger resource/object processing region that uses globals rooted at0x4aa,0x4ac, and0x7e22. - The mid-body is now better classified as a non-return transition driver rather than a generic handoff stub: it branches on the returned
SIstate after the sprite/object traversal, with one path performing the fuller runtime/display setup, one path taking a small local special-case handler (0004:2661), and one path marking the active dispatch entry before callingruntime_callback_object_teardown_once(1). - The
SI == 2special-case branch is now slightly tighter: its local helper0004:2661forwards intoFUN_0004_25df, which is a small type-stamped dispatch-entry constructor that allocates when needed, runsentity_dispatch_entry_init, stamps type0x04b6, and stores the caller-supplied mode/state word at+0x32. - The fuller setup path clears and restores active-dispatch state, calls through the
0x2bd8object vtable, restores the live palette through0009:6f5a, re-runs render/dispatch rectangle helpers (entity_conditional_render_dispatch,entity_rect_compare_and_dispatch), and finishes through the seg126 trampolinethunk_callf_0000_ffff_000c_82f9. - The recovered tail confirms a clean end at
0004:2420, with the next separate function beginning at0004:2421.
- It begins by forcing an all-black palette through
- Conservative conclusion:
- The main blocker on seg005 is no longer structural recovery; it is naming the exact state entered by this now-navigable startup/display transition driver.
- The presence of
animation_ctor_variant_b, palette forcing, active-dispatch toggles, sprite-node traversal, and the0x2bd8vtable lane makes this look more like a real mode/state transition than a one-off helper, but the exact gameplay, intro, or front-end label still needs one more caller/data pass.
Follow-up: seg126 wrappers feeding the 0004:1e00 transition lane
- Two previously unbounded seg126 callers around the recovered seg005 handoff are now real functions in Ghidra:
FUN_000c_7412(000c:7412-000c:7432)transition_preentry_run_until_complete_or_abort(000c:c9f4-000c:ca1c)
- The larger fallthrough body rooted at
000c:c890is now also recovered as a real function object:transition_preentry_release_resources(000c:c890-000c:c9f3). - The previously blocked seg126 helpers are now also recovered as standalone functions after overlap repair in the surrounding namespace:
transition_preentry_setup_resources(000c:c63a-000c:c88f)transition_preentry_step_script(000c:ca1d-000c:cd52)
- Current verified behavior from direct MCP recovery/decompile:
FUN_000c_7412is a compact wrapper into the seg005 transition lane: it clears the redraw state on the sprite/object pair rooted at0x5e82:0x5e84, forces a black palette throughvga_palette_set_all_black, runs seg126 pre-entry state prep throughFUN_000c_c9f4, then tail-callsFUN_0004_1e00.transition_preentry_run_until_complete_or_abortis the short pre-entry state wrapper: it runs local seg126 setup helpers, repeatedly executes a local prep loop while local ready byte0x62feis clear and external gate word0x31a2remains zero, ticks the local palette-fade controller on each pass, then dispatches intotransition_preentry_release_resourcesbefore returning to callers that continue intoFUN_0004_1e00.transition_preentry_release_resourcesis the cleanup/finalize body behind that wrapper: it releases the paired temporary presenter objects at0x8c5cand0x8c60, conditionally frees the local stream buffer at0x6301:0x6303, runs palette/render reset, conditionally constructs animation state throughanimation_ctor_variant_aon local storageDS:0x6341when0x844and0x62feare both set, then immediately marks byte+0x40on the shared global owner at0x6828, marks the active dispatch entry, primes sprite redraw state, drains the event queue, and zeroes0x8a94-0x8a98before returning.wait_for_vga_vertical_retrace(000c:c62c-000c:c639) is now recovered as a real helper: it polls VGA status port0x3dauntil the vertical-retrace edge and is called from both the seg126 pre-entry loop and nearby fade/update paths.transition_preentry_setup_resourcescaptures baseline coordinates into0x8c58:0x8c5a, constructs two paired temporary presenter objects at0x8c5c:0x8c5eand0x8c60:0x8c62through000a:9748with preset IDs0x10and0x11, stages the local stream buffer at0x6301:0x6303, seeds palette/render state, and resets the neighboring control globals at0x62fa-0x6318before returning.- The shared seg088 helpers behind that object family are now tighter too:
text_renderer_measure_string_width(000a:30aa) returns width through the object's virtual measure path, andtext_renderer_draw_string_at(000a:30d7) stores x/y into the object and draws a null-terminated string. That is enough to reclassify0x8c5cand0x8c60as a paired temporary text-renderer lane rather than generic sprite/object state. transition_preentry_step_scriptearly-outs while fade state0x630ais active or when the tracked object position at0x2de4+0x40/+0x42is unchanged, decrements the local countdown at0x62fa:0x62fc, interprets the0x6301byte stream with control values including0x21,0x23,0x24,0x26,0x2a,0x40, and0x5e, uses0x62ffas the stream cursor, uses the temporary text-renderer objects at0x8c5cand0x8c60for layout/render work, sets ready flag0x62feon the0x23case, uses0x6305as a one-shot redraw latch around the0x26/0x2acontrol path, and leaves0x630a/0x630bto the neighboring palette-fade controller.- Direct follow-up on the old
0x31a2ambiguity now points away from local script state:event_queue_state_resetat0008:89c1clears0x31a2, interrupt-side queue code around0008:a283/0008:a314increments and decrements it, and several busy-wait helpers (000c:e4d8,000c:e546,000c:e5c6) spin on it. In the seg126 wrapper this makes0x31a2a plausible external input/event break gate rather than part of the local pre-entry bytecode interpreter. - The seg136 owner flag at
g_active_dispatch_entry_farptr[+0x40]is now less abstract too.active_dispatch_entry_mark_enabled/active_dispatch_entry_mark_disabledstill force it high or low during entry setup, but nearby seg136 helpers also copy it into fresh entries, clear it when the current owner becomes inactive, and decrement it only while0x31a2 > 0. That ties the0x6828owner lane more directly to the same external input/event gate that seg126 polls. - The same segment now also has a clearer transition-control shell around that prep body:
thunk_callf_0000_ffff_000c_827dis the pre-transition side that restores redraw/event state, runs the0x2bd8controller callbacks, redraws the0x5e82:0x5e84sprite/object pair, and leaves local state bytes set for the subsequentFUN_0004_1e00call.thunk_callf_0000_ffff_000c_82f9is the post-transition side that resets the slot table throughFUN_0008_39e9, clears local state bytes, runs the0x5e82:0x5e84cleanup path, and returns the lane to its quiescent state.FUN_000c_834ais a small guard wrapper that conditionally callsFUN_000c_8231()when gate byte0x85fis set; this same helper is used at the start ofFUN_0004_1e00and in the local seg126 caller family.
- This is now enough to tie the seg076 caller at
000c:742cto the same startup/display transition lane already reached fromFUN_0004_60c0.
- Conservative conclusion:
- seg126 is now beyond a mere foothold: it contains a coherent transition-entry and transition-exit control lane around the seg005 startup/display state, with pre-entry prep, guarded entry, post-transition cleanup, and local state/fade integration.
- The unresolved part is the exact higher-level UI/transition role of the paired text-renderer lanes at
0x8c5cand0x8c60, the precise event semantics of external gate0x31a2, and the exact relationship between local animation storageDS:0x6341and the shared global owner at0x6828whose+0x40byte follows that same gate, not whether the transition lane itself is real.
Follow-up: seg127 palette fade controller tied to the same transition lane
- The nearby seg127 state/fade controller is now anchored by verified functions in Ghidra:
palette_fade_begin_full_up(000c:c600-000c:c615)palette_fade_begin_full_down(000c:c616-000c:c62b)transition_palette_fade_tick(000c:cd53-000c:cd75)transition_palette_fade_begin(000c:cd76-000c:cddd)transition_palette_fade_out_step(000c:cdde-000c:ce56) andtransition_palette_fade_in_step(000c:ce57-000c:cecb)
- Current verified behavior from direct MCP recovery/decompile:
palette_fade_begin_full_upandpalette_fade_begin_full_downare fixed-range wrappers overtransition_palette_fade_begin: both use the full0x80-entry palette range with step size4, differing only in direction/state (2for up,1for down).- The current local callers are now visible too:
000c:cd1ainvokespalette_fade_begin_full_upfor theES:[DI] == 0x26case and sets0x6305 = 1, while000c:cd3fand000c:cb06invokepalette_fade_begin_full_downwhen the fade controller is idle. transition_palette_fade_begintakes a palette source pointer, start index, count, step amount, and direction/state, stores them into local controller state at0x630e-0x6316, sets active flag byte0x630a = 1, stores the direction/state word at0x630b, then runs the local prep helper and one immediate fade step.transition_palette_fade_tickis the small controller gate over that state: it returns when inactive, dispatches totransition_palette_fade_out_stepwhen0x630b == 1, and totransition_palette_fade_in_stepwhen0x630b == 2.transition_palette_fade_out_stepandtransition_palette_fade_in_stepiterate over palette range[0x6312 .. 0x6312 + 0x6314), writing VGA DAC entries through ports0x3c8/0x3c9from the source palette at0x630e:0x6310while updating brightness offset byte0x630dby step0x6316; both clear active flag0x630awhen the fade reaches its terminal black or full-bright state.- This controller is tied directly into the transition lane already under study:
transition_preentry_run_until_complete_or_abortcallstransition_palette_fade_tick, andtransition_preentry_setup_resourcesseeds the neighboring seg126 controller state at0x62fa-0x6318before the script interpreter starts using it.
- Conservative conclusion:
- seg127 is no longer just a foothold; it is a real palette fade controller subsystem adjacent to the same startup/display entry path, with verified initializer, dispatcher, step bodies, fixed-range wrappers, and caller-side state gating.
- The remaining question is not the local fade mechanics, but which exact transition states and tracked objects choose the fade direction and palette source.
Follow-up: seg049 watch/camera controller object at 0x2bd8
- The longstanding
0x2bd8watch/camera lane is now tighter and partially corrected from older notes: it is not just a raw watched-entity pointer, but a real controller object lane. - Current verified behavior from direct MCP recovery/decompile:
watch_entity_controller_create(0007:ba45) allocates or reuses an object, runs the local constructor path at000a:8627, stamps type0x2c2b, stores the resulting FAR pointer globally at0x2bd8, sets event type0x0219, and installs callback target0x2be4through the callback table at0x39ca.watch_entity_controller_create_global(0007:ba00) is a thin wrapper that constructs the default global controller and stores the returned FAR pointer at0x2bd8:0x2bda.watch_entity_controller_dispatch_if_present(0007:ba13) is the paired non-null dispatcher that calls controller vtable slots+0x2cand+0x30when the global exists.- Existing callers in the seg005 transition lane (
FUN_0004_1e00) call through the same0x2bd8vtable+0x2cslot before palette restore and post-transition redraw prep, which strengthens the interpretation that this is a real watch/camera-side controller object participating in display-state transitions.
- Conservative conclusion:
- seg049 now has a real foothold in the watch/camera controller subsystem.
- The remaining ambiguity is the exact distinction between the controller object at
0x2bd8and the ultimately watched entity or map object it may point at or manage, not whether the controller itself is real.
Follow-up: seg108 sprite/object flag lane at 0x4f38
- The global sprite/object lane at
0x4f38:0x4f3anow has two recovered flag helpers in addition to the existing redraw/timer sync evidence:sprite_object_clear_flag40_if_present(000b:2b08-000b:2b1f)sprite_object_set_flag40_if_present(000b:2b20-000b:2b37)
- Current verified behavior from direct MCP recovery/decompile:
sprite_object_clear_flag40_if_presentchecks whether the global sprite/object FAR pointer at0x4f38is non-null and clears bit0x40in the word at object offset+0x32.sprite_object_set_flag40_if_presentperforms the same guard and sets bit0x40at that same+0x32word.- This fits the already-confirmed sync behavior in
sprite_node_get_or_traverse, where0x4588callback emissions are immediately mirrored throughFUN_000b_1e39using the global sprite/object pointer. Together, that narrows0x4f38to an active sprite/object instance whose state bits are toggled during the same startup/display transition path that usesFUN_0004_60c0andFUN_0004_1e00.
- Conservative conclusion:
- seg108 now has a real foothold in the active sprite/object state lane.
- The remaining ambiguity is what bit
0x40means semantically and how the0x4f38object relates at a higher level to the0x2bd8watch/camera controller and the0x4588callback object.
Follow-up: ASYLUM.24 vs nearby ASYLUM ordinals
ASYLUM.24remains unresolved by name, but its call pattern is now narrower.- In
runtime_cache_reset_sequence(0004:2592), it is a parameterless import call placed aftergame_mode_init(*(0x27c4))and beforecache_reset_runtime_stateplus the tracked-handle/cache-side reset tail. - That makes it look like a module-level reset/init hook rather than a per-object method.
- In
- Nearby
ASYLUMordinals in the seg011 caller at0004:6f15show a different pattern:ASYLUM.36returns an object-like handle that is used immediately through indirect vtable calls.ASYLUM.37is then called with explicit arguments against that object flow.
- Current conservative conclusion:
ASYLUM.24is probably from the same external module family, but it does not currently match the object-construction / object-method calling pattern observed forASYLUM.36andASYLUM.37.- Keep the import unresolved by name until another caller or string anchor narrows the exact module role.
Repair note: overlapping bad function body
- Recovery of
cache_initrequired a conservative boundary repair: a stray function objectFUN_000a_eee3had incorrectly claimed body range000a:6710-000a:fe79, blocking creation of the realcache_initbody. - The bad overlap was removed,
cache_initwas created at000a:6600-67d8, andFUN_000a_eee3was recreated conservatively as the contiguous visible body000a:eee3-f00b.
Current interpretation of the 0x69ac / 0x4673 client layer:
- It is an entity-linked consumer of the generic cache manager.
- Its bucket values (
0x40,0x32,0x20,0x10,0x08) still look attenuation- or priority-like rather than purely visibility-like. - That is enough to keep the structural names, but not enough yet to safely promote the subsystem to a concrete audio/effect name.
Entity Table Pointers (DS-relative, discovered in tier 5):
| DS Offset | Type | Stride | Purpose |
|---|---|---|---|
0x7dfd |
byte[] | 1 | Entity flags byte (entity_get_flags_byte) |
0x7e01 |
word[] | 2 | Entity class flags (bit 8 = has parent in hierarchy) |
0x7e05 |
word[] | 2 | Entity sprite/visual index |
0x7ded |
word[] | 2 | Entity parent/hierarchy index |
0x7df1 |
word[] | 2 | Entity base type word |
0x7e1e |
struct[] | 0x79 | Entity class detail records (121 bytes per class) |
Recent Manual Boundary Repairs
Recent high-traffic addresses recovered with manual function creation in Ghidra/PyGhidra:
| Address | NE Segment | Callers | Notes |
|---|---|---|---|
000a:48ff |
seg091:04ff | 55 | Recovered as rng_next_modulo; manual boundary repair narrowed to 000a:48ff-000a:4912. |
000b:2e00 |
seg109:0000 | 74 | Start of segment 109. |
0007:5a00 |
seg043:0000 | 64 | Start of segment 43. Earlier seg001 debris_spawn port was rejected; still needs manual function creation and direct analysis. |
0009:a200 |
seg082:0000 | - | Target of mem_alloc. Start of segment 82. |
000c:db68 |
cursor_nav_update_and_dispatch |
Calls cursor_zone_quadrant_classify; updates [+0x37..+0x3a]; reads [0x63da]; switch on direction (0–8); maps scancodes 0x48/0x50/0x4b/0x4d/0x39 |
|
000c:d3e9 |
cursor_set_ref_and_dispatch |
Null-checks param; sets *param_1 = &DAT_0000_638e; calls dispatch |
|
000c:d710 |
cursor_set_ref2_and_dispatch |
Same pattern; sets *param_1 = &DAT_0000_6346 |
|
000c:d75e |
entity_call_vtable_1e_via_ptr |
Calls (*[*param_1 + 0x3c])() — vtable offset 0x1e |
|
000c:d775 |
entity_call_vtable_1e_via_ptr_b |
Near-identical to d75e; duplicate generated by compiler |
|
000c:d7c6 |
stub_noop_000c_d7c6 |
Empty stub | |
000c:d7cb |
stub_noop_000c_d7cb |
Empty stub |
Direction code mapping (from cursor_nav_update_and_dispatch switch):
- 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW, 8=Center
Timer Rate Sync (cursor subsystem)
| Address | Name | Evidence |
|---|---|---|
000c:e4e0 |
cursor_timer_rate_sync |
If [0x63e0] (cursor active) non-zero: copies PIT rate [0x39ce] into entity object at [0x4458+0x24], clears +0x26/+0x27, then calls far thunk. Called from cursor_nav_update_and_dispatch at 000c:db97. |
Raw 000c UI Listbox Event Handler Batch
Analysis of 000c:880c and 000c:88b4 — the primary UI event-dispatch island in segment 000c.
Overview
ui_listbox_event_handler (000c:88b4) is a large UI widget event handler (~8KB of code, 88b4 through 9daa). It takes the signature (entity_far_ptr, seg, event_far_ptr) and is stored as a far-pointer vtable entry (no direct code xrefs found; referenced from data). It dispatches on the event code at [event+0x6] (keystroke/mouse codes) and updates entity fields to drive menu/listbox navigation state.
Guard function entity_state_if_flag80_call_thunk (000c:880c) sits just before the handler: it checks entity flag bit 0x80 at [param_1+0x5b] and gates a far thunk call.
Functions
| Address | Name | Evidence |
|---|---|---|
000c:88b4 |
ui_listbox_event_handler |
Dispatches on event code [event+0x6]: confirm keys 0xd/0x20/0x152 → entity_state_tick_dispatch; nav keys 0x148/0x14b/0x14d/0x150 → same; list-prev 0x2c/0x3c/0x55 → entity_state_advance_next_or_fallback_b; list-next 0x2e/0x3e/0x53/0x73/0x75 → entity_state_advance_next_or_fallback_a; Esc 0x1b → tick dispatch; comma 0x2c/0x3c → ac8f; misc UI events 0x6f/0x7e/0x13b–0x143/0x241/0x410/0x420/0x426/0x42f/0x432/0x441 → various paths. Guards: [BX+0x5b] & 0x80 (active), & 0x100 (modal lock from [0x604b], [0x844]). Large scan loop 0x100..0x27ff at 000c:a80f for option/device enumeration. Falls through to common retf at 9da7. |
000c:880c |
entity_state_if_flag80_call_thunk |
Guard: tests entity [param_1+0x5b] & 0x80; returns if clear; else calls far thunk. Active/visible gate for forwarding display/tick calls. |
000c:ace5 |
display_fullscreen_blit_with_entity |
If coords (param_2:param_1) are 0:0, calls wait thunk (0x48c=1164ms?); loads display object at [0x4cd0], reads byte [+0x5] (video mode/palette), builds full-screen 640×480 rect (0..0x27f=639, 0..0x1df=479), then calls far display function with entity + position params. Returns display-space coordinate pair in DX:AX. |
Key Globals in this Handler
| Address | Meaning |
|---|---|
[0x7e22] |
String/resource pointer used as data context ([0x7e24] = flag) |
[0x604b] |
Modal active lock (nonzero = block most events) |
[0x844] |
Engine/game-ready flag (0 = block hardware toggle events) |
[0x604f] |
Toggle state for event 0x410 |
[0x6045] |
Toggle state for event 0x7e |
[0x8638] |
Counter for event 0x432 (wraps 0x11→0x12..0x14→0x0) |
[0x5e82/0x5e84] |
Far pointer called when entity flag 0x40 is cleared |
[0x4cd0] |
Display/screen manager object far pointer |
Event Code Reference Table (partial)
| Code | Meaning |
|---|---|
0x0d / 0x20 |
Enter / Space (confirm) |
0x1b |
Escape (cancel) |
0x2c / 0x3c |
Comma / < (prev in list) |
0x2e / 0x3e / 0x53 / 0x73 |
Period / > / S / s (next in list) |
0x55 / 0x75 |
U / u (up action with far-ptr check) |
0x6f |
o (some option panel event) |
0x7e |
~ (toggle event, gates on [0x844] and [0x6045]) |
0x13b |
Shift+F1 area |
0x13c |
Jump-to-display event |
0x13e |
Blanked event (gates on [0x604b]) |
0x141 / 0x142 / 0x143 |
Option toggles (gate on [0x844], [0x2bca/0x2bc9]) |
0x148 / 0x14b / 0x14d / 0x150 |
Arrow keys (Up/Left/Right/Down) |
0x152 |
Insert key (alias confirm) |
0x241 |
Display position query → display_fullscreen_blit_with_entity |
0x410 |
Toggle event with [0x604f], gates on [0x844] |
0x420 |
List-scan init (loop 0x100..0x27ff) |
0x426 |
String-push event (calls DS:[0x7e22]) |
0x42f |
Push 0x103 thunk event |
0x432 |
Counter cycle event ([0x8638] wrap 0x11→0x12, 0x14→0x0) |
0x441 |
Pattern-fill loop event |
NE Segment 21 Analysis — Timer/Event Dispatch System
File: seg021_code_off_50200_len_4486.bin | File Offset: 0x50200 | Length: 0x4486 bytes
Ghidra Load: RAM 0000:0000 – 0000:4485, x86 16-bit Protected Mode, base 0x0000
Functions: 88 total (87 renamed + input_keyboard_handler pre-existing)
Subsystem Summary
Segment 21 implements the hardware-level timer interrupt and entity event dispatch system — Crusader's real-time task scheduler. Key responsibilities:
- Programs and services the Intel 8253 PIT timer (I/O ports 0x40/0x43)
- Manages three entity dispatch lists: timer list (0x39d4), input list (0x39e3), render list (0x3a10)
- Maintains the entity pool at 0x39b0 (same pool as seg001; these segments share DS)
- Provides event queue (32-slot circular buffer at 0x31cc)
- Handles save/load serialization of the entire entity system
- Controls keyboard/interrupt locks and deferred scheduling
Function Groups
Entity Pool Management (0x0207–0x0483)
| Address | Name | Notes |
|---|---|---|
0x0207 |
entity_count_by_type_a |
Count entities matching type+event; filters DEAD flag (0x8) |
0x0297 |
entity_count_by_type_b |
Identical logic to 0x0207 (compiler duplicate) |
0x0327 |
entity_find_free_slot |
Scan pool for null entry; calls panic if full; returns slot or 0xFFFF |
0x038f |
entity_register |
Write far ptr to entity_list, group to entity_data, vtable to registry; inc count |
0x044d |
entity_get_ptr_raw |
Read entity far ptr from pool slot (may be null) |
0x0483 |
entity_get_ptr |
Safe wrapper: verifies non-null, returns offset only |
Event Dispatch (0x04f3–0x08be)
| Address | Name | Notes |
|---|---|---|
0x04f3 |
entity_dispatch_reset_all |
Fires event code 0x21 (reset/init) to all entities |
0x050d |
entity_clear_deferred_flags |
Clears DEFERRED bit (0x200) from up to N=0x3998 entities |
0x059e |
entity_fire_event_broadcast |
Dispatch event to all matching entities; calls vtable[6]; respects 0x200 deferred flag |
0x06f4 |
entity_fire_event_type_include |
Fire only entities whose type IS in given list (up to 10, 0x0d=end) |
0x08be |
entity_fire_event_type_exclude |
Fire only entities whose type is NOT in given list |
0x0a8e |
input_keyboard_handler |
(pre-existing) OS-level key router: 0x0d=scroll+, 0x01=action, 0x2c=save, 0x44=load |
Entity Iterator / Linker (0x0bb7–0x106b)
| Address | Name | Notes |
|---|---|---|
0x0bb7 |
entity_link |
Cross-link two entities; skips if flag 0x400 set |
0x0c34 |
entity_find_first |
Init iterator 0x39fa=3; find first entity matching saved type/event at 0x399a/0x399c |
0x0cec |
entity_find_next |
Continue iterator from 0x39fa cursor |
0x0dad |
timer_entity_find_by_event |
Find entity handling event in range 0xf0-0xf7; checks bit 0x1000; writes to 0x3993 |
0x0e82 |
entity_find_by_priority |
Walk priority chain at 0x39d4; find entity matching source/event at 0x3993 |
0x0fc8 |
entity_set_cursor |
Validate flag 0x800; set cursor 0x3993 = param_1 (slot) |
0x100c |
entity_get_cursor |
Return entity at 0x39bf if valid and not dead |
0x106b |
entity_relink |
Re-link: find by event, walk priority chain, call set-link vtable funcs |
Entity Lifecycle (0x1133–0x131d)
| Address | Name | Notes |
|---|---|---|
0x1133 |
entity_unregister |
Full removal: dec sprite type count, vtable cleanup, dec total, update masks |
0x1202 |
entity_slot_clear |
Zero pool slot (0x39b0), registry slot (0x39ca), group data (0x39b4) |
0x1245 |
entity_layer_set |
Write 0x39c9 (active layer ID) if changed; set dirty flag 0x39a2 |
0x125d |
entity_check_overdue |
If entity_is_overdue: set bit 0x40 on entity+0x16 |
0x127c |
entity_is_overdue |
Return 1 if entity index > 0x39bf and flag 0x39c2 set |
0x129b |
entity_list_call_update |
For all entities where entity+0x0e & param_3 != 0: call vtable[8] |
0x131d |
entity_set_pending |
Write param to 0x3995 (next entity to register); error if already set |
Entity System Init/Shutdown (0x133e–0x1705)
| Address | Name | Notes |
|---|---|---|
0x133e |
entity_system_init |
Alloc all entity pool buffers (see decompiler comment); init three lists; clear event state |
0x14bc |
entity_system_flush_normal |
Finalize (vtable[10]) then free all non-deferred active entities |
0x158d |
entity_system_flush_deferred |
Same as flush_normal for deferred entities |
0x165c |
entity_process_pending_deletes |
Free entities marked DEAD (flag & 0x8); dec 0x399e counter |
0x1705 |
entity_system_shutdown |
Full shutdown: flush normal, flush deferred, process deletes, free all pools |
Save / Load (0x1851–0x1d21)
| Address | Name | Notes |
|---|---|---|
0x1851 |
event_queue_state_reset |
Zero ring buffer state tables (0x334e, 0x364e), queue ptrs (0x31c8/0x31ca) |
0x18ce |
level_load |
Full level load: shutdown + reinit + deserialize all entities via vtable[12] |
0x1d21 |
save_game |
Serialize entity system: arrays + each entity via vtable[14]; magic check 0x3a21==0xed |
PIT Timer / Hardware (0x2300–0x2975)
| Address | Name | Notes |
|---|---|---|
0x2300 |
pit_timer_program |
OUT 0x43, 0x36; OUT 0x40, lo; OUT 0x40, hi — raw PIT channel 0 program |
0x2316 |
pit_timer_set_hz |
Validates divisor <= 0xd688; stores at 0x39ce; calls pit_timer_program |
0x23a5 |
pit_timer_tick_handler |
Timer ISR: iterates 0x39d4 timer list, fires vtable callbacks per layer/mode |
0x25fc |
timer_entity_active |
Check 0x3987/0x398b for active timer entity (mode-dependent) |
0x264c |
timer_entity_get_current |
Get ptr from 0x3987 or 0x398b based on 0x3991 mode flag |
0x2668 |
timer_entity_enable |
Set ENABLED flag (0x400), inc counter, insert into timer list, reprograms PIT |
0x2745 |
timer_entity_disable |
Clear ENABLED, dec counter, reprograms PIT; if list empty calls interrupt_request_cancel |
0x2975 |
timer_recompute_hz |
Scan timer list; find smallest time_period (+0x38/+0x3a); call pit_timer_set_hz |
Interrupt / Lock Control (0x283a–0x294b)
| Address | Name | Notes |
|---|---|---|
0x283a |
interrupt_lock_acquire |
Re-entrant acquire on 0x31c7 (interrupt lock) |
0x2870 |
interrupt_lock_release |
Release 0x31c7 |
0x289b |
entity_lock_acquire |
Re-entrant acquire on 0x39aa (entity system lock) |
0x28d5 |
entity_lock_release |
Release 0x39aa |
0x290d |
interrupt_request_schedule |
Set deferred IRQ flags 0x39ab and 0x398f (or 0x39a9 in sync mode) |
0x294b |
interrupt_request_cancel |
Clear IRQ request flags |
Timer Loop / Deferred State (0x2a5f–0x2ad8)
| Address | Name | Notes |
|---|---|---|
0x2a5f |
timer_event_loop |
Main game loop: polls player tick counter at 0x2de4; busy-waits; fires optional callback; stores delta to 0x3a00/0x3a02 |
0x2ac2 |
timer_deferred_reschedule |
If deferred mode flag 0x39b8 set, call reschedule |
0x2ad8 |
timer_snapshot_deferred |
Copy 0x39a9 → 0x39b8; call interrupt handler if 0x39a9 set |
Event Queue (0x2c73–0x3364)
| Address | Name | Notes |
|---|---|---|
0x2c73 |
event_queue_drain |
Drain circular queue; call event_queue_dequeue while 0x31c8 != 0x31ca; reset state |
0x2ca2 |
mouse_button_check |
Return 1 if BIOS 0x31a4 bit 0x10 set AND 0x39af (mouse enable) set |
0x2cbc |
stub_noop_2cbc |
Empty stub function |
0x2cd7 |
bios_keyboard_flags_write |
Write param to 0x400:0017 (BIOS keyboard flags at segment 0x40, offset 0x17) |
0x2cf2 |
input_event_dispatch |
Dispatch display event 0x10 to input list entities with flag 0x100 and 0xc bits set |
0x2dc3 |
event_queue_push |
Push event to circular queue (write ptr 0x31ca); calls event_queue_is_full check |
0x3276 |
keyboard_state_read |
INT 16h AX=0: read raw keyboard state into 0x31a4 |
0x328b |
keyboard_acquire |
If not locked (0x31c6): INT lock, read keyboard, set lock flag |
0x32cc |
keyboard_release |
If locked: unlock, clear 0x31c6 |
0x3304 |
event_queue_count |
Count pending events: 0x31ca - 0x31c8 (circular) |
0x333d |
event_queue_is_full |
Return 1 if ((0x31ca+1) mod 32) == 0x31c8 |
0x3364 |
event_queue_dequeue |
Read from ring buffer (0x31cc + 0x31c8*0xc, entry size 0xc), advance read ptr |
Event Subscription and Bitmask Helpers (0x34dd–0x3878)
| Address | Name | Notes |
|---|---|---|
0x34dd |
event_queue_process_all |
Drain queue; for each event find listener entities in 0x39e3; call vtable[0x14] |
0x35e9 |
event_queue_set_mode |
Write low 2 bits to 0x334c; call keyboard_interrupt_call |
0x35fb |
event_queue_set_param |
Write low 5 bits to 0x334d; call keyboard_interrupt_call |
0x360d |
keyboard_interrupt_call |
INT 16h (raw BIOS keyboard services call) |
0x3630 |
entity_validate_indices |
Debug assert: verify entity+0x02 (slot_index) == pool position for all entities |
0x369b |
typemask_set_bit |
Set bit at 0x3a04 + (param>>3), bit (param & 7) — entity type present bitmask |
0x36d4 |
typemask_clear_bit |
Clear bit in 0x3a04 bitmask |
0x370f |
typemask_update |
If entity type has listeners (entity_find_first != 0): set bit; else clear |
0x3744 |
typemask_test_bit |
Test bit in 0x3a04; return 1 if entity type has registered listeners |
0x377d |
event_subscription_set |
Set subscription bit in 0x3a08 buffer |
0x37b2 |
event_subscription_clear |
Clear subscription bit in 0x3a08 buffer |
0x37e9 |
event_subscription_update |
If entity has listeners: set bit; else clear (driven by entity_find_first result) |
0x3825 |
event_subscription_test |
Test subscription bit in 0x3a08; return 1 if subscribed |
0x3864 |
event_state_clear |
Zero entire 0x3a0c event use-count buffer (0x4000 bytes = 8192 uint16s) |
0x3878 |
event_use_count_increment |
Increment 64-bit counter at 0x3a0c[entity_event_type*4] |
Input / Render List (0x38c2–0x3ae9)
| Address | Name | Notes |
|---|---|---|
0x38c2 |
input_event_broadcast |
Dispatch input event 0x40 to all render-list entities with flag 0x40; uses counter 0x39ad |
0x39a1 |
subscribe_to_render_list |
Add entity to 0x3a10 list; set flag bit 0x40; inc 0x3a1f |
0x3a13 |
unsubscribe_from_render_list |
Remove entity from 0x3a10; clear bit 0x40; dec 0x3a1f |
0x3404 |
subscribe_to_input_list |
Add entity to 0x39e3 list; check flag 0x100; set bit 0x80; inc 0x39c3 |
0x3477 |
unsubscribe_from_input_list |
Remove entity from 0x39e3; clear bit 0x80; dec 0x39c3 |
0x3a78 |
entity_lists_init |
Init three linked lists with sentinel vtable 0x3a89; write head vtable 0x2d10 |
0x3ae9 |
entity_lists_reset |
Call external reset + reinit 0x39e3 and 0x39d4 lists |
Entity Object Field Layout (as used in Seg21)
| Offset | Field | Type | Description |
|---|---|---|---|
+0x00 |
vtable_ptr | far ptr | Pointer to entity's vtable dispatch table |
+0x02 |
slot_index | uint16 | Entity's own slot number in pool |
+0x04 |
source_type | uint16 | Source/owner entity type (event matching) |
+0x06 |
event_type | uint16 | Event type this entity handles |
+0x08 |
flags_byte | uint8 | Low 5 bits = sprite group ID |
+0x0e |
capability_mask | uint16 | Bitmask of supported event capabilities |
+0x16 |
state_flags | uint16 | bit3=DEAD, bit8=REGISTERED, bit9=ACTIVE, bit10=ENABLED, bit11=HAS_TIMER, bit13=IS_IRQ_HANDLER |
+0x18 |
flags2 | uint16 | bit6=IN_RENDER_LIST, bit7=IN_INPUT_LIST, bit9=DEFERRED |
+0x1e |
priority_chain | far ptr | Priority chain entries (entity_find_by_priority) |
+0x20 |
priority_count | uint16 | Count of priority chain entries |
+0x38 |
time_period_lo | uint16 | Timer period low word (PIT frequency calc) |
+0x3a |
time_period_hi | uint16 | Timer period high word |
Vtable Layout (Seg21 usage)
| Slot | Byte offset | Prototype | Purpose |
|---|---|---|---|
| [6] | +0x0c |
handle_event(entity, CS, type, param) |
Event callback |
| [8] | +0x10 |
update(entity, CS, capability_mask) |
Per-tick update |
| [10] | +0x14 |
finalize(entity, CS) |
Cleanup/shutdown |
| [12] | +0x18 |
load(entity, CS, file_ptr, CS) |
Deserialize from save |
| [14] | +0x1c |
save(entity, CS, file_ptr, CS) |
Serialize to save |
| [16] | +0x20 |
set_backref(entity, CS, list_ptr) |
Set back-reference |
| [20] | +0x28 |
dispatch_callback(entity, CS, event_id, 0, data_ptr) |
Generic dispatch |
Key Global Data (Seg21 — additions to DS)
| Address | Name | Description |
|---|---|---|
0x31a4 |
bios_key_state | Raw INT 16h keyboard state |
0x31c6 |
keyboard_lock | Keyboard acquired flag |
0x31c7 |
interrupt_lock | Interrupt lock flag (re-entrant) |
0x31c8 |
queue_read_ptr | Event queue read index (0–31) |
0x31ca |
queue_write_ptr | Event queue write index |
0x31cc |
event_queue_base | Ring buffer, 32 entries × 0xc bytes |
0x334c |
queue_mode | Event queue mode bits (0–1) |
0x334d |
queue_param | Event queue param bits (0–4) |
0x39b0 |
entity_list | Far ptr to entity far-ptr array (count×4) — shared with seg001 |
0x39b4 |
entity_data | Far ptr to group/sprite-ID array (count×2) |
0x39b9 |
entity_max_count | Max capacity of entity pool |
0x39bb |
entity_count | Total registered entity count |
0x39c9 |
active_layer | Current active entity layer/group ID |
0x39ca |
entity_registry | Far ptr to vtable dispatch array (count×4) — shared with seg001 |
0x39ce |
pit_divisor | Current PIT timer divisor |
0x39d4 |
timer_list | Intrusive linked list: timer-dispatch entities |
0x39e3 |
input_list | Intrusive linked list: input-handler entities |
0x3a04 |
typemask_buf | Far ptr to entity type present bitmask (0x480 bytes) |
0x3a08 |
evt_sub_buf | Far ptr to event subscription bitmask (0x2400 bytes) |
0x3a0c |
evt_state_buf | Far ptr to event use-count table (0x4000 bytes) |
0x3a10 |
render_list | Intrusive linked list: render-callback entities |
0x3a21 |
save_magic | Must be 0xed (-0x13) for valid save |
0x3a70 |
default_registry_vtable | Default vtable written to entity_registry slots on register |
0x3a89 |
list_sentinel_vtable | Sentinel vtable written to list head nodes |