- **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.
| `0x279a` | `cheat_code_check` | Checks entity byte+1 against a five-byte event-code table at 0x2833 (`50 80 3e fd 27 00`, counter 0x283d); on full match, toggles 0x844/0x6045, emits helper/event code `0x103`, and takes one of two local success-side dispatch paths based on the new toggle state |
### Follow-up: `cheat_code_check` internals
- Direct raw-EXE recovery now tightens the cheat path substantially:
-`cheat_code_check` in `CRUSADER-RAW.EXE` is `0007:0d0a-0007:0e08`.
- It has exactly one direct caller in this build: `FUN_0007_04dc` at `0007:0511`, which prepares a small local event record and then calls `cheat_code_check` before continuing normal input dispatch.
- The byte compared on each call is `entity_or_event_record[+1]`, not a character fetched from a typed string buffer.
- Ghidra-side cleanup is now applied inside the function too: the decompiler shows `input_event_record`, `input_event_offset`, `new_cheat_enabled`, and `cheat_status_display_root`, plus a function comment describing the matched sequence and the cheat-toggle side effects.
- Variable and constant roles from the recovered body:
-`0x2833` is a local byte table, not an ASCII string in the raw EXE. The first five bytes are `50 80 3e fd 27`, followed by a `0x00` terminator. The checker walks that table one byte at a time.
-`0x283d` is the current match index into that byte table. On a successful byte match it increments; on mismatch it resets to zero and immediately retries the current input byte against the first table byte, so overlapping prefixes still work.
-`entity[+1]` is the compared input/event token. Because the cheat table is non-ASCII bytes, this path is matching higher-level input/event codes rather than literal typed letters.
-`0x844` is the main cheat-enable flag. The success path toggles it by converting the current byte to a boolean and negating it (`0 -> 1`, non-zero -> `0`).
-`0x6045` is written with the same post-toggle value as `0x844`, so it is a mirrored or secondary cheat-state latch rather than an independent control.
- Constant `0x103` is pushed into the shared helper at `000a:5276` immediately after the toggle. Existing notes already tie that constant to event `0x42f`; the local meaning is “emit the cheat-toggle side effect” rather than part of the matcher itself.
-`0x8c52` is forced to `1` on success before the side-effect path continues.
- Success-path structure:
- After a full table match, the code resets `0x283d` to zero, sets `0x8c52 = 1`, toggles `0x844` and `0x6045`, and calls the shared `0x103` helper.
- It then branches on the new cheat state. The `cheats-on` path uses local code pointer `DS:0x287b`; the `cheats-off` path uses `DS:0x2892`.
- Those values are not text strings and not vtable IDs. They land inside local helper code around `entity_registry_decrement` (`0007:286d`) and `entity_sprite_move_delta` (`0007:2884`), then pass through `display_null_check_dispatch` (`000b:1446`) and `sprite_node_get_or_traverse` (`000a:b988`). The exact UI/presentation object built by that path is still open, but it is clearly a local success-side dispatch path rather than “spawn vtable 0x287b/0x2892”.
- Conservative conclusion:
-`cheat_code_check` is a compact stateful matcher over event-code bytes, not text input.
- The interesting remaining question is what upstream input normalization turns user actions into the five-byte sequence `50 80 3e fd 27`, and what exact presentation object or notification path the two success-side helper targets construct for cheats-on versus cheats-off.
### Follow-up: cheat-enable sources and verified cheat-only actions
- Two independent cheat-enable sources are now verified in this build:
- The hidden input matcher in `cheat_code_check` toggles `0x844` and `0x6045` after matching the five-byte event-code table at `0x2833`.
- The command-line parser at `0004:635c-0004:63b8` recognizes the literal switch `-laurie` and directly sets `0x844 = 1` before taking a local notification path. This is the clearest readable cheat-enabler in the raw EXE.
-`jassica16` is still not directly visible in the raw EXE:
- No literal `jassica` string is present in the current string table, while `-laurie` is present as plain text.
- The matcher table remains the raw byte sequence `50 80 3e fd 27 00`, so if `jassica16` is a real player-facing input for this build it must be produced by an upstream normalization/compression step rather than stored as literal text.
- That upstream producer is still open; current evidence does not justify claiming a direct byte-for-character correspondence.
- A plain `F10` cheat action is now verified in the low-level keyboard path:
-`seg001_input_keyboard_handler` at `0006:ec29` handles input byte `0x44` and immediately returns unless cheats are enabled through `0x6045`.
- That branch does not test a modifier bit before the cheat action, so the code currently supports “plain F10 when cheats are enabled” much more strongly than “Ctrl+F10”.
- The branch emits event `0x261`, refreshes the active `0x7e22` entity/object lane, rebuilds or destroys several linked entities, and fires the follow-up event batch `0x33d`, `0x33f`, `0x340`, `0x341`, `0x33e` before re-enabling channels `4`, `1`, and `0`.
- The exact gameplay-side names of those follow-up events are still open, but this is consistent with a substantial restore/reset path such as the reported full-heal/resurrect action. No separate god-mode latch has been found in this branch.
-`FUN_0007_04dc` exposes a second cluster of cheat-only hotkeys once the same cheat gate is open:
- Confirmed byte tests in the caller-side dispatch are `0x37`, `0x4a`, `0x4e`, `0x52`, `0x53`, `0x0f`, `0x24`, plus the already-visible character events `'9'` and `'R'`.
-`0x37` calls `000c:8072`, while `0x4a` calls the neighboring helper at `000c:81c0`.
-`000c:8072` cycles a small `1..5` selector, writes that choice into the per-entity `0x7e1e` table at field `+0x15` through `FUN_0006_162d`, chooses one of the small sprite/state IDs `0x2e`, `0x2f`, `0x24`, `0x25`, and then calls `entity_table_set_sprite` at `0007:14af`.
-`000c:81c0` walks a broader `0x0b..0x19` selector range and writes the chosen value into the same per-entity table at field `+0x19` through `FUN_0006_1671`.
- Together these two helpers look like cheat/debug selector tooling tied to the current `0x7e22` object lane, not to health, invulnerability, or the cheat-enable toggle itself.
-`0x4e`, `0x52`, and `0x53` all route through the same object-method dispatch on the currently selected `0x49fb` entry after building a shared argument block with `func_0x000b2e00`, which again looks like debug/view tooling rather than a passive status flag.
- The UI/event layer also exposes multiple cheat-gated overlay or visualizer toggles behind internal event codes:
- Event `0x441` reaches `000c:8e16` and toggles byte `0xee0` before refreshing the `0x2bd8` controller object.
- Event `0x241` reaches `000c:8e46` and toggles byte `0x2bc9` before the same refresh.
- Event `0x141` reaches `000c:8e72` and toggles byte `0x2bca` before the same refresh.
- Event `0x410` reaches `000c:9703` and toggles byte `0x604f`, then takes one of two display/notification paths.
- Events `0x142` and `0x143` also dispatch into large cheat-gated view-building paths at `000c:9154` and `000c:92cd`; they clearly redraw substantial display state, but their exact user-facing names remain open.
- Current bottom line on the folklore claims:
- “Cheats can be enabled with `-laurie`” is directly verified.
- “There is a hidden input-sequence cheat enabler” is directly verified, but its exact human-readable spelling is still unresolved at the binary level.
- “F10 performs a large cheat-only restore/reset action” is directly verified.
- “Ctrl+F10 enables god mode” is not supported by the current code path; the verified F10 branch does not require a modifier and no dedicated god-mode latch has been recovered yet.
- “Other cheat-only debug visualizers/tools exist” is directly verified, though several are still known only by internal event codes or selector helpers rather than final user-facing names.
| `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]`. |
| `0007:ba00` | `watch_entity_controller_create_global` | Thin global-create wrapper around `watch_entity_controller_create`; allocates default controller object and stores it at `0x2bd8`. |
| `0007:ba13` | `watch_entity_controller_dispatch_if_present` | If `0x2bd8` is non-null, calls controller vtable slots `+0x2c` and `+0x30`. |
| `0007:ba45` | `watch_entity_controller_create` | Allocates/initializes a type `0x2c2b` controller object, stores it at `0x2bd8`, sets event type `0x0219`, and installs callback-table entry `0x2be4` through `0x39ca`. |
| `0007:bab5` | `entity_set_watch_ptr` | Legacy name still in place, but newer constructor evidence now shows `0x2bd8` is a controller object lane rather than just a raw watched-entity FAR pointer. |
| `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`. |
| `0x2bd8` | `g_watch_entity_controller_farptr` | FAR ptr to the watch/camera controller object created by `watch_entity_controller_create`; older notes treating it as a raw entity pointer were too narrow |
| `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:
- The larger fallthrough body rooted at `000c:c890` is now also recovered as a real function object: `transition_preentry_release_resources` (`000c:c890-000c:c9f3`).
- The previously blocked seg126 helpers are now also recovered as standalone functions after overlap repair in the surrounding namespace:
- 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`.
-`transition_preentry_run_until_complete_or_abort` is the short pre-entry state wrapper: it runs local seg126 setup helpers, repeatedly executes a local prep loop while local ready byte `0x62fe` is clear and external gate word `0x31a2` remains zero, ticks the local palette-fade controller on each pass, then dispatches into `transition_preentry_release_resources` before returning to callers that continue into `FUN_0004_1e00`.
-`transition_preentry_release_resources` is the cleanup/finalize body behind that wrapper: it releases the paired temporary presenter objects at `0x8c5c` and `0x8c60`, conditionally frees the local stream buffer at `0x6301:0x6303`, runs palette/render reset, conditionally constructs animation state through `animation_ctor_variant_a` on local storage `DS:0x6341` when `0x844` and `0x62fe` are both set, then immediately marks byte `+0x40` on the shared global owner at `0x6828`, marks the active dispatch entry, primes sprite redraw state, drains the event queue, and zeroes `0x8a94-0x8a98` before returning.
-`wait_for_vga_vertical_retrace` (`000c:c62c-000c:c639`) is now recovered as a real helper: it polls VGA status port `0x3da` until the vertical-retrace edge and is called from both the seg126 pre-entry loop and nearby fade/update paths.
-`transition_preentry_setup_resources` captures baseline coordinates into `0x8c58:0x8c5a`, constructs two paired temporary presenter objects at `0x8c5c:0x8c5e` and `0x8c60:0x8c62` through `000a:9748` with preset IDs `0x10` and `0x11`, stages the local stream buffer at `0x6301:0x6303`, seeds palette/render state, and resets the neighboring control globals at `0x62fa-0x6318` before returning.
- The shared seg088 helpers behind that object family are now tighter too: `text_renderer_measure_string_width` (`000a:30aa`) returns width through the object's virtual measure path, and `text_renderer_draw_string_at` (`000a:30d7`) stores x/y into the object and draws a null-terminated string. That is enough to reclassify `0x8c5c` and `0x8c60` as a paired temporary text-renderer lane rather than generic sprite/object state.
-`transition_preentry_step_script` early-outs while fade state `0x630a` is active or when the tracked object position at `0x2de4+0x40/+0x42` is unchanged, decrements the local countdown at `0x62fa:0x62fc`, interprets the `0x6301` byte stream with control values including `0x21`, `0x23`, `0x24`, `0x26`, `0x2a`, `0x40`, and `0x5e`, uses `0x62ff` as the stream cursor, uses the temporary text-renderer objects at `0x8c5c` and `0x8c60` for layout/render work, sets ready flag `0x62fe` on the `0x23` case, uses `0x6305` as a one-shot redraw latch around the `0x26`/`0x2a` control path, and leaves `0x630a` / `0x630b` to the neighboring palette-fade controller.
- Direct follow-up on the old `0x31a2` ambiguity now points away from local script state: `event_queue_state_reset` at `0008:89c1` clears `0x31a2`, interrupt-side queue code around `0008:a283` / `0008:a314` increments and decrements it, and several busy-wait helpers (`000c:e4d8`, `000c:e546`, `000c:e5c6`) spin on it. In the seg126 wrapper this makes `0x31a2` a plausible external input/event break gate rather than part of the local pre-entry bytecode interpreter.
- The seg136 owner flag at `g_active_dispatch_entry_farptr[+0x40]` is now less abstract too. `active_dispatch_entry_mark_enabled` / `active_dispatch_entry_mark_disabled` still force it high or low during entry setup, but nearby seg136 helpers also copy it into fresh entries, clear it when the current owner becomes inactive, and decrement it only while `0x31a2 > 0`. That ties the `0x6828` owner lane more directly to the same external input/event gate that seg126 polls.
- The same segment now also has a clearer transition-control shell around that prep body:
-`thunk_callf_0000_ffff_000c_827d` is the pre-transition side that restores redraw/event state, runs the `0x2bd8` controller callbacks, redraws the `0x5e82:0x5e84` sprite/object pair, and leaves local state bytes set for the subsequent `FUN_0004_1e00` call.
-`thunk_callf_0000_ffff_000c_82f9` is the post-transition side that resets the slot table through `FUN_0008_39e9`, clears local state bytes, runs the `0x5e82:0x5e84` cleanup path, and returns the lane to its quiescent state.
-`FUN_000c_834a` is a small guard wrapper that conditionally calls `FUN_000c_8231()` when gate byte `0x85f` is set; this same helper is used at the start of `FUN_0004_1e00` and in the local seg126 caller family.
- seg126 is now beyond a mere foothold: it contains a coherent transition-entry and transition-exit control lane around the seg005 startup/display state, with pre-entry prep, guarded entry, post-transition cleanup, and local state/fade integration.
- The unresolved part is the exact higher-level UI/transition role of the paired text-renderer lanes at `0x8c5c` and `0x8c60`, the precise event semantics of external gate `0x31a2`, and the exact relationship between local animation storage `DS:0x6341` and the shared global owner at `0x6828` whose `+0x40` byte follows that same gate, not whether the transition lane itself is real.
### Follow-up: seg127 palette fade controller tied to the same transition lane
- The nearby seg127 state/fade controller is now anchored by verified functions in Ghidra:
-`transition_palette_fade_out_step` (`000c:cdde-000c:ce56`) and `transition_palette_fade_in_step` (`000c:ce57-000c:cecb`)
- Current verified behavior from direct MCP recovery/decompile:
-`palette_fade_begin_full_up` and `palette_fade_begin_full_down` are fixed-range wrappers over `transition_palette_fade_begin`: both use the full `0x80`-entry palette range with step size `4`, differing only in direction/state (`2` for up, `1` for down).
- The current local callers are now visible too: `000c:cd1a` invokes `palette_fade_begin_full_up` for the `ES:[DI] == 0x26` case and sets `0x6305 = 1`, while `000c:cd3f` and `000c:cb06` invoke `palette_fade_begin_full_down` when the fade controller is idle.
-`transition_palette_fade_begin` takes a palette source pointer, start index, count, step amount, and direction/state, stores them into local controller state at `0x630e-0x6316`, sets active flag byte `0x630a = 1`, stores the direction/state word at `0x630b`, then runs the local prep helper and one immediate fade step.
-`transition_palette_fade_tick` is the small controller gate over that state: it returns when inactive, dispatches to `transition_palette_fade_out_step` when `0x630b == 1`, and to `transition_palette_fade_in_step` when `0x630b == 2`.
-`transition_palette_fade_out_step` and `transition_palette_fade_in_step` iterate over palette range `[0x6312 .. 0x6312 + 0x6314)`, writing VGA DAC entries through ports `0x3c8/0x3c9` from the source palette at `0x630e:0x6310` while updating brightness offset byte `0x630d` by step `0x6316`; both clear active flag `0x630a` when the fade reaches its terminal black or full-bright state.
- This controller is tied directly into the transition lane already under study: `transition_preentry_run_until_complete_or_abort` calls `transition_palette_fade_tick`, and `transition_preentry_setup_resources` seeds the neighboring seg126 controller state at `0x62fa-0x6318` before the script interpreter starts using it.
- Conservative conclusion:
- seg127 is no longer just a foothold; it is a real palette fade controller subsystem adjacent to the same startup/display entry path, with verified initializer, dispatcher, step bodies, fixed-range wrappers, and caller-side state gating.
- The remaining question is not the local fade mechanics, but which exact transition states and tracked objects choose the fade direction and palette source.
### Follow-up: seg049 watch/camera controller object at `0x2bd8`
- The longstanding `0x2bd8` watch/camera lane is now tighter and partially corrected from older notes: it is not just a raw watched-entity pointer, but a real controller object lane.
- Current verified behavior from direct MCP recovery/decompile:
-`watch_entity_controller_create` (`0007:ba45`) allocates or reuses an object, runs the local constructor path at `000a:8627`, stamps type `0x2c2b`, stores the resulting FAR pointer globally at `0x2bd8`, sets event type `0x0219`, and installs callback target `0x2be4` through the callback table at `0x39ca`.
-`watch_entity_controller_create_global` (`0007:ba00`) is a thin wrapper that constructs the default global controller and stores the returned FAR pointer at `0x2bd8:0x2bda`.
-`watch_entity_controller_dispatch_if_present` (`0007:ba13`) is the paired non-null dispatcher that calls controller vtable slots `+0x2c` and `+0x30` when the global exists.
- Existing callers in the seg005 transition lane (`FUN_0004_1e00`) call through the same `0x2bd8` vtable `+0x2c` slot before palette restore and post-transition redraw prep, which strengthens the interpretation that this is a real watch/camera-side controller object participating in display-state transitions.
- Conservative conclusion:
- seg049 now has a real foothold in the watch/camera controller subsystem.
- The remaining ambiguity is the exact distinction between the controller object at `0x2bd8` and the ultimately watched entity or map object it may point at or manage, not whether the controller itself is real.
### Follow-up: seg108 sprite/object flag lane at `0x4f38`
- The global sprite/object lane at `0x4f38:0x4f3a` now has two recovered flag helpers in addition to the existing redraw/timer sync evidence:
- Current verified behavior from direct MCP recovery/decompile:
-`sprite_object_clear_flag40_if_present` checks whether the global sprite/object FAR pointer at `0x4f38` is non-null and clears bit `0x40` in the word at object offset `+0x32`.
-`sprite_object_set_flag40_if_present` performs the same guard and sets bit `0x40` at that same `+0x32` word.
- This fits the already-confirmed sync behavior in `sprite_node_get_or_traverse`, where `0x4588` callback emissions are immediately mirrored through `FUN_000b_1e39` using the global sprite/object pointer. Together, that narrows `0x4f38` to an active sprite/object instance whose state bits are toggled during the same startup/display transition path that uses `FUN_0004_60c0` and `FUN_0004_1e00`.
- Conservative conclusion:
- seg108 now has a real foothold in the active sprite/object state lane.
- The remaining ambiguity is what bit `0x40` means semantically and how the `0x4f38` object relates at a higher level to the `0x2bd8` watch/camera controller and the `0x4588` callback object.
### 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) |