- 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.
17 KiB
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:
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, 1for screen_x — signed arithmetic shift preserves sign for negative (world_x - world_y) differencesSHR AX, 2for 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.