feat: add vcall! macro and typed vtable structs for Palette and InputCtrl
Browse filesAdd 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 |
-
##
|
| 32 |
|
| 33 |
-
|
| 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: *
|
| 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: *
|
| 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 *
|
| 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 |
-
|
| 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 *
|
| 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 |
-
|
| 468 |
-
|
| 469 |
-
|
| 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;
|