use war3parser::formats::w3i::{FormatVersion, War3MapW3i};
#[derive(Default)]
struct W3iWriter {
buf: Vec<u8>,
}
impl W3iWriter {
fn u8(&mut self, v: u8) {
self.buf.push(v);
}
fn u32(&mut self, v: u32) {
self.buf.extend(v.to_le_bytes());
}
fn i32(&mut self, v: i32) {
self.buf.extend(v.to_le_bytes());
}
fn f32(&mut self, v: f32) {
self.buf.extend(v.to_le_bytes());
}
fn cstr(&mut self, s: &str) {
self.buf.extend(s.as_bytes());
self.buf.push(0);
}
fn raw(&mut self, bytes: &[u8]) {
self.buf.extend(bytes);
}
}
fn build_w3i(version: u32) -> Vec<u8> {
let v = version;
let mut w = W3iWriter::default();
w.u32(v);
if v >= 18 {
w.u32(5); w.u32(6059); }
if v >= 27 {
w.u32(1);
w.u32(32);
w.u32(10);
w.u32(12345); }
w.cstr("Test Map");
w.cstr("An Author");
w.cstr("A description");
w.cstr("1v1");
if v == 8 {
w.f32(1.0);
w.i32(2);
w.f32(3.0);
w.f32(4.0);
w.f32(5.0);
w.i32(6);
}
for i in 0..8 {
w.f32(i as f32 * 100.0); }
if v >= 15 {
for i in 0..4 {
w.i32(i); }
}
w.i32(64); w.i32(64); if v == 8 {
w.i32(7);
}
w.u32(0x8000); w.u8(b'L');
if v >= 23 {
w.i32(-1); w.cstr("LoadingScreen.mdx");
} else if v >= 18 {
w.i32(3); } else if v >= 15 {
w.cstr("LoadingScreen.mdx");
}
if v >= 10 {
w.cstr("loading text");
w.cstr("loading title");
if v >= 15 {
w.cstr("loading subtitle");
}
if v >= 23 {
w.i32(2); w.cstr("Prologue.mdx");
} else if v >= 18 {
w.i32(1); } else if v >= 15 {
w.cstr("Prologue.mdx");
}
if v >= 11 {
w.cstr("prologue text");
w.cstr("prologue title");
if v >= 15 {
w.cstr("prologue subtitle");
}
}
if v >= 23 {
w.i32(1); w.f32(3000.0); w.f32(5000.0); w.f32(0.5); w.raw(&[10, 20, 30, 255]); if v >= 25 {
w.i32(0x4C726169); }
w.cstr("LordaeronSummerDay");
w.u8(b'L'); w.raw(&[0, 64, 128, 255]); }
if v >= 28 {
w.u32(1); }
if v >= 31 {
w.u32(3); w.u32(1); }
if v >= 32 {
w.i32(1650); w.i32(3000); }
if v >= 33 {
w.i32(1200); }
}
w.u32(1);
w.i32(0); w.i32(1); w.i32(1); w.i32(1); w.cstr("Player 1");
w.f32(-1024.0);
w.f32(1024.0);
w.u32(0); w.u32(0); if v >= 31 {
w.u32(0); w.u32(0); }
w.u32(1);
w.u32(0b11); w.u32(0xFFFF_FFFF); w.cstr("Force 1");
w.u32(1);
w.u32(1); w.raw(b"Rhme");
w.i32(0); w.i32(2); w.u32(1);
w.u32(1); w.raw(b"hgtw");
if v >= 15 {
w.u32(0); }
if v >= 24 {
w.u32(0); }
if (26..28).contains(&v) {
w.i32(0); }
w.buf
}
const ALL_VERSIONS: &[u32] = &[8, 10, 11, 15, 18, 23, 24, 25, 26, 27, 28, 31, 32, 33];
#[test]
fn every_known_version_parses_completely() {
for &v in ALL_VERSIONS {
let buf = build_w3i(v);
let info = War3MapW3i::parse(&buf).unwrap_or_else(|e| panic!("v{v}: {e}"));
assert_eq!(info.version, FormatVersion(v));
assert_eq!(info.name, "Test Map", "v{v} name");
assert_eq!(info.recommended_players, "1v1", "v{v} recommended");
assert_eq!(info.playable_size, [64, 64], "v{v} playable size");
assert_eq!(info.flags, 0x8000, "v{v} flags");
assert_eq!(info.tileset, b'L', "v{v} tileset");
assert_eq!(info.players.len(), 1, "v{v} players");
assert_eq!(info.players[0].name, "Player 1", "v{v} player name");
assert_eq!(info.forces.len(), 1, "v{v} forces");
assert_eq!(info.forces[0].name, "Force 1", "v{v} force name");
assert_eq!(
info.upgrade_availability_changes.len(),
1,
"v{v} upgrade changes"
);
assert_eq!(info.upgrade_availability_changes[0].id, *b"Rhme");
assert_eq!(info.tech_availability_changes.len(), 1, "v{v} tech changes");
assert_eq!(info.tech_availability_changes[0].id, *b"hgtw");
assert!(!info.skipped_optional_sections, "v{v} not skipped");
}
}
#[test]
fn version_gates_toggle_fields() {
for &v in ALL_VERSIONS {
let info = War3MapW3i::parse(&build_w3i(v)).unwrap();
let fv = FormatVersion(v);
assert_eq!(info.saves.is_some(), v >= 18, "v{v} saves");
assert_eq!(info.build_version.is_some(), v >= 27, "v{v} build version");
assert_eq!(info.legacy_v8.is_some(), v == 8, "v{v} legacy fields");
assert_eq!(
info.camera_bounds_complements.is_some(),
v >= 15,
"v{v} complements"
);
assert_eq!(
info.loading_screen_background.is_some(),
v >= 23,
"v{v} loading bg"
);
assert_eq!(
info.campaign_background.is_some(),
(18..23).contains(&v),
"v{v} campaign bg"
);
assert_eq!(
info.loading_screen_model.is_some(),
(15..18).contains(&v) || v >= 23,
"v{v} loading model"
);
assert_eq!(
info.loading_screen_text.is_some(),
v >= 10,
"v{v} loading text"
);
assert_eq!(
info.loading_screen_subtitle.is_some(),
v >= 15,
"v{v} loading subtitle"
);
assert_eq!(info.game_data_set.is_some(), v >= 23, "v{v} game data set");
assert_eq!(
info.loading_screen_index.is_some(),
(18..23).contains(&v),
"v{v} loading index"
);
assert_eq!(
info.prologue_screen_text.is_some(),
v >= 11,
"v{v} prologue text"
);
assert_eq!(info.fog_style.is_some(), v >= 23, "v{v} fog");
assert_eq!(info.global_weather.is_some(), v >= 25, "v{v} weather");
assert_eq!(info.script_mode.is_some(), v >= 28, "v{v} script mode");
assert_eq!(info.graphics_mode.is_some(), v >= 31, "v{v} graphics mode");
assert_eq!(
info.default_camera_zoom.is_some(),
v >= 32,
"v{v} default zoom"
);
assert_eq!(info.min_camera_zoom.is_some(), v >= 33, "v{v} min zoom");
assert_eq!(
info.players[0].enemy_low_priorities.is_some(),
v >= 31,
"v{v} enemy priorities"
);
assert_eq!(fv.is_tft(), v >= 23, "v{v} is_tft");
assert_eq!(fv.is_reforged(), v >= 31, "v{v} is_reforged");
}
}
#[test]
fn gated_values_roundtrip_exactly() {
let info = War3MapW3i::parse(&build_w3i(33)).unwrap();
assert_eq!(info.saves, Some(5));
assert_eq!(info.editor_version, Some(6059));
assert_eq!(info.build_version, Some([1, 32, 10, 12345]));
assert_eq!(info.get_build_version(), 132);
assert_eq!(info.loading_screen_background, Some(-1));
assert_eq!(
info.loading_screen_model.as_deref(),
Some("LoadingScreen.mdx")
);
assert_eq!(info.game_data_set, Some(2));
assert_eq!(info.fog_style, Some(1));
assert_eq!(info.fog_height, Some([3000.0, 5000.0]));
assert_eq!(info.fog_density, Some(0.5));
assert_eq!(info.fog_color, Some([10, 20, 30, 255]));
assert_eq!(
info.sound_environment.as_deref(),
Some("LordaeronSummerDay")
);
assert_eq!(info.light_environment_tileset, Some(b'L'));
assert_eq!(info.script_mode, Some(1));
assert_eq!(info.graphics_mode, Some(3));
assert_eq!(info.game_data_version, Some(1));
assert_eq!(info.default_camera_zoom, Some(1650));
assert_eq!(info.max_camera_zoom, Some(3000));
assert_eq!(info.min_camera_zoom, Some(1200));
}
#[test]
fn v8_legacy_fields_roundtrip() {
let info = War3MapW3i::parse(&build_w3i(8)).unwrap();
let legacy = info.legacy_v8.expect("v8 legacy fields");
assert_eq!(legacy.unk2, 2);
assert_eq!(legacy.unk6, 6);
assert_eq!(legacy.unk7, 7);
assert!(info.saves.is_none());
assert!(info.loading_screen_subtitle.is_none());
assert!(info.fog_style.is_none());
}
#[test]
fn skip_marker_after_forces_is_honored() {
let mut w = build_w3i(25);
let cut = w.len() - 40;
w.truncate(cut);
w.push(0xFF);
let info = War3MapW3i::parse(&w).unwrap();
assert!(info.skipped_optional_sections);
assert!(info.upgrade_availability_changes.is_empty());
assert!(info.tech_availability_changes.is_empty());
assert_eq!(info.players.len(), 1);
}
#[test]
fn truncation_after_forces_yields_empty_sections() {
let mut w = build_w3i(31);
let cut = w.len() - 40;
w.truncate(cut);
let info = War3MapW3i::parse(&w).unwrap();
assert!(!info.skipped_optional_sections);
assert!(info.upgrade_availability_changes.is_empty());
assert_eq!(info.forces.len(), 1);
}
#[test]
fn unknown_future_version_parses_with_nearest_layout() {
let mut buf = build_w3i(28);
buf[0..4].copy_from_slice(&29u32.to_le_bytes());
let info = War3MapW3i::parse(&buf).unwrap();
assert_eq!(info.version, FormatVersion(29));
assert!(!info.version.is_known());
assert_eq!(info.script_mode, Some(1));
assert!(info.graphics_mode.is_none());
}