paavohtl Claude Opus 4.6 (1M context) commited on
Commit
3f54e7c
·
1 Parent(s): cbd02f2

refactor: add typed fields for sound dispatch, remove pointer arithmetic

Browse files

Add DDGame.frame_counter (0x5CC) and GameInfo sound_start_frame
(0xF344) / sound_mute (0xF348) as proper struct fields. Use typed
pointers in all hook signatures instead of u32 casts — 32-bit target
means pointers and u32 are interchangeable in ABI.

Also remove DSSound vtable from validation (all 24 slots are Rust).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -120,13 +120,25 @@ pub struct DDGame {
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.
 
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-0x5CB: Unknown.
124
  ///
125
  /// Runtime observations (not yet linked to code):
126
  /// - 0x5C4: value matches code address 0x5755D0 (fixed-point normalize fn)
127
+ /// - 0x5C8-0x5CB: config-like values
128
+ pub _unknown_550: [u8; 0x5CC - 0x550],
129
+
130
+ /// 0x5CC: Game frame counter. Incremented at end of each frame in
131
+ /// GameFrameDispatcher. Compared against GameInfo+0xF344 (sound start
132
+ /// threshold) by IsSoundSuppressed and DispatchGlobalSound.
133
+ pub frame_counter: i32,
134
+
135
+ /// 0x5D0-0x25FF: Large unverified region.
136
+ ///
137
+ /// Runtime observations:
138
+ /// - 0x5D0: frame processing flag (set 0/1 by GameFrameEndProcessor)
139
+ /// - 0x5D8-0x5FF: small config-like values (2048, 150, 3000, 696, 896, 100, 300)
140
  /// - 0x600-0x25FF: identity permutation [0,1,2,...,~2048] — purpose unknown
141
+ pub _unknown_5d0: [u8; 0x2600 - 0x5D0],
142
 
143
  /// 0x2600-0x2DFF: Block of 0xFFFFFFFF values at runtime (512 i32 entries).
144
  /// May be unused slots in a parallel table to the 0x600 permutation.
crates/openwa-core/src/engine/game_info.rs CHANGED
@@ -46,8 +46,20 @@ pub struct GameInfo {
46
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
47
  pub land_dat_path: [u8; 14],
48
 
49
- /// 0xDAFA-0xF373: Unknown
50
- pub _unknown_dafa: [u8; 0xF374 - 0xDAFA],
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  /// 0xF374: Display flags passed to DDDisplay::Init.
53
  pub display_flags: u32,
 
46
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
47
  pub land_dat_path: [u8; 14],
48
 
49
+ /// 0xDAFA-0xF343: Unknown
50
+ pub _unknown_dafa: [u8; 0xF344 - 0xDAFA],
51
+
52
+ /// 0xF344: Sound start frame threshold (i32). Sound is suppressed when
53
+ /// DDGame.frame_counter < this value. Checked by IsSoundSuppressed and
54
+ /// DispatchGlobalSound.
55
+ pub sound_start_frame: i32,
56
+
57
+ /// 0xF348: Sound mute flag (byte). Nonzero = all sound suppressed.
58
+ /// Checked by IsSoundSuppressed, DispatchGlobalSound, PlaySoundPooled_Direct.
59
+ pub sound_mute: u8,
60
+
61
+ /// 0xF349-0xF373: Unknown
62
+ pub _unknown_f349: [u8; 0xF374 - 0xF349],
63
 
64
  /// 0xF374: Display flags passed to DDDisplay::Init.
65
  pub display_flags: u32,
crates/openwa-wormkit/src/replacements/sound.rs CHANGED
@@ -10,9 +10,9 @@
10
  use std::sync::atomic::Ordering;
11
 
12
  use openwa_core::address::va;
13
- use openwa_core::engine::{DDGame, SoundQueueEntry};
14
  use openwa_core::audio::SoundId;
15
- use openwa_core::task::CGameTask;
16
 
17
  use crate::hook;
18
  use crate::log_line;
@@ -63,7 +63,7 @@ unsafe fn queue_sound(
63
  // __thiscall: ECX = CTask* this, 4 stack params, RET 0x10
64
 
65
  unsafe extern "thiscall" fn hook_play_sound_global(
66
- this: u32,
67
  sound_id: u32,
68
  flags: u32,
69
  volume: u32,
@@ -74,13 +74,12 @@ unsafe extern "thiscall" fn hook_play_sound_global(
74
  .map(|s| format!("{s:?}"))
75
  .unwrap_or_else(|v| format!("#{v}"));
76
  let _ = log_line(&format!(
77
- "[Sound] Global: task=0x{this:08X} id={sound_id}({sound_name}) \
78
  p3={flags} p4={volume} p5={pitch}"
79
  ));
80
  }
81
 
82
- let task = &*(this as *const CGameTask);
83
- queue_sound(task.base.ddgame, sound_id, flags, volume, pitch).is_some() as u32
84
  }
85
 
86
  // ============================================================
@@ -95,7 +94,7 @@ hook::usercall_trampoline!(fn trampoline_play_sound_local; impl_fn = play_sound_
95
  unsafe extern "cdecl" fn play_sound_local_impl(
96
  pitch: u32,
97
  volume: u32,
98
- task: u32,
99
  sound_id: u32,
100
  flags: u32,
101
  ) -> u32 {
@@ -104,12 +103,12 @@ unsafe extern "cdecl" fn play_sound_local_impl(
104
  .map(|s| format!("{s:?}"))
105
  .unwrap_or_else(|v| format!("#{v}"));
106
  let _ = log_line(&format!(
107
- "[Sound] Local: eax={pitch} ecx=0x{volume:08X} task=0x{task:08X} \
108
  id={sound_id}({sound_name}) flags={flags}"
109
  ));
110
  }
111
 
112
- let gt = &*(task as *const CGameTask);
113
  let ddgame = gt.base.ddgame;
114
  let entry = match queue_sound(ddgame, sound_id, flags, volume, pitch) {
115
  Some(e) => e,
@@ -127,8 +126,7 @@ unsafe extern "cdecl" fn play_sound_local_impl(
127
  ((*emitter.vtable).get_position)(emitter, &mut (*entry).pos_x, &mut (*entry).pos_y);
128
 
129
  // Increment local sound count
130
- let gt_mut = &mut *(task as *mut CGameTask);
131
- gt_mut.sound_emitter.local_sound_count += 1;
132
 
133
  1
134
  }
@@ -139,50 +137,37 @@ unsafe extern "cdecl" fn play_sound_local_impl(
139
 
140
  /// IsSoundSuppressed (0x5261E0) — thiscall(ECX=DDGame*), plain RET.
141
  /// Returns 0 if sound playback is allowed, 1 if suppressed.
142
- ///
143
- /// Checks:
144
- /// - GameInfo+0xF348 (byte): sound mute flag
145
- /// - DDGame+0x5CC (i32) vs GameInfo+0xF344 (i32): frame counter ≥ threshold
146
  unsafe extern "thiscall" fn hook_is_sound_suppressed(ddgame: *mut DDGame) -> u32 {
147
- let gi = (*ddgame).game_info as *const u8;
148
- // Mute flag
149
- if *gi.add(0xF348) != 0 {
150
  return 1;
151
  }
152
- // Frame counter must be ≥ sound start threshold
153
- let frame_counter = *(ddgame as *const u8).add(0x5CC).cast::<i32>();
154
- let threshold = *gi.add(0xF344).cast::<i32>();
155
- if frame_counter < threshold {
156
  return 1;
157
  }
158
  0
159
  }
160
 
161
- /// DispatchGlobalSound (0x526270) — fastcall(ECX=unused, EDX=task_turn_game) + 4 stack, RET 0x10.
162
  /// Checks sound suppression, then calls DSSound vtable slot 3 (play_sound).
163
  unsafe extern "fastcall" fn hook_dispatch_global_sound(
164
  _ecx: u32,
165
- task_turn_game: u32,
166
  sound_id: u32,
167
  flags: u32,
168
  volume: u32,
169
  pitch: u32,
170
  ) -> u32 {
171
- let ddgame = *((task_turn_game as *const u8).add(0x488) as *const *mut DDGame);
172
- let gi = (*ddgame).game_info as *const u8;
173
 
174
- // Suppression check (same logic as IsSoundSuppressed but also different return for suppressed)
175
- if *gi.add(0xF348) != 0 {
176
- return 0xFFFF_FFFF; // -1
177
- }
178
- let frame_counter = *(ddgame as *const u8).add(0x5CC).cast::<i32>();
179
- let threshold = *gi.add(0xF344).cast::<i32>();
180
- if frame_counter < threshold {
181
- return 0xFFFF_FFFF; // -1
182
  }
183
 
184
- // Get DSSound
185
- let dssound = (*ddgame).sound;
186
  if dssound.is_null() {
187
  return 0;
188
  }
@@ -198,32 +183,25 @@ unsafe extern "fastcall" fn hook_dispatch_global_sound(
198
  /// Bypasses queue, checks suppression + fast-forward, calls DSSound vtable slot 4 directly.
199
  unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
200
  _ecx: u32,
201
- task: u32,
202
  param1: u32,
203
  param2: u32,
204
  param3: u32,
205
  ) -> u32 {
206
- let ddgame = *((task as *const u8).add(0x2C) as *const *mut DDGame);
207
- let gi = (*ddgame).game_info as *const u8;
208
 
209
  // Suppression check
210
- if *gi.add(0xF348) != 0 {
211
- return 0xFFFF_FFFF;
212
- }
213
- let frame_counter = *(ddgame as *const u8).add(0x5CC).cast::<i32>();
214
- let threshold = *gi.add(0xF344).cast::<i32>();
215
- if frame_counter < threshold {
216
  return 0xFFFF_FFFF;
217
  }
218
 
219
  // Fast-forward check (unique to this function)
220
- let ff_active = *(ddgame as *const u8).add(0x98B0).cast::<i32>();
221
- if ff_active != 0 {
222
  return 0xFFFF_FFFF;
223
  }
224
 
225
- // Get DSSound
226
- let dssound = (*ddgame).sound;
227
  if dssound.is_null() {
228
  return 0;
229
  }
 
10
  use std::sync::atomic::Ordering;
11
 
12
  use openwa_core::address::va;
13
+ use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
14
  use openwa_core::audio::SoundId;
15
+ use openwa_core::task::{CGameTask, CTask};
16
 
17
  use crate::hook;
18
  use crate::log_line;
 
63
  // __thiscall: ECX = CTask* this, 4 stack params, RET 0x10
64
 
65
  unsafe extern "thiscall" fn hook_play_sound_global(
66
+ this: *const CGameTask,
67
  sound_id: u32,
68
  flags: u32,
69
  volume: u32,
 
74
  .map(|s| format!("{s:?}"))
75
  .unwrap_or_else(|v| format!("#{v}"));
76
  let _ = log_line(&format!(
77
+ "[Sound] Global: task=0x{this:08X?} id={sound_id}({sound_name}) \
78
  p3={flags} p4={volume} p5={pitch}"
79
  ));
80
  }
81
 
82
+ queue_sound((*this).base.ddgame, sound_id, flags, volume, pitch).is_some() as u32
 
83
  }
84
 
85
  // ============================================================
 
94
  unsafe extern "cdecl" fn play_sound_local_impl(
95
  pitch: u32,
96
  volume: u32,
97
+ task: *mut CGameTask,
98
  sound_id: u32,
99
  flags: u32,
100
  ) -> u32 {
 
103
  .map(|s| format!("{s:?}"))
104
  .unwrap_or_else(|v| format!("#{v}"));
105
  let _ = log_line(&format!(
106
+ "[Sound] Local: eax={pitch} ecx=0x{volume:08X} task=0x{task:08X?} \
107
  id={sound_id}({sound_name}) flags={flags}"
108
  ));
109
  }
110
 
111
+ let gt = &*task;
112
  let ddgame = gt.base.ddgame;
113
  let entry = match queue_sound(ddgame, sound_id, flags, volume, pitch) {
114
  Some(e) => e,
 
126
  ((*emitter.vtable).get_position)(emitter, &mut (*entry).pos_x, &mut (*entry).pos_y);
127
 
128
  // Increment local sound count
129
+ (*task).sound_emitter.local_sound_count += 1;
 
130
 
131
  1
132
  }
 
137
 
138
  /// IsSoundSuppressed (0x5261E0) — thiscall(ECX=DDGame*), plain RET.
139
  /// Returns 0 if sound playback is allowed, 1 if suppressed.
 
 
 
 
140
  unsafe extern "thiscall" fn hook_is_sound_suppressed(ddgame: *mut DDGame) -> u32 {
141
+ let g = &*ddgame;
142
+ let gi = &*g.game_info;
143
+ if gi.sound_mute != 0 {
144
  return 1;
145
  }
146
+ if g.frame_counter < gi.sound_start_frame {
 
 
 
147
  return 1;
148
  }
149
  0
150
  }
151
 
152
+ /// DispatchGlobalSound (0x526270) — fastcall(ECX=unused, EDX=ddgame_wrapper) + 4 stack, RET 0x10.
153
  /// Checks sound suppression, then calls DSSound vtable slot 3 (play_sound).
154
  unsafe extern "fastcall" fn hook_dispatch_global_sound(
155
  _ecx: u32,
156
+ ddgame_wrapper: *const DDGameWrapper,
157
  sound_id: u32,
158
  flags: u32,
159
  volume: u32,
160
  pitch: u32,
161
  ) -> u32 {
162
+ let g = &*(*ddgame_wrapper).ddgame;
163
+ let gi = &*g.game_info;
164
 
165
+ // Suppression check — returns -1 if muted or before sound start frame
166
+ if gi.sound_mute != 0 || g.frame_counter < gi.sound_start_frame {
167
+ return 0xFFFF_FFFF;
 
 
 
 
 
168
  }
169
 
170
+ let dssound = g.sound;
 
171
  if dssound.is_null() {
172
  return 0;
173
  }
 
183
  /// Bypasses queue, checks suppression + fast-forward, calls DSSound vtable slot 4 directly.
184
  unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
185
  _ecx: u32,
186
+ task: *const CTask,
187
  param1: u32,
188
  param2: u32,
189
  param3: u32,
190
  ) -> u32 {
191
+ let g = &*(*task).ddgame;
192
+ let gi = &*g.game_info;
193
 
194
  // Suppression check
195
+ if gi.sound_mute != 0 || g.frame_counter < gi.sound_start_frame {
 
 
 
 
 
196
  return 0xFFFF_FFFF;
197
  }
198
 
199
  // Fast-forward check (unique to this function)
200
+ if g.fast_forward_active != 0 {
 
201
  return 0xFFFF_FFFF;
202
  }
203
 
204
+ let dssound = g.sound;
 
205
  if dssound.is_null() {
206
  return 0;
207
  }