diff --git a/.gitignore b/.gitignore index 6985cf1..0177cbf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +# output from example +basic.vpx diff --git a/README.md b/README.md index cfd092f..b3bc20f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ Join [#vpxtool on "Virtual Pinball Chat" discord](https://discord.gg/eYsvyMu8) f https://docs.rs/vpin +## Example code + +Check the [examples folder](examples/) + ## Projects using vpin https://github.com/francisdb/vpxtool diff --git a/basic.ini b/basic.ini new file mode 100644 index 0000000..fa9c716 --- /dev/null +++ b/basic.ini @@ -0,0 +1,15 @@ +[TableOverride] +ViewCabMode = 2 +ViewCabScaleX = 1.280866 +ViewCabScaleY = 1.000000 +ViewCabScaleZ = 1.000000 +ViewCabRotation = 0.000000 +ViewCabHOfs = 0.000000 +ViewCabVOfs = -11.195301 +ViewCabWindowTop = 400.000000 +ViewCabWindowBot = 210.000000 + +[Player] +ScreenPlayerX = 0.000000 +ScreenPlayerY = -5.000000 +ScreenPlayerZ = 75.000000 \ No newline at end of file diff --git a/examples/basic.vbs b/examples/basic.vbs new file mode 100644 index 0000000..ee3437e --- /dev/null +++ b/examples/basic.vbs @@ -0,0 +1,38 @@ +Option Explicit +Randomize + +ExecuteGlobal GetTextFile("controller.vbs") + +Dim bFlippersEnabled + +Sub Table1_Init + debug.print "Hello, World!" + 'add a ball + bFlippersEnabled = True +End Sub + +Sub Table1_KeyDown(ByVal Keycode) + debug.print "Down Keycode: " & Keycode + If keycode = LeftFlipperKey and bFlippersEnabled Then + LeftFlipper.RotateToEnd + End If + If keycode = RightFlipperKey and bFlippersEnabled Then + RightFlipper.RotateToEnd + End If + If keycode = PlungerKey then + Plunger.PullBack + End If +End Sub + +Sub Table1_KeyUp(ByVal Keycode) + debug.print "Up Keycode: " & Keycode + If keycode = LeftFlipperKey and bFlippersEnabled Then + LeftFlipper.RotateToStart + End If + If keycode = RightFlipperKey and bFlippersEnabled Then + RightFlipper.RotateToStart + End If + If keycode = PlungerKey then + Plunger.Fire + End If +End Sub diff --git a/examples/create_basic_vpx_file.rs b/examples/create_basic_vpx_file.rs new file mode 100644 index 0000000..beb6e2b --- /dev/null +++ b/examples/create_basic_vpx_file.rs @@ -0,0 +1,64 @@ +use std::path::Path; +use vpin::vpx; +use vpin::vpx::color::ColorNoAlpha; +use vpin::vpx::gameitem::bumper::Bumper; +use vpin::vpx::gameitem::flipper::Flipper; +use vpin::vpx::gameitem::GameItemEnum; +use vpin::vpx::material::Material; +use vpin::vpx::VPX; + +fn main() -> Result<(), Box> { + let mut vpx = VPX::default(); + + // playfield material + let mut material = Material::default(); + material.name = "Playfield".to_string(); + // material defaults to purple + material.base_color = ColorNoAlpha::from_rgb(0x966F33); // Wood + vpx.gamedata.materials = Some(vec![material]); + + // black background (default is bluish gray) + vpx.gamedata.backdrop_color = ColorNoAlpha::from_rgb(0x060606); // Dark Gray + vpx.gamedata.playfield_material = "Playfield".to_string(); + + // add a plunger + let mut plunger = vpx::gameitem::plunger::Plunger::default(); + plunger.name = "Plunger".to_string(); + plunger.center.x = 898.027; + plunger.center.y = 2105.312; + vpx.add_game_item(GameItemEnum::Plunger(plunger)); + + // add a bumper in the center of the playfield + let mut bumper = Bumper::default(); + bumper.name = "Bumper1".to_string(); + bumper.center.x = (vpx.gamedata.left + vpx.gamedata.right) / 2.; + bumper.center.y = (vpx.gamedata.top + vpx.gamedata.bottom) / 2.; + vpx.add_game_item(GameItemEnum::Bumper(bumper)); + + // add 2 flippers + let mut flipper_left = Flipper::default(); + flipper_left.name = "LeftFlipper".to_string(); + flipper_left.center.x = 278.2138; + flipper_left.center.y = 1803.271; + flipper_left.start_angle = 120.5; + flipper_left.end_angle = 70.; + vpx.add_game_item(GameItemEnum::Flipper(flipper_left)); + + let mut flipper_right = Flipper::default(); + flipper_right.name = "RightFlipper".to_string(); + flipper_right.center.x = 595.869; + flipper_right.center.y = 1803.271; + flipper_right.start_angle = -120.5; + flipper_right.end_angle = -70.; + vpx.add_game_item(GameItemEnum::Flipper(flipper_right)); + + // add a script + let script = std::fs::read_to_string(Path::new("examples").join("basic.vbs"))?; + vpx.set_script(script); + + vpx::write("basic.vpx", &vpx)?; + + println!("Wrote basic.vpx."); + println!(r#"Try running it with "VPinballX_GL -play basic.vpx""#); + Ok(()) +} diff --git a/src/vpx/color.rs b/src/vpx/color.rs index 4c645fe..a4550fe 100644 --- a/src/vpx/color.rs +++ b/src/vpx/color.rs @@ -4,6 +4,83 @@ use serde::{Deserialize, Serialize}; use super::biff::BiffWriter; +#[derive(Debug, PartialEq, Clone, Copy, Dummy)] +pub struct ColorNoAlpha { + r: u8, + g: u8, + b: u8, +} + +/// Serialize as a string in the format "#RRGGBB". +impl Serialize for ColorNoAlpha { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let s = format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b); + serializer.serialize_str(&s) + } +} + +// Deserialize from a string in the format "#RRGGBB". +impl<'de> Deserialize<'de> for ColorNoAlpha { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s.len() != 7 { + return Err(serde::de::Error::custom( + "Invalid color format, expected #RRGGBB", + )); + } + if &s[0..1] != "#" { + return Err(serde::de::Error::custom( + "Invalid color format, expected #RRGGBB", + )); + } + let r = u8::from_str_radix(&s[1..3], 16).map_err(serde::de::Error::custom)?; + let g = u8::from_str_radix(&s[3..5], 16).map_err(serde::de::Error::custom)?; + let b = u8::from_str_radix(&s[5..7], 16).map_err(serde::de::Error::custom)?; + Ok(ColorNoAlpha { r, g, b }) + } +} + +impl ColorNoAlpha { + pub fn from_rgb(arg: u32) -> Self { + let r = ((arg >> 16) & 0xff) as u8; + let g = ((arg >> 8) & 0xff) as u8; + let b = (arg & 0xff) as u8; + ColorNoAlpha { r, g, b } + } + + pub fn to_rgb(&self) -> u32 { + let r = (self.r as u32) << 16; + let g = (self.g as u32) << 8; + let b = self.b as u32; + r | g | b + } + + pub fn rgb(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b } + } + + pub fn biff_read(reader: &mut BiffReader<'_>) -> ColorNoAlpha { + let r = reader.get_u8(); + let g = reader.get_u8(); + let b = reader.get_u8(); + let _ = reader.get_u8(); + ColorNoAlpha { r, g, b } + } + + pub fn biff_write(&self, writer: &mut BiffWriter) { + writer.write_u8(self.r); + writer.write_u8(self.g); + writer.write_u8(self.b); + writer.write_u8(0); + } +} + #[derive(Debug, PartialEq, Clone, Copy, Dummy)] pub struct Color { a: u8, @@ -134,9 +211,7 @@ impl Color { b: 0, }; - // TODO do we want a BiffRead with a parameter? - - pub fn biff_read_argb(reader: &mut BiffReader<'_>) -> Color { + pub fn biff_read_bgr(reader: &mut BiffReader<'_>) -> Color { let a = reader.get_u8(); let r = reader.get_u8(); let g = reader.get_u8(); @@ -144,27 +219,12 @@ impl Color { Color { a, r, g, b } } - pub fn biff_read_bgr(reader: &mut BiffReader<'_>) -> Color { - let a = reader.get_u8(); - let b = reader.get_u8(); - let g = reader.get_u8(); - let r = reader.get_u8(); - Color { a, r, g, b } - } - - pub fn biff_write_argb(&self, writer: &mut BiffWriter) { + pub fn biff_write_bgr(&self, writer: &mut BiffWriter) { writer.write_u8(self.a); writer.write_u8(self.r); writer.write_u8(self.g); writer.write_u8(self.b); } - - pub fn biff_write_bgr(&self, writer: &mut BiffWriter) { - writer.write_u8(self.a); - writer.write_u8(self.b); - writer.write_u8(self.g); - writer.write_u8(self.r); - } } impl std::fmt::Display for Color { diff --git a/src/vpx/gamedata.rs b/src/vpx/gamedata.rs index e2ffddd..6801854 100644 --- a/src/vpx/gamedata.rs +++ b/src/vpx/gamedata.rs @@ -6,7 +6,7 @@ use super::{ version::Version, }; use crate::vpx::biff::{BiffRead, BiffWrite}; -use crate::vpx::color::{Color, ColorJson}; +use crate::vpx::color::{Color, ColorJson, ColorNoAlpha}; use crate::vpx::json::F32WithNanInf; use crate::vpx::material::{Material, SaveMaterial, SavePhysicsMaterial}; use crate::vpx::math::{dequantize_u8, quantize_u8}; @@ -172,7 +172,7 @@ pub struct GameData { pub glass_bottom_height: Option, // GLAB 70.5 (added in 10.8) pub table_height: Option, // TBLH 71 (optional in 10.8) pub playfield_material: String, // PLMA 72 - pub backdrop_color: u32, // BCLR 73 (color bgr) + pub backdrop_color: ColorNoAlpha, // BCLR 73 (color bgr) pub global_difficulty: f32, // TDFT 74 pub light_ambient: u32, // LZAM 75 (color) pub light0_emission: u32, // LZDI 76 (color) @@ -341,7 +341,7 @@ pub(crate) struct GameDataJson { pub glass_bottom_height: Option, pub table_height: Option, pub playfield_material: String, - pub backdrop_color: u32, + pub backdrop_color: ColorNoAlpha, pub global_difficulty: f32, pub light_ambient: u32, pub light0_emission: u32, @@ -827,7 +827,7 @@ impl Default for GameData { glass_bottom_height: None, // new default 210 for both table_height: None, //0.0, playfield_material: "".to_string(), - backdrop_color: 0x232323ff, // bgra + backdrop_color: ColorNoAlpha::from_rgb(0x626E8E), // Waikawa/Bluish Gray global_difficulty: 0.2, light_ambient: 0x000000ff, // TODO what is the format for all these? light0_emission: 0xfffff0ff, // TODO is this correct? @@ -1125,7 +1125,7 @@ pub fn write_all_gamedata_records(gamedata: &GameData, version: &Version) -> Vec writer.write_tagged_f32("TBLH", table_height); } writer.write_tagged_string("PLMA", &gamedata.playfield_material); - writer.write_tagged_u32("BCLR", gamedata.backdrop_color); + writer.write_tagged_with("BCLR", &gamedata.backdrop_color, ColorNoAlpha::biff_write); writer.write_tagged_f32("TDFT", gamedata.global_difficulty); writer.write_tagged_u32("LZAM", gamedata.light_ambient); writer.write_tagged_u32("LZDI", gamedata.light0_emission); @@ -1373,7 +1373,7 @@ pub fn read_all_gamedata_records(input: &[u8], version: &Version) -> GameData { "GLAB" => gamedata.glass_bottom_height = Some(reader.get_f32()), "TBLH" => gamedata.table_height = Some(reader.get_f32()), "PLMA" => gamedata.playfield_material = reader.get_string(), - "BCLR" => gamedata.backdrop_color = reader.get_u32(), + "BCLR" => gamedata.backdrop_color = ColorNoAlpha::biff_read(reader), "TDFT" => gamedata.global_difficulty = reader.get_f32(), "LZAM" => gamedata.light_ambient = reader.get_u32(), "LZDI" => gamedata.light0_emission = reader.get_u32(), @@ -1593,7 +1593,7 @@ mod tests { glass_bottom_height: Some(123.0), table_height: Some(12.0), playfield_material: "material_pf".to_string(), - backdrop_color: 0x333333ff, + backdrop_color: ColorNoAlpha::rgb(0x11, 0x22, 0x33), global_difficulty: 0.3, light_ambient: 0x11223344, light0_emission: 0xaabbccdd, diff --git a/src/vpx/gameitem.rs b/src/vpx/gameitem.rs index 93adb68..ccdbb5f 100644 --- a/src/vpx/gameitem.rs +++ b/src/vpx/gameitem.rs @@ -13,6 +13,7 @@ pub mod lightsequencer; pub mod plunger; pub mod primitive; pub mod ramp; +pub mod ramp_image_alignment; pub mod reel; pub mod rubber; pub mod spinner; diff --git a/src/vpx/gameitem/flasher.rs b/src/vpx/gameitem/flasher.rs index f90eb03..d1e57b4 100644 --- a/src/vpx/gameitem/flasher.rs +++ b/src/vpx/gameitem/flasher.rs @@ -1,4 +1,5 @@ use crate::vpx::color::ColorJson; +use crate::vpx::gameitem::ramp_image_alignment::RampImageAlignment; use crate::vpx::{ biff::{self, BiffRead, BiffReader, BiffWrite}, color::Color, @@ -101,92 +102,6 @@ impl<'de> Deserialize<'de> for Filter { } } -#[derive(Debug, PartialEq, Clone, Dummy, Default)] -pub enum ImageAlignment { - ImageModeWorld = 0, - #[default] - ImageModeWrap = 1, -} - -impl From for ImageAlignment { - fn from(value: u32) -> Self { - match value { - 0 => ImageAlignment::ImageModeWorld, - 1 => ImageAlignment::ImageModeWrap, - _ => panic!("Invalid ImageAlignment value {}", value), - } - } -} - -impl From<&ImageAlignment> for u32 { - fn from(value: &ImageAlignment) -> Self { - match value { - ImageAlignment::ImageModeWorld => 0, - ImageAlignment::ImageModeWrap => 1, - } - } -} - -/// Serialize ImageAlignment as a lowercase string -impl Serialize for ImageAlignment { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let value = match self { - ImageAlignment::ImageModeWorld => "world", - ImageAlignment::ImageModeWrap => "wrap", - }; - serializer.serialize_str(value) - } -} - -/// Deserialize ImageAlignment from a lowercase string -/// or number for backwards compatibility -impl<'de> Deserialize<'de> for ImageAlignment { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct ImageAlignmentVisitor; - - impl<'de> serde::de::Visitor<'de> for ImageAlignmentVisitor { - type Value = ImageAlignment; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string or number representing a TargetType") - } - - fn visit_u64(self, value: u64) -> Result - where - E: serde::de::Error, - { - match value { - 0 => Ok(ImageAlignment::ImageModeWorld), - 1 => Ok(ImageAlignment::ImageModeWrap), - _ => Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Unsigned(value), - &"0 or 1", - )), - } - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - match value { - "world" => Ok(ImageAlignment::ImageModeWorld), - "wrap" => Ok(ImageAlignment::ImageModeWrap), - _ => Err(serde::de::Error::unknown_variant(value, &["world", "wrap"])), - } - } - } - - deserializer.deserialize_any(ImageAlignmentVisitor) - } -} - #[derive(Debug, PartialEq, Dummy)] pub struct Flasher { pub height: f32, @@ -209,7 +124,7 @@ pub struct Flasher { // IDMD added in 10.2? pub display_texture: bool, pub depth_bias: f32, - pub image_alignment: ImageAlignment, + pub image_alignment: RampImageAlignment, pub filter: Filter, pub filter_amount: u32, // FIAM @@ -245,7 +160,7 @@ pub(crate) struct FlasherJson { is_dmd: Option, display_texture: bool, depth_bias: f32, - image_alignment: ImageAlignment, + image_alignment: RampImageAlignment, filter: Filter, filter_amount: u32, light_map: Option, @@ -359,7 +274,7 @@ impl BiffRead for Flasher { let mut is_dmd = None; let mut display_texture = Default::default(); let mut depth_bias = Default::default(); - let mut image_alignment = ImageAlignment::ImageModeWrap; + let mut image_alignment = RampImageAlignment::Wrap; let mut filter = Filter::Overlay; let mut filter_amount: u32 = 100; let mut light_map: Option = None; @@ -606,25 +521,6 @@ mod tests { assert_eq!(flasher, flasher_read); } - #[test] - fn test_alignment_json() { - let sizing_type = ImageAlignment::ImageModeWrap; - let json = serde_json::to_string(&sizing_type).unwrap(); - assert_eq!(json, "\"wrap\""); - let sizing_type_read: ImageAlignment = serde_json::from_str(&json).unwrap(); - assert_eq!(sizing_type, sizing_type_read); - let json = serde_json::Value::from(0); - let sizing_type_read: ImageAlignment = serde_json::from_value(json).unwrap(); - assert_eq!(ImageAlignment::ImageModeWorld, sizing_type_read); - } - - #[test] - #[should_panic = "Error(\"unknown variant `foo`, expected `world` or `wrap`\", line: 0, column: 0)"] - fn test_alignment_json_fail() { - let json = serde_json::Value::from("foo"); - let _: ImageAlignment = serde_json::from_value(json).unwrap(); - } - #[test] fn test_filter_json() { let sizing_type = Filter::Overlay; diff --git a/src/vpx/gameitem/flipper.rs b/src/vpx/gameitem/flipper.rs index b6f325f..a08e6a2 100644 --- a/src/vpx/gameitem/flipper.rs +++ b/src/vpx/gameitem/flipper.rs @@ -6,13 +6,13 @@ use super::{vertex2d::Vertex2D, GameItem}; #[derive(Debug, PartialEq, Clone, Dummy)] pub struct Flipper { - center: Vertex2D, + pub center: Vertex2D, base_radius: f32, end_radius: f32, flipper_radius_max: f32, return_: f32, - start_angle: f32, - end_angle: f32, + pub start_angle: f32, + pub end_angle: f32, override_physics: u32, mass: f32, is_timer_enabled: bool, diff --git a/src/vpx/gameitem/gate.rs b/src/vpx/gameitem/gate.rs index 5e759f0..a5e99d6 100644 --- a/src/vpx/gameitem/gate.rs +++ b/src/vpx/gameitem/gate.rs @@ -41,10 +41,10 @@ impl Serialize for GateType { S: Serializer, { match self { - GateType::WireW => serializer.serialize_str("WireW"), - GateType::WireRectangle => serializer.serialize_str("WireRectangle"), - GateType::Plate => serializer.serialize_str("Plate"), - GateType::LongPlate => serializer.serialize_str("LongPlate"), + GateType::WireW => serializer.serialize_str("wire_w"), + GateType::WireRectangle => serializer.serialize_str("wire_rectangle"), + GateType::Plate => serializer.serialize_str("plate"), + GateType::LongPlate => serializer.serialize_str("long_plate"), } } } @@ -54,13 +54,17 @@ impl<'de> Deserialize<'de> for GateType { where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "WireW" => Ok(GateType::WireW), - "WireRectangle" => Ok(GateType::WireRectangle), - "Plate" => Ok(GateType::Plate), - "LongPlate" => Ok(GateType::LongPlate), - _ => Err(serde::de::Error::custom(format!("Unknown GateType: {}, expecting \"WireW\", \"WireRectangle\", \"Plate\" or \"LongPlate\"", s))), + let value = String::deserialize(deserializer)?; + let s = value.as_str(); + match s { + "wire_w" => Ok(GateType::WireW), + "wire_rectangle" => Ok(GateType::WireRectangle), + "plate" => Ok(GateType::Plate), + "long_plate" => Ok(GateType::LongPlate), + _ => Err(serde::de::Error::unknown_variant( + s, + &["wire_w", "wire_rectangle", "plate", "long_plate"], + )), } } } @@ -456,13 +460,13 @@ mod tests { fn test_gate_type_json() { let gate_type = GateType::WireRectangle; let json = serde_json::to_string(&gate_type).unwrap(); - assert_eq!(json, "\"WireRectangle\""); + assert_eq!(json, "\"wire_rectangle\""); let gate_type_read: GateType = serde_json::from_str(&json).unwrap(); assert_eq!(gate_type, gate_type_read); } #[test] - #[should_panic = "Error(\"Unknown GateType: Unknown, expecting \\\"WireW\\\", \\\"WireRectangle\\\", \\\"Plate\\\" or \\\"LongPlate\\\"\", line: 0, column: 0)"] + #[should_panic = "Error(\"unknown variant `Unknown`, expected one of `wire_w`, `wire_rectangle`, `plate`, `long_plate`\", line: 0, column: 0)"] fn test_gate_type_json_panic() { let json = Value::from("Unknown"); let _: GateType = serde_json::from_value(json).unwrap(); diff --git a/src/vpx/gameitem/plunger.rs b/src/vpx/gameitem/plunger.rs index c8102bd..e91a0f2 100644 --- a/src/vpx/gameitem/plunger.rs +++ b/src/vpx/gameitem/plunger.rs @@ -6,6 +6,9 @@ use super::vertex2d::Vertex2D; #[derive(Debug, PartialEq, Clone, Dummy)] pub enum PlungerType { + /// non-official, found in Star Wars (Data East 1992)/Star Wars (Data East 1992) VPW v1.2.2.vpx + /// This is not in the official VPX documentation + Unknown = 0, Modern = 1, Flat = 2, Custom = 3, @@ -14,6 +17,7 @@ pub enum PlungerType { impl From for PlungerType { fn from(value: u32) -> Self { match value { + 0 => PlungerType::Unknown, 1 => PlungerType::Modern, 2 => PlungerType::Flat, 3 => PlungerType::Custom, @@ -25,6 +29,7 @@ impl From for PlungerType { impl From<&PlungerType> for u32 { fn from(value: &PlungerType) -> Self { match value { + PlungerType::Unknown => 0, PlungerType::Modern => 1, PlungerType::Flat => 2, PlungerType::Custom => 3, @@ -39,6 +44,7 @@ impl Serialize for PlungerType { S: Serializer, { match self { + PlungerType::Unknown => serializer.serialize_str("unknown"), PlungerType::Modern => serializer.serialize_str("modern"), PlungerType::Flat => serializer.serialize_str("flat"), PlungerType::Custom => serializer.serialize_str("custom"), @@ -67,6 +73,7 @@ impl<'de> Deserialize<'de> for PlungerType { E: serde::de::Error, { match value { + 0 => Ok(PlungerType::Unknown), 1 => Ok(PlungerType::Modern), 2 => Ok(PlungerType::Flat), 3 => Ok(PlungerType::Custom), @@ -82,6 +89,7 @@ impl<'de> Deserialize<'de> for PlungerType { E: serde::de::Error, { match value { + "unknown" => Ok(PlungerType::Unknown), "modern" => Ok(PlungerType::Modern), "flat" => Ok(PlungerType::Flat), "custom" => Ok(PlungerType::Custom), @@ -100,7 +108,7 @@ impl<'de> Deserialize<'de> for PlungerType { #[derive(Debug, PartialEq, Dummy)] pub struct Plunger { - center: Vertex2D, + pub center: Vertex2D, width: f32, height: f32, z_adjust: f32, @@ -141,6 +149,50 @@ pub struct Plunger { pub editor_layer_visibility: Option, } +impl Default for Plunger { + fn default() -> Self { + Self { + center: Vertex2D::default(), + width: 25.0, + height: 20.0, + z_adjust: 0.0, + stroke: 80.0, + speed_pull: 0.5, + speed_fire: 80.0, + plunger_type: PlungerType::Modern, + anim_frames: 1, + material: String::default(), + image: String::default(), + mech_strength: 85.0, + is_mech_plunger: false, + auto_plunger: false, + park_position: 0.5 / 3.0, + scatter_velocity: 0.0, + momentum_xfer: 1.0, + is_timer_enabled: false, + timer_interval: 0, + is_visible: true, + is_reflection_enabled: Some(true), + surface: String::default(), + name: String::default(), + tip_shape: "0 .34; 2 .6; 3 .64; 5 .7; 7 .84; 8 .88; 9 .9; 11 .92; 14 .92; 39 .84" + .to_string(), + rod_diam: 0.6, + ring_gap: 2.0, + ring_diam: 0.94, + ring_width: 3.0, + spring_diam: 0.77, + spring_gauge: 1.38, + spring_loops: 8.0, + spring_end_loops: 2.5, + is_locked: false, + editor_layer: 0, + editor_layer_name: None, + editor_layer_visibility: None, + } + } +} + #[derive(Serialize, Deserialize)] struct PlungerJson { center: Vertex2D, @@ -282,46 +334,11 @@ impl<'de> Deserialize<'de> for Plunger { impl BiffRead for Plunger { fn biff_read(reader: &mut BiffReader<'_>) -> Self { - let mut center = Vertex2D::default(); - let mut width: f32 = 25.0; - let mut height: f32 = 20.0; - let mut z_adjust: f32 = 0.0; - let mut stroke: f32 = 80.0; - let mut speed_pull: f32 = 0.5; - let mut speed_fire: f32 = 80.0; - let mut plunger_type: PlungerType = PlungerType::Modern; - let mut anim_frames: u32 = 1; - let mut material = String::default(); - let mut image = String::default(); - let mut mech_strength: f32 = 85.0; - let mut is_mech_plunger: bool = false; - let mut auto_plunger: bool = false; - let mut park_position: f32 = 0.5 / 3.0; - let mut scatter_velocity: f32 = 0.0; - let mut momentum_xfer: f32 = 1.0; - let mut is_timer_enabled: bool = false; - let mut timer_interval: u32 = 0; - let mut is_visible: bool = true; - let mut is_reflection_enabled: Option = None; // true - let mut surface = String::default(); - let mut name = Default::default(); - let mut tip_shape = - "0 .34; 2 .6; 3 .64; 5 .7; 7 .84; 8 .88; 9 .9; 11 .92; 14 .92; 39 .84".to_string(); - let mut rod_diam: f32 = 0.6; - let mut ring_gap: f32 = 2.0; - let mut ring_diam: f32 = 0.94; - let mut ring_width: f32 = 3.0; - let mut spring_diam: f32 = 0.77; - let mut spring_gauge: f32 = 1.38; - let mut spring_loops: f32 = 8.0; - let mut spring_end_loops: f32 = 2.5; - - // these are shared between all items - let mut is_locked: bool = false; - let mut editor_layer: u32 = Default::default(); - let mut editor_layer_name: Option = None; - let mut editor_layer_visibility: Option = None; - + // for reading to be backwards compatible some fields need to be None by default + let mut plunger = Plunger { + is_reflection_enabled: None, + ..Default::default() + }; loop { reader.next(biff::WARN); if reader.is_eof() { @@ -331,114 +348,114 @@ impl BiffRead for Plunger { let tag_str = tag.as_str(); match tag_str { "VCEN" => { - center = Vertex2D::biff_read(reader); + plunger.center = Vertex2D::biff_read(reader); } "WDTH" => { - width = reader.get_f32(); + plunger.width = reader.get_f32(); } "HIGH" => { - height = reader.get_f32(); + plunger.height = reader.get_f32(); } "ZADJ" => { - z_adjust = reader.get_f32(); + plunger.z_adjust = reader.get_f32(); } "HPSL" => { - stroke = reader.get_f32(); + plunger.stroke = reader.get_f32(); } "SPDP" => { - speed_pull = reader.get_f32(); + plunger.speed_pull = reader.get_f32(); } "SPDF" => { - speed_fire = reader.get_f32(); + plunger.speed_fire = reader.get_f32(); } "TYPE" => { - plunger_type = reader.get_u32().into(); + plunger.plunger_type = reader.get_u32().into(); } "ANFR" => { - anim_frames = reader.get_u32(); + plunger.anim_frames = reader.get_u32(); } "MATR" => { - material = reader.get_string(); + plunger.material = reader.get_string(); } "IMAG" => { - image = reader.get_string(); + plunger.image = reader.get_string(); } "MEST" => { - mech_strength = reader.get_f32(); + plunger.mech_strength = reader.get_f32(); } "MECH" => { - is_mech_plunger = reader.get_bool(); + plunger.is_mech_plunger = reader.get_bool(); } "APLG" => { - auto_plunger = reader.get_bool(); + plunger.auto_plunger = reader.get_bool(); } "MPRK" => { - park_position = reader.get_f32(); + plunger.park_position = reader.get_f32(); } "PSCV" => { - scatter_velocity = reader.get_f32(); + plunger.scatter_velocity = reader.get_f32(); } "MOMX" => { - momentum_xfer = reader.get_f32(); + plunger.momentum_xfer = reader.get_f32(); } "TMON" => { - is_timer_enabled = reader.get_bool(); + plunger.is_timer_enabled = reader.get_bool(); } "TMIN" => { - timer_interval = reader.get_u32(); + plunger.timer_interval = reader.get_u32(); } "VSBL" => { - is_visible = reader.get_bool(); + plunger.is_visible = reader.get_bool(); } "REEN" => { - is_reflection_enabled = Some(reader.get_bool()); + plunger.is_reflection_enabled = Some(reader.get_bool()); } "SURF" => { - surface = reader.get_string(); + plunger.surface = reader.get_string(); } "NAME" => { - name = reader.get_wide_string(); + plunger.name = reader.get_wide_string(); } "TIPS" => { - tip_shape = reader.get_string(); + plunger.tip_shape = reader.get_string(); } "RODD" => { - rod_diam = reader.get_f32(); + plunger.rod_diam = reader.get_f32(); } "RNGG" => { - ring_gap = reader.get_f32(); + plunger.ring_gap = reader.get_f32(); } "RNGD" => { - ring_diam = reader.get_f32(); + plunger.ring_diam = reader.get_f32(); } "RNGW" => { - ring_width = reader.get_f32(); + plunger.ring_width = reader.get_f32(); } "SPRD" => { - spring_diam = reader.get_f32(); + plunger.spring_diam = reader.get_f32(); } "SPRG" => { - spring_gauge = reader.get_f32(); + plunger.spring_gauge = reader.get_f32(); } "SPRL" => { - spring_loops = reader.get_f32(); + plunger.spring_loops = reader.get_f32(); } "SPRE" => { - spring_end_loops = reader.get_f32(); + plunger.spring_end_loops = reader.get_f32(); } // shared "LOCK" => { - is_locked = reader.get_bool(); + plunger.is_locked = reader.get_bool(); } "LAYR" => { - editor_layer = reader.get_u32(); + plunger.editor_layer = reader.get_u32(); } "LANR" => { - editor_layer_name = Some(reader.get_string()); + plunger.editor_layer_name = Some(reader.get_string()); } "LVIS" => { - editor_layer_visibility = Some(reader.get_bool()); + plunger.editor_layer_visibility = Some(reader.get_bool()); } _ => { println!( @@ -450,44 +467,7 @@ impl BiffRead for Plunger { } } } - Self { - center, - width, - height, - z_adjust, - stroke, - speed_pull, - speed_fire, - plunger_type, - anim_frames, - material, - image, - mech_strength, - is_mech_plunger, - auto_plunger, - park_position, - scatter_velocity, - momentum_xfer, - is_timer_enabled, - timer_interval, - is_visible, - is_reflection_enabled, - surface, - name, - tip_shape, - rod_diam, - ring_gap, - ring_diam, - ring_width, - spring_diam, - spring_gauge, - spring_loops, - spring_end_loops, - is_locked, - editor_layer, - editor_layer_name, - editor_layer_visibility, - } + plunger } } diff --git a/src/vpx/gameitem/ramp.rs b/src/vpx/gameitem/ramp.rs index 7da300c..7f75059 100644 --- a/src/vpx/gameitem/ramp.rs +++ b/src/vpx/gameitem/ramp.rs @@ -1,4 +1,5 @@ use crate::vpx::biff::{self, BiffRead, BiffReader, BiffWrite}; +use crate::vpx::gameitem::ramp_image_alignment::RampImageAlignment; use fake::Dummy; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -122,90 +123,6 @@ impl<'de> Deserialize<'de> for RampType { } } -#[derive(Debug, PartialEq, Clone, Dummy)] -pub enum RampImageAlignment { - World = 0, - Wrap = 1, -} - -impl From for RampImageAlignment { - fn from(value: u32) -> Self { - match value { - 0 => RampImageAlignment::World, - 1 => RampImageAlignment::Wrap, - _ => panic!("Invalid RampImageAlignment {}", value), - } - } -} - -impl From<&RampImageAlignment> for u32 { - fn from(value: &RampImageAlignment) -> Self { - match value { - RampImageAlignment::World => 0, - RampImageAlignment::Wrap => 1, - } - } -} - -/// Serializes RampImageAlignment to lowercase string -impl Serialize for RampImageAlignment { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - RampImageAlignment::World => serializer.serialize_str("world"), - RampImageAlignment::Wrap => serializer.serialize_str("wrap"), - } - } -} - -/// Deserializes RampImageAlignment from lowercase string -/// or number for backwards compatibility -impl<'de> Deserialize<'de> for RampImageAlignment { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct RampImageAlignmentVisitor; - - impl<'de> serde::de::Visitor<'de> for RampImageAlignmentVisitor { - type Value = RampImageAlignment; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string or number representing a TargetType") - } - - fn visit_u64(self, value: u64) -> Result - where - E: serde::de::Error, - { - match value { - 0 => Ok(RampImageAlignment::World), - 1 => Ok(RampImageAlignment::Wrap), - _ => Err(serde::de::Error::unknown_variant( - &value.to_string(), - &["0", "1"], - )), - } - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - match value { - "world" => Ok(RampImageAlignment::World), - "wrap" => Ok(RampImageAlignment::Wrap), - _ => Err(serde::de::Error::unknown_variant(value, &["world", "wrap"])), - } - } - } - - deserializer.deserialize_any(RampImageAlignmentVisitor) - } -} - #[derive(Debug, PartialEq, Dummy)] pub struct Ramp { pub height_bottom: f32, // 1 diff --git a/src/vpx/gameitem/ramp_image_alignment.rs b/src/vpx/gameitem/ramp_image_alignment.rs new file mode 100644 index 0000000..d929f1b --- /dev/null +++ b/src/vpx/gameitem/ramp_image_alignment.rs @@ -0,0 +1,118 @@ +use fake::Dummy; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, PartialEq, Clone, Dummy)] +pub enum RampImageAlignment { + World = 0, + Wrap = 1, + /// non-official, found in Andromeda (Game Plan 1985) v4.vpx + /// This is not in the official VPX documentation + Unknown = 2, +} + +impl From for RampImageAlignment { + fn from(value: u32) -> Self { + match value { + 0 => RampImageAlignment::World, + 1 => RampImageAlignment::Wrap, + 2 => RampImageAlignment::Unknown, + _ => panic!("Invalid RampImageAlignment {}", value), + } + } +} + +impl From<&RampImageAlignment> for u32 { + fn from(value: &RampImageAlignment) -> Self { + match value { + RampImageAlignment::World => 0, + RampImageAlignment::Wrap => 1, + RampImageAlignment::Unknown => 2, + } + } +} + +/// Serializes RampImageAlignment to lowercase string +impl Serialize for RampImageAlignment { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + RampImageAlignment::World => serializer.serialize_str("world"), + RampImageAlignment::Wrap => serializer.serialize_str("wrap"), + RampImageAlignment::Unknown => serializer.serialize_str("unknown"), + } + } +} + +/// Deserializes RampImageAlignment from lowercase string +/// or number for backwards compatibility +impl<'de> Deserialize<'de> for RampImageAlignment { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct RampImageAlignmentVisitor; + + impl<'de> serde::de::Visitor<'de> for RampImageAlignmentVisitor { + type Value = RampImageAlignment; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or number representing a TargetType") + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + match value { + 0 => Ok(RampImageAlignment::World), + 1 => Ok(RampImageAlignment::Wrap), + 2 => Ok(RampImageAlignment::Unknown), + _ => Err(serde::de::Error::unknown_variant( + &value.to_string(), + &["0", "1"], + )), + } + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + match value { + "world" => Ok(RampImageAlignment::World), + "wrap" => Ok(RampImageAlignment::Wrap), + "unknown" => Ok(RampImageAlignment::Unknown), + _ => Err(serde::de::Error::unknown_variant(value, &["world", "wrap"])), + } + } + } + + deserializer.deserialize_any(RampImageAlignmentVisitor) + } +} + +#[cfg(test)] +mod test { + use crate::vpx::gameitem::ramp_image_alignment::RampImageAlignment; + + #[test] + fn test_alignment_json() { + let sizing_type = RampImageAlignment::Wrap; + let json = serde_json::to_string(&sizing_type).unwrap(); + pretty_assertions::assert_eq!(json, "\"wrap\""); + let sizing_type_read: RampImageAlignment = serde_json::from_str(&json).unwrap(); + pretty_assertions::assert_eq!(sizing_type, sizing_type_read); + let json = serde_json::Value::from(0); + let sizing_type_read: RampImageAlignment = serde_json::from_value(json).unwrap(); + pretty_assertions::assert_eq!(RampImageAlignment::World, sizing_type_read); + } + + #[test] + #[should_panic = "Error(\"unknown variant `foo`, expected `world` or `wrap`\", line: 0, column: 0)"] + fn test_alignment_json_fail() { + let json = serde_json::Value::from("foo"); + let _: RampImageAlignment = serde_json::from_value(json).unwrap(); + } +} diff --git a/src/vpx/material.rs b/src/vpx/material.rs index cb6827d..c16e9ae 100644 --- a/src/vpx/material.rs +++ b/src/vpx/material.rs @@ -1,39 +1,95 @@ use crate::vpx::biff; use crate::vpx::biff::{BiffRead, BiffReader, BiffWrite, BiffWriter}; -use crate::vpx::color::{Color, ColorJson}; +use crate::vpx::color::{Color, ColorJson, ColorNoAlpha}; use crate::vpx::json::F32WithNanInf; use crate::vpx::math::quantize_u8; use bytes::{Buf, BufMut, BytesMut}; use encoding_rs::mem::{decode_latin1, encode_latin1_lossy}; use fake::Dummy; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::ffi::CStr; use std::io; const MAX_NAME_BUFFER: usize = 32; -#[derive(Dummy, Debug, Clone, PartialEq, Serialize, Deserialize)] -enum MaterialType { - Basic, - Metal, +#[derive(Dummy, Debug, Clone, PartialEq)] +pub enum MaterialType { + Unknown = -1, // found in Hot Line (Williams 1966) SG1bsoN.vpx + Basic = 0, + Metal = 1, } -impl MaterialType { - fn from_i32(i: i32) -> Self { - match i { +impl From for MaterialType { + fn from(value: i32) -> Self { + match value { + -1 => MaterialType::Unknown, 0 => MaterialType::Basic, 1 => MaterialType::Metal, - _ => panic!("Unknown MaterialType {}", i), + _ => panic!("Invalid MaterialType {}", value), } } - fn to_i32(&self) -> i32 { - match self { +} + +impl From<&MaterialType> for i32 { + fn from(value: &MaterialType) -> Self { + match value { + MaterialType::Unknown => -1, MaterialType::Basic => 0, MaterialType::Metal => 1, } } } +/// Serialize to lowercase string +impl Serialize for MaterialType { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + MaterialType::Unknown => serializer.serialize_str("unknown"), + MaterialType::Basic => serializer.serialize_str("basic"), + MaterialType::Metal => serializer.serialize_str("metal"), + } + } +} + +/// Deserialize from lowercase string +/// or case-insensitive string for backwards compatibility +impl<'de> Deserialize<'de> for MaterialType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MaterialTypeVisitor; + + impl<'de> serde::de::Visitor<'de> for MaterialTypeVisitor { + type Value = MaterialType; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing a MaterialType") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + match value.to_lowercase().as_str() { + "unknown" => Ok(MaterialType::Unknown), + "basic" => Ok(MaterialType::Basic), + "metal" => Ok(MaterialType::Metal), + _ => Err(serde::de::Error::unknown_variant( + value, + &["basic", "metal"], + )), + } + } + } + + deserializer.deserialize_str(MaterialTypeVisitor) + } +} + /** * Only used for backward compatibility loading and saving (VPX version < 10.8) */ @@ -44,7 +100,7 @@ pub struct SaveMaterial { * Base color of the material * Can be overridden by texture on object itself */ - pub base_color: Color, + pub base_color: ColorNoAlpha, /** * Specular of glossy layer */ @@ -153,7 +209,7 @@ impl From<&Material> for SaveMaterial { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct SaveMaterialJson { name: String, - base_color: ColorJson, + base_color: ColorNoAlpha, glossy_color: ColorJson, clearcoat_color: ColorJson, wrap_lighting: f32, @@ -170,7 +226,7 @@ impl SaveMaterialJson { pub fn from_save_material(save_material: &SaveMaterial) -> Self { Self { name: save_material.name.clone(), - base_color: ColorJson::from_color(&save_material.base_color), + base_color: save_material.base_color, glossy_color: ColorJson::from_color(&save_material.glossy_color), clearcoat_color: ColorJson::from_color(&save_material.clearcoat_color), wrap_lighting: save_material.wrap_lighting, @@ -186,7 +242,7 @@ impl SaveMaterialJson { pub fn to_save_material(&self) -> SaveMaterial { SaveMaterial { name: self.name.clone(), - base_color: self.base_color.to_color(), + base_color: self.base_color, glossy_color: self.glossy_color.to_color(), clearcoat_color: self.clearcoat_color.to_color(), wrap_lighting: self.wrap_lighting, @@ -229,7 +285,7 @@ impl SaveMaterial { SaveMaterial { name, - base_color: Color::from_argb(base_color), + base_color: ColorNoAlpha::from_rgb(base_color), glossy_color: Color::from_argb(glossy_color), clearcoat_color: Color::from_argb(clearcoat_color), wrap_lighting, @@ -245,7 +301,7 @@ impl SaveMaterial { pub(crate) fn write(&self, bytes: &mut BytesMut) { write_padded_cstring(self.name.as_str(), bytes, MAX_NAME_BUFFER); - bytes.put_u32_le(self.base_color.argb()); + bytes.put_u32_le(self.base_color.to_rgb()); bytes.put_u32_le(self.glossy_color.argb()); bytes.put_u32_le(self.clearcoat_color.argb()); bytes.put_f32_le(self.wrap_lighting); @@ -401,21 +457,22 @@ fn get_padding_3_validate(bytes: &mut BytesMut) { #[derive(Dummy, Debug, PartialEq)] pub struct Material { - name: String, + pub name: String, // shading properties - type_: MaterialType, - wrap_lighting: f32, - roughness: f32, - glossy_image_lerp: f32, - thickness: f32, - edge: f32, - edge_alpha: f32, - opacity: f32, - base_color: Color, - glossy_color: Color, - clearcoat_color: Color, - opacity_active: bool, + pub type_: MaterialType, + pub wrap_lighting: f32, + pub roughness: f32, + pub glossy_image_lerp: f32, + pub thickness: f32, + pub edge: f32, + pub edge_alpha: f32, + pub opacity: f32, + pub base_color: ColorNoAlpha, + pub glossy_color: Color, + pub clearcoat_color: Color, + // Transparency active in the UI + pub opacity_active: bool, // physic properties elasticity: f32, @@ -437,7 +494,7 @@ pub(crate) struct MaterialJson { edge: f32, edge_alpha: f32, opacity: f32, - base_color: ColorJson, + base_color: ColorNoAlpha, glossy_color: ColorJson, clearcoat_color: ColorJson, opacity_active: bool, @@ -460,7 +517,7 @@ impl MaterialJson { edge: material.edge, edge_alpha: material.edge_alpha, opacity: material.opacity, - base_color: ColorJson::from_color(&material.base_color), + base_color: material.base_color, glossy_color: ColorJson::from_color(&material.glossy_color), clearcoat_color: ColorJson::from_color(&material.clearcoat_color), opacity_active: material.opacity_active, @@ -482,7 +539,7 @@ impl MaterialJson { edge: self.edge, edge_alpha: self.edge_alpha, opacity: self.opacity, - base_color: self.base_color.to_color(), + base_color: self.base_color, glossy_color: self.glossy_color.to_color(), clearcoat_color: self.clearcoat_color.to_color(), opacity_active: self.opacity_active, @@ -506,7 +563,7 @@ impl Default for Material { edge: 1.0, edge_alpha: 1.0, opacity: 1.0, - base_color: Color::from_argb(0xB469FF), + base_color: ColorNoAlpha::from_rgb(0xB469FF), // Purple / Heliotrope glossy_color: Color::from_argb(0), clearcoat_color: Color::from_argb(0), opacity_active: false, @@ -524,7 +581,7 @@ impl Default for SaveMaterial { fn default() -> Self { SaveMaterial { name: "dummyMaterial".to_string(), - base_color: Color::from_argb(0xB469FF), + base_color: ColorNoAlpha::from_rgb(0xB469FF), glossy_color: Color::from_argb(0), clearcoat_color: Color::from_argb(0), wrap_lighting: 0.0, @@ -581,7 +638,7 @@ impl BiffRead for Material { let tag = reader.tag(); let tag_str = tag.as_str(); match tag_str { - "TYPE" => material.type_ = MaterialType::from_i32(reader.get_i32()), + "TYPE" => material.type_ = reader.get_i32().into(), "NAME" => material.name = reader.get_string(), "WLIG" => material.wrap_lighting = reader.get_f32(), "ROUG" => material.roughness = reader.get_f32(), @@ -590,7 +647,7 @@ impl BiffRead for Material { "EDGE" => material.edge = reader.get_f32(), "EALP" => material.edge_alpha = reader.get_f32(), "OPAC" => material.opacity = reader.get_f32(), - "BASE" => material.base_color = Color::from_argb(reader.get_u32()), + "BASE" => material.base_color = ColorNoAlpha::biff_read(reader), "GLOS" => material.glossy_color = Color::from_argb(reader.get_u32()), "COAT" => material.clearcoat_color = Color::from_argb(reader.get_u32()), "RTNT" => material.refraction_tint = Color::from_argb(reader.get_u32()), @@ -615,7 +672,7 @@ impl BiffRead for Material { impl BiffWrite for Material { fn biff_write(&self, writer: &mut BiffWriter) { - writer.write_tagged_i32("TYPE", self.type_.to_i32()); + writer.write_tagged_i32("TYPE", (&self.type_).into()); writer.write_tagged_string("NAME", &self.name); writer.write_tagged_f32("WLIG", self.wrap_lighting); writer.write_tagged_f32("ROUG", self.roughness); @@ -624,7 +681,7 @@ impl BiffWrite for Material { writer.write_tagged_f32("EDGE", self.edge); writer.write_tagged_f32("EALP", self.edge_alpha); writer.write_tagged_f32("OPAC", self.opacity); - writer.write_tagged_u32("BASE", self.base_color.argb()); + writer.write_tagged_with("BASE", &self.base_color, ColorNoAlpha::biff_write); writer.write_tagged_u32("GLOS", self.glossy_color.argb()); writer.write_tagged_u32("COAT", self.clearcoat_color.argb()); writer.write_tagged_u32("RTNT", self.refraction_tint.argb()); @@ -697,7 +754,7 @@ mod tests { edge: 0.5, edge_alpha: 0.9, opacity: 0.5, - base_color: Color::from_argb(0x123456), + base_color: ColorNoAlpha::from_rgb(0x123456), glossy_color: Color::from_argb(0x123456), clearcoat_color: Color::from_argb(0x123456), opacity_active: true, @@ -713,4 +770,20 @@ mod tests { assert_eq!(save_material.thickness, 128); assert_eq!(save_material.opacity_active_edge_alpha, 231); } + + #[test] + fn test_material_type_json() { + let sizing_type = MaterialType::Metal; + let json = serde_json::to_string(&sizing_type).unwrap(); + assert_eq!(json, "\"metal\""); + let sizing_type_read: MaterialType = serde_json::from_str(&json).unwrap(); + assert_eq!(sizing_type, sizing_type_read); + } + + #[test] + #[should_panic = "Error(\"unknown variant `foo`, expected `basic` or `metal`\", line: 0, column: 0)"] + fn test_material_type_json_fail() { + let json = serde_json::Value::from("foo"); + let _: MaterialType = serde_json::from_value(json).unwrap(); + } } diff --git a/src/vpx/mod.rs b/src/vpx/mod.rs index 1448232..3412445 100644 --- a/src/vpx/mod.rs +++ b/src/vpx/mod.rs @@ -82,7 +82,7 @@ pub(crate) mod wav; /// println!("table name: {}", vpx.info.table_name.unwrap_or("unknown".to_string())); /// ``` -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Default)] pub struct VPX { /// This is mainly here to have an ordering for custom info tags pub custominfotags: CustomInfoTags, // this is a bit redundant @@ -96,6 +96,19 @@ pub struct VPX { pub collections: Vec, } +impl VPX { + pub fn add_game_item(&mut self, item: GameItemEnum) -> &Self { + self.gameitems.push(item); + self.gamedata.gameitems_size = self.gameitems.len() as u32; + self + } + + pub fn set_script(&mut self, script: String) -> &Self { + self.gamedata.set_code(script); + self + } +} + pub enum ExtractResult { Extracted(PathBuf), Existed(PathBuf), @@ -208,7 +221,7 @@ pub fn read(path: &PathBuf) -> io::Result { /// Writes a VPX file from memory to disk /// /// see also [`read()`] -pub fn write(path: &PathBuf, vpx: &VPX) -> io::Result<()> { +pub fn write>(path: P, vpx: &VPX) -> io::Result<()> { let file = File::options() .read(true) .write(true) @@ -242,17 +255,17 @@ fn read_vpx(comp: &mut CompoundFile) -> io::Result(comp: &mut CompoundFile, original: &VPX) -> io::Result<()> { +fn write_vpx(comp: &mut CompoundFile, vpx: &VPX) -> io::Result<()> { create_game_storage(comp)?; - write_custominfotags(comp, &original.custominfotags)?; - write_tableinfo(comp, &original.info)?; - write_version(comp, &original.version)?; - write_game_data(comp, &original.gamedata, &original.version)?; - write_game_items(comp, &original.gameitems)?; - write_images(comp, &original.images)?; - write_sounds(comp, &original.sounds, &original.version)?; - write_fonts(comp, &original.fonts)?; - write_collections(comp, &original.collections)?; + write_custominfotags(comp, &vpx.custominfotags)?; + write_tableinfo(comp, &vpx.info)?; + write_version(comp, &vpx.version)?; + write_game_data(comp, &vpx.gamedata, &vpx.version)?; + write_game_items(comp, &vpx.gameitems)?; + write_images(comp, &vpx.images)?; + write_sounds(comp, &vpx.sounds, &vpx.version)?; + write_fonts(comp, &vpx.fonts)?; + write_collections(comp, &vpx.collections)?; let mac = generate_mac(comp)?; write_mac(comp, &mac) } @@ -846,7 +859,7 @@ mod tests { let mac = read_mac(&mut comp)?; let expected = [ - 197, 157, 117, 26, 180, 53, 40, 250, 243, 252, 134, 86, 190, 22, 83, 119, + 244, 78, 31, 241, 224, 80, 178, 192, 252, 104, 96, 110, 72, 115, 225, 83, ]; assert_eq!(mac, expected); Ok(()) diff --git a/src/vpx/tableinfo.rs b/src/vpx/tableinfo.rs index da92301..a237294 100644 --- a/src/vpx/tableinfo.rs +++ b/src/vpx/tableinfo.rs @@ -28,6 +28,7 @@ pub struct TableInfo { // the keys (and ordering) for these are defined in "GameStg/CustomInfoTags" pub properties: HashMap, } + impl TableInfo { pub(crate) fn new() -> TableInfo { // current data as ISO string diff --git a/src/vpx/version.rs b/src/vpx/version.rs index 4e7d9f4..0e52cad 100644 --- a/src/vpx/version.rs +++ b/src/vpx/version.rs @@ -25,6 +25,12 @@ impl Version { } } +impl Default for Version { + fn default() -> Self { + Version(1080) + } +} + impl Version { pub fn new(version: u32) -> Self { Version(version)