feat: add #[vtable(...)] attribute macro for typed vtable definitions
Browse filesAdd a proc macro that transforms sparse vtable slot definitions into
full #[repr(C)] structs with auto-filled gap slots, registry metadata,
calling wrappers, and optional address registration. Key features:
- #[slot(N)] for sparse vtables (gaps auto-filled with usize)
- Sequential slot inference when #[slot] is omitted (fully-typed vtables)
- fn(...) auto-normalized to unsafe extern "thiscall" fn(...)
- Named params (fn(this: *mut T, mode: u32)) flow through to bind wrappers
- bind_XxxVtable! companion macro generates method wrappers on class structs
- VtableInfo/VtableSlotEntry registry with inventory integration
- vtable_replace! macro for type-safe in-place vtable slot patching
- Compile-time size assertion and optional pub const + AddrEntry generation
All 8 vtable structs migrated: PaletteVtable, InputCtrlVtable,
MapViewVtable, DDDisplayVtable (38β18 lines), SoundEmitterVTable,
CTaskWormVTable, DisplayBaseVtable, DSSoundVtable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/address.rs +13 -10
- crates/openwa-core/src/audio/dssound.rs +49 -52
- crates/openwa-core/src/display/base.rs +34 -38
- crates/openwa-core/src/display/dd_display.rs +19 -140
- crates/openwa-core/src/display/palette.rs +19 -30
- crates/openwa-core/src/engine/ddgame.rs +17 -20
- crates/openwa-core/src/frontend/map_view.rs +4 -5
- crates/openwa-core/src/input/controller.rs +13 -9
- crates/openwa-core/src/lib.rs +1 -0
- crates/openwa-core/src/macros.rs +53 -0
- crates/openwa-core/src/registry.rs +123 -0
- crates/openwa-core/src/task/game_task.rs +17 -15
- crates/openwa-core/src/task/worm.rs +47 -47
- crates/openwa-derive/src/lib.rs +502 -0
- crates/openwa-wormkit/src/replacements/hardware_init.rs +1 -1
- docs/superpowers/specs/2026-03-25-vtable-derive-design.md +337 -0
|
@@ -61,6 +61,15 @@ pub mod va {
|
|
| 61 |
pub use crate::task::filter::{CTASK_FILTER_VTABLE, CTASK_FILTER_CTOR};
|
| 62 |
pub use crate::task::fire::{CTASK_FIRE_VTABLE, CTASK_FIRE_CTOR};
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
crate::define_addresses! {
|
| 65 |
class "DDGameWrapper" {
|
| 66 |
/// DDGameWrapper vtable
|
|
@@ -123,8 +132,7 @@ pub mod va {
|
|
| 123 |
}
|
| 124 |
|
| 125 |
class "DSSound" {
|
| 126 |
-
//
|
| 127 |
-
vtable DS_SOUND_VTABLE = 0x0066_AF20;
|
| 128 |
/// DSSound constructor β usercall(EAX=this), plain RET
|
| 129 |
ctor/Usercall CONSTRUCT_DS_SOUND = 0x0057_3D50;
|
| 130 |
/// DSSound init buffers β usercall(EAX=dssound), plain RET
|
|
@@ -144,14 +152,10 @@ pub mod va {
|
|
| 144 |
fn/Stdcall DDKEYBOARD_POLL_KEYBOARD_STATE = 0x0057_2290;
|
| 145 |
}
|
| 146 |
|
| 147 |
-
|
| 148 |
-
/// Palette vtable (0x28-byte palette object)
|
| 149 |
-
vtable PALETTE_VTABLE_MAYBE = 0x0066_A2E4;
|
| 150 |
-
}
|
| 151 |
|
| 152 |
class "DisplayBase" {
|
| 153 |
-
//
|
| 154 |
-
vtable DISPLAY_BASE_VTABLE = 0x0066_45F8;
|
| 155 |
/// DisplayBase headless vtable
|
| 156 |
vtable DISPLAY_BASE_HEADLESS_VTABLE = 0x0066_A0F8;
|
| 157 |
/// DisplayBase constructor (0x3560-byte object)
|
|
@@ -159,8 +163,7 @@ pub mod va {
|
|
| 159 |
}
|
| 160 |
|
| 161 |
class "InputCtrl" {
|
| 162 |
-
//
|
| 163 |
-
vtable INPUT_CTRL_VTABLE = 0x0066_B3FC;
|
| 164 |
/// Input controller initializer
|
| 165 |
fn/Usercall INPUT_CTRL_INIT = 0x0058_C0D0;
|
| 166 |
}
|
|
|
|
| 61 |
pub use crate::task::filter::{CTASK_FILTER_VTABLE, CTASK_FILTER_CTOR};
|
| 62 |
pub use crate::task::fire::{CTASK_FIRE_VTABLE, CTASK_FIRE_CTOR};
|
| 63 |
|
| 64 |
+
// Re-exported from modules using #[vtable(...)] attribute
|
| 65 |
+
pub use crate::display::palette::PALETTE_VTABLE;
|
| 66 |
+
pub use crate::display::dd_display::DD_DISPLAY_VTABLE;
|
| 67 |
+
pub use crate::input::controller::INPUT_CTRL_VTABLE;
|
| 68 |
+
pub use crate::frontend::map_view::MAP_VIEW_VTABLE;
|
| 69 |
+
pub use crate::task::game_task::SOUND_EMITTER_VTABLE;
|
| 70 |
+
pub use crate::display::base::DISPLAY_BASE_VTABLE;
|
| 71 |
+
pub use crate::audio::dssound::DS_SOUND_VTABLE;
|
| 72 |
+
|
| 73 |
crate::define_addresses! {
|
| 74 |
class "DDGameWrapper" {
|
| 75 |
/// DDGameWrapper vtable
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
class "DSSound" {
|
| 135 |
+
// Vtable now defined via #[vtable(...)] in audio/dssound.rs
|
|
|
|
| 136 |
/// DSSound constructor β usercall(EAX=this), plain RET
|
| 137 |
ctor/Usercall CONSTRUCT_DS_SOUND = 0x0057_3D50;
|
| 138 |
/// DSSound init buffers β usercall(EAX=dssound), plain RET
|
|
|
|
| 152 |
fn/Stdcall DDKEYBOARD_POLL_KEYBOARD_STATE = 0x0057_2290;
|
| 153 |
}
|
| 154 |
|
| 155 |
+
// Palette vtable is now defined via #[derive(Vtable)] in display/palette.rs
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
class "DisplayBase" {
|
| 158 |
+
// Primary vtable now defined via #[vtable(...)] in display/base.rs
|
|
|
|
| 159 |
/// DisplayBase headless vtable
|
| 160 |
vtable DISPLAY_BASE_HEADLESS_VTABLE = 0x0066_A0F8;
|
| 161 |
/// DisplayBase constructor (0x3560-byte object)
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
class "InputCtrl" {
|
| 166 |
+
// Vtable now defined via #[vtable(...)] in input/controller.rs
|
|
|
|
| 167 |
/// Input controller initializer
|
| 168 |
fn/Usercall INPUT_CTRL_INIT = 0x0058_C0D0;
|
| 169 |
}
|
|
@@ -77,61 +77,58 @@ impl ChannelDescriptor {
|
|
| 77 |
///
|
| 78 |
/// Methods operate on the 8 channel descriptors and 64-entry buffer pool.
|
| 79 |
/// Trivial slots (stubs, noops, return-constant) are marked.
|
| 80 |
-
#[
|
| 81 |
pub struct DSSoundVtable {
|
| 82 |
-
///
|
| 83 |
-
pub destructor:
|
| 84 |
-
///
|
| 85 |
-
pub update_channels:
|
| 86 |
-
///
|
| 87 |
-
pub set_volume_params:
|
| 88 |
-
///
|
| 89 |
-
pub play_sound:
|
| 90 |
-
///
|
| 91 |
-
pub play_sound_pooled:
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
pub returns_1_23: unsafe extern "thiscall" fn(*mut DSSound) -> u32,
|
| 131 |
}
|
| 132 |
|
| 133 |
-
const _: () = assert!(core::mem::size_of::<DSSoundVtable>() == 24 * 4);
|
| 134 |
-
|
| 135 |
#[repr(C)]
|
| 136 |
pub struct DSSound {
|
| 137 |
/// 0x000: Vtable pointer (0x66AF20)
|
|
|
|
| 77 |
///
|
| 78 |
/// Methods operate on the 8 channel descriptors and 64-entry buffer pool.
|
| 79 |
/// Trivial slots (stubs, noops, return-constant) are marked.
|
| 80 |
+
#[openwa_core::vtable(size = 24, va = 0x0066_AF20, class = "DSSound")]
|
| 81 |
pub struct DSSoundVtable {
|
| 82 |
+
/// destructor
|
| 83 |
+
pub destructor: fn(this: *mut DSSound, flags: u8) -> *mut DSSound,
|
| 84 |
+
/// update_channels β iterates 8 descs, releases finished buffers
|
| 85 |
+
pub update_channels: fn(this: *mut DSSound),
|
| 86 |
+
/// set_volume_params β sets status_1/2, adjusts channel volumes
|
| 87 |
+
pub set_volume_params: fn(this: *mut DSSound, status: u32, value: i32),
|
| 88 |
+
/// play_sound β wrapper around core play. RET 0x14 = 5 stack params.
|
| 89 |
+
pub play_sound: fn(this: *mut DSSound, slot: u32, flags: u32, volume: u32, pan: u32, freq: u32) -> bool,
|
| 90 |
+
/// play_sound_pooled β allocates from buffer pool, plays. RET 0x14.
|
| 91 |
+
pub play_sound_pooled: fn(this: *mut DSSound, slot: u32, flags: u32, volume: u32, pan: u32, freq: u32) -> i32,
|
| 92 |
+
/// set_pan β sets pan on channel (dB lookup)
|
| 93 |
+
pub set_pan: fn(this: *mut DSSound, channel: u32, pan: u32) -> u32,
|
| 94 |
+
/// **stub** β returns 0
|
| 95 |
+
pub stub_6: fn(this: *mut DSSound) -> u32,
|
| 96 |
+
/// set_master_volume β sets volume, adjusts all channels
|
| 97 |
+
pub set_master_volume: fn(this: *mut DSSound, volume: i32) -> u32,
|
| 98 |
+
/// set_channel_volume β volume on specific channel
|
| 99 |
+
pub set_channel_volume: fn(this: *mut DSSound, channel: i32, volume: i32) -> u32,
|
| 100 |
+
/// is_channel_finished β returns 1 if stopped, 0 if playing
|
| 101 |
+
pub is_channel_finished: fn(this: *mut DSSound, channel: i32) -> u8,
|
| 102 |
+
/// stop_channel β stops + releases buffer, returns to pool
|
| 103 |
+
pub stop_channel: fn(this: *mut DSSound, channel: i32) -> u32,
|
| 104 |
+
/// release_finished β releases finished buffers, returns count
|
| 105 |
+
pub release_finished: fn(this: *mut DSSound) -> i32,
|
| 106 |
+
/// load_wav β opens WAV file, parses RIFF, creates buffer
|
| 107 |
+
pub load_wav: fn(this: *mut DSSound, slot: i32, path: *const u8) -> u32,
|
| 108 |
+
/// is_slot_loaded β returns channel_slots[idx] != 0
|
| 109 |
+
pub is_slot_loaded: fn(this: *mut DSSound, slot: i32) -> bool,
|
| 110 |
+
/// sub_destructor β sets secondary vtable
|
| 111 |
+
pub sub_destructor: fn(this: *mut DSSound, flags: u8) -> *mut DSSound,
|
| 112 |
+
/// **noop**
|
| 113 |
+
pub noop_15: fn(this: *mut DSSound),
|
| 114 |
+
/// **noop**
|
| 115 |
+
pub noop_16: fn(this: *mut DSSound),
|
| 116 |
+
/// **returns_0**
|
| 117 |
+
pub returns_0_17: fn(this: *mut DSSound) -> u32,
|
| 118 |
+
/// **returns_0** (same as 17)
|
| 119 |
+
pub returns_0_18: fn(this: *mut DSSound) -> u32,
|
| 120 |
+
/// **stub** β returns 0
|
| 121 |
+
pub stub_19: fn(this: *mut DSSound) -> u32,
|
| 122 |
+
/// **stub** β returns 0
|
| 123 |
+
pub stub_20: fn(this: *mut DSSound) -> u32,
|
| 124 |
+
/// **returns_0**
|
| 125 |
+
pub returns_0_21: fn(this: *mut DSSound) -> u32,
|
| 126 |
+
/// **stub** β returns 0
|
| 127 |
+
pub stub_22: fn(this: *mut DSSound) -> u32,
|
| 128 |
+
/// **returns_1**
|
| 129 |
+
pub returns_1_23: fn(this: *mut DSSound) -> u32,
|
|
|
|
| 130 |
}
|
| 131 |
|
|
|
|
|
|
|
| 132 |
#[repr(C)]
|
| 133 |
pub struct DSSound {
|
| 134 |
/// 0x000: Vtable pointer (0x66AF20)
|
|
@@ -62,47 +62,43 @@ const _: () = assert!(core::mem::size_of::<DisplayBase>() == 0x3560);
|
|
| 62 |
///
|
| 63 |
/// In headless mode, most slots are no-op stubs. The primary vtable (0x6645F8)
|
| 64 |
/// has _purecall for the drawing slots; the headless overlay replaces them.
|
| 65 |
-
#[
|
| 66 |
pub struct DisplayBaseVtable {
|
| 67 |
-
///
|
| 68 |
-
pub destructor:
|
| 69 |
-
|
| 70 |
-
pub
|
| 71 |
-
|
| 72 |
-
pub
|
| 73 |
-
pub
|
| 74 |
-
pub
|
| 75 |
-
pub
|
| 76 |
-
pub
|
| 77 |
-
pub
|
| 78 |
-
pub
|
| 79 |
-
pub
|
| 80 |
-
pub
|
| 81 |
-
pub
|
| 82 |
-
pub
|
| 83 |
-
pub
|
| 84 |
-
pub
|
| 85 |
-
pub
|
| 86 |
-
pub
|
| 87 |
-
pub
|
| 88 |
-
pub
|
| 89 |
-
pub
|
| 90 |
-
pub
|
| 91 |
-
pub
|
| 92 |
-
pub
|
| 93 |
-
pub
|
| 94 |
-
pub
|
| 95 |
-
pub
|
| 96 |
-
pub
|
| 97 |
-
pub
|
| 98 |
-
pub
|
| 99 |
-
pub
|
| 100 |
-
pub slot_30: unsafe extern "thiscall" fn(*mut DisplayBase),
|
| 101 |
-
pub slot_31: unsafe extern "thiscall" fn(*mut DisplayBase),
|
| 102 |
}
|
| 103 |
|
| 104 |
-
const _: () = assert!(core::mem::size_of::<DisplayBaseVtable>() == 32 * 4);
|
| 105 |
-
|
| 106 |
// ββ Sprite cache sub-objects ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 107 |
|
| 108 |
/// Sprite buffer control block (0x3C bytes, inner).
|
|
|
|
| 62 |
///
|
| 63 |
/// In headless mode, most slots are no-op stubs. The primary vtable (0x6645F8)
|
| 64 |
/// has _purecall for the drawing slots; the headless overlay replaces them.
|
| 65 |
+
#[openwa_core::vtable(size = 32, va = 0x0066_45F8, class = "DisplayBase")]
|
| 66 |
pub struct DisplayBaseVtable {
|
| 67 |
+
/// destructor
|
| 68 |
+
pub destructor: fn(this: *mut DisplayBase, flags: u8) -> *mut DisplayBase,
|
| 69 |
+
pub slot_01: fn(this: *mut DisplayBase),
|
| 70 |
+
pub slot_02: fn(this: *mut DisplayBase),
|
| 71 |
+
pub slot_03: fn(this: *mut DisplayBase),
|
| 72 |
+
pub slot_04: fn(this: *mut DisplayBase),
|
| 73 |
+
pub slot_05: fn(this: *mut DisplayBase),
|
| 74 |
+
pub slot_06: fn(this: *mut DisplayBase),
|
| 75 |
+
pub slot_07: fn(this: *mut DisplayBase),
|
| 76 |
+
pub slot_08: fn(this: *mut DisplayBase),
|
| 77 |
+
pub slot_09: fn(this: *mut DisplayBase),
|
| 78 |
+
pub slot_10: fn(this: *mut DisplayBase),
|
| 79 |
+
pub slot_11: fn(this: *mut DisplayBase),
|
| 80 |
+
pub slot_12: fn(this: *mut DisplayBase),
|
| 81 |
+
pub slot_13: fn(this: *mut DisplayBase),
|
| 82 |
+
pub slot_14: fn(this: *mut DisplayBase),
|
| 83 |
+
pub slot_15: fn(this: *mut DisplayBase),
|
| 84 |
+
pub slot_16: fn(this: *mut DisplayBase),
|
| 85 |
+
pub slot_17: fn(this: *mut DisplayBase),
|
| 86 |
+
pub slot_18: fn(this: *mut DisplayBase),
|
| 87 |
+
pub slot_19: fn(this: *mut DisplayBase),
|
| 88 |
+
pub slot_20: fn(this: *mut DisplayBase),
|
| 89 |
+
pub slot_21: fn(this: *mut DisplayBase),
|
| 90 |
+
pub slot_22: fn(this: *mut DisplayBase),
|
| 91 |
+
pub slot_23: fn(this: *mut DisplayBase),
|
| 92 |
+
pub slot_24: fn(this: *mut DisplayBase),
|
| 93 |
+
pub slot_25: fn(this: *mut DisplayBase),
|
| 94 |
+
pub slot_26: fn(this: *mut DisplayBase),
|
| 95 |
+
pub slot_27: fn(this: *mut DisplayBase),
|
| 96 |
+
pub slot_28: fn(this: *mut DisplayBase),
|
| 97 |
+
pub slot_29: fn(this: *mut DisplayBase),
|
| 98 |
+
pub slot_30: fn(this: *mut DisplayBase),
|
| 99 |
+
pub slot_31: fn(this: *mut DisplayBase),
|
|
|
|
|
|
|
| 100 |
}
|
| 101 |
|
|
|
|
|
|
|
| 102 |
// ββ Sprite cache sub-objects ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 103 |
|
| 104 |
/// Sprite buffer control block (0x3C bytes, inner).
|
|
@@ -17,146 +17,25 @@ pub struct DDDisplay {
|
|
| 17 |
|
| 18 |
/// DDDisplay vtable (0x66A218, 38 slots).
|
| 19 |
///
|
| 20 |
-
/// Only actively-used slots have typed signatures. Unknown slots are
|
| 21 |
-
|
| 22 |
-
#[repr(C)]
|
| 23 |
pub struct DDDisplayVtable {
|
| 24 |
-
///
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
pub
|
| 30 |
-
///
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
pub
|
| 36 |
-
///
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
pub _slot_07: usize,
|
| 40 |
-
/// Slot 8
|
| 41 |
-
pub _slot_08: usize,
|
| 42 |
-
/// Slot 9
|
| 43 |
-
pub _slot_09: usize,
|
| 44 |
-
/// Slot 10
|
| 45 |
-
pub _slot_10: usize,
|
| 46 |
-
/// Slot 11
|
| 47 |
-
pub _slot_11: usize,
|
| 48 |
-
/// Slot 12
|
| 49 |
-
pub _slot_12: usize,
|
| 50 |
-
/// Slot 13
|
| 51 |
-
pub _slot_13: usize,
|
| 52 |
-
/// Slot 14
|
| 53 |
-
pub _slot_14: usize,
|
| 54 |
-
/// Slot 15
|
| 55 |
-
pub _slot_15: usize,
|
| 56 |
-
/// Slot 16
|
| 57 |
-
pub _slot_16: usize,
|
| 58 |
-
/// Slot 17
|
| 59 |
-
pub _slot_17: usize,
|
| 60 |
-
/// Slot 18
|
| 61 |
-
pub _slot_18: usize,
|
| 62 |
-
/// Slot 19
|
| 63 |
-
pub _slot_19: usize,
|
| 64 |
-
/// Slot 20
|
| 65 |
-
pub _slot_20: usize,
|
| 66 |
-
/// Slot 21
|
| 67 |
-
pub _slot_21: usize,
|
| 68 |
-
/// Slot 22
|
| 69 |
-
pub _slot_22: usize,
|
| 70 |
-
/// Slot 23 (0x5C): set layer visibility β thiscall(this, layer, value)
|
| 71 |
-
pub set_layer_visibility: unsafe extern "thiscall" fn(*mut DDDisplay, i32, i32),
|
| 72 |
-
/// Slot 24
|
| 73 |
-
pub _slot_24: usize,
|
| 74 |
-
/// Slot 25
|
| 75 |
-
pub _slot_25: usize,
|
| 76 |
-
/// Slot 26
|
| 77 |
-
pub _slot_26: usize,
|
| 78 |
-
/// Slot 27
|
| 79 |
-
pub _slot_27: usize,
|
| 80 |
-
/// Slot 28
|
| 81 |
-
pub _slot_28: usize,
|
| 82 |
-
/// Slot 29
|
| 83 |
-
pub _slot_29: usize,
|
| 84 |
-
/// Slot 30
|
| 85 |
-
pub _slot_30: usize,
|
| 86 |
-
/// Slot 31 (0x7C): load sprite with flag β thiscall(this, layer, id, flag, gfx, name), RET 0x14
|
| 87 |
-
pub load_sprite: unsafe extern "thiscall" fn(
|
| 88 |
-
*mut DDDisplay,
|
| 89 |
-
u32,
|
| 90 |
-
u32,
|
| 91 |
-
u32,
|
| 92 |
-
*mut u8,
|
| 93 |
-
*const core::ffi::c_char,
|
| 94 |
-
) -> i32,
|
| 95 |
-
/// Slot 32
|
| 96 |
-
pub _slot_32: usize,
|
| 97 |
-
/// Slot 33
|
| 98 |
-
pub _slot_33: usize,
|
| 99 |
-
/// Slot 34
|
| 100 |
-
pub _slot_34: usize,
|
| 101 |
-
/// Slot 35
|
| 102 |
-
pub _slot_35: usize,
|
| 103 |
-
/// Slot 36
|
| 104 |
-
pub _slot_36: usize,
|
| 105 |
-
/// Slot 37 (0x94): load sprite by layer β thiscall(this, layer, id, gfx, name), RET 0x10
|
| 106 |
-
pub load_sprite_by_layer: unsafe extern "thiscall" fn(
|
| 107 |
-
*mut DDDisplay,
|
| 108 |
-
u32,
|
| 109 |
-
u32,
|
| 110 |
-
*mut u8,
|
| 111 |
-
*const core::ffi::c_char,
|
| 112 |
-
) -> i32,
|
| 113 |
}
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
/// Typed wrappers for DDDisplay vtable calls.
|
| 118 |
-
/// All take `*mut DDDisplay` since thiscall passes `this` as mutable.
|
| 119 |
-
impl DDDisplay {
|
| 120 |
-
/// Vtable slot 5: set active display layer, returns layer context pointer.
|
| 121 |
-
#[inline]
|
| 122 |
-
pub unsafe fn set_active_layer(this: *mut Self, layer: i32) -> *mut u8 {
|
| 123 |
-
((*(*this).vtable).set_active_layer)(this, layer)
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
/// Vtable slot 4: set color for a display layer.
|
| 127 |
-
#[inline]
|
| 128 |
-
pub unsafe fn set_layer_color(this: *mut Self, layer: i32, color: i32) {
|
| 129 |
-
((*(*this).vtable).set_layer_color)(this, layer, color)
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
/// Vtable slot 23: set layer visibility.
|
| 133 |
-
#[inline]
|
| 134 |
-
pub unsafe fn set_layer_visibility(this: *mut Self, layer: i32, value: i32) {
|
| 135 |
-
((*(*this).vtable).set_layer_visibility)(this, layer, value)
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
/// Vtable slot 37: load sprite into a layer by name.
|
| 139 |
-
#[inline]
|
| 140 |
-
pub unsafe fn load_sprite_by_layer(
|
| 141 |
-
this: *mut Self,
|
| 142 |
-
layer: u32,
|
| 143 |
-
id: u32,
|
| 144 |
-
gfx: *mut u8,
|
| 145 |
-
name: *const core::ffi::c_char,
|
| 146 |
-
) -> i32 {
|
| 147 |
-
((*(*this).vtable).load_sprite_by_layer)(this, layer, id, gfx, name)
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
/// Vtable slot 31: load sprite with flag.
|
| 151 |
-
#[inline]
|
| 152 |
-
pub unsafe fn load_sprite(
|
| 153 |
-
this: *mut Self,
|
| 154 |
-
layer: u32,
|
| 155 |
-
id: u32,
|
| 156 |
-
flag: u32,
|
| 157 |
-
gfx: *mut u8,
|
| 158 |
-
name: *const core::ffi::c_char,
|
| 159 |
-
) -> i32 {
|
| 160 |
-
((*(*this).vtable).load_sprite)(this, layer, id, flag, gfx, name)
|
| 161 |
-
}
|
| 162 |
-
}
|
|
|
|
| 17 |
|
| 18 |
/// DDDisplay vtable (0x66A218, 38 slots).
|
| 19 |
///
|
| 20 |
+
/// Only actively-used slots have typed signatures. Unknown slots are auto-filled.
|
| 21 |
+
#[openwa_core::vtable(size = 38, va = 0x0066_A218, class = "DDDisplay")]
|
|
|
|
| 22 |
pub struct DDDisplayVtable {
|
| 23 |
+
/// set layer color
|
| 24 |
+
#[slot(4)]
|
| 25 |
+
pub set_layer_color: fn(this: *mut DDDisplay, layer: i32, color: i32),
|
| 26 |
+
/// set active layer, returns layer context ptr
|
| 27 |
+
#[slot(5)]
|
| 28 |
+
pub set_active_layer: fn(this: *mut DDDisplay, layer: i32) -> *mut u8,
|
| 29 |
+
/// set layer visibility
|
| 30 |
+
#[slot(23)]
|
| 31 |
+
pub set_layer_visibility: fn(this: *mut DDDisplay, layer: i32, value: i32),
|
| 32 |
+
/// load sprite with flag (RET 0x14)
|
| 33 |
+
#[slot(31)]
|
| 34 |
+
pub load_sprite: fn(this: *mut DDDisplay, layer: u32, id: u32, flag: u32, gfx: *mut u8, name: *const core::ffi::c_char) -> i32,
|
| 35 |
+
/// load sprite by layer (RET 0x10)
|
| 36 |
+
#[slot(37)]
|
| 37 |
+
pub load_sprite_by_layer: fn(this: *mut DDDisplay, layer: u32, id: u32, gfx: *mut u8, name: *const core::ffi::c_char) -> i32,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
+
// Generate calling wrappers: DDDisplay::set_layer_color(), etc.
|
| 41 |
+
bind_DDDisplayVtable!(DDDisplay, vtable);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/// Palette β palette management object.
|
| 2 |
///
|
| 3 |
/// Vtable: 0x66A2E4.
|
|
@@ -17,23 +34,8 @@ pub struct Palette {
|
|
| 17 |
|
| 18 |
const _: () = assert!(core::mem::size_of::<Palette>() == 0x28);
|
| 19 |
|
| 20 |
-
//
|
| 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 |
/// Create a new Palette with inline construction (no native C++ ctor).
|
|
@@ -47,17 +49,4 @@ impl Palette {
|
|
| 47 |
_unknown_008: [0; 0x20],
|
| 48 |
}
|
| 49 |
}
|
| 50 |
-
|
| 51 |
-
/// Vtable[4]: Reset palette state.
|
| 52 |
-
pub unsafe fn reset(&mut self) {
|
| 53 |
-
vcall!(self, reset)
|
| 54 |
-
}
|
| 55 |
-
/// Vtable[3]: Initialize palette.
|
| 56 |
-
pub unsafe fn init(&mut self) {
|
| 57 |
-
vcall!(self, init)
|
| 58 |
-
}
|
| 59 |
-
/// Vtable[2]: Set palette mode.
|
| 60 |
-
pub unsafe fn set_mode(&mut self, mode: u32) {
|
| 61 |
-
vcall!(self, set_mode, mode)
|
| 62 |
-
}
|
| 63 |
}
|
|
|
|
| 1 |
+
/// Vtable for Palette (0x66A2E4).
|
| 2 |
+
///
|
| 3 |
+
/// Slots 2-4 are called by GameEngine__InitHardware after DDGameWrapper construction.
|
| 4 |
+
/// Slots 0-1 are unknown.
|
| 5 |
+
#[openwa_core::vtable(size = 5, va = 0x0066_A2E4, class = "Palette")]
|
| 6 |
+
pub struct PaletteVtable {
|
| 7 |
+
/// set_mode(this, mode) β called with mode=7 during hardware init
|
| 8 |
+
#[slot(2)]
|
| 9 |
+
pub set_mode: fn(this: *mut Palette, mode: u32),
|
| 10 |
+
/// init β called during hardware init
|
| 11 |
+
#[slot(3)]
|
| 12 |
+
pub init: fn(this: *mut Palette),
|
| 13 |
+
/// reset β called first during hardware init
|
| 14 |
+
#[slot(4)]
|
| 15 |
+
pub reset: fn(this: *mut Palette),
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
/// Palette β palette management object.
|
| 19 |
///
|
| 20 |
/// Vtable: 0x66A2E4.
|
|
|
|
| 34 |
|
| 35 |
const _: () = assert!(core::mem::size_of::<Palette>() == 0x28);
|
| 36 |
|
| 37 |
+
// Generate calling wrappers: Palette::set_mode(), Palette::init(), Palette::reset()
|
| 38 |
+
bind_PaletteVtable!(Palette, vtable);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
impl Palette {
|
| 41 |
/// Create a new Palette with inline construction (no native C++ ctor).
|
|
|
|
| 49 |
_unknown_008: [0; 0x20],
|
| 50 |
}
|
| 51 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
|
@@ -494,9 +494,9 @@ pub unsafe fn display_layer_color_init(wrapper: *mut DDGameWrapper) {
|
|
| 494 |
};
|
| 495 |
|
| 496 |
let display = (*wrapper).display;
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
}
|
| 501 |
|
| 502 |
/// Initialize runtime addresses for the constructor bridges.
|
|
@@ -841,16 +841,15 @@ unsafe fn init_graphics_and_resources(
|
|
| 841 |
}
|
| 842 |
let disp = (*wrapper).display;
|
| 843 |
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
disp,
|
| 847 |
1,
|
| 848 |
1,
|
| 849 |
0,
|
| 850 |
gfx_dir,
|
| 851 |
rb(va::STR_CDROM_SPR) as *const c_char,
|
| 852 |
);
|
| 853 |
-
|
| 854 |
|
| 855 |
// Palette slot range init (raw byte offsets into DisplayBase)
|
| 856 |
let disp_raw = disp as *mut u8;
|
|
@@ -875,7 +874,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 875 |
// ββ Display vtable slot 5 (offset 0x14) ββ
|
| 876 |
// Original: CALL EAX (vtable[5]), saves return value in ESI for use as
|
| 877 |
// the `output` parameter in the color-entries GfxResource__Create call below.
|
| 878 |
-
let layer_ctx =
|
| 879 |
|
| 880 |
// ββ GfxDir color entries DDGame+0x730C..0x732C ββ
|
| 881 |
// Original logic: if gfx_mode!=0, try GfxResource__Create for colours.img.
|
|
@@ -1089,7 +1088,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 1089 |
name_buf[5] = b'0' + (i / 10) as u8;
|
| 1090 |
name_buf[6] = b'0' + (i % 10) as u8;
|
| 1091 |
|
| 1092 |
-
let layer_ctx =
|
| 1093 |
|
| 1094 |
let entry = gfx_dir_find_entry(name_buf.as_ptr().cast(), gfx_dir);
|
| 1095 |
|
|
@@ -1272,14 +1271,13 @@ unsafe fn init_graphics_and_resources(
|
|
| 1272 |
);
|
| 1273 |
|
| 1274 |
let disp = (*wrapper).display;
|
| 1275 |
-
|
| 1276 |
|
| 1277 |
// back.spr and debris.spr must be loaded unconditionally β they're used by
|
| 1278 |
// GenerateDebrisParticles (0x546F70) for particle effects, which affects
|
| 1279 |
// the game RNG (DDGame+0x45EC). The original constructor loads them even
|
| 1280 |
// in headless mode. Skipping them causes replay desync.
|
| 1281 |
-
|
| 1282 |
-
disp,
|
| 1283 |
3,
|
| 1284 |
0x26D,
|
| 1285 |
land_layer,
|
|
@@ -1289,10 +1287,9 @@ unsafe fn init_graphics_and_resources(
|
|
| 1289 |
// GenerateDebrisParticles (0x546F70) for particle effects, which
|
| 1290 |
// affects the game RNG (DDGame+0x45EC). Skipping it in headless
|
| 1291 |
// mode causes desync (longbow replay checksum mismatch at frame 1350).
|
| 1292 |
-
|
| 1293 |
|
| 1294 |
-
|
| 1295 |
-
disp,
|
| 1296 |
2,
|
| 1297 |
0x26C,
|
| 1298 |
water_layer,
|
|
@@ -1303,7 +1300,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 1303 |
|
| 1304 |
// ββ Gradient image (0x030) ββ
|
| 1305 |
let level_height = (*ddgame).level_height as i32;
|
| 1306 |
-
let layer3_ctx =
|
| 1307 |
let s_var1 = *(layer3_ctx.add(0x606) as *const i16);
|
| 1308 |
|
| 1309 |
if s_var1 < 0x61 && level_height == 0x2B8 {
|
|
@@ -1316,7 +1313,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 1316 |
|
| 1317 |
// ββ Fill image β fill_pixel (0x7338) ββ
|
| 1318 |
{
|
| 1319 |
-
let layer2_ctx =
|
| 1320 |
// In the original, fill.img uses piStack_126c which the decompiler
|
| 1321 |
// shows was set from piVar3 (water_layer from landscape+0xB38).
|
| 1322 |
let fill_sprite = call_gfx_find_and_load(water_layer, c"fill.img", layer2_ctx);
|
|
@@ -1381,9 +1378,9 @@ unsafe fn init_graphics_and_resources(
|
|
| 1381 |
// ββ Final display layer visibility ββ
|
| 1382 |
{
|
| 1383 |
let disp = (*wrapper).display;
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
}
|
| 1388 |
|
| 1389 |
let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
|
|
|
|
| 494 |
};
|
| 495 |
|
| 496 |
let display = (*wrapper).display;
|
| 497 |
+
(*display).set_layer_color(1, layer1_color);
|
| 498 |
+
(*display).set_layer_color(2, 0x20);
|
| 499 |
+
(*display).set_layer_color(3, 0x70);
|
| 500 |
}
|
| 501 |
|
| 502 |
/// Initialize runtime addresses for the constructor bridges.
|
|
|
|
| 841 |
}
|
| 842 |
let disp = (*wrapper).display;
|
| 843 |
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 844 |
+
(*disp).set_layer_color(1, 0xFE);
|
| 845 |
+
(*disp).load_sprite(
|
|
|
|
| 846 |
1,
|
| 847 |
1,
|
| 848 |
0,
|
| 849 |
gfx_dir,
|
| 850 |
rb(va::STR_CDROM_SPR) as *const c_char,
|
| 851 |
);
|
| 852 |
+
(*disp).set_layer_visibility(1, -100);
|
| 853 |
|
| 854 |
// Palette slot range init (raw byte offsets into DisplayBase)
|
| 855 |
let disp_raw = disp as *mut u8;
|
|
|
|
| 874 |
// ββ Display vtable slot 5 (offset 0x14) ββ
|
| 875 |
// Original: CALL EAX (vtable[5]), saves return value in ESI for use as
|
| 876 |
// the `output` parameter in the color-entries GfxResource__Create call below.
|
| 877 |
+
let layer_ctx = (*(*ddgame).display).set_active_layer(1);
|
| 878 |
|
| 879 |
// ββ GfxDir color entries DDGame+0x730C..0x732C ββ
|
| 880 |
// Original logic: if gfx_mode!=0, try GfxResource__Create for colours.img.
|
|
|
|
| 1088 |
name_buf[5] = b'0' + (i / 10) as u8;
|
| 1089 |
name_buf[6] = b'0' + (i % 10) as u8;
|
| 1090 |
|
| 1091 |
+
let layer_ctx = (*(*ddgame).display).set_active_layer(1);
|
| 1092 |
|
| 1093 |
let entry = gfx_dir_find_entry(name_buf.as_ptr().cast(), gfx_dir);
|
| 1094 |
|
|
|
|
| 1271 |
);
|
| 1272 |
|
| 1273 |
let disp = (*wrapper).display;
|
| 1274 |
+
(*disp).set_active_layer(3);
|
| 1275 |
|
| 1276 |
// back.spr and debris.spr must be loaded unconditionally β they're used by
|
| 1277 |
// GenerateDebrisParticles (0x546F70) for particle effects, which affects
|
| 1278 |
// the game RNG (DDGame+0x45EC). The original constructor loads them even
|
| 1279 |
// in headless mode. Skipping them causes replay desync.
|
| 1280 |
+
(*disp).load_sprite_by_layer(
|
|
|
|
| 1281 |
3,
|
| 1282 |
0x26D,
|
| 1283 |
land_layer,
|
|
|
|
| 1287 |
// GenerateDebrisParticles (0x546F70) for particle effects, which
|
| 1288 |
// affects the game RNG (DDGame+0x45EC). Skipping it in headless
|
| 1289 |
// mode causes desync (longbow replay checksum mismatch at frame 1350).
|
| 1290 |
+
(*disp).load_sprite(3, 0x26E, 0, land_layer, c"debris.spr".as_ptr());
|
| 1291 |
|
| 1292 |
+
(*disp).load_sprite_by_layer(
|
|
|
|
| 1293 |
2,
|
| 1294 |
0x26C,
|
| 1295 |
water_layer,
|
|
|
|
| 1300 |
|
| 1301 |
// ββ Gradient image (0x030) ββ
|
| 1302 |
let level_height = (*ddgame).level_height as i32;
|
| 1303 |
+
let layer3_ctx = (*disp).set_active_layer(3);
|
| 1304 |
let s_var1 = *(layer3_ctx.add(0x606) as *const i16);
|
| 1305 |
|
| 1306 |
if s_var1 < 0x61 && level_height == 0x2B8 {
|
|
|
|
| 1313 |
|
| 1314 |
// ββ Fill image β fill_pixel (0x7338) ββ
|
| 1315 |
{
|
| 1316 |
+
let layer2_ctx = (*(*ddgame).display).set_active_layer(2);
|
| 1317 |
// In the original, fill.img uses piStack_126c which the decompiler
|
| 1318 |
// shows was set from piVar3 (water_layer from landscape+0xB38).
|
| 1319 |
let fill_sprite = call_gfx_find_and_load(water_layer, c"fill.img", layer2_ctx);
|
|
|
|
| 1378 |
// ββ Final display layer visibility ββ
|
| 1379 |
{
|
| 1380 |
let disp = (*wrapper).display;
|
| 1381 |
+
(*disp).set_layer_visibility(1, 0);
|
| 1382 |
+
(*disp).set_layer_visibility(2, 0);
|
| 1383 |
+
(*disp).set_layer_visibility(3, 1);
|
| 1384 |
}
|
| 1385 |
|
| 1386 |
let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
|
|
@@ -36,10 +36,9 @@ pub struct MapView {
|
|
| 36 |
const _: () = assert!(core::mem::size_of::<MapView>() == 0x29628);
|
| 37 |
|
| 38 |
/// MapView vtable (partial β only known slots).
|
| 39 |
-
#[
|
| 40 |
pub struct MapViewVtable {
|
| 41 |
-
///
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
pub destructor: unsafe extern "thiscall" fn(*mut MapView, i32),
|
| 45 |
}
|
|
|
|
| 36 |
const _: () = assert!(core::mem::size_of::<MapView>() == 0x29628);
|
| 37 |
|
| 38 |
/// MapView vtable (partial β only known slots).
|
| 39 |
+
#[openwa_core::vtable(size = 2, va = 0x0064_FC60, class = "MapView")]
|
| 40 |
pub struct MapViewVtable {
|
| 41 |
+
/// Destructor β thiscall(this, free_flag). Frees the object if flag & 1.
|
| 42 |
+
#[slot(1)]
|
| 43 |
+
pub destructor: fn(this: *mut MapView, free_flag: i32),
|
|
|
|
| 44 |
}
|
|
@@ -1,3 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/// InputCtrl β input controller subsystem.
|
| 2 |
///
|
| 3 |
/// Initializer: FUN_0058C0D0, usercall(ESI=this) + stdcall(4 params), RET 0x10.
|
|
@@ -20,18 +30,12 @@ pub struct InputCtrl {
|
|
| 20 |
|
| 21 |
const _: () = assert!(core::mem::size_of::<InputCtrl>() == 0x1800);
|
| 22 |
|
| 23 |
-
//
|
| 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) {
|
| 35 |
-
|
| 36 |
}
|
| 37 |
}
|
|
|
|
| 1 |
+
/// Vtable for InputCtrl (0x66B3FC).
|
| 2 |
+
///
|
| 3 |
+
/// Only slot 0 (destructor) is known β called on init failure cleanup.
|
| 4 |
+
#[openwa_core::vtable(size = 1, va = 0x0066_B3FC, class = "InputCtrl")]
|
| 5 |
+
pub struct InputCtrlVtable {
|
| 6 |
+
/// Destructor(this, flags) β scalar deleting destructor
|
| 7 |
+
#[slot(0)]
|
| 8 |
+
pub destructor: fn(this: *mut InputCtrl, flags: u32),
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
/// InputCtrl β input controller subsystem.
|
| 12 |
///
|
| 13 |
/// Initializer: FUN_0058C0D0, usercall(ESI=this) + stdcall(4 params), RET 0x10.
|
|
|
|
| 30 |
|
| 31 |
const _: () = assert!(core::mem::size_of::<InputCtrl>() == 0x1800);
|
| 32 |
|
| 33 |
+
// Generate calling wrappers: InputCtrl::destructor()
|
| 34 |
+
bind_InputCtrlVtable!(InputCtrl, vtable);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
impl InputCtrl {
|
| 37 |
/// Vtable[0]: Destroy and optionally free (flags & 1 = free).
|
| 38 |
pub unsafe fn destroy(&mut self, flags: u32) {
|
| 39 |
+
self.destructor(flags);
|
| 40 |
}
|
| 41 |
}
|
|
@@ -23,6 +23,7 @@ pub use inventory;
|
|
| 23 |
|
| 24 |
// Re-export derive macros so users write `use openwa_core::FieldRegistry;`
|
| 25 |
pub use openwa_derive::FieldRegistry;
|
|
|
|
| 26 |
|
| 27 |
#[macro_use]
|
| 28 |
pub mod macros;
|
|
|
|
| 23 |
|
| 24 |
// Re-export derive macros so users write `use openwa_core::FieldRegistry;`
|
| 25 |
pub use openwa_derive::FieldRegistry;
|
| 26 |
+
pub use openwa_derive::vtable;
|
| 27 |
|
| 28 |
#[macro_use]
|
| 29 |
pub mod macros;
|
|
@@ -149,6 +149,59 @@ macro_rules! define_addresses {
|
|
| 149 |
};
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
/// Call a virtual method through a typed vtable pointer.
|
| 153 |
///
|
| 154 |
/// Assumes `$obj` is a raw pointer to a struct whose first field `vtable`
|
|
|
|
| 149 |
};
|
| 150 |
}
|
| 151 |
|
| 152 |
+
/// Replace specific slots in a WA.exe vtable with Rust function pointers.
|
| 153 |
+
///
|
| 154 |
+
/// Expands to executable code for use inside `install()` functions.
|
| 155 |
+
/// Uses `patch_vtable` (VirtualProtect) to make .rdata writable.
|
| 156 |
+
///
|
| 157 |
+
/// # Syntax
|
| 158 |
+
///
|
| 159 |
+
/// ```ignore
|
| 160 |
+
/// vtable_replace!(PaletteVtable, va::PALETTE_VTABLE, {
|
| 161 |
+
/// slot 2 => my_set_mode, // pure replace
|
| 162 |
+
/// slot 3 [originals::INIT] => my_init, // replace + save original
|
| 163 |
+
/// })?;
|
| 164 |
+
/// ```
|
| 165 |
+
///
|
| 166 |
+
/// The `[static]` syntax saves the original fn pointer to the given `AtomicU32`
|
| 167 |
+
/// before overwriting. Omit it for pure replacement (no call-through).
|
| 168 |
+
///
|
| 169 |
+
/// Size is derived from `size_of::<VtableType>() / 4`.
|
| 170 |
+
#[macro_export]
|
| 171 |
+
macro_rules! vtable_replace {
|
| 172 |
+
($vtable_ty:ty, $va:expr, { $($slot:tt)* }) => {{
|
| 173 |
+
let vtable_ptr = $crate::rebase::rb($va) as *mut u32;
|
| 174 |
+
let slot_count = core::mem::size_of::<$vtable_ty>() / 4;
|
| 175 |
+
unsafe {
|
| 176 |
+
$crate::vtable::patch_vtable(vtable_ptr, slot_count, |vt| {
|
| 177 |
+
$crate::vtable_replace!(@slots vt $($slot)*);
|
| 178 |
+
})
|
| 179 |
+
}
|
| 180 |
+
}};
|
| 181 |
+
|
| 182 |
+
// Slot with original save: slot N [static_path] => replacement,
|
| 183 |
+
(@slots $vt:ident slot $idx:literal [$orig:expr] => $replacement:expr, $($rest:tt)*) => {
|
| 184 |
+
{
|
| 185 |
+
let slot = $vt.add($idx);
|
| 186 |
+
$orig.store(unsafe { *slot }, core::sync::atomic::Ordering::Relaxed);
|
| 187 |
+
unsafe { *slot = $replacement as *const () as u32; }
|
| 188 |
+
}
|
| 189 |
+
$crate::vtable_replace!(@slots $vt $($rest)*);
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
// Slot without original save: slot N => replacement,
|
| 193 |
+
(@slots $vt:ident slot $idx:literal => $replacement:expr, $($rest:tt)*) => {
|
| 194 |
+
{
|
| 195 |
+
let slot = $vt.add($idx);
|
| 196 |
+
unsafe { *slot = $replacement as *const () as u32; }
|
| 197 |
+
}
|
| 198 |
+
$crate::vtable_replace!(@slots $vt $($rest)*);
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
+
// Base case
|
| 202 |
+
(@slots $vt:ident) => {};
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
/// Call a virtual method through a typed vtable pointer.
|
| 206 |
///
|
| 207 |
/// Assumes `$obj` is a raw pointer to a struct whose first field `vtable`
|
|
@@ -308,6 +308,82 @@ pub fn field_at_inherited(struct_name: &str, offset: u32) -> Option<&'static Fie
|
|
| 308 |
None
|
| 309 |
}
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
// =========================================================================
|
| 312 |
// Live object tracker
|
| 313 |
// =========================================================================
|
|
@@ -602,4 +678,51 @@ mod tests {
|
|
| 602 |
assert!(fields.is_some(), "CTask fields not found via vtable");
|
| 603 |
assert_eq!(fields.unwrap().struct_name, "CTask");
|
| 604 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
}
|
|
|
|
| 308 |
None
|
| 309 |
}
|
| 310 |
|
| 311 |
+
// =========================================================================
|
| 312 |
+
// Vtable registry (for #[derive(Vtable)])
|
| 313 |
+
// =========================================================================
|
| 314 |
+
|
| 315 |
+
/// Metadata for a single vtable slot.
|
| 316 |
+
#[derive(Debug, Clone, Copy)]
|
| 317 |
+
pub struct VtableSlotEntry {
|
| 318 |
+
/// Slot index in the vtable (0-based).
|
| 319 |
+
pub index: u32,
|
| 320 |
+
/// Method name (e.g., "set_mode").
|
| 321 |
+
pub name: &'static str,
|
| 322 |
+
/// Brief description from doc comment.
|
| 323 |
+
pub doc: &'static str,
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/// Metadata for a vtable struct.
|
| 327 |
+
#[derive(Debug)]
|
| 328 |
+
pub struct VtableInfo {
|
| 329 |
+
/// Vtable struct name (e.g., "PaletteVtable").
|
| 330 |
+
pub struct_name: &'static str,
|
| 331 |
+
/// Owning C++ class name (e.g., "Palette"), if specified.
|
| 332 |
+
pub class_name: &'static str,
|
| 333 |
+
/// Ghidra VA of the vtable in .rdata (0 if not specified).
|
| 334 |
+
pub ghidra_va: u32,
|
| 335 |
+
/// Total number of slots in the vtable.
|
| 336 |
+
pub slot_count: u32,
|
| 337 |
+
/// Known (named) slots only.
|
| 338 |
+
pub slots: &'static [VtableSlotEntry],
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
impl VtableInfo {
|
| 342 |
+
/// Look up a slot by method name.
|
| 343 |
+
pub fn slot_by_name(&self, name: &str) -> Option<&VtableSlotEntry> {
|
| 344 |
+
self.slots.iter().find(|s| s.name == name)
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/// Look up a slot by index.
|
| 348 |
+
pub fn slot_by_index(&self, index: u32) -> Option<&VtableSlotEntry> {
|
| 349 |
+
self.slots.iter().find(|s| s.index == index)
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/// Trait for vtable structs with auto-generated registry metadata.
|
| 354 |
+
pub trait HasVtableRegistry {
|
| 355 |
+
fn vtable_info() -> &'static VtableInfo;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
/// A vtable's registry submitted to the global collection via `inventory`.
|
| 359 |
+
pub struct VtableRegistration {
|
| 360 |
+
pub info: &'static VtableInfo,
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
inventory::collect!(VtableRegistration);
|
| 364 |
+
|
| 365 |
+
static VTABLE_MAP: OnceLock<std::collections::HashMap<&'static str, &'static VtableInfo>> =
|
| 366 |
+
OnceLock::new();
|
| 367 |
+
|
| 368 |
+
fn vtable_map() -> &'static std::collections::HashMap<&'static str, &'static VtableInfo> {
|
| 369 |
+
VTABLE_MAP.get_or_init(|| {
|
| 370 |
+
inventory::iter::<VtableRegistration>
|
| 371 |
+
.into_iter()
|
| 372 |
+
.map(|r| (r.info.struct_name, r.info))
|
| 373 |
+
.collect()
|
| 374 |
+
})
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
/// Look up vtable metadata by struct name.
|
| 378 |
+
pub fn vtable_info_for(struct_name: &str) -> Option<&'static VtableInfo> {
|
| 379 |
+
vtable_map().get(struct_name).copied()
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
/// Return all registered vtable info entries.
|
| 383 |
+
pub fn all_vtable_info() -> impl Iterator<Item = &'static VtableInfo> {
|
| 384 |
+
vtable_map().values().copied()
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
// =========================================================================
|
| 388 |
// Live object tracker
|
| 389 |
// =========================================================================
|
|
|
|
| 678 |
assert!(fields.is_some(), "CTask fields not found via vtable");
|
| 679 |
assert_eq!(fields.unwrap().struct_name, "CTask");
|
| 680 |
}
|
| 681 |
+
|
| 682 |
+
#[test]
|
| 683 |
+
fn vtable_registry_palette() {
|
| 684 |
+
// PaletteVtable should be registered via #[vtable(...)] attribute
|
| 685 |
+
let info = vtable_info_for("PaletteVtable");
|
| 686 |
+
assert!(info.is_some(), "PaletteVtable not found in vtable registry");
|
| 687 |
+
let info = info.unwrap();
|
| 688 |
+
|
| 689 |
+
assert_eq!(info.struct_name, "PaletteVtable");
|
| 690 |
+
assert_eq!(info.class_name, "Palette");
|
| 691 |
+
assert_eq!(info.ghidra_va, 0x0066_A2E4);
|
| 692 |
+
assert_eq!(info.slot_count, 5);
|
| 693 |
+
assert_eq!(info.slots.len(), 3); // set_mode, init, reset
|
| 694 |
+
|
| 695 |
+
// Check individual slots
|
| 696 |
+
let set_mode = info.slot_by_name("set_mode");
|
| 697 |
+
assert!(set_mode.is_some(), "set_mode not found");
|
| 698 |
+
assert_eq!(set_mode.unwrap().index, 2);
|
| 699 |
+
|
| 700 |
+
let init = info.slot_by_index(3);
|
| 701 |
+
assert!(init.is_some(), "slot 3 not found");
|
| 702 |
+
assert_eq!(init.unwrap().name, "init");
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
#[test]
|
| 706 |
+
fn vtable_generates_addr_entry() {
|
| 707 |
+
use crate::address::va;
|
| 708 |
+
|
| 709 |
+
// The vtable macro should have generated PALETTE_VTABLE const
|
| 710 |
+
assert_eq!(va::PALETTE_VTABLE, 0x0066_A2E4);
|
| 711 |
+
|
| 712 |
+
// And an AddrEntry in the registry
|
| 713 |
+
let entry = lookup_va_exact(0x0066_A2E4);
|
| 714 |
+
assert!(entry.is_some(), "PALETTE_VTABLE addr entry not found");
|
| 715 |
+
let entry = entry.unwrap();
|
| 716 |
+
assert_eq!(entry.kind, AddrKind::Vtable);
|
| 717 |
+
assert_eq!(entry.class_name, Some("Palette"));
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
#[test]
|
| 721 |
+
fn vtable_size_assertion() {
|
| 722 |
+
// PaletteVtable should be 5 * 4 = 20 bytes
|
| 723 |
+
assert_eq!(
|
| 724 |
+
core::mem::size_of::<crate::display::palette::PaletteVtable>(),
|
| 725 |
+
5 * core::mem::size_of::<usize>()
|
| 726 |
+
);
|
| 727 |
+
}
|
| 728 |
}
|
|
@@ -6,8 +6,7 @@ crate::define_addresses! {
|
|
| 6 |
class "CGameTask" {
|
| 7 |
/// CGameTask vtable - extends CTask vtable with 12 more methods
|
| 8 |
vtable CGAMETASK_VTABLE = 0x0066_41F8;
|
| 9 |
-
//
|
| 10 |
-
vtable CGAMETASK_SOUND_EMITTER_VT = 0x0066_9CF8;
|
| 11 |
/// CGameTask constructor - calls CTask ctor, sets physics defaults
|
| 12 |
ctor/Stdcall CGAMETASK_CONSTRUCTOR = 0x004F_ED50;
|
| 13 |
/// CGameTask::vtable0 override
|
|
@@ -19,6 +18,9 @@ crate::define_addresses! {
|
|
| 19 |
}
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
| 22 |
/// Game task - extends CTask with physics and gameplay data.
|
| 23 |
///
|
| 24 |
/// PARTIAL: Most fields between 0x30-0x83 and 0x98-0xE7 are unknown.
|
|
@@ -81,18 +83,18 @@ const _: () = assert!(core::mem::size_of::<SoundEmitter>() == 0x14);
|
|
| 81 |
///
|
| 82 |
/// Slots [0]-[4] are the sound emitter's own interface.
|
| 83 |
/// Slots [5]-[11] are inherited CTask base methods.
|
| 84 |
-
#[
|
| 85 |
pub struct SoundEmitterVTable {
|
| 86 |
-
///
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
pub
|
| 92 |
-
///
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
pub
|
| 98 |
}
|
|
|
|
| 6 |
class "CGameTask" {
|
| 7 |
/// CGameTask vtable - extends CTask vtable with 12 more methods
|
| 8 |
vtable CGAMETASK_VTABLE = 0x0066_41F8;
|
| 9 |
+
// Sound emitter vtable now defined via #[vtable(...)] on SoundEmitterVTable
|
|
|
|
| 10 |
/// CGameTask constructor - calls CTask ctor, sets physics defaults
|
| 11 |
ctor/Stdcall CGAMETASK_CONSTRUCTOR = 0x004F_ED50;
|
| 12 |
/// CGameTask::vtable0 override
|
|
|
|
| 18 |
}
|
| 19 |
}
|
| 20 |
|
| 21 |
+
/// Backward-compatible alias for the SoundEmitter vtable const.
|
| 22 |
+
pub const CGAMETASK_SOUND_EMITTER_VT: u32 = SOUND_EMITTER_VTABLE;
|
| 23 |
+
|
| 24 |
/// Game task - extends CTask with physics and gameplay data.
|
| 25 |
///
|
| 26 |
/// PARTIAL: Most fields between 0x30-0x83 and 0x98-0xE7 are unknown.
|
|
|
|
| 83 |
///
|
| 84 |
/// Slots [0]-[4] are the sound emitter's own interface.
|
| 85 |
/// Slots [5]-[11] are inherited CTask base methods.
|
| 86 |
+
#[openwa_core::vtable(size = 12, va = 0x0066_9CF8, class = "SoundEmitter")]
|
| 87 |
pub struct SoundEmitterVTable {
|
| 88 |
+
/// GetPosition(this, out_x, out_y) β reads pos_x/pos_y via owner
|
| 89 |
+
#[slot(0)]
|
| 90 |
+
pub get_position: fn(this: *const SoundEmitter, out_x: *mut u32, out_y: *mut u32),
|
| 91 |
+
/// GetPosition2 β reads CGameTask+0x38/0x3C
|
| 92 |
+
#[slot(1)]
|
| 93 |
+
pub get_position2: fn(this: *const SoundEmitter, out_x: *mut u32, out_y: *mut u32),
|
| 94 |
+
/// Destructor
|
| 95 |
+
#[slot(3)]
|
| 96 |
+
pub destructor: fn(this: *mut SoundEmitter, flags: u32) -> *mut SoundEmitter,
|
| 97 |
+
/// HandleMessage β sound queue manager
|
| 98 |
+
#[slot(4)]
|
| 99 |
+
pub handle_message: fn(this: *mut SoundEmitter, sender: u32, msg_type: u32, size: u32, data: u32),
|
| 100 |
}
|
|
@@ -25,56 +25,56 @@ crate::define_addresses! {
|
|
| 25 |
/// 0x38 SetState 0x3C CheckPendingAction 0x40 IsNotOnRope
|
| 26 |
/// 0x44 inherited 0x48 GetTeamIndex 0x4C inherited
|
| 27 |
/// ```
|
| 28 |
-
#[
|
| 29 |
pub struct CTaskWormVTable {
|
| 30 |
-
///
|
| 31 |
-
|
| 32 |
-
pub write_replay_state:
|
| 33 |
-
///
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
///
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
///
|
| 44 |
-
|
| 45 |
-
pub on_contact_entity:
|
| 46 |
-
///
|
| 47 |
-
|
| 48 |
-
pub on_worm_push:
|
| 49 |
-
///
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
pub
|
| 58 |
-
//
|
| 59 |
-
///
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
///
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
pub
|
| 68 |
-
///
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
///
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
|
| 76 |
-
const _: () = assert!(core::mem::size_of::<CTaskWormVTable>() == 20 * 4);
|
| 77 |
-
|
| 78 |
/// Worm entity task β the primary playable character in WA.
|
| 79 |
///
|
| 80 |
/// Extends CGameTask (0xFC bytes) with worm identity, physics overrides, and
|
|
|
|
| 25 |
/// 0x38 SetState 0x3C CheckPendingAction 0x40 IsNotOnRope
|
| 26 |
/// 0x44 inherited 0x48 GetTeamIndex 0x4C inherited
|
| 27 |
/// ```
|
| 28 |
+
#[openwa_core::vtable(size = 20)]
|
| 29 |
pub struct CTaskWormVTable {
|
| 30 |
+
/// WriteReplayState β serializes worm state to a replay stream
|
| 31 |
+
#[slot(0)]
|
| 32 |
+
pub write_replay_state: fn(this: *mut CTaskWorm, stream: *mut u8),
|
| 33 |
+
/// Free β calls inner destructor, then `_free(this)` if flags & 1
|
| 34 |
+
#[slot(1)]
|
| 35 |
+
pub free: fn(this: *mut CTaskWorm, flags: u8) -> *mut CTaskWorm,
|
| 36 |
+
/// HandleMessage β processes all TaskMessages sent to this worm
|
| 37 |
+
#[slot(2)]
|
| 38 |
+
pub handle_message: fn(this: *mut CTaskWorm, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 39 |
+
/// GetEntityData β returns worm data by query code
|
| 40 |
+
#[slot(3)]
|
| 41 |
+
pub get_entity_data: fn(this: *mut CTaskWorm, query: u32, param: u32, out: *mut u32) -> u32,
|
| 42 |
+
// Slots 4-6: Inherited CTask stubs (auto-filled as usize)
|
| 43 |
+
/// OnContactEntity β handles physical contact with another entity
|
| 44 |
+
#[slot(7)]
|
| 45 |
+
pub on_contact_entity: fn(this: *mut CTaskWorm, other: *mut CGameTask, flags: u32) -> u32,
|
| 46 |
+
/// OnWormPush β post-contact worm-worm push impulse
|
| 47 |
+
#[slot(8)]
|
| 48 |
+
pub on_worm_push: fn(this: *mut CTaskWorm, other: *mut CGameTask, flags: u32) -> u32,
|
| 49 |
+
/// OnLandBounce β worm lands on terrain; plays thud sound, bounce physics
|
| 50 |
+
#[slot(9)]
|
| 51 |
+
pub on_land_bounce: fn(this: *mut CTaskWorm),
|
| 52 |
+
/// OnLandSlide β secondary landing callback; sliding/friction physics
|
| 53 |
+
#[slot(10)]
|
| 54 |
+
pub on_land_slide: fn(this: *mut CTaskWorm),
|
| 55 |
+
/// OnSink β worm sinks in water/acid; transitions to drowning state
|
| 56 |
+
#[slot(11)]
|
| 57 |
+
pub on_sink: fn(this: *mut CTaskWorm, dx: i32, dy: i32) -> u32,
|
| 58 |
+
// Slot 12: Inherited (auto-filled)
|
| 59 |
+
/// OnKilled β worm death; plays death sound, transitions to dead state
|
| 60 |
+
#[slot(13)]
|
| 61 |
+
pub on_killed: fn(this: *mut CTaskWorm),
|
| 62 |
+
/// SetState β worm state machine; handles all state transitions
|
| 63 |
+
#[slot(14)]
|
| 64 |
+
pub set_state: fn(this: *mut CTaskWorm, state: u32),
|
| 65 |
+
/// CheckPendingAction β if field +0xBC is set, calls SetState(0x73)
|
| 66 |
+
#[slot(15)]
|
| 67 |
+
pub check_pending_action: fn(this: *mut CTaskWorm),
|
| 68 |
+
/// IsNotOnRope β returns true if worm state != 0x7C (rope-swinging)
|
| 69 |
+
#[slot(16)]
|
| 70 |
+
pub is_not_on_rope: fn(this: *const CTaskWorm) -> bool,
|
| 71 |
+
// Slot 17: Inherited (auto-filled)
|
| 72 |
+
/// GetTeamIndex β returns worm's team index (field +0xFC)
|
| 73 |
+
#[slot(18)]
|
| 74 |
+
pub get_team_index: fn(this: *const CTaskWorm) -> u32,
|
| 75 |
+
// Slot 19: Inherited (auto-filled)
|
| 76 |
}
|
| 77 |
|
|
|
|
|
|
|
| 78 |
/// Worm entity task β the primary playable character in WA.
|
| 79 |
///
|
| 80 |
/// Extends CGameTask (0xFC bytes) with worm identity, physics overrides, and
|
|
@@ -25,6 +25,508 @@ use proc_macro::TokenStream;
|
|
| 25 |
use quote::quote;
|
| 26 |
use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta};
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
/// Derive `HasFieldRegistry` for a `#[repr(C)]` struct.
|
| 29 |
///
|
| 30 |
/// Generates a static `StructFields` table with one `FieldEntry` per named
|
|
|
|
| 25 |
use quote::quote;
|
| 26 |
use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta};
|
| 27 |
|
| 28 |
+
// =========================================================================
|
| 29 |
+
// #[derive(Vtable)]
|
| 30 |
+
// =========================================================================
|
| 31 |
+
|
| 32 |
+
/// Derive a typed vtable struct from a sparse slot definition.
|
| 33 |
+
///
|
| 34 |
+
/// Only declare known methods with `#[slot(N)]`. The macro generates the full
|
| 35 |
+
/// `#[repr(C)]` struct with `usize` gap-fillers for unknown slots, plus:
|
| 36 |
+
///
|
| 37 |
+
/// - Registry metadata (`HasVtableRegistry` impl + `inventory` registration)
|
| 38 |
+
/// - A companion `bind_{VtableName}!` macro for generating calling wrappers
|
| 39 |
+
/// - Optional `pub const` + `AddrEntry` if `va` is specified
|
| 40 |
+
/// - Compile-time size assertion
|
| 41 |
+
///
|
| 42 |
+
/// # Attributes
|
| 43 |
+
///
|
| 44 |
+
/// - `#[vtable(size = N)]` β total number of slots (required)
|
| 45 |
+
/// - `#[vtable(va = 0x...)]` β Ghidra VA of the vtable in .rdata (optional)
|
| 46 |
+
/// - `#[vtable(class = "Name")]` β owning C++ class name (optional)
|
| 47 |
+
///
|
| 48 |
+
/// # Example
|
| 49 |
+
///
|
| 50 |
+
/// ```ignore
|
| 51 |
+
/// #[derive(Vtable)]
|
| 52 |
+
/// #[vtable(size = 5, va = 0x0066_A2E4, class = "Palette")]
|
| 53 |
+
/// pub struct PaletteVtable {
|
| 54 |
+
/// #[slot(2)]
|
| 55 |
+
/// pub set_mode: unsafe extern "thiscall" fn(*mut Palette, u32),
|
| 56 |
+
/// #[slot(3)]
|
| 57 |
+
/// pub init: unsafe extern "thiscall" fn(*mut Palette),
|
| 58 |
+
/// #[slot(4)]
|
| 59 |
+
/// pub reset: unsafe extern "thiscall" fn(*mut Palette),
|
| 60 |
+
/// }
|
| 61 |
+
/// ```
|
| 62 |
+
/// Attribute macro that transforms a sparse vtable definition into a full
|
| 63 |
+
/// `#[repr(C)]` struct with gap-fillers, registry metadata, and a binding macro.
|
| 64 |
+
///
|
| 65 |
+
/// See module docs for usage.
|
| 66 |
+
#[proc_macro_attribute]
|
| 67 |
+
pub fn vtable(attr: TokenStream, item: TokenStream) -> TokenStream {
|
| 68 |
+
let input = parse_macro_input!(item as DeriveInput);
|
| 69 |
+
let attr_tokens = proc_macro2::TokenStream::from(attr);
|
| 70 |
+
match vtable_impl(attr_tokens, input) {
|
| 71 |
+
Ok(ts) => ts.into(),
|
| 72 |
+
Err(e) => e.to_compile_error().into(),
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
fn vtable_impl(
|
| 77 |
+
attr: proc_macro2::TokenStream,
|
| 78 |
+
input: DeriveInput,
|
| 79 |
+
) -> syn::Result<proc_macro2::TokenStream> {
|
| 80 |
+
let struct_name = &input.ident;
|
| 81 |
+
let struct_name_str = struct_name.to_string();
|
| 82 |
+
let vis = &input.vis;
|
| 83 |
+
|
| 84 |
+
// Parse vtable(size = N, va = 0x..., class = "...") from the attribute args
|
| 85 |
+
let (vtable_size, vtable_va, class_name) = parse_vtable_attr_args(attr, struct_name)?;
|
| 86 |
+
|
| 87 |
+
// Parse fields β each must have #[slot(N)]
|
| 88 |
+
let fields = match &input.data {
|
| 89 |
+
Data::Struct(data) => match &data.fields {
|
| 90 |
+
Fields::Named(named) => &named.named,
|
| 91 |
+
_ => {
|
| 92 |
+
return Err(syn::Error::new_spanned(
|
| 93 |
+
struct_name,
|
| 94 |
+
"Vtable requires named fields",
|
| 95 |
+
));
|
| 96 |
+
}
|
| 97 |
+
},
|
| 98 |
+
_ => {
|
| 99 |
+
return Err(syn::Error::new_spanned(
|
| 100 |
+
struct_name,
|
| 101 |
+
"Vtable can only be used on structs",
|
| 102 |
+
));
|
| 103 |
+
}
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
// Collect slots. #[slot(N)] is optional β if omitted, indices are assigned
|
| 107 |
+
// sequentially. If ANY field has #[slot(N)], all must (mixing is an error).
|
| 108 |
+
let mut slots: Vec<SlotInfo> = Vec::new();
|
| 109 |
+
let mut has_explicit_slot = false;
|
| 110 |
+
let mut has_implicit_slot = false;
|
| 111 |
+
|
| 112 |
+
for (field_idx, field) in fields.iter().enumerate() {
|
| 113 |
+
let ident = field.ident.as_ref().ok_or_else(|| {
|
| 114 |
+
syn::Error::new_spanned(field, "Vtable fields must be named")
|
| 115 |
+
})?;
|
| 116 |
+
|
| 117 |
+
let explicit_index = parse_slot_attr(&field.attrs, ident)?;
|
| 118 |
+
|
| 119 |
+
let slot_index = match explicit_index {
|
| 120 |
+
Some(idx) => {
|
| 121 |
+
has_explicit_slot = true;
|
| 122 |
+
idx
|
| 123 |
+
}
|
| 124 |
+
None => {
|
| 125 |
+
has_implicit_slot = true;
|
| 126 |
+
field_idx as u32
|
| 127 |
+
}
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
if has_explicit_slot && has_implicit_slot {
|
| 131 |
+
return Err(syn::Error::new_spanned(
|
| 132 |
+
ident,
|
| 133 |
+
"cannot mix fields with and without #[slot(N)] β use it on all or none",
|
| 134 |
+
));
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
if slot_index >= vtable_size {
|
| 138 |
+
return Err(syn::Error::new_spanned(
|
| 139 |
+
ident,
|
| 140 |
+
format!("slot index {} >= vtable size {}", slot_index, vtable_size),
|
| 141 |
+
));
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Check for duplicates
|
| 145 |
+
if slots.iter().any(|s| s.index == slot_index) {
|
| 146 |
+
return Err(syn::Error::new_spanned(
|
| 147 |
+
ident,
|
| 148 |
+
format!("duplicate slot index {}", slot_index),
|
| 149 |
+
));
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
let doc = extract_doc_comment(&field.attrs);
|
| 153 |
+
let kept_attrs: Vec<_> = field
|
| 154 |
+
.attrs
|
| 155 |
+
.iter()
|
| 156 |
+
.filter(|a| !a.path().is_ident("slot"))
|
| 157 |
+
.cloned()
|
| 158 |
+
.collect();
|
| 159 |
+
|
| 160 |
+
// Normalize fn type: plain `fn(...)` becomes `unsafe extern "thiscall" fn(...)`
|
| 161 |
+
let normalized_ty = normalize_vtable_fn_type(&field.ty);
|
| 162 |
+
|
| 163 |
+
slots.push(SlotInfo {
|
| 164 |
+
index: slot_index,
|
| 165 |
+
ident: ident.clone(),
|
| 166 |
+
ty: normalized_ty,
|
| 167 |
+
doc,
|
| 168 |
+
attrs: kept_attrs,
|
| 169 |
+
vis: field.vis.clone(),
|
| 170 |
+
});
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Sort by slot index
|
| 174 |
+
slots.sort_by_key(|s| s.index);
|
| 175 |
+
|
| 176 |
+
// Generate struct fields: gap-fillers + typed slots
|
| 177 |
+
let mut struct_fields = Vec::new();
|
| 178 |
+
let mut next_index = 0u32;
|
| 179 |
+
|
| 180 |
+
for slot in &slots {
|
| 181 |
+
// Fill gaps with usize fields
|
| 182 |
+
while next_index < slot.index {
|
| 183 |
+
let gap_name = quote::format_ident!("_slot_{}", next_index);
|
| 184 |
+
struct_fields.push(quote! {
|
| 185 |
+
pub #gap_name: usize
|
| 186 |
+
});
|
| 187 |
+
next_index += 1;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Emit the typed slot
|
| 191 |
+
let ident = &slot.ident;
|
| 192 |
+
let ty = &slot.ty;
|
| 193 |
+
let attrs = &slot.attrs;
|
| 194 |
+
let slot_vis = &slot.vis;
|
| 195 |
+
struct_fields.push(quote! {
|
| 196 |
+
#(#attrs)*
|
| 197 |
+
#slot_vis #ident: #ty
|
| 198 |
+
});
|
| 199 |
+
next_index = slot.index + 1;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Fill trailing gaps
|
| 203 |
+
while next_index < vtable_size {
|
| 204 |
+
let gap_name = quote::format_ident!("_slot_{}", next_index);
|
| 205 |
+
struct_fields.push(quote! {
|
| 206 |
+
pub #gap_name: usize
|
| 207 |
+
});
|
| 208 |
+
next_index += 1;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Generate registry slot entries
|
| 212 |
+
let slot_entries: Vec<_> = slots
|
| 213 |
+
.iter()
|
| 214 |
+
.map(|s| {
|
| 215 |
+
let idx = s.index;
|
| 216 |
+
let name = s.ident.to_string();
|
| 217 |
+
let doc = &s.doc;
|
| 218 |
+
quote! {
|
| 219 |
+
openwa_core::registry::VtableSlotEntry {
|
| 220 |
+
index: #idx,
|
| 221 |
+
name: #name,
|
| 222 |
+
doc: #doc,
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
})
|
| 226 |
+
.collect();
|
| 227 |
+
let slot_entry_count = slot_entries.len();
|
| 228 |
+
|
| 229 |
+
let class_name_str = class_name.as_deref().unwrap_or("");
|
| 230 |
+
let va_value = vtable_va.unwrap_or(0);
|
| 231 |
+
|
| 232 |
+
// Generate the info static name
|
| 233 |
+
let info_static = quote::format_ident!("_VTABLE_INFO_{}", struct_name);
|
| 234 |
+
|
| 235 |
+
// Generate optional pub const + AddrEntry for the VA
|
| 236 |
+
let va_items = if let Some(va) = vtable_va {
|
| 237 |
+
// Derive const name: class "Palette" β PALETTE_VTABLE, or struct PaletteVtable β PALETTE_VTABLE
|
| 238 |
+
let const_name_str = if let Some(ref class) = class_name {
|
| 239 |
+
format!("{}_VTABLE", to_screaming_snake(class))
|
| 240 |
+
} else {
|
| 241 |
+
to_screaming_snake(&struct_name_str)
|
| 242 |
+
};
|
| 243 |
+
let const_ident = quote::format_ident!("{}", const_name_str);
|
| 244 |
+
let class_expr = if let Some(ref class) = class_name {
|
| 245 |
+
quote! { Some(#class) }
|
| 246 |
+
} else {
|
| 247 |
+
quote! { None }
|
| 248 |
+
};
|
| 249 |
+
quote! {
|
| 250 |
+
#vis const #const_ident: u32 = #va;
|
| 251 |
+
|
| 252 |
+
openwa_core::inventory::submit! {
|
| 253 |
+
openwa_core::registry::AddrEntry {
|
| 254 |
+
va: #va,
|
| 255 |
+
name: #const_name_str,
|
| 256 |
+
kind: openwa_core::registry::AddrKind::Vtable,
|
| 257 |
+
calling_conv: None,
|
| 258 |
+
class_name: #class_expr,
|
| 259 |
+
doc: "",
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
} else {
|
| 264 |
+
quote! {}
|
| 265 |
+
};
|
| 266 |
+
|
| 267 |
+
// Generate the companion bind macro
|
| 268 |
+
let bind_macro_name = quote::format_ident!("bind_{}", struct_name);
|
| 269 |
+
let bind_methods = generate_bind_methods(&slots)?;
|
| 270 |
+
|
| 271 |
+
let vtable_size_usize = vtable_size as usize;
|
| 272 |
+
|
| 273 |
+
let expanded = quote! {
|
| 274 |
+
#[repr(C)]
|
| 275 |
+
#vis struct #struct_name {
|
| 276 |
+
#(#struct_fields),*
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
const _: () = assert!(
|
| 280 |
+
core::mem::size_of::<#struct_name>() == #vtable_size_usize * core::mem::size_of::<usize>(),
|
| 281 |
+
"vtable struct size mismatch"
|
| 282 |
+
);
|
| 283 |
+
|
| 284 |
+
// Vtable registry metadata
|
| 285 |
+
#[doc(hidden)]
|
| 286 |
+
#[allow(non_upper_case_globals)]
|
| 287 |
+
static #info_static: openwa_core::registry::VtableInfo = openwa_core::registry::VtableInfo {
|
| 288 |
+
struct_name: #struct_name_str,
|
| 289 |
+
class_name: #class_name_str,
|
| 290 |
+
ghidra_va: #va_value,
|
| 291 |
+
slot_count: #vtable_size,
|
| 292 |
+
slots: {
|
| 293 |
+
const ENTRIES: [openwa_core::registry::VtableSlotEntry; #slot_entry_count] = [
|
| 294 |
+
#(#slot_entries),*
|
| 295 |
+
];
|
| 296 |
+
&ENTRIES
|
| 297 |
+
},
|
| 298 |
+
};
|
| 299 |
+
|
| 300 |
+
impl openwa_core::registry::HasVtableRegistry for #struct_name {
|
| 301 |
+
fn vtable_info() -> &'static openwa_core::registry::VtableInfo {
|
| 302 |
+
&#info_static
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
openwa_core::inventory::submit! {
|
| 307 |
+
openwa_core::registry::VtableRegistration {
|
| 308 |
+
info: &#info_static,
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
#va_items
|
| 313 |
+
|
| 314 |
+
/// Bind this vtable's known methods as wrapper methods on a class struct.
|
| 315 |
+
///
|
| 316 |
+
/// Usage: `bind_VtableName!(ClassName, vtable_field_name);`
|
| 317 |
+
#[macro_export]
|
| 318 |
+
macro_rules! #bind_macro_name {
|
| 319 |
+
($class:ty, $vtable_field:ident) => {
|
| 320 |
+
impl $class {
|
| 321 |
+
#bind_methods
|
| 322 |
+
}
|
| 323 |
+
};
|
| 324 |
+
}
|
| 325 |
+
};
|
| 326 |
+
|
| 327 |
+
Ok(expanded)
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
/// A parsed vtable slot definition.
|
| 331 |
+
struct SlotInfo {
|
| 332 |
+
index: u32,
|
| 333 |
+
ident: syn::Ident,
|
| 334 |
+
ty: syn::Type,
|
| 335 |
+
doc: String,
|
| 336 |
+
attrs: Vec<syn::Attribute>,
|
| 337 |
+
vis: syn::Visibility,
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/// Generate the method wrappers for the bind macro.
|
| 341 |
+
///
|
| 342 |
+
/// For each slot, parses the fn pointer type to extract:
|
| 343 |
+
/// - `this` mutability (*mut β &mut self, *const β &self)
|
| 344 |
+
/// - Remaining parameters (with generated names p0, p1, ...)
|
| 345 |
+
/// - Return type
|
| 346 |
+
fn generate_bind_methods(slots: &[SlotInfo]) -> syn::Result<proc_macro2::TokenStream> {
|
| 347 |
+
let mut methods = Vec::new();
|
| 348 |
+
|
| 349 |
+
for slot in slots {
|
| 350 |
+
let ident = &slot.ident;
|
| 351 |
+
let doc = &slot.doc;
|
| 352 |
+
|
| 353 |
+
// Parse the fn pointer type
|
| 354 |
+
let bare_fn = match &slot.ty {
|
| 355 |
+
syn::Type::BareFn(bf) => bf,
|
| 356 |
+
other => {
|
| 357 |
+
return Err(syn::Error::new_spanned(
|
| 358 |
+
other,
|
| 359 |
+
"vtable slot type must be a bare fn pointer (e.g., unsafe extern \"thiscall\" fn(...))",
|
| 360 |
+
));
|
| 361 |
+
}
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
+
// First param is `this` β determine mutability
|
| 365 |
+
let inputs = &bare_fn.inputs;
|
| 366 |
+
if inputs.is_empty() {
|
| 367 |
+
return Err(syn::Error::new_spanned(
|
| 368 |
+
bare_fn,
|
| 369 |
+
"vtable fn must have at least a `this` parameter",
|
| 370 |
+
));
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
let this_is_const = is_const_ptr(&inputs[0].ty);
|
| 374 |
+
let self_param = if this_is_const {
|
| 375 |
+
quote! { &self }
|
| 376 |
+
} else {
|
| 377 |
+
quote! { &mut self }
|
| 378 |
+
};
|
| 379 |
+
let self_cast = if this_is_const {
|
| 380 |
+
quote! { self as *const Self }
|
| 381 |
+
} else {
|
| 382 |
+
quote! { self as *mut Self }
|
| 383 |
+
};
|
| 384 |
+
|
| 385 |
+
// Remaining params
|
| 386 |
+
let mut param_decls = Vec::new();
|
| 387 |
+
let mut param_names = Vec::new();
|
| 388 |
+
for (i, arg) in inputs.iter().skip(1).enumerate() {
|
| 389 |
+
let param_name = if let Some(ref name) = arg.name {
|
| 390 |
+
let (ident, _) = name;
|
| 391 |
+
ident.clone()
|
| 392 |
+
} else {
|
| 393 |
+
quote::format_ident!("p{}", i)
|
| 394 |
+
};
|
| 395 |
+
let param_ty = &arg.ty;
|
| 396 |
+
param_decls.push(quote! { #param_name: #param_ty });
|
| 397 |
+
param_names.push(param_name);
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// Return type
|
| 401 |
+
let ret_ty = match &bare_fn.output {
|
| 402 |
+
syn::ReturnType::Default => quote! {},
|
| 403 |
+
syn::ReturnType::Type(_, ty) => quote! { -> #ty },
|
| 404 |
+
};
|
| 405 |
+
|
| 406 |
+
methods.push(quote! {
|
| 407 |
+
#[doc = #doc]
|
| 408 |
+
pub unsafe fn #ident(#self_param #(, #param_decls)*) #ret_ty {
|
| 409 |
+
((*self.$vtable_field).#ident)(#self_cast #(, #param_names)*)
|
| 410 |
+
}
|
| 411 |
+
});
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
Ok(quote! { #(#methods)* })
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
/// Normalize a vtable fn pointer type: if it's a plain `fn(...)` (no `unsafe`,
|
| 418 |
+
/// no explicit ABI), transform it to `unsafe extern "thiscall" fn(...)`.
|
| 419 |
+
///
|
| 420 |
+
/// Already-qualified types (e.g., `unsafe extern "thiscall" fn(...)`) pass through unchanged.
|
| 421 |
+
fn normalize_vtable_fn_type(ty: &syn::Type) -> syn::Type {
|
| 422 |
+
if let syn::Type::BareFn(bare) = ty {
|
| 423 |
+
let needs_unsafe = bare.unsafety.is_none();
|
| 424 |
+
let needs_abi = bare.abi.is_none();
|
| 425 |
+
|
| 426 |
+
if needs_unsafe || needs_abi {
|
| 427 |
+
let mut normalized = bare.clone();
|
| 428 |
+
if needs_unsafe {
|
| 429 |
+
normalized.unsafety = Some(syn::token::Unsafe::default());
|
| 430 |
+
}
|
| 431 |
+
if needs_abi {
|
| 432 |
+
normalized.abi = Some(syn::Abi {
|
| 433 |
+
extern_token: syn::token::Extern::default(),
|
| 434 |
+
name: Some(syn::LitStr::new("thiscall", proc_macro2::Span::call_site())),
|
| 435 |
+
});
|
| 436 |
+
}
|
| 437 |
+
return syn::Type::BareFn(normalized);
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
ty.clone()
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/// Check if a type is `*const T` (as opposed to `*mut T`).
|
| 444 |
+
fn is_const_ptr(ty: &syn::Type) -> bool {
|
| 445 |
+
if let syn::Type::Ptr(ptr) = ty {
|
| 446 |
+
ptr.const_token.is_some()
|
| 447 |
+
} else {
|
| 448 |
+
false
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
/// Parse the attribute arguments: `size = N, va = 0x..., class = "..."`.
|
| 453 |
+
fn parse_vtable_attr_args(
|
| 454 |
+
attr: proc_macro2::TokenStream,
|
| 455 |
+
span: &syn::Ident,
|
| 456 |
+
) -> syn::Result<(u32, Option<u32>, Option<String>)> {
|
| 457 |
+
// Parse as a comma-separated list of key = value pairs
|
| 458 |
+
struct VtableArgs {
|
| 459 |
+
size: Option<u32>,
|
| 460 |
+
va: Option<u32>,
|
| 461 |
+
class: Option<String>,
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
let mut args = VtableArgs {
|
| 465 |
+
size: None,
|
| 466 |
+
va: None,
|
| 467 |
+
class: None,
|
| 468 |
+
};
|
| 469 |
+
|
| 470 |
+
// Use syn's parser by wrapping in parens
|
| 471 |
+
let parser = syn::meta::parser(|meta| {
|
| 472 |
+
if meta.path.is_ident("size") {
|
| 473 |
+
let _eq: syn::Token![=] = meta.input.parse()?;
|
| 474 |
+
let lit: syn::LitInt = meta.input.parse()?;
|
| 475 |
+
args.size = Some(lit.base10_parse()?);
|
| 476 |
+
} else if meta.path.is_ident("va") {
|
| 477 |
+
let _eq: syn::Token![=] = meta.input.parse()?;
|
| 478 |
+
let lit: syn::LitInt = meta.input.parse()?;
|
| 479 |
+
args.va = Some(lit.base10_parse()?);
|
| 480 |
+
} else if meta.path.is_ident("class") {
|
| 481 |
+
let _eq: syn::Token![=] = meta.input.parse()?;
|
| 482 |
+
let lit: syn::LitStr = meta.input.parse()?;
|
| 483 |
+
args.class = Some(lit.value());
|
| 484 |
+
} else {
|
| 485 |
+
return Err(meta.error("unknown vtable attribute"));
|
| 486 |
+
}
|
| 487 |
+
Ok(())
|
| 488 |
+
});
|
| 489 |
+
|
| 490 |
+
syn::parse::Parser::parse2(parser, attr)?;
|
| 491 |
+
|
| 492 |
+
let size = args.size.ok_or_else(|| {
|
| 493 |
+
syn::Error::new_spanned(span, "vtable attribute requires `size = N`")
|
| 494 |
+
})?;
|
| 495 |
+
|
| 496 |
+
Ok((size, args.va, args.class))
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
/// Parse #[slot(N)] attribute from a field.
|
| 500 |
+
fn parse_slot_attr(attrs: &[syn::Attribute], _span: &syn::Ident) -> syn::Result<Option<u32>> {
|
| 501 |
+
for attr in attrs {
|
| 502 |
+
if attr.path().is_ident("slot") {
|
| 503 |
+
let index: syn::LitInt = attr.parse_args()?;
|
| 504 |
+
return Ok(Some(index.base10_parse()?));
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
Ok(None)
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/// Convert "PaletteVtable" or "Palette" to "PALETTE_VTABLE" or "PALETTE".
|
| 511 |
+
fn to_screaming_snake(name: &str) -> String {
|
| 512 |
+
let mut result = String::new();
|
| 513 |
+
for (i, ch) in name.chars().enumerate() {
|
| 514 |
+
if ch.is_uppercase() && i > 0 {
|
| 515 |
+
// Don't insert underscore between consecutive uppercase (e.g., "DDGame" β "DD_GAME")
|
| 516 |
+
let prev = name.chars().nth(i - 1).unwrap_or('_');
|
| 517 |
+
if prev.is_lowercase() || prev.is_numeric() {
|
| 518 |
+
result.push('_');
|
| 519 |
+
} else if let Some(next) = name.chars().nth(i + 1) {
|
| 520 |
+
if next.is_lowercase() {
|
| 521 |
+
result.push('_');
|
| 522 |
+
}
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
result.push(ch.to_ascii_uppercase());
|
| 526 |
+
}
|
| 527 |
+
result
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
/// Derive `HasFieldRegistry` for a `#[repr(C)]` struct.
|
| 531 |
///
|
| 532 |
/// Generates a static `StructFields` table with one `FieldEntry` per named
|
|
@@ -368,7 +368,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
|
|
| 368 |
(*session).keyboard = kb;
|
| 369 |
|
| 370 |
// ββ Palette (inline construction) βββββββββββββββββββββββββββββββββββββ
|
| 371 |
-
let pal = WABox::from_value(Palette::new(rb(va::
|
| 372 |
(*session).palette = pal;
|
| 373 |
|
| 374 |
// ββ DSSound βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 368 |
(*session).keyboard = kb;
|
| 369 |
|
| 370 |
// ββ Palette (inline construction) βββββββββββββββββββββββββββββββββββββ
|
| 371 |
+
let pal = WABox::from_value(Palette::new(rb(va::PALETTE_VTABLE))).leak();
|
| 372 |
(*session).palette = pal;
|
| 373 |
|
| 374 |
// ββ DSSound βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -0,0 +1,337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design: `#[vtable(...)]` System for OpenWA
|
| 2 |
+
|
| 3 |
+
## Context
|
| 4 |
+
|
| 5 |
+
OpenWA has 7+ manually-written vtable structs (`PaletteVtable`, `DisplayBaseVtable`, `DDDisplayVtable`, etc.) with hand-coded `#[repr(C)]` layouts, manual `impl` wrapper methods, and ad-hoc vtable patching. The existing `#[derive(FieldRegistry)]` macro and `inventory`-based registry provide a proven pattern for auto-generating struct metadata. This design extends that infrastructure to vtables, providing a single definition per vtable that handles introspection, calling, and replacement.
|
| 6 |
+
|
| 7 |
+
**Constraints:**
|
| 8 |
+
- A C++ class can have multiple vtables at different struct offsets (multiple inheritance)
|
| 9 |
+
- The same vtable type can theoretically serve multiple classes
|
| 10 |
+
- All vtable methods are `unsafe extern "thiscall"` (ECX = this)
|
| 11 |
+
- Target is i686-pc-windows-msvc (32-bit, 4-byte pointers/fn pointers)
|
| 12 |
+
|
| 13 |
+
## Design
|
| 14 |
+
|
| 15 |
+
### 1. Vtable Definition (`#[vtable(...)]`)
|
| 16 |
+
|
| 17 |
+
**Input syntax** β declare only known methods with `#[slot(N)]`:
|
| 18 |
+
|
| 19 |
+
```rust
|
| 20 |
+
#[openwa_core::vtable(size = 5, va = 0x0066_A2E4, class = "Palette")]
|
| 21 |
+
pub struct PaletteVtable {
|
| 22 |
+
/// set_mode(this, mode) β called with mode=7 during hardware init
|
| 23 |
+
#[slot(2)]
|
| 24 |
+
pub set_mode: unsafe extern "thiscall" fn(*mut Palette, u32),
|
| 25 |
+
/// init(this) β called during hardware init
|
| 26 |
+
#[slot(3)]
|
| 27 |
+
pub init: unsafe extern "thiscall" fn(*mut Palette),
|
| 28 |
+
/// reset(this) β called first during hardware init
|
| 29 |
+
#[slot(4)]
|
| 30 |
+
pub reset: unsafe extern "thiscall" fn(*mut Palette),
|
| 31 |
+
}
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
**Generated output:**
|
| 35 |
+
|
| 36 |
+
```rust
|
| 37 |
+
// 1. The actual #[repr(C)] struct with gap-fillers
|
| 38 |
+
#[repr(C)]
|
| 39 |
+
pub struct PaletteVtable {
|
| 40 |
+
pub _slot_0: usize,
|
| 41 |
+
pub _slot_1: usize,
|
| 42 |
+
/// set_mode(this, mode) β called with mode=7 during hardware init
|
| 43 |
+
pub set_mode: unsafe extern "thiscall" fn(*mut Palette, u32),
|
| 44 |
+
/// init(this) β called during hardware init
|
| 45 |
+
pub init: unsafe extern "thiscall" fn(*mut Palette),
|
| 46 |
+
/// reset(this) β called first during hardware init
|
| 47 |
+
pub reset: unsafe extern "thiscall" fn(*mut Palette),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// 2. Registry integration (introspection)
|
| 51 |
+
impl HasVtableRegistry for PaletteVtable {
|
| 52 |
+
fn vtable_info() -> &'static VtableInfo {
|
| 53 |
+
static INFO: VtableInfo = VtableInfo {
|
| 54 |
+
struct_name: "PaletteVtable",
|
| 55 |
+
class_name: "Palette",
|
| 56 |
+
ghidra_va: 0x0066_A2E4,
|
| 57 |
+
slot_count: 5,
|
| 58 |
+
slots: &[
|
| 59 |
+
VtableSlotEntry { index: 2, name: "set_mode", doc: "set_mode(this, mode) ..." },
|
| 60 |
+
VtableSlotEntry { index: 3, name: "init", doc: "init(this) ..." },
|
| 61 |
+
VtableSlotEntry { index: 4, name: "reset", doc: "reset(this) ..." },
|
| 62 |
+
],
|
| 63 |
+
};
|
| 64 |
+
&INFO
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
inventory::submit!(VtableRegistration {
|
| 69 |
+
info: <PaletteVtable as HasVtableRegistry>::vtable_info(),
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
// 3. Address registry entry (replaces define_addresses! for this vtable)
|
| 73 |
+
inventory::submit!(AddrEntry {
|
| 74 |
+
va: 0x0066_A2E4,
|
| 75 |
+
name: "PALETTE_VTABLE",
|
| 76 |
+
kind: AddrKind::Vtable,
|
| 77 |
+
class_name: Some("Palette"),
|
| 78 |
+
calling_convention: None,
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
// 4. Companion binding macro (see Part 2)
|
| 82 |
+
#[macro_export]
|
| 83 |
+
macro_rules! bind_PaletteVtable { ... }
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
**Proc macro implementation** in `openwa-derive/src/lib.rs`:
|
| 87 |
+
- Parse `#[vtable(...)]` attributes for `size`, `va` (optional), `class` (optional)
|
| 88 |
+
- Parse `#[slot(N)]` on each field to determine position
|
| 89 |
+
- Validate: no duplicate slots, all slots < size, slot indices ascending
|
| 90 |
+
- Generate gap-filler fields `_slot_N: usize` for missing indices
|
| 91 |
+
- Preserve doc comments, visibility, and field types from input
|
| 92 |
+
- Generate compile-time size assertion: `const _: () = assert!(size_of::<VtableName>() == size * 4);`
|
| 93 |
+
- Parse fn pointer types to extract `this` type (`*mut T` or `*const T`), params, and return type
|
| 94 |
+
- Generate `HasVtableRegistry` impl, inventory submissions, and binding macro
|
| 95 |
+
- If `va` is provided: generate `pub const` and `AddrEntry` submission. Name derived from `class` attribute: `"Palette"` β `PALETTE_VTABLE`. If no `class`, derive from struct name: `PaletteVtable` β `PALETTE_VTABLE`
|
| 96 |
+
- If `va` is omitted: no `AddrEntry`, no const β vtable exists purely as a type
|
| 97 |
+
|
| 98 |
+
### 2. Calling Wrappers (`bind_XxxVtable!`)
|
| 99 |
+
|
| 100 |
+
The derive generates a companion macro named `bind_{VtableName}!` that creates method wrappers:
|
| 101 |
+
|
| 102 |
+
```rust
|
| 103 |
+
// Generated by #[derive(Vtable)] on PaletteVtable:
|
| 104 |
+
#[macro_export]
|
| 105 |
+
macro_rules! bind_PaletteVtable {
|
| 106 |
+
($class:ty, $vtable_field:ident) => {
|
| 107 |
+
impl $class {
|
| 108 |
+
#[doc = "set_mode(this, mode) β called with mode=7 during hardware init"]
|
| 109 |
+
pub unsafe fn set_mode(&mut self, mode: u32) {
|
| 110 |
+
((*self.$vtable_field).set_mode)(self as *mut Self, mode)
|
| 111 |
+
}
|
| 112 |
+
#[doc = "init(this) β called during hardware init"]
|
| 113 |
+
pub unsafe fn init(&mut self) {
|
| 114 |
+
((*self.$vtable_field).init)(self as *mut Self)
|
| 115 |
+
}
|
| 116 |
+
#[doc = "reset(this) β called first during hardware init"]
|
| 117 |
+
pub unsafe fn reset(&mut self) {
|
| 118 |
+
((*self.$vtable_field).reset)(self as *mut Self)
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
};
|
| 122 |
+
}
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
**Usage:**
|
| 126 |
+
```rust
|
| 127 |
+
// In palette.rs, after struct Palette { vtable: *const PaletteVtable, ... }
|
| 128 |
+
bind_PaletteVtable!(Palette, vtable);
|
| 129 |
+
|
| 130 |
+
// Now you can write:
|
| 131 |
+
(*pal).set_mode(7); // instead of vcall!(pal, set_mode, 7)
|
| 132 |
+
(*pal).init();
|
| 133 |
+
(*pal).reset();
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
**Multiple vtables (multiple inheritance):**
|
| 137 |
+
```rust
|
| 138 |
+
// CGameTask has primary vtable at +0x00
|
| 139 |
+
bind_CGameTaskVtable!(CGameTask, vtable);
|
| 140 |
+
|
| 141 |
+
// SoundEmitter is a sub-object with its own vtable β bind on its own struct
|
| 142 |
+
bind_SoundEmitterVTable!(SoundEmitter, vtable);
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
**Method signature extraction:** The proc macro parses the fn pointer type as `syn::Type::BareFn` to extract:
|
| 146 |
+
- First param is `this` β if `*const T`, generates `&self`; if `*mut T`, generates `&mut self`
|
| 147 |
+
- Remaining params become the wrapper's named parameters (e.g., `p0: u32`, `p1: *mut u8`)
|
| 148 |
+
- Return type is preserved (default `()` if absent)
|
| 149 |
+
- The wrapper calls through `self.$vtable_field` and casts `self` to the correct pointer type
|
| 150 |
+
|
| 151 |
+
**Type safety:** The binding macro passes `self as *mut Self` (or `*const Self`) as the `this` argument. The `$class` type passed to the macro must match the `this` pointer type in the vtable fn signatures. If mismatched, Rust's pointer cast rules apply silently β the macro does not enforce this, but the mismatch would be visible at the invocation site.
|
| 152 |
+
|
| 153 |
+
**Note on `&mut self`:** This follows the existing hand-written pattern (e.g., `Palette::reset(&mut self)`). Using `&mut self` creates a mutable reference from what is often a raw pointer, which could theoretically violate aliasing rules. This is acceptable for this project since all callers are already in `unsafe` blocks working with raw game memory, and the `vcall!` macro remains available for raw-pointer-only dispatch.
|
| 154 |
+
|
| 155 |
+
### 3. Vtable Replacement (`vtable_replace!`)
|
| 156 |
+
|
| 157 |
+
A declarative macro that expands to executable code for use inside `install()` functions:
|
| 158 |
+
|
| 159 |
+
```rust
|
| 160 |
+
pub fn install() -> Result<(), String> {
|
| 161 |
+
unsafe {
|
| 162 |
+
vtable_replace!(PaletteVtable at va::PALETTE_VTABLE {
|
| 163 |
+
slot set_mode => my_set_mode,
|
| 164 |
+
slot init => my_init,
|
| 165 |
+
})?;
|
| 166 |
+
}
|
| 167 |
+
Ok(())
|
| 168 |
+
}
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
**Expanded code** (for `slot 2 => my_set_mode` β pure replace, no original saved):
|
| 172 |
+
|
| 173 |
+
```rust
|
| 174 |
+
{
|
| 175 |
+
let vtable_ptr = $crate::rebase::rb(va::PALETTE_VTABLE) as *mut u32;
|
| 176 |
+
let slot_count = core::mem::size_of::<PaletteVtable>() / 4;
|
| 177 |
+
$crate::vtable::patch_vtable(vtable_ptr, slot_count, |vt| {
|
| 178 |
+
// Slot 2: pure replace
|
| 179 |
+
let slot = vt.add(2);
|
| 180 |
+
*slot = my_set_mode as *const () as u32;
|
| 181 |
+
})
|
| 182 |
+
}
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
**Expanded code** (for `slot 3 [originals::INIT] => my_init` β replace + save original):
|
| 186 |
+
|
| 187 |
+
```rust
|
| 188 |
+
{
|
| 189 |
+
let vtable_ptr = $crate::rebase::rb(va::PALETTE_VTABLE) as *mut u32;
|
| 190 |
+
let slot_count = core::mem::size_of::<PaletteVtable>() / 4;
|
| 191 |
+
$crate::vtable::patch_vtable(vtable_ptr, slot_count, |vt| {
|
| 192 |
+
// Slot 3: save original, then replace
|
| 193 |
+
let slot = vt.add(3);
|
| 194 |
+
originals::INIT.store(*slot, core::sync::atomic::Ordering::Relaxed);
|
| 195 |
+
*slot = my_init as *const () as u32;
|
| 196 |
+
})
|
| 197 |
+
}
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
**Calling the original:** User defines statics in a visible scope and passes them via `[static]` syntax:
|
| 201 |
+
|
| 202 |
+
```rust
|
| 203 |
+
mod originals {
|
| 204 |
+
pub static INIT: core::sync::atomic::AtomicU32 =
|
| 205 |
+
core::sync::atomic::AtomicU32::new(0);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
vtable_replace!(PaletteVtable at va::PALETTE_VTABLE {
|
| 209 |
+
slot 2 => my_set_mode, // no original needed
|
| 210 |
+
slot 3 [originals::INIT] => my_init, // save original
|
| 211 |
+
})?;
|
| 212 |
+
|
| 213 |
+
// In the replacement:
|
| 214 |
+
unsafe extern "thiscall" fn my_init(this: *mut Palette) {
|
| 215 |
+
log!("init()");
|
| 216 |
+
let orig: unsafe extern "thiscall" fn(*mut Palette) =
|
| 217 |
+
core::mem::transmute(originals::INIT.load(Ordering::Relaxed));
|
| 218 |
+
orig(this);
|
| 219 |
+
}
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
The `[static_name]` syntax is optional β if omitted, no original is saved (pure replacement, no call-through). The macro does NOT generate inaccessible statics.
|
| 223 |
+
|
| 224 |
+
**Slot index resolution:** Slot indices are specified explicitly in the macro call. This is compile-time, simple, and the indices are directly visible in the vtable definition above.
|
| 225 |
+
|
| 226 |
+
**Size derivation:** The `size` parameter is NOT required β the macro computes it as `size_of::<VtableType>() / 4` from the vtable struct type, which already encodes the correct size via `#[vtable(size = N)]`.
|
| 227 |
+
|
| 228 |
+
Revised syntax:
|
| 229 |
+
```rust
|
| 230 |
+
vtable_replace!(PaletteVtable at va::PALETTE_VTABLE {
|
| 231 |
+
slot 2 => my_set_mode, // pure replace, original discarded
|
| 232 |
+
slot 3 [originals::INIT] => my_init, // replace + save original to static
|
| 233 |
+
})?;
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
### 4. New Types in `openwa-core::registry`
|
| 237 |
+
|
| 238 |
+
```rust
|
| 239 |
+
/// Metadata for a single vtable slot
|
| 240 |
+
pub struct VtableSlotEntry {
|
| 241 |
+
pub index: u32,
|
| 242 |
+
pub name: &'static str,
|
| 243 |
+
pub doc: &'static str,
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/// Metadata for a vtable struct
|
| 247 |
+
pub struct VtableInfo {
|
| 248 |
+
pub struct_name: &'static str,
|
| 249 |
+
pub class_name: &'static str,
|
| 250 |
+
pub ghidra_va: u32,
|
| 251 |
+
pub slot_count: u32,
|
| 252 |
+
pub slots: &'static [VtableSlotEntry],
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
impl VtableInfo {
|
| 256 |
+
pub fn slot_by_name(&self, name: &str) -> Option<&VtableSlotEntry> { ... }
|
| 257 |
+
pub fn slot_by_index(&self, index: u32) -> Option<&VtableSlotEntry> { ... }
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
/// Trait implemented by #[derive(Vtable)] structs
|
| 261 |
+
pub trait HasVtableRegistry {
|
| 262 |
+
fn vtable_info() -> &'static VtableInfo;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
/// Inventory collection wrapper
|
| 266 |
+
pub struct VtableRegistration {
|
| 267 |
+
pub info: &'static VtableInfo,
|
| 268 |
+
}
|
| 269 |
+
inventory::collect!(VtableRegistration);
|
| 270 |
+
|
| 271 |
+
/// Query API
|
| 272 |
+
pub fn vtable_info_for(struct_name: &str) -> Option<&'static VtableInfo> { ... }
|
| 273 |
+
pub fn all_vtable_info() -> impl Iterator<Item = &'static VtableInfo> { ... }
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
### 5. `vtable::patch_vtable` Helper
|
| 277 |
+
|
| 278 |
+
Move/generalize the existing vtable patching logic from `display.rs` into a shared utility:
|
| 279 |
+
|
| 280 |
+
```rust
|
| 281 |
+
// openwa-core::vtable (new module)
|
| 282 |
+
|
| 283 |
+
/// Temporarily makes a vtable writable, calls the patcher, restores protection.
|
| 284 |
+
/// Returns Err if VirtualProtect fails.
|
| 285 |
+
pub unsafe fn patch_vtable(
|
| 286 |
+
vtable_ptr: *mut u32,
|
| 287 |
+
slot_count: usize,
|
| 288 |
+
patcher: impl FnOnce(*mut u32),
|
| 289 |
+
) -> Result<(), String> {
|
| 290 |
+
let size = slot_count * 4;
|
| 291 |
+
let mut old_protect = 0u32;
|
| 292 |
+
if VirtualProtect(vtable_ptr as _, size, PAGE_READWRITE, &mut old_protect) == 0 {
|
| 293 |
+
return Err("VirtualProtect failed".into());
|
| 294 |
+
}
|
| 295 |
+
patcher(vtable_ptr);
|
| 296 |
+
VirtualProtect(vtable_ptr as _, size, old_protect, &mut old_protect);
|
| 297 |
+
Ok(())
|
| 298 |
+
}
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
This already exists in `display.rs` β factor it out.
|
| 302 |
+
|
| 303 |
+
## File Changes
|
| 304 |
+
|
| 305 |
+
| File | Change |
|
| 306 |
+
|------|--------|
|
| 307 |
+
| `crates/openwa-derive/src/lib.rs` | Add `#[vtable(...)]` proc macro (~150 lines) |
|
| 308 |
+
| `crates/openwa-core/src/registry.rs` | Add `VtableInfo`, `VtableSlotEntry`, `HasVtableRegistry`, `VtableRegistration`, query API (~60 lines) |
|
| 309 |
+
| `crates/openwa-core/src/vtable.rs` | New module: `patch_vtable` helper (factored from display.rs) |
|
| 310 |
+
| `crates/openwa-core/src/macros.rs` | Add `vtable_replace!` macro (~40 lines) |
|
| 311 |
+
| `crates/openwa-core/src/lib.rs` | Add `pub mod vtable;`, re-export `Vtable` derive |
|
| 312 |
+
|
| 313 |
+
## Migration: PaletteVtable (first target)
|
| 314 |
+
|
| 315 |
+
1. Rewrite `PaletteVtable` in `display/palette.rs` to use `#[vtable(...)]`
|
| 316 |
+
2. Add `bind_PaletteVtable!(Palette, vtable)` β replaces 3 manual wrapper methods
|
| 317 |
+
3. Remove the `define_addresses!` entry for `PALETTE_VTABLE_MAYBE` (now in derive attribute)
|
| 318 |
+
4. Verify `vcall!(pal, set_mode, 7)` still works (struct layout identical)
|
| 319 |
+
5. Run replay tests
|
| 320 |
+
|
| 321 |
+
## Migration Order (after Palette)
|
| 322 |
+
|
| 323 |
+
1. `InputCtrlVtable` (1 slot) β trivial
|
| 324 |
+
2. `MapViewVtable` (2 slots) β trivial
|
| 325 |
+
3. `SoundEmitterVTable` (12 slots, 5 known)
|
| 326 |
+
4. `DisplayBaseVtable` (32 slots, all typed)
|
| 327 |
+
5. `DSSoundVtable` (24 slots)
|
| 328 |
+
6. `DDDisplayVtable` (38 slots)
|
| 329 |
+
7. `CTaskWormVTable` (20+ slots)
|
| 330 |
+
|
| 331 |
+
## Verification
|
| 332 |
+
|
| 333 |
+
- `cargo build -p openwa-core` β types compile
|
| 334 |
+
- `cargo test -p openwa-core` β existing tests pass
|
| 335 |
+
- `cargo build -p openwa-wormkit --release` β DLL builds
|
| 336 |
+
- `.\run-tests.ps1` β replay tests pass (no desync from layout changes)
|
| 337 |
+
- Validate introspection: `vtable_info_for("PaletteVtable")` returns correct slot metadata
|