First commit
This commit is contained in:
commit
b96aaf48c2
127 changed files with 990 additions and 0 deletions
715
crusader_decompilation_notes.md
Normal file
715
crusader_decompilation_notes.md
Normal 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 (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 |
|
||||
|
||||
---
|
||||
Loading…
Add table
Add a link
Reference in a new issue