# 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 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 (0x0060–0x0d5f) | Address | Name | Description | |----------|---------------------------|-------------| | `0x0060` | `cursor_update_hover` | Hover update: if mouse active & entity set, calls cursor_set_target | | `0x00e9` | `cursor_set_target` | Positions cursor on entity, updates sprite + direction visual | | `0x0322` | `cursor_shutdown` | Frees cursor resources, resets state | | `0x0398` | `cursor_animation_update` | Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6 | | `0x050f` | `cursor_draw_tick` | Per-frame cursor draw (calls cursor_animation_update if dirty) | | `0x0c24` | `action_key_valid` | Returns 1 if action code (param_1) is a valid game action key | | `0x0d5f` | `cursor_direction_input` | Arrow-key input: rotates cursor angle, updates direction sprite | ### Input Handling | Address | Name | Description | |----------|-------------------------|-------------| | `0x0526` | `input_keyboard_handler`| Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load | ### Cursor State Data (at DS:0x27xx) | Address | Field | Meaning | |---------|-------|---------| | `0x27c4` | cursor_sel1 | Selection counter 1 | | `0x27c6` | cursor_sel2 | Selection counter 2 | | `0x27c8` | current_entity | Handle to currently targeted entity | | `0x27ca–0x27ce` | cursor_state | Cursor interaction state bytes | | `0x27d0` | cursor_entity_type | Current entity type index | | `0x27d2` | z_offset | Z-height offset for terrain adjustment | | `0x27d4` | cursor_angle | Rotation angle (0–359) | | `0x27d6` | cursor_sprite | Sprite handle for cursor visual | | `0x27d8` | cursor_dirty | Set when cursor needs redraw | | `0x27d9` | cursor_active | Master cursor enabled flag | | `0x27da` | cursor_no_turn | Flag disabling cursor rotation | | `0x27ed` | difficulty | Enemy accuracy divisor (used in projectile_init_vector) | | `0x27fd` | hard_mode | Two-step mode (combat vs. explore) | | `0x27fe` | move_mode | Movement phase flag | | `0x27ff` | mouse_active | Mouse/input system active | | `0x2800`–`0x2811` | various | UI state: active sprite, facing byte, cur entity handle | | `0x283f`/`0x2841` | menu_obj_ptr | Active menu/dialog object far pointer | | `0x2844` | in_save | In-progress save game flag | | `0x290e` | entity_count | Number of active entities | | `0x2910`–`0x2947` | snap_type_ids[10] | Entity types that snap-to-ground in snap_entity_to_ground | ### Input / Action Dispatch | Address | Name | Description | |----------|---------------------------|-------------| | `0x2420` | `entity_command_dispatch` | Dispatches player commands to target entity; reads 0x27d0, 0x2de4, sends events 0x14/0xf, handles state machine 0x27ca/0x27cd/0x27ce | | `0x279a` | `cheat_code_check` | Checks entity byte+1 vs cheat sequence at 0x2833 (counter 0x283d); on full match, toggles 0x844/0x6045 and spawns vtable 0x287b/0x2892 | ### Menu / Event Callbacks | Address | Name | Description | |----------|---------------------------|-------------| | `0x2e53` | `cursor_event_notify_a` | Vtable thunk: forwards event to 0x27ca area handler | | `0x2e96` | `cursor_event_notify_b` | Vtable thunk: forwards event to 0x27ca area handler (alt path) | | `0x2ed9` | `menu_event_notify_a` | Vtable thunk: forwards event to 0x2843 (near menu object) | | `0x2f0c` | `menu_event_notify_b` | Vtable thunk: forwards event to 0x2843 (alt path) | | `0x2ff3` | `stub_noop_2ff3` | Empty stub, noop | | `0x2ff8` | `entity_collision_callback_a` | Calls touch handler then func(entity+0x1e, seg, 2); opt: extra func if param_3&1 | | `0x3046` | `set_active_menu` | Writes param_1/param_2 to 0x283f/0x2841 (active menu far pointer) | | `0x3058` | `entity_collision_callback_b` | Same as entity_collision_callback_a (second vtable entry) | ### Entity System (0x2401–0x5a50) | Address | Name | Description | |----------|------------------------------|-------------| | `0x2401` | `clear_cursor_selection` | Zeros 0x27c4/0x27c6 (selection counters) | | `0x2899` | `cursor_switch_target_entity`| Switches cursor target: unloads old entity, loads new, re-registers | | `0x29d8` | `get_z_offset` | Returns func() + *(0x27d2) = adjusted Z/height | | `0x2a09` | `is_player_in_range` | Checks if entity is at player (0x2de4) X/Y +/-0xf0 range | | `0x2a46` | `entity_ai_update_loop` | Loops entities 2–255, checks visibility, triggers fire/move | | `0x2c36` | `ui_update_callback` | Calls cursor_state_clear then vtable[2] on menu object | | `0x2c6b` | `cursor_state_clear` | Clears cursor state bytes 0x27ca–0x27ce, clears entity flag bit1 | | `0x2c92` | `dialog_spawn` | Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca | | `0x2d47` | `entity_pick_handler` | Handles entity selection or save-game trigger (type 0x38d) | | `0x2df9` | `clear_active_menu` | Zeros 0x283f/0x2841 (active menu far pointer) | | `0x2e18` | `game_mode_init` | Initializes game mode state, resets sprite/cursor/menu state | | `0x2f3f` | `entity_table_set_sprite` | Reads 0x7df9+slot*2; writes entity type table 0x7e1e[slot*0x79+0x0d]=param_2, +0x10=0 | | `0x3c97` | `snap_entity_to_ground` | If entity type in snap_type_ids[10], resets Z to 0xf0 and adjusts XY | | `0x3d6e` | `spawn_entity_checked` | Spawns entity with explosion pool limit check (0x84c0, 0x84c2) | | `0x3f2f` | `entity_spawn` | Allocates entity, vtable=0x29aa/0x39ca, positions it | | `0x40d4` | `entity_remove` | Removes entity: destroys sprites, clears 0x2802/0x2804 if needed | | `0x4172` | `entity_animation_frame_update`| Advances/retreats anim frame ([+0x1d]) toward target [+0x1c/0x1b] based on quality | | `0x42f8` | `stub_noop_42f8` | Empty stub, noop | | `0x42fd` | `entity_registry_decrement` | Calls cleanup func then decrements entity count at 0x290e | | `0x4314` | `entity_sprite_move_delta` | Updates shot sprite handle (entity+0x3f) position by adding delta params | | `0x4552` | `entity_set_position` | Sets entity+0x3e (type_handle), world_x/y (entity+0x45/47), base_x/y (entity+0x4f/51) | | `0x452b` | `shot_set_spawn_pos` | Calls entity_set_position then sets entity+0xbe = param_3 (extra spawn field) | | `0x4591` | `entity_try_place` | entity_set_position with validation — position only set if placement succeeds | | `0x5092` | `entity_deactivate` | Calls vtable[2] to deactivate, or finds in registry and removes | | `0x5a50` | `entity_list_contains` | Checks if entity ptr exists in active entity list at 0x294c | | `0x5b05` | `stub_noop_5b05` | Empty stub, noop | ### Entity Object Layout (NE Segment 1 entities) | Offset | Field | Meaning | |--------|-------|---------| | `+0x00` | vtable_ptr | Vtable pointer (0x29aa for generic, 0x2a57 for debris) | | `+0x02` | slot_index | Entity slot index (used for registry at 0x39ca) | | `+0x04` | entity_type | Entity type ID | | `+0x19`/`+0x1a` | flags | Entity flags (bit0=debris, bit1=cleared by cursor_state_clear, bit6=active, bit8=valid) | | `+0x1b` | vel_x | X velocity (clamped ±0x20) | | `+0x1c` | vel_y | Y velocity (clamped ±0x20) | | `+0x1d` | vel_z | Z velocity (clamped ±0x10) | | `+0x1e` | fire_handle | Weapon/fire handle | | `+0x1f` | is_enemy | 1 if entity is an enemy type | | `+0x20`/`+0x21` | pos_frac_x/y | Fractional position (sub-tile) for movement | | `+0x22` | pos_frac_z | Fractional Z | | `+0x36` | weapon_type | Active weapon type ID | | `+0x38` | facing | Current facing direction (0–15) | | `+0x3c` | sprite_handle | Sprite for this entity | | `+0x3f` | shot_sprite | Sprite handle for active projectile (0xFFFF = none) | | `+0x45`/`+0x47`/`+0x49` | world_x/y/z | Current world position (integer) | | `+0x4f`/`+0x51`/`+0x53` | base_x/y/z | Base/spawn position | | `+0x54`/`+0x56`/`+0x58` | prev_x/y/z | Previous frame position | | `+0x59` | attack_active | Attack in progress flag | | `+0x5a` | at_target | Reached target flag | | `+0x5e`–`+0x65` | delta_x/y/z/high | Per-step movement deltas (fixed point) | | `+0x66`/`+0x68` | step_active | Stepping active (1=yes, 0=off) | | `+0x6a`/`+0x6c` | weapon_slot/dist | Weapon slot and total travel distance | | `+0x6e` | delta_z | Alt Z delta | | `+0x70` | projectile_type | Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain) | | `+0x72`/`+0x74`/`+0x76` | target_x/y/z | Target position with deviation | | `+0x77` | target_entity | Target entity handle | | `+0x79` | secondary_pos | Secondary position struct pointer | | `+0xad` | owner_entity | Owning entity handle | | `+0xaf` | shot_owner_flags | Shot owner (entity/player) | | `+0xb1` | bounce_count | Bounce counter (used with homing, type 5) | | `+0xb3` | has_bounce | Has bounce trajectory active | | `+0xbd` | actor_type | Actor type byte (used for direction table lookups) | ### Shot Entity Lifecycle (0x435e–0x44a9) | Address | Name | Description | |----------|----------------------------|-------------| | `0x435e` | `shot_entity_alloc` | Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7 | | `0x44a9` | `shot_entity_free` | Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1 | ### Projectile / Combat (0x4659–0x5a99) | Address | Name | Description | |----------|----------------------------|-------------| | `0x4659` | `projectile_init_vector` | Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536 | | `0x4a91` | `entity_fire_weapon` | Fires weapon from entity using 0x129b/0x12ac direction offset tables | | `0x4b18` | `fire_weapon_from_cursor` | Gets cursor angle sprites, fires projectile at cursor target | | `0x4b78` | `projectile_check_hit` | Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity) | | `0x4c2e` | `projectile_step_update` | Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked | | `0x4d28` | `projectile_trace_ray` | Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate | | `0x51ad` | `projectile_update_tick` | Full projectile tick: move, check reach target, bounce, call projectile_check_hit | | `0x5a99` | `projectile_apply_hit` | Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner | ### Weapon Type Table (0x2536) - Each entry is 0x11 bytes (17), accessed as `weapon_type * 0x11` - `[0]` = step divisor for distance calculation - `[0x19]` = max range threshold (used in projectile_update_tick) ### Direction Tables (0x129b / 0x12ac) - Indexed by facing (0–15): dx offsets at 0x129b, dy offsets at 0x12ac - Values are multiplied by distance (e.g. `*0x500`) for projectile spawn offsets ### Collision Detection (0x60c1–0x621e) | Address | Name | Description | |----------|----------------|-------------| | `0x60c1` | `aabb_overlaps_3d` | 3D AABB overlap test — box layout [xmin,ymin,zmin,_,_,xmax,_,ymax,_,zmax] | | `0x621e` | `bbox_translate` | Translates a 3D bounding box by (dx, dy, dz) — both min and max points | ### Enemy AI / Spawning (0x6aed–0x6d21) | Address | Name | Description | |----------|----------------------------|-------------| | `0x6aed` | `map_find_spawn_point` | Finds map tile matching entity conditions; returns packed XYZ tile coords | | `0x6bfc` | `actor_find_in_view` | Finds actor visible in current view frustum (temp data at 0x7eca) | | `0x6ce9` | `enemy_spawn_with_target` | Wrapper: spawns enemy with player as target (param5=1) | | `0x6d05` | `enemy_spawn_no_target` | Wrapper: spawns enemy without targeting player (param5=0) | | `0x6d21` | `enemy_spawn_at_position` | Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A) | ### Player / HUD | Address | Name | Description | |----------|-------------------------------|-------------| | `0x50ee` | `player_position_update` | Updates player position from direction data; clamps to screen bounds | | `0x6ff7` | `player_health_update_and_effect` | Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect | ### Destruction / Death (0x7490–0x75ff) | Address | Name | Description | |----------|---------------|-------------| | `0x7490` | `debris_spawn`| Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list | | `0x75ff` | `entity_die` | Death handler: spawns 1–4 debris objects, picks best explosion direction | ### Entity Type Constants (weapon_type/entity class) | Value | Entity Class | |--------|--------------| | `0x17` | Robot/mech type A | | `0x18` | Robot/mech type B | | `0x1` through `0x3c` | Various entity/weapon types | | `0x3d` | Robot/mech type C | | `0x3e` | Robot/mech type D | | `0x2f5`–`0x2f7` | Special movement entity | | `0x595`/`0x597` | Platform/elevator entities | | `0x31c`/`0x322`–`0x327` | Explosive/effect entities | | `0x38d` | Save game trigger entity | | `0x426` | Spark/scatter sub-shot | | `0x59a` | Player cursor/select indicator | ### Entity Data Table at 0x7e1e - Stride: `0x79` bytes (121 bytes per entry) - Indexed by entity type (integer) or entity slot - `+0x5a` offset = flags byte (bit4 = special entity flag, bit3 = armor/shield flag) - `+0x68` = targeting flag ### Map / Resource Tables | Address | Content | |---------|---------| | `0x2833` | Cheat code input sequence (null-terminated) | | `0x283d` | Cheat sequence match position counter | | `0x7ded` | Map X coordinate array (2 bytes per entry) | | `0x7df1` | Map Y coordinate array (2 bytes per entry) | | `0x7df5` | Map Z array (1 byte per entry) | | `0x7df9` | Entity state array (2 bytes per slot) | | `0x7e46` | Player state block far pointer | | `0x7e1e` | Entity type table (stride 0x79) | ### Entity Vtable Index (NE Segment 1) | Address | Entity Class | |---------|-------------| | `0x28b5` | Dialog/menu object vtable | | `0x287b` | Cheat-spawned entity (cheat ON) vtable | | `0x2892` | Cheat-spawned entity (cheat OFF) vtable | | `0x2969` | Entity registry vtable (stored at 0x39ca+slot*4, not entity's own vtable) | | `0x297e` | Shot/projectile entity vtable | | `0x29aa` | Generic/AI entity vtable | | `0x2a1a` | Corpse entity vtable (variant) | | `0x2a33` | Actor/corpse entity vtable | | `0x2a57` | Debris fragment entity vtable | ## Next Steps 1. ✅ **NE Segment 1 imported and analyzed** — all 58 identified functions renamed and annotated 2. **Import additional NE segments** — priority: segments 22, 30, 59, 86 (segment 21 complete) 3. **Analyze additional segments** — apply same decompile→rename→annotate workflow 4. **Map file format loaders** — `.FLX`, `.SHP`, `.MAP`, `.TNT` resource formats 5. **Cross-reference entity type constants** with game entities (robots, platforms, triggers) 6. **Identify external segment calls** — the `func_0x0000ffff()` placeholders are all cross-segment calls; resolving them requires importing the referenced segments --- ## NE Segment 21 Analysis — Timer/Event Dispatch System **File**: `seg021_code_off_50200_len_4486.bin` | **File Offset**: 0x50200 | **Length**: 0x4486 bytes **Ghidra Load**: RAM `0000:0000 – 0000:4485`, x86 16-bit Protected Mode, base 0x0000 **Functions**: 88 total (87 renamed + `input_keyboard_handler` pre-existing) ### Subsystem Summary Segment 21 implements the **hardware-level timer interrupt and entity event dispatch system** — Crusader's real-time task scheduler. Key responsibilities: - Programs and services the Intel 8253 **PIT timer** (I/O ports 0x40/0x43) - Manages three **entity dispatch lists**: timer list (0x39d4), input list (0x39e3), render list (0x3a10) - Maintains the **entity pool** at 0x39b0 (same pool as seg001; these segments share DS) - Provides **event queue** (32-slot circular buffer at 0x31cc) - Handles **save/load** serialization of the entire entity system - Controls **keyboard/interrupt locks** and deferred scheduling ### Function Groups #### Entity Pool Management (0x0207–0x0483) | Address | Name | Notes | |---------|------|-------| | `0x0207` | `entity_count_by_type_a` | Count entities matching type+event; filters DEAD flag (0x8) | | `0x0297` | `entity_count_by_type_b` | Identical logic to 0x0207 (compiler duplicate) | | `0x0327` | `entity_find_free_slot` | Scan pool for null entry; calls panic if full; returns slot or 0xFFFF | | `0x038f` | `entity_register` | Write far ptr to entity_list, group to entity_data, vtable to registry; inc count | | `0x044d` | `entity_get_ptr_raw` | Read entity far ptr from pool slot (may be null) | | `0x0483` | `entity_get_ptr` | Safe wrapper: verifies non-null, returns offset only | #### Event Dispatch (0x04f3–0x08be) | Address | Name | Notes | |---------|------|-------| | `0x04f3` | `entity_dispatch_reset_all` | Fires event code 0x21 (reset/init) to all entities | | `0x050d` | `entity_clear_deferred_flags` | Clears DEFERRED bit (0x200) from up to N=0x3998 entities | | `0x059e` | `entity_fire_event_broadcast` | Dispatch event to all matching entities; calls vtable[6]; respects 0x200 deferred flag | | `0x06f4` | `entity_fire_event_type_include` | Fire only entities whose type IS in given list (up to 10, 0x0d=end) | | `0x08be` | `entity_fire_event_type_exclude` | Fire only entities whose type is NOT in given list | | `0x0a8e` | `input_keyboard_handler` | (pre-existing) OS-level key router: 0x0d=scroll+, 0x01=action, 0x2c=save, 0x44=load | #### Entity Iterator / Linker (0x0bb7–0x106b) | Address | Name | Notes | |---------|------|-------| | `0x0bb7` | `entity_link` | Cross-link two entities; skips if flag 0x400 set | | `0x0c34` | `entity_find_first` | Init iterator 0x39fa=3; find first entity matching saved type/event at 0x399a/0x399c | | `0x0cec` | `entity_find_next` | Continue iterator from 0x39fa cursor | | `0x0dad` | `timer_entity_find_by_event` | Find entity handling event in range 0xf0-0xf7; checks bit 0x1000; writes to 0x3993 | | `0x0e82` | `entity_find_by_priority` | Walk priority chain at 0x39d4; find entity matching source/event at 0x3993 | | `0x0fc8` | `entity_set_cursor` | Validate flag 0x800; set cursor 0x3993 = param_1 (slot) | | `0x100c` | `entity_get_cursor` | Return entity at 0x39bf if valid and not dead | | `0x106b` | `entity_relink` | Re-link: find by event, walk priority chain, call set-link vtable funcs | #### Entity Lifecycle (0x1133–0x131d) | Address | Name | Notes | |---------|------|-------| | `0x1133` | `entity_unregister` | Full removal: dec sprite type count, vtable cleanup, dec total, update masks | | `0x1202` | `entity_slot_clear` | Zero pool slot (0x39b0), registry slot (0x39ca), group data (0x39b4) | | `0x1245` | `entity_layer_set` | Write 0x39c9 (active layer ID) if changed; set dirty flag 0x39a2 | | `0x125d` | `entity_check_overdue` | If entity_is_overdue: set bit 0x40 on entity+0x16 | | `0x127c` | `entity_is_overdue` | Return 1 if entity index > 0x39bf and flag 0x39c2 set | | `0x129b` | `entity_list_call_update` | For all entities where entity+0x0e & param_3 != 0: call vtable[8] | | `0x131d` | `entity_set_pending` | Write param to 0x3995 (next entity to register); error if already set | #### Entity System Init/Shutdown (0x133e–0x1705) | Address | Name | Notes | |---------|------|-------| | `0x133e` | `entity_system_init` | Alloc all entity pool buffers (see decompiler comment); init three lists; clear event state | | `0x14bc` | `entity_system_flush_normal` | Finalize (vtable[10]) then free all non-deferred active entities | | `0x158d` | `entity_system_flush_deferred` | Same as flush_normal for deferred entities | | `0x165c` | `entity_process_pending_deletes` | Free entities marked DEAD (flag & 0x8); dec 0x399e counter | | `0x1705` | `entity_system_shutdown` | Full shutdown: flush normal, flush deferred, process deletes, free all pools | #### Save / Load (0x1851–0x1d21) | Address | Name | Notes | |---------|------|-------| | `0x1851` | `event_queue_state_reset` | Zero ring buffer state tables (0x334e, 0x364e), queue ptrs (0x31c8/0x31ca) | | `0x18ce` | `level_load` | Full level load: shutdown + reinit + deserialize all entities via vtable[12] | | `0x1d21` | `save_game` | Serialize entity system: arrays + each entity via vtable[14]; magic check 0x3a21==0xed | #### PIT Timer / Hardware (0x2300–0x2975) | Address | Name | Notes | |---------|------|-------| | `0x2300` | `pit_timer_program` | OUT 0x43, 0x36; OUT 0x40, lo; OUT 0x40, hi — raw PIT channel 0 program | | `0x2316` | `pit_timer_set_hz` | Validates divisor <= 0xd688; stores at 0x39ce; calls pit_timer_program | | `0x23a5` | `pit_timer_tick_handler` | Timer ISR: iterates 0x39d4 timer list, fires vtable callbacks per layer/mode | | `0x25fc` | `timer_entity_active` | Check 0x3987/0x398b for active timer entity (mode-dependent) | | `0x264c` | `timer_entity_get_current` | Get ptr from 0x3987 or 0x398b based on 0x3991 mode flag | | `0x2668` | `timer_entity_enable` | Set ENABLED flag (0x400), inc counter, insert into timer list, reprograms PIT | | `0x2745` | `timer_entity_disable` | Clear ENABLED, dec counter, reprograms PIT; if list empty calls interrupt_request_cancel | | `0x2975` | `timer_recompute_hz` | Scan timer list; find smallest time_period (+0x38/+0x3a); call pit_timer_set_hz | #### Interrupt / Lock Control (0x283a–0x294b) | Address | Name | Notes | |---------|------|-------| | `0x283a` | `interrupt_lock_acquire` | Re-entrant acquire on 0x31c7 (interrupt lock) | | `0x2870` | `interrupt_lock_release` | Release 0x31c7 | | `0x289b` | `entity_lock_acquire` | Re-entrant acquire on 0x39aa (entity system lock) | | `0x28d5` | `entity_lock_release` | Release 0x39aa | | `0x290d` | `interrupt_request_schedule` | Set deferred IRQ flags 0x39ab and 0x398f (or 0x39a9 in sync mode) | | `0x294b` | `interrupt_request_cancel` | Clear IRQ request flags | #### Timer Loop / Deferred State (0x2a5f–0x2ad8) | Address | Name | Notes | |---------|------|-------| | `0x2a5f` | `timer_event_loop` | **Main game loop**: polls player tick counter at 0x2de4; busy-waits; fires optional callback; stores delta to 0x3a00/0x3a02 | | `0x2ac2` | `timer_deferred_reschedule` | If deferred mode flag 0x39b8 set, call reschedule | | `0x2ad8` | `timer_snapshot_deferred` | Copy 0x39a9 → 0x39b8; call interrupt handler if 0x39a9 set | #### Event Queue (0x2c73–0x3364) | Address | Name | Notes | |---------|------|-------| | `0x2c73` | `event_queue_drain` | Drain circular queue; call event_queue_dequeue while 0x31c8 != 0x31ca; reset state | | `0x2ca2` | `mouse_button_check` | Return 1 if BIOS 0x31a4 bit 0x10 set AND 0x39af (mouse enable) set | | `0x2cbc` | `stub_noop_2cbc` | Empty stub function | | `0x2cd7` | `bios_keyboard_flags_write` | Write param to 0x400:0017 (BIOS keyboard flags at segment 0x40, offset 0x17) | | `0x2cf2` | `input_event_dispatch` | Dispatch display event 0x10 to input list entities with flag 0x100 and 0xc bits set | | `0x2dc3` | `event_queue_push` | Push event to circular queue (write ptr 0x31ca); calls event_queue_is_full check | | `0x3276` | `keyboard_state_read` | INT 16h AX=0: read raw keyboard state into 0x31a4 | | `0x328b` | `keyboard_acquire` | If not locked (0x31c6): INT lock, read keyboard, set lock flag | | `0x32cc` | `keyboard_release` | If locked: unlock, clear 0x31c6 | | `0x3304` | `event_queue_count` | Count pending events: 0x31ca - 0x31c8 (circular) | | `0x333d` | `event_queue_is_full` | Return 1 if ((0x31ca+1) mod 32) == 0x31c8 | | `0x3364` | `event_queue_dequeue` | Read from ring buffer (0x31cc + 0x31c8*0xc, entry size 0xc), advance read ptr | #### Event Subscription and Bitmask Helpers (0x34dd–0x3878) | Address | Name | Notes | |---------|------|-------| | `0x34dd` | `event_queue_process_all` | Drain queue; for each event find listener entities in 0x39e3; call vtable[0x14] | | `0x35e9` | `event_queue_set_mode` | Write low 2 bits to 0x334c; call keyboard_interrupt_call | | `0x35fb` | `event_queue_set_param` | Write low 5 bits to 0x334d; call keyboard_interrupt_call | | `0x360d` | `keyboard_interrupt_call` | INT 16h (raw BIOS keyboard services call) | | `0x3630` | `entity_validate_indices` | Debug assert: verify entity+0x02 (slot_index) == pool position for all entities | | `0x369b` | `typemask_set_bit` | Set bit at 0x3a04 + (param>>3), bit (param & 7) — entity type present bitmask | | `0x36d4` | `typemask_clear_bit` | Clear bit in 0x3a04 bitmask | | `0x370f` | `typemask_update` | If entity type has listeners (entity_find_first != 0): set bit; else clear | | `0x3744` | `typemask_test_bit` | Test bit in 0x3a04; return 1 if entity type has registered listeners | | `0x377d` | `event_subscription_set` | Set subscription bit in 0x3a08 buffer | | `0x37b2` | `event_subscription_clear` | Clear subscription bit in 0x3a08 buffer | | `0x37e9` | `event_subscription_update` | If entity has listeners: set bit; else clear (driven by entity_find_first result) | | `0x3825` | `event_subscription_test` | Test subscription bit in 0x3a08; return 1 if subscribed | | `0x3864` | `event_state_clear` | Zero entire 0x3a0c event use-count buffer (0x4000 bytes = 8192 uint16s) | | `0x3878` | `event_use_count_increment` | Increment 64-bit counter at 0x3a0c[entity_event_type*4] | #### Input / Render List (0x38c2–0x3ae9) | Address | Name | Notes | |---------|------|-------| | `0x38c2` | `input_event_broadcast` | Dispatch input event 0x40 to all render-list entities with flag 0x40; uses counter 0x39ad | | `0x39a1` | `subscribe_to_render_list` | Add entity to 0x3a10 list; set flag bit 0x40; inc 0x3a1f | | `0x3a13` | `unsubscribe_from_render_list` | Remove entity from 0x3a10; clear bit 0x40; dec 0x3a1f | | `0x3404` | `subscribe_to_input_list` | Add entity to 0x39e3 list; check flag 0x100; set bit 0x80; inc 0x39c3 | | `0x3477` | `unsubscribe_from_input_list` | Remove entity from 0x39e3; clear bit 0x80; dec 0x39c3 | | `0x3a78` | `entity_lists_init` | Init three linked lists with sentinel vtable 0x3a89; write head vtable 0x2d10 | | `0x3ae9` | `entity_lists_reset` | Call external reset + reinit 0x39e3 and 0x39d4 lists | ### Entity Object Field Layout (as used in Seg21) | Offset | Field | Type | Description | |--------|-------|------|-------------| | `+0x00` | vtable_ptr | far ptr | Pointer to entity's vtable dispatch table | | `+0x02` | slot_index | uint16 | Entity's own slot number in pool | | `+0x04` | source_type | uint16 | Source/owner entity type (event matching) | | `+0x06` | event_type | uint16 | Event type this entity handles | | `+0x08` | flags_byte | uint8 | Low 5 bits = sprite group ID | | `+0x0e` | capability_mask | uint16 | Bitmask of supported event capabilities | | `+0x16` | state_flags | uint16 | bit3=DEAD, bit8=REGISTERED, bit9=ACTIVE, bit10=ENABLED, bit11=HAS_TIMER, bit13=IS_IRQ_HANDLER | | `+0x18` | flags2 | uint16 | bit6=IN_RENDER_LIST, bit7=IN_INPUT_LIST, bit9=DEFERRED | | `+0x1e` | priority_chain | far ptr | Priority chain entries (entity_find_by_priority) | | `+0x20` | priority_count | uint16 | Count of priority chain entries | | `+0x38` | time_period_lo | uint16 | Timer period low word (PIT frequency calc) | | `+0x3a` | time_period_hi | uint16 | Timer period high word | ### Vtable Layout (Seg21 usage) | Slot | Byte offset | Prototype | Purpose | |------|-------------|-----------|---------| | [6] | `+0x0c` | `handle_event(entity, CS, type, param)` | Event callback | | [8] | `+0x10` | `update(entity, CS, capability_mask)` | Per-tick update | | [10] | `+0x14` | `finalize(entity, CS)` | Cleanup/shutdown | | [12] | `+0x18` | `load(entity, CS, file_ptr, CS)` | Deserialize from save | | [14] | `+0x1c` | `save(entity, CS, file_ptr, CS)` | Serialize to save | | [16] | `+0x20` | `set_backref(entity, CS, list_ptr)` | Set back-reference | | [20] | `+0x28` | `dispatch_callback(entity, CS, event_id, 0, data_ptr)` | Generic dispatch | ### Key Global Data (Seg21 — additions to DS) | Address | Name | Description | |---------|------|-------------| | `0x31a4` | bios_key_state | Raw INT 16h keyboard state | | `0x31c6` | keyboard_lock | Keyboard acquired flag | | `0x31c7` | interrupt_lock | Interrupt lock flag (re-entrant) | | `0x31c8` | queue_read_ptr | Event queue read index (0–31) | | `0x31ca` | queue_write_ptr | Event queue write index | | `0x31cc` | event_queue_base | Ring buffer, 32 entries × 0xc bytes | | `0x334c` | queue_mode | Event queue mode bits (0–1) | | `0x334d` | queue_param | Event queue param bits (0–4) | | `0x39b0` | entity_list | Far ptr to entity far-ptr array (count×4) — **shared with seg001** | | `0x39b4` | entity_data | Far ptr to group/sprite-ID array (count×2) | | `0x39b9` | entity_max_count | Max capacity of entity pool | | `0x39bb` | entity_count | Total registered entity count | | `0x39c9` | active_layer | Current active entity layer/group ID | | `0x39ca` | entity_registry | Far ptr to vtable dispatch array (count×4) — **shared with seg001** | | `0x39ce` | pit_divisor | Current PIT timer divisor | | `0x39d4` | timer_list | Intrusive linked list: timer-dispatch entities | | `0x39e3` | input_list | Intrusive linked list: input-handler entities | | `0x3a04` | typemask_buf | Far ptr to entity type present bitmask (0x480 bytes) | | `0x3a08` | evt_sub_buf | Far ptr to event subscription bitmask (0x2400 bytes) | | `0x3a0c` | evt_state_buf | Far ptr to event use-count table (0x4000 bytes) | | `0x3a10` | render_list | Intrusive linked list: render-callback entities | | `0x3a21` | save_magic | Must be 0xed (-0x13) for valid save | | `0x3a70` | default_registry_vtable | Default vtable written to entity_registry slots on register | | `0x3a89` | list_sentinel_vtable | Sentinel vtable written to list head nodes | ---