Crusader_Decomp/crusader_decompilation_notes.md
2026-03-19 16:39:57 +01:00

45 KiB
Raw Blame History

Crusader: No Remorse - Decompilation Notes

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.

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)
  • Decompiler comments were added on key raw-import projectile functions to preserve provenance for later passes.
  • Quick verification from current raw import:
    • entity_fire_weapon currently decompiles as a thin wrapper that calls projectile_init_vector.
    • fire_weapon_from_cursor still decompiles poorly in the raw import, but disassembly shows it begins by pushing cursor sprite/state data from the 0x27d6 area, consistent with the existing seg001 notes.

Raw 000e Parser Helper Cluster

  • A small helper cluster in the raw 000e: area now appears to implement a fixed-size CRLF record parser/table builder, likely used by startup/config or script-ish text data.
  • Newly renamed helpers:
    • 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:38f8 = record_parser_find_marker
  • Current behavior read from raw-import decompilation/disassembly:
    • 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_find_marker scans forward until an @ marker or end-of-data; optionally consumes the remaining length from the parser state.
    • Helper at 000e:39cc remains intentionally unnamed for now; disassembly shows it only activates when the current substring begins with @, then skips 7 bytes and dispatches through a thunk.

Raw 000e RIFF/Animation Cluster

The 000e: segment contains a RIFF/AVI streaming animation subsystem. Animation objects have a confirmed field layout (offsets relative to the object base pointer).

Animation object field map:

  • +0xb0 = active/valid flag
  • +0xb4, +0xb6, +0xb8, +0xba, +0xbc, +0xbe, +0xc0, +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 = some entry index (multiplied by 0x30 to reach per-entry data at +0x1c7)

RIFF format notes: Game uses standard RIFF/IFF: LIST and RIFF header magic (0x5453494c = "LIST", 0x46464952 = "RIFF"), "movi" FourCC subchunk for frames. Audio frames tagged "01wb" (0x62773130), video frames in 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

Unresolved callee:

  • 000e:053d000e:ffb0 (thin wrapper, ffb0 decompiles garbled due to overlapping instructions at 000f:0085/000f:0086). Likely handles video frame loading to pair with anim_load_audio_frame. Not renamed.

Constructor pattern (000e:2777, 000e:2860, 000e:2969): All three 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 (direct analysis):

  • 000e:223d = assert_alive_sentinel
  • 000e:2777 = animation_ctor_variant_a
  • 000e:2860 = animation_ctor_variant_b
  • 000e:2969 = animation_ctor_variant_c

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

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)

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.

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.

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.

NE Segment 1 Analysis — Game Logic Functions (seg001_code_off_37600_len_8400.bin)

This segment was imported as Raw Binary at base 0x0000, language x86:LE:16:Protected Mode. All 35+ identified functions 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
0x28000x2811 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
0x29100x2947 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 entity byte+1 vs cheat sequence at 0x2833 (counter 0x283d); on full match, toggles 0x844/0x6045 and spawns vtable 0x287b/0x2892

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+slot2; writes entity type table 0x7e1e[slot0x79+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
0x2f50x2f7 Special movement entity
0x595/0x597 Platform/elevator entities
0x31c/0x3220x327 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
  • +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

Next Steps

  1. NE Segment 1 imported and analyzed — all 58 identified functions renamed and annotated
  2. Import additional NE segments — priority: segments 22, 30, 59, 86 (segment 21 complete)
  3. Analyze additional segments — apply same decompile→rename→annotate workflow
  4. Map file format loaders.FLX, .SHP, .MAP, .TNT resource formats
  5. Cross-reference entity type constants with game entities (robots, platforms, triggers)
  6. Identify external segment calls — the func_0x0000ffff() placeholders are all cross-segment calls; resolving them requires importing the referenced segments

NE Segment 21 Analysis — Timer/Event Dispatch System

File: seg021_code_off_50200_len_4486.bin | File Offset: 0x50200 | Length: 0x4486 bytes Ghidra Load: RAM 0000:0000 0000:4485, x86 16-bit Protected Mode, base 0x0000 Functions: 88 total (87 renamed + input_keyboard_handler pre-existing)

Subsystem Summary

Segment 21 implements the hardware-level timer interrupt and entity event dispatch system — Crusader's real-time task scheduler. Key responsibilities:

  • Programs and services the Intel 8253 PIT timer (I/O ports 0x40/0x43)
  • Manages three entity dispatch lists: timer list (0x39d4), input list (0x39e3), render list (0x3a10)
  • Maintains the entity pool at 0x39b0 (same pool as seg001; these segments share DS)
  • Provides event queue (32-slot circular buffer at 0x31cc)
  • Handles save/load serialization of the entire entity system
  • Controls keyboard/interrupt locks and deferred scheduling

Function Groups

Entity Pool Management (0x02070x0483)

Address Name Notes
0x0207 entity_count_by_type_a Count entities matching type+event; filters DEAD flag (0x8)
0x0297 entity_count_by_type_b Identical logic to 0x0207 (compiler duplicate)
0x0327 entity_find_free_slot Scan pool for null entry; calls panic if full; returns slot or 0xFFFF
0x038f entity_register Write far ptr to entity_list, group to entity_data, vtable to registry; inc count
0x044d entity_get_ptr_raw Read entity far ptr from pool slot (may be null)
0x0483 entity_get_ptr Safe wrapper: verifies non-null, returns offset only

Event Dispatch (0x04f30x08be)

Address Name Notes
0x04f3 entity_dispatch_reset_all Fires event code 0x21 (reset/init) to all entities
0x050d entity_clear_deferred_flags Clears DEFERRED bit (0x200) from up to N=0x3998 entities
0x059e entity_fire_event_broadcast Dispatch event to all matching entities; calls vtable[6]; respects 0x200 deferred flag
0x06f4 entity_fire_event_type_include Fire only entities whose type IS in given list (up to 10, 0x0d=end)
0x08be entity_fire_event_type_exclude Fire only entities whose type is NOT in given list
0x0a8e input_keyboard_handler (pre-existing) OS-level key router: 0x0d=scroll+, 0x01=action, 0x2c=save, 0x44=load

Entity Iterator / Linker (0x0bb70x106b)

Address Name Notes
0x0bb7 entity_link Cross-link two entities; skips if flag 0x400 set
0x0c34 entity_find_first Init iterator 0x39fa=3; find first entity matching saved type/event at 0x399a/0x399c
0x0cec entity_find_next Continue iterator from 0x39fa cursor
0x0dad timer_entity_find_by_event Find entity handling event in range 0xf0-0xf7; checks bit 0x1000; writes to 0x3993
0x0e82 entity_find_by_priority Walk priority chain at 0x39d4; find entity matching source/event at 0x3993
0x0fc8 entity_set_cursor Validate flag 0x800; set cursor 0x3993 = param_1 (slot)
0x100c entity_get_cursor Return entity at 0x39bf if valid and not dead
0x106b entity_relink Re-link: find by event, walk priority chain, call set-link vtable funcs

Entity Lifecycle (0x11330x131d)

Address Name Notes
0x1133 entity_unregister Full removal: dec sprite type count, vtable cleanup, dec total, update masks
0x1202 entity_slot_clear Zero pool slot (0x39b0), registry slot (0x39ca), group data (0x39b4)
0x1245 entity_layer_set Write 0x39c9 (active layer ID) if changed; set dirty flag 0x39a2
0x125d entity_check_overdue If entity_is_overdue: set bit 0x40 on entity+0x16
0x127c entity_is_overdue Return 1 if entity index > 0x39bf and flag 0x39c2 set
0x129b entity_list_call_update For all entities where entity+0x0e & param_3 != 0: call vtable[8]
0x131d entity_set_pending Write param to 0x3995 (next entity to register); error if already set

Entity System Init/Shutdown (0x133e0x1705)

Address Name Notes
0x133e entity_system_init Alloc all entity pool buffers (see decompiler comment); init three lists; clear event state
0x14bc entity_system_flush_normal Finalize (vtable[10]) then free all non-deferred active entities
0x158d entity_system_flush_deferred Same as flush_normal for deferred entities
0x165c entity_process_pending_deletes Free entities marked DEAD (flag & 0x8); dec 0x399e counter
0x1705 entity_system_shutdown Full shutdown: flush normal, flush deferred, process deletes, free all pools

Save / Load (0x18510x1d21)

Address Name Notes
0x1851 event_queue_state_reset Zero ring buffer state tables (0x334e, 0x364e), queue ptrs (0x31c8/0x31ca)
0x18ce level_load Full level load: shutdown + reinit + deserialize all entities via vtable[12]
0x1d21 save_game Serialize entity system: arrays + each entity via vtable[14]; magic check 0x3a21==0xed

PIT Timer / Hardware (0x23000x2975)

Address Name Notes
0x2300 pit_timer_program OUT 0x43, 0x36; OUT 0x40, lo; OUT 0x40, hi — raw PIT channel 0 program
0x2316 pit_timer_set_hz Validates divisor <= 0xd688; stores at 0x39ce; calls pit_timer_program
0x23a5 pit_timer_tick_handler Timer ISR: iterates 0x39d4 timer list, fires vtable callbacks per layer/mode
0x25fc timer_entity_active Check 0x3987/0x398b for active timer entity (mode-dependent)
0x264c timer_entity_get_current Get ptr from 0x3987 or 0x398b based on 0x3991 mode flag
0x2668 timer_entity_enable Set ENABLED flag (0x400), inc counter, insert into timer list, reprograms PIT
0x2745 timer_entity_disable Clear ENABLED, dec counter, reprograms PIT; if list empty calls interrupt_request_cancel
0x2975 timer_recompute_hz Scan timer list; find smallest time_period (+0x38/+0x3a); call pit_timer_set_hz

Interrupt / Lock Control (0x283a0x294b)

Address Name Notes
0x283a interrupt_lock_acquire Re-entrant acquire on 0x31c7 (interrupt lock)
0x2870 interrupt_lock_release Release 0x31c7
0x289b entity_lock_acquire Re-entrant acquire on 0x39aa (entity system lock)
0x28d5 entity_lock_release Release 0x39aa
0x290d interrupt_request_schedule Set deferred IRQ flags 0x39ab and 0x398f (or 0x39a9 in sync mode)
0x294b interrupt_request_cancel Clear IRQ request flags

Timer Loop / Deferred State (0x2a5f0x2ad8)

Address Name Notes
0x2a5f timer_event_loop Main game loop: polls player tick counter at 0x2de4; busy-waits; fires optional callback; stores delta to 0x3a00/0x3a02
0x2ac2 timer_deferred_reschedule If deferred mode flag 0x39b8 set, call reschedule
0x2ad8 timer_snapshot_deferred Copy 0x39a9 → 0x39b8; call interrupt handler if 0x39a9 set

Event Queue (0x2c730x3364)

Address Name Notes
0x2c73 event_queue_drain Drain circular queue; call event_queue_dequeue while 0x31c8 != 0x31ca; reset state
0x2ca2 mouse_button_check Return 1 if BIOS 0x31a4 bit 0x10 set AND 0x39af (mouse enable) set
0x2cbc stub_noop_2cbc Empty stub function
0x2cd7 bios_keyboard_flags_write Write param to 0x400:0017 (BIOS keyboard flags at segment 0x40, offset 0x17)
0x2cf2 input_event_dispatch Dispatch display event 0x10 to input list entities with flag 0x100 and 0xc bits set
0x2dc3 event_queue_push Push event to circular queue (write ptr 0x31ca); calls event_queue_is_full check
0x3276 keyboard_state_read INT 16h AX=0: read raw keyboard state into 0x31a4
0x328b keyboard_acquire If not locked (0x31c6): INT lock, read keyboard, set lock flag
0x32cc keyboard_release If locked: unlock, clear 0x31c6
0x3304 event_queue_count Count pending events: 0x31ca - 0x31c8 (circular)
0x333d event_queue_is_full Return 1 if ((0x31ca+1) mod 32) == 0x31c8
0x3364 event_queue_dequeue Read from ring buffer (0x31cc + 0x31c8*0xc, entry size 0xc), advance read ptr

Event Subscription and Bitmask Helpers (0x34dd0x3878)

Address Name Notes
0x34dd event_queue_process_all Drain queue; for each event find listener entities in 0x39e3; call vtable[0x14]
0x35e9 event_queue_set_mode Write low 2 bits to 0x334c; call keyboard_interrupt_call
0x35fb event_queue_set_param Write low 5 bits to 0x334d; call keyboard_interrupt_call
0x360d keyboard_interrupt_call INT 16h (raw BIOS keyboard services call)
0x3630 entity_validate_indices Debug assert: verify entity+0x02 (slot_index) == pool position for all entities
0x369b typemask_set_bit Set bit at 0x3a04 + (param>>3), bit (param & 7) — entity type present bitmask
0x36d4 typemask_clear_bit Clear bit in 0x3a04 bitmask
0x370f typemask_update If entity type has listeners (entity_find_first != 0): set bit; else clear
0x3744 typemask_test_bit Test bit in 0x3a04; return 1 if entity type has registered listeners
0x377d event_subscription_set Set subscription bit in 0x3a08 buffer
0x37b2 event_subscription_clear Clear subscription bit in 0x3a08 buffer
0x37e9 event_subscription_update If entity has listeners: set bit; else clear (driven by entity_find_first result)
0x3825 event_subscription_test Test subscription bit in 0x3a08; return 1 if subscribed
0x3864 event_state_clear Zero entire 0x3a0c event use-count buffer (0x4000 bytes = 8192 uint16s)
0x3878 event_use_count_increment Increment 64-bit counter at 0x3a0c[entity_event_type*4]

Input / Render List (0x38c20x3ae9)

Address Name Notes
0x38c2 input_event_broadcast Dispatch input event 0x40 to all render-list entities with flag 0x40; uses counter 0x39ad
0x39a1 subscribe_to_render_list Add entity to 0x3a10 list; set flag bit 0x40; inc 0x3a1f
0x3a13 unsubscribe_from_render_list Remove entity from 0x3a10; clear bit 0x40; dec 0x3a1f
0x3404 subscribe_to_input_list Add entity to 0x39e3 list; check flag 0x100; set bit 0x80; inc 0x39c3
0x3477 unsubscribe_from_input_list Remove entity from 0x39e3; clear bit 0x80; dec 0x39c3
0x3a78 entity_lists_init Init three linked lists with sentinel vtable 0x3a89; write head vtable 0x2d10
0x3ae9 entity_lists_reset Call external reset + reinit 0x39e3 and 0x39d4 lists

Entity Object Field Layout (as used in Seg21)

Offset Field Type Description
+0x00 vtable_ptr far ptr Pointer to entity's vtable dispatch table
+0x02 slot_index uint16 Entity's own slot number in pool
+0x04 source_type uint16 Source/owner entity type (event matching)
+0x06 event_type uint16 Event type this entity handles
+0x08 flags_byte uint8 Low 5 bits = sprite group ID
+0x0e capability_mask uint16 Bitmask of supported event capabilities
+0x16 state_flags uint16 bit3=DEAD, bit8=REGISTERED, bit9=ACTIVE, bit10=ENABLED, bit11=HAS_TIMER, bit13=IS_IRQ_HANDLER
+0x18 flags2 uint16 bit6=IN_RENDER_LIST, bit7=IN_INPUT_LIST, bit9=DEFERRED
+0x1e priority_chain far ptr Priority chain entries (entity_find_by_priority)
+0x20 priority_count uint16 Count of priority chain entries
+0x38 time_period_lo uint16 Timer period low word (PIT frequency calc)
+0x3a time_period_hi uint16 Timer period high word

Vtable Layout (Seg21 usage)

Slot Byte offset Prototype Purpose
[6] +0x0c handle_event(entity, CS, type, param) Event callback
[8] +0x10 update(entity, CS, capability_mask) Per-tick update
[10] +0x14 finalize(entity, CS) Cleanup/shutdown
[12] +0x18 load(entity, CS, file_ptr, CS) Deserialize from save
[14] +0x1c save(entity, CS, file_ptr, CS) Serialize to save
[16] +0x20 set_backref(entity, CS, list_ptr) Set back-reference
[20] +0x28 dispatch_callback(entity, CS, event_id, 0, data_ptr) Generic dispatch

Key Global Data (Seg21 — additions to DS)

Address Name Description
0x31a4 bios_key_state Raw INT 16h keyboard state
0x31c6 keyboard_lock Keyboard acquired flag
0x31c7 interrupt_lock Interrupt lock flag (re-entrant)
0x31c8 queue_read_ptr Event queue read index (031)
0x31ca queue_write_ptr Event queue write index
0x31cc event_queue_base Ring buffer, 32 entries × 0xc bytes
0x334c queue_mode Event queue mode bits (01)
0x334d queue_param Event queue param bits (04)
0x39b0 entity_list Far ptr to entity far-ptr array (count×4) — shared with seg001
0x39b4 entity_data Far ptr to group/sprite-ID array (count×2)
0x39b9 entity_max_count Max capacity of entity pool
0x39bb entity_count Total registered entity count
0x39c9 active_layer Current active entity layer/group ID
0x39ca entity_registry Far ptr to vtable dispatch array (count×4) — shared with seg001
0x39ce pit_divisor Current PIT timer divisor
0x39d4 timer_list Intrusive linked list: timer-dispatch entities
0x39e3 input_list Intrusive linked list: input-handler entities
0x3a04 typemask_buf Far ptr to entity type present bitmask (0x480 bytes)
0x3a08 evt_sub_buf Far ptr to event subscription bitmask (0x2400 bytes)
0x3a0c evt_state_buf Far ptr to event use-count table (0x4000 bytes)
0x3a10 render_list Intrusive linked list: render-callback entities
0x3a21 save_magic Must be 0xed (-0x13) for valid save
0x3a70 default_registry_vtable Default vtable written to entity_registry slots on register
0x3a89 list_sentinel_vtable Sentinel vtable written to list head nodes