- **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 original NE image is a **different** external or inter-segment call patched by the NE loader at runtime. The body at `0000:ffff` is just fixup placeholder data, so decompiling it as a function is meaningless.
Repair status in `CRUSADER-RAW.EXE`:
- A PyGhidra repair pass now applies the verified NE relocation table directly to the raw-program bytes for literal internal `CALLF 9A ptr16:16` sites, then re-disassembles each patched instruction.
- Current verified batch results:
-`8851` internal literal `CALLF` sites patched to their real segment:offset targets.
-`2841` far-pointer relocation entries skipped because they were not literal `CALLF` instructions (data or other non-call uses).
-`119` import callsites annotated as `NE IMPORT -> module.symbol`.
- Remaining xrefs to `0000:ffff` should now mostly be import callsites or non-literal far-pointer cases rather than unresolved intra-game far calls.
- 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 seg091 Boundary Recovery (init/context + RNG helpers)
- Conservative PyGhidra boundary repair created the missing seg091 functions in `CRUSADER-RAW.EXE`:
-`000a:44fd` = `seg091_func_00fd`, body `000a:44fd-000a:454c`
-`000a:454d` = `seg091_func_014d`, body `000a:454d-000a:45fd`
-`000a:48a0` = `rng_advance_state`, body `000a:48a0-000a:48e2`
-`000a:48ff` = `rng_next_modulo`, body `000a:48ff-000a:4912`
- Additional adjacent helper identified directly in the raw import:
-`000a:48e3` = `rng_set_seed`
- Verified current behavior from the raw import:
-`seg091_func_00fd` shares runtime flag `0x44a4` with `runtime_init_or_abort`; if the flag is clear it sets it and dispatches through an unresolved far thunk, then falls into a second unresolved thunk path that Ghidra currently marks as non-returning.
-`seg091_func_014d` also shares flag `0x44a4`; it checks an optional long argument against the global context/cookie at `0x45a6`, zeroes the pointed byte when the argument is null, then dispatches through an unresolved far thunk. Keep the positional name until caller-side analysis resolves the thunk target and full signature.
-`rng_set_seed` writes the 32-bit RNG seed/state pair at `0x4584:0x4586` and forces the low word odd.
-`rng_advance_state` updates the same 32-bit state with a simple multiply/add step.
-`rng_next_modulo` advances the RNG state and returns the result modulo the requested bound, or `0` when the bound is zero.
- Short decompiler comments were added in Ghidra at all five seg091 entries so the current evidence stays attached to the raw database.
- Rebuilt `000a:b988` as `sprite_node_get_or_traverse` with full body `000a:b988-000a:bab5`.
- This repair absorbs both callback-state sync callsites at `000a:b9e5` and `000a:ba66` that were previously in a no-function gap.
- Verified callback-object behavior from this pass:
-`runtime_callback_object_init_once` sets one-time guard `0x4594`, snapshots state words (`0x458c`/`0x4590`) via `video_bios_state_snapshot`, installs the object FAR pointer at `0x4588`, and ensures fallback buffer allocation at `0x45a6`.
-`runtime_callback_object_teardown_once` sets one-time guard `0x4595`, clears `0x4588`, conditionally emits vtable `+0x0c` callback when current/previous state differ, then calls vtable `+0x04` release path.
-`runtime_callback_object_phase_finalize` invokes vtable `+0x08` twice and sweeps table entries via `allocator_head_finalize_sweep`.
- Large caller `FUN_000d_9afd` contains both additional vtable `+0x0c` callsites (`000d:9d5e` and `000d:a3b7`) and remains the best next target for concrete subsystem naming.
- Short decompiler comments were added at the three renamed lifecycle functions to preserve current evidence.
### Raw 0x4588 Follow-up Batch (allocator/video helper clarification)
- New conservative helper renames from direct disassembly/decompile evidence:
-`0009:a961` = `allocator_head_finalize_sweep`
-`000a:4a1f` = `video_bios_state_snapshot`
- Verified behavior anchors:
-`allocator_head_finalize_sweep` performs per-head chain compaction/finalize work over allocator table entries used by `runtime_callback_object_phase_finalize`.
-`video_bios_state_snapshot` executes BIOS video interrupts (`INT 10h` with `AX=4F03` and `AX=1130,BH=3`) and returns packed state in `DX:AX`; callers store/compare this pair around callback emissions.
- Decompiler comments were updated so downstream analysis sees the new helper names directly.
### Raw 0x4588 Follow-up Batch 2 (cleanup + mode-state wrapper)
- New conservative structural renames (direct decompile/disassembly evidence):
-`video_mode_set_and_record_state` stores requested mode/state to `0x4590`, handles VBE-style mode values (`0x101`/`0x103`/`0x105`) via helper checks, and falls back to `INT 10h` mode path for other values.
-`entity_cleanup_resources_and_dispatch` is a large teardown/finalize path for an entity-like object: it clears flags, frees multiple owned buffers/palette handles, performs conditional callback dispatch through `0x4588` vtable `+0x0c`, then destroys object word-list structures.
- Decompiler comments were added at both renamed addresses to preserve this provenance.
### Raw 0x4588 Follow-up Batch 3 (cleanup-callee helper classification)
- New conservative helper renames from direct MCP decompile evidence:
-`palette_buffer_alloc_and_init_256` ensures a caller-provided far buffer exists, allocates/initializes a `0x100`-entry palette/work block, and fills it from static table data.
-`file_handle_alloc_init_and_open` allocates a handle structure on demand, seeds sentinels, then delegates to `file_handle_open_with_mode`.
-`file_handle_open_with_mode` performs path/open initialization with optional pre-delete behavior and stores DOS open result metadata into the handle structure.
-`surface_release_and_maybe_free` wraps `surface_release_internal` and conditionally frees memory when `(flags & 1) != 0`.
-`sprite_redraw_global_if_active` redraws the global sprite/object pointer at `0x4f38` only when the global gate byte `0x68e5` is enabled.
-`entity_cleanup_resources_and_dispatch` now has direct named callees for file/surface/palette cleanup paths, reducing the remaining ambiguity to callback-object role naming and the `000d:7e00` event-dispatch constructor path.
- Short decompiler comments were added at all six renamed helpers to preserve evidence provenance in-database.
### Raw 0x4588 Follow-up Batch 4 (function-object recovery around `000d:7e00`)
- Missing function objects recovered from direct disassembly boundaries:
-`000d:7e00-000d:8077` created and renamed to `entity_dispatch_entry_init_runtime_state`
-`000d:8078-000d:819f` renamed to `entity_dispatch_entry_release_runtime_state`
-`0003:a880-0003:a896` created as `FUN_0003_a880` (arithmetic helper; decompiler currently simplifies it)
-`0003:b8e2-0003:bb39` created and renamed to `far_buffer_alloc_with_mode_flags`
- Verified behavior anchors:
-`entity_dispatch_entry_init_runtime_state` is a constructor-side helper that initializes runtime fields (`+0x41/+0x42/+0x44`), clears and allocates paired work/palette buffers (`+0x46/+0x48` and `+0x4a/+0x4c`), applies event/setup calls through seg061 helpers, then finalizes activation.
-`entity_dispatch_entry_release_runtime_state` is the destructor-side pair: it frees the same paired buffers, propagates active-state changes via global `0x6828`, and destroys embedded word-list members.
-`far_buffer_alloc_with_mode_flags` is a low-level far-buffer utility that allocates/reuses a destination pointer and dispatches mode-dependent copy/fill behavior via an internal flag table.
- This resolves the previous `000d:7e00` "missing function object" blocker and improves readability for `entity_cleanup_resources_and_dispatch` callback/setup paths.
### Raw 0x4588 Follow-up Batch 5 (seg061/064/076 helper stabilization)
- New conservative helper renames:
-`0009:6ec7` = `vga_palette_read`
-`0008:d3ba` = `timer_entity_enable_wrapper`
- Additional evidence-preserving decompiler comments were added (without speculative renames) on:
-`0008:eb43`
-`0008:ebe7`
-`0008:eac8`
-`0008:ec23`
- Verified behavior anchors:
-`vga_palette_read` mirrors `vga_palette_write` and reads DAC entries through ports `0x3c7/0x3c9` into a far palette buffer.
-`timer_entity_enable_wrapper` is a thin forwarder to `timer_entity_enable` and is widely used in lifecycle/setup paths.
- The seg064 gate helpers (`0008:eb43`/`0008:ebe7`/`0008:ec23`) control one-shot global flag transitions at `0x3b72/0x3b73`, then dispatch via unresolved thunk paths; names remain intentionally conservative pending stronger subsystem identity.
- Callback callsite clarification retained:
-`entity_cleanup_resources_and_dispatch` vtable `+0x0c` call at `000d:9d5e` passes object fields `+0x12d/+0x12f`.
-`palette_buffer_alloc_copy_from_source` allocates/replaces destination palette buffer metadata and copies RGB triplets from a source far pointer (`entry_count * 3` bytes).
- Disassembly annotations added on both callback emit callsites so payload provenance remains attached in-database:
-`000d:9d5e` -> vtable `+0x0c` payload from `+0x12d/+0x12f`
-`000d:a3b7` -> vtable `+0x0c` payload from `+0x74f/+0x751`
- Global data labels were promoted for the callback lane (where symbolization applies in decompiler views):
-`g_active_dispatch_entry_farptr` at `0x6828`
- callback-state/object globals at `0x4588/0x458c/0x4590/0x4594/0x4595/0x45a6`
-`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.
- The placeholder at `0000:ffff` still exists as a symbol, but the relevant internal literal `CALLF` sites are no longer the best source of truth: they have been patched in-place to their real NE targets.
- For `snap_entity_to_ground`, the formerly unresolved call at `0007:2261` now disassembles to `CALLF 0004:e7bd`, i.e. `world_to_screen_coords`.
- Remaining `0000:ffff` sightings in the raw import are now primarily import calls or non-literal far-pointer cases, not evidence that this gameplay helper still dispatches through a single shared runtime function.
**`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 repaired call at `0007:2261` now lands at `world_to_screen_coords` (`0004:e7bd`), so the next step is to reinterpret the helper with the real callee in view rather than through the old `0000:ffff` placeholder model.
-`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.
- After the NE far-call repair pass, the first call at `0007:101c` now disassembles and decompiles directly as `entity_resolve_slot_ptr` (`0005:0466`) instead of `CALLF 0000:ffff`.
- The repaired call chain now exposes several concrete helpers used by the sweep:
-`0005:42c8` = `entity_projected_bbox_overlaps_viewport` — projects the entity slot via `world_to_screen_coords`, subtracts entity height from `0x7df5[slot]`, derives sprite/flag context, and returns `bbox_overlap_test` against the active viewport rectangle referenced from global `0x4014`.
-`0005:3cf5` = `entity_class_has_flag2000` — class-word flag test over `entity_get_class_word(slot) & 0x2000`.
-`0005:ff2d` = `entity_class_get_flag8` — returns bit `0x08` from entity-class detail byte `0x7e1e[type*0x79 + 0x59]`.
-`0006:1305` = `entity_class_get_word_02` — raw accessor for word `+0x02` in the `0x7e1e` class-detail record.
-`0006:0ca4` = `entity_class_get_word_0a` — raw accessor for word `+0x0a` in the same class-detail record.
-`0006:11a1` = `entity_class_clear_flag8_and_dispatch` — clears bit `0x08` in class-detail byte `+0x59`, then performs follow-up entity/type checks and callback dispatch. Name intentionally stays flag-centric until the downstream side effects are fully mapped.
- 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 |
-`+0x59` offset = class-detail flags byte (`entity_class_get_flag8` returns bit `0x08`; other callers also clear bit `0x10` here during at-target facing updates)
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` | internal block only *(no function after repair)* | Direct raw-analysis behavior remains useful as a local label: this block 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. After the PyGhidra boundary repair, `0007:5b6f` is no longer a function entry and should be treated only as an internal control-flow label inside the first repaired seg043 routine. |
- 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:
- The first recovered standalone function spans `0x0090..0x0179`, which means raw `0007:5b6f` falls inside the tail of that routine and overlaps the true return at raw `0007:5b79`.
- Repair status: applied in `CRUSADER-RAW.EXE` via the local PyGhidra toolkit. The bad function object at `0007:5b6f` was removed, and three conservative replacement functions were created:
-`0007:5a90` = `seg043_func_0090` with body `0007:5a90..0007:5b79`
-`0007:5b7a` = `entity_set_at_target_update_facing` with body `0007:5b7a..0007:5c1b`
-`0007:5c1c` = `seg043_func_021c` with body `0007:5c1c..0007:5c80`
- Follow-up re-decompilation now supports one real behavioral rename: `0007:5b7a` sets entity `+0x3a` to 1, calls `entity_set_facing_direction`, clears class-detail bit `0x10` at `0x7e1e[type*0x79+0x59]`, then continues into downstream dispatch, so the repaired middle function has been renamed `entity_set_at_target_update_facing`.
-`0007:5a90` now has a stronger structural read from standalone disassembly: it allocates an object when the incoming far pointer is null (literal `0x98`), runs a far setup helper using DS:`0x4b48..0x4b4e` and the second incoming far pointer, writes `0x4c13` at the object base, calls `entity_set_at_target_update_facing` with the third incoming far pointer, then adjusts the nested object at `+0x38` using extents read from the object at `+0x34` before returning the object pointer.
-`0007:5c1c` also has a stronger structural read: it optionally calls a virtual method through `[object->vtable + 0x4c]` when `object+0x44/+0x46` is non-null, passes a local stack word through `entity_class_get_flag20`, then dispatches one or two downstream far helpers using `object+0x48`, gated by a local status byte at `[bp-0xe]`.
-`0007:5a90` and `0007:5c1c` remain intentionally positional because their current decompiles still collapse into unresolved thunk dispatches and do not yet support safe behavioral names.
| `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:bd53` | `entity_dispatch_entry_unlink` | Clears bit `0x1000` in flags2 at `+0x18` and zeroes the four link/state words at `+0x0a..+0x10`; used as the common unlink/reset tail in the local dispatch-entry pruning path |
| `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.
-`0008:bd79` remains positional, but current decompiler evidence shows it compares an entry extent/position tuple against the player world position (`g_player_entity_farptr + 0x40/+0x42`), optionally fires the vtable callback at `+0x28` when flag `0x100` is armed, then calls `entity_dispatch_entry_unlink`.
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:cefb` | `entity_dispatch_entry_ctor_vtbl_3ad2` | Constructor variant: allocates if null, reinitializes via `entity_dispatch_entry_init`, sets vtable `0x3ad2`, sets flag `0x100` at `+0x16`, and zeroes the extension words at `+0x32/+0x34` |
| `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 (`0x3ad2`, `0x3aa6`) until more direct gameplay semantics are recovered from their callback dispatch paths.
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.
| 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 |
| 43 | `000a:48ff` | `rng_next_modulo` | 55 | Advances seg091 RNG state and returns the result modulo the requested bound; returns 0 when bound is 0. |
| 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` no longer exists as a function after the PyGhidra repair pass. Its old raw-analysis behavior now lines up with the repaired function `0007:5b7a = entity_set_at_target_update_facing`, so `0007:5b6f` should be treated only as an internal control-flow location inside that function.
- The seg043 split has now been repaired in Ghidra. Verified temporary functions exist at raw `0007:5a90`, `0007:5b7a`, and `0007:5c1c`.
- The repaired middle function at `0007:5b7a` has now been promoted from a positional label to `entity_set_at_target_update_facing` based on direct decompile/disassembly behavior.
- The remaining repaired functions at `0007:5a90` and `0007:5c1c` should keep their positional names until a later pass resolves the thunk-heavy bodies more clearly.
- The next pass on this region should continue re-decompiling `seg043_func_0090` and `seg043_func_021c`, resolve the still-unknown far thunks they call, and replace the positional names only when their behavior is directly supported.
| `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. |
| 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) |
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 |
| 102 | `0009:80db` | `bbox_overlap_test` | 25 | Boolean rectangle overlap test for two 4-word bbox structs; unlike `bbox_intersect`, it does not write back an intersection |
| 103 | `000d:cc00` | `entity_compute_proximity_or_visibility_bucket` | 25 | Returns `0x40` if entity is null or projected bbox overlaps viewport; otherwise buckets world-distance from current reference entity (`0x7e22`) into `0x32/0x20/0x10/0x08` |
| 104 | `000d:d413` | `entity_refresh_recent_proximity_or_visibility_buckets` | 24 | Recomputes bucket values for the last four active entries in `0x69ac` and notifies backing handles via `000a:6343` when a bucket changes |
## Raw 000d Proximity/Visibility Bucket Cluster (new)
Small conservative rename batch from the `000d:cc00-d413` region after the far-call repair exposed the viewport helper in live decompilation.
| Address | Name | Evidence |
|---------|------|---------|
| `000d:cc00` | `entity_compute_proximity_or_visibility_bucket` | Returns bucket `0x40` for null or on-screen entities (`entity_projected_bbox_overlaps_viewport`), else computes a distance bucket from the current reference entity at `0x7e22` with thresholds `0x17d`, `0x281`, `0x3c1` mapping to `0x32`, `0x20`, `0x10`, `0x08` |
| `000d:d413` | `entity_refresh_recent_proximity_or_visibility_buckets` | Walks the last four active records in the `0x69ac` array, recomputes the same bucket, stores it back to each entry, and calls `000a:6343` when the bucket changes |
| `000d:cdd0` | `tracked_entity_bucket_prune_invalid_entries` | Walks the `0x69ac` array, validates backing handles through `000a:637a`, and clears entry handles to `0xffff` when the backing object is gone |
| `000d:cd62` | `tracked_entity_bucket_find_free_main_slot` | Finds the first free entry in the main portion of the `0x69ac` array (`0 .. count-4`) |
| `000d:cd9a` | `tracked_entity_bucket_find_free_aux_slot` | Finds the first free entry in the auxiliary tail portion of the `0x69ac` array (`count-4 .. count-1`) |
Supporting caller notes:
-`000d:ce1e` populates one `0x69ac` entry by reserving a free slot, computing the initial bucket through `entity_compute_proximity_or_visibility_bucket`, storing both current and previous bucket fields, then allocating/linking the backing handle through `000a:5f36`.
-`000d:d409` is a thin wrapper that only calls `entity_refresh_recent_proximity_or_visibility_buckets`.
-`000d:cfad` is an update-or-allocate helper for `(param_1,param_2)` pairs: it tries to update an existing tracked entry through `000a:606a`, clears dead entries, and falls back to `000d:ce1e` allocation when no live match remains.
-`000d:cec5` is the auxiliary-slot allocator: it prunes invalid entries, uses `tracked_entity_bucket_find_free_aux_slot`, tags the new entry with byte `+0x0a = 1`, and seeds its handle via `000a:5f36(..., flag=1)`.
-`000a:606a` = `tracked_entity_bucket_handle_update_or_alloc` — updates the backing handle for an existing tracked bucket entry when possible, or falls back to allocation via `000a:5f36` if the handle has gone stale.
-`000d:d350` = `tracked_entity_bucket_set_value` — finds a tracked `(entity_id, entity_ref)` entry in `0x69ac` and pushes a new bucket value into its backing handle through `000a:6343`.
-`000d:d10b` = `tracked_entity_bucket_clear_ref_field` — clears only the `+0x02` reference field for all matching entries.
-`000d:d151` = `tracked_entity_bucket_remove_by_ref` — marks matching entries' backing handles for removal and clears the local entry handle/reference fields.
-`000d:d1b1` = `tracked_entity_bucket_remove_tagged_by_ref` — same removal path, but only for entries whose byte `+0x0a` tag is set.
## Raw 000a Tracked-Handle Table (new)
The `0x4673` table now reads as the backing handle registry for the `0x69ac` tracked-entry bucket subsystem.
That client layer sits on top of a separate generic cache manager rooted at `0x4688..0x46b7`, so the current `tracked_entity_*` names should be read as client-side structure names rather than names for the cache internals themselves.
| Address | Name | Evidence |
|---------|------|---------|
| `000a:5f02` | `tracked_entity_handle_find_slot` | Linear scan over 12 entries in the `0x4673` table for a matching 32-bit handle id |
| `000a:602b` | `tracked_entity_handle_is_live` | Returns true only when a handle exists in `0x4673` and its flag word at `+0x0a` does not have bit `0x0002` set |
| `000a:60eb` | `tracked_entity_handle_mark_remove` | Sets bit `0x0002` in the handle-table flag word and dispatches through the unresolved cleanup path |
| `000a:612e` | `tracked_entity_handle_mark_remove_all` | Iterates all 12 handle-table entries and marks each live handle for removal |
| `000a:6167` | `tracked_entity_handle_alloc_slot` | Allocates a slot in one of two ranges (`0..7` or `8..11`) depending on the aux flag; when full, wraps in a ring and evicts via `tracked_entity_handle_mark_remove` before reusing the slot |
| `000a:6228` | `tracked_entity_handle_prune_removed` | Reaps entries previously marked with bit `0x0002`, clears dead slots, and refreshes high-index entries through `000a:6b2d` |
| `000a:63bc` | `tracked_entity_handle_find_by_entity` | Finds the first live handle-table entry whose key/entity word at `+0x04` matches the requested entity id |
Current structural read of one `0x4673` entry (stride `0x0c`):
-`+0x00` = 32-bit handle id
-`+0x04` = key/entity id
-`+0x06` = class/group/source-style selector passed in from tracked-entry allocation
-`+0x08` = current bucket/value
-`+0x0a` = flags (`bit0` set by aux-slot allocation, `bit1` = pending removal)
Thin public wrappers on top of the tracked-handle client layer:
-`000a:5276` = `entity_bucket_track_default_main` — gated by `0x45aa`; creates or refreshes a main-slot tracked handle with bucket `0x40` and selector `0xff`.
-`000a:5294` = `entity_bucket_track_main` — same path, but takes the bucket value as an argument for the main-slot range.
-`000a:52d0` = `entity_bucket_track_default_aux` — aux-slot variant with default bucket `0x40`.
-`000a:52ee` = `entity_bucket_track_aux` — aux-slot variant with explicit bucket argument.
## Raw 000a Generic Cache Manager (new)
Follow-up analysis of `000a:6b2d` and the `0x4688..0x46b7` globals shows that this region is a generic cache manager used by the tracked-handle layer, not part of the tracked-entity subsystem itself.
| Address | Name | Evidence |
|---------|------|---------|
| `000a:6b2d` | `cache_lookup_or_load_entry_by_id` | Fast-paths the last id via `0x46af/0x46b1`, otherwise searches `0x469d`, evicts older cache slots until there is room under byte budget `0x46a5`, allocates a block from the free-list, clears/initializes the payload, records the id, and dispatches through the loader interface at `0x468c` |
| `000a:6a95` | `cache_release_entry_by_slot` | Releases a cached slot by index, clears any client references through `000a:62d8`, frees its backing block through `cache_free_block_by_slot`, and marks the slot id in `0x469d` as unused (`0xffff`) |
| `000a:6d07` | `cache_alloc_block_for_slot` | Allocates or splits a block from the free-list anchored at `0x4688`, tags it with the owning cache slot index, and updates the in-use byte count at `0x46a9` |
| `000a:6f4d` | `cache_free_block_by_slot` | Finds the free-list node for a cache slot, marks it free, subtracts its size from `0x46a9`, and coalesces adjacent free blocks |
| `000a:67d9` | `cache_shutdown` | Tears down the generic cache manager: flushes/reset state, frees slot arrays at `0x4699/0x469d/0x46b3`, frees the free-list container at `0x4688`, and closes backing state at `0x4691` |
| `000a:6898` | `cache_set_loader_interface` | Installs the backend loader/callback interface pointer at `0x468c` |
Current structural read of the cache globals:
-`0x4688` = intrusive free-list / block-list head used by `cache_alloc_block_for_slot` and `cache_free_block_by_slot`
-`0x468c` = backend loader interface / callback table; `+0x34` returns the payload size for an id and `+0x0c` loads or binds a block after allocation
-`0x4695` = base pointer for the raw cache payload arena
-`0x4699` = per-slot payload-pointer table
-`0x469d` = per-slot cached id table (`0xffff` = unused)
-`0x46a5` = cache byte budget / arena capacity
-`0x46a9` = current bytes in use
-`0x46af` / `0x46b1` = one-entry fast-path cache of the last requested id and slot index
-`0x46b3` = per-slot block metadata pointer mirror used when releasing or refreshing slots
### Follow-up: caller resolution for the public bucket wrappers
-`0005:3b34` = `tracked_entity_bucket_alloc_main_if_enabled` and `0005:3b53` = `tracked_entity_bucket_alloc_aux_if_enabled`; these are the two thin gate wrappers that feed the `0x69ac` tracked-entry layer.
-`0005:3b72` = `tracked_entity_bucket_remove_by_entity_and_ref_if_enabled`, which forwards into `000d:d086 = tracked_entity_bucket_remove_by_entity_and_ref` when `0x45aa` is set.
-`0006:d404` is not a standalone function entry; it is the tail call inside the unnamed destructor block immediately before `0006:d414`. That block tears down a `0x2774` dispatch-entry object, pushes `(entity_id = 0xdb, entity_ref = *0x7e22)`, and removes the matching aux tracked bucket entry.
-`0006:d5ae` is the analogous tail call immediately before `0006:d5be`. It tears down the sibling `0x2750` dispatch-entry object, pushes `(entity_id = 0xa4, entity_ref = *0x7e22)`, and removes the matching aux tracked bucket entry.
- The matching constructor sides are `0006:d370` and `0006:d51a`: both allocate/init a dispatch entry, stamp source type `8`, seed a per-object field from the current reference entity at `0x7e22`, and then call `tracked_entity_bucket_alloc_aux_if_enabled`.
-`0007:cce8` is the tail call at the end of `scroll_camera_set_state_params`. After the camera scroll state is updated and the new screen-space origin is committed to `0x2bb7/0x2bb9`, it refreshes the recent proximity/visibility buckets through `000d:d409` when `0x45aa` is enabled.
### Follow-up: `0x45aa` gate and cache loader installation
-`0x45aa` now reads as `tracked_entity_bucket_system_enabled`, not a one-off debug/test flag. It gates all three public wrapper helpers above and the camera-side refresh in `scroll_camera_set_state_params`.
- The enable bit is set only by the unlabeled init block around `000a:51d0..5222`: that code stores the incoming backend/interface pointer into `0x45ab/0x45ad`, installs it into `0x468c` via `cache_set_loader_interface`, allocates the tracked-handle table (`000a:5e00`) and the 32-entry bucket array (`000d:cca3(0x20)`), then sets `0x45aa = 1`.
- The matching unlabeled shutdown block starts at `000a:5223`: it checks `0x45aa`, tears down the tracked-handle table through `000a:5e59`, frees the bucket array through `000d:ccec`, and only then clears/uses the backend pointer state at `0x45ab`.
-`0x468c` remains best named as a generic `cache_loader_interface`: the current verified evidence is a cache backend callback table (`+0x34` = size query, `+0x0c` = load/bind callback) shared by the tracked-entry service. The newly traced gameplay callers prove this service participates in camera/entity-interest updates, but they are not yet strong enough to justify an audio-specific or resource-specific subsystem rename.
### Follow-up: init/shutdown entry points around `000a:51xx`
-`0x45ab/0x45ad` now read as `tracked_entity_bucket_backend_object`, a cached backend/interface object pointer used by init/shutdown in addition to the lower-level `cache_loader_interface` callback table at `0x468c`.
-`tracked_entity_bucket_system_init` first allocates a rotating buffer via `0009:3600`, lazily creates `tracked_entity_bucket_backend_object` through `0009:5600` when absent, installs that object into `cache_loader_interface`, allocates the tracked handle table via the missing function entry at `000a:5e00`, allocates the 32-entry `0x69ac` bucket array via `000d:cca3(0x20)`, then sets `tracked_entity_bucket_system_enabled`.
-`tracked_entity_bucket_system_shutdown` is called from the wider engine teardown routine at `0004:621b`; it tears down the tracked handle table, frees the `0x69ac` bucket array, calls backend-object vtable slot `+0x38` with `(3, backend_object)`, and clears `tracked_entity_bucket_backend_object`.
### Follow-up: backend object constructor at `0009:5600`
- The missing raw-import function entry at `0009:5600` has now been recovered in-place as `cache_backend_object_init` with body `0009:5600-0009:57b9`.
- Current verified behavior is still structural, but stronger than before:
- Allocates a `0x20`-byte object when the caller passes null.
- Initializes embedded DOS file-handle state via `dos_file_handle_init` (`0009:1c00`).
- Seeds internal method-table / state fields at object offsets `+0x08`, `+0x0c`, `+0x10`, `+0x14`, `+0x16`, `+0x18`, and `+0x1c`.
- Dispatches through the object method table during construction and returns the object pointer later cached at `0x45ab/0x45ad`.
- This is enough to justify the structural `cache_backend_object_init` name, but not yet enough to promote the backend object to a file-, audio-, or resource-specific subsystem name.
- backend vtable `+0x34` = size query callback for a cache entry id (used before allocation/eviction).
- backend vtable `+0x0c` = load/bind callback that populates the newly allocated slot buffer for the requested id.
### Follow-up: missing function entry at `000a:5e00`
- The missing raw-import function entry at `000a:5e00` has now been recovered in-place as `tracked_entity_handle_table_init` with body `000a:5e00-000a:5e58`.
- Verified behavior: if `0x4672` is clear, it allocates `0x90` bytes at `0x4673/0x4675`, aborts through `runtime_init_or_abort` on allocation failure, calls `000a:577d` and local helper `000a:5e95`, then sets `0x4672 = 1`.
- This matches the existing client-layer interpretation: `0x4673` holds 12 handle entries (`12 * 0x0c = 0x90` bytes), and `000a:5e00` is the table allocator/initializer used by the tracked bucket subsystem startup path.
### Follow-up: missing function entry at `000a:6600`
- The missing raw-import function entry at `000a:6600` has now been recovered in-place as `cache_init` with body `000a:6600-000a:67d8`.
- Verified behavior from the repaired body:
- Stores the requested slot count in `0x46ad`.
- Allocates the per-slot payload-pointer table at `0x4699` (`count * 4`) and aborts on failure.
- Seeds each slot with allocator-returned pointers / zero low words before running local pointer normalization helpers (`0009:c496`, `0009:c400`, `0009:c6ae`).
- Queries/derives the cache arena size, subtracts `0x1000`, and stores the byte budget in `0x46a5`.
- Allocates the arena backing object at `0x4691`, derives the payload base pointer `0x4695`, and aborts through `seg091_func_00fd` on failure.
- Allocates the per-slot block-metadata mirror at `0x46b3` (`count * 4`) and per-slot cached-id table at `0x469d` (`count * 2`).
- Allocates and initializes the free-list head object at `0x4688`, then calls local helper `000a:68aa` before returning.
### Follow-up: cache reset / handle-table helpers
-`000a:68aa` = `cache_reset_runtime_state`.
- Verified role: shared cache reset/bootstrap helper called from `cache_init`, `cache_shutdown`, and one wider external reset path. It allocates per-slot arena-header / metadata nodes, rebinds slot pointers to the arena base, clears the cached-id table, seeds the free-list head, and resets `0x46a9` (bytes in use) plus `0x46af` (last-id fast path).
-`000a:703e` = `cache_compact_arena_blocks`.
- Verified role: compacts live cache arena blocks into earlier free holes when `cache_alloc_block_for_slot` cannot find a large-enough free block, updates per-slot payload pointers, and merges adjacent free-list headers afterward.
- Verified role: when `tracked_entity_handle_table_active` is set, it zeroes the full `0x90`-byte handle table at `0x4673`, resets adjacent local state at `0x4677/0x4679/0x467b`, then dispatches through the remaining thunked follow-up path.
- Verified role: thin gate wrapper that only forwards to `tracked_entity_handle_mark_remove_all` when `tracked_entity_bucket_system_enabled` is set.
### Follow-up: external reset paths using the cache/tracked-handle layer
- The unlabeled path around `0004:25a9` now has enough local evidence to classify as an external reset sequence: it calls `cache_reset_runtime_state`, then `tracked_entity_handle_table_clear_and_dispatch`, then continues through additional tracked-entry/cache-side refresh helpers (`000d:cd22`, `000d:44b3`, `0006:ae66`, `0006:ae00`, etc.).
- The unlabeled path around `0004:eb80` is a conditional tracked-bucket reset/update sequence: when `tracked_entity_bucket_system_enabled` is set, it calls `tracked_entity_handle_mark_remove_all_if_enabled`, then `tracked_entity_handle_table_clear_and_dispatch`, then `cache_compact_arena_blocks`, before resuming its outer flow.
- These caller sites strengthen the current interpretation that the `0x45aa` / `0x4673` / `0x4688..46b7` layer is a shared runtime cache service used by gameplay/system reset flows, but they still do not expose a resource-specific subsystem name by themselves.
### Follow-up: repaired seg004 reset-path function objects
-`0004:2592` had been mis-modeled as a one-instruction thunk body. It has now been repaired to the full body `0004:2592-25de` and renamed `runtime_cache_reset_sequence`.
- Current verified behavior for `runtime_cache_reset_sequence`:
- Then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers, and resumes the wider runtime reset flow.
- Known caller so far: `0004:262d` inside the tiny wrapper at `0004:2620`, which sets byte `+0x40` on the object at `0x6828` before invoking the reset sequence.
-`0004:eb1f` had also been truncated. It has now been repaired to the full body `0004:eb1f-eb9b` and renamed `entity_dispatch_entry_ctor_0f3a_with_cache_reset`.
- Verified behavior for `entity_dispatch_entry_ctor_0f3a_with_cache_reset`:
- Allocates/initializes an entity dispatch entry when needed.
- Stamps entry type `0x0f3a`.
- Stores its two word payload fields from the incoming args.
- Runs local setup through the embedded helper at `0004:ebf4`, which dispatches `entity_dispatch_reset_all(*0x7e22, 0x00f0)` and, when the local flag plus global `0x0ee1` allow it, allocates a type `0x0f5e` dispatch entry and passes it to `entity_pair_sync_b`.
- When `tracked_entity_bucket_system_enabled` is set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through `0009:b1c3` in phase `0`.
- The sibling at `0004:eb9c` remains separate and valid; it builds the same `0x0f3a` entry type without the extra cache-reset tail, so the repaired `0004:eb1f` boundary stops cleanly at `0004:eb9b`.
-`0009:a229` is now verified as the public size-only wrapper around the seg082 allocator path.
- Caller evidence:
-`saveslot_table_clear` requests `0x2800` bytes through `0009:a229`, stores the returned FAR pointer at `0x2ba3/0x2ba5`, then zeroes the result in `0x400`-byte chunks.
- The wrapper lazily initializes the allocator on first use through `0009:bcb9`, then calls `allocator_try_alloc_from_head_table(size, default_tag, 0xff)`.
-`0009:bcb9` is now annotated as the one-time lazy initializer for this path.
- It parses an optional `-x` tuning value from the PSP command line, clamps the derived percentage into `0x14..0x50`, then seeds local seg082 helpers before setting init flag `0x4096 = 1`.
- Table structure around `0x8724` is tighter now:
-`0x8724` is an array of `0x0c`-byte allocator heads.
-`0x879c` is the active head count / table limit.
- The per-node size/value encoding used under each head is manipulated through `0009:c628` and `0009:c6ae`, which read/write a packed 32-bit quantity split across `word + byte + byte` fields.
-`0009:af87` is now annotated as the free-space probe for the same cluster.
- It walks the node chain rooted at `0x8724`.
- For each node, it accumulates `node_size - 9` into a running total and tracks the largest single free block.
- Known callers include `cache_init` and the seg013 path at `0004:833b`, both of which use it to size subsequent allocation work.
-`0009:b06b` is now renamed `allocator_try_alloc_from_head_table`.
- It validates the requested size, reserves a temporary work token through `0009:e15f`, and scans the `0x8724` allocator head table in `0x0c`-byte entries via the local helper at `0009:a336`.
- When a pass does not find a fit, it interleaves up to two finalize phases through `allocator_phase_finalize_pass(phase)` before the final retry, then releases the work token through `0009:e1f6`.
- It normalizes the requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds the local `0x0a` node header overhead, and enforces a minimum allocation size of `0x10` bytes.
- It walks the node chain for one allocator head until it finds a free span large enough.
- On success it unlinks the chosen free node, either consumes it whole or splits off a remainder node when at least `0x10` bytes remain, stores the owner/tag word, and returns `payload_ptr + 0x0a`.
- Boundary follow-up from the same read-only scan:
- The adjacent missing body at `0009:a5d1` has now also been recovered in-place as `allocator_head_free_block` with body `0009:a5d1-0009:a960`.
- It is the per-head free helper paired with `allocator_head_try_alloc_block`.
- It rebuilds the node header from a payload pointer (`payload - 0x0a`), validates the owner/tag word against the expected caller-supplied tag, reinserts the block into one allocator head, and coalesces with adjacent free neighbors when possible.
- Its earlier branch targets `0009:a7a1` and `0009:a7b8` are now confirmed to be internal labels, not separate function entries.
- It walks one `0x8724` head's node chain, skips odd-tagged spans, coalesces or rewrites eligible spans, and updates head/back-pointer links when deferred space needs to be merged back into the chain.
- This strengthens the current interpretation that `allocator_phase_finalize_pass` is allocator-side callback/finalize glue rather than a cache-specific public API.
-`0009:b224` is now named `allocator_free_block_by_ptr`.
- Current verified behavior: converts the payload pointer back through the local header helpers, scans the `0x8724` head table for the owning range, dispatches to `allocator_head_free_block`, and aborts if no owning head is found.
- Known wrappers `0009:a24f` and `0009:a27a` are now clearly small checked entry points into this free-by-pointer path.
- With both recovered bodies in place, the seg082 cluster now has a verified alloc/free pair at the per-head level:
-`allocator_phase_finalize_pass` now has a corrected single-byte phase parameter in Ghidra; the object at `0x4588` is still left unnamed because its subsystem role is not yet strong enough.
- This narrows the remaining ambiguity around `allocator_phase_finalize_pass`: the unresolved part is now the role of the object at `0x4588`, not the local allocator mechanics around `0x8724`. `ASYLUM.24` is still not identified from the current evidence.
- A direct instruction scan found real uses of `0x4588` / `0x458a` even though normal static reference APIs were not materializing them.
- Current verified behavior from those uses:
-`entity_conditional_render_dispatch` (`0009:9216`) calls through the runtime-installed object at `0x4588` via vtable slot `+0x0c` when the entity flags allow the alternate path and `param_2 == 0`.
-`000a:4a56` is a one-shot teardown/reset path for the same object: it checks a local once-flag at `0x4595`, clears `0x4588` when non-null, optionally performs a vtable `+0x0c` callback when `0x4590 != 0x458c`, then calls vtable slot `+0x04` followed by `FUN_0009_0d30()`.
- The two callback sync sites inside `sprite_node_get_or_traverse` (`000a:b9e5` and `000a:ba66`) only emit vtable `+0x0c` when the candidate two-word pair differs from the current pair, then immediately mirror that pair through `000b:1e39` using global sprite/object pointer `0x4f38/0x4f3a`.
- A read-only data probe of `0x4588` in the current database returned all zero bytes, so the object pointer is null-initialized statically and likely installed later at runtime.
- Conservative conclusion:
- The `0x4588` object now looks like a runtime-installed callback / dispatch object that participates in conditional render or presentation-side flow and has an explicit teardown path.
- That is enough for comments and ledger progress, but still not enough to safely rename `0009:b1c3` or the global itself to a concrete subsystem name.
### Follow-up: `0x4588` install/clear windows from the no-function hit list
- A read-only PyGhidra instruction-window pass against an unlocked project clone confirmed that the planned no-function hit list is real code, not aligned data.
- New verified lifecycle evidence:
-`000a:4932` and `000a:4936` store the same incoming dword into `0x4590` and `0x458c`, then `000a:493e` stores the incoming FAR object pointer into `0x4588`.
-`0004:5b8c` and `0004:5bbf` both clear `0x4588` immediately before the fatal/reporting-style seg091 call through `000a:454d`.
-`0004:5ea7` and `0004:6430` both clear `0x4588` and then immediately run the one-shot teardown path `000a:4a56(1)`.
-`000a:b9e5`, `000a:ba66`, `000d:9d5e`, and `000d:a3b7` all push a two-word value pair followed by the `0x4588` FAR pointer and call the object's vtable slot `+0x0c`.
-`entity_conditional_render_dispatch` remains the only named caller found so far for the same slot, but it passes a single literal `0x0101` argument instead of a two-word pair.
- Conservative conclusion after the window pass:
-`0x4588` is definitely a nullable runtime-installed FAR object with explicit install, clear, callback, and teardown transitions.
- The unresolved part is now its concrete subsystem identity, not whether the object lifecycle is real.
- The best next cheap win is no longer broad instruction searching; it is caller-side recovery around the still-unbounded `000a:b9e5` / `000a:ba66` and `000d:9d5e` / `000d:a3b7` windows.
-`FUN_000d_938c` is now confirmed as a real caller-side helper with body `000d:938c-000d:9583`, and an evidence-preserving decompiler comment was added in Ghidra instead of forcing a speculative rename.
- Current verified behavior from direct MCP decompile/disassembly:
- When the mode/global gate is not already in the `0x13:0x0008` state and entity byte `+0x33` is clear, it allocates a scratch palette buffer, constructs a dispatch entry, sets type `0x051e`, and initializes runtime state through `entity_dispatch_entry_init_runtime_state` with entry kind `0x3c`.
- Later in the same helper it constructs a second dispatch entry from the current palette globals at `0x4e4:0x4e6`, again sets type `0x051e`, and initializes runtime state with entry kind `0x14` and active-state parameters `(1,0,1)`.
- Both created entries are polled until their runtime flag word clears bit `0x0002`, after which the helper redraws the global sprite path, syncs display-state byte `0x58e` from the entity when the global display object exists, calls `FUN_0006_16e1`, clears `g_active_dispatch_entry_farptr[+0x40]`, and finally dispatches through the input object's vtable slot `+0x08`.
- Conservative conclusion:
- seg138 now has one more verified caller tying `entity_dispatch_entry_init_runtime_state` to palette/presentation-side emission work around entity cleanup and redraw flow.
- This narrows the open question from "is the dispatch-entry lane real?" to "what exact presentation/event subsystem does this lane belong to?"
### Follow-up: seg137 palette and dispatch-entry helper family
- A larger direct MCP rename batch now stabilizes a coherent seg137 palette helper family:
- Current verified behavior from direct MCP decompile/disassembly:
-`vga_palette_set_all_black` corrects the earlier overreach rename at `000d:85da`: it allocates a `0x100`-entry palette buffer filled with zero RGB triplets, writes it to VGA, and frees the scratch buffer. The previous `map_object_set_dirty_flag` name was not supported by the recovered body.
-`vga_palette_set_all_white` is the same helper shape with all three RGB components initialized to `0x3f`, then written through `vga_palette_write`.
-`vga_palette_set_all_rgb` takes caller-supplied RGB bytes, replicates them across a `0x100`-entry palette buffer, writes the result to VGA, and frees the scratch palette.
-`dispatch_entry_create_black_palette_state_active` and `dispatch_entry_create_black_palette_state` both build a runtime-state dispatch entry of type `0x051e` from a black `0x100`-entry palette buffer; the `_active` form first sets `g_active_dispatch_entry_farptr[+0x40] = 1`, while the quiet form does not.
-`dispatch_entry_create_grayscale_palette_state_active` reads the current VGA palette, normalizes each triplet by copying the first channel across all three RGB bytes, then builds a runtime-state dispatch entry from that grayscale palette while marking the active dispatch entry.
-`dispatch_entry_create_solid_palette_state_active` and `dispatch_entry_create_solid_palette_state` validate `0..0x3f` RGB inputs, fill a scratch `0x100`-entry palette buffer with that solid color, and build the same `0x051e` runtime-state dispatch entry, again split into active-marking and quiet variants.
- Additional caller-side comments were added instead of speculative renames on:
-`000d:84f4` (current-palette dispatch entry paired with a second object of type `0x68bf` through `entity_pair_sync_b`)
-`000d:89c6` (parameterized current-palette runtime-state wrapper with active-state flags)
- Conservative conclusion:
- seg137 is now materially beyond a foothold: it contains a coherent palette-write and palette-backed dispatch-entry emission family tied to the same runtime-state constructor lane.
- The remaining uncertainty is higher-level script/event meaning, especially the paired `0x68bf` object and the exact role of the `0004:5ad4-5b6e` caller sequence, not the local palette-helper mechanics.
### Follow-up: seg005 startup/display orchestration and seg136 active dispatch entry
- A new direct MCP recovery pass stabilized one high-value handoff path and its nearby active-dispatch helpers:
-`0004:60c0-0004:621a` recovered as `FUN_0004_60c0` with a decompiler comment summarizing the orchestration flow.
-`000d:7600-000d:760d` created and renamed to `active_dispatch_entry_mark_enabled`.
-`000d:760e` renamed to `active_dispatch_entry_mark_disabled`.
-`000d:761c` renamed to `active_dispatch_entry_create_default`.
- Current verified behavior from direct MCP decompile/disassembly:
-`FUN_0004_60c0` is a recovered startup/display orchestration path: it performs broad setup calls, reads the live VGA palette, validates a caller-provided object through vtable slot `+0x0c`, drives the sprite/object lane through `0x4f38`, runs the seg137 palette and dispatch-entry helper family, creates the default active dispatch entry through `active_dispatch_entry_create_default`, programs mouse interrupt state via seg056 `INT 33h` wrappers, then hands off into the still-unrecovered `0004:1e00` routine.
- The old `0004:5ad4-5b6e` caller sequence is now confirmed as one internal sub-sequence within that larger recovered function rather than an isolated orphan.
-`active_dispatch_entry_create_default` allocates or reuses a `0x42`-byte dispatch entry, stamps type `0x687f`, installs a callback-table pointer through `0x39ca`, sets event type `0x248`, sets update period `0x1e`, marks the entry as the global active dispatch entry at `0x6828`, toggles the local `+0x40` state byte through `active_dispatch_entry_mark_enabled` and `active_dispatch_entry_mark_disabled`, then enables the timer wrapper.
-`active_dispatch_entry_mark_enabled` and `active_dispatch_entry_mark_disabled` are tiny helpers that set or clear `g_active_dispatch_entry_farptr[+0x40]` respectively.
- Conservative conclusion:
- seg005 now has its first strong high-value foothold in a startup/display handoff path, even though the downstream `0004:1e00` target still needs recovery and naming.
- seg136 now has a concrete active-dispatch-entry foothold rather than being empty ledger space.
### Follow-up: seg005 large runtime/display handoff body recovered at `0004:1e00`
-`0004:1e00-0004:2420` has now been recovered as a real function object in Ghidra as `FUN_0004_1e00` with an evidence-preserving comment, replacing the earlier no-function gap.
- Current verified behavior from the recovered body:
- It begins by forcing an all-black palette through `vga_palette_set_all_black`, then performs several startup/display setup calls before manipulating the active dispatch entry at `0x6828`.
- It constructs two animation-side objects through `animation_ctor_variant_b` (`000e:2860`) using DS-local descriptors at `0x04ae` and `0x04b2`, then waits on global words around `0x8a94-0x8a98` when the alternate startup flag path is active.
- It conditionally calls `sprite_node_get_or_traverse` (`000a:b988`) after a seg122 helper path, toggles seg064 gate helpers, and then enters a larger resource/object processing region that uses globals rooted at `0x4aa`, `0x4ac`, and `0x7e22`.
- The mid-body is now better classified as a non-return transition driver rather than a generic handoff stub: it branches on the returned `SI` state after the sprite/object traversal, with one path performing the fuller runtime/display setup, one path taking a small local special-case handler (`0004:2661`), and one path marking the active dispatch entry before calling `runtime_callback_object_teardown_once(1)`.
- The `SI == 2` special-case branch is now slightly tighter: its local helper `0004:2661` forwards into `FUN_0004_25df`, which is a small type-stamped dispatch-entry constructor that allocates when needed, runs `entity_dispatch_entry_init`, stamps type `0x04b6`, and stores the caller-supplied mode/state word at `+0x32`.
- The fuller setup path clears and restores active-dispatch state, calls through the `0x2bd8` object vtable, restores the live palette through `0009:6f5a`, re-runs render/dispatch rectangle helpers (`entity_conditional_render_dispatch`, `entity_rect_compare_and_dispatch`), and finishes through the seg126 trampoline `thunk_callf_0000_ffff_000c_82f9`.
- The recovered tail confirms a clean end at `0004:2420`, with the next separate function beginning at `0004:2421`.
- Conservative conclusion:
- The main blocker on seg005 is no longer structural recovery; it is naming the exact state entered by this now-navigable startup/display transition driver.
- The presence of `animation_ctor_variant_b`, palette forcing, active-dispatch toggles, sprite-node traversal, and the `0x2bd8` vtable lane makes this look more like a real mode/state transition than a one-off helper, but the exact gameplay, intro, or front-end label still needs one more caller/data pass.
### Follow-up: seg126 wrappers feeding the `0004:1e00` transition lane
- Two previously unbounded seg126 callers around the recovered seg005 handoff are now real functions in Ghidra:
-`FUN_000c_7412` (`000c:7412-000c:7432`)
-`FUN_000c_c9f4` (`000c:c9f4-000c:ca1c`)
- The larger fallthrough body rooted at `000c:c890` is now also recovered as a real function object: `FUN_000c_c890` (`000c:c890-000c:c9f3`).
- Current verified behavior from direct MCP recovery/decompile:
-`FUN_000c_7412` is a compact wrapper into the seg005 transition lane: it clears the redraw state on the sprite/object pair rooted at `0x5e82:0x5e84`, forces a black palette through `vga_palette_set_all_black`, runs seg126 pre-entry state prep through `FUN_000c_c9f4`, then tail-calls `FUN_0004_1e00`.
-`FUN_000c_c9f4` is a short pre-entry state wrapper: it runs local seg126 setup helpers, repeatedly executes a local prep loop while state byte `0x62fe` is clear and fallback word `0x31a2` is zero, then dispatches into local helper `000c:c890` before returning to callers that continue into `FUN_0004_1e00`.
-`FUN_000c_c890` is the main seg126 pre-entry preparation body behind that wrapper: it releases up to two tracked object pairs at `0x8c5c` and `0x8c60`, conditionally frees the local pair at `0x6301:0x6303`, runs palette/render reset, conditionally constructs animation state through `animation_ctor_variant_a` on `DS:0x6341` when `0x844` and `0x62fe` are both set, marks the active dispatch entry, primes sprite redraw state, drains the event queue, and zeroes `0x8a94-0x8a98` before returning.
- This is now enough to tie the seg076 caller at `000c:742c` to the same startup/display transition lane already reached from `FUN_0004_60c0`.
- Conservative conclusion:
- seg126 now has a real foothold in the startup/runtime entry path rather than only isolated thunks and trampolines.
- The unresolved part is the exact meaning of the local state bytes and object lanes around `0x62fe`, `0x31a2`, `0x8c5c`, `0x8c60`, and `DS:0x6341`, not whether the wrapper lane itself is real.
### Follow-up: `ASYLUM.24` vs nearby `ASYLUM` ordinals
-`ASYLUM.24` remains unresolved by name, but its call pattern is now narrower.
- In `runtime_cache_reset_sequence` (`0004:2592`), it is a parameterless import call placed after `game_mode_init(*(0x27c4))` and before `cache_reset_runtime_state` plus the tracked-handle/cache-side reset tail.
- That makes it look like a module-level reset/init hook rather than a per-object method.
- Nearby `ASYLUM` ordinals in the seg011 caller at `0004:6f15` show a different pattern:
-`ASYLUM.36` returns an object-like handle that is used immediately through indirect vtable calls.
-`ASYLUM.37` is then called with explicit arguments against that object flow.
- Current conservative conclusion:
-`ASYLUM.24` is probably from the same external module family, but it does not currently match the object-construction / object-method calling pattern observed for `ASYLUM.36` and `ASYLUM.37`.
- Keep the import unresolved by name until another caller or string anchor narrows the exact module role.
- Recovery of `cache_init` required a conservative boundary repair: a stray function object `FUN_000a_eee3` had incorrectly claimed body range `000a:6710-000a:fe79`, blocking creation of the real `cache_init` body.
- The bad overlap was removed, `cache_init` was created at `000a:6600-67d8`, and `FUN_000a_eee3` was recreated conservatively as the contiguous visible body `000a:eee3-f00b`.
Current interpretation of the `0x69ac` / `0x4673` client layer:
- It is an entity-linked consumer of the generic cache manager.
- Its bucket values (`0x40`, `0x32`, `0x20`, `0x10`, `0x08`) still look attenuation- or priority-like rather than purely visibility-like.
- That is enough to keep the structural names, but not enough yet to safely promote the subsystem to a concrete audio/effect name.
| `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) |