Add various scripts and JSON plans for Ghidra project
- Introduced `seg043_boundary_repair.json` to manage function boundaries in segment 043. - Created `read_file.py` for reading and printing file content size. - Added `resolve_bb4f.py` to resolve specific function call targets. - Implemented `resolve_top_targets.py` to find resolved NE targets for top-called wrapper functions. - Added `script_contents.txt` to summarize NE relocation far calls. - Updated `tier4_ghidra.txt`, `tier4_ghidra_check.txt`, `tier4_output.txt`, and `tier4_result.txt` with function call statistics. - Created `tier5_errors.txt` for error logging and `tier5_output.txt` for additional function call statistics. - Established `tools` directory with helper scripts for the Ghidra project, including CLI and common functionalities. - Implemented command-line interface in `cli.py` for various project operations. - Added `common.py` for shared functions and configurations across tools. - Introduced `validate_fixups.py` to validate NE relocation fixups against known addresses.
This commit is contained in:
parent
6b9eb205d4
commit
24d4416003
36 changed files with 145712 additions and 14 deletions
|
|
@ -30,6 +30,33 @@
|
|||
- Naming note:
|
||||
- `seg001` and `seg021` both contain a keyboard handler; in the full program database, the seg001 copy is named `seg001_input_keyboard_handler` to avoid a symbol collision with seg021 `input_keyboard_handler`.
|
||||
|
||||
### Address Space Layout in the Raw Import
|
||||
|
||||
Ghidra segment:offset `SSSS:OOOO` = flat address `SSSS * 0x10000 + OOOO`.
|
||||
|
||||
| Flat range | Content |
|
||||
|---|---|
|
||||
| `0x00000`–`0x36F6F` | Phar Lap 286 DOS extender (outer MZ stub code) |
|
||||
| `0x36F70` | NE header (145-segment game image begins here in file) |
|
||||
| `0x6E570`+ | NE game segments at their Phar Lap linear load addresses |
|
||||
|
||||
Mapping rule (verified for seg001 and seg021):
|
||||
```
|
||||
runtime_flat_base = NE_segment_file_offset + 0x36F70
|
||||
```
|
||||
Example: seg004 at file `0x40A00` → runtime `0x77970` → Ghidra `0007:7970`.
|
||||
|
||||
Functions at Ghidra `0003:XXXX` / `0004:XXXX` are **Phar Lap extender code** (flat < `0x40000` is below any game segment). Functions at `0006:E570`+ are game NE segments.
|
||||
|
||||
### `0000:ffff` — NE Fixup Placeholder (not a dispatcher)
|
||||
|
||||
`unresolved_far_thunk_dispatch` at `0000:ffff` is NOT a runtime function. Every `CALLF 0x0000:ffff` in the binary is a **different** external or inter-segment call patched by the NE loader at runtime. The decompiler body is garbled (it reads NE fixup-chain sentinel data). Decompiler comment added in Ghidra. See individual call sites for per-site behavioral annotations.
|
||||
|
||||
Known call-site classifications (by argument pattern):
|
||||
- `PUSH DS; PUSH imm_ordinal; CALLF` — Phar Lap extender calling a runtime-imported procedure by ordinal
|
||||
- `PUSH ptr_seg; PUSH ptr_off; CALLF` — inter-NE-segment function call (intra-game far call)
|
||||
- Multiple typed pushes then CALLF — external C runtime / game subsystem call with normal args
|
||||
|
||||
### Latest Raw Full-EXE Porting Progress
|
||||
|
||||
- Newly ported and renamed into `CRUSADER-RAW.EXE` from verified `seg001` mapping (`base 0x6E570`):
|
||||
|
|
@ -57,7 +84,9 @@
|
|||
- Current 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 enters the existing unresolved thunk path (`0000:ffff`).
|
||||
- `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 at `0007:8666` as `entity_tile_type_notify(tile_index_ptr, type_byte)`.
|
||||
- New entity field found this pass:
|
||||
- `entity[+0x57]` (byte) = entity type/class byte (passed to tile-type notification; meaning not yet fully established — adjacent to named fields `+0x54`/`+0x55`)
|
||||
|
||||
### Raw 0007 Gameplay Helper Batch (facing/direction)
|
||||
|
||||
|
|
@ -125,9 +154,25 @@ void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
|
|||
}
|
||||
```
|
||||
|
||||
#### Next RE target (to close remaining uncertainty)
|
||||
#### Architectural Resolution: `unresolved_far_thunk_dispatch` / `0000:ffff`
|
||||
|
||||
- Recover the true callee behind `0000:ffff` for the `0007:224b` call site by relocation/import-table reconstruction or by matching this call path in a cleaner segment-mapped database. That should reveal exact per-slot use of the two dispatch tables and final coordinate math.
|
||||
**`unresolved_far_thunk_dispatch` is NOT a real dispatcher.** It is the NE binary fixup placeholder.
|
||||
|
||||
- In a Phar Lap 286 NE executable, inter-segment and external far calls are stored in the binary as `CALLF 0x0000:ffff` (or similar invalid sentinel values).
|
||||
- The Phar Lap NE loader patches each of these call sites to the real segment:offset at load time using the per-segment relocation records in the NE file.
|
||||
- In Ghidra's raw import, those fixups are never applied. Every unresolved far call collapses to the same `0000:ffff` stub, where the decompiler produces garbled output (it's reading fixup-chain data, not real instructions).
|
||||
- **Each `CALLF 0x0000:ffff` in the binary is a DIFFERENT call with a DIFFERENT actual target.** Identifying the target requires either parsing the NE relocation table or cross-matching with the resolved standalone segment extracts.
|
||||
|
||||
Address layout in the raw import (flat_address = `SSSS:OOOO` where flat = `SSSS * 0x10000 + OOOO`):
|
||||
- `0000:` – `0003:` (flat < `0x40000`) = Phar Lap 286 DOS extender code (the outer MZ stub portion)
|
||||
- `0006:E570` onwards = NE game segments (seg001+ at their Phar Lap-assigned linear addresses)
|
||||
- Mapping rule verified: `runtime_flat = NE_segment_file_offset + 0x36F70` (the NE header offset in the EXE)
|
||||
|
||||
Decompiler comment added to `0000:ffff` in Ghidra documenting this.
|
||||
|
||||
#### Next RE targets for `snap_entity_to_ground`
|
||||
|
||||
- The `0007:224b` thunk call is an intra-NE inter-segment call (calling into a different game segment with ground-aligned coordinate math). Identifying it requires the NE relocation table or matching the disassembly in the standalone extracts.
|
||||
|
||||
### Raw 0007 Gameplay Helper Follow-up: AI sweep + checked spawn path
|
||||
|
||||
|
|
@ -150,8 +195,11 @@ void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
|
|||
- Added disassembly + decompiler comments capturing stable behavior:
|
||||
- Reads player entity FAR pointer from global `0x2de4`.
|
||||
- Copies player world position fields (`+0x40`, `+0x42`) into globals `0x27e7` / `0x27e9` (AI focus position cache used by downstream logic).
|
||||
- Iterates entity IDs from `2` through `255` and dispatches per-entity processing through the shared thunk path.
|
||||
- This function now has enough recovered semantics to treat it as the frame-level AI sweep dispatcher even though individual thunked callees remain unresolved in the raw import.
|
||||
- Iterates entity IDs from `2` through `255` and dispatches per-entity processing through two sequential thunked calls per entity.
|
||||
- New disassembly comments added at both dispatch call sites:
|
||||
- `0007:101c`: `entity_slot_fetch(SS:&entity_id)` — first call; resolves entity slot/pointer from loop ID
|
||||
- `0007:1093`: `entity_tick_dispatch(SS:&entity_id, g_0x27c8)` — second call; per-entity AI tick with global `0x27c8` mode/context word
|
||||
- Global `0x27c8` is now confirmed as the current targeted/current entity handle: `entity_is_type_match` compares against it directly, and both spawn helpers `map_find_spawn_point` / `enemy_spawn_at_position` snapshot it before their thunked core paths.
|
||||
|
||||
### Raw 0007 Gameplay Logic: animation / range / command globals
|
||||
|
||||
|
|
@ -172,14 +220,18 @@ void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
|
|||
- `g_speed_double_flag` (`0x27fd`) — doubles speed_factor to 2 when set (fast game mode).
|
||||
- Local variables renamed: `speed_factor` (1 or 2) and `advance_steps` (0–4, number of frame advances this tick).
|
||||
- Entity struct fields confirmed (relative to `entity_ptr` as `int*`):
|
||||
- `[0x1b]` = frame_min (backward direction counter)
|
||||
- `[0x1c]` = frame_max
|
||||
- `[0x1d]` = current_frame
|
||||
- `[0x1e]` = loop_flag
|
||||
- `[0x1f]` = reverse_direction_flag
|
||||
- `+0x3f` (as `char*`) = completion handle/sentinel (`-1` = none, `0x2802` = player entity)
|
||||
- On frame overflow: if completion handle valid and not player-entity, fires thunked event; calls vtable `[+8]` method.
|
||||
- Added decompiler comment at function entry explaining all fields and behavior.
|
||||
- `[0x1b]` (byte `+0x36`) = frame_min (backward direction counter)
|
||||
- `[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, byte-offset) = completion handle/sentinel (`-1` = none, `0x2802` = player entity)
|
||||
- `+0x00` (far ptr) = vtable pointer
|
||||
- New disassembly comments added at all three `CALLF 0x0000:ffff` sites and the vtable indirect call:
|
||||
- `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)` — core frame-advance call; advance_amount = `entity[+0x3c] * (steps+1) * speed_factor`
|
||||
|
||||
#### `entity_command_dispatch` (`0007:0990`) — partially decompiled
|
||||
|
||||
|
|
@ -191,10 +243,24 @@ void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
|
|||
- Dispatches entity command through shared thunk; actual command table data not yet resolved.
|
||||
- No incoming XREFs found in the raw import (likely called via table or vtable dispatch).
|
||||
|
||||
#### Enemy spawn helper cluster (`0007:505d`, `0007:5259`, `0007:5275`, `0007:5291`)
|
||||
|
||||
- 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`)
|
||||
- Current verified raw-import behavior:
|
||||
- `enemy_spawn_with_target` is a thin wrapper over `enemy_spawn_at_position(..., target_player_flag = 1)`.
|
||||
- `enemy_spawn_no_target` is the same wrapper but passes `target_player_flag = 0`.
|
||||
- `map_find_spawn_point` and `enemy_spawn_at_position` both copy DS:`0x27c8` into locals before entering their unresolved thunk body, matching the standalone notes that treat `0x27c8` as the current targeted/current entity handle.
|
||||
- Short decompiler comments were added in Ghidra on the raw spawn helpers to preserve this provenance.
|
||||
|
||||
#### Global map additions (renamed in Ghidra)
|
||||
|
||||
| Address | Name | Evidence |
|
||||
|---------|------|---------|
|
||||
| `0x27c8` | `g_current_entity_handle` | Compared directly by `entity_is_type_match`; also captured by `entity_ai_update_loop`, `map_find_spawn_point`, and `enemy_spawn_at_position` as the current targeted/current entity handle |
|
||||
| `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` |
|
||||
|
|
@ -219,12 +285,14 @@ void snap_entity_to_ground(entity_type, spawn_x, spawn_y, spawn_layer) {
|
|||
- `000e:35ef` = `record_table_next_slot`
|
||||
- `000e:3639` = `record_table_parse_buffer`
|
||||
- `000e:3798` = `record_parser_read_line`
|
||||
- `000e:38a0` = `record_parser_seek_next_marker`
|
||||
- `000e:38f8` = `record_parser_find_marker`
|
||||
- `000e:39cc` = `record_parser_dispatch_at_directive`
|
||||
- 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_seek_next_marker` updates the parser's current marker cursor at `+0x18/+0x1a` by calling `record_parser_find_marker`; returns 1 if another marker was found, 0 at end-of-data.
|
||||
- `record_parser_find_marker` scans forward until an `@` marker or end-of-data; optionally consumes the remaining length from the parser state.
|
||||
- `record_parser_dispatch_at_directive` returns `0` unless the current substring begins with `@`; in the `@` case, it advances by 7 bytes and dispatches through a FAR thunk (`0000:ffff`).
|
||||
|
||||
|
|
@ -758,7 +826,23 @@ A scroll/camera management cluster found in the `0007:bxxx–0007:dxxx` range.
|
|||
|
||||
| Address | Name | Evidence |
|
||||
|---------|------|---------|
|
||||
| `0007:5b6f` | `entity_set_at_target_update_facing` | Sets entity `+0x3a = 1` (arrived flag); calls `entity_set_facing_direction`; clears bit `0x10` from entity type table `0x7e1e[type*0x79+0x59]`; tail-calls thunk to advance state. Called in the entity state machine context. |
|
||||
| `0007:5b6f` | `entity_set_at_target_update_facing` *(likely internal block, not true top-level function)* | Direct raw-analysis name from the visible local behavior: sets entity `+0x3a = 1` (arrived flag); calls `entity_set_facing_direction`; clears bit `0x10` from entity type table `0x7e1e[type*0x79+0x59]`; then tail-calls onward. Relocation data places it at `seg043:016f`, and resolved call sites exist immediately before/after it (`5b36`, `5b44`, `5bb9`), so this address is likely an internal labeled block inside the larger missing `0007:5a00` seg043 function rather than a true entrypoint. |
|
||||
|
||||
### 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 in the standalone extract.
|
||||
- The first three clean 16-bit prologues in seg043 are at:
|
||||
- `seg043:0090` -> raw `0007:5a90`
|
||||
- `seg043:017a` -> raw `0007:5b7a`
|
||||
- `seg043:021c` -> raw `0007:5c1c`
|
||||
- The first recovered standalone function spans `0x0090..0x0179`, which means the current raw label at `0007:5b6f` falls inside the tail of that routine and overlaps the true return at raw `0007:5b79`.
|
||||
- Practical consequence: the missing raw `0007:5a00` seg043 function boundary should not start at segment offset `0x0000`, and the current `0007:5b6f` function object should be treated as a mis-split internal block until Ghidra-side function creation/repair is available.
|
||||
|
||||
### Entity Class Flag Helper
|
||||
|
||||
| Address | Name | Evidence |
|
||||
|---------|------|---------|
|
||||
| `0006:02cc` | `entity_class_get_flag20` | Returns `((class_detail[type*0x79 + 0x59] & 0x20) >> 5)`. Conservative raw-analysis name; bit meaning still unknown, so the helper is named after the observed flag mask rather than a guessed behavior. |
|
||||
|
||||
### Animation Start Frame Helper
|
||||
|
||||
|
|
@ -1213,6 +1297,278 @@ Globals: `[0x63da]` = mouse button state, `[0x63d6]/[0x63d8]` = cursor X/Y, `[0x
|
|||
| Address | Name | Evidence |
|
||||
|---------|------|---------|
|
||||
| `000c:dac1` | `cursor_nav_state_reset` | Zeros all directional/button flags; sets `[+0x32/+0x33]=0xff`, `[+0x47]=0xffff` |
|
||||
|
||||
## Top-40 Most-Called Far-Call Targets (NE Fixup Resolution)
|
||||
|
||||
Named via systematic analysis of 11,692 NE relocation fixup entries. These are the functions most frequently called through the `CALLF 0x0000:ffff` thunk mechanism.
|
||||
|
||||
### Tier 1: Top 20 (73+ callers)
|
||||
|
||||
| Rank | Address | Name | Calls | Description |
|
||||
|------|---------|------|-------|-------------|
|
||||
| 1 | `000a:44fd` | *(no function in Ghidra)* | 331 | Analysis gap at seg091:00fd. In comutils.c segment near joystick code. Needs manual function creation. |
|
||||
| 2 | `0003:ac7e` | `mem_alloc` | 272 | Allocation wrapper → seg082:0000 (`0009:a200`) |
|
||||
| 3 | `0008:dbec` | `entity_word_list_destroy` | 238 | Already named. Frees entity word-list buffer. |
|
||||
| 4 | `0003:a751` | `mem_free` | 207 | Free wrapper → seg082:007a (`0009:a27a` = `mem_free_checked`) |
|
||||
| 5 | `0008:bb4f` | `mem_alloc_far` | 174 | Thin wrapper → `mem_alloc` |
|
||||
| 6 | `0003:a897` | `far_memcpy` | 165 | REP MOVSW + trailing MOVSB |
|
||||
| 7 | `0005:088f` | `entity_get_type_word` | 130 | Returns type word from table 0x7df9 indexed by slot |
|
||||
| 8 | `000b:358d` | `sprite_tree_accumulate_pos` | 122 | Recursively sums X/Y offsets (+0x21/+0x23) through linked child nodes (+0x19/+0x1b), copies 8-byte position block via far_memcpy |
|
||||
| 9 | `0008:ce3d` | `entity_call_two_vtables` | 118 | Calls vtable[+4] at entity+0x1e and +0x28 |
|
||||
| 10 | `0004:26cd` | `nop_void_stub` | 118 | Empty function, returns void |
|
||||
| 11 | `0008:ce00` | `entity_call_two_vtables_base` | 117 | Calls vtable[0] at entity+0x1e and +0x28 |
|
||||
| 12 | `0008:bb8c` | `entity_check_flag_0x4000` | 115 | Short-circuits if flag 0x4000 set at +0x16 |
|
||||
| 13 | `0008:cda7` | `entity_free_both_word_lists` | 115 | Frees word lists at entity+0x1e and +0x28 if optional pointers at +0x24/+0x26 and +0x2e/+0x30 non-null. Both call `entity_word_list_free_existing`. |
|
||||
| 14 | `0004:26d2` | `nop_void_stub_b` | 111 | Empty function, returns void |
|
||||
| 15 | `000a:45fe` | `runtime_init_or_abort` | 108 | Reentrancy-guarded init. Flag at 0x44a4; flushes via FUN_000a_4a56, then calls `crt_exit_wrapper(1)`. Hidden code gap 0x4616-0x4643. |
|
||||
| 16 | `0004:3324` | `nop_return_zero` | 95 | Returns 0 |
|
||||
| 17 | `0009:c563` | `event_queue_push` | 82 | Circular buffer enqueue. Ring index (+0xe) masked 0x3f, slot masked 0xfff8. Writes event type word + data byte pair. |
|
||||
| 18 | `0005:c448` | `list_remove_and_free` | 74 | Unlinks node from linked list via FUN_0005_c495, optionally calls `mem_free` if bit 0 of flags set |
|
||||
| 19 | `000b:2e00` | *(no function in Ghidra)* | 74 | Analysis gap at seg109:0000. Needs manual function creation. |
|
||||
| 20 | `0009:1f12` | `dos_file_lseek` | 73 | DOS LSEEK (INT 21h AH=42h) wrapper with error reporting to 0x867a |
|
||||
|
||||
### Tier 2: Ranks 21-40 (56-73 callers)
|
||||
|
||||
| Rank | Address | Name | Calls | Description |
|
||||
|------|---------|------|-------|-------------|
|
||||
| 21 | `0009:3600` | `rotating_buffer_advance` | 73 | Advances 5-slot circular counter at 0x3eb6, zeros pointer in table at 0x867c, dispatches via jump table |
|
||||
| 22 | `0009:943a` | `entity_rect_compare_and_dispatch` | 68 | Compares bounding rectangles of two entities, dispatches based on flag bits 4/2/1 at +0x16 |
|
||||
| 23 | `0009:1e61` | `dos_file_close` | 65 | DOS file close (INT 21h), error reporting, sets handle to -1 |
|
||||
| 24 | `0005:e252` | *(unnamed — unclear)* | 65 | Copies 11 words from Phar Lap extender area (FUN_0000_12c6+5), then calls thunk. Interrupt/trampoline setup? |
|
||||
| 25 | `0003:dbcc` | `crt_format_string` | 64 | MetaWare High C formatting wrapper. Calls FUN_0003_bb92 with runtime format dispatch table. |
|
||||
| 26 | `0007:5a00` | *(no function in Ghidra)* | 64 | High-traffic raw target at `seg043:0000`. Earlier `debris_spawn` / seg001 mapping was rejected after checking relocation labels. Still needs manual function creation and direct analysis. |
|
||||
| 27 | `000a:4742` | `assert_buffer_valid` | 63 | Validates handle: asserts param_2 == cookie at 0x45a6 and param_1 < limit at 0x87e0 |
|
||||
| 28 | `0009:9216` | `entity_conditional_render_dispatch` | 63 | Checks entity flag bits 4 and 1 at +0x16, dispatches to vtable[+0xc] or thunk |
|
||||
| 29 | `0008:cb2c` | `entity_flag20_clear_and_update_target` | 61 | *(already named)* Clears flag bit 0x20, writes target +0x12/+0x14, calls refresh |
|
||||
| 30 | `0008:cb5c` | `entity_flag20_set_and_init_target` | 61 | *(already named)* Sets flag bit 0x20, inits target if zero, calls refresh |
|
||||
| 31 | `0007:7306` | `entity_create_stack_object` | 58 | Allocates 0xCC bytes on stack, inits via `object_init_zero_fields` (0005:c400), calls thunk |
|
||||
| 32 | `0007:8709` | `entity_mark_dirty_and_sync_tile_aux` | 58 | *(already named)* Syncs tile aux, sets flag bit 0x04 at +0x42 |
|
||||
| 33 | `0007:87c5` | `entity_set_flag20_from_field42` | 58 | Reads entity+0x42/+0x44, calls `entity_flag20_set_and_init_target` with those values |
|
||||
| 34 | `0007:8508` | `entity_table_lookup_and_dispatch` | 58 | *(already named)* Searches table at 0x2b46, dispatches via indirect jump |
|
||||
| 35 | `0007:8920` | `entity_call_vtable_slot0c` | 58 | *(already named)* Calls vtable entry at +0x0c |
|
||||
| 36 | `000a:b988` | `sprite_node_get_or_traverse` | 57 | If child pointer at +0x19/+0x1b non-null, traverses; otherwise returns leaf value |
|
||||
| 37 | `0003:a98b` | `crt_signed_div32` | 56 | Entry: adjusts near→far stack, sets CX=0 (signed quotient), jumps to `crt_div32_impl` |
|
||||
| 38 | `000a:7b44` | `nop_return_void_a` | 56 | Empty function (default vtable slot?) |
|
||||
| 39 | `000a:7b49` | `nop_return_void_b` | 56 | Empty function (default vtable slot?) |
|
||||
| 40 | `000a:7b53` | `nop_return_void_c` | 56 | Empty function (default vtable slot?) |
|
||||
|
||||
### Supporting Functions Discovered
|
||||
|
||||
| Address | Name | Description |
|
||||
|---------|------|-------------|
|
||||
| `000b:3a00` | `sprite_tree_sum_x_offset` | Recursive: sums field +0x21 through child chain +0x19/+0x1b |
|
||||
| `000b:3a35` | `sprite_tree_sum_y_offset` | Recursive: sums field +0x23 through child chain +0x19/+0x1b |
|
||||
| `0003:a845` | `crt_exit_wrapper` | Calls `crt_exit_impl(param,0,0)` |
|
||||
| `0003:a7ee` | `crt_exit_impl` | Full C exit: atexit handlers, stdio flush, MetaWare runtime cleanup |
|
||||
| `0003:a9a8` | `crt_div32_impl` | 32-bit division core. CX flags: bit0=unsigned, bit1=modulo, bit2=negate |
|
||||
| `0005:c400` | `object_init_zero_fields` | Zeros fields +0x25, +0x29, +0x31, +0x32 of a struct. Returns pointer. |
|
||||
| `000a:4440` | `joystick_read_axes_and_buttons` | Reads PC game port 0x201. Times axis responses, reads button nibble to 0x44a2 |
|
||||
| `000b:3380` | `sprite_node_is_dirty` | Checks flags at obj+0x29 & 3 == 1 or 3 → returns bool |
|
||||
| `000b:33a6` | `sprite_node_mark_dirty` | If not dirty, calls FUN_000b_3965 with mode=3 to invalidate |
|
||||
|
||||
### Tier 3: Ranks 41-60 (42-56 callers)
|
||||
|
||||
| Rank | Address | Name | Calls | Description |
|
||||
|------|---------|------|-------|-------------|
|
||||
| 41 | `000a:7b58` | `nop_return_zero_b` | 56 | Returns 0 (default vtable slot) |
|
||||
| 42 | `000b:3ab2` | `sprite_node_dispatch_event` | 56 | Large event dispatch: checks event type (2/4/8/0x100), updates global focus ptr at [0x4fd0:4fd2], dispatches via vtable methods [+0x14/+0x18/+0x20/+0x24] by event code. Switch table for 16 event types. |
|
||||
| 43 | `000a:48ff` | *(no function in Ghidra)* | 55 | Analysis gap in comutils.c segment |
|
||||
| 44 | `000b:3362` | `sprite_tree_unwind_check` | 55 | Validates SS == param_2 (stack segment guard), then decrements global counter at [0x4fd6] |
|
||||
| 45 | `000b:40ee` | `sprite_node_update_and_dispatch` | 55 | If `sprite_node_is_dirty` returns false: marks dirty, calcs accumulated bounds via `sprite_tree_get_accumulated_bounds` (3ed8), then dispatches via thunk |
|
||||
| 46 | `000a:7b5f` | `vtable_stub_trampoline` | 55 | Calls through fixup thunk (forwarder to another function) |
|
||||
| 47 | `000a:7b78` | `nop_return_void_e` | 55 | Empty function (default vtable slot) |
|
||||
| 48 | `000a:7b7d` | `nop_return_void_f` | 55 | Empty function (default vtable slot) |
|
||||
| 49 | `000a:7b4e` | `nop_return_void_d` | 54 | Empty function (default vtable slot) |
|
||||
| 50 | `000b:330c` | `sprite_tree_dispatch_wrapper` | 52 | Pure thunk wrapper: calls through fixup |
|
||||
| 51 | `0009:2034` | `dos_file_seek` | 51 | INT 21h AH=42h (LSEEK). Takes file object ptr, extracts handle at obj+4, seeks to offset param. Error reporting to [0x867a]. |
|
||||
| 52 | `0005:0466` | `entity_resolve_slot_ptr` | 50 | *(already named)* |
|
||||
| 53 | `0003:a880` | *(no function in Ghidra)* | 49 | Analysis gap in CRT segment |
|
||||
| 54 | `0006:170c` | `tile_class_get_byte` | 47 | Looks up class data: indexes into table at [0x7e1e] by (*param_1 * 0x79), returns byte at offset +0xc |
|
||||
| 55 | `000b:4097` | `sprite_dispatch_with_event` | 45 | Pushes event params + global [0x49c2:0x49c4], calls thunk |
|
||||
| 56 | `0005:02c1` | `entity_is_type_match` | 43 | Compares *param_1 against global at [0x27c8], returns 1 if equal, 0 otherwise |
|
||||
| 57 | `0003:ad75` | *(no function in Ghidra)* | 43 | Analysis gap in CRT segment |
|
||||
| 58 | `000a:e709` | `render_dispatch_by_flag` | 43 | Dispatches between two thunk paths based on boolean flag at stack+0x10 |
|
||||
| 59 | `0003:d0ff` | `crt_sprintf_wrapper` | 42 | Calls FUN_0003_bb92 (format engine) with rearranged params and string constant at 0x67ac |
|
||||
| 60 | `000b:326e` | `sprite_node_destroy` | 42 | Destructor: sets vtable ptr to 0x501a, clears global [0x4fd0:4fd2] if self, releases child nodes, calls mem_free via thunk |
|
||||
|
||||
### Updated Analysis Gaps
|
||||
|
||||
`0007:5a00` / `0007:5b6f` reconciliation:
|
||||
- The earlier standalone seg001 port hypothesis in this subrange was wrong.
|
||||
- Relocation data places raw `0007:5a00` at `seg043:0000`, and the already-named helper at `0007:5b6f` sits at `seg043:016f`.
|
||||
- Because of that segment placement, standalone seg001 names such as `debris_spawn` (`0x7490`) and `entity_die` (`0x75ff`) should NOT be ported into this raw range.
|
||||
- `0007:5b6f` currently remains `entity_set_at_target_update_facing` from direct raw analysis; its behavioral name is no longer in conflict with the standalone seg001 `entity_die` note.
|
||||
- Additional resolved call targets inside the missing seg043 block were annotated in Ghidra from relocation data:
|
||||
- `0007:5a8a` -> `entity_set_event_type_checked`
|
||||
- `0007:5a98` -> `FUN_0008_cc01` (timer-related flag/event helper; tests `+0x16 & 0x2`, sets `+0x16 |= 0x800`, copies event field `+0x06` to `+0x22`, checks `0x1000`, then conditionally dispatches)
|
||||
- `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`
|
||||
- Current boundary caveat:
|
||||
- Ghidra likely split the real seg043 routine incorrectly. `0007:5b6f` has no inbound xrefs, while relocation-resolved calls exist on both sides of it inside the same segment window. Treat the current `0007:5b6f` label as a behavioral anchor for one internal block, not yet as a proven standalone function boundary.
|
||||
- Standalone seg043 disassembly now strengthens that conclusion: real prologues are at raw `0007:5a90`, `0007:5b7a`, and `0007:5c1c`, so the current `0007:5b6f` boundary demonstrably overlaps an earlier function.
|
||||
|
||||
| Address | NE Segment | Callers | Notes |
|
||||
|---------|-----------|---------|-------|
|
||||
| `000a:44fd` | seg091:00fd | 331 | #1 most-called target! In comutils.c segment. |
|
||||
| `000b:2e00` | seg109:0000 | 74 | Start of segment 109. |
|
||||
| `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. |
|
||||
| `000a:48ff` | seg091:04ff | 55 | In comutils.c segment near joystick code. |
|
||||
| `0003:a880` | seg005:0880 | 49 | In CRT segment near `far_memcpy`. |
|
||||
| `0003:ad75` | seg005:0d75 | 43 | In CRT segment near `mem_alloc`. |
|
||||
| `000a:454d` | seg091:014d | 32 | In comutils.c segment. |
|
||||
|
||||
### Tier 4: Ranks 61-80 (29-42 callers)
|
||||
|
||||
| Rank | Address | Name | Calls | Description |
|
||||
|------|---------|------|-------|-------------|
|
||||
| 61 | `000b:30a5` | `sprite_tree_forward_wrapper` | 42 | Pure thunk forwarder |
|
||||
| 62 | `0008:bc27` | `entity_set_event_type_checked` | 41 | *(pre-existing name)* Sets event code at +0x06 with range/timer checks |
|
||||
| 63 | `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | 40 | *(pre-existing name)* Constructor: alloc 0x40, vtbl 3AA6, flag 0x200 |
|
||||
| 64 | `0005:1565` | `entity_action_by_type_dispatch` | 39 | Checks entity type against whitelist (0x432,0x5a0,0x1fd,0x1fe,0x8f,0x59f,0x2b3,0x2ca), dispatches by flags at [0xc76] and [0x85f] |
|
||||
| 65 | `0008:4bba` | `channel_slot_enable` | 39 | Sets enable byte=1 in 5-slot table at 0x84ca (slot * 0xd stride) |
|
||||
| 66 | `0009:6f5a` | `vga_palette_write` | 38 | Writes RGB triplets to VGA DAC (port 0x3C8/0x3C9). Range param_2..param_3 from palette data at *param_1 |
|
||||
| 67 | `0009:8ef6` | `line_draw_dispatch` | 38 | Compares abs(dx) vs abs(dy) to determine major axis, dispatches to appropriate line draw routine |
|
||||
| 68 | `000a:7b30` | `nop_return_void_g` | 38 | Empty function (default vtable slot) |
|
||||
| 69 | `000a:7b3f` | `nop_return_void_h` | 38 | Empty function (default vtable slot) |
|
||||
| 70 | `0009:6e7f` | `palette_free_if_set` | 35 | Frees existing palette data if ptr non-null, checks alignment |
|
||||
| 71 | `000a:7b35` | `nop_return_void_i` | 35 | Empty function (default vtable slot) |
|
||||
| 72 | `0009:c433` | `event_queue_align_index` | 34 | Returns `param_1 & 0xFFF8` — aligns ring index to 8-byte event slot boundary |
|
||||
| 73 | `0009:2156` | `dos_file_get_size` | 33 | Saves file position, does INT 21h AH=42h AL=02 (seek to end), restores position. Returns file size in DX:AX |
|
||||
| 74 | `000a:2c41` | `list_iterate_next` | 33 | Linked list iterator: if *out==0 returns first from obj+2; else follows next at ptr+2/+4. Returns bool (has more) |
|
||||
| 75 | `000a:454d` | *(no function in Ghidra)* | 32 | Analysis gap in comutils.c segment |
|
||||
| 76 | `000b:2446` | `sprite_clear_redraw_flag` | 31 | Clears flag at obj+0x17e, then dispatches via thunk |
|
||||
| 77 | `0005:1238` | `entity_get_class_word` | 30 | Looks up table at [0x7e01] indexed by *param_1 * 2, returns word. Sister of `entity_get_type_word` (which uses [0x7df9]) |
|
||||
| 78 | `000b:1446` | `display_null_check_dispatch` | 30 | Null-checks far ptr params, dispatches to different thunks based on result |
|
||||
| 79 | `000d:85da` | `map_object_set_dirty_flag` | 29 | Sets byte at global_obj[0x6828]+0x40 = 1 if global non-null, then calls thunk |
|
||||
| 80 | `0005:1511` | `entity_destroy_trampoline` | 29 | Pure thunk forwarder to entity destruction |
|
||||
|
||||
---
|
||||
|
||||
## Deep Analysis: Coordinate Transform System
|
||||
|
||||
### `world_to_screen_coords` at `0004:e7bd` (NE seg018:07bd)
|
||||
|
||||
**Signature:**
|
||||
```c
|
||||
void world_to_screen_coords(int world_x, int world_y, int *screen_x, int *screen_y)
|
||||
```
|
||||
|
||||
**Isometric Projection Math:**
|
||||
```
|
||||
screen_x = (world_x - world_y) / 2 - camera_x // SAR 1 (signed divide)
|
||||
screen_y = (world_x + world_y) / 4 - camera_y // SHR 2 (unsigned divide)
|
||||
```
|
||||
|
||||
Camera globals: `g_scroll_offset_x` (DS:0x2bb7), `g_scroll_offset_y` (DS:0x2bb9).
|
||||
|
||||
**Assembly detail:**
|
||||
- `SAR AX, 1` for screen_x — signed arithmetic shift preserves sign for negative (world_x - world_y) differences
|
||||
- `SHR AX, 2` for screen_y — unsigned logical shift (sum world_x + world_y is always positive)
|
||||
- The 2:1 ratio (÷2 for X, ÷4 for Y) produces the classic 2:1 isometric diamond tile shape
|
||||
|
||||
**Coordinate axes on screen:**
|
||||
- World X axis → lower-right on screen (+0.5 screen_x, +0.25 screen_y per world unit)
|
||||
- World Y axis → lower-left on screen (-0.5 screen_x, +0.25 screen_y per world unit)
|
||||
- Camera subtraction converts absolute world-space to viewport-relative screen coordinates
|
||||
|
||||
**Callers (17 across 8 NE segments):**
|
||||
|
||||
| Call site | NE Segment | Context |
|
||||
|-----------|-----------|---------|
|
||||
| `0004:7d6f` | seg012 | Map/tile rendering |
|
||||
| `0005:0305` | seg021 | Entity system |
|
||||
| `0005:432f` | seg021 | Entity placement |
|
||||
| `0005:4457` | seg021 | Entity placement |
|
||||
| `0005:6f8f` | seg022 | Entity rendering |
|
||||
| `0005:7263` | seg022 | Entity rendering |
|
||||
| `0007:2262` | seg040 | `snap_entity_to_ground` — ground alignment |
|
||||
| `0007:237d` | seg040 | Ground snap dispatch |
|
||||
| `0007:cf4e` | seg049 | Entity positioning |
|
||||
| `0007:d039` | seg049 | Entity positioning |
|
||||
| `0007:d43f` | seg049 | Entity positioning |
|
||||
| `0007:d6fe` | seg049 | Entity positioning |
|
||||
| `0008:3223` | seg053 | Entity-to-screen render setup |
|
||||
| `0008:32e7` | seg053 | Entity-to-screen render setup |
|
||||
| `0008:334b` | seg053 | Entity-to-screen render setup |
|
||||
| `000b:858b` | seg115 | Sprite system |
|
||||
| `000b:f100` | seg120 | Sprite system |
|
||||
|
||||
**Entity struct layout (from seg053 caller at `0008:31f6`):**
|
||||
```
|
||||
entity_array_base = far ptr at [DS:0x2cff]
|
||||
entity_struct_size = 19 bytes (0x13)
|
||||
entity.world_x = offset +0x0a (word)
|
||||
entity.world_y = offset +0x0c (word)
|
||||
```
|
||||
|
||||
### Comparison: Two Coordinate Transform Functions
|
||||
|
||||
| Property | `world_to_screen_coords` (0004:e7bd) | `world_to_screen_isometric` (0007:be67) |
|
||||
|----------|---------------------------------------|----------------------------------------|
|
||||
| Input type | Fine-grained world units (entity positions) | Coarse tile-grid units (map rendering) |
|
||||
| screen_x | `(wx - wy) / 2 - cam_x` | `(wx + sx) + (wy + sy) * 2` |
|
||||
| screen_y | `(wx + wy) / 4 - cam_y` | `(wy + sy) * 2 - (wx + sx)` |
|
||||
| Camera handling | Subtracted after transform | Added before transform |
|
||||
| Operations | Division (SAR/SHR) | Multiplication (SHL) |
|
||||
| Aspect ratio | 2:1 (from /2 : /4) | 2:1 (from 1 : 2 multipliers) |
|
||||
|
||||
Both functions implement the same 2:1 isometric projection but at different coordinate scales. `world_to_screen_coords` divides down from fine world units while `world_to_screen_isometric` multiplies up from coarse tile units.
|
||||
|
||||
### Adjacent Function: `map_position_equal` at `0004:e784`
|
||||
|
||||
Compares two 5-byte `map_position` structs: `{ x:word, y:word, layer:byte }`. Returns 1 (AL) if all three fields match, 0 otherwise. Located immediately before `world_to_screen_coords` in seg018.
|
||||
|
||||
---
|
||||
|
||||
### Tier 5: Ranks 81-100 (25-29 callers)
|
||||
|
||||
| Rank | Address | Name | Calls | Description |
|
||||
|------|---------|------|-------|-------------|
|
||||
| 81 | `0009:1c00` | `dos_file_handle_init` | 29 | Inits 6-byte file handle struct: dword=0, word+4=0xFFFF (invalid). Aborts on null ptr |
|
||||
| 82 | `0008:75f3` | `entity_get_ptr` | 29 | *(pre-existing)* Looks up entity far ptr from table at DS:0x39b0, indexed by id*4 |
|
||||
| 83 | `0006:0208` | `entity_class_get_flag4` | 29 | Returns bit 2 of classinfo byte at [0x7e1e]+*p1*0x79+0x13 → 0 or 1 |
|
||||
| 84 | `000a:30d7` | `list_node_set_if_context` | 29 | Sets node fields +2/+4 if params match context globals at 0x45a6/0x45a8 |
|
||||
| 85 | `0009:c45f` | `object_init_and_get_next` | 29 | Calls `object_init_zero_fields` then returns *(result+2) — init+accessor combo |
|
||||
| 86 | `0004:d7a0` | `object_deref_get_word4` | 28 | Dereferences far ptr chain: returns word at *(*(param_1)+4) |
|
||||
| 87 | `000a:5276` | `debug_check_flag_45aa` | 28 | If byte at DS:0x45aa non-zero, calls thunk (diagnostic/assert check) |
|
||||
| 88 | `0003:d94f` | `far_memset` | 28 | Wrapper reordering params for CRT memset impl at 0003:d92b (odd-aligned, word-fill loop) |
|
||||
| 89 | `000a:7b3a` | `nop_return_void_j` | 28 | Empty function (default vtable slot) |
|
||||
| 90 | `0008:ca18` | `entity_pair_sync_b` | 27 | *(pre-existing)* Pairwise sync wrapper direction B |
|
||||
| 91 | `0008:bd20` | `entity_sprite_set_target_pos` | 27 | *(pre-existing)* Sets flag 0x1000, copies player pos to entity +0x0a/+0x0c |
|
||||
| 92 | `0009:3ceb` | `buffer_release_and_dispatch` | 27 | Frees far ptr at obj+0x3b if set, nulls it; conditionally dispatches on bit 0 |
|
||||
| 93 | `0005:09b4` | `entity_get_flags_byte` | 27 | Reads byte from [0x7dfd]+id, conditionally extends with classinfo byte at [0x7e1e]+id*0x79+0xf |
|
||||
| 94 | `0005:0fbb` | `entity_lookup_sprite_word` | 27 | Returns word from [0x7e05]+*p1*2 — sprite/visual index table |
|
||||
| 95 | `0008:d27e` | `entity_dispatch_trampoline_b` | 26 | Pure forwarder thunk (CALLF thunk only) |
|
||||
| 96 | `0005:0376` | `entity_resolve_base_type` | 26 | Walks entity class hierarchy (bit 8 in [0x7e01]) via [0x7ded], returns base type from [0x7df1] |
|
||||
| 97 | `000b:2492` | `sprite_redraw_if_needed` | 26 | If redraw flag at +0x17e is clear, calls update routine + thunk |
|
||||
| 98 | `0003:e4d3` | `dos_file_open_wrapper` | 26 | Zeros output byte, delegates to file open impl at 0003:bb92 |
|
||||
| 99 | `0005:033e` | `entity_resolve_base_parent` | 25 | Same hierarchy walk as `entity_resolve_base_type` but returns parent from [0x7ded] |
|
||||
| 100 | `000a:87fd` | `render_clip_rect_to_viewport` | 25 | Clips 4 rect params to viewport bounds at [0x4014], sets dirty flag at 0x8a16, increments draw counter at 0x4716 |
|
||||
|
||||
**Entity Table Pointers (DS-relative, discovered in tier 5):**
|
||||
|
||||
| DS Offset | Type | Stride | Purpose |
|
||||
|-----------|------|--------|---------|
|
||||
| `0x7dfd` | byte[] | 1 | Entity flags byte (entity_get_flags_byte) |
|
||||
| `0x7e01` | word[] | 2 | Entity class flags (bit 8 = has parent in hierarchy) |
|
||||
| `0x7e05` | word[] | 2 | Entity sprite/visual index |
|
||||
| `0x7ded` | word[] | 2 | Entity parent/hierarchy index |
|
||||
| `0x7df1` | word[] | 2 | Entity base type word |
|
||||
| `0x7e1e` | struct[] | 0x79 | Entity class detail records (121 bytes per class) |
|
||||
|
||||
### Analysis Gaps (No Function in Ghidra)
|
||||
|
||||
These high-traffic addresses need manual function creation in Ghidra (Script Manager or UI):
|
||||
|
||||
| Address | NE Segment | Callers | Notes |
|
||||
|---------|-----------|---------|-------|
|
||||
| `000a:44fd` | seg091:00fd | 331 | #1 most-called target! In comutils.c segment. |
|
||||
| `000b:2e00` | seg109:0000 | 74 | Start of segment 109. |
|
||||
| `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. |
|
||||
| `0009:a200` | seg082:0000 | - | Target of `mem_alloc`. Start of segment 82. |
|
||||
| `000c:db68` | `cursor_nav_update_and_dispatch` | Calls `cursor_zone_quadrant_classify`; updates `[+0x37..+0x3a]`; reads `[0x63da]`; switch on direction (0–8); maps scancodes 0x48/0x50/0x4b/0x4d/0x39 |
|
||||
| `000c:d3e9` | `cursor_set_ref_and_dispatch` | Null-checks param; sets `*param_1 = &DAT_0000_638e`; calls dispatch |
|
||||
| `000c:d710` | `cursor_set_ref2_and_dispatch` | Same pattern; sets `*param_1 = &DAT_0000_6346` |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue