Add extractor for Crusader's EUSECODE.FLX container

- Implemented a Python script to extract data from the EUSECODE.FLX file format.
- Defined data structures for candidate entries and extracted chunks using dataclasses.
- Added functions to read and parse the FLX table, extract candidate data, and generate human-readable output files.
- Included functionality for analyzing extracted data, including generating summaries, descriptors, and event family reports.
- Implemented utilities for calculating printable ratios, zero ratios, and identifying text-like data.
- Added support for writing various output formats, including JSON, TSV, and Markdown.
This commit is contained in:
MaddoScientisto 2026-03-22 14:27:38 +01:00
commit 3daffbf113
58 changed files with 30295 additions and 2504 deletions

179
docs/far-call-targets.md Normal file
View file

@ -0,0 +1,179 @@
# Far-Call Targets: Top-104 Most-Called Functions
Content extracted from `crusader_decompilation_notes.md`. Named via systematic analysis of 11,692 NE relocation fixup entries — the functions most frequently called through the `CALLF 0x0000:ffff` thunk mechanism.
---
## Tier 1: Ranks 120 (73+ callers)
| Rank | Address | Name | Calls | Description |
|------|---------|------|-------|-------------|
| 1 | `000a:44fd` | `seg091_func_00fd` | 331 | Recovered boundary. Shares init flag `0x44a4` with `runtime_init_or_abort`; thunk-heavy non-returning wrapper. |
| 2 | `0003:ac7e` | `mem_alloc` | 272 | Allocation wrapper → seg082:0000 (`0009:a200`) |
| 3 | `0008:dbec` | `entity_word_list_destroy` | 238 | Frees entity word-list buffer. |
| 4 | `0003:a751` | `mem_free` | 207 | Free wrapper → seg082:007a (`0009:a27a` = `mem_free_checked`) |
| 5 | `0008:bb4f` | `mem_alloc_far` | 174 | Thin wrapper → `mem_alloc` |
| 6 | `0003:a897` | `far_memcpy` | 165 | REP MOVSW + trailing MOVSB |
| 7 | `0005:088f` | `entity_get_type_word` | 130 | Returns type word from table `0x7df9` indexed by slot |
| 8 | `000b:358d` | `sprite_tree_accumulate_pos` | 122 | Recursively sums X/Y offsets (`+0x21/+0x23`) through linked child nodes (`+0x19/+0x1b`), copies 8-byte position block via `far_memcpy` |
| 9 | `0008:ce3d` | `entity_call_two_vtables` | 118 | Calls vtable[`+4`] at entity+`0x1e` and `+0x28` |
| 10 | `0004:26cd` | `nop_void_stub` | 118 | Empty function, returns void |
| 11 | `0008:ce00` | `entity_call_two_vtables_base` | 117 | Calls vtable[0] at entity+`0x1e` and `+0x28` |
| 12 | `0008:bb8c` | `entity_check_flag_0x4000` | 115 | Short-circuits if flag `0x4000` set at `+0x16` |
| 13 | `0008:cda7` | `entity_free_both_word_lists` | 115 | Frees word lists at entity+`0x1e` and `+0x28` if optional pointers at `+0x24/+0x26` and `+0x2e/+0x30` non-null. Both call `entity_word_list_free_existing`. |
| 14 | `0004:26d2` | `nop_void_stub_b` | 111 | Empty function, returns void |
| 15 | `000a:45fe` | `runtime_init_or_abort` | 108 | Reentrancy-guarded init. Flag at `0x44a4`; flushes via `FUN_000a_4a56`, then calls `crt_exit_wrapper(1)`. Hidden code gap `0x4616-0x4643`. |
| 16 | `0004:3324` | `nop_return_zero` | 95 | Returns 0 |
| 17 | `0009:c563` | `event_queue_push` | 82 | Circular buffer enqueue. Ring index (`+0xe`) masked `0x3f`, slot masked `0xfff8`. Writes event type word + data byte pair. |
| 18 | `0005:c448` | `list_remove_and_free` | 74 | Unlinks node from linked list via `FUN_0005_c495`, optionally calls `mem_free` if bit 0 of flags set |
| 19 | `000b:2e00` | *(no function in Ghidra)* | 74 | Analysis gap at seg109:0000. Needs manual function creation. |
| 20 | `0009:1f12` | `dos_file_lseek` | 73 | DOS LSEEK (INT 21h AH=42h) wrapper with error reporting to `0x867a` |
---
## Tier 2: Ranks 2140 (5673 callers)
| Rank | Address | Name | Calls | Description |
|------|---------|------|-------|-------------|
| 21 | `0009:3600` | `rotating_buffer_advance` | 73 | Advances 5-slot circular counter at `0x3eb6`, zeros pointer in table at `0x867c`, dispatches via jump table |
| 22 | `0009:943a` | `entity_rect_compare_and_dispatch` | 68 | Compares bounding rectangles of two entities, dispatches based on flag bits `4/2/1` at `+0x16` |
| 23 | `0009:1e61` | `dos_file_close` | 65 | DOS file close (INT 21h), error reporting, sets handle to `-1` |
| 24 | `0005:e252` | *(unnamed — unclear)* | 65 | Copies 11 words from Phar Lap extender area (`FUN_0000_12c6+5`), then calls thunk. Interrupt/trampoline setup? |
| 25 | `0003:dbcc` | `crt_format_string` | 64 | MetaWare High C formatting wrapper. Calls `FUN_0003_bb92` with runtime format dispatch table. |
| 26 | `0007:5a00` | *(no function in Ghidra)* | 64 | High-traffic raw target at `seg043:0000`. Earlier `debris_spawn` / seg001 mapping was rejected after checking relocation labels. Still needs manual function creation and direct analysis. |
| 27 | `000a:4742` | `assert_buffer_valid` | 63 | Validates handle: asserts `param_2 == cookie` at `0x45a6` and `param_1 < limit` at `0x87e0` |
| 28 | `0009:9216` | `entity_conditional_render_dispatch` | 63 | Checks entity flag bits 4 and 1 at `+0x16`, dispatches to vtable[`+0xc`] or thunk |
| 29 | `0008:cb2c` | `entity_flag20_clear_and_update_target` | 61 | Clears flag bit `0x20`, writes target `+0x12/+0x14`, calls refresh |
| 30 | `0008:cb5c` | `entity_flag20_set_and_init_target` | 61 | Sets flag bit `0x20`, inits target if zero, calls refresh |
| 31 | `0007:7306` | `entity_create_stack_object` | 58 | Allocates `0xCC` bytes on stack, inits via `object_init_zero_fields` (`0005:c400`), calls thunk |
| 32 | `0007:8709` | `entity_mark_dirty_and_sync_tile_aux` | 58 | Syncs tile aux, sets flag bit `0x04` at `+0x42` |
| 33 | `0007:87c5` | `entity_set_flag20_from_field42` | 58 | Reads entity `+0x42/+0x44`, calls `entity_flag20_set_and_init_target` with those values |
| 34 | `0007:8508` | `entity_table_lookup_and_dispatch` | 58 | Searches table at `0x2b46`, dispatches via indirect jump |
| 35 | `0007:8920` | `entity_call_vtable_slot0c` | 58 | Calls vtable entry at `+0x0c` |
| 36 | `000a:b988` | `sprite_node_get_or_traverse` | 57 | If child pointer at `+0x19/+0x1b` non-null, traverses; otherwise returns leaf value |
| 37 | `0003:a98b` | `crt_signed_div32` | 56 | Entry: adjusts near→far stack, sets `CX=0` (signed quotient), jumps to `crt_div32_impl` |
| 38 | `000a:7b44` | `nop_return_void_a` | 56 | Empty function (default vtable slot?) |
| 39 | `000a:7b49` | `nop_return_void_b` | 56 | Empty function (default vtable slot?) |
| 40 | `000a:7b53` | `nop_return_void_c` | 56 | Empty function (default vtable slot?) |
---
## Supporting Functions Discovered
| Address | Name | Description |
|---------|------|-------------|
| `000b:3a00` | `sprite_tree_sum_x_offset` | Recursive: sums field `+0x21` through child chain `+0x19/+0x1b` |
| `000b:3a35` | `sprite_tree_sum_y_offset` | Recursive: sums field `+0x23` through child chain `+0x19/+0x1b` |
| `0003:a845` | `crt_exit_wrapper` | Calls `crt_exit_impl(param,0,0)` |
| `0003:a7ee` | `crt_exit_impl` | Full C exit: atexit handlers, stdio flush, MetaWare runtime cleanup |
| `0003:a9a8` | `crt_div32_impl` | 32-bit division core. CX flags: bit0=unsigned, bit1=modulo, bit2=negate |
| `0005:c400` | `object_init_zero_fields` | Zeros fields `+0x25`, `+0x29`, `+0x31`, `+0x32` of a struct. Returns pointer. |
| `000a:4440` | `joystick_read_axes_and_buttons` | Reads PC game port `0x201`. Times axis responses, reads button nibble to `0x44a2` |
| `000b:3380` | `sprite_node_is_dirty` | Checks flags at `obj+0x29 & 3 == 1 or 3` → returns bool |
| `000b:33a6` | `sprite_node_mark_dirty` | If not dirty, calls `FUN_000b_3965` with `mode=3` to invalidate |
---
## Tier 3: Ranks 4160 (4256 callers)
| Rank | Address | Name | Calls | Description |
|------|---------|------|-------|-------------|
| 41 | `000a:7b58` | `nop_return_zero_b` | 56 | Returns 0 (default vtable slot) |
| 42 | `000b:3ab2` | `sprite_node_dispatch_event` | 56 | Large event dispatch: checks event type (`2/4/8/0x100`), updates global focus ptr at `[0x4fd0:4fd2]`, dispatches via vtable methods `[+0x14/+0x18/+0x20/+0x24]` by event code. Switch table for 16 event types. |
| 43 | `000a:48ff` | `rng_next_modulo` | 55 | Advances seg091 RNG state and returns the result modulo the requested bound; returns 0 when bound is 0. |
| 44 | `000b:3362` | `sprite_tree_unwind_check` | 55 | Validates `SS == param_2` (stack segment guard), then decrements global counter at `[0x4fd6]` |
| 45 | `000b:40ee` | `sprite_node_update_and_dispatch` | 55 | If `sprite_node_is_dirty` returns false: marks dirty, calcs accumulated bounds via `sprite_tree_get_accumulated_bounds` (`3ed8`), then dispatches via thunk |
| 46 | `000a:7b5f` | `vtable_stub_trampoline` | 55 | Calls through fixup thunk (forwarder to another function) |
| 47 | `000a:7b78` | `nop_return_void_e` | 55 | Empty function (default vtable slot) |
| 48 | `000a:7b7d` | `nop_return_void_f` | 55 | Empty function (default vtable slot) |
| 49 | `000a:7b4e` | `nop_return_void_d` | 54 | Empty function (default vtable slot) |
| 50 | `000b:330c` | `sprite_tree_dispatch_wrapper` | 52 | Pure thunk wrapper: calls through fixup |
| 51 | `0009:2034` | `dos_file_seek` | 51 | INT 21h AH=42h (LSEEK). Takes file object ptr, extracts handle at `obj+4`, seeks to offset param. Error reporting to `[0x867a]`. |
| 52 | `0005:0466` | `entity_resolve_slot_ptr` | 50 | *(pre-existing name)* |
| 53 | `0003:a880` | *(no function in Ghidra)* | 49 | Analysis gap in CRT segment |
| 54 | `0006:170c` | `tile_class_get_byte` | 47 | Looks up class data: indexes into table at `[0x7e1e]` by `(*param_1 * 0x79)`, returns byte at offset `+0xc` |
| 55 | `000b:4097` | `sprite_dispatch_with_event` | 45 | Pushes event params + global `[0x49c2:0x49c4]`, calls thunk |
| 56 | `0005:02c1` | `entity_is_type_match` | 43 | Compares `*param_1` against global at `[0x27c8]`, returns 1 if equal, 0 otherwise |
| 57 | `0003:ad75` | *(no function in Ghidra)* | 43 | Analysis gap in CRT segment |
| 58 | `000a:e709` | `render_dispatch_by_flag` | 43 | Dispatches between two thunk paths based on boolean flag at `stack+0x10` |
| 59 | `0003:d0ff` | `crt_sprintf_wrapper` | 42 | Calls `FUN_0003_bb92` (format engine) with rearranged params and string constant at `0x67ac` |
| 60 | `000b:326e` | `sprite_node_destroy` | 42 | Destructor: sets vtable ptr to `0x501a`, clears global `[0x4fd0:4fd2]` if self, releases child nodes, calls `mem_free` via thunk |
---
## Analysis Gaps
| Address | NE Segment | Callers | Notes |
|---------|-----------|---------|-------|
| `000a:44fd` | seg091:00fd | 331 | Recovered as `seg091_func_00fd`; thunk-heavy init wrapper sharing flag `0x44a4`. |
| `000b:2e00` | seg109:0000 | 74 | Start of segment 109. |
| `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. |
| `000a:48ff` | seg091:04ff | 55 | Recovered as `rng_next_modulo`; bounded wrapper around seg091 RNG state advance. |
| `0003:a880` | seg005:0880 | 49 | In CRT segment near `far_memcpy`. |
| `0003:ad75` | seg005:0d75 | 43 | In CRT segment near `mem_alloc`. |
| `000a:454d` | seg091:014d | 32 | Recovered as `seg091_func_014d`; init/context helper using the `0x45a6` cookie/context global. |
**seg043 reconciliation:**
- The earlier standalone seg001 port hypothesis in this subrange was wrong.
- Relocation data places raw `0007:5a00` at `seg043:0000`, and the 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 behavior now lines up with the repaired function `0007:5b7a = entity_set_at_target_update_facing`.
- The currently repaired functions at `0007:5a90` and `0007:5c1c` should keep their positional names until a later pass resolves the thunk-heavy bodies more clearly.
---
## Tier 4: Ranks 6180 (2942 callers)
| Rank | Address | Name | Calls | Description |
|------|---------|------|-------|-------------|
| 61 | `000b:30a5` | `sprite_tree_forward_wrapper` | 42 | Pure thunk forwarder |
| 62 | `0008:bc27` | `entity_set_event_type_checked` | 41 | Sets event code at `+0x06` with range/timer checks |
| 63 | `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | 40 | Constructor: alloc `0x40`, vtbl `3AA6`, flag `0x200` |
| 64 | `0005:1565` | `entity_action_by_type_dispatch` | 39 | Checks entity type against whitelist (`0x432,0x5a0,0x1fd,0x1fe,0x8f,0x59f,0x2b3,0x2ca`), dispatches by flags at `[0xc76]` and `[0x85f]` |
| 65 | `0008:4bba` | `channel_slot_enable` | 39 | Sets enable byte=1 in 5-slot table at `0x84ca` (slot * `0xd` stride) |
| 66 | `0009:6f5a` | `vga_palette_write` | 38 | Writes RGB triplets to VGA DAC (port `0x3C8/0x3C9`). Range `param_2..param_3` from palette data at `*param_1` |
| 67 | `0009:8ef6` | `line_draw_dispatch` | 38 | Compares `abs(dx)` vs `abs(dy)` to determine major axis, dispatches to appropriate line draw routine |
| 68 | `000a:7b30` | `nop_return_void_g` | 38 | Empty function (default vtable slot) |
| 69 | `000a:7b3f` | `nop_return_void_h` | 38 | Empty function (default vtable slot) |
| 70 | `0009:6e7f` | `palette_free_if_set` | 35 | Frees existing palette data if ptr non-null, checks alignment |
| 71 | `000a:7b35` | `nop_return_void_i` | 35 | Empty function (default vtable slot) |
| 72 | `0009:c433` | `event_queue_align_index` | 34 | Returns `param_1 & 0xFFF8` — aligns ring index to 8-byte event slot boundary |
| 73 | `0009:2156` | `dos_file_get_size` | 33 | Saves file position, does INT 21h AH=42h AL=02 (seek to end), restores position. Returns file size in DX:AX |
| 74 | `000a:2c41` | `list_iterate_next` | 33 | Linked list iterator: if `*out==0` returns first from `obj+2`; else follows next at `ptr+2/+4`. Returns bool (has more) |
| 75 | `000a:454d` | `seg091_func_014d` | 32 | Recovered boundary. Shares flag `0x44a4`; checks optional long argument against the `0x45a6` cookie/context global. |
| 76 | `000b:2446` | `sprite_clear_redraw_flag` | 31 | Clears flag at `obj+0x17e`, then dispatches via thunk |
| 77 | `0005:1238` | `entity_get_class_word` | 30 | Looks up table at `[0x7e01]` indexed by `*param_1 * 2`, returns word. Sister of `entity_get_type_word` (which uses `[0x7df9]`) |
| 78 | `000b:1446` | `display_null_check_dispatch` | 30 | Null-checks far ptr params, dispatches to different thunks based on result |
| 79 | `000d:85da` | `vga_palette_set_all_black` | 29 | Sets byte at `global_obj[0x6828]+0x40 = 1` if global non-null, then calls thunk. *(Note: earlier name `map_object_set_dirty_flag` was incorrect; corrected in seg137 analysis.)* |
| 80 | `0005:1511` | `entity_destroy_trampoline` | 29 | Pure thunk forwarder to entity destruction |
---
## Tier 5: Ranks 81104 (2429 callers)
| Rank | Address | Name | Calls | Description |
|------|---------|------|-------|-------------|
| 81 | `0009:1c00` | `dos_file_handle_init` | 29 | Inits 6-byte file handle struct: dword=0, word+4=`0xFFFF` (invalid). Aborts on null ptr |
| 82 | `0008:75f3` | `entity_get_ptr` | 29 | Looks up entity far ptr from table at `DS:0x39b0`, indexed by `id*4` |
| 83 | `0006:0208` | `entity_class_get_flag4` | 29 | Returns bit 2 of classinfo byte at `[0x7e1e]+*p1*0x79+0x13` → 0 or 1 |
| 84 | `000a:30d7` | `list_node_set_if_context` | 29 | Sets node fields `+2/+4` if params match context globals at `0x45a6/0x45a8` |
| 85 | `0009:c45f` | `object_init_and_get_next` | 29 | Calls `object_init_zero_fields` then returns `*(result+2)` — init+accessor combo |
| 86 | `0004:d7a0` | `object_deref_get_word4` | 28 | Dereferences far ptr chain: returns word at `*(*(param_1)+4)` |
| 87 | `000a:5276` | `debug_check_flag_45aa` | 28 | If byte at `DS:0x45aa` non-zero, calls thunk (diagnostic/assert check) |
| 88 | `0003:d94f` | `far_memset` | 28 | Wrapper reordering params for CRT memset impl at `0003:d92b` (odd-aligned, word-fill loop) |
| 89 | `000a:7b3a` | `nop_return_void_j` | 28 | Empty function (default vtable slot) |
| 90 | `0008:ca18` | `entity_pair_sync_b` | 27 | Pairwise sync wrapper direction B |
| 91 | `0008:bd20` | `entity_sprite_set_target_pos` | 27 | Sets flag `0x1000`, copies player pos to entity `+0x0a/+0x0c` |
| 92 | `0009:3ceb` | `buffer_release_and_dispatch` | 27 | Frees far ptr at `obj+0x3b` if set, nulls it; conditionally dispatches on bit 0 |
| 93 | `0005:09b4` | `entity_get_flags_byte` | 27 | Reads byte from `[0x7dfd]+id`, conditionally extends with classinfo byte at `[0x7e1e]+id*0x79+0xf` |
| 94 | `0005:0fbb` | `entity_lookup_sprite_word` | 27 | Returns word from `[0x7e05]+*p1*2` — sprite/visual index table |
| 95 | `0008:d27e` | `entity_dispatch_trampoline_b` | 26 | Pure forwarder thunk (CALLF thunk only) |
| 96 | `0005:0376` | `entity_resolve_base_type` | 26 | Walks entity class hierarchy (bit 8 in `[0x7e01]`) via `[0x7ded]`, returns base type from `[0x7df1]` |
| 97 | `000b:2492` | `sprite_redraw_if_needed` | 26 | If redraw flag at `+0x17e` is clear, calls update routine + thunk |
| 98 | `0003:e4d3` | `dos_file_open_wrapper` | 26 | Zeros output byte, delegates to file open impl at `0003:bb92` |
| 99 | `0005:033e` | `entity_resolve_base_parent` | 25 | Same hierarchy walk as `entity_resolve_base_type` but returns parent from `[0x7ded]` |
| 100 | `000a:87fd` | `render_clip_rect_to_viewport` | 25 | Clips 4 rect params to viewport bounds at `[0x4014]`, sets dirty flag at `0x8a16`, increments draw counter at `0x4716` |
| 101 | `0005:3cf5` | `entity_class_has_flag2000` | 25 | Returns `(entity_get_class_word(slot) & 0x2000) != 0` |
| 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 |

392
docs/ne-segment1.md Normal file
View file

@ -0,0 +1,392 @@
# Crusader: No Remorse — NE Segment 1 Game Logic
This file covers the standalone analysis of NE Segment 1 (`seg001_code_off_37600_len_8400.bin`), imported as a raw binary at base `0x0000`, language `x86:LE:16:Protected Mode`. All 35+ identified functions have been renamed and annotated in Ghidra.
## Cursor Subsystem (0x00600x0d5f)
| Address | Name | Description |
|----------|---------------------------|-------------|
| `0x0060` | `cursor_update_hover` | Hover update: if mouse active & entity set, calls cursor_set_target |
| `0x00e9` | `cursor_set_target` | Positions cursor on entity, updates sprite + direction visual |
| `0x0322` | `cursor_shutdown` | Frees cursor resources, resets state |
| `0x0398` | `cursor_animation_update` | Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6 |
| `0x050f` | `cursor_draw_tick` | Per-frame cursor draw (calls cursor_animation_update if dirty) |
| `0x0c24` | `action_key_valid` | Returns 1 if action code (param_1) is a valid game action key |
| `0x0d5f` | `cursor_direction_input` | Arrow-key input: rotates cursor angle, updates direction sprite |
## Input Handling
| Address | Name | Description |
|----------|-------------------------|-------------|
| `0x0526` | `input_keyboard_handler`| Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load |
## Cursor State Data (at DS:0x27xx)
| Address | Field | Meaning |
|---------|-------|---------|
| `0x27c4` | cursor_sel1 | Selection counter 1 |
| `0x27c6` | cursor_sel2 | Selection counter 2 |
| `0x27c8` | current_entity | Handle to currently targeted entity |
| `0x27ca0x27ce` | cursor_state | Cursor interaction state bytes |
| `0x27d0` | cursor_entity_type | Current entity type index |
| `0x27d2` | z_offset | Z-height offset for terrain adjustment |
| `0x27d4` | cursor_angle | Rotation angle (0359) |
| `0x27d6` | cursor_sprite | Sprite handle for cursor visual |
| `0x27d8` | cursor_dirty | Set when cursor needs redraw |
| `0x27d9` | cursor_active | Master cursor enabled flag |
| `0x27da` | cursor_no_turn | Flag disabling cursor rotation |
| `0x27ed` | difficulty | Enemy accuracy divisor (used in projectile_init_vector) |
| `0x27fd` | hard_mode | Two-step mode (combat vs. explore) |
| `0x27fe` | move_mode | Movement phase flag |
| `0x27ff` | mouse_active | Mouse/input system active |
| `0x2800``0x2811` | various | UI state: active sprite, facing byte, cur entity handle |
| `0x283f`/`0x2841` | menu_obj_ptr | Active menu/dialog object far pointer |
| `0x2844` | in_save | In-progress save game flag |
| `0x290e` | entity_count | Number of active entities |
| `0x2910``0x2947` | snap_type_ids[10] | Entity types that snap-to-ground in snap_entity_to_ground |
## Input / Action Dispatch
| Address | Name | Description |
|----------|---------------------------|-------------|
| `0x2420` | `entity_command_dispatch` | Dispatches player commands to target entity; reads 0x27d0, 0x2de4, sends events 0x14/0xf, handles state machine 0x27ca/0x27cd/0x27ce |
| `0x279a` | `cheat_code_check` | Checks input record byte+1 against five bytes starting at `0x2833`; toggles `0x844`/`0x6045`, emits helper/event code `0x103` |
## Menu / Event Callbacks
| Address | Name | Description |
|----------|---------------------------|-------------|
| `0x2e53` | `cursor_event_notify_a` | Vtable thunk: forwards event to 0x27ca area handler |
| `0x2e96` | `cursor_event_notify_b` | Vtable thunk: forwards event to 0x27ca area handler (alt path) |
| `0x2ed9` | `menu_event_notify_a` | Vtable thunk: forwards event to 0x2843 (near menu object) |
| `0x2f0c` | `menu_event_notify_b` | Vtable thunk: forwards event to 0x2843 (alt path) |
| `0x2ff3` | `stub_noop_2ff3` | Empty stub, noop |
| `0x2ff8` | `entity_collision_callback_a` | Calls touch handler then func(entity+0x1e, seg, 2); opt: extra func if param_3&1 |
| `0x3046` | `set_active_menu` | Writes param_1/param_2 to 0x283f/0x2841 (active menu far pointer) |
| `0x3058` | `entity_collision_callback_b` | Same as entity_collision_callback_a (second vtable entry) |
## Entity System (0x24010x5a50)
| Address | Name | Description |
|----------|------------------------------|-------------|
| `0x2401` | `clear_cursor_selection` | Zeros 0x27c4/0x27c6 (selection counters) |
| `0x2899` | `cursor_switch_target_entity`| Switches cursor target: unloads old entity, loads new, re-registers |
| `0x29d8` | `get_z_offset` | Returns func() + *(0x27d2) = adjusted Z/height |
| `0x2a09` | `is_player_in_range` | Checks if entity is at player (0x2de4) X/Y +/-0xf0 range |
| `0x2a46` | `entity_ai_update_loop` | Loops entities 2255, checks visibility, triggers fire/move |
| `0x2c36` | `ui_update_callback` | Calls cursor_state_clear then vtable[2] on menu object |
| `0x2c6b` | `cursor_state_clear` | Clears cursor state bytes 0x27ca0x27ce, clears entity flag bit1 |
| `0x2c92` | `dialog_spawn` | Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca |
| `0x2d47` | `entity_pick_handler` | Handles entity selection or save-game trigger (type 0x38d) |
| `0x2df9` | `clear_active_menu` | Zeros 0x283f/0x2841 (active menu far pointer) |
| `0x2e18` | `game_mode_init` | Initializes game mode state, resets sprite/cursor/menu state |
| `0x2f3f` | `entity_table_set_sprite` | Reads 0x7df9+slot*2; writes entity type table 0x7e1e[slot*0x79+0x0d]=param_2, +0x10=0 |
| `0x3c97` | `snap_entity_to_ground` | If entity type in snap_type_ids[10], resets Z to 0xf0 and adjusts XY |
| `0x3d6e` | `spawn_entity_checked` | Spawns entity with explosion pool limit check (0x84c0, 0x84c2) |
| `0x3f2f` | `entity_spawn` | Allocates entity, vtable=0x29aa/0x39ca, positions it |
| `0x40d4` | `entity_remove` | Removes entity: destroys sprites, clears 0x2802/0x2804 if needed |
| `0x4172` | `entity_animation_frame_update`| Advances/retreats anim frame ([+0x1d]) toward target [+0x1c/0x1b] based on quality |
| `0x42f8` | `stub_noop_42f8` | Empty stub, noop |
| `0x42fd` | `entity_registry_decrement` | Calls cleanup func then decrements entity count at 0x290e |
| `0x4314` | `entity_sprite_move_delta` | Updates shot sprite handle (entity+0x3f) position by adding delta params |
| `0x4552` | `entity_set_position` | Sets entity+0x3e (type_handle), world_x/y (entity+0x45/47), base_x/y (entity+0x4f/51) |
| `0x452b` | `shot_set_spawn_pos` | Calls entity_set_position then sets entity+0xbe = param_3 (extra spawn field) |
| `0x4591` | `entity_try_place` | entity_set_position with validation — position only set if placement succeeds |
| `0x5092` | `entity_deactivate` | Calls vtable[2] to deactivate, or finds in registry and removes |
| `0x5a50` | `entity_list_contains` | Checks if entity ptr exists in active entity list at 0x294c |
| `0x5b05` | `stub_noop_5b05` | Empty stub, noop |
## Entity Object Layout (NE Segment 1 entities)
| Offset | Field | Meaning |
|--------|-------|---------|
| `+0x00` | vtable_ptr | Vtable pointer (0x29aa for generic, 0x2a57 for debris) |
| `+0x02` | slot_index | Entity slot index (used for registry at 0x39ca) |
| `+0x04` | entity_type | Entity type ID |
| `+0x19`/`+0x1a` | flags | Entity flags (bit0=debris, bit1=cleared by cursor_state_clear, bit6=active, bit8=valid) |
| `+0x1b` | vel_x | X velocity (clamped ±0x20) |
| `+0x1c` | vel_y | Y velocity (clamped ±0x20) |
| `+0x1d` | vel_z | Z velocity (clamped ±0x10) |
| `+0x1e` | fire_handle | Weapon/fire handle |
| `+0x1f` | is_enemy | 1 if entity is an enemy type |
| `+0x20`/`+0x21` | pos_frac_x/y | Fractional position (sub-tile) for movement |
| `+0x22` | pos_frac_z | Fractional Z |
| `+0x36` | weapon_type | Active weapon type ID |
| `+0x38` | facing | Current facing direction (015) |
| `+0x3c` | sprite_handle | Sprite for this entity |
| `+0x3f` | shot_sprite | Sprite handle for active projectile (0xFFFF = none) |
| `+0x45`/`+0x47`/`+0x49` | world_x/y/z | Current world position (integer) |
| `+0x4f`/`+0x51`/`+0x53` | base_x/y/z | Base/spawn position |
| `+0x54`/`+0x56`/`+0x58` | prev_x/y/z | Previous frame position |
| `+0x59` | attack_active | Attack in progress flag |
| `+0x5a` | at_target | Reached target flag |
| `+0x5e``+0x65` | delta_x/y/z/high | Per-step movement deltas (fixed point) |
| `+0x66`/`+0x68` | step_active | Stepping active (1=yes, 0=off) |
| `+0x6a`/`+0x6c` | weapon_slot/dist | Weapon slot and total travel distance |
| `+0x6e` | delta_z | Alt Z delta |
| `+0x70` | projectile_type | Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain) |
| `+0x72`/`+0x74`/`+0x76` | target_x/y/z | Target position with deviation |
| `+0x77` | target_entity | Target entity handle |
| `+0x79` | secondary_pos | Secondary position struct pointer |
| `+0xad` | owner_entity | Owning entity handle |
| `+0xaf` | shot_owner_flags | Shot owner (entity/player) |
| `+0xb1` | bounce_count | Bounce counter (used with homing, type 5) |
| `+0xb3` | has_bounce | Has bounce trajectory active |
| `+0xbd` | actor_type | Actor type byte (used for direction table lookups) |
## Shot Entity Lifecycle (0x435e0x44a9)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x435e` | `shot_entity_alloc` | Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7 |
| `0x44a9` | `shot_entity_free` | Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1 |
## Projectile / Combat (0x46590x5a99)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x4659` | `projectile_init_vector` | Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536 |
| `0x4a91` | `entity_fire_weapon` | Fires weapon from entity using 0x129b/0x12ac direction offset tables |
| `0x4b18` | `fire_weapon_from_cursor` | Gets cursor angle sprites, fires projectile at cursor target |
| `0x4b78` | `projectile_check_hit` | Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity) |
| `0x4c2e` | `projectile_step_update` | Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked |
| `0x4d28` | `projectile_trace_ray` | Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate |
| `0x51ad` | `projectile_update_tick` | Full projectile tick: move, check reach target, bounce, call projectile_check_hit |
| `0x5a99` | `projectile_apply_hit` | Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner |
## Weapon Type Table (0x2536)
- Each entry is 0x11 bytes (17), accessed as `weapon_type * 0x11`
- `[0]` = step divisor for distance calculation
- `[0x19]` = max range threshold (used in projectile_update_tick)
## Direction Tables (0x129b / 0x12ac)
- Indexed by facing (015): dx offsets at 0x129b, dy offsets at 0x12ac
- Values are multiplied by distance (e.g. `*0x500`) for projectile spawn offsets
## Collision Detection (0x60c10x621e)
| Address | Name | Description |
|----------|----------------|-------------|
| `0x60c1` | `aabb_overlaps_3d` | 3D AABB overlap test — box layout [xmin,ymin,zmin,_,_,xmax,_,ymax,_,zmax] |
| `0x621e` | `bbox_translate` | Translates a 3D bounding box by (dx, dy, dz) — both min and max points |
## Enemy AI / Spawning (0x6aed0x6d21)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x6aed` | `map_find_spawn_point` | Finds map tile matching entity conditions; returns packed XYZ tile coords |
| `0x6bfc` | `actor_find_in_view` | Finds actor visible in current view frustum (temp data at 0x7eca) |
| `0x6ce9` | `enemy_spawn_with_target` | Wrapper: spawns enemy with player as target (param5=1) |
| `0x6d05` | `enemy_spawn_no_target` | Wrapper: spawns enemy without targeting player (param5=0) |
| `0x6d21` | `enemy_spawn_at_position` | Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A) |
## Player / HUD
| Address | Name | Description |
|----------|-------------------------------|-------------|
| `0x50ee` | `player_position_update` | Updates player position from direction data; clamps to screen bounds |
| `0x6ff7` | `player_health_update_and_effect` | Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect |
## Destruction / Death (0x74900x75ff)
| Address | Name | Description |
|----------|---------------|-------------|
| `0x7490` | `debris_spawn`| Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list |
| `0x75ff` | `entity_die` | Death handler: spawns 14 debris objects, picks best explosion direction |
## Entity Type Constants (weapon_type/entity class)
| Value | Entity Class |
|--------|--------------|
| `0x17` | Robot/mech type A |
| `0x18` | Robot/mech type B |
| `0x1` through `0x3c` | Various entity/weapon types |
| `0x3d` | Robot/mech type C |
| `0x3e` | Robot/mech type D |
| `0x2f5``0x2f7` | Special movement entity |
| `0x595`/`0x597` | Platform/elevator entities |
| `0x31c`/`0x322``0x327` | Explosive/effect entities |
| `0x38d` | Save game trigger entity |
| `0x426` | Spark/scatter sub-shot |
| `0x59a` | Player cursor/select indicator |
## Entity Data Table at 0x7e1e
- Stride: `0x79` bytes (121 bytes per entry)
- Indexed by entity type (integer) or entity slot
- `+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)
- `+0x5a` offset = flags byte (bit4 = special entity flag, bit3 = armor/shield flag)
- `+0x68` = targeting flag
## Map / Resource Tables
| Address | Content |
|---------|---------|
| `0x2833` | Cheat code input sequence (null-terminated) |
| `0x283d` | Cheat sequence match position counter |
| `0x7ded` | Map X coordinate array (2 bytes per entry) |
| `0x7df1` | Map Y coordinate array (2 bytes per entry) |
| `0x7df5` | Map Z array (1 byte per entry) |
| `0x7df9` | Entity state array (2 bytes per slot) |
| `0x7e46` | Player state block far pointer |
| `0x7e1e` | Entity type table (stride 0x79) |
## Entity Vtable Index (NE Segment 1)
| Address | Entity Class |
|---------|-------------|
| `0x28b5` | Dialog/menu object vtable |
| `0x287b` | Cheat-spawned entity (cheat ON) vtable |
| `0x2892` | Cheat-spawned entity (cheat OFF) vtable |
| `0x2969` | Entity registry vtable (stored at 0x39ca+slot*4, not entity's own vtable) |
| `0x297e` | Shot/projectile entity vtable |
| `0x29aa` | Generic/AI entity vtable |
| `0x2a1a` | Corpse entity vtable (variant) |
| `0x2a33` | Actor/corpse entity vtable |
| `0x2a57` | Debris fragment entity vtable |
---
## Cheat System Analysis
### `cheat_code_check` internals
- Direct raw-EXE recovery: `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.
- `FUN_0007_04dc` itself is not only a low keyboard-queue consumer: `drawlist_init` at `0007:f654` calls it directly during higher-level setup, so the cheat-dispatch body can be reached from at least one non-ISR path in this build.
Variable and constant roles from the recovered body:
- `0x2833` is not a clean dedicated data object in this raw EXE — raw bytes and surrounding disassembly show that it lands in the middle of `entity_animation_frame_update` (`0007:26e2-0007:2867`). Starting on `PUSH AX; CMP byte ptr [0x27fd],0; ...` the first five bytes are `50 80 3e fd 27` followed by a `0x00`.
- `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 (overlapping prefixes still work).
- `input_event_record[+1]` is the compared input/event token.
- `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` — a mirrored cheat-state latch.
- Constant `0x103` is pushed into the shared helper at `000a:5276` immediately after the toggle (emit the cheat-toggle side effect).
- `0x8c52` is forced to `1` on success before the side-effect path continues.
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: cheats-on uses `DS:0x287b`; cheats-off uses `DS:0x2892`.
### Cheat-enable sources
Two independent cheat-enable sources verified:
1. The hidden input matcher in `cheat_code_check` toggles `0x844` and `0x6045` after matching the five-byte event-code table at `0x2833`.
2. The command-line parser at `0004:635c-0004:63b8` recognizes the literal switch `-laurie` and directly sets `0x844 = 1`. This path does **not** write `0x6045`.
Current model for the two cheat bytes:
- `0x844` = master cheat-permitted / cheat-framework-enabled flag.
- `0x6045` = live cheat-active mirror latch used by low-level keyboard handling.
- The hidden five-byte matcher enables both at once, but `-laurie` only enables the master flag.
- The separate event-`0x7e` path at `000c:942d` requires `0x844 != 0`, flips `0x6045`, and displays one of two local notification messages (`0x6087` vs `0x6091`).
`jassica16` status:
- No literal `jassica` string is present in the current string table, while `-laurie` is present as plain text.
- The ordinary keyboard ISR producer still does not support the old byte-for-character model cleanly: it feeds normalized scan-code-style values into record byte `+1`, while the matcher source at `0x2833` is a live code-byte sequence with two values (`0x80`, `0xfd`) that do not fit that ISR path.
- The direct call `drawlist_init -> FUN_0007_04dc` is the first concrete static evidence for a higher-level path in this build.
F10 key behavior (verified in raw build):
- `seg001_input_keyboard_handler` at `0006:ec29` handles input byte `0x44` and immediately returns unless cheats are enabled through `0x6045`.
- "Plain F10 when cheats are enabled" is verified; "Ctrl+F10 enables god mode" is **not** supported by the current code path.
- When a current `0x7e22` entity exists, the branch resolves the current selection and refreshes per-entity bookkeeping.
- In the `local_4 == 1` case the branch becomes a large restore/reset routine that tears down and rebuilds multiple linked objects around `0x7e22`, retries dispatch up to `0x14` times per stage, and fires the event batch `0x33d`, `0x33f`, `0x340`, `0x341`, `0x33e` before re-enabling channels `4`, `1`, and `0`.
### Cheat-related string table (seg014 / `000e:xxxx`)
| Address | String | Notes |
|-------------|-------------------------------------------------|-------|
| `000e:9c5e` | `"FART ...TRY... -laurie (Have fun, Jely)"` | Dev Easter-egg comment; no static code xref |
| `000e:9c87` | `"CHEATS ON"` | Cheat-on status string |
| `000e:9c91` | `"CHEATS OFF"` | Cheat-off status string |
| `000e:9c9c` | `"TARGETING RETICLE ACTIVE."` | Correlates to event `0x441` / byte `0xee0` toggle |
| `000e:9cb6` | `"TARGETING RETICLE INACTIVE."` | Paired off-state |
| `000e:9cd2` | `"CD TRANSFER DISPLAY ACTIVE."` | Correlates to event `0x241` / `0x141` toggle area |
| `000e:9cee` | `"CD TRANSFER DISPLAY INACTIVE."` | Paired off-state |
| `000e:9dff` | `"HACK MOVER ON"` | No static code xref; USECODE/scripting layer |
| `000e:9e0d` | `"HACK MOVER OFF"` | No static code xref; USECODE/scripting layer |
| `000e:6450` | `"Immortality disabled."` | No static code xref; USECODE/scripting layer |
| `000e:6466` | `"Immortality enabled."` | No static code xref; USECODE/scripting layer |
| `000e:647b` | `"Cheats are now active."` | Shown in `-laurie` startup path |
| `000e:6492` | `"Cheats are now inactive."` | Paired off-state |
### Cheat event dispatch summary (000c segment)
All cheat-related event case-handlers reside as shared-frame case bodies within a large event dispatch function in segment 000c. Each body inherits BP from the enclosing prologue and exits via `POP DI; POP SI; LEAVE; RETF`.
| Address | Symbol | Event | Action |
|-------------|------------------------------------------------|---------|--------|
| `000c:8e16` | `event_0x441_cheat_debug_overlay_toggle` | `0x441` | Toggles `DS:0xee0` (boolean-NOT); calls `[0x2bd8]` vtable `+0x2c`; gate = `DS:0x844` |
| `000c:8e46` | `event_0x241_cheat_debug_overlay_toggle` | `0x241` | Toggles `DS:0x2bc9` (1-current); same vtable dispatch; gate = `DS:0x844` |
| `000c:8e72` | `event_0x141_cheat_debug_overlay_toggle` | `0x141` | Toggles `DS:0x2bca` (1-current); same vtable dispatch; gate = `DS:0x844` |
| `000c:942d` | `event_0x7e_cheat_latch_runtime_toggle` | `0x7e` | Requires `0x844 != 0`; flips live latch `DS:0x6045`; notification at `DS:0x6087` (on) or `DS:0x6091` (off) |
| `000c:9154` | `event_0x142_cheat_fullscreen_mode1_refresh` | `0x142` | Gate = `DS:0x604b`; palette-black, seg126 shell, mode-1 `000c:3c0e`, tail `0004:70f1` |
| `000c:92cd` | `event_0x143_cheat_fullscreen_mode0_refresh` | `0x143` | Same as `0x142` but mode-0 `000c:3c0e`, tail `0004:6f15` |
| `000c:9703` | `event_0x410_cheat_flag_604f_toggle` | `0x410` | Toggles `DS:0x604f` (boolean-NOT); notification at `DS:0x60d2` (on) or `DS:0x60ee` (off); gate = `DS:0x844` |
### Cheat-dispatch keyboard functions (seg007)
| Address | Name | Description |
|-------------|-----------------------------------------------|-------------|
| `0007:04dc` | `keyboard_input_cheat_dispatch` | Processes one keyboard event: calls `cheat_code_check`, then dispatches on raw scan-code `[record+1]`. Tab/J (0x0f/0x24) → context-sensitive entity action via FUN_0005_e119/252; KP* (0x37) → `cheat_entity_slot_cycle_and_update_sprite`; Space (0x39) → movement/entity_command_dispatch; KP- (0x4a) → `cheat_anim_type_cycle_and_refresh`; KP+/KP0/KPDel (0x4e/0x52/0x53) → selected object vtable `+0x18`(0xb,...) dispatch. ASCII H (0x48) absent; HACK MOVER comes from a higher scripting layer. |
| `0007:0d0a` | `cheat_code_check` | Five-byte stateful matcher at `DS:0x2833`; toggles `0x844`+`0x6045`; cheats-on notification via `display_null_check_dispatch(..., 0x287b)`; cheats-off via `display_null_check_dispatch(..., 0x2892)`. |
### Cheat-dispatch helpers (000c:81xx)
| Address | Name | Description |
|-------------|-----------------------------------------------|-------------|
| `000c:8072` | `cheat_entity_slot_cycle_and_update_sprite` | Cycles slot 1..5 for `0x7e22` entity; picks sprite ID by class flags; calls `entity_table_set_sprite`. |
| `000c:81c0` | `cheat_anim_type_cycle_and_refresh` | Cycles animation-type 0x0b..0x19 for `0x7e22`; writes per-entity `+0x19`; calls `0008:4bba(0x20)`. |
| `000c:8221` | `cheat_flag_6050_clear` | Clears `DS:0x6050` to 0. |
| `000c:8227` | `cheat_flag_6050_read` | Returns `DS:0x6050` in AL. |
| `000c:822b` | `cheat_flag_6050_set` | Sets `DS:0x6050` to 1. |
### Additional cheat-dispatch hotkeys in `keyboard_input_cheat_dispatch`
Verified byte tests in the caller-side dispatch:
- `0x37` calls `000c:8072` (`cheat_entity_slot_cycle_and_update_sprite`)
- `0x4a` calls `000c:81c0` (`cheat_anim_type_cycle_and_refresh`)
- `0x0f` and `0x24` share a context-sensitive branch via `FUN_0005_e252` with event IDs `0x3a`, `0x38`, or `0x0b`
- `0x39` and `0x52` share a branch computing a queued delta via `entity_command_dispatch`
- `0x4e` and `0x53` are separate guarded selected-object lanes dispatching through the selected object's method table
### Immortality mechanics (event 0x410 / flag 0x604f)
**How immortality works at the C level:**
`DS:0x604f` is the Immortality flag. It is toggled by `event_0x410_cheat_flag_604f_toggle` at `000c:9703`.
The sole gameplay read site is `player_receive_damage_and_dispatch_effects` (`0004:c055`) at `0004:c205`.
When `0x604f != 0` (Immortality **ON**), the damage path in `0004:c205` does:
1. `CALLF 0009:9ea1` — begin hit-effect lock (animation gating sequence)
2. `CALLF 0003:c368(0x10001)` — arm anim-stagger mode (seg001:4d68 path)
3. `IDIV 0x40000` — divide the 32-bit incoming damage value by **262,144** → result is effectively 0 for any realistic HP scale
4. Apply the negligible reduced damage via `CALLF 0003:dbcc`
5. Spin on `DS:0x31a2 != 0` event-break gate before re-enabling channels
When `0x604f == 0` (Immortality **OFF**, normal path):
- Jump to `0004:c25b``CALLF 0003:ac7e` (seg001:367e) — full damage / death dispatch
The hit stagger **still plays** in immortality mode (the Silencer visually flinches). Technically HP decreases by 0 per hit (integer truncation from /262144), so there is no true invulnerability flag that bypasses all HP accounting, just extreme attenuation.
**What sends event 0x410 to toggle it:**
The 000c event handler at `000c:9703` is entered via the large cheat-event dispatch switch at `000c:8c56-000c:8d16`. That switch is driven by the seg021 event scheduler, not by the static keyboard dispatch in `keyboard_input_cheat_dispatch`.
Key negative result: no function in the compiled C code directly pushes the value `0x410` into the game's event broadcast path. All three occurrences of the immediate `0x410` in the disassembly are: (a) the `CMP BX,0x410` comparison inside the 000c switch, (b) a multi-event subscription list at `000b:b5cb` (registering to receive the event), and (c) an abort-function error code at `000d:5290` unrelated to the cheat.
Conclusion: event 0x410 is generated exclusively by the **interpreted USECODE lane** (centered on `EUSECODE.FLX`), not by any static keyboard-level scan-code path in the compiled binary. The F10 keyboard branch in `seg001_input_keyboard_handler` is a separate `0x44` path gated by `0x6045`, not by `0x410`. Separate follow-up work on the imported `ASYLUM.DLL` shows that DLL exports `ASS_*` audio routines, so it should not be conflated with the immortality toggle path. The in-game trigger is still best modeled as a USECODE item or controller script, consistent with the surrounding string evidence (`000e:6337 "CruHealer"`, `000e:6341 "BatteryCharger"`, `000e:6445 "Controller"`, `000e:64ab "AutoFirer"` — these are USECODE process class names bracketing the Immortality string).
**Secondary handler (000b:b62c):**
`000b:b62c` subscribes to event 0x410 via the registration at `000b:b5cb`. When event 0x410 is received by this handler, it writes state code `0xe` (decimal 14) into the event object's field `+0x6` and passes it to `000b:b7f3` for processing. This is a parallel state-machine path that runs alongside the 000c toggle; likely it drives an associated USECODE process or animation object into state 14.
| Address | Symbol | Role |
|-------------|-------------------------------|------|
| `0004:c055` | `player_receive_damage_and_dispatch_effects` | Renamed. Contains the `0x604f` immortality gate at `0004:c205`. |
| `DS:0x604f` | Immortality flag | Set/cleared by event `0x410`. Read only at `0004:c205`. |
| `DS:0x60d2` | Immortality-on notification ptr | Near pointer in DS; resolves to far ptr → "Immortality enabled." display. |
| `DS:0x60ee` | Immortality-off notification ptr | Near pointer in DS; resolves to far ptr → "Immortality disabled." display. |
| `000a:b988` | `video_bios_state_snapshot` | Called after notification display in the 0x410 toggle to refresh screen state. |
### Conservative folklore verification
- "Cheats can be enabled with `-laurie`" is **directly verified**.
- "There is a hidden five-byte matcher that toggles cheats" is **directly verified**.
- "F10 performs a large cheat-only restore/reset action" is **directly verified**.
- "Ctrl+F10 enables god mode" is **not supported** — the verified F10 branch does not require a modifier.
- "H enables hack mover" is **real at runtime** (strings confirmed), but not found in the static low-level byte dispatch; the activation comes from the USECODE scripting layer.
- "Immortality makes the player invincible" is **partially verified**: damage is divided by 262,144, making HP loss negligible; the hit stagger still plays. There is no bypass of the HP system entirely.
- "Immortality is toggled with a keyboard combo" is **not supported in compiled C code**: event 0x410 has no static keyboard dispatch path. It is USECODE-triggered.
- The hidden five-byte matcher compares bytes from live code at `0007:2833`, and the ordinary keyboard ISR producer does not naturally emit byte values `0x80` and `0xfd` into record byte `+1`.

122
docs/overview.md Normal file
View file

@ -0,0 +1,122 @@
# Crusader: No Remorse — Binary Overview
## Binary Overview
- **Game**: Crusader: No Remorse (Origin Systems, 1995)
- **Platform**: DOS (16-bit protected mode)
- **DOS Extender**: Phar Lap 286 DOS-Extender (RUN286)
- **Executable Format**: Bound `MZ -> NE` executable with Phar Lap DOS-extender code
- **Entry Point**: `10da:7c40`
## Installed Copy Findings
- No standalone `.EXP` file exists in `F:\Apps\Crusader No Remorse`.
- `CRUSADER.EXE` is the original game binary and contains a valid internal `NE` header.
- Outer DOS `MZ` header points to `e_lfanew = 0x36F70`.
- Internal header at `0x36F70` starts with `NE` and describes **145 segments**.
- The NE segment table references data from the original file directly, so there is no separate embedded payload that needs to be carved out first.
- `CNRCEXP.EXE` is a modern Win32 helper tool, not part of the original DOS execution path.
## Raw Full-EXE Import Mapping
- A separate raw-binary import of the full executable (`crusader-raw.exe`) is usable: Ghidra discovers thousands of functions across a single flat `ram` block.
- Direct `file_offset -> flat_address` mapping from the standalone segment extracts is not reliable for porting names into that raw import.
- The extracted `segNNN_*.bin` files match `CRUSADER_NE.EXE`, but the raw full-EXE import must be mapped by verified byte signatures / known function bodies.
- Verified segment bases in the raw full-EXE import:
- `seg001` base = `0x6E570` (`cursor_update_hover` at `0006:e5d0`, rel `0x0060`)
- `seg021` base = `0x87170` (`entity_count_by_type_a` at `0008:7377`, rel `0x0207`)
- Porting rule for these verified segments:
- `raw_full_exe_flat = verified_segment_base + standalone_segment_relative_offset`
- Naming note:
- `seg001` and `seg021` both contain a keyboard handler; in the full program database, the seg001 copy is named `seg001_input_keyboard_handler` to avoid a symbol collision with seg021 `input_keyboard_handler`.
### Address Space Layout in the Raw Import
Ghidra segment:offset `SSSS:OOOO` = flat address `SSSS * 0x10000 + OOOO`.
| Flat range | Content |
|---|---|
| `0x00000``0x36F6F` | Phar Lap 286 DOS extender (outer MZ stub code) |
| `0x36F70` | NE header (145-segment game image begins here in file) |
| `0x6E570`+ | NE game segments at their Phar Lap linear load addresses |
Mapping rule (verified for seg001 and seg021):
```
runtime_flat_base = NE_segment_file_offset + 0x36F70
```
Example: seg004 at file `0x40A00` → runtime `0x77970` → Ghidra `0007:7970`.
Functions at Ghidra `0003:XXXX` / `0004:XXXX` are **Phar Lap extender code** (flat < `0x40000` is below any game segment). Functions at `0006:E570`+ are game NE segments.
### `0000:ffff` — NE Fixup Placeholder (not a dispatcher)
`unresolved_far_thunk_dispatch` at `0000:ffff` is NOT a runtime function. Every `CALLF 0x0000:ffff` in the 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.
**`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.
- **Each `CALLF 0x0000:ffff` in the binary is a DIFFERENT call with a DIFFERENT actual target.**
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`.
Known call-site classifications (by argument pattern):
- `PUSH DS; PUSH imm_ordinal; CALLF` — Phar Lap extender calling a runtime-imported procedure by ordinal
- `PUSH ptr_seg; PUSH ptr_off; CALLF` — inter-NE-segment function call (intra-game far call)
- Multiple typed pushes then CALLF — external C runtime / game subsystem call with normal args
### Latest Raw Full-EXE Porting Progress
Newly ported and renamed into `CRUSADER-RAW.EXE` from verified `seg001` mapping (`base 0x6E570`):
- `0007:28ce` = `shot_entity_alloc` (`seg001 + 0x435e`)
- `0007:2a19` = `shot_entity_free` (`seg001 + 0x44a9`)
- `0007:2bc9` = `projectile_init_vector` (`seg001 + 0x4659`)
- `0007:3001` = `entity_fire_weapon` (`seg001 + 0x4a91`)
- `0007:3088` = `fire_weapon_from_cursor` (`seg001 + 0x4b18`)
- `0007:30e8` = `projectile_check_hit` (`seg001 + 0x4b78`)
- `0007:319e` = `projectile_step_update` (`seg001 + 0x4c2e`)
- `0007:3298` = `projectile_trace_ray` (`seg001 + 0x4d28`)
- `0007:371d` = `projectile_update_tick` (`seg001 + 0x51ad`)
- `0007:4009` = `projectile_apply_hit` (`seg001 + 0x5a99`)
## Segment Map
| Segment | Address Range | Purpose |
|---------|--------------|---------|
| CODE_0 | `1000:0000 - 1000:01ff` | Interrupt dispatch table / thunks |
| CODE_1 | `1020:0000 - 1020:0b9f` | Low-level interrupt handlers, mode switching |
| CODE_2 | `10da:0000 - 10da:25ef` | **Main runtime** — C library, I/O, formatting, entry point |
| CODE_3 | `1339:0000 - 1339:0c2f` | **DOS/DPMI services** — INT 21h/31h wrappers, interrupt vector mgmt, fast memcpy |
| CODE_4 | `13fc:0000 - 13fc:27af` | **String data & runtime constants** — error messages, format strings, Phar Lap ID |
| CODE_5 | `1677:0000 - 1677:0e8f` | **EMS/XMS memory management** — expanded memory handlers |
| CODE_6 | `1760:0000 - 1760:7ccd` | **DOS Extender core** — EXP loader, command-line parser, memory management, system init |
| DATA | `1760:7cd0 - 1760:7cdf` | Global data |
| HEADER | `HEADER::0000 - HEADER::044f` | MZ/P2 file header |
## 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.
## Next Steps
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

108
docs/phar-lap-extender.md Normal file
View file

@ -0,0 +1,108 @@
# Crusader: No Remorse — Phar Lap DOS Extender Analysis
This file covers the Phar Lap 286 DOS extender code portion of `CRUSADER.EXE` — the outer MZ/P2 stub that bootstraps the NE game image.
## Architecture Notes
### Correction: The Game Ships As A Bound NE Executable
**Important**: The installed copy does **not** contain a separate `.EXP` file. `CRUSADER.EXE` is a bound executable with an outer DOS `MZ` stub and an internal `NE` executable image. The Phar Lap loader/runtime code and the game's real segment layout are both described inside this same file.
The flow is:
1. `entry` → checks DOS version, CPU type
2. `init_dos_extender` → sets up protected mode (VCPI/DPMI)
3. `load_exp_file` → opens the game's `.EXP` file
4. `load_executable_image` → parses P2/MZ headers, creates segments, applies relocations
5. `task_switch_to_child` → transfers control to the actual game code
For the installed retail copy, this means the currently loaded Ghidra program is only one interpretation of `CRUSADER.EXE`. The next import should target the **NE layer of the same file**, not a missing external `.EXP`.
### 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.
## Named Functions
### Entry & Startup
| Address | Name | Description |
|---------|------|-------------|
| `10da:7c40` | `entry` | Program entry point — checks CPU, parses command line, launches game |
| `10da:1816` | `main_init_and_run` | Main initialization — loads child EXP, sets up subsystems, runs game |
| `1760:1432` | `parse_cmdline_and_run` | Parses command-line args and invokes main_init_and_run |
| `1760:42fa` | `init_dos_extender` | Initializes Phar Lap 286 DOS extender (CPU check, VCPI/DPMI setup) |
### Executable Loading
| Address | Name | Description |
|---------|------|-------------|
| `1760:2cdf` | `load_exp_file` | Loads .EXP executable — opens file, reads headers, allocates memory |
| `1760:1dfc` | `load_executable_image` | Parses P2/MZ headers, loads segments, creates LDT entries |
| `1760:24a6` | `apply_relocations` | Applies segment relocations to loaded executable |
| `1760:5eca` | `exec_child_process` | Executes child process with command-line arguments |
| `1760:5fee` | `exec_program_with_args` | Builds command line, locates and executes a program |
| `10da:1f7e` | `load_and_run_child` | Wrapper: loads child EXP and initializes it |
### System Services
| Address | Name | Description |
|---------|------|-------------|
| `10da:2330` | `dos_exit` | Calls INT 21h AH=4Ch (terminate program) |
| `1760:42aa` | `detect_cpu_type` | Detects CPU: 0=8086, 2=286, 3=386+ |
| `1339:04a6` | `dpmi_set_interrupt_vector` | INT 31h — DPMI set interrupt vector |
| `1339:06ca` | `switch_to_real_mode` | Switches CPU from protected to real mode |
| `1339:06f2` | `switch_to_protected_mode` | Switches CPU from real to protected mode |
| `1339:0076` | `setup_interrupt_handlers` | Configures interrupt vectors via INT 21h |
| `1339:0a38` | `dos_int21h_wrapper` | Simple INT 21h call wrapper |
| `1339:0a82` | `dos_int21h_with_regs` | INT 21h call with register parameters |
| `10da:2360` | `get_flags_register` | Returns CPU FLAGS register |
| `10da:2363` | `set_flags_register` | Sets CPU FLAGS register |
### Memory Management
| Address | Name | Description |
|---------|------|-------------|
| `1677:0d12` | `cleanup_ems_memory` | Frees EMS (INT 67h) memory handles |
| `10da:14fc` | `init_stack_fill_cc` | Fills stack with 0xCC (INT 3) for debugging/guard |
| `10da:1706` | `get_segment_base_addr` | Computes linear base address from segment descriptor |
### Task Management
| Address | Name | Description |
|---------|------|-------------|
| `10da:19ca` | `task_switch_to_child` | Context switch to child process |
| `10da:1946` | `task_switch_from_child` | Context switch back from child process |
| `10da:1af4` | `call_termination_handler` | Calls registered termination callback |
### I/O & Output
| Address | Name | Description |
|---------|------|-------------|
| `10da:00d6` | `flush_output_buffer` | Flushes buffered output via function pointer |
| `10da:0132` | `putchar_buffered` | Writes character to buffer, flushes on newline |
| `10da:0808` | `memcopy_to_buffer` | Copies N bytes from source to destination buffer |
| `10da:178c` | `print_error_message` | Formats and prints load error (references "not loaded: %s") |
| `10da:09e4` | `print_fatal_error` | Prints "Fatal Error" prefix + message |
| `10da:192a` | `print_internal_error` | Prints "Internal Error" message |
### Interrupt Management
| Address | Name | Description |
|---------|------|-------------|
| `10da:1ec0` | `restore_interrupt_vectors` | Restores INT 2Fh and INT 67h vectors |
| `10da:2249` | `restore_int_2f_67` | Restores INT 15h vector if saved |
| `1760:3d86` | `init_system_check` | Validates system (CPU, DOS version, VCPI/DPMI, memory) |
### Utility
| Address | Name | Description |
|---------|------|-------------|
| `10da:15ea` | `check_ds_segment` | Returns true if DS == 0x10 (checks data segment selector) |
| `1760:3c9e` | `nop_stub` | Always returns 0 (unused hook) |
## Key String References
| Address | String | Context |
|---------|--------|---------|
| `13fc:0016` | `$Id: comhighc.c 1.1 91/08/06...` | Phar Lap C runtime source ID |
| `13fc:0048` | `$Id: comutils.c 1.1 91/08/06...` | Phar Lap utility functions source ID |
| `13fc:0078` | `Serial Number ` | DOS extender serial validation |
| `13fc:14ca` | `Internal Error` | Error class prefix |
| `13fc:14da` | `Fatal Error` | Fatal error class prefix |
| `13fc:156a-1628` | File error messages | Not found, bad format, no memory, etc. |
| `1760:665c` | `Copyright (C) 1986-93 Phar Lap Software, Inc.` | DOS extender copyright |
| `1760:73da` | `-LDTSIZE 4096 -EXTHIGH D0_0000h -NI 18 -ISTKSIZE 3` | Default extender config |
| `1760:76fc-7c5a` | Numbered error messages | System requirement errors (1000-2170) |

247
docs/raw-0007-rendering.md Normal file
View file

@ -0,0 +1,247 @@
# Raw 0007: Rendering, Scroll/Camera & Coordinate Transforms
Content extracted from `crusader_decompilation_notes.md`. Covers the sprite draw list subsystem, tile-based visibility, map scroll/camera, and the isometric coordinate transform system.
---
## Raw 0007 Rendering & Sprite Draw List Subsystem
### Draw List Node Format
Each draw node is `0x2a` (42) bytes. The free pool holds 1500 nodes based at `0x2cc7`.
| Offset | Field |
|--------|-------|
| `+8` | child link (for `drawlist_enqueue_sprite_children`) |
| `+10` | z-order dependency forward link |
| `+0x14` | enqueued flag (bit 0) |
| `+0x15` | z-depth (1 = root sentinel) |
| `+0x16` | flags/type byte |
| `+0x17` | redraw must-flag |
### Draw List Functions
| Address | Name | Evidence |
|---------|------|---------|
| `0007:eb36` | `drawlist_pool_init` | Inits free pool: 1500 nodes × `0x2a` 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. |
### Isometric Coordinate Transform (draw list side)
| Address | Name | Evidence |
|---------|------|---------|
| `0007:be67` | `world_to_screen_isometric` | Coarse tile-grid → screen transform. Uses `(wx+sx) + (wy+sy)*2` for screen_x, `(wy+sy)*2 - (wx+sx)` for screen_y. Camera (`sx`, `sy`) added before transform. |
| `0007:bef8` | `world_to_screen_isometric_wrapper` | Thin wrapper around `world_to_screen_isometric`. |
---
## 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
A scroll/camera management cluster found in the `0007:bxxx0007:dxxx` range.
### Scroll/Camera Functions
| Address | Name | Evidence |
|---------|------|---------|
| `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: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.
| 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:b46d` | `entity_dispatch_if_slot82e2_valid` | Guard: if `*(int *)0x82e2 != -1`, calls dispatch thunk. |
### 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_controller_farptr` | FAR ptr to the watch/camera controller object |
| `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) |
---
## Deep Analysis: Coordinate Transform System
### `world_to_screen_coords` at `0004:e7bd` (NE seg018:07bd)
**Signature:**
```c
void world_to_screen_coords(int world_x, int world_y, int *screen_x, int *screen_y)
```
**Isometric Projection Math:**
```
screen_x = (world_x - world_y) / 2 - camera_x // SAR 1 (signed divide)
screen_y = (world_x + world_y) / 4 - camera_y // SHR 2 (unsigned divide)
```
Camera globals: `g_scroll_offset_x` (DS:`0x2bb7`), `g_scroll_offset_y` (DS:`0x2bb9`).
**Assembly detail:**
- `SAR AX, 1` for screen_x — signed arithmetic shift preserves sign for negative (world_x - world_y) differences
- `SHR AX, 2` for screen_y — unsigned logical shift (sum world_x + world_y is always positive)
- The 2:1 ratio (÷2 for X, ÷4 for Y) produces the classic 2:1 isometric diamond tile shape
**Coordinate axes on screen:**
- World X axis → lower-right on screen (+0.5 screen_x, +0.25 screen_y per world unit)
- World Y axis → lower-left on screen (-0.5 screen_x, +0.25 screen_y per world unit)
- Camera subtraction converts absolute world-space to viewport-relative screen coordinates
**Callers (17 across 8 NE segments):**
| Call site | NE Segment | Context |
|-----------|-----------|---------|
| `0004:7d6f` | seg012 | Map/tile rendering |
| `0005:0305` | seg021 | Entity system |
| `0005:432f` | seg021 | Entity placement |
| `0005:4457` | seg021 | Entity placement |
| `0005:6f8f` | seg022 | Entity rendering |
| `0005:7263` | seg022 | Entity rendering |
| `0007:2262` | seg040 | `snap_entity_to_ground` — ground alignment |
| `0007:237d` | seg040 | Ground snap dispatch |
| `0007:cf4e` | seg049 | Entity positioning |
| `0007:d039` | seg049 | Entity positioning |
| `0007:d43f` | seg049 | Entity positioning |
| `0007:d6fe` | seg049 | Entity positioning |
| `0008:3223` | seg053 | Entity-to-screen render setup |
| `0008:32e7` | seg053 | Entity-to-screen render setup |
| `0008:334b` | seg053 | Entity-to-screen render setup |
| `000b:858b` | seg115 | Sprite system |
| `000b:f100` | seg120 | Sprite system |
**Entity struct layout (from seg053 caller at `0008:31f6`):**
```
entity_array_base = far ptr at [DS:0x2cff]
entity_struct_size = 19 bytes (0x13)
entity.world_x = offset +0x0a (word)
entity.world_y = offset +0x0c (word)
```
### Comparison: Two Coordinate Transform Functions
| Property | `world_to_screen_coords` (`0004:e7bd`) | `world_to_screen_isometric` (`0007:be67`) |
|----------|---------------------------------------|----------------------------------------|
| Input type | Fine-grained world units (entity positions) | Coarse tile-grid units (map rendering) |
| screen_x | `(wx - wy) / 2 - cam_x` | `(wx + sx) + (wy + sy) * 2` |
| screen_y | `(wx + wy) / 4 - cam_y` | `(wy + sy) * 2 - (wx + sx)` |
| Camera handling | Subtracted after transform | Added before transform |
| Operations | Division (SAR/SHR) | Multiplication (SHL) |
| Aspect ratio | 2:1 (from /2 : /4) | 2:1 (from 1 : 2 multipliers) |
Both functions implement the same 2:1 isometric projection but at different coordinate scales. `world_to_screen_coords` divides down from fine world units while `world_to_screen_isometric` multiplies up from coarse tile units.
### Adjacent Function: `map_position_equal` at `0004:e784`
Compares two 5-byte `map_position` structs: `{ x:word, y:word, layer:byte }`. Returns 1 (AL) if all three fields match, 0 otherwise. Located immediately before `world_to_screen_coords` in seg018.

361
docs/raw-0008-000c.md Normal file
View file

@ -0,0 +1,361 @@
# Raw 0008 & 000c: Dispatch Helpers & State Machine
Content extracted from `crusader_decompilation_notes.md`. Covers the 0008 gameplay dispatch helper cluster and all 000c state machine helpers.
---
## Raw 0008 Gameplay Dispatch Helper Batch
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` (`0008:bae4`) and `entity_increment_group_id` (`0008:be57`).
- `entity_set_source_type` is used from `FUN_0008_c92f` (`0008:c94d`, `0008:c96d`) and `FUN_0008_ca18` (`0008:ca36`, `0008:ca56`).
- `0008:bd79` remains positional, but current 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`.
### Gameplay relevance
This cluster manages core dispatch-entry metadata (`source_type`, `event_type`, group/layer byte and counters) that feeds the seg021 scheduler/event system. The field offsets match the current seg021 entity/dispatch layout notes (`+0x04`, `+0x06`, `+0x08`).
---
## Raw 0008 Pair-Sync Helper Batch
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` |
---
## Raw 0008 Flag-0x20 Target-State Helpers
Two complementary helpers near the pair-sync cluster.
| 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` |
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
Follow-up rename batch for the shared refresh node used by the 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`) |
State pipeline after target/link changes: flag-gated status clear → mark refreshed (`0x4000`) → vtable update callback → flag-conditioned subsystem handlers.
---
## Raw Import Note: `0000:ffff` Thunk Target
`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.
Treat calls to `unresolved_far_thunk_dispatch` as unresolved external/indirect dispatch edges. Semantic recovery should continue from call-site argument setup and local field effects.
---
## Raw 0008 Flag-0x100 and Constructor-Variant Batch
| 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` |
---
## Raw 0008 Periodic/Counter Helpers
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` |
The `0x39f4/0x39f6` counter swap implies global bookkeeping for a scheduler subset associated with these entries.
---
## Raw 0008 Word-List Management Batch
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` |
Entry fields used by this subsystem: count at `+0x02`, list far pointer at `+0x06/+0x08`. The explicit `0x0408` terminator appears in both scanner/build logic and append path.
---
## Raw 0008 Word-List Access/Mutation Batch
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 |
List entries pack a 10-bit id plus flag bits (`0x400` observed).
---
## Raw 0008 Gate-Callback Wrapper Batch
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 |
---
## Additional Unresolved Thunk Stubs
Follow-up thunk census after inspecting `0000:ffff` behavior. All of the following are single-instruction wrappers (`CALLF 0000:ffff`):
| 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`) |
`unresolved_far_thunk_dispatch` is represented by multiple local trampoline copies in different segment regions. Separating them by address improves call-graph navigation.
---
## Raw 000c State-Dispatch Helper Cluster
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` |
---
## Raw 000c State-Flag Guard / Input Handler Batch
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:**
- `field49` = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
- `field47` = keystroke-combo counter
- `field3f` = linked data pointer (event/record reference)
- `[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
### VGA Palette Fade
| 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
| 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 |
### Current EUSECODE / event bridge notes
- `entity_vm_set_value_from_slot_plus_offset` (`000c:f95f`) now provides a concrete bridge from the `000c` mini-VM cluster into the `000d` event/countdown lane:
- it calls `FUN_000d_5572(*(word *)0x6611, *(word *)0x6613, param_3, param_4, 0, 0)`
- then stores the returned far pair into target object fields `+0xd6/+0xd8`
- `entity_vm_slot_load_value_plus_offset` (`000d:5572`) is a thin wrapper over `entity_vm_slot_load_value` (`000d:51fd`), and `entity_vm_slot_load_value` contains a verified `PUSH 0x410` path at `000d:5290` before calling the unresolved seg091 event/abort lane at `000a:44fd`.
- This is not enough yet to say that `entity_vm_set_value_from_slot_plus_offset` is the immortality trigger, but it does show that the `000c` mini-VM / record-player cluster can hand work directly into a `000d` helper that emits event `0x410`.
- Supporting renamed helpers in the same lane now include:
- `entity_vm_slot_find_or_select` (`000d:4e7c`): scans 0x26-byte slot records, returns a matching slot id when present, and tracks one fallback slot for reuse/eviction
- `entity_vm_slot_decrement_use_count` (`000d:558d`): decrements one slot-use counter and traps on underflow
- `entity_vm_slot_release_value` (`000d:5617`): releases one slot value, restores the owner's `0x1300/0x1302` budget pair, writes the slot state back to `-1`, and notifies through `000a:2b9d`
- `entity_vm_opcode_finish` (`000d:3350`): shared VM opcode epilogue used by the `000d:039f`, `000d:08a2`, `000d:0988`, `000d:177c`, and `000d:1acb` handlers; if the local result slot is non-zero it writes the current referent id to `0x8c94`, optionally pops one `slot_array` frame through `0x659c/0x659e`, and returns the opcode result from local state
- `entity_vm_referent_chain_remove_matching_from` (`000d:6a9a`): destructive chain-difference helper used by the `0x1a/0x1b` opcode path in `000d:0988`; it walks one source chain against a destination chain, removes matching entries in place, and frees removed registry nodes / indirect payloads
- `entity_vm_referent_chain_set_entry_data_at` (`000d:6cf6`): finds one chain entry by index and overwrites its payload in place, including indirect/string cleanup when the chain uses indirect storage
- The surrounding runtime/context family is now materially clearer too:
- `entity_vm_runtime_create` / `entity_vm_runtime_init_slots` / `entity_vm_runtime_release_slots` / `entity_vm_runtime_destroy` (`000d:4c99`, `000d:4d36`, `000d:4d75`, `000d:4e01`) are the global `0x6611` owner for this lane; they allocate the 0x2040-byte runtime body, clear the 0x80-entry slot table, manage the runtime budget/default fields at `+0x1300..+0x1314`, and retain one owner/resource object at `+0x1315/+0x1317` returned by `000d:7000`
- `entity_vm_slot_index_from_entity` (`000d:45c5`) computes one slot index from a gameplay entity by branching on seg021 class/type helpers and then adding one of the current runtime base offsets `0x8c7c/0x8c7e/0x8c80`
- `entity_vm_context_try_create_masked_for_entity` (`000d:463a`) uses that slot index to test one owner-side mask entry before it creates a context, which is the strongest current bridge from gameplay entities into this VM lane
- `entity_vm_context_create_from_slot_index` (`000d:46ec`) allocates one `0x6714` context object, seeds its `+0xd6/+0xd8` lane through `entity_vm_slot_load_value_plus_offset`, initializes the local mini-VM state, and can prepend caller data into the backward-growing buffer at `+0x102`
- `entity_vm_context_sync_global_value_and_dispatch` (`000d:48da`) is the current context-side runner/sync point: it marks the context busy at `+0x123`, calls `entity_vm_set_field_da_to_global`, optionally writes the current value through `+0x11b/+0x11d`, and dispatches through the context vtable on success
- `entity_vm_context_save` / `entity_vm_context_load` / `entity_vm_context_destroy` / `entity_vm_context_free_buffer` (`000d:498f`, `000d:4a78`, `000d:4962`, `000d:48b6`) now pin down the lifecycle of this object family rather than leaving the whole `000d:45xx..4exx` island anonymous
- `entity_vm_context_try_create_masked_for_entity` is now better constrained at the return-value level too: after the runtime-disable check at `0x6610` and the owner-side slot-mask test succeed, it reports two distinct success shapes. Immediate-flagged contexts (`+0x16 & 0x0008`) clear the caller output word, while object-backed contexts return the created object's low word. That makes the helper a typed bridge from gameplay entities into VM-backed object results, not only a yes/no mask probe.
- The first opcode-level behavior split inside that runtime is now visible in the `000d:0988` family:
- one branch calls `entity_vm_referent_chain_append_unique_from`, which looks like an attach/union operation on the current referent payload chain
- the `0x1a/0x1b` branch instead calls `entity_vm_referent_chain_remove_matching_from`, which looks like the inverse operation and makes the opcode family materially closer to a graph-editing script VM than a flat event list
- both paths return through `entity_vm_opcode_finish`, so the referent-global write to `0x8c94` is now better understood as a shared interpreter epilogue rather than a unique quirk of one helper
- One additional runtime layer is now named under that context family: the referent registry at `0x8c8c/0x8c8e/0x8c90/0x8c94`.
- `entity_vm_referent_registry_init` / `entity_vm_referent_registry_destroy` (`000d:6000`, `000d:60bf`) allocate and free the registry buffer, seed the free-list/root metadata, and clear the current referent id at `0x8c94`
- `entity_vm_referent_registry_alloc` (`000d:613e`) allocates one registry node from the free list and stores the current referent id from `0x8c94` into node field `+0x04`
- `entity_vm_referent_registry_release_by_id` / `entity_vm_referent_registry_free_node` (`000d:6251`, `000d:62ac`) release all live nodes for one referent id and coalesce adjacent free nodes
- this makes `entity_vm_set_field_da_to_global` more important than it first looked: it writes `0x8c94` from the current context `+0xda` lane and then immediately enters the still-misaligned `000c:3350` body, so the referent selected by the context is now visibly feeding runtime registry state
- the registry nodes are not flat scalars only; the surrounding container helpers are now named too:
- `entity_vm_referent_chain_copy` / `entity_vm_referent_chain_append_unique_from` (`000d:6694`, `000d:68c3`) build deep-copied or deduplicated chains of referent-linked payloads
- `entity_vm_referent_chain_destroy`, `entity_vm_referent_chain_next`, and `entity_vm_referent_chain_append_node` (`000d:6602`, `000d:6651`, `000d:687b`) provide the list management shell
- `entity_vm_referent_chain_contains_entry`, `entity_vm_referent_chain_get_entry_data_at`, and `entity_vm_referent_chain_get_indirect_data` (`000d:6c31`, `000d:67f2`, `000d:6860`) show that some chains carry fixed-size inline payloads while others carry indirect string-like payload nodes
- This matters for script readability: the current runtime model is no longer only "one referent id hits one event." It now supports a more useful intermediate representation where one referent anchor can own one or more payload chains, and neighboring event-bearing descriptors can attach behavior to that anchor without duplicating the anchor's own record.
- Nearby descriptor work on `EUSECODE.FLX` is consistent with that model: event-bearing classes (`EVENT`, `NPCTRIG`, `SFXTRIG`, several `*_BOOT` records) use a stable `69:0A00 -> event` tag, while `JELYHACK` / `JELYH2` remain referent-only descriptors in a neighborhood that includes `TRIGPAD`, `SPECIAL`, `REE_BOOT`, `SURCAMEW`, and `SFXTRIG`.
- The strongest current callsites into this context-construction path are the large `000d:208b` and `000d:21ed` bodies, which both feed per-object stream/data state from `+0xcc/+0xce` into `entity_vm_context_create_from_slot_index` before continuing bytecode-style reads from the newly seeded `+0xd6/+0xd8` lane. That makes the `000d` interpreter/object lane a better current immortality target than the older `000e` text-parser hypothesis.
- The immediate producer chain for that `+0xcc/+0xce` stream state is now one layer tighter:
- `entity_vm_context_create_from_slot_index` (`000d:46ec`) allocates the `0x6714` context, then calls `entity_vm_context_setup` (`000c:f844`) on the embedded mini-VM object at context `+0x36`.
- `entity_vm_context_setup` delegates to `entity_vm_stack_init_with_data` (`000c:f6e8`), which seeds `[mini_vm+0xcc..+0xd2]` to point into the object's own payload area and optionally prepends caller-owned inline bytes by moving the stack pointer backward.
- `entity_vm_state_copy` (`000c:f772`) copies that same `+0xcc..+0xd2` stream/base quartet verbatim when one mini-VM object is cloned.
- Upstream of the setup helper, `000d:46ec` derives the source payload from the runtime owner table behind `0x6611 -> +0x1315/+0x1317`: with slot index `SI`, it walks owner table `*(owner+0x10/+0x12) + 0x0d*SI + 4`, passes that far pointer into `000c:f844`, and mirrors the resulting per-slot source into `0x39ca[slot]`.
- This sharpens the current JELYHACK-side model rather than overturning it: the code-side producer recovered in this batch is still a generic slot-backed VM source object keyed by gameplay-entity slot selection and owner-side mask bits, not a direct hard-coded descriptor-class switch on `JELYHACK` or `JELYH2`. Combined with the extractor evidence that `JELYHACK` / `JELYH2` remain referent-only while `REE_BOOT` / `SFXTRIG` keep active `event` tags and `SURCAMEW` keeps `eventTrigger`, the better fit is still `referent anchor -> slot-backed payload chain -> neighboring event-bearing attachment`.
- One exact numeric collision is now ruled out as unrelated noise rather than a second VM source: `000e:0953` in the animation/audio lane pushes literal `0x410` into imported `ASYLUM.27` immediately after setting the local audio-completion byte at `+0xef1`. Because `ASYLUM.DLL` is the `ASS_*` audio/media library, this does not weaken the attribution of gameplay event `0x410` to the `000d` VM/USECODE lane.
- Current best JELYHACK reading after this pass: `JELYHACK` itself still looks like a referent-only map/object descriptor, but that no longer makes it inert. A referent-only record can still matter by supplying the referent id that populates the VM referent registry, while neighboring classes such as `REE_BOOT`, `SURCAMEW`, and `SFXTRIG` supply the event-bearing logic attached to the same local object island.
| `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:f95f` | `entity_vm_set_value_from_slot_plus_offset` | Calls `entity_vm_slot_load_value_plus_offset` and writes the resulting 32-bit value into `[ptr+0xd6/+0xd8]` |
| `000c:f98b` | `entity_vm_set_field_da_to_global` | Writes `[param_2+0xda far-ptr + 2]` into `[0x8c94]` |
**VM field offsets:** `+0xcc`=VM stack ptr, `+0xce/+0xd0`=segment regs, `+0xd2`=base, `+0xd4`=frame depth, `+0xd6/+0xd8`=32-bit VM value/counter lane, `+0xda/+0xdc`=additional VM pointer/bounds lane. 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
### 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
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.
| Address | Name | Evidence |
|---------|------|---------|
| `000c:dac1` | `cursor_nav_state_reset` | Zeros all directional/button flags; sets `[+0x32/+0x33]=0xff`, `[+0x47]=0xffff` |

257
docs/raw-000a-000d.md Normal file
View file

@ -0,0 +1,257 @@
# Raw 000a & 000d: Tracked Handles, Cache Manager & Proximity Buckets
Content extracted from `crusader_decompilation_notes.md`. Covers the 000d proximity/visibility bucket cluster, 000a tracked-handle table, generic cache manager, seg082 allocator, seg137/138 palette helpers, and seg004/seg005 startup paths.
---
## Raw 000d Proximity/Visibility Bucket Cluster
Small conservative rename batch from the `000d:cc00-d413` region.
| 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 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
The `0x4673` table is 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`.
| 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 |
### Handle entry layout (stride `0x0c`)
| Offset | Field |
|--------|-------|
| `+0x00` | 32-bit handle id |
| `+0x04` | key/entity id |
| `+0x06` | class/group/source-style selector |
| `+0x08` | current bucket/value |
| `+0x0a` | flags (`bit0` = aux-slot allocation, `bit1` = pending removal) |
### Thin public wrappers
| Address | Name |
|---------|------|
| `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
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` |
### Cache globals
| Address | Name | Notes |
|---------|------|-------|
| `0x4688` | free-list/block-list head | Used by `cache_alloc_block_for_slot` and `cache_free_block_by_slot` |
| `0x468c` | cache_loader_interface | Backend callback table; `+0x34` = size query, `+0x0c` = load/bind callback |
| `0x4695` | arena base pointer | Base for the raw cache payload arena |
| `0x4699` | per-slot payload-pointer table | |
| `0x469d` | per-slot cached id table | `0xffff` = unused |
| `0x46a5` | byte budget / arena capacity | |
| `0x46a9` | bytes currently in use | |
| `0x46af/0x46b1` | fast-path cache | Last requested id and slot index |
| `0x46b3` | per-slot block metadata mirror | Used when releasing or refreshing slots |
---
## Follow-up: Cache Init and Runtime State
| Address | Name | Notes |
|---------|------|-------|
| `000a:6600` | `cache_init` | Stores slot count in `0x46ad`; allocates per-slot payload-pointer table; seeds each slot; queries/derives arena size; allocates arena backing object at `0x4691`; allocates per-slot metadata mirrors; initializes free-list head at `0x4688`; calls `cache_reset_runtime_state` |
| `000a:68aa` | `cache_reset_runtime_state` | Shared cache reset/bootstrap helper called from `cache_init`, `cache_shutdown`, and external reset paths. Allocates per-slot arena-header nodes, rebinds slot pointers to 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` | Compacts live cache arena blocks into earlier free holes when allocation would fail, updates per-slot payload pointers, and merges adjacent free-list headers afterward |
---
## Follow-up: Tracked-Handle Table Init/Shutdown
| Address | Name | Notes |
|---------|------|-------|
| `000a:5e00` | `tracked_entity_handle_table_init` | If `0x4672` is clear, allocates `0x90` bytes at `0x4673/0x4675`, aborts through `runtime_init_or_abort` on failure, calls `000a:577d` and local helper `000a:5e95`, then sets `0x4672 = 1` |
| `000a:5e59` | `tracked_entity_handle_table_shutdown` | Matching teardown for `tracked_entity_handle_table_init` |
| `000a:5e95` | `tracked_entity_handle_table_clear_and_dispatch` | When `tracked_entity_handle_table_active` is set, 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 |
| `000a:5339` | `tracked_entity_handle_mark_remove_all_if_enabled` | Thin gate wrapper that only forwards to `tracked_entity_handle_mark_remove_all` when `tracked_entity_bucket_system_enabled` is set |
Table globals: `0x4672` = `tracked_entity_handle_table_active`, `0x4673/0x4675` = `tracked_entity_handle_table` (12 entries × `0x0c` = `0x90` bytes).
---
## Follow-up: Tracked Bucket System Init/Shutdown
| Address | Name | Notes |
|---------|------|-------|
| `000a:5186` | `tracked_entity_bucket_system_init` | 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 `000a:5e00`, allocates the 32-entry `0x69ac` bucket array via `000d:cca3(0x20)`, then sets `tracked_entity_bucket_system_enabled` |
| `000a:538e` | `tracked_entity_bucket_system_init_if_configured` | Only calls the init routine when config/feature gate `0x89f4` is non-zero |
| `000a:5223` | `tracked_entity_bucket_system_shutdown` | Tears down the tracked handle table, frees the `0x69ac` bucket array, calls backend-object vtable slot `+0x38` with `(3, backend_object)`, clears `tracked_entity_bucket_backend_object`; called from the wider engine teardown routine at `0004:621b` |
System globals: `0x45aa` = `tracked_entity_bucket_system_enabled`, `0x45ab/0x45ad` = `tracked_entity_bucket_backend_object`.
Public thin gate wrappers that feed the `0x69ac` tracked-entry layer:
- `0005:3b34` = `tracked_entity_bucket_alloc_main_if_enabled`
- `0005:3b53` = `tracked_entity_bucket_alloc_aux_if_enabled`
- `0005:3b72` = `tracked_entity_bucket_remove_by_entity_and_ref_if_enabled` → forwards into `000d:d086 = tracked_entity_bucket_remove_by_entity_and_ref` when `0x45aa` is set.
---
## Follow-up: Backend Object Constructor
| Address | Name | Notes |
|---------|------|-------|
| `0009:5600` | `cache_backend_object_init` | Allocates a `0x20`-byte object when caller passes null; initializes embedded DOS file-handle state via `dos_file_handle_init`; 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; returns the object pointer cached at `0x45ab/0x45ad` |
Verified callback roles inside `cache_lookup_or_load_entry_by_id`:
- 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: External Reset Paths
- The path around `0004:25a9` classifies 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 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.
---
## Follow-up: Repaired seg004 Reset-Path Function Objects
| Address | Name | Notes |
|---------|------|-------|
| `0004:2592` | `runtime_cache_reset_sequence` | Calls `0008:7bfe`; calls `game_mode_init(*(0x27c4))`; calls import-resolved site `0004:25a4`, now verified from the separately imported `ASYLUM.DLL` as ordinal `24` = `_ASS_StopAllSFX`; then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers. Known caller: `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` | `entity_dispatch_entry_ctor_0f3a_with_cache_reset` | Allocates/initializes an entity dispatch entry; stamps entry type `0x0f3a`; stores its two word payload fields; runs local setup through 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`. |
| `0004:ea00` | `entity_dispatch_entry_alloc_type_0f5e` | Reuses the incoming FAR pointer when non-null; otherwise allocates `0x33` bytes through `mem_alloc_far`; initializes the entry through `entity_dispatch_entry_init`; stamps the entry type word at `+0x00` to `0x0f5e`. |
---
## Follow-up: seg082 Allocator Cluster
| Address | Name | Notes |
|---------|------|-------|
| `0009:a229` | *(size-only wrapper)* | Public size-only wrapper around the seg082 allocator. Lazily initializes the allocator on first use through `0009:bcb9`, then calls `allocator_try_alloc_from_head_table(size, default_tag, 0xff)`. |
| `0009:bcb9` | *(lazy initializer)* | One-time lazy initializer. Parses an optional `-x` tuning value from the PSP command line, clamps derived percentage into `0x14..0x50`, seeds local seg082 helpers, then sets init flag `0x4096 = 1`. |
| `0009:b06b` | `allocator_try_alloc_from_head_table` | Validates requested size, reserves a temporary work token through `0009:e15f`, scans the `0x8724` allocator head table in `0x0c`-byte entries via `allocator_head_try_alloc_block`. On success, commits the result through `0009:e2b4`, clears failure flag `0x4098`. When a pass does not find a fit, interleaves up to two finalize phases through `allocator_phase_finalize_pass(phase)` before the final retry. |
| `0009:a336` | `allocator_head_try_alloc_block` | Per-head first-fit allocator. Normalizes requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds `0x0a` node header overhead, enforces minimum of `0x10` bytes. Walks the node chain for one allocator head until it finds a free span large enough. On success, unlinks the chosen free node, either consumes it whole or splits off a remainder when `>= 0x10` bytes remain. On failure, returns `0`. |
| `0009:a5d1` | `allocator_head_free_block` | Per-head free paired with `allocator_head_try_alloc_block`. Rebuilds the node header from a payload pointer (`payload - 0x0a`), validates the owner/tag word, reinserts the block into one allocator head, and coalesces with adjacent free neighbors when possible. |
| `0009:b224` | `allocator_free_block_by_ptr` | Converts the payload pointer back through 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 small checked entry points into this path. |
| `0009:b1c3` | `allocator_phase_finalize_pass` | Accepts phase bytes `0` or `1`. Forwards that byte twice to the object rooted at `0x4588` through vtable slot `+0x08`. Then sweeps the allocator head table at `0x8724` up to the active head count at `0x879c`, calling `allocator_head_finalize_sweep` on each entry. |
| `0009:af87` | *(free-space probe)* | Walks the node chain rooted at `0x8724`. For each node, accumulates `node_size - 9` into a running total and tracks the largest single free block. Used by `cache_init` and seg013 path at `0004:833b`. |
| `0009:a961` | *(per-head finalize sweep)* | 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. |
**Allocator head table structure:**
- `0x8724` = array of `0x0c`-byte allocator heads
- `0x879c` = active head count / table limit
- Per-node size/value encoding manipulated through `0009:c628` and `0009:c6ae`, which read/write a packed 32-bit quantity split across `word + byte + byte` fields
---
## Follow-up: seg137 Palette and Dispatch-Entry Helper Family
A coherent palette-write and palette-backed dispatch-entry emission family tied to the same runtime-state constructor lane.
| Address | Name | Evidence |
|---------|------|---------|
| `000d:85da` | `vga_palette_set_all_black` | Allocates a `0x100`-entry palette buffer filled with zero RGB triplets, writes it to VGA, frees the scratch buffer. (Previously mis-named `map_object_set_dirty_flag`.) |
| `000d:8653` | `vga_palette_set_all_white` | Same shape as black — all three RGB components initialized to `0x3f`, then written through `vga_palette_write`. |
| `000d:86cc` | `vga_palette_set_all_rgb` | Takes caller-supplied RGB bytes, replicates them across a `0x100`-entry palette buffer, writes the result to VGA, frees the scratch palette. |
| `000d:82ea` | `dispatch_entry_create_black_palette_state_active` | Builds a runtime-state dispatch entry of type `0x051e` from a black `0x100`-entry palette buffer; first sets `g_active_dispatch_entry_farptr[+0x40] = 1`. |
| `000d:8a47` | `dispatch_entry_create_black_palette_state` | Same as above without marking the active dispatch entry. |
| `000d:83be` | `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. |
| `000d:875d` | `dispatch_entry_create_solid_palette_state_active` | Validates `0..0x3f` RGB inputs, fills a scratch `0x100`-entry palette buffer with that solid color, builds the same `0x051e` runtime-state dispatch entry, marks the active entry. |
| `000d:88b2` | `dispatch_entry_create_solid_palette_state` | Same as above without marking the active dispatch entry. |
Additional caller-side comments (not renamed) added 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
---
## Follow-up: seg138 Caller-Side Dispatch-Entry Emission Helper
`FUN_000d_938c` (`000d:938c-000d:9583`) — a real caller-side helper with an evidence-preserving decompiler comment added in Ghidra instead of forcing a speculative rename.
Current verified behavior:
- 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`.
---
## Follow-up: seg005 Startup/Display Orchestration
| Address | Name | Notes |
|---------|------|-------|
| `0004:60c0` | `FUN_0004_60c0` | Startup/display orchestration path: 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. |
| `000d:7600` | `active_dispatch_entry_mark_enabled` | Marks the active dispatch entry enabled |
| `000d:760e` | `active_dispatch_entry_mark_disabled` | Marks the active dispatch entry disabled |
| `000d:761c` | `active_dispatch_entry_create_default` | Creates the default active dispatch entry |
---
## Follow-up: `0x4588` Object-Role Evidence
The `0x4588` FAR object is a runtime-installed callback/dispatch object that participates in conditional render or presentation-side flow. It has an explicit install, clear, callback, and teardown lifecycle.
### Verified lifecycle
- **Install:** `000a:4932` and `000a:4936` store the incoming dword into `0x4590` and `0x458c`, then `000a:493e` stores the incoming FAR object pointer into `0x4588`.
- **Clear:** `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)`.
- **Teardown:** `000a:4a56` checks a 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()`.
- **Callbacks:** `000a:b9e5`, `000a:ba66`, `000d:9d5e`, and `000d:a3b7` all push a two-word value pair followed by the `0x4588` FAR pointer and call vtable slot `+0x0c`. `entity_conditional_render_dispatch` calls the same vtable slot with a single literal `0x0101` argument.
### Payload pairs from payload sync callsites
- `000d:9d5e` → vtable `+0x0c` payload from object fields `+0x12d/+0x12f`
- `000d:a3b7` → vtable `+0x0c` payload from object fields `+0x74f/+0x751`
- `000a:b9e5`, `000a:ba66` → emitting only when the candidate two-word pair differs from the current pair, then mirroring that pair through `000b:1e39` using global sprite/object pointer `0x4f38/0x4f3a`
### Globals
| Address | Name |
|---------|------|
| `0x4588` | runtime FAR object pointer (nullable) |
| `0x458c` | callback sync field (compared against `0x4590` in teardown) |
| `0x4590` | paired sync field |
| `0x4594/0x4595` | state flags |
| `0x45a6` | clock/cookie global used by `assert_buffer_valid` |
| `0x39ca` | dispatch callback-table pointer |
| `0x6828` | `g_active_dispatch_entry_farptr` |

204
docs/raw-000e.md Normal file
View file

@ -0,0 +1,204 @@
# Raw 000e: Parser & RIFF/Animation Clusters
Content extracted from `crusader_decompilation_notes.md`. Covers the `000e:` segment parser helper cluster and the RIFF/AVI animation streaming subsystem.
---
## Raw 000e Parser Helper Cluster
A small helper cluster in the raw `000e:` area implements a fixed-size CRLF record parser/table builder, likely used by startup/config or script-ish text data.
### Newly renamed helpers
| Address | Name |
|---------|------|
| `000e:345e` | `record_table_init` |
| `000e:34cc` | `record_table_destroy` |
| `000e:35c6` | `record_table_release_buffer` |
| `000e:35ef` | `record_table_next_slot` |
| `000e:3639` | `record_table_parse_buffer` |
| `000e:3798` | `record_parser_read_line` |
| `000e:38a0` | `record_parser_seek_next_marker` |
| `000e:38f8` | `record_parser_find_marker` |
| `000e:39cc` | `record_parser_dispatch_at_directive` |
### Behavior notes
- `record_table_init` clears the table header and zeroes 300 words of inline storage.
- `record_table_parse_buffer` walks a CRLF-separated text buffer, captures each line, splits around a marker helper path, and stores parsed entry state into `0x0c`-byte records.
- `record_parser_read_line` advances to the next CRLF-delimited line, rejects lines that start with `@` or with non-identifier punctuation, and terminates the line in-place with `0`.
- `record_parser_seek_next_marker` updates the parser's current marker cursor at `+0x18/+0x1a` by calling `record_parser_find_marker`; returns `1` if another marker was found, `0` at end-of-data.
- `record_parser_find_marker` scans forward until an `@` marker or end-of-data; optionally consumes the remaining length from the parser state.
- `record_parser_dispatch_at_directive` returns `0` unless the current substring begins with `@`; in the `@` case, it advances by 7 bytes and dispatches through a FAR thunk (`0000:ffff`).
### EUSECODE.FLX extraction notes
- `USECODE/EUSECODE.FLX` does not look like a loadable code image or plain text script. It is now validated as an indexed binary container.
- Current table model:
- entry count at file offset `0x54`
- entry table at `0x80`
- 8-byte records: `<u32 data_offset, u32 declared_size>`
- `entry_count = 3074`
- `table_end = 0x6090`, which matches the first non-zero payload offset
- `403` non-zero entries in the current file
- `tools/extract_eusecode_flx.py` now parses the full validated table and emits all `403` non-zero entries under `USECODE/EUSECODE_extracted/`, including `entry_index.tsv`, `descriptor_index.tsv`, `descriptor_neighborhoods.tsv`, `summary.json`, per-chunk `.bin`, and `.strings.txt` sidecars.
- The generated reports now expose lightweight descriptor summaries (`primary_label`, `field_names`, `field_tags`) so the object lane can be searched by field grammar instead of only by raw names.
- The extracted data now separates into at least two lanes:
- text-heavy records that fit the `000e:` CRLF parser model, such as `DATALINK` mission/objective text and `TEXTFIL1` message banks
- binary object/behavior descriptors whose sidecars expose object names and field names, such as `EVENT`, `NPCTRIG`, `CRUZTRIG`, `TRIGPAD`, `JELYHACK`, `JELYH2`, `SPECIAL`, `SURCAMNS`, and `SURCAMEW`
- The descriptor lane also shows a repeatable tagged field trailer rather than raw trailing strings only. Current spot-checks show patterns like `69 xx 00 <name>` and `24 xx 02 <name>` immediately before field names in `NPCTRIG`, `CRUZTRIG`, `TRIGPAD`, `SPECIAL`, and `SFXTRIG`. This is strong evidence that the field names belong to compact per-field metadata records, not accidental string leakage.
- The strongest currently stable tag readings are:
- `69:0000 -> referent`
- `69:0A00 -> event` on event-capable classes such as `EVENT`, `NPCTRIG`, `COR_BOOT`, `REE_BOOT`, `SFXTRIG`, `FLAMEBOX`, `NOSTRIL`, `VAR_BOOT`, and `STEAMBOX`
- `24:FE02` / `24:FC02` / `24:FA02` on object-reference-like fields such as `item`, `elev`, `door`, `source`, `dest`, `monster1`, `deadGuy`, and related referent-style links
- `24:0A02 -> eventTrigger` on `SURCAMNS` / `SURCAMEW`
- The tag report is not a full type system yet, but it is already enough to separate scalar/event slots from pointer-like object links in many descriptor classes.
- Confirmed descriptor examples from the full index:
- `EVENT`: `referent,event,item,source,dest,door,counter,counter2,link,time,post1,post2,floor,flicMan`
- `NPCTRIG`: `referent,event,item,item2,typeNpc`
- `CRUZTRIG`: `referent,item,elev`
- `TRIGPAD`: `referent,item,elev`
- `JELYHACK`: `referent`
- `JELYH2`: `referent`
- `SURCAMNS` / `SURCAMEW`: `referent,textFile,monit,valueBox,passcode,link,code,screen,cameraEgg,trueRef,therma,eventTrigger,foundGun`
- Current immortality-lane status inside EUSECODE:
- the trigger/object namespace now clearly includes `JELYHACK`, `NPCTRIG`, `CRUZTRIG`, and `TRIGPAD`
- `JELYHACK` / `JELYH2` sit in a local extraction neighborhood beside `SPECIAL`, `TRIGPAD`, `DATALINK`, `HOFFMAN`, `REE_BOOT`, `SURCAMEW`, and `SFXTRIG`, which looks more like a map/object grouping than random table order
- that neighborhood does not make `JELYHACK` itself event-bearing, but it does place it immediately beside multiple event-capable or trigger-adjacent classes (`REE_BOOT`, `SFXTRIG`, `SURCAMEW.eventTrigger`)
- no extracted chunk has yet been tied directly to event `0x410`
- one exact `0x410` collision in compiled code is now explained away: `000e:0953` pushes `0x410` into imported `ASYLUM.27` from the animation audio-subframe path immediately after setting the local audio-completion byte at `+0xef1`. Since `ASYLUM.DLL` is the `ASS_*` audio/media library, treat this as a media ordinal/value collision rather than a second gameplay or USECODE event source.
- the present best reading is that `0x410` is likely carried by data relationships between generic event-capable descriptors (`EVENT`, `NPCTRIG`, `SFXTRIG`, etc.) and map/object references rather than by a plain-text script line
- The `000e:` record parser helpers still matter, but they now appear to cover only the text-oriented subset rather than the entire FLX payload. The strongest concrete caller so far is the raw window at `000e:1b9f..1d49`, where `record_table_parse_buffer` is invoked after setup of fields that match the known animation object layout (`+0x117/+0x11b/+0x11f/+0x123`, `+0xeaf/+0xeb1`, `+0x10f/+0x111`). That makes the currently verified `000e:3639` consumer part of the animation-object lane, not a clean standalone EUSECODE loader.
- This shifts the current working model: treat `record_table_parse_buffer` as a text/metadata helper used by at least one animation/resource object, while the EUSECODE binary descriptor lane is more likely consumed by the `000d` VM/object interpreter path.
- That `000d` path is now materially less anonymous:
- the global runtime object at `0x6611` is now named `entity_vm_runtime_create` / `entity_vm_runtime_init_slots` / `entity_vm_runtime_release_slots` / `entity_vm_runtime_destroy`
- it owns the 0x80-entry slot table and a retained owner/resource object at `+0x1315/+0x1317`
- `entity_vm_slot_index_from_entity` and `entity_vm_context_try_create_masked_for_entity` show that gameplay entities are filtered through one owner-side slot-mask table before a context is created
- `entity_vm_context_try_create_masked_for_entity` is now better constrained too: after the owner-side mask check succeeds, an immediate-flagged context result clears the caller output word while an object-backed result returns the created object's low word
- `entity_vm_context_create_from_slot_index` then seeds one `0x6714` context from `entity_vm_slot_load_value_plus_offset`, while the large callers at `000d:208b` and `000d:21ed` continue by reading bytecode-like data from the seeded `+0xd6/+0xd8` lane
- The context lane now also has a separate referent-registry subsystem:
- `entity_vm_set_field_da_to_global` writes the current referent id to `0x8c94` from context field `+0xda` and then enters the still-misaligned `000c:3350` body
- `entity_vm_referent_registry_init` / `entity_vm_referent_registry_destroy` / `entity_vm_referent_registry_alloc` / `entity_vm_referent_registry_release_by_id` / `entity_vm_referent_registry_free_node` show that `0x8c8c/0x8c8e/0x8c90/0x8c94` implement one free-list-backed registry keyed by that current referent id
- this is the first solid runtime mechanism showing how referent-only descriptors can still drive script state even when the actual event field lives in a separate neighboring descriptor
- the registry now also has a named chain container layer: `entity_vm_referent_chain_copy`, `entity_vm_referent_chain_append_unique_from`, `entity_vm_referent_chain_contains_entry`, `entity_vm_referent_chain_get_entry_data_at`, and `entity_vm_referent_chain_get_indirect_data` show that one referent can own copied/deduplicated payload chains with either inline fixed-size payloads or indirect string-like payloads
- That chain layer is now less one-sided than before:
- `entity_vm_referent_chain_remove_matching_from` (`000d:6a9a`) removes entries from one chain when they match a second chain, using either inline compare or indirect string compare depending on the chain type byte
- `entity_vm_referent_chain_set_entry_data_at` (`000d:6cf6`) updates the payload of the Nth chain entry in place, freeing old indirect payload storage first when needed
- `entity_vm_opcode_finish` (`000d:3350`) is now identified as the common opcode epilogue that writes `0x8c94` from the current frame result and unwinds the temporary slot-array state before returning the opcode result
- That makes the emerging human-readable script model less ad hoc. A plausible future IR is now: `referent anchor -> payload chain(s) -> event-bearing attachment(s)` rather than a flat list of isolated descriptor rows.
- The opcode side now reinforces that IR too: at least one handler family around `000d:0988` can either append unique payload entries or remove matching ones before returning through the same epilogue, which is a better fit for a graph-editing/object-attachment VM than for a pure linear trigger list.
- That `000d:0988` family is now classified more tightly at the opcode-id level:
- opcode `0x19` = append unique indirect/string-like payload entries into the referent chain
- opcode `0x1a` = remove matching indirect/string-like payload entries from the referent chain
- opcode `0x1b` = remove matching inline/fixed-size payload entries from the referent chain
- the same helper body also implies the missing sibling `0x18` as the inline/fixed-size append-unique form, because only `0x19/0x1a` set the indirect compare flag while only `0x1a/0x1b` take the removal path
- The first concrete `000c` to `000d` bridge inside that lane remains `entity_vm_set_value_from_slot_plus_offset` at `000c:f95f`: it calls `entity_vm_slot_load_value_plus_offset`, stores its return pair into object fields `+0xd6/+0xd8`, and sits immediately beside other `entity_vm_*` helpers in the `000c:f6b8..f9d9` mini-VM cluster. On the `000d` side, `entity_vm_slot_load_value_plus_offset` wraps `entity_vm_slot_load_value`, and `entity_vm_slot_load_value` contains a concrete `PUSH 0x410` event-emission path at `000d:5290`.
- The two main `000d` caller blocks beneath that bridge now have a first stable byte/value reading too:
- internal block `000d:208b` is the simple materialize-or-forward path: it creates one VM context from the caller's stream state, checks the returned object flags, and either writes the returned value pair straight to the caller output slot or forwards the created object's low word through the shared opcode epilogue
- internal block `000d:21ed` is the inline-payload path: it creates the same VM context, prepends the caller-owned blob into the backward-growing context buffer at `+0x102`, then consumes two bytes from the seeded `+0xd6/+0xd8` lane as small shape/count metadata before building an `entity_link` closure matrix from the following caller-stream words and pushing back the non-`0x0400` results
- that is the first concrete evidence that the `+0xd6/+0xd8` lane is not only carrying immediate event/value ids; it also carries compact metadata bytes that parameterize larger inline payloads copied from the caller stream
- Current JELYHACK implication: because `JELYHACK` and `JELYH2` still expose only `referent`, the most defensible model is now that they provide map/object identity into the referent-registry lane, while one adjacent event-capable record (`REE_BOOT`, `SURCAMEW.eventTrigger`, `SFXTRIG.event`, or another nearby generic `EVENT`/`NPCTRIG`) carries the actual event semantics that can eventually reach `0x410`.
- The immediate runtime-owner writer is now pinned down one step further too. `entity_vm_runtime_create` (`000d:4c99`) is the only verified writer of runtime `+0x1315/+0x1317`, and it does so by calling newly recovered `entity_vm_runtime_owner_resource_create` (`000d:7000`). That helper does not simply copy a caller-supplied owner table: it constructs one embedded seg069/070 helper object, queries the needed table size through vtable `+0x04`, allocates child `+0x10/+0x12`, then fills the `0x0d`-stride per-slot producer records through vtable `+0x0c`. The paired release path is `entity_vm_runtime_owner_resource_destroy` (`000d:70fd`).
- That narrows the owner/resource classification safely but still stops short of speculative source-format naming. The embedded helper goes through the same seg069/070 object lifecycle used by other file/resource-style helpers (`0009:1c00` init, `0009:1800` destroy), so the most defensible current description is still `runtime owner/resource helper` rather than `USECODE file loader` or a descriptor-specific name.
- The first gameplay-side mask families around `entity_vm_context_try_create_masked_for_entity` are also now explicit from instruction evidence:
- local wrapper `0004:f033` passes slot mask `0x8000:0007`
- `FUN_0004_f05c` passes slot mask `0x2000:0015` and is reached from `0004:f2b3` after overlap/proximity checks plus entity byte `+0x32` state toggling
- `FUN_0005_27a4` passes slot mask `0x0001:0000` and is reached from the `000c:a09e` entity `+0x5b` bit-`0x0004` branch
- Those masks are enough to prove that the runtime is exposing multiple gameplay-side materialization lanes into the same owner/resource table, but they are not yet enough to tie one lane specifically to the `JELYHACK`/`JELYH2` anchor pair instead of the neighboring event-bearing descriptors (`REE_BOOT`, `SURCAMEW`, `SFXTRIG`, or another local trigger record).
- The extractor now emits a first graph-oriented view of that claim too: `referent_anchor_event_graph.tsv` groups referent-bearing rows with nearby event-bearing neighbors, and `jelyhack_island_graph.md` renders the `JELYHACK` / `JELYH2` island as edges to local descriptors. On the current data, the strongest event-bearing neighbors in that island are `REE_BOOT` (`event`), `SURCAMEW` (`eventTrigger`), and `SFXTRIG` (`event`).
- The new focused comparison report (`jelyhack_descriptor_compare.tsv`) makes one more structural point explicit: `JELYHACK` and `JELYH2` have identical first 16 header words and the same lone `referent` field tag, while differing only in the label string and one small trailing `wx[...]` literal. That strengthens the reading that they are sibling referent-anchor classes rather than separate event-bearing behavior records.
- The same comparison also helps separate anchor classes from event-bearing neighbors: `REE_BOOT`, `SURCAMEW`, and `SFXTRIG` all carry materially richer header/state patterns than `JELYHACK` / `JELYH2`, which is consistent with them holding actual trigger or attachment semantics beside the anchor-only classes.
- The `000d:21ed` callee chain is now tighter too. The nested call at `0008:7d27` is `entity_link`, which appends one entity id into another entity's word-list and, unless bit `0x0400` is set, also updates the reciprocal pair-link slots. So the `22bc..2433` opcode block is best understood as building a bidirectional entity-link closure matrix from streamed entity ids, not merely copying opaque words around.
- Ghidra now carries that interpretation as a conservative disassembly comment at `000d:22bc`, but not yet as a symbol rename, because the surrounding `000d:208b/21ed/22bc` region is still mis-split into artificial function bodies.
- The new `EVENT`-focused reports (`event_island_graph.md`, `event_descriptor_compare.tsv`) broaden the descriptor-side picture beyond the JELYHACK anchor case. The strongest second island is the compact local cluster at indices `186..195`, where `COR_BOOT`, `EVENT`, and `NPCTRIG` all expose explicit `69:0A00 -> event` tags while `ROLL_NS`, `CRUZTRIG`, `NPC_ONLY`, and `VMAIL` stay on the referent/link/text side.
- That cluster looks structurally different from JELYHACK in a useful way: `EVENT` is the large hub payload (`0x20AA`) carrying `source`, `dest`, `door`, `link`, `time`, `counter`, `post1`, `post2`, `floor`, and `flicMan`, while `COR_BOOT` and `NPCTRIG` are smaller event-bearing satellites and the surrounding records (`ROLL_NS`, `CRUZTRIG`, `NPC_ONLY`, `VMAIL`) look like attached state/trigger/object descriptors rather than alternate event cores.
- The first compare pass on that island is already informative. `COR_BOOT`, `EVENT`, `CRUZTRIG`, `NPC_ONLY`, and `VMAIL` share the same leading `0x00000000` dword class shape, `NPCTRIG` moves to a nearby `0x00000001` shape, and `ROLL_NS` is the obvious outlier with first dword `0x00000002` plus rider/time/cargo fields. So the present best reading is one three-node event-bearing core embedded inside a wider referent-neighbor island, not one flat run of equivalent trigger records.
- The extractor now also emits a global event-family pass (`event_family_index.tsv`, `event_family_summary.md`), which turns the local island findings into a wider descriptor taxonomy. Current validated families are:
- `event-hub`: `EVENT`
- `boot-event-core`: `AND_BOOT`, `BRO_BOOT`, `COR_BOOT`, `VAR_BOOT`, `REE_BOOT`
- `npc-trigger`: `NPCTRIG`
- `minimal-event-core`: `SFXTRIG`
- `environmental-event`: `FLAMEBOX`, `NOSTRIL`, `STEAMBOX`
- `callback-eventtrigger`: `SURCAMNS`, `SURCAMEW`
- That split matters because it is the first extractor-backed distinction between active event carriers and callback-only trigger holders. The `69:0A00 -> event` classes now look like the active event-bearing core of the descriptor system, while the surveillance classes with `24:0A02 -> eventTrigger` are better treated as callback/attachment endpoints rather than peer event hubs.
- The next focused pass tightened the `_BOOT` lane too. `boot_family_compare.tsv` now shows that all five `_BOOT` event cores (`AND_BOOT`, `BRO_BOOT`, `COR_BOOT`, `VAR_BOOT`, `REE_BOOT`) share the same header skeleton and the same compact field shape (`referent,event,counter,item`). The meaningful differences are payload size and local neighborhood, not descriptor schema.
- The new `boot_frontier_graph.md` makes the best early `_BOOT` frontier explicit: `AND_BOOT` and `BRO_BOOT` sit in one compact referent-heavy neighborhood (`OFFWORK`, `GUARD`, `GDOOR_N`, `GDOOR_E`, `BIGCAN`, `CRUMORPH`, `GUARDSQ`, `CARD_NS`, `CARD_EW`, `EWALLEW`/`EWALLNS`) and also point directly at each other as adjacent event-bearing siblings. So the present best reading is a reusable boot-event core template instantiated in several different local object islands, not a set of unrelated boot scripts.
- The environmental hazard lane is now similarly constrained. `environmental_family_compare.tsv` shows that `FLAMEBOX` and `STEAMBOX` are close structural siblings with the same active-event backbone (`referent,event,<hazard>,<hazard2>,direction,count`) and matching `24:0A02 / 24:FC02 / 24:FE02` object-link pattern, while `NOSTRIL` is a smaller fire-specific variant that keeps the active `event` plus dual fire references and count fields but drops the direction/newType side.
- Their neighborhoods are different enough to matter: `environmental_event_graph.md` shows `FLAMEBOX` embedded among vent/door/bridge/copy records, `NOSTRIL` among flame/pad/desk/blaster/keypad records, and `STEAMBOX` among bounce/hover/fade/steam/flame box records. So this looks like one hazard-event descriptor family reused across distinct local object islands rather than one single environmental mega-cluster.
- The callback lane is tighter too. `callback_trigger_compare.tsv` confirms that `SURCAMNS` and `SURCAMEW` are effectively the same callback-trigger template: identical field set (`referent,textFile,monit,valueBox,passcode,link,code,screen,cameraEgg,trueRef,therma,eventTrigger,foundGun`) and identical tag grammar except for the `therma` slot offset (`24:F102` vs `24:F602`). That keeps the `eventTrigger` split credible as a true callback/attachment lane rather than only a spelling variation on active `event` carriers.
- The first runtime-side follow-through on those descriptor gains is now a little tighter too. Instruction search around `000d:ebe3` confirms one fixed sequenced VM/opcode driver body, not just a vague constructor helper: it calls `000d:177c`, `000d:1acb`, `000d:0988`, the internal `000d:22bc` link-matrix block, then `000d:1d4a` and `000d:2104` in order. The key negative result is just as useful: `000d:ec31` is only the internal `CALL 000d:22bc` site inside that body, not a standalone function entry.
- Ghidra now carries that as a conservative disassembly comment at `000d:ebe3`. That is still short of a safe rename, but it does promote the lane from “suspected constructor chain” to “verified ordered opcode/handler sequence,” which is the clearest current bridge from the descriptor-side event families back into the `000d` VM/object runtime.
---
## Raw 000e RIFF/Animation Cluster
The `000e:` segment contains a RIFF/AVI streaming animation subsystem.
### Animation object field map
Field offsets relative to the object base pointer:
| Offset | Field |
|--------|-------|
| `+0xb0` | active/valid flag |
| `+0xb4``+0xc2` | constructor-initialized flags |
| `+0xd4` | alive sentinel (must be `-1` for "alive") |
| `+0xe4` | paused flag (`0` = running) |
| `+0xeaf`/`+0xeb1` | far pointer to current RIFF chunk |
| `+0xedb` | animation frame stack depth counter (max 9) |
| `+0xee1` | frame data from current chunk `+4` |
| `+0xeef` | current subframe index |
| `+0x1b3` | subframe count |
| `+0xef1` | audio completion flag |
| `+0x11b` | ring buffer write pointer |
| `+0x11f` | ring buffer read pointer |
| `+0x117` | ring buffer base |
| `+0x123` | ring buffer end (capacity boundary) |
| `+0x102` | resource pointer |
| `+0xde` | entry index (multiplied by `0x30` to reach per-entry data at `+0x1c7`) |
### RIFF format notes
The game uses standard RIFF/IFF:
- LIST magic: `0x5453494c` = `"LIST"`
- RIFF magic: `0x46464952` = `"RIFF"`
- `"movi"` FourCC subchunk for animation frames
- Audio frames tagged `"01wb"` (`0x62773130`)
- Video frames handled through a separate path
### Newly renamed functions
| Address | Name | Evidence |
|---------|------|---------|
| `000e:2a28` | `riff_find_chunk_by_type` | Walks RIFF LIST/RIFF chunk list; compares each node's FourCC at `+8` vs `param_2`; returns pointer to matching chunk or NULL |
| `000e:2104` | `animation_start` | Finds `"movi"` chunk via `riff_find_chunk_by_type`, inits ring buffer ptrs at `+0x11b` from `+0x117 + duration`, calls `animation_advance_frame`, loops `anim_load_audio_frame` and a second frame-loader thunk path per subframe |
| `000e:12f4` | `animation_advance_frame` | Fixed-point `0x1000` timer arithmetic; checks `+0xe4` (paused), advances ring buffer `+0x11b/+0x11f/+0x117/+0x123`; calls advance thunk |
| `000e:103f` | `animation_tick` | Guard wrapper: checks `param_1+0xd4 != -1`, then calls `animation_advance_frame(param_1, 0)` |
| `000e:06f7` | `anim_load_audio_frame` | Checks chunk tag == `0x62773130` (`"01wb"` = audio stream 1); computes ring buffer free space; copies chunk payload via `0x0000:ffff` thunk; increments subframe index at `+0xeef`; resets at subframe count `+0x1b3` |
| `000e:053d` | `anim_load_video_frame_wrapper` | Called once per subframe in `animation_start` immediately after `anim_load_audio_frame`; thin wrapper that forwards to `000e:ffb0` |
### Unresolved callee
- `000e:ffb0` remains unresolved (decompiles garbled due to overlapping instructions at `000f:0085/000f:0086`). Current evidence from the `animation_start` loop suggests this path is the video-side subframe loader paired with `anim_load_audio_frame`.
### Constructor pattern
All three constructor variants (`000e:2777`, `000e:2860`, `000e:2969`) follow the same layout:
1. Call `FUN_000e_e935` (allocator — produces garbled 11KB decompile, not renamed)
2. Set fields `+0xb4` through `+0xc2` on the result
3. Call `000d:ebe3` (multi-step chain initializer: calls `177c`, `1acb`, `0988`, `22bc`, `1d4a`, `2104` in sequence)
4. Call `assert_alive_sentinel` (assertion: checks `+0xd4 != -1`)
5. Call `func_0x000eec83`
The chain at `000d:ebe3` steps through VM opcode handlers (`000d:177c`, `000d:1acb`, `000d:0988`) that operate on a bytecode VM object with stack pointer at `+0xcc` (decremented by 2 per push) and segment base at `+0xce`.
### Constructor variant renames
| Address | Name |
|---------|------|
| `000e:223d` | `assert_alive_sentinel` |
| `000e:2777` | `animation_ctor_variant_a` |
| `000e:2860` | `animation_ctor_variant_b` |
| `000e:2969` | `animation_ctor_variant_c` |

View file

@ -0,0 +1,333 @@
# Crusader: No Remorse — Raw Import Porting Progress & Gameplay Batches
This file covers the raw `CRUSADER-RAW.EXE` porting batches: seg091 RNG helpers, the 0x4588 runtime callback lifecycle batches, and raw 0007 gameplay analysis batches.
---
## 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 behavior:
- `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.
- `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.
- `seg091_func_014d` 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.
---
## Raw 0x4588 Runtime Callback Lifecycle Batch
New conservative runtime-callback lifecycle renames (direct analysis):
- `000a:4913` = `runtime_callback_object_init_once`
- `000a:4a56` = `runtime_callback_object_teardown_once`
- `0009:b1c3` = `runtime_callback_object_phase_finalize`
Boundary repair applied with MCP edit-plan API:
- Rebuilt `000a:b988` as `sprite_node_get_or_traverse` with full body `000a:b988-000a:bab5`.
Verified callback-object behavior:
- `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.
## Raw 0x4588 Follow-up Batch (allocator/video helper clarification)
New conservative helper renames:
- `0009:a961` = `allocator_head_finalize_sweep`
- `000a:4a1f` = `video_bios_state_snapshot`
Verified behavior:
- `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.
## Raw 0x4588 Follow-up Batch 2 (cleanup + mode-state wrapper)
New conservative structural renames:
- `000a:4972` = `video_mode_set_and_record_state`
- `000d:9afd` = `entity_cleanup_resources_and_dispatch`
Verified behavior:
- `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.
## Raw 0x4588 Follow-up Batch 3 (cleanup-callee helper classification)
New conservative helper renames:
- `0009:7853` = `palette_buffer_alloc_and_init_256`
- `0009:1c3a` = `file_handle_alloc_init_and_open`
- `0009:1d6a` = `file_handle_open_with_mode`
- `0009:8d7b` = `surface_release_internal`
- `0009:8e0a` = `surface_release_and_maybe_free`
- `000d:9231` = `sprite_redraw_global_if_active`
Verified behavior:
- `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.
## Raw 0x4588 Follow-up Batch 4 (function-object recovery around `000d:7e00`)
Missing function objects recovered:
- `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)
- `0003:b8e2-0003:bb39` created and renamed to `far_buffer_alloc_with_mode_flags`
Verified behavior:
- `entity_dispatch_entry_init_runtime_state` 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` 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.
## 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 added on: `0008:eb43`, `0008:ebe7`, `0008:eac8`, `0008:ec23`.
Verified behavior:
- `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.
Callback callsite clarification:
- `entity_cleanup_resources_and_dispatch` vtable `+0x0c` call at `000d:9d5e` passes object fields `+0x12d/+0x12f`.
- Matching vtable `+0x0c` call at `000d:a3b7` passes object fields `+0x74f/+0x751`.
## Raw 0x4588 Follow-up Batch 6 (constructor lane naming + callback globals)
New conservative helper renames:
- `0008:d27e` = `entity_set_update_period_and_reschedule`
- `0009:7905` = `palette_buffer_alloc_copy_from_source`
Verified behavior:
- `entity_set_update_period_and_reschedule` stores timing/update-period fields (`+0x36/+0x38/+0x3a`), clears deferred fields (`+0x3c/+0x3e`), then triggers timer recompute/reschedule helpers.
- `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).
Global data labels promoted:
- `g_active_dispatch_entry_farptr` at `0x6828`
- callback-state/object globals at `0x4588/0x458c/0x4590/0x4594/0x4595/0x45a6`
- dispatch callback-table pointer at `0x39ca`
---
## Raw 0007 Gameplay Helper Batch (entity/tile aux state)
New conservative gameplay-side helper renames (direct analysis from field writes and call structure):
- `0007:85f6` = `entity_sync_tile_aux_state`
- `0007:8865` = `entity_sync_tile_aux_if_linked`
- `0007:8709` = `entity_mark_dirty_and_sync_tile_aux`
Verified behavior:
- `entity_sync_tile_aux_state` reads entity tile index at `+0x4`, toggles bit `0x04` in tile record `+0x59` based on entity byte `+0x54`, and copies entity word `+0x55` into tile record `+0x0d`.
- `entity_sync_tile_aux_if_linked` only performs the sync when entity link/pointer `+0x50/+0x52` is non-null.
- `entity_mark_dirty_and_sync_tile_aux` calls the linked-sync helper, sets entity flag bit `0x04` at `+0x42`, then calls through `0000:ffff` with args `(SS:&tile_index, entity[+0x57])` — annotated as `entity_tile_type_notify(tile_index_ptr, type_byte)`.
New entity field found: `entity[+0x57]` (byte) = entity type/class byte (passed to tile-type notification).
## Raw 0007 Gameplay Helper Batch (facing/direction)
New gameplay helper rename:
- `0007:8bd9` = `entity_set_facing_direction`
Verified behavior:
- Updates entity facing byte `+0x38` using incoming direction/event code values (notably `0x10/0x11/0x12`) with parity-aware adjustment.
- Uses entity flags at `+0x4d` to select increment/decrement behavior for clockwise/counterclockwise facing updates.
## 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` (off), `0x2924` (seg), `0x2938` (entity type IDs).
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 real callee (`world_to_screen_coords` at `0004:e7bd` after the far-call repair pass) with spawn coordinate-derived arguments.
4. If no table entry matches, it exits without modifying the request.
#### Entity ID allow-list
Exactly 10 entity IDs: `0x31c`, `0x31f`, `0x320`, `0x321`, `0x322`, `0x323`, `0x324`, `0x325`, `0x326`, `0x327`.
#### 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) {
// Repaired: CALLF 0004:e7bd = world_to_screen_coords
call_thunk_with_spawn_context(spawn_x, spawn_y, ...);
}
}
}
```
## Raw 0007 Gameplay Helper Follow-up: AI sweep + checked spawn path
### `spawn_entity_checked` (`0007:22de`) refinements
- Function signature expanded to 7 arguments: `entity_type`, `spawn_flags_a`, `spawn_flags_b`, `spawn_flags_c`, `spawn_x`, `spawn_y`, `spawn_layer_arg`
- New comments added:
- `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
### `entity_ai_update_loop` (`0007:0fb6`) structural recovery
- Reads player entity FAR pointer from global `0x2de4`.
- Copies player world position fields (`+0x40`, `+0x42`) into globals `0x27e7` / `0x27e9` (AI focus position cache).
- Iterates entity IDs from `2` through `255` and dispatches per-entity processing through two sequential thunked calls per entity.
- After the NE far-call repair pass, the first call at `0007:101c` now decompiles directly as `entity_resolve_slot_ptr` (`0005:0466`) instead of `CALLF 0000:ffff`.
Repaired call chain helpers now exposed:
- `0005:42c8` = `entity_projected_bbox_overlaps_viewport` — projects entity slot via `world_to_screen_coords`, derives sprite/flag context, tests against the active viewport rectangle at 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.
Dispatch call sites annotated:
- `0007:101c`: `entity_slot_fetch(SS:&entity_id)` — resolves entity slot/pointer from loop ID
- `0007:1093`: `entity_tick_dispatch(SS:&entity_id, g_0x27c8)` — per-entity AI tick with global `0x27c8` mode/context word
Global `0x27c8` confirmed as the current targeted/current entity handle.
## Raw 0007 Gameplay Logic: animation / range / command globals
### `is_player_in_range` (`0007:0f79`)
- Prototype: `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.
### `entity_animation_frame_update` (`0007:26e2`)
- Prototype: `void entity_animation_frame_update(int *entity_ptr)`
- Key globals: `g_anim_tick_counter` (`0x3a00`), `g_anim_tick_overdrive_flag` (`0x3a02`), `g_speed_double_flag` (`0x27fd`).
- Entity struct fields confirmed:
- `[0x1b]` (byte `+0x36`) = frame_min; `[0x1c]` (byte `+0x38`) = frame_max; `[0x1d]` (byte `+0x3a`) = current_frame
- `[0x1e]` (byte `+0x3c`) = loop_flag (0 = animation disabled)
- `[0x1f]` (byte `+0x3e`) = reverse_direction_flag / double-speed flag
- `+0x3f` (word) = completion handle/sentinel (`-1` = none, `0x2802` = player entity)
- `+0x00` (far ptr) = vtable pointer
Disassembly comments added:
- `0007:27dc`: `entity_completion_callback(handle)` — fires when loop wraps; skips player handle
- `0007:27fd`: vtable indirect `entity->vtable[+8](entity, 0, 0)``on_loop_complete` virtual method
- `0007:281e`: `notify_frame_progress(handle, current_frame)` — per-frame notification
- `0007:2851`: `entity_sprite_advance(entity_far_ptr, advance_amount, 0)`
### `entity_command_dispatch` (`0007:0990`)
- 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 into `g_player_delta_x` (`0x27f5`) and `g_player_delta_y` (`0x27f7`).
- Clears cached origin globals `g_cmd_effect_origin_x` (`0x27f1`) and `g_cmd_effect_origin_y` (`0x27f3`) after use.
### Enemy spawn helper cluster
Existing raw names align with prior standalone seg001 notes:
- `0007:505d` = `map_find_spawn_point` (`seg001 + 0x6aed`)
- `0007:5259` = `enemy_spawn_with_target` (`seg001 + 0x6ce9`)
- `0007:5275` = `enemy_spawn_no_target` (`seg001 + 0x6d05`)
- `0007:5291` = `enemy_spawn_at_position` (`seg001 + 0x6d21`)
### Global map additions (renamed in Ghidra)
| Address | Name | Evidence |
|---------|------|---------|
| `0x27c8` | `g_current_entity_handle` | Compared directly by `entity_is_type_match`; captured by `entity_ai_update_loop`, `map_find_spawn_point`, and `enemy_spawn_at_position` |
| `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) |
---
## seg043 Standalone Boundary Recovery
Direct disassembly of `NE_segments/seg043_code_off_75A00_len_336F.bin` shows the first non-zero bytes at offset `0x0090`; offsets `0x0000..0x008f` are all zero.
First three clean 16-bit prologues:
- `seg043:0090` → raw `0007:5a90`
- `seg043:017a` → raw `0007:5b7a`
- `seg043:021c` → raw `0007:5c1c`
Repair status: applied in `CRUSADER-RAW.EXE` via the local PyGhidra toolkit:
- `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`
Verified behavior:
- `entity_set_at_target_update_facing` 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.
- `0007:5a90` 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`.
- `0007:5c1c` optionally calls a virtual method through `[object->vtable + 0x4c]` when `object+0x44/+0x46` is non-null, then dispatches one or two downstream far helpers using `object+0x48`.
Additional resolved call targets inside the missing seg043 block (from relocation data):
- `0007:5a8a``entity_set_event_type_checked`
- `0007:5a98``FUN_0008_cc01` (timer-related flag/event helper)
- `0007:5b36``entity_get_type_word`
- `0007:5b44``saveslot_read_entry_flags`
- `0007:5bb8``entity_is_type_match`
- `0007:5c49``entity_class_get_flag20`
- `0007:5c8b``mem_alloc_far`
## Additional Raw 0007 Helpers
### Entity Class Flag Helper
- `0006:02cc` = `entity_class_get_flag20` — Returns `((class_detail[type*0x79 + 0x59] & 0x20) >> 5)`.
### Animation Start Frame Helper
- `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`.
### Combat Helper
- `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
- `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
- `0007:8854` = `entity_set_active_flag` — Sets entity `+0x40 = 1` (active); increments global `0x2800`.
### Dispatch Table Lookup
- `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]`.