- Introduced `seg043_boundary_repair.json` to manage function boundaries in segment 043. - Created `read_file.py` for reading and printing file content size. - Added `resolve_bb4f.py` to resolve specific function call targets. - Implemented `resolve_top_targets.py` to find resolved NE targets for top-called wrapper functions. - Added `script_contents.txt` to summarize NE relocation far calls. - Updated `tier4_ghidra.txt`, `tier4_ghidra_check.txt`, `tier4_output.txt`, and `tier4_result.txt` with function call statistics. - Created `tier5_errors.txt` for error logging and `tier5_output.txt` for additional function call statistics. - Established `tools` directory with helper scripts for the Ghidra project, including CLI and common functionalities. - Implemented command-line interface in `cli.py` for various project operations. - Added `common.py` for shared functions and configurations across tools. - Introduced `validate_fixups.py` to validate NE relocation fixups against known addresses.
1852 lines
128 KiB
Markdown
1852 lines
128 KiB
Markdown
# Crusader: No Remorse - Decompilation Notes
|
||
|
||
## Binary Overview
|
||
|
||
- **Game**: Crusader: No Remorse (Origin Systems, 1995)
|
||
- **Platform**: DOS (16-bit protected mode)
|
||
- **DOS Extender**: Phar Lap 286 DOS-Extender (RUN286)
|
||
- **Executable Format**: Bound `MZ -> NE` executable with Phar Lap DOS-extender code
|
||
- **Entry Point**: `10da:7c40`
|
||
|
||
## Installed Copy Findings
|
||
|
||
- No standalone `.EXP` file exists in `F:\Apps\Crusader No Remorse`.
|
||
- `CRUSADER.EXE` is the original game binary and contains a valid internal `NE` header.
|
||
- Outer DOS `MZ` header points to `e_lfanew = 0x36F70`.
|
||
- Internal header at `0x36F70` starts with `NE` and describes **145 segments**.
|
||
- The NE segment table references data from the original file directly, so there is no separate embedded payload that needs to be carved out first.
|
||
- `CNRCEXP.EXE` is a modern Win32 helper tool, not part of the original DOS execution path.
|
||
|
||
## Raw Full-EXE Import Mapping
|
||
|
||
- A separate raw-binary import of the full executable (`crusader-raw.exe`) is usable: Ghidra discovers thousands of functions across a single flat `ram` block.
|
||
- Direct `file_offset -> flat_address` mapping from the standalone segment extracts is not reliable for porting names into that raw import.
|
||
- The extracted `segNNN_*.bin` files match `CRUSADER_NE.EXE`, but the raw full-EXE import must be mapped by verified byte signatures / known function bodies.
|
||
- Verified segment bases in the raw full-EXE import:
|
||
- `seg001` base = `0x6E570` (`cursor_update_hover` at `0006:e5d0`, rel `0x0060`)
|
||
- `seg021` base = `0x87170` (`entity_count_by_type_a` at `0008:7377`, rel `0x0207`)
|
||
- Porting rule for these verified segments:
|
||
- `raw_full_exe_flat = verified_segment_base + standalone_segment_relative_offset`
|
||
- Naming note:
|
||
- `seg001` and `seg021` both contain a keyboard handler; in the full program database, the seg001 copy is named `seg001_input_keyboard_handler` to avoid a symbol collision with seg021 `input_keyboard_handler`.
|
||
|
||
### Address Space Layout in the Raw Import
|
||
|
||
Ghidra segment:offset `SSSS:OOOO` = flat address `SSSS * 0x10000 + OOOO`.
|
||
|
||
| Flat range | Content |
|
||
|---|---|
|
||
| `0x00000`–`0x36F6F` | Phar Lap 286 DOS extender (outer MZ stub code) |
|
||
| `0x36F70` | NE header (145-segment game image begins here in file) |
|
||
| `0x6E570`+ | NE game segments at their Phar Lap linear load addresses |
|
||
|
||
Mapping rule (verified for seg001 and seg021):
|
||
```
|
||
runtime_flat_base = NE_segment_file_offset + 0x36F70
|
||
```
|
||
Example: seg004 at file `0x40A00` → runtime `0x77970` → Ghidra `0007:7970`.
|
||
|
||
Functions at Ghidra `0003:XXXX` / `0004:XXXX` are **Phar Lap extender code** (flat < `0x40000` is below any game segment). Functions at `0006:E570`+ are game NE segments.
|
||
|
||
### `0000:ffff` — NE Fixup Placeholder (not a dispatcher)
|
||
|
||
`unresolved_far_thunk_dispatch` at `0000:ffff` is NOT a runtime function. Every `CALLF 0x0000:ffff` in the binary is a **different** external or inter-segment call patched by the NE loader at runtime. The decompiler body is garbled (it reads NE fixup-chain sentinel data). Decompiler comment added in Ghidra. See individual call sites for per-site behavioral annotations.
|
||
|
||
Known call-site classifications (by argument pattern):
|
||
- `PUSH DS; PUSH imm_ordinal; CALLF` — Phar Lap extender calling a runtime-imported procedure by ordinal
|
||
- `PUSH ptr_seg; PUSH ptr_off; CALLF` — inter-NE-segment function call (intra-game far call)
|
||
- Multiple typed pushes then CALLF — external C runtime / game subsystem call with normal args
|
||
|
||
### Latest Raw Full-EXE Porting Progress
|
||
|
||
- Newly ported and renamed into `CRUSADER-RAW.EXE` from verified `seg001` mapping (`base 0x6E570`):
|
||
- `0007:28ce` = `shot_entity_alloc` (`seg001 + 0x435e`)
|
||
- `0007:2a19` = `shot_entity_free` (`seg001 + 0x44a9`)
|
||
- `0007:2bc9` = `projectile_init_vector` (`seg001 + 0x4659`)
|
||
- `0007:3001` = `entity_fire_weapon` (`seg001 + 0x4a91`)
|
||
- `0007:3088` = `fire_weapon_from_cursor` (`seg001 + 0x4b18`)
|
||
- `0007:30e8` = `projectile_check_hit` (`seg001 + 0x4b78`)
|
||
- `0007:319e` = `projectile_step_update` (`seg001 + 0x4c2e`)
|
||
- `0007:3298` = `projectile_trace_ray` (`seg001 + 0x4d28`)
|
||
- `0007:371d` = `projectile_update_tick` (`seg001 + 0x51ad`)
|
||
- `0007:4009` = `projectile_apply_hit` (`seg001 + 0x5a99`)
|
||
- Decompiler comments were added on key raw-import projectile functions to preserve provenance for later passes.
|
||
- Quick verification from current raw import:
|
||
- `entity_fire_weapon` currently decompiles as a thin wrapper that calls `projectile_init_vector`.
|
||
- `fire_weapon_from_cursor` still decompiles poorly in the raw import, but disassembly shows it begins by pushing cursor sprite/state data from the `0x27d6` area, consistent with the existing seg001 notes.
|
||
|
||
### Raw 0007 Gameplay Helper Batch (entity/tile aux state)
|
||
|
||
- New conservative gameplay-side helper renames (direct analysis from field writes and call structure):
|
||
- `0007:85f6` = `entity_sync_tile_aux_state`
|
||
- `0007:8865` = `entity_sync_tile_aux_if_linked`
|
||
- `0007:8709` = `entity_mark_dirty_and_sync_tile_aux`
|
||
- Current verified behavior:
|
||
- `entity_sync_tile_aux_state` reads entity tile index at `+0x4`, toggles bit `0x04` in tile record `+0x59` based on entity byte `+0x54`, and copies entity word `+0x55` into tile record `+0x0d`.
|
||
- `entity_sync_tile_aux_if_linked` only performs the sync when entity link/pointer `+0x50/+0x52` is non-null.
|
||
- `entity_mark_dirty_and_sync_tile_aux` calls the linked-sync helper, sets entity flag bit `0x04` at `+0x42`, then calls through `0000:ffff` with args `(SS:&tile_index, entity[+0x57])` — annotated at `0007:8666` as `entity_tile_type_notify(tile_index_ptr, type_byte)`.
|
||
- New entity field found this pass:
|
||
- `entity[+0x57]` (byte) = entity type/class byte (passed to tile-type notification; meaning not yet fully established — adjacent to named fields `+0x54`/`+0x55`)
|
||
|
||
### Raw 0007 Gameplay Helper Batch (facing/direction)
|
||
|
||
- New gameplay helper rename (direct analysis):
|
||
- `0007:8bd9` = `entity_set_facing_direction`
|
||
- Current verified behavior:
|
||
- Updates entity facing byte `+0x38` using incoming direction/event code values (notably `0x10/0x11/0x12`) with parity-aware adjustment.
|
||
- Uses entity flags at `+0x4d` to select increment/decrement behavior for clockwise/counterclockwise facing updates.
|
||
- Called from the large gameplay update state machine at `0007:5b9a` inside `FUN_0007_5b6f`.
|
||
|
||
### Raw 0007 Gameplay Helper Deep Dive: snap_entity_to_ground
|
||
|
||
- Function: `0007:2207` = `snap_entity_to_ground`
|
||
- Caller in gameplay flow: `spawn_entity_checked` (`0007:22de`, call at `0007:2366`)
|
||
- Purpose (high confidence): pre-spawn position adjustment for a small allow-list of entity types so they land on valid ground/height context before normal spawn allocation.
|
||
|
||
#### Variable replacement pass (applied in Ghidra)
|
||
|
||
- `param_1` -> `entity_type`
|
||
- `local_48` -> `snap_entity_type_table`
|
||
- `local_34` -> `snap_dispatch_seg_table`
|
||
- `local_20` -> `snap_dispatch_off_table`
|
||
- `local_c` -> `entity_type_cursor`
|
||
- `local_4` -> `dispatch_index`
|
||
|
||
#### What the function does structurally
|
||
|
||
1. Copies three 10-entry static tables into stack-local scratch buffers:
|
||
- from `0x2910` into `snap_dispatch_off_table`
|
||
- from `0x2924` into `snap_dispatch_seg_table`
|
||
- from `0x2938` into `snap_entity_type_table`
|
||
2. Performs a linear scan across 10 entity IDs in `snap_entity_type_table`.
|
||
3. If `entity_type` matches an entry, it calls into the unresolved shared FAR thunk target (`0000:ffff`) with spawn coordinate-derived arguments.
|
||
4. If no table entry matches, it exits without modifying the request.
|
||
|
||
#### Why this is "snap to ground" behavior
|
||
|
||
- The only known caller (`spawn_entity_checked`) gates this function behind:
|
||
- global mode flag `*(char *)0x27fe != 0`
|
||
- a hardcoded list of exactly 10 entity IDs (`0x31c`, `0x31f`, `0x320`, `0x321`, `0x322`, `0x323`, `0x324`, `0x325`, `0x326`, `0x327`)
|
||
- That caller prepares local spawn position values, calls `snap_entity_to_ground`, then proceeds to spawn logic. This pattern strongly indicates pre-placement correction rather than generic AI or render logic.
|
||
- The segment/offset companion tables strongly suggest per-entity handler dispatch metadata (or per-entity parameter blocks) used by the thunked path.
|
||
|
||
#### Current limitation in raw import
|
||
|
||
- The common thunk endpoint is still imported as `unresolved_far_thunk_dispatch` at `0000:ffff`.
|
||
- In this raw database, that body decompiles as overlapped/bad instruction data, so exact arithmetic internals of the final coordinate projection cannot yet be recovered from this symbol alone.
|
||
- Despite that, caller context + table shape + argument flow make the gameplay role of this helper clear enough for naming and control-flow analysis.
|
||
|
||
#### Working pseudocode (behavioral)
|
||
|
||
```c
|
||
void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
|
||
copy_10_words(local_off_table, DATA_2910);
|
||
copy_10_words(local_seg_table, DATA_2924);
|
||
copy_10_words(local_type_table, DATA_2938);
|
||
|
||
for (dispatch_index = 0; dispatch_index < 10; dispatch_index++) {
|
||
if (local_type_table[dispatch_index] == entity_type) {
|
||
// Through unresolved FAR thunk in raw import.
|
||
// Uses spawn position context to compute a ground-aligned placement.
|
||
call_thunk_with_spawn_context(spawn_x, spawn_y, ...);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Architectural Resolution: `unresolved_far_thunk_dispatch` / `0000:ffff`
|
||
|
||
**`unresolved_far_thunk_dispatch` is NOT a real dispatcher.** It is the NE binary fixup placeholder.
|
||
|
||
- In a Phar Lap 286 NE executable, inter-segment and external far calls are stored in the binary as `CALLF 0x0000:ffff` (or similar invalid sentinel values).
|
||
- The Phar Lap NE loader patches each of these call sites to the real segment:offset at load time using the per-segment relocation records in the NE file.
|
||
- In Ghidra's raw import, those fixups are never applied. Every unresolved far call collapses to the same `0000:ffff` stub, where the decompiler produces garbled output (it's reading fixup-chain data, not real instructions).
|
||
- **Each `CALLF 0x0000:ffff` in the binary is a DIFFERENT call with a DIFFERENT actual target.** Identifying the target requires either parsing the NE relocation table or cross-matching with the resolved standalone segment extracts.
|
||
|
||
Address layout in the raw import (flat_address = `SSSS:OOOO` where flat = `SSSS * 0x10000 + OOOO`):
|
||
- `0000:` – `0003:` (flat < `0x40000`) = Phar Lap 286 DOS extender code (the outer MZ stub portion)
|
||
- `0006:E570` onwards = NE game segments (seg001+ at their Phar Lap-assigned linear addresses)
|
||
- Mapping rule verified: `runtime_flat = NE_segment_file_offset + 0x36F70` (the NE header offset in the EXE)
|
||
|
||
Decompiler comment added to `0000:ffff` in Ghidra documenting this.
|
||
|
||
#### Next RE targets for `snap_entity_to_ground`
|
||
|
||
- The `0007:224b` thunk call is an intra-NE inter-segment call (calling into a different game segment with ground-aligned coordinate math). Identifying it requires the NE relocation table or matching the disassembly in the standalone extracts.
|
||
|
||
### Raw 0007 Gameplay Helper Follow-up: AI sweep + checked spawn path
|
||
|
||
- Additional gameplay-side annotation pass completed directly in `CRUSADER-RAW.EXE`.
|
||
|
||
#### `spawn_entity_checked` (`0007:22de`) refinements
|
||
|
||
- Function signature was expanded to 7 arguments in Ghidra so stack arguments remain visible in decompile:
|
||
- `entity_type`, `spawn_flags_a`, `spawn_flags_b`, `spawn_flags_c`, `spawn_x`, `spawn_y`, `spawn_layer_arg`
|
||
- Parameter/local naming pass applied for readability in decompiled output.
|
||
- New comments added at key control-flow points:
|
||
- `0007:22f8`: allow-list gate for ground-snap mode (`0x27fe != 0` + entity IDs `0x31c..0x327` subset)
|
||
- `0007:2366`: explicit `snap_entity_to_ground(entity_type, &spawn_x, &spawn_y, &spawn_layer)` handoff
|
||
- `0007:247e`: fallback path that calls core `entity_spawn` with original arguments
|
||
- Current caveat:
|
||
- Decompiler still aliases the temporary y/layer scratch region imperfectly around the thunked call site, but disassembly confirms the call setup uses local `x/y/layer` temporaries (`[bp-6]`, `[bp-8]`, `[bp-9]`) before spawn.
|
||
|
||
#### `entity_ai_update_loop` (`0007:0fb6`) structural recovery
|
||
|
||
- Added disassembly + decompiler comments capturing stable behavior:
|
||
- Reads player entity FAR pointer from global `0x2de4`.
|
||
- Copies player world position fields (`+0x40`, `+0x42`) into globals `0x27e7` / `0x27e9` (AI focus position cache used by downstream logic).
|
||
- Iterates entity IDs from `2` through `255` and dispatches per-entity processing through two sequential thunked calls per entity.
|
||
- New disassembly comments added at both dispatch call sites:
|
||
- `0007:101c`: `entity_slot_fetch(SS:&entity_id)` — first call; resolves entity slot/pointer from loop ID
|
||
- `0007:1093`: `entity_tick_dispatch(SS:&entity_id, g_0x27c8)` — second call; per-entity AI tick with global `0x27c8` mode/context word
|
||
- Global `0x27c8` is now confirmed as the current targeted/current entity handle: `entity_is_type_match` compares against it directly, and both spawn helpers `map_find_spawn_point` / `enemy_spawn_at_position` snapshot it before their thunked core paths.
|
||
|
||
### Raw 0007 Gameplay Logic: animation / range / command globals
|
||
|
||
#### `is_player_in_range` (`0007:0f79`) — fully recoverable
|
||
|
||
- Prototype updated in Ghidra: `int is_player_in_range(int entity_x, int entity_y)`
|
||
- Reads player world position from `g_player_entity_farptr` (`0x2de4`, fields `+0x40` (x) and `+0x42` (y)`.
|
||
- Computes unsigned delta from AI focus globals `g_ai_focus_pos_x` (`0x27e7`) / `g_ai_focus_pos_y` (`0x27e9`).
|
||
- Returns 1 if player Y delta == 0 AND player X delta < 0xF0 (240 world units), else 0.
|
||
- Only confirmed caller so far: `0007:0bcb` (in unanalyzed function region).
|
||
|
||
#### `entity_animation_frame_update` (`0007:26e2`) — fully decompiled
|
||
|
||
- Prototype updated: `void entity_animation_frame_update(int *entity_ptr)`
|
||
- Key globals read:
|
||
- `g_anim_tick_counter` (`0x3a00`) — frame timing tick counter.
|
||
- `g_anim_tick_overdrive_flag` (`0x3a02`) — if set, forces max-advance (4 steps).
|
||
- `g_speed_double_flag` (`0x27fd`) — doubles speed_factor to 2 when set (fast game mode).
|
||
- Local variables renamed: `speed_factor` (1 or 2) and `advance_steps` (0–4, number of frame advances this tick).
|
||
- Entity struct fields confirmed (relative to `entity_ptr` as `int*`):
|
||
- `[0x1b]` (byte `+0x36`) = frame_min (backward direction counter)
|
||
- `[0x1c]` (byte `+0x38`) = frame_max
|
||
- `[0x1d]` (byte `+0x3a`) = current_frame
|
||
- `[0x1e]` (byte `+0x3c`) = loop_flag (0 = animation disabled)
|
||
- `[0x1f]` (byte `+0x3e`) = reverse_direction_flag / double-speed flag
|
||
- `+0x3f` (word, byte-offset) = completion handle/sentinel (`-1` = none, `0x2802` = player entity)
|
||
- `+0x00` (far ptr) = vtable pointer
|
||
- New disassembly comments added at all three `CALLF 0x0000:ffff` sites and the vtable indirect call:
|
||
- `0007:27dc`: `entity_completion_callback(handle)` — fires when loop wraps; skips player handle
|
||
- `0007:27fd`: vtable indirect `entity->vtable[+8](entity, 0, 0)` — `on_loop_complete` virtual method
|
||
- `0007:281e`: `notify_frame_progress(handle, current_frame)` — per-frame notification
|
||
- `0007:2851`: `entity_sprite_advance(entity_far_ptr, advance_amount, 0)` — core frame-advance call; advance_amount = `entity[+0x3c] * (steps+1) * speed_factor`
|
||
|
||
#### `entity_command_dispatch` (`0007:0990`) — partially decompiled
|
||
|
||
- Prototype: `void entity_command_dispatch(int entity_handle, int target_seg, int command_type, byte absolute_pos_flag)`
|
||
- When `absolute_pos_flag == 0`: computes player-relative delta using `g_player_entity_farptr` and stores result:
|
||
- delta_x → `g_player_delta_x` (`0x27f5`)
|
||
- delta_y → `g_player_delta_y` (`0x27f7`)
|
||
- Clears cached origin globals `g_cmd_effect_origin_x` (`0x27f1`) and `g_cmd_effect_origin_y` (`0x27f3`) after use.
|
||
- Dispatches entity command through shared thunk; actual command table data not yet resolved.
|
||
- No incoming XREFs found in the raw import (likely called via table or vtable dispatch).
|
||
|
||
#### Enemy spawn helper cluster (`0007:505d`, `0007:5259`, `0007:5275`, `0007:5291`)
|
||
|
||
- Existing raw names align with prior standalone seg001 notes:
|
||
- `0007:505d` = `map_find_spawn_point` (`seg001 + 0x6aed`)
|
||
- `0007:5259` = `enemy_spawn_with_target` (`seg001 + 0x6ce9`)
|
||
- `0007:5275` = `enemy_spawn_no_target` (`seg001 + 0x6d05`)
|
||
- `0007:5291` = `enemy_spawn_at_position` (`seg001 + 0x6d21`)
|
||
- Current verified raw-import behavior:
|
||
- `enemy_spawn_with_target` is a thin wrapper over `enemy_spawn_at_position(..., target_player_flag = 1)`.
|
||
- `enemy_spawn_no_target` is the same wrapper but passes `target_player_flag = 0`.
|
||
- `map_find_spawn_point` and `enemy_spawn_at_position` both copy DS:`0x27c8` into locals before entering their unresolved thunk body, matching the standalone notes that treat `0x27c8` as the current targeted/current entity handle.
|
||
- Short decompiler comments were added in Ghidra on the raw spawn helpers to preserve this provenance.
|
||
|
||
#### Global map additions (renamed in Ghidra)
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0x27c8` | `g_current_entity_handle` | Compared directly by `entity_is_type_match`; also captured by `entity_ai_update_loop`, `map_find_spawn_point`, and `enemy_spawn_at_position` as the current targeted/current entity handle |
|
||
| `0x2de4` | `g_player_entity_farptr` | FAR ptr to player entity; `+0x40`/`+0x42` are world X/Y |
|
||
| `0x27e7` | `g_ai_focus_pos_x` | Set by `entity_ai_update_loop` from player entity `+0x40` |
|
||
| `0x27e9` | `g_ai_focus_pos_y` | Set by `entity_ai_update_loop` from player entity `+0x42` |
|
||
| `0x27f1` | `g_cmd_effect_origin_x` | Cached effect origin X, cleared after delta in `entity_command_dispatch` |
|
||
| `0x27f3` | `g_cmd_effect_origin_y` | Cached effect origin Y |
|
||
| `0x27f5` | `g_player_delta_x` | Player X delta from last effect origin |
|
||
| `0x27f7` | `g_player_delta_y` | Player Y delta from last effect origin |
|
||
| `0x27fd` | `g_speed_double_flag` | 0 = normal, 1 = double speed animation |
|
||
| `0x27fe` | `g_ground_snap_mode_flag` | Non-zero = ground-snap prepass active for placements |
|
||
| `0x27d0` | `g_entity_update_max_id` | Max entity ID used by `entity_ai_update_loop` sweep |
|
||
| `0x3a00` | `g_anim_tick_counter` | Animation tick counter for frame-advance step budget |
|
||
| `0x3a02` | `g_anim_tick_overdrive_flag` | 0 = normal, non-zero = force max frame advance step |
|
||
| `0x2802` | `g_player_entity_handle` | Player entity handle (used as sentinel in animation completion checks) |
|
||
|
||
### Raw 000e Parser Helper Cluster
|
||
|
||
- A small helper cluster in the raw `000e:` area now appears to implement a fixed-size CRLF record parser/table builder, likely used by startup/config or script-ish text data.
|
||
- Newly renamed helpers:
|
||
- `000e:345e` = `record_table_init`
|
||
- `000e:34cc` = `record_table_destroy`
|
||
- `000e:35c6` = `record_table_release_buffer`
|
||
- `000e:35ef` = `record_table_next_slot`
|
||
- `000e:3639` = `record_table_parse_buffer`
|
||
- `000e:3798` = `record_parser_read_line`
|
||
- `000e:38a0` = `record_parser_seek_next_marker`
|
||
- `000e:38f8` = `record_parser_find_marker`
|
||
- `000e:39cc` = `record_parser_dispatch_at_directive`
|
||
- Current behavior read from raw-import decompilation/disassembly:
|
||
- `record_table_init` clears the table header and zeroes 300 words of inline storage.
|
||
- `record_table_parse_buffer` walks a CRLF-separated text buffer, captures each line, splits around a marker helper path, and stores parsed entry state into 0x0c-byte records.
|
||
- `record_parser_read_line` advances to the next CRLF-delimited line, rejects lines that start with `@` or with non-identifier punctuation, and terminates the line in-place with `0`.
|
||
- `record_parser_seek_next_marker` updates the parser's current marker cursor at `+0x18/+0x1a` by calling `record_parser_find_marker`; returns 1 if another marker was found, 0 at end-of-data.
|
||
- `record_parser_find_marker` scans forward until an `@` marker or end-of-data; optionally consumes the remaining length from the parser state.
|
||
- `record_parser_dispatch_at_directive` returns `0` unless the current substring begins with `@`; in the `@` case, it advances by 7 bytes and dispatches through a FAR thunk (`0000:ffff`).
|
||
|
||
### Raw 000e RIFF/Animation Cluster
|
||
|
||
The `000e:` segment contains a RIFF/AVI streaming animation subsystem. Animation objects have a confirmed field layout (offsets relative to the object base pointer).
|
||
|
||
**Animation object field map:**
|
||
- `+0xb0` = active/valid flag
|
||
- `+0xb4`, `+0xb6`, `+0xb8`, `+0xba`, `+0xbc`, `+0xbe`, `+0xc0`, `+0xc2` = constructor-initialized flags
|
||
- `+0xd4` = alive sentinel (must be `-1` for "alive")
|
||
- `+0xe4` = paused flag (0 = running)
|
||
- `+0xeaf` / `+0xeb1` = far pointer to current RIFF chunk
|
||
- `+0xedb` = animation frame stack depth counter (max 9)
|
||
- `+0xee1` = frame data from current chunk `+4`
|
||
- `+0xeef` = current subframe index
|
||
- `+0x1b3` = subframe count
|
||
- `+0xef1` = audio completion flag
|
||
- `+0x11b` = ring buffer write pointer
|
||
- `+0x11f` = ring buffer read pointer
|
||
- `+0x117` = ring buffer base
|
||
- `+0x123` = ring buffer end (capacity boundary)
|
||
- `+0x102` = resource pointer
|
||
- `+0xde` = some entry index (multiplied by `0x30` to reach per-entry data at `+0x1c7`)
|
||
|
||
**RIFF format notes:** Game uses standard RIFF/IFF: LIST and RIFF header magic (`0x5453494c` = `"LIST"`, `0x46464952` = `"RIFF"`), `"movi"` FourCC subchunk for frames. Audio frames tagged `"01wb"` (`0x62773130`), video frames in a separate path.
|
||
|
||
**Newly renamed functions:**
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000e:2a28` | `riff_find_chunk_by_type` | Walks RIFF LIST/RIFF chunk list; compares each node's FourCC at `+8` vs `param_2`; returns pointer to matching chunk or NULL |
|
||
| `000e:2104` | `animation_start` | Finds `"movi"` chunk via `riff_find_chunk_by_type`, inits ring buffer ptrs at `+0x11b` from `+0x117 + duration`, calls `animation_advance_frame`, loops `anim_load_audio_frame` and a second frame-loader thunk path per subframe |
|
||
| `000e:12f4` | `animation_advance_frame` | Fixed-point `0x1000` timer arithmetic; checks `+0xe4` (paused), advances ring buffer `+0x11b`/`+0x11f`/`+0x117`/`+0x123`; calls advance thunk |
|
||
| `000e:103f` | `animation_tick` | Guard wrapper: checks `param_1+0xd4 != -1`, then calls `animation_advance_frame(param_1, 0)` |
|
||
| `000e:06f7` | `anim_load_audio_frame` | Checks chunk tag == `0x62773130` (`"01wb"` = audio stream 1); computes ring buffer free space; copies chunk payload via `0x0000:ffff` thunk; increments subframe index at `+0xeef`; resets at subframe count `+0x1b3` |
|
||
| `000e:053d` | `anim_load_video_frame_wrapper` | Called once per subframe in `animation_start` immediately after `anim_load_audio_frame`; thin wrapper that forwards to `000e:ffb0` |
|
||
|
||
**Unresolved callee:**
|
||
- `000e:ffb0` remains unresolved (decompiles garbled due to overlapping instructions at `000f:0085`/`000f:0086`). Current evidence from `animation_start` loop suggests this path is the video-side subframe loader paired with `anim_load_audio_frame`.
|
||
|
||
**Constructor pattern (`000e:2777`, `000e:2860`, `000e:2969`):**
|
||
All three follow the same layout:
|
||
1. Call `FUN_000e_e935` (allocator — produces garbled 11KB decompile, not renamed)
|
||
2. Set fields `+0xb4` through `+0xc2` on the result
|
||
3. Call `000d:ebe3` (multi-step chain initializer: calls `177c`, `1acb`, `0988`, `22bc`, `1d4a`, `2104` in sequence)
|
||
4. Call `assert_alive_sentinel` (assertion: checks `+0xd4 != -1`)
|
||
5. Call `func_0x000eec83`
|
||
|
||
The chain at `000d:ebe3` steps through VM opcode handlers (`000d:177c`, `000d:1acb`, `000d:0988`) that operate on a bytecode VM object with stack pointer at `+0xcc` (decremented by 2 per push) and segment base at `+0xce`.
|
||
|
||
**Constructor variant renames (direct analysis):**
|
||
- `000e:223d` = `assert_alive_sentinel`
|
||
- `000e:2777` = `animation_ctor_variant_a`
|
||
- `000e:2860` = `animation_ctor_variant_b`
|
||
- `000e:2969` = `animation_ctor_variant_c`
|
||
|
||
## Segment Map
|
||
|
||
| Segment | Address Range | Purpose |
|
||
|---------|--------------|---------|
|
||
| CODE_0 | `1000:0000 - 1000:01ff` | Interrupt dispatch table / thunks |
|
||
| CODE_1 | `1020:0000 - 1020:0b9f` | Low-level interrupt handlers, mode switching |
|
||
| CODE_2 | `10da:0000 - 10da:25ef` | **Main runtime** — C library, I/O, formatting, entry point |
|
||
| CODE_3 | `1339:0000 - 1339:0c2f` | **DOS/DPMI services** — INT 21h/31h wrappers, interrupt vector mgmt, fast memcpy |
|
||
| CODE_4 | `13fc:0000 - 13fc:27af` | **String data & runtime constants** — error messages, format strings, Phar Lap ID |
|
||
| CODE_5 | `1677:0000 - 1677:0e8f` | **EMS/XMS memory management** — expanded memory handlers |
|
||
| CODE_6 | `1760:0000 - 1760:7ccd` | **DOS Extender core** — EXP loader, command-line parser, memory management, system init |
|
||
| DATA | `1760:7cd0 - 1760:7cdf` | Global data |
|
||
| HEADER | `HEADER::0000 - HEADER::044f` | MZ/P2 file header |
|
||
|
||
## Named Functions
|
||
|
||
### Entry & Startup
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `10da:7c40` | `entry` | Program entry point — checks CPU, parses command line, launches game |
|
||
| `10da:1816` | `main_init_and_run` | Main initialization — loads child EXP, sets up subsystems, runs game |
|
||
| `1760:1432` | `parse_cmdline_and_run` | Parses command-line args and invokes main_init_and_run |
|
||
| `1760:42fa` | `init_dos_extender` | Initializes Phar Lap 286 DOS extender (CPU check, VCPI/DPMI setup) |
|
||
|
||
### Executable Loading
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `1760:2cdf` | `load_exp_file` | Loads .EXP executable — opens file, reads headers, allocates memory |
|
||
| `1760:1dfc` | `load_executable_image` | Parses P2/MZ headers, loads segments, creates LDT entries |
|
||
| `1760:24a6` | `apply_relocations` | Applies segment relocations to loaded executable |
|
||
| `1760:5eca` | `exec_child_process` | Executes child process with command-line arguments |
|
||
| `1760:5fee` | `exec_program_with_args` | Builds command line, locates and executes a program |
|
||
| `10da:1f7e` | `load_and_run_child` | Wrapper: loads child EXP and initializes it |
|
||
|
||
### System Services
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `10da:2330` | `dos_exit` | Calls INT 21h AH=4Ch (terminate program) |
|
||
| `1760:42aa` | `detect_cpu_type` | Detects CPU: 0=8086, 2=286, 3=386+ |
|
||
| `1339:04a6` | `dpmi_set_interrupt_vector` | INT 31h — DPMI set interrupt vector |
|
||
| `1339:06ca` | `switch_to_real_mode` | Switches CPU from protected to real mode |
|
||
| `1339:06f2` | `switch_to_protected_mode` | Switches CPU from real to protected mode |
|
||
| `1339:0076` | `setup_interrupt_handlers` | Configures interrupt vectors via INT 21h |
|
||
| `1339:0a38` | `dos_int21h_wrapper` | Simple INT 21h call wrapper |
|
||
| `1339:0a82` | `dos_int21h_with_regs` | INT 21h call with register parameters |
|
||
| `10da:2360` | `get_flags_register` | Returns CPU FLAGS register |
|
||
| `10da:2363` | `set_flags_register` | Sets CPU FLAGS register |
|
||
|
||
### Memory Management
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `1677:0d12` | `cleanup_ems_memory` | Frees EMS (INT 67h) memory handles |
|
||
| `10da:14fc` | `init_stack_fill_cc` | Fills stack with 0xCC (INT 3) for debugging/guard |
|
||
| `10da:1706` | `get_segment_base_addr` | Computes linear base address from segment descriptor |
|
||
|
||
### Task Management
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `10da:19ca` | `task_switch_to_child` | Context switch to child process |
|
||
| `10da:1946` | `task_switch_from_child` | Context switch back from child process |
|
||
| `10da:1af4` | `call_termination_handler` | Calls registered termination callback |
|
||
|
||
### I/O & Output
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `10da:00d6` | `flush_output_buffer` | Flushes buffered output via function pointer |
|
||
| `10da:0132` | `putchar_buffered` | Writes character to buffer, flushes on newline |
|
||
| `10da:0808` | `memcopy_to_buffer` | Copies N bytes from source to destination buffer |
|
||
| `10da:178c` | `print_error_message` | Formats and prints load error (references "not loaded: %s") |
|
||
| `10da:09e4` | `print_fatal_error` | Prints "Fatal Error" prefix + message |
|
||
| `10da:192a` | `print_internal_error` | Prints "Internal Error" message |
|
||
|
||
### Interrupt Management
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `10da:1ec0` | `restore_interrupt_vectors` | Restores INT 2Fh and INT 67h vectors |
|
||
| `10da:2249` | `restore_int_2f_67` | Restores INT 15h vector if saved |
|
||
| `1760:3d86` | `init_system_check` | Validates system (CPU, DOS version, VCPI/DPMI, memory) |
|
||
|
||
### Utility
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `10da:15ea` | `check_ds_segment` | Returns true if DS == 0x10 (checks data segment selector) |
|
||
| `1760:3c9e` | `nop_stub` | Always returns 0 (unused hook) |
|
||
|
||
## Key String References
|
||
|
||
| Address | String | Context |
|
||
|---------|--------|---------|
|
||
| `13fc:0016` | `$Id: comhighc.c 1.1 91/08/06...` | Phar Lap C runtime source ID |
|
||
| `13fc:0048` | `$Id: comutils.c 1.1 91/08/06...` | Phar Lap utility functions source ID |
|
||
| `13fc:0078` | `Serial Number ` | DOS extender serial validation |
|
||
| `13fc:14ca` | `Internal Error` | Error class prefix |
|
||
| `13fc:14da` | `Fatal Error` | Fatal error class prefix |
|
||
| `13fc:156a-1628` | File error messages | Not found, bad format, no memory, etc. |
|
||
| `1760:665c` | `Copyright (C) 1986-93 Phar Lap Software, Inc.` | DOS extender copyright |
|
||
| `1760:73da` | `-LDTSIZE 4096 -EXTHIGH D0_0000h -NI 18 -ISTKSIZE 3` | Default extender config |
|
||
| `1760:76fc-7c5a` | Numbered error messages | System requirement errors (1000-2170) |
|
||
|
||
## Architecture Notes
|
||
|
||
### Correction: The Game Ships As A Bound NE Executable
|
||
**Important**: The installed copy does **not** contain a separate `.EXP` file. `CRUSADER.EXE` is a bound executable with an outer DOS `MZ` stub and an internal `NE` executable image. The Phar Lap loader/runtime code and the game's real segment layout are both described inside this same file.
|
||
|
||
The flow is:
|
||
1. `entry` → checks DOS version, CPU type
|
||
2. `init_dos_extender` → sets up protected mode (VCPI/DPMI)
|
||
3. `load_exp_file` → opens the game's `.EXP` file
|
||
4. `load_executable_image` → parses P2/MZ headers, creates segments, applies relocations
|
||
5. `task_switch_to_child` → transfers control to the actual game code
|
||
|
||
For the installed retail copy, this means the currently loaded Ghidra program is only one interpretation of `CRUSADER.EXE`. The next import should target the **NE layer of the same file**, not a missing external `.EXP`.
|
||
|
||
### NE Import Details
|
||
- File to import: `F:\Apps\Crusader No Remorse\CRUSADER.EXE`
|
||
- Outer DOS header: `MZ`
|
||
- `e_lfanew`: `0x36F70`
|
||
- Internal executable header: `NE`
|
||
- Segment count: `145`
|
||
- Initial `CS:IP`: `0001:0000`
|
||
- Initial `SS:SP`: `0091:2000`
|
||
|
||
The currently analyzed protected-mode code at addresses like `10da:7c40` is consistent with the Phar Lap runtime/loader path. To reach the rest of the program, import `CRUSADER.EXE` again using an **NE-aware loader** or a workflow that starts from the internal NE header rather than the outer DOS stub.
|
||
|
||
### Segment 1339: Fast Memory Operations
|
||
`FUN_1339_02a8` contains an unrolled loop (Duff's device pattern with 57 iterations) — a hand-optimized **fast memory fill/add** routine, typical in DOS game graphics engines.
|
||
|
||
### EMS Memory (Segment 1677)
|
||
The game uses **EMS (Expanded Memory Specification)** via INT 67h for additional memory beyond the 1MB real-mode limit. Functions in segment 1677 manage EMS page frames and handle allocation/deallocation.
|
||
|
||
## NE Segment 1 Analysis — Game Logic Functions (seg001_code_off_37600_len_8400.bin)
|
||
|
||
This segment was imported as Raw Binary at base `0x0000`, language `x86:LE:16:Protected Mode`.
|
||
All 35+ identified functions renamed and annotated in Ghidra.
|
||
|
||
### Cursor Subsystem (0x0060–0x0d5f)
|
||
|
||
| Address | Name | Description |
|
||
|----------|---------------------------|-------------|
|
||
| `0x0060` | `cursor_update_hover` | Hover update: if mouse active & entity set, calls cursor_set_target |
|
||
| `0x00e9` | `cursor_set_target` | Positions cursor on entity, updates sprite + direction visual |
|
||
| `0x0322` | `cursor_shutdown` | Frees cursor resources, resets state |
|
||
| `0x0398` | `cursor_animation_update` | Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6 |
|
||
| `0x050f` | `cursor_draw_tick` | Per-frame cursor draw (calls cursor_animation_update if dirty) |
|
||
| `0x0c24` | `action_key_valid` | Returns 1 if action code (param_1) is a valid game action key |
|
||
| `0x0d5f` | `cursor_direction_input` | Arrow-key input: rotates cursor angle, updates direction sprite |
|
||
|
||
### Input Handling
|
||
|
||
| Address | Name | Description |
|
||
|----------|-------------------------|-------------|
|
||
| `0x0526` | `input_keyboard_handler`| Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load |
|
||
|
||
### Cursor State Data (at DS:0x27xx)
|
||
| Address | Field | Meaning |
|
||
|---------|-------|---------|
|
||
| `0x27c4` | cursor_sel1 | Selection counter 1 |
|
||
| `0x27c6` | cursor_sel2 | Selection counter 2 |
|
||
| `0x27c8` | current_entity | Handle to currently targeted entity |
|
||
| `0x27ca–0x27ce` | cursor_state | Cursor interaction state bytes |
|
||
| `0x27d0` | cursor_entity_type | Current entity type index |
|
||
| `0x27d2` | z_offset | Z-height offset for terrain adjustment |
|
||
| `0x27d4` | cursor_angle | Rotation angle (0–359) |
|
||
| `0x27d6` | cursor_sprite | Sprite handle for cursor visual |
|
||
| `0x27d8` | cursor_dirty | Set when cursor needs redraw |
|
||
| `0x27d9` | cursor_active | Master cursor enabled flag |
|
||
| `0x27da` | cursor_no_turn | Flag disabling cursor rotation |
|
||
| `0x27ed` | difficulty | Enemy accuracy divisor (used in projectile_init_vector) |
|
||
| `0x27fd` | hard_mode | Two-step mode (combat vs. explore) |
|
||
| `0x27fe` | move_mode | Movement phase flag |
|
||
| `0x27ff` | mouse_active | Mouse/input system active |
|
||
| `0x2800`–`0x2811` | various | UI state: active sprite, facing byte, cur entity handle |
|
||
| `0x283f`/`0x2841` | menu_obj_ptr | Active menu/dialog object far pointer |
|
||
| `0x2844` | in_save | In-progress save game flag |
|
||
| `0x290e` | entity_count | Number of active entities |
|
||
| `0x2910`–`0x2947` | snap_type_ids[10] | Entity types that snap-to-ground in snap_entity_to_ground |
|
||
|
||
### Input / Action Dispatch
|
||
|
||
| Address | Name | Description |
|
||
|----------|---------------------------|-------------|
|
||
| `0x2420` | `entity_command_dispatch` | Dispatches player commands to target entity; reads 0x27d0, 0x2de4, sends events 0x14/0xf, handles state machine 0x27ca/0x27cd/0x27ce |
|
||
| `0x279a` | `cheat_code_check` | Checks entity byte+1 vs cheat sequence at 0x2833 (counter 0x283d); on full match, toggles 0x844/0x6045 and spawns vtable 0x287b/0x2892 |
|
||
|
||
### Menu / Event Callbacks
|
||
|
||
| Address | Name | Description |
|
||
|----------|---------------------------|-------------|
|
||
| `0x2e53` | `cursor_event_notify_a` | Vtable thunk: forwards event to 0x27ca area handler |
|
||
| `0x2e96` | `cursor_event_notify_b` | Vtable thunk: forwards event to 0x27ca area handler (alt path) |
|
||
| `0x2ed9` | `menu_event_notify_a` | Vtable thunk: forwards event to 0x2843 (near menu object) |
|
||
| `0x2f0c` | `menu_event_notify_b` | Vtable thunk: forwards event to 0x2843 (alt path) |
|
||
| `0x2ff3` | `stub_noop_2ff3` | Empty stub, noop |
|
||
| `0x2ff8` | `entity_collision_callback_a` | Calls touch handler then func(entity+0x1e, seg, 2); opt: extra func if param_3&1 |
|
||
| `0x3046` | `set_active_menu` | Writes param_1/param_2 to 0x283f/0x2841 (active menu far pointer) |
|
||
| `0x3058` | `entity_collision_callback_b` | Same as entity_collision_callback_a (second vtable entry) |
|
||
|
||
### Entity System (0x2401–0x5a50)
|
||
|
||
| Address | Name | Description |
|
||
|----------|------------------------------|-------------|
|
||
| `0x2401` | `clear_cursor_selection` | Zeros 0x27c4/0x27c6 (selection counters) |
|
||
| `0x2899` | `cursor_switch_target_entity`| Switches cursor target: unloads old entity, loads new, re-registers |
|
||
| `0x29d8` | `get_z_offset` | Returns func() + *(0x27d2) = adjusted Z/height |
|
||
| `0x2a09` | `is_player_in_range` | Checks if entity is at player (0x2de4) X/Y +/-0xf0 range |
|
||
| `0x2a46` | `entity_ai_update_loop` | Loops entities 2–255, checks visibility, triggers fire/move |
|
||
| `0x2c36` | `ui_update_callback` | Calls cursor_state_clear then vtable[2] on menu object |
|
||
| `0x2c6b` | `cursor_state_clear` | Clears cursor state bytes 0x27ca–0x27ce, clears entity flag bit1 |
|
||
| `0x2c92` | `dialog_spawn` | Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca |
|
||
| `0x2d47` | `entity_pick_handler` | Handles entity selection or save-game trigger (type 0x38d) |
|
||
| `0x2df9` | `clear_active_menu` | Zeros 0x283f/0x2841 (active menu far pointer) |
|
||
| `0x2e18` | `game_mode_init` | Initializes game mode state, resets sprite/cursor/menu state |
|
||
| `0x2f3f` | `entity_table_set_sprite` | Reads 0x7df9+slot*2; writes entity type table 0x7e1e[slot*0x79+0x0d]=param_2, +0x10=0 |
|
||
| `0x3c97` | `snap_entity_to_ground` | If entity type in snap_type_ids[10], resets Z to 0xf0 and adjusts XY |
|
||
| `0x3d6e` | `spawn_entity_checked` | Spawns entity with explosion pool limit check (0x84c0, 0x84c2) |
|
||
| `0x3f2f` | `entity_spawn` | Allocates entity, vtable=0x29aa/0x39ca, positions it |
|
||
| `0x40d4` | `entity_remove` | Removes entity: destroys sprites, clears 0x2802/0x2804 if needed |
|
||
| `0x4172` | `entity_animation_frame_update`| Advances/retreats anim frame ([+0x1d]) toward target [+0x1c/0x1b] based on quality |
|
||
| `0x42f8` | `stub_noop_42f8` | Empty stub, noop |
|
||
| `0x42fd` | `entity_registry_decrement` | Calls cleanup func then decrements entity count at 0x290e |
|
||
| `0x4314` | `entity_sprite_move_delta` | Updates shot sprite handle (entity+0x3f) position by adding delta params |
|
||
| `0x4552` | `entity_set_position` | Sets entity+0x3e (type_handle), world_x/y (entity+0x45/47), base_x/y (entity+0x4f/51) |
|
||
| `0x452b` | `shot_set_spawn_pos` | Calls entity_set_position then sets entity+0xbe = param_3 (extra spawn field) |
|
||
| `0x4591` | `entity_try_place` | entity_set_position with validation — position only set if placement succeeds |
|
||
| `0x5092` | `entity_deactivate` | Calls vtable[2] to deactivate, or finds in registry and removes |
|
||
| `0x5a50` | `entity_list_contains` | Checks if entity ptr exists in active entity list at 0x294c |
|
||
| `0x5b05` | `stub_noop_5b05` | Empty stub, noop |
|
||
|
||
### Entity Object Layout (NE Segment 1 entities)
|
||
| Offset | Field | Meaning |
|
||
|--------|-------|---------|
|
||
| `+0x00` | vtable_ptr | Vtable pointer (0x29aa for generic, 0x2a57 for debris) |
|
||
| `+0x02` | slot_index | Entity slot index (used for registry at 0x39ca) |
|
||
| `+0x04` | entity_type | Entity type ID |
|
||
| `+0x19`/`+0x1a` | flags | Entity flags (bit0=debris, bit1=cleared by cursor_state_clear, bit6=active, bit8=valid) |
|
||
| `+0x1b` | vel_x | X velocity (clamped ±0x20) |
|
||
| `+0x1c` | vel_y | Y velocity (clamped ±0x20) |
|
||
| `+0x1d` | vel_z | Z velocity (clamped ±0x10) |
|
||
| `+0x1e` | fire_handle | Weapon/fire handle |
|
||
| `+0x1f` | is_enemy | 1 if entity is an enemy type |
|
||
| `+0x20`/`+0x21` | pos_frac_x/y | Fractional position (sub-tile) for movement |
|
||
| `+0x22` | pos_frac_z | Fractional Z |
|
||
| `+0x36` | weapon_type | Active weapon type ID |
|
||
| `+0x38` | facing | Current facing direction (0–15) |
|
||
| `+0x3c` | sprite_handle | Sprite for this entity |
|
||
| `+0x3f` | shot_sprite | Sprite handle for active projectile (0xFFFF = none) |
|
||
| `+0x45`/`+0x47`/`+0x49` | world_x/y/z | Current world position (integer) |
|
||
| `+0x4f`/`+0x51`/`+0x53` | base_x/y/z | Base/spawn position |
|
||
| `+0x54`/`+0x56`/`+0x58` | prev_x/y/z | Previous frame position |
|
||
| `+0x59` | attack_active | Attack in progress flag |
|
||
| `+0x5a` | at_target | Reached target flag |
|
||
| `+0x5e`–`+0x65` | delta_x/y/z/high | Per-step movement deltas (fixed point) |
|
||
| `+0x66`/`+0x68` | step_active | Stepping active (1=yes, 0=off) |
|
||
| `+0x6a`/`+0x6c` | weapon_slot/dist | Weapon slot and total travel distance |
|
||
| `+0x6e` | delta_z | Alt Z delta |
|
||
| `+0x70` | projectile_type | Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain) |
|
||
| `+0x72`/`+0x74`/`+0x76` | target_x/y/z | Target position with deviation |
|
||
| `+0x77` | target_entity | Target entity handle |
|
||
| `+0x79` | secondary_pos | Secondary position struct pointer |
|
||
| `+0xad` | owner_entity | Owning entity handle |
|
||
| `+0xaf` | shot_owner_flags | Shot owner (entity/player) |
|
||
| `+0xb1` | bounce_count | Bounce counter (used with homing, type 5) |
|
||
| `+0xb3` | has_bounce | Has bounce trajectory active |
|
||
| `+0xbd` | actor_type | Actor type byte (used for direction table lookups) |
|
||
|
||
### Shot Entity Lifecycle (0x435e–0x44a9)
|
||
|
||
| Address | Name | Description |
|
||
|----------|----------------------------|-------------|
|
||
| `0x435e` | `shot_entity_alloc` | Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7 |
|
||
| `0x44a9` | `shot_entity_free` | Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1 |
|
||
|
||
### Projectile / Combat (0x4659–0x5a99)
|
||
|
||
| Address | Name | Description |
|
||
|----------|----------------------------|-------------|
|
||
| `0x4659` | `projectile_init_vector` | Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536 |
|
||
| `0x4a91` | `entity_fire_weapon` | Fires weapon from entity using 0x129b/0x12ac direction offset tables |
|
||
| `0x4b18` | `fire_weapon_from_cursor` | Gets cursor angle sprites, fires projectile at cursor target |
|
||
| `0x4b78` | `projectile_check_hit` | Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity) |
|
||
| `0x4c2e` | `projectile_step_update` | Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked |
|
||
| `0x4d28` | `projectile_trace_ray` | Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate |
|
||
| `0x51ad` | `projectile_update_tick` | Full projectile tick: move, check reach target, bounce, call projectile_check_hit |
|
||
| `0x5a99` | `projectile_apply_hit` | Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner |
|
||
|
||
### Weapon Type Table (0x2536)
|
||
- Each entry is 0x11 bytes (17), accessed as `weapon_type * 0x11`
|
||
- `[0]` = step divisor for distance calculation
|
||
- `[0x19]` = max range threshold (used in projectile_update_tick)
|
||
|
||
### Direction Tables (0x129b / 0x12ac)
|
||
- Indexed by facing (0–15): dx offsets at 0x129b, dy offsets at 0x12ac
|
||
- Values are multiplied by distance (e.g. `*0x500`) for projectile spawn offsets
|
||
|
||
### Collision Detection (0x60c1–0x621e)
|
||
|
||
| Address | Name | Description |
|
||
|----------|----------------|-------------|
|
||
| `0x60c1` | `aabb_overlaps_3d` | 3D AABB overlap test — box layout [xmin,ymin,zmin,_,_,xmax,_,ymax,_,zmax] |
|
||
| `0x621e` | `bbox_translate` | Translates a 3D bounding box by (dx, dy, dz) — both min and max points |
|
||
|
||
### Enemy AI / Spawning (0x6aed–0x6d21)
|
||
|
||
| Address | Name | Description |
|
||
|----------|----------------------------|-------------|
|
||
| `0x6aed` | `map_find_spawn_point` | Finds map tile matching entity conditions; returns packed XYZ tile coords |
|
||
| `0x6bfc` | `actor_find_in_view` | Finds actor visible in current view frustum (temp data at 0x7eca) |
|
||
| `0x6ce9` | `enemy_spawn_with_target` | Wrapper: spawns enemy with player as target (param5=1) |
|
||
| `0x6d05` | `enemy_spawn_no_target` | Wrapper: spawns enemy without targeting player (param5=0) |
|
||
| `0x6d21` | `enemy_spawn_at_position` | Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A) |
|
||
|
||
### Player / HUD
|
||
|
||
| Address | Name | Description |
|
||
|----------|-------------------------------|-------------|
|
||
| `0x50ee` | `player_position_update` | Updates player position from direction data; clamps to screen bounds |
|
||
| `0x6ff7` | `player_health_update_and_effect` | Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect |
|
||
|
||
### Destruction / Death (0x7490–0x75ff)
|
||
|
||
| Address | Name | Description |
|
||
|----------|---------------|-------------|
|
||
| `0x7490` | `debris_spawn`| Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list |
|
||
| `0x75ff` | `entity_die` | Death handler: spawns 1–4 debris objects, picks best explosion direction |
|
||
|
||
### Entity Type Constants (weapon_type/entity class)
|
||
| Value | Entity Class |
|
||
|--------|--------------|
|
||
| `0x17` | Robot/mech type A |
|
||
| `0x18` | Robot/mech type B |
|
||
| `0x1` through `0x3c` | Various entity/weapon types |
|
||
| `0x3d` | Robot/mech type C |
|
||
| `0x3e` | Robot/mech type D |
|
||
| `0x2f5`–`0x2f7` | Special movement entity |
|
||
| `0x595`/`0x597` | Platform/elevator entities |
|
||
| `0x31c`/`0x322`–`0x327` | Explosive/effect entities |
|
||
| `0x38d` | Save game trigger entity |
|
||
| `0x426` | Spark/scatter sub-shot |
|
||
| `0x59a` | Player cursor/select indicator |
|
||
|
||
### Entity Data Table at 0x7e1e
|
||
- Stride: `0x79` bytes (121 bytes per entry)
|
||
- Indexed by entity type (integer) or entity slot
|
||
- `+0x5a` offset = flags byte (bit4 = special entity flag, bit3 = armor/shield flag)
|
||
- `+0x68` = targeting flag
|
||
|
||
### Map / Resource Tables
|
||
| Address | Content |
|
||
|---------|---------|
|
||
| `0x2833` | Cheat code input sequence (null-terminated) |
|
||
| `0x283d` | Cheat sequence match position counter |
|
||
| `0x7ded` | Map X coordinate array (2 bytes per entry) |
|
||
| `0x7df1` | Map Y coordinate array (2 bytes per entry) |
|
||
| `0x7df5` | Map Z array (1 byte per entry) |
|
||
| `0x7df9` | Entity state array (2 bytes per slot) |
|
||
| `0x7e46` | Player state block far pointer |
|
||
| `0x7e1e` | Entity type table (stride 0x79) |
|
||
|
||
### Entity Vtable Index (NE Segment 1)
|
||
| Address | Entity Class |
|
||
|---------|-------------|
|
||
| `0x28b5` | Dialog/menu object vtable |
|
||
| `0x287b` | Cheat-spawned entity (cheat ON) vtable |
|
||
| `0x2892` | Cheat-spawned entity (cheat OFF) vtable |
|
||
| `0x2969` | Entity registry vtable (stored at 0x39ca+slot*4, not entity's own vtable) |
|
||
| `0x297e` | Shot/projectile entity vtable |
|
||
| `0x29aa` | Generic/AI entity vtable |
|
||
| `0x2a1a` | Corpse entity vtable (variant) |
|
||
| `0x2a33` | Actor/corpse entity vtable |
|
||
| `0x2a57` | Debris fragment entity vtable |
|
||
|
||
## Next Steps
|
||
|
||
1. ✅ **NE Segment 1 imported and analyzed** — all 58 identified functions renamed and annotated
|
||
2. **Import additional NE segments** — priority: segments 22, 30, 59, 86 (segment 21 complete)
|
||
3. **Analyze additional segments** — apply same decompile→rename→annotate workflow
|
||
4. **Map file format loaders** — `.FLX`, `.SHP`, `.MAP`, `.TNT` resource formats
|
||
5. **Cross-reference entity type constants** with game entities (robots, platforms, triggers)
|
||
6. **Identify external segment calls** — the `func_0x0000ffff()` placeholders are all cross-segment calls; resolving them requires importing the referenced segments
|
||
|
||
1. ✅ **NE Segment 1 imported and analyzed** — all 58 identified functions renamed and annotated
|
||
2. ✅ **Raw 0007 segment analyzed** — rendering, camera/scroll, save slot, and scroll region subsystems documented (~60 functions renamed and annotated)
|
||
3. **Import additional NE segments** — priority: segments 22, 30, 59, 86 (segment 21 complete)
|
||
4. **Analyze raw 0007 draw helper cluster** — `FUN_0007_03b4`, `FUN_0007_04b8`, `FUN_0007_04dc`, `FUN_0007_057f`, `FUN_0007_0614`; called by sprite/draw list functions
|
||
5. **Analyze `FUN_0007_4cdf`** — large 15-case animation/movement dispatcher; overlapping instruction warnings; cases 0, 2, 3, 6, 9, 0xa, 0xe are clean
|
||
6. **Map file format loaders** — `.FLX`, `.SHP`, `.MAP`, `.TNT` resource formats
|
||
7. **Cross-reference entity type constants** with game entities (robots, platforms, triggers)
|
||
8. **Identify external segment calls** — the `func_0x0000ffff()` placeholders are all cross-segment calls; resolving them requires importing the referenced segments
|
||
|
||
## Raw 0007 Rendering & Sprite Draw List Subsystem (new)
|
||
|
||
A complete sprite draw list and tile-based visibility system was recovered from the `0007:e000–0007:fe00` flat range (above seg001, separate segment loaded at `0x7E000+`).
|
||
|
||
### Isometric Coordinate Transform
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:be67` | `world_to_screen_isometric` | Classic 2:1 isometric formula: `screen_x = (wx + sx) + (wy + sy)*2`, `screen_y = (wy + sy)*2 - (wx + sx)`. Scroll globals: `0x2bb7` (X), `0x2bb9` (Y). Output to `*param_3`, `*param_4`. |
|
||
| `0007:be9e` | `world_to_screen_isometric_wrapper` | Thin wrapper — calls `world_to_screen_isometric` with args shifted by one |
|
||
|
||
### Draw List Node Format
|
||
Sprite/draw node (size `0x18` = 24 bytes from pool, allocated by `linked_list_pop_2cc3`):
|
||
| Offset | Field | Notes |
|
||
|--------|-------|-------|
|
||
| `+0x0` | vtable | Function ptr for render callback (vtable[0] = draw) |
|
||
| `+0x8` | dep_from_list | Near ptr to list of sprites that depend ON this |
|
||
| `+0xa` | dep_to_list | Near ptr to list of sprites this depends ON |
|
||
| `+0xc` | bbox_xmin | Screen bounding box (4 ints) |
|
||
| `+0xe` | bbox_ymin | |
|
||
| `+0x10` | bbox_xmax | |
|
||
| `+0x12` | bbox_ymax | |
|
||
| `+0x14` | flags | bit 0=queued in draw list, bit 5=visible/rendered, bit 6=root sentinel, bit 7=must-redraw |
|
||
| `+0x15` | z_depth | int; sort key for painter's algorithm. 1 = root sentinel |
|
||
| `+0x17` | order_flag | if set, propagates must-redraw to dependent sprites |
|
||
| `+0x18` | tile_index | sparse tile index (for dirty bitmask bit addressing) |
|
||
|
||
### Draw List Functions
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:eb36` | `drawlist_pool_init` | Inits free pool: 1500 nodes × `0x2a` (42) bytes from base `0x2cc7`. Sets `0x2ccf = 1500` (count), `0x2cc3` = linked list head. Copies `0x2cc9` → `0x2cae`. |
|
||
| `0007:eb12` | `linked_list_push_2cc3` | LIFO push: `node->next = head_2cc3; head = node; count_2ccf++`. |
|
||
| `0007:eada` | `linked_list_pop_2cc3` | LIFO pop: dequeues node from `0x2cc3` head, decrements `0x2ccf` count. |
|
||
| `0007:ebd9` | `linked_list_dequeue_headtail` | FIFO dequeue from head/tail pair list; calls `FUN_0007_03ec` after dequeue. |
|
||
| `0007:ec2c` | `drawlist_enqueue` | FIFO enqueue: appends node to draw list (head if empty, tail always updated). |
|
||
| `0007:ec63` | `drawlist_remove_node` | Unlinks specific node from head or tail position; calls `FUN_0007_03ec`. |
|
||
| `0007:eca8` | `drawlist_process_and_render` | **Two-stage render pass.** Stage 1: drains `0x8442` (pending), viewport-tests each sprite, moves in-bounds to `0x8446` (visible). Stage 2: drains `0x8446`, calls sprite vtable[0] to draw. Recursive re-run if `0x2cb2` defer flag set. |
|
||
| `0007:edfa` | `drawlist_enqueue_sprite_children` | Enqueues all child sprites at node `+8` linkage to draw pending list if not already queued or rendered. Skips if `+0x15` z-depth == 1. |
|
||
| `0007:ef3d` | `sprite_add_draw_dependency` | Links sprites A and B with z-order dependency. Allocates link node via `FUN_0007_057f`. Stores in A's `+10` and B's `+8`. Sets must-redraw on B if A has `+0x17` set. |
|
||
| `0007:ef9f` | `sprite_enqueue_for_draw` | Null-guard; sets node `+0x14 \|= 1`, enqueues to `0x8442`. |
|
||
| `0007:efca` | `sprite_invalidate_and_unlink` | Full sprite removal: re-enqueues all dependents, frees link nodes via `FUN_0007_0614`, clears `+8/+10` lists, dequeues self from `0x8442`. |
|
||
| `0007:f2a0` | `sprite_sort_by_depth_and_link` | Compares `+0x15` z-depth of two sprites; calls `sprite_add_draw_dependency` in the correct order (lower z first). Handles equal-depth via thunk. |
|
||
| `0007:f654` | `drawlist_init` | Full draw system init: `drawlist_pool_init`, init full-screen viewport (`0x844a..0x8450 = 0..639, 0..screen_h`), allocate root sentinel node (vtable `FUN_0000_2ce4`, z-depth=1, flag=0x40), stored at `0x846a`. |
|
||
| `0007:ea00` | `bbox_intersect` | In-place 2D rect intersection: max of mins, min of maxes. Input/output: 4-int array `[xmin,ymin,xmax,ymax]`. |
|
||
| `0007:ea6d` | `bbox_union` | In-place 2D rect union: min of mins, max of maxes. |
|
||
| `0007:ee5a` | `viewport_update_from_sprite_bounds` | Subtracts scroll offsets from sprite bbox; clips to screen rect (`0x2ca6..0x2cac`); calls `bbox_intersect`; stores updated viewport at `0x844a..0x8450`; dispatches to render. |
|
||
|
||
### Tile-Based Visibility System
|
||
|
||
The rendering system uses a **6×5 tile grid** at DS:`0x846a` (30 entries × 2 bytes each =60 bytes). Tiles represent screen regions of approximately 128 screen pixels per tile. Each tile holds a near pointer to a linked list of sprite nodes overlapping that tile. Bitmasks track dirty and renderable tiles.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:f9e2` | `drawlist_mark_dirty_tiles` | Converts bbox to tile grid coords (divide by 128, clamp to 6×5). Walks all sprites in overlapping tiles' spatial buckets; re-enqueues overlapping sprites. Sets bits in dirty bitmask `0x2cbb`. |
|
||
| `0007:fb53` | `tile_visibility_update` | Iterates redraw bitmask (`0x2cbb`, up to 10240 tiles). For each dirty tile with a sprite, computes isometric screen position from map X/Y/Z. Checks sprite dimension bitfields at `pbVar8+2..+3` against 640×480 viewport. Sets corresponding bit in render bitmask `0x2cb7`. Clears dirty bitmask after processing. |
|
||
| `0007:fd98` | `tilemap_draw_if_dirty` | Guard: if render bitmask `0x2cb7` non-zero, calls `thunk_FUN_0007_001d` to trigger tilemap draw. |
|
||
|
||
### Draw List / Viewport Globals
|
||
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x2bb7` | `g_scroll_offset_x` | World-to-screen X offset (scroll) |
|
||
| `0x2bb9` | `g_scroll_offset_y` | World-to-screen Y offset (scroll) |
|
||
| `0x2ca6..0x2cac` | `g_screen_clip_rect` | Screen clip rectangle [xmin,ymin,xmax,ymax] |
|
||
| `0x2cae` | `g_draw_segment` | DS segment for draw node far pointers |
|
||
| `0x2cb0` | `g_free_pool_seg` | Segment for node free pool far calls |
|
||
| `0x2cb2` | `g_render_defer_flag` | If set, defers current render pass |
|
||
| `0x2cb7` | `g_render_tile_bitmask` | Far ptr to bitmask of tiles ready to render |
|
||
| `0x2cbb` | `g_dirty_tile_bitmask` | Far ptr to bitmask of dirty/changed tiles |
|
||
| `0x2cbf` | `g_tile_origin_x` | Tile grid world origin X (for tile coordinate math) |
|
||
| `0x2cc1` | `g_tile_origin_y` | Tile grid world origin Y |
|
||
| `0x2cc3` | `g_free_pool_head` | Free node pool linked list head |
|
||
| `0x2ccf` | `g_free_pool_count` | Free node pool remaining count (max 1500) |
|
||
| `0x8442` | `g_draw_pending_list` | Draw pending list head/tail (near ptrs) |
|
||
| `0x8446` | `g_draw_visible_list` | Draw visible list head/tail (near ptrs) |
|
||
| `0x844a..0x8450` | `g_viewport_rect` | Current viewport [xmin,ymin,xmax,ymax] |
|
||
| `0x846a` | `g_tile_grid_base` | 6×5 tile grid spatial index (near ptr per tile) |
|
||
|
||
## Raw 0007 Map Scroll / Camera Subsystem (new)
|
||
|
||
A scroll/camera management cluster found in the `0007:bxxx–0007:dxxx` range.
|
||
|
||
### Entity State Transition Helper
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:5b6f` | `entity_set_at_target_update_facing` *(likely internal block, not true top-level function)* | Direct raw-analysis name from the visible local behavior: sets entity `+0x3a = 1` (arrived flag); calls `entity_set_facing_direction`; clears bit `0x10` from entity type table `0x7e1e[type*0x79+0x59]`; then tail-calls onward. Relocation data places it at `seg043:016f`, and resolved call sites exist immediately before/after it (`5b36`, `5b44`, `5bb9`), so this address is likely an internal labeled block inside the larger missing `0007:5a00` seg043 function rather than a true entrypoint. |
|
||
|
||
### seg043 Standalone Boundary Recovery
|
||
|
||
- Direct disassembly of `NE_segments/seg043_code_off_75A00_len_336F.bin` shows the first non-zero bytes at offset `0x0090`; offsets `0x0000..0x008f` are all zero in the standalone extract.
|
||
- The first three clean 16-bit prologues in seg043 are at:
|
||
- `seg043:0090` -> raw `0007:5a90`
|
||
- `seg043:017a` -> raw `0007:5b7a`
|
||
- `seg043:021c` -> raw `0007:5c1c`
|
||
- The first recovered standalone function spans `0x0090..0x0179`, which means the current raw label at `0007:5b6f` falls inside the tail of that routine and overlaps the true return at raw `0007:5b79`.
|
||
- Practical consequence: the missing raw `0007:5a00` seg043 function boundary should not start at segment offset `0x0000`, and the current `0007:5b6f` function object should be treated as a mis-split internal block until Ghidra-side function creation/repair is available.
|
||
|
||
### Entity Class Flag Helper
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0006:02cc` | `entity_class_get_flag20` | Returns `((class_detail[type*0x79 + 0x59] & 0x20) >> 5)`. Conservative raw-analysis name; bit meaning still unknown, so the helper is named after the observed flag mask rather than a guessed behavior. |
|
||
|
||
### Animation Start Frame Helper
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:71b2` | `entity_set_anim_start_frame_from_flags` | Reads entity `+0x4b` flags. If bit 1 set: uses type table `+0x59 & 4` (attack active) to select last frame (`+0x39 - 1`), zero, or half-frame (`+0x39 >> 1`). Writes computed value to type table `+0x10`. If bit 2 set without bit 1: dispatch thunk. |
|
||
|
||
### Combat Helper
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:894b` | `entity_check_attack_flags_and_dispatch` | Guards on entity `+0x4b` bit 1 AND target object `+5` bits `0x1c`. If both set: dispatches thunk attack event. |
|
||
|
||
### Vtable Dispatch Helpers
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:8920` | `entity_call_vtable_slot0c` | Calls `(*param_1)[vtable+0xc]()`. |
|
||
| `0007:8cb8` | `entity_call_vtable_slot08` | Calls `(*param_1)[vtable+0x8]()`. |
|
||
| `0007:ccf1` | `entity_call_vtable_slot28` | Calls `(*param_1)[vtable+0x28]()`. |
|
||
|
||
### Active Flag / Counter
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:8854` | `entity_set_active_flag` | Sets entity `+0x40 = 1` (active); increments global `0x2800`. |
|
||
|
||
### Dispatch Table Lookup
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:8508` | `entity_table_lookup_and_dispatch` | Searches 1-entry table at `0x2b46` for `(param_3, param_4)` key pair; on match, calls the entry's function pointer at `[2]`. |
|
||
|
||
### Scroll/Camera Functions
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:bab5` | `entity_set_watch_ptr` | Stores FAR entity ptr to `0x2bd8` (the watch target entity). |
|
||
| `0007:baea` | `camera_update_and_check_player_scroll` | Calls watch entity vtable `+0x24`; if `0x2bd1` flag clear checks if player position (from `g_player_entity_farptr+0x40`) has moved > 32 units since `0x2be0`; if so, updates `0x2be0` and conditionally dispatches scroll event via `0x45aa`. |
|
||
| `0007:c6ba` | `scroll_camera_set_state_params` | Stores word params to `0x8354`, `0x8356`, byte to `0x8358`; dispatches. |
|
||
| `0007:cd56` | `dispatch_if_flag_2bd3_set` | Returns unless `0x2bd3` non-zero. |
|
||
| `0007:cfef` | `dispatch_if_mode_flags_set` | Two-flag check: dispatches if `0x2bca` or `0xee0` is non-zero. |
|
||
| `0007:d0f6` | `scroll_call_set_params_unless_blocked` | Calls `scroll_camera_set_state_params` only if `0x2bbb == 0`. |
|
||
| `0007:d119` | `scroll_update_direction_tracking` | Guards on `0x2bd3`. Calls `scroll_call_set_params_unless_blocked`. Compares direction bytes from `0x2cf4/0x2cf5` against cached `0x2bbd/0x2bbe`; if changed, clears `0x2bbc`. Dispatches. |
|
||
| `0007:d4a5` | `scroll_set_option_value` | Sets `0x2bc6 = param_1`. |
|
||
| `0007:d4b0` | `scroll_set_params_default` | Unconditional call to `scroll_camera_set_state_params`. |
|
||
| `0007:d4d3` | `scroll_set_map_index_validated` | If `param_1` in `[0..250]` and differs from `0x2bbf`, updates `0x2bbf` and clears `0x2bbc/0x2bbb`. |
|
||
| `0007:d655` | `map_position_has_changed` | Compares map arrays `0x7ded/0x7df1/0x7df5` at index `0x2bc6` against cached `0x2bc1/0x2bc3/0x2bc5`. Returns 1 if changed, 0 if same. |
|
||
| `0007:d6b1` | `scroll_clear_dirty_flags_and_dispatch` | Clears `0x2bbb = 0` and `0x2bbc = 0`; dispatches. |
|
||
| `0007:de57` | `entity_check_player_range_and_update` | Reads player world position (`g_player_entity_farptr+0x40`); if moved > 59 units from `entity+0x32` (cached pos), updates cache and calls `scrollregion_find_and_dispatch`. |
|
||
|
||
### Scroll Region Table (`0x835a`)
|
||
8 entries × `0x19` (25) bytes = 200 bytes. Entry layout:
|
||
| Offset | Field | Notes |
|
||
|--------|-------|-------|
|
||
| `+0x0/+0x2` | key pair | Matched by scroll region key lookup |
|
||
| `+0x8` | refcount | Reference count (incremented on match) |
|
||
| `+0xe` | count2 | Secondary counter |
|
||
| `+0x10` | active | Non-zero when region is live |
|
||
| `+0x11/+0x13` | x_start/x_end | Bounding X coordinates |
|
||
| `+0x15/+0x17` | y_start/y_end | Bounding Y coordinates |
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:e194` | `scrollregion_process_active` | Iterates active scroll entries (key non-zero AND `+0x10 != 0`), reads bounding box fields `+0x11/+0x13/+0x15/+0x17`, dispatches with bounds args. |
|
||
| `0007:e214` | `scrollregion_find_and_dispatch` | Finds empty scroll entry (zero key) and dispatches. |
|
||
| `0007:e29c` | `scrollregion_register` | Finds or allocates scroll region entry. Existing match: bump refcount / set active. New: init via `scrollregion_entry_init`. Special key `0x4ed`: thunk dispatch instead. |
|
||
| `0007:e50f` | `scrollregion_entry_init` | Null-guards param_1; zeroes output param_2, param_3, param_4; dispatches continuation. |
|
||
| `0007:e74a` | (unnamed) | Sets up call to walk `0x835a` table (8 entries, stride `0x19`, filter `0x968`), with callback return label `e763`. |
|
||
|
||
### Save Slot System (`0x8337` + `0x2ba3`)
|
||
|
||
10 save slots, each `0x400` (1024) bytes. Handle table at `0x8337` (10 words). Slot buffer base at FAR ptr `0x2ba3`.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:ac13` | `saveslot_table_clear` | Fills `0x8337..0x834b` (10 words) with `0xFFFF` (all slots empty). |
|
||
| `0007:acab` | `saveslot_free_if_empty` | Scans slot `0x2ba3[param_1*0x400]` for non-zero data; if empty, sets handle `0x8337[param_1] = 0xFFFF`. |
|
||
| `0007:ad47` | `saveslot_find_index_by_id` | Linear scan of 10-word handle table `0x8337`; returns index of matching word or `-1`. |
|
||
| `0007:ad79` | (unnamed) | Finds a free (0xFFFF) slot index. Complement of `saveslot_find_index_by_id`. |
|
||
| `0007:afd4` | `saveslot_get_or_alloc` | Gets slot pointer: calls `saveslot_find_index_by_id`; if not found calls `ad79` to get free slot; returns `0x2ba3 + slot * 0x400`. Returns 0 if no free slot. |
|
||
| `0007:b02c` | `saveslot_write_entry` | Navigates to `slot_base[param_3 * 4]`; dispatches thunk paths for write (existing, overwrite, new). |
|
||
| `0007:b0de` | `saveslot_read_entry_flags` | Reads from slot entry far pointer at `slot_base[param_3*4]`; extracts 4-byte packed bitfield from `+4..+7` in entry record into `*param_1`. Bit-by-bit extraction loop for 4 bytes. |
|
||
|
||
### String & Memory Utilities
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:a96d` | `entity_copy_string_truncated80` | Strlen(param_3) ≤ 0x50 guard; copies string word-by-word from param_3 into `param_2+8`. |
|
||
| `0007:b813` | `memcpy_4words` | Copies 4 words (8 bytes) from `param_2` to `param_1`. |
|
||
| `0007:ba45` | (unnamed) | Null/non-null far-ptr dispatch: different thunk paths based on `param_2 == 0`. |
|
||
| `0007:b46d` | `entity_dispatch_if_slot82e2_valid` | Guard: if `*(int *)0x82e2 != -1`, calls dispatch thunk. |
|
||
|
||
### Linked List Utilities (draw pool + sprite)
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:ea00` | `bbox_intersect` | In-place rect intersection: `[xmin,ymin,xmax,ymax]` = max(mins) × min(maxes). |
|
||
| `0007:ea6d` | `bbox_union` | In-place rect union: `[xmin,ymin,xmax,ymax]` = min(mins) × max(maxes). |
|
||
|
||
### Scroll/Camera Globals
|
||
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x2bb7` | `g_scroll_offset_x` | Isometric scroll X — added to world_x in screen transform |
|
||
| `0x2bb9` | `g_scroll_offset_y` | Isometric scroll Y |
|
||
| `0x2bbb` | `g_scroll_blocked` | If non-zero, blocks `scroll_camera_set_state_params` call |
|
||
| `0x2bbc` | `g_scroll_dirty` | Scroll direction changed flag (cleared when direction tracks) |
|
||
| `0x2bbd` | `g_scroll_dir_x` | Cached scroll direction X |
|
||
| `0x2bbe` | `g_scroll_dir_y` | Cached scroll direction Y |
|
||
| `0x2bbf` | `g_map_index` | Current map/level index `[0..250]` |
|
||
| `0x2bc1/0x2bc3/0x2bc5` | `g_map_entry_x/y/z` | Cached map entry X/Y/Z (vs. live map arrays) |
|
||
| `0x2bc6` | `g_map_slot_index` | Index into `0x7ded/0x7df1/0x7df5` arrays for current map slot |
|
||
| `0x2bca/0x2bc9` | `g_option_toggle_state` | UI option toggle state flags |
|
||
| `0x2bd1` | `g_scroll_block_flag` | Blocks camera update path if non-zero |
|
||
| `0x2bd3` | `g_scroll_active` | Non-zero = scroll system active |
|
||
| `0x2bd8` | `g_watch_entity_ptr` | FAR ptr to entity being tracked by camera |
|
||
| `0x2be0` | `g_player_scroll_pos` | Cached player world X+Y (ulong) for scroll threshold detection |
|
||
| `0x8354..0x8358` | `g_scroll_state_params` | Three scroll state params (word, word, byte) |
|
||
|
||
|
||
## Raw 0008 Gameplay Dispatch Helper Batch (new)
|
||
|
||
Small conservative rename batch from direct field-write behavior in the `0008:ba00-0008:be05` cluster.
|
||
|
||
### Newly renamed functions
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:ba00` | `entity_dispatch_entry_init` | Constructor-style init: optional alloc (`0x32` bytes), vtable/list-link setup (`0x3b06`, `0x2d10`, `0x3afe`), zeroes state fields, seeds group from global active layer `0x39c9` via `entity_set_group_id` |
|
||
| `0008:bbb6` | `entity_set_source_type` | Writes entry word field `+0x04` from incoming parameter, then dispatches through FAR thunk path |
|
||
| `0008:bc27` | `entity_set_event_type_checked` | Writes entry word field `+0x06`; when source field `+0x04` is non-zero, validates old/new event transition, including special checks for `0xF0-0xF7` and upper bound `<= 0x0FFF` |
|
||
| `0008:bca8` | `entity_set_group_id` | Validates group id range `1..31`, writes low 5-bit group in byte `+0x08`, decrements old per-group counter and increments new one via counter table pointed to by `0x39c5` |
|
||
| `0008:be05` | `entity_increment_group_id` | Computes `((entry+0x08)&0x1F)+1`, validates against active-layer assumptions (`0x39c9`), then applies through `entity_set_group_id` |
|
||
|
||
### Verified call/xref notes
|
||
|
||
- `entity_set_group_id` is called from:
|
||
- `entity_dispatch_entry_init` at `0008:bae4`
|
||
- `entity_increment_group_id` at `0008:be57`
|
||
- `entity_set_source_type` is used from at least:
|
||
- `FUN_0008_c92f` (`0008:c94d`, `0008:c96d`)
|
||
- `FUN_0008_ca18` (`0008:ca36`, `0008:ca56`)
|
||
|
||
### Gameplay relevance
|
||
|
||
- This cluster appears to manage core dispatch-entry metadata (`source_type`, `event_type`, group/layer byte and counters) that feeds the seg021 scheduler/event system previously documented.
|
||
- The field offsets match the current seg021 entity/dispatch layout notes (`+0x04`, `+0x06`, `+0x08`), strengthening confidence in cross-function struct consistency.
|
||
|
||
## Raw 0008 Pair-Sync Helper Batch (new)
|
||
|
||
Conservative directional rename batch from the `0008:c7f1-0008:cad7` cluster.
|
||
These functions are clearly paired and structurally symmetric, but final gameplay semantics are still partial due to FAR-thunk heavy internals.
|
||
|
||
### Newly renamed functions
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:c7f1` | `entity_pair_update_link_slot_a` | Guards on entry flags (`+0x16` must not include `0x4000`), then dispatches through FAR thunk using entry local struct at `+0x28` and partner-side key/id input |
|
||
| `0008:c890` | `entity_pair_update_link_slot_b` | Twin of `entity_pair_update_link_slot_a` with identical call shape and guard behavior; used in opposite order by pair-sync wrappers |
|
||
| `0008:c92f` | `entity_pair_sync_a` | If either side has unset `source_type` (`+0x04`), copies from partner via `entity_set_source_type`; then calls link-slot helpers in A-order and ends in FAR thunk using first side `+0x1e` data |
|
||
| `0008:ca18` | `entity_pair_sync_b` | Mirror of `entity_pair_sync_a` with reversed side/order for helper calls and final thunk argument ordering |
|
||
| `0008:c9ee` | `entity_pair_mark_and_sync_a` | Sets bit `0x10` in entry flags at `+0x16`, then calls `entity_pair_sync_a` |
|
||
| `0008:cad7` | `entity_pair_mark_and_sync_b` | Sets bit `0x10` in entry flags at `+0x16`, then calls `entity_pair_sync_b` |
|
||
|
||
### Verified call/xref notes
|
||
|
||
- `entity_pair_sync_a` is called from `entity_pair_mark_and_sync_a` (`0008:ca10`).
|
||
- `entity_pair_sync_b` is called from `entity_pair_mark_and_sync_b` (`0008:caf9`).
|
||
- Shared helper use inside pair sync wrappers:
|
||
- `entity_pair_update_link_slot_b` at `0008:c981` and `0008:ca6a`
|
||
- `entity_pair_update_link_slot_a` at `0008:c995` and `0008:ca7e`
|
||
|
||
### Gameplay relevance
|
||
|
||
- This cluster likely handles directional two-entity relationship synchronization in the scheduler/entity-dispatch layer (source/type propagation plus paired link-slot updates).
|
||
- Offsets used here (`+0x04`, `+0x16`, `+0x1e`, `+0x28`) align with the existing seg021 object-field and linker/list usage patterns, which increases confidence while preserving conservative naming.
|
||
|
||
## Raw 0008 Flag-0x20 Target-State Helpers (new)
|
||
|
||
Two complementary helpers near the pair-sync cluster were renamed using strict field/bit behavior only.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:cb2c` | `entity_flag20_clear_and_update_target` | Clears bit `0x20` at entry flags `+0x16`; if non-null target args are provided, writes far-pointer target fields `+0x12/+0x14`; then calls shared refresh helper `0008:c01d` |
|
||
| `0008:cb5c` | `entity_flag20_set_and_init_target` | Sets bit `0x20` at entry flags `+0x16`; initializes target far-pointer fields `+0x12/+0x14` only when currently zero; then calls shared refresh helper `0008:c01d` |
|
||
|
||
Notes:
|
||
- Naming intentionally stays flag-centric because high-level gameplay meaning of bit `0x20` is not yet fully resolved.
|
||
- Both helpers share the same post-update refresh path (`0008:c01d`), suggesting they are two state transitions in one target/link-management subsystem.
|
||
|
||
## Raw 0008 Dispatch Refresh Pipeline (new)
|
||
|
||
Follow-up rename batch for the shared refresh node used by the new flag-0x20 helpers.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:c01d` | `entity_refresh_dispatch_state` | Early-exit when flags at `+0x16` indicate dead (`0x8`) or already refreshed (`0x4000`); otherwise runs pre-clear, sets `0x4000`, calls update vfunc path, then runs flag-conditioned handlers |
|
||
| `0008:bfb2` | `entity_clear_status_bits_from_flags` | Clears specific bits in status word at `+0x32` based on state flags (`+0x16:0x400`, `+0x18:0x40/0x80`) |
|
||
| `0008:bf8e` | `entity_call_update_vfunc14` | Calls helper `0008:be6b`, then dispatches entity vtable call at offset `+0x14` |
|
||
| `0008:beee` | `entity_run_flagged_handlers` | Executes handler calls gated by flags (`+0x16:0x400/0x4`, `+0x18:0x40/0x80`) and then dispatches via FAR thunk using entry slot/index (`+0x2`) |
|
||
|
||
Verified xref context:
|
||
- `entity_refresh_dispatch_state` is directly called from:
|
||
- `entity_flag20_clear_and_update_target` (`0008:cb54`)
|
||
- `entity_flag20_set_and_init_target` (`0008:cb86`)
|
||
|
||
Gameplay relevance:
|
||
- This establishes a concrete state pipeline for dispatch entries after target/link changes: flag-gated status clear -> mark refreshed (`0x4000`) -> vtable update callback -> flag-conditioned subsystem handlers.
|
||
|
||
## Raw 0008 Flag-0x100 and Constructor-Variant Batch (new)
|
||
|
||
Additional conservative renames from the `0008:d1a4-0008:d27d` cluster.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:d1a4` | `entity_set_flag100_in_flags2` | Gate-checked setter: ORs bit `0x100` into entry word at `+0x18` |
|
||
| `0008:d1dc` | `entity_clear_flag100_in_flags2` | Gate-checked clearer: ANDs entry word at `+0x18` with `0xFEFF` (clears bit `0x100`) |
|
||
| `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | Constructor variant: allocates `0x40` bytes if null, reinitializes via `0008:cefb`, sets vtable to `0x3aa6`, sets flag `0x200` at `+0x16`, zeroes fields `+0x38..+0x3e` |
|
||
|
||
Notes:
|
||
- The `entity_set_flag100_in_flags2` / `entity_clear_flag100_in_flags2` pair is a verified complementary toggle with identical gate logic (`0x39a8/0x39f9/0x3991` check path).
|
||
- Constructor naming is intentionally vtable-centric (`0x3aa6`) until more direct gameplay semantics are recovered from its callback dispatch paths.
|
||
|
||
## Raw 0008 Periodic/Counter Helpers (new)
|
||
|
||
Follow-up renames from the `0008:d313-0008:d47d` cluster tied to the `0x3aa6` constructor branch.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:d313` | `entity_periodic_accumulate_and_dispatch` | Adds global delta (`0x39d0/0x39d2`) into entry accumulator (`+0x3c/+0x3e`), wraps against period (`+0x38/+0x3a`), and on wrap invokes entry vtable callback at `+0x28` with reentrancy guard bit `0x400` in `+0x18` |
|
||
| `0008:d3e6` | `entity_set_flag2000_and_update_active_counters` | Atomic (CLI/PUSHF) set of bit `0x2000` in `+0x16`; if bit `0x400` is set, decrements global counter `0x39f6` and increments `0x39f4` |
|
||
| `0008:d433` | `entity_clear_flag2000_and_update_active_counters` | Atomic clear of bit `0x2000` in `+0x16`; if bit `0x400` is set, decrements `0x39f4` and increments `0x39f6` |
|
||
|
||
Gameplay relevance:
|
||
- This identifies a concrete periodic dispatch mechanism (accumulator+wrap callback) and a paired active/inactive counter transition path around flag `0x2000`.
|
||
- The `0x39f4/0x39f6` counter swap strongly suggests global bookkeeping for a scheduler subset associated with these entries.
|
||
|
||
## Raw 0008 Word-List Management Batch (new)
|
||
|
||
Verified helper cluster for entry-owned word-list storage (sentinel-terminated with `0x0408`).
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:da00` | `entity_word_list_set_0408_terminated` | Rebuilds/replaces entry list from stack-provided words terminated by `0x0408`; frees prior list pointer at `+0x06/+0x08`; allocates and populates new list |
|
||
| `0008:dba3` | `entity_word_list_free_existing` | Validates list pointer exists, then frees old list buffer referenced by `+0x06/+0x08` |
|
||
| `0008:dbec` | `entity_word_list_destroy` | Resets vtable to `0x2d10`, frees list if present via `entity_word_list_free_existing`, and optionally frees object when destroy flag bit `1` is set |
|
||
| `0008:dc38` | `entity_word_list_ensure_contains` | Scans existing list for a given word; if missing, appends through `entity_word_list_append_unique` |
|
||
| `0008:dcab` | `entity_word_list_append_unique` | Allocates larger list, copies existing words, appends new word plus `0x0408` terminator, frees old list, then rebuilds via `entity_word_list_set_0408_terminated` |
|
||
|
||
Notes:
|
||
- Entry fields used by this subsystem: count at `+0x02`, list far pointer at `+0x06/+0x08`.
|
||
- The explicit `0x0408` terminator appears both in scanner/build logic and append path, making it a reliable list format marker.
|
||
|
||
## Raw 0008 Word-List Access/Mutation Batch (new)
|
||
|
||
Follow-up renames extending the same list subsystem.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:deea` | `entity_word_list_get_at` | Bounds-checks index against count (`+0x02`) and returns word from list pointer (`+0x06/+0x08`, stride 2) |
|
||
| `0008:df1b` | `entity_word_list_set_at` | Bounds-checks index then writes value into list element (`+0x06/+0x08`, stride 2) |
|
||
| `0008:dfa1` | `entity_word_list_find_unflagged_by_id10` | Scans list and returns first value satisfying `(value & 0x400)==0` and `(value & 0x3ff)==requested_id`; writes `0` when not found |
|
||
| `0008:ddaf` | `entity_word_list_remove_value` | Removes matching value(s) by counting survivors, rebuilding compact storage for non-matching entries, freeing old list storage, and updating list state |
|
||
|
||
Notes:
|
||
- `entity_word_list_find_unflagged_by_id10` implies list entries pack a 10-bit id plus flag bits (`0x400` observed).
|
||
- This further supports that the `0008:da00..dfa1` region is a compact encoded-ID list manager used by gameplay dispatch entries.
|
||
|
||
## Raw Import Note: `0000:ffff` Thunk Target (new)
|
||
|
||
Requested deep-dive on `FUN_0000_ffff`:
|
||
|
||
- Renamed to `unresolved_far_thunk_dispatch`.
|
||
- Current raw-import evidence indicates this is **not valid local executable logic** in this program view:
|
||
- Decompiler emits overlapping-instruction warnings and bad-control-flow warnings.
|
||
- Disassembly from `0000:ffff` into `0001:xxxx` is nonsensical/misaligned (mixed data/code artifacts).
|
||
- The body is heavily shared as a call sink from many segments, consistent with unresolved inter-segment thunking in this import mode.
|
||
|
||
Practical interpretation:
|
||
- Treat calls to `unresolved_far_thunk_dispatch` as unresolved external/indirect dispatch edges, not as meaningful function internals to recover in the raw flat import.
|
||
- Semantic recovery should continue from call-site argument setup and local field effects (the workflow used in recent 0008 batches).
|
||
|
||
## Raw 0008 Gate-Callback Wrapper Batch (new)
|
||
|
||
Conservative renames for callback wrappers sharing the same global gate condition.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0008:d00e` | `entity_gate_callback_wrapper_a` | Gate check on globals `0x39a8/0x39f9/0x3991`; on pass dispatches callback through unresolved thunk using entry `+0x2` and `[0x3b32:0x3b34] + 0x32` |
|
||
| `0008:d05f` | `entity_gate_callback_wrapper_b` | Same gate pattern; callback wrapper variant via unresolved thunk |
|
||
| `0008:d0b0` | `entity_gate_callback_wrapper_c` | Same gate pattern; passthrough-style callback wrapper |
|
||
| `0008:d0ed` | `entity_gate_callback_wrapper_d` | Same gate pattern; passthrough-style callback wrapper |
|
||
| `0008:d12a` | `entity_gate_callback_wrapper_e` | Same gate pattern; passthrough-style callback wrapper |
|
||
| `0008:d167` | `entity_gate_callback_wrapper_f` | Same gate pattern; passthrough-style callback wrapper |
|
||
| `0008:d3d2` | `entity_slot_callback_wrapper` | Thin wrapper: pushes entry slot/index (`+0x2`) and dispatches through unresolved thunk |
|
||
|
||
Notes:
|
||
- Names remain wrapper-oriented because target callbacks are unresolved in this raw-import model.
|
||
- These wrappers are now easier to track from call sites while preserving conservative semantics.
|
||
|
||
## Additional Unresolved Thunk Stubs (new)
|
||
|
||
Follow-up thunk census after inspecting `0000:ffff` behavior.
|
||
|
||
### Confirmed trampoline-only stubs
|
||
|
||
All of the following are single-instruction wrappers (`CALLF 0000:ffff`) and were given unique labels:
|
||
|
||
| Address | New Name | Observed Caller(s) |
|
||
|---------|----------|--------------------|
|
||
| `0004:2592` | `thunk_callf_0000_ffff_0004_2592` | `0004:262d` (`FUN_0004_2620`) |
|
||
| `000b:f924` | `thunk_callf_0000_ffff_000b_f924` | `000b:0144` (`FUN_000b_010b`) |
|
||
| `000c:827d` | `thunk_callf_0000_ffff_000c_827d` | `000c:8985`, `000c:8f96` (`FUN_000c_88b4`) |
|
||
| `000c:82f9` | `thunk_callf_0000_ffff_000c_82f9` | `000c:8a10`, `000c:8f79`, `000c:9052` |
|
||
| `000c:8356` | `thunk_callf_0000_ffff_000c_8356` | `000c:84a9` (`FUN_000c_84a5`) |
|
||
| `000c:e4f9` | `thunk_callf_0000_ffff_000c_e4f9` | `000c:e4f5` (`FUN_000c_e4e0`) |
|
||
|
||
Notes:
|
||
- This confirms `unresolved_far_thunk_dispatch` is represented by multiple local trampoline copies in different segment regions.
|
||
- Separating them by address improves call-graph navigation and makes subsystem-specific tracing less ambiguous.
|
||
|
||
## Raw 000c State-Dispatch Helper Cluster (new)
|
||
|
||
After separating thunk stubs, a coherent local state/chain management cluster was lifted in `000c:ab32-000c:ac8f`.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:ab32` | `entity_state_tick_dispatch` | Core state tick helper using fields `+0x38/+0x39/+0x3b/+0x3d/+0x5b`; clears mode bit `0x100` when `+0x38==0`, may call cleanup helper `000c:ac55`, calls `000c:7730(state,1)`, and conditionally advances chain |
|
||
| `000c:ab96` | `entity_state_reset_and_tick_dispatch` | Reset wrapper: zeroes `+0x38` and `+0x39` then calls `entity_state_tick_dispatch` |
|
||
| `000c:abb4` | `entity_state_advance_next_or_fallback_a` | Advance path A: when `+0x49!=0`, follows node-next pointers from `+0x3b/+0x3d` using offsets `+2/+4`; when exhausted, either clears active flag and re-dispatches, or falls back to backup pointer `+0x41/+0x43` |
|
||
| `000c:ac8f` | `entity_state_advance_next_or_fallback_b` | Advance path B: same structure as A but follows alternate node offsets `+6/+8` and fallback pointer `+0x45/+0x47` |
|
||
|
||
Notes:
|
||
- This cluster gives a concrete local interpretation for part of the large `000c:88b4` control flow without relying on unresolved thunk internals.
|
||
- Naming remains direction/path based (`a`/`b`) where high-level gameplay meaning is still pending.
|
||
|
||
## Raw 000c State-Flag Guard / Input Handler Batch (new)
|
||
|
||
Second sweep through `000c` adjacent helpers — gated thunk wrappers and input/animation tick handlers.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:9f74` | `entity_state_flag100_check_and_dispatch` | Init latch guard at `[0x6053]`; clears `[0x8c55]` on first call; checks `[ptr+0x5b]` bits `0x100` and `0x40`; three-path thunk dispatch |
|
||
| `000c:a1ad` | `entity_state_clear_flag40_and_dispatch` | Skips if `[ptr+0x5b]` has `0x180` bits set; if `0x40` set, clears it and calls far ptr at `[0x5e82/0x5e84]`; then dispatches twice with args `(0x0b,0x10,0x1,0x0)` (record/state-key pattern) |
|
||
| `000c:a74e` | `entity_state_dispatch_if_flag_bit2` | Tests `[ptr+0x5b]` bit `0x2`; if set pushes extra arg + ptr and dispatches via thunk |
|
||
| `000c:84c3` | `entity_state_set_byte40_at_global_ptr` | Sets byte `[DAT_0000_6828 ptr + 0x40] = 1` then calls thunk unconditionally; enables global entity flag |
|
||
| `000c:ac55` | `entity_state_fire_if_handle_valid` | Guard: fires thunk dispatch only when `[0x6054] != -1`; no-op otherwise |
|
||
| `000c:ac6d` | `entity_state_fire_with_args_if_handle_valid` | 3-arg variant: pushes `[BP+0xe]` (byte), `[BP+0xc]`, `[BP+0xa]`, handle `[0x6054]`, then `CALLF 0000:ffff` |
|
||
| `000c:afa5` | `entity_state_check_field49_and_call_vfunc3c` | Checks field `[ptr+0x49]`: −1→reset to 0 return 1; 2→call `vtable[0x3c]` return 0; else thunk dispatch |
|
||
| `000c:b153` | `entity_state_animation_done_tick` | Checks `[param_2+0x14+0xa]` animation-complete flag; if zero increments `field49` and calls `entity_state_check_field49_and_call_vfunc3c`; if set calls `vtable[0x3c]` |
|
||
| `000c:b199` | `entity_state_input_key_handler` | Full input dispatcher: ESC/x/X → `vtable[0x3c]` (cancel); Left/Right arrows `0x14b`/`0x148` → prev state; n/N/0x14d/0x150 → next state; e/E → set `field47=1`; `-` with counter → trigger at 4. Manages `field47` and `field49` |
|
||
| `000c:b2c3` | `stub_noop_000c_b2c3` | Empty stub; returns immediately |
|
||
| `000c:b2c8` | `entity_state_dispatch_if_field49_eq4` | Fires thunk only when `[ptr+0x49]==4` |
|
||
| `000c:b349` | `entity_state_dispatch_if_far_ptr_nonzero_a` | Fires thunk if far-pointer args non-zero |
|
||
| `000c:b383` | `entity_state_set_field3f_and_dispatch` | If non-NULL: writes `&DAT_0000_2d18` to `[ptr+0x3f]`, then dispatches |
|
||
| `000c:b3d8` | `entity_state_dispatch_if_far_ptr_nonzero_b` | Same null-guard pattern as `b349`, variant b |
|
||
|
||
Patterns confirmed in this batch:
|
||
- `field49` = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
|
||
- `field47` = keystroke-combo counter
|
||
- `field3f` = linked data pointer (event/record reference)
|
||
- Global `[0x6054]` = current entity handle; `[0x6828]` = another global entity far pointer
|
||
- Bits in `[ptr+0x5b]`: `0x1=init`, `0x2=active/event`, `0x40=pending dispatch`, `0x100=flag100`, `0x180=skip-all mask`
|
||
|
||
|
||
## Raw 000c Palette Fade + Entity VM Cluster (new)
|
||
|
||
Two distinct subsystems identified in `000c:cdde-000c:f98b`.
|
||
|
||
### VGA Palette Fade (000c:cdde, 000c:ce57)
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:cdde` | `palette_fade_step_down` | Writes (R−offset, G−offset, B−offset) clamped to 0 to VGA I/O `0x3c8`/`0x3c9`; decrements `[0x630d]` by step `[0x6316]`; clears active at `[0x630a]` when black |
|
||
| `000c:ce57` | `palette_fade_step_up` | Same loop, adds offset, clamps at 63 (0x3f full VGA). Clears `[0x630a]` when fully bright |
|
||
|
||
Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src ptr, `[0x630d]`=brightness offset, `[0x6316]`=step, `[0x630a]`=active flag.
|
||
|
||
### Entity Mini-VM / Record-Player Context (000c:f6b8-000c:f98b)
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:f6b8` | `record_table_get_by_index` | Bounds check `param < [0x8c88]`; return `word at [0x8c84 + param*4]`. Table at 0x8c84 |
|
||
| `000c:f6e8` | `entity_vm_stack_init_with_data` | Init stack ptrs at `[ptr+0xcc..+0xd4]` pointing to self; max depth 199; copies optional initial data |
|
||
| `000c:f772` | `entity_vm_state_copy` | Copies 200 bytes (100 words from `[src+4]` to `[dst+4]`), then copies 4 words at `+0xcc..+0xd2` |
|
||
| `000c:f7c7` | `entity_vm_stack_push_frame` | Push call-frame: saves ret offset at `[ptr+0xd4]`, decrements `[ptr+0xcc]` by param_size, zeroes new frame |
|
||
| `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params |
|
||
| `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full |
|
||
| `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty |
|
||
| `000c:f94f` | `entity_vm_counter_add` | `[ptr+0xd6] += param_2`; simple accumulator |
|
||
| `000c:f98b` | `entity_vm_set_field_da_to_global` | Writes `[param_2+0xda far-ptr + 2]` into `[0x8c94]` |
|
||
|
||
Notes:
|
||
- Field offsets `+0xcc`=VM stack ptr, `+0xce/+0xd0`=segment regs, `+0xd2`=base, `+0xd4`=frame depth, `+0xd6`=counter, `+0xd8/+0xda/+0xdc`=VM position/bounds.
|
||
- The 200-byte body region at `[ptr+4..+0xcc]` holds 100 words of script/state payload.
|
||
- The pair-stack (field `+0x80`) is separate — likely pass/return value stack for the mini-script.
|
||
|
||
## Raw 000c Cursor Zone / Slot Array / String Queue Batch (new)
|
||
|
||
Three utility subsystems identified in `000c:e6d9-000c:eadd`, plus companion slot array API.
|
||
|
||
### Cursor / Directional Zone Classifier
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:e6d9` | `cursor_zone_quadrant_classify` | Splits screen by `[0x63d6]/2` and `[0x63d8]/2` vs bounds `[0x8c6c..0x8c72]`; returns directional code from 9-word table at `0x6401` |
|
||
|
||
Zone table layout (9 entries): NW/N/NE / W/Center/E / SW/S/SE based on horizontal threshold at `0x8c6c/0x8c70` and vertical at `0x8c6e/0x8c72`.
|
||
|
||
### Slot Array System (000c:ea53-000c:ee44)
|
||
A complete 29-slot menu/choice array with fixed stride 0x15 bytes, base at `[ptr+0x67]`, count at `[ptr+0x7a]`:
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:ea53` | `entity_slot_count_update_and_notify` | Sets `[ptr+0x72]=param-1`; calls `slot_array_get_current_entry` and `slot_array_find_and_dispatch`; calls `vtable[0]()` when `+0x75` flags set |
|
||
| `000c:eba5` | `slot_array_dispatch_matching` | Walks 0xb-stride array from `[ptr+4]`; calls thunk for each entry where `[entry+9]==param_4` |
|
||
| `000c:ec30` | `slot_array_dispatch_if_nonempty` | Returns `0xffff` if count < 1; else dispatches |
|
||
| `000c:ec9e` | `slot_array_find_and_dispatch` | Searches 0xb-stride array for `[entry+9]==param_4`; calls thunk on first match |
|
||
| `000c:ecf5` | `slot_array_push_entry` | Copies named string to `[base+0xc]`; writes 6 param words at `+0x12..0x20`; increments count |
|
||
| `000c:edb0` | `slot_array_push_raw` | Copies 0x15-byte raw entry from `param_2`; increments count |
|
||
| `000c:edf7` | `slot_array_pop` | Decrements `[ptr+0x7a]`; asserts `>= 0` |
|
||
| `000c:ee19` | `slot_array_init` | Sets `[ptr+0x78]=0`, `[ptr+0x76]=0`, `[ptr+0x75]=1` (active flag) |
|
||
| `000c:ee32` | `slot_array_clear_flags` | Clears `[ptr+0x74]=0`, `[ptr+0x75]=0` |
|
||
| `000c:ee44` | `slot_array_get_current_entry` | Returns `ptr + [ptr+0x7a]*0x15 + 0x67` (current entry ptr); 0 if count <= 0 |
|
||
|
||
### String Queue
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:eadd` | `string_queue_push` | Appends string to 10-entry queue at `[ptr+4]`; count at `[ptr+2]`; sets `[ptr+0xd]=param_4` |
|
||
|
||
### Additional VM-Adjacent Helpers
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:f2e7` | `entity_call_vtable_entry_10_if_valid` | Null-guard: calls `(*[ptr+8+0x10])()` if param_1 non-null |
|
||
| `000c:f39f` | `string_table_lookup` | Searches `[0x65bc/0x65be]` table by key string; returns matching words to out-params |
|
||
|
||
## Raw 000c Cursor Nav Dispatcher / State Reset Batch
|
||
|
||
Cursor navigation subsystem in `000c:d3e9–000c:db68`. Manages directional zone changes, mouse button reads, keyboard scancodes, and entity vtable dispatch for interactive UI cursors.
|
||
|
||
### Cursor Navigation Fields (entity object offsets)
|
||
| Offset | Purpose |
|
||
|--------|---------|
|
||
| `+0x32` | Current zone code (0–8) |
|
||
| `+0x33` | Previous zone code |
|
||
| `+0x37–+0x3a` | Directional booleans: N/S/W/E |
|
||
| `+0x3f–+0x42` | Mouse button flags |
|
||
| `+0x45` | Last keyboard scancode |
|
||
| `+0x47` | Navigation index |
|
||
|
||
Globals: `[0x63da]` = mouse button state, `[0x63d6]/[0x63d8]` = cursor X/Y, `[0x638e]` and `[0x6346]` = reference data tables.
|
||
|
||
### Functions
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:dac1` | `cursor_nav_state_reset` | Zeros all directional/button flags; sets `[+0x32/+0x33]=0xff`, `[+0x47]=0xffff` |
|
||
|
||
## Top-40 Most-Called Far-Call Targets (NE Fixup Resolution)
|
||
|
||
Named via systematic analysis of 11,692 NE relocation fixup entries. These are the functions most frequently called through the `CALLF 0x0000:ffff` thunk mechanism.
|
||
|
||
### Tier 1: Top 20 (73+ callers)
|
||
|
||
| Rank | Address | Name | Calls | Description |
|
||
|------|---------|------|-------|-------------|
|
||
| 1 | `000a:44fd` | *(no function in Ghidra)* | 331 | Analysis gap at seg091:00fd. In comutils.c segment near joystick code. Needs manual function creation. |
|
||
| 2 | `0003:ac7e` | `mem_alloc` | 272 | Allocation wrapper → seg082:0000 (`0009:a200`) |
|
||
| 3 | `0008:dbec` | `entity_word_list_destroy` | 238 | Already named. Frees entity word-list buffer. |
|
||
| 4 | `0003:a751` | `mem_free` | 207 | Free wrapper → seg082:007a (`0009:a27a` = `mem_free_checked`) |
|
||
| 5 | `0008:bb4f` | `mem_alloc_far` | 174 | Thin wrapper → `mem_alloc` |
|
||
| 6 | `0003:a897` | `far_memcpy` | 165 | REP MOVSW + trailing MOVSB |
|
||
| 7 | `0005:088f` | `entity_get_type_word` | 130 | Returns type word from table 0x7df9 indexed by slot |
|
||
| 8 | `000b:358d` | `sprite_tree_accumulate_pos` | 122 | Recursively sums X/Y offsets (+0x21/+0x23) through linked child nodes (+0x19/+0x1b), copies 8-byte position block via far_memcpy |
|
||
| 9 | `0008:ce3d` | `entity_call_two_vtables` | 118 | Calls vtable[+4] at entity+0x1e and +0x28 |
|
||
| 10 | `0004:26cd` | `nop_void_stub` | 118 | Empty function, returns void |
|
||
| 11 | `0008:ce00` | `entity_call_two_vtables_base` | 117 | Calls vtable[0] at entity+0x1e and +0x28 |
|
||
| 12 | `0008:bb8c` | `entity_check_flag_0x4000` | 115 | Short-circuits if flag 0x4000 set at +0x16 |
|
||
| 13 | `0008:cda7` | `entity_free_both_word_lists` | 115 | Frees word lists at entity+0x1e and +0x28 if optional pointers at +0x24/+0x26 and +0x2e/+0x30 non-null. Both call `entity_word_list_free_existing`. |
|
||
| 14 | `0004:26d2` | `nop_void_stub_b` | 111 | Empty function, returns void |
|
||
| 15 | `000a:45fe` | `runtime_init_or_abort` | 108 | Reentrancy-guarded init. Flag at 0x44a4; flushes via FUN_000a_4a56, then calls `crt_exit_wrapper(1)`. Hidden code gap 0x4616-0x4643. |
|
||
| 16 | `0004:3324` | `nop_return_zero` | 95 | Returns 0 |
|
||
| 17 | `0009:c563` | `event_queue_push` | 82 | Circular buffer enqueue. Ring index (+0xe) masked 0x3f, slot masked 0xfff8. Writes event type word + data byte pair. |
|
||
| 18 | `0005:c448` | `list_remove_and_free` | 74 | Unlinks node from linked list via FUN_0005_c495, optionally calls `mem_free` if bit 0 of flags set |
|
||
| 19 | `000b:2e00` | *(no function in Ghidra)* | 74 | Analysis gap at seg109:0000. Needs manual function creation. |
|
||
| 20 | `0009:1f12` | `dos_file_lseek` | 73 | DOS LSEEK (INT 21h AH=42h) wrapper with error reporting to 0x867a |
|
||
|
||
### Tier 2: Ranks 21-40 (56-73 callers)
|
||
|
||
| Rank | Address | Name | Calls | Description |
|
||
|------|---------|------|-------|-------------|
|
||
| 21 | `0009:3600` | `rotating_buffer_advance` | 73 | Advances 5-slot circular counter at 0x3eb6, zeros pointer in table at 0x867c, dispatches via jump table |
|
||
| 22 | `0009:943a` | `entity_rect_compare_and_dispatch` | 68 | Compares bounding rectangles of two entities, dispatches based on flag bits 4/2/1 at +0x16 |
|
||
| 23 | `0009:1e61` | `dos_file_close` | 65 | DOS file close (INT 21h), error reporting, sets handle to -1 |
|
||
| 24 | `0005:e252` | *(unnamed — unclear)* | 65 | Copies 11 words from Phar Lap extender area (FUN_0000_12c6+5), then calls thunk. Interrupt/trampoline setup? |
|
||
| 25 | `0003:dbcc` | `crt_format_string` | 64 | MetaWare High C formatting wrapper. Calls FUN_0003_bb92 with runtime format dispatch table. |
|
||
| 26 | `0007:5a00` | *(no function in Ghidra)* | 64 | High-traffic raw target at `seg043:0000`. Earlier `debris_spawn` / seg001 mapping was rejected after checking relocation labels. Still needs manual function creation and direct analysis. |
|
||
| 27 | `000a:4742` | `assert_buffer_valid` | 63 | Validates handle: asserts param_2 == cookie at 0x45a6 and param_1 < limit at 0x87e0 |
|
||
| 28 | `0009:9216` | `entity_conditional_render_dispatch` | 63 | Checks entity flag bits 4 and 1 at +0x16, dispatches to vtable[+0xc] or thunk |
|
||
| 29 | `0008:cb2c` | `entity_flag20_clear_and_update_target` | 61 | *(already named)* Clears flag bit 0x20, writes target +0x12/+0x14, calls refresh |
|
||
| 30 | `0008:cb5c` | `entity_flag20_set_and_init_target` | 61 | *(already named)* Sets flag bit 0x20, inits target if zero, calls refresh |
|
||
| 31 | `0007:7306` | `entity_create_stack_object` | 58 | Allocates 0xCC bytes on stack, inits via `object_init_zero_fields` (0005:c400), calls thunk |
|
||
| 32 | `0007:8709` | `entity_mark_dirty_and_sync_tile_aux` | 58 | *(already named)* Syncs tile aux, sets flag bit 0x04 at +0x42 |
|
||
| 33 | `0007:87c5` | `entity_set_flag20_from_field42` | 58 | Reads entity+0x42/+0x44, calls `entity_flag20_set_and_init_target` with those values |
|
||
| 34 | `0007:8508` | `entity_table_lookup_and_dispatch` | 58 | *(already named)* Searches table at 0x2b46, dispatches via indirect jump |
|
||
| 35 | `0007:8920` | `entity_call_vtable_slot0c` | 58 | *(already named)* Calls vtable entry at +0x0c |
|
||
| 36 | `000a:b988` | `sprite_node_get_or_traverse` | 57 | If child pointer at +0x19/+0x1b non-null, traverses; otherwise returns leaf value |
|
||
| 37 | `0003:a98b` | `crt_signed_div32` | 56 | Entry: adjusts near→far stack, sets CX=0 (signed quotient), jumps to `crt_div32_impl` |
|
||
| 38 | `000a:7b44` | `nop_return_void_a` | 56 | Empty function (default vtable slot?) |
|
||
| 39 | `000a:7b49` | `nop_return_void_b` | 56 | Empty function (default vtable slot?) |
|
||
| 40 | `000a:7b53` | `nop_return_void_c` | 56 | Empty function (default vtable slot?) |
|
||
|
||
### Supporting Functions Discovered
|
||
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `000b:3a00` | `sprite_tree_sum_x_offset` | Recursive: sums field +0x21 through child chain +0x19/+0x1b |
|
||
| `000b:3a35` | `sprite_tree_sum_y_offset` | Recursive: sums field +0x23 through child chain +0x19/+0x1b |
|
||
| `0003:a845` | `crt_exit_wrapper` | Calls `crt_exit_impl(param,0,0)` |
|
||
| `0003:a7ee` | `crt_exit_impl` | Full C exit: atexit handlers, stdio flush, MetaWare runtime cleanup |
|
||
| `0003:a9a8` | `crt_div32_impl` | 32-bit division core. CX flags: bit0=unsigned, bit1=modulo, bit2=negate |
|
||
| `0005:c400` | `object_init_zero_fields` | Zeros fields +0x25, +0x29, +0x31, +0x32 of a struct. Returns pointer. |
|
||
| `000a:4440` | `joystick_read_axes_and_buttons` | Reads PC game port 0x201. Times axis responses, reads button nibble to 0x44a2 |
|
||
| `000b:3380` | `sprite_node_is_dirty` | Checks flags at obj+0x29 & 3 == 1 or 3 → returns bool |
|
||
| `000b:33a6` | `sprite_node_mark_dirty` | If not dirty, calls FUN_000b_3965 with mode=3 to invalidate |
|
||
|
||
### Tier 3: Ranks 41-60 (42-56 callers)
|
||
|
||
| Rank | Address | Name | Calls | Description |
|
||
|------|---------|------|-------|-------------|
|
||
| 41 | `000a:7b58` | `nop_return_zero_b` | 56 | Returns 0 (default vtable slot) |
|
||
| 42 | `000b:3ab2` | `sprite_node_dispatch_event` | 56 | Large event dispatch: checks event type (2/4/8/0x100), updates global focus ptr at [0x4fd0:4fd2], dispatches via vtable methods [+0x14/+0x18/+0x20/+0x24] by event code. Switch table for 16 event types. |
|
||
| 43 | `000a:48ff` | *(no function in Ghidra)* | 55 | Analysis gap in comutils.c segment |
|
||
| 44 | `000b:3362` | `sprite_tree_unwind_check` | 55 | Validates SS == param_2 (stack segment guard), then decrements global counter at [0x4fd6] |
|
||
| 45 | `000b:40ee` | `sprite_node_update_and_dispatch` | 55 | If `sprite_node_is_dirty` returns false: marks dirty, calcs accumulated bounds via `sprite_tree_get_accumulated_bounds` (3ed8), then dispatches via thunk |
|
||
| 46 | `000a:7b5f` | `vtable_stub_trampoline` | 55 | Calls through fixup thunk (forwarder to another function) |
|
||
| 47 | `000a:7b78` | `nop_return_void_e` | 55 | Empty function (default vtable slot) |
|
||
| 48 | `000a:7b7d` | `nop_return_void_f` | 55 | Empty function (default vtable slot) |
|
||
| 49 | `000a:7b4e` | `nop_return_void_d` | 54 | Empty function (default vtable slot) |
|
||
| 50 | `000b:330c` | `sprite_tree_dispatch_wrapper` | 52 | Pure thunk wrapper: calls through fixup |
|
||
| 51 | `0009:2034` | `dos_file_seek` | 51 | INT 21h AH=42h (LSEEK). Takes file object ptr, extracts handle at obj+4, seeks to offset param. Error reporting to [0x867a]. |
|
||
| 52 | `0005:0466` | `entity_resolve_slot_ptr` | 50 | *(already named)* |
|
||
| 53 | `0003:a880` | *(no function in Ghidra)* | 49 | Analysis gap in CRT segment |
|
||
| 54 | `0006:170c` | `tile_class_get_byte` | 47 | Looks up class data: indexes into table at [0x7e1e] by (*param_1 * 0x79), returns byte at offset +0xc |
|
||
| 55 | `000b:4097` | `sprite_dispatch_with_event` | 45 | Pushes event params + global [0x49c2:0x49c4], calls thunk |
|
||
| 56 | `0005:02c1` | `entity_is_type_match` | 43 | Compares *param_1 against global at [0x27c8], returns 1 if equal, 0 otherwise |
|
||
| 57 | `0003:ad75` | *(no function in Ghidra)* | 43 | Analysis gap in CRT segment |
|
||
| 58 | `000a:e709` | `render_dispatch_by_flag` | 43 | Dispatches between two thunk paths based on boolean flag at stack+0x10 |
|
||
| 59 | `0003:d0ff` | `crt_sprintf_wrapper` | 42 | Calls FUN_0003_bb92 (format engine) with rearranged params and string constant at 0x67ac |
|
||
| 60 | `000b:326e` | `sprite_node_destroy` | 42 | Destructor: sets vtable ptr to 0x501a, clears global [0x4fd0:4fd2] if self, releases child nodes, calls mem_free via thunk |
|
||
|
||
### Updated Analysis Gaps
|
||
|
||
`0007:5a00` / `0007:5b6f` reconciliation:
|
||
- The earlier standalone seg001 port hypothesis in this subrange was wrong.
|
||
- Relocation data places raw `0007:5a00` at `seg043:0000`, and the already-named helper at `0007:5b6f` sits at `seg043:016f`.
|
||
- Because of that segment placement, standalone seg001 names such as `debris_spawn` (`0x7490`) and `entity_die` (`0x75ff`) should NOT be ported into this raw range.
|
||
- `0007:5b6f` currently remains `entity_set_at_target_update_facing` from direct raw analysis; its behavioral name is no longer in conflict with the standalone seg001 `entity_die` note.
|
||
- Additional resolved call targets inside the missing seg043 block were annotated in Ghidra from relocation data:
|
||
- `0007:5a8a` -> `entity_set_event_type_checked`
|
||
- `0007:5a98` -> `FUN_0008_cc01` (timer-related flag/event helper; tests `+0x16 & 0x2`, sets `+0x16 |= 0x800`, copies event field `+0x06` to `+0x22`, checks `0x1000`, then conditionally dispatches)
|
||
- `0007:5b36` -> `entity_get_type_word`
|
||
- `0007:5b44` -> `saveslot_read_entry_flags`
|
||
- `0007:5bb8` -> `entity_is_type_match`
|
||
- `0007:5c49` -> `entity_class_get_flag20`
|
||
- `0007:5c8b` -> `mem_alloc_far`
|
||
- Current boundary caveat:
|
||
- Ghidra likely split the real seg043 routine incorrectly. `0007:5b6f` has no inbound xrefs, while relocation-resolved calls exist on both sides of it inside the same segment window. Treat the current `0007:5b6f` label as a behavioral anchor for one internal block, not yet as a proven standalone function boundary.
|
||
- Standalone seg043 disassembly now strengthens that conclusion: real prologues are at raw `0007:5a90`, `0007:5b7a`, and `0007:5c1c`, so the current `0007:5b6f` boundary demonstrably overlaps an earlier function.
|
||
|
||
| Address | NE Segment | Callers | Notes |
|
||
|---------|-----------|---------|-------|
|
||
| `000a:44fd` | seg091:00fd | 331 | #1 most-called target! In comutils.c segment. |
|
||
| `000b:2e00` | seg109:0000 | 74 | Start of segment 109. |
|
||
| `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. |
|
||
| `000a:48ff` | seg091:04ff | 55 | In comutils.c segment near joystick code. |
|
||
| `0003:a880` | seg005:0880 | 49 | In CRT segment near `far_memcpy`. |
|
||
| `0003:ad75` | seg005:0d75 | 43 | In CRT segment near `mem_alloc`. |
|
||
| `000a:454d` | seg091:014d | 32 | In comutils.c segment. |
|
||
|
||
### Tier 4: Ranks 61-80 (29-42 callers)
|
||
|
||
| Rank | Address | Name | Calls | Description |
|
||
|------|---------|------|-------|-------------|
|
||
| 61 | `000b:30a5` | `sprite_tree_forward_wrapper` | 42 | Pure thunk forwarder |
|
||
| 62 | `0008:bc27` | `entity_set_event_type_checked` | 41 | *(pre-existing name)* Sets event code at +0x06 with range/timer checks |
|
||
| 63 | `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | 40 | *(pre-existing name)* Constructor: alloc 0x40, vtbl 3AA6, flag 0x200 |
|
||
| 64 | `0005:1565` | `entity_action_by_type_dispatch` | 39 | Checks entity type against whitelist (0x432,0x5a0,0x1fd,0x1fe,0x8f,0x59f,0x2b3,0x2ca), dispatches by flags at [0xc76] and [0x85f] |
|
||
| 65 | `0008:4bba` | `channel_slot_enable` | 39 | Sets enable byte=1 in 5-slot table at 0x84ca (slot * 0xd stride) |
|
||
| 66 | `0009:6f5a` | `vga_palette_write` | 38 | Writes RGB triplets to VGA DAC (port 0x3C8/0x3C9). Range param_2..param_3 from palette data at *param_1 |
|
||
| 67 | `0009:8ef6` | `line_draw_dispatch` | 38 | Compares abs(dx) vs abs(dy) to determine major axis, dispatches to appropriate line draw routine |
|
||
| 68 | `000a:7b30` | `nop_return_void_g` | 38 | Empty function (default vtable slot) |
|
||
| 69 | `000a:7b3f` | `nop_return_void_h` | 38 | Empty function (default vtable slot) |
|
||
| 70 | `0009:6e7f` | `palette_free_if_set` | 35 | Frees existing palette data if ptr non-null, checks alignment |
|
||
| 71 | `000a:7b35` | `nop_return_void_i` | 35 | Empty function (default vtable slot) |
|
||
| 72 | `0009:c433` | `event_queue_align_index` | 34 | Returns `param_1 & 0xFFF8` — aligns ring index to 8-byte event slot boundary |
|
||
| 73 | `0009:2156` | `dos_file_get_size` | 33 | Saves file position, does INT 21h AH=42h AL=02 (seek to end), restores position. Returns file size in DX:AX |
|
||
| 74 | `000a:2c41` | `list_iterate_next` | 33 | Linked list iterator: if *out==0 returns first from obj+2; else follows next at ptr+2/+4. Returns bool (has more) |
|
||
| 75 | `000a:454d` | *(no function in Ghidra)* | 32 | Analysis gap in comutils.c segment |
|
||
| 76 | `000b:2446` | `sprite_clear_redraw_flag` | 31 | Clears flag at obj+0x17e, then dispatches via thunk |
|
||
| 77 | `0005:1238` | `entity_get_class_word` | 30 | Looks up table at [0x7e01] indexed by *param_1 * 2, returns word. Sister of `entity_get_type_word` (which uses [0x7df9]) |
|
||
| 78 | `000b:1446` | `display_null_check_dispatch` | 30 | Null-checks far ptr params, dispatches to different thunks based on result |
|
||
| 79 | `000d:85da` | `map_object_set_dirty_flag` | 29 | Sets byte at global_obj[0x6828]+0x40 = 1 if global non-null, then calls thunk |
|
||
| 80 | `0005:1511` | `entity_destroy_trampoline` | 29 | Pure thunk forwarder to entity destruction |
|
||
|
||
---
|
||
|
||
## Deep Analysis: Coordinate Transform System
|
||
|
||
### `world_to_screen_coords` at `0004:e7bd` (NE seg018:07bd)
|
||
|
||
**Signature:**
|
||
```c
|
||
void world_to_screen_coords(int world_x, int world_y, int *screen_x, int *screen_y)
|
||
```
|
||
|
||
**Isometric Projection Math:**
|
||
```
|
||
screen_x = (world_x - world_y) / 2 - camera_x // SAR 1 (signed divide)
|
||
screen_y = (world_x + world_y) / 4 - camera_y // SHR 2 (unsigned divide)
|
||
```
|
||
|
||
Camera globals: `g_scroll_offset_x` (DS:0x2bb7), `g_scroll_offset_y` (DS:0x2bb9).
|
||
|
||
**Assembly detail:**
|
||
- `SAR AX, 1` for screen_x — signed arithmetic shift preserves sign for negative (world_x - world_y) differences
|
||
- `SHR AX, 2` for screen_y — unsigned logical shift (sum world_x + world_y is always positive)
|
||
- The 2:1 ratio (÷2 for X, ÷4 for Y) produces the classic 2:1 isometric diamond tile shape
|
||
|
||
**Coordinate axes on screen:**
|
||
- World X axis → lower-right on screen (+0.5 screen_x, +0.25 screen_y per world unit)
|
||
- World Y axis → lower-left on screen (-0.5 screen_x, +0.25 screen_y per world unit)
|
||
- Camera subtraction converts absolute world-space to viewport-relative screen coordinates
|
||
|
||
**Callers (17 across 8 NE segments):**
|
||
|
||
| Call site | NE Segment | Context |
|
||
|-----------|-----------|---------|
|
||
| `0004:7d6f` | seg012 | Map/tile rendering |
|
||
| `0005:0305` | seg021 | Entity system |
|
||
| `0005:432f` | seg021 | Entity placement |
|
||
| `0005:4457` | seg021 | Entity placement |
|
||
| `0005:6f8f` | seg022 | Entity rendering |
|
||
| `0005:7263` | seg022 | Entity rendering |
|
||
| `0007:2262` | seg040 | `snap_entity_to_ground` — ground alignment |
|
||
| `0007:237d` | seg040 | Ground snap dispatch |
|
||
| `0007:cf4e` | seg049 | Entity positioning |
|
||
| `0007:d039` | seg049 | Entity positioning |
|
||
| `0007:d43f` | seg049 | Entity positioning |
|
||
| `0007:d6fe` | seg049 | Entity positioning |
|
||
| `0008:3223` | seg053 | Entity-to-screen render setup |
|
||
| `0008:32e7` | seg053 | Entity-to-screen render setup |
|
||
| `0008:334b` | seg053 | Entity-to-screen render setup |
|
||
| `000b:858b` | seg115 | Sprite system |
|
||
| `000b:f100` | seg120 | Sprite system |
|
||
|
||
**Entity struct layout (from seg053 caller at `0008:31f6`):**
|
||
```
|
||
entity_array_base = far ptr at [DS:0x2cff]
|
||
entity_struct_size = 19 bytes (0x13)
|
||
entity.world_x = offset +0x0a (word)
|
||
entity.world_y = offset +0x0c (word)
|
||
```
|
||
|
||
### Comparison: Two Coordinate Transform Functions
|
||
|
||
| Property | `world_to_screen_coords` (0004:e7bd) | `world_to_screen_isometric` (0007:be67) |
|
||
|----------|---------------------------------------|----------------------------------------|
|
||
| Input type | Fine-grained world units (entity positions) | Coarse tile-grid units (map rendering) |
|
||
| screen_x | `(wx - wy) / 2 - cam_x` | `(wx + sx) + (wy + sy) * 2` |
|
||
| screen_y | `(wx + wy) / 4 - cam_y` | `(wy + sy) * 2 - (wx + sx)` |
|
||
| Camera handling | Subtracted after transform | Added before transform |
|
||
| Operations | Division (SAR/SHR) | Multiplication (SHL) |
|
||
| Aspect ratio | 2:1 (from /2 : /4) | 2:1 (from 1 : 2 multipliers) |
|
||
|
||
Both functions implement the same 2:1 isometric projection but at different coordinate scales. `world_to_screen_coords` divides down from fine world units while `world_to_screen_isometric` multiplies up from coarse tile units.
|
||
|
||
### Adjacent Function: `map_position_equal` at `0004:e784`
|
||
|
||
Compares two 5-byte `map_position` structs: `{ x:word, y:word, layer:byte }`. Returns 1 (AL) if all three fields match, 0 otherwise. Located immediately before `world_to_screen_coords` in seg018.
|
||
|
||
---
|
||
|
||
### Tier 5: Ranks 81-100 (25-29 callers)
|
||
|
||
| Rank | Address | Name | Calls | Description |
|
||
|------|---------|------|-------|-------------|
|
||
| 81 | `0009:1c00` | `dos_file_handle_init` | 29 | Inits 6-byte file handle struct: dword=0, word+4=0xFFFF (invalid). Aborts on null ptr |
|
||
| 82 | `0008:75f3` | `entity_get_ptr` | 29 | *(pre-existing)* Looks up entity far ptr from table at DS:0x39b0, indexed by id*4 |
|
||
| 83 | `0006:0208` | `entity_class_get_flag4` | 29 | Returns bit 2 of classinfo byte at [0x7e1e]+*p1*0x79+0x13 → 0 or 1 |
|
||
| 84 | `000a:30d7` | `list_node_set_if_context` | 29 | Sets node fields +2/+4 if params match context globals at 0x45a6/0x45a8 |
|
||
| 85 | `0009:c45f` | `object_init_and_get_next` | 29 | Calls `object_init_zero_fields` then returns *(result+2) — init+accessor combo |
|
||
| 86 | `0004:d7a0` | `object_deref_get_word4` | 28 | Dereferences far ptr chain: returns word at *(*(param_1)+4) |
|
||
| 87 | `000a:5276` | `debug_check_flag_45aa` | 28 | If byte at DS:0x45aa non-zero, calls thunk (diagnostic/assert check) |
|
||
| 88 | `0003:d94f` | `far_memset` | 28 | Wrapper reordering params for CRT memset impl at 0003:d92b (odd-aligned, word-fill loop) |
|
||
| 89 | `000a:7b3a` | `nop_return_void_j` | 28 | Empty function (default vtable slot) |
|
||
| 90 | `0008:ca18` | `entity_pair_sync_b` | 27 | *(pre-existing)* Pairwise sync wrapper direction B |
|
||
| 91 | `0008:bd20` | `entity_sprite_set_target_pos` | 27 | *(pre-existing)* Sets flag 0x1000, copies player pos to entity +0x0a/+0x0c |
|
||
| 92 | `0009:3ceb` | `buffer_release_and_dispatch` | 27 | Frees far ptr at obj+0x3b if set, nulls it; conditionally dispatches on bit 0 |
|
||
| 93 | `0005:09b4` | `entity_get_flags_byte` | 27 | Reads byte from [0x7dfd]+id, conditionally extends with classinfo byte at [0x7e1e]+id*0x79+0xf |
|
||
| 94 | `0005:0fbb` | `entity_lookup_sprite_word` | 27 | Returns word from [0x7e05]+*p1*2 — sprite/visual index table |
|
||
| 95 | `0008:d27e` | `entity_dispatch_trampoline_b` | 26 | Pure forwarder thunk (CALLF thunk only) |
|
||
| 96 | `0005:0376` | `entity_resolve_base_type` | 26 | Walks entity class hierarchy (bit 8 in [0x7e01]) via [0x7ded], returns base type from [0x7df1] |
|
||
| 97 | `000b:2492` | `sprite_redraw_if_needed` | 26 | If redraw flag at +0x17e is clear, calls update routine + thunk |
|
||
| 98 | `0003:e4d3` | `dos_file_open_wrapper` | 26 | Zeros output byte, delegates to file open impl at 0003:bb92 |
|
||
| 99 | `0005:033e` | `entity_resolve_base_parent` | 25 | Same hierarchy walk as `entity_resolve_base_type` but returns parent from [0x7ded] |
|
||
| 100 | `000a:87fd` | `render_clip_rect_to_viewport` | 25 | Clips 4 rect params to viewport bounds at [0x4014], sets dirty flag at 0x8a16, increments draw counter at 0x4716 |
|
||
|
||
**Entity Table Pointers (DS-relative, discovered in tier 5):**
|
||
|
||
| DS Offset | Type | Stride | Purpose |
|
||
|-----------|------|--------|---------|
|
||
| `0x7dfd` | byte[] | 1 | Entity flags byte (entity_get_flags_byte) |
|
||
| `0x7e01` | word[] | 2 | Entity class flags (bit 8 = has parent in hierarchy) |
|
||
| `0x7e05` | word[] | 2 | Entity sprite/visual index |
|
||
| `0x7ded` | word[] | 2 | Entity parent/hierarchy index |
|
||
| `0x7df1` | word[] | 2 | Entity base type word |
|
||
| `0x7e1e` | struct[] | 0x79 | Entity class detail records (121 bytes per class) |
|
||
|
||
### Analysis Gaps (No Function in Ghidra)
|
||
|
||
These high-traffic addresses need manual function creation in Ghidra (Script Manager or UI):
|
||
|
||
| Address | NE Segment | Callers | Notes |
|
||
|---------|-----------|---------|-------|
|
||
| `000a:44fd` | seg091:00fd | 331 | #1 most-called target! In comutils.c segment. |
|
||
| `000b:2e00` | seg109:0000 | 74 | Start of segment 109. |
|
||
| `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. |
|
||
| `0009:a200` | seg082:0000 | - | Target of `mem_alloc`. Start of segment 82. |
|
||
| `000c:db68` | `cursor_nav_update_and_dispatch` | Calls `cursor_zone_quadrant_classify`; updates `[+0x37..+0x3a]`; reads `[0x63da]`; switch on direction (0–8); maps scancodes 0x48/0x50/0x4b/0x4d/0x39 |
|
||
| `000c:d3e9` | `cursor_set_ref_and_dispatch` | Null-checks param; sets `*param_1 = &DAT_0000_638e`; calls dispatch |
|
||
| `000c:d710` | `cursor_set_ref2_and_dispatch` | Same pattern; sets `*param_1 = &DAT_0000_6346` |
|
||
| `000c:d75e` | `entity_call_vtable_1e_via_ptr` | Calls `(*[*param_1 + 0x3c])()` — vtable offset 0x1e |
|
||
| `000c:d775` | `entity_call_vtable_1e_via_ptr_b` | Near-identical to `d75e`; duplicate generated by compiler |
|
||
| `000c:d7c6` | `stub_noop_000c_d7c6` | Empty stub |
|
||
| `000c:d7cb` | `stub_noop_000c_d7cb` | Empty stub |
|
||
|
||
Direction code mapping (from `cursor_nav_update_and_dispatch` switch):
|
||
- 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW, 8=Center
|
||
|
||
### Timer Rate Sync (cursor subsystem)
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:e4e0` | `cursor_timer_rate_sync` | If `[0x63e0]` (cursor active) non-zero: copies PIT rate `[0x39ce]` into entity object at `[0x4458+0x24]`, clears `+0x26/+0x27`, then calls far thunk. Called from `cursor_nav_update_and_dispatch` at `000c:db97`. |
|
||
|
||
## Raw 000c UI Listbox Event Handler Batch
|
||
|
||
Analysis of `000c:880c` and `000c:88b4` — the primary UI event-dispatch island in segment `000c`.
|
||
|
||
### Overview
|
||
`ui_listbox_event_handler` (`000c:88b4`) is a **large UI widget event handler** (~8KB of code, `88b4` through `9daa`). It takes the signature `(entity_far_ptr, seg, event_far_ptr)` and is stored as a far-pointer vtable entry (no direct code xrefs found; referenced from data). It dispatches on the event code at `[event+0x6]` (keystroke/mouse codes) and updates entity fields to drive menu/listbox navigation state.
|
||
|
||
Guard function `entity_state_if_flag80_call_thunk` (`000c:880c`) sits just before the handler: it checks entity flag bit `0x80` at `[param_1+0x5b]` and gates a far thunk call.
|
||
|
||
### Functions
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:88b4` | `ui_listbox_event_handler` | Dispatches on event code `[event+0x6]`: confirm keys `0xd`/`0x20`/`0x152` → `entity_state_tick_dispatch`; nav keys `0x148`/`0x14b`/`0x14d`/`0x150` → same; list-prev `0x2c`/`0x3c`/`0x55` → `entity_state_advance_next_or_fallback_b`; list-next `0x2e`/`0x3e`/`0x53`/`0x73`/`0x75` → `entity_state_advance_next_or_fallback_a`; Esc `0x1b` → tick dispatch; comma `0x2c`/`0x3c` → `ac8f`; misc UI events `0x6f`/`0x7e`/`0x13b–0x143`/`0x241`/`0x410`/`0x420`/`0x426`/`0x42f`/`0x432`/`0x441` → various paths. Guards: `[BX+0x5b] & 0x80` (active), `& 0x100` (modal lock from `[0x604b]`, `[0x844]`). Large scan loop `0x100..0x27ff` at `000c:a80f` for option/device enumeration. Falls through to common retf at `9da7`. |
|
||
| `000c:880c` | `entity_state_if_flag80_call_thunk` | Guard: tests entity `[param_1+0x5b] & 0x80`; returns if clear; else calls far thunk. Active/visible gate for forwarding display/tick calls. |
|
||
| `000c:ace5` | `display_fullscreen_blit_with_entity` | If coords (param_2:param_1) are 0:0, calls wait thunk (`0x48c`=1164ms?); loads display object at `[0x4cd0]`, reads byte `[+0x5]` (video mode/palette), builds full-screen 640×480 rect (`0`..`0x27f`=639, `0`..`0x1df`=479), then calls far display function with entity + position params. Returns display-space coordinate pair in DX:AX. |
|
||
|
||
### Key Globals in this Handler
|
||
| Address | Meaning |
|
||
|---------|---------|
|
||
| `[0x7e22]` | String/resource pointer used as data context (`[0x7e24]` = flag) |
|
||
| `[0x604b]` | Modal active lock (nonzero = block most events) |
|
||
| `[0x844]` | Engine/game-ready flag (0 = block hardware toggle events) |
|
||
| `[0x604f]` | Toggle state for event `0x410` |
|
||
| `[0x6045]` | Toggle state for event `0x7e` |
|
||
| `[0x8638]` | Counter for event `0x432` (wraps 0x11→0x12..0x14→0x0) |
|
||
| `[0x5e82/0x5e84]` | Far pointer called when entity flag `0x40` is cleared |
|
||
| `[0x4cd0]` | Display/screen manager object far pointer |
|
||
|
||
### Event Code Reference Table (partial)
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `0x0d` / `0x20` | Enter / Space (confirm) |
|
||
| `0x1b` | Escape (cancel) |
|
||
| `0x2c` / `0x3c` | Comma / `<` (prev in list) |
|
||
| `0x2e` / `0x3e` / `0x53` / `0x73` | Period / `>` / S / s (next in list) |
|
||
| `0x55` / `0x75` | U / u (up action with far-ptr check) |
|
||
| `0x6f` | o (some option panel event) |
|
||
| `0x7e` | `~` (toggle event, gates on `[0x844]` and `[0x6045]`) |
|
||
| `0x13b` | Shift+F1 area |
|
||
| `0x13c` | Jump-to-display event |
|
||
| `0x13e` | Blanked event (gates on `[0x604b]`) |
|
||
| `0x141` / `0x142` / `0x143` | Option toggles (gate on `[0x844]`, `[0x2bca/0x2bc9]`) |
|
||
| `0x148` / `0x14b` / `0x14d` / `0x150` | Arrow keys (Up/Left/Right/Down) |
|
||
| `0x152` | Insert key (alias confirm) |
|
||
| `0x241` | Display position query → `display_fullscreen_blit_with_entity` |
|
||
| `0x410` | Toggle event with `[0x604f]`, gates on `[0x844]` |
|
||
| `0x420` | List-scan init (loop 0x100..0x27ff) |
|
||
| `0x426` | String-push event (calls DS:[0x7e22]) |
|
||
| `0x42f` | Push `0x103` thunk event |
|
||
| `0x432` | Counter cycle event (`[0x8638]` wrap 0x11→0x12, 0x14→0x0) |
|
||
| `0x441` | Pattern-fill loop event |
|
||
|
||
## NE Segment 21 Analysis — Timer/Event Dispatch System
|
||
|
||
**File**: `seg021_code_off_50200_len_4486.bin` | **File Offset**: 0x50200 | **Length**: 0x4486 bytes
|
||
**Ghidra Load**: RAM `0000:0000 – 0000:4485`, x86 16-bit Protected Mode, base 0x0000
|
||
**Functions**: 88 total (87 renamed + `input_keyboard_handler` pre-existing)
|
||
|
||
### Subsystem Summary
|
||
|
||
Segment 21 implements the **hardware-level timer interrupt and entity event dispatch system** — Crusader's real-time task scheduler. Key responsibilities:
|
||
|
||
- Programs and services the Intel 8253 **PIT timer** (I/O ports 0x40/0x43)
|
||
- Manages three **entity dispatch lists**: timer list (0x39d4), input list (0x39e3), render list (0x3a10)
|
||
- Maintains the **entity pool** at 0x39b0 (same pool as seg001; these segments share DS)
|
||
- Provides **event queue** (32-slot circular buffer at 0x31cc)
|
||
- Handles **save/load** serialization of the entire entity system
|
||
- Controls **keyboard/interrupt locks** and deferred scheduling
|
||
|
||
### Function Groups
|
||
|
||
#### Entity Pool Management (0x0207–0x0483)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x0207` | `entity_count_by_type_a` | Count entities matching type+event; filters DEAD flag (0x8) |
|
||
| `0x0297` | `entity_count_by_type_b` | Identical logic to 0x0207 (compiler duplicate) |
|
||
| `0x0327` | `entity_find_free_slot` | Scan pool for null entry; calls panic if full; returns slot or 0xFFFF |
|
||
| `0x038f` | `entity_register` | Write far ptr to entity_list, group to entity_data, vtable to registry; inc count |
|
||
| `0x044d` | `entity_get_ptr_raw` | Read entity far ptr from pool slot (may be null) |
|
||
| `0x0483` | `entity_get_ptr` | Safe wrapper: verifies non-null, returns offset only |
|
||
|
||
#### Event Dispatch (0x04f3–0x08be)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x04f3` | `entity_dispatch_reset_all` | Fires event code 0x21 (reset/init) to all entities |
|
||
| `0x050d` | `entity_clear_deferred_flags` | Clears DEFERRED bit (0x200) from up to N=0x3998 entities |
|
||
| `0x059e` | `entity_fire_event_broadcast` | Dispatch event to all matching entities; calls vtable[6]; respects 0x200 deferred flag |
|
||
| `0x06f4` | `entity_fire_event_type_include` | Fire only entities whose type IS in given list (up to 10, 0x0d=end) |
|
||
| `0x08be` | `entity_fire_event_type_exclude` | Fire only entities whose type is NOT in given list |
|
||
| `0x0a8e` | `input_keyboard_handler` | (pre-existing) OS-level key router: 0x0d=scroll+, 0x01=action, 0x2c=save, 0x44=load |
|
||
|
||
#### Entity Iterator / Linker (0x0bb7–0x106b)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x0bb7` | `entity_link` | Cross-link two entities; skips if flag 0x400 set |
|
||
| `0x0c34` | `entity_find_first` | Init iterator 0x39fa=3; find first entity matching saved type/event at 0x399a/0x399c |
|
||
| `0x0cec` | `entity_find_next` | Continue iterator from 0x39fa cursor |
|
||
| `0x0dad` | `timer_entity_find_by_event` | Find entity handling event in range 0xf0-0xf7; checks bit 0x1000; writes to 0x3993 |
|
||
| `0x0e82` | `entity_find_by_priority` | Walk priority chain at 0x39d4; find entity matching source/event at 0x3993 |
|
||
| `0x0fc8` | `entity_set_cursor` | Validate flag 0x800; set cursor 0x3993 = param_1 (slot) |
|
||
| `0x100c` | `entity_get_cursor` | Return entity at 0x39bf if valid and not dead |
|
||
| `0x106b` | `entity_relink` | Re-link: find by event, walk priority chain, call set-link vtable funcs |
|
||
|
||
#### Entity Lifecycle (0x1133–0x131d)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x1133` | `entity_unregister` | Full removal: dec sprite type count, vtable cleanup, dec total, update masks |
|
||
| `0x1202` | `entity_slot_clear` | Zero pool slot (0x39b0), registry slot (0x39ca), group data (0x39b4) |
|
||
| `0x1245` | `entity_layer_set` | Write 0x39c9 (active layer ID) if changed; set dirty flag 0x39a2 |
|
||
| `0x125d` | `entity_check_overdue` | If entity_is_overdue: set bit 0x40 on entity+0x16 |
|
||
| `0x127c` | `entity_is_overdue` | Return 1 if entity index > 0x39bf and flag 0x39c2 set |
|
||
| `0x129b` | `entity_list_call_update` | For all entities where entity+0x0e & param_3 != 0: call vtable[8] |
|
||
| `0x131d` | `entity_set_pending` | Write param to 0x3995 (next entity to register); error if already set |
|
||
|
||
#### Entity System Init/Shutdown (0x133e–0x1705)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x133e` | `entity_system_init` | Alloc all entity pool buffers (see decompiler comment); init three lists; clear event state |
|
||
| `0x14bc` | `entity_system_flush_normal` | Finalize (vtable[10]) then free all non-deferred active entities |
|
||
| `0x158d` | `entity_system_flush_deferred` | Same as flush_normal for deferred entities |
|
||
| `0x165c` | `entity_process_pending_deletes` | Free entities marked DEAD (flag & 0x8); dec 0x399e counter |
|
||
| `0x1705` | `entity_system_shutdown` | Full shutdown: flush normal, flush deferred, process deletes, free all pools |
|
||
|
||
#### Save / Load (0x1851–0x1d21)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x1851` | `event_queue_state_reset` | Zero ring buffer state tables (0x334e, 0x364e), queue ptrs (0x31c8/0x31ca) |
|
||
| `0x18ce` | `level_load` | Full level load: shutdown + reinit + deserialize all entities via vtable[12] |
|
||
| `0x1d21` | `save_game` | Serialize entity system: arrays + each entity via vtable[14]; magic check 0x3a21==0xed |
|
||
|
||
#### PIT Timer / Hardware (0x2300–0x2975)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x2300` | `pit_timer_program` | OUT 0x43, 0x36; OUT 0x40, lo; OUT 0x40, hi — raw PIT channel 0 program |
|
||
| `0x2316` | `pit_timer_set_hz` | Validates divisor <= 0xd688; stores at 0x39ce; calls pit_timer_program |
|
||
| `0x23a5` | `pit_timer_tick_handler` | Timer ISR: iterates 0x39d4 timer list, fires vtable callbacks per layer/mode |
|
||
| `0x25fc` | `timer_entity_active` | Check 0x3987/0x398b for active timer entity (mode-dependent) |
|
||
| `0x264c` | `timer_entity_get_current` | Get ptr from 0x3987 or 0x398b based on 0x3991 mode flag |
|
||
| `0x2668` | `timer_entity_enable` | Set ENABLED flag (0x400), inc counter, insert into timer list, reprograms PIT |
|
||
| `0x2745` | `timer_entity_disable` | Clear ENABLED, dec counter, reprograms PIT; if list empty calls interrupt_request_cancel |
|
||
| `0x2975` | `timer_recompute_hz` | Scan timer list; find smallest time_period (+0x38/+0x3a); call pit_timer_set_hz |
|
||
|
||
#### Interrupt / Lock Control (0x283a–0x294b)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x283a` | `interrupt_lock_acquire` | Re-entrant acquire on 0x31c7 (interrupt lock) |
|
||
| `0x2870` | `interrupt_lock_release` | Release 0x31c7 |
|
||
| `0x289b` | `entity_lock_acquire` | Re-entrant acquire on 0x39aa (entity system lock) |
|
||
| `0x28d5` | `entity_lock_release` | Release 0x39aa |
|
||
| `0x290d` | `interrupt_request_schedule` | Set deferred IRQ flags 0x39ab and 0x398f (or 0x39a9 in sync mode) |
|
||
| `0x294b` | `interrupt_request_cancel` | Clear IRQ request flags |
|
||
|
||
#### Timer Loop / Deferred State (0x2a5f–0x2ad8)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x2a5f` | `timer_event_loop` | **Main game loop**: polls player tick counter at 0x2de4; busy-waits; fires optional callback; stores delta to 0x3a00/0x3a02 |
|
||
| `0x2ac2` | `timer_deferred_reschedule` | If deferred mode flag 0x39b8 set, call reschedule |
|
||
| `0x2ad8` | `timer_snapshot_deferred` | Copy 0x39a9 → 0x39b8; call interrupt handler if 0x39a9 set |
|
||
|
||
#### Event Queue (0x2c73–0x3364)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x2c73` | `event_queue_drain` | Drain circular queue; call event_queue_dequeue while 0x31c8 != 0x31ca; reset state |
|
||
| `0x2ca2` | `mouse_button_check` | Return 1 if BIOS 0x31a4 bit 0x10 set AND 0x39af (mouse enable) set |
|
||
| `0x2cbc` | `stub_noop_2cbc` | Empty stub function |
|
||
| `0x2cd7` | `bios_keyboard_flags_write` | Write param to 0x400:0017 (BIOS keyboard flags at segment 0x40, offset 0x17) |
|
||
| `0x2cf2` | `input_event_dispatch` | Dispatch display event 0x10 to input list entities with flag 0x100 and 0xc bits set |
|
||
| `0x2dc3` | `event_queue_push` | Push event to circular queue (write ptr 0x31ca); calls event_queue_is_full check |
|
||
| `0x3276` | `keyboard_state_read` | INT 16h AX=0: read raw keyboard state into 0x31a4 |
|
||
| `0x328b` | `keyboard_acquire` | If not locked (0x31c6): INT lock, read keyboard, set lock flag |
|
||
| `0x32cc` | `keyboard_release` | If locked: unlock, clear 0x31c6 |
|
||
| `0x3304` | `event_queue_count` | Count pending events: 0x31ca - 0x31c8 (circular) |
|
||
| `0x333d` | `event_queue_is_full` | Return 1 if ((0x31ca+1) mod 32) == 0x31c8 |
|
||
| `0x3364` | `event_queue_dequeue` | Read from ring buffer (0x31cc + 0x31c8*0xc, entry size 0xc), advance read ptr |
|
||
|
||
#### Event Subscription and Bitmask Helpers (0x34dd–0x3878)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x34dd` | `event_queue_process_all` | Drain queue; for each event find listener entities in 0x39e3; call vtable[0x14] |
|
||
| `0x35e9` | `event_queue_set_mode` | Write low 2 bits to 0x334c; call keyboard_interrupt_call |
|
||
| `0x35fb` | `event_queue_set_param` | Write low 5 bits to 0x334d; call keyboard_interrupt_call |
|
||
| `0x360d` | `keyboard_interrupt_call` | INT 16h (raw BIOS keyboard services call) |
|
||
| `0x3630` | `entity_validate_indices` | Debug assert: verify entity+0x02 (slot_index) == pool position for all entities |
|
||
| `0x369b` | `typemask_set_bit` | Set bit at 0x3a04 + (param>>3), bit (param & 7) — entity type present bitmask |
|
||
| `0x36d4` | `typemask_clear_bit` | Clear bit in 0x3a04 bitmask |
|
||
| `0x370f` | `typemask_update` | If entity type has listeners (entity_find_first != 0): set bit; else clear |
|
||
| `0x3744` | `typemask_test_bit` | Test bit in 0x3a04; return 1 if entity type has registered listeners |
|
||
| `0x377d` | `event_subscription_set` | Set subscription bit in 0x3a08 buffer |
|
||
| `0x37b2` | `event_subscription_clear` | Clear subscription bit in 0x3a08 buffer |
|
||
| `0x37e9` | `event_subscription_update` | If entity has listeners: set bit; else clear (driven by entity_find_first result) |
|
||
| `0x3825` | `event_subscription_test` | Test subscription bit in 0x3a08; return 1 if subscribed |
|
||
| `0x3864` | `event_state_clear` | Zero entire 0x3a0c event use-count buffer (0x4000 bytes = 8192 uint16s) |
|
||
| `0x3878` | `event_use_count_increment` | Increment 64-bit counter at 0x3a0c[entity_event_type*4] |
|
||
|
||
#### Input / Render List (0x38c2–0x3ae9)
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x38c2` | `input_event_broadcast` | Dispatch input event 0x40 to all render-list entities with flag 0x40; uses counter 0x39ad |
|
||
| `0x39a1` | `subscribe_to_render_list` | Add entity to 0x3a10 list; set flag bit 0x40; inc 0x3a1f |
|
||
| `0x3a13` | `unsubscribe_from_render_list` | Remove entity from 0x3a10; clear bit 0x40; dec 0x3a1f |
|
||
| `0x3404` | `subscribe_to_input_list` | Add entity to 0x39e3 list; check flag 0x100; set bit 0x80; inc 0x39c3 |
|
||
| `0x3477` | `unsubscribe_from_input_list` | Remove entity from 0x39e3; clear bit 0x80; dec 0x39c3 |
|
||
| `0x3a78` | `entity_lists_init` | Init three linked lists with sentinel vtable 0x3a89; write head vtable 0x2d10 |
|
||
| `0x3ae9` | `entity_lists_reset` | Call external reset + reinit 0x39e3 and 0x39d4 lists |
|
||
|
||
### Entity Object Field Layout (as used in Seg21)
|
||
|
||
| Offset | Field | Type | Description |
|
||
|--------|-------|------|-------------|
|
||
| `+0x00` | vtable_ptr | far ptr | Pointer to entity's vtable dispatch table |
|
||
| `+0x02` | slot_index | uint16 | Entity's own slot number in pool |
|
||
| `+0x04` | source_type | uint16 | Source/owner entity type (event matching) |
|
||
| `+0x06` | event_type | uint16 | Event type this entity handles |
|
||
| `+0x08` | flags_byte | uint8 | Low 5 bits = sprite group ID |
|
||
| `+0x0e` | capability_mask | uint16 | Bitmask of supported event capabilities |
|
||
| `+0x16` | state_flags | uint16 | bit3=DEAD, bit8=REGISTERED, bit9=ACTIVE, bit10=ENABLED, bit11=HAS_TIMER, bit13=IS_IRQ_HANDLER |
|
||
| `+0x18` | flags2 | uint16 | bit6=IN_RENDER_LIST, bit7=IN_INPUT_LIST, bit9=DEFERRED |
|
||
| `+0x1e` | priority_chain | far ptr | Priority chain entries (entity_find_by_priority) |
|
||
| `+0x20` | priority_count | uint16 | Count of priority chain entries |
|
||
| `+0x38` | time_period_lo | uint16 | Timer period low word (PIT frequency calc) |
|
||
| `+0x3a` | time_period_hi | uint16 | Timer period high word |
|
||
|
||
### Vtable Layout (Seg21 usage)
|
||
|
||
| Slot | Byte offset | Prototype | Purpose |
|
||
|------|-------------|-----------|---------|
|
||
| [6] | `+0x0c` | `handle_event(entity, CS, type, param)` | Event callback |
|
||
| [8] | `+0x10` | `update(entity, CS, capability_mask)` | Per-tick update |
|
||
| [10] | `+0x14` | `finalize(entity, CS)` | Cleanup/shutdown |
|
||
| [12] | `+0x18` | `load(entity, CS, file_ptr, CS)` | Deserialize from save |
|
||
| [14] | `+0x1c` | `save(entity, CS, file_ptr, CS)` | Serialize to save |
|
||
| [16] | `+0x20` | `set_backref(entity, CS, list_ptr)` | Set back-reference |
|
||
| [20] | `+0x28` | `dispatch_callback(entity, CS, event_id, 0, data_ptr)` | Generic dispatch |
|
||
|
||
### Key Global Data (Seg21 — additions to DS)
|
||
|
||
| Address | Name | Description |
|
||
|---------|------|-------------|
|
||
| `0x31a4` | bios_key_state | Raw INT 16h keyboard state |
|
||
| `0x31c6` | keyboard_lock | Keyboard acquired flag |
|
||
| `0x31c7` | interrupt_lock | Interrupt lock flag (re-entrant) |
|
||
| `0x31c8` | queue_read_ptr | Event queue read index (0–31) |
|
||
| `0x31ca` | queue_write_ptr | Event queue write index |
|
||
| `0x31cc` | event_queue_base | Ring buffer, 32 entries × 0xc bytes |
|
||
| `0x334c` | queue_mode | Event queue mode bits (0–1) |
|
||
| `0x334d` | queue_param | Event queue param bits (0–4) |
|
||
| `0x39b0` | entity_list | Far ptr to entity far-ptr array (count×4) — **shared with seg001** |
|
||
| `0x39b4` | entity_data | Far ptr to group/sprite-ID array (count×2) |
|
||
| `0x39b9` | entity_max_count | Max capacity of entity pool |
|
||
| `0x39bb` | entity_count | Total registered entity count |
|
||
| `0x39c9` | active_layer | Current active entity layer/group ID |
|
||
| `0x39ca` | entity_registry | Far ptr to vtable dispatch array (count×4) — **shared with seg001** |
|
||
| `0x39ce` | pit_divisor | Current PIT timer divisor |
|
||
| `0x39d4` | timer_list | Intrusive linked list: timer-dispatch entities |
|
||
| `0x39e3` | input_list | Intrusive linked list: input-handler entities |
|
||
| `0x3a04` | typemask_buf | Far ptr to entity type present bitmask (0x480 bytes) |
|
||
| `0x3a08` | evt_sub_buf | Far ptr to event subscription bitmask (0x2400 bytes) |
|
||
| `0x3a0c` | evt_state_buf | Far ptr to event use-count table (0x4000 bytes) |
|
||
| `0x3a10` | render_list | Intrusive linked list: render-callback entities |
|
||
| `0x3a21` | save_magic | Must be 0xed (-0x13) for valid save |
|
||
| `0x3a70` | default_registry_vtable | Default vtable written to entity_registry slots on register |
|
||
| `0x3a89` | list_sentinel_vtable | Sentinel vtable written to list head nodes |
|
||
|
||
---
|