paavohtl Claude Opus 4.6 commited on
Commit
c7dca4b
·
1 Parent(s): 323b7e5

feat: add TurnOrderWidget structs and expand DDGame field coverage

Browse files

Add turn_order.rs with AnimatedItemList, TurnOrderWidget, TurnOrderAllianceGroup,
and TurnOrderTeamEntry structs for the in-game team banner health bar UI.
Retype DDGame.turn_order_widget from *mut u8 to *mut TurnOrderWidget.

DDGame fields verified via runtime DWORD dumps and Ghidra cross-references:
- team_health_ratio[6] at 0x45F0 (fixed-point, renders health bar width)
- game_speed/game_speed_target at 0x72D8/0x72DC
- rng_state_1/2 at 0x72EC/0x72F0
- viewport_width/height at 0x7380-0x738C
- camera_x/y/target at 0x73A0-0x73AC
- level_bound_min/max at 0x779C-0x77A8
- task_land (CTaskLand) at 0x54C, hud_panel at 0x534
- turn_status_text at 0x818C, turn_time_limit at 0x7EA8

Remove diagnostic dump invocations from input.rs, keep utilities for reuse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

crates/openwa-core/src/address.rs CHANGED
@@ -47,6 +47,8 @@ pub mod va {
47
  pub const OPENGL_CPU_VTABLE: u32 = 0x0067_74C0;
48
  /// WaterEffect vtable (0xBC-byte object)
49
  pub const WATER_EFFECT_VTABLE: u32 = 0x0066_B268;
 
 
50
  /// Sprite vtable (0x70-byte objects, 8 entries)
51
  pub const SPRITE_VTABLE: u32 = 0x0066_418C;
52
 
 
47
  pub const OPENGL_CPU_VTABLE: u32 = 0x0067_74C0;
48
  /// WaterEffect vtable (0xBC-byte object)
49
  pub const WATER_EFFECT_VTABLE: u32 = 0x0066_B268;
50
+ /// CTaskLand vtable - 12 entries, landscape/terrain task (DDGame+0x054C)
51
+ pub const CTASK_LAND_VTABLE: u32 = 0x0066_4388;
52
  /// Sprite vtable (0x70-byte objects, 8 entries)
53
  pub const SPRITE_VTABLE: u32 = 0x0066_418C;
54
 
crates/openwa-core/src/ddgame.rs CHANGED
@@ -8,6 +8,7 @@ use crate::music::Music;
8
  use crate::palette::Palette;
9
  use crate::render::RenderQueue;
10
  use crate::speech::SpeechSlotTable;
 
11
 
12
  /// DDGame — the main game engine object.
13
  ///
@@ -62,12 +63,16 @@ pub struct DDGame {
62
  pub arrow_gfxdirs: [*mut u8; 32],
63
  /// 0x138: DisplayGfx object pointer (vtable 0x664144)
64
  pub display_gfx: *mut u8,
65
- /// 0x13C-0x37F: Unknown
66
- pub _unknown_13c: [u8; 0x244],
 
 
 
67
  /// 0x380: TaskStateMachine pointer (vtable 0x664118, 0x2C bytes)
68
  pub task_state_machine: *mut u8,
69
- /// 0x384-0x467: Unknown
70
- pub _unknown_384: [u8; 0xE4],
 
71
  /// 0x468: Landscape-derived value
72
  pub _landscape_val: *mut u8,
73
  /// 0x46C-0x488: 8 SpriteRegion pointers (0x9C bytes each, vtable 0x66B268)
@@ -80,24 +85,98 @@ pub struct DDGame {
80
  pub coord_list: *mut u8,
81
  /// 0x510: Weapon table pointer
82
  pub weapon_table: *mut u8,
83
- /// 0x514-0x523: Unknown
84
- pub _unknown_514: [u8; 0x10],
 
 
 
 
 
 
85
  /// 0x524: RenderQueue pointer (passed as `this` to all Draw* functions)
86
  pub render_queue: *mut RenderQueue,
87
- /// 0x528-0x547: Unknown
88
- pub _unknown_528: [u8; 0x20],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  /// 0x548: Weapon panel pointer
90
  pub weapon_panel: *mut u8,
91
- /// 0x54C-0x7323: Sparse fields (see offsets module)
 
 
 
92
  ///
93
- /// Known landmarks:
94
- /// - 0x64D8: cleared by init
95
- /// - 0x72A4: cleared by init
96
- /// - 0x730C-0x731C: 5 GfxDir color entries
 
 
 
 
 
 
 
97
  ///
98
- /// Also includes FUN_00526120 zeroed offsets at stride 0x194:
99
  /// 0x379C, 0x3930, 0x3AC4, 0x3C58, 0x3DEC, 0x3F80, 0x4114, 0x42A8, 0x443C, 0x45D0
100
- pub _unknown_54c: [u8; 0x7324 - 0x54C],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  /// 0x7324: Crosshair line color param (DrawPolygon param_2).
103
  /// Part of GfxDir color entries at 0x730C.
@@ -110,8 +189,56 @@ pub struct DDGame {
110
  pub _unknown_7330: [u8; 8],
111
  /// 0x7338: Fill pixel value
112
  pub fill_pixel: u32,
113
- /// 0x733C-0x77BF: Unknown
114
- pub _unknown_733c: [u8; 0x77C0 - 0x733C],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  /// 0x77C0: Level width in pixels (set by PCLandscape constructor).
117
  pub level_width: u32,
@@ -127,12 +254,27 @@ pub struct DDGame {
127
  /// Cleared by DSSound_LoadAllSpeechBanks (0x571A70), filled by DSSound_LoadSpeechWAV (0x571530).
128
  pub speech_slot_table: SpeechSlotTable,
129
 
130
- /// 0x7D84-0x7EFF: Unknown
131
- ///
132
- /// Known landmarks:
133
- /// - 0x7EF8: flag from game_info+0xF914
134
- /// - 0x7EFC: init 1
135
- pub _unknown_7d84: [u8; 0x7F00 - 0x7D84],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  // === Sound queue (0x7F00-0x8143) ===
138
  /// 0x7F00: Sound queue (16 entries, stride 0x24). Appended by PlaySoundGlobal.
@@ -145,14 +287,39 @@ pub struct DDGame {
145
  /// 0x8150: Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
146
  pub crosshair_scale: i32,
147
 
148
- /// 0x8154-0x98AF: Remaining fields
 
 
 
 
 
 
 
 
149
  ///
150
  /// Known landmarks:
151
- /// - 0x8CBC-0x8CF0: 4x 0x10-byte entries (zeroed at +0, +4)
152
- /// - 0x9850-0x9884: 4x 0x10-byte entries (zeroed at +0, +4)
153
- /// - 0x98A4: checkpoint active flag
154
- /// - 0x98AC: fast-forward request flag
155
- pub _unknown_8154: [u8; 0x98B0 - 0x8154],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  /// 0x98B0: Fast-forward active flag.
158
  /// When set to 1, FUN_005307A0 processes up to 50 game frames per render
@@ -217,14 +384,54 @@ pub mod offsets {
217
  pub const INIT_TABLE_BASE: usize = 0x379C;
218
  pub const INIT_TABLE_STRIDE: usize = 0x194;
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  // === Sparse fields in upper region ===
221
- pub const FIELD_64D8: usize = 0x64D8;
222
- pub const FIELD_72A4: usize = 0x72A4;
223
  pub const GFX_COLOR_ENTRIES: usize = 0x730C;
224
- pub const FLAG_7EF8: usize = 0x7EF8;
225
- pub const FIELD_7EFC: usize = 0x7EFC;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  /// Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
227
  pub const CROSSHAIR_SCALE: usize = 0x8150;
 
 
 
 
 
 
228
 
229
  // === Speech slot table (DDGame + 0x77E4) ===
230
  /// Speech slot table: maps (team, speech_line_id) → DSSound buffer index.
 
8
  use crate::palette::Palette;
9
  use crate::render::RenderQueue;
10
  use crate::speech::SpeechSlotTable;
11
+ use crate::turn_order::TurnOrderWidget;
12
 
13
  /// DDGame — the main game engine object.
14
  ///
 
63
  pub arrow_gfxdirs: [*mut u8; 32],
64
  /// 0x138: DisplayGfx object pointer (vtable 0x664144)
65
  pub display_gfx: *mut u8,
66
+ /// 0x13C-0x37F: Sprite/image object cache (145 pointer slots).
67
+ /// All populated entries have vtable 0x664144 (same class as `display_gfx`).
68
+ /// Not initialized in DDGame__Constructor — filled during gameplay with
69
+ /// weapon sprites, effect images, cursor graphics, etc.
70
+ pub sprite_cache: [*mut u8; 145],
71
  /// 0x380: TaskStateMachine pointer (vtable 0x664118, 0x2C bytes)
72
  pub task_state_machine: *mut u8,
73
+ /// 0x384-0x467: Additional sprite/image object slots.
74
+ /// Same vtable 0x664144 as sprite_cache. ~20 entries populated at runtime.
75
+ pub sprite_cache_2: [*mut u8; 57],
76
  /// 0x468: Landscape-derived value
77
  pub _landscape_val: *mut u8,
78
  /// 0x46C-0x488: 8 SpriteRegion pointers (0x9C bytes each, vtable 0x66B268)
 
85
  pub coord_list: *mut u8,
86
  /// 0x510: Weapon table pointer
87
  pub weapon_table: *mut u8,
88
+ /// 0x514: Unknown pointer (populated at runtime)
89
+ pub _unknown_514: *mut u8,
90
+ /// 0x518: Unknown pointer (populated at runtime)
91
+ pub _unknown_518: *mut u8,
92
+ /// 0x51C: Unknown pointer (populated at runtime)
93
+ pub _unknown_51c: *mut u8,
94
+ /// 0x520: Unknown (zero in runtime dump)
95
+ pub _unknown_520: u32,
96
  /// 0x524: RenderQueue pointer (passed as `this` to all Draw* functions)
97
  pub render_queue: *mut RenderQueue,
98
+ /// 0x528: Game state stream object (vtable 0x664194, vt[0]=0x4FB5C0).
99
+ /// Created in DDGame_InitGameState_Maybe (0x526690), constructor 0x4FB5F0.
100
+ /// Reads from replay/packet data stream.
101
+ pub game_state_stream: *mut u8,
102
+ /// 0x52C: Unknown pointer
103
+ pub _unknown_52c: *mut u8,
104
+ /// 0x530: Turn order widget (vtable 0x66A088, vt[0]=0x563E90).
105
+ /// Constructor 0x563D40. UI component that renders team banners with
106
+ /// animated sliding transitions (sin-table interpolation). Groups teams
107
+ /// by alliance, creates per-team entries with textbox + DisplayGfx.
108
+ pub turn_order_widget: *mut TurnOrderWidget,
109
+ /// 0x534: HUD panel object (vtable 0x664698, vt[0]=0x5241F0).
110
+ /// Constructor 0x524070. 104×28 px, 3 DisplayGfx layers, 2296-byte LUT.
111
+ pub hud_panel: *mut u8,
112
+ /// 0x538-0x53F: Unknown (zero in runtime dump)
113
+ pub _unknown_538: [u8; 8],
114
+ /// 0x540: Unknown pointer
115
+ pub _unknown_540: *mut u8,
116
+ /// 0x544: Unknown pointer (*=0x1BC at runtime)
117
+ pub _unknown_544: *mut u8,
118
  /// 0x548: Weapon panel pointer
119
  pub weapon_panel: *mut u8,
120
+ /// 0x54C: CTaskLand pointer (set by CTaskLand__InitLandscape at 0x5056F0).
121
+ /// The landscape/terrain task. Vtable at 0x664388.
122
+ pub task_land: *mut u8,
123
+ /// 0x550-0x25FF: Large unverified region.
124
  ///
125
+ /// Runtime observations (not yet linked to code):
126
+ /// - 0x5C4: value matches code address 0x5755D0 (fixed-point normalize fn)
127
+ /// - 0x5C8-0x5FF: small config-like values (2048, 150, 3000, 696, 896, 100, 300)
128
+ /// - 0x600-0x25FF: identity permutation [0,1,2,...,~2048] — purpose unknown
129
+ pub _unknown_550: [u8; 0x2600 - 0x550],
130
+
131
+ /// 0x2600-0x2DFF: Block of 0xFFFFFFFF values at runtime (512 i32 entries).
132
+ /// May be unused slots in a parallel table to the 0x600 permutation.
133
+ pub _unknown_2600: [u8; 0x2E00 - 0x2600],
134
+
135
+ /// 0x2E00-0x45EB: Unknown (mostly zero at runtime)
136
  ///
137
+ /// Contains FUN_00526120 zeroed offsets at stride 0x194:
138
  /// 0x379C, 0x3930, 0x3AC4, 0x3C58, 0x3DEC, 0x3F80, 0x4114, 0x42A8, 0x443C, 0x45D0
139
+ pub _unknown_2e00: [u8; 0x45EC - 0x2E00],
140
+
141
+ /// 0x45EC: Unknown (0xA307A169 at runtime — not part of team scale arrays).
142
+ pub _unknown_45ec: u32,
143
+ /// 0x45F0-0x4607: Per-team health ratio (Fixed-point, 6 entries, 1-indexed).
144
+ /// 0x10000 = 100% health. Rendered as bar width: `value * 100 >> 16 + 4` pixels.
145
+ /// Read by TurnOrderTeamEntry render method (0x563620).
146
+ /// Initialized to 0x10000 (1.0) by TurnOrderTeamEntry constructor (0x5630B0).
147
+ pub team_health_ratio: [i32; 6],
148
+ /// 0x4608-0x461F: Per-team health ratio 2 (Fixed-point, 6 entries, 1-indexed).
149
+ /// Initialized to 0x10000 (1.0). Not read by the render method — may be
150
+ /// target/previous value for interpolation, or used by update logic.
151
+ pub team_health_ratio_2: [i32; 6],
152
+ /// 0x4620-0x64D7: Unknown
153
+ ///
154
+ /// Known landmarks:
155
+ /// - 0x64D8: cleared by init
156
+ pub _unknown_4620: [u8; 0x64D8 - 0x4620],
157
+ /// 0x64D8: Cleared by init.
158
+ pub _field_64d8: u32,
159
+ /// 0x64DC-0x72A3: Unknown
160
+ pub _unknown_64dc: [u8; 0x72A4 - 0x64DC],
161
+ /// 0x72A4: Cleared by init.
162
+ pub _field_72a4: u32,
163
+ /// 0x72A8-0x72D7: Unknown
164
+ pub _unknown_72a8: [u8; 0x72D8 - 0x72A8],
165
+
166
+ /// 0x72D8: Game speed multiplier (Fixed-point, 0x10000 = 1.0x).
167
+ pub game_speed: i32,
168
+ /// 0x72DC: Game speed target (Fixed-point, 0x10000 = 1.0x).
169
+ pub game_speed_target: i32,
170
+ /// 0x72E0-0x72EB: Unknown
171
+ pub _unknown_72e0: [u8; 0x72EC - 0x72E0],
172
+ /// 0x72EC: RNG state word 1 (changes every frame).
173
+ pub rng_state_1: u32,
174
+ /// 0x72F0: RNG state word 2 (changes every frame).
175
+ pub rng_state_2: u32,
176
+ /// 0x72F4-0x730B: Unknown
177
+ pub _unknown_72f4: [u8; 0x730C - 0x72F4],
178
+ /// 0x730C-0x731C: 5 GfxDir color entries
179
+ pub _gfx_color_entries: [u8; 0x7324 - 0x730C],
180
 
181
  /// 0x7324: Crosshair line color param (DrawPolygon param_2).
182
  /// Part of GfxDir color entries at 0x730C.
 
189
  pub _unknown_7330: [u8; 8],
190
  /// 0x7338: Fill pixel value
191
  pub fill_pixel: u32,
192
+ /// 0x733C-0x737F: Unknown
193
+ pub _unknown_733c: [u8; 0x7380 - 0x733C],
194
+
195
+ /// 0x7380: Viewport width (Fixed-point, e.g. 960.0 = 0x03C00000).
196
+ pub viewport_width: i32,
197
+ /// 0x7384: Viewport height (Fixed-point, e.g. 348.0 = 0x015C0000).
198
+ pub viewport_height: i32,
199
+ /// 0x7388: Viewport width max/duplicate (Fixed-point).
200
+ pub viewport_width_2: i32,
201
+ /// 0x738C: Viewport height max/duplicate (Fixed-point).
202
+ pub viewport_height_2: i32,
203
+ /// 0x7390-0x739F: Unknown
204
+ pub _unknown_7390: [u8; 0x73A0 - 0x7390],
205
+
206
+ /// 0x73A0: Camera X position (Fixed-point, e.g. 393.0).
207
+ pub camera_x: i32,
208
+ /// 0x73A4: Camera Y position (Fixed-point, e.g. 532.0).
209
+ pub camera_y: i32,
210
+ /// 0x73A8: Camera target X (Fixed-point, duplicate/interpolation target).
211
+ pub camera_target_x: i32,
212
+ /// 0x73AC: Camera target Y (Fixed-point).
213
+ pub camera_target_y: i32,
214
+
215
+ /// 0x73B0-0x764F: Unknown
216
+ pub _unknown_73b0: [u8; 0x7650 - 0x73B0],
217
+
218
+ /// 0x7650-0x768F: Team index mapping table 1.
219
+ /// Packed u16 pairs: [0,1], [2,3], ..., [14,15]. Team-to-slot or turn order.
220
+ pub team_index_map_1: [u8; 0x7690 - 0x7650],
221
+ /// 0x7690-0x76AF: Unknown
222
+ pub _unknown_7690: [u8; 0x76B0 - 0x7690],
223
+ /// 0x76B0-0x76EF: Team index mapping table 2 (same pattern).
224
+ pub team_index_map_2: [u8; 0x76F0 - 0x76B0],
225
+ /// 0x76F0-0x7717: Unknown
226
+ pub _unknown_76f0: [u8; 0x7718 - 0x76F0],
227
+ /// 0x7718-0x7757: Team index mapping table 3 (similar pattern, slightly different end).
228
+ pub team_index_map_3: [u8; 0x7758 - 0x7718],
229
+ /// 0x7758-0x779B: Unknown
230
+ pub _unknown_7758: [u8; 0x779C - 0x7758],
231
+
232
+ /// 0x779C: Level bound min X (Fixed-point, negative = off-screen left).
233
+ pub level_bound_min_x: i32,
234
+ /// 0x77A0: Level bound max X (Fixed-point).
235
+ pub level_bound_max_x: i32,
236
+ /// 0x77A4: Level bound min Y (Fixed-point, same as min_x typically).
237
+ pub level_bound_min_y: i32,
238
+ /// 0x77A8: Level bound max Y (Fixed-point).
239
+ pub level_bound_max_y: i32,
240
+ /// 0x77AC-0x77BF: Unknown
241
+ pub _unknown_77ac: [u8; 0x77C0 - 0x77AC],
242
 
243
  /// 0x77C0: Level width in pixels (set by PCLandscape constructor).
244
  pub level_width: u32,
 
254
  /// Cleared by DSSound_LoadAllSpeechBanks (0x571A70), filled by DSSound_LoadSpeechWAV (0x571530).
255
  pub speech_slot_table: SpeechSlotTable,
256
 
257
+ /// 0x7D84-0x7E9F: Unknown
258
+ pub _unknown_7d84: [u8; 0x7EA0 - 0x7D84],
259
+
260
+ /// 0x7EA0: Unknown flag/counter (value 4 at runtime — team count?)
261
+ pub _field_7ea0: u32,
262
+ /// 0x7EA4: Unknown
263
+ pub _unknown_7ea4: u32,
264
+ /// 0x7EA8: Turn time limit in seconds (150 = 2:30 default).
265
+ pub turn_time_limit: u32,
266
+ /// 0x7EAC-0x7ECF: Unknown
267
+ pub _unknown_7eac: [u8; 0x7ED0 - 0x7EAC],
268
+ /// 0x7ED0-0x7EEF: Unknown
269
+ pub _unknown_7ed0: [u8; 0x7EF0 - 0x7ED0],
270
+ /// 0x7EF0: Unknown flag (-1 = 0xFFFFFFFF at runtime)
271
+ pub _field_7ef0: i32,
272
+ /// 0x7EF4: Unknown
273
+ pub _unknown_7ef4: u32,
274
+ /// 0x7EF8: Sound available flag (1 when game_info+0xF914 == 0, i.e. not headless).
275
+ pub sound_available: u32,
276
+ /// 0x7EFC: Always initialized to 1 in constructor.
277
+ pub _field_7efc: u32,
278
 
279
  // === Sound queue (0x7F00-0x8143) ===
280
  /// 0x7F00: Sound queue (16 entries, stride 0x24). Appended by PlaySoundGlobal.
 
287
  /// 0x8150: Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
288
  pub crosshair_scale: i32,
289
 
290
+ /// 0x8154-0x818B: Unknown
291
+ pub _unknown_8154: [u8; 0x818C - 0x8154],
292
+
293
+ /// 0x818C: Turn status text buffer (null-terminated ASCII).
294
+ /// Shows on screen during gameplay, e.g. "Cheesy harkitseee siirtoaan"
295
+ /// ("Cheesy is considering their move" in Finnish).
296
+ pub turn_status_text: [u8; 64],
297
+
298
+ /// 0x81CC-0x8CBB: Unknown
299
  ///
300
  /// Known landmarks:
301
+ /// - 0x8174: value 0x3FC (1020) at runtime
302
+ pub _unknown_81cc: [u8; 0x8CBC - 0x81CC],
303
+
304
+ /// 0x8CBC-0x8CF8: 4× coordinate entries (0x10-byte stride).
305
+ /// InitFields zeroes +0 and +4 of each. At runtime contains fixed-point
306
+ /// screen coordinates (e.g. 393.0, 532.0, 960.0, 348.0).
307
+ pub coord_entries_8cbc: [u8; 0x8CFC - 0x8CBC],
308
+
309
+ /// 0x8CFC-0x984F: Unknown
310
+ pub _unknown_8cfc: [u8; 0x9850 - 0x8CFC],
311
+
312
+ /// 0x9850-0x988F: 4× coordinate entries (0x10-byte stride, zeroed by InitFields).
313
+ pub coord_entries_9850: [u8; 0x9890 - 0x9850],
314
+
315
+ /// 0x9890-0x98A3: Flags
316
+ pub _unknown_9890: [u8; 0x98A4 - 0x9890],
317
+ /// 0x98A4: Checkpoint active flag.
318
+ pub checkpoint_active: u32,
319
+ /// 0x98A8: Unknown
320
+ pub _unknown_98a8: u32,
321
+ /// 0x98AC: Fast-forward request flag.
322
+ pub fast_forward_request: u32,
323
 
324
  /// 0x98B0: Fast-forward active flag.
325
  /// When set to 1, FUN_005307A0 processes up to 50 game frames per render
 
384
  pub const INIT_TABLE_BASE: usize = 0x379C;
385
  pub const INIT_TABLE_STRIDE: usize = 0x194;
386
 
387
+ // === Game objects (0x528-0x54C) ===
388
+ pub const GAME_STATE_STREAM: usize = 0x528;
389
+ pub const TURN_ORDER_WIDGET: usize = 0x530;
390
+ pub const HUD_PANEL: usize = 0x534;
391
+ /// CTaskLand pointer (landscape/terrain task, vtable 0x664388).
392
+ pub const TASK_LAND: usize = 0x54C;
393
+
394
+ // === Per-team health ratio (turn order health bar) ===
395
+ /// Per-team health ratio array (6 × i32, 1-indexed by team).
396
+ /// 0x10000 = 100%. Rendered as `value * 100 >> 16` pixel width.
397
+ pub const TEAM_HEALTH_RATIO: usize = 0x45F0;
398
+ /// Per-team health ratio 2 (6 × i32, 1-indexed by team).
399
+ pub const TEAM_HEALTH_RATIO_2: usize = 0x4608;
400
+
401
+ // === RNG (0x72EC) ===
402
+ pub const RNG_STATE_1: usize = 0x72EC;
403
+ pub const RNG_STATE_2: usize = 0x72F0;
404
+
405
  // === Sparse fields in upper region ===
 
 
406
  pub const GFX_COLOR_ENTRIES: usize = 0x730C;
407
+
408
+ // === Camera/viewport (0x7380-0x73AC) ===
409
+ pub const VIEWPORT_WIDTH: usize = 0x7380;
410
+ pub const VIEWPORT_HEIGHT: usize = 0x7384;
411
+ pub const CAMERA_X: usize = 0x73A0;
412
+ pub const CAMERA_Y: usize = 0x73A4;
413
+ pub const CAMERA_TARGET_X: usize = 0x73A8;
414
+ pub const CAMERA_TARGET_Y: usize = 0x73AC;
415
+
416
+ // === Game speed (0x72D8) ===
417
+ pub const GAME_SPEED: usize = 0x72D8;
418
+ pub const GAME_SPEED_TARGET: usize = 0x72DC;
419
+
420
+ // === Level bounds (0x779C-0x77A8) ===
421
+ pub const LEVEL_BOUND_MIN_X: usize = 0x779C;
422
+ pub const LEVEL_BOUND_MAX_X: usize = 0x77A0;
423
+ pub const LEVEL_BOUND_MIN_Y: usize = 0x77A4;
424
+ pub const LEVEL_BOUND_MAX_Y: usize = 0x77A8;
425
+ pub const TURN_TIME_LIMIT: usize = 0x7EA8;
426
+ pub const SOUND_AVAILABLE: usize = 0x7EF8;
427
  /// Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
428
  pub const CROSSHAIR_SCALE: usize = 0x8150;
429
+ /// Turn status text (null-terminated ASCII, shown during gameplay).
430
+ pub const TURN_STATUS_TEXT: usize = 0x818C;
431
+ /// Checkpoint active flag.
432
+ pub const CHECKPOINT_ACTIVE: usize = 0x98A4;
433
+ /// Fast-forward request flag.
434
+ pub const FAST_FORWARD_REQUEST: usize = 0x98AC;
435
 
436
  // === Speech slot table (DDGame + 0x77E4) ===
437
  /// Speech slot table: maps (team, speech_line_id) → DSSound buffer index.
crates/openwa-core/src/lib.rs CHANGED
@@ -22,6 +22,7 @@ pub mod frontend;
22
  pub mod game_info;
23
  pub mod scheme;
24
  pub mod speech;
 
25
  pub mod address;
26
  pub mod rebase;
27
  pub mod wa;
 
22
  pub mod game_info;
23
  pub mod scheme;
24
  pub mod speech;
25
+ pub mod turn_order;
26
  pub mod address;
27
  pub mod rebase;
28
  pub mod wa;
crates/openwa-core/src/turn_order.rs ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Turn order display widget — the in-game team banner list with health bars.
2
+ //!
3
+ //! Three-level hierarchy of UI components:
4
+ //! - [`TurnOrderWidget`] — top level, groups teams by alliance
5
+ //! - [`TurnOrderAllianceGroup`] — per-alliance, holds teams sharing an alliance
6
+ //! - [`TurnOrderTeamEntry`] — per-team, renders banner + health bar
7
+ //!
8
+ //! All inherit from an animated item list base class (vtable 0x669C90) that
9
+ //! provides sin-table interpolated sliding transitions between items.
10
+
11
+ use crate::ddgame::DDGame;
12
+
13
+ /// Animated item list — base class for the turn order hierarchy.
14
+ ///
15
+ /// Generic container with count/capacity/item array and animation state
16
+ /// for smooth sliding transitions between items using sin-table interpolation.
17
+ ///
18
+ /// Vtable: 0x669C90. Constructor: 0x5457D0.
19
+ /// Size: 0x2C bytes (11 DWORDs).
20
+ #[repr(C)]
21
+ pub struct AnimatedItemList {
22
+ /// 0x00: Vtable pointer.
23
+ pub vtable: u32,
24
+ /// 0x04: Number of items currently in the list.
25
+ pub count: i32,
26
+ /// 0x08: Maximum capacity (6 for turn order lists).
27
+ pub capacity: i32,
28
+ /// 0x0C: Pointer to allocated array of item pointers.
29
+ pub items: *mut *mut u8,
30
+ /// 0x10: Current active item index (-1 = none).
31
+ pub current_index: i32,
32
+ /// 0x14: Animation progress counter.
33
+ pub animation_progress: i32,
34
+ /// 0x18: Animation mode (0 = none, 1 = slide out, 2 = slide in).
35
+ pub animation_mode: i32,
36
+ /// 0x1C: Target item index for animation (-1 = none).
37
+ pub animation_target: i32,
38
+ /// 0x20: Animation offset.
39
+ pub animation_offset: i32,
40
+ /// 0x24: Unknown.
41
+ pub _unknown_24: i32,
42
+ /// 0x28: Initialized flag (set to 1 after setup).
43
+ pub initialized: i32,
44
+ }
45
+
46
+ const _: () = assert!(core::mem::size_of::<AnimatedItemList>() == 0x2C);
47
+
48
+ /// Turn order widget — top-level UI component at DDGame+0x530.
49
+ ///
50
+ /// Groups teams by alliance. Each alliance gets a [`TurnOrderAllianceGroup`].
51
+ /// Inherits animated sliding from [`AnimatedItemList`].
52
+ ///
53
+ /// Vtable: 0x66A088. Constructor: 0x563D40 (stdcall, params: this, DDGame*).
54
+ /// Destructor: 0x563E90. Size: 0x4C bytes.
55
+ #[repr(C)]
56
+ pub struct TurnOrderWidget {
57
+ /// 0x00-0x2B: AnimatedItemList base class.
58
+ pub base: AnimatedItemList,
59
+ /// 0x2C: Number of unique alliances.
60
+ pub alliance_count: i32,
61
+ /// 0x30-0x44: Alliance group pointers (up to 6).
62
+ pub alliance_groups: [*mut TurnOrderAllianceGroup; 6],
63
+ /// 0x48: DDGame pointer (back-reference).
64
+ pub ddgame: *mut DDGame,
65
+ }
66
+
67
+ const _: () = assert!(core::mem::size_of::<TurnOrderWidget>() == 0x4C);
68
+
69
+ /// Turn order alliance group — per-alliance container.
70
+ ///
71
+ /// Holds a list of teams that share the same alliance.
72
+ /// Calls [`AnimatedItemList`] constructor but repurposes base fields.
73
+ ///
74
+ /// Vtable: 0x66A04C. Constructor: 0x563B50. Size: 0x30 bytes.
75
+ #[repr(C)]
76
+ pub struct TurnOrderAllianceGroup {
77
+ /// 0x00: Vtable pointer (0x66A04C).
78
+ pub vtable: u32,
79
+ /// 0x04: Alliance ID.
80
+ pub alliance_id: i32,
81
+ /// 0x08: DDGame pointer.
82
+ pub ddgame: *mut DDGame,
83
+ /// 0x0C: Inner team list (AnimatedItemList holding TurnOrderTeamEntry items).
84
+ pub team_list: *mut AnimatedItemList,
85
+ /// 0x10-0x2F: Unknown (zeroed, not set by constructor).
86
+ pub _unknown_10: [u8; 0x30 - 0x10],
87
+ }
88
+
89
+ const _: () = assert!(core::mem::size_of::<TurnOrderAllianceGroup>() == 0x30);
90
+
91
+ /// Turn order team entry — per-team banner with health bar.
92
+ ///
93
+ /// Renders the team banner, name text, and health bar in the turn order panel.
94
+ /// The health bar width comes from DDGame.team_health_ratio[team_index]:
95
+ /// `bar_pixels = ratio * 100 >> 16 + 4`.
96
+ ///
97
+ /// Vtable: 0x669FA8. Constructor: 0x5630B0. Render: 0x563620. Size: 0x24 bytes.
98
+ #[repr(C)]
99
+ pub struct TurnOrderTeamEntry {
100
+ /// 0x00: Vtable pointer (0x669FA8).
101
+ pub vtable: u32,
102
+ /// 0x04: Team index (1-based).
103
+ pub team_index: i32,
104
+ /// 0x08: Color/style index (from game_info per-team data).
105
+ pub color_index: i32,
106
+ /// 0x0C: DDGame pointer (back-reference for accessing health ratios etc).
107
+ pub ddgame: *mut DDGame,
108
+ /// 0x10: Textbox object pointer (DDDisplay text renderer, 0x158 bytes).
109
+ /// NULL when sound is disabled (headless mode).
110
+ pub textbox: *mut u8,
111
+ /// 0x14: DisplayGfx sprite for the health bar (vtable 0x664144).
112
+ pub health_bar_sprite: *mut u8,
113
+ /// 0x18: Cached bar width (pixels). Redrawn when this changes.
114
+ pub cached_bar_width: i32,
115
+ /// 0x1C: Cached flash state (eliminated team flashing).
116
+ pub cached_flash_state: i32,
117
+ /// 0x20: Bar height (pixels).
118
+ pub bar_height: i32,
119
+ }
120
+
121
+ const _: () = assert!(core::mem::size_of::<TurnOrderTeamEntry>() == 0x24);
crates/openwa-wormkit/src/replacements/input.rs CHANGED
@@ -10,7 +10,9 @@
10
  //!
11
  //! This is the same mechanism triggered by key 0x35 (spacebar) during replay.
12
 
13
- use std::sync::atomic::{AtomicU32, Ordering};
 
 
14
 
15
  use crate::hook;
16
  use crate::log_line;
@@ -19,9 +21,16 @@ use openwa_core::address::va;
19
  use openwa_core::ddgame::DDGame;
20
  use openwa_core::ddgame_wrapper::DDGameWrapper;
21
 
 
 
 
 
22
  /// Trampoline to the original TurnManager_ProcessFrame.
23
  static ORIG_TURN_MANAGER: AtomicU32 = AtomicU32::new(0);
24
 
 
 
 
25
  /// Get the DDGame pointer (session+0xA0 → DDGameWrapper.ddgame).
26
  #[inline]
27
  unsafe fn get_ddgame() -> *mut DDGame {
@@ -36,10 +45,94 @@ unsafe fn get_ddgame() -> *mut DDGame {
36
  (*wrapper_ptr).ddgame
37
  }
38
 
39
- /// Hook for TurnManager_ProcessFrame (stdcall, 1 param = TurnGame*).
 
 
 
 
 
 
40
  ///
41
- /// Called every frame from TurnGame_HandleMessage case 2 (FrameFinish).
42
- /// Sets DDGame.fast_forward_active = 1 to enable multi-frame processing.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  unsafe extern "stdcall" fn hook_turn_manager(turngame: u32) {
44
  // Call original first
45
  let orig: unsafe extern "stdcall" fn(u32) =
@@ -51,16 +144,19 @@ unsafe extern "stdcall" fn hook_turn_manager(turngame: u32) {
51
  return;
52
  }
53
 
54
- // Set fast-forward active flag
55
- (*ddgame).fast_forward_active = 1;
 
 
56
  }
57
 
58
  pub fn install() -> Result<(), String> {
59
- if std::env::var("OPENWA_REPLAY_TEST").is_err() {
60
- return Ok(());
 
61
  }
62
 
63
- let _ = log_line("[Input] Replay test mode — hooking TurnManager_ProcessFrame for fast-forward");
64
 
65
  unsafe {
66
  let trampoline = hook::install(
 
10
  //!
11
  //! This is the same mechanism triggered by key 0x35 (spacebar) during replay.
12
 
13
+ #![allow(dead_code)]
14
+
15
+ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
16
 
17
  use crate::hook;
18
  use crate::log_line;
 
21
  use openwa_core::ddgame::DDGame;
22
  use openwa_core::ddgame_wrapper::DDGameWrapper;
23
 
24
+ extern "system" {
25
+ fn IsBadReadPtr(lp: *const u8, ucb: u32) -> i32;
26
+ }
27
+
28
  /// Trampoline to the original TurnManager_ProcessFrame.
29
  static ORIG_TURN_MANAGER: AtomicU32 = AtomicU32::new(0);
30
 
31
+ /// Whether to set fast-forward flag (only in replay test mode).
32
+ static FAST_FORWARD: AtomicBool = AtomicBool::new(false);
33
+
34
  /// Get the DDGame pointer (session+0xA0 → DDGameWrapper.ddgame).
35
  #[inline]
36
  unsafe fn get_ddgame() -> *mut DDGame {
 
45
  (*wrapper_ptr).ddgame
46
  }
47
 
48
+ /// Check if a pointer is safe to read.
49
+ #[inline]
50
+ pub unsafe fn can_read(ptr: u32, size: u32) -> bool {
51
+ ptr >= 0x10000 && IsBadReadPtr(ptr as *const u8, size) == 0
52
+ }
53
+
54
+ /// Dump a region of DDGame as DWORDs with classification.
55
  ///
56
+ /// Classifies each non-zero DWORD as vtable (.rdata), code (.text), data,
57
+ /// heap object (dereferences to check for vtable), small integer, or raw value.
58
+ pub unsafe fn dump_region(ddgame: *mut DDGame, offset: usize, size: usize, label: &str) {
59
+ let base_ptr = ddgame as *const u8;
60
+ let wa_base = rb(va::IMAGE_BASE);
61
+ let delta = wa_base.wrapping_sub(va::IMAGE_BASE);
62
+
63
+ let _ = log_line(&format!("\n=== DDGame+0x{:04X}..0x{:04X}: {} ===", offset, offset + size, label));
64
+
65
+ let dword_count = size / 4;
66
+ for i in 0..dword_count {
67
+ let field_offset = offset + i * 4;
68
+ let val = *(base_ptr.add(field_offset) as *const u32);
69
+ if val == 0 {
70
+ continue; // Skip zeros to reduce noise
71
+ }
72
+
73
+ let ghidra_val = val.wrapping_sub(delta);
74
+
75
+ // Check if value itself is in .rdata (direct vtable pointer)
76
+ if ghidra_val >= va::RDATA_START && ghidra_val < va::DATA_START {
77
+ if can_read(val, 4) {
78
+ let vt0 = *(val as *const u32);
79
+ let _ = log_line(&format!(
80
+ " +0x{:04X}: 0x{:08X} [VTABLE] g:0x{:08X} vt[0]=g:0x{:08X}",
81
+ field_offset, val, ghidra_val, vt0.wrapping_sub(delta)
82
+ ));
83
+ } else {
84
+ let _ = log_line(&format!(
85
+ " +0x{:04X}: 0x{:08X} [VTABLE] g:0x{:08X} (unreadable)",
86
+ field_offset, val, ghidra_val
87
+ ));
88
+ }
89
+ } else if ghidra_val >= va::TEXT_START && ghidra_val <= va::TEXT_END {
90
+ let _ = log_line(&format!(
91
+ " +0x{:04X}: 0x{:08X} [CODE] g:0x{:08X}",
92
+ field_offset, val, ghidra_val
93
+ ));
94
+ } else if ghidra_val >= va::DATA_START && ghidra_val < 0x008C5000 {
95
+ let _ = log_line(&format!(
96
+ " +0x{:04X}: 0x{:08X} [DATA] g:0x{:08X}",
97
+ field_offset, val, ghidra_val
98
+ ));
99
+ } else if val < 0x10000 {
100
+ let _ = log_line(&format!(
101
+ " +0x{:04X}: 0x{:08X} [small={}]",
102
+ field_offset, val, val
103
+ ));
104
+ } else if can_read(val, 4) {
105
+ // Heap pointer — safely read first DWORD to check for vtable
106
+ let first = *(val as *const u32);
107
+ let ghidra_first = first.wrapping_sub(delta);
108
+ if ghidra_first >= va::RDATA_START && ghidra_first < va::DATA_START {
109
+ // It's an object with a vtable!
110
+ let vt0_str = if can_read(first, 4) {
111
+ let vt0 = *(first as *const u32);
112
+ format!("vt[0]=g:0x{:08X}", vt0.wrapping_sub(delta))
113
+ } else {
114
+ "vt[0]=?".to_string()
115
+ };
116
+ let _ = log_line(&format!(
117
+ " +0x{:04X}: 0x{:08X} [OBJECT] vtable=g:0x{:08X} {}",
118
+ field_offset, val, ghidra_first, vt0_str
119
+ ));
120
+ } else {
121
+ let _ = log_line(&format!(
122
+ " +0x{:04X}: 0x{:08X} [ptr] *=0x{:08X}",
123
+ field_offset, val, first
124
+ ));
125
+ }
126
+ } else {
127
+ let _ = log_line(&format!(
128
+ " +0x{:04X}: 0x{:08X} [value]",
129
+ field_offset, val
130
+ ));
131
+ }
132
+ }
133
+ }
134
+
135
+ /// Hook for TurnManager_ProcessFrame (stdcall, 1 param = TurnGame*).
136
  unsafe extern "stdcall" fn hook_turn_manager(turngame: u32) {
137
  // Call original first
138
  let orig: unsafe extern "stdcall" fn(u32) =
 
144
  return;
145
  }
146
 
147
+ // Fast-forward for replay test
148
+ if FAST_FORWARD.load(Ordering::Relaxed) {
149
+ (*ddgame).fast_forward_active = 1;
150
+ }
151
  }
152
 
153
  pub fn install() -> Result<(), String> {
154
+ if std::env::var("OPENWA_REPLAY_TEST").is_ok() {
155
+ FAST_FORWARD.store(true, Ordering::Relaxed);
156
+ let _ = log_line("[Input] Replay test mode — fast-forward enabled");
157
  }
158
 
159
+ let _ = log_line("[Input] Hooking TurnManager_ProcessFrame");
160
 
161
  unsafe {
162
  let trampoline = hook::install(