Crusader_Decomp/crusader_decompilation_notes.md
MaddoScientisto d1222a2a4f Enhance segment coverage ledger and mid-project plan with detailed updates
- Added new binary files for segment coverage in `Crusader.rep/idata/00/~00000006.db/`
- Updated `crusader_segment_coverage_ledger.csv` to reflect new findings and classifications, including:
  - Renamed segments for clarity on allocator mechanics and dispatch entry roles.
  - Adjusted coverage status for segments related to startup/display orchestration and allocator phase finalization.
- Revised `plan-mid.md` to include recent progress on segment recovery and classification, emphasizing the ongoing work on the `0x4588` callback object and related functions.
2026-03-21 20:32:21 +01:00

184 KiB
Raw Blame History

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 -> NE executable with Phar Lap DOS-extender code
  • Entry Point: 10da:7c40

Installed Copy Findings

  • No standalone .EXP file exists in F:\Apps\Crusader No Remorse.
  • CRUSADER.EXE is the original game binary and contains a valid internal NE header.
  • Outer DOS MZ header points to e_lfanew = 0x36F70.
  • Internal header at 0x36F70 starts with NE and 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.EXE is 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 flat ram block.
  • Direct file_offset -> flat_address mapping from the standalone segment extracts is not reliable for porting names into that raw import.
  • The extracted segNNN_*.bin files match CRUSADER_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:
    • seg001 base = 0x6E570 (cursor_update_hover at 0006:e5d0, rel 0x0060)
    • seg021 base = 0x87170 (entity_count_by_type_a at 0008:7377, rel 0x0207)
  • Porting rule for these verified segments:
    • raw_full_exe_flat = verified_segment_base + standalone_segment_relative_offset
  • Naming note:
    • seg001 and seg021 both contain a keyboard handler; in the full program database, the seg001 copy is named seg001_input_keyboard_handler to avoid a symbol collision with seg021 input_keyboard_handler.

Address Space Layout in the Raw Import

Ghidra segment:offset SSSS:OOOO = flat address SSSS * 0x10000 + OOOO.

Flat range Content
0x000000x36F6F 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:16 sites, then re-disassembles each patched instruction.
  • Current verified batch results:
    • 8851 internal literal CALLF sites patched to their real segment:offset targets.
    • 2841 far-pointer relocation entries skipped because they were not literal CALLF instructions (data or other non-call uses).
    • 119 import callsites annotated as NE IMPORT -> module.symbol.
  • Remaining xrefs to 0000:ffff should 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 ordinal
  • PUSH 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.EXE from verified seg001 mapping (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_weapon currently decompiles as a thin wrapper that calls projectile_init_vector.
    • fire_weapon_from_cursor still decompiles poorly in the raw import, but disassembly shows it begins by pushing cursor sprite/state data from the 0x27d6 area, 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, body 000a:44fd-000a:454c
    • 000a:454d = seg091_func_014d, body 000a:454d-000a:45fd
    • 000a:48a0 = rng_advance_state, body 000a:48a0-000a:48e2
    • 000a:48ff = rng_next_modulo, body 000a: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_00fd shares runtime flag 0x44a4 with runtime_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_014d also shares flag 0x44a4; it checks an optional long argument against the global context/cookie at 0x45a6, 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_seed writes the 32-bit RNG seed/state pair at 0x4584:0x4586 and forces the low word odd.
    • rng_advance_state updates the same 32-bit state with a simple multiply/add step.
    • rng_next_modulo advances the RNG state and returns the result modulo the requested bound, or 0 when 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_once
    • 000a:4a56 = runtime_callback_object_teardown_once
    • 0009:b1c3 = runtime_callback_object_phase_finalize
  • Boundary repair applied with MCP edit-plan API:
    • Rebuilt 000a:b988 as sprite_node_get_or_traverse with full body 000a:b988-000a:bab5.
    • This repair absorbs both callback-state sync callsites at 000a:b9e5 and 000a:ba66 that were previously in a no-function gap.
  • Verified callback-object behavior from this pass:
    • runtime_callback_object_init_once sets one-time guard 0x4594, snapshots state words (0x458c/0x4590) via video_bios_state_snapshot, installs the object FAR pointer at 0x4588, and ensures fallback buffer allocation at 0x45a6.
    • runtime_callback_object_teardown_once sets one-time guard 0x4595, clears 0x4588, conditionally emits vtable +0x0c callback when current/previous state differ, then calls vtable +0x04 release path.
    • runtime_callback_object_phase_finalize invokes vtable +0x08 twice and sweeps table entries via allocator_head_finalize_sweep.
    • Large caller FUN_000d_9afd contains both additional vtable +0x0c callsites (000d:9d5e and 000d: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_sweep
    • 000a:4a1f = video_bios_state_snapshot
  • Verified behavior anchors:
    • allocator_head_finalize_sweep performs per-head chain compaction/finalize work over allocator table entries used by runtime_callback_object_phase_finalize.
    • video_bios_state_snapshot executes BIOS video interrupts (INT 10h with AX=4F03 and AX=1130,BH=3) and returns packed state in DX: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_state
    • 000d:9afd = entity_cleanup_resources_and_dispatch
  • Verified behavior anchors:
    • video_mode_set_and_record_state stores requested mode/state to 0x4590, handles VBE-style mode values (0x101/0x103/0x105) via helper checks, and falls back to INT 10h mode path for other values.
    • entity_cleanup_resources_and_dispatch is a large teardown/finalize path for an entity-like object: it clears flags, frees multiple owned buffers/palette handles, performs conditional callback dispatch through 0x4588 vtable +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_256
    • 0009:1c3a = file_handle_alloc_init_and_open
    • 0009:1d6a = file_handle_open_with_mode
    • 0009:8d7b = surface_release_internal
    • 0009:8e0a = surface_release_and_maybe_free
    • 000d:9231 = sprite_redraw_global_if_active
  • Verified behavior anchors:
    • palette_buffer_alloc_and_init_256 ensures a caller-provided far buffer exists, allocates/initializes a 0x100-entry palette/work block, and fills it from static table data.
    • file_handle_alloc_init_and_open allocates a handle structure on demand, seeds sentinels, then delegates to file_handle_open_with_mode.
    • file_handle_open_with_mode performs path/open initialization with optional pre-delete behavior and stores DOS open result metadata into the handle structure.
    • surface_release_and_maybe_free wraps surface_release_internal and conditionally frees memory when (flags & 1) != 0.
    • sprite_redraw_global_if_active redraws the global sprite/object pointer at 0x4f38 only when the global gate byte 0x68e5 is enabled.
  • entity_cleanup_resources_and_dispatch now has direct named callees for file/surface/palette cleanup paths, reducing the remaining ambiguity to callback-object role naming and the 000d:7e00 event-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:8077 created and renamed to entity_dispatch_entry_init_runtime_state
    • 000d:8078-000d:819f renamed to entity_dispatch_entry_release_runtime_state
    • 0003:a880-0003:a896 created as FUN_0003_a880 (arithmetic helper; decompiler currently simplifies it)
    • 0003:b8e2-0003:bb39 created and renamed to far_buffer_alloc_with_mode_flags
  • Verified behavior anchors:
    • entity_dispatch_entry_init_runtime_state is a constructor-side helper that initializes runtime fields (+0x41/+0x42/+0x44), clears and allocates paired work/palette buffers (+0x46/+0x48 and +0x4a/+0x4c), applies event/setup calls through seg061 helpers, then finalizes activation.
    • entity_dispatch_entry_release_runtime_state is the destructor-side pair: it frees the same paired buffers, propagates active-state changes via global 0x6828, and destroys embedded word-list members.
    • far_buffer_alloc_with_mode_flags is 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 for entity_cleanup_resources_and_dispatch callback/setup paths.

Raw 0x4588 Follow-up Batch 5 (seg061/064/076 helper stabilization)

  • New conservative helper renames:
    • 0009:6ec7 = vga_palette_read
    • 0008:d3ba = timer_entity_enable_wrapper
  • Additional evidence-preserving decompiler comments were added (without speculative renames) on:
    • 0008:eb43
    • 0008:ebe7
    • 0008:eac8
    • 0008:ec23
  • Verified behavior anchors:
    • vga_palette_read mirrors vga_palette_write and reads DAC entries through ports 0x3c7/0x3c9 into a far palette buffer.
    • timer_entity_enable_wrapper is a thin forwarder to timer_entity_enable and is widely used in lifecycle/setup paths.
    • The seg064 gate helpers (0008:eb43/0008:ebe7/0008:ec23) control one-shot global flag transitions at 0x3b72/0x3b73, then dispatch via unresolved thunk paths; names remain intentionally conservative pending stronger subsystem identity.
  • Callback callsite clarification retained:
    • entity_cleanup_resources_and_dispatch vtable +0x0c call at 000d:9d5e passes object fields +0x12d/+0x12f.
    • Matching vtable +0x0c call at 000d:a3b7 passes 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_reschedule
    • 0009:7905 = palette_buffer_alloc_copy_from_source
  • Verified behavior anchors:
    • entity_set_update_period_and_reschedule stores timing/update-period fields (+0x36/+0x38/+0x3a), clears deferred fields (+0x3c/+0x3e), then triggers timer recompute/reschedule helpers.
    • palette_buffer_alloc_copy_from_source allocates/replaces destination palette buffer metadata and copies RGB triplets from a source far pointer (entry_count * 3 bytes).
  • Disassembly annotations added on both callback emit callsites so payload provenance remains attached in-database:
    • 000d:9d5e -> vtable +0x0c payload from +0x12d/+0x12f
    • 000d:a3b7 -> vtable +0x0c payload from +0x74f/+0x751
  • Global data labels were promoted for the callback lane (where symbolization applies in decompiler views):
    • g_active_dispatch_entry_farptr at 0x6828
    • 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_state
    • 0007:8865 = entity_sync_tile_aux_if_linked
    • 0007:8709 = entity_mark_dirty_and_sync_tile_aux
  • Current verified behavior:
    • entity_sync_tile_aux_state reads entity tile index at +0x4, toggles bit 0x04 in tile record +0x59 based on entity byte +0x54, and copies entity word +0x55 into tile record +0x0d.
    • entity_sync_tile_aux_if_linked only performs the sync when entity link/pointer +0x50/+0x52 is non-null.
    • entity_mark_dirty_and_sync_tile_aux calls the linked-sync helper, sets entity flag bit 0x04 at +0x42, then calls through 0000:ffff with args (SS:&tile_index, entity[+0x57]) — annotated at 0007:8666 as entity_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 +0x38 using incoming direction/event code values (notably 0x10/0x11/0x12) with parity-aware adjustment.
    • Uses entity flags at +0x4d to select increment/decrement behavior for clockwise/counterclockwise facing updates.
    • Called from the large gameplay update state machine at 0007:5b9a inside FUN_0007_5b6f.

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 at 0007: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_type
  • local_48 -> snap_entity_type_table
  • local_34 -> snap_dispatch_seg_table
  • local_20 -> snap_dispatch_off_table
  • local_c -> entity_type_cursor
  • local_4 -> dispatch_index

What the function does structurally

  1. Copies three 10-entry static tables into stack-local scratch buffers:
    • from 0x2910 into snap_dispatch_off_table
    • from 0x2924 into snap_dispatch_seg_table
    • from 0x2938 into snap_entity_type_table
  2. Performs a linear scan across 10 entity IDs in snap_entity_type_table.
  3. If entity_type matches an entry, it calls into the unresolved shared FAR thunk target (0000:ffff) with spawn coordinate-derived arguments.
  4. 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)
  • 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:ffff still exists as a symbol, but the relevant internal literal CALLF sites 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 at 0007:2261 now disassembles to CALLF 0004:e7bd, i.e. world_to_screen_coords.
  • Remaining 0000:ffff sightings 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:ffff stub, where the decompiler produces garbled output (it's reading fixup-chain data, not real instructions).
  • Each CALLF 0x0000:ffff in 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:E570 onwards = 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:2261 now lands at world_to_screen_coords (0004:e7bd), so the next step is to reinterpret the helper with the real callee in view rather than through the old 0000:ffff placeholder 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 IDs 0x31c..0x327 subset)
    • 0007:2366: explicit snap_entity_to_ground(entity_type, &spawn_x, &spawn_y, &spawn_layer) handoff
    • 0007:247e: fallback path that calls core entity_spawn with 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/layer temporaries ([bp-6], [bp-8], [bp-9]) before spawn.

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 globals 0x27e7 / 0x27e9 (AI focus position cache used by downstream logic).
    • Iterates entity IDs from 2 through 255 and dispatches per-entity processing through two sequential thunked calls per entity.
  • After the NE far-call repair pass, the first call at 0007:101c now disassembles and decompiles directly as entity_resolve_slot_ptr (0005:0466) instead of CALLF 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 via world_to_screen_coords, subtracts entity height from 0x7df5[slot], derives sprite/flag context, and returns bbox_overlap_test against the active viewport rectangle referenced from global 0x4014.
    • 0005:3cf5 = entity_class_has_flag2000 — class-word flag test over entity_get_class_word(slot) & 0x2000.
    • 0005:ff2d = entity_class_get_flag8 — returns bit 0x08 from entity-class detail byte 0x7e1e[type*0x79 + 0x59].
    • 0006:1305 = entity_class_get_word_02 — raw accessor for word +0x02 in the 0x7e1e class-detail record.
    • 0006:0ca4 = entity_class_get_word_0a — raw accessor for word +0x0a in the same class-detail record.
    • 0006:11a1 = entity_class_clear_flag8_and_dispatch — clears bit 0x08 in 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 ID
    • 0007:1093: entity_tick_dispatch(SS:&entity_id, g_0x27c8) — second call; per-entity AI tick with global 0x27c8 mode/context word
  • Global 0x27c8 is now confirmed as the current targeted/current entity handle: entity_is_type_match compares against it directly, and both spawn helpers map_find_spawn_point / enemy_spawn_at_position snapshot 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) and advance_steps (04, number of frame advances this tick).
  • Entity struct fields confirmed (relative to entity_ptr as int*):
    • [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:ffff sites and the vtable indirect call:
    • 0007:27dc: entity_completion_callback(handle) — fires when loop wraps; skips player handle
    • 0007:27fd: vtable indirect entity->vtable[+8](entity, 0, 0)on_loop_complete virtual method
    • 0007:281e: notify_frame_progress(handle, current_frame) — per-frame notification
    • 0007: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 using g_player_entity_farptr and 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) and g_cmd_effect_origin_y (0x27f3) after use.
  • 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_target is a thin wrapper over enemy_spawn_at_position(..., target_player_flag = 1).
    • enemy_spawn_no_target is the same wrapper but passes target_player_flag = 0.
    • map_find_spawn_point and enemy_spawn_at_position both copy DS:0x27c8 into locals before entering their unresolved thunk body, matching the standalone notes that treat 0x27c8 as 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_init
    • 000e:34cc = record_table_destroy
    • 000e:35c6 = record_table_release_buffer
    • 000e:35ef = record_table_next_slot
    • 000e:3639 = record_table_parse_buffer
    • 000e:3798 = record_parser_read_line
    • 000e:38a0 = record_parser_seek_next_marker
    • 000e:38f8 = record_parser_find_marker
    • 000e:39cc = record_parser_dispatch_at_directive
  • Current behavior read from raw-import decompilation/disassembly:
    • record_table_init clears the table header and zeroes 300 words of inline storage.
    • record_table_parse_buffer walks a CRLF-separated text buffer, captures each line, splits around a marker helper path, and stores parsed entry state into 0x0c-byte records.
    • record_parser_read_line advances to the next CRLF-delimited line, rejects lines that start with @ or with non-identifier punctuation, and terminates the line in-place with 0.
    • record_parser_seek_next_marker updates the parser's current marker cursor at +0x18/+0x1a by calling record_parser_find_marker; returns 1 if another marker was found, 0 at end-of-data.
    • record_parser_find_marker scans forward until an @ marker or end-of-data; optionally consumes the remaining length from the parser state.
    • record_parser_dispatch_at_directive returns 0 unless the current substring begins with @; in the @ case, it advances by 7 bytes and dispatches through a FAR thunk (0000:ffff).

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 -1 for "alive")
  • +0xe4 = paused flag (0 = running)
  • +0xeaf / +0xeb1 = far pointer to current RIFF chunk
  • +0xedb = animation frame stack depth counter (max 9)
  • +0xee1 = frame data from current chunk +4
  • +0xeef = current subframe index
  • +0x1b3 = subframe count
  • +0xef1 = audio completion flag
  • +0x11b = ring buffer write pointer
  • +0x11f = ring buffer read pointer
  • +0x117 = ring buffer base
  • +0x123 = ring buffer end (capacity boundary)
  • +0x102 = resource pointer
  • +0xde = some entry index (multiplied by 0x30 to 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:ffb0 remains unresolved (decompiles garbled due to overlapping instructions at 000f:0085/000f:0086). Current evidence from animation_start loop suggests this path is the video-side subframe loader paired with anim_load_audio_frame.

Constructor pattern (000e:2777, 000e:2860, 000e:2969): All three follow the same layout:

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

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

Constructor variant renames (direct analysis):

  • 000e:223d = assert_alive_sentinel
  • 000e:2777 = animation_ctor_variant_a
  • 000e:2860 = animation_ctor_variant_b
  • 000e: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:

  1. entry → checks DOS version, CPU type
  2. init_dos_extender → sets up protected mode (VCPI/DPMI)
  3. load_exp_file → opens the game's .EXP file
  4. load_executable_image → parses P2/MZ headers, creates segments, applies relocations
  5. task_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 (0x00600x0d5f)

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
0x27ca0x27ce 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 (0359)
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
0x28000x2811 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
0x29100x2947 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 vs cheat sequence at 0x2833 (counter 0x283d); on full match, toggles 0x844/0x6045 and spawns vtable 0x287b/0x2892

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 (0x24010x5a50)

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 2255, 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 0x27ca0x27ce, 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 (015)
+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 (0x435e0x44a9)

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 (0x46590x5a99)

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 (015): dx offsets at 0x129b, dy offsets at 0x12ac
  • Values are multiplied by distance (e.g. *0x500) for projectile spawn offsets

Collision Detection (0x60c10x621e)

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 (0x6aed0x6d21)

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 (0x74900x75ff)

Address Name Description
0x7490 debris_spawn Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list
0x75ff entity_die Death handler: spawns 14 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
0x2f50x2f7 Special movement entity
0x595/0x597 Platform/elevator entities
0x31c/0x3220x327 Explosive/effect entities
0x38d Save game trigger entity
0x426 Spark/scatter sub-shot
0x59a Player cursor/select indicator

Entity Data Table at 0x7e1e

  • Stride: 0x79 bytes (121 bytes per entry)
  • Indexed by entity type (integer) or entity slot
  • +0x59 offset = class-detail flags byte (entity_class_get_flag8 returns bit 0x08; other callers also clear bit 0x10 here during at-target facing updates)
  • +0x5a offset = 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

  1. NE Segment 1 imported and analyzed — all 58 identified functions renamed and annotated

  2. Import additional NE segments — priority: segments 22, 30, 59, 86 (segment 21 complete)

  3. Analyze additional segments — apply same decompile→rename→annotate workflow

  4. Map file format loaders.FLX, .SHP, .MAP, .TNT resource formats

  5. Cross-reference entity type constants with game entities (robots, platforms, triggers)

  6. Identify external segment calls — the func_0x0000ffff() placeholders are all cross-segment calls; resolving them requires importing the referenced segments

  7. NE Segment 1 imported and analyzed — all 58 identified functions renamed and annotated

  8. Raw 0007 segment analyzed — rendering, camera/scroll, save slot, and scroll region subsystems documented (~60 functions renamed and annotated)

  9. Import additional NE segments — priority: segments 22, 30, 59, 86 (segment 21 complete)

  10. Analyze raw 0007 draw helper clusterFUN_0007_03b4, FUN_0007_04b8, FUN_0007_04dc, FUN_0007_057f, FUN_0007_0614; called by sprite/draw list functions

  11. Analyze FUN_0007_4cdf — large 15-case animation/movement dispatcher; overlapping instruction warnings; cases 0, 2, 3, 6, 9, 0xa, 0xe are clean

  12. Map file format loaders.FLX, .SHP, .MAP, .TNT resource formats

  13. Cross-reference entity type constants with game entities (robots, platforms, triggers)

  14. 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:e0000007: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 0x2cc90x2cae.
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:bxxx0007: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.bin shows the first non-zero bytes at offset 0x0090; offsets 0x0000..0x008f are all zero in the standalone extract.
  • The first three clean 16-bit prologues in seg043 are at:
    • seg043:0090 -> raw 0007:5a90
    • seg043:017a -> raw 0007:5b7a
    • seg043:021c -> raw 0007:5c1c
  • The first recovered standalone function spans 0x0090..0x0179, which means raw 0007:5b6f falls inside the tail of that routine and overlaps the true return at raw 0007:5b79.
  • Repair status: applied in CRUSADER-RAW.EXE via the local PyGhidra toolkit. The bad function object at 0007:5b6f was removed, and three conservative replacement functions were created:
    • 0007:5a90 = seg043_func_0090 with body 0007:5a90..0007:5b79
    • 0007:5b7a = entity_set_at_target_update_facing with body 0007:5b7a..0007:5c1b
    • 0007:5c1c = seg043_func_021c with body 0007:5c1c..0007:5c80
  • Follow-up re-decompilation now supports one real behavioral rename: 0007:5b7a sets entity +0x3a to 1, calls entity_set_facing_direction, clears class-detail bit 0x10 at 0x7e1e[type*0x79+0x59], then continues into downstream dispatch, so the repaired middle function has been renamed entity_set_at_target_update_facing.
  • 0007:5a90 now has a stronger structural read from standalone disassembly: it allocates an object when the incoming far pointer is null (literal 0x98), runs a far setup helper using DS:0x4b48..0x4b4e and the second incoming far pointer, writes 0x4c13 at the object base, calls entity_set_at_target_update_facing with the third incoming far pointer, then adjusts the nested object at +0x38 using extents read from the object at +0x34 before returning the object pointer.
  • 0007:5c1c also has a stronger structural read: it optionally calls a virtual method through [object->vtable + 0x4c] when object+0x44/+0x46 is non-null, passes a local stack word through entity_class_get_flag20, then dispatches one or two downstream far helpers using object+0x48, gated by a local status byte at [bp-0xe].
  • 0007:5a90 and 0007:5c1c remain 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:bab5 entity_set_watch_ptr Stores FAR entity ptr to 0x2bd8 (the watch target entity).
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:ba45 (unnamed) Null/non-null far-ptr dispatch: different thunk paths based on param_2 == 0.
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_ptr FAR ptr to entity being tracked by camera
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_id is called from:
    • entity_dispatch_entry_init at 0008:bae4
    • entity_increment_group_id at 0008:be57
  • entity_set_source_type is 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:bd79 remains 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 +0x28 when flag 0x100 is armed, then calls entity_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_a is called from entity_pair_mark_and_sync_a (0008:ca10).
  • entity_pair_sync_b is called from entity_pair_mark_and_sync_b (0008:caf9).
  • Shared helper use inside pair sync wrappers:
    • entity_pair_update_link_slot_b at 0008:c981 and 0008:ca6a
    • entity_pair_update_link_slot_a at 0008:c995 and 0008: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 0x20 is 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_state is 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_flags2 pair is a verified complementary toggle with identical gate logic (0x39a8/0x39f9/0x3991 check 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/0x39f6 counter 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 0x0408 terminator 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_id10 implies list entries pack a 10-bit id plus flag bits (0x400 observed).
  • This further supports that the 0008:da00..dfa1 region 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:ffff into 0001:xxxx is 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_dispatch as 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_dispatch is 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:88b4 control 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 end
  • field47 = keystroke-combo counter
  • field3f = 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 (Roffset, Goffset, Boffset) 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:d3e9000c: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 (08)
+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:5a00 at seg043:0000, and the already-named helper at 0007:5b6f sits at seg043:016f.
  • Because of that segment placement, standalone seg001 names such as debris_spawn (0x7490) and entity_die (0x75ff) should NOT be ported into this raw range.
  • 0007:5b6f no longer exists as a function after the PyGhidra repair pass. Its old raw-analysis behavior now lines up with the repaired function 0007:5b7a = entity_set_at_target_update_facing, so 0007:5b6f should 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_checked
    • 0007:5a98 -> FUN_0008_cc01 (timer-related flag/event helper; tests +0x16 & 0x2, sets +0x16 |= 0x800, copies event field +0x06 to +0x22, checks 0x1000, then conditionally dispatches)
    • 0007:5b36 -> entity_get_type_word
    • 0007:5b44 -> saveslot_read_entry_flags
    • 0007:5bb8 -> entity_is_type_match
    • 0007:5c49 -> entity_class_get_flag20
    • 0007: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, and 0007:5c1c.
    • The repaired middle function at 0007:5b7a has now been promoted from a positional label to entity_set_at_target_update_facing based on direct decompile/disassembly behavior.
    • The remaining repaired functions at 0007:5a90 and 0007:5c1c should 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_0090 and seg043_func_021c, resolve the still-unknown far thunks they call, and replace the positional names only when their behavior is directly supported.
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, 1 for screen_x — signed arithmetic shift preserves sign for negative (world_x - world_y) differences
  • SHR AX, 2 for 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:ce1e populates one 0x69ac entry by reserving a free slot, computing the initial bucket through entity_compute_proximity_or_visibility_bucket, storing both current and previous bucket fields, then allocating/linking the backing handle through 000a:5f36.
  • 000d:d409 is a thin wrapper that only calls entity_refresh_recent_proximity_or_visibility_buckets.
  • 000d:cfad is an update-or-allocate helper for (param_1,param_2) pairs: it tries to update an existing tracked entry through 000a:606a, clears dead entries, and falls back to 000d:ce1e allocation when no live match remains.
  • 000d:cec5 is the auxiliary-slot allocator: it prunes invalid entries, uses tracked_entity_bucket_find_free_aux_slot, tags the new entry with byte +0x0a = 1, and seeds its handle via 000a: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 via 000a:5f36 if the handle has gone stale.
  • 000d:d350 = tracked_entity_bucket_set_value — finds a tracked (entity_id, entity_ref) entry in 0x69ac and pushes a new bucket value into its backing handle through 000a:6343.
  • 000d:d10b = tracked_entity_bucket_clear_ref_field — clears only the +0x02 reference 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 +0x0a tag 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 (bit0 set 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 by 0x45aa; creates or refreshes a main-slot tracked handle with bucket 0x40 and selector 0xff.
  • 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 bucket 0x40.
  • 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 by cache_alloc_block_for_slot and cache_free_block_by_slot
  • 0x468c = backend loader interface / callback table; +0x34 returns the payload size for an id and +0x0c loads or binds a block after allocation
  • 0x4695 = base pointer for the raw cache payload arena
  • 0x4699 = per-slot payload-pointer table
  • 0x469d = per-slot cached id table (0xffff = unused)
  • 0x46a5 = cache byte budget / arena capacity
  • 0x46a9 = current bytes in use
  • 0x46af / 0x46b1 = one-entry fast-path cache of the last requested id and slot index
  • 0x46b3 = 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_enabled and 0005:3b53 = tracked_entity_bucket_alloc_aux_if_enabled; these are the two thin gate wrappers that feed the 0x69ac tracked-entry layer.
  • 0005:3b72 = tracked_entity_bucket_remove_by_entity_and_ref_if_enabled, which forwards into 000d:d086 = tracked_entity_bucket_remove_by_entity_and_ref when 0x45aa is set.
  • 0006:d404 is not a standalone function entry; it is the tail call inside the unnamed destructor block immediately before 0006:d414. That block tears down a 0x2774 dispatch-entry object, pushes (entity_id = 0xdb, entity_ref = *0x7e22), and removes the matching aux tracked bucket entry.
  • 0006:d5ae is the analogous tail call immediately before 0006:d5be. It tears down the sibling 0x2750 dispatch-entry object, pushes (entity_id = 0xa4, entity_ref = *0x7e22), and removes the matching aux tracked bucket entry.
  • The matching constructor sides are 0006:d370 and 0006:d51a: both allocate/init a dispatch entry, stamp source type 8, seed a per-object field from the current reference entity at 0x7e22, and then call tracked_entity_bucket_alloc_aux_if_enabled.
  • 0007:cce8 is the tail call at the end of scroll_camera_set_state_params. After the camera scroll state is updated and the new screen-space origin is committed to 0x2bb7/0x2bb9, it refreshes the recent proximity/visibility buckets through 000d:d409 when 0x45aa is enabled.

Follow-up: 0x45aa gate and cache loader installation

  • 0x45aa now reads as tracked_entity_bucket_system_enabled, not a one-off debug/test flag. It gates all three public wrapper helpers above and the camera-side refresh in scroll_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 into 0x45ab/0x45ad, installs it into 0x468c via cache_set_loader_interface, allocates the tracked-handle table (000a:5e00) and the 32-entry bucket array (000d:cca3(0x20)), then sets 0x45aa = 1.
  • The matching unlabeled shutdown block starts at 000a:5223: it checks 0x45aa, tears down the tracked-handle table through 000a:5e59, frees the bucket array through 000d:ccec, and only then clears/uses the backend pointer state at 0x45ab.
  • 0x468c remains best named as a generic cache_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 gate 0x89f4 is non-zero.
  • 000a:5223 = tracked_entity_bucket_system_shutdown.
  • 0x45ab/0x45ad now read as tracked_entity_bucket_backend_object, a cached backend/interface object pointer used by init/shutdown in addition to the lower-level cache_loader_interface callback table at 0x468c.
  • tracked_entity_bucket_system_init first allocates a rotating buffer via 0009:3600, lazily creates tracked_entity_bucket_backend_object through 0009:5600 when absent, installs that object into cache_loader_interface, allocates the tracked handle table via the missing function entry at 000a:5e00, allocates the 32-entry 0x69ac bucket array via 000d:cca3(0x20), then sets tracked_entity_bucket_system_enabled.
  • tracked_entity_bucket_system_shutdown is called from the wider engine teardown routine at 0004:621b; it tears down the tracked handle table, frees the 0x69ac bucket array, calls backend-object vtable slot +0x38 with (3, backend_object), and clears tracked_entity_bucket_backend_object.

Follow-up: backend object constructor at 0009:5600

  • The missing raw-import function entry at 0009:5600 has now been recovered in-place as cache_backend_object_init with body 0009: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.
  • This is enough to justify the structural cache_backend_object_init name, 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.

Follow-up: missing function entry at 000a:5e00

  • The missing raw-import function entry at 000a:5e00 has now been recovered in-place as tracked_entity_handle_table_init with body 000a:5e00-000a:5e58.
  • Verified behavior: if 0x4672 is clear, it allocates 0x90 bytes at 0x4673/0x4675, aborts through runtime_init_or_abort on allocation failure, calls 000a:577d and local helper 000a:5e95, then sets 0x4672 = 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_active
    • 0x4673/0x4675 = tracked_entity_handle_table
  • This matches the existing client-layer interpretation: 0x4673 holds 12 handle entries (12 * 0x0c = 0x90 bytes), and 000a:5e00 is 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:6600 has now been recovered in-place as cache_init with body 000a: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 in 0x46a5.
    • Allocates the arena backing object at 0x4691, derives the payload base pointer 0x4695, and aborts through seg091_func_00fd on failure.
    • Allocates the per-slot block-metadata mirror at 0x46b3 (count * 4) and per-slot cached-id table at 0x469d (count * 2).
    • Allocates and initializes the free-list head object at 0x4688, then calls local helper 000a:68aa before returning.

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 resets 0x46a9 (bytes in use) plus 0x46af (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_slot cannot 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_active is set, it zeroes the full 0x90-byte handle table at 0x4673, resets adjacent local state at 0x4677/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_all when tracked_entity_bucket_system_enabled is set.

Follow-up: external reset paths using the cache/tracked-handle layer

  • The unlabeled path around 0004:25a9 now has enough local evidence to classify as an external reset sequence: it calls cache_reset_runtime_state, then tracked_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:eb80 is a conditional tracked-bucket reset/update sequence: when tracked_entity_bucket_system_enabled is set, it calls tracked_entity_handle_mark_remove_all_if_enabled, then tracked_entity_handle_table_clear_and_dispatch, then cache_compact_arena_blocks, before resuming its outer flow.
  • These caller sites strengthen the current interpretation that the 0x45aa / 0x4673 / 0x4688..46b7 layer 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:2592 had been mis-modeled as a one-instruction thunk body. It has now been repaired to the full body 0004:2592-25de and renamed runtime_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 as NE IMPORT -> ASYLUM.24.
    • Then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers, and resumes the wider runtime reset flow.
  • Known caller so far: 0004:262d inside the tiny wrapper at 0004:2620, which sets byte +0x40 on the object at 0x6828 before invoking the reset sequence.
  • 0004:eb1f had also been truncated. It has now been repaired to the full body 0004:eb1f-eb9b and renamed entity_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 dispatches entity_dispatch_reset_all(*0x7e22, 0x00f0) and, when the local flag plus global 0x0ee1 allow it, allocates a type 0x0f5e dispatch entry and passes it to entity_pair_sync_b.
    • When tracked_entity_bucket_system_enabled is set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through 0009:b1c3 in phase 0.
  • The sibling at 0004:eb9c remains separate and valid; it builds the same 0x0f3a entry type without the extra cache-reset tail, so the repaired 0004:eb1f boundary stops cleanly at 0004:eb9b.

Follow-up: new local helper classification around the repaired seg004 path

  • 0004:ea00 is now a real function object named entity_dispatch_entry_alloc_type_0f5e with body 0004:ea00-0004:ea46.
  • Verified behavior for entity_dispatch_entry_alloc_type_0f5e:
    • Reuses the incoming FAR pointer when non-null; otherwise allocates 0x33 bytes through mem_alloc_far.
    • Initializes the entry through entity_dispatch_entry_init.
    • Stamps the entry type word at +0x00 to 0x0f5e before returning it.
  • 0009:b1c3 is now renamed allocator_phase_finalize_pass and remains intentionally allocator-scoped rather than subsystem-specific:
    • Both known call sites pass only phase bytes 0 or 1.
    • It forwards that byte twice to the object rooted at 0x4588 through vtable slot +0x08.
    • It then sweeps the allocator head table at 0x8724 up to the active head count at 0x879c, calling allocator_head_finalize_sweep on each entry.
  • That evidence is strong enough for the allocator-side rename, but not yet enough to promote 0x4588 to a more specific subsystem name.

Follow-up: seg082 allocator cluster (0009:a229, 0009:af87, 0009:b06b, 0009:b1c3)

  • 0009:a229 is now verified as the public size-only wrapper around the seg082 allocator path.
  • Caller evidence:
    • saveslot_table_clear requests 0x2800 bytes through 0009:a229, stores the returned FAR pointer at 0x2ba3/0x2ba5, then zeroes the result in 0x400-byte chunks.
    • The wrapper lazily initializes the allocator on first use through 0009:bcb9, then calls allocator_try_alloc_from_head_table(size, default_tag, 0xff).
  • 0009:bcb9 is now annotated as the one-time lazy initializer for this path.
    • It parses an optional -x tuning value from the PSP command line, clamps the derived percentage into 0x14..0x50, then seeds local seg082 helpers before setting init flag 0x4096 = 1.
  • Table structure around 0x8724 is tighter now:
    • 0x8724 is an array of 0x0c-byte allocator heads.
    • 0x879c is the active head count / table limit.
    • The per-node size/value encoding used under each head is manipulated through 0009:c628 and 0009:c6ae, which read/write a packed 32-bit quantity split across word + byte + byte fields.
  • 0009:af87 is 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 - 9 into a running total and tracks the largest single free block.
    • Known callers include cache_init and the seg013 path at 0004:833b, both of which use it to size subsequent allocation work.
  • 0009:b06b is now renamed allocator_try_alloc_from_head_table.
    • It validates the requested size, reserves a temporary work token through 0009:e15f, and scans the 0x8724 allocator head table in 0x0c-byte entries via the local helper at 0009:a336.
    • On a successful fit, it commits the result through 0009:e2b4, clears failure flag 0x4098, 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 through 0009:e1f6.
  • The formerly missing callee at 0009:a336 has now been recovered in-place as allocator_head_try_alloc_block with body 0009:a336-0009:a5d0.
    • It is the per-head first-fit allocator helper used by allocator_try_alloc_from_head_table while sweeping the 0x8724 allocator head table.
    • It normalizes the requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds the local 0x0a node header overhead, and enforces a minimum allocation size of 0x10 bytes.
    • 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 0x10 bytes remain, stores the owner/tag word, and returns payload_ptr + 0x0a.
    • On failure for that head it returns 0, which matches the calling pattern in allocator_try_alloc_from_head_table.
  • Boundary follow-up from the same read-only scan:
    • The adjacent missing body at 0009:a5d1 has now also been recovered in-place as allocator_head_free_block with body 0009: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:a7a1 and 0009:a7b8 are now confirmed to be internal labels, not separate function entries.
  • 0009:a961 is now better constrained as the per-head finalize sweep used by allocator_phase_finalize_pass.
    • It walks one 0x8724 head'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_pass is allocator-side callback/finalize glue rather than a cache-specific public API.
  • 0009:b224 is now named allocator_free_block_by_ptr.
    • Current verified behavior: converts the payload pointer back through the local header helpers, scans the 0x8724 head table for the owning range, dispatches to allocator_head_free_block, and aborts if no owning head is found.
    • Known wrappers 0009:a24f and 0009:a27a are now clearly small checked entry points into this free-by-pointer path.
  • 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_pass now has a corrected single-byte phase parameter in Ghidra; the object at 0x4588 is 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 at 0x4588, not the local allocator mechanics around 0x8724. ASYLUM.24 is 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 / 0x458a even 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 at 0x4588 via vtable slot +0x0c when the entity flags allow the alternate path and param_2 == 0.
    • 000a:4a56 is a one-shot teardown/reset path for the same object: it checks a local once-flag at 0x4595, clears 0x4588 when non-null, optionally performs a vtable +0x0c callback when 0x4590 != 0x458c, then calls vtable slot +0x04 followed by FUN_0009_0d30().
    • The two callback sync sites inside sprite_node_get_or_traverse (000a:b9e5 and 000a:ba66) only emit vtable +0x0c when the candidate two-word pair differs from the current pair, then immediately mirror that pair through 000b:1e39 using global sprite/object pointer 0x4f38/0x4f3a.
    • A read-only data probe of 0x4588 in the current database returned all zero bytes, so the object pointer is null-initialized statically and likely installed later at runtime.
  • Conservative conclusion:
    • The 0x4588 object 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:b1c3 or the global itself to a concrete subsystem name.

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:4932 and 000a:4936 store the same incoming dword into 0x4590 and 0x458c, then 000a:493e stores the incoming FAR object pointer into 0x4588.
    • 0004:5b8c and 0004:5bbf both clear 0x4588 immediately before the fatal/reporting-style seg091 call through 000a:454d.
    • 0004:5ea7 and 0004:6430 both clear 0x4588 and then immediately run the one-shot teardown path 000a:4a56(1).
    • 000a:b9e5, 000a:ba66, 000d:9d5e, and 000d:a3b7 all push a two-word value pair followed by the 0x4588 FAR pointer and call the object's vtable slot +0x0c.
    • entity_conditional_render_dispatch remains the only named caller found so far for the same slot, but it passes a single literal 0x0101 argument instead of a two-word pair.
  • Conservative conclusion after the window pass:
    • 0x4588 is 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:ba66 and 000d:9d5e / 000d:a3b7 windows.

Follow-up: seg138 caller-side dispatch-entry emission helper

  • FUN_000d_938c is now confirmed as a real caller-side helper with body 000d: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:0x0008 state and entity byte +0x33 is clear, it allocates a scratch palette buffer, constructs a dispatch entry, sets type 0x051e, and initializes runtime state through entity_dispatch_entry_init_runtime_state with entry kind 0x3c.
    • Later in the same helper it constructs a second dispatch entry from the current palette globals at 0x4e4:0x4e6, again sets type 0x051e, and initializes runtime state with entry kind 0x14 and 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 byte 0x58e from the entity when the global display object exists, calls FUN_0006_16e1, clears g_active_dispatch_entry_farptr[+0x40], and finally dispatches through the input object's vtable slot +0x08.
  • Conservative conclusion:
    • seg138 now has one more verified caller tying entity_dispatch_entry_init_runtime_state to 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?"

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_active
    • 000d:83be = dispatch_entry_create_grayscale_palette_state_active
    • 000d:85da = vga_palette_set_all_black
    • 000d:8653 = vga_palette_set_all_white
    • 000d:86cc = vga_palette_set_all_rgb
    • 000d:875d = dispatch_entry_create_solid_palette_state_active
    • 000d:88b2 = dispatch_entry_create_solid_palette_state
    • 000d:8a47 = dispatch_entry_create_black_palette_state
  • Current verified behavior from direct MCP decompile/disassembly:
    • vga_palette_set_all_black corrects the earlier overreach rename at 000d:85da: it allocates a 0x100-entry palette buffer filled with zero RGB triplets, writes it to VGA, and frees the scratch buffer. The previous map_object_set_dirty_flag name was not supported by the recovered body.
    • vga_palette_set_all_white is the same helper shape with all three RGB components initialized to 0x3f, then written through vga_palette_write.
    • vga_palette_set_all_rgb takes caller-supplied RGB bytes, replicates them across a 0x100-entry palette buffer, writes the result to VGA, and frees the scratch palette.
    • dispatch_entry_create_black_palette_state_active and dispatch_entry_create_black_palette_state both build a runtime-state dispatch entry of type 0x051e from a black 0x100-entry palette buffer; the _active form first sets g_active_dispatch_entry_farptr[+0x40] = 1, while the quiet form does not.
    • dispatch_entry_create_grayscale_palette_state_active reads 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_active and dispatch_entry_create_solid_palette_state validate 0..0x3f RGB inputs, fill a scratch 0x100-entry palette buffer with that solid color, and build the same 0x051e runtime-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 type 0x68bf through entity_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 0x68bf object and the exact role of the 0004:5ad4-5b6e caller 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:621a recovered as FUN_0004_60c0 with a decompiler comment summarizing the orchestration flow.
    • 000d:7600-000d:760d created and renamed to active_dispatch_entry_mark_enabled.
    • 000d:760e renamed to active_dispatch_entry_mark_disabled.
    • 000d:761c renamed to active_dispatch_entry_create_default.
  • Current verified behavior from direct MCP decompile/disassembly:
    • FUN_0004_60c0 is 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 through 0x4f38, runs the seg137 palette and dispatch-entry helper family, creates the default active dispatch entry through active_dispatch_entry_create_default, programs mouse interrupt state via seg056 INT 33h wrappers, then hands off into the still-unrecovered 0004:1e00 routine.
    • The old 0004:5ad4-5b6e caller sequence is now confirmed as one internal sub-sequence within that larger recovered function rather than an isolated orphan.
    • active_dispatch_entry_create_default allocates or reuses a 0x42-byte dispatch entry, stamps type 0x687f, installs a callback-table pointer through 0x39ca, sets event type 0x248, sets update period 0x1e, marks the entry as the global active dispatch entry at 0x6828, toggles the local +0x40 state byte through active_dispatch_entry_mark_enabled and active_dispatch_entry_mark_disabled, then enables the timer wrapper.
    • active_dispatch_entry_mark_enabled and active_dispatch_entry_mark_disabled are tiny helpers that set or clear g_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:1e00 target still needs recovery and naming.
    • seg136 now has a concrete active-dispatch-entry foothold rather than being empty ledger space.

Follow-up: seg005 large runtime/display handoff body recovered at 0004:1e00

  • 0004:1e00-0004:2420 has now been recovered as a real function object in Ghidra as FUN_0004_1e00 with 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 at 0x6828.
    • It constructs two animation-side objects through animation_ctor_variant_b (000e:2860) using DS-local descriptors at 0x04ae and 0x04b2, then waits on global words around 0x8a94-0x8a98 when 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 at 0x4aa, 0x4ac, and 0x7e22.
    • The mid-body is now better classified as a non-return transition driver rather than a generic handoff stub: it branches on the returned SI state 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 calling runtime_callback_object_teardown_once(1).
    • The SI == 2 special-case branch is now slightly tighter: its local helper 0004:2661 forwards into FUN_0004_25df, which is a small type-stamped dispatch-entry constructor that allocates when needed, runs entity_dispatch_entry_init, stamps type 0x04b6, and stores the caller-supplied mode/state word at +0x32.
    • The fuller setup path clears and restores active-dispatch state, calls through the 0x2bd8 object vtable, restores the live palette through 0009:6f5a, re-runs render/dispatch rectangle helpers (entity_conditional_render_dispatch, entity_rect_compare_and_dispatch), and finishes through the seg126 trampoline thunk_callf_0000_ffff_000c_82f9.
    • The recovered tail confirms a clean end at 0004:2420, with the next separate function beginning at 0004:2421.
  • 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 the 0x2bd8 vtable 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)
    • FUN_000c_c9f4 (000c:c9f4-000c:ca1c)
  • The larger fallthrough body rooted at 000c:c890 is now also recovered as a real function object: FUN_000c_c890 (000c:c890-000c:c9f3).
  • Current verified behavior from direct MCP recovery/decompile:
    • FUN_000c_7412 is a compact wrapper into the seg005 transition lane: it clears the redraw state on the sprite/object pair rooted at 0x5e82:0x5e84, forces a black palette through vga_palette_set_all_black, runs seg126 pre-entry state prep through FUN_000c_c9f4, then tail-calls FUN_0004_1e00.
    • FUN_000c_c9f4 is a short pre-entry state wrapper: it runs local seg126 setup helpers, repeatedly executes a local prep loop while state byte 0x62fe is clear and fallback word 0x31a2 is zero, then dispatches into local helper 000c:c890 before returning to callers that continue into FUN_0004_1e00.
    • FUN_000c_c890 is the main seg126 pre-entry preparation body behind that wrapper: it releases up to two tracked object pairs at 0x8c5c and 0x8c60, conditionally frees the local pair at 0x6301:0x6303, runs palette/render reset, conditionally constructs animation state through animation_ctor_variant_a on DS:0x6341 when 0x844 and 0x62fe are both set, marks the active dispatch entry, primes sprite redraw state, drains the event queue, and zeroes 0x8a94-0x8a98 before returning.
    • This is now enough to tie the seg076 caller at 000c:742c to the same startup/display transition lane already reached from FUN_0004_60c0.
  • Conservative conclusion:
    • seg126 now has a real foothold in the startup/runtime entry path rather than only isolated thunks and trampolines.
    • The unresolved part is the exact meaning of the local state bytes and object lanes around 0x62fe, 0x31a2, 0x8c5c, 0x8c60, and DS:0x6341, not whether the wrapper lane itself is real.

Follow-up: ASYLUM.24 vs nearby ASYLUM ordinals

  • ASYLUM.24 remains unresolved by name, but its call pattern is now narrower.
    • In runtime_cache_reset_sequence (0004:2592), it is a parameterless import call placed after game_mode_init(*(0x27c4)) and before cache_reset_runtime_state plus the tracked-handle/cache-side reset tail.
    • That makes it look like a module-level reset/init hook rather than a per-object method.
  • Nearby ASYLUM ordinals in the seg011 caller at 0004:6f15 show a different pattern:
    • ASYLUM.36 returns an object-like handle that is used immediately through indirect vtable calls.
    • ASYLUM.37 is then called with explicit arguments against that object flow.
  • Current conservative conclusion:
    • ASYLUM.24 is probably from the same external module family, but it does not currently match the object-construction / object-method calling pattern observed for ASYLUM.36 and ASYLUM.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_init required a conservative boundary repair: a stray function object FUN_000a_eee3 had incorrectly claimed body range 000a:6710-000a:fe79, blocking creation of the real cache_init body.
  • The bad overlap was removed, cache_init was created at 000a:6600-67d8, and FUN_000a_eee3 was recreated conservatively as the contiguous visible body 000a: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 (08); 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/0x152entity_state_tick_dispatch; nav keys 0x148/0x14b/0x14d/0x150 → same; list-prev 0x2c/0x3c/0x55entity_state_advance_next_or_fallback_b; list-next 0x2e/0x3e/0x53/0x73/0x75entity_state_advance_next_or_fallback_a; Esc 0x1b → tick dispatch; comma 0x2c/0x3cac8f; misc UI events 0x6f/0x7e/0x13b0x143/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 (0x02070x0483)

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 (0x04f30x08be)

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 (0x0bb70x106b)

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 (0x11330x131d)

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 (0x133e0x1705)

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 (0x18510x1d21)

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 (0x23000x2975)

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 (0x283a0x294b)

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 (0x2a5f0x2ad8)

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 (0x2c730x3364)

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 (0x34dd0x3878)

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 (0x38c20x3ae9)

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 (031)
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 (01)
0x334d queue_param Event queue param bits (04)
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