Crusader_Decomp/docs/raw-0007-rendering.md
MaddoScientisto 3daffbf113 Add extractor for Crusader's EUSECODE.FLX container
- Implemented a Python script to extract data from the EUSECODE.FLX file format.
- Defined data structures for candidate entries and extracted chunks using dataclasses.
- Added functions to read and parse the FLX table, extract candidate data, and generate human-readable output files.
- Included functionality for analyzing extracted data, including generating summaries, descriptors, and event family reports.
- Implemented utilities for calculating printable ratios, zero ratios, and identifying text-like data.
- Added support for writing various output formats, including JSON, TSV, and Markdown.
2026-03-22 14:27:38 +01:00

247 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:bxxx0007: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.