Crusader_Decomp/docs/raw-porting-progress.md
MaddoScientisto 3daffbf113 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.
2026-03-22 14:27:38 +01:00

20 KiB

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_1entity_type
  • local_48snap_entity_type_table
  • local_34snap_dispatch_seg_table
  • local_20snap_dispatch_off_table
  • local_centity_type_cursor
  • local_4dispatch_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)

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:5a8aentity_set_event_type_checked
  • 0007:5a98FUN_0008_cc01 (timer-related flag/event helper)
  • 0007:5b36entity_get_type_word
  • 0007:5b44saveslot_read_entry_flags
  • 0007:5bb8entity_is_type_match
  • 0007:5c49entity_class_get_flag20
  • 0007:5c8bmem_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].