paavohtl Claude Opus 4.6 commited on
Commit
b10ab27
·
1 Parent(s): 8f33a03

feat: add .wsc scheme file parser + Scheme__ReadFile hook

Browse files

Add SchemeFile type to openwa-types with from_bytes/to_bytes for the
SCHM container format (v1: 0xD8 payload, v2: 0x124 payload). Tested
against all 18 bundled scheme files with round-trip verification.

RE discoveries: Scheme__Load (0x4D4CD0) is actually just a file-exists
check, Scheme__ParseFile (0x4D44F0) is a save function. The real reader
is Scheme__ReadFile (0x4D3890), stdcall with 4 params. Hook confirms it
fires when selecting schemes in the multiplayer setup dialog.

8 new Ghidra labels: scheme functions + CFile Read/Write/Open/Close.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

crates/openwa-types/src/address.rs CHANGED
@@ -218,6 +218,22 @@ pub mod va {
218
  pub const FRONTEND_LOBBY_HOST_CTOR: u32 = 0x004B_0160;
219
  pub const FRONTEND_LOBBY_GAME_START_CTOR: u32 = 0x004B_DBE0;
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  // === Lobby / network ===
222
 
223
  pub const LOBBY_HOST_COMMANDS: u32 = 0x004B_9B00;
 
218
  pub const FRONTEND_LOBBY_HOST_CTOR: u32 = 0x004B_0160;
219
  pub const FRONTEND_LOBBY_GAME_START_CTOR: u32 = 0x004B_DBE0;
220
 
221
+ // === Scheme file operations ===
222
+
223
+ /// Reads .wsc file into scheme struct: stdcall(dest, path, flag, out_ptr), RET 0x10
224
+ /// Opens modeRead|typeBinary, reads header + version-dependent payload to dest+0x14
225
+ pub const SCHEME_READ_FILE: u32 = 0x004D_3890;
226
+ /// Checks if scheme file exists: stdcall(name) → 0=not found, 1=found, RET 0x4
227
+ pub const SCHEME_FILE_EXISTS: u32 = 0x004D_4CD0;
228
+ /// Saves scheme struct to .wsc file: thiscall(this, name, flag), RET 0x8
229
+ pub const SCHEME_SAVE_FILE: u32 = 0x004D_44F0;
230
+ /// Variant file-exists check for numbered schemes ({%02d} Name.wsc)
231
+ pub const SCHEME_FILE_EXISTS_NUMBERED: u32 = 0x004D_4E00;
232
+ /// Version detection: compares patterns to determine v1/v2/v3
233
+ pub const SCHEME_DETECT_VERSION: u32 = 0x004D_4480;
234
+ /// Extracts built-in schemes from PE resources to User\Schemes\ directory
235
+ pub const SCHEME_EXTRACT_BUILTINS: u32 = 0x004D_5720;
236
+
237
  // === Lobby / network ===
238
 
239
  pub const LOBBY_HOST_COMMANDS: u32 = 0x004B_9B00;
crates/openwa-types/src/lib.rs CHANGED
@@ -11,4 +11,5 @@ pub mod landscape;
11
  pub mod dssound;
12
  pub mod opengl;
13
  pub mod frontend;
 
14
  pub mod address;
 
11
  pub mod dssound;
12
  pub mod opengl;
13
  pub mod frontend;
14
+ pub mod scheme;
15
  pub mod address;
crates/openwa-types/src/scheme.rs ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// Scheme file (.wsc) parser for Worms Armageddon.
2
+ ///
3
+ /// .wsc files store game settings (turn time, wind, per-weapon ammo/delay, etc.).
4
+ /// Located in `User\Schemes\*.wsc`.
5
+ ///
6
+ /// Binary format:
7
+ /// Bytes 0-3: Magic "SCHM" (0x5343484D)
8
+ /// Byte 4: Version byte
9
+ /// Bytes 5+: Payload (size depends on version)
10
+ ///
11
+ /// Version → payload size:
12
+ /// 0x01 → 0xD8 bytes (216)
13
+ /// 0x02 → 0x124 bytes (292)
14
+ /// other → version_byte as usize + 0x124 bytes
15
+ ///
16
+ /// Source: Ghidra decompilation of Scheme__Load (0x4D4CD0),
17
+ /// Scheme__ParseFile (0x4D44F0), hex analysis of bundled .wsc files.
18
+
19
+ /// Magic header bytes for .wsc files.
20
+ pub const SCHEME_MAGIC: [u8; 4] = *b"SCHM";
21
+
22
+ /// Size of the file header (magic + version byte).
23
+ pub const SCHEME_HEADER_SIZE: usize = 5;
24
+
25
+ /// Payload size for version 1 schemes.
26
+ pub const SCHEME_PAYLOAD_V1: usize = 0xD8;
27
+
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 {
34
+ /// Version 1: 0xD8 byte payload (total file: 221 bytes)
35
+ V1,
36
+ /// Version 2: 0x124 byte payload (total file: 297 bytes)
37
+ V2,
38
+ /// Extended: payload = version_byte + 0x124 bytes
39
+ Extended(u8),
40
+ }
41
+
42
+ impl SchemeVersion {
43
+ /// Raw version byte as stored in the file.
44
+ pub fn to_byte(self) -> u8 {
45
+ match self {
46
+ SchemeVersion::V1 => 1,
47
+ SchemeVersion::V2 => 2,
48
+ SchemeVersion::Extended(v) => v,
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
+ v => SchemeVersion::Extended(v),
58
+ }
59
+ }
60
+
61
+ /// Expected payload size for this version.
62
+ pub fn payload_size(self) -> usize {
63
+ match self {
64
+ SchemeVersion::V1 => SCHEME_PAYLOAD_V1,
65
+ SchemeVersion::V2 => SCHEME_PAYLOAD_V2,
66
+ SchemeVersion::Extended(v) => v as usize + SCHEME_PAYLOAD_V2,
67
+ }
68
+ }
69
+ }
70
+
71
+ /// Errors from parsing a .wsc scheme file.
72
+ #[derive(Debug, Clone, PartialEq, Eq)]
73
+ pub enum SchemeError {
74
+ /// File is too short to contain the header.
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
+ }
81
+
82
+ impl core::fmt::Display for SchemeError {
83
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84
+ match self {
85
+ SchemeError::TooShort { len } => {
86
+ write!(f, "file too short ({len} bytes, need at least {SCHEME_HEADER_SIZE})")
87
+ }
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
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ /// A parsed .wsc scheme file.
99
+ ///
100
+ /// The payload bytes are initially opaque. Typed field accessors will be
101
+ /// added incrementally as we map individual byte offsets through RE.
102
+ #[derive(Debug, Clone, PartialEq, Eq)]
103
+ pub struct SchemeFile {
104
+ pub version: SchemeVersion,
105
+ /// Raw payload bytes (game options + per-weapon settings).
106
+ /// Length matches `version.payload_size()`.
107
+ pub payload: Vec<u8>,
108
+ }
109
+
110
+ impl SchemeFile {
111
+ /// Parse a scheme file from raw bytes (entire file contents).
112
+ pub fn from_bytes(data: &[u8]) -> Result<Self, SchemeError> {
113
+ if data.len() < SCHEME_HEADER_SIZE {
114
+ return Err(SchemeError::TooShort { len: data.len() });
115
+ }
116
+
117
+ let magic: [u8; 4] = data[0..4].try_into().unwrap();
118
+ if magic != SCHEME_MAGIC {
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
+
126
+ if actual_payload != expected_payload {
127
+ return Err(SchemeError::PayloadMismatch {
128
+ expected: expected_payload,
129
+ got: actual_payload,
130
+ });
131
+ }
132
+
133
+ Ok(SchemeFile {
134
+ version,
135
+ payload: data[SCHEME_HEADER_SIZE..].to_vec(),
136
+ })
137
+ }
138
+
139
+ /// Serialize back to .wsc format.
140
+ pub fn to_bytes(&self) -> Vec<u8> {
141
+ let mut buf = Vec::with_capacity(SCHEME_HEADER_SIZE + self.payload.len());
142
+ buf.extend_from_slice(&SCHEME_MAGIC);
143
+ buf.push(self.version.to_byte());
144
+ buf.extend_from_slice(&self.payload);
145
+ buf
146
+ }
147
+
148
+ /// Total file size (header + payload).
149
+ pub fn file_size(&self) -> usize {
150
+ SCHEME_HEADER_SIZE + self.payload.len()
151
+ }
152
+ }
153
+
154
+ impl SchemeFile {
155
+ /// Load a scheme file from disk.
156
+ pub fn from_file(path: &std::path::Path) -> Result<Self, SchemeFileError> {
157
+ let data = std::fs::read(path).map_err(SchemeFileError::Io)?;
158
+ Self::from_bytes(&data).map_err(SchemeFileError::Parse)
159
+ }
160
+
161
+ /// Save a scheme file to disk.
162
+ pub fn to_file(&self, path: &std::path::Path) -> Result<(), std::io::Error> {
163
+ std::fs::write(path, self.to_bytes())
164
+ }
165
+ }
166
+
167
+ /// Error type for file-based scheme operations.
168
+ #[derive(Debug)]
169
+ pub enum SchemeFileError {
170
+ Io(std::io::Error),
171
+ Parse(SchemeError),
172
+ }
173
+
174
+ impl core::fmt::Display for SchemeFileError {
175
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
176
+ match self {
177
+ SchemeFileError::Io(e) => write!(f, "I/O error: {e}"),
178
+ SchemeFileError::Parse(e) => write!(f, "parse error: {e}"),
179
+ }
180
+ }
181
+ }
182
+
183
+ #[cfg(test)]
184
+ mod tests {
185
+ use super::*;
186
+
187
+ #[test]
188
+ fn version_roundtrip() {
189
+ for v in [SchemeVersion::V1, SchemeVersion::V2, SchemeVersion::Extended(5)] {
190
+ assert_eq!(SchemeVersion::from_byte(v.to_byte()), v);
191
+ }
192
+ }
193
+
194
+ #[test]
195
+ fn payload_sizes() {
196
+ assert_eq!(SchemeVersion::V1.payload_size(), 0xD8);
197
+ assert_eq!(SchemeVersion::V2.payload_size(), 0x124);
198
+ assert_eq!(SchemeVersion::Extended(3).payload_size(), 3 + 0x124);
199
+ }
200
+
201
+ #[test]
202
+ fn parse_v1_synthetic() {
203
+ let mut data = Vec::new();
204
+ data.extend_from_slice(b"SCHM");
205
+ data.push(0x01);
206
+ data.extend_from_slice(&[0xAA; SCHEME_PAYLOAD_V1]);
207
+ assert_eq!(data.len(), 221);
208
+
209
+ let scheme = SchemeFile::from_bytes(&data).unwrap();
210
+ assert_eq!(scheme.version, SchemeVersion::V1);
211
+ assert_eq!(scheme.payload.len(), SCHEME_PAYLOAD_V1);
212
+ assert!(scheme.payload.iter().all(|&b| b == 0xAA));
213
+ }
214
+
215
+ #[test]
216
+ fn parse_v2_synthetic() {
217
+ let mut data = Vec::new();
218
+ data.extend_from_slice(b"SCHM");
219
+ data.push(0x02);
220
+ data.extend_from_slice(&[0xBB; SCHEME_PAYLOAD_V2]);
221
+ assert_eq!(data.len(), 297);
222
+
223
+ let scheme = SchemeFile::from_bytes(&data).unwrap();
224
+ assert_eq!(scheme.version, SchemeVersion::V2);
225
+ assert_eq!(scheme.payload.len(), SCHEME_PAYLOAD_V2);
226
+ }
227
+
228
+ #[test]
229
+ fn roundtrip_synthetic() {
230
+ let mut data = Vec::new();
231
+ data.extend_from_slice(b"SCHM");
232
+ data.push(0x01);
233
+ data.extend_from_slice(&[0x42; SCHEME_PAYLOAD_V1]);
234
+
235
+ let scheme = SchemeFile::from_bytes(&data).unwrap();
236
+ assert_eq!(scheme.to_bytes(), data);
237
+ }
238
+
239
+ #[test]
240
+ fn error_too_short() {
241
+ assert_eq!(
242
+ SchemeFile::from_bytes(b"SCH"),
243
+ Err(SchemeError::TooShort { len: 3 })
244
+ );
245
+ }
246
+
247
+ #[test]
248
+ fn error_bad_magic() {
249
+ let mut data = vec![b'N', b'O', b'P', b'E', 0x01];
250
+ data.extend_from_slice(&[0; SCHEME_PAYLOAD_V1]);
251
+ assert!(matches!(
252
+ SchemeFile::from_bytes(&data),
253
+ Err(SchemeError::BadMagic([b'N', b'O', b'P', b'E']))
254
+ ));
255
+ }
256
+
257
+ #[test]
258
+ fn error_payload_mismatch() {
259
+ // v1 header but only 10 bytes of payload
260
+ let mut data = Vec::new();
261
+ data.extend_from_slice(b"SCHM");
262
+ data.push(0x01);
263
+ data.extend_from_slice(&[0; 10]);
264
+
265
+ assert!(matches!(
266
+ SchemeFile::from_bytes(&data),
267
+ Err(SchemeError::PayloadMismatch { expected: 0xD8, got: 10 })
268
+ ));
269
+ }
270
+ }
crates/openwa-types/tests/fixtures/beginner.wsc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:632057e77927f0a3fd506963015795de622244aabea78d70bd3e28bab888a10f
3
+ size 297
crates/openwa-types/tests/fixtures/classic.wsc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:12980f196d85c7acc0284d34935c998a01296ca4b28439de83d5cf2d5820f6cb
3
+ size 221
crates/openwa-types/tests/fixtures/shopping.wsc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea83352929ad768d506238f32ea58da4ab6ac210a751db99b404002fb3617fd0
3
+ size 221
crates/openwa-types/tests/scheme_parse.rs ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::path::Path;
2
+
3
+ use openwa_types::scheme::{SchemeFile, SchemeVersion, SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2};
4
+
5
+ const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
6
+
7
+ #[test]
8
+ fn parse_beginner_v2() {
9
+ let path = Path::new(FIXTURES).join("beginner.wsc");
10
+ let scheme = SchemeFile::from_file(&path).expect("failed to parse beginner.wsc");
11
+ assert_eq!(scheme.version, SchemeVersion::V2);
12
+ assert_eq!(scheme.payload.len(), SCHEME_PAYLOAD_V2);
13
+ assert_eq!(scheme.file_size(), 297);
14
+ }
15
+
16
+ #[test]
17
+ fn parse_classic_v1() {
18
+ let path = Path::new(FIXTURES).join("classic.wsc");
19
+ let scheme = SchemeFile::from_file(&path).expect("failed to parse classic.wsc");
20
+ assert_eq!(scheme.version, SchemeVersion::V1);
21
+ assert_eq!(scheme.payload.len(), SCHEME_PAYLOAD_V1);
22
+ assert_eq!(scheme.file_size(), 221);
23
+ }
24
+
25
+ #[test]
26
+ fn parse_shopping_v1() {
27
+ let path = Path::new(FIXTURES).join("shopping.wsc");
28
+ let scheme = SchemeFile::from_file(&path).expect("failed to parse shopping.wsc");
29
+ assert_eq!(scheme.version, SchemeVersion::V1);
30
+ assert_eq!(scheme.payload.len(), SCHEME_PAYLOAD_V1);
31
+ }
32
+
33
+ #[test]
34
+ fn roundtrip_beginner() {
35
+ let path = Path::new(FIXTURES).join("beginner.wsc");
36
+ let original = std::fs::read(&path).unwrap();
37
+ let scheme = SchemeFile::from_bytes(&original).unwrap();
38
+ assert_eq!(scheme.to_bytes(), original);
39
+ }
40
+
41
+ #[test]
42
+ fn roundtrip_classic() {
43
+ let path = Path::new(FIXTURES).join("classic.wsc");
44
+ let original = std::fs::read(&path).unwrap();
45
+ let scheme = SchemeFile::from_bytes(&original).unwrap();
46
+ assert_eq!(scheme.to_bytes(), original);
47
+ }
48
+
49
+ #[test]
50
+ fn roundtrip_shopping() {
51
+ let path = Path::new(FIXTURES).join("shopping.wsc");
52
+ let original = std::fs::read(&path).unwrap();
53
+ let scheme = SchemeFile::from_bytes(&original).unwrap();
54
+ assert_eq!(scheme.to_bytes(), original);
55
+ }
56
+
57
+ /// Parse all .wsc files from the game directory if available.
58
+ #[test]
59
+ fn parse_all_game_schemes() {
60
+ let schemes_dir = Path::new("I:/games/SteamLibrary/steamapps/common/Worms Armageddon/User/Schemes");
61
+ if !schemes_dir.exists() {
62
+ eprintln!("Skipping: game schemes directory not found");
63
+ return;
64
+ }
65
+
66
+ let mut count = 0;
67
+ for entry in std::fs::read_dir(schemes_dir).unwrap() {
68
+ let entry = entry.unwrap();
69
+ let path = entry.path();
70
+ if path.extension().map_or(false, |e| e == "wsc") {
71
+ let scheme = SchemeFile::from_file(&path)
72
+ .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display()));
73
+
74
+ // Roundtrip check
75
+ let original = std::fs::read(&path).unwrap();
76
+ assert_eq!(
77
+ scheme.to_bytes(),
78
+ original,
79
+ "roundtrip failed for {}",
80
+ path.display()
81
+ );
82
+ count += 1;
83
+ }
84
+ }
85
+ eprintln!("Successfully parsed and round-tripped {count} scheme files");
86
+ }
crates/openwa-wormkit/src/replacements/mod.rs CHANGED
@@ -1,6 +1,8 @@
1
  mod frontend;
 
2
 
3
  pub fn install_all() -> Result<(), String> {
4
  frontend::install()?;
 
5
  Ok(())
6
  }
 
1
  mod frontend;
2
+ mod scheme;
3
 
4
  pub fn install_all() -> Result<(), String> {
5
  frontend::install()?;
6
+ scheme::install()?;
7
  Ok(())
8
  }
crates/openwa-wormkit/src/replacements/scheme.rs ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Scheme file (.wsc) hook.
2
+ //!
3
+ //! Hooks Scheme__ReadFile (0x4D3890) to log when schemes are loaded.
4
+ //! Currently a passthrough — calls the original and logs the result.
5
+ //! Future: replace the file I/O with our Rust SchemeFile parser.
6
+
7
+ use std::ffi::c_void;
8
+ use std::sync::atomic::{AtomicU32, Ordering};
9
+
10
+ use minhook::MinHook;
11
+
12
+ use crate::log_line;
13
+ use crate::rebase::rb;
14
+ use openwa_types::address::va;
15
+
16
+ /// Trampoline to original Scheme__ReadFile.
17
+ static ORIG_SCHEME_READ_FILE: AtomicU32 = AtomicU32::new(0);
18
+
19
+ /// Hook for Scheme__ReadFile (0x4D3890).
20
+ /// stdcall(dest_struct, file_path, flag, out_ptr) -> u32, RET 0x10
21
+ ///
22
+ /// - dest_struct: pointer to destination scheme struct (payload written to +0x14)
23
+ /// - file_path: scheme file path string
24
+ /// - flag: boolean flag (passed to error display)
25
+ /// - out_ptr: optional output byte pointer
26
+ /// - returns: 0 on success, 0xFFFFFFFF on failure
27
+ unsafe extern "stdcall" fn hook_scheme_read_file(
28
+ dest_struct: u32,
29
+ file_path: u32,
30
+ flag: u32,
31
+ out_ptr: u32,
32
+ ) -> u32 {
33
+ // Read the scheme name from the file_path pointer
34
+ let name = if file_path != 0 {
35
+ let cstr = std::ffi::CStr::from_ptr(file_path as *const i8);
36
+ cstr.to_string_lossy().into_owned()
37
+ } else {
38
+ "<null>".to_string()
39
+ };
40
+
41
+ // Call original
42
+ let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
43
+ core::mem::transmute(ORIG_SCHEME_READ_FILE.load(Ordering::Relaxed));
44
+ let result = orig(dest_struct, file_path, flag, out_ptr);
45
+
46
+ // Log the result
47
+ if result == 0 {
48
+ // Read the version byte from dest_struct+0x14 (first 5 bytes are the header)
49
+ // Actually the header is consumed during read; the payload starts at +0x14
50
+ let _ = log_line(&format!(
51
+ "[Scheme] Loaded: {name} -> dest=0x{dest_struct:08X}"
52
+ ));
53
+ } else {
54
+ let _ = log_line(&format!(
55
+ "[Scheme] FAILED to load: {name} (result=0x{result:08X})"
56
+ ));
57
+ }
58
+
59
+ result
60
+ }
61
+
62
+ pub fn install() -> Result<(), String> {
63
+ unsafe {
64
+ let target = rb(va::SCHEME_READ_FILE) as *mut c_void;
65
+ let detour = hook_scheme_read_file as *const () as *mut c_void;
66
+
67
+ let trampoline = MinHook::create_hook(target, detour)
68
+ .map_err(|e| format!("MinHook create_hook failed for Scheme__ReadFile: {e}"))?;
69
+
70
+ MinHook::enable_hook(target)
71
+ .map_err(|e| format!("MinHook enable_hook failed for Scheme__ReadFile: {e}"))?;
72
+
73
+ ORIG_SCHEME_READ_FILE.store(trampoline as u32, Ordering::Relaxed);
74
+
75
+ let _ = log_line(&format!(
76
+ " [HOOK] Scheme__ReadFile: target 0x{:08X}, trampoline 0x{:08X}",
77
+ target as u32, trampoline as u32
78
+ ));
79
+ }
80
+
81
+ Ok(())
82
+ }