paavohtl Claude Opus 4.6 (1M context) commited on
Commit
17e3bdc
Β·
1 Parent(s): 6795996

feat: add vcall! macro and typed vtable structs for Palette and InputCtrl

Browse files

Add vcall! macro for one-liner vtable method dispatch, replacing the
3-line transmute boilerplate. Define PaletteVtable (slots 2-4) and
InputCtrlVtable (slot 0 = destructor) with typed function pointers.
Add impl method wrappers so callers write (*pal).reset() instead of
manual vtable indexing.

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

CLAUDE.md CHANGED
@@ -28,19 +28,9 @@ cargo test --target i686-pc-windows-msvc -p openwa-harness
28
  cargo test -p openwa-core -- scheme_parse::parse_beginner_v2
29
  ```
30
 
31
- ## Deploy to Game
32
 
33
- Game directory: `I:\games\SteamLibrary\steamapps\common\Worms Armageddon`
34
-
35
- Copy the unified DLL from `target/i686-pc-windows-msvc/release/` to the game directory:
36
- - `openwa_wormkit.dll` β†’ rename to `wkOpenWA.dll`
37
-
38
- Quick deploy:
39
- ```bash
40
- cp target/i686-pc-windows-msvc/release/openwa_wormkit.dll "I:/games/SteamLibrary/steamapps/common/Worms Armageddon/wkOpenWA.dll"
41
- ```
42
-
43
- WormKit auto-loads any `wk*.dll` from the game directory. Logs appear in the game directory as `OpenWA.log` and `OpenWA_validation.log` (when validation is enabled).
44
 
45
  ## Replay Testing
46
 
@@ -133,6 +123,9 @@ A Ghidra MCP bridge is configured in `.mcp.json`. When using Ghidra tools:
133
  - Unknown struct fields as `_unknown_XX` padding arrays
134
  - Fixed-point: `Fixed(i32)` newtype, 16.16 format (0x10000 = 1.0)
135
  - Naked asm uses `naked_asm!` (Rust 1.79+ syntax), not `asm!`
 
 
 
136
 
137
  ## FFI Style
138
 
 
28
  cargo test -p openwa-core -- scheme_parse::parse_beginner_v2
29
  ```
30
 
31
+ ## How to run the game with the DLL
32
 
33
+ It used to be necessary to copy the built DLLs to the game directory and launch WA.exe. **HOWEVER**, we now have the launcher crate (`openwa-launcher`) that automatically starts the game with the correct DLL injected.
 
 
 
 
 
 
 
 
 
 
34
 
35
  ## Replay Testing
36
 
 
123
  - Unknown struct fields as `_unknown_XX` padding arrays
124
  - Fixed-point: `Fixed(i32)` newtype, 16.16 format (0x10000 = 1.0)
125
  - Naked asm uses `naked_asm!` (Rust 1.79+ syntax), not `asm!`
126
+ - **Typed vtable structs**: Define `#[repr(C)]` vtable structs with typed function pointers for known slots and `usize` for unknown slots (see `PaletteVtable`, `SoundEmitterVTable`). Set the class struct's vtable field to `*const FooVtable` instead of `*mut u8`.
127
+ - **`vcall!` macro**: Use `vcall!(obj, method, args...)` for one-liner vtable dispatch. Expands to `((*(*obj).vtable).method)(obj, args...)`.
128
+ - **Virtual method wrappers**: Add `impl` methods on the class struct that wrap `vcall!`. Callers write `(*obj).method(args)` β€” idiomatic, type-safe, and hides the vtable indirection.
129
 
130
  ## FFI Style
131
 
crates/openwa-core/src/input_ctrl.rs CHANGED
@@ -11,7 +11,7 @@
11
  #[repr(C)]
12
  pub struct InputCtrl {
13
  /// 0x000: Vtable pointer (0x66B3FC)
14
- pub vtable: *mut u8,
15
  pub _unknown_004: [u8; 0xD74 - 4],
16
  /// 0xD74: Set to 0x3F9 during inline construction.
17
  pub _field_d74: u32,
@@ -19,3 +19,17 @@ pub struct InputCtrl {
19
  }
20
 
21
  const _: () = assert!(core::mem::size_of::<InputCtrl>() == 0x1800);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  #[repr(C)]
12
  pub struct InputCtrl {
13
  /// 0x000: Vtable pointer (0x66B3FC)
14
+ pub vtable: *const InputCtrlVtable,
15
  pub _unknown_004: [u8; 0xD74 - 4],
16
  /// 0xD74: Set to 0x3F9 during inline construction.
17
  pub _field_d74: u32,
 
19
  }
20
 
21
  const _: () = assert!(core::mem::size_of::<InputCtrl>() == 0x1800);
22
+
23
+ /// Vtable for InputCtrl (0x66B3FC).
24
+ ///
25
+ /// Only slot 0 (destructor) is known β€” called on init failure cleanup.
26
+ #[repr(C)]
27
+ pub struct InputCtrlVtable {
28
+ /// [0]: Destructor(this, flags) β€” scalar deleting destructor
29
+ pub destructor: unsafe extern "thiscall" fn(*mut InputCtrl, u32),
30
+ }
31
+
32
+ impl InputCtrl {
33
+ /// Vtable[0]: Destroy and optionally free (flags & 1 = free).
34
+ pub unsafe fn destroy(&mut self, flags: u32) { vcall!(self, destructor, flags) }
35
+ }
crates/openwa-core/src/lib.rs CHANGED
@@ -1,5 +1,8 @@
1
  #![allow(non_snake_case)]
2
 
 
 
 
3
  pub mod active_sound;
4
  pub mod class_type;
5
  pub mod message;
 
1
  #![allow(non_snake_case)]
2
 
3
+ #[macro_use]
4
+ pub mod macros;
5
+
6
  pub mod active_sound;
7
  pub mod class_type;
8
  pub mod message;
crates/openwa-core/src/macros.rs ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// Call a virtual method through a typed vtable pointer.
2
+ ///
3
+ /// Assumes `$obj` is a raw pointer to a struct whose first field `vtable`
4
+ /// is `*const SomeVtable`, and `$method` is a function pointer field on
5
+ /// that vtable struct.
6
+ ///
7
+ /// # Examples
8
+ ///
9
+ /// ```ignore
10
+ /// // Single arg (thiscall β€” `self` is implicit in ECX):
11
+ /// vcall!(pal, reset);
12
+ ///
13
+ /// // With extra arguments:
14
+ /// vcall!(pal, set_mode, 7);
15
+ /// ```
16
+ ///
17
+ /// Expands to: `((*(*$obj).vtable).$method)($obj, $($args),*)`
18
+ #[macro_export]
19
+ macro_rules! vcall {
20
+ ($obj:expr, $method:ident $(, $args:expr)*) => {
21
+ ((*(*$obj).vtable).$method)($obj $(, $args)*)
22
+ };
23
+ }
crates/openwa-core/src/palette.rs CHANGED
@@ -8,7 +8,7 @@
8
  #[repr(C)]
9
  pub struct Palette {
10
  /// 0x000: Vtable pointer (0x66A2E4)
11
- pub vtable: *mut u8,
12
  /// 0x004: Initialized to 0xFFFFFFFF during inline construction.
13
  pub _field_004: u32,
14
  /// 0x008-0x027: Unknown
@@ -16,3 +16,30 @@ pub struct Palette {
16
  }
17
 
18
  const _: () = assert!(core::mem::size_of::<Palette>() == 0x28);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  #[repr(C)]
9
  pub struct Palette {
10
  /// 0x000: Vtable pointer (0x66A2E4)
11
+ pub vtable: *const PaletteVtable,
12
  /// 0x004: Initialized to 0xFFFFFFFF during inline construction.
13
  pub _field_004: u32,
14
  /// 0x008-0x027: Unknown
 
16
  }
17
 
18
  const _: () = assert!(core::mem::size_of::<Palette>() == 0x28);
19
+
20
+ /// Vtable for Palette (0x66A2E4).
21
+ ///
22
+ /// Slots 2-4 are called by GameEngine__InitHardware after DDGameWrapper construction.
23
+ /// Slots 0-1 are unknown.
24
+ #[repr(C)]
25
+ pub struct PaletteVtable {
26
+ /// [0]: Unknown
27
+ pub _slot_0: usize,
28
+ /// [1]: Unknown
29
+ pub _slot_1: usize,
30
+ /// [2]: set_mode(this, mode) β€” called with mode=7 during hardware init
31
+ pub set_mode: unsafe extern "thiscall" fn(*mut Palette, u32),
32
+ /// [3]: init(this) β€” called during hardware init
33
+ pub init: unsafe extern "thiscall" fn(*mut Palette),
34
+ /// [4]: reset(this) β€” called first during hardware init
35
+ pub reset: unsafe extern "thiscall" fn(*mut Palette),
36
+ }
37
+
38
+ impl Palette {
39
+ /// Vtable[4]: Reset palette state.
40
+ pub unsafe fn reset(&mut self) { vcall!(self, reset) }
41
+ /// Vtable[3]: Initialize palette.
42
+ pub unsafe fn init(&mut self) { vcall!(self, init) }
43
+ /// Vtable[2]: Set palette mode.
44
+ pub unsafe fn set_mode(&mut self, mode: u32) { vcall!(self, set_mode, mode) }
45
+ }
crates/openwa-wormkit/src/replacements/hardware_init.rs CHANGED
@@ -49,8 +49,8 @@ use openwa_core::ddgame_wrapper::DDGameWrapper;
49
  use openwa_core::ddkeyboard::DDKeyboard;
50
  use openwa_core::dddisplay::DDDisplay;
51
  use openwa_core::dssound::DSSound;
52
- use openwa_core::palette::Palette;
53
- use openwa_core::input_ctrl::InputCtrl;
54
  use openwa_core::game_timer::GameTimer;
55
  use openwa_core::game_stats::GameStats;
56
  use openwa_core::display_gfx::DisplayGfx;
@@ -266,7 +266,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
266
  } else {
267
  let ctrl = WABox::<InputCtrl>::alloc(0x1800, 0x17E0).leak();
268
  (*ctrl)._field_d74 = 0x3F9;
269
- (*ctrl).vtable = rb(va::INPUT_CTRL_VTABLE) as *mut u8;
270
  (*session).input_ctrl = ctrl as *mut u8;
271
 
272
  // Original passes GameInfo+4 (skips first DWORD of unknown padding).
@@ -274,10 +274,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
274
  INPUT_CTRL_ESI = ctrl as u32;
275
  let ok = call_input_ctrl_init(game_info_plus_4, param3, param4, crosshair_threshold);
276
  if ok == 0 {
277
- let vtbl = *(ctrl as *const *const usize);
278
- let dtor: unsafe extern "thiscall" fn(*mut u8, u32) =
279
- core::mem::transmute(*vtbl);
280
- dtor(ctrl as *mut u8, 1);
281
  (*session).input_ctrl = core::ptr::null_mut();
282
  return 0;
283
  }
@@ -375,7 +372,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
375
 
376
  // ── Palette (inline construction) ─────────────────────────────────────
377
  let pal = WABox::<Palette>::alloc(0x28, 0).leak();
378
- (*pal).vtable = rb(va::PALETTE_VTABLE_MAYBE) as *mut u8;
379
  (*pal)._field_004 = 0xFFFF_FFFF;
380
  (*session).palette = pal;
381
 
@@ -464,16 +461,9 @@ unsafe extern "cdecl" fn impl_init_hardware(
464
  if !headless {
465
  let pal = (*session).palette;
466
  if !pal.is_null() {
467
- let vtbl = *(pal as *const *const usize);
468
- let vt4: unsafe extern "thiscall" fn(*mut Palette) =
469
- core::mem::transmute(*vtbl.add(4));
470
- let vt3: unsafe extern "thiscall" fn(*mut Palette) =
471
- core::mem::transmute(*vtbl.add(3));
472
- let vt2: unsafe extern "thiscall" fn(*mut Palette, u32) =
473
- core::mem::transmute(*vtbl.add(2));
474
- vt4(pal);
475
- vt3(pal);
476
- vt2(pal, 7);
477
  }
478
 
479
  let kb = (*session).keyboard;
 
49
  use openwa_core::ddkeyboard::DDKeyboard;
50
  use openwa_core::dddisplay::DDDisplay;
51
  use openwa_core::dssound::DSSound;
52
+ use openwa_core::palette::{Palette, PaletteVtable};
53
+ use openwa_core::input_ctrl::{InputCtrl, InputCtrlVtable};
54
  use openwa_core::game_timer::GameTimer;
55
  use openwa_core::game_stats::GameStats;
56
  use openwa_core::display_gfx::DisplayGfx;
 
266
  } else {
267
  let ctrl = WABox::<InputCtrl>::alloc(0x1800, 0x17E0).leak();
268
  (*ctrl)._field_d74 = 0x3F9;
269
+ (*ctrl).vtable = rb(va::INPUT_CTRL_VTABLE) as *const InputCtrlVtable;
270
  (*session).input_ctrl = ctrl as *mut u8;
271
 
272
  // Original passes GameInfo+4 (skips first DWORD of unknown padding).
 
274
  INPUT_CTRL_ESI = ctrl as u32;
275
  let ok = call_input_ctrl_init(game_info_plus_4, param3, param4, crosshair_threshold);
276
  if ok == 0 {
277
+ (*ctrl).destroy(1);
 
 
 
278
  (*session).input_ctrl = core::ptr::null_mut();
279
  return 0;
280
  }
 
372
 
373
  // ── Palette (inline construction) ─────────────────────────────────────
374
  let pal = WABox::<Palette>::alloc(0x28, 0).leak();
375
+ (*pal).vtable = rb(va::PALETTE_VTABLE_MAYBE) as *const PaletteVtable;
376
  (*pal)._field_004 = 0xFFFF_FFFF;
377
  (*session).palette = pal;
378
 
 
461
  if !headless {
462
  let pal = (*session).palette;
463
  if !pal.is_null() {
464
+ (*pal).reset();
465
+ (*pal).init();
466
+ (*pal).set_mode(7);
 
 
 
 
 
 
 
467
  }
468
 
469
  let kb = (*session).keyboard;