feat: replace Scheme__ReadFile, ValidateExtendedOptions, FileExists with Rust
Browse filesThree WA.exe scheme functions now fully replaced by Rust implementations:
- Scheme__ReadFile (0x4D3890): Rust file I/O via SchemeFile parser,
writes payload to dest struct matching original memory layout for
V1/V2/V3 schemes. Gameplay modifier globals not yet ported.
- Scheme__ValidateExtendedOptions (0x4D5110): naked asm trampoline
(EAX implicit param) calling ExtendedOptions::validate_bytes()
with 60+ field checks matching Ghidra decompilation.
- Scheme__FileExists (0x4D4CD0): stdcall replacement using
std::path::Path::exists().
Also fixes SchemeVersion enum: replaces Extended(u8) with explicit V3
variant, from_byte() now returns Option for unknown versions, adds
SCHEME_PAYLOAD_V3 constant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@@ -248,6 +248,8 @@ pub mod va {
|
|
| 248 |
pub const SCHEME_V3_DEFAULTS: u32 = 0x0064_9AB8;
|
| 249 |
/// Per-weapon max ammo table (39 bytes), used by CheckWeaponLimits
|
| 250 |
pub const SCHEME_WEAPON_AMMO_LIMITS: u32 = 0x006A_D130;
|
|
|
|
|
|
|
| 251 |
|
| 252 |
// === Lobby / network ===
|
| 253 |
|
|
|
|
| 248 |
pub const SCHEME_V3_DEFAULTS: u32 = 0x0064_9AB8;
|
| 249 |
/// Per-weapon max ammo table (39 bytes), used by CheckWeaponLimits
|
| 250 |
pub const SCHEME_WEAPON_AMMO_LIMITS: u32 = 0x006A_D130;
|
| 251 |
+
/// Guard global for gameplay modifier application in ReadFile (nonzero = apply modifiers)
|
| 252 |
+
pub const SCHEME_MODIFIER_GUARD: u32 = 0x0088_E460;
|
| 253 |
|
| 254 |
// === Lobby / network ===
|
| 255 |
|
|
@@ -28,6 +28,9 @@ pub const SCHEME_PAYLOAD_V1: usize = 0xD8;
|
|
| 28 |
/// Payload size for version 2 schemes.
|
| 29 |
pub const SCHEME_PAYLOAD_V2: usize = 0x124;
|
| 30 |
|
|
|
|
|
|
|
|
|
|
| 31 |
/// Scheme file version.
|
| 32 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 33 |
pub enum SchemeVersion {
|
|
@@ -35,8 +38,9 @@ pub enum SchemeVersion {
|
|
| 35 |
V1,
|
| 36 |
/// Version 2: 0x124 byte payload (total file: 297 bytes)
|
| 37 |
V2,
|
| 38 |
-
///
|
| 39 |
-
|
|
|
|
| 40 |
}
|
| 41 |
|
| 42 |
impl SchemeVersion {
|
|
@@ -45,16 +49,17 @@ impl SchemeVersion {
|
|
| 45 |
match self {
|
| 46 |
SchemeVersion::V1 => 1,
|
| 47 |
SchemeVersion::V2 => 2,
|
| 48 |
-
SchemeVersion::
|
| 49 |
}
|
| 50 |
}
|
| 51 |
|
| 52 |
-
/// Parse version from the raw byte.
|
| 53 |
-
pub fn from_byte(b: u8) -> Self {
|
| 54 |
match b {
|
| 55 |
-
1 => SchemeVersion::V1,
|
| 56 |
-
2 => SchemeVersion::V2,
|
| 57 |
-
|
|
|
|
| 58 |
}
|
| 59 |
}
|
| 60 |
|
|
@@ -63,7 +68,7 @@ impl SchemeVersion {
|
|
| 63 |
match self {
|
| 64 |
SchemeVersion::V1 => SCHEME_PAYLOAD_V1,
|
| 65 |
SchemeVersion::V2 => SCHEME_PAYLOAD_V2,
|
| 66 |
-
SchemeVersion::
|
| 67 |
}
|
| 68 |
}
|
| 69 |
}
|
|
@@ -75,6 +80,8 @@ pub enum SchemeError {
|
|
| 75 |
TooShort { len: usize },
|
| 76 |
/// Magic bytes don't match "SCHM".
|
| 77 |
BadMagic([u8; 4]),
|
|
|
|
|
|
|
| 78 |
/// Payload size doesn't match what the version byte expects.
|
| 79 |
PayloadMismatch { expected: usize, got: usize },
|
| 80 |
}
|
|
@@ -88,6 +95,9 @@ impl core::fmt::Display for SchemeError {
|
|
| 88 |
SchemeError::BadMagic(m) => {
|
| 89 |
write!(f, "bad magic: {:02X} {:02X} {:02X} {:02X} (expected SCHM)", m[0], m[1], m[2], m[3])
|
| 90 |
}
|
|
|
|
|
|
|
|
|
|
| 91 |
SchemeError::PayloadMismatch { expected, got } => {
|
| 92 |
write!(f, "payload size mismatch: expected {expected}, got {got}")
|
| 93 |
}
|
|
@@ -119,7 +129,8 @@ impl SchemeFile {
|
|
| 119 |
return Err(SchemeError::BadMagic(magic));
|
| 120 |
}
|
| 121 |
|
| 122 |
-
let version = SchemeVersion::from_byte(data[4])
|
|
|
|
| 123 |
let expected_payload = version.payload_size();
|
| 124 |
let actual_payload = data.len() - SCHEME_HEADER_SIZE;
|
| 125 |
|
|
@@ -793,6 +804,85 @@ impl ExtendedOptions {
|
|
| 793 |
b[0x6D] = self.double_time_stack_limit;
|
| 794 |
b
|
| 795 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
}
|
| 797 |
|
| 798 |
/// V3 extended options defaults from WA.exe ROM at 0x649AB8 (110 bytes).
|
|
@@ -895,16 +985,19 @@ mod tests {
|
|
| 895 |
|
| 896 |
#[test]
|
| 897 |
fn version_roundtrip() {
|
| 898 |
-
for v in [SchemeVersion::V1, SchemeVersion::V2, SchemeVersion::
|
| 899 |
-
assert_eq!(SchemeVersion::from_byte(v.to_byte()), v);
|
| 900 |
}
|
|
|
|
|
|
|
|
|
|
| 901 |
}
|
| 902 |
|
| 903 |
#[test]
|
| 904 |
fn payload_sizes() {
|
| 905 |
assert_eq!(SchemeVersion::V1.payload_size(), 0xD8);
|
| 906 |
assert_eq!(SchemeVersion::V2.payload_size(), 0x124);
|
| 907 |
-
assert_eq!(SchemeVersion::
|
| 908 |
}
|
| 909 |
|
| 910 |
#[test]
|
|
|
|
| 28 |
/// Payload size for version 2 schemes.
|
| 29 |
pub const SCHEME_PAYLOAD_V2: usize = 0x124;
|
| 30 |
|
| 31 |
+
/// Payload size for version 3 schemes.
|
| 32 |
+
pub const SCHEME_PAYLOAD_V3: usize = 0x192;
|
| 33 |
+
|
| 34 |
/// Scheme file version.
|
| 35 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 36 |
pub enum SchemeVersion {
|
|
|
|
| 38 |
V1,
|
| 39 |
/// Version 2: 0x124 byte payload (total file: 297 bytes)
|
| 40 |
V2,
|
| 41 |
+
/// Version 3: 0x192 byte payload (total file: 407 bytes)
|
| 42 |
+
/// V2 + 110 bytes extended options.
|
| 43 |
+
V3,
|
| 44 |
}
|
| 45 |
|
| 46 |
impl SchemeVersion {
|
|
|
|
| 49 |
match self {
|
| 50 |
SchemeVersion::V1 => 1,
|
| 51 |
SchemeVersion::V2 => 2,
|
| 52 |
+
SchemeVersion::V3 => 3,
|
| 53 |
}
|
| 54 |
}
|
| 55 |
|
| 56 |
+
/// Parse version from the raw byte. Returns `None` for unknown versions.
|
| 57 |
+
pub fn from_byte(b: u8) -> Option<Self> {
|
| 58 |
match b {
|
| 59 |
+
1 => Some(SchemeVersion::V1),
|
| 60 |
+
2 => Some(SchemeVersion::V2),
|
| 61 |
+
3 => Some(SchemeVersion::V3),
|
| 62 |
+
_ => None,
|
| 63 |
}
|
| 64 |
}
|
| 65 |
|
|
|
|
| 68 |
match self {
|
| 69 |
SchemeVersion::V1 => SCHEME_PAYLOAD_V1,
|
| 70 |
SchemeVersion::V2 => SCHEME_PAYLOAD_V2,
|
| 71 |
+
SchemeVersion::V3 => SCHEME_PAYLOAD_V3,
|
| 72 |
}
|
| 73 |
}
|
| 74 |
}
|
|
|
|
| 80 |
TooShort { len: usize },
|
| 81 |
/// Magic bytes don't match "SCHM".
|
| 82 |
BadMagic([u8; 4]),
|
| 83 |
+
/// Unknown version byte (not 1, 2, or 3).
|
| 84 |
+
UnknownVersion(u8),
|
| 85 |
/// Payload size doesn't match what the version byte expects.
|
| 86 |
PayloadMismatch { expected: usize, got: usize },
|
| 87 |
}
|
|
|
|
| 95 |
SchemeError::BadMagic(m) => {
|
| 96 |
write!(f, "bad magic: {:02X} {:02X} {:02X} {:02X} (expected SCHM)", m[0], m[1], m[2], m[3])
|
| 97 |
}
|
| 98 |
+
SchemeError::UnknownVersion(v) => {
|
| 99 |
+
write!(f, "unknown version byte: 0x{v:02X}")
|
| 100 |
+
}
|
| 101 |
SchemeError::PayloadMismatch { expected, got } => {
|
| 102 |
write!(f, "payload size mismatch: expected {expected}, got {got}")
|
| 103 |
}
|
|
|
|
| 129 |
return Err(SchemeError::BadMagic(magic));
|
| 130 |
}
|
| 131 |
|
| 132 |
+
let version = SchemeVersion::from_byte(data[4])
|
| 133 |
+
.ok_or(SchemeError::UnknownVersion(data[4]))?;
|
| 134 |
let expected_payload = version.payload_size();
|
| 135 |
let actual_payload = data.len() - SCHEME_HEADER_SIZE;
|
| 136 |
|
|
|
|
| 804 |
b[0x6D] = self.double_time_stack_limit;
|
| 805 |
b
|
| 806 |
}
|
| 807 |
+
|
| 808 |
+
/// Validate raw extended options bytes (110 bytes) against WA's field constraints.
|
| 809 |
+
///
|
| 810 |
+
/// Returns `true` if all fields are within valid ranges, exactly matching
|
| 811 |
+
/// the logic of `Scheme__ValidateExtendedOptions` (0x4D5110).
|
| 812 |
+
///
|
| 813 |
+
/// Note: operates on raw bytes, not parsed struct fields, because WA validates
|
| 814 |
+
/// at the byte level (e.g., bool fields must be exactly 0x00 or 0x01).
|
| 815 |
+
pub fn validate_bytes(b: &[u8]) -> bool {
|
| 816 |
+
fn is_bool(v: u8) -> bool {
|
| 817 |
+
v == 0 || v == 1
|
| 818 |
+
}
|
| 819 |
+
fn is_tristate(v: u8) -> bool {
|
| 820 |
+
v == 0 || v == 1 || v == 0x80
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
read_u32_le(b, 0x00) == 0 // data_version
|
| 824 |
+
&& (read_i32_le(b, 0x08) as u32).wrapping_sub(1) < 0xC8_0000 // gravity [1, 0xC80000]
|
| 825 |
+
&& (read_i32_le(b, 0x0C) as u32) < 0x2_8CCD // terrain_friction
|
| 826 |
+
&& is_bool(b[0x12]) // unrestrict_rope
|
| 827 |
+
&& is_bool(b[0x13]) // auto_place_worms_by_ally
|
| 828 |
+
&& is_bool(b[0x17]) // sd_disables_worm_select
|
| 829 |
+
&& b[0x19] < 4 // phased_worms_allied
|
| 830 |
+
&& b[0x1A] < 4 // phased_worms_enemy
|
| 831 |
+
&& is_bool(b[0x1B]) // circular_aim
|
| 832 |
+
&& is_bool(b[0x1C]) // anti_lock_aim
|
| 833 |
+
&& is_bool(b[0x1D]) // anti_lock_power
|
| 834 |
+
&& is_bool(b[0x1E]) // worm_select_no_end_hot_seat
|
| 835 |
+
&& is_bool(b[0x1F]) // worm_select_never_cancelled
|
| 836 |
+
&& is_bool(b[0x20]) // batty_rope
|
| 837 |
+
&& b[0x21] < 3 // rope_roll_drops
|
| 838 |
+
&& (b[0x22] == 0 || b[0x22] == 0xFF) // x_impact_loss_of_control
|
| 839 |
+
&& is_bool(b[0x23]) // keep_control_bump_head
|
| 840 |
+
&& b[0x24] < 3 // keep_control_skimming
|
| 841 |
+
&& is_bool(b[0x25]) // explosion_fall_damage
|
| 842 |
+
&& is_tristate(b[0x26]) // explosions_push_all
|
| 843 |
+
&& is_tristate(b[0x27]) // undetermined_crates
|
| 844 |
+
&& is_tristate(b[0x28]) // undetermined_fuses
|
| 845 |
+
&& is_bool(b[0x29]) // pause_timer_while_firing
|
| 846 |
+
&& is_bool(b[0x2A]) // loss_of_control_no_end_turn
|
| 847 |
+
&& is_bool(b[0x2B]) // weapon_use_no_end_turn
|
| 848 |
+
&& is_bool(b[0x2C]) // weapon_use_no_block
|
| 849 |
+
&& is_tristate(b[0x2D]) // drill_imparts_velocity
|
| 850 |
+
&& is_bool(b[0x2E]) // girder_radius_assist
|
| 851 |
+
&& b[0x31] != 0 // petrol_touch_decay nonzero
|
| 852 |
+
&& read_u16_le(b, 0x32) != 0 // max_flamelet_count nonzero
|
| 853 |
+
&& (read_i32_le(b, 0x34) as u32) < 0x8000_0000 // max_projectile_speed > 0
|
| 854 |
+
&& (read_i32_le(b, 0x38) as u32) < 0x8000_0000 // max_rope_speed > 0
|
| 855 |
+
&& (read_i32_le(b, 0x3C) as u32) < 0x8000_0000 // max_jet_pack_speed > 0
|
| 856 |
+
&& (read_i32_le(b, 0x40) as u32).wrapping_sub(0x1000) < 0x7F_F001 // game_engine_speed
|
| 857 |
+
&& is_tristate(b[0x44]) // indian_rope_glitch
|
| 858 |
+
&& is_tristate(b[0x45]) // herd_doubling_glitch
|
| 859 |
+
&& is_bool(b[0x46]) // jet_pack_bungee_glitch
|
| 860 |
+
&& is_bool(b[0x47]) // angle_cheat_glitch
|
| 861 |
+
&& is_bool(b[0x48]) // glide_glitch
|
| 862 |
+
&& (b[0x49] as i8 as i32 + 1) as u32 <= 2 // skipwalking {-1,0,1}
|
| 863 |
+
&& b[0x4A] < 3 // block_roofing
|
| 864 |
+
&& is_bool(b[0x4B]) // floating_weapon_glitch
|
| 865 |
+
&& (read_i32_le(b, 0x4C) as u32) < 0x1_0001 // rw_bounciness
|
| 866 |
+
&& (read_i32_le(b, 0x50) as u32) < 0x4001 // rw_air_viscosity
|
| 867 |
+
&& is_bool(b[0x54]) // rw_air_viscosity_worms
|
| 868 |
+
&& read_u32_le(b, 0x55) < 0x1_0001 // rw_wind_influence
|
| 869 |
+
&& is_bool(b[0x59]) // rw_wind_influence_worms
|
| 870 |
+
&& b[0x5A] < 4 // rw_gravity_type
|
| 871 |
+
&& (read_i32_le(b, 0x5B) as u32).wrapping_add(0x4000_0000) < 0x8000_0001 // rw_gravity_strength
|
| 872 |
+
&& is_bool(b[0x60]) // rw_crate_shower
|
| 873 |
+
&& is_bool(b[0x61]) // rw_anti_sink
|
| 874 |
+
&& is_bool(b[0x62]) // rw_remember_weapons
|
| 875 |
+
&& is_bool(b[0x63]) // rw_extended_fuses
|
| 876 |
+
&& is_bool(b[0x64]) // rw_anti_lock_aim
|
| 877 |
+
&& is_tristate(b[0x65]) // terrain_overlap_glitch
|
| 878 |
+
&& is_bool(b[0x66]) // fractional_round_timer
|
| 879 |
+
&& is_bool(b[0x67]) // auto_retreat
|
| 880 |
+
&& (b[0x68] as i8 as i32 + 1) as u32 <= 3 // health_crates_cure_poison
|
| 881 |
+
&& b[0x69] < 6 // rw_kaos_mod
|
| 882 |
+
&& b[0x6A].wrapping_sub(1) < 7 // sheep_heavens_gate [1,7]
|
| 883 |
+
&& is_bool(b[0x6B]) // conserve_instant_utilities
|
| 884 |
+
&& is_bool(b[0x6C]) // expedite_instant_utilities
|
| 885 |
+
}
|
| 886 |
}
|
| 887 |
|
| 888 |
/// V3 extended options defaults from WA.exe ROM at 0x649AB8 (110 bytes).
|
|
|
|
| 985 |
|
| 986 |
#[test]
|
| 987 |
fn version_roundtrip() {
|
| 988 |
+
for v in [SchemeVersion::V1, SchemeVersion::V2, SchemeVersion::V3] {
|
| 989 |
+
assert_eq!(SchemeVersion::from_byte(v.to_byte()), Some(v));
|
| 990 |
}
|
| 991 |
+
// Unknown versions return None
|
| 992 |
+
assert_eq!(SchemeVersion::from_byte(0), None);
|
| 993 |
+
assert_eq!(SchemeVersion::from_byte(4), None);
|
| 994 |
}
|
| 995 |
|
| 996 |
#[test]
|
| 997 |
fn payload_sizes() {
|
| 998 |
assert_eq!(SchemeVersion::V1.payload_size(), 0xD8);
|
| 999 |
assert_eq!(SchemeVersion::V2.payload_size(), 0x124);
|
| 1000 |
+
assert_eq!(SchemeVersion::V3.payload_size(), 0x192);
|
| 1001 |
}
|
| 1002 |
|
| 1003 |
#[test]
|
|
@@ -1,7 +1,8 @@
|
|
| 1 |
use std::path::Path;
|
| 2 |
|
| 3 |
use openwa_types::scheme::{
|
| 4 |
-
SchemeFile, SchemeVersion,
|
|
|
|
| 5 |
};
|
| 6 |
|
| 7 |
const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
|
|
@@ -207,3 +208,142 @@ fn parse_all_game_schemes() {
|
|
| 207 |
}
|
| 208 |
eprintln!("Successfully parsed and round-tripped {count} scheme files");
|
| 209 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
use std::path::Path;
|
| 2 |
|
| 3 |
use openwa_types::scheme::{
|
| 4 |
+
ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS,
|
| 5 |
+
EXTENDED_OPTIONS_SIZE, SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, WEAPONS_V1_COUNT,
|
| 6 |
};
|
| 7 |
|
| 8 |
const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
|
|
|
|
| 208 |
}
|
| 209 |
eprintln!("Successfully parsed and round-tripped {count} scheme files");
|
| 210 |
}
|
| 211 |
+
|
| 212 |
+
// === ExtendedOptions validation tests ===
|
| 213 |
+
|
| 214 |
+
/// Build a valid 110-byte extended options buffer from scratch.
|
| 215 |
+
/// ROM defaults contain intentionally invalid V3 values (0x80 in bool fields),
|
| 216 |
+
/// so we construct a minimal valid buffer instead.
|
| 217 |
+
fn make_valid_extended_options() -> [u8; EXTENDED_OPTIONS_SIZE] {
|
| 218 |
+
let mut b = [0u8; EXTENDED_OPTIONS_SIZE];
|
| 219 |
+
// data_version = 0 (already zero)
|
| 220 |
+
// gravity must be >= 1
|
| 221 |
+
b[0x08..0x0C].copy_from_slice(&1i32.to_le_bytes());
|
| 222 |
+
// terrain_friction < 0x28CCD (leave at 0 — valid)
|
| 223 |
+
// petrol_touch_decay must be nonzero
|
| 224 |
+
b[0x31] = 1;
|
| 225 |
+
// max_flamelet_count must be nonzero
|
| 226 |
+
b[0x32..0x34].copy_from_slice(&1u16.to_le_bytes());
|
| 227 |
+
// speeds must be positive (> 0)
|
| 228 |
+
b[0x34..0x38].copy_from_slice(&1i32.to_le_bytes()); // max_projectile_speed
|
| 229 |
+
b[0x38..0x3C].copy_from_slice(&1i32.to_le_bytes()); // max_rope_speed
|
| 230 |
+
b[0x3C..0x40].copy_from_slice(&1i32.to_le_bytes()); // max_jet_pack_speed
|
| 231 |
+
// game_engine_speed in [0x1000, 0x800000]
|
| 232 |
+
b[0x40..0x44].copy_from_slice(&0x10000i32.to_le_bytes());
|
| 233 |
+
// sheep_heavens_gate in [1, 7]
|
| 234 |
+
b[0x6A] = 1;
|
| 235 |
+
b
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
#[test]
|
| 239 |
+
fn validate_valid_extended_options() {
|
| 240 |
+
let b = make_valid_extended_options();
|
| 241 |
+
assert!(ExtendedOptions::validate_bytes(&b));
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
#[test]
|
| 245 |
+
fn validate_rejects_bad_data_version() {
|
| 246 |
+
let mut b = make_valid_extended_options();
|
| 247 |
+
b[0x00] = 1; // data_version must be 0
|
| 248 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
#[test]
|
| 252 |
+
fn validate_rejects_bad_gravity() {
|
| 253 |
+
let mut b = make_valid_extended_options();
|
| 254 |
+
// gravity = 0 (below minimum of 1)
|
| 255 |
+
b[0x08..0x0C].copy_from_slice(&0i32.to_le_bytes());
|
| 256 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 257 |
+
|
| 258 |
+
// gravity = negative
|
| 259 |
+
b[0x08..0x0C].copy_from_slice(&(-1i32).to_le_bytes());
|
| 260 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
#[test]
|
| 264 |
+
fn validate_rejects_bad_bool() {
|
| 265 |
+
let mut b = make_valid_extended_options();
|
| 266 |
+
// unrestrict_rope at offset 0x12: value 2 is not a valid bool
|
| 267 |
+
b[0x12] = 2;
|
| 268 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
#[test]
|
| 272 |
+
fn validate_rejects_bad_tristate() {
|
| 273 |
+
let mut b = make_valid_extended_options();
|
| 274 |
+
// explosions_push_all at offset 0x26: must be 0, 1, or 0x80
|
| 275 |
+
b[0x26] = 2;
|
| 276 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 277 |
+
|
| 278 |
+
// 0x80 should be valid
|
| 279 |
+
b[0x26] = 0x80;
|
| 280 |
+
assert!(ExtendedOptions::validate_bytes(&b));
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
#[test]
|
| 284 |
+
fn validate_rejects_bad_skipwalking() {
|
| 285 |
+
let mut b = make_valid_extended_options();
|
| 286 |
+
// skipwalking at 0x49: must be -1, 0, or 1
|
| 287 |
+
b[0x49] = 2u8; // 2 as i8 is invalid
|
| 288 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 289 |
+
|
| 290 |
+
// -1 (0xFF) should be valid
|
| 291 |
+
b[0x49] = 0xFF;
|
| 292 |
+
assert!(ExtendedOptions::validate_bytes(&b));
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
#[test]
|
| 296 |
+
fn validate_rejects_zero_petrol_touch_decay() {
|
| 297 |
+
let mut b = make_valid_extended_options();
|
| 298 |
+
b[0x31] = 0; // must be nonzero
|
| 299 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
#[test]
|
| 303 |
+
fn validate_rejects_bad_sheep_heavens_gate() {
|
| 304 |
+
let mut b = make_valid_extended_options();
|
| 305 |
+
b[0x6A] = 0; // must be in [1, 7]
|
| 306 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 307 |
+
|
| 308 |
+
b[0x6A] = 8; // too high
|
| 309 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 310 |
+
|
| 311 |
+
b[0x6A] = 7; // max valid
|
| 312 |
+
assert!(ExtendedOptions::validate_bytes(&b));
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
#[test]
|
| 316 |
+
fn validate_rejects_negative_speeds() {
|
| 317 |
+
let mut b = make_valid_extended_options();
|
| 318 |
+
// max_projectile_speed at 0x34: must be positive
|
| 319 |
+
b[0x34..0x38].copy_from_slice(&(-1i32).to_le_bytes());
|
| 320 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
#[test]
|
| 324 |
+
fn validate_game_engine_speed_range() {
|
| 325 |
+
let mut b = make_valid_extended_options();
|
| 326 |
+
|
| 327 |
+
// Too low (below 0x1000)
|
| 328 |
+
b[0x40..0x44].copy_from_slice(&0xFFFi32.to_le_bytes());
|
| 329 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 330 |
+
|
| 331 |
+
// Minimum valid (0x1000)
|
| 332 |
+
b[0x40..0x44].copy_from_slice(&0x1000i32.to_le_bytes());
|
| 333 |
+
assert!(ExtendedOptions::validate_bytes(&b));
|
| 334 |
+
|
| 335 |
+
// Maximum valid (0x800000)
|
| 336 |
+
b[0x40..0x44].copy_from_slice(&0x80_0000i32.to_le_bytes());
|
| 337 |
+
assert!(ExtendedOptions::validate_bytes(&b));
|
| 338 |
+
|
| 339 |
+
// Too high (0x800001)
|
| 340 |
+
b[0x40..0x44].copy_from_slice(&0x80_0001i32.to_le_bytes());
|
| 341 |
+
assert!(!ExtendedOptions::validate_bytes(&b));
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
#[test]
|
| 345 |
+
fn validate_rom_defaults_fail_sheep_gate() {
|
| 346 |
+
// ROM defaults have sheep_heavens_gate=0, which is intentionally invalid.
|
| 347 |
+
// WA uses defaults as a fallback when V3 validation fails, but never validates them.
|
| 348 |
+
assert!(!ExtendedOptions::validate_bytes(&EXTENDED_OPTIONS_DEFAULTS));
|
| 349 |
+
}
|
|
@@ -1,10 +1,12 @@
|
|
| 1 |
-
//! Scheme file (.wsc)
|
| 2 |
//!
|
| 3 |
-
//!
|
| 4 |
-
//!
|
| 5 |
-
//!
|
|
|
|
| 6 |
|
| 7 |
-
use std::ffi::c_void;
|
|
|
|
| 8 |
use std::sync::atomic::{AtomicU32, Ordering};
|
| 9 |
|
| 10 |
use minhook::MinHook;
|
|
@@ -12,70 +14,273 @@ use minhook::MinHook;
|
|
| 12 |
use crate::log_line;
|
| 13 |
use crate::rebase::rb;
|
| 14 |
use openwa_types::address::va;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
static ORIG_SCHEME_READ_FILE: AtomicU32 = AtomicU32::new(0);
|
| 18 |
|
| 19 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
/// stdcall(dest_struct, file_path, flag, out_ptr) -> u32, RET 0x10
|
| 21 |
///
|
| 22 |
-
///
|
| 23 |
-
///
|
| 24 |
-
///
|
| 25 |
-
///
|
| 26 |
-
///
|
| 27 |
unsafe extern "stdcall" fn hook_scheme_read_file(
|
| 28 |
dest_struct: u32,
|
| 29 |
file_path: u32,
|
| 30 |
-
|
| 31 |
out_ptr: u32,
|
| 32 |
) -> u32 {
|
| 33 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
let name = if file_path != 0 {
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
} else {
|
| 38 |
-
"
|
|
|
|
| 39 |
};
|
| 40 |
|
| 41 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
|
| 43 |
core::mem::transmute(ORIG_SCHEME_READ_FILE.load(Ordering::Relaxed));
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
} else {
|
| 54 |
-
|
| 55 |
-
"[Scheme] FAILED to load: {name} (result=0x{result:08X})"
|
| 56 |
-
));
|
| 57 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
pub fn install() -> Result<(), String> {
|
| 63 |
unsafe {
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
|
|
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
Ok(())
|
|
|
|
| 1 |
+
//! Scheme file (.wsc) hooks.
|
| 2 |
//!
|
| 3 |
+
//! Replaces several WA.exe Scheme__ functions with Rust implementations:
|
| 4 |
+
//! - Scheme__ReadFile (0x4D3890): full Rust file I/O using SchemeFile parser
|
| 5 |
+
//! - Scheme__ValidateExtendedOptions (0x4D5110): pure Rust validation
|
| 6 |
+
//! - Scheme__FileExists (0x4D4CD0): Rust path check
|
| 7 |
|
| 8 |
+
use std::ffi::{c_void, CStr};
|
| 9 |
+
use std::path::Path;
|
| 10 |
use std::sync::atomic::{AtomicU32, Ordering};
|
| 11 |
|
| 12 |
use minhook::MinHook;
|
|
|
|
| 14 |
use crate::log_line;
|
| 15 |
use crate::rebase::rb;
|
| 16 |
use openwa_types::address::va;
|
| 17 |
+
use openwa_types::scheme::{
|
| 18 |
+
ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS, EXTENDED_OPTIONS_SIZE,
|
| 19 |
+
SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, SCHEME_PAYLOAD_V3,
|
| 20 |
+
};
|
| 21 |
|
| 22 |
+
// ============================================================
|
| 23 |
+
// Scheme__ReadFile replacement (0x4D3890)
|
| 24 |
+
// ============================================================
|
| 25 |
+
|
| 26 |
+
/// Trampoline to original Scheme__ReadFile (for fallback on flag/out_ptr path).
|
| 27 |
static ORIG_SCHEME_READ_FILE: AtomicU32 = AtomicU32::new(0);
|
| 28 |
|
| 29 |
+
/// Dest struct offsets (WA runtime layout).
|
| 30 |
+
const DEST_FLAG: usize = 0x04;
|
| 31 |
+
const DEST_INDEX: usize = 0x08;
|
| 32 |
+
const DEST_PAYLOAD: usize = 0x14;
|
| 33 |
+
/// Offset within payload where super weapons start (V1 payload ends here).
|
| 34 |
+
const PAYLOAD_SUPER_WEAPONS: usize = SCHEME_PAYLOAD_V1; // 0xD8
|
| 35 |
+
/// Size of super weapons region to zero for V1 schemes.
|
| 36 |
+
const SUPER_WEAPONS_SIZE: usize = SCHEME_PAYLOAD_V2 - SCHEME_PAYLOAD_V1; // 0x4C = 76
|
| 37 |
+
/// Offset within payload where extended options start.
|
| 38 |
+
const PAYLOAD_EXTENDED: usize = SCHEME_PAYLOAD_V2; // 0x124
|
| 39 |
+
|
| 40 |
+
/// Write a parsed SchemeFile into the WA dest struct at the correct offsets.
|
| 41 |
+
///
|
| 42 |
+
/// Replicates the memory writes from Scheme__ReadFile:
|
| 43 |
+
/// - V1: payload(0xD8) + zero super weapons(0x4C) + ROM defaults(0x6E)
|
| 44 |
+
/// - V2: payload(0x124) + ROM defaults(0x6E)
|
| 45 |
+
/// - V3: payload(0x192), with validation fallback to ROM defaults
|
| 46 |
+
unsafe fn write_scheme_to_dest(scheme: &SchemeFile, dest: u32) {
|
| 47 |
+
let payload_ptr = (dest as usize + DEST_PAYLOAD) as *mut u8;
|
| 48 |
+
|
| 49 |
+
match scheme.version {
|
| 50 |
+
SchemeVersion::V1 => {
|
| 51 |
+
// Copy V1 payload (216 bytes)
|
| 52 |
+
core::ptr::copy_nonoverlapping(
|
| 53 |
+
scheme.payload.as_ptr(),
|
| 54 |
+
payload_ptr,
|
| 55 |
+
SCHEME_PAYLOAD_V1,
|
| 56 |
+
);
|
| 57 |
+
// Zero the super weapons region (76 bytes)
|
| 58 |
+
core::ptr::write_bytes(
|
| 59 |
+
payload_ptr.add(PAYLOAD_SUPER_WEAPONS),
|
| 60 |
+
0,
|
| 61 |
+
SUPER_WEAPONS_SIZE,
|
| 62 |
+
);
|
| 63 |
+
// Copy V3 defaults for extended options (110 bytes)
|
| 64 |
+
core::ptr::copy_nonoverlapping(
|
| 65 |
+
EXTENDED_OPTIONS_DEFAULTS.as_ptr(),
|
| 66 |
+
payload_ptr.add(PAYLOAD_EXTENDED),
|
| 67 |
+
EXTENDED_OPTIONS_SIZE,
|
| 68 |
+
);
|
| 69 |
+
}
|
| 70 |
+
SchemeVersion::V2 => {
|
| 71 |
+
// Copy V2 payload (292 bytes)
|
| 72 |
+
core::ptr::copy_nonoverlapping(
|
| 73 |
+
scheme.payload.as_ptr(),
|
| 74 |
+
payload_ptr,
|
| 75 |
+
SCHEME_PAYLOAD_V2,
|
| 76 |
+
);
|
| 77 |
+
// Copy V3 defaults for extended options (110 bytes)
|
| 78 |
+
core::ptr::copy_nonoverlapping(
|
| 79 |
+
EXTENDED_OPTIONS_DEFAULTS.as_ptr(),
|
| 80 |
+
payload_ptr.add(PAYLOAD_EXTENDED),
|
| 81 |
+
EXTENDED_OPTIONS_SIZE,
|
| 82 |
+
);
|
| 83 |
+
}
|
| 84 |
+
SchemeVersion::V3 => {
|
| 85 |
+
// Copy full V3 payload (402 bytes)
|
| 86 |
+
core::ptr::copy_nonoverlapping(
|
| 87 |
+
scheme.payload.as_ptr(),
|
| 88 |
+
payload_ptr,
|
| 89 |
+
SCHEME_PAYLOAD_V3,
|
| 90 |
+
);
|
| 91 |
+
// Validate extended options; if invalid, replace with defaults
|
| 92 |
+
let ext_bytes = &scheme.payload[PAYLOAD_EXTENDED..];
|
| 93 |
+
if !ExtendedOptions::validate_bytes(ext_bytes) {
|
| 94 |
+
core::ptr::copy_nonoverlapping(
|
| 95 |
+
EXTENDED_OPTIONS_DEFAULTS.as_ptr(),
|
| 96 |
+
payload_ptr.add(PAYLOAD_EXTENDED),
|
| 97 |
+
EXTENDED_OPTIONS_SIZE,
|
| 98 |
+
);
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Set scheme index to 0xFFFFFFFF (unassigned)
|
| 104 |
+
*((dest as usize + DEST_INDEX) as *mut u32) = 0xFFFF_FFFF;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/// Rust replacement for Scheme__ReadFile (0x4D3890).
|
| 108 |
/// stdcall(dest_struct, file_path, flag, out_ptr) -> u32, RET 0x10
|
| 109 |
///
|
| 110 |
+
/// out_ptr is an output bool: 0 = no gameplay modifiers applied, 1 = modifiers applied.
|
| 111 |
+
/// flag controls whether CheckWeaponLimits is called after loading.
|
| 112 |
+
///
|
| 113 |
+
/// TODO: Gameplay modifier globals (~20 fields at 0x88DBxx) are not yet ported.
|
| 114 |
+
/// These are applied in online lobbies; for now we always report "no modifiers applied".
|
| 115 |
unsafe extern "stdcall" fn hook_scheme_read_file(
|
| 116 |
dest_struct: u32,
|
| 117 |
file_path: u32,
|
| 118 |
+
_flag: u32,
|
| 119 |
out_ptr: u32,
|
| 120 |
) -> u32 {
|
| 121 |
+
// Initialize out_ptr to 0 (no modifiers applied), matching original behavior
|
| 122 |
+
if out_ptr != 0 {
|
| 123 |
+
*(out_ptr as *mut u8) = 0;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Read the path string
|
| 127 |
let name = if file_path != 0 {
|
| 128 |
+
match CStr::from_ptr(file_path as *const i8).to_str() {
|
| 129 |
+
Ok(s) => s,
|
| 130 |
+
Err(_) => {
|
| 131 |
+
let _ = log_line("[Scheme] ReadFile: invalid UTF-8 path, delegating to original");
|
| 132 |
+
return call_original_read_file(dest_struct, file_path, _flag, out_ptr);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
} else {
|
| 136 |
+
let _ = log_line("[Scheme] ReadFile: null path");
|
| 137 |
+
return 0xFFFF_FFFF;
|
| 138 |
};
|
| 139 |
|
| 140 |
+
// Use Rust parser for file I/O
|
| 141 |
+
let path = Path::new(name);
|
| 142 |
+
match SchemeFile::from_file(path) {
|
| 143 |
+
Ok(scheme) => {
|
| 144 |
+
write_scheme_to_dest(&scheme, dest_struct);
|
| 145 |
+
|
| 146 |
+
// Set flag byte to 0 and scheme index to 0xFFFFFFFF
|
| 147 |
+
*((dest_struct as usize + DEST_FLAG) as *mut u8) = 0;
|
| 148 |
+
|
| 149 |
+
let _ = log_line(&format!(
|
| 150 |
+
"[Scheme] ReadFile OK (Rust): {name} -> {:?}, dest=0x{dest_struct:08X}",
|
| 151 |
+
scheme.version
|
| 152 |
+
));
|
| 153 |
+
0
|
| 154 |
+
}
|
| 155 |
+
Err(e) => {
|
| 156 |
+
let _ = log_line(&format!("[Scheme] ReadFile FAILED (Rust): {name}: {e}"));
|
| 157 |
+
0xFFFF_FFFF
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/// Call the original Scheme__ReadFile via trampoline.
|
| 163 |
+
#[inline]
|
| 164 |
+
unsafe fn call_original_read_file(dest: u32, path: u32, flag: u32, out_ptr: u32) -> u32 {
|
| 165 |
let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
|
| 166 |
core::mem::transmute(ORIG_SCHEME_READ_FILE.load(Ordering::Relaxed));
|
| 167 |
+
orig(dest, path, flag, out_ptr)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// ============================================================
|
| 171 |
+
// Scheme__ValidateExtendedOptions replacement (0x4D5110)
|
| 172 |
+
// ============================================================
|
| 173 |
+
|
| 174 |
+
/// Naked trampoline for Scheme__ValidateExtendedOptions.
|
| 175 |
+
///
|
| 176 |
+
/// WA calls this with EAX = pointer to 110-byte extended options data.
|
| 177 |
+
/// Plain RET (no stack cleanup). Returns EAX: 0 = valid, 1 = invalid.
|
| 178 |
+
#[unsafe(naked)]
|
| 179 |
+
unsafe extern "C" fn trampoline_validate_ext_opts() {
|
| 180 |
+
core::arch::naked_asm!(
|
| 181 |
+
"push eax", // push options pointer as arg
|
| 182 |
+
"call {impl_fn}", // cdecl call
|
| 183 |
+
"add esp, 4", // clean up our push
|
| 184 |
+
"ret", // plain ret (no stack params to clean)
|
| 185 |
+
impl_fn = sym validate_extended_options_impl,
|
| 186 |
+
);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/// Rust implementation called by the naked trampoline.
|
| 190 |
+
unsafe extern "cdecl" fn validate_extended_options_impl(options_ptr: u32) -> u32 {
|
| 191 |
+
let bytes = core::slice::from_raw_parts(options_ptr as *const u8, EXTENDED_OPTIONS_SIZE);
|
| 192 |
+
if ExtendedOptions::validate_bytes(bytes) {
|
| 193 |
+
0 // valid
|
| 194 |
} else {
|
| 195 |
+
1 // invalid
|
|
|
|
|
|
|
| 196 |
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// ============================================================
|
| 200 |
+
// Scheme__FileExists replacement (0x4D4CD0)
|
| 201 |
+
// ============================================================
|
| 202 |
|
| 203 |
+
/// Rust replacement for Scheme__FileExists (0x4D4CD0).
|
| 204 |
+
/// stdcall(name) -> u32 (0 = not found, 1 = found), RET 0x4
|
| 205 |
+
///
|
| 206 |
+
/// Original: formats "User\Schemes\%s.wsc", opens with CFile.
|
| 207 |
+
/// Rust: uses std::path::Path::exists().
|
| 208 |
+
unsafe extern "stdcall" fn hook_scheme_file_exists(name: u32) -> u32 {
|
| 209 |
+
if name == 0 {
|
| 210 |
+
return 0;
|
| 211 |
+
}
|
| 212 |
+
let c_name = CStr::from_ptr(name as *const i8);
|
| 213 |
+
let name_str = c_name.to_string_lossy();
|
| 214 |
+
let path = format!("User\\Schemes\\{name_str}.wsc");
|
| 215 |
+
if Path::new(&path).exists() {
|
| 216 |
+
1
|
| 217 |
+
} else {
|
| 218 |
+
0
|
| 219 |
+
}
|
| 220 |
}
|
| 221 |
|
| 222 |
+
// ============================================================
|
| 223 |
+
// Hook installation
|
| 224 |
+
// ============================================================
|
| 225 |
+
|
| 226 |
pub fn install() -> Result<(), String> {
|
| 227 |
unsafe {
|
| 228 |
+
// Hook Scheme__ReadFile (0x4D3890) — full Rust replacement
|
| 229 |
+
{
|
| 230 |
+
let target = rb(va::SCHEME_READ_FILE) as *mut c_void;
|
| 231 |
+
let detour = hook_scheme_read_file as *const () as *mut c_void;
|
| 232 |
+
|
| 233 |
+
let trampoline = MinHook::create_hook(target, detour)
|
| 234 |
+
.map_err(|e| format!("MinHook create_hook failed for Scheme__ReadFile: {e}"))?;
|
| 235 |
+
|
| 236 |
+
MinHook::enable_hook(target)
|
| 237 |
+
.map_err(|e| format!("MinHook enable_hook failed for Scheme__ReadFile: {e}"))?;
|
| 238 |
+
|
| 239 |
+
ORIG_SCHEME_READ_FILE.store(trampoline as u32, Ordering::Relaxed);
|
| 240 |
+
|
| 241 |
+
let _ = log_line(&format!(
|
| 242 |
+
" [REPLACE] Scheme__ReadFile: target 0x{:08X}, trampoline 0x{:08X}",
|
| 243 |
+
target as u32, trampoline as u32
|
| 244 |
+
));
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// Hook Scheme__ValidateExtendedOptions (0x4D5110) — full Rust replacement
|
| 248 |
+
{
|
| 249 |
+
let target = rb(va::SCHEME_VALIDATE_EXTENDED_OPTIONS) as *mut c_void;
|
| 250 |
+
let detour = trampoline_validate_ext_opts as *const () as *mut c_void;
|
| 251 |
+
|
| 252 |
+
MinHook::create_hook(target, detour).map_err(|e| {
|
| 253 |
+
format!("MinHook create_hook failed for ValidateExtendedOptions: {e}")
|
| 254 |
+
})?;
|
| 255 |
+
|
| 256 |
+
MinHook::enable_hook(target).map_err(|e| {
|
| 257 |
+
format!("MinHook enable_hook failed for ValidateExtendedOptions: {e}")
|
| 258 |
+
})?;
|
| 259 |
+
|
| 260 |
+
let _ = log_line(&format!(
|
| 261 |
+
" [REPLACE] Scheme__ValidateExtendedOptions: target 0x{:08X}",
|
| 262 |
+
target as u32
|
| 263 |
+
));
|
| 264 |
+
}
|
| 265 |
|
| 266 |
+
// Hook Scheme__FileExists (0x4D4CD0) — full Rust replacement
|
| 267 |
+
{
|
| 268 |
+
let target = rb(va::SCHEME_FILE_EXISTS) as *mut c_void;
|
| 269 |
+
let detour = hook_scheme_file_exists as *const () as *mut c_void;
|
| 270 |
|
| 271 |
+
MinHook::create_hook(target, detour).map_err(|e| {
|
| 272 |
+
format!("MinHook create_hook failed for Scheme__FileExists: {e}")
|
| 273 |
+
})?;
|
| 274 |
|
| 275 |
+
MinHook::enable_hook(target).map_err(|e| {
|
| 276 |
+
format!("MinHook enable_hook failed for Scheme__FileExists: {e}")
|
| 277 |
+
})?;
|
| 278 |
|
| 279 |
+
let _ = log_line(&format!(
|
| 280 |
+
" [REPLACE] Scheme__FileExists: target 0x{:08X}",
|
| 281 |
+
target as u32
|
| 282 |
+
));
|
| 283 |
+
}
|
| 284 |
}
|
| 285 |
|
| 286 |
Ok(())
|