diff --git a/src/vpx/biff.rs b/src/vpx/biff.rs index ed51e89..0e6d775 100644 --- a/src/vpx/biff.rs +++ b/src/vpx/biff.rs @@ -90,6 +90,11 @@ impl<'a> BiffReader<'a> { } pub fn get_bool(&mut self) -> bool { + let all = &self.data[self.pos..self.pos + 4]; + // Any other value is suspicious as it is not a boolean + if all != [0, 0, 0, 0] && all != [1, 0, 0, 0] { + panic!("Unexpected bytes for bool: {:?}", all); + } let b = self.data[self.pos] != 0; self.pos += 4; self.bytes_in_record_remaining -= 4; @@ -144,10 +149,16 @@ impl<'a> BiffReader<'a> { } pub fn get_f32(&mut self) -> f32 { - let i: Result<(&[u8], f32), nom::Err<()>> = le_f32(&self.data[self.pos..]); + let data = &self.data[self.pos..self.pos + 4]; + let i: Result<(&[u8], f32), nom::Err<()>> = le_f32(data); self.pos += 4; self.bytes_in_record_remaining -= 4; - i.unwrap().1 + + let res = i.unwrap().1; + if res.is_nan() { + eprintln!("NaN value found in f32 for tag {}: {:?}", self.tag, data); + } + res } pub fn get_str(&mut self, count: usize) -> String { diff --git a/src/vpx/gamedata.rs b/src/vpx/gamedata.rs index e8f9ac3..9f1ceb9 100644 --- a/src/vpx/gamedata.rs +++ b/src/vpx/gamedata.rs @@ -7,6 +7,7 @@ use super::{ }; use crate::vpx::biff::{BiffRead, BiffWrite}; use crate::vpx::color::{Color, ColorJson}; +use crate::vpx::json::F32WithNanInf; use crate::vpx::material::{Material, SaveMaterial, SavePhysicsMaterial}; use crate::vpx::math::{dequantize_u8, quantize_u8}; use crate::vpx::renderprobe::RenderProbeWithGarbage; @@ -221,7 +222,11 @@ pub struct GameData { pub custom_colors: [Color; 16], //[Color; 16], // CCUS 113 pub protection_data: Option>, // SECB (removed in ?) pub code: StringWithEncoding, // CODE 114 - pub is_locked: Option, // TLCK (added in 10.8 for tournament mode?) + /// TLCK (added in 10.8 for tournament mode?) + /// Flag that disables all table edition. Lock toggles are counted to identify + /// version changes in a table (for example to guarantee untouched table for tournament) + /// Used to be a boolean for a while in the 10.8 dev cycle but now is a lock counter. + pub locked: Option, // TLCK (added in 10.8 for tournament mode?) // This is a bit of a hack because we want reproducible builds. // 10.8.0 beta 1-4 had EFSS at the old location, but it was moved to the new location in beta 5 // Some tables were released with these old betas, so we need to support both locations to be 100% reproducing the orignal table @@ -281,10 +286,10 @@ pub(crate) struct GameDataJson { pub bg_layback_full_single_screen: Option, pub bg_fov_full_single_screen: Option, pub bg_offset_x_full_single_screen: Option, - pub bg_offset_y_full_single_screen: Option, + pub bg_offset_y_full_single_screen: Option, pub bg_offset_z_full_single_screen: Option, pub bg_scale_x_full_single_screen: Option, - pub bg_scale_y_full_single_screen: Option, + pub bg_scale_y_full_single_screen: Option, pub bg_scale_z_full_single_screen: Option, pub bg_view_horizontal_offset_full_single_screen: Option, pub bg_view_vertical_offset_full_single_screen: Option, @@ -371,7 +376,7 @@ pub(crate) struct GameDataJson { pub custom_colors: [ColorJson; 16], pub protection_data: Option>, //pub code: StringWithEncoding, - pub is_locked: Option, + pub locked: Option, #[serde(skip_serializing_if = "Option::is_none")] pub is_10_8_0_beta1_to_beta4: Option, } @@ -437,10 +442,10 @@ impl GameDataJson { bg_layback_full_single_screen: self.bg_layback_full_single_screen, bg_fov_full_single_screen: self.bg_fov_full_single_screen, bg_offset_x_full_single_screen: self.bg_offset_x_full_single_screen, - bg_offset_y_full_single_screen: self.bg_offset_y_full_single_screen, + bg_offset_y_full_single_screen: self.bg_offset_y_full_single_screen.map(f32::from), bg_offset_z_full_single_screen: self.bg_offset_z_full_single_screen, bg_scale_x_full_single_screen: self.bg_scale_x_full_single_screen, - bg_scale_y_full_single_screen: self.bg_scale_y_full_single_screen, + bg_scale_y_full_single_screen: self.bg_scale_y_full_single_screen.map(f32::from), bg_scale_z_full_single_screen: self.bg_scale_z_full_single_screen, bg_view_horizontal_offset_full_single_screen: self .bg_view_horizontal_offset_full_single_screen, @@ -555,7 +560,7 @@ impl GameDataJson { custom_colors, protection_data: self.protection_data.clone(), code: StringWithEncoding::empty(), - is_locked: self.is_locked, + locked: self.locked, is_10_8_0_beta1_to_beta4: self.is_10_8_0_beta1_to_beta4.unwrap_or(false), } } @@ -617,10 +622,14 @@ impl GameDataJson { bg_layback_full_single_screen: game_data.bg_layback_full_single_screen, bg_fov_full_single_screen: game_data.bg_fov_full_single_screen, bg_offset_x_full_single_screen: game_data.bg_offset_x_full_single_screen, - bg_offset_y_full_single_screen: game_data.bg_offset_y_full_single_screen, + bg_offset_y_full_single_screen: game_data + .bg_offset_y_full_single_screen + .map(F32WithNanInf::from), bg_offset_z_full_single_screen: game_data.bg_offset_z_full_single_screen, bg_scale_x_full_single_screen: game_data.bg_scale_x_full_single_screen, - bg_scale_y_full_single_screen: game_data.bg_scale_y_full_single_screen, + bg_scale_y_full_single_screen: game_data + .bg_scale_y_full_single_screen + .map(F32WithNanInf::from), bg_scale_z_full_single_screen: game_data.bg_scale_z_full_single_screen, bg_view_horizontal_offset_full_single_screen: game_data .bg_view_horizontal_offset_full_single_screen, @@ -717,7 +726,7 @@ impl GameDataJson { custom_colors, protection_data: game_data.protection_data.clone(), // code: game_data.code.clone(), - is_locked: game_data.is_locked, + locked: game_data.locked, is_10_8_0_beta1_to_beta4: Some(game_data.is_10_8_0_beta1_to_beta4) .filter(|x| x == &true), } @@ -892,7 +901,7 @@ impl Default for GameData { bg_window_bottom_x_offset_full_single_screen: None, bg_window_bottom_y_offset_full_single_screen: None, bg_window_bottom_z_offset_full_single_screen: None, - is_locked: None, + locked: None, } } } @@ -1226,8 +1235,8 @@ pub fn write_all_gamedata_records(gamedata: &GameData, version: &Version) -> Vec writer.write_tagged_data("SECB", protection_data); } writer.write_tagged_string_with_encoding_no_size("CODE", &gamedata.code); - if let Some(is_locked) = gamedata.is_locked { - writer.write_tagged_bool("TLCK", is_locked); + if let Some(is_locked) = gamedata.locked { + writer.write_tagged_u32("TLCK", is_locked); } writer.close(true); @@ -1458,7 +1467,7 @@ pub fn read_all_gamedata_records(input: &[u8], version: &Version) -> GameData { // at least a the time of 1060, some code was still encoded in latin1 gamedata.code = reader.get_str_with_encoding_no_remaining_update(len as usize); } - "TLCK" => gamedata.is_locked = Some(reader.get_bool()), + "TLCK" => gamedata.locked = Some(reader.get_u32()), other => { let data = reader.get_record_data(false); println!("unhandled tag {} {} bytes", other, data.len()); @@ -1658,7 +1667,7 @@ mod tests { bg_window_bottom_x_offset_full_single_screen: None, bg_window_bottom_y_offset_full_single_screen: None, bg_window_bottom_z_offset_full_single_screen: None, - is_locked: Some(true), + locked: Faker.fake(), is_10_8_0_beta1_to_beta4: false, }; let version = Version::new(1074); diff --git a/src/vpx/gameitem/gate.rs b/src/vpx/gameitem/gate.rs index b9fd755..0accd12 100644 --- a/src/vpx/gameitem/gate.rs +++ b/src/vpx/gameitem/gate.rs @@ -124,7 +124,7 @@ impl Default for Gate { name: Default::default(), two_way: false, is_reflection_enabled: None, //true, - gate_type: Some(GateType::Plate), + gate_type: None, //Some(GateType::Plate), is_locked: false, editor_layer: Default::default(), editor_layer_name: None, diff --git a/src/vpx/sound.rs b/src/vpx/sound.rs index edc2dba..8336960 100644 --- a/src/vpx/sound.rs +++ b/src/vpx/sound.rs @@ -247,6 +247,11 @@ pub(crate) fn read(file_version: &Version, reader: &mut BiffReader) -> SoundData 10 }; + // We have seen below case for a 1040 file: + // Legacy behavior, where the BG selection was encoded into the strings directly + // path = "* Backglass Output *" or name contains "bgout_" + // This is still seen as a wav file, even if the path does not end with .wav! + for i in 0..num_values { match i { 0 => { @@ -303,8 +308,13 @@ pub(crate) fn read(file_version: &Version, reader: &mut BiffReader) -> SoundData } } +/// Check if the path is a wav file. +/// If the path does not have an extension, it is also considered a wav file! fn is_wav(path: &str) -> bool { - path.to_lowercase().ends_with(".wav") + match path.rfind('.') { + Some(pos) => path[(pos + 1)..].eq_ignore_ascii_case("wav"), + None => true, + } } pub(crate) fn write(file_version: &Version, sound: &SoundData, writer: &mut BiffWriter) { diff --git a/tests/vpx_read_extract_assemble_write_compare_all.rs b/tests/vpx_read_extract_assemble_write_compare_all.rs index 53b7f7b..1a2c300 100644 --- a/tests/vpx_read_extract_assemble_write_compare_all.rs +++ b/tests/vpx_read_extract_assemble_write_compare_all.rs @@ -21,32 +21,35 @@ mod test { let paths = find_files(&folder, "vpx")?; // testdir can not be used in non-main threads let dir: PathBuf = testdir!(); + // Example tables caused problems in the past: // // * Inhabiting Mars RC 4 - for animation frame // * DieHard_272.vpx - primitive "BM_pAirDuctGate" has a NaN value for nx // * Johnny Mnemonic (Williams 1995) VPW v1.0.2.vpx - animated frames that overlap with primitive names - - // TODO why is par_iter() not faster but just consuming all cpu cores? - paths + // * Future Spa (Bally 1979) v4.3.vpx - NaN in table setup values + let filtered: Vec<&PathBuf> = paths .iter() // .filter(|path| { // let name = path.file_name().unwrap().to_str().unwrap(); // name.contains("Bob") // }) - .try_for_each(|path| { - println!("testing: {:?}", path); - let ReadAndWriteResult { - extracted, - test_vpx, - } = read_and_write_vpx(&dir, &path)?; - assert_equal_vpx(path, test_vpx.clone()); - // panic!("stop"); - // if all is good we remove the test file and the extracted dir - std::fs::remove_file(&test_vpx)?; - std::fs::remove_dir_all(&extracted)?; - Ok(()) - }) + .collect(); + + // TODO why is par_iter() not faster but just consuming all cpu cores? + filtered.iter().enumerate().try_for_each(|(n, path)| { + println!("testing {}/{}: {:?}", n + 1, filtered.len(), path); + let ReadAndWriteResult { + extracted, + test_vpx, + } = read_and_write_vpx(&dir, &path)?; + assert_equal_vpx(path, test_vpx.clone()); + // panic!("stop"); + // if all is good we remove the test file and the extracted dir + std::fs::remove_file(&test_vpx)?; + std::fs::remove_dir_all(&extracted)?; + Ok(()) + }) } struct ReadAndWriteResult {