- 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.
247 lines
17 KiB
Markdown
247 lines
17 KiB
Markdown
# Raw 0007: Rendering, Scroll/Camera & Coordinate Transforms
|
||
|
||
Content extracted from `crusader_decompilation_notes.md`. Covers the sprite draw list subsystem, tile-based visibility, map scroll/camera, and the isometric coordinate transform system.
|
||
|
||
---
|
||
|
||
## Raw 0007 Rendering & Sprite Draw List Subsystem
|
||
|
||
### Draw List Node Format
|
||
|
||
Each draw node is `0x2a` (42) bytes. The free pool holds 1500 nodes based at `0x2cc7`.
|
||
|
||
| Offset | Field |
|
||
|--------|-------|
|
||
| `+8` | child link (for `drawlist_enqueue_sprite_children`) |
|
||
| `+10` | z-order dependency forward link |
|
||
| `+0x14` | enqueued flag (bit 0) |
|
||
| `+0x15` | z-depth (1 = root sentinel) |
|
||
| `+0x16` | flags/type byte |
|
||
| `+0x17` | redraw must-flag |
|
||
|
||
### Draw List Functions
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:eb36` | `drawlist_pool_init` | Inits free pool: 1500 nodes × `0x2a` bytes from base `0x2cc7`. Sets `0x2ccf = 1500` (count), `0x2cc3` = linked list head. Copies `0x2cc9` → `0x2cae`. |
|
||
| `0007:eb12` | `linked_list_push_2cc3` | LIFO push: `node->next = head_2cc3; head = node; count_2ccf++`. |
|
||
| `0007:eada` | `linked_list_pop_2cc3` | LIFO pop: dequeues node from `0x2cc3` head, decrements `0x2ccf` count. |
|
||
| `0007:ebd9` | `linked_list_dequeue_headtail` | FIFO dequeue from head/tail pair list; calls `FUN_0007_03ec` after dequeue. |
|
||
| `0007:ec2c` | `drawlist_enqueue` | FIFO enqueue: appends node to draw list (head if empty, tail always updated). |
|
||
| `0007:ec63` | `drawlist_remove_node` | Unlinks specific node from head or tail position; calls `FUN_0007_03ec`. |
|
||
| `0007:eca8` | `drawlist_process_and_render` | **Two-stage render pass.** Stage 1: drains `0x8442` (pending), viewport-tests each sprite, moves in-bounds to `0x8446` (visible). Stage 2: drains `0x8446`, calls sprite vtable[0] to draw. Recursive re-run if `0x2cb2` defer flag set. |
|
||
| `0007:edfa` | `drawlist_enqueue_sprite_children` | Enqueues all child sprites at node `+8` linkage to draw pending list if not already queued or rendered. Skips if `+0x15` z-depth == 1. |
|
||
| `0007:ef3d` | `sprite_add_draw_dependency` | Links sprites A and B with z-order dependency. Allocates link node via `FUN_0007_057f`. Stores in A's `+10` and B's `+8`. Sets must-redraw on B if A has `+0x17` set. |
|
||
| `0007:ef9f` | `sprite_enqueue_for_draw` | Null-guard; sets node `+0x14 \|= 1`, enqueues to `0x8442`. |
|
||
| `0007:efca` | `sprite_invalidate_and_unlink` | Full sprite removal: re-enqueues all dependents, frees link nodes via `FUN_0007_0614`, clears `+8/+10` lists, dequeues self from `0x8442`. |
|
||
| `0007:f2a0` | `sprite_sort_by_depth_and_link` | Compares `+0x15` z-depth of two sprites; calls `sprite_add_draw_dependency` in the correct order (lower z first). Handles equal-depth via thunk. |
|
||
| `0007:f654` | `drawlist_init` | Full draw system init: `drawlist_pool_init`, init full-screen viewport (`0x844a..0x8450 = 0..639, 0..screen_h`), allocate root sentinel node (vtable `FUN_0000_2ce4`, z-depth=1, flag=0x40), stored at `0x846a`. |
|
||
| `0007:ea00` | `bbox_intersect` | In-place 2D rect intersection: max of mins, min of maxes. Input/output: 4-int array `[xmin,ymin,xmax,ymax]`. |
|
||
| `0007:ea6d` | `bbox_union` | In-place 2D rect union: min of mins, max of maxes. |
|
||
| `0007:ee5a` | `viewport_update_from_sprite_bounds` | Subtracts scroll offsets from sprite bbox; clips to screen rect (`0x2ca6..0x2cac`); calls `bbox_intersect`; stores updated viewport at `0x844a..0x8450`; dispatches to render. |
|
||
|
||
### Isometric Coordinate Transform (draw list side)
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:be67` | `world_to_screen_isometric` | Coarse tile-grid → screen transform. Uses `(wx+sx) + (wy+sy)*2` for screen_x, `(wy+sy)*2 - (wx+sx)` for screen_y. Camera (`sx`, `sy`) added before transform. |
|
||
| `0007:bef8` | `world_to_screen_isometric_wrapper` | Thin wrapper around `world_to_screen_isometric`. |
|
||
|
||
---
|
||
|
||
## Tile-Based Visibility System
|
||
|
||
The rendering system uses a **6×5 tile grid** at DS:`0x846a` (30 entries × 2 bytes each = 60 bytes). Tiles represent screen regions of approximately 128 screen pixels per tile. Each tile holds a near pointer to a linked list of sprite nodes overlapping that tile. Bitmasks track dirty and renderable tiles.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:f9e2` | `drawlist_mark_dirty_tiles` | Converts bbox to tile grid coords (divide by 128, clamp to 6×5). Walks all sprites in overlapping tiles' spatial buckets; re-enqueues overlapping sprites. Sets bits in dirty bitmask `0x2cbb`. |
|
||
| `0007:fb53` | `tile_visibility_update` | Iterates redraw bitmask (`0x2cbb`, up to 10240 tiles). For each dirty tile with a sprite, computes isometric screen position from map X/Y/Z. Checks sprite dimension bitfields at `pbVar8+2..+3` against 640×480 viewport. Sets corresponding bit in render bitmask `0x2cb7`. Clears dirty bitmask after processing. |
|
||
| `0007:fd98` | `tilemap_draw_if_dirty` | Guard: if render bitmask `0x2cb7` non-zero, calls `thunk_FUN_0007_001d` to trigger tilemap draw. |
|
||
|
||
### Draw List / Viewport Globals
|
||
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x2bb7` | `g_scroll_offset_x` | World-to-screen X offset (scroll) |
|
||
| `0x2bb9` | `g_scroll_offset_y` | World-to-screen Y offset (scroll) |
|
||
| `0x2ca6..0x2cac` | `g_screen_clip_rect` | Screen clip rectangle `[xmin,ymin,xmax,ymax]` |
|
||
| `0x2cae` | `g_draw_segment` | DS segment for draw node far pointers |
|
||
| `0x2cb0` | `g_free_pool_seg` | Segment for node free pool far calls |
|
||
| `0x2cb2` | `g_render_defer_flag` | If set, defers current render pass |
|
||
| `0x2cb7` | `g_render_tile_bitmask` | Far ptr to bitmask of tiles ready to render |
|
||
| `0x2cbb` | `g_dirty_tile_bitmask` | Far ptr to bitmask of dirty/changed tiles |
|
||
| `0x2cbf` | `g_tile_origin_x` | Tile grid world origin X (for tile coordinate math) |
|
||
| `0x2cc1` | `g_tile_origin_y` | Tile grid world origin Y |
|
||
| `0x2cc3` | `g_free_pool_head` | Free node pool linked list head |
|
||
| `0x2ccf` | `g_free_pool_count` | Free node pool remaining count (max 1500) |
|
||
| `0x8442` | `g_draw_pending_list` | Draw pending list head/tail (near ptrs) |
|
||
| `0x8446` | `g_draw_visible_list` | Draw visible list head/tail (near ptrs) |
|
||
| `0x844a..0x8450` | `g_viewport_rect` | Current viewport `[xmin,ymin,xmax,ymax]` |
|
||
| `0x846a` | `g_tile_grid_base` | 6×5 tile grid spatial index (near ptr per tile) |
|
||
|
||
---
|
||
|
||
## Raw 0007 Map Scroll / Camera Subsystem
|
||
|
||
A scroll/camera management cluster found in the `0007:bxxx–0007:dxxx` range.
|
||
|
||
### Scroll/Camera Functions
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:ba00` | `watch_entity_controller_create_global` | Thin global-create wrapper around `watch_entity_controller_create`; allocates default controller object and stores it at `0x2bd8`. |
|
||
| `0007:ba13` | `watch_entity_controller_dispatch_if_present` | If `0x2bd8` is non-null, calls controller vtable slots `+0x2c` and `+0x30`. |
|
||
| `0007:ba45` | `watch_entity_controller_create` | Allocates/initializes a type `0x2c2b` controller object, stores it at `0x2bd8`, sets event type `0x0219`, and installs callback-table entry `0x2be4` through `0x39ca`. |
|
||
| `0007:bab5` | `entity_set_watch_ptr` | Legacy name still in place, but newer constructor evidence now shows `0x2bd8` is a controller object lane rather than just a raw watched-entity FAR pointer. |
|
||
| `0007:baea` | `camera_update_and_check_player_scroll` | Calls watch entity vtable `+0x24`; if `0x2bd1` flag clear checks if player position (from `g_player_entity_farptr+0x40`) has moved > 32 units since `0x2be0`; if so, updates `0x2be0` and conditionally dispatches scroll event via `0x45aa`. |
|
||
| `0007:c6ba` | `scroll_camera_set_state_params` | Stores word params to `0x8354`, `0x8356`, byte to `0x8358`; dispatches. |
|
||
| `0007:cd56` | `dispatch_if_flag_2bd3_set` | Returns unless `0x2bd3` non-zero. |
|
||
| `0007:cfef` | `dispatch_if_mode_flags_set` | Two-flag check: dispatches if `0x2bca` or `0xee0` is non-zero. |
|
||
| `0007:d0f6` | `scroll_call_set_params_unless_blocked` | Calls `scroll_camera_set_state_params` only if `0x2bbb == 0`. |
|
||
| `0007:d119` | `scroll_update_direction_tracking` | Guards on `0x2bd3`. Calls `scroll_call_set_params_unless_blocked`. Compares direction bytes from `0x2cf4/0x2cf5` against cached `0x2bbd/0x2bbe`; if changed, clears `0x2bbc`. Dispatches. |
|
||
| `0007:d4a5` | `scroll_set_option_value` | Sets `0x2bc6 = param_1`. |
|
||
| `0007:d4b0` | `scroll_set_params_default` | Unconditional call to `scroll_camera_set_state_params`. |
|
||
| `0007:d4d3` | `scroll_set_map_index_validated` | If `param_1` in `[0..250]` and differs from `0x2bbf`, updates `0x2bbf` and clears `0x2bbc/0x2bbb`. |
|
||
| `0007:d655` | `map_position_has_changed` | Compares map arrays `0x7ded/0x7df1/0x7df5` at index `0x2bc6` against cached `0x2bc1/0x2bc3/0x2bc5`. Returns 1 if changed, 0 if same. |
|
||
| `0007:d6b1` | `scroll_clear_dirty_flags_and_dispatch` | Clears `0x2bbb = 0` and `0x2bbc = 0`; dispatches. |
|
||
| `0007:de57` | `entity_check_player_range_and_update` | Reads player world position (`g_player_entity_farptr+0x40`); if moved > 59 units from `entity+0x32` (cached pos), updates cache and calls `scrollregion_find_and_dispatch`. |
|
||
|
||
### Scroll Region Table (`0x835a`)
|
||
|
||
8 entries × `0x19` (25) bytes = 200 bytes.
|
||
|
||
| Offset | Field | Notes |
|
||
|--------|-------|-------|
|
||
| `+0x0/+0x2` | key pair | Matched by scroll region key lookup |
|
||
| `+0x8` | refcount | Reference count (incremented on match) |
|
||
| `+0xe` | count2 | Secondary counter |
|
||
| `+0x10` | active | Non-zero when region is live |
|
||
| `+0x11/+0x13` | x_start/x_end | Bounding X coordinates |
|
||
| `+0x15/+0x17` | y_start/y_end | Bounding Y coordinates |
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:e194` | `scrollregion_process_active` | Iterates active scroll entries (key non-zero AND `+0x10 != 0`), reads bounding box fields `+0x11/+0x13/+0x15/+0x17`, dispatches with bounds args. |
|
||
| `0007:e214` | `scrollregion_find_and_dispatch` | Finds empty scroll entry (zero key) and dispatches. |
|
||
| `0007:e29c` | `scrollregion_register` | Finds or allocates scroll region entry. Existing match: bump refcount / set active. New: init via `scrollregion_entry_init`. Special key `0x4ed`: thunk dispatch instead. |
|
||
| `0007:e50f` | `scrollregion_entry_init` | Null-guards param_1; zeroes output param_2, param_3, param_4; dispatches continuation. |
|
||
| `0007:e74a` | *(unnamed)* | Sets up call to walk `0x835a` table (8 entries, stride `0x19`, filter `0x968`), with callback return label `e763`. |
|
||
|
||
### Save Slot System (`0x8337` + `0x2ba3`)
|
||
|
||
10 save slots, each `0x400` (1024) bytes. Handle table at `0x8337` (10 words). Slot buffer base at FAR ptr `0x2ba3`.
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:ac13` | `saveslot_table_clear` | Fills `0x8337..0x834b` (10 words) with `0xFFFF` (all slots empty). |
|
||
| `0007:acab` | `saveslot_free_if_empty` | Scans slot `0x2ba3[param_1*0x400]` for non-zero data; if empty, sets handle `0x8337[param_1] = 0xFFFF`. |
|
||
| `0007:ad47` | `saveslot_find_index_by_id` | Linear scan of 10-word handle table `0x8337`; returns index of matching word or `-1`. |
|
||
| `0007:ad79` | *(unnamed)* | Finds a free (`0xFFFF`) slot index. Complement of `saveslot_find_index_by_id`. |
|
||
| `0007:afd4` | `saveslot_get_or_alloc` | Gets slot pointer: calls `saveslot_find_index_by_id`; if not found calls `ad79` to get free slot; returns `0x2ba3 + slot * 0x400`. Returns 0 if no free slot. |
|
||
| `0007:b02c` | `saveslot_write_entry` | Navigates to `slot_base[param_3 * 4]`; dispatches thunk paths for write (existing, overwrite, new). |
|
||
| `0007:b0de` | `saveslot_read_entry_flags` | Reads from slot entry far pointer at `slot_base[param_3*4]`; extracts 4-byte packed bitfield from `+4..+7` in entry record into `*param_1`. Bit-by-bit extraction loop for 4 bytes. |
|
||
|
||
### String & Memory Utilities
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `0007:a96d` | `entity_copy_string_truncated80` | Strlen(param_3) ≤ `0x50` guard; copies string word-by-word from param_3 into `param_2+8`. |
|
||
| `0007:b813` | `memcpy_4words` | Copies 4 words (8 bytes) from `param_2` to `param_1`. |
|
||
| `0007:b46d` | `entity_dispatch_if_slot82e2_valid` | Guard: if `*(int *)0x82e2 != -1`, calls dispatch thunk. |
|
||
|
||
### Scroll/Camera Globals
|
||
|
||
| Address | Name | Notes |
|
||
|---------|------|-------|
|
||
| `0x2bb7` | `g_scroll_offset_x` | Isometric scroll X — added to world_x in screen transform |
|
||
| `0x2bb9` | `g_scroll_offset_y` | Isometric scroll Y |
|
||
| `0x2bbb` | `g_scroll_blocked` | If non-zero, blocks `scroll_camera_set_state_params` call |
|
||
| `0x2bbc` | `g_scroll_dirty` | Scroll direction changed flag (cleared when direction tracks) |
|
||
| `0x2bbd` | `g_scroll_dir_x` | Cached scroll direction X |
|
||
| `0x2bbe` | `g_scroll_dir_y` | Cached scroll direction Y |
|
||
| `0x2bbf` | `g_map_index` | Current map/level index `[0..250]` |
|
||
| `0x2bc1/0x2bc3/0x2bc5` | `g_map_entry_x/y/z` | Cached map entry X/Y/Z (vs. live map arrays) |
|
||
| `0x2bc6` | `g_map_slot_index` | Index into `0x7ded/0x7df1/0x7df5` arrays for current map slot |
|
||
| `0x2bca/0x2bc9` | `g_option_toggle_state` | UI option toggle state flags |
|
||
| `0x2bd1` | `g_scroll_block_flag` | Blocks camera update path if non-zero |
|
||
| `0x2bd3` | `g_scroll_active` | Non-zero = scroll system active |
|
||
| `0x2bd8` | `g_watch_entity_controller_farptr` | FAR ptr to the watch/camera controller object |
|
||
| `0x2be0` | `g_player_scroll_pos` | Cached player world X+Y (ulong) for scroll threshold detection |
|
||
| `0x8354..0x8358` | `g_scroll_state_params` | Three scroll state params (word, word, byte) |
|
||
|
||
---
|
||
|
||
## 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.
|