First commit

This commit is contained in:
Marco 2026-03-19 16:39:57 +01:00
commit b96aaf48c2
127 changed files with 990 additions and 0 deletions

View file

@ -0,0 +1,715 @@
# 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 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`
- 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.
- Helper at `000e:39cc` remains intentionally unnamed for now; disassembly shows it only activates when the current substring begins with `@`, then skips 7 bytes and dispatches through a thunk.
### 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` |
**Unresolved callee:**
- `000e:053d``000e:ffb0` (thin wrapper, ffb0 decompiles garbled due to overlapping instructions at `000f:0085`/`000f:0086`). Likely handles video frame loading to pair with `anim_load_audio_frame`. Not renamed.
**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
---
## 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 |
---