Crusader_Decomp/crusader_decompilation_notes.md
2026-03-20 00:24:27 +01:00

100 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.

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 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 enters the existing unresolved thunk path (0000:ffff).

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 common thunk endpoint is still imported as unresolved_far_thunk_dispatch at 0000:ffff.
  • In this raw database, that body decompiles as overlapped/bad instruction data, so exact arithmetic internals of the final coordinate projection cannot yet be recovered from this symbol alone.
  • Despite that, caller context + table shape + argument flow make the gameplay role of this helper clear enough for naming and control-flow analysis.

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, ...);
		}
	}
}

Next RE target (to close remaining uncertainty)

  • Recover the true callee behind 0000:ffff for the 0007:224b call site by relocation/import-table reconstruction or by matching this call path in a cleaner segment-mapped database. That should reveal exact per-slot use of the two dispatch tables and final coordinate math.

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 the shared thunk path.
  • This function now has enough recovered semantics to treat it as the frame-level AI sweep dispatcher even though individual thunked callees remain unresolved in the raw import.

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] = frame_min (backward direction counter)
    • [0x1c] = frame_max
    • [0x1d] = current_frame
    • [0x1e] = loop_flag
    • [0x1f] = reverse_direction_flag
    • +0x3f (as char*) = completion handle/sentinel (-1 = none, 0x2802 = player entity)
  • On frame overflow: if completion handle valid and not player-entity, fires thunked event; calls vtable [+8] method.
  • Added decompiler comment at function entry explaining all fields and behavior.

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).

Global map additions (renamed in Ghidra)

Address Name Evidence
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: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_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
  • +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 entity_set_at_target_update_facing Sets entity +0x3a = 1 (arrived flag); calls entity_set_facing_direction; clears bit 0x10 from entity type table 0x7e1e[type*0x79+0x59]; tail-calls thunk to advance state. Called in the entity state machine context.

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: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.

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: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 (0x3aa6) until more direct gameplay semantics are recovered from its 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
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