docs: add configuration system design and implementation plan
Browse filesDesign doc and implementation plan for porting WA's configuration
subsystem (Theme I/O, Registry, GameInfo, CrashReportURL) to Rust.
These were used to guide the config.rs implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cargo.lock
CHANGED
|
@@ -60,6 +60,7 @@ name = "openwa-lib"
|
|
| 60 |
version = "0.1.0"
|
| 61 |
dependencies = [
|
| 62 |
"openwa-types",
|
|
|
|
| 63 |
]
|
| 64 |
|
| 65 |
[[package]]
|
|
|
|
| 60 |
version = "0.1.0"
|
| 61 |
dependencies = [
|
| 62 |
"openwa-types",
|
| 63 |
+
"windows-sys",
|
| 64 |
]
|
| 65 |
|
| 66 |
[[package]]
|
docs/plans/2026-03-06-configuration-system-design.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Port Configuration System to Rust
|
| 2 |
+
|
| 3 |
+
## Context
|
| 4 |
+
|
| 5 |
+
The WA configuration system has three subsystems: theme file I/O, Windows Registry management, and game options loading. All are self-contained leaf functions with no graphics or game state dependencies beyond simple struct field writes.
|
| 6 |
+
|
| 7 |
+
## Scope — 7 Functions
|
| 8 |
+
|
| 9 |
+
### Theme File I/O (3 functions)
|
| 10 |
+
|
| 11 |
+
| Function | Address | Convention | Params | What it does |
|
| 12 |
+
|----------|---------|------------|--------|-------------|
|
| 13 |
+
| `Theme__GetFileSize` | 0x44BA80 | cdecl | 0 | Opens `data\current.thm`, returns file length (0 if missing) |
|
| 14 |
+
| `Theme__Load` | 0x44BB20 | stdcall | 1 (dest buffer) | Reads entire theme file into buffer; MessageBox on failure |
|
| 15 |
+
| `Theme__Save` | 0x44BBC0 | stdcall | 2 (buffer, size) | Writes buffer to theme file; MessageBox on create failure |
|
| 16 |
+
|
| 17 |
+
Replace MFC CFile with `std::fs`. Replace `AfxMessageBox` with `MessageBoxA` from the `windows` crate.
|
| 18 |
+
|
| 19 |
+
### Registry Management (2 functions)
|
| 20 |
+
|
| 21 |
+
| Function | Address | Convention | Params | What it does |
|
| 22 |
+
|----------|---------|------------|--------|-------------|
|
| 23 |
+
| `Registry__DeleteKeyRecursive` | 0x4E4D10 | stdcall | 2 (hkey, subkey) | Recursively enumerates and deletes registry subkeys |
|
| 24 |
+
| `Registry__CleanAll` | 0x4C90D0 | stdcall | 1 (struct ptr) | Deletes Data/Options/ExportVideo/VSyncAssist subsections + clears INI |
|
| 25 |
+
|
| 26 |
+
Pure Win32 registry API. Original uses malloc/realloc for key name buffer — Rust `Vec<u8>` replaces this.
|
| 27 |
+
|
| 28 |
+
### Game Options (2 functions)
|
| 29 |
+
|
| 30 |
+
| Function | Address | Convention | Params | What it does |
|
| 31 |
+
|----------|---------|------------|--------|-------------|
|
| 32 |
+
| `GameInfo__LoadOptions` | 0x460AC0 | stdcall | 1 (GameInfo ptr) | Reads ~12 registry values into GameInfo struct fields |
|
| 33 |
+
| `Options__GetCrashReportURL` | 0x5A63F0 | cdecl | 0 | Reads CrashReportURL from Options registry key |
|
| 34 |
+
|
| 35 |
+
LoadOptions reads from `HKCU\Software\Team17SoftwareLTD\WormsArmageddon\Options` via MFC `GetProfileIntW`. We replace with direct `RegOpenKeyExA` + `RegQueryValueExA`.
|
| 36 |
+
|
| 37 |
+
Key value transformations in LoadOptions:
|
| 38 |
+
- `BackgroundDebrisParallax`: clamp to i16 range, then `<< 16` (fixed-point 16.16)
|
| 39 |
+
- `CameraUnlockMouseSpeed`: clamp to max 0xB504, then square
|
| 40 |
+
- `InfoSpy`: stored as `!= 0` (bool)
|
| 41 |
+
- All others: simple cast to byte or store as u32
|
| 42 |
+
|
| 43 |
+
LoadOptions also copies various globals into the GameInfo struct and formats the speech path via sprintf.
|
| 44 |
+
|
| 45 |
+
## Dependencies
|
| 46 |
+
|
| 47 |
+
### `windows` crate
|
| 48 |
+
|
| 49 |
+
Add to `openwa-lib` with features:
|
| 50 |
+
- `Win32_System_Registry` — RegOpenKeyExA, RegQueryValueExA, RegEnumKeyExA, RegDeleteKeyA, RegCloseKey
|
| 51 |
+
- `Win32_UI_WindowsAndMessaging` — MessageBoxA, WriteProfileSectionA
|
| 52 |
+
|
| 53 |
+
Existing raw FFI in `resource.rs` and `rebase.rs` stays as-is for now — migration to `windows` crate is a separate future task.
|
| 54 |
+
|
| 55 |
+
### Registry helper in openwa-lib
|
| 56 |
+
|
| 57 |
+
`read_profile_int(section: &str, key: &str, default: u32) -> u32`
|
| 58 |
+
|
| 59 |
+
Opens `HKCU\Software\Team17SoftwareLTD\WormsArmageddon\{section}`, reads a DWORD via `RegQueryValueExA`, returns default on any failure. Matches MFC `GetProfileIntW` behavior for the registry-mode path.
|
| 60 |
+
|
| 61 |
+
## Hook Strategy
|
| 62 |
+
|
| 63 |
+
All 7 functions are full Rust replacements — no trampolines needed. All conventions are plain cdecl or stdcall (confirmed via RET instruction analysis).
|
| 64 |
+
|
| 65 |
+
## File Layout
|
| 66 |
+
|
| 67 |
+
| File | Change |
|
| 68 |
+
|------|--------|
|
| 69 |
+
| `crates/openwa-types/src/address.rs` | Add 7 address constants |
|
| 70 |
+
| `crates/openwa-lib/Cargo.toml` | Add `windows` crate dependency |
|
| 71 |
+
| `crates/openwa-lib/src/wa/registry.rs` (new) | `read_profile_int`, `delete_key_recursive` |
|
| 72 |
+
| `crates/openwa-lib/src/wa/mod.rs` | Add `pub mod registry;` |
|
| 73 |
+
| `crates/openwa-wormkit/src/replacements/config.rs` (new) | 7 hook functions + `install()` |
|
| 74 |
+
| `crates/openwa-wormkit/src/replacements/mod.rs` | Add `pub mod config;` |
|
| 75 |
+
|
| 76 |
+
## Global Data References in LoadOptions
|
| 77 |
+
|
| 78 |
+
| Global Address | Description | GameInfo Offset |
|
| 79 |
+
|---------------|-------------|-----------------|
|
| 80 |
+
| 0x0088E282 | Base directory string | +0xF404 (speech path) |
|
| 81 |
+
| 0x0088DFF3 | 64 bytes copied verbatim | +0xF485 |
|
| 82 |
+
| 0x0064DA58 | `"data\land.dat"` string (14 bytes) | +0xDAEC |
|
| 83 |
+
| 0x007C0D38 | Unknown byte | +0xF3A0 |
|
| 84 |
+
| 0x0088E39C..0x0088E3AC | 5 DWORDs | +0xF3B4..+0xF3D0 |
|
| 85 |
+
| 0x0088C374 | Guard flag for conditional copies | — |
|
| 86 |
+
| 0x0088E3B8..0x0088E3C4 | 4 DWORDs (conditional) | +0xF3F4..+0xF400 |
|
| 87 |
+
| 0x0088E390 | DWORD | +0xDAE8 |
|
| 88 |
+
| 0x0088E3B0..0x0088E3B4 | 2 DWORDs | +0xF3D4..+0xF3D8 |
|
| 89 |
+
| 0x0088E400..0x0088E408 | 3 DWORDs | +0xF3C4..+0xF3CC |
|
| 90 |
+
| 0x0088E44C | DWORD | +0xF3E4 |
|
| 91 |
+
|
| 92 |
+
These are read via rebased pointers (`rb(addr)`) and written to the GameInfo struct at the given offsets.
|
| 93 |
+
|
| 94 |
+
## Verification
|
| 95 |
+
|
| 96 |
+
1. Build: `cargo build --release -p openwa-wormkit` — clean
|
| 97 |
+
2. Deploy wkOpenWA.dll to game directory
|
| 98 |
+
3. Launch game, check OpenWA.log for config hook messages
|
| 99 |
+
4. Verify game options persist (change Detail Level, restart, confirm it's saved)
|
| 100 |
+
5. Verify theme loads correctly (visual appearance unchanged)
|
| 101 |
+
6. Test Registry__CleanAll if reachable (it's called from a "reset all settings" path)
|
| 102 |
+
7. Check CrashReportURL is read correctly (visible in crash dialog if triggered)
|
| 103 |
+
|
| 104 |
+
## Decisions
|
| 105 |
+
|
| 106 |
+
- **`windows` crate**: Add now, migrate existing FFI later
|
| 107 |
+
- **Theme error messages**: Use `MessageBoxA` from `windows` crate (replaces AfxMessageBox)
|
| 108 |
+
- **Registry reads**: Direct Win32 API, not MFC GetProfileIntW (Steam always uses registry mode)
|
| 109 |
+
- **GameInfo struct**: Write to raw pointer at known offsets (don't type the full 0xF500-byte struct)
|
| 110 |
+
- **std::fs over CFile**: For theme I/O
|
docs/plans/2026-03-06-configuration-system-plan.md
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration System Port — Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
| 4 |
+
|
| 5 |
+
**Goal:** Port 7 WA.exe configuration functions (theme I/O, registry management, game options loading) to Rust.
|
| 6 |
+
|
| 7 |
+
**Architecture:** New `config.rs` replacement module in openwa-wormkit hooks 7 functions. Registry helpers live in openwa-lib for reuse. Uses `windows` crate for Win32 registry/MessageBox APIs, `std::fs` for theme file I/O.
|
| 8 |
+
|
| 9 |
+
**Tech Stack:** Rust, `windows` crate (Win32 registry + UI), MinHook inline hooks, `std::fs`
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
### Task 1: Add `windows` crate dependency to openwa-lib
|
| 14 |
+
|
| 15 |
+
**Files:**
|
| 16 |
+
- Modify: `crates/openwa-lib/Cargo.toml`
|
| 17 |
+
|
| 18 |
+
**Step 1: Add the dependency**
|
| 19 |
+
|
| 20 |
+
```toml
|
| 21 |
+
[dependencies]
|
| 22 |
+
openwa-types = { path = "../openwa-types" }
|
| 23 |
+
|
| 24 |
+
[target.'cfg(target_os = "windows")'.dependencies]
|
| 25 |
+
windows-sys = { version = "0.59", features = [
|
| 26 |
+
"Win32_System_Registry",
|
| 27 |
+
"Win32_UI_WindowsAndMessaging",
|
| 28 |
+
] }
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
Note: Use `windows-sys` (not `windows`) — it's the raw FFI bindings crate, already used by openwa-wormkit. Lower overhead, no COM wrappers.
|
| 32 |
+
|
| 33 |
+
**Step 2: Build to verify**
|
| 34 |
+
|
| 35 |
+
Run: `cargo build --release -p openwa-lib`
|
| 36 |
+
Expected: clean build
|
| 37 |
+
|
| 38 |
+
**Step 3: Commit**
|
| 39 |
+
|
| 40 |
+
```
|
| 41 |
+
feat: add windows-sys registry + UI features to openwa-lib
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
### Task 2: Add address constants
|
| 47 |
+
|
| 48 |
+
**Files:**
|
| 49 |
+
- Modify: `crates/openwa-types/src/address.rs`
|
| 50 |
+
|
| 51 |
+
**Step 1: Add constants**
|
| 52 |
+
|
| 53 |
+
Add to the `va` module, in a new `// === Configuration / registry ===` section after the scheme section:
|
| 54 |
+
|
| 55 |
+
```rust
|
| 56 |
+
// === Configuration / registry ===
|
| 57 |
+
|
| 58 |
+
/// Theme file size check: cdecl() -> u32 (file length or 0)
|
| 59 |
+
pub const THEME_GET_FILE_SIZE: u32 = 0x0044_BA80;
|
| 60 |
+
/// Theme file load: stdcall(dest_buffer)
|
| 61 |
+
pub const THEME_LOAD: u32 = 0x0044_BB20;
|
| 62 |
+
/// Theme file save: stdcall(buffer, size)
|
| 63 |
+
pub const THEME_SAVE: u32 = 0x0044_BBC0;
|
| 64 |
+
/// Recursive registry key deletion: stdcall(hkey, subkey) -> i32
|
| 65 |
+
pub const REGISTRY_DELETE_KEY_RECURSIVE: u32 = 0x004E_4D10;
|
| 66 |
+
/// Registry cleanup — deletes all 4 subsections + clears INI: stdcall(struct_ptr)
|
| 67 |
+
pub const REGISTRY_CLEAN_ALL: u32 = 0x004C_90D0;
|
| 68 |
+
/// Loads game options from registry into GameInfo struct: stdcall(game_info_ptr)
|
| 69 |
+
pub const GAMEINFO_LOAD_OPTIONS: u32 = 0x0046_0AC0;
|
| 70 |
+
/// Reads CrashReportURL from Options registry key: cdecl() -> *const u8
|
| 71 |
+
pub const OPTIONS_GET_CRASH_REPORT_URL: u32 = 0x005A_63F0;
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
**Step 2: Build to verify**
|
| 75 |
+
|
| 76 |
+
Run: `cargo build --release -p openwa-types`
|
| 77 |
+
Expected: clean build
|
| 78 |
+
|
| 79 |
+
**Step 3: Commit**
|
| 80 |
+
|
| 81 |
+
```
|
| 82 |
+
feat: add configuration system address constants
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
---
|
| 86 |
+
|
| 87 |
+
### Task 3: Add registry helpers to openwa-lib
|
| 88 |
+
|
| 89 |
+
**Files:**
|
| 90 |
+
- Create: `crates/openwa-lib/src/wa/registry.rs`
|
| 91 |
+
- Modify: `crates/openwa-lib/src/wa/mod.rs`
|
| 92 |
+
|
| 93 |
+
**Step 1: Add `pub mod registry;` to mod.rs**
|
| 94 |
+
|
| 95 |
+
After the existing `pub mod resource;` line:
|
| 96 |
+
|
| 97 |
+
```rust
|
| 98 |
+
pub mod registry;
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
**Step 2: Create registry.rs**
|
| 102 |
+
|
| 103 |
+
```rust
|
| 104 |
+
//! Win32 registry helpers for WA configuration.
|
| 105 |
+
|
| 106 |
+
use windows_sys::Win32::System::Registry::*;
|
| 107 |
+
|
| 108 |
+
const WA_REGISTRY_PATH: &str = "Software\\Team17SoftwareLTD\\WormsArmageddon\\";
|
| 109 |
+
|
| 110 |
+
/// Read an integer value from the WA registry.
|
| 111 |
+
///
|
| 112 |
+
/// Opens `HKCU\Software\Team17SoftwareLTD\WormsArmageddon\{section}\{key}`
|
| 113 |
+
/// and reads it as a DWORD. Returns `default` on any failure.
|
| 114 |
+
///
|
| 115 |
+
/// This replaces MFC's `CWinApp::GetProfileIntW` for the registry-mode path.
|
| 116 |
+
pub fn read_profile_int(section: &str, key: &str, default: u32) -> u32 {
|
| 117 |
+
let mut full_path = String::with_capacity(WA_REGISTRY_PATH.len() + section.len() + 1);
|
| 118 |
+
full_path.push_str(WA_REGISTRY_PATH);
|
| 119 |
+
full_path.push_str(section);
|
| 120 |
+
full_path.push('\0');
|
| 121 |
+
|
| 122 |
+
let mut key_buf = String::with_capacity(key.len() + 1);
|
| 123 |
+
key_buf.push_str(key);
|
| 124 |
+
key_buf.push('\0');
|
| 125 |
+
|
| 126 |
+
unsafe {
|
| 127 |
+
let mut hkey: HKEY = 0;
|
| 128 |
+
let status = RegOpenKeyExA(
|
| 129 |
+
HKEY_CURRENT_USER,
|
| 130 |
+
full_path.as_ptr(),
|
| 131 |
+
0,
|
| 132 |
+
KEY_READ,
|
| 133 |
+
&mut hkey,
|
| 134 |
+
);
|
| 135 |
+
if status != 0 {
|
| 136 |
+
return default;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
let mut value: u32 = 0;
|
| 140 |
+
let mut value_size: u32 = 4;
|
| 141 |
+
let mut value_type: u32 = 0;
|
| 142 |
+
let status = RegQueryValueExA(
|
| 143 |
+
hkey,
|
| 144 |
+
key_buf.as_ptr(),
|
| 145 |
+
core::ptr::null_mut(),
|
| 146 |
+
&mut value_type,
|
| 147 |
+
&mut value as *mut u32 as *mut u8,
|
| 148 |
+
&mut value_size,
|
| 149 |
+
);
|
| 150 |
+
RegCloseKey(hkey);
|
| 151 |
+
|
| 152 |
+
if status == 0 && value_type == REG_DWORD {
|
| 153 |
+
value
|
| 154 |
+
} else {
|
| 155 |
+
default
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/// Read a string value from the WA registry.
|
| 161 |
+
///
|
| 162 |
+
/// Opens `HKCU\Software\Team17SoftwareLTD\WormsArmageddon\{section}\{key}`
|
| 163 |
+
/// and reads it as a REG_SZ string into `buf`. Returns the number of bytes
|
| 164 |
+
/// written (excluding null terminator), or 0 on failure.
|
| 165 |
+
pub fn read_profile_string(section: &str, key: &str, buf: &mut [u8]) -> usize {
|
| 166 |
+
let mut full_path = String::with_capacity(WA_REGISTRY_PATH.len() + section.len() + 1);
|
| 167 |
+
full_path.push_str(WA_REGISTRY_PATH);
|
| 168 |
+
full_path.push_str(section);
|
| 169 |
+
full_path.push('\0');
|
| 170 |
+
|
| 171 |
+
let mut key_buf = String::with_capacity(key.len() + 1);
|
| 172 |
+
key_buf.push_str(key);
|
| 173 |
+
key_buf.push('\0');
|
| 174 |
+
|
| 175 |
+
unsafe {
|
| 176 |
+
let mut hkey: HKEY = 0;
|
| 177 |
+
let status = RegOpenKeyExA(
|
| 178 |
+
HKEY_CURRENT_USER,
|
| 179 |
+
full_path.as_ptr(),
|
| 180 |
+
0,
|
| 181 |
+
KEY_READ,
|
| 182 |
+
&mut hkey,
|
| 183 |
+
);
|
| 184 |
+
if status != 0 {
|
| 185 |
+
return 0;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
let mut value_size: u32 = buf.len() as u32;
|
| 189 |
+
let mut value_type: u32 = 0;
|
| 190 |
+
let status = RegQueryValueExA(
|
| 191 |
+
hkey,
|
| 192 |
+
key_buf.as_ptr(),
|
| 193 |
+
core::ptr::null_mut(),
|
| 194 |
+
&mut value_type,
|
| 195 |
+
buf.as_mut_ptr(),
|
| 196 |
+
&mut value_size,
|
| 197 |
+
);
|
| 198 |
+
RegCloseKey(hkey);
|
| 199 |
+
|
| 200 |
+
if status == 0 && value_type == REG_SZ && value_size > 0 {
|
| 201 |
+
// value_size includes null terminator
|
| 202 |
+
(value_size - 1) as usize
|
| 203 |
+
} else {
|
| 204 |
+
0
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/// Recursively delete a registry key and all its subkeys.
|
| 210 |
+
///
|
| 211 |
+
/// Replacement for WA's Registry__DeleteKeyRecursive (0x4E4D10).
|
| 212 |
+
/// Returns 0 on success, Win32 error code on failure.
|
| 213 |
+
pub unsafe fn delete_key_recursive(parent: HKEY, subkey: &str) -> i32 {
|
| 214 |
+
if subkey.is_empty() {
|
| 215 |
+
return 0x3F2; // ERROR_INVALID_PARAMETER equivalent used by original
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
let mut subkey_buf = String::with_capacity(subkey.len() + 1);
|
| 219 |
+
subkey_buf.push_str(subkey);
|
| 220 |
+
subkey_buf.push('\0');
|
| 221 |
+
|
| 222 |
+
let mut hkey: HKEY = 0;
|
| 223 |
+
let status = RegOpenKeyExA(
|
| 224 |
+
parent,
|
| 225 |
+
subkey_buf.as_ptr(),
|
| 226 |
+
0,
|
| 227 |
+
KEY_READ | KEY_ENUMERATE_SUB_KEYS,
|
| 228 |
+
&mut hkey,
|
| 229 |
+
);
|
| 230 |
+
if status != 0 {
|
| 231 |
+
return status;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Enumerate and recursively delete all child keys
|
| 235 |
+
let mut name_buf = vec![0u8; 256];
|
| 236 |
+
loop {
|
| 237 |
+
let mut name_len = name_buf.len() as u32;
|
| 238 |
+
let status = RegEnumKeyExA(
|
| 239 |
+
hkey,
|
| 240 |
+
0, // always index 0 since we delete as we go
|
| 241 |
+
name_buf.as_mut_ptr(),
|
| 242 |
+
&mut name_len,
|
| 243 |
+
core::ptr::null_mut(),
|
| 244 |
+
core::ptr::null_mut(),
|
| 245 |
+
core::ptr::null_mut(),
|
| 246 |
+
core::ptr::null_mut(),
|
| 247 |
+
);
|
| 248 |
+
|
| 249 |
+
if status == 0x103 {
|
| 250 |
+
// ERROR_NO_MORE_ITEMS — all children deleted
|
| 251 |
+
break;
|
| 252 |
+
}
|
| 253 |
+
if status != 0 {
|
| 254 |
+
RegCloseKey(hkey);
|
| 255 |
+
return status;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
if name_len as usize == name_buf.len() {
|
| 259 |
+
// Name buffer too small, grow it
|
| 260 |
+
name_buf.resize(name_buf.len() * 2, 0);
|
| 261 |
+
continue;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Null-terminate and recurse
|
| 265 |
+
name_buf[name_len as usize] = 0;
|
| 266 |
+
let child_name = core::str::from_utf8_unchecked(&name_buf[..name_len as usize]);
|
| 267 |
+
let result = delete_key_recursive(hkey, child_name);
|
| 268 |
+
if result != 0 {
|
| 269 |
+
RegCloseKey(hkey);
|
| 270 |
+
return result;
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// All children deleted, now delete the key itself
|
| 275 |
+
let status = RegDeleteKeyA(parent, subkey_buf.as_ptr());
|
| 276 |
+
RegCloseKey(hkey);
|
| 277 |
+
status
|
| 278 |
+
}
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
**Step 3: Build to verify**
|
| 282 |
+
|
| 283 |
+
Run: `cargo build --release -p openwa-lib`
|
| 284 |
+
Expected: clean build
|
| 285 |
+
|
| 286 |
+
**Step 4: Commit**
|
| 287 |
+
|
| 288 |
+
```
|
| 289 |
+
feat: add Win32 registry helpers to openwa-lib
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
---
|
| 293 |
+
|
| 294 |
+
### Task 4: Create config.rs with Theme hooks
|
| 295 |
+
|
| 296 |
+
**Files:**
|
| 297 |
+
- Create: `crates/openwa-wormkit/src/replacements/config.rs`
|
| 298 |
+
- Modify: `crates/openwa-wormkit/src/replacements/mod.rs`
|
| 299 |
+
|
| 300 |
+
**Step 1: Add `mod config;` to mod.rs and call install**
|
| 301 |
+
|
| 302 |
+
```rust
|
| 303 |
+
mod config;
|
| 304 |
+
mod frontend;
|
| 305 |
+
mod scheme;
|
| 306 |
+
|
| 307 |
+
pub fn install_all() -> Result<(), String> {
|
| 308 |
+
frontend::install()?;
|
| 309 |
+
scheme::install()?;
|
| 310 |
+
config::install()?;
|
| 311 |
+
Ok(())
|
| 312 |
+
}
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
**Step 2: Create config.rs with theme functions**
|
| 316 |
+
|
| 317 |
+
```rust
|
| 318 |
+
//! Configuration system hooks.
|
| 319 |
+
//!
|
| 320 |
+
//! Replaces WA.exe configuration functions with Rust implementations:
|
| 321 |
+
//! - Theme__GetFileSize (0x44BA80): theme file size query
|
| 322 |
+
//! - Theme__Load (0x44BB20): theme file read
|
| 323 |
+
//! - Theme__Save (0x44BBC0): theme file write
|
| 324 |
+
//! - Registry__DeleteKeyRecursive (0x4E4D10): recursive registry deletion
|
| 325 |
+
//! - Registry__CleanAll (0x4C90D0): full registry cleanup
|
| 326 |
+
//! - GameInfo__LoadOptions (0x460AC0): game options from registry
|
| 327 |
+
//! - Options__GetCrashReportURL (0x5A63F0): crash report URL from registry
|
| 328 |
+
|
| 329 |
+
use crate::log_line;
|
| 330 |
+
use openwa_lib::rebase::rb;
|
| 331 |
+
use openwa_types::address::va;
|
| 332 |
+
|
| 333 |
+
const THEME_PATH: &str = "data\\current.thm";
|
| 334 |
+
|
| 335 |
+
// ============================================================
|
| 336 |
+
// Theme__GetFileSize replacement (0x44BA80)
|
| 337 |
+
// ============================================================
|
| 338 |
+
|
| 339 |
+
/// Rust replacement for Theme__GetFileSize.
|
| 340 |
+
/// cdecl() -> u32 (file length, or 0 if missing)
|
| 341 |
+
unsafe extern "cdecl" fn hook_theme_get_file_size() -> u32 {
|
| 342 |
+
match std::fs::metadata(THEME_PATH) {
|
| 343 |
+
Ok(m) => m.len() as u32,
|
| 344 |
+
Err(_) => 0,
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
// ============================================================
|
| 349 |
+
// Theme__Load replacement (0x44BB20)
|
| 350 |
+
// ============================================================
|
| 351 |
+
|
| 352 |
+
/// Rust replacement for Theme__Load.
|
| 353 |
+
/// stdcall(dest_buffer: *mut u8)
|
| 354 |
+
unsafe extern "stdcall" fn hook_theme_load(dest: u32) {
|
| 355 |
+
match std::fs::read(THEME_PATH) {
|
| 356 |
+
Ok(data) => {
|
| 357 |
+
core::ptr::copy_nonoverlapping(data.as_ptr(), dest as *mut u8, data.len());
|
| 358 |
+
}
|
| 359 |
+
Err(_) => {
|
| 360 |
+
show_error_message("ERROR: NO CURRENT.THM FILE FOUND");
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
// ============================================================
|
| 366 |
+
// Theme__Save replacement (0x44BBC0)
|
| 367 |
+
// ============================================================
|
| 368 |
+
|
| 369 |
+
/// Rust replacement for Theme__Save.
|
| 370 |
+
/// stdcall(buffer: *const u8, size: u32)
|
| 371 |
+
unsafe extern "stdcall" fn hook_theme_save(buffer: u32, size: u32) {
|
| 372 |
+
let data = core::slice::from_raw_parts(buffer as *const u8, size as usize);
|
| 373 |
+
if let Err(_) = std::fs::write(THEME_PATH, data) {
|
| 374 |
+
show_error_message("ERROR: Could Not create CURRENT.THM File");
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
/// Show an error message box, matching AfxMessageBox behavior.
|
| 379 |
+
fn show_error_message(msg: &str) {
|
| 380 |
+
use windows_sys::Win32::UI::WindowsAndMessaging::*;
|
| 381 |
+
let mut msg_buf: Vec<u8> = msg.bytes().collect();
|
| 382 |
+
msg_buf.push(0);
|
| 383 |
+
unsafe {
|
| 384 |
+
MessageBoxA(0, msg_buf.as_ptr(), core::ptr::null(), MB_OK);
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
// ============================================================
|
| 389 |
+
// Hook installation
|
| 390 |
+
// ============================================================
|
| 391 |
+
|
| 392 |
+
pub fn install() -> Result<(), String> {
|
| 393 |
+
unsafe {
|
| 394 |
+
let _ = crate::hook::install(
|
| 395 |
+
"Theme__GetFileSize",
|
| 396 |
+
va::THEME_GET_FILE_SIZE,
|
| 397 |
+
hook_theme_get_file_size as *const (),
|
| 398 |
+
)?;
|
| 399 |
+
|
| 400 |
+
let _ = crate::hook::install(
|
| 401 |
+
"Theme__Load",
|
| 402 |
+
va::THEME_LOAD,
|
| 403 |
+
hook_theme_load as *const (),
|
| 404 |
+
)?;
|
| 405 |
+
|
| 406 |
+
let _ = crate::hook::install(
|
| 407 |
+
"Theme__Save",
|
| 408 |
+
va::THEME_SAVE,
|
| 409 |
+
hook_theme_save as *const (),
|
| 410 |
+
)?;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
Ok(())
|
| 414 |
+
}
|
| 415 |
+
```
|
| 416 |
+
|
| 417 |
+
**Step 3: Add windows-sys dependency to openwa-wormkit Cargo.toml**
|
| 418 |
+
|
| 419 |
+
The `windows-sys` dep in openwa-wormkit needs the UI feature too. Update the existing entry:
|
| 420 |
+
|
| 421 |
+
```toml
|
| 422 |
+
[target.'cfg(target_os = "windows")'.dependencies]
|
| 423 |
+
windows-sys = { version = "0.59", features = [
|
| 424 |
+
"Win32_System_Memory",
|
| 425 |
+
"Win32_UI_WindowsAndMessaging",
|
| 426 |
+
] }
|
| 427 |
+
```
|
| 428 |
+
|
| 429 |
+
**Step 4: Build to verify**
|
| 430 |
+
|
| 431 |
+
Run: `cargo build --release -p openwa-wormkit`
|
| 432 |
+
Expected: clean build
|
| 433 |
+
|
| 434 |
+
**Step 5: Deploy + test theme**
|
| 435 |
+
|
| 436 |
+
Copy DLL, launch game. Theme should load normally (visual appearance unchanged). Check OpenWA.log for hook messages.
|
| 437 |
+
|
| 438 |
+
**Step 6: Commit**
|
| 439 |
+
|
| 440 |
+
```
|
| 441 |
+
feat: add Theme file I/O hooks (GetFileSize, Load, Save)
|
| 442 |
+
```
|
| 443 |
+
|
| 444 |
+
---
|
| 445 |
+
|
| 446 |
+
### Task 5: Add Registry hooks
|
| 447 |
+
|
| 448 |
+
**Files:**
|
| 449 |
+
- Modify: `crates/openwa-wormkit/src/replacements/config.rs`
|
| 450 |
+
|
| 451 |
+
**Step 1: Add registry hook functions**
|
| 452 |
+
|
| 453 |
+
Add after the Theme section, before `install()`:
|
| 454 |
+
|
| 455 |
+
```rust
|
| 456 |
+
// ============================================================
|
| 457 |
+
// Registry__DeleteKeyRecursive replacement (0x4E4D10)
|
| 458 |
+
// ============================================================
|
| 459 |
+
|
| 460 |
+
/// Rust replacement for Registry__DeleteKeyRecursive.
|
| 461 |
+
/// stdcall(hkey: HKEY, subkey: *const u8) -> i32
|
| 462 |
+
unsafe extern "stdcall" fn hook_delete_key_recursive(hkey: u32, subkey: u32) -> i32 {
|
| 463 |
+
use windows_sys::Win32::System::Registry::HKEY;
|
| 464 |
+
|
| 465 |
+
let c_subkey = std::ffi::CStr::from_ptr(subkey as *const i8);
|
| 466 |
+
let subkey_str = c_subkey.to_string_lossy();
|
| 467 |
+
|
| 468 |
+
let _ = log_line(&format!("[Config] DeleteKeyRecursive: {subkey_str}"));
|
| 469 |
+
|
| 470 |
+
let result = openwa_lib::wa::registry::delete_key_recursive(
|
| 471 |
+
hkey as HKEY,
|
| 472 |
+
&subkey_str,
|
| 473 |
+
);
|
| 474 |
+
|
| 475 |
+
let _ = log_line(&format!("[Config] DeleteKeyRecursive result: {result}"));
|
| 476 |
+
result
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
// ============================================================
|
| 480 |
+
// Registry__CleanAll replacement (0x4C90D0)
|
| 481 |
+
// ============================================================
|
| 482 |
+
|
| 483 |
+
/// Rust replacement for Registry__CleanAll.
|
| 484 |
+
/// stdcall(struct_ptr: u32)
|
| 485 |
+
unsafe extern "stdcall" fn hook_registry_clean_all(struct_ptr: u32) {
|
| 486 |
+
use windows_sys::Win32::System::Registry::HKEY_CURRENT_USER;
|
| 487 |
+
use windows_sys::Win32::UI::WindowsAndMessaging::*;
|
| 488 |
+
|
| 489 |
+
let _ = log_line("[Config] CleanAll: deleting registry sections");
|
| 490 |
+
|
| 491 |
+
let sections = [
|
| 492 |
+
"Software\\Team17SoftwareLTD\\WormsArmageddon\\Data",
|
| 493 |
+
"Software\\Team17SoftwareLTD\\WormsArmageddon\\Options",
|
| 494 |
+
"Software\\Team17SoftwareLTD\\WormsArmageddon\\ExportVideo",
|
| 495 |
+
"Software\\Team17SoftwareLTD\\WormsArmageddon\\VSyncAssist",
|
| 496 |
+
];
|
| 497 |
+
|
| 498 |
+
for section in §ions {
|
| 499 |
+
openwa_lib::wa::registry::delete_key_recursive(HKEY_CURRENT_USER, section);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
// Clear the NetSettings INI section
|
| 503 |
+
WriteProfileSectionA(b"NetSettings\0".as_ptr(), b"\0".as_ptr());
|
| 504 |
+
|
| 505 |
+
// Set struct_ptr + 0xE0 = 0
|
| 506 |
+
*((struct_ptr + 0xE0) as *mut u8) = 0;
|
| 507 |
+
|
| 508 |
+
let _ = log_line("[Config] CleanAll completed");
|
| 509 |
+
}
|
| 510 |
+
```
|
| 511 |
+
|
| 512 |
+
**Step 2: Wire up in install()**
|
| 513 |
+
|
| 514 |
+
Add before the closing `Ok(())`:
|
| 515 |
+
|
| 516 |
+
```rust
|
| 517 |
+
let _ = crate::hook::install(
|
| 518 |
+
"Registry__DeleteKeyRecursive",
|
| 519 |
+
va::REGISTRY_DELETE_KEY_RECURSIVE,
|
| 520 |
+
hook_delete_key_recursive as *const (),
|
| 521 |
+
)?;
|
| 522 |
+
|
| 523 |
+
let _ = crate::hook::install(
|
| 524 |
+
"Registry__CleanAll",
|
| 525 |
+
va::REGISTRY_CLEAN_ALL,
|
| 526 |
+
hook_registry_clean_all as *const (),
|
| 527 |
+
)?;
|
| 528 |
+
```
|
| 529 |
+
|
| 530 |
+
**Step 3: Build to verify**
|
| 531 |
+
|
| 532 |
+
Run: `cargo build --release -p openwa-wormkit`
|
| 533 |
+
Expected: clean build
|
| 534 |
+
|
| 535 |
+
**Step 4: Commit**
|
| 536 |
+
|
| 537 |
+
```
|
| 538 |
+
feat: add Registry cleanup hooks (DeleteKeyRecursive, CleanAll)
|
| 539 |
+
```
|
| 540 |
+
|
| 541 |
+
---
|
| 542 |
+
|
| 543 |
+
### Task 6: Add GameInfo__LoadOptions hook
|
| 544 |
+
|
| 545 |
+
**Files:**
|
| 546 |
+
- Modify: `crates/openwa-wormkit/src/replacements/config.rs`
|
| 547 |
+
- Modify: `crates/openwa-types/src/address.rs` (global data addresses)
|
| 548 |
+
|
| 549 |
+
**Step 1: Add global data address constants**
|
| 550 |
+
|
| 551 |
+
Add to `address.rs` in the `// === Global variables (in .data) ===` section:
|
| 552 |
+
|
| 553 |
+
```rust
|
| 554 |
+
/// Base directory string (null-terminated)
|
| 555 |
+
pub const G_BASE_DIR: u32 = 0x0088_E282;
|
| 556 |
+
/// 64-byte data block copied into GameInfo+0xF485
|
| 557 |
+
pub const G_GAMEINFO_BLOCK_F485: u32 = 0x0088_DFF3;
|
| 558 |
+
/// "data\land.dat" string constant (14 bytes)
|
| 559 |
+
pub const G_LAND_DAT_STRING: u32 = 0x0064_DA58;
|
| 560 |
+
/// Unknown byte read into GameInfo+0xF3A0
|
| 561 |
+
pub const G_CONFIG_BYTE_F3A0: u32 = 0x007C_0D38;
|
| 562 |
+
/// 5 DWORDs: GameInfo offsets +0xF3B4..+0xF3D0
|
| 563 |
+
pub const G_CONFIG_DWORDS_F3B4: u32 = 0x0088_E39C;
|
| 564 |
+
/// Guard flag for conditional config copies
|
| 565 |
+
pub const G_CONFIG_GUARD: u32 = 0x0088_C374;
|
| 566 |
+
/// 4 DWORDs (conditional): GameInfo offsets +0xF3F4..+0xF400
|
| 567 |
+
pub const G_CONFIG_DWORDS_F3F4: u32 = 0x0088_E3B8;
|
| 568 |
+
/// DWORD → GameInfo+0xDAE8
|
| 569 |
+
pub const G_CONFIG_DWORD_DAE8: u32 = 0x0088_E390;
|
| 570 |
+
/// 2 DWORDs → GameInfo+0xF3D4, +0xF3D8
|
| 571 |
+
pub const G_CONFIG_DWORDS_F3D4: u32 = 0x0088_E3B0;
|
| 572 |
+
/// 3 DWORDs → GameInfo+0xF3C4..+0xF3CC
|
| 573 |
+
pub const G_CONFIG_DWORDS_F3C4: u32 = 0x0088_E400;
|
| 574 |
+
/// DWORD → GameInfo+0xF3E4
|
| 575 |
+
pub const G_CONFIG_DWORD_F3E4: u32 = 0x0088_E44C;
|
| 576 |
+
/// Streams directory path buffer
|
| 577 |
+
pub const G_STREAMS_DIR: u32 = 0x0088_AE18;
|
| 578 |
+
/// Random stream indices (16 entries)
|
| 579 |
+
pub const G_STREAM_INDICES: u32 = 0x0088_AE9C;
|
| 580 |
+
/// Stream index end sentinel
|
| 581 |
+
pub const G_STREAM_INDICES_END: u32 = 0x0088_AEDC;
|
| 582 |
+
/// DAT_0088E394 flag for stream volume
|
| 583 |
+
pub const G_STREAM_FLAG: u32 = 0x0088_E394;
|
| 584 |
+
/// Stream volume byte
|
| 585 |
+
pub const G_STREAM_VOLUME: u32 = 0x0088_AEDD;
|
| 586 |
+
```
|
| 587 |
+
|
| 588 |
+
**Step 2: Add LoadOptions hook function**
|
| 589 |
+
|
| 590 |
+
Add to config.rs before `install()`:
|
| 591 |
+
|
| 592 |
+
```rust
|
| 593 |
+
// ============================================================
|
| 594 |
+
// GameInfo__LoadOptions replacement (0x460AC0)
|
| 595 |
+
// ============================================================
|
| 596 |
+
|
| 597 |
+
/// Rust replacement for GameInfo__LoadOptions.
|
| 598 |
+
/// stdcall(game_info: u32)
|
| 599 |
+
///
|
| 600 |
+
/// Reads game options from the Windows registry and copies various globals
|
| 601 |
+
/// into the GameInfo struct at known offsets.
|
| 602 |
+
unsafe extern "stdcall" fn hook_load_options(gi: u32) {
|
| 603 |
+
use openwa_lib::wa::registry::read_profile_int;
|
| 604 |
+
|
| 605 |
+
let _ = log_line("[Config] LoadOptions: loading game options from registry");
|
| 606 |
+
|
| 607 |
+
// Format speech path: "%s\user\speech"
|
| 608 |
+
let base_dir = rb(va::G_BASE_DIR) as *const u8;
|
| 609 |
+
let speech_dest = (gi + 0xF404) as *mut u8;
|
| 610 |
+
let base_str = std::ffi::CStr::from_ptr(base_dir as *const i8);
|
| 611 |
+
let speech_path = format!("{}\\user\\speech\0", base_str.to_string_lossy());
|
| 612 |
+
core::ptr::copy_nonoverlapping(
|
| 613 |
+
speech_path.as_ptr(),
|
| 614 |
+
speech_dest,
|
| 615 |
+
speech_path.len(),
|
| 616 |
+
);
|
| 617 |
+
|
| 618 |
+
// Copy 64 bytes from global 0x88DFF3 → GameInfo+0xF485
|
| 619 |
+
core::ptr::copy_nonoverlapping(
|
| 620 |
+
rb(va::G_GAMEINFO_BLOCK_F485) as *const u8,
|
| 621 |
+
(gi + 0xF485) as *mut u8,
|
| 622 |
+
64,
|
| 623 |
+
);
|
| 624 |
+
|
| 625 |
+
// Format streams directory and randomize stream indices
|
| 626 |
+
let streams_dest = rb(va::G_STREAMS_DIR) as *mut u8;
|
| 627 |
+
let streams_path = format!("{}\\streams\0", base_str.to_string_lossy());
|
| 628 |
+
core::ptr::copy_nonoverlapping(
|
| 629 |
+
streams_path.as_ptr(),
|
| 630 |
+
streams_dest,
|
| 631 |
+
streams_path.len(),
|
| 632 |
+
);
|
| 633 |
+
|
| 634 |
+
// Randomize stream indices (16 entries, each rand() % 11 + 1)
|
| 635 |
+
let indices = rb(va::G_STREAM_INDICES) as *mut u32;
|
| 636 |
+
let indices_end = rb(va::G_STREAM_INDICES_END) as usize;
|
| 637 |
+
let mut ptr = indices;
|
| 638 |
+
while (ptr as usize) < indices_end {
|
| 639 |
+
extern "cdecl" { fn rand() -> i32; }
|
| 640 |
+
*ptr = (rand() % 11 + 1) as u32;
|
| 641 |
+
ptr = ptr.add(1);
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
// Stream volume: 0x10 if flag set, else 0
|
| 645 |
+
let stream_vol_addr = rb(va::G_STREAM_INDICES_END) as *mut u8;
|
| 646 |
+
*stream_vol_addr = if *(rb(va::G_STREAM_FLAG) as *const u32) != 0 { 0x10 } else { 0 };
|
| 647 |
+
// Secondary volume byte
|
| 648 |
+
*(rb(va::G_STREAM_VOLUME) as *mut u8) = 0x4B;
|
| 649 |
+
|
| 650 |
+
// Copy "data\land.dat" string (14 bytes) → GameInfo+0xDAEC
|
| 651 |
+
core::ptr::copy_nonoverlapping(
|
| 652 |
+
rb(va::G_LAND_DAT_STRING) as *const u8,
|
| 653 |
+
(gi + 0xDAEC) as *mut u8,
|
| 654 |
+
14,
|
| 655 |
+
);
|
| 656 |
+
|
| 657 |
+
// Copy byte from global → GameInfo+0xF3A0
|
| 658 |
+
*((gi + 0xF3A0) as *mut u8) = *(rb(va::G_CONFIG_BYTE_F3A0) as *const u8);
|
| 659 |
+
|
| 660 |
+
// Read registry values from "Options" section
|
| 661 |
+
let detail = read_profile_int("Options", "DetailLevel", 5);
|
| 662 |
+
*((gi + 0xF3A1) as *mut u8) = detail as u8;
|
| 663 |
+
|
| 664 |
+
// Zero 2 bytes at +0xF3F0
|
| 665 |
+
*((gi + 0xF3F0) as *mut u16) = 0;
|
| 666 |
+
|
| 667 |
+
// Copy 5 DWORDs from globals → GameInfo+0xF3B4..+0xF3D0
|
| 668 |
+
let src = rb(va::G_CONFIG_DWORDS_F3B4) as *const u32;
|
| 669 |
+
for i in 0u32..5 {
|
| 670 |
+
let offset = 0xF3B4 + i * 4;
|
| 671 |
+
*((gi + offset) as *mut u32) = *src.add(i as usize);
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
// Conditional copy: 4 DWORDs if guard == 0
|
| 675 |
+
if *(rb(va::G_CONFIG_GUARD) as *const u32) == 0 {
|
| 676 |
+
let src = rb(va::G_CONFIG_DWORDS_F3F4) as *const u32;
|
| 677 |
+
for i in 0u32..4 {
|
| 678 |
+
let offset = 0xF3F4 + i * 4;
|
| 679 |
+
*((gi + offset) as *mut u32) = *src.add(i as usize);
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// Single DWORDs from globals
|
| 684 |
+
*((gi + 0xDAE8) as *mut u32) = *(rb(va::G_CONFIG_DWORD_DAE8) as *const u32);
|
| 685 |
+
|
| 686 |
+
let src_d4 = rb(va::G_CONFIG_DWORDS_F3D4) as *const u32;
|
| 687 |
+
*((gi + 0xF3D4) as *mut u32) = *src_d4;
|
| 688 |
+
*((gi + 0xF3D8) as *mut u32) = *src_d4.add(1);
|
| 689 |
+
|
| 690 |
+
// EnergyBar
|
| 691 |
+
let energy = read_profile_int("Options", "EnergyBar", 1);
|
| 692 |
+
*((gi + 0xF3A2) as *mut u8) = energy as u8;
|
| 693 |
+
|
| 694 |
+
// 3 DWORDs from globals → +0xF3C4..+0xF3CC
|
| 695 |
+
let src_c4 = rb(va::G_CONFIG_DWORDS_F3C4) as *const u32;
|
| 696 |
+
for i in 0u32..3 {
|
| 697 |
+
let offset = 0xF3C4 + i * 4;
|
| 698 |
+
*((gi + offset) as *mut u32) = *src_c4.add(i as usize);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// Remaining registry values
|
| 702 |
+
let info_trans = read_profile_int("Options", "InfoTransparency", 0);
|
| 703 |
+
*((gi + 0xF3A3) as *mut u8) = info_trans as u8;
|
| 704 |
+
|
| 705 |
+
let info_spy = read_profile_int("Options", "InfoSpy", 1);
|
| 706 |
+
*((gi + 0xF3A4) as *mut u8) = if info_spy != 0 { 1 } else { 0 };
|
| 707 |
+
|
| 708 |
+
let chat_pinned = read_profile_int("Options", "ChatPinned", 0);
|
| 709 |
+
*((gi + 0xF3A5) as *mut u8) = chat_pinned as u8;
|
| 710 |
+
|
| 711 |
+
let chat_lines = read_profile_int("Options", "ChatLines", 0);
|
| 712 |
+
*((gi + 0xF3A8) as *mut u32) = chat_lines;
|
| 713 |
+
|
| 714 |
+
let pinned_lines = read_profile_int("Options", "PinnedChatLines", 0xFFFFFFFF);
|
| 715 |
+
*((gi + 0xF3AC) as *mut u32) = pinned_lines;
|
| 716 |
+
|
| 717 |
+
let home_lock = read_profile_int("Options", "HomeLock", 0);
|
| 718 |
+
*((gi + 0xF3B0) as *mut u8) = home_lock as u8;
|
| 719 |
+
|
| 720 |
+
// BackgroundDebrisParallax: clamp to i16 range, then << 16
|
| 721 |
+
let mut parallax = read_profile_int("Options", "BackgroundDebrisParallax", 0x50);
|
| 722 |
+
let parallax_i32 = parallax as i32;
|
| 723 |
+
if parallax_i32 < -0x8000 || parallax_i32 > 0x7FFF {
|
| 724 |
+
// Clamp to i16 range
|
| 725 |
+
if parallax_i32 < 0 {
|
| 726 |
+
parallax = (-0x8000i32) as u32;
|
| 727 |
+
} else {
|
| 728 |
+
parallax = 0x7FFF;
|
| 729 |
+
}
|
| 730 |
+
}
|
| 731 |
+
*((gi + 0xF3E8) as *mut u32) = parallax << 16;
|
| 732 |
+
|
| 733 |
+
let onomatopoeia = read_profile_int("Options", "TopmostExplosionOnomatopoeia", 0);
|
| 734 |
+
*((gi + 0xF3EC) as *mut u32) = onomatopoeia;
|
| 735 |
+
|
| 736 |
+
let capture_png = read_profile_int("Options", "CaptureTransparentPNGs", 0);
|
| 737 |
+
*((gi + 0xF3DC) as *mut u32) = capture_png;
|
| 738 |
+
|
| 739 |
+
// CameraUnlockMouseSpeed: clamp to max 0xB504, then square
|
| 740 |
+
let mut mouse_speed = read_profile_int("Options", "CameraUnlockMouseSpeed", 0x10);
|
| 741 |
+
if mouse_speed > 0xB504 {
|
| 742 |
+
if (mouse_speed as i32) < 0 {
|
| 743 |
+
mouse_speed = 0; // negative → 0
|
| 744 |
+
} else {
|
| 745 |
+
mouse_speed = 0xB504;
|
| 746 |
+
}
|
| 747 |
+
}
|
| 748 |
+
*((gi + 0xF3E0) as *mut u32) = mouse_speed * mouse_speed;
|
| 749 |
+
|
| 750 |
+
// Final global DWORD
|
| 751 |
+
*((gi + 0xF3E4) as *mut u32) = *(rb(va::G_CONFIG_DWORD_F3E4) as *const u32);
|
| 752 |
+
|
| 753 |
+
let _ = log_line("[Config] LoadOptions completed (Rust)");
|
| 754 |
+
}
|
| 755 |
+
```
|
| 756 |
+
|
| 757 |
+
**Step 3: Wire up in install()**
|
| 758 |
+
|
| 759 |
+
Add to the unsafe block in `install()`:
|
| 760 |
+
|
| 761 |
+
```rust
|
| 762 |
+
let _ = crate::hook::install(
|
| 763 |
+
"GameInfo__LoadOptions",
|
| 764 |
+
va::GAMEINFO_LOAD_OPTIONS,
|
| 765 |
+
hook_load_options as *const (),
|
| 766 |
+
)?;
|
| 767 |
+
```
|
| 768 |
+
|
| 769 |
+
**Step 4: Build to verify**
|
| 770 |
+
|
| 771 |
+
Run: `cargo build --release -p openwa-wormkit`
|
| 772 |
+
Expected: clean build
|
| 773 |
+
|
| 774 |
+
**Step 5: Deploy + test options**
|
| 775 |
+
|
| 776 |
+
Launch game, change Detail Level in options, restart, verify it persists. Check OpenWA.log.
|
| 777 |
+
|
| 778 |
+
**Step 6: Commit**
|
| 779 |
+
|
| 780 |
+
```
|
| 781 |
+
feat: add GameInfo__LoadOptions hook (registry reads)
|
| 782 |
+
```
|
| 783 |
+
|
| 784 |
+
---
|
| 785 |
+
|
| 786 |
+
### Task 7: Add Options__GetCrashReportURL hook
|
| 787 |
+
|
| 788 |
+
**Files:**
|
| 789 |
+
- Modify: `crates/openwa-wormkit/src/replacements/config.rs`
|
| 790 |
+
- Modify: `crates/openwa-types/src/address.rs`
|
| 791 |
+
|
| 792 |
+
**Step 1: Add global address for the URL buffer**
|
| 793 |
+
|
| 794 |
+
Add to address.rs globals section:
|
| 795 |
+
|
| 796 |
+
```rust
|
| 797 |
+
/// CrashReportURL static buffer (0x400 bytes)
|
| 798 |
+
pub const G_CRASH_REPORT_URL: u32 = 0x0079_FFD8;
|
| 799 |
+
```
|
| 800 |
+
|
| 801 |
+
**Step 2: Add hook function**
|
| 802 |
+
|
| 803 |
+
Add to config.rs before `install()`:
|
| 804 |
+
|
| 805 |
+
```rust
|
| 806 |
+
// ============================================================
|
| 807 |
+
// Options__GetCrashReportURL replacement (0x5A63F0)
|
| 808 |
+
// ============================================================
|
| 809 |
+
|
| 810 |
+
/// Rust replacement for Options__GetCrashReportURL.
|
| 811 |
+
/// cdecl() -> *const u8 (pointer to static buffer, or null)
|
| 812 |
+
unsafe extern "cdecl" fn hook_get_crash_report_url() -> u32 {
|
| 813 |
+
let buf = rb(va::G_CRASH_REPORT_URL) as *mut u8;
|
| 814 |
+
let mut buf_slice = core::slice::from_raw_parts_mut(buf, 0x401);
|
| 815 |
+
|
| 816 |
+
let len = openwa_lib::wa::registry::read_profile_string(
|
| 817 |
+
"Options",
|
| 818 |
+
"CrashReportURL",
|
| 819 |
+
&mut buf_slice[..0x400],
|
| 820 |
+
);
|
| 821 |
+
|
| 822 |
+
if len > 0 {
|
| 823 |
+
// Null-terminate
|
| 824 |
+
*buf.add(len) = 0;
|
| 825 |
+
buf as u32
|
| 826 |
+
} else {
|
| 827 |
+
0 // null pointer = not found
|
| 828 |
+
}
|
| 829 |
+
}
|
| 830 |
+
```
|
| 831 |
+
|
| 832 |
+
**Step 3: Wire up in install()**
|
| 833 |
+
|
| 834 |
+
```rust
|
| 835 |
+
let _ = crate::hook::install(
|
| 836 |
+
"Options__GetCrashReportURL",
|
| 837 |
+
va::OPTIONS_GET_CRASH_REPORT_URL,
|
| 838 |
+
hook_get_crash_report_url as *const (),
|
| 839 |
+
)?;
|
| 840 |
+
```
|
| 841 |
+
|
| 842 |
+
**Step 4: Build to verify**
|
| 843 |
+
|
| 844 |
+
Run: `cargo build --release -p openwa-wormkit`
|
| 845 |
+
Expected: clean build
|
| 846 |
+
|
| 847 |
+
**Step 5: Commit**
|
| 848 |
+
|
| 849 |
+
```
|
| 850 |
+
feat: add Options__GetCrashReportURL hook
|
| 851 |
+
```
|
| 852 |
+
|
| 853 |
+
---
|
| 854 |
+
|
| 855 |
+
### Task 8: Full integration test + final commit
|
| 856 |
+
|
| 857 |
+
**Step 1: Build clean**
|
| 858 |
+
|
| 859 |
+
Run: `cargo build --release -p openwa-wormkit`
|
| 860 |
+
Expected: clean build, no warnings
|
| 861 |
+
|
| 862 |
+
**Step 2: Deploy**
|
| 863 |
+
|
| 864 |
+
```bash
|
| 865 |
+
cp target/i686-pc-windows-msvc/release/openwa_wormkit.dll "I:/games/SteamLibrary/steamapps/common/Worms Armageddon/wkOpenWA.dll"
|
| 866 |
+
```
|
| 867 |
+
|
| 868 |
+
**Step 3: In-game verification**
|
| 869 |
+
|
| 870 |
+
1. Launch game → main menu
|
| 871 |
+
2. Check OpenWA.log for all 7 `[REPLACE]` messages + `[Config] LoadOptions completed`
|
| 872 |
+
3. Verify theme loads correctly (visual appearance unchanged)
|
| 873 |
+
4. Go to Options, change Detail Level, accept
|
| 874 |
+
5. Restart game, verify Detail Level persists
|
| 875 |
+
6. Verify existing scheme functionality still works (quick game setup)
|
| 876 |
+
|
| 877 |
+
**Step 4: Squash/rebase if desired, or leave incremental commits**
|