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

1496 lines
100 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Crusader: No Remorse - Decompilation Notes
## Binary Overview
- **Game**: Crusader: No Remorse (Origin Systems, 1995)
- **Platform**: DOS (16-bit protected mode)
- **DOS Extender**: Phar Lap 286 DOS-Extender (RUN286)
- **Executable Format**: Bound `MZ -> NE` executable with Phar Lap DOS-extender code
- **Entry Point**: `10da:7c40`
## Installed Copy Findings
- No standalone `.EXP` file exists in `F:\Apps\Crusader No Remorse`.
- `CRUSADER.EXE` is the original game binary and contains a valid internal `NE` header.
- Outer DOS `MZ` header points to `e_lfanew = 0x36F70`.
- Internal header at `0x36F70` starts with `NE` and describes **145 segments**.
- The NE segment table references data from the original file directly, so there is no separate embedded payload that needs to be carved out first.
- `CNRCEXP.EXE` is a modern Win32 helper tool, not part of the original DOS execution path.
## Raw Full-EXE Import Mapping
- A separate raw-binary import of the full executable (`crusader-raw.exe`) is usable: Ghidra discovers thousands of functions across a single flat `ram` block.
- Direct `file_offset -> flat_address` mapping from the standalone segment extracts is not reliable for porting names into that raw import.
- The extracted `segNNN_*.bin` files match `CRUSADER_NE.EXE`, but the raw full-EXE import must be mapped by verified byte signatures / known function bodies.
- Verified segment bases in the raw full-EXE import:
- `seg001` base = `0x6E570` (`cursor_update_hover` at `0006:e5d0`, rel `0x0060`)
- `seg021` base = `0x87170` (`entity_count_by_type_a` at `0008:7377`, rel `0x0207`)
- Porting rule for these verified segments:
- `raw_full_exe_flat = verified_segment_base + standalone_segment_relative_offset`
- Naming note:
- `seg001` and `seg021` both contain a keyboard handler; in the full program database, the seg001 copy is named `seg001_input_keyboard_handler` to avoid a symbol collision with seg021 `input_keyboard_handler`.
### Latest Raw Full-EXE Porting Progress
- Newly ported and renamed into `CRUSADER-RAW.EXE` from verified `seg001` mapping (`base 0x6E570`):
- `0007:28ce` = `shot_entity_alloc` (`seg001 + 0x435e`)
- `0007:2a19` = `shot_entity_free` (`seg001 + 0x44a9`)
- `0007:2bc9` = `projectile_init_vector` (`seg001 + 0x4659`)
- `0007:3001` = `entity_fire_weapon` (`seg001 + 0x4a91`)
- `0007:3088` = `fire_weapon_from_cursor` (`seg001 + 0x4b18`)
- `0007:30e8` = `projectile_check_hit` (`seg001 + 0x4b78`)
- `0007:319e` = `projectile_step_update` (`seg001 + 0x4c2e`)
- `0007:3298` = `projectile_trace_ray` (`seg001 + 0x4d28`)
- `0007:371d` = `projectile_update_tick` (`seg001 + 0x51ad`)
- `0007:4009` = `projectile_apply_hit` (`seg001 + 0x5a99`)
- Decompiler comments were added on key raw-import projectile functions to preserve provenance for later passes.
- Quick verification from current raw import:
- `entity_fire_weapon` currently decompiles as a thin wrapper that calls `projectile_init_vector`.
- `fire_weapon_from_cursor` still decompiles poorly in the raw import, but disassembly shows it begins by pushing cursor sprite/state data from the `0x27d6` area, consistent with the existing seg001 notes.
### Raw 0007 Gameplay Helper Batch (entity/tile aux state)
- New conservative gameplay-side helper renames (direct analysis from field writes and call structure):
- `0007:85f6` = `entity_sync_tile_aux_state`
- `0007:8865` = `entity_sync_tile_aux_if_linked`
- `0007:8709` = `entity_mark_dirty_and_sync_tile_aux`
- Current verified behavior:
- `entity_sync_tile_aux_state` reads entity tile index at `+0x4`, toggles bit `0x04` in tile record `+0x59` based on entity byte `+0x54`, and copies entity word `+0x55` into tile record `+0x0d`.
- `entity_sync_tile_aux_if_linked` only performs the sync when entity link/pointer `+0x50/+0x52` is non-null.
- `entity_mark_dirty_and_sync_tile_aux` calls the linked-sync helper, sets entity flag bit `0x04` at `+0x42`, then enters the existing unresolved thunk path (`0000:ffff`).
### Raw 0007 Gameplay Helper Batch (facing/direction)
- New gameplay helper rename (direct analysis):
- `0007:8bd9` = `entity_set_facing_direction`
- Current verified behavior:
- Updates entity facing byte `+0x38` using incoming direction/event code values (notably `0x10/0x11/0x12`) with parity-aware adjustment.
- Uses entity flags at `+0x4d` to select increment/decrement behavior for clockwise/counterclockwise facing updates.
- Called from the large gameplay update state machine at `0007:5b9a` inside `FUN_0007_5b6f`.
### Raw 0007 Gameplay Helper Deep Dive: snap_entity_to_ground
- Function: `0007:2207` = `snap_entity_to_ground`
- Caller in gameplay flow: `spawn_entity_checked` (`0007:22de`, call at `0007:2366`)
- Purpose (high confidence): pre-spawn position adjustment for a small allow-list of entity types so they land on valid ground/height context before normal spawn allocation.
#### Variable replacement pass (applied in Ghidra)
- `param_1` -> `entity_type`
- `local_48` -> `snap_entity_type_table`
- `local_34` -> `snap_dispatch_seg_table`
- `local_20` -> `snap_dispatch_off_table`
- `local_c` -> `entity_type_cursor`
- `local_4` -> `dispatch_index`
#### What the function does structurally
1. Copies three 10-entry static tables into stack-local scratch buffers:
- from `0x2910` into `snap_dispatch_off_table`
- from `0x2924` into `snap_dispatch_seg_table`
- from `0x2938` into `snap_entity_type_table`
2. Performs a linear scan across 10 entity IDs in `snap_entity_type_table`.
3. If `entity_type` matches an entry, it calls into the unresolved shared FAR thunk target (`0000:ffff`) with spawn coordinate-derived arguments.
4. If no table entry matches, it exits without modifying the request.
#### Why this is "snap to ground" behavior
- The only known caller (`spawn_entity_checked`) gates this function behind:
- global mode flag `*(char *)0x27fe != 0`
- a hardcoded list of exactly 10 entity IDs (`0x31c`, `0x31f`, `0x320`, `0x321`, `0x322`, `0x323`, `0x324`, `0x325`, `0x326`, `0x327`)
- That caller prepares local spawn position values, calls `snap_entity_to_ground`, then proceeds to spawn logic. This pattern strongly indicates pre-placement correction rather than generic AI or render logic.
- The segment/offset companion tables strongly suggest per-entity handler dispatch metadata (or per-entity parameter blocks) used by the thunked path.
#### Current limitation in raw import
- The common thunk endpoint is still imported as `unresolved_far_thunk_dispatch` at `0000:ffff`.
- In this raw database, that body decompiles as overlapped/bad instruction data, so exact arithmetic internals of the final coordinate projection cannot yet be recovered from this symbol alone.
- Despite that, caller context + table shape + argument flow make the gameplay role of this helper clear enough for naming and control-flow analysis.
#### Working pseudocode (behavioral)
```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, ...);
}
}
}
```
#### Next RE target (to close remaining uncertainty)
- Recover the true callee behind `0000:ffff` for the `0007:224b` call site by relocation/import-table reconstruction or by matching this call path in a cleaner segment-mapped database. That should reveal exact per-slot use of the two dispatch tables and final coordinate math.
### Raw 0007 Gameplay Helper Follow-up: AI sweep + checked spawn path
- Additional gameplay-side annotation pass completed directly in `CRUSADER-RAW.EXE`.
#### `spawn_entity_checked` (`0007:22de`) refinements
- Function signature was expanded to 7 arguments in Ghidra so stack arguments remain visible in decompile:
- `entity_type`, `spawn_flags_a`, `spawn_flags_b`, `spawn_flags_c`, `spawn_x`, `spawn_y`, `spawn_layer_arg`
- Parameter/local naming pass applied for readability in decompiled output.
- New comments added at key control-flow points:
- `0007:22f8`: allow-list gate for ground-snap mode (`0x27fe != 0` + entity IDs `0x31c..0x327` subset)
- `0007:2366`: explicit `snap_entity_to_ground(entity_type, &spawn_x, &spawn_y, &spawn_layer)` handoff
- `0007:247e`: fallback path that calls core `entity_spawn` with original arguments
- Current caveat:
- Decompiler still aliases the temporary y/layer scratch region imperfectly around the thunked call site, but disassembly confirms the call setup uses local `x/y/layer` temporaries (`[bp-6]`, `[bp-8]`, `[bp-9]`) before spawn.
#### `entity_ai_update_loop` (`0007:0fb6`) structural recovery
- Added disassembly + decompiler comments capturing stable behavior:
- Reads player entity FAR pointer from global `0x2de4`.
- Copies player world position fields (`+0x40`, `+0x42`) into globals `0x27e7` / `0x27e9` (AI focus position cache used by downstream logic).
- Iterates entity IDs from `2` through `255` and dispatches per-entity processing through the shared thunk path.
- This function now has enough recovered semantics to treat it as the frame-level AI sweep dispatcher even though individual thunked callees remain unresolved in the raw import.
### Raw 0007 Gameplay Logic: animation / range / command globals
#### `is_player_in_range` (`0007:0f79`) — fully recoverable
- Prototype updated in Ghidra: `int is_player_in_range(int entity_x, int entity_y)`
- Reads player world position from `g_player_entity_farptr` (`0x2de4`, fields `+0x40` (x) and `+0x42` (y)`.
- Computes unsigned delta from AI focus globals `g_ai_focus_pos_x` (`0x27e7`) / `g_ai_focus_pos_y` (`0x27e9`).
- Returns 1 if player Y delta == 0 AND player X delta < 0xF0 (240 world units), else 0.
- Only confirmed caller so far: `0007:0bcb` (in unanalyzed function region).
#### `entity_animation_frame_update` (`0007:26e2`) — fully decompiled
- Prototype updated: `void entity_animation_frame_update(int *entity_ptr)`
- Key globals read:
- `g_anim_tick_counter` (`0x3a00`) — frame timing tick counter.
- `g_anim_tick_overdrive_flag` (`0x3a02`) — if set, forces max-advance (4 steps).
- `g_speed_double_flag` (`0x27fd`) — doubles speed_factor to 2 when set (fast game mode).
- Local variables renamed: `speed_factor` (1 or 2) and `advance_steps` (04, number of frame advances this tick).
- Entity struct fields confirmed (relative to `entity_ptr` as `int*`):
- `[0x1b]` = frame_min (backward direction counter)
- `[0x1c]` = frame_max
- `[0x1d]` = current_frame
- `[0x1e]` = loop_flag
- `[0x1f]` = reverse_direction_flag
- `+0x3f` (as `char*`) = completion handle/sentinel (`-1` = none, `0x2802` = player entity)
- On frame overflow: if completion handle valid and not player-entity, fires thunked event; calls vtable `[+8]` method.
- Added decompiler comment at function entry explaining all fields and behavior.
#### `entity_command_dispatch` (`0007:0990`) — partially decompiled
- Prototype: `void entity_command_dispatch(int entity_handle, int target_seg, int command_type, byte absolute_pos_flag)`
- When `absolute_pos_flag == 0`: computes player-relative delta using `g_player_entity_farptr` and stores result:
- delta_x → `g_player_delta_x` (`0x27f5`)
- delta_y → `g_player_delta_y` (`0x27f7`)
- Clears cached origin globals `g_cmd_effect_origin_x` (`0x27f1`) and `g_cmd_effect_origin_y` (`0x27f3`) after use.
- Dispatches entity command through shared thunk; actual command table data not yet resolved.
- No incoming XREFs found in the raw import (likely called via table or vtable dispatch).
#### Global map additions (renamed in Ghidra)
| Address | Name | Evidence |
|---------|------|---------|
| `0x2de4` | `g_player_entity_farptr` | FAR ptr to player entity; `+0x40`/`+0x42` are world X/Y |
| `0x27e7` | `g_ai_focus_pos_x` | Set by `entity_ai_update_loop` from player entity `+0x40` |
| `0x27e9` | `g_ai_focus_pos_y` | Set by `entity_ai_update_loop` from player entity `+0x42` |
| `0x27f1` | `g_cmd_effect_origin_x` | Cached effect origin X, cleared after delta in `entity_command_dispatch` |
| `0x27f3` | `g_cmd_effect_origin_y` | Cached effect origin Y |
| `0x27f5` | `g_player_delta_x` | Player X delta from last effect origin |
| `0x27f7` | `g_player_delta_y` | Player Y delta from last effect origin |
| `0x27fd` | `g_speed_double_flag` | 0 = normal, 1 = double speed animation |
| `0x27fe` | `g_ground_snap_mode_flag` | Non-zero = ground-snap prepass active for placements |
| `0x27d0` | `g_entity_update_max_id` | Max entity ID used by `entity_ai_update_loop` sweep |
| `0x3a00` | `g_anim_tick_counter` | Animation tick counter for frame-advance step budget |
| `0x3a02` | `g_anim_tick_overdrive_flag` | 0 = normal, non-zero = force max frame advance step |
| `0x2802` | `g_player_entity_handle` | Player entity handle (used as sentinel in animation completion checks) |
### Raw 000e Parser Helper Cluster
- A small helper cluster in the raw `000e:` area now appears to implement a fixed-size CRLF record parser/table builder, likely used by startup/config or script-ish text data.
- Newly renamed helpers:
- `000e:345e` = `record_table_init`
- `000e:34cc` = `record_table_destroy`
- `000e:35c6` = `record_table_release_buffer`
- `000e:35ef` = `record_table_next_slot`
- `000e:3639` = `record_table_parse_buffer`
- `000e:3798` = `record_parser_read_line`
- `000e:38f8` = `record_parser_find_marker`
- `000e:39cc` = `record_parser_dispatch_at_directive`
- Current behavior read from raw-import decompilation/disassembly:
- `record_table_init` clears the table header and zeroes 300 words of inline storage.
- `record_table_parse_buffer` walks a CRLF-separated text buffer, captures each line, splits around a marker helper path, and stores parsed entry state into 0x0c-byte records.
- `record_parser_read_line` advances to the next CRLF-delimited line, rejects lines that start with `@` or with non-identifier punctuation, and terminates the line in-place with `0`.
- `record_parser_find_marker` scans forward until an `@` marker or end-of-data; optionally consumes the remaining length from the parser state.
- `record_parser_dispatch_at_directive` returns `0` unless the current substring begins with `@`; in the `@` case, it advances by 7 bytes and dispatches through a FAR thunk (`0000:ffff`).
### Raw 000e RIFF/Animation Cluster
The `000e:` segment contains a RIFF/AVI streaming animation subsystem. Animation objects have a confirmed field layout (offsets relative to the object base pointer).
**Animation object field map:**
- `+0xb0` = active/valid flag
- `+0xb4`, `+0xb6`, `+0xb8`, `+0xba`, `+0xbc`, `+0xbe`, `+0xc0`, `+0xc2` = constructor-initialized flags
- `+0xd4` = alive sentinel (must be `-1` for "alive")
- `+0xe4` = paused flag (0 = running)
- `+0xeaf` / `+0xeb1` = far pointer to current RIFF chunk
- `+0xedb` = animation frame stack depth counter (max 9)
- `+0xee1` = frame data from current chunk `+4`
- `+0xeef` = current subframe index
- `+0x1b3` = subframe count
- `+0xef1` = audio completion flag
- `+0x11b` = ring buffer write pointer
- `+0x11f` = ring buffer read pointer
- `+0x117` = ring buffer base
- `+0x123` = ring buffer end (capacity boundary)
- `+0x102` = resource pointer
- `+0xde` = some entry index (multiplied by `0x30` to reach per-entry data at `+0x1c7`)
**RIFF format notes:** Game uses standard RIFF/IFF: LIST and RIFF header magic (`0x5453494c` = `"LIST"`, `0x46464952` = `"RIFF"`), `"movi"` FourCC subchunk for frames. Audio frames tagged `"01wb"` (`0x62773130`), video frames in a separate path.
**Newly renamed functions:**
| Address | Name | Evidence |
|---------|------|---------|
| `000e:2a28` | `riff_find_chunk_by_type` | Walks RIFF LIST/RIFF chunk list; compares each node's FourCC at `+8` vs `param_2`; returns pointer to matching chunk or NULL |
| `000e:2104` | `animation_start` | Finds `"movi"` chunk via `riff_find_chunk_by_type`, inits ring buffer ptrs at `+0x11b` from `+0x117 + duration`, calls `animation_advance_frame`, loops `anim_load_audio_frame` and a second frame-loader thunk path per subframe |
| `000e:12f4` | `animation_advance_frame` | Fixed-point `0x1000` timer arithmetic; checks `+0xe4` (paused), advances ring buffer `+0x11b`/`+0x11f`/`+0x117`/`+0x123`; calls advance thunk |
| `000e:103f` | `animation_tick` | Guard wrapper: checks `param_1+0xd4 != -1`, then calls `animation_advance_frame(param_1, 0)` |
| `000e:06f7` | `anim_load_audio_frame` | Checks chunk tag == `0x62773130` (`"01wb"` = audio stream 1); computes ring buffer free space; copies chunk payload via `0x0000:ffff` thunk; increments subframe index at `+0xeef`; resets at subframe count `+0x1b3` |
| `000e:053d` | `anim_load_video_frame_wrapper` | Called once per subframe in `animation_start` immediately after `anim_load_audio_frame`; thin wrapper that forwards to `000e:ffb0` |
**Unresolved callee:**
- `000e:ffb0` remains unresolved (decompiles garbled due to overlapping instructions at `000f:0085`/`000f:0086`). Current evidence from `animation_start` loop suggests this path is the video-side subframe loader paired with `anim_load_audio_frame`.
**Constructor pattern (`000e:2777`, `000e:2860`, `000e:2969`):**
All three follow the same layout:
1. Call `FUN_000e_e935` (allocator — produces garbled 11KB decompile, not renamed)
2. Set fields `+0xb4` through `+0xc2` on the result
3. Call `000d:ebe3` (multi-step chain initializer: calls `177c`, `1acb`, `0988`, `22bc`, `1d4a`, `2104` in sequence)
4. Call `assert_alive_sentinel` (assertion: checks `+0xd4 != -1`)
5. Call `func_0x000eec83`
The chain at `000d:ebe3` steps through VM opcode handlers (`000d:177c`, `000d:1acb`, `000d:0988`) that operate on a bytecode VM object with stack pointer at `+0xcc` (decremented by 2 per push) and segment base at `+0xce`.
**Constructor variant renames (direct analysis):**
- `000e:223d` = `assert_alive_sentinel`
- `000e:2777` = `animation_ctor_variant_a`
- `000e:2860` = `animation_ctor_variant_b`
- `000e:2969` = `animation_ctor_variant_c`
## Segment Map
| Segment | Address Range | Purpose |
|---------|--------------|---------|
| CODE_0 | `1000:0000 - 1000:01ff` | Interrupt dispatch table / thunks |
| CODE_1 | `1020:0000 - 1020:0b9f` | Low-level interrupt handlers, mode switching |
| CODE_2 | `10da:0000 - 10da:25ef` | **Main runtime** — C library, I/O, formatting, entry point |
| CODE_3 | `1339:0000 - 1339:0c2f` | **DOS/DPMI services** — INT 21h/31h wrappers, interrupt vector mgmt, fast memcpy |
| CODE_4 | `13fc:0000 - 13fc:27af` | **String data & runtime constants** — error messages, format strings, Phar Lap ID |
| CODE_5 | `1677:0000 - 1677:0e8f` | **EMS/XMS memory management** — expanded memory handlers |
| CODE_6 | `1760:0000 - 1760:7ccd` | **DOS Extender core** — EXP loader, command-line parser, memory management, system init |
| DATA | `1760:7cd0 - 1760:7cdf` | Global data |
| HEADER | `HEADER::0000 - HEADER::044f` | MZ/P2 file header |
## Named Functions
### Entry & Startup
| Address | Name | Description |
|---------|------|-------------|
| `10da:7c40` | `entry` | Program entry point — checks CPU, parses command line, launches game |
| `10da:1816` | `main_init_and_run` | Main initialization — loads child EXP, sets up subsystems, runs game |
| `1760:1432` | `parse_cmdline_and_run` | Parses command-line args and invokes main_init_and_run |
| `1760:42fa` | `init_dos_extender` | Initializes Phar Lap 286 DOS extender (CPU check, VCPI/DPMI setup) |
### Executable Loading
| Address | Name | Description |
|---------|------|-------------|
| `1760:2cdf` | `load_exp_file` | Loads .EXP executable — opens file, reads headers, allocates memory |
| `1760:1dfc` | `load_executable_image` | Parses P2/MZ headers, loads segments, creates LDT entries |
| `1760:24a6` | `apply_relocations` | Applies segment relocations to loaded executable |
| `1760:5eca` | `exec_child_process` | Executes child process with command-line arguments |
| `1760:5fee` | `exec_program_with_args` | Builds command line, locates and executes a program |
| `10da:1f7e` | `load_and_run_child` | Wrapper: loads child EXP and initializes it |
### System Services
| Address | Name | Description |
|---------|------|-------------|
| `10da:2330` | `dos_exit` | Calls INT 21h AH=4Ch (terminate program) |
| `1760:42aa` | `detect_cpu_type` | Detects CPU: 0=8086, 2=286, 3=386+ |
| `1339:04a6` | `dpmi_set_interrupt_vector` | INT 31h — DPMI set interrupt vector |
| `1339:06ca` | `switch_to_real_mode` | Switches CPU from protected to real mode |
| `1339:06f2` | `switch_to_protected_mode` | Switches CPU from real to protected mode |
| `1339:0076` | `setup_interrupt_handlers` | Configures interrupt vectors via INT 21h |
| `1339:0a38` | `dos_int21h_wrapper` | Simple INT 21h call wrapper |
| `1339:0a82` | `dos_int21h_with_regs` | INT 21h call with register parameters |
| `10da:2360` | `get_flags_register` | Returns CPU FLAGS register |
| `10da:2363` | `set_flags_register` | Sets CPU FLAGS register |
### Memory Management
| Address | Name | Description |
|---------|------|-------------|
| `1677:0d12` | `cleanup_ems_memory` | Frees EMS (INT 67h) memory handles |
| `10da:14fc` | `init_stack_fill_cc` | Fills stack with 0xCC (INT 3) for debugging/guard |
| `10da:1706` | `get_segment_base_addr` | Computes linear base address from segment descriptor |
### Task Management
| Address | Name | Description |
|---------|------|-------------|
| `10da:19ca` | `task_switch_to_child` | Context switch to child process |
| `10da:1946` | `task_switch_from_child` | Context switch back from child process |
| `10da:1af4` | `call_termination_handler` | Calls registered termination callback |
### I/O & Output
| Address | Name | Description |
|---------|------|-------------|
| `10da:00d6` | `flush_output_buffer` | Flushes buffered output via function pointer |
| `10da:0132` | `putchar_buffered` | Writes character to buffer, flushes on newline |
| `10da:0808` | `memcopy_to_buffer` | Copies N bytes from source to destination buffer |
| `10da:178c` | `print_error_message` | Formats and prints load error (references "not loaded: %s") |
| `10da:09e4` | `print_fatal_error` | Prints "Fatal Error" prefix + message |
| `10da:192a` | `print_internal_error` | Prints "Internal Error" message |
### Interrupt Management
| Address | Name | Description |
|---------|------|-------------|
| `10da:1ec0` | `restore_interrupt_vectors` | Restores INT 2Fh and INT 67h vectors |
| `10da:2249` | `restore_int_2f_67` | Restores INT 15h vector if saved |
| `1760:3d86` | `init_system_check` | Validates system (CPU, DOS version, VCPI/DPMI, memory) |
### Utility
| Address | Name | Description |
|---------|------|-------------|
| `10da:15ea` | `check_ds_segment` | Returns true if DS == 0x10 (checks data segment selector) |
| `1760:3c9e` | `nop_stub` | Always returns 0 (unused hook) |
## Key String References
| Address | String | Context |
|---------|--------|---------|
| `13fc:0016` | `$Id: comhighc.c 1.1 91/08/06...` | Phar Lap C runtime source ID |
| `13fc:0048` | `$Id: comutils.c 1.1 91/08/06...` | Phar Lap utility functions source ID |
| `13fc:0078` | `Serial Number ` | DOS extender serial validation |
| `13fc:14ca` | `Internal Error` | Error class prefix |
| `13fc:14da` | `Fatal Error` | Fatal error class prefix |
| `13fc:156a-1628` | File error messages | Not found, bad format, no memory, etc. |
| `1760:665c` | `Copyright (C) 1986-93 Phar Lap Software, Inc.` | DOS extender copyright |
| `1760:73da` | `-LDTSIZE 4096 -EXTHIGH D0_0000h -NI 18 -ISTKSIZE 3` | Default extender config |
| `1760:76fc-7c5a` | Numbered error messages | System requirement errors (1000-2170) |
## Architecture Notes
### Correction: The Game Ships As A Bound NE Executable
**Important**: The installed copy does **not** contain a separate `.EXP` file. `CRUSADER.EXE` is a bound executable with an outer DOS `MZ` stub and an internal `NE` executable image. The Phar Lap loader/runtime code and the game's real segment layout are both described inside this same file.
The flow is:
1. `entry` → checks DOS version, CPU type
2. `init_dos_extender` → sets up protected mode (VCPI/DPMI)
3. `load_exp_file` → opens the game's `.EXP` file
4. `load_executable_image` → parses P2/MZ headers, creates segments, applies relocations
5. `task_switch_to_child` → transfers control to the actual game code
For the installed retail copy, this means the currently loaded Ghidra program is only one interpretation of `CRUSADER.EXE`. The next import should target the **NE layer of the same file**, not a missing external `.EXP`.
### NE Import Details
- File to import: `F:\Apps\Crusader No Remorse\CRUSADER.EXE`
- Outer DOS header: `MZ`
- `e_lfanew`: `0x36F70`
- Internal executable header: `NE`
- Segment count: `145`
- Initial `CS:IP`: `0001:0000`
- Initial `SS:SP`: `0091:2000`
The currently analyzed protected-mode code at addresses like `10da:7c40` is consistent with the Phar Lap runtime/loader path. To reach the rest of the program, import `CRUSADER.EXE` again using an **NE-aware loader** or a workflow that starts from the internal NE header rather than the outer DOS stub.
### Segment 1339: Fast Memory Operations
`FUN_1339_02a8` contains an unrolled loop (Duff's device pattern with 57 iterations) — a hand-optimized **fast memory fill/add** routine, typical in DOS game graphics engines.
### EMS Memory (Segment 1677)
The game uses **EMS (Expanded Memory Specification)** via INT 67h for additional memory beyond the 1MB real-mode limit. Functions in segment 1677 manage EMS page frames and handle allocation/deallocation.
## NE Segment 1 Analysis — Game Logic Functions (seg001_code_off_37600_len_8400.bin)
This segment was imported as Raw Binary at base `0x0000`, language `x86:LE:16:Protected Mode`.
All 35+ identified functions renamed and annotated in Ghidra.
### Cursor Subsystem (0x00600x0d5f)
| Address | Name | Description |
|----------|---------------------------|-------------|
| `0x0060` | `cursor_update_hover` | Hover update: if mouse active & entity set, calls cursor_set_target |
| `0x00e9` | `cursor_set_target` | Positions cursor on entity, updates sprite + direction visual |
| `0x0322` | `cursor_shutdown` | Frees cursor resources, resets state |
| `0x0398` | `cursor_animation_update` | Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6 |
| `0x050f` | `cursor_draw_tick` | Per-frame cursor draw (calls cursor_animation_update if dirty) |
| `0x0c24` | `action_key_valid` | Returns 1 if action code (param_1) is a valid game action key |
| `0x0d5f` | `cursor_direction_input` | Arrow-key input: rotates cursor angle, updates direction sprite |
### Input Handling
| Address | Name | Description |
|----------|-------------------------|-------------|
| `0x0526` | `input_keyboard_handler`| Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load |
### Cursor State Data (at DS:0x27xx)
| Address | Field | Meaning |
|---------|-------|---------|
| `0x27c4` | cursor_sel1 | Selection counter 1 |
| `0x27c6` | cursor_sel2 | Selection counter 2 |
| `0x27c8` | current_entity | Handle to currently targeted entity |
| `0x27ca0x27ce` | cursor_state | Cursor interaction state bytes |
| `0x27d0` | cursor_entity_type | Current entity type index |
| `0x27d2` | z_offset | Z-height offset for terrain adjustment |
| `0x27d4` | cursor_angle | Rotation angle (0359) |
| `0x27d6` | cursor_sprite | Sprite handle for cursor visual |
| `0x27d8` | cursor_dirty | Set when cursor needs redraw |
| `0x27d9` | cursor_active | Master cursor enabled flag |
| `0x27da` | cursor_no_turn | Flag disabling cursor rotation |
| `0x27ed` | difficulty | Enemy accuracy divisor (used in projectile_init_vector) |
| `0x27fd` | hard_mode | Two-step mode (combat vs. explore) |
| `0x27fe` | move_mode | Movement phase flag |
| `0x27ff` | mouse_active | Mouse/input system active |
| `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 (0x24010x5a50)
| Address | Name | Description |
|----------|------------------------------|-------------|
| `0x2401` | `clear_cursor_selection` | Zeros 0x27c4/0x27c6 (selection counters) |
| `0x2899` | `cursor_switch_target_entity`| Switches cursor target: unloads old entity, loads new, re-registers |
| `0x29d8` | `get_z_offset` | Returns func() + *(0x27d2) = adjusted Z/height |
| `0x2a09` | `is_player_in_range` | Checks if entity is at player (0x2de4) X/Y +/-0xf0 range |
| `0x2a46` | `entity_ai_update_loop` | Loops entities 2255, checks visibility, triggers fire/move |
| `0x2c36` | `ui_update_callback` | Calls cursor_state_clear then vtable[2] on menu object |
| `0x2c6b` | `cursor_state_clear` | Clears cursor state bytes 0x27ca0x27ce, clears entity flag bit1 |
| `0x2c92` | `dialog_spawn` | Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca |
| `0x2d47` | `entity_pick_handler` | Handles entity selection or save-game trigger (type 0x38d) |
| `0x2df9` | `clear_active_menu` | Zeros 0x283f/0x2841 (active menu far pointer) |
| `0x2e18` | `game_mode_init` | Initializes game mode state, resets sprite/cursor/menu state |
| `0x2f3f` | `entity_table_set_sprite` | Reads 0x7df9+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 (015) |
| `+0x3c` | sprite_handle | Sprite for this entity |
| `+0x3f` | shot_sprite | Sprite handle for active projectile (0xFFFF = none) |
| `+0x45`/`+0x47`/`+0x49` | world_x/y/z | Current world position (integer) |
| `+0x4f`/`+0x51`/`+0x53` | base_x/y/z | Base/spawn position |
| `+0x54`/`+0x56`/`+0x58` | prev_x/y/z | Previous frame position |
| `+0x59` | attack_active | Attack in progress flag |
| `+0x5a` | at_target | Reached target flag |
| `+0x5e``+0x65` | delta_x/y/z/high | Per-step movement deltas (fixed point) |
| `+0x66`/`+0x68` | step_active | Stepping active (1=yes, 0=off) |
| `+0x6a`/`+0x6c` | weapon_slot/dist | Weapon slot and total travel distance |
| `+0x6e` | delta_z | Alt Z delta |
| `+0x70` | projectile_type | Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain) |
| `+0x72`/`+0x74`/`+0x76` | target_x/y/z | Target position with deviation |
| `+0x77` | target_entity | Target entity handle |
| `+0x79` | secondary_pos | Secondary position struct pointer |
| `+0xad` | owner_entity | Owning entity handle |
| `+0xaf` | shot_owner_flags | Shot owner (entity/player) |
| `+0xb1` | bounce_count | Bounce counter (used with homing, type 5) |
| `+0xb3` | has_bounce | Has bounce trajectory active |
| `+0xbd` | actor_type | Actor type byte (used for direction table lookups) |
### Shot Entity Lifecycle (0x435e0x44a9)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x435e` | `shot_entity_alloc` | Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7 |
| `0x44a9` | `shot_entity_free` | Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1 |
### Projectile / Combat (0x46590x5a99)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x4659` | `projectile_init_vector` | Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536 |
| `0x4a91` | `entity_fire_weapon` | Fires weapon from entity using 0x129b/0x12ac direction offset tables |
| `0x4b18` | `fire_weapon_from_cursor` | Gets cursor angle sprites, fires projectile at cursor target |
| `0x4b78` | `projectile_check_hit` | Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity) |
| `0x4c2e` | `projectile_step_update` | Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked |
| `0x4d28` | `projectile_trace_ray` | Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate |
| `0x51ad` | `projectile_update_tick` | Full projectile tick: move, check reach target, bounce, call projectile_check_hit |
| `0x5a99` | `projectile_apply_hit` | Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner |
### Weapon Type Table (0x2536)
- Each entry is 0x11 bytes (17), accessed as `weapon_type * 0x11`
- `[0]` = step divisor for distance calculation
- `[0x19]` = max range threshold (used in projectile_update_tick)
### Direction Tables (0x129b / 0x12ac)
- Indexed by facing (015): dx offsets at 0x129b, dy offsets at 0x12ac
- Values are multiplied by distance (e.g. `*0x500`) for projectile spawn offsets
### Collision Detection (0x60c10x621e)
| Address | Name | Description |
|----------|----------------|-------------|
| `0x60c1` | `aabb_overlaps_3d` | 3D AABB overlap test — box layout [xmin,ymin,zmin,_,_,xmax,_,ymax,_,zmax] |
| `0x621e` | `bbox_translate` | Translates a 3D bounding box by (dx, dy, dz) — both min and max points |
### Enemy AI / Spawning (0x6aed0x6d21)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x6aed` | `map_find_spawn_point` | Finds map tile matching entity conditions; returns packed XYZ tile coords |
| `0x6bfc` | `actor_find_in_view` | Finds actor visible in current view frustum (temp data at 0x7eca) |
| `0x6ce9` | `enemy_spawn_with_target` | Wrapper: spawns enemy with player as target (param5=1) |
| `0x6d05` | `enemy_spawn_no_target` | Wrapper: spawns enemy without targeting player (param5=0) |
| `0x6d21` | `enemy_spawn_at_position` | Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A) |
### Player / HUD
| Address | Name | Description |
|----------|-------------------------------|-------------|
| `0x50ee` | `player_position_update` | Updates player position from direction data; clamps to screen bounds |
| `0x6ff7` | `player_health_update_and_effect` | Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect |
### Destruction / Death (0x74900x75ff)
| Address | Name | Description |
|----------|---------------|-------------|
| `0x7490` | `debris_spawn`| Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list |
| `0x75ff` | `entity_die` | Death handler: spawns 14 debris objects, picks best explosion direction |
### Entity Type Constants (weapon_type/entity class)
| Value | Entity Class |
|--------|--------------|
| `0x17` | Robot/mech type A |
| `0x18` | Robot/mech type B |
| `0x1` through `0x3c` | Various entity/weapon types |
| `0x3d` | Robot/mech type C |
| `0x3e` | Robot/mech type D |
| `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:e0000007:fe00` flat range (above seg001, separate segment loaded at `0x7E000+`).
### Isometric Coordinate Transform
| Address | Name | Evidence |
|---------|------|---------|
| `0007:be67` | `world_to_screen_isometric` | Classic 2:1 isometric formula: `screen_x = (wx + sx) + (wy + sy)*2`, `screen_y = (wy + sy)*2 - (wx + sx)`. Scroll globals: `0x2bb7` (X), `0x2bb9` (Y). Output to `*param_3`, `*param_4`. |
| `0007:be9e` | `world_to_screen_isometric_wrapper` | Thin wrapper — calls `world_to_screen_isometric` with args shifted by one |
### Draw List Node Format
Sprite/draw node (size `0x18` = 24 bytes from pool, allocated by `linked_list_pop_2cc3`):
| Offset | Field | Notes |
|--------|-------|-------|
| `+0x0` | vtable | Function ptr for render callback (vtable[0] = draw) |
| `+0x8` | dep_from_list | Near ptr to list of sprites that depend ON this |
| `+0xa` | dep_to_list | Near ptr to list of sprites this depends ON |
| `+0xc` | bbox_xmin | Screen bounding box (4 ints) |
| `+0xe` | bbox_ymin | |
| `+0x10` | bbox_xmax | |
| `+0x12` | bbox_ymax | |
| `+0x14` | flags | bit 0=queued in draw list, bit 5=visible/rendered, bit 6=root sentinel, bit 7=must-redraw |
| `+0x15` | z_depth | int; sort key for painter's algorithm. 1 = root sentinel |
| `+0x17` | order_flag | if set, propagates must-redraw to dependent sprites |
| `+0x18` | tile_index | sparse tile index (for dirty bitmask bit addressing) |
### Draw List Functions
| Address | Name | Evidence |
|---------|------|---------|
| `0007:eb36` | `drawlist_pool_init` | Inits free pool: 1500 nodes × `0x2a` (42) bytes from base `0x2cc7`. Sets `0x2ccf = 1500` (count), `0x2cc3` = linked list head. Copies `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:bxxx0007:dxxx` range.
### Entity State Transition Helper
| Address | Name | Evidence |
|---------|------|---------|
| `0007:5b6f` | `entity_set_at_target_update_facing` | Sets entity `+0x3a = 1` (arrived flag); calls `entity_set_facing_direction`; clears bit `0x10` from entity type table `0x7e1e[type*0x79+0x59]`; tail-calls thunk to advance state. Called in the entity state machine context. |
### Animation Start Frame Helper
| Address | Name | Evidence |
|---------|------|---------|
| `0007:71b2` | `entity_set_anim_start_frame_from_flags` | Reads entity `+0x4b` flags. If bit 1 set: uses type table `+0x59 & 4` (attack active) to select last frame (`+0x39 - 1`), zero, or half-frame (`+0x39 >> 1`). Writes computed value to type table `+0x10`. If bit 2 set without bit 1: dispatch thunk. |
### Combat Helper
| Address | Name | Evidence |
|---------|------|---------|
| `0007:894b` | `entity_check_attack_flags_and_dispatch` | Guards on entity `+0x4b` bit 1 AND target object `+5` bits `0x1c`. If both set: dispatches thunk attack event. |
### Vtable Dispatch Helpers
| Address | Name | Evidence |
|---------|------|---------|
| `0007:8920` | `entity_call_vtable_slot0c` | Calls `(*param_1)[vtable+0xc]()`. |
| `0007:8cb8` | `entity_call_vtable_slot08` | Calls `(*param_1)[vtable+0x8]()`. |
| `0007:ccf1` | `entity_call_vtable_slot28` | Calls `(*param_1)[vtable+0x28]()`. |
### Active Flag / Counter
| Address | Name | Evidence |
|---------|------|---------|
| `0007:8854` | `entity_set_active_flag` | Sets entity `+0x40 = 1` (active); increments global `0x2800`. |
### Dispatch Table Lookup
| Address | Name | Evidence |
|---------|------|---------|
| `0007:8508` | `entity_table_lookup_and_dispatch` | Searches 1-entry table at `0x2b46` for `(param_3, param_4)` key pair; on match, calls the entry's function pointer at `[2]`. |
### Scroll/Camera Functions
| Address | Name | Evidence |
|---------|------|---------|
| `0007:bab5` | `entity_set_watch_ptr` | Stores FAR entity ptr to `0x2bd8` (the watch target entity). |
| `0007:baea` | `camera_update_and_check_player_scroll` | Calls watch entity vtable `+0x24`; if `0x2bd1` flag clear checks if player position (from `g_player_entity_farptr+0x40`) has moved > 32 units since `0x2be0`; if so, updates `0x2be0` and conditionally dispatches scroll event via `0x45aa`. |
| `0007:c6ba` | `scroll_camera_set_state_params` | Stores word params to `0x8354`, `0x8356`, byte to `0x8358`; dispatches. |
| `0007:cd56` | `dispatch_if_flag_2bd3_set` | Returns unless `0x2bd3` non-zero. |
| `0007:cfef` | `dispatch_if_mode_flags_set` | Two-flag check: dispatches if `0x2bca` or `0xee0` is non-zero. |
| `0007:d0f6` | `scroll_call_set_params_unless_blocked` | Calls `scroll_camera_set_state_params` only if `0x2bbb == 0`. |
| `0007:d119` | `scroll_update_direction_tracking` | Guards on `0x2bd3`. Calls `scroll_call_set_params_unless_blocked`. Compares direction bytes from `0x2cf4/0x2cf5` against cached `0x2bbd/0x2bbe`; if changed, clears `0x2bbc`. Dispatches. |
| `0007:d4a5` | `scroll_set_option_value` | Sets `0x2bc6 = param_1`. |
| `0007:d4b0` | `scroll_set_params_default` | Unconditional call to `scroll_camera_set_state_params`. |
| `0007:d4d3` | `scroll_set_map_index_validated` | If `param_1` in `[0..250]` and differs from `0x2bbf`, updates `0x2bbf` and clears `0x2bbc/0x2bbb`. |
| `0007:d655` | `map_position_has_changed` | Compares map arrays `0x7ded/0x7df1/0x7df5` at index `0x2bc6` against cached `0x2bc1/0x2bc3/0x2bc5`. Returns 1 if changed, 0 if same. |
| `0007:d6b1` | `scroll_clear_dirty_flags_and_dispatch` | Clears `0x2bbb = 0` and `0x2bbc = 0`; dispatches. |
| `0007:de57` | `entity_check_player_range_and_update` | Reads player world position (`g_player_entity_farptr+0x40`); if moved > 59 units from `entity+0x32` (cached pos), updates cache and calls `scrollregion_find_and_dispatch`. |
### Scroll Region Table (`0x835a`)
8 entries × `0x19` (25) bytes = 200 bytes. Entry layout:
| Offset | Field | Notes |
|--------|-------|-------|
| `+0x0/+0x2` | key pair | Matched by scroll region key lookup |
| `+0x8` | refcount | Reference count (incremented on match) |
| `+0xe` | count2 | Secondary counter |
| `+0x10` | active | Non-zero when region is live |
| `+0x11/+0x13` | x_start/x_end | Bounding X coordinates |
| `+0x15/+0x17` | y_start/y_end | Bounding Y coordinates |
| Address | Name | Evidence |
|---------|------|---------|
| `0007:e194` | `scrollregion_process_active` | Iterates active scroll entries (key non-zero AND `+0x10 != 0`), reads bounding box fields `+0x11/+0x13/+0x15/+0x17`, dispatches with bounds args. |
| `0007:e214` | `scrollregion_find_and_dispatch` | Finds empty scroll entry (zero key) and dispatches. |
| `0007:e29c` | `scrollregion_register` | Finds or allocates scroll region entry. Existing match: bump refcount / set active. New: init via `scrollregion_entry_init`. Special key `0x4ed`: thunk dispatch instead. |
| `0007:e50f` | `scrollregion_entry_init` | Null-guards param_1; zeroes output param_2, param_3, param_4; dispatches continuation. |
| `0007:e74a` | (unnamed) | Sets up call to walk `0x835a` table (8 entries, stride `0x19`, filter `0x968`), with callback return label `e763`. |
### Save Slot System (`0x8337` + `0x2ba3`)
10 save slots, each `0x400` (1024) bytes. Handle table at `0x8337` (10 words). Slot buffer base at FAR ptr `0x2ba3`.
| Address | Name | Evidence |
|---------|------|---------|
| `0007:ac13` | `saveslot_table_clear` | Fills `0x8337..0x834b` (10 words) with `0xFFFF` (all slots empty). |
| `0007:acab` | `saveslot_free_if_empty` | Scans slot `0x2ba3[param_1*0x400]` for non-zero data; if empty, sets handle `0x8337[param_1] = 0xFFFF`. |
| `0007:ad47` | `saveslot_find_index_by_id` | Linear scan of 10-word handle table `0x8337`; returns index of matching word or `-1`. |
| `0007:ad79` | (unnamed) | Finds a free (0xFFFF) slot index. Complement of `saveslot_find_index_by_id`. |
| `0007:afd4` | `saveslot_get_or_alloc` | Gets slot pointer: calls `saveslot_find_index_by_id`; if not found calls `ad79` to get free slot; returns `0x2ba3 + slot * 0x400`. Returns 0 if no free slot. |
| `0007:b02c` | `saveslot_write_entry` | Navigates to `slot_base[param_3 * 4]`; dispatches thunk paths for write (existing, overwrite, new). |
| `0007:b0de` | `saveslot_read_entry_flags` | Reads from slot entry far pointer at `slot_base[param_3*4]`; extracts 4-byte packed bitfield from `+4..+7` in entry record into `*param_1`. Bit-by-bit extraction loop for 4 bytes. |
### String & Memory Utilities
| Address | Name | Evidence |
|---------|------|---------|
| `0007:a96d` | `entity_copy_string_truncated80` | Strlen(param_3) ≤ 0x50 guard; copies string word-by-word from param_3 into `param_2+8`. |
| `0007:b813` | `memcpy_4words` | Copies 4 words (8 bytes) from `param_2` to `param_1`. |
| `0007:ba45` | (unnamed) | Null/non-null far-ptr dispatch: different thunk paths based on `param_2 == 0`. |
| `0007:b46d` | `entity_dispatch_if_slot82e2_valid` | Guard: if `*(int *)0x82e2 != -1`, calls dispatch thunk. |
### Linked List Utilities (draw pool + sprite)
| Address | Name | Evidence |
|---------|------|---------|
| `0007:ea00` | `bbox_intersect` | In-place rect intersection: `[xmin,ymin,xmax,ymax]` = max(mins) × min(maxes). |
| `0007:ea6d` | `bbox_union` | In-place rect union: `[xmin,ymin,xmax,ymax]` = min(mins) × max(maxes). |
### Scroll/Camera Globals
| Address | Name | Notes |
|---------|------|-------|
| `0x2bb7` | `g_scroll_offset_x` | Isometric scroll X — added to world_x in screen transform |
| `0x2bb9` | `g_scroll_offset_y` | Isometric scroll Y |
| `0x2bbb` | `g_scroll_blocked` | If non-zero, blocks `scroll_camera_set_state_params` call |
| `0x2bbc` | `g_scroll_dirty` | Scroll direction changed flag (cleared when direction tracks) |
| `0x2bbd` | `g_scroll_dir_x` | Cached scroll direction X |
| `0x2bbe` | `g_scroll_dir_y` | Cached scroll direction Y |
| `0x2bbf` | `g_map_index` | Current map/level index `[0..250]` |
| `0x2bc1/0x2bc3/0x2bc5` | `g_map_entry_x/y/z` | Cached map entry X/Y/Z (vs. live map arrays) |
| `0x2bc6` | `g_map_slot_index` | Index into `0x7ded/0x7df1/0x7df5` arrays for current map slot |
| `0x2bca/0x2bc9` | `g_option_toggle_state` | UI option toggle state flags |
| `0x2bd1` | `g_scroll_block_flag` | Blocks camera update path if non-zero |
| `0x2bd3` | `g_scroll_active` | Non-zero = scroll system active |
| `0x2bd8` | `g_watch_entity_ptr` | FAR ptr to entity being tracked by camera |
| `0x2be0` | `g_player_scroll_pos` | Cached player world X+Y (ulong) for scroll threshold detection |
| `0x8354..0x8358` | `g_scroll_state_params` | Three scroll state params (word, word, byte) |
## Raw 0008 Gameplay Dispatch Helper Batch (new)
Small conservative rename batch from direct field-write behavior in the `0008:ba00-0008:be05` cluster.
### Newly renamed functions
| Address | Name | Evidence |
|---------|------|---------|
| `0008:ba00` | `entity_dispatch_entry_init` | Constructor-style init: optional alloc (`0x32` bytes), vtable/list-link setup (`0x3b06`, `0x2d10`, `0x3afe`), zeroes state fields, seeds group from global active layer `0x39c9` via `entity_set_group_id` |
| `0008:bbb6` | `entity_set_source_type` | Writes entry word field `+0x04` from incoming parameter, then dispatches through FAR thunk path |
| `0008:bc27` | `entity_set_event_type_checked` | Writes entry word field `+0x06`; when source field `+0x04` is non-zero, validates old/new event transition, including special checks for `0xF0-0xF7` and upper bound `<= 0x0FFF` |
| `0008:bca8` | `entity_set_group_id` | Validates group id range `1..31`, writes low 5-bit group in byte `+0x08`, decrements old per-group counter and increments new one via counter table pointed to by `0x39c5` |
| `0008:be05` | `entity_increment_group_id` | Computes `((entry+0x08)&0x1F)+1`, validates against active-layer assumptions (`0x39c9`), then applies through `entity_set_group_id` |
### Verified call/xref notes
- `entity_set_group_id` is called from:
- `entity_dispatch_entry_init` at `0008:bae4`
- `entity_increment_group_id` at `0008:be57`
- `entity_set_source_type` is used from at least:
- `FUN_0008_c92f` (`0008:c94d`, `0008:c96d`)
- `FUN_0008_ca18` (`0008:ca36`, `0008:ca56`)
### Gameplay relevance
- This cluster appears to manage core dispatch-entry metadata (`source_type`, `event_type`, group/layer byte and counters) that feeds the seg021 scheduler/event system previously documented.
- The field offsets match the current seg021 entity/dispatch layout notes (`+0x04`, `+0x06`, `+0x08`), strengthening confidence in cross-function struct consistency.
## Raw 0008 Pair-Sync Helper Batch (new)
Conservative directional rename batch from the `0008:c7f1-0008:cad7` cluster.
These functions are clearly paired and structurally symmetric, but final gameplay semantics are still partial due to FAR-thunk heavy internals.
### Newly renamed functions
| Address | Name | Evidence |
|---------|------|---------|
| `0008:c7f1` | `entity_pair_update_link_slot_a` | Guards on entry flags (`+0x16` must not include `0x4000`), then dispatches through FAR thunk using entry local struct at `+0x28` and partner-side key/id input |
| `0008:c890` | `entity_pair_update_link_slot_b` | Twin of `entity_pair_update_link_slot_a` with identical call shape and guard behavior; used in opposite order by pair-sync wrappers |
| `0008:c92f` | `entity_pair_sync_a` | If either side has unset `source_type` (`+0x04`), copies from partner via `entity_set_source_type`; then calls link-slot helpers in A-order and ends in FAR thunk using first side `+0x1e` data |
| `0008:ca18` | `entity_pair_sync_b` | Mirror of `entity_pair_sync_a` with reversed side/order for helper calls and final thunk argument ordering |
| `0008:c9ee` | `entity_pair_mark_and_sync_a` | Sets bit `0x10` in entry flags at `+0x16`, then calls `entity_pair_sync_a` |
| `0008:cad7` | `entity_pair_mark_and_sync_b` | Sets bit `0x10` in entry flags at `+0x16`, then calls `entity_pair_sync_b` |
### Verified call/xref notes
- `entity_pair_sync_a` is called from `entity_pair_mark_and_sync_a` (`0008:ca10`).
- `entity_pair_sync_b` is called from `entity_pair_mark_and_sync_b` (`0008:caf9`).
- Shared helper use inside pair sync wrappers:
- `entity_pair_update_link_slot_b` at `0008:c981` and `0008:ca6a`
- `entity_pair_update_link_slot_a` at `0008:c995` and `0008:ca7e`
### Gameplay relevance
- This cluster likely handles directional two-entity relationship synchronization in the scheduler/entity-dispatch layer (source/type propagation plus paired link-slot updates).
- Offsets used here (`+0x04`, `+0x16`, `+0x1e`, `+0x28`) align with the existing seg021 object-field and linker/list usage patterns, which increases confidence while preserving conservative naming.
## Raw 0008 Flag-0x20 Target-State Helpers (new)
Two complementary helpers near the pair-sync cluster were renamed using strict field/bit behavior only.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:cb2c` | `entity_flag20_clear_and_update_target` | Clears bit `0x20` at entry flags `+0x16`; if non-null target args are provided, writes far-pointer target fields `+0x12/+0x14`; then calls shared refresh helper `0008:c01d` |
| `0008:cb5c` | `entity_flag20_set_and_init_target` | Sets bit `0x20` at entry flags `+0x16`; initializes target far-pointer fields `+0x12/+0x14` only when currently zero; then calls shared refresh helper `0008:c01d` |
Notes:
- Naming intentionally stays flag-centric because high-level gameplay meaning of bit `0x20` is not yet fully resolved.
- Both helpers share the same post-update refresh path (`0008:c01d`), suggesting they are two state transitions in one target/link-management subsystem.
## Raw 0008 Dispatch Refresh Pipeline (new)
Follow-up rename batch for the shared refresh node used by the new flag-0x20 helpers.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:c01d` | `entity_refresh_dispatch_state` | Early-exit when flags at `+0x16` indicate dead (`0x8`) or already refreshed (`0x4000`); otherwise runs pre-clear, sets `0x4000`, calls update vfunc path, then runs flag-conditioned handlers |
| `0008:bfb2` | `entity_clear_status_bits_from_flags` | Clears specific bits in status word at `+0x32` based on state flags (`+0x16:0x400`, `+0x18:0x40/0x80`) |
| `0008:bf8e` | `entity_call_update_vfunc14` | Calls helper `0008:be6b`, then dispatches entity vtable call at offset `+0x14` |
| `0008:beee` | `entity_run_flagged_handlers` | Executes handler calls gated by flags (`+0x16:0x400/0x4`, `+0x18:0x40/0x80`) and then dispatches via FAR thunk using entry slot/index (`+0x2`) |
Verified xref context:
- `entity_refresh_dispatch_state` is directly called from:
- `entity_flag20_clear_and_update_target` (`0008:cb54`)
- `entity_flag20_set_and_init_target` (`0008:cb86`)
Gameplay relevance:
- This establishes a concrete state pipeline for dispatch entries after target/link changes: flag-gated status clear -> mark refreshed (`0x4000`) -> vtable update callback -> flag-conditioned subsystem handlers.
## Raw 0008 Flag-0x100 and Constructor-Variant Batch (new)
Additional conservative renames from the `0008:d1a4-0008:d27d` cluster.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:d1a4` | `entity_set_flag100_in_flags2` | Gate-checked setter: ORs bit `0x100` into entry word at `+0x18` |
| `0008:d1dc` | `entity_clear_flag100_in_flags2` | Gate-checked clearer: ANDs entry word at `+0x18` with `0xFEFF` (clears bit `0x100`) |
| `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | Constructor variant: allocates `0x40` bytes if null, reinitializes via `0008:cefb`, sets vtable to `0x3aa6`, sets flag `0x200` at `+0x16`, zeroes fields `+0x38..+0x3e` |
Notes:
- The `entity_set_flag100_in_flags2` / `entity_clear_flag100_in_flags2` pair is a verified complementary toggle with identical gate logic (`0x39a8/0x39f9/0x3991` check path).
- Constructor naming is intentionally vtable-centric (`0x3aa6`) until more direct gameplay semantics are recovered from its callback dispatch paths.
## Raw 0008 Periodic/Counter Helpers (new)
Follow-up renames from the `0008:d313-0008:d47d` cluster tied to the `0x3aa6` constructor branch.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:d313` | `entity_periodic_accumulate_and_dispatch` | Adds global delta (`0x39d0/0x39d2`) into entry accumulator (`+0x3c/+0x3e`), wraps against period (`+0x38/+0x3a`), and on wrap invokes entry vtable callback at `+0x28` with reentrancy guard bit `0x400` in `+0x18` |
| `0008:d3e6` | `entity_set_flag2000_and_update_active_counters` | Atomic (CLI/PUSHF) set of bit `0x2000` in `+0x16`; if bit `0x400` is set, decrements global counter `0x39f6` and increments `0x39f4` |
| `0008:d433` | `entity_clear_flag2000_and_update_active_counters` | Atomic clear of bit `0x2000` in `+0x16`; if bit `0x400` is set, decrements `0x39f4` and increments `0x39f6` |
Gameplay relevance:
- This identifies a concrete periodic dispatch mechanism (accumulator+wrap callback) and a paired active/inactive counter transition path around flag `0x2000`.
- The `0x39f4/0x39f6` counter swap strongly suggests global bookkeeping for a scheduler subset associated with these entries.
## Raw 0008 Word-List Management Batch (new)
Verified helper cluster for entry-owned word-list storage (sentinel-terminated with `0x0408`).
| Address | Name | Evidence |
|---------|------|---------|
| `0008:da00` | `entity_word_list_set_0408_terminated` | Rebuilds/replaces entry list from stack-provided words terminated by `0x0408`; frees prior list pointer at `+0x06/+0x08`; allocates and populates new list |
| `0008:dba3` | `entity_word_list_free_existing` | Validates list pointer exists, then frees old list buffer referenced by `+0x06/+0x08` |
| `0008:dbec` | `entity_word_list_destroy` | Resets vtable to `0x2d10`, frees list if present via `entity_word_list_free_existing`, and optionally frees object when destroy flag bit `1` is set |
| `0008:dc38` | `entity_word_list_ensure_contains` | Scans existing list for a given word; if missing, appends through `entity_word_list_append_unique` |
| `0008:dcab` | `entity_word_list_append_unique` | Allocates larger list, copies existing words, appends new word plus `0x0408` terminator, frees old list, then rebuilds via `entity_word_list_set_0408_terminated` |
Notes:
- Entry fields used by this subsystem: count at `+0x02`, list far pointer at `+0x06/+0x08`.
- The explicit `0x0408` terminator appears both in scanner/build logic and append path, making it a reliable list format marker.
## Raw 0008 Word-List Access/Mutation Batch (new)
Follow-up renames extending the same list subsystem.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:deea` | `entity_word_list_get_at` | Bounds-checks index against count (`+0x02`) and returns word from list pointer (`+0x06/+0x08`, stride 2) |
| `0008:df1b` | `entity_word_list_set_at` | Bounds-checks index then writes value into list element (`+0x06/+0x08`, stride 2) |
| `0008:dfa1` | `entity_word_list_find_unflagged_by_id10` | Scans list and returns first value satisfying `(value & 0x400)==0` and `(value & 0x3ff)==requested_id`; writes `0` when not found |
| `0008:ddaf` | `entity_word_list_remove_value` | Removes matching value(s) by counting survivors, rebuilding compact storage for non-matching entries, freeing old list storage, and updating list state |
Notes:
- `entity_word_list_find_unflagged_by_id10` implies list entries pack a 10-bit id plus flag bits (`0x400` observed).
- This further supports that the `0008:da00..dfa1` region is a compact encoded-ID list manager used by gameplay dispatch entries.
## Raw Import Note: `0000:ffff` Thunk Target (new)
Requested deep-dive on `FUN_0000_ffff`:
- Renamed to `unresolved_far_thunk_dispatch`.
- Current raw-import evidence indicates this is **not valid local executable logic** in this program view:
- Decompiler emits overlapping-instruction warnings and bad-control-flow warnings.
- Disassembly from `0000:ffff` into `0001:xxxx` is nonsensical/misaligned (mixed data/code artifacts).
- The body is heavily shared as a call sink from many segments, consistent with unresolved inter-segment thunking in this import mode.
Practical interpretation:
- Treat calls to `unresolved_far_thunk_dispatch` as unresolved external/indirect dispatch edges, not as meaningful function internals to recover in the raw flat import.
- Semantic recovery should continue from call-site argument setup and local field effects (the workflow used in recent 0008 batches).
## Raw 0008 Gate-Callback Wrapper Batch (new)
Conservative renames for callback wrappers sharing the same global gate condition.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:d00e` | `entity_gate_callback_wrapper_a` | Gate check on globals `0x39a8/0x39f9/0x3991`; on pass dispatches callback through unresolved thunk using entry `+0x2` and `[0x3b32:0x3b34] + 0x32` |
| `0008:d05f` | `entity_gate_callback_wrapper_b` | Same gate pattern; callback wrapper variant via unresolved thunk |
| `0008:d0b0` | `entity_gate_callback_wrapper_c` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d0ed` | `entity_gate_callback_wrapper_d` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d12a` | `entity_gate_callback_wrapper_e` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d167` | `entity_gate_callback_wrapper_f` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d3d2` | `entity_slot_callback_wrapper` | Thin wrapper: pushes entry slot/index (`+0x2`) and dispatches through unresolved thunk |
Notes:
- Names remain wrapper-oriented because target callbacks are unresolved in this raw-import model.
- These wrappers are now easier to track from call sites while preserving conservative semantics.
## Additional Unresolved Thunk Stubs (new)
Follow-up thunk census after inspecting `0000:ffff` behavior.
### Confirmed trampoline-only stubs
All of the following are single-instruction wrappers (`CALLF 0000:ffff`) and were given unique labels:
| Address | New Name | Observed Caller(s) |
|---------|----------|--------------------|
| `0004:2592` | `thunk_callf_0000_ffff_0004_2592` | `0004:262d` (`FUN_0004_2620`) |
| `000b:f924` | `thunk_callf_0000_ffff_000b_f924` | `000b:0144` (`FUN_000b_010b`) |
| `000c:827d` | `thunk_callf_0000_ffff_000c_827d` | `000c:8985`, `000c:8f96` (`FUN_000c_88b4`) |
| `000c:82f9` | `thunk_callf_0000_ffff_000c_82f9` | `000c:8a10`, `000c:8f79`, `000c:9052` |
| `000c:8356` | `thunk_callf_0000_ffff_000c_8356` | `000c:84a9` (`FUN_000c_84a5`) |
| `000c:e4f9` | `thunk_callf_0000_ffff_000c_e4f9` | `000c:e4f5` (`FUN_000c_e4e0`) |
Notes:
- This confirms `unresolved_far_thunk_dispatch` is represented by multiple local trampoline copies in different segment regions.
- Separating them by address improves call-graph navigation and makes subsystem-specific tracing less ambiguous.
## Raw 000c State-Dispatch Helper Cluster (new)
After separating thunk stubs, a coherent local state/chain management cluster was lifted in `000c:ab32-000c:ac8f`.
| Address | Name | Evidence |
|---------|------|---------|
| `000c:ab32` | `entity_state_tick_dispatch` | Core state tick helper using fields `+0x38/+0x39/+0x3b/+0x3d/+0x5b`; clears mode bit `0x100` when `+0x38==0`, may call cleanup helper `000c:ac55`, calls `000c:7730(state,1)`, and conditionally advances chain |
| `000c:ab96` | `entity_state_reset_and_tick_dispatch` | Reset wrapper: zeroes `+0x38` and `+0x39` then calls `entity_state_tick_dispatch` |
| `000c:abb4` | `entity_state_advance_next_or_fallback_a` | Advance path A: when `+0x49!=0`, follows node-next pointers from `+0x3b/+0x3d` using offsets `+2/+4`; when exhausted, either clears active flag and re-dispatches, or falls back to backup pointer `+0x41/+0x43` |
| `000c:ac8f` | `entity_state_advance_next_or_fallback_b` | Advance path B: same structure as A but follows alternate node offsets `+6/+8` and fallback pointer `+0x45/+0x47` |
Notes:
- This cluster gives a concrete local interpretation for part of the large `000c:88b4` control flow without relying on unresolved thunk internals.
- Naming remains direction/path based (`a`/`b`) where high-level gameplay meaning is still pending.
## Raw 000c State-Flag Guard / Input Handler Batch (new)
Second sweep through `000c` adjacent helpers — gated thunk wrappers and input/animation tick handlers.
| Address | Name | Evidence |
|---------|------|---------|
| `000c:9f74` | `entity_state_flag100_check_and_dispatch` | Init latch guard at `[0x6053]`; clears `[0x8c55]` on first call; checks `[ptr+0x5b]` bits `0x100` and `0x40`; three-path thunk dispatch |
| `000c:a1ad` | `entity_state_clear_flag40_and_dispatch` | Skips if `[ptr+0x5b]` has `0x180` bits set; if `0x40` set, clears it and calls far ptr at `[0x5e82/0x5e84]`; then dispatches twice with args `(0x0b,0x10,0x1,0x0)` (record/state-key pattern) |
| `000c:a74e` | `entity_state_dispatch_if_flag_bit2` | Tests `[ptr+0x5b]` bit `0x2`; if set pushes extra arg + ptr and dispatches via thunk |
| `000c:84c3` | `entity_state_set_byte40_at_global_ptr` | Sets byte `[DAT_0000_6828 ptr + 0x40] = 1` then calls thunk unconditionally; enables global entity flag |
| `000c:ac55` | `entity_state_fire_if_handle_valid` | Guard: fires thunk dispatch only when `[0x6054] != -1`; no-op otherwise |
| `000c:ac6d` | `entity_state_fire_with_args_if_handle_valid` | 3-arg variant: pushes `[BP+0xe]` (byte), `[BP+0xc]`, `[BP+0xa]`, handle `[0x6054]`, then `CALLF 0000:ffff` |
| `000c:afa5` | `entity_state_check_field49_and_call_vfunc3c` | Checks field `[ptr+0x49]`: 1→reset to 0 return 1; 2→call `vtable[0x3c]` return 0; else thunk dispatch |
| `000c:b153` | `entity_state_animation_done_tick` | Checks `[param_2+0x14+0xa]` animation-complete flag; if zero increments `field49` and calls `entity_state_check_field49_and_call_vfunc3c`; if set calls `vtable[0x3c]` |
| `000c:b199` | `entity_state_input_key_handler` | Full input dispatcher: ESC/x/X → `vtable[0x3c]` (cancel); Left/Right arrows `0x14b`/`0x148` → prev state; n/N/0x14d/0x150 → next state; e/E → set `field47=1`; `-` with counter → trigger at 4. Manages `field47` and `field49` |
| `000c:b2c3` | `stub_noop_000c_b2c3` | Empty stub; returns immediately |
| `000c:b2c8` | `entity_state_dispatch_if_field49_eq4` | Fires thunk only when `[ptr+0x49]==4` |
| `000c:b349` | `entity_state_dispatch_if_far_ptr_nonzero_a` | Fires thunk if far-pointer args non-zero |
| `000c:b383` | `entity_state_set_field3f_and_dispatch` | If non-NULL: writes `&DAT_0000_2d18` to `[ptr+0x3f]`, then dispatches |
| `000c:b3d8` | `entity_state_dispatch_if_far_ptr_nonzero_b` | Same null-guard pattern as `b349`, variant b |
Patterns confirmed in this batch:
- `field49` = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
- `field47` = keystroke-combo counter
- `field3f` = linked data pointer (event/record reference)
- Global `[0x6054]` = current entity handle; `[0x6828]` = another global entity far pointer
- Bits in `[ptr+0x5b]`: `0x1=init`, `0x2=active/event`, `0x40=pending dispatch`, `0x100=flag100`, `0x180=skip-all mask`
## Raw 000c Palette Fade + Entity VM Cluster (new)
Two distinct subsystems identified in `000c:cdde-000c:f98b`.
### VGA Palette Fade (000c:cdde, 000c:ce57)
| Address | Name | Evidence |
|---------|------|---------|
| `000c:cdde` | `palette_fade_step_down` | Writes (Roffset, Goffset, Boffset) clamped to 0 to VGA I/O `0x3c8`/`0x3c9`; decrements `[0x630d]` by step `[0x6316]`; clears active at `[0x630a]` when black |
| `000c:ce57` | `palette_fade_step_up` | Same loop, adds offset, clamps at 63 (0x3f full VGA). Clears `[0x630a]` when fully bright |
Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src ptr, `[0x630d]`=brightness offset, `[0x6316]`=step, `[0x630a]`=active flag.
### Entity Mini-VM / Record-Player Context (000c:f6b8-000c:f98b)
| Address | Name | Evidence |
|---------|------|---------|
| `000c:f6b8` | `record_table_get_by_index` | Bounds check `param < [0x8c88]`; return `word at [0x8c84 + param*4]`. Table at 0x8c84 |
| `000c:f6e8` | `entity_vm_stack_init_with_data` | Init stack ptrs at `[ptr+0xcc..+0xd4]` pointing to self; max depth 199; copies optional initial data |
| `000c:f772` | `entity_vm_state_copy` | Copies 200 bytes (100 words from `[src+4]` to `[dst+4]`), then copies 4 words at `+0xcc..+0xd2` |
| `000c:f7c7` | `entity_vm_stack_push_frame` | Push call-frame: saves ret offset at `[ptr+0xd4]`, decrements `[ptr+0xcc]` by param_size, zeroes new frame |
| `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params |
| `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full |
| `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty |
| `000c:f94f` | `entity_vm_counter_add` | `[ptr+0xd6] += param_2`; simple accumulator |
| `000c:f98b` | `entity_vm_set_field_da_to_global` | Writes `[param_2+0xda far-ptr + 2]` into `[0x8c94]` |
Notes:
- Field offsets `+0xcc`=VM stack ptr, `+0xce/+0xd0`=segment regs, `+0xd2`=base, `+0xd4`=frame depth, `+0xd6`=counter, `+0xd8/+0xda/+0xdc`=VM position/bounds.
- The 200-byte body region at `[ptr+4..+0xcc]` holds 100 words of script/state payload.
- The pair-stack (field `+0x80`) is separate — likely pass/return value stack for the mini-script.
## Raw 000c Cursor Zone / Slot Array / String Queue Batch (new)
Three utility subsystems identified in `000c:e6d9-000c:eadd`, plus companion slot array API.
### Cursor / Directional Zone Classifier
| Address | Name | Evidence |
|---------|------|---------|
| `000c:e6d9` | `cursor_zone_quadrant_classify` | Splits screen by `[0x63d6]/2` and `[0x63d8]/2` vs bounds `[0x8c6c..0x8c72]`; returns directional code from 9-word table at `0x6401` |
Zone table layout (9 entries): NW/N/NE / W/Center/E / SW/S/SE based on horizontal threshold at `0x8c6c/0x8c70` and vertical at `0x8c6e/0x8c72`.
### Slot Array System (000c:ea53-000c:ee44)
A complete 29-slot menu/choice array with fixed stride 0x15 bytes, base at `[ptr+0x67]`, count at `[ptr+0x7a]`:
| Address | Name | Evidence |
|---------|------|---------|
| `000c:ea53` | `entity_slot_count_update_and_notify` | Sets `[ptr+0x72]=param-1`; calls `slot_array_get_current_entry` and `slot_array_find_and_dispatch`; calls `vtable[0]()` when `+0x75` flags set |
| `000c:eba5` | `slot_array_dispatch_matching` | Walks 0xb-stride array from `[ptr+4]`; calls thunk for each entry where `[entry+9]==param_4` |
| `000c:ec30` | `slot_array_dispatch_if_nonempty` | Returns `0xffff` if count < 1; else dispatches |
| `000c:ec9e` | `slot_array_find_and_dispatch` | Searches 0xb-stride array for `[entry+9]==param_4`; calls thunk on first match |
| `000c:ecf5` | `slot_array_push_entry` | Copies named string to `[base+0xc]`; writes 6 param words at `+0x12..0x20`; increments count |
| `000c:edb0` | `slot_array_push_raw` | Copies 0x15-byte raw entry from `param_2`; increments count |
| `000c:edf7` | `slot_array_pop` | Decrements `[ptr+0x7a]`; asserts `>= 0` |
| `000c:ee19` | `slot_array_init` | Sets `[ptr+0x78]=0`, `[ptr+0x76]=0`, `[ptr+0x75]=1` (active flag) |
| `000c:ee32` | `slot_array_clear_flags` | Clears `[ptr+0x74]=0`, `[ptr+0x75]=0` |
| `000c:ee44` | `slot_array_get_current_entry` | Returns `ptr + [ptr+0x7a]*0x15 + 0x67` (current entry ptr); 0 if count <= 0 |
### String Queue
| Address | Name | Evidence |
|---------|------|---------|
| `000c:eadd` | `string_queue_push` | Appends string to 10-entry queue at `[ptr+4]`; count at `[ptr+2]`; sets `[ptr+0xd]=param_4` |
### Additional VM-Adjacent Helpers
| Address | Name | Evidence |
|---------|------|---------|
| `000c:f2e7` | `entity_call_vtable_entry_10_if_valid` | Null-guard: calls `(*[ptr+8+0x10])()` if param_1 non-null |
| `000c:f39f` | `string_table_lookup` | Searches `[0x65bc/0x65be]` table by key string; returns matching words to out-params |
## Raw 000c Cursor Nav Dispatcher / State Reset Batch
Cursor navigation subsystem in `000c:d3e9000c:db68`. Manages directional zone changes, mouse button reads, keyboard scancodes, and entity vtable dispatch for interactive UI cursors.
### Cursor Navigation Fields (entity object offsets)
| Offset | Purpose |
|--------|---------|
| `+0x32` | Current zone code (08) |
| `+0x33` | Previous zone code |
| `+0x37+0x3a` | Directional booleans: N/S/W/E |
| `+0x3f+0x42` | Mouse button flags |
| `+0x45` | Last keyboard scancode |
| `+0x47` | Navigation index |
Globals: `[0x63da]` = mouse button state, `[0x63d6]/[0x63d8]` = cursor X/Y, `[0x638e]` and `[0x6346]` = reference data tables.
### Functions
| Address | Name | Evidence |
|---------|------|---------|
| `000c:dac1` | `cursor_nav_state_reset` | Zeros all directional/button flags; sets `[+0x32/+0x33]=0xff`, `[+0x47]=0xffff` |
| `000c:db68` | `cursor_nav_update_and_dispatch` | Calls `cursor_zone_quadrant_classify`; updates `[+0x37..+0x3a]`; reads `[0x63da]`; switch on direction (08); maps scancodes 0x48/0x50/0x4b/0x4d/0x39 |
| `000c:d3e9` | `cursor_set_ref_and_dispatch` | Null-checks param; sets `*param_1 = &DAT_0000_638e`; calls dispatch |
| `000c:d710` | `cursor_set_ref2_and_dispatch` | Same pattern; sets `*param_1 = &DAT_0000_6346` |
| `000c:d75e` | `entity_call_vtable_1e_via_ptr` | Calls `(*[*param_1 + 0x3c])()` — vtable offset 0x1e |
| `000c:d775` | `entity_call_vtable_1e_via_ptr_b` | Near-identical to `d75e`; duplicate generated by compiler |
| `000c:d7c6` | `stub_noop_000c_d7c6` | Empty stub |
| `000c:d7cb` | `stub_noop_000c_d7cb` | Empty stub |
Direction code mapping (from `cursor_nav_update_and_dispatch` switch):
- 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW, 8=Center
### Timer Rate Sync (cursor subsystem)
| Address | Name | Evidence |
|---------|------|---------|
| `000c:e4e0` | `cursor_timer_rate_sync` | If `[0x63e0]` (cursor active) non-zero: copies PIT rate `[0x39ce]` into entity object at `[0x4458+0x24]`, clears `+0x26/+0x27`, then calls far thunk. Called from `cursor_nav_update_and_dispatch` at `000c:db97`. |
## Raw 000c UI Listbox Event Handler Batch
Analysis of `000c:880c` and `000c:88b4` — the primary UI event-dispatch island in segment `000c`.
### Overview
`ui_listbox_event_handler` (`000c:88b4`) is a **large UI widget event handler** (~8KB of code, `88b4` through `9daa`). It takes the signature `(entity_far_ptr, seg, event_far_ptr)` and is stored as a far-pointer vtable entry (no direct code xrefs found; referenced from data). It dispatches on the event code at `[event+0x6]` (keystroke/mouse codes) and updates entity fields to drive menu/listbox navigation state.
Guard function `entity_state_if_flag80_call_thunk` (`000c:880c`) sits just before the handler: it checks entity flag bit `0x80` at `[param_1+0x5b]` and gates a far thunk call.
### Functions
| Address | Name | Evidence |
|---------|------|---------|
| `000c:88b4` | `ui_listbox_event_handler` | Dispatches on event code `[event+0x6]`: confirm keys `0xd`/`0x20`/`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`/`0x13b0x143`/`0x241`/`0x410`/`0x420`/`0x426`/`0x42f`/`0x432`/`0x441` → various paths. Guards: `[BX+0x5b] & 0x80` (active), `& 0x100` (modal lock from `[0x604b]`, `[0x844]`). Large scan loop `0x100..0x27ff` at `000c:a80f` for option/device enumeration. Falls through to common retf at `9da7`. |
| `000c:880c` | `entity_state_if_flag80_call_thunk` | Guard: tests entity `[param_1+0x5b] & 0x80`; returns if clear; else calls far thunk. Active/visible gate for forwarding display/tick calls. |
| `000c:ace5` | `display_fullscreen_blit_with_entity` | If coords (param_2:param_1) are 0:0, calls wait thunk (`0x48c`=1164ms?); loads display object at `[0x4cd0]`, reads byte `[+0x5]` (video mode/palette), builds full-screen 640×480 rect (`0`..`0x27f`=639, `0`..`0x1df`=479), then calls far display function with entity + position params. Returns display-space coordinate pair in DX:AX. |
### Key Globals in this Handler
| Address | Meaning |
|---------|---------|
| `[0x7e22]` | String/resource pointer used as data context (`[0x7e24]` = flag) |
| `[0x604b]` | Modal active lock (nonzero = block most events) |
| `[0x844]` | Engine/game-ready flag (0 = block hardware toggle events) |
| `[0x604f]` | Toggle state for event `0x410` |
| `[0x6045]` | Toggle state for event `0x7e` |
| `[0x8638]` | Counter for event `0x432` (wraps 0x11→0x12..0x14→0x0) |
| `[0x5e82/0x5e84]` | Far pointer called when entity flag `0x40` is cleared |
| `[0x4cd0]` | Display/screen manager object far pointer |
### Event Code Reference Table (partial)
| Code | Meaning |
|------|---------|
| `0x0d` / `0x20` | Enter / Space (confirm) |
| `0x1b` | Escape (cancel) |
| `0x2c` / `0x3c` | Comma / `<` (prev in list) |
| `0x2e` / `0x3e` / `0x53` / `0x73` | Period / `>` / S / s (next in list) |
| `0x55` / `0x75` | U / u (up action with far-ptr check) |
| `0x6f` | o (some option panel event) |
| `0x7e` | `~` (toggle event, gates on `[0x844]` and `[0x6045]`) |
| `0x13b` | Shift+F1 area |
| `0x13c` | Jump-to-display event |
| `0x13e` | Blanked event (gates on `[0x604b]`) |
| `0x141` / `0x142` / `0x143` | Option toggles (gate on `[0x844]`, `[0x2bca/0x2bc9]`) |
| `0x148` / `0x14b` / `0x14d` / `0x150` | Arrow keys (Up/Left/Right/Down) |
| `0x152` | Insert key (alias confirm) |
| `0x241` | Display position query → `display_fullscreen_blit_with_entity` |
| `0x410` | Toggle event with `[0x604f]`, gates on `[0x844]` |
| `0x420` | List-scan init (loop 0x100..0x27ff) |
| `0x426` | String-push event (calls DS:[0x7e22]) |
| `0x42f` | Push `0x103` thunk event |
| `0x432` | Counter cycle event (`[0x8638]` wrap 0x11→0x12, 0x14→0x0) |
| `0x441` | Pattern-fill loop event |
## NE Segment 21 Analysis — Timer/Event Dispatch System
**File**: `seg021_code_off_50200_len_4486.bin` | **File Offset**: 0x50200 | **Length**: 0x4486 bytes
**Ghidra Load**: RAM `0000:0000 0000:4485`, x86 16-bit Protected Mode, base 0x0000
**Functions**: 88 total (87 renamed + `input_keyboard_handler` pre-existing)
### Subsystem Summary
Segment 21 implements the **hardware-level timer interrupt and entity event dispatch system** — Crusader's real-time task scheduler. Key responsibilities:
- Programs and services the Intel 8253 **PIT timer** (I/O ports 0x40/0x43)
- Manages three **entity dispatch lists**: timer list (0x39d4), input list (0x39e3), render list (0x3a10)
- Maintains the **entity pool** at 0x39b0 (same pool as seg001; these segments share DS)
- Provides **event queue** (32-slot circular buffer at 0x31cc)
- Handles **save/load** serialization of the entire entity system
- Controls **keyboard/interrupt locks** and deferred scheduling
### Function Groups
#### Entity Pool Management (0x02070x0483)
| Address | Name | Notes |
|---------|------|-------|
| `0x0207` | `entity_count_by_type_a` | Count entities matching type+event; filters DEAD flag (0x8) |
| `0x0297` | `entity_count_by_type_b` | Identical logic to 0x0207 (compiler duplicate) |
| `0x0327` | `entity_find_free_slot` | Scan pool for null entry; calls panic if full; returns slot or 0xFFFF |
| `0x038f` | `entity_register` | Write far ptr to entity_list, group to entity_data, vtable to registry; inc count |
| `0x044d` | `entity_get_ptr_raw` | Read entity far ptr from pool slot (may be null) |
| `0x0483` | `entity_get_ptr` | Safe wrapper: verifies non-null, returns offset only |
#### Event Dispatch (0x04f30x08be)
| Address | Name | Notes |
|---------|------|-------|
| `0x04f3` | `entity_dispatch_reset_all` | Fires event code 0x21 (reset/init) to all entities |
| `0x050d` | `entity_clear_deferred_flags` | Clears DEFERRED bit (0x200) from up to N=0x3998 entities |
| `0x059e` | `entity_fire_event_broadcast` | Dispatch event to all matching entities; calls vtable[6]; respects 0x200 deferred flag |
| `0x06f4` | `entity_fire_event_type_include` | Fire only entities whose type IS in given list (up to 10, 0x0d=end) |
| `0x08be` | `entity_fire_event_type_exclude` | Fire only entities whose type is NOT in given list |
| `0x0a8e` | `input_keyboard_handler` | (pre-existing) OS-level key router: 0x0d=scroll+, 0x01=action, 0x2c=save, 0x44=load |
#### Entity Iterator / Linker (0x0bb70x106b)
| Address | Name | Notes |
|---------|------|-------|
| `0x0bb7` | `entity_link` | Cross-link two entities; skips if flag 0x400 set |
| `0x0c34` | `entity_find_first` | Init iterator 0x39fa=3; find first entity matching saved type/event at 0x399a/0x399c |
| `0x0cec` | `entity_find_next` | Continue iterator from 0x39fa cursor |
| `0x0dad` | `timer_entity_find_by_event` | Find entity handling event in range 0xf0-0xf7; checks bit 0x1000; writes to 0x3993 |
| `0x0e82` | `entity_find_by_priority` | Walk priority chain at 0x39d4; find entity matching source/event at 0x3993 |
| `0x0fc8` | `entity_set_cursor` | Validate flag 0x800; set cursor 0x3993 = param_1 (slot) |
| `0x100c` | `entity_get_cursor` | Return entity at 0x39bf if valid and not dead |
| `0x106b` | `entity_relink` | Re-link: find by event, walk priority chain, call set-link vtable funcs |
#### Entity Lifecycle (0x11330x131d)
| Address | Name | Notes |
|---------|------|-------|
| `0x1133` | `entity_unregister` | Full removal: dec sprite type count, vtable cleanup, dec total, update masks |
| `0x1202` | `entity_slot_clear` | Zero pool slot (0x39b0), registry slot (0x39ca), group data (0x39b4) |
| `0x1245` | `entity_layer_set` | Write 0x39c9 (active layer ID) if changed; set dirty flag 0x39a2 |
| `0x125d` | `entity_check_overdue` | If entity_is_overdue: set bit 0x40 on entity+0x16 |
| `0x127c` | `entity_is_overdue` | Return 1 if entity index > 0x39bf and flag 0x39c2 set |
| `0x129b` | `entity_list_call_update` | For all entities where entity+0x0e & param_3 != 0: call vtable[8] |
| `0x131d` | `entity_set_pending` | Write param to 0x3995 (next entity to register); error if already set |
#### Entity System Init/Shutdown (0x133e0x1705)
| Address | Name | Notes |
|---------|------|-------|
| `0x133e` | `entity_system_init` | Alloc all entity pool buffers (see decompiler comment); init three lists; clear event state |
| `0x14bc` | `entity_system_flush_normal` | Finalize (vtable[10]) then free all non-deferred active entities |
| `0x158d` | `entity_system_flush_deferred` | Same as flush_normal for deferred entities |
| `0x165c` | `entity_process_pending_deletes` | Free entities marked DEAD (flag & 0x8); dec 0x399e counter |
| `0x1705` | `entity_system_shutdown` | Full shutdown: flush normal, flush deferred, process deletes, free all pools |
#### Save / Load (0x18510x1d21)
| Address | Name | Notes |
|---------|------|-------|
| `0x1851` | `event_queue_state_reset` | Zero ring buffer state tables (0x334e, 0x364e), queue ptrs (0x31c8/0x31ca) |
| `0x18ce` | `level_load` | Full level load: shutdown + reinit + deserialize all entities via vtable[12] |
| `0x1d21` | `save_game` | Serialize entity system: arrays + each entity via vtable[14]; magic check 0x3a21==0xed |
#### PIT Timer / Hardware (0x23000x2975)
| Address | Name | Notes |
|---------|------|-------|
| `0x2300` | `pit_timer_program` | OUT 0x43, 0x36; OUT 0x40, lo; OUT 0x40, hi — raw PIT channel 0 program |
| `0x2316` | `pit_timer_set_hz` | Validates divisor <= 0xd688; stores at 0x39ce; calls pit_timer_program |
| `0x23a5` | `pit_timer_tick_handler` | Timer ISR: iterates 0x39d4 timer list, fires vtable callbacks per layer/mode |
| `0x25fc` | `timer_entity_active` | Check 0x3987/0x398b for active timer entity (mode-dependent) |
| `0x264c` | `timer_entity_get_current` | Get ptr from 0x3987 or 0x398b based on 0x3991 mode flag |
| `0x2668` | `timer_entity_enable` | Set ENABLED flag (0x400), inc counter, insert into timer list, reprograms PIT |
| `0x2745` | `timer_entity_disable` | Clear ENABLED, dec counter, reprograms PIT; if list empty calls interrupt_request_cancel |
| `0x2975` | `timer_recompute_hz` | Scan timer list; find smallest time_period (+0x38/+0x3a); call pit_timer_set_hz |
#### Interrupt / Lock Control (0x283a0x294b)
| Address | Name | Notes |
|---------|------|-------|
| `0x283a` | `interrupt_lock_acquire` | Re-entrant acquire on 0x31c7 (interrupt lock) |
| `0x2870` | `interrupt_lock_release` | Release 0x31c7 |
| `0x289b` | `entity_lock_acquire` | Re-entrant acquire on 0x39aa (entity system lock) |
| `0x28d5` | `entity_lock_release` | Release 0x39aa |
| `0x290d` | `interrupt_request_schedule` | Set deferred IRQ flags 0x39ab and 0x398f (or 0x39a9 in sync mode) |
| `0x294b` | `interrupt_request_cancel` | Clear IRQ request flags |
#### Timer Loop / Deferred State (0x2a5f0x2ad8)
| Address | Name | Notes |
|---------|------|-------|
| `0x2a5f` | `timer_event_loop` | **Main game loop**: polls player tick counter at 0x2de4; busy-waits; fires optional callback; stores delta to 0x3a00/0x3a02 |
| `0x2ac2` | `timer_deferred_reschedule` | If deferred mode flag 0x39b8 set, call reschedule |
| `0x2ad8` | `timer_snapshot_deferred` | Copy 0x39a9 → 0x39b8; call interrupt handler if 0x39a9 set |
#### Event Queue (0x2c730x3364)
| Address | Name | Notes |
|---------|------|-------|
| `0x2c73` | `event_queue_drain` | Drain circular queue; call event_queue_dequeue while 0x31c8 != 0x31ca; reset state |
| `0x2ca2` | `mouse_button_check` | Return 1 if BIOS 0x31a4 bit 0x10 set AND 0x39af (mouse enable) set |
| `0x2cbc` | `stub_noop_2cbc` | Empty stub function |
| `0x2cd7` | `bios_keyboard_flags_write` | Write param to 0x400:0017 (BIOS keyboard flags at segment 0x40, offset 0x17) |
| `0x2cf2` | `input_event_dispatch` | Dispatch display event 0x10 to input list entities with flag 0x100 and 0xc bits set |
| `0x2dc3` | `event_queue_push` | Push event to circular queue (write ptr 0x31ca); calls event_queue_is_full check |
| `0x3276` | `keyboard_state_read` | INT 16h AX=0: read raw keyboard state into 0x31a4 |
| `0x328b` | `keyboard_acquire` | If not locked (0x31c6): INT lock, read keyboard, set lock flag |
| `0x32cc` | `keyboard_release` | If locked: unlock, clear 0x31c6 |
| `0x3304` | `event_queue_count` | Count pending events: 0x31ca - 0x31c8 (circular) |
| `0x333d` | `event_queue_is_full` | Return 1 if ((0x31ca+1) mod 32) == 0x31c8 |
| `0x3364` | `event_queue_dequeue` | Read from ring buffer (0x31cc + 0x31c8*0xc, entry size 0xc), advance read ptr |
#### Event Subscription and Bitmask Helpers (0x34dd0x3878)
| Address | Name | Notes |
|---------|------|-------|
| `0x34dd` | `event_queue_process_all` | Drain queue; for each event find listener entities in 0x39e3; call vtable[0x14] |
| `0x35e9` | `event_queue_set_mode` | Write low 2 bits to 0x334c; call keyboard_interrupt_call |
| `0x35fb` | `event_queue_set_param` | Write low 5 bits to 0x334d; call keyboard_interrupt_call |
| `0x360d` | `keyboard_interrupt_call` | INT 16h (raw BIOS keyboard services call) |
| `0x3630` | `entity_validate_indices` | Debug assert: verify entity+0x02 (slot_index) == pool position for all entities |
| `0x369b` | `typemask_set_bit` | Set bit at 0x3a04 + (param>>3), bit (param & 7) — entity type present bitmask |
| `0x36d4` | `typemask_clear_bit` | Clear bit in 0x3a04 bitmask |
| `0x370f` | `typemask_update` | If entity type has listeners (entity_find_first != 0): set bit; else clear |
| `0x3744` | `typemask_test_bit` | Test bit in 0x3a04; return 1 if entity type has registered listeners |
| `0x377d` | `event_subscription_set` | Set subscription bit in 0x3a08 buffer |
| `0x37b2` | `event_subscription_clear` | Clear subscription bit in 0x3a08 buffer |
| `0x37e9` | `event_subscription_update` | If entity has listeners: set bit; else clear (driven by entity_find_first result) |
| `0x3825` | `event_subscription_test` | Test subscription bit in 0x3a08; return 1 if subscribed |
| `0x3864` | `event_state_clear` | Zero entire 0x3a0c event use-count buffer (0x4000 bytes = 8192 uint16s) |
| `0x3878` | `event_use_count_increment` | Increment 64-bit counter at 0x3a0c[entity_event_type*4] |
#### Input / Render List (0x38c20x3ae9)
| Address | Name | Notes |
|---------|------|-------|
| `0x38c2` | `input_event_broadcast` | Dispatch input event 0x40 to all render-list entities with flag 0x40; uses counter 0x39ad |
| `0x39a1` | `subscribe_to_render_list` | Add entity to 0x3a10 list; set flag bit 0x40; inc 0x3a1f |
| `0x3a13` | `unsubscribe_from_render_list` | Remove entity from 0x3a10; clear bit 0x40; dec 0x3a1f |
| `0x3404` | `subscribe_to_input_list` | Add entity to 0x39e3 list; check flag 0x100; set bit 0x80; inc 0x39c3 |
| `0x3477` | `unsubscribe_from_input_list` | Remove entity from 0x39e3; clear bit 0x80; dec 0x39c3 |
| `0x3a78` | `entity_lists_init` | Init three linked lists with sentinel vtable 0x3a89; write head vtable 0x2d10 |
| `0x3ae9` | `entity_lists_reset` | Call external reset + reinit 0x39e3 and 0x39d4 lists |
### Entity Object Field Layout (as used in Seg21)
| Offset | Field | Type | Description |
|--------|-------|------|-------------|
| `+0x00` | vtable_ptr | far ptr | Pointer to entity's vtable dispatch table |
| `+0x02` | slot_index | uint16 | Entity's own slot number in pool |
| `+0x04` | source_type | uint16 | Source/owner entity type (event matching) |
| `+0x06` | event_type | uint16 | Event type this entity handles |
| `+0x08` | flags_byte | uint8 | Low 5 bits = sprite group ID |
| `+0x0e` | capability_mask | uint16 | Bitmask of supported event capabilities |
| `+0x16` | state_flags | uint16 | bit3=DEAD, bit8=REGISTERED, bit9=ACTIVE, bit10=ENABLED, bit11=HAS_TIMER, bit13=IS_IRQ_HANDLER |
| `+0x18` | flags2 | uint16 | bit6=IN_RENDER_LIST, bit7=IN_INPUT_LIST, bit9=DEFERRED |
| `+0x1e` | priority_chain | far ptr | Priority chain entries (entity_find_by_priority) |
| `+0x20` | priority_count | uint16 | Count of priority chain entries |
| `+0x38` | time_period_lo | uint16 | Timer period low word (PIT frequency calc) |
| `+0x3a` | time_period_hi | uint16 | Timer period high word |
### Vtable Layout (Seg21 usage)
| Slot | Byte offset | Prototype | Purpose |
|------|-------------|-----------|---------|
| [6] | `+0x0c` | `handle_event(entity, CS, type, param)` | Event callback |
| [8] | `+0x10` | `update(entity, CS, capability_mask)` | Per-tick update |
| [10] | `+0x14` | `finalize(entity, CS)` | Cleanup/shutdown |
| [12] | `+0x18` | `load(entity, CS, file_ptr, CS)` | Deserialize from save |
| [14] | `+0x1c` | `save(entity, CS, file_ptr, CS)` | Serialize to save |
| [16] | `+0x20` | `set_backref(entity, CS, list_ptr)` | Set back-reference |
| [20] | `+0x28` | `dispatch_callback(entity, CS, event_id, 0, data_ptr)` | Generic dispatch |
### Key Global Data (Seg21 — additions to DS)
| Address | Name | Description |
|---------|------|-------------|
| `0x31a4` | bios_key_state | Raw INT 16h keyboard state |
| `0x31c6` | keyboard_lock | Keyboard acquired flag |
| `0x31c7` | interrupt_lock | Interrupt lock flag (re-entrant) |
| `0x31c8` | queue_read_ptr | Event queue read index (031) |
| `0x31ca` | queue_write_ptr | Event queue write index |
| `0x31cc` | event_queue_base | Ring buffer, 32 entries × 0xc bytes |
| `0x334c` | queue_mode | Event queue mode bits (01) |
| `0x334d` | queue_param | Event queue param bits (04) |
| `0x39b0` | entity_list | Far ptr to entity far-ptr array (count×4) — **shared with seg001** |
| `0x39b4` | entity_data | Far ptr to group/sprite-ID array (count×2) |
| `0x39b9` | entity_max_count | Max capacity of entity pool |
| `0x39bb` | entity_count | Total registered entity count |
| `0x39c9` | active_layer | Current active entity layer/group ID |
| `0x39ca` | entity_registry | Far ptr to vtable dispatch array (count×4) — **shared with seg001** |
| `0x39ce` | pit_divisor | Current PIT timer divisor |
| `0x39d4` | timer_list | Intrusive linked list: timer-dispatch entities |
| `0x39e3` | input_list | Intrusive linked list: input-handler entities |
| `0x3a04` | typemask_buf | Far ptr to entity type present bitmask (0x480 bytes) |
| `0x3a08` | evt_sub_buf | Far ptr to event subscription bitmask (0x2400 bytes) |
| `0x3a0c` | evt_state_buf | Far ptr to event use-count table (0x4000 bytes) |
| `0x3a10` | render_list | Intrusive linked list: render-callback entities |
| `0x3a21` | save_magic | Must be 0xed (-0x13) for valid save |
| `0x3a70` | default_registry_vtable | Default vtable written to entity_registry slots on register |
| `0x3a89` | list_sentinel_vtable | Sentinel vtable written to list head nodes |
---