- **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`)
-`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`.
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
- 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.
-`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 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.
**`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.
- 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.
-`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.
- 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.
-`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.
| `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 |
- 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.
- 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_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`).
The `000e:` segment contains a RIFF/AVI streaming animation subsystem. Animation objects have a confirmed field layout (offsets relative to the object base 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: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` |
-`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`.
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`.
### 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)
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.
| `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 |
| `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 |
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
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
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+`).
| `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: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) |
| `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. |
| `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. |
| `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: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: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: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`. |
| `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`. |
| `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:
- 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 |
- 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: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` |
## 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 |
| `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 |
- 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` |
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. |
| 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`. |
| 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. |
| 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 |
| 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 |
| 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.
| `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`. |
| 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] |
| 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 |
| 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)
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.
| 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):**
| `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: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) |