feat: add typed scheme payload structs + field accessors
Browse filesAdd SchemeOptions (36B), WeaponSettings (4B), and ExtendedOptions (110B)
structs with from_bytes/to_bytes for safe typed access to .wsc payload
fields. Include V3 ROM defaults, scheme weapon order table, and 8 new
tests covering typed field access, roundtrips, and version boundaries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crates/openwa-types/src/address.rs
CHANGED
|
@@ -233,6 +233,21 @@ pub mod va {
|
|
| 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 |
|
|
|
|
| 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 |
+
/// Copies payload data + V3 defaults into scheme struct: fastcall(?, data, dest, name)
|
| 237 |
+
pub const SCHEME_INIT_FROM_DATA: u32 = 0x004D_5020;
|
| 238 |
+
/// Validates weapon ammo counts against max table (39 weapons): returns 0=ok, 1=over limit
|
| 239 |
+
pub const SCHEME_CHECK_WEAPON_LIMITS: u32 = 0x004D_50E0;
|
| 240 |
+
/// Validates V3 extended options field ranges: returns 0=valid, 1=invalid
|
| 241 |
+
pub const SCHEME_VALIDATE_EXTENDED_OPTIONS: u32 = 0x004D_5110;
|
| 242 |
+
/// Scans User\Schemes\ for {NN} name.wsc files, marks found indices in global array
|
| 243 |
+
pub const SCHEME_SCAN_DIRECTORY: u32 = 0x004D_54E0;
|
| 244 |
+
|
| 245 |
+
// === Scheme data (in .rdata/.data) ===
|
| 246 |
+
|
| 247 |
+
/// V3 extended options defaults (110 bytes), applied to V1/V2 schemes
|
| 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 |
|
crates/openwa-types/src/scheme.rs
CHANGED
|
@@ -9,12 +9,12 @@
|
|
| 9 |
/// Bytes 5+: Payload (size depends on version)
|
| 10 |
///
|
| 11 |
/// Version → payload size:
|
| 12 |
-
/// 0x01 → 0xD8 bytes (216)
|
| 13 |
-
/// 0x02 → 0x124 bytes (292)
|
| 14 |
-
///
|
| 15 |
///
|
| 16 |
-
/// Source: Ghidra decompilation of
|
| 17 |
-
///
|
| 18 |
|
| 19 |
/// Magic header bytes for .wsc files.
|
| 20 |
pub const SCHEME_MAGIC: [u8; 4] = *b"SCHM";
|
|
@@ -164,6 +164,715 @@ impl SchemeFile {
|
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
/// Error type for file-based scheme operations.
|
| 168 |
#[derive(Debug)]
|
| 169 |
pub enum SchemeFileError {
|
|
|
|
| 9 |
/// Bytes 5+: Payload (size depends on version)
|
| 10 |
///
|
| 11 |
/// Version → payload size:
|
| 12 |
+
/// 0x01 → 0xD8 bytes (216): 36 options + 45 weapons × 4
|
| 13 |
+
/// 0x02 → 0x124 bytes (292): V1 + 19 super weapons × 4
|
| 14 |
+
/// 0x03 → 0x192 bytes (402): V2 + 110 bytes extended options
|
| 15 |
///
|
| 16 |
+
/// Source: Ghidra decompilation of Scheme__ReadFile (0x4D3890),
|
| 17 |
+
/// Scheme__SaveFile (0x4D44F0), worms2d.info/Game_scheme_file
|
| 18 |
|
| 19 |
/// Magic header bytes for .wsc files.
|
| 20 |
pub const SCHEME_MAGIC: [u8; 4] = *b"SCHM";
|
|
|
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
| 167 |
+
// === Payload layout constants ===
|
| 168 |
+
|
| 169 |
+
/// Byte offset within the payload where game options start.
|
| 170 |
+
pub const OPTIONS_OFFSET: usize = 0;
|
| 171 |
+
/// Size of the game options section in the payload.
|
| 172 |
+
pub const OPTIONS_SIZE: usize = 36;
|
| 173 |
+
/// Byte offset within the payload where V1 weapon settings start.
|
| 174 |
+
pub const WEAPONS_V1_OFFSET: usize = OPTIONS_SIZE; // 36
|
| 175 |
+
/// Number of weapons in V1 schemes.
|
| 176 |
+
pub const WEAPONS_V1_COUNT: usize = 45;
|
| 177 |
+
/// Number of super weapons added in V2.
|
| 178 |
+
pub const WEAPONS_V2_COUNT: usize = 19;
|
| 179 |
+
/// Total weapons in V2+ schemes.
|
| 180 |
+
pub const WEAPONS_TOTAL_COUNT: usize = WEAPONS_V1_COUNT + WEAPONS_V2_COUNT; // 64
|
| 181 |
+
/// Bytes per weapon entry.
|
| 182 |
+
pub const WEAPON_ENTRY_SIZE: usize = 4;
|
| 183 |
+
/// Byte offset where V2 super weapons start.
|
| 184 |
+
pub const WEAPONS_V2_OFFSET: usize = WEAPONS_V1_OFFSET + WEAPONS_V1_COUNT * WEAPON_ENTRY_SIZE; // 216 = 0xD8
|
| 185 |
+
/// Byte offset where V3 extended options start.
|
| 186 |
+
pub const EXTENDED_OPTIONS_OFFSET: usize = WEAPONS_V2_OFFSET + WEAPONS_V2_COUNT * WEAPON_ENTRY_SIZE; // 292 = 0x124
|
| 187 |
+
/// Size of the V3 extended options section.
|
| 188 |
+
pub const EXTENDED_OPTIONS_SIZE: usize = 110; // 0x6E
|
| 189 |
+
|
| 190 |
+
/// Per-weapon settings (4 bytes each in the .wsc file).
|
| 191 |
+
///
|
| 192 |
+
/// Weapon order in the file differs from the runtime `Weapon` enum —
|
| 193 |
+
/// the .wsc uses the "scheme order" defined by the original game UI.
|
| 194 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 195 |
+
pub struct WeaponSettings {
|
| 196 |
+
/// Ammo count. 0 = none, 1-10 = count, 10/0x80+ = infinite.
|
| 197 |
+
pub ammo: u8,
|
| 198 |
+
/// Power level. 0-20 (max varies by weapon).
|
| 199 |
+
pub power: u8,
|
| 200 |
+
/// Turn delay before weapon becomes available. 0 = immediate.
|
| 201 |
+
pub delay: u8,
|
| 202 |
+
/// Crate probability. 0-100 percentage.
|
| 203 |
+
pub crate_probability: u8,
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
impl WeaponSettings {
|
| 207 |
+
pub fn from_bytes(b: &[u8]) -> Self {
|
| 208 |
+
Self {
|
| 209 |
+
ammo: b[0],
|
| 210 |
+
power: b[1],
|
| 211 |
+
delay: b[2],
|
| 212 |
+
crate_probability: b[3],
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
pub fn to_bytes(self) -> [u8; 4] {
|
| 217 |
+
[self.ammo, self.power, self.delay, self.crate_probability]
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/// Game options (first 36 bytes of payload).
|
| 222 |
+
///
|
| 223 |
+
/// These are the core game settings displayed in the scheme editor.
|
| 224 |
+
/// All offsets are relative to the start of the payload (file offset 0x05).
|
| 225 |
+
///
|
| 226 |
+
/// Source: worms2d.info/Game_scheme_file, Ghidra analysis of Scheme__ReadFile.
|
| 227 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 228 |
+
pub struct SchemeOptions {
|
| 229 |
+
/// Seconds between turns (hot-seat delay). Payload +0x00, file 0x05.
|
| 230 |
+
pub hot_seat_delay: u8,
|
| 231 |
+
/// Seconds to retreat after weapon use (grounded). Payload +0x01, file 0x06.
|
| 232 |
+
pub retreat_time: u8,
|
| 233 |
+
/// Seconds to retreat after weapon use (on rope). Payload +0x02, file 0x07.
|
| 234 |
+
pub rope_retreat_time: u8,
|
| 235 |
+
/// Display total round time on screen. Payload +0x03, file 0x08.
|
| 236 |
+
pub display_total_round_time: bool,
|
| 237 |
+
/// Enable automatic replays. Payload +0x04, file 0x09.
|
| 238 |
+
pub automatic_replays: bool,
|
| 239 |
+
/// Fall damage amount at critical velocity. Payload +0x05, file 0x0A.
|
| 240 |
+
pub fall_damage: u8,
|
| 241 |
+
/// Artillery mode (worms can't move). Payload +0x06, file 0x0B.
|
| 242 |
+
pub artillery_mode: bool,
|
| 243 |
+
/// Bounty mode marker. 0x00=unset, 0x5F/0x89=editor magic. Payload +0x07, file 0x0C.
|
| 244 |
+
pub bounty_mode: u8,
|
| 245 |
+
/// Stockpiling mode: 0=Off, 1=On, 2=Anti. Payload +0x08, file 0x0D.
|
| 246 |
+
pub stockpiling: u8,
|
| 247 |
+
/// Worm selection: 0=Sequential, 1=Manual, 2=Random. Payload +0x09, file 0x0E.
|
| 248 |
+
pub worm_select: u8,
|
| 249 |
+
/// Sudden death event: 0=RoundEnds, 1=NuclearStrike, 2=HP→1, 3=Nothing. Payload +0x0A, file 0x0F.
|
| 250 |
+
pub sudden_death_event: u8,
|
| 251 |
+
/// Water rise rate during sudden death. Payload +0x0B, file 0x10.
|
| 252 |
+
pub water_rise_rate: u8,
|
| 253 |
+
/// Weapon crate probability (-100 to 100, signed). Payload +0x0C, file 0x11.
|
| 254 |
+
pub weapon_crate_probability: i8,
|
| 255 |
+
/// Donor cards enabled. Payload +0x0D, file 0x12.
|
| 256 |
+
pub donor_cards: bool,
|
| 257 |
+
/// Health crate probability (-100 to 100, signed). Payload +0x0E, file 0x13.
|
| 258 |
+
pub health_crate_probability: i8,
|
| 259 |
+
/// Energy gained from health crates. Payload +0x0F, file 0x14.
|
| 260 |
+
pub health_crate_energy: u8,
|
| 261 |
+
/// Utility crate probability (-100 to 100, signed). Payload +0x10, file 0x15.
|
| 262 |
+
pub utility_crate_probability: i8,
|
| 263 |
+
/// Hazardous object type bitmask. Payload +0x11, file 0x16.
|
| 264 |
+
pub hazardous_object_types: u8,
|
| 265 |
+
/// Mine fuse delay in seconds. 0x80+ = random. Payload +0x12, file 0x17.
|
| 266 |
+
pub mine_delay: i8,
|
| 267 |
+
/// Dud mines (some mines don't explode). Payload +0x13, file 0x18.
|
| 268 |
+
pub dud_mines: bool,
|
| 269 |
+
/// Manual worm placement at start. Payload +0x14, file 0x19.
|
| 270 |
+
pub manual_worm_placement: bool,
|
| 271 |
+
/// Initial worm energy. 0=instant death. Payload +0x15, file 0x1A.
|
| 272 |
+
pub worm_energy: u8,
|
| 273 |
+
/// Turn time in seconds. Encoding varies by range. Payload +0x16, file 0x1B.
|
| 274 |
+
pub turn_time: u8,
|
| 275 |
+
/// Round time in minutes. 0=immediate SD. Payload +0x17, file 0x1C.
|
| 276 |
+
pub round_time: u8,
|
| 277 |
+
/// Number of round wins required to win match. Payload +0x18, file 0x1D.
|
| 278 |
+
pub number_of_wins: u8,
|
| 279 |
+
/// Blood color: false=pink, true=red. Payload +0x19, file 0x1E.
|
| 280 |
+
pub blood: bool,
|
| 281 |
+
/// Aqua Sheep mode. Payload +0x1A, file 0x1F.
|
| 282 |
+
pub aqua_sheep: bool,
|
| 283 |
+
/// Sheep Heaven mode. Payload +0x1B, file 0x20.
|
| 284 |
+
pub sheep_heaven: bool,
|
| 285 |
+
/// God Worms (infinite health). Payload +0x1C, file 0x21.
|
| 286 |
+
pub god_worms: bool,
|
| 287 |
+
/// Indestructible terrain. Payload +0x1D, file 0x22.
|
| 288 |
+
pub indestructible_land: bool,
|
| 289 |
+
/// Upgraded grenades. Payload +0x1E, file 0x23.
|
| 290 |
+
pub upgraded_grenade: bool,
|
| 291 |
+
/// Upgraded shotgun. Payload +0x1F, file 0x24.
|
| 292 |
+
pub upgraded_shotgun: bool,
|
| 293 |
+
/// Upgraded cluster bombs. Payload +0x20, file 0x25.
|
| 294 |
+
pub upgraded_clusters: bool,
|
| 295 |
+
/// Upgraded longbow. Payload +0x21, file 0x26.
|
| 296 |
+
pub upgraded_longbow: bool,
|
| 297 |
+
/// Team weapons enabled. Payload +0x22, file 0x27.
|
| 298 |
+
pub team_weapons: bool,
|
| 299 |
+
/// Super weapons enabled. Payload +0x23, file 0x28.
|
| 300 |
+
pub super_weapons: bool,
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
impl SchemeOptions {
|
| 304 |
+
/// Read options from the first 36 bytes of a payload slice.
|
| 305 |
+
pub fn from_bytes(b: &[u8]) -> Self {
|
| 306 |
+
Self {
|
| 307 |
+
hot_seat_delay: b[0],
|
| 308 |
+
retreat_time: b[1],
|
| 309 |
+
rope_retreat_time: b[2],
|
| 310 |
+
display_total_round_time: b[3] != 0,
|
| 311 |
+
automatic_replays: b[4] != 0,
|
| 312 |
+
fall_damage: b[5],
|
| 313 |
+
artillery_mode: b[6] != 0,
|
| 314 |
+
bounty_mode: b[7],
|
| 315 |
+
stockpiling: b[8],
|
| 316 |
+
worm_select: b[9],
|
| 317 |
+
sudden_death_event: b[10],
|
| 318 |
+
water_rise_rate: b[11],
|
| 319 |
+
weapon_crate_probability: b[12] as i8,
|
| 320 |
+
donor_cards: b[13] != 0,
|
| 321 |
+
health_crate_probability: b[14] as i8,
|
| 322 |
+
health_crate_energy: b[15],
|
| 323 |
+
utility_crate_probability: b[16] as i8,
|
| 324 |
+
hazardous_object_types: b[17],
|
| 325 |
+
mine_delay: b[18] as i8,
|
| 326 |
+
dud_mines: b[19] != 0,
|
| 327 |
+
manual_worm_placement: b[20] != 0,
|
| 328 |
+
worm_energy: b[21],
|
| 329 |
+
turn_time: b[22],
|
| 330 |
+
round_time: b[23],
|
| 331 |
+
number_of_wins: b[24],
|
| 332 |
+
blood: b[25] != 0,
|
| 333 |
+
aqua_sheep: b[26] != 0,
|
| 334 |
+
sheep_heaven: b[27] != 0,
|
| 335 |
+
god_worms: b[28] != 0,
|
| 336 |
+
indestructible_land: b[29] != 0,
|
| 337 |
+
upgraded_grenade: b[30] != 0,
|
| 338 |
+
upgraded_shotgun: b[31] != 0,
|
| 339 |
+
upgraded_clusters: b[32] != 0,
|
| 340 |
+
upgraded_longbow: b[33] != 0,
|
| 341 |
+
team_weapons: b[34] != 0,
|
| 342 |
+
super_weapons: b[35] != 0,
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
/// Serialize options back to 36 bytes.
|
| 347 |
+
pub fn to_bytes(&self) -> [u8; OPTIONS_SIZE] {
|
| 348 |
+
[
|
| 349 |
+
self.hot_seat_delay,
|
| 350 |
+
self.retreat_time,
|
| 351 |
+
self.rope_retreat_time,
|
| 352 |
+
self.display_total_round_time as u8,
|
| 353 |
+
self.automatic_replays as u8,
|
| 354 |
+
self.fall_damage,
|
| 355 |
+
self.artillery_mode as u8,
|
| 356 |
+
self.bounty_mode,
|
| 357 |
+
self.stockpiling,
|
| 358 |
+
self.worm_select,
|
| 359 |
+
self.sudden_death_event,
|
| 360 |
+
self.water_rise_rate,
|
| 361 |
+
self.weapon_crate_probability as u8,
|
| 362 |
+
self.donor_cards as u8,
|
| 363 |
+
self.health_crate_probability as u8,
|
| 364 |
+
self.health_crate_energy,
|
| 365 |
+
self.utility_crate_probability as u8,
|
| 366 |
+
self.hazardous_object_types,
|
| 367 |
+
self.mine_delay as u8,
|
| 368 |
+
self.dud_mines as u8,
|
| 369 |
+
self.manual_worm_placement as u8,
|
| 370 |
+
self.worm_energy,
|
| 371 |
+
self.turn_time,
|
| 372 |
+
self.round_time,
|
| 373 |
+
self.number_of_wins,
|
| 374 |
+
self.blood as u8,
|
| 375 |
+
self.aqua_sheep as u8,
|
| 376 |
+
self.sheep_heaven as u8,
|
| 377 |
+
self.god_worms as u8,
|
| 378 |
+
self.indestructible_land as u8,
|
| 379 |
+
self.upgraded_grenade as u8,
|
| 380 |
+
self.upgraded_shotgun as u8,
|
| 381 |
+
self.upgraded_clusters as u8,
|
| 382 |
+
self.upgraded_longbow as u8,
|
| 383 |
+
self.team_weapons as u8,
|
| 384 |
+
self.super_weapons as u8,
|
| 385 |
+
]
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/// Weapon order in .wsc scheme files (indices 0-63).
|
| 390 |
+
///
|
| 391 |
+
/// This order differs from the runtime `Weapon` enum. The scheme file uses
|
| 392 |
+
/// the UI panel order established by the original game.
|
| 393 |
+
///
|
| 394 |
+
/// Source: worms2d.info/Game_scheme_file
|
| 395 |
+
pub const SCHEME_WEAPON_ORDER: [&str; 64] = [
|
| 396 |
+
// V1 weapons (0-44)
|
| 397 |
+
"Bazooka", // 0
|
| 398 |
+
"Homing Missile", // 1
|
| 399 |
+
"Mortar", // 2
|
| 400 |
+
"Grenade", // 3
|
| 401 |
+
"Cluster Bomb", // 4
|
| 402 |
+
"Skunk", // 5
|
| 403 |
+
"Petrol Bomb", // 6
|
| 404 |
+
"Banana Bomb", // 7
|
| 405 |
+
"Handgun", // 8
|
| 406 |
+
"Shotgun", // 9
|
| 407 |
+
"Uzi", // 10
|
| 408 |
+
"Minigun", // 11
|
| 409 |
+
"Longbow", // 12
|
| 410 |
+
"Airstrike", // 13
|
| 411 |
+
"Napalm Strike", // 14
|
| 412 |
+
"Mine", // 15
|
| 413 |
+
"Fire Punch", // 16
|
| 414 |
+
"Dragon Ball", // 17
|
| 415 |
+
"Kamikaze", // 18
|
| 416 |
+
"Prod", // 19
|
| 417 |
+
"Battle Axe", // 20
|
| 418 |
+
"Blowtorch", // 21
|
| 419 |
+
"Pneumatic Drill", // 22
|
| 420 |
+
"Girder", // 23
|
| 421 |
+
"Ninja Rope", // 24
|
| 422 |
+
"Parachute", // 25
|
| 423 |
+
"Bungee", // 26
|
| 424 |
+
"Teleport", // 27
|
| 425 |
+
"Dynamite", // 28
|
| 426 |
+
"Sheep", // 29
|
| 427 |
+
"Baseball Bat", // 30
|
| 428 |
+
"Flame Thrower", // 31
|
| 429 |
+
"Homing Pigeon", // 32
|
| 430 |
+
"Mad Cow", // 33
|
| 431 |
+
"Holy Hand Grenade", // 34
|
| 432 |
+
"Old Woman", // 35
|
| 433 |
+
"Sheep Launcher", // 36
|
| 434 |
+
"Super Sheep", // 37
|
| 435 |
+
"Mole Bomb", // 38
|
| 436 |
+
"Jet Pack", // 39
|
| 437 |
+
"Low Gravity", // 40
|
| 438 |
+
"Laser Sight", // 41
|
| 439 |
+
"Fast Walk", // 42
|
| 440 |
+
"Invisibility", // 43
|
| 441 |
+
"Damage x2", // 44
|
| 442 |
+
// V2 super weapons (45-63)
|
| 443 |
+
"Freeze", // 45
|
| 444 |
+
"Super Banana Bomb", // 46
|
| 445 |
+
"Mine Strike", // 47
|
| 446 |
+
"Girder Starter Pack", // 48
|
| 447 |
+
"Earthquake", // 49
|
| 448 |
+
"Scales of Justice", // 50
|
| 449 |
+
"Ming Vase", // 51
|
| 450 |
+
"Mike's Carpet Bomb", // 52
|
| 451 |
+
"Patsy's Magic Bullet", // 53
|
| 452 |
+
"Indian Nuclear Test", // 54
|
| 453 |
+
"Select Worm", // 55
|
| 454 |
+
"Salvation Army", // 56
|
| 455 |
+
"Mole Squadron", // 57
|
| 456 |
+
"MB Bomb", // 58
|
| 457 |
+
"Concrete Donkey", // 59
|
| 458 |
+
"Suicide Bomber", // 60
|
| 459 |
+
"Sheep Strike", // 61
|
| 460 |
+
"Mail Strike", // 62
|
| 461 |
+
"Armageddon", // 63
|
| 462 |
+
];
|
| 463 |
+
|
| 464 |
+
/// V3 extended options (110 bytes, payload offset 0x124).
|
| 465 |
+
///
|
| 466 |
+
/// Present in V3 schemes; for V1/V2, the game fills this region from
|
| 467 |
+
/// ROM defaults at 0x649AB8.
|
| 468 |
+
///
|
| 469 |
+
/// Many fields use fixed-point 16.16 format (`Fixed`), tri-state (0/1/0x80),
|
| 470 |
+
/// or small enums. See field docs for valid ranges.
|
| 471 |
+
///
|
| 472 |
+
/// Source: worms2d.info/Game_scheme_file, Ghidra FUN_004d5110 validation.
|
| 473 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 474 |
+
pub struct ExtendedOptions {
|
| 475 |
+
/// Data version (currently 0). Offset +0x00.
|
| 476 |
+
pub data_version: u32,
|
| 477 |
+
/// Constant wind enabled. Offset +0x04.
|
| 478 |
+
pub constant_wind: bool,
|
| 479 |
+
/// Wind strength (signed). Offset +0x05.
|
| 480 |
+
pub wind: i16,
|
| 481 |
+
/// Wind bias. Offset +0x07.
|
| 482 |
+
pub wind_bias: u8,
|
| 483 |
+
/// Gravity (fixed 16.16). Offset +0x08. Default 0xF5C2.
|
| 484 |
+
pub gravity: i32,
|
| 485 |
+
/// Terrain friction (fixed 16.16). Offset +0x0C. Default 0xF5C2.
|
| 486 |
+
pub terrain_friction: i32,
|
| 487 |
+
/// Rope knocking. 0xFF = use default. Offset +0x10.
|
| 488 |
+
pub rope_knocking: u8,
|
| 489 |
+
/// Blood level. 0xFF = use default. Offset +0x11.
|
| 490 |
+
pub blood_level: u8,
|
| 491 |
+
/// Unrestrict rope. Offset +0x12.
|
| 492 |
+
pub unrestrict_rope: bool,
|
| 493 |
+
/// Auto-place worms by ally. Offset +0x13.
|
| 494 |
+
pub auto_place_worms_by_ally: bool,
|
| 495 |
+
/// No-crate probability. 0xFF = ignore. Offset +0x14.
|
| 496 |
+
pub no_crate_probability: u8,
|
| 497 |
+
/// Maximum crate count. Offset +0x15.
|
| 498 |
+
pub max_crate_count: u16,
|
| 499 |
+
/// Sudden death disables worm select. Offset +0x17.
|
| 500 |
+
pub sd_disables_worm_select: bool,
|
| 501 |
+
/// Sudden death worm damage per turn. Offset +0x18.
|
| 502 |
+
pub sd_worm_damage_per_turn: u8,
|
| 503 |
+
/// Phased worms (allied). 0-3. Offset +0x19.
|
| 504 |
+
pub phased_worms_allied: u8,
|
| 505 |
+
/// Phased worms (enemy). 0-3. Offset +0x1A.
|
| 506 |
+
pub phased_worms_enemy: u8,
|
| 507 |
+
/// Circular aim. Offset +0x1B.
|
| 508 |
+
pub circular_aim: bool,
|
| 509 |
+
/// Anti-lock aim. Offset +0x1C.
|
| 510 |
+
pub anti_lock_aim: bool,
|
| 511 |
+
/// Anti-lock power. Offset +0x1D.
|
| 512 |
+
pub anti_lock_power: bool,
|
| 513 |
+
/// Worm selection doesn't end hot seat. Offset +0x1E.
|
| 514 |
+
pub worm_select_no_end_hot_seat: bool,
|
| 515 |
+
/// Worm selection is never cancelled. Offset +0x1F.
|
| 516 |
+
pub worm_select_never_cancelled: bool,
|
| 517 |
+
/// Batty rope. Offset +0x20.
|
| 518 |
+
pub batty_rope: bool,
|
| 519 |
+
/// Rope-roll drops. 0-2. Offset +0x21.
|
| 520 |
+
pub rope_roll_drops: u8,
|
| 521 |
+
/// X-impact loss of control. 0 or 0xFF. Offset +0x22.
|
| 522 |
+
pub x_impact_loss_of_control: u8,
|
| 523 |
+
/// Keep control after bumping head. Offset +0x23.
|
| 524 |
+
pub keep_control_bump_head: bool,
|
| 525 |
+
/// Keep control after skimming. 0-2. Offset +0x24.
|
| 526 |
+
pub keep_control_skimming: u8,
|
| 527 |
+
/// Fall damage triggered by explosions. Offset +0x25.
|
| 528 |
+
pub explosion_fall_damage: bool,
|
| 529 |
+
/// Explosions push all objects. Tri-state: 0/1/0x80. Offset +0x26.
|
| 530 |
+
pub explosions_push_all: u8,
|
| 531 |
+
/// Undetermined crates. Tri-state. Offset +0x27.
|
| 532 |
+
pub undetermined_crates: u8,
|
| 533 |
+
/// Undetermined fuses. Tri-state. Offset +0x28.
|
| 534 |
+
pub undetermined_fuses: u8,
|
| 535 |
+
/// Pause timer while firing. Offset +0x29.
|
| 536 |
+
pub pause_timer_while_firing: bool,
|
| 537 |
+
/// Loss of control doesn't end turn. Offset +0x2A.
|
| 538 |
+
pub loss_of_control_no_end_turn: bool,
|
| 539 |
+
/// Weapon use doesn't end turn. Offset +0x2B.
|
| 540 |
+
pub weapon_use_no_end_turn: bool,
|
| 541 |
+
/// Above option doesn't block any weapons. Offset +0x2C.
|
| 542 |
+
pub weapon_use_no_block: bool,
|
| 543 |
+
/// Pneumatic drill imparts velocity. Tri-state. Offset +0x2D.
|
| 544 |
+
pub drill_imparts_velocity: u8,
|
| 545 |
+
/// Girder radius assist. Offset +0x2E.
|
| 546 |
+
pub girder_radius_assist: bool,
|
| 547 |
+
/// Petrol turn decay (fixed 16.16 fractional part). Offset +0x2F.
|
| 548 |
+
pub petrol_turn_decay: u16,
|
| 549 |
+
/// Petrol touch decay. Offset +0x31.
|
| 550 |
+
pub petrol_touch_decay: u8,
|
| 551 |
+
/// Maximum flamelet count. Offset +0x32.
|
| 552 |
+
pub max_flamelet_count: u16,
|
| 553 |
+
/// Maximum projectile speed (fixed 16.16). Offset +0x34.
|
| 554 |
+
pub max_projectile_speed: i32,
|
| 555 |
+
/// Maximum rope speed (fixed 16.16). Offset +0x38.
|
| 556 |
+
pub max_rope_speed: i32,
|
| 557 |
+
/// Maximum jet pack speed (fixed 16.16). Offset +0x3C.
|
| 558 |
+
pub max_jet_pack_speed: i32,
|
| 559 |
+
/// Game engine speed (fixed 16.16). Offset +0x40.
|
| 560 |
+
pub game_engine_speed: i32,
|
| 561 |
+
/// Indian rope glitch. Tri-state. Offset +0x44.
|
| 562 |
+
pub indian_rope_glitch: u8,
|
| 563 |
+
/// Herd-doubling glitch. Tri-state. Offset +0x45.
|
| 564 |
+
pub herd_doubling_glitch: u8,
|
| 565 |
+
/// Jet pack bungee glitch. Offset +0x46.
|
| 566 |
+
pub jet_pack_bungee_glitch: bool,
|
| 567 |
+
/// Angle cheat glitch. Offset +0x47.
|
| 568 |
+
pub angle_cheat_glitch: bool,
|
| 569 |
+
/// Glide glitch. Offset +0x48.
|
| 570 |
+
pub glide_glitch: bool,
|
| 571 |
+
/// Skipwalking. 0/1/0xFF. Offset +0x49.
|
| 572 |
+
pub skipwalking: i8,
|
| 573 |
+
/// Block roofing. 0-2. Offset +0x4A.
|
| 574 |
+
pub block_roofing: u8,
|
| 575 |
+
/// Floating weapon glitch. Offset +0x4B.
|
| 576 |
+
pub floating_weapon_glitch: bool,
|
| 577 |
+
/// RubberWorm bounciness (fixed 16.16). Offset +0x4C.
|
| 578 |
+
pub rw_bounciness: i32,
|
| 579 |
+
/// RubberWorm air viscosity (fixed 16.16). Offset +0x50.
|
| 580 |
+
pub rw_air_viscosity: i32,
|
| 581 |
+
/// RW air viscosity applies to worms. Offset +0x54.
|
| 582 |
+
pub rw_air_viscosity_worms: bool,
|
| 583 |
+
/// RubberWorm wind influence (fixed 16.16). Offset +0x55.
|
| 584 |
+
pub rw_wind_influence: u32,
|
| 585 |
+
/// RW wind influence applies to worms. Offset +0x59.
|
| 586 |
+
pub rw_wind_influence_worms: bool,
|
| 587 |
+
/// RW gravity type. 0-3. Offset +0x5A.
|
| 588 |
+
pub rw_gravity_type: u8,
|
| 589 |
+
/// RW gravity strength (fixed 16.16). Offset +0x5B.
|
| 590 |
+
pub rw_gravity_strength: i32,
|
| 591 |
+
/// RW crate rate. Offset +0x5F.
|
| 592 |
+
pub rw_crate_rate: u8,
|
| 593 |
+
/// RW crate shower. Offset +0x60.
|
| 594 |
+
pub rw_crate_shower: bool,
|
| 595 |
+
/// RW anti-sink. Offset +0x61.
|
| 596 |
+
pub rw_anti_sink: bool,
|
| 597 |
+
/// RW remember weapons. Offset +0x62.
|
| 598 |
+
pub rw_remember_weapons: bool,
|
| 599 |
+
/// RW extended fuses/herds. Offset +0x63.
|
| 600 |
+
pub rw_extended_fuses: bool,
|
| 601 |
+
/// RW anti-lock aim. Offset +0x64.
|
| 602 |
+
pub rw_anti_lock_aim: bool,
|
| 603 |
+
/// Terrain overlap phasing glitch. Tri-state. Offset +0x65.
|
| 604 |
+
pub terrain_overlap_glitch: u8,
|
| 605 |
+
/// Fractional round timer. Offset +0x66.
|
| 606 |
+
pub fractional_round_timer: bool,
|
| 607 |
+
/// Automatic end-of-turn retreat. Offset +0x67.
|
| 608 |
+
pub auto_retreat: bool,
|
| 609 |
+
/// Health crates cure poison. 0/1/2/0xFF. Offset +0x68.
|
| 610 |
+
pub health_crates_cure_poison: i8,
|
| 611 |
+
/// RW Kaos mod. 0-5. Offset +0x69.
|
| 612 |
+
pub rw_kaos_mod: u8,
|
| 613 |
+
/// Sheep Heaven's Gate bitmask. Offset +0x6A.
|
| 614 |
+
pub sheep_heavens_gate: u8,
|
| 615 |
+
/// Conserve instant utilities. Offset +0x6B.
|
| 616 |
+
pub conserve_instant_utilities: bool,
|
| 617 |
+
/// Expedite instant utilities. Offset +0x6C.
|
| 618 |
+
pub expedite_instant_utilities: bool,
|
| 619 |
+
/// Double time stack limit. Offset +0x6D.
|
| 620 |
+
pub double_time_stack_limit: u8,
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
/// Helper: read a little-endian u16 from a byte slice.
|
| 624 |
+
fn read_u16_le(b: &[u8], offset: usize) -> u16 {
|
| 625 |
+
u16::from_le_bytes([b[offset], b[offset + 1]])
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/// Helper: read a little-endian i32 from a byte slice.
|
| 629 |
+
fn read_i32_le(b: &[u8], offset: usize) -> i32 {
|
| 630 |
+
i32::from_le_bytes([b[offset], b[offset + 1], b[offset + 2], b[offset + 3]])
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
/// Helper: read a little-endian u32 from a byte slice.
|
| 634 |
+
fn read_u32_le(b: &[u8], offset: usize) -> u32 {
|
| 635 |
+
u32::from_le_bytes([b[offset], b[offset + 1], b[offset + 2], b[offset + 3]])
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
impl ExtendedOptions {
|
| 639 |
+
/// Read extended options from a 110-byte slice (payload offset 0x124).
|
| 640 |
+
pub fn from_bytes(b: &[u8]) -> Self {
|
| 641 |
+
Self {
|
| 642 |
+
data_version: read_u32_le(b, 0x00),
|
| 643 |
+
constant_wind: b[0x04] != 0,
|
| 644 |
+
wind: read_u16_le(b, 0x05) as i16,
|
| 645 |
+
wind_bias: b[0x07],
|
| 646 |
+
gravity: read_i32_le(b, 0x08),
|
| 647 |
+
terrain_friction: read_i32_le(b, 0x0C),
|
| 648 |
+
rope_knocking: b[0x10],
|
| 649 |
+
blood_level: b[0x11],
|
| 650 |
+
unrestrict_rope: b[0x12] != 0,
|
| 651 |
+
auto_place_worms_by_ally: b[0x13] != 0,
|
| 652 |
+
no_crate_probability: b[0x14],
|
| 653 |
+
max_crate_count: read_u16_le(b, 0x15),
|
| 654 |
+
sd_disables_worm_select: b[0x17] != 0,
|
| 655 |
+
sd_worm_damage_per_turn: b[0x18],
|
| 656 |
+
phased_worms_allied: b[0x19],
|
| 657 |
+
phased_worms_enemy: b[0x1A],
|
| 658 |
+
circular_aim: b[0x1B] != 0,
|
| 659 |
+
anti_lock_aim: b[0x1C] != 0,
|
| 660 |
+
anti_lock_power: b[0x1D] != 0,
|
| 661 |
+
worm_select_no_end_hot_seat: b[0x1E] != 0,
|
| 662 |
+
worm_select_never_cancelled: b[0x1F] != 0,
|
| 663 |
+
batty_rope: b[0x20] != 0,
|
| 664 |
+
rope_roll_drops: b[0x21],
|
| 665 |
+
x_impact_loss_of_control: b[0x22],
|
| 666 |
+
keep_control_bump_head: b[0x23] != 0,
|
| 667 |
+
keep_control_skimming: b[0x24],
|
| 668 |
+
explosion_fall_damage: b[0x25] != 0,
|
| 669 |
+
explosions_push_all: b[0x26],
|
| 670 |
+
undetermined_crates: b[0x27],
|
| 671 |
+
undetermined_fuses: b[0x28],
|
| 672 |
+
pause_timer_while_firing: b[0x29] != 0,
|
| 673 |
+
loss_of_control_no_end_turn: b[0x2A] != 0,
|
| 674 |
+
weapon_use_no_end_turn: b[0x2B] != 0,
|
| 675 |
+
weapon_use_no_block: b[0x2C] != 0,
|
| 676 |
+
drill_imparts_velocity: b[0x2D],
|
| 677 |
+
girder_radius_assist: b[0x2E] != 0,
|
| 678 |
+
petrol_turn_decay: read_u16_le(b, 0x2F),
|
| 679 |
+
petrol_touch_decay: b[0x31],
|
| 680 |
+
max_flamelet_count: read_u16_le(b, 0x32),
|
| 681 |
+
max_projectile_speed: read_i32_le(b, 0x34),
|
| 682 |
+
max_rope_speed: read_i32_le(b, 0x38),
|
| 683 |
+
max_jet_pack_speed: read_i32_le(b, 0x3C),
|
| 684 |
+
game_engine_speed: read_i32_le(b, 0x40),
|
| 685 |
+
indian_rope_glitch: b[0x44],
|
| 686 |
+
herd_doubling_glitch: b[0x45],
|
| 687 |
+
jet_pack_bungee_glitch: b[0x46] != 0,
|
| 688 |
+
angle_cheat_glitch: b[0x47] != 0,
|
| 689 |
+
glide_glitch: b[0x48] != 0,
|
| 690 |
+
skipwalking: b[0x49] as i8,
|
| 691 |
+
block_roofing: b[0x4A],
|
| 692 |
+
floating_weapon_glitch: b[0x4B] != 0,
|
| 693 |
+
rw_bounciness: read_i32_le(b, 0x4C),
|
| 694 |
+
rw_air_viscosity: read_i32_le(b, 0x50),
|
| 695 |
+
rw_air_viscosity_worms: b[0x54] != 0,
|
| 696 |
+
rw_wind_influence: read_u32_le(b, 0x55),
|
| 697 |
+
rw_wind_influence_worms: b[0x59] != 0,
|
| 698 |
+
rw_gravity_type: b[0x5A],
|
| 699 |
+
rw_gravity_strength: read_i32_le(b, 0x5B),
|
| 700 |
+
rw_crate_rate: b[0x5F],
|
| 701 |
+
rw_crate_shower: b[0x60] != 0,
|
| 702 |
+
rw_anti_sink: b[0x61] != 0,
|
| 703 |
+
rw_remember_weapons: b[0x62] != 0,
|
| 704 |
+
rw_extended_fuses: b[0x63] != 0,
|
| 705 |
+
rw_anti_lock_aim: b[0x64] != 0,
|
| 706 |
+
terrain_overlap_glitch: b[0x65],
|
| 707 |
+
fractional_round_timer: b[0x66] != 0,
|
| 708 |
+
auto_retreat: b[0x67] != 0,
|
| 709 |
+
health_crates_cure_poison: b[0x68] as i8,
|
| 710 |
+
rw_kaos_mod: b[0x69],
|
| 711 |
+
sheep_heavens_gate: b[0x6A],
|
| 712 |
+
conserve_instant_utilities: b[0x6B] != 0,
|
| 713 |
+
expedite_instant_utilities: b[0x6C] != 0,
|
| 714 |
+
double_time_stack_limit: b[0x6D],
|
| 715 |
+
}
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
/// Serialize extended options to 110 bytes.
|
| 719 |
+
pub fn to_bytes(&self) -> [u8; EXTENDED_OPTIONS_SIZE] {
|
| 720 |
+
let mut b = [0u8; EXTENDED_OPTIONS_SIZE];
|
| 721 |
+
b[0x00..0x04].copy_from_slice(&self.data_version.to_le_bytes());
|
| 722 |
+
b[0x04] = self.constant_wind as u8;
|
| 723 |
+
b[0x05..0x07].copy_from_slice(&self.wind.to_le_bytes());
|
| 724 |
+
b[0x07] = self.wind_bias;
|
| 725 |
+
b[0x08..0x0C].copy_from_slice(&self.gravity.to_le_bytes());
|
| 726 |
+
b[0x0C..0x10].copy_from_slice(&self.terrain_friction.to_le_bytes());
|
| 727 |
+
b[0x10] = self.rope_knocking;
|
| 728 |
+
b[0x11] = self.blood_level;
|
| 729 |
+
b[0x12] = self.unrestrict_rope as u8;
|
| 730 |
+
b[0x13] = self.auto_place_worms_by_ally as u8;
|
| 731 |
+
b[0x14] = self.no_crate_probability;
|
| 732 |
+
b[0x15..0x17].copy_from_slice(&self.max_crate_count.to_le_bytes());
|
| 733 |
+
b[0x17] = self.sd_disables_worm_select as u8;
|
| 734 |
+
b[0x18] = self.sd_worm_damage_per_turn;
|
| 735 |
+
b[0x19] = self.phased_worms_allied;
|
| 736 |
+
b[0x1A] = self.phased_worms_enemy;
|
| 737 |
+
b[0x1B] = self.circular_aim as u8;
|
| 738 |
+
b[0x1C] = self.anti_lock_aim as u8;
|
| 739 |
+
b[0x1D] = self.anti_lock_power as u8;
|
| 740 |
+
b[0x1E] = self.worm_select_no_end_hot_seat as u8;
|
| 741 |
+
b[0x1F] = self.worm_select_never_cancelled as u8;
|
| 742 |
+
b[0x20] = self.batty_rope as u8;
|
| 743 |
+
b[0x21] = self.rope_roll_drops;
|
| 744 |
+
b[0x22] = self.x_impact_loss_of_control;
|
| 745 |
+
b[0x23] = self.keep_control_bump_head as u8;
|
| 746 |
+
b[0x24] = self.keep_control_skimming;
|
| 747 |
+
b[0x25] = self.explosion_fall_damage as u8;
|
| 748 |
+
b[0x26] = self.explosions_push_all;
|
| 749 |
+
b[0x27] = self.undetermined_crates;
|
| 750 |
+
b[0x28] = self.undetermined_fuses;
|
| 751 |
+
b[0x29] = self.pause_timer_while_firing as u8;
|
| 752 |
+
b[0x2A] = self.loss_of_control_no_end_turn as u8;
|
| 753 |
+
b[0x2B] = self.weapon_use_no_end_turn as u8;
|
| 754 |
+
b[0x2C] = self.weapon_use_no_block as u8;
|
| 755 |
+
b[0x2D] = self.drill_imparts_velocity;
|
| 756 |
+
b[0x2E] = self.girder_radius_assist as u8;
|
| 757 |
+
b[0x2F..0x31].copy_from_slice(&self.petrol_turn_decay.to_le_bytes());
|
| 758 |
+
b[0x31] = self.petrol_touch_decay;
|
| 759 |
+
b[0x32..0x34].copy_from_slice(&self.max_flamelet_count.to_le_bytes());
|
| 760 |
+
b[0x34..0x38].copy_from_slice(&self.max_projectile_speed.to_le_bytes());
|
| 761 |
+
b[0x38..0x3C].copy_from_slice(&self.max_rope_speed.to_le_bytes());
|
| 762 |
+
b[0x3C..0x40].copy_from_slice(&self.max_jet_pack_speed.to_le_bytes());
|
| 763 |
+
b[0x40..0x44].copy_from_slice(&self.game_engine_speed.to_le_bytes());
|
| 764 |
+
b[0x44] = self.indian_rope_glitch;
|
| 765 |
+
b[0x45] = self.herd_doubling_glitch;
|
| 766 |
+
b[0x46] = self.jet_pack_bungee_glitch as u8;
|
| 767 |
+
b[0x47] = self.angle_cheat_glitch as u8;
|
| 768 |
+
b[0x48] = self.glide_glitch as u8;
|
| 769 |
+
b[0x49] = self.skipwalking as u8;
|
| 770 |
+
b[0x4A] = self.block_roofing;
|
| 771 |
+
b[0x4B] = self.floating_weapon_glitch as u8;
|
| 772 |
+
b[0x4C..0x50].copy_from_slice(&self.rw_bounciness.to_le_bytes());
|
| 773 |
+
b[0x50..0x54].copy_from_slice(&self.rw_air_viscosity.to_le_bytes());
|
| 774 |
+
b[0x54] = self.rw_air_viscosity_worms as u8;
|
| 775 |
+
b[0x55..0x59].copy_from_slice(&self.rw_wind_influence.to_le_bytes());
|
| 776 |
+
b[0x59] = self.rw_wind_influence_worms as u8;
|
| 777 |
+
b[0x5A] = self.rw_gravity_type;
|
| 778 |
+
b[0x5B..0x5F].copy_from_slice(&self.rw_gravity_strength.to_le_bytes());
|
| 779 |
+
b[0x5F] = self.rw_crate_rate;
|
| 780 |
+
b[0x60] = self.rw_crate_shower as u8;
|
| 781 |
+
b[0x61] = self.rw_anti_sink as u8;
|
| 782 |
+
b[0x62] = self.rw_remember_weapons as u8;
|
| 783 |
+
b[0x63] = self.rw_extended_fuses as u8;
|
| 784 |
+
b[0x64] = self.rw_anti_lock_aim as u8;
|
| 785 |
+
b[0x65] = self.terrain_overlap_glitch;
|
| 786 |
+
b[0x66] = self.fractional_round_timer as u8;
|
| 787 |
+
b[0x67] = self.auto_retreat as u8;
|
| 788 |
+
b[0x68] = self.health_crates_cure_poison as u8;
|
| 789 |
+
b[0x69] = self.rw_kaos_mod;
|
| 790 |
+
b[0x6A] = self.sheep_heavens_gate;
|
| 791 |
+
b[0x6B] = self.conserve_instant_utilities as u8;
|
| 792 |
+
b[0x6C] = self.expedite_instant_utilities as u8;
|
| 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).
|
| 799 |
+
///
|
| 800 |
+
/// Applied to V1/V2 schemes at struct+0x138 (payload+0x124).
|
| 801 |
+
/// Dumped directly from the binary — tri-state fields use 0x80 = "engine default".
|
| 802 |
+
pub const EXTENDED_OPTIONS_DEFAULTS: [u8; EXTENDED_OPTIONS_SIZE] = [
|
| 803 |
+
// +0x00 +0x04
|
| 804 |
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x0F, // data_version, const_wind, wind=100, bias=15
|
| 805 |
+
0x70, 0x3D, 0x00, 0x00, 0xC2, 0xF5, 0x00, 0x00, // gravity=0x3D70, friction=0xF5C2
|
| 806 |
+
// +0x10 +0x14
|
| 807 |
+
0xFF, 0xFF, 0x00, 0x01, 0x05, 0x00, 0x00, 0x00, // rope_knock=FF, blood=FF, unrestrict=0, ally=1, nocrate=5
|
| 808 |
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sd, phased, circular, anti_lock...
|
| 809 |
+
// +0x20 +0x24
|
| 810 |
+
0x00, 0x00, 0x80, 0x80, 0x80, 0x01, 0x00, 0x00, // batty=0, rope_roll=0, x_impact=80, bump=80, skim=80, expl_fall=1
|
| 811 |
+
0x00, 0x80, 0x00, 0x32, 0x33, 0x1E, 0xC8, 0x00, // push_all=0, timer=80, loc=0, skim2=50... petrol_decay, touch=200
|
| 812 |
+
// +0x30 +0x34
|
| 813 |
+
0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x10, 0x00, // flamelet=0, proj_speed=0x200000
|
| 814 |
+
0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, // rope_speed=0x100000, jet_speed=0x50000, engine (low)
|
| 815 |
+
// +0x40 +0x44
|
| 816 |
+
0x80, 0x80, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, // engine (high), rope_glitch, herd, jetbungee, angle, glide, skip
|
| 817 |
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // block, float, rw_bounce...
|
| 818 |
+
// +0x50 +0x54
|
| 819 |
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rw_air_visc, rw_air_worms, rw_wind...
|
| 820 |
+
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rw_wind_worms, rw_grav_type=1, rw_grav_str=0
|
| 821 |
+
// +0x60 +0x64
|
| 822 |
+
0x00, 0x80, 0x00, 0x00, 0x01, 0x00, 0x07, 0x00, // rw_crate_rate=0, shower=0x80, sink=0, remember=0, fuses=1, lock=0, overlap=7
|
| 823 |
+
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // frac_timer=0, retreat=1, cure_poison=0, kaos=0, sheep_gate=0, conserve=0
|
| 824 |
+
];
|
| 825 |
+
|
| 826 |
+
// === Typed accessors on SchemeFile ===
|
| 827 |
+
|
| 828 |
+
impl SchemeFile {
|
| 829 |
+
/// Get the game options from the payload.
|
| 830 |
+
pub fn options(&self) -> SchemeOptions {
|
| 831 |
+
SchemeOptions::from_bytes(&self.payload[OPTIONS_OFFSET..])
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
/// Get weapon settings for a weapon by scheme index (0-63).
|
| 835 |
+
///
|
| 836 |
+
/// Indices 0-44 are V1 weapons, 45-63 are V2 super weapons.
|
| 837 |
+
/// Returns `None` if the index is out of range for this scheme version.
|
| 838 |
+
pub fn weapon(&self, scheme_index: usize) -> Option<WeaponSettings> {
|
| 839 |
+
if scheme_index >= WEAPONS_TOTAL_COUNT {
|
| 840 |
+
return None;
|
| 841 |
+
}
|
| 842 |
+
// V1 schemes only have weapons 0-44
|
| 843 |
+
if scheme_index >= WEAPONS_V1_COUNT && self.version == SchemeVersion::V1 {
|
| 844 |
+
return None;
|
| 845 |
+
}
|
| 846 |
+
let offset = WEAPONS_V1_OFFSET + scheme_index * WEAPON_ENTRY_SIZE;
|
| 847 |
+
Some(WeaponSettings::from_bytes(&self.payload[offset..]))
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
/// Get extended options (V3 only). Returns `None` for V1/V2 schemes.
|
| 851 |
+
pub fn extended_options(&self) -> Option<ExtendedOptions> {
|
| 852 |
+
if self.payload.len() > EXTENDED_OPTIONS_OFFSET {
|
| 853 |
+
Some(ExtendedOptions::from_bytes(
|
| 854 |
+
&self.payload[EXTENDED_OPTIONS_OFFSET..],
|
| 855 |
+
))
|
| 856 |
+
} else {
|
| 857 |
+
None
|
| 858 |
+
}
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
/// Get extended options, falling back to ROM defaults for V1/V2 schemes.
|
| 862 |
+
pub fn extended_options_or_defaults(&self) -> ExtendedOptions {
|
| 863 |
+
self.extended_options()
|
| 864 |
+
.unwrap_or_else(|| ExtendedOptions::from_bytes(&EXTENDED_OPTIONS_DEFAULTS))
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
/// Number of weapons available in this scheme version.
|
| 868 |
+
pub fn weapon_count(&self) -> usize {
|
| 869 |
+
match self.version {
|
| 870 |
+
SchemeVersion::V1 => WEAPONS_V1_COUNT,
|
| 871 |
+
_ => WEAPONS_TOTAL_COUNT,
|
| 872 |
+
}
|
| 873 |
+
}
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
/// Error type for file-based scheme operations.
|
| 877 |
#[derive(Debug)]
|
| 878 |
pub enum SchemeFileError {
|
crates/openwa-types/tests/scheme_parse.rs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
use std::path::Path;
|
| 2 |
|
| 3 |
-
use openwa_types::scheme::{
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
|
| 6 |
|
|
@@ -54,6 +56,127 @@ fn roundtrip_shopping() {
|
|
| 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() {
|
|
|
|
| 1 |
use std::path::Path;
|
| 2 |
|
| 3 |
+
use openwa_types::scheme::{
|
| 4 |
+
SchemeFile, SchemeVersion, SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, WEAPONS_V1_COUNT,
|
| 5 |
+
};
|
| 6 |
|
| 7 |
const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
|
| 8 |
|
|
|
|
| 56 |
assert_eq!(scheme.to_bytes(), original);
|
| 57 |
}
|
| 58 |
|
| 59 |
+
// === Typed field accessor tests ===
|
| 60 |
+
|
| 61 |
+
#[test]
|
| 62 |
+
fn beginner_options() {
|
| 63 |
+
let path = Path::new(FIXTURES).join("beginner.wsc");
|
| 64 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 65 |
+
let opts = scheme.options();
|
| 66 |
+
|
| 67 |
+
// Verified against hex dump of beginner.wsc payload bytes
|
| 68 |
+
assert_eq!(opts.hot_seat_delay, 10); // 0x0A
|
| 69 |
+
assert_eq!(opts.retreat_time, 5);
|
| 70 |
+
assert_eq!(opts.rope_retreat_time, 5);
|
| 71 |
+
assert_eq!(opts.display_total_round_time, true);
|
| 72 |
+
assert_eq!(opts.automatic_replays, true);
|
| 73 |
+
assert_eq!(opts.fall_damage, 0);
|
| 74 |
+
assert_eq!(opts.artillery_mode, false);
|
| 75 |
+
assert_eq!(opts.bounty_mode, 0);
|
| 76 |
+
assert_eq!(opts.stockpiling, 1); // On
|
| 77 |
+
assert_eq!(opts.worm_select, 1); // Manual
|
| 78 |
+
assert_eq!(opts.sudden_death_event, 3); // Nothing
|
| 79 |
+
assert_eq!(opts.worm_energy, 150); // 0x96
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
#[test]
|
| 83 |
+
fn classic_options() {
|
| 84 |
+
let path = Path::new(FIXTURES).join("classic.wsc");
|
| 85 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 86 |
+
let opts = scheme.options();
|
| 87 |
+
|
| 88 |
+
// Verified against hex dump: 05 05 05 00 01 01 00 00 00 01 02 ...
|
| 89 |
+
assert_eq!(opts.hot_seat_delay, 5);
|
| 90 |
+
assert_eq!(opts.retreat_time, 5);
|
| 91 |
+
assert_eq!(opts.rope_retreat_time, 5);
|
| 92 |
+
assert_eq!(opts.display_total_round_time, false);
|
| 93 |
+
assert_eq!(opts.automatic_replays, true);
|
| 94 |
+
assert_eq!(opts.fall_damage, 1);
|
| 95 |
+
assert_eq!(opts.artillery_mode, false);
|
| 96 |
+
assert_eq!(opts.stockpiling, 0); // Off
|
| 97 |
+
assert_eq!(opts.worm_select, 1); // Manual
|
| 98 |
+
assert_eq!(opts.worm_energy, 100); // hex: 0x64
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
#[test]
|
| 102 |
+
fn beginner_weapons() {
|
| 103 |
+
let path = Path::new(FIXTURES).join("beginner.wsc");
|
| 104 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 105 |
+
|
| 106 |
+
// Weapon 0 = Bazooka: at payload offset 36
|
| 107 |
+
// hex bytes at file offset 0x29: 0A 03 00 00
|
| 108 |
+
let bazooka = scheme.weapon(0).unwrap();
|
| 109 |
+
assert_eq!(bazooka.ammo, 10);
|
| 110 |
+
assert_eq!(bazooka.power, 3);
|
| 111 |
+
assert_eq!(bazooka.delay, 0);
|
| 112 |
+
assert_eq!(bazooka.crate_probability, 0);
|
| 113 |
+
|
| 114 |
+
// V2 weapon: index 45 = Freeze, at payload offset 36 + 45*4 = 216
|
| 115 |
+
// Should be available for V2 scheme
|
| 116 |
+
let freeze = scheme.weapon(45);
|
| 117 |
+
assert!(freeze.is_some());
|
| 118 |
+
|
| 119 |
+
// Total weapon count for V2
|
| 120 |
+
assert_eq!(scheme.weapon_count(), 64);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
#[test]
|
| 124 |
+
fn classic_weapons_v1_limits() {
|
| 125 |
+
let path = Path::new(FIXTURES).join("classic.wsc");
|
| 126 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 127 |
+
|
| 128 |
+
// V1 has 45 weapons
|
| 129 |
+
assert_eq!(scheme.weapon_count(), WEAPONS_V1_COUNT);
|
| 130 |
+
|
| 131 |
+
// Weapon 0 = Bazooka — classic has 10 ammo too
|
| 132 |
+
let bazooka = scheme.weapon(0).unwrap();
|
| 133 |
+
assert_eq!(bazooka.ammo, 10);
|
| 134 |
+
|
| 135 |
+
// V2 super weapon index should return None for V1 scheme
|
| 136 |
+
assert!(scheme.weapon(45).is_none());
|
| 137 |
+
assert!(scheme.weapon(63).is_none());
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
#[test]
|
| 141 |
+
fn weapon_out_of_range() {
|
| 142 |
+
let path = Path::new(FIXTURES).join("beginner.wsc");
|
| 143 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 144 |
+
assert!(scheme.weapon(64).is_none());
|
| 145 |
+
assert!(scheme.weapon(100).is_none());
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
#[test]
|
| 149 |
+
fn options_roundtrip() {
|
| 150 |
+
let path = Path::new(FIXTURES).join("beginner.wsc");
|
| 151 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 152 |
+
let opts = scheme.options();
|
| 153 |
+
let serialized = opts.to_bytes();
|
| 154 |
+
assert_eq!(&serialized[..], &scheme.payload[..36]);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
#[test]
|
| 158 |
+
fn v1_no_extended_options() {
|
| 159 |
+
let path = Path::new(FIXTURES).join("classic.wsc");
|
| 160 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 161 |
+
assert!(scheme.extended_options().is_none());
|
| 162 |
+
|
| 163 |
+
// But defaults should still work
|
| 164 |
+
let defaults = scheme.extended_options_or_defaults();
|
| 165 |
+
assert_eq!(defaults.data_version, 0);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
#[test]
|
| 169 |
+
fn v2_super_weapons_zeroed_region() {
|
| 170 |
+
// In the beginner.wsc V2 file, super weapons after the used ones should exist
|
| 171 |
+
let path = Path::new(FIXTURES).join("beginner.wsc");
|
| 172 |
+
let scheme = SchemeFile::from_file(&path).unwrap();
|
| 173 |
+
|
| 174 |
+
// Armageddon is weapon index 63 (last V2 super weapon)
|
| 175 |
+
let armageddon = scheme.weapon(63).unwrap();
|
| 176 |
+
// It should be parseable (values may be zero)
|
| 177 |
+
let _ = armageddon.ammo;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
/// Parse all .wsc files from the game directory if available.
|
| 181 |
#[test]
|
| 182 |
fn parse_all_game_schemes() {
|
docs/re-notes/configuration.md
CHANGED
|
@@ -80,14 +80,100 @@ Binary format, variable length. Contains visual/audio theme settings.
|
|
| 80 |
3. Calls `Frontend__SaveTheme` (0x490E60) with path `"data\current.thm"`
|
| 81 |
4. Calls `FrontendChangeScreen` to navigate away
|
| 82 |
|
| 83 |
-
## Weapon Schemes
|
| 84 |
-
|
| 85 |
-
Stored as `.wsc` files under `User\Schemes\`.
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
## Data Files
|
| 93 |
|
|
|
|
| 80 |
3. Calls `Frontend__SaveTheme` (0x490E60) with path `"data\current.thm"`
|
| 81 |
4. Calls `FrontendChangeScreen` to navigate away
|
| 82 |
|
| 83 |
+
## Weapon Schemes (.wsc)
|
| 84 |
+
|
| 85 |
+
Stored as `.wsc` files under `User\Schemes\`. Full format spec: [worms2d.info/Game_scheme_file](https://worms2d.info/Game_scheme_file).
|
| 86 |
+
|
| 87 |
+
### Functions
|
| 88 |
+
|
| 89 |
+
| Function | Address | Signature | Notes |
|
| 90 |
+
|----------|---------|-----------|-------|
|
| 91 |
+
| `Scheme__ReadFile` | 0x4D3890 | stdcall(dest, path, flag, out_ptr) → u32, RET 0x10 | Reads .wsc into dest+0x14 |
|
| 92 |
+
| `Scheme__SaveFile` | 0x4D44F0 | thiscall(this, name, flag), RET 0x8 | Writes struct to .wsc |
|
| 93 |
+
| `Scheme__DetectVersion` | 0x4D4480 | Uses ESI (non-standard) → 1/2/3 | Compares V3 extension bytes |
|
| 94 |
+
| `Scheme__FileExists` | 0x4D4CD0 | stdcall(name) → 0/1, RET 0x4 | Path: `User\Schemes\%s.wsc` |
|
| 95 |
+
| `Scheme__FileExistsNumbered` | 0x4D4E00 | stdcall(...) | Path: `User\Schemes\{%02d} %s.wsc` |
|
| 96 |
+
| `Scheme__InitFromData` | 0x4D5020 | fastcall(?, data, dest, name) | Copies payload + V3 defaults |
|
| 97 |
+
| `Scheme__CheckWeaponLimits` | 0x4D50E0 | — → 0/1 | Validates ammo vs max table (39 weapons) |
|
| 98 |
+
| `Scheme__ValidateExtendedOptions` | 0x4D5110 | Uses EAX → 0/1 | V3 field range checks |
|
| 99 |
+
| `Scheme__ScanDirectory` | 0x4D54E0 | — | Finds `{NN} name.wsc` files |
|
| 100 |
+
| `Scheme__ExtractBuiltins` | 0x4D5720 | — | Extracts PE resources (IDs 0x3CA-0x3D6) |
|
| 101 |
+
|
| 102 |
+
### Data
|
| 103 |
+
|
| 104 |
+
| Symbol | Address | Size | Notes |
|
| 105 |
+
|--------|---------|------|-------|
|
| 106 |
+
| `SCHEME_V3_DEFAULTS` | 0x649AB8 | 110 bytes | Default extended options for V1/V2 |
|
| 107 |
+
| `SCHEME_WEAPON_AMMO_LIMITS` | 0x6AD130 | 39 bytes | Max ammo per weapon (V1 set) |
|
| 108 |
+
|
| 109 |
+
### File Format
|
| 110 |
+
|
| 111 |
+
Binary, little-endian. Header: `"SCHM"` (4 bytes) + version byte (1 byte) + payload.
|
| 112 |
+
|
| 113 |
+
| Version | Payload | Total | Content |
|
| 114 |
+
|---------|---------|-------|---------|
|
| 115 |
+
| V1 (0x01) | 0xD8 (216) | 221 | 36 options + 45 weapons × 4 |
|
| 116 |
+
| V2 (0x02) | 0x124 (292) | 297 | V1 + 19 super weapons × 4 |
|
| 117 |
+
| V3 (0x03) | 0x192 (402) | 407 | V2 + 110 extended options |
|
| 118 |
+
|
| 119 |
+
### Payload Layout
|
| 120 |
+
|
| 121 |
+
**Game Options (36 bytes, payload +0x00):**
|
| 122 |
+
|
| 123 |
+
| Offset | Size | Field | Notes |
|
| 124 |
+
|--------|------|-------|-------|
|
| 125 |
+
| +0x00 | 1 | hot_seat_delay | Seconds between turns |
|
| 126 |
+
| +0x01 | 1 | retreat_time | Seconds after weapon use |
|
| 127 |
+
| +0x02 | 1 | rope_retreat_time | Seconds after rope weapon use |
|
| 128 |
+
| +0x03 | 1 | display_total_round_time | bool |
|
| 129 |
+
| +0x04 | 1 | automatic_replays | bool |
|
| 130 |
+
| +0x05 | 1 | fall_damage | Damage at critical velocity |
|
| 131 |
+
| +0x06 | 1 | artillery_mode | bool |
|
| 132 |
+
| +0x07 | 1 | bounty_mode | 0x00/0x5F/0x89 |
|
| 133 |
+
| +0x08 | 1 | stockpiling | 0=Off, 1=On, 2=Anti |
|
| 134 |
+
| +0x09 | 1 | worm_select | 0=Sequential, 1=Manual, 2=Random |
|
| 135 |
+
| +0x0A | 1 | sudden_death_event | 0=RoundEnds, 1=Nuke, 2=HP→1, 3=Nothing |
|
| 136 |
+
| +0x0B | 1 | water_rise_rate | Flooding speed |
|
| 137 |
+
| +0x0C | 1 | weapon_crate_probability | i8, -100 to 100 |
|
| 138 |
+
| +0x0D | 1 | donor_cards | bool |
|
| 139 |
+
| +0x0E | 1 | health_crate_probability | i8 |
|
| 140 |
+
| +0x0F | 1 | health_crate_energy | Energy from health crate |
|
| 141 |
+
| +0x10 | 1 | utility_crate_probability | i8 |
|
| 142 |
+
| +0x11 | 1 | hazardous_object_types | Bitmask |
|
| 143 |
+
| +0x12 | 1 | mine_delay | i8, seconds (0x80+=random) |
|
| 144 |
+
| +0x13 | 1 | dud_mines | bool |
|
| 145 |
+
| +0x14 | 1 | manual_worm_placement | bool |
|
| 146 |
+
| +0x15 | 1 | worm_energy | 0=instant death |
|
| 147 |
+
| +0x16 | 1 | turn_time | Seconds |
|
| 148 |
+
| +0x17 | 1 | round_time | Minutes (0=immediate SD) |
|
| 149 |
+
| +0x18 | 1 | number_of_wins | Rounds to win match |
|
| 150 |
+
| +0x19 | 1 | blood | false=pink, true=red |
|
| 151 |
+
| +0x1A | 1 | aqua_sheep | bool |
|
| 152 |
+
| +0x1B | 1 | sheep_heaven | bool |
|
| 153 |
+
| +0x1C | 1 | god_worms | bool |
|
| 154 |
+
| +0x1D | 1 | indestructible_land | bool |
|
| 155 |
+
| +0x1E | 1 | upgraded_grenade | bool |
|
| 156 |
+
| +0x1F | 1 | upgraded_shotgun | bool |
|
| 157 |
+
| +0x20 | 1 | upgraded_clusters | bool |
|
| 158 |
+
| +0x21 | 1 | upgraded_longbow | bool |
|
| 159 |
+
| +0x22 | 1 | team_weapons | bool |
|
| 160 |
+
| +0x23 | 1 | super_weapons | bool |
|
| 161 |
+
|
| 162 |
+
**Per-Weapon (4 bytes each, payload +0x24):**
|
| 163 |
+
|
| 164 |
+
`[ammo, power, delay, crate_probability]` — 45 weapons (V1), 64 weapons (V2+).
|
| 165 |
+
|
| 166 |
+
**V3 Extended Options (110 bytes, payload +0x124):**
|
| 167 |
+
|
| 168 |
+
Physics (gravity, friction, wind), RubberWorm modifiers, glitch toggles, and gameplay extensions. See `scheme.rs::ExtendedOptions` for full field list. Uses fixed-point 16.16 and tri-state (0/1/0x80) types.
|
| 169 |
+
|
| 170 |
+
### Callers of Scheme__ReadFile
|
| 171 |
+
|
| 172 |
+
| Address | Context |
|
| 173 |
+
|---------|---------|
|
| 174 |
+
| 0x4A1D18 | Frontend/options dialog |
|
| 175 |
+
| 0x4B4B92 | Multiplayer scheme loading |
|
| 176 |
+
| 0x4CC425 | Mission/campaign loading |
|
| 177 |
|
| 178 |
## Data Files
|
| 179 |
|