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

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