diff --git a/Cargo.toml b/Cargo.toml index 0b0fb2d..167d506 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,11 +26,14 @@ serde = { version = "1.0.188", features = ["derive"] } serde_json = { version = "1.0.107", features = ["preserve_order"] } utf16string = "0.2.0" quick-xml = { version = "0.30.0", features = ["serialize"] } +serde_repr = "0.1.16" [dev-dependencies] dirs = "5.0.1" pretty_assertions = "1.4.0" rand = "0.8.5" +rayon = "1.8.0" testdir = "0.8.1" walkdir = "2.4.0" - +roxmltree = "0.18.1" +testresult = "0.3.0" diff --git a/README.md b/README.md index 0557ac8..f39ee76 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,14 @@ https://github.com/francisdb/vpxtool * VPForums https://www.vpforums.org/ * Virtual Pinball Chat on Discord https://discord.com/invite/YHcBrtT +## Running the integration tests + +We expect a folder `~/vpinball/tables` to exist that contains a lot of `vpx` and `directb2s` files. The tests will recursively search for these files and run the tests on them. + +```bash +cargo test --release -- --ignored --nocapture +``` + ## Making a release We use https://github.com/MarcoIeni/release-plz which creates a release pr on every commit to master diff --git a/src/directb2s/mod.rs b/src/directb2s/mod.rs index 719446c..efcdef3 100644 --- a/src/directb2s/mod.rs +++ b/src/directb2s/mod.rs @@ -1,23 +1,49 @@ //! Library for reading and writing [B2S-Backglass](https://github.com/vpinball/b2s-backglass) `directb2s` files use std::fmt::Debug; +use std::io::BufRead; -use quick_xml::de::from_str; use quick_xml::de::*; -use serde::Deserialize; +use quick_xml::se::*; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; // The xml model is based on this // https://github.com/vpinball/b2s-backglass/blob/f43ae8aacbb79d3413531991e4c0156264442c39/b2sbackglassdesigner/b2sbackglassdesigner/classes/CreateCode/Coding.vb#L30 -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ValueTag { #[serde(rename = "@Value")] pub value: String, } -#[derive(Deserialize)] -pub struct ImageTag { +#[derive(Debug, Deserialize, Serialize)] +pub struct ImageValueTag { + #[serde(rename = "@Value"/*, serialize_with = "as_str_encoded"*/)] + pub value: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DestTypeTag { #[serde(rename = "@Value")] + pub value: DestType, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ReelRollingDirectionTag { + #[serde(rename = "@Value")] + pub value: ReelRollingDirection, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DmdTypeTag { + #[serde(rename = "@Value")] + pub value: DMDType, +} + +#[derive(Deserialize, Serialize)] +pub struct ImageTag { + #[serde(rename = "@Value"/*, serialize_with = "as_str_encoded"*/)] pub value: String, #[serde(rename = "@FileName")] pub file_name: String, @@ -33,16 +59,16 @@ impl Debug for ImageTag { } } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] pub struct OnImageTag { #[serde(rename = "@Value")] pub value: String, #[serde(rename = "@FileName")] pub file_name: String, - #[serde(rename = "@RomID")] + #[serde(rename = "@RomID", skip_serializing_if = "Option::is_none")] pub rom_id: Option, - #[serde(rename = "@RomIDType")] - pub rom_id_type: Option, + #[serde(rename = "@RomIDType", skip_serializing_if = "Option::is_none")] + pub rom_id_type: Option, } // debug for ImageTag not showing length of value @@ -55,23 +81,23 @@ impl Debug for OnImageTag { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Images { - #[serde(rename = "BackglassOffImage")] + #[serde(rename = "BackglassOffImage", skip_serializing_if = "Option::is_none")] pub backglass_off_image: Option, - #[serde(rename = "BackglassOnImage")] + #[serde(rename = "BackglassOnImage", skip_serializing_if = "Option::is_none")] pub backglass_on_image: Option, - #[serde(rename = "BackglassImage")] + #[serde(rename = "BackglassImage", skip_serializing_if = "Option::is_none")] pub backglass_image: Option, - #[serde(rename = "DMDImage")] + #[serde(rename = "DMDImage", skip_serializing_if = "Option::is_none")] pub dmd_image: Option, - #[serde(rename = "IlluminationImage")] + #[serde(rename = "IlluminationImage", skip_serializing_if = "Option::is_none")] pub illumination_image: Option, #[serde(rename = "ThumbnailImage")] - pub thumbnail_image: ValueTag, + pub thumbnail_image: ImageValueTag, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct AnimationStep { #[serde(rename = "@Step")] pub step: String, @@ -85,14 +111,14 @@ pub struct AnimationStep { pub wait_loops_after_off: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Animation { #[serde(rename = "@Name")] pub name: String, #[serde(rename = "@Parent")] pub parent: String, - #[serde(rename = "@DualMode")] - pub dual_mode: String, + #[serde(rename = "@DualMode", skip_serializing_if = "Option::is_none")] + pub dual_mode: Option, #[serde(rename = "@Interval")] pub interval: String, #[serde(rename = "@Loops")] @@ -102,11 +128,11 @@ pub struct Animation { #[serde(rename = "@StartAnimationAtBackglassStartup")] pub start_animation_at_backglass_startup: String, #[serde(rename = "@LightsStateAtAnimationStart")] - pub lights_state_at_animation_start: String, + pub lights_state_at_animation_start: Option, #[serde(rename = "@LightsStateAtAnimationEnd")] pub lights_state_at_animation_end: String, #[serde(rename = "@AnimationStopBehaviour")] - pub animation_stop_behaviour: String, + pub animation_stop_behaviour: Option, #[serde(rename = "@LockInvolvedLamps")] pub lock_involved_lamps: String, #[serde(rename = "@HideScoreDisplays")] @@ -117,45 +143,45 @@ pub struct Animation { pub animation_step: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Animations { - #[serde(rename = "Animation")] + #[serde(rename = "Animation", skip_serializing_if = "Option::is_none")] pub animation: Option>, } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] pub struct Bulb { - #[serde(rename = "@ID")] - pub id: String, #[serde(rename = "@Parent")] pub parent: Option, + #[serde(rename = "@ID")] + pub id: String, #[serde(rename = "@Name")] pub name: String, - #[serde(rename = "@B2SID")] + #[serde(rename = "@B2SID", skip_serializing_if = "Option::is_none")] pub b2s_id: Option, - #[serde(rename = "@B2SIDType")] - pub b2s_id_type: Option, - #[serde(rename = "@B2SValue")] + #[serde(rename = "@B2SIDType", skip_serializing_if = "Option::is_none")] + pub b2s_id_type: Option, + #[serde(rename = "@B2SValue", skip_serializing_if = "Option::is_none")] pub b2s_value: Option, - #[serde(rename = "@RomID")] + #[serde(rename = "@RomID", skip_serializing_if = "Option::is_none")] pub rom_id: Option, - #[serde(rename = "@RomIDType")] - pub rom_id_type: Option, - #[serde(rename = "@RomInverted")] + #[serde(rename = "@RomIDType", skip_serializing_if = "Option::is_none")] + pub rom_id_type: Option, + #[serde(rename = "@RomInverted", skip_serializing_if = "Option::is_none")] pub rom_inverted: Option, #[serde(rename = "@InitialState")] pub initial_state: String, #[serde(rename = "@DualMode")] - pub dual_mode: String, + pub dual_mode: Option, #[serde(rename = "@Intensity")] pub intensity: String, #[serde(rename = "@LightColor")] - pub light_color: String, + pub light_color: Option, #[serde(rename = "@DodgeColor")] pub dodge_color: String, - #[serde(rename = "@IlluMode")] + #[serde(rename = "@IlluMode", skip_serializing_if = "Option::is_none")] pub illu_mode: Option, - #[serde(rename = "@ZOrder")] + #[serde(rename = "@ZOrder", skip_serializing_if = "Option::is_none")] pub z_order: Option, #[serde(rename = "@Visible")] pub visible: String, @@ -169,16 +195,16 @@ pub struct Bulb { pub height: String, #[serde(rename = "@IsImageSnippit")] pub is_image_snippit: String, - #[serde(rename = "@SnippitType")] + #[serde(rename = "@SnippitType", skip_serializing_if = "Option::is_none")] // SnippitMechID // SnippitRotatingSteps // SnippitRotatingDirection // SnippitRotatingStopBehaviour // SnippitRotatingInterval - pub snippit_type: Option, + pub snippit_type: Option, #[serde(rename = "@Image")] pub image: String, - #[serde(rename = "@OffImage")] + #[serde(rename = "@OffImage", skip_serializing_if = "Option::is_none")] pub off_image: Option, #[serde(rename = "@Text")] pub text: String, @@ -223,37 +249,37 @@ impl Debug for Bulb { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Illumination { - #[serde(rename = "Bulb")] + #[serde(rename = "Bulb", skip_serializing_if = "Option::is_none")] pub bulb: Option>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Score { - #[serde(rename = "@Parent")] - pub parent: String, #[serde(rename = "@ID")] pub id: String, - #[serde(rename = "@B2SStartDigit")] - pub b2s_start_digit: String, - #[serde(rename = "@B2SScoreType")] - pub b2s_score_type: String, - #[serde(rename = "@B2SPlayerNo")] - pub b2s_player_no: String, + #[serde(rename = "@Parent")] + pub parent: String, + #[serde(rename = "@B2SStartDigit", skip_serializing_if = "Option::is_none")] + pub b2s_start_digit: Option, + #[serde(rename = "@B2SScoreType", skip_serializing_if = "Option::is_none")] + pub b2s_score_type: Option, + #[serde(rename = "@B2SPlayerNo", skip_serializing_if = "Option::is_none")] + pub b2s_player_no: Option, #[serde(rename = "@ReelType")] pub reel_type: String, - #[serde(rename = "@ReelIlluImageSet")] + #[serde(rename = "@ReelIlluImageSet", skip_serializing_if = "Option::is_none")] pub reel_illu_image_set: Option, - #[serde(rename = "@ReelIlluLocation")] + #[serde(rename = "@ReelIlluLocation", skip_serializing_if = "Option::is_none")] pub reel_illu_location: Option, - #[serde(rename = "@ReelIlluIntensity")] + #[serde(rename = "@ReelIlluIntensity", skip_serializing_if = "Option::is_none")] pub reel_illu_intensity: Option, - #[serde(rename = "@ReelIlluB2SID")] + #[serde(rename = "@ReelIlluB2SID", skip_serializing_if = "Option::is_none")] pub reel_illu_b2s_id: Option, - #[serde(rename = "@ReelIlluB2SIDType")] - pub reel_illu_b2s_id_type: Option, - #[serde(rename = "@ReelIlluB2SValue")] + #[serde(rename = "@ReelIlluB2SIDType", skip_serializing_if = "Option::is_none")] + pub reel_illu_b2s_id_type: Option, + #[serde(rename = "@ReelIlluB2SValue", skip_serializing_if = "Option::is_none")] pub reel_illu_b2s_value: Option, #[serde(rename = "@ReelLitColor")] pub reel_lit_color: String, @@ -270,7 +296,7 @@ pub struct Score { #[serde(rename = "@Spacing")] pub spacing: String, #[serde(rename = "@DisplayState")] - pub display_state: String, + pub display_state: Option, #[serde(rename = "@LocX")] pub loc_x: String, #[serde(rename = "@LocY")] @@ -280,15 +306,29 @@ pub struct Score { #[serde(rename = "@Height")] pub height: String, // following fields are not really in use as far as I know - #[serde(rename = "@Sound3")] + #[serde(rename = "@Sound1", skip_serializing_if = "Option::is_none")] + pub sound1: Option, + #[serde(rename = "@Sound2", skip_serializing_if = "Option::is_none")] + pub sound2: Option, + #[serde(rename = "@Sound3", skip_serializing_if = "Option::is_none")] pub sound3: Option, - #[serde(rename = "@Sound4")] + #[serde(rename = "@Sound4", skip_serializing_if = "Option::is_none")] pub sound4: Option, - #[serde(rename = "@Sound5")] + #[serde(rename = "@Sound5", skip_serializing_if = "Option::is_none")] pub sound5: Option, + #[serde(rename = "@Sound6", skip_serializing_if = "Option::is_none")] + pub sound6: Option, + #[serde(rename = "@Sound7", skip_serializing_if = "Option::is_none")] + pub sound7: Option, + #[serde(rename = "@Sound8", skip_serializing_if = "Option::is_none")] + pub sound8: Option, + #[serde(rename = "@Sound9", skip_serializing_if = "Option::is_none")] + pub sound9: Option, + #[serde(rename = "@Sound10", skip_serializing_if = "Option::is_none")] + pub sound10: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Scores { #[serde(rename = "@ReelCountOfIntermediates")] pub reel_count_of_intermediates: String, @@ -297,11 +337,11 @@ pub struct Scores { #[serde(rename = "@ReelRollingInterval")] pub reel_rolling_interval: String, - #[serde(rename = "Score")] + #[serde(rename = "Score", skip_serializing_if = "Option::is_none")] pub score: Option>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ReelsImage { // TODO there might be dynamic fields here for IntermediateImage0, IntermediateImage1, etc. #[serde(rename = "@Name")] @@ -309,16 +349,37 @@ pub struct ReelsImage { #[serde(rename = "@CountOfIntermediates")] pub count_of_intermediates: String, #[serde(rename = "@Image")] - pub image: String, // base64 encoded image + pub image: String, + // base64 encoded image + #[serde( + rename = "@IntermediateImage1", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image1: Option, + #[serde( + rename = "@IntermediateImage2", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image2: Option, + #[serde( + rename = "@IntermediateImage3", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image3: Option, + #[serde( + rename = "@IntermediateImage4", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image4: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ReelsImages { - #[serde(rename = "Image")] + #[serde(rename = "Image", skip_serializing_if = "Option::is_none")] pub image: Option>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ReelsIlluminatedImage { // TODO there might be dynamic fields here for IntermediateImage0, IntermediateImage1, etc. #[serde(rename = "@Name")] @@ -326,10 +387,31 @@ pub struct ReelsIlluminatedImage { #[serde(rename = "@CountOfIntermediates")] pub count_of_intermediates: String, #[serde(rename = "@Image")] - pub image: String, // base64 encoded image + pub image: String, + // base64 encoded image + #[serde( + rename = "@IntermediateImage1", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image1: Option, + #[serde( + rename = "@IntermediateImage2", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image2: Option, + #[serde( + rename = "@IntermediateImage3", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image3: Option, + #[serde( + rename = "@IntermediateImage4", + skip_serializing_if = "Option::is_none" + )] + pub intermediate_image4: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ReelsIlluminatedImagesSet { #[serde(rename = "@ID")] pub id: String, @@ -337,13 +419,13 @@ pub struct ReelsIlluminatedImagesSet { pub illuminated_image: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ReelsIlluminatedImages { - #[serde(rename = "Set")] + #[serde(rename = "Set", skip_serializing_if = "Option::is_none")] pub set: Option>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Reels { #[serde(rename = "Images")] pub images: ReelsImages, @@ -351,12 +433,12 @@ pub struct Reels { pub illuminated_images: ReelsIlluminatedImages, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Sounds { // as far as I can see this is not in use } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct DMDDefaultLocation { #[serde(rename = "@LocX")] pub loc_x: String, @@ -364,15 +446,15 @@ pub struct DMDDefaultLocation { pub loc_y: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct GrillHeight { #[serde(rename = "@Value")] pub value: String, - #[serde(rename = "@Small")] + #[serde(rename = "@Small", skip_serializing_if = "Option::is_none")] pub small: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct DirectB2SData { #[serde(rename = "@Version")] pub version: String, @@ -381,7 +463,7 @@ pub struct DirectB2SData { #[serde(rename = "TableType")] pub table_type: ValueTag, #[serde(rename = "DMDType")] - pub dmd_type: ValueTag, + pub dmd_type: DmdTypeTag, #[serde(rename = "DMDDefaultLocation")] pub dmd_default_location: DMDDefaultLocation, #[serde(rename = "GrillHeight")] @@ -395,11 +477,11 @@ pub struct DirectB2SData { #[serde(rename = "VSName")] pub vsname: ValueTag, #[serde(rename = "DualBackglass")] - pub dual_backglass: ValueTag, + pub dual_backglass: Option, #[serde(rename = "Author")] pub author: ValueTag, #[serde(rename = "Artwork")] - pub artwork: ValueTag, + pub artwork: Option, #[serde(rename = "GameName")] pub game_name: ValueTag, #[serde(rename = "AddEMDefaults")] @@ -407,7 +489,7 @@ pub struct DirectB2SData { #[serde(rename = "CommType")] pub comm_type: ValueTag, #[serde(rename = "DestType")] - pub dest_type: ValueTag, + pub dest_type: DestTypeTag, #[serde(rename = "NumberOfPlayers")] pub number_of_players: ValueTag, #[serde(rename = "B2SDataCount")] @@ -422,10 +504,10 @@ pub struct DirectB2SData { pub d7_thickness: ValueTag, #[serde(rename = "D7Shear")] pub d7_shear: ValueTag, - #[serde(rename = "ReelColor")] + #[serde(rename = "ReelColor", skip_serializing_if = "Option::is_none")] pub reel_color: Option, #[serde(rename = "ReelRollingDirection")] - pub reel_rolling_direction: ValueTag, + pub reel_rolling_direction: ReelRollingDirectionTag, #[serde(rename = "ReelRollingInterval")] pub reel_rolling_interval: ValueTag, #[serde(rename = "ReelIntermediateImageCount")] @@ -434,17 +516,205 @@ pub struct DirectB2SData { pub animations: Animations, #[serde(rename = "Scores")] pub scores: Option, - #[serde(rename = "Reels")] + #[serde(rename = "Reels", skip_serializing_if = "Option::is_none")] pub reels: Option, #[serde(rename = "Illumination")] pub illumination: Illumination, - #[serde(rename = "Sounds")] + #[serde(rename = "Sounds", skip_serializing_if = "Option::is_none")] pub sounds: Option, #[serde(rename = "Images")] pub images: Images, } -pub fn load(text: &str) -> Result { +pub fn read(reader: R) -> Result { // this will probably use up a lot of memory - from_str::(text) + from_reader(reader) +} + +pub fn write(data: &DirectB2SData, writer: &mut W) -> Result<(), DeError> { + // to_writer(writer, data) + let mut ser = Serializer::new(writer); + ser.indent(' ', 2); + data.serialize(ser) +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum TableType { + NotDefined = 0, + EM = 1, + SS = 2, + SSDMD = 3, + ORI = 4, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum DMDType { + NotDefined = 0, + NoB2SDMD = 1, + B2SAlwaysOnSecondMonitor = 2, + B2SAlwaysOnThirdMonitor = 3, + B2SOnSecondOrThirdMonitor = 4, } + +// TODO we could probably use derive_more but that comes with a slew of dependencies +impl std::fmt::Display for DMDType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + DMDType::NotDefined => "NotDefined", + DMDType::NoB2SDMD => "NoB2SDMD", + DMDType::B2SAlwaysOnSecondMonitor => "B2SAlwaysOnSecondMonitor", + DMDType::B2SAlwaysOnThirdMonitor => "B2SAlwaysOnThirdMonitor", + DMDType::B2SOnSecondOrThirdMonitor => "B2SOnSecondOrThirdMonitor", + }; + write!(f, "{}", s) + } +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum CommType { + NotDefined = 0, + Rom = 1, + B2S = 2, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum DestType { + NotDefined = 0, + DirectB2S = 1, + VisualStudio2010 = 2, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum ImageSetType { + NotDefined = 0, + ReelImages = 1, + CreditReelImages = 2, + LEDImages = 3, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum ParentForm { + NotDefined = 0, + Backglass = 1, + DMD = 2, +} + +#[allow(non_camel_case_types)] +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum B2SScoreType { + NotUsed = 0, + Scores_01 = 1, + Credits_29 = 2, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum B2SPlayerNo { + NotUsed = 0, + Player1 = 1, + Player2 = 2, + Player3 = 3, + Player4 = 4, + Player5 = 5, + // not in original code, found in "Dogies (Bally 1967).directb2s" + Player6 = 6, // not in original code, found in "Capersville (Bally 1966).directb2s" +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum ScoreDisplayState { + Visible = 0, + Hidden = 1, +} + +#[allow(non_camel_case_types)] +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum B2SIDType { + NotUsed = 0, + ScoreRolloverPlayer1_25 = 1, + ScoreRolloverPlayer2_26 = 2, + ScoreRolloverPlayer3_27 = 3, + ScoreRolloverPlayer4_28 = 4, + PlayerUp_30 = 5, + CanPlay_31 = 6, + BallInPlay_32 = 7, + Tilt_33 = 8, + Match_34 = 9, + GameOver_35 = 10, + ShootAgain_36 = 11, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum RomIDType { + NotUsed = 0, + Lamp = 1, + Solenoid = 2, + GIString = 3, + Unknown = 4, // not in original code, found in "Diner (Williams 1990) VPW Mod 1.0.2.directb2s"? +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum DualMode { + Both = 0, + Authentic = 1, + Fantasy = 2, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum SnippitType { + StandardImage = 0, + SelfRotatingImage = 1, + MechRotatingImage = 2, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum SnippitRotationDirection { + Clockwise = 0, + AntiClockwise = 1, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum SnippitRotationStopBehaviour { + SpinOff = 0, + StopImmediatelly = 1, + RunAnimationTillEnd = 2, + RunAnimationToFirstStep = 3, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum ReelIlluminationLocation { + Off = 0, + Above = 1, + Below = 2, + AboveAndBelow = 3, +} + +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[repr(u8)] +pub enum ReelRollingDirection { + Up = 0, + Down = 1, +} + +// // workaround for https://github.com/tafia/quick-xml/issues/670 +// fn as_str_encoded(v: &String, serializer: S) -> Result { +// //serializer.serialize_str(&base64::encode(v.as_ref())) +// // CR -> +// // LF -> +// let serialized = v.replace("\r", " ").replace("\n", " "); +// serializer.serialize_str(&serialized) +// } diff --git a/src/vpx/mod.rs b/src/vpx/mod.rs index 3b58de2..29c40a5 100644 --- a/src/vpx/mod.rs +++ b/src/vpx/mod.rs @@ -144,13 +144,11 @@ impl VpxFile { } pub fn read_gameitems(&mut self) -> io::Result> { - let version = self.read_version()?; let gamedata = self.read_gamedata()?; read_gameitems(&mut self.compound_file, &gamedata) } pub fn read_images(&mut self) -> io::Result> { - let version = self.read_version()?; let gamedata = self.read_gamedata()?; read_images(&mut self.compound_file, &gamedata) } @@ -162,13 +160,11 @@ impl VpxFile { } pub fn read_fonts(&mut self) -> io::Result> { - let version = self.read_version()?; let gamedata = self.read_gamedata()?; read_fonts(&mut self.compound_file, &gamedata) } pub fn read_collections(&mut self) -> io::Result> { - let version = self.read_version()?; let gamedata = self.read_gamedata()?; read_collections(&mut self.compound_file, &gamedata) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..6ce359c --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,22 @@ +use std::ffi::OsStr; +use std::io; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +pub fn find_files>(tables_path: P, extension: &str) -> io::Result> { + let mut found = Vec::new(); + let mut entries = WalkDir::new(tables_path).into_iter(); + let os_extension = OsStr::new(extension); + entries.try_for_each(|entry| { + let dir_entry = entry?; + let path = dir_entry.path(); + if path.is_file() { + match path.extension() { + Some(ex) if ex == os_extension => found.push(path.to_path_buf()), + _ => {} + } + } + Ok::<(), io::Error>(()) + })?; + Ok(found) +} diff --git a/tests/directb2s_read_write_compare_all.rs b/tests/directb2s_read_write_compare_all.rs new file mode 100644 index 0000000..55d6b7c --- /dev/null +++ b/tests/directb2s_read_write_compare_all.rs @@ -0,0 +1,153 @@ +use pretty_assertions::assert_eq; +use rayon::prelude::*; +use roxmltree::{Document, Node, NodeType}; +use std::collections::hash_map::DefaultHasher; +use std::fmt::Write; +use std::hash::{Hash, Hasher}; +use std::io; +use std::io::{Error, ErrorKind, Read}; +use std::path::PathBuf; +use testresult::TestResult; +use vpin::directb2s; +use vpin::directb2s::DirectB2SData; + +mod common; + +#[test] +#[ignore = "slow integration test that only runs on correctly set up machines"] +fn read_all() -> TestResult { + let home = dirs::home_dir().expect("no home dir"); + let folder = home.join("vpinball").join("tables"); + if !folder.exists() { + panic!("folder does not exist: {:?}", folder); + } + let paths = common::find_files(&folder, "directb2s")?; + + //paths.par_iter().panic_fuse().try_for_each(|path| { + paths.iter().try_for_each(|path| { + println!("testing: {:?}", path); + + // read file to data + let loaded = read_directb2s(&path)?; + + // write data to buffer + let mut written = String::new(); + directb2s::write(&loaded, &mut written)?; + + // read original file as xml ast using minidom + let mut file = std::fs::File::open(&path)?; + let mut doc = String::new(); + file.read_to_string(&mut doc)?; + + // FIXME workaround for https://github.com/tafia/quick-xml/issues/670 + let mut written = written.replace("\r\n", " "); + // let original_tail = tail(&doc, 100); + // let written_tail2 = tail(&written, 100); + // assert_eq!(original_tail, written_tail2); + // --- + + let original = roxmltree::Document::parse(&doc)?; + + // read buffer as xml ast + let written = roxmltree::Document::parse(&mut written)?; + + let original = doc_tree(&original)?; + let written = doc_tree(&written)?; + + // compare both + assert_eq!(original, written); + Ok(()) + }) +} + +fn tail(written: &String, n: usize) -> String { + written + .chars() + .rev() + .take(n) + .collect::() + .chars() + .rev() + .collect::() + .to_owned() +} + +fn doc_tree(doc: &Document) -> Result { + let mut writer = String::new(); + doc_to_tag_tree(&doc.root(), "".to_string(), &mut writer)?; + Ok(writer) +} + +fn doc_to_tag_tree( + node: &Node, + indent: String, + writer: &mut W, +) -> Result<(), std::fmt::Error> { + let t = node.node_type(); + match node.node_type() { + NodeType::Element => { + write_node(node, &indent, writer, t)?; + } + NodeType::Root => { + write_node(node, &indent, writer, t)?; + } + _ => { + // skip processing instructions, comments and text + // println!("skipping: {:?}", t) + } + } + node.children() + .try_for_each(|child| doc_to_tag_tree(&child, format!("{} ", indent), writer)) +} + +fn write_node( + node: &Node, + indent: &String, + writer: &mut W, + t: NodeType, +) -> Result<(), std::fmt::Error> { + let mut sorted_attributes = node.attributes().collect::>(); + sorted_attributes.sort_by_cached_key(|a| a.name()); + let attributes = sorted_attributes + .iter() + .map(|a| { + let value = a.value(); + if value.len() > 100 { + format!( + "{}=hash[{}]{}|{}", + a.name(), + &value.len(), + calculate_hash(&value), + &value[0..100] + ) + } else { + format!("{}={}", a.name(), a.value()) + } + }) + .collect::>(); + let attributes = attributes.join(" "); + write!( + writer, + "{} {:?} {} {}\n", + indent, + t, + node.tag_name().name(), + attributes + )?; + Ok(()) +} + +fn read_directb2s(path: &PathBuf) -> Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + directb2s::read(reader).map_err(|e| { + let msg = format!("Error for {}: {}", path.display(), e); + io::Error::new(ErrorKind::Other, msg) + }) +} + +fn calculate_hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} diff --git a/tests/vpx_read_write_compare_all.rs b/tests/vpx_read_write_compare_all.rs index af52741..1a22d29 100644 --- a/tests/vpx_read_write_compare_all.rs +++ b/tests/vpx_read_write_compare_all.rs @@ -1,17 +1,19 @@ use cfb::CompoundFile; use pretty_assertions::assert_eq; +use rayon::prelude::*; use std::collections::hash_map::DefaultHasher; -use std::ffi::OsStr; use std::hash::{Hash, Hasher}; use std::io; use std::io::{Read, Seek}; use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR}; use testdir::testdir; +use testresult::TestResult; use vpin::vpx::biff::BiffReader; -use walkdir::WalkDir; + +mod common; #[test] -fn read_and_write() -> io::Result<()> { +fn read_and_write() -> TestResult { let path = PathBuf::from("testdata/completely_blank_table_10_7_4.vpx"); let original = vpin::vpx::read(&path)?; @@ -32,23 +34,24 @@ fn read_and_write_all() -> io::Result<()> { if !folder.exists() { panic!("folder does not exist: {:?}", folder); } - let paths = find_vpx_files(&folder)?; - + let paths = common::find_files(&folder, "vpx")?; + // testdir can not be used in non-main threads + let dir: PathBuf = testdir!(); + // TODO why is par_iter() not faster but just consuming all cpu cores? paths.iter().try_for_each(|path| { println!("testing: {:?}", path); - let test_vpx_path = read_and_write_vpx(&path)?; + let test_vpx_path = read_and_write_vpx(&dir, &path)?; assert_equal_vpx(path, test_vpx_path); Ok(()) }) } -fn read_and_write_vpx(path: &Path) -> io::Result { +fn read_and_write_vpx(dir: &PathBuf, path: &Path) -> io::Result { let original = vpin::vpx::read(&path.to_path_buf())?; - // create temp file and write the vpx to it - let dir: PathBuf = testdir!(); - let test_vpx_path = dir.join("test.vpx"); + let file_name = path.file_name().unwrap(); + let test_vpx_path = dir.join(file_name); vpin::vpx::write(&test_vpx_path, &original)?; Ok(test_vpx_path) } @@ -190,19 +193,3 @@ fn biff_tags_and_hashes(reader: &mut BiffReader) -> Vec<(String, usize, u64)> { } tags } - -pub fn find_vpx_files>(tables_path: P) -> io::Result> { - let mut vpx_files = Vec::new(); - let mut entries = WalkDir::new(tables_path).into_iter(); - entries.try_for_each(|entry| { - let dir_entry = entry?; - let path = dir_entry.path(); - if path.is_file() { - if let Some("vpx") = path.extension().and_then(OsStr::to_str) { - vpx_files.push(path.to_path_buf()); - } - } - Ok::<(), io::Error>(()) - })?; - Ok(vpx_files) -}