Continued decomp

This commit is contained in:
MaddoScientisto 2026-03-20 00:24:27 +01:00
commit 6b9eb205d4
7 changed files with 769 additions and 14 deletions

View file

@ -25,9 +25,11 @@ applyTo: "**"
- Prefer a single decompile call first.
- If the decompiler collapses to thunk-heavy output, use one disassembly lookup to confirm the wrapper or parameter setup.
- **When `decompile_function` output is too large** (>~50KB), the result is written to a temp JSON file that `read_file` returns as empty `{}`. Use `disassemble_function` instead — it returns inline assembly directly and is fully navigable for large functions.
- Add a short decompiler comment when a rename is mapped from verified notes so the provenance stays visible in Ghidra.
- Keep `crusader_decompilation_notes.md` updated after each verified batch.
- Record raw-import addresses alongside original segment-relative offsets when porting names.
- **Always use `rename_function_by_address`**`rename_function` (by name) fails with "must have required property 'old_name'" and is broken. Use `"function_address": "000c:XXXX"` format.
# Current Verified Raw-Import Ports

Binary file not shown.

View file

@ -1,4 +1,10 @@
VERSION=1
/
NEXT-ID:0
00000002:udf_c0a8647bd82013090643707800:c0a8647bd36436342237796300
00000004:udf_c0a8647bdaf715949048416300:c0a8647bd36636342258818700
00000005:udf_c0a8647bdba516037949612600:c0a8647bd36736342281220400
00000000:udf_c0a8647bf0178892741854800:c0a8647bd36236342207469100
00000001:udf_c0a8647bf4b212984786819600:c0a8647bd36336342224113900
00000003:udf_c0a8647bfe7615910786193500:c0a8647bd36536342248279100
NEXT-ID:6
MD5:d41d8cd98f00b204e9800998ecf8427e

View file

@ -1,12 +0,0 @@
IADD:00000000:/udf_c0a8647bf0178892741854800
IDSET:/udf_c0a8647bf0178892741854800:c0a8647bd36236342207469100
IADD:00000001:/udf_c0a8647bf4b212984786819600
IDSET:/udf_c0a8647bf4b212984786819600:c0a8647bd36336342224113900
IADD:00000002:/udf_c0a8647bd82013090643707800
IDSET:/udf_c0a8647bd82013090643707800:c0a8647bd36436342237796300
IADD:00000003:/udf_c0a8647bfe7615910786193500
IDSET:/udf_c0a8647bfe7615910786193500:c0a8647bd36536342248279100
IADD:00000004:/udf_c0a8647bdaf715949048416300
IDSET:/udf_c0a8647bdaf715949048416300:c0a8647bd36636342258818700
IADD:00000005:/udf_c0a8647bdba516037949612600
IDSET:/udf_c0a8647bdba516037949612600:c0a8647bd36736342281220400

View file

@ -68,6 +68,147 @@
- Uses entity flags at `+0x4d` to select increment/decrement behavior for clockwise/counterclockwise facing updates.
- Called from the large gameplay update state machine at `0007:5b9a` inside `FUN_0007_5b6f`.
### Raw 0007 Gameplay Helper Deep Dive: snap_entity_to_ground
- Function: `0007:2207` = `snap_entity_to_ground`
- Caller in gameplay flow: `spawn_entity_checked` (`0007:22de`, call at `0007:2366`)
- Purpose (high confidence): pre-spawn position adjustment for a small allow-list of entity types so they land on valid ground/height context before normal spawn allocation.
#### Variable replacement pass (applied in Ghidra)
- `param_1` -> `entity_type`
- `local_48` -> `snap_entity_type_table`
- `local_34` -> `snap_dispatch_seg_table`
- `local_20` -> `snap_dispatch_off_table`
- `local_c` -> `entity_type_cursor`
- `local_4` -> `dispatch_index`
#### What the function does structurally
1. Copies three 10-entry static tables into stack-local scratch buffers:
- from `0x2910` into `snap_dispatch_off_table`
- from `0x2924` into `snap_dispatch_seg_table`
- from `0x2938` into `snap_entity_type_table`
2. Performs a linear scan across 10 entity IDs in `snap_entity_type_table`.
3. If `entity_type` matches an entry, it calls into the unresolved shared FAR thunk target (`0000:ffff`) with spawn coordinate-derived arguments.
4. If no table entry matches, it exits without modifying the request.
#### Why this is "snap to ground" behavior
- The only known caller (`spawn_entity_checked`) gates this function behind:
- global mode flag `*(char *)0x27fe != 0`
- a hardcoded list of exactly 10 entity IDs (`0x31c`, `0x31f`, `0x320`, `0x321`, `0x322`, `0x323`, `0x324`, `0x325`, `0x326`, `0x327`)
- That caller prepares local spawn position values, calls `snap_entity_to_ground`, then proceeds to spawn logic. This pattern strongly indicates pre-placement correction rather than generic AI or render logic.
- The segment/offset companion tables strongly suggest per-entity handler dispatch metadata (or per-entity parameter blocks) used by the thunked path.
#### Current limitation in raw import
- The common thunk endpoint is still imported as `unresolved_far_thunk_dispatch` at `0000:ffff`.
- In this raw database, that body decompiles as overlapped/bad instruction data, so exact arithmetic internals of the final coordinate projection cannot yet be recovered from this symbol alone.
- Despite that, caller context + table shape + argument flow make the gameplay role of this helper clear enough for naming and control-flow analysis.
#### Working pseudocode (behavioral)
```c
void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
copy_10_words(local_off_table, DATA_2910);
copy_10_words(local_seg_table, DATA_2924);
copy_10_words(local_type_table, DATA_2938);
for (dispatch_index = 0; dispatch_index < 10; dispatch_index++) {
if (local_type_table[dispatch_index] == entity_type) {
// Through unresolved FAR thunk in raw import.
// Uses spawn position context to compute a ground-aligned placement.
call_thunk_with_spawn_context(spawn_x, spawn_y, ...);
}
}
}
```
#### Next RE target (to close remaining uncertainty)
- Recover the true callee behind `0000:ffff` for the `0007:224b` call site by relocation/import-table reconstruction or by matching this call path in a cleaner segment-mapped database. That should reveal exact per-slot use of the two dispatch tables and final coordinate math.
### Raw 0007 Gameplay Helper Follow-up: AI sweep + checked spawn path
- Additional gameplay-side annotation pass completed directly in `CRUSADER-RAW.EXE`.
#### `spawn_entity_checked` (`0007:22de`) refinements
- Function signature was expanded to 7 arguments in Ghidra so stack arguments remain visible in decompile:
- `entity_type`, `spawn_flags_a`, `spawn_flags_b`, `spawn_flags_c`, `spawn_x`, `spawn_y`, `spawn_layer_arg`
- Parameter/local naming pass applied for readability in decompiled output.
- New comments added at key control-flow points:
- `0007:22f8`: allow-list gate for ground-snap mode (`0x27fe != 0` + entity IDs `0x31c..0x327` subset)
- `0007:2366`: explicit `snap_entity_to_ground(entity_type, &spawn_x, &spawn_y, &spawn_layer)` handoff
- `0007:247e`: fallback path that calls core `entity_spawn` with original arguments
- Current caveat:
- Decompiler still aliases the temporary y/layer scratch region imperfectly around the thunked call site, but disassembly confirms the call setup uses local `x/y/layer` temporaries (`[bp-6]`, `[bp-8]`, `[bp-9]`) before spawn.
#### `entity_ai_update_loop` (`0007:0fb6`) structural recovery
- Added disassembly + decompiler comments capturing stable behavior:
- Reads player entity FAR pointer from global `0x2de4`.
- Copies player world position fields (`+0x40`, `+0x42`) into globals `0x27e7` / `0x27e9` (AI focus position cache used by downstream logic).
- Iterates entity IDs from `2` through `255` and dispatches per-entity processing through the shared thunk path.
- This function now has enough recovered semantics to treat it as the frame-level AI sweep dispatcher even though individual thunked callees remain unresolved in the raw import.
### Raw 0007 Gameplay Logic: animation / range / command globals
#### `is_player_in_range` (`0007:0f79`) — fully recoverable
- Prototype updated in Ghidra: `int is_player_in_range(int entity_x, int entity_y)`
- Reads player world position from `g_player_entity_farptr` (`0x2de4`, fields `+0x40` (x) and `+0x42` (y)`.
- Computes unsigned delta from AI focus globals `g_ai_focus_pos_x` (`0x27e7`) / `g_ai_focus_pos_y` (`0x27e9`).
- Returns 1 if player Y delta == 0 AND player X delta < 0xF0 (240 world units), else 0.
- Only confirmed caller so far: `0007:0bcb` (in unanalyzed function region).
#### `entity_animation_frame_update` (`0007:26e2`) — fully decompiled
- Prototype updated: `void entity_animation_frame_update(int *entity_ptr)`
- Key globals read:
- `g_anim_tick_counter` (`0x3a00`) — frame timing tick counter.
- `g_anim_tick_overdrive_flag` (`0x3a02`) — if set, forces max-advance (4 steps).
- `g_speed_double_flag` (`0x27fd`) — doubles speed_factor to 2 when set (fast game mode).
- Local variables renamed: `speed_factor` (1 or 2) and `advance_steps` (04, number of frame advances this tick).
- Entity struct fields confirmed (relative to `entity_ptr` as `int*`):
- `[0x1b]` = frame_min (backward direction counter)
- `[0x1c]` = frame_max
- `[0x1d]` = current_frame
- `[0x1e]` = loop_flag
- `[0x1f]` = reverse_direction_flag
- `+0x3f` (as `char*`) = completion handle/sentinel (`-1` = none, `0x2802` = player entity)
- On frame overflow: if completion handle valid and not player-entity, fires thunked event; calls vtable `[+8]` method.
- Added decompiler comment at function entry explaining all fields and behavior.
#### `entity_command_dispatch` (`0007:0990`) — partially decompiled
- Prototype: `void entity_command_dispatch(int entity_handle, int target_seg, int command_type, byte absolute_pos_flag)`
- When `absolute_pos_flag == 0`: computes player-relative delta using `g_player_entity_farptr` and stores result:
- delta_x → `g_player_delta_x` (`0x27f5`)
- delta_y → `g_player_delta_y` (`0x27f7`)
- Clears cached origin globals `g_cmd_effect_origin_x` (`0x27f1`) and `g_cmd_effect_origin_y` (`0x27f3`) after use.
- Dispatches entity command through shared thunk; actual command table data not yet resolved.
- No incoming XREFs found in the raw import (likely called via table or vtable dispatch).
#### Global map additions (renamed in Ghidra)
| Address | Name | Evidence |
|---------|------|---------|
| `0x2de4` | `g_player_entity_farptr` | FAR ptr to player entity; `+0x40`/`+0x42` are world X/Y |
| `0x27e7` | `g_ai_focus_pos_x` | Set by `entity_ai_update_loop` from player entity `+0x40` |
| `0x27e9` | `g_ai_focus_pos_y` | Set by `entity_ai_update_loop` from player entity `+0x42` |
| `0x27f1` | `g_cmd_effect_origin_x` | Cached effect origin X, cleared after delta in `entity_command_dispatch` |
| `0x27f3` | `g_cmd_effect_origin_y` | Cached effect origin Y |
| `0x27f5` | `g_player_delta_x` | Player X delta from last effect origin |
| `0x27f7` | `g_player_delta_y` | Player Y delta from last effect origin |
| `0x27fd` | `g_speed_double_flag` | 0 = normal, 1 = double speed animation |
| `0x27fe` | `g_ground_snap_mode_flag` | Non-zero = ground-snap prepass active for placements |
| `0x27d0` | `g_entity_update_max_id` | Max entity ID used by `entity_ai_update_loop` sweep |
| `0x3a00` | `g_anim_tick_counter` | Animation tick counter for frame-advance step budget |
| `0x3a02` | `g_anim_tick_overdrive_flag` | 0 = normal, non-zero = force max frame advance step |
| `0x2802` | `g_player_entity_handle` | Player entity handle (used as sentinel in animation completion checks) |
### Raw 000e Parser Helper Cluster
- A small helper cluster in the raw `000e:` area now appears to implement a fixed-size CRLF record parser/table builder, likely used by startup/config or script-ish text data.
@ -521,7 +662,625 @@ All 35+ identified functions renamed and annotated in Ghidra.
5. **Cross-reference entity type constants** with game entities (robots, platforms, triggers)
6. **Identify external segment calls** — the `func_0x0000ffff()` placeholders are all cross-segment calls; resolving them requires importing the referenced segments
---
1. ✅ **NE Segment 1 imported and analyzed** — all 58 identified functions renamed and annotated
2. ✅ **Raw 0007 segment analyzed** — rendering, camera/scroll, save slot, and scroll region subsystems documented (~60 functions renamed and annotated)
3. **Import additional NE segments** — priority: segments 22, 30, 59, 86 (segment 21 complete)
4. **Analyze raw 0007 draw helper cluster**`FUN_0007_03b4`, `FUN_0007_04b8`, `FUN_0007_04dc`, `FUN_0007_057f`, `FUN_0007_0614`; called by sprite/draw list functions
5. **Analyze `FUN_0007_4cdf`** — large 15-case animation/movement dispatcher; overlapping instruction warnings; cases 0, 2, 3, 6, 9, 0xa, 0xe are clean
6. **Map file format loaders**`.FLX`, `.SHP`, `.MAP`, `.TNT` resource formats
7. **Cross-reference entity type constants** with game entities (robots, platforms, triggers)
8. **Identify external segment calls** — the `func_0x0000ffff()` placeholders are all cross-segment calls; resolving them requires importing the referenced segments
## Raw 0007 Rendering & Sprite Draw List Subsystem (new)
A complete sprite draw list and tile-based visibility system was recovered from the `0007:e0000007:fe00` flat range (above seg001, separate segment loaded at `0x7E000+`).
### Isometric Coordinate Transform
| Address | Name | Evidence |
|---------|------|---------|
| `0007:be67` | `world_to_screen_isometric` | Classic 2:1 isometric formula: `screen_x = (wx + sx) + (wy + sy)*2`, `screen_y = (wy + sy)*2 - (wx + sx)`. Scroll globals: `0x2bb7` (X), `0x2bb9` (Y). Output to `*param_3`, `*param_4`. |
| `0007:be9e` | `world_to_screen_isometric_wrapper` | Thin wrapper — calls `world_to_screen_isometric` with args shifted by one |
### Draw List Node Format
Sprite/draw node (size `0x18` = 24 bytes from pool, allocated by `linked_list_pop_2cc3`):
| Offset | Field | Notes |
|--------|-------|-------|
| `+0x0` | vtable | Function ptr for render callback (vtable[0] = draw) |
| `+0x8` | dep_from_list | Near ptr to list of sprites that depend ON this |
| `+0xa` | dep_to_list | Near ptr to list of sprites this depends ON |
| `+0xc` | bbox_xmin | Screen bounding box (4 ints) |
| `+0xe` | bbox_ymin | |
| `+0x10` | bbox_xmax | |
| `+0x12` | bbox_ymax | |
| `+0x14` | flags | bit 0=queued in draw list, bit 5=visible/rendered, bit 6=root sentinel, bit 7=must-redraw |
| `+0x15` | z_depth | int; sort key for painter's algorithm. 1 = root sentinel |
| `+0x17` | order_flag | if set, propagates must-redraw to dependent sprites |
| `+0x18` | tile_index | sparse tile index (for dirty bitmask bit addressing) |
### Draw List Functions
| Address | Name | Evidence |
|---------|------|---------|
| `0007:eb36` | `drawlist_pool_init` | Inits free pool: 1500 nodes × `0x2a` (42) bytes from base `0x2cc7`. Sets `0x2ccf = 1500` (count), `0x2cc3` = linked list head. Copies `0x2cc9``0x2cae`. |
| `0007:eb12` | `linked_list_push_2cc3` | LIFO push: `node->next = head_2cc3; head = node; count_2ccf++`. |
| `0007:eada` | `linked_list_pop_2cc3` | LIFO pop: dequeues node from `0x2cc3` head, decrements `0x2ccf` count. |
| `0007:ebd9` | `linked_list_dequeue_headtail` | FIFO dequeue from head/tail pair list; calls `FUN_0007_03ec` after dequeue. |
| `0007:ec2c` | `drawlist_enqueue` | FIFO enqueue: appends node to draw list (head if empty, tail always updated). |
| `0007:ec63` | `drawlist_remove_node` | Unlinks specific node from head or tail position; calls `FUN_0007_03ec`. |
| `0007:eca8` | `drawlist_process_and_render` | **Two-stage render pass.** Stage 1: drains `0x8442` (pending), viewport-tests each sprite, moves in-bounds to `0x8446` (visible). Stage 2: drains `0x8446`, calls sprite vtable[0] to draw. Recursive re-run if `0x2cb2` defer flag set. |
| `0007:edfa` | `drawlist_enqueue_sprite_children` | Enqueues all child sprites at node `+8` linkage to draw pending list if not already queued or rendered. Skips if `+0x15` z-depth == 1. |
| `0007:ef3d` | `sprite_add_draw_dependency` | Links sprites A and B with z-order dependency. Allocates link node via `FUN_0007_057f`. Stores in A's `+10` and B's `+8`. Sets must-redraw on B if A has `+0x17` set. |
| `0007:ef9f` | `sprite_enqueue_for_draw` | Null-guard; sets node `+0x14 \|= 1`, enqueues to `0x8442`. |
| `0007:efca` | `sprite_invalidate_and_unlink` | Full sprite removal: re-enqueues all dependents, frees link nodes via `FUN_0007_0614`, clears `+8/+10` lists, dequeues self from `0x8442`. |
| `0007:f2a0` | `sprite_sort_by_depth_and_link` | Compares `+0x15` z-depth of two sprites; calls `sprite_add_draw_dependency` in the correct order (lower z first). Handles equal-depth via thunk. |
| `0007:f654` | `drawlist_init` | Full draw system init: `drawlist_pool_init`, init full-screen viewport (`0x844a..0x8450 = 0..639, 0..screen_h`), allocate root sentinel node (vtable `FUN_0000_2ce4`, z-depth=1, flag=0x40), stored at `0x846a`. |
| `0007:ea00` | `bbox_intersect` | In-place 2D rect intersection: max of mins, min of maxes. Input/output: 4-int array `[xmin,ymin,xmax,ymax]`. |
| `0007:ea6d` | `bbox_union` | In-place 2D rect union: min of mins, max of maxes. |
| `0007:ee5a` | `viewport_update_from_sprite_bounds` | Subtracts scroll offsets from sprite bbox; clips to screen rect (`0x2ca6..0x2cac`); calls `bbox_intersect`; stores updated viewport at `0x844a..0x8450`; dispatches to render. |
### Tile-Based Visibility System
The rendering system uses a **6×5 tile grid** at DS:`0x846a` (30 entries × 2 bytes each =60 bytes). Tiles represent screen regions of approximately 128 screen pixels per tile. Each tile holds a near pointer to a linked list of sprite nodes overlapping that tile. Bitmasks track dirty and renderable tiles.
| Address | Name | Evidence |
|---------|------|---------|
| `0007:f9e2` | `drawlist_mark_dirty_tiles` | Converts bbox to tile grid coords (divide by 128, clamp to 6×5). Walks all sprites in overlapping tiles' spatial buckets; re-enqueues overlapping sprites. Sets bits in dirty bitmask `0x2cbb`. |
| `0007:fb53` | `tile_visibility_update` | Iterates redraw bitmask (`0x2cbb`, up to 10240 tiles). For each dirty tile with a sprite, computes isometric screen position from map X/Y/Z. Checks sprite dimension bitfields at `pbVar8+2..+3` against 640×480 viewport. Sets corresponding bit in render bitmask `0x2cb7`. Clears dirty bitmask after processing. |
| `0007:fd98` | `tilemap_draw_if_dirty` | Guard: if render bitmask `0x2cb7` non-zero, calls `thunk_FUN_0007_001d` to trigger tilemap draw. |
### Draw List / Viewport Globals
| Address | Name | Notes |
|---------|------|-------|
| `0x2bb7` | `g_scroll_offset_x` | World-to-screen X offset (scroll) |
| `0x2bb9` | `g_scroll_offset_y` | World-to-screen Y offset (scroll) |
| `0x2ca6..0x2cac` | `g_screen_clip_rect` | Screen clip rectangle [xmin,ymin,xmax,ymax] |
| `0x2cae` | `g_draw_segment` | DS segment for draw node far pointers |
| `0x2cb0` | `g_free_pool_seg` | Segment for node free pool far calls |
| `0x2cb2` | `g_render_defer_flag` | If set, defers current render pass |
| `0x2cb7` | `g_render_tile_bitmask` | Far ptr to bitmask of tiles ready to render |
| `0x2cbb` | `g_dirty_tile_bitmask` | Far ptr to bitmask of dirty/changed tiles |
| `0x2cbf` | `g_tile_origin_x` | Tile grid world origin X (for tile coordinate math) |
| `0x2cc1` | `g_tile_origin_y` | Tile grid world origin Y |
| `0x2cc3` | `g_free_pool_head` | Free node pool linked list head |
| `0x2ccf` | `g_free_pool_count` | Free node pool remaining count (max 1500) |
| `0x8442` | `g_draw_pending_list` | Draw pending list head/tail (near ptrs) |
| `0x8446` | `g_draw_visible_list` | Draw visible list head/tail (near ptrs) |
| `0x844a..0x8450` | `g_viewport_rect` | Current viewport [xmin,ymin,xmax,ymax] |
| `0x846a` | `g_tile_grid_base` | 6×5 tile grid spatial index (near ptr per tile) |
## Raw 0007 Map Scroll / Camera Subsystem (new)
A scroll/camera management cluster found in the `0007:bxxx0007:dxxx` range.
### Entity State Transition Helper
| Address | Name | Evidence |
|---------|------|---------|
| `0007:5b6f` | `entity_set_at_target_update_facing` | Sets entity `+0x3a = 1` (arrived flag); calls `entity_set_facing_direction`; clears bit `0x10` from entity type table `0x7e1e[type*0x79+0x59]`; tail-calls thunk to advance state. Called in the entity state machine context. |
### Animation Start Frame Helper
| Address | Name | Evidence |
|---------|------|---------|
| `0007:71b2` | `entity_set_anim_start_frame_from_flags` | Reads entity `+0x4b` flags. If bit 1 set: uses type table `+0x59 & 4` (attack active) to select last frame (`+0x39 - 1`), zero, or half-frame (`+0x39 >> 1`). Writes computed value to type table `+0x10`. If bit 2 set without bit 1: dispatch thunk. |
### Combat Helper
| Address | Name | Evidence |
|---------|------|---------|
| `0007:894b` | `entity_check_attack_flags_and_dispatch` | Guards on entity `+0x4b` bit 1 AND target object `+5` bits `0x1c`. If both set: dispatches thunk attack event. |
### Vtable Dispatch Helpers
| Address | Name | Evidence |
|---------|------|---------|
| `0007:8920` | `entity_call_vtable_slot0c` | Calls `(*param_1)[vtable+0xc]()`. |
| `0007:8cb8` | `entity_call_vtable_slot08` | Calls `(*param_1)[vtable+0x8]()`. |
| `0007:ccf1` | `entity_call_vtable_slot28` | Calls `(*param_1)[vtable+0x28]()`. |
### Active Flag / Counter
| Address | Name | Evidence |
|---------|------|---------|
| `0007:8854` | `entity_set_active_flag` | Sets entity `+0x40 = 1` (active); increments global `0x2800`. |
### Dispatch Table Lookup
| Address | Name | Evidence |
|---------|------|---------|
| `0007:8508` | `entity_table_lookup_and_dispatch` | Searches 1-entry table at `0x2b46` for `(param_3, param_4)` key pair; on match, calls the entry's function pointer at `[2]`. |
### Scroll/Camera Functions
| Address | Name | Evidence |
|---------|------|---------|
| `0007:bab5` | `entity_set_watch_ptr` | Stores FAR entity ptr to `0x2bd8` (the watch target entity). |
| `0007:baea` | `camera_update_and_check_player_scroll` | Calls watch entity vtable `+0x24`; if `0x2bd1` flag clear checks if player position (from `g_player_entity_farptr+0x40`) has moved > 32 units since `0x2be0`; if so, updates `0x2be0` and conditionally dispatches scroll event via `0x45aa`. |
| `0007:c6ba` | `scroll_camera_set_state_params` | Stores word params to `0x8354`, `0x8356`, byte to `0x8358`; dispatches. |
| `0007:cd56` | `dispatch_if_flag_2bd3_set` | Returns unless `0x2bd3` non-zero. |
| `0007:cfef` | `dispatch_if_mode_flags_set` | Two-flag check: dispatches if `0x2bca` or `0xee0` is non-zero. |
| `0007:d0f6` | `scroll_call_set_params_unless_blocked` | Calls `scroll_camera_set_state_params` only if `0x2bbb == 0`. |
| `0007:d119` | `scroll_update_direction_tracking` | Guards on `0x2bd3`. Calls `scroll_call_set_params_unless_blocked`. Compares direction bytes from `0x2cf4/0x2cf5` against cached `0x2bbd/0x2bbe`; if changed, clears `0x2bbc`. Dispatches. |
| `0007:d4a5` | `scroll_set_option_value` | Sets `0x2bc6 = param_1`. |
| `0007:d4b0` | `scroll_set_params_default` | Unconditional call to `scroll_camera_set_state_params`. |
| `0007:d4d3` | `scroll_set_map_index_validated` | If `param_1` in `[0..250]` and differs from `0x2bbf`, updates `0x2bbf` and clears `0x2bbc/0x2bbb`. |
| `0007:d655` | `map_position_has_changed` | Compares map arrays `0x7ded/0x7df1/0x7df5` at index `0x2bc6` against cached `0x2bc1/0x2bc3/0x2bc5`. Returns 1 if changed, 0 if same. |
| `0007:d6b1` | `scroll_clear_dirty_flags_and_dispatch` | Clears `0x2bbb = 0` and `0x2bbc = 0`; dispatches. |
| `0007:de57` | `entity_check_player_range_and_update` | Reads player world position (`g_player_entity_farptr+0x40`); if moved > 59 units from `entity+0x32` (cached pos), updates cache and calls `scrollregion_find_and_dispatch`. |
### Scroll Region Table (`0x835a`)
8 entries × `0x19` (25) bytes = 200 bytes. Entry layout:
| Offset | Field | Notes |
|--------|-------|-------|
| `+0x0/+0x2` | key pair | Matched by scroll region key lookup |
| `+0x8` | refcount | Reference count (incremented on match) |
| `+0xe` | count2 | Secondary counter |
| `+0x10` | active | Non-zero when region is live |
| `+0x11/+0x13` | x_start/x_end | Bounding X coordinates |
| `+0x15/+0x17` | y_start/y_end | Bounding Y coordinates |
| Address | Name | Evidence |
|---------|------|---------|
| `0007:e194` | `scrollregion_process_active` | Iterates active scroll entries (key non-zero AND `+0x10 != 0`), reads bounding box fields `+0x11/+0x13/+0x15/+0x17`, dispatches with bounds args. |
| `0007:e214` | `scrollregion_find_and_dispatch` | Finds empty scroll entry (zero key) and dispatches. |
| `0007:e29c` | `scrollregion_register` | Finds or allocates scroll region entry. Existing match: bump refcount / set active. New: init via `scrollregion_entry_init`. Special key `0x4ed`: thunk dispatch instead. |
| `0007:e50f` | `scrollregion_entry_init` | Null-guards param_1; zeroes output param_2, param_3, param_4; dispatches continuation. |
| `0007:e74a` | (unnamed) | Sets up call to walk `0x835a` table (8 entries, stride `0x19`, filter `0x968`), with callback return label `e763`. |
### Save Slot System (`0x8337` + `0x2ba3`)
10 save slots, each `0x400` (1024) bytes. Handle table at `0x8337` (10 words). Slot buffer base at FAR ptr `0x2ba3`.
| Address | Name | Evidence |
|---------|------|---------|
| `0007:ac13` | `saveslot_table_clear` | Fills `0x8337..0x834b` (10 words) with `0xFFFF` (all slots empty). |
| `0007:acab` | `saveslot_free_if_empty` | Scans slot `0x2ba3[param_1*0x400]` for non-zero data; if empty, sets handle `0x8337[param_1] = 0xFFFF`. |
| `0007:ad47` | `saveslot_find_index_by_id` | Linear scan of 10-word handle table `0x8337`; returns index of matching word or `-1`. |
| `0007:ad79` | (unnamed) | Finds a free (0xFFFF) slot index. Complement of `saveslot_find_index_by_id`. |
| `0007:afd4` | `saveslot_get_or_alloc` | Gets slot pointer: calls `saveslot_find_index_by_id`; if not found calls `ad79` to get free slot; returns `0x2ba3 + slot * 0x400`. Returns 0 if no free slot. |
| `0007:b02c` | `saveslot_write_entry` | Navigates to `slot_base[param_3 * 4]`; dispatches thunk paths for write (existing, overwrite, new). |
| `0007:b0de` | `saveslot_read_entry_flags` | Reads from slot entry far pointer at `slot_base[param_3*4]`; extracts 4-byte packed bitfield from `+4..+7` in entry record into `*param_1`. Bit-by-bit extraction loop for 4 bytes. |
### String & Memory Utilities
| Address | Name | Evidence |
|---------|------|---------|
| `0007:a96d` | `entity_copy_string_truncated80` | Strlen(param_3) ≤ 0x50 guard; copies string word-by-word from param_3 into `param_2+8`. |
| `0007:b813` | `memcpy_4words` | Copies 4 words (8 bytes) from `param_2` to `param_1`. |
| `0007:ba45` | (unnamed) | Null/non-null far-ptr dispatch: different thunk paths based on `param_2 == 0`. |
| `0007:b46d` | `entity_dispatch_if_slot82e2_valid` | Guard: if `*(int *)0x82e2 != -1`, calls dispatch thunk. |
### Linked List Utilities (draw pool + sprite)
| Address | Name | Evidence |
|---------|------|---------|
| `0007:ea00` | `bbox_intersect` | In-place rect intersection: `[xmin,ymin,xmax,ymax]` = max(mins) × min(maxes). |
| `0007:ea6d` | `bbox_union` | In-place rect union: `[xmin,ymin,xmax,ymax]` = min(mins) × max(maxes). |
### Scroll/Camera Globals
| Address | Name | Notes |
|---------|------|-------|
| `0x2bb7` | `g_scroll_offset_x` | Isometric scroll X — added to world_x in screen transform |
| `0x2bb9` | `g_scroll_offset_y` | Isometric scroll Y |
| `0x2bbb` | `g_scroll_blocked` | If non-zero, blocks `scroll_camera_set_state_params` call |
| `0x2bbc` | `g_scroll_dirty` | Scroll direction changed flag (cleared when direction tracks) |
| `0x2bbd` | `g_scroll_dir_x` | Cached scroll direction X |
| `0x2bbe` | `g_scroll_dir_y` | Cached scroll direction Y |
| `0x2bbf` | `g_map_index` | Current map/level index `[0..250]` |
| `0x2bc1/0x2bc3/0x2bc5` | `g_map_entry_x/y/z` | Cached map entry X/Y/Z (vs. live map arrays) |
| `0x2bc6` | `g_map_slot_index` | Index into `0x7ded/0x7df1/0x7df5` arrays for current map slot |
| `0x2bca/0x2bc9` | `g_option_toggle_state` | UI option toggle state flags |
| `0x2bd1` | `g_scroll_block_flag` | Blocks camera update path if non-zero |
| `0x2bd3` | `g_scroll_active` | Non-zero = scroll system active |
| `0x2bd8` | `g_watch_entity_ptr` | FAR ptr to entity being tracked by camera |
| `0x2be0` | `g_player_scroll_pos` | Cached player world X+Y (ulong) for scroll threshold detection |
| `0x8354..0x8358` | `g_scroll_state_params` | Three scroll state params (word, word, byte) |
## Raw 0008 Gameplay Dispatch Helper Batch (new)
Small conservative rename batch from direct field-write behavior in the `0008:ba00-0008:be05` cluster.
### Newly renamed functions
| Address | Name | Evidence |
|---------|------|---------|
| `0008:ba00` | `entity_dispatch_entry_init` | Constructor-style init: optional alloc (`0x32` bytes), vtable/list-link setup (`0x3b06`, `0x2d10`, `0x3afe`), zeroes state fields, seeds group from global active layer `0x39c9` via `entity_set_group_id` |
| `0008:bbb6` | `entity_set_source_type` | Writes entry word field `+0x04` from incoming parameter, then dispatches through FAR thunk path |
| `0008:bc27` | `entity_set_event_type_checked` | Writes entry word field `+0x06`; when source field `+0x04` is non-zero, validates old/new event transition, including special checks for `0xF0-0xF7` and upper bound `<= 0x0FFF` |
| `0008:bca8` | `entity_set_group_id` | Validates group id range `1..31`, writes low 5-bit group in byte `+0x08`, decrements old per-group counter and increments new one via counter table pointed to by `0x39c5` |
| `0008:be05` | `entity_increment_group_id` | Computes `((entry+0x08)&0x1F)+1`, validates against active-layer assumptions (`0x39c9`), then applies through `entity_set_group_id` |
### Verified call/xref notes
- `entity_set_group_id` is called from:
- `entity_dispatch_entry_init` at `0008:bae4`
- `entity_increment_group_id` at `0008:be57`
- `entity_set_source_type` is used from at least:
- `FUN_0008_c92f` (`0008:c94d`, `0008:c96d`)
- `FUN_0008_ca18` (`0008:ca36`, `0008:ca56`)
### Gameplay relevance
- This cluster appears to manage core dispatch-entry metadata (`source_type`, `event_type`, group/layer byte and counters) that feeds the seg021 scheduler/event system previously documented.
- The field offsets match the current seg021 entity/dispatch layout notes (`+0x04`, `+0x06`, `+0x08`), strengthening confidence in cross-function struct consistency.
## Raw 0008 Pair-Sync Helper Batch (new)
Conservative directional rename batch from the `0008:c7f1-0008:cad7` cluster.
These functions are clearly paired and structurally symmetric, but final gameplay semantics are still partial due to FAR-thunk heavy internals.
### Newly renamed functions
| Address | Name | Evidence |
|---------|------|---------|
| `0008:c7f1` | `entity_pair_update_link_slot_a` | Guards on entry flags (`+0x16` must not include `0x4000`), then dispatches through FAR thunk using entry local struct at `+0x28` and partner-side key/id input |
| `0008:c890` | `entity_pair_update_link_slot_b` | Twin of `entity_pair_update_link_slot_a` with identical call shape and guard behavior; used in opposite order by pair-sync wrappers |
| `0008:c92f` | `entity_pair_sync_a` | If either side has unset `source_type` (`+0x04`), copies from partner via `entity_set_source_type`; then calls link-slot helpers in A-order and ends in FAR thunk using first side `+0x1e` data |
| `0008:ca18` | `entity_pair_sync_b` | Mirror of `entity_pair_sync_a` with reversed side/order for helper calls and final thunk argument ordering |
| `0008:c9ee` | `entity_pair_mark_and_sync_a` | Sets bit `0x10` in entry flags at `+0x16`, then calls `entity_pair_sync_a` |
| `0008:cad7` | `entity_pair_mark_and_sync_b` | Sets bit `0x10` in entry flags at `+0x16`, then calls `entity_pair_sync_b` |
### Verified call/xref notes
- `entity_pair_sync_a` is called from `entity_pair_mark_and_sync_a` (`0008:ca10`).
- `entity_pair_sync_b` is called from `entity_pair_mark_and_sync_b` (`0008:caf9`).
- Shared helper use inside pair sync wrappers:
- `entity_pair_update_link_slot_b` at `0008:c981` and `0008:ca6a`
- `entity_pair_update_link_slot_a` at `0008:c995` and `0008:ca7e`
### Gameplay relevance
- This cluster likely handles directional two-entity relationship synchronization in the scheduler/entity-dispatch layer (source/type propagation plus paired link-slot updates).
- Offsets used here (`+0x04`, `+0x16`, `+0x1e`, `+0x28`) align with the existing seg021 object-field and linker/list usage patterns, which increases confidence while preserving conservative naming.
## Raw 0008 Flag-0x20 Target-State Helpers (new)
Two complementary helpers near the pair-sync cluster were renamed using strict field/bit behavior only.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:cb2c` | `entity_flag20_clear_and_update_target` | Clears bit `0x20` at entry flags `+0x16`; if non-null target args are provided, writes far-pointer target fields `+0x12/+0x14`; then calls shared refresh helper `0008:c01d` |
| `0008:cb5c` | `entity_flag20_set_and_init_target` | Sets bit `0x20` at entry flags `+0x16`; initializes target far-pointer fields `+0x12/+0x14` only when currently zero; then calls shared refresh helper `0008:c01d` |
Notes:
- Naming intentionally stays flag-centric because high-level gameplay meaning of bit `0x20` is not yet fully resolved.
- Both helpers share the same post-update refresh path (`0008:c01d`), suggesting they are two state transitions in one target/link-management subsystem.
## Raw 0008 Dispatch Refresh Pipeline (new)
Follow-up rename batch for the shared refresh node used by the new flag-0x20 helpers.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:c01d` | `entity_refresh_dispatch_state` | Early-exit when flags at `+0x16` indicate dead (`0x8`) or already refreshed (`0x4000`); otherwise runs pre-clear, sets `0x4000`, calls update vfunc path, then runs flag-conditioned handlers |
| `0008:bfb2` | `entity_clear_status_bits_from_flags` | Clears specific bits in status word at `+0x32` based on state flags (`+0x16:0x400`, `+0x18:0x40/0x80`) |
| `0008:bf8e` | `entity_call_update_vfunc14` | Calls helper `0008:be6b`, then dispatches entity vtable call at offset `+0x14` |
| `0008:beee` | `entity_run_flagged_handlers` | Executes handler calls gated by flags (`+0x16:0x400/0x4`, `+0x18:0x40/0x80`) and then dispatches via FAR thunk using entry slot/index (`+0x2`) |
Verified xref context:
- `entity_refresh_dispatch_state` is directly called from:
- `entity_flag20_clear_and_update_target` (`0008:cb54`)
- `entity_flag20_set_and_init_target` (`0008:cb86`)
Gameplay relevance:
- This establishes a concrete state pipeline for dispatch entries after target/link changes: flag-gated status clear -> mark refreshed (`0x4000`) -> vtable update callback -> flag-conditioned subsystem handlers.
## Raw 0008 Flag-0x100 and Constructor-Variant Batch (new)
Additional conservative renames from the `0008:d1a4-0008:d27d` cluster.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:d1a4` | `entity_set_flag100_in_flags2` | Gate-checked setter: ORs bit `0x100` into entry word at `+0x18` |
| `0008:d1dc` | `entity_clear_flag100_in_flags2` | Gate-checked clearer: ANDs entry word at `+0x18` with `0xFEFF` (clears bit `0x100`) |
| `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | Constructor variant: allocates `0x40` bytes if null, reinitializes via `0008:cefb`, sets vtable to `0x3aa6`, sets flag `0x200` at `+0x16`, zeroes fields `+0x38..+0x3e` |
Notes:
- The `entity_set_flag100_in_flags2` / `entity_clear_flag100_in_flags2` pair is a verified complementary toggle with identical gate logic (`0x39a8/0x39f9/0x3991` check path).
- Constructor naming is intentionally vtable-centric (`0x3aa6`) until more direct gameplay semantics are recovered from its callback dispatch paths.
## Raw 0008 Periodic/Counter Helpers (new)
Follow-up renames from the `0008:d313-0008:d47d` cluster tied to the `0x3aa6` constructor branch.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:d313` | `entity_periodic_accumulate_and_dispatch` | Adds global delta (`0x39d0/0x39d2`) into entry accumulator (`+0x3c/+0x3e`), wraps against period (`+0x38/+0x3a`), and on wrap invokes entry vtable callback at `+0x28` with reentrancy guard bit `0x400` in `+0x18` |
| `0008:d3e6` | `entity_set_flag2000_and_update_active_counters` | Atomic (CLI/PUSHF) set of bit `0x2000` in `+0x16`; if bit `0x400` is set, decrements global counter `0x39f6` and increments `0x39f4` |
| `0008:d433` | `entity_clear_flag2000_and_update_active_counters` | Atomic clear of bit `0x2000` in `+0x16`; if bit `0x400` is set, decrements `0x39f4` and increments `0x39f6` |
Gameplay relevance:
- This identifies a concrete periodic dispatch mechanism (accumulator+wrap callback) and a paired active/inactive counter transition path around flag `0x2000`.
- The `0x39f4/0x39f6` counter swap strongly suggests global bookkeeping for a scheduler subset associated with these entries.
## Raw 0008 Word-List Management Batch (new)
Verified helper cluster for entry-owned word-list storage (sentinel-terminated with `0x0408`).
| Address | Name | Evidence |
|---------|------|---------|
| `0008:da00` | `entity_word_list_set_0408_terminated` | Rebuilds/replaces entry list from stack-provided words terminated by `0x0408`; frees prior list pointer at `+0x06/+0x08`; allocates and populates new list |
| `0008:dba3` | `entity_word_list_free_existing` | Validates list pointer exists, then frees old list buffer referenced by `+0x06/+0x08` |
| `0008:dbec` | `entity_word_list_destroy` | Resets vtable to `0x2d10`, frees list if present via `entity_word_list_free_existing`, and optionally frees object when destroy flag bit `1` is set |
| `0008:dc38` | `entity_word_list_ensure_contains` | Scans existing list for a given word; if missing, appends through `entity_word_list_append_unique` |
| `0008:dcab` | `entity_word_list_append_unique` | Allocates larger list, copies existing words, appends new word plus `0x0408` terminator, frees old list, then rebuilds via `entity_word_list_set_0408_terminated` |
Notes:
- Entry fields used by this subsystem: count at `+0x02`, list far pointer at `+0x06/+0x08`.
- The explicit `0x0408` terminator appears both in scanner/build logic and append path, making it a reliable list format marker.
## Raw 0008 Word-List Access/Mutation Batch (new)
Follow-up renames extending the same list subsystem.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:deea` | `entity_word_list_get_at` | Bounds-checks index against count (`+0x02`) and returns word from list pointer (`+0x06/+0x08`, stride 2) |
| `0008:df1b` | `entity_word_list_set_at` | Bounds-checks index then writes value into list element (`+0x06/+0x08`, stride 2) |
| `0008:dfa1` | `entity_word_list_find_unflagged_by_id10` | Scans list and returns first value satisfying `(value & 0x400)==0` and `(value & 0x3ff)==requested_id`; writes `0` when not found |
| `0008:ddaf` | `entity_word_list_remove_value` | Removes matching value(s) by counting survivors, rebuilding compact storage for non-matching entries, freeing old list storage, and updating list state |
Notes:
- `entity_word_list_find_unflagged_by_id10` implies list entries pack a 10-bit id plus flag bits (`0x400` observed).
- This further supports that the `0008:da00..dfa1` region is a compact encoded-ID list manager used by gameplay dispatch entries.
## Raw Import Note: `0000:ffff` Thunk Target (new)
Requested deep-dive on `FUN_0000_ffff`:
- Renamed to `unresolved_far_thunk_dispatch`.
- Current raw-import evidence indicates this is **not valid local executable logic** in this program view:
- Decompiler emits overlapping-instruction warnings and bad-control-flow warnings.
- Disassembly from `0000:ffff` into `0001:xxxx` is nonsensical/misaligned (mixed data/code artifacts).
- The body is heavily shared as a call sink from many segments, consistent with unresolved inter-segment thunking in this import mode.
Practical interpretation:
- Treat calls to `unresolved_far_thunk_dispatch` as unresolved external/indirect dispatch edges, not as meaningful function internals to recover in the raw flat import.
- Semantic recovery should continue from call-site argument setup and local field effects (the workflow used in recent 0008 batches).
## Raw 0008 Gate-Callback Wrapper Batch (new)
Conservative renames for callback wrappers sharing the same global gate condition.
| Address | Name | Evidence |
|---------|------|---------|
| `0008:d00e` | `entity_gate_callback_wrapper_a` | Gate check on globals `0x39a8/0x39f9/0x3991`; on pass dispatches callback through unresolved thunk using entry `+0x2` and `[0x3b32:0x3b34] + 0x32` |
| `0008:d05f` | `entity_gate_callback_wrapper_b` | Same gate pattern; callback wrapper variant via unresolved thunk |
| `0008:d0b0` | `entity_gate_callback_wrapper_c` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d0ed` | `entity_gate_callback_wrapper_d` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d12a` | `entity_gate_callback_wrapper_e` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d167` | `entity_gate_callback_wrapper_f` | Same gate pattern; passthrough-style callback wrapper |
| `0008:d3d2` | `entity_slot_callback_wrapper` | Thin wrapper: pushes entry slot/index (`+0x2`) and dispatches through unresolved thunk |
Notes:
- Names remain wrapper-oriented because target callbacks are unresolved in this raw-import model.
- These wrappers are now easier to track from call sites while preserving conservative semantics.
## Additional Unresolved Thunk Stubs (new)
Follow-up thunk census after inspecting `0000:ffff` behavior.
### Confirmed trampoline-only stubs
All of the following are single-instruction wrappers (`CALLF 0000:ffff`) and were given unique labels:
| Address | New Name | Observed Caller(s) |
|---------|----------|--------------------|
| `0004:2592` | `thunk_callf_0000_ffff_0004_2592` | `0004:262d` (`FUN_0004_2620`) |
| `000b:f924` | `thunk_callf_0000_ffff_000b_f924` | `000b:0144` (`FUN_000b_010b`) |
| `000c:827d` | `thunk_callf_0000_ffff_000c_827d` | `000c:8985`, `000c:8f96` (`FUN_000c_88b4`) |
| `000c:82f9` | `thunk_callf_0000_ffff_000c_82f9` | `000c:8a10`, `000c:8f79`, `000c:9052` |
| `000c:8356` | `thunk_callf_0000_ffff_000c_8356` | `000c:84a9` (`FUN_000c_84a5`) |
| `000c:e4f9` | `thunk_callf_0000_ffff_000c_e4f9` | `000c:e4f5` (`FUN_000c_e4e0`) |
Notes:
- This confirms `unresolved_far_thunk_dispatch` is represented by multiple local trampoline copies in different segment regions.
- Separating them by address improves call-graph navigation and makes subsystem-specific tracing less ambiguous.
## Raw 000c State-Dispatch Helper Cluster (new)
After separating thunk stubs, a coherent local state/chain management cluster was lifted in `000c:ab32-000c:ac8f`.
| Address | Name | Evidence |
|---------|------|---------|
| `000c:ab32` | `entity_state_tick_dispatch` | Core state tick helper using fields `+0x38/+0x39/+0x3b/+0x3d/+0x5b`; clears mode bit `0x100` when `+0x38==0`, may call cleanup helper `000c:ac55`, calls `000c:7730(state,1)`, and conditionally advances chain |
| `000c:ab96` | `entity_state_reset_and_tick_dispatch` | Reset wrapper: zeroes `+0x38` and `+0x39` then calls `entity_state_tick_dispatch` |
| `000c:abb4` | `entity_state_advance_next_or_fallback_a` | Advance path A: when `+0x49!=0`, follows node-next pointers from `+0x3b/+0x3d` using offsets `+2/+4`; when exhausted, either clears active flag and re-dispatches, or falls back to backup pointer `+0x41/+0x43` |
| `000c:ac8f` | `entity_state_advance_next_or_fallback_b` | Advance path B: same structure as A but follows alternate node offsets `+6/+8` and fallback pointer `+0x45/+0x47` |
Notes:
- This cluster gives a concrete local interpretation for part of the large `000c:88b4` control flow without relying on unresolved thunk internals.
- Naming remains direction/path based (`a`/`b`) where high-level gameplay meaning is still pending.
## Raw 000c State-Flag Guard / Input Handler Batch (new)
Second sweep through `000c` adjacent helpers — gated thunk wrappers and input/animation tick handlers.
| Address | Name | Evidence |
|---------|------|---------|
| `000c:9f74` | `entity_state_flag100_check_and_dispatch` | Init latch guard at `[0x6053]`; clears `[0x8c55]` on first call; checks `[ptr+0x5b]` bits `0x100` and `0x40`; three-path thunk dispatch |
| `000c:a1ad` | `entity_state_clear_flag40_and_dispatch` | Skips if `[ptr+0x5b]` has `0x180` bits set; if `0x40` set, clears it and calls far ptr at `[0x5e82/0x5e84]`; then dispatches twice with args `(0x0b,0x10,0x1,0x0)` (record/state-key pattern) |
| `000c:a74e` | `entity_state_dispatch_if_flag_bit2` | Tests `[ptr+0x5b]` bit `0x2`; if set pushes extra arg + ptr and dispatches via thunk |
| `000c:84c3` | `entity_state_set_byte40_at_global_ptr` | Sets byte `[DAT_0000_6828 ptr + 0x40] = 1` then calls thunk unconditionally; enables global entity flag |
| `000c:ac55` | `entity_state_fire_if_handle_valid` | Guard: fires thunk dispatch only when `[0x6054] != -1`; no-op otherwise |
| `000c:ac6d` | `entity_state_fire_with_args_if_handle_valid` | 3-arg variant: pushes `[BP+0xe]` (byte), `[BP+0xc]`, `[BP+0xa]`, handle `[0x6054]`, then `CALLF 0000:ffff` |
| `000c:afa5` | `entity_state_check_field49_and_call_vfunc3c` | Checks field `[ptr+0x49]`: 1→reset to 0 return 1; 2→call `vtable[0x3c]` return 0; else thunk dispatch |
| `000c:b153` | `entity_state_animation_done_tick` | Checks `[param_2+0x14+0xa]` animation-complete flag; if zero increments `field49` and calls `entity_state_check_field49_and_call_vfunc3c`; if set calls `vtable[0x3c]` |
| `000c:b199` | `entity_state_input_key_handler` | Full input dispatcher: ESC/x/X → `vtable[0x3c]` (cancel); Left/Right arrows `0x14b`/`0x148` → prev state; n/N/0x14d/0x150 → next state; e/E → set `field47=1`; `-` with counter → trigger at 4. Manages `field47` and `field49` |
| `000c:b2c3` | `stub_noop_000c_b2c3` | Empty stub; returns immediately |
| `000c:b2c8` | `entity_state_dispatch_if_field49_eq4` | Fires thunk only when `[ptr+0x49]==4` |
| `000c:b349` | `entity_state_dispatch_if_far_ptr_nonzero_a` | Fires thunk if far-pointer args non-zero |
| `000c:b383` | `entity_state_set_field3f_and_dispatch` | If non-NULL: writes `&DAT_0000_2d18` to `[ptr+0x3f]`, then dispatches |
| `000c:b3d8` | `entity_state_dispatch_if_far_ptr_nonzero_b` | Same null-guard pattern as `b349`, variant b |
Patterns confirmed in this batch:
- `field49` = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
- `field47` = keystroke-combo counter
- `field3f` = linked data pointer (event/record reference)
- Global `[0x6054]` = current entity handle; `[0x6828]` = another global entity far pointer
- Bits in `[ptr+0x5b]`: `0x1=init`, `0x2=active/event`, `0x40=pending dispatch`, `0x100=flag100`, `0x180=skip-all mask`
## Raw 000c Palette Fade + Entity VM Cluster (new)
Two distinct subsystems identified in `000c:cdde-000c:f98b`.
### VGA Palette Fade (000c:cdde, 000c:ce57)
| Address | Name | Evidence |
|---------|------|---------|
| `000c:cdde` | `palette_fade_step_down` | Writes (Roffset, Goffset, Boffset) clamped to 0 to VGA I/O `0x3c8`/`0x3c9`; decrements `[0x630d]` by step `[0x6316]`; clears active at `[0x630a]` when black |
| `000c:ce57` | `palette_fade_step_up` | Same loop, adds offset, clamps at 63 (0x3f full VGA). Clears `[0x630a]` when fully bright |
Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src ptr, `[0x630d]`=brightness offset, `[0x6316]`=step, `[0x630a]`=active flag.
### Entity Mini-VM / Record-Player Context (000c:f6b8-000c:f98b)
| Address | Name | Evidence |
|---------|------|---------|
| `000c:f6b8` | `record_table_get_by_index` | Bounds check `param < [0x8c88]`; return `word at [0x8c84 + param*4]`. Table at 0x8c84 |
| `000c:f6e8` | `entity_vm_stack_init_with_data` | Init stack ptrs at `[ptr+0xcc..+0xd4]` pointing to self; max depth 199; copies optional initial data |
| `000c:f772` | `entity_vm_state_copy` | Copies 200 bytes (100 words from `[src+4]` to `[dst+4]`), then copies 4 words at `+0xcc..+0xd2` |
| `000c:f7c7` | `entity_vm_stack_push_frame` | Push call-frame: saves ret offset at `[ptr+0xd4]`, decrements `[ptr+0xcc]` by param_size, zeroes new frame |
| `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params |
| `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full |
| `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty |
| `000c:f94f` | `entity_vm_counter_add` | `[ptr+0xd6] += param_2`; simple accumulator |
| `000c:f98b` | `entity_vm_set_field_da_to_global` | Writes `[param_2+0xda far-ptr + 2]` into `[0x8c94]` |
Notes:
- Field offsets `+0xcc`=VM stack ptr, `+0xce/+0xd0`=segment regs, `+0xd2`=base, `+0xd4`=frame depth, `+0xd6`=counter, `+0xd8/+0xda/+0xdc`=VM position/bounds.
- The 200-byte body region at `[ptr+4..+0xcc]` holds 100 words of script/state payload.
- The pair-stack (field `+0x80`) is separate — likely pass/return value stack for the mini-script.
## Raw 000c Cursor Zone / Slot Array / String Queue Batch (new)
Three utility subsystems identified in `000c:e6d9-000c:eadd`, plus companion slot array API.
### Cursor / Directional Zone Classifier
| Address | Name | Evidence |
|---------|------|---------|
| `000c:e6d9` | `cursor_zone_quadrant_classify` | Splits screen by `[0x63d6]/2` and `[0x63d8]/2` vs bounds `[0x8c6c..0x8c72]`; returns directional code from 9-word table at `0x6401` |
Zone table layout (9 entries): NW/N/NE / W/Center/E / SW/S/SE based on horizontal threshold at `0x8c6c/0x8c70` and vertical at `0x8c6e/0x8c72`.
### Slot Array System (000c:ea53-000c:ee44)
A complete 29-slot menu/choice array with fixed stride 0x15 bytes, base at `[ptr+0x67]`, count at `[ptr+0x7a]`:
| Address | Name | Evidence |
|---------|------|---------|
| `000c:ea53` | `entity_slot_count_update_and_notify` | Sets `[ptr+0x72]=param-1`; calls `slot_array_get_current_entry` and `slot_array_find_and_dispatch`; calls `vtable[0]()` when `+0x75` flags set |
| `000c:eba5` | `slot_array_dispatch_matching` | Walks 0xb-stride array from `[ptr+4]`; calls thunk for each entry where `[entry+9]==param_4` |
| `000c:ec30` | `slot_array_dispatch_if_nonempty` | Returns `0xffff` if count < 1; else dispatches |
| `000c:ec9e` | `slot_array_find_and_dispatch` | Searches 0xb-stride array for `[entry+9]==param_4`; calls thunk on first match |
| `000c:ecf5` | `slot_array_push_entry` | Copies named string to `[base+0xc]`; writes 6 param words at `+0x12..0x20`; increments count |
| `000c:edb0` | `slot_array_push_raw` | Copies 0x15-byte raw entry from `param_2`; increments count |
| `000c:edf7` | `slot_array_pop` | Decrements `[ptr+0x7a]`; asserts `>= 0` |
| `000c:ee19` | `slot_array_init` | Sets `[ptr+0x78]=0`, `[ptr+0x76]=0`, `[ptr+0x75]=1` (active flag) |
| `000c:ee32` | `slot_array_clear_flags` | Clears `[ptr+0x74]=0`, `[ptr+0x75]=0` |
| `000c:ee44` | `slot_array_get_current_entry` | Returns `ptr + [ptr+0x7a]*0x15 + 0x67` (current entry ptr); 0 if count <= 0 |
### String Queue
| Address | Name | Evidence |
|---------|------|---------|
| `000c:eadd` | `string_queue_push` | Appends string to 10-entry queue at `[ptr+4]`; count at `[ptr+2]`; sets `[ptr+0xd]=param_4` |
### Additional VM-Adjacent Helpers
| Address | Name | Evidence |
|---------|------|---------|
| `000c:f2e7` | `entity_call_vtable_entry_10_if_valid` | Null-guard: calls `(*[ptr+8+0x10])()` if param_1 non-null |
| `000c:f39f` | `string_table_lookup` | Searches `[0x65bc/0x65be]` table by key string; returns matching words to out-params |
## Raw 000c Cursor Nav Dispatcher / State Reset Batch
Cursor navigation subsystem in `000c:d3e9000c:db68`. Manages directional zone changes, mouse button reads, keyboard scancodes, and entity vtable dispatch for interactive UI cursors.
### Cursor Navigation Fields (entity object offsets)
| Offset | Purpose |
|--------|---------|
| `+0x32` | Current zone code (08) |
| `+0x33` | Previous zone code |
| `+0x37+0x3a` | Directional booleans: N/S/W/E |
| `+0x3f+0x42` | Mouse button flags |
| `+0x45` | Last keyboard scancode |
| `+0x47` | Navigation index |
Globals: `[0x63da]` = mouse button state, `[0x63d6]/[0x63d8]` = cursor X/Y, `[0x638e]` and `[0x6346]` = reference data tables.
### Functions
| Address | Name | Evidence |
|---------|------|---------|
| `000c:dac1` | `cursor_nav_state_reset` | Zeros all directional/button flags; sets `[+0x32/+0x33]=0xff`, `[+0x47]=0xffff` |
| `000c:db68` | `cursor_nav_update_and_dispatch` | Calls `cursor_zone_quadrant_classify`; updates `[+0x37..+0x3a]`; reads `[0x63da]`; switch on direction (08); maps scancodes 0x48/0x50/0x4b/0x4d/0x39 |
| `000c:d3e9` | `cursor_set_ref_and_dispatch` | Null-checks param; sets `*param_1 = &DAT_0000_638e`; calls dispatch |
| `000c:d710` | `cursor_set_ref2_and_dispatch` | Same pattern; sets `*param_1 = &DAT_0000_6346` |
| `000c:d75e` | `entity_call_vtable_1e_via_ptr` | Calls `(*[*param_1 + 0x3c])()` — vtable offset 0x1e |
| `000c:d775` | `entity_call_vtable_1e_via_ptr_b` | Near-identical to `d75e`; duplicate generated by compiler |
| `000c:d7c6` | `stub_noop_000c_d7c6` | Empty stub |
| `000c:d7cb` | `stub_noop_000c_d7cb` | Empty stub |
Direction code mapping (from `cursor_nav_update_and_dispatch` switch):
- 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW, 8=Center
### Timer Rate Sync (cursor subsystem)
| Address | Name | Evidence |
|---------|------|---------|
| `000c:e4e0` | `cursor_timer_rate_sync` | If `[0x63e0]` (cursor active) non-zero: copies PIT rate `[0x39ce]` into entity object at `[0x4458+0x24]`, clears `+0x26/+0x27`, then calls far thunk. Called from `cursor_nav_update_and_dispatch` at `000c:db97`. |
## Raw 000c UI Listbox Event Handler Batch
Analysis of `000c:880c` and `000c:88b4` — the primary UI event-dispatch island in segment `000c`.
### Overview
`ui_listbox_event_handler` (`000c:88b4`) is a **large UI widget event handler** (~8KB of code, `88b4` through `9daa`). It takes the signature `(entity_far_ptr, seg, event_far_ptr)` and is stored as a far-pointer vtable entry (no direct code xrefs found; referenced from data). It dispatches on the event code at `[event+0x6]` (keystroke/mouse codes) and updates entity fields to drive menu/listbox navigation state.
Guard function `entity_state_if_flag80_call_thunk` (`000c:880c`) sits just before the handler: it checks entity flag bit `0x80` at `[param_1+0x5b]` and gates a far thunk call.
### Functions
| Address | Name | Evidence |
|---------|------|---------|
| `000c:88b4` | `ui_listbox_event_handler` | Dispatches on event code `[event+0x6]`: confirm keys `0xd`/`0x20`/`0x152``entity_state_tick_dispatch`; nav keys `0x148`/`0x14b`/`0x14d`/`0x150` → same; list-prev `0x2c`/`0x3c`/`0x55``entity_state_advance_next_or_fallback_b`; list-next `0x2e`/`0x3e`/`0x53`/`0x73`/`0x75``entity_state_advance_next_or_fallback_a`; Esc `0x1b` → tick dispatch; comma `0x2c`/`0x3c``ac8f`; misc UI events `0x6f`/`0x7e`/`0x13b0x143`/`0x241`/`0x410`/`0x420`/`0x426`/`0x42f`/`0x432`/`0x441` → various paths. Guards: `[BX+0x5b] & 0x80` (active), `& 0x100` (modal lock from `[0x604b]`, `[0x844]`). Large scan loop `0x100..0x27ff` at `000c:a80f` for option/device enumeration. Falls through to common retf at `9da7`. |
| `000c:880c` | `entity_state_if_flag80_call_thunk` | Guard: tests entity `[param_1+0x5b] & 0x80`; returns if clear; else calls far thunk. Active/visible gate for forwarding display/tick calls. |
| `000c:ace5` | `display_fullscreen_blit_with_entity` | If coords (param_2:param_1) are 0:0, calls wait thunk (`0x48c`=1164ms?); loads display object at `[0x4cd0]`, reads byte `[+0x5]` (video mode/palette), builds full-screen 640×480 rect (`0`..`0x27f`=639, `0`..`0x1df`=479), then calls far display function with entity + position params. Returns display-space coordinate pair in DX:AX. |
### Key Globals in this Handler
| Address | Meaning |
|---------|---------|
| `[0x7e22]` | String/resource pointer used as data context (`[0x7e24]` = flag) |
| `[0x604b]` | Modal active lock (nonzero = block most events) |
| `[0x844]` | Engine/game-ready flag (0 = block hardware toggle events) |
| `[0x604f]` | Toggle state for event `0x410` |
| `[0x6045]` | Toggle state for event `0x7e` |
| `[0x8638]` | Counter for event `0x432` (wraps 0x11→0x12..0x14→0x0) |
| `[0x5e82/0x5e84]` | Far pointer called when entity flag `0x40` is cleared |
| `[0x4cd0]` | Display/screen manager object far pointer |
### Event Code Reference Table (partial)
| Code | Meaning |
|------|---------|
| `0x0d` / `0x20` | Enter / Space (confirm) |
| `0x1b` | Escape (cancel) |
| `0x2c` / `0x3c` | Comma / `<` (prev in list) |
| `0x2e` / `0x3e` / `0x53` / `0x73` | Period / `>` / S / s (next in list) |
| `0x55` / `0x75` | U / u (up action with far-ptr check) |
| `0x6f` | o (some option panel event) |
| `0x7e` | `~` (toggle event, gates on `[0x844]` and `[0x6045]`) |
| `0x13b` | Shift+F1 area |
| `0x13c` | Jump-to-display event |
| `0x13e` | Blanked event (gates on `[0x604b]`) |
| `0x141` / `0x142` / `0x143` | Option toggles (gate on `[0x844]`, `[0x2bca/0x2bc9]`) |
| `0x148` / `0x14b` / `0x14d` / `0x150` | Arrow keys (Up/Left/Right/Down) |
| `0x152` | Insert key (alias confirm) |
| `0x241` | Display position query → `display_fullscreen_blit_with_entity` |
| `0x410` | Toggle event with `[0x604f]`, gates on `[0x844]` |
| `0x420` | List-scan init (loop 0x100..0x27ff) |
| `0x426` | String-push event (calls DS:[0x7e22]) |
| `0x42f` | Push `0x103` thunk event |
| `0x432` | Counter cycle event (`[0x8638]` wrap 0x11→0x12, 0x14→0x0) |
| `0x441` | Pattern-fill loop event |
## NE Segment 21 Analysis — Timer/Event Dispatch System