# 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 | |---|---| | `0x00000`–`0x36F6F` | Phar Lap 286 DOS extender (outer MZ stub code) | | `0x36F70` | NE header (145-segment game image begins here in file) | | `0x6E570`+ | NE game segments at their Phar Lap linear load addresses | Mapping rule (verified for seg001 and seg021): ``` runtime_flat_base = NE_segment_file_offset + 0x36F70 ``` Example: seg004 at file `0x40A00` → runtime `0x77970` → Ghidra `0007:7970`. Functions at Ghidra `0003:XXXX` / `0004:XXXX` are **Phar Lap extender code** (flat < `0x40000` is below any game segment). Functions at `0006:E570`+ are game NE segments. ### `0000:ffff` — NE Fixup Placeholder (not a dispatcher) `unresolved_far_thunk_dispatch` at `0000:ffff` is NOT a runtime function. Every `CALLF 0x0000:ffff` in the binary is a **different** external or inter-segment call patched by the NE loader at runtime. The decompiler body is garbled (it reads NE fixup-chain sentinel data). Decompiler comment added in Ghidra. See individual call sites for per-site behavioral annotations. 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 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 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) ```c 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 `0007:224b` thunk call is an intra-NE inter-segment call (calling into a different game segment with ground-aligned coordinate math). Identifying it requires the NE relocation table or matching the disassembly in the standalone extracts. ### 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. - 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` (0–4, 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 (0x0060–0x0d5f) | Address | Name | Description | |----------|---------------------------|-------------| | `0x0060` | `cursor_update_hover` | Hover update: if mouse active & entity set, calls cursor_set_target | | `0x00e9` | `cursor_set_target` | Positions cursor on entity, updates sprite + direction visual | | `0x0322` | `cursor_shutdown` | Frees cursor resources, resets state | | `0x0398` | `cursor_animation_update` | Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6 | | `0x050f` | `cursor_draw_tick` | Per-frame cursor draw (calls cursor_animation_update if dirty) | | `0x0c24` | `action_key_valid` | Returns 1 if action code (param_1) is a valid game action key | | `0x0d5f` | `cursor_direction_input` | Arrow-key input: rotates cursor angle, updates direction sprite | ### Input Handling | Address | Name | Description | |----------|-------------------------|-------------| | `0x0526` | `input_keyboard_handler`| Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load | ### Cursor State Data (at DS:0x27xx) | Address | Field | Meaning | |---------|-------|---------| | `0x27c4` | cursor_sel1 | Selection counter 1 | | `0x27c6` | cursor_sel2 | Selection counter 2 | | `0x27c8` | current_entity | Handle to currently targeted entity | | `0x27ca–0x27ce` | cursor_state | Cursor interaction state bytes | | `0x27d0` | cursor_entity_type | Current entity type index | | `0x27d2` | z_offset | Z-height offset for terrain adjustment | | `0x27d4` | cursor_angle | Rotation angle (0–359) | | `0x27d6` | cursor_sprite | Sprite handle for cursor visual | | `0x27d8` | cursor_dirty | Set when cursor needs redraw | | `0x27d9` | cursor_active | Master cursor enabled flag | | `0x27da` | cursor_no_turn | Flag disabling cursor rotation | | `0x27ed` | difficulty | Enemy accuracy divisor (used in projectile_init_vector) | | `0x27fd` | hard_mode | Two-step mode (combat vs. explore) | | `0x27fe` | move_mode | Movement phase flag | | `0x27ff` | mouse_active | Mouse/input system active | | `0x2800`–`0x2811` | various | UI state: active sprite, facing byte, cur entity handle | | `0x283f`/`0x2841` | menu_obj_ptr | Active menu/dialog object far pointer | | `0x2844` | in_save | In-progress save game flag | | `0x290e` | entity_count | Number of active entities | | `0x2910`–`0x2947` | snap_type_ids[10] | Entity types that snap-to-ground in snap_entity_to_ground | ### Input / Action Dispatch | Address | Name | Description | |----------|---------------------------|-------------| | `0x2420` | `entity_command_dispatch` | Dispatches player commands to target entity; reads 0x27d0, 0x2de4, sends events 0x14/0xf, handles state machine 0x27ca/0x27cd/0x27ce | | `0x279a` | `cheat_code_check` | Checks entity byte+1 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 (0x2401–0x5a50) | Address | Name | Description | |----------|------------------------------|-------------| | `0x2401` | `clear_cursor_selection` | Zeros 0x27c4/0x27c6 (selection counters) | | `0x2899` | `cursor_switch_target_entity`| Switches cursor target: unloads old entity, loads new, re-registers | | `0x29d8` | `get_z_offset` | Returns func() + *(0x27d2) = adjusted Z/height | | `0x2a09` | `is_player_in_range` | Checks if entity is at player (0x2de4) X/Y +/-0xf0 range | | `0x2a46` | `entity_ai_update_loop` | Loops entities 2–255, checks visibility, triggers fire/move | | `0x2c36` | `ui_update_callback` | Calls cursor_state_clear then vtable[2] on menu object | | `0x2c6b` | `cursor_state_clear` | Clears cursor state bytes 0x27ca–0x27ce, clears entity flag bit1 | | `0x2c92` | `dialog_spawn` | Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca | | `0x2d47` | `entity_pick_handler` | Handles entity selection or save-game trigger (type 0x38d) | | `0x2df9` | `clear_active_menu` | Zeros 0x283f/0x2841 (active menu far pointer) | | `0x2e18` | `game_mode_init` | Initializes game mode state, resets sprite/cursor/menu state | | `0x2f3f` | `entity_table_set_sprite` | Reads 0x7df9+slot*2; writes entity type table 0x7e1e[slot*0x79+0x0d]=param_2, +0x10=0 | | `0x3c97` | `snap_entity_to_ground` | If entity type in snap_type_ids[10], resets Z to 0xf0 and adjusts XY | | `0x3d6e` | `spawn_entity_checked` | Spawns entity with explosion pool limit check (0x84c0, 0x84c2) | | `0x3f2f` | `entity_spawn` | Allocates entity, vtable=0x29aa/0x39ca, positions it | | `0x40d4` | `entity_remove` | Removes entity: destroys sprites, clears 0x2802/0x2804 if needed | | `0x4172` | `entity_animation_frame_update`| Advances/retreats anim frame ([+0x1d]) toward target [+0x1c/0x1b] based on quality | | `0x42f8` | `stub_noop_42f8` | Empty stub, noop | | `0x42fd` | `entity_registry_decrement` | Calls cleanup func then decrements entity count at 0x290e | | `0x4314` | `entity_sprite_move_delta` | Updates shot sprite handle (entity+0x3f) position by adding delta params | | `0x4552` | `entity_set_position` | Sets entity+0x3e (type_handle), world_x/y (entity+0x45/47), base_x/y (entity+0x4f/51) | | `0x452b` | `shot_set_spawn_pos` | Calls entity_set_position then sets entity+0xbe = param_3 (extra spawn field) | | `0x4591` | `entity_try_place` | entity_set_position with validation — position only set if placement succeeds | | `0x5092` | `entity_deactivate` | Calls vtable[2] to deactivate, or finds in registry and removes | | `0x5a50` | `entity_list_contains` | Checks if entity ptr exists in active entity list at 0x294c | | `0x5b05` | `stub_noop_5b05` | Empty stub, noop | ### Entity Object Layout (NE Segment 1 entities) | Offset | Field | Meaning | |--------|-------|---------| | `+0x00` | vtable_ptr | Vtable pointer (0x29aa for generic, 0x2a57 for debris) | | `+0x02` | slot_index | Entity slot index (used for registry at 0x39ca) | | `+0x04` | entity_type | Entity type ID | | `+0x19`/`+0x1a` | flags | Entity flags (bit0=debris, bit1=cleared by cursor_state_clear, bit6=active, bit8=valid) | | `+0x1b` | vel_x | X velocity (clamped ±0x20) | | `+0x1c` | vel_y | Y velocity (clamped ±0x20) | | `+0x1d` | vel_z | Z velocity (clamped ±0x10) | | `+0x1e` | fire_handle | Weapon/fire handle | | `+0x1f` | is_enemy | 1 if entity is an enemy type | | `+0x20`/`+0x21` | pos_frac_x/y | Fractional position (sub-tile) for movement | | `+0x22` | pos_frac_z | Fractional Z | | `+0x36` | weapon_type | Active weapon type ID | | `+0x38` | facing | Current facing direction (0–15) | | `+0x3c` | sprite_handle | Sprite for this entity | | `+0x3f` | shot_sprite | Sprite handle for active projectile (0xFFFF = none) | | `+0x45`/`+0x47`/`+0x49` | world_x/y/z | Current world position (integer) | | `+0x4f`/`+0x51`/`+0x53` | base_x/y/z | Base/spawn position | | `+0x54`/`+0x56`/`+0x58` | prev_x/y/z | Previous frame position | | `+0x59` | attack_active | Attack in progress flag | | `+0x5a` | at_target | Reached target flag | | `+0x5e`–`+0x65` | delta_x/y/z/high | Per-step movement deltas (fixed point) | | `+0x66`/`+0x68` | step_active | Stepping active (1=yes, 0=off) | | `+0x6a`/`+0x6c` | weapon_slot/dist | Weapon slot and total travel distance | | `+0x6e` | delta_z | Alt Z delta | | `+0x70` | projectile_type | Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain) | | `+0x72`/`+0x74`/`+0x76` | target_x/y/z | Target position with deviation | | `+0x77` | target_entity | Target entity handle | | `+0x79` | secondary_pos | Secondary position struct pointer | | `+0xad` | owner_entity | Owning entity handle | | `+0xaf` | shot_owner_flags | Shot owner (entity/player) | | `+0xb1` | bounce_count | Bounce counter (used with homing, type 5) | | `+0xb3` | has_bounce | Has bounce trajectory active | | `+0xbd` | actor_type | Actor type byte (used for direction table lookups) | ### Shot Entity Lifecycle (0x435e–0x44a9) | Address | Name | Description | |----------|----------------------------|-------------| | `0x435e` | `shot_entity_alloc` | Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7 | | `0x44a9` | `shot_entity_free` | Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1 | ### Projectile / Combat (0x4659–0x5a99) | Address | Name | Description | |----------|----------------------------|-------------| | `0x4659` | `projectile_init_vector` | Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536 | | `0x4a91` | `entity_fire_weapon` | Fires weapon from entity using 0x129b/0x12ac direction offset tables | | `0x4b18` | `fire_weapon_from_cursor` | Gets cursor angle sprites, fires projectile at cursor target | | `0x4b78` | `projectile_check_hit` | Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity) | | `0x4c2e` | `projectile_step_update` | Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked | | `0x4d28` | `projectile_trace_ray` | Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate | | `0x51ad` | `projectile_update_tick` | Full projectile tick: move, check reach target, bounce, call projectile_check_hit | | `0x5a99` | `projectile_apply_hit` | Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner | ### Weapon Type Table (0x2536) - Each entry is 0x11 bytes (17), accessed as `weapon_type * 0x11` - `[0]` = step divisor for distance calculation - `[0x19]` = max range threshold (used in projectile_update_tick) ### Direction Tables (0x129b / 0x12ac) - Indexed by facing (0–15): dx offsets at 0x129b, dy offsets at 0x12ac - Values are multiplied by distance (e.g. `*0x500`) for projectile spawn offsets ### Collision Detection (0x60c1–0x621e) | Address | Name | Description | |----------|----------------|-------------| | `0x60c1` | `aabb_overlaps_3d` | 3D AABB overlap test — box layout [xmin,ymin,zmin,_,_,xmax,_,ymax,_,zmax] | | `0x621e` | `bbox_translate` | Translates a 3D bounding box by (dx, dy, dz) — both min and max points | ### Enemy AI / Spawning (0x6aed–0x6d21) | Address | Name | Description | |----------|----------------------------|-------------| | `0x6aed` | `map_find_spawn_point` | Finds map tile matching entity conditions; returns packed XYZ tile coords | | `0x6bfc` | `actor_find_in_view` | Finds actor visible in current view frustum (temp data at 0x7eca) | | `0x6ce9` | `enemy_spawn_with_target` | Wrapper: spawns enemy with player as target (param5=1) | | `0x6d05` | `enemy_spawn_no_target` | Wrapper: spawns enemy without targeting player (param5=0) | | `0x6d21` | `enemy_spawn_at_position` | Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A) | ### Player / HUD | Address | Name | Description | |----------|-------------------------------|-------------| | `0x50ee` | `player_position_update` | Updates player position from direction data; clamps to screen bounds | | `0x6ff7` | `player_health_update_and_effect` | Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect | ### Destruction / Death (0x7490–0x75ff) | Address | Name | Description | |----------|---------------|-------------| | `0x7490` | `debris_spawn`| Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list | | `0x75ff` | `entity_die` | Death handler: spawns 1–4 debris objects, picks best explosion direction | ### Entity Type Constants (weapon_type/entity class) | Value | Entity Class | |--------|--------------| | `0x17` | Robot/mech type A | | `0x18` | Robot/mech type B | | `0x1` through `0x3c` | Various entity/weapon types | | `0x3d` | Robot/mech type C | | `0x3e` | Robot/mech type D | | `0x2f5`–`0x2f7` | Special movement entity | | `0x595`/`0x597` | Platform/elevator entities | | `0x31c`/`0x322`–`0x327` | Explosive/effect entities | | `0x38d` | Save game trigger entity | | `0x426` | Spark/scatter sub-shot | | `0x59a` | Player cursor/select indicator | ### Entity Data Table at 0x7e1e - Stride: `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 1. ✅ **NE Segment 1 imported and analyzed** — all 58 identified functions renamed and annotated 2. ✅ **Raw 0007 segment analyzed** — rendering, camera/scroll, save slot, and scroll region subsystems documented (~60 functions renamed and annotated) 3. **Import additional NE segments** — priority: segments 22, 30, 59, 86 (segment 21 complete) 4. **Analyze raw 0007 draw helper cluster** — `FUN_0007_03b4`, `FUN_0007_04b8`, `FUN_0007_04dc`, `FUN_0007_057f`, `FUN_0007_0614`; called by sprite/draw list functions 5. **Analyze `FUN_0007_4cdf`** — large 15-case animation/movement dispatcher; overlapping instruction warnings; cases 0, 2, 3, 6, 9, 0xa, 0xe are clean 6. **Map file format loaders** — `.FLX`, `.SHP`, `.MAP`, `.TNT` resource formats 7. **Cross-reference entity type constants** with game entities (robots, platforms, triggers) 8. **Identify external segment calls** — the `func_0x0000ffff()` placeholders are all cross-segment calls; resolving them requires importing the referenced segments ## Raw 0007 Rendering & Sprite Draw List Subsystem (new) A complete sprite draw list and tile-based visibility system was recovered from the `0007:e000–0007:fe00` flat range (above seg001, separate segment loaded at `0x7E000+`). ### Isometric Coordinate Transform | Address | Name | Evidence | |---------|------|---------| | `0007:be67` | `world_to_screen_isometric` | Classic 2:1 isometric formula: `screen_x = (wx + sx) + (wy + sy)*2`, `screen_y = (wy + sy)*2 - (wx + sx)`. Scroll globals: `0x2bb7` (X), `0x2bb9` (Y). Output to `*param_3`, `*param_4`. | | `0007:be9e` | `world_to_screen_isometric_wrapper` | Thin wrapper — calls `world_to_screen_isometric` with args shifted by one | ### Draw List Node Format Sprite/draw node (size `0x18` = 24 bytes from pool, allocated by `linked_list_pop_2cc3`): | Offset | Field | Notes | |--------|-------|-------| | `+0x0` | vtable | Function ptr for render callback (vtable[0] = draw) | | `+0x8` | dep_from_list | Near ptr to list of sprites that depend ON this | | `+0xa` | dep_to_list | Near ptr to list of sprites this depends ON | | `+0xc` | bbox_xmin | Screen bounding box (4 ints) | | `+0xe` | bbox_ymin | | | `+0x10` | bbox_xmax | | | `+0x12` | bbox_ymax | | | `+0x14` | flags | bit 0=queued in draw list, bit 5=visible/rendered, bit 6=root sentinel, bit 7=must-redraw | | `+0x15` | z_depth | int; sort key for painter's algorithm. 1 = root sentinel | | `+0x17` | order_flag | if set, propagates must-redraw to dependent sprites | | `+0x18` | tile_index | sparse tile index (for dirty bitmask bit addressing) | ### Draw List Functions | Address | Name | Evidence | |---------|------|---------| | `0007:eb36` | `drawlist_pool_init` | Inits free pool: 1500 nodes × `0x2a` (42) bytes from base `0x2cc7`. Sets `0x2ccf = 1500` (count), `0x2cc3` = linked list head. Copies `0x2cc9` → `0x2cae`. | | `0007:eb12` | `linked_list_push_2cc3` | LIFO push: `node->next = head_2cc3; head = node; count_2ccf++`. | | `0007:eada` | `linked_list_pop_2cc3` | LIFO pop: dequeues node from `0x2cc3` head, decrements `0x2ccf` count. | | `0007:ebd9` | `linked_list_dequeue_headtail` | FIFO dequeue from head/tail pair list; calls `FUN_0007_03ec` after dequeue. | | `0007:ec2c` | `drawlist_enqueue` | FIFO enqueue: appends node to draw list (head if empty, tail always updated). | | `0007:ec63` | `drawlist_remove_node` | Unlinks specific node from head or tail position; calls `FUN_0007_03ec`. | | `0007:eca8` | `drawlist_process_and_render` | **Two-stage render pass.** Stage 1: drains `0x8442` (pending), viewport-tests each sprite, moves in-bounds to `0x8446` (visible). Stage 2: drains `0x8446`, calls sprite vtable[0] to draw. Recursive re-run if `0x2cb2` defer flag set. | | `0007:edfa` | `drawlist_enqueue_sprite_children` | Enqueues all child sprites at node `+8` linkage to draw pending list if not already queued or rendered. Skips if `+0x15` z-depth == 1. | | `0007:ef3d` | `sprite_add_draw_dependency` | Links sprites A and B with z-order dependency. Allocates link node via `FUN_0007_057f`. Stores in A's `+10` and B's `+8`. Sets must-redraw on B if A has `+0x17` set. | | `0007:ef9f` | `sprite_enqueue_for_draw` | Null-guard; sets node `+0x14 \|= 1`, enqueues to `0x8442`. | | `0007:efca` | `sprite_invalidate_and_unlink` | Full sprite removal: re-enqueues all dependents, frees link nodes via `FUN_0007_0614`, clears `+8/+10` lists, dequeues self from `0x8442`. | | `0007:f2a0` | `sprite_sort_by_depth_and_link` | Compares `+0x15` z-depth of two sprites; calls `sprite_add_draw_dependency` in the correct order (lower z first). Handles equal-depth via thunk. | | `0007:f654` | `drawlist_init` | Full draw system init: `drawlist_pool_init`, init full-screen viewport (`0x844a..0x8450 = 0..639, 0..screen_h`), allocate root sentinel node (vtable `FUN_0000_2ce4`, z-depth=1, flag=0x40), stored at `0x846a`. | | `0007:ea00` | `bbox_intersect` | In-place 2D rect intersection: max of mins, min of maxes. Input/output: 4-int array `[xmin,ymin,xmax,ymax]`. | | `0007:ea6d` | `bbox_union` | In-place 2D rect union: min of mins, max of maxes. | | `0007:ee5a` | `viewport_update_from_sprite_bounds` | Subtracts scroll offsets from sprite bbox; clips to screen rect (`0x2ca6..0x2cac`); calls `bbox_intersect`; stores updated viewport at `0x844a..0x8450`; dispatches to render. | ### Tile-Based Visibility System The rendering system uses a **6×5 tile grid** at DS:`0x846a` (30 entries × 2 bytes each =60 bytes). Tiles represent screen regions of approximately 128 screen pixels per tile. Each tile holds a near pointer to a linked list of sprite nodes overlapping that tile. Bitmasks track dirty and renderable tiles. | Address | Name | Evidence | |---------|------|---------| | `0007:f9e2` | `drawlist_mark_dirty_tiles` | Converts bbox to tile grid coords (divide by 128, clamp to 6×5). Walks all sprites in overlapping tiles' spatial buckets; re-enqueues overlapping sprites. Sets bits in dirty bitmask `0x2cbb`. | | `0007:fb53` | `tile_visibility_update` | Iterates redraw bitmask (`0x2cbb`, up to 10240 tiles). For each dirty tile with a sprite, computes isometric screen position from map X/Y/Z. Checks sprite dimension bitfields at `pbVar8+2..+3` against 640×480 viewport. Sets corresponding bit in render bitmask `0x2cb7`. Clears dirty bitmask after processing. | | `0007:fd98` | `tilemap_draw_if_dirty` | Guard: if render bitmask `0x2cb7` non-zero, calls `thunk_FUN_0007_001d` to trigger tilemap draw. | ### Draw List / Viewport Globals | Address | Name | Notes | |---------|------|-------| | `0x2bb7` | `g_scroll_offset_x` | World-to-screen X offset (scroll) | | `0x2bb9` | `g_scroll_offset_y` | World-to-screen Y offset (scroll) | | `0x2ca6..0x2cac` | `g_screen_clip_rect` | Screen clip rectangle [xmin,ymin,xmax,ymax] | | `0x2cae` | `g_draw_segment` | DS segment for draw node far pointers | | `0x2cb0` | `g_free_pool_seg` | Segment for node free pool far calls | | `0x2cb2` | `g_render_defer_flag` | If set, defers current render pass | | `0x2cb7` | `g_render_tile_bitmask` | Far ptr to bitmask of tiles ready to render | | `0x2cbb` | `g_dirty_tile_bitmask` | Far ptr to bitmask of dirty/changed tiles | | `0x2cbf` | `g_tile_origin_x` | Tile grid world origin X (for tile coordinate math) | | `0x2cc1` | `g_tile_origin_y` | Tile grid world origin Y | | `0x2cc3` | `g_free_pool_head` | Free node pool linked list head | | `0x2ccf` | `g_free_pool_count` | Free node pool remaining count (max 1500) | | `0x8442` | `g_draw_pending_list` | Draw pending list head/tail (near ptrs) | | `0x8446` | `g_draw_visible_list` | Draw visible list head/tail (near ptrs) | | `0x844a..0x8450` | `g_viewport_rect` | Current viewport [xmin,ymin,xmax,ymax] | | `0x846a` | `g_tile_grid_base` | 6×5 tile grid spatial index (near ptr per tile) | ## Raw 0007 Map Scroll / Camera Subsystem (new) A scroll/camera management cluster found in the `0007:bxxx–0007:dxxx` range. ### Entity State Transition Helper | Address | Name | Evidence | |---------|------|---------| | `0007:5b6f` | internal block only *(no function after repair)* | Direct raw-analysis behavior remains useful as a local label: this block sets entity `+0x3a = 1` (arrived flag), calls `entity_set_facing_direction`, clears bit `0x10` from entity type table `0x7e1e[type*0x79+0x59]`, then tail-calls onward. After the PyGhidra boundary repair, `0007:5b6f` is no longer a function entry and should be treated only as an internal control-flow label inside the first repaired seg043 routine. | ### seg043 Standalone Boundary Recovery - Direct disassembly of `NE_segments/seg043_code_off_75A00_len_336F.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: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 (R−offset, G−offset, B−offset) clamped to 0 to VGA I/O `0x3c8`/`0x3c9`; decrements `[0x630d]` by step `[0x6316]`; clears active at `[0x630a]` when black | | `000c:ce57` | `palette_fade_step_up` | Same loop, adds offset, clamps at 63 (0x3f full VGA). Clears `[0x630a]` when fully bright | Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src ptr, `[0x630d]`=brightness offset, `[0x6316]`=step, `[0x630a]`=active flag. ### Entity Mini-VM / Record-Player Context (000c:f6b8-000c:f98b) | Address | Name | Evidence | |---------|------|---------| | `000c:f6b8` | `record_table_get_by_index` | Bounds check `param < [0x8c88]`; return `word at [0x8c84 + param*4]`. Table at 0x8c84 | | `000c:f6e8` | `entity_vm_stack_init_with_data` | Init stack ptrs at `[ptr+0xcc..+0xd4]` pointing to self; max depth 199; copies optional initial data | | `000c:f772` | `entity_vm_state_copy` | Copies 200 bytes (100 words from `[src+4]` to `[dst+4]`), then copies 4 words at `+0xcc..+0xd2` | | `000c:f7c7` | `entity_vm_stack_push_frame` | Push call-frame: saves ret offset at `[ptr+0xd4]`, decrements `[ptr+0xcc]` by param_size, zeroes new frame | | `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params | | `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full | | `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty | | `000c:f94f` | `entity_vm_counter_add` | `[ptr+0xd6] += param_2`; simple accumulator | | `000c:f98b` | `entity_vm_set_field_da_to_global` | Writes `[param_2+0xda far-ptr + 2]` into `[0x8c94]` | Notes: - Field offsets `+0xcc`=VM stack ptr, `+0xce/+0xd0`=segment regs, `+0xd2`=base, `+0xd4`=frame depth, `+0xd6`=counter, `+0xd8/+0xda/+0xdc`=VM position/bounds. - The 200-byte body region at `[ptr+4..+0xcc]` holds 100 words of script/state payload. - The pair-stack (field `+0x80`) is separate — likely pass/return value stack for the mini-script. ## Raw 000c Cursor Zone / Slot Array / String Queue Batch (new) Three utility subsystems identified in `000c:e6d9-000c:eadd`, plus companion slot array API. ### Cursor / Directional Zone Classifier | Address | Name | Evidence | |---------|------|---------| | `000c:e6d9` | `cursor_zone_quadrant_classify` | Splits screen by `[0x63d6]/2` and `[0x63d8]/2` vs bounds `[0x8c6c..0x8c72]`; returns directional code from 9-word table at `0x6401` | Zone table layout (9 entries): NW/N/NE / W/Center/E / SW/S/SE based on horizontal threshold at `0x8c6c/0x8c70` and vertical at `0x8c6e/0x8c72`. ### Slot Array System (000c:ea53-000c:ee44) A complete 29-slot menu/choice array with fixed stride 0x15 bytes, base at `[ptr+0x67]`, count at `[ptr+0x7a]`: | Address | Name | Evidence | |---------|------|---------| | `000c:ea53` | `entity_slot_count_update_and_notify` | Sets `[ptr+0x72]=param-1`; calls `slot_array_get_current_entry` and `slot_array_find_and_dispatch`; calls `vtable[0]()` when `+0x75` flags set | | `000c:eba5` | `slot_array_dispatch_matching` | Walks 0xb-stride array from `[ptr+4]`; calls thunk for each entry where `[entry+9]==param_4` | | `000c:ec30` | `slot_array_dispatch_if_nonempty` | Returns `0xffff` if count < 1; else dispatches | | `000c:ec9e` | `slot_array_find_and_dispatch` | Searches 0xb-stride array for `[entry+9]==param_4`; calls thunk on first match | | `000c:ecf5` | `slot_array_push_entry` | Copies named string to `[base+0xc]`; writes 6 param words at `+0x12..0x20`; increments count | | `000c:edb0` | `slot_array_push_raw` | Copies 0x15-byte raw entry from `param_2`; increments count | | `000c:edf7` | `slot_array_pop` | Decrements `[ptr+0x7a]`; asserts `>= 0` | | `000c:ee19` | `slot_array_init` | Sets `[ptr+0x78]=0`, `[ptr+0x76]=0`, `[ptr+0x75]=1` (active flag) | | `000c:ee32` | `slot_array_clear_flags` | Clears `[ptr+0x74]=0`, `[ptr+0x75]=0` | | `000c:ee44` | `slot_array_get_current_entry` | Returns `ptr + [ptr+0x7a]*0x15 + 0x67` (current entry ptr); 0 if count <= 0 | ### String Queue | Address | Name | Evidence | |---------|------|---------| | `000c:eadd` | `string_queue_push` | Appends string to 10-entry queue at `[ptr+4]`; count at `[ptr+2]`; sets `[ptr+0xd]=param_4` | ### Additional VM-Adjacent Helpers | Address | Name | Evidence | |---------|------|---------| | `000c:f2e7` | `entity_call_vtable_entry_10_if_valid` | Null-guard: calls `(*[ptr+8+0x10])()` if param_1 non-null | | `000c:f39f` | `string_table_lookup` | Searches `[0x65bc/0x65be]` table by key string; returns matching words to out-params | ## Raw 000c Cursor Nav Dispatcher / State Reset Batch Cursor navigation subsystem in `000c:d3e9–000c:db68`. Manages directional zone changes, mouse button reads, keyboard scancodes, and entity vtable dispatch for interactive UI cursors. ### Cursor Navigation Fields (entity object offsets) | Offset | Purpose | |--------|---------| | `+0x32` | Current zone code (0–8) | | `+0x33` | Previous zone code | | `+0x37–+0x3a` | Directional booleans: N/S/W/E | | `+0x3f–+0x42` | Mouse button flags | | `+0x45` | Last keyboard scancode | | `+0x47` | Navigation index | Globals: `[0x63da]` = mouse button state, `[0x63d6]/[0x63d8]` = cursor X/Y, `[0x638e]` and `[0x6346]` = reference data tables. ### Functions | Address | Name | Evidence | |---------|------|---------| | `000c:dac1` | `cursor_nav_state_reset` | Zeros all directional/button flags; sets `[+0x32/+0x33]=0xff`, `[+0x47]=0xffff` | ## Top-40 Most-Called Far-Call Targets (NE Fixup Resolution) Named via systematic analysis of 11,692 NE relocation fixup entries. These are the functions most frequently called through the `CALLF 0x0000:ffff` thunk mechanism. ### Tier 1: Top 20 (73+ callers) | Rank | Address | Name | Calls | Description | |------|---------|------|-------|-------------| | 1 | `000a:44fd` | `seg091_func_00fd` | 331 | Recovered boundary. Shares init flag `0x44a4` with `runtime_init_or_abort`; thunk-heavy non-returning wrapper. | | 2 | `0003:ac7e` | `mem_alloc` | 272 | Allocation wrapper → seg082:0000 (`0009:a200`) | | 3 | `0008:dbec` | `entity_word_list_destroy` | 238 | Already named. Frees entity word-list buffer. | | 4 | `0003:a751` | `mem_free` | 207 | Free wrapper → seg082:007a (`0009:a27a` = `mem_free_checked`) | | 5 | `0008:bb4f` | `mem_alloc_far` | 174 | Thin wrapper → `mem_alloc` | | 6 | `0003:a897` | `far_memcpy` | 165 | REP MOVSW + trailing MOVSB | | 7 | `0005:088f` | `entity_get_type_word` | 130 | Returns type word from table 0x7df9 indexed by slot | | 8 | `000b:358d` | `sprite_tree_accumulate_pos` | 122 | Recursively sums X/Y offsets (+0x21/+0x23) through linked child nodes (+0x19/+0x1b), copies 8-byte position block via far_memcpy | | 9 | `0008:ce3d` | `entity_call_two_vtables` | 118 | Calls vtable[+4] at entity+0x1e and +0x28 | | 10 | `0004:26cd` | `nop_void_stub` | 118 | Empty function, returns void | | 11 | `0008:ce00` | `entity_call_two_vtables_base` | 117 | Calls vtable[0] at entity+0x1e and +0x28 | | 12 | `0008:bb8c` | `entity_check_flag_0x4000` | 115 | Short-circuits if flag 0x4000 set at +0x16 | | 13 | `0008:cda7` | `entity_free_both_word_lists` | 115 | Frees word lists at entity+0x1e and +0x28 if optional pointers at +0x24/+0x26 and +0x2e/+0x30 non-null. Both call `entity_word_list_free_existing`. | | 14 | `0004:26d2` | `nop_void_stub_b` | 111 | Empty function, returns void | | 15 | `000a:45fe` | `runtime_init_or_abort` | 108 | Reentrancy-guarded init. Flag at 0x44a4; flushes via FUN_000a_4a56, then calls `crt_exit_wrapper(1)`. Hidden code gap 0x4616-0x4643. | | 16 | `0004:3324` | `nop_return_zero` | 95 | Returns 0 | | 17 | `0009:c563` | `event_queue_push` | 82 | Circular buffer enqueue. Ring index (+0xe) masked 0x3f, slot masked 0xfff8. Writes event type word + data byte pair. | | 18 | `0005:c448` | `list_remove_and_free` | 74 | Unlinks node from linked list via FUN_0005_c495, optionally calls `mem_free` if bit 0 of flags set | | 19 | `000b:2e00` | *(no function in Ghidra)* | 74 | Analysis gap at seg109:0000. Needs manual function creation. | | 20 | `0009:1f12` | `dos_file_lseek` | 73 | DOS LSEEK (INT 21h AH=42h) wrapper with error reporting to 0x867a | ### Tier 2: Ranks 21-40 (56-73 callers) | Rank | Address | Name | Calls | Description | |------|---------|------|-------|-------------| | 21 | `0009:3600` | `rotating_buffer_advance` | 73 | Advances 5-slot circular counter at 0x3eb6, zeros pointer in table at 0x867c, dispatches via jump table | | 22 | `0009:943a` | `entity_rect_compare_and_dispatch` | 68 | Compares bounding rectangles of two entities, dispatches based on flag bits 4/2/1 at +0x16 | | 23 | `0009:1e61` | `dos_file_close` | 65 | DOS file close (INT 21h), error reporting, sets handle to -1 | | 24 | `0005:e252` | *(unnamed — unclear)* | 65 | Copies 11 words from Phar Lap extender area (FUN_0000_12c6+5), then calls thunk. Interrupt/trampoline setup? | | 25 | `0003:dbcc` | `crt_format_string` | 64 | MetaWare High C formatting wrapper. Calls FUN_0003_bb92 with runtime format dispatch table. | | 26 | `0007:5a00` | *(no function in Ghidra)* | 64 | High-traffic raw target at `seg043:0000`. Earlier `debris_spawn` / seg001 mapping was rejected after checking relocation labels. Still needs manual function creation and direct analysis. | | 27 | `000a:4742` | `assert_buffer_valid` | 63 | Validates handle: asserts param_2 == cookie at 0x45a6 and param_1 < limit at 0x87e0 | | 28 | `0009:9216` | `entity_conditional_render_dispatch` | 63 | Checks entity flag bits 4 and 1 at +0x16, dispatches to vtable[+0xc] or thunk | | 29 | `0008:cb2c` | `entity_flag20_clear_and_update_target` | 61 | *(already named)* Clears flag bit 0x20, writes target +0x12/+0x14, calls refresh | | 30 | `0008:cb5c` | `entity_flag20_set_and_init_target` | 61 | *(already named)* Sets flag bit 0x20, inits target if zero, calls refresh | | 31 | `0007:7306` | `entity_create_stack_object` | 58 | Allocates 0xCC bytes on stack, inits via `object_init_zero_fields` (0005:c400), calls thunk | | 32 | `0007:8709` | `entity_mark_dirty_and_sync_tile_aux` | 58 | *(already named)* Syncs tile aux, sets flag bit 0x04 at +0x42 | | 33 | `0007:87c5` | `entity_set_flag20_from_field42` | 58 | Reads entity+0x42/+0x44, calls `entity_flag20_set_and_init_target` with those values | | 34 | `0007:8508` | `entity_table_lookup_and_dispatch` | 58 | *(already named)* Searches table at 0x2b46, dispatches via indirect jump | | 35 | `0007:8920` | `entity_call_vtable_slot0c` | 58 | *(already named)* Calls vtable entry at +0x0c | | 36 | `000a:b988` | `sprite_node_get_or_traverse` | 57 | If child pointer at +0x19/+0x1b non-null, traverses; otherwise returns leaf value | | 37 | `0003:a98b` | `crt_signed_div32` | 56 | Entry: adjusts near→far stack, sets CX=0 (signed quotient), jumps to `crt_div32_impl` | | 38 | `000a:7b44` | `nop_return_void_a` | 56 | Empty function (default vtable slot?) | | 39 | `000a:7b49` | `nop_return_void_b` | 56 | Empty function (default vtable slot?) | | 40 | `000a:7b53` | `nop_return_void_c` | 56 | Empty function (default vtable slot?) | ### Supporting Functions Discovered | Address | Name | Description | |---------|------|-------------| | `000b:3a00` | `sprite_tree_sum_x_offset` | Recursive: sums field +0x21 through child chain +0x19/+0x1b | | `000b:3a35` | `sprite_tree_sum_y_offset` | Recursive: sums field +0x23 through child chain +0x19/+0x1b | | `0003:a845` | `crt_exit_wrapper` | Calls `crt_exit_impl(param,0,0)` | | `0003:a7ee` | `crt_exit_impl` | Full C exit: atexit handlers, stdio flush, MetaWare runtime cleanup | | `0003:a9a8` | `crt_div32_impl` | 32-bit division core. CX flags: bit0=unsigned, bit1=modulo, bit2=negate | | `0005:c400` | `object_init_zero_fields` | Zeros fields +0x25, +0x29, +0x31, +0x32 of a struct. Returns pointer. | | `000a:4440` | `joystick_read_axes_and_buttons` | Reads PC game port 0x201. Times axis responses, reads button nibble to 0x44a2 | | `000b:3380` | `sprite_node_is_dirty` | Checks flags at obj+0x29 & 3 == 1 or 3 → returns bool | | `000b:33a6` | `sprite_node_mark_dirty` | If not dirty, calls FUN_000b_3965 with mode=3 to invalidate | ### Tier 3: Ranks 41-60 (42-56 callers) | Rank | Address | Name | Calls | Description | |------|---------|------|-------|-------------| | 41 | `000a:7b58` | `nop_return_zero_b` | 56 | Returns 0 (default vtable slot) | | 42 | `000b:3ab2` | `sprite_node_dispatch_event` | 56 | Large event dispatch: checks event type (2/4/8/0x100), updates global focus ptr at [0x4fd0:4fd2], dispatches via vtable methods [+0x14/+0x18/+0x20/+0x24] by event code. Switch table for 16 event types. | | 43 | `000a:48ff` | `rng_next_modulo` | 55 | Advances seg091 RNG state and returns the result modulo the requested bound; returns 0 when bound is 0. | | 44 | `000b:3362` | `sprite_tree_unwind_check` | 55 | Validates SS == param_2 (stack segment guard), then decrements global counter at [0x4fd6] | | 45 | `000b:40ee` | `sprite_node_update_and_dispatch` | 55 | If `sprite_node_is_dirty` returns false: marks dirty, calcs accumulated bounds via `sprite_tree_get_accumulated_bounds` (3ed8), then dispatches via thunk | | 46 | `000a:7b5f` | `vtable_stub_trampoline` | 55 | Calls through fixup thunk (forwarder to another function) | | 47 | `000a:7b78` | `nop_return_void_e` | 55 | Empty function (default vtable slot) | | 48 | `000a:7b7d` | `nop_return_void_f` | 55 | Empty function (default vtable slot) | | 49 | `000a:7b4e` | `nop_return_void_d` | 54 | Empty function (default vtable slot) | | 50 | `000b:330c` | `sprite_tree_dispatch_wrapper` | 52 | Pure thunk wrapper: calls through fixup | | 51 | `0009:2034` | `dos_file_seek` | 51 | INT 21h AH=42h (LSEEK). Takes file object ptr, extracts handle at obj+4, seeks to offset param. Error reporting to [0x867a]. | | 52 | `0005:0466` | `entity_resolve_slot_ptr` | 50 | *(already named)* | | 53 | `0003:a880` | *(no function in Ghidra)* | 49 | Analysis gap in CRT segment | | 54 | `0006:170c` | `tile_class_get_byte` | 47 | Looks up class data: indexes into table at [0x7e1e] by (*param_1 * 0x79), returns byte at offset +0xc | | 55 | `000b:4097` | `sprite_dispatch_with_event` | 45 | Pushes event params + global [0x49c2:0x49c4], calls thunk | | 56 | `0005:02c1` | `entity_is_type_match` | 43 | Compares *param_1 against global at [0x27c8], returns 1 if equal, 0 otherwise | | 57 | `0003:ad75` | *(no function in Ghidra)* | 43 | Analysis gap in CRT segment | | 58 | `000a:e709` | `render_dispatch_by_flag` | 43 | Dispatches between two thunk paths based on boolean flag at stack+0x10 | | 59 | `0003:d0ff` | `crt_sprintf_wrapper` | 42 | Calls FUN_0003_bb92 (format engine) with rearranged params and string constant at 0x67ac | | 60 | `000b:326e` | `sprite_node_destroy` | 42 | Destructor: sets vtable ptr to 0x501a, clears global [0x4fd0:4fd2] if self, releases child nodes, calls mem_free via thunk | ### Updated Analysis Gaps `0007:5a00` / `0007:5b6f` reconciliation: - The earlier standalone seg001 port hypothesis in this subrange was wrong. - Relocation data places raw `0007: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:** ```c 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]+*p1*0x79+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]+*p1*2 — 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 | **Entity Table Pointers (DS-relative, discovered in tier 5):** | DS Offset | Type | Stride | Purpose | |-----------|------|--------|---------| | `0x7dfd` | byte[] | 1 | Entity flags byte (entity_get_flags_byte) | | `0x7e01` | word[] | 2 | Entity class flags (bit 8 = has parent in hierarchy) | | `0x7e05` | word[] | 2 | Entity sprite/visual index | | `0x7ded` | word[] | 2 | Entity parent/hierarchy index | | `0x7df1` | word[] | 2 | Entity base type word | | `0x7e1e` | struct[] | 0x79 | Entity class detail records (121 bytes per class) | ### Recent Manual Boundary Repairs Recent high-traffic addresses recovered with manual function creation in Ghidra/PyGhidra: | Address | NE Segment | Callers | Notes | |---------|-----------|---------|-------| | `000a:48ff` | seg091:04ff | 55 | Recovered as `rng_next_modulo`; manual boundary repair narrowed to `000a:48ff-000a:4912`. | | `000b:2e00` | seg109:0000 | 74 | Start of segment 109. | | `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. | | `0009:a200` | seg082:0000 | - | Target of `mem_alloc`. Start of segment 82. | | `000c:db68` | `cursor_nav_update_and_dispatch` | Calls `cursor_zone_quadrant_classify`; updates `[+0x37..+0x3a]`; reads `[0x63da]`; switch on direction (0–8); maps scancodes 0x48/0x50/0x4b/0x4d/0x39 | | `000c:d3e9` | `cursor_set_ref_and_dispatch` | Null-checks param; sets `*param_1 = &DAT_0000_638e`; calls dispatch | | `000c:d710` | `cursor_set_ref2_and_dispatch` | Same pattern; sets `*param_1 = &DAT_0000_6346` | | `000c:d75e` | `entity_call_vtable_1e_via_ptr` | Calls `(*[*param_1 + 0x3c])()` — vtable offset 0x1e | | `000c:d775` | `entity_call_vtable_1e_via_ptr_b` | Near-identical to `d75e`; duplicate generated by compiler | | `000c:d7c6` | `stub_noop_000c_d7c6` | Empty stub | | `000c:d7cb` | `stub_noop_000c_d7cb` | Empty stub | Direction code mapping (from `cursor_nav_update_and_dispatch` switch): - 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW, 8=Center ### Timer Rate Sync (cursor subsystem) | Address | Name | Evidence | |---------|------|---------| | `000c:e4e0` | `cursor_timer_rate_sync` | If `[0x63e0]` (cursor active) non-zero: copies PIT rate `[0x39ce]` into entity object at `[0x4458+0x24]`, clears `+0x26/+0x27`, then calls far thunk. Called from `cursor_nav_update_and_dispatch` at `000c:db97`. | ## Raw 000c UI Listbox Event Handler Batch Analysis of `000c:880c` and `000c:88b4` — the primary UI event-dispatch island in segment `000c`. ### Overview `ui_listbox_event_handler` (`000c:88b4`) is a **large UI widget event handler** (~8KB of code, `88b4` through `9daa`). It takes the signature `(entity_far_ptr, seg, event_far_ptr)` and is stored as a far-pointer vtable entry (no direct code xrefs found; referenced from data). It dispatches on the event code at `[event+0x6]` (keystroke/mouse codes) and updates entity fields to drive menu/listbox navigation state. Guard function `entity_state_if_flag80_call_thunk` (`000c:880c`) sits just before the handler: it checks entity flag bit `0x80` at `[param_1+0x5b]` and gates a far thunk call. ### Functions | Address | Name | Evidence | |---------|------|---------| | `000c:88b4` | `ui_listbox_event_handler` | Dispatches on event code `[event+0x6]`: confirm keys `0xd`/`0x20`/`0x152` → `entity_state_tick_dispatch`; nav keys `0x148`/`0x14b`/`0x14d`/`0x150` → same; list-prev `0x2c`/`0x3c`/`0x55` → `entity_state_advance_next_or_fallback_b`; list-next `0x2e`/`0x3e`/`0x53`/`0x73`/`0x75` → `entity_state_advance_next_or_fallback_a`; Esc `0x1b` → tick dispatch; comma `0x2c`/`0x3c` → `ac8f`; misc UI events `0x6f`/`0x7e`/`0x13b–0x143`/`0x241`/`0x410`/`0x420`/`0x426`/`0x42f`/`0x432`/`0x441` → various paths. Guards: `[BX+0x5b] & 0x80` (active), `& 0x100` (modal lock from `[0x604b]`, `[0x844]`). Large scan loop `0x100..0x27ff` at `000c:a80f` for option/device enumeration. Falls through to common retf at `9da7`. | | `000c:880c` | `entity_state_if_flag80_call_thunk` | Guard: tests entity `[param_1+0x5b] & 0x80`; returns if clear; else calls far thunk. Active/visible gate for forwarding display/tick calls. | | `000c:ace5` | `display_fullscreen_blit_with_entity` | If coords (param_2:param_1) are 0:0, calls wait thunk (`0x48c`=1164ms?); loads display object at `[0x4cd0]`, reads byte `[+0x5]` (video mode/palette), builds full-screen 640×480 rect (`0`..`0x27f`=639, `0`..`0x1df`=479), then calls far display function with entity + position params. Returns display-space coordinate pair in DX:AX. | ### Key Globals in this Handler | Address | Meaning | |---------|---------| | `[0x7e22]` | String/resource pointer used as data context (`[0x7e24]` = flag) | | `[0x604b]` | Modal active lock (nonzero = block most events) | | `[0x844]` | Engine/game-ready flag (0 = block hardware toggle events) | | `[0x604f]` | Toggle state for event `0x410` | | `[0x6045]` | Toggle state for event `0x7e` | | `[0x8638]` | Counter for event `0x432` (wraps 0x11→0x12..0x14→0x0) | | `[0x5e82/0x5e84]` | Far pointer called when entity flag `0x40` is cleared | | `[0x4cd0]` | Display/screen manager object far pointer | ### Event Code Reference Table (partial) | Code | Meaning | |------|---------| | `0x0d` / `0x20` | Enter / Space (confirm) | | `0x1b` | Escape (cancel) | | `0x2c` / `0x3c` | Comma / `<` (prev in list) | | `0x2e` / `0x3e` / `0x53` / `0x73` | Period / `>` / S / s (next in list) | | `0x55` / `0x75` | U / u (up action with far-ptr check) | | `0x6f` | o (some option panel event) | | `0x7e` | `~` (toggle event, gates on `[0x844]` and `[0x6045]`) | | `0x13b` | Shift+F1 area | | `0x13c` | Jump-to-display event | | `0x13e` | Blanked event (gates on `[0x604b]`) | | `0x141` / `0x142` / `0x143` | Option toggles (gate on `[0x844]`, `[0x2bca/0x2bc9]`) | | `0x148` / `0x14b` / `0x14d` / `0x150` | Arrow keys (Up/Left/Right/Down) | | `0x152` | Insert key (alias confirm) | | `0x241` | Display position query → `display_fullscreen_blit_with_entity` | | `0x410` | Toggle event with `[0x604f]`, gates on `[0x844]` | | `0x420` | List-scan init (loop 0x100..0x27ff) | | `0x426` | String-push event (calls DS:[0x7e22]) | | `0x42f` | Push `0x103` thunk event | | `0x432` | Counter cycle event (`[0x8638]` wrap 0x11→0x12, 0x14→0x0) | | `0x441` | Pattern-fill loop event | ## NE Segment 21 Analysis — Timer/Event Dispatch System **File**: `seg021_code_off_50200_len_4486.bin` | **File Offset**: 0x50200 | **Length**: 0x4486 bytes **Ghidra Load**: RAM `0000:0000 – 0000:4485`, x86 16-bit Protected Mode, base 0x0000 **Functions**: 88 total (87 renamed + `input_keyboard_handler` pre-existing) ### Subsystem Summary Segment 21 implements the **hardware-level timer interrupt and entity event dispatch system** — Crusader's real-time task scheduler. Key responsibilities: - Programs and services the Intel 8253 **PIT timer** (I/O ports 0x40/0x43) - Manages three **entity dispatch lists**: timer list (0x39d4), input list (0x39e3), render list (0x3a10) - Maintains the **entity pool** at 0x39b0 (same pool as seg001; these segments share DS) - Provides **event queue** (32-slot circular buffer at 0x31cc) - Handles **save/load** serialization of the entire entity system - Controls **keyboard/interrupt locks** and deferred scheduling ### Function Groups #### Entity Pool Management (0x0207–0x0483) | Address | Name | Notes | |---------|------|-------| | `0x0207` | `entity_count_by_type_a` | Count entities matching type+event; filters DEAD flag (0x8) | | `0x0297` | `entity_count_by_type_b` | Identical logic to 0x0207 (compiler duplicate) | | `0x0327` | `entity_find_free_slot` | Scan pool for null entry; calls panic if full; returns slot or 0xFFFF | | `0x038f` | `entity_register` | Write far ptr to entity_list, group to entity_data, vtable to registry; inc count | | `0x044d` | `entity_get_ptr_raw` | Read entity far ptr from pool slot (may be null) | | `0x0483` | `entity_get_ptr` | Safe wrapper: verifies non-null, returns offset only | #### Event Dispatch (0x04f3–0x08be) | Address | Name | Notes | |---------|------|-------| | `0x04f3` | `entity_dispatch_reset_all` | Fires event code 0x21 (reset/init) to all entities | | `0x050d` | `entity_clear_deferred_flags` | Clears DEFERRED bit (0x200) from up to N=0x3998 entities | | `0x059e` | `entity_fire_event_broadcast` | Dispatch event to all matching entities; calls vtable[6]; respects 0x200 deferred flag | | `0x06f4` | `entity_fire_event_type_include` | Fire only entities whose type IS in given list (up to 10, 0x0d=end) | | `0x08be` | `entity_fire_event_type_exclude` | Fire only entities whose type is NOT in given list | | `0x0a8e` | `input_keyboard_handler` | (pre-existing) OS-level key router: 0x0d=scroll+, 0x01=action, 0x2c=save, 0x44=load | #### Entity Iterator / Linker (0x0bb7–0x106b) | Address | Name | Notes | |---------|------|-------| | `0x0bb7` | `entity_link` | Cross-link two entities; skips if flag 0x400 set | | `0x0c34` | `entity_find_first` | Init iterator 0x39fa=3; find first entity matching saved type/event at 0x399a/0x399c | | `0x0cec` | `entity_find_next` | Continue iterator from 0x39fa cursor | | `0x0dad` | `timer_entity_find_by_event` | Find entity handling event in range 0xf0-0xf7; checks bit 0x1000; writes to 0x3993 | | `0x0e82` | `entity_find_by_priority` | Walk priority chain at 0x39d4; find entity matching source/event at 0x3993 | | `0x0fc8` | `entity_set_cursor` | Validate flag 0x800; set cursor 0x3993 = param_1 (slot) | | `0x100c` | `entity_get_cursor` | Return entity at 0x39bf if valid and not dead | | `0x106b` | `entity_relink` | Re-link: find by event, walk priority chain, call set-link vtable funcs | #### Entity Lifecycle (0x1133–0x131d) | Address | Name | Notes | |---------|------|-------| | `0x1133` | `entity_unregister` | Full removal: dec sprite type count, vtable cleanup, dec total, update masks | | `0x1202` | `entity_slot_clear` | Zero pool slot (0x39b0), registry slot (0x39ca), group data (0x39b4) | | `0x1245` | `entity_layer_set` | Write 0x39c9 (active layer ID) if changed; set dirty flag 0x39a2 | | `0x125d` | `entity_check_overdue` | If entity_is_overdue: set bit 0x40 on entity+0x16 | | `0x127c` | `entity_is_overdue` | Return 1 if entity index > 0x39bf and flag 0x39c2 set | | `0x129b` | `entity_list_call_update` | For all entities where entity+0x0e & param_3 != 0: call vtable[8] | | `0x131d` | `entity_set_pending` | Write param to 0x3995 (next entity to register); error if already set | #### Entity System Init/Shutdown (0x133e–0x1705) | Address | Name | Notes | |---------|------|-------| | `0x133e` | `entity_system_init` | Alloc all entity pool buffers (see decompiler comment); init three lists; clear event state | | `0x14bc` | `entity_system_flush_normal` | Finalize (vtable[10]) then free all non-deferred active entities | | `0x158d` | `entity_system_flush_deferred` | Same as flush_normal for deferred entities | | `0x165c` | `entity_process_pending_deletes` | Free entities marked DEAD (flag & 0x8); dec 0x399e counter | | `0x1705` | `entity_system_shutdown` | Full shutdown: flush normal, flush deferred, process deletes, free all pools | #### Save / Load (0x1851–0x1d21) | Address | Name | Notes | |---------|------|-------| | `0x1851` | `event_queue_state_reset` | Zero ring buffer state tables (0x334e, 0x364e), queue ptrs (0x31c8/0x31ca) | | `0x18ce` | `level_load` | Full level load: shutdown + reinit + deserialize all entities via vtable[12] | | `0x1d21` | `save_game` | Serialize entity system: arrays + each entity via vtable[14]; magic check 0x3a21==0xed | #### PIT Timer / Hardware (0x2300–0x2975) | Address | Name | Notes | |---------|------|-------| | `0x2300` | `pit_timer_program` | OUT 0x43, 0x36; OUT 0x40, lo; OUT 0x40, hi — raw PIT channel 0 program | | `0x2316` | `pit_timer_set_hz` | Validates divisor <= 0xd688; stores at 0x39ce; calls pit_timer_program | | `0x23a5` | `pit_timer_tick_handler` | Timer ISR: iterates 0x39d4 timer list, fires vtable callbacks per layer/mode | | `0x25fc` | `timer_entity_active` | Check 0x3987/0x398b for active timer entity (mode-dependent) | | `0x264c` | `timer_entity_get_current` | Get ptr from 0x3987 or 0x398b based on 0x3991 mode flag | | `0x2668` | `timer_entity_enable` | Set ENABLED flag (0x400), inc counter, insert into timer list, reprograms PIT | | `0x2745` | `timer_entity_disable` | Clear ENABLED, dec counter, reprograms PIT; if list empty calls interrupt_request_cancel | | `0x2975` | `timer_recompute_hz` | Scan timer list; find smallest time_period (+0x38/+0x3a); call pit_timer_set_hz | #### Interrupt / Lock Control (0x283a–0x294b) | Address | Name | Notes | |---------|------|-------| | `0x283a` | `interrupt_lock_acquire` | Re-entrant acquire on 0x31c7 (interrupt lock) | | `0x2870` | `interrupt_lock_release` | Release 0x31c7 | | `0x289b` | `entity_lock_acquire` | Re-entrant acquire on 0x39aa (entity system lock) | | `0x28d5` | `entity_lock_release` | Release 0x39aa | | `0x290d` | `interrupt_request_schedule` | Set deferred IRQ flags 0x39ab and 0x398f (or 0x39a9 in sync mode) | | `0x294b` | `interrupt_request_cancel` | Clear IRQ request flags | #### Timer Loop / Deferred State (0x2a5f–0x2ad8) | Address | Name | Notes | |---------|------|-------| | `0x2a5f` | `timer_event_loop` | **Main game loop**: polls player tick counter at 0x2de4; busy-waits; fires optional callback; stores delta to 0x3a00/0x3a02 | | `0x2ac2` | `timer_deferred_reschedule` | If deferred mode flag 0x39b8 set, call reschedule | | `0x2ad8` | `timer_snapshot_deferred` | Copy 0x39a9 → 0x39b8; call interrupt handler if 0x39a9 set | #### Event Queue (0x2c73–0x3364) | Address | Name | Notes | |---------|------|-------| | `0x2c73` | `event_queue_drain` | Drain circular queue; call event_queue_dequeue while 0x31c8 != 0x31ca; reset state | | `0x2ca2` | `mouse_button_check` | Return 1 if BIOS 0x31a4 bit 0x10 set AND 0x39af (mouse enable) set | | `0x2cbc` | `stub_noop_2cbc` | Empty stub function | | `0x2cd7` | `bios_keyboard_flags_write` | Write param to 0x400:0017 (BIOS keyboard flags at segment 0x40, offset 0x17) | | `0x2cf2` | `input_event_dispatch` | Dispatch display event 0x10 to input list entities with flag 0x100 and 0xc bits set | | `0x2dc3` | `event_queue_push` | Push event to circular queue (write ptr 0x31ca); calls event_queue_is_full check | | `0x3276` | `keyboard_state_read` | INT 16h AX=0: read raw keyboard state into 0x31a4 | | `0x328b` | `keyboard_acquire` | If not locked (0x31c6): INT lock, read keyboard, set lock flag | | `0x32cc` | `keyboard_release` | If locked: unlock, clear 0x31c6 | | `0x3304` | `event_queue_count` | Count pending events: 0x31ca - 0x31c8 (circular) | | `0x333d` | `event_queue_is_full` | Return 1 if ((0x31ca+1) mod 32) == 0x31c8 | | `0x3364` | `event_queue_dequeue` | Read from ring buffer (0x31cc + 0x31c8*0xc, entry size 0xc), advance read ptr | #### Event Subscription and Bitmask Helpers (0x34dd–0x3878) | Address | Name | Notes | |---------|------|-------| | `0x34dd` | `event_queue_process_all` | Drain queue; for each event find listener entities in 0x39e3; call vtable[0x14] | | `0x35e9` | `event_queue_set_mode` | Write low 2 bits to 0x334c; call keyboard_interrupt_call | | `0x35fb` | `event_queue_set_param` | Write low 5 bits to 0x334d; call keyboard_interrupt_call | | `0x360d` | `keyboard_interrupt_call` | INT 16h (raw BIOS keyboard services call) | | `0x3630` | `entity_validate_indices` | Debug assert: verify entity+0x02 (slot_index) == pool position for all entities | | `0x369b` | `typemask_set_bit` | Set bit at 0x3a04 + (param>>3), bit (param & 7) — entity type present bitmask | | `0x36d4` | `typemask_clear_bit` | Clear bit in 0x3a04 bitmask | | `0x370f` | `typemask_update` | If entity type has listeners (entity_find_first != 0): set bit; else clear | | `0x3744` | `typemask_test_bit` | Test bit in 0x3a04; return 1 if entity type has registered listeners | | `0x377d` | `event_subscription_set` | Set subscription bit in 0x3a08 buffer | | `0x37b2` | `event_subscription_clear` | Clear subscription bit in 0x3a08 buffer | | `0x37e9` | `event_subscription_update` | If entity has listeners: set bit; else clear (driven by entity_find_first result) | | `0x3825` | `event_subscription_test` | Test subscription bit in 0x3a08; return 1 if subscribed | | `0x3864` | `event_state_clear` | Zero entire 0x3a0c event use-count buffer (0x4000 bytes = 8192 uint16s) | | `0x3878` | `event_use_count_increment` | Increment 64-bit counter at 0x3a0c[entity_event_type*4] | #### Input / Render List (0x38c2–0x3ae9) | Address | Name | Notes | |---------|------|-------| | `0x38c2` | `input_event_broadcast` | Dispatch input event 0x40 to all render-list entities with flag 0x40; uses counter 0x39ad | | `0x39a1` | `subscribe_to_render_list` | Add entity to 0x3a10 list; set flag bit 0x40; inc 0x3a1f | | `0x3a13` | `unsubscribe_from_render_list` | Remove entity from 0x3a10; clear bit 0x40; dec 0x3a1f | | `0x3404` | `subscribe_to_input_list` | Add entity to 0x39e3 list; check flag 0x100; set bit 0x80; inc 0x39c3 | | `0x3477` | `unsubscribe_from_input_list` | Remove entity from 0x39e3; clear bit 0x80; dec 0x39c3 | | `0x3a78` | `entity_lists_init` | Init three linked lists with sentinel vtable 0x3a89; write head vtable 0x2d10 | | `0x3ae9` | `entity_lists_reset` | Call external reset + reinit 0x39e3 and 0x39d4 lists | ### Entity Object Field Layout (as used in Seg21) | Offset | Field | Type | Description | |--------|-------|------|-------------| | `+0x00` | vtable_ptr | far ptr | Pointer to entity's vtable dispatch table | | `+0x02` | slot_index | uint16 | Entity's own slot number in pool | | `+0x04` | source_type | uint16 | Source/owner entity type (event matching) | | `+0x06` | event_type | uint16 | Event type this entity handles | | `+0x08` | flags_byte | uint8 | Low 5 bits = sprite group ID | | `+0x0e` | capability_mask | uint16 | Bitmask of supported event capabilities | | `+0x16` | state_flags | uint16 | bit3=DEAD, bit8=REGISTERED, bit9=ACTIVE, bit10=ENABLED, bit11=HAS_TIMER, bit13=IS_IRQ_HANDLER | | `+0x18` | flags2 | uint16 | bit6=IN_RENDER_LIST, bit7=IN_INPUT_LIST, bit9=DEFERRED | | `+0x1e` | priority_chain | far ptr | Priority chain entries (entity_find_by_priority) | | `+0x20` | priority_count | uint16 | Count of priority chain entries | | `+0x38` | time_period_lo | uint16 | Timer period low word (PIT frequency calc) | | `+0x3a` | time_period_hi | uint16 | Timer period high word | ### Vtable Layout (Seg21 usage) | Slot | Byte offset | Prototype | Purpose | |------|-------------|-----------|---------| | [6] | `+0x0c` | `handle_event(entity, CS, type, param)` | Event callback | | [8] | `+0x10` | `update(entity, CS, capability_mask)` | Per-tick update | | [10] | `+0x14` | `finalize(entity, CS)` | Cleanup/shutdown | | [12] | `+0x18` | `load(entity, CS, file_ptr, CS)` | Deserialize from save | | [14] | `+0x1c` | `save(entity, CS, file_ptr, CS)` | Serialize to save | | [16] | `+0x20` | `set_backref(entity, CS, list_ptr)` | Set back-reference | | [20] | `+0x28` | `dispatch_callback(entity, CS, event_id, 0, data_ptr)` | Generic dispatch | ### Key Global Data (Seg21 — additions to DS) | Address | Name | Description | |---------|------|-------------| | `0x31a4` | bios_key_state | Raw INT 16h keyboard state | | `0x31c6` | keyboard_lock | Keyboard acquired flag | | `0x31c7` | interrupt_lock | Interrupt lock flag (re-entrant) | | `0x31c8` | queue_read_ptr | Event queue read index (0–31) | | `0x31ca` | queue_write_ptr | Event queue write index | | `0x31cc` | event_queue_base | Ring buffer, 32 entries × 0xc bytes | | `0x334c` | queue_mode | Event queue mode bits (0–1) | | `0x334d` | queue_param | Event queue param bits (0–4) | | `0x39b0` | entity_list | Far ptr to entity far-ptr array (count×4) — **shared with seg001** | | `0x39b4` | entity_data | Far ptr to group/sprite-ID array (count×2) | | `0x39b9` | entity_max_count | Max capacity of entity pool | | `0x39bb` | entity_count | Total registered entity count | | `0x39c9` | active_layer | Current active entity layer/group ID | | `0x39ca` | entity_registry | Far ptr to vtable dispatch array (count×4) — **shared with seg001** | | `0x39ce` | pit_divisor | Current PIT timer divisor | | `0x39d4` | timer_list | Intrusive linked list: timer-dispatch entities | | `0x39e3` | input_list | Intrusive linked list: input-handler entities | | `0x3a04` | typemask_buf | Far ptr to entity type present bitmask (0x480 bytes) | | `0x3a08` | evt_sub_buf | Far ptr to event subscription bitmask (0x2400 bytes) | | `0x3a0c` | evt_state_buf | Far ptr to event use-count table (0x4000 bytes) | | `0x3a10` | render_list | Intrusive linked list: render-callback entities | | `0x3a21` | save_magic | Must be 0xed (-0x13) for valid save | | `0x3a70` | default_registry_vtable | Default vtable written to entity_registry slots on register | | `0x3a89` | list_sentinel_vtable | Sentinel vtable written to list head nodes | ---