# 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]`.