From 52e829862de9ee2a9bee13810e266f4890e1981e Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 19 Apr 2024 18:26:44 +0200 Subject: [PATCH 01/10] docs: example for programmatic table creation --- basic.vpx | Bin 0 -> 20480 bytes examples/basic.vbs | 32 ++++++++++ examples/create_basic_vpx_file.rs | 64 +++++++++++++++++++ src/vpx/color.rs | 98 ++++++++++++++++++++++++------ src/vpx/gamedata.rs | 14 ++--- src/vpx/gameitem/flipper.rs | 6 +- src/vpx/gameitem/plunger.rs | 47 +++++++++++++- src/vpx/material.rs | 34 +++++------ src/vpx/mod.rs | 39 ++++++++---- src/vpx/tableinfo.rs | 1 + src/vpx/version.rs | 6 ++ 11 files changed, 281 insertions(+), 60 deletions(-) create mode 100644 basic.vpx create mode 100644 examples/basic.vbs create mode 100644 examples/create_basic_vpx_file.rs diff --git a/basic.vpx b/basic.vpx new file mode 100644 index 0000000000000000000000000000000000000000..7c621ff286f45de133f219ac19982bdcaba1ae5b GIT binary patch literal 20480 zcmeHNUu+yl86PK&+n7+&U`qecur;AgLNRgdHjdqt?CrnXWcPNpyS~JJfZFk%ed2s~ za?T;?0}2R<2Pz>d6p;#{ph`%TJb=`PhzEqiOCM6fTP4(o=7l$)JWwPgVt(Ju&fe9o z$=WqYsb;Ou^WEGx^PBJYeX}#Ow}1S@L+}3V*N@$1YpxgAeeBNa9@d}L_o0-+tM)Rs zBdfc!y1JT~F{tXghZvA?Q>9`+F`yVw3@8Q^1BwB~fMP%~pcqgLe7qU>H_obbewT6& z9bMH>6$6R^#eiZ!F`yVw3@8Q^1BwB~fMP%~pcp8~K*U=3>oAR3tch}&UE0(GkB8Z= zJ_$wd2Nbz>!szm+gr&n~*%C`x4D5v>jn~-hWqll<`lR;}KJ5D2;E^XTyz-|$S@zl( zY=xZ%b_=6$!9WdKf-xk;1e#rr&1K3 z^5j4k0EzDcJw*;2+2jBQGyMBEf&T^A{wtd-Yw<_6h)?bm_5Vw-eLurli+^;B_`5fO z{}tGNX9NF7w}?-^(^aJTb1?dDhVL_-8ATdxVDms0e}P^M{{;HN>r>luQ7%jDqW*6S zaMp>!SNyB|>oTyNbE`;SYW}JD*Ui3S>rauqDr%$hPvxJ=zb*qs=goH6ebo6UfA^>J zGo8uk97ks=N@@_C2`c@i=Np>Z5Q`9;W6kf$LBA%`G`A@uz-D8CFD&OXywjm}GSW}$P# z801+9*&ahV4ml2)fJ{Q3he&_B5mTCCKrx^gPz)#r6a$I@#eiZ!F`yVw3@8RZ{tVEW z->R4;6kEE+1Vwp?-TkoWAEu%{S0H}-;*929&9duHPc~}>#z|zaBYbN+*}84 zt^*gk#I@1qJJ}d)^m+ZJz_romn})!$&(>-`&v5PO+b_K#^&B^dqEO;GdNujhFK%-i zJ2H}2uUbq8Pbv6WoESI@_Z`iiu1eWPpRxikjR)^l_FIHtT3^>)cPEp>-;_@+V6 zv_j9<>DgDTNUVxouqolv*H{3cX876fCBffAg~&Me368Hj~;q)^5ZL4 z_!vs2AD|092FKKmzDzH{rdIni@5k3;muzUx`g&shMU)X?C)1&YfC1P+%**FbSL`IqpIu!Mm?jh_ zCBc1|*IQ^TZA8Wkd&{$P=k3T{m+|s0M~=ppnb2ph#}!ld7x!dWY*+FOi47uOJl!N> z_CW8xwKKw~1H+i2u~QCgS!hf~U|Rh<84KE-daJ2f7u${cxq4?Hnr+Uvmg+C10|VB@ z^xR4(bsDX?*@ot%og}^3vFnZW;Ly2Nv$Nc4G}7ha3v=g(4h;;L^(Af2Zq(cDbosp1 zoSkc=^8*9%%AA&vKJs<9iQY9_^Xc5m!f<=J-t1^Y<+RagJ)@mzEjQ+$9OB|)lO*1@=7{jHRm<-{{fKG=_RxE&E~<7n(oWF)_y(1x_9 z(q5n4fh(k|CNAciWp_vhd#}s)kkh&9`4P z@BC;dzulfOlQPy=zRs6j2l_Rx&*`{`Yye2dnu*Lk15WdK)T>7ke3G5s(){ma)6^V@b98&zrJ2Y$W>N&FyR z(-Pm!^=e%5(8K?*T-RG-q>$tEJx1Uul-8uG&~+5*kIkzxV;y2S7+?9 zIO8A8id|?%cVvn`?L*}LBhpPQbz&d35tm6hqCFEIfgMCxHQHIe(9-(`k74A-d0qrj zHx9k=FX_IHA@H1vPxt5_5|a`lvc%dvA|t$op;uiQ#Zp|2rG9tc36rd)|6hCj`^NRf z+hV`0$6)I^_Xl$bddPL#3!(4e`!yR&>*5cW_-d_J9B}b>?lW{m;AO4{)$ZO?a3uKJ zh*Oz|lm(I_`n`{Gl5*37=QX@*X(Ko5;6X6Kb7rc`T$GgaU0GD7ovL6r!nBrpoJ0=0 zyqnK<+6!Rc!6Qj~f%gx-!)F)m1@in1rw~bzhrP4Ns&r?R9d&n-HpeY<7K+7!Mz-0j zH-10;=b-r!<9Cyo%;LxO?TDX-J}G{Wpo|c;1-7Un2rlT_-FW%d#J=AiKKzf77e_9f Lnh>@3&j0@a@_i1I literal 0 HcmV?d00001 diff --git a/examples/basic.vbs b/examples/basic.vbs new file mode 100644 index 0000000..9a9f0be --- /dev/null +++ b/examples/basic.vbs @@ -0,0 +1,32 @@ +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 +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 +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/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/plunger.rs b/src/vpx/gameitem/plunger.rs index c8102bd..f37cb80 100644 --- a/src/vpx/gameitem/plunger.rs +++ b/src/vpx/gameitem/plunger.rs @@ -100,7 +100,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 +141,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,6 +326,7 @@ impl<'de> Deserialize<'de> for Plunger { impl BiffRead for Plunger { fn biff_read(reader: &mut BiffReader<'_>) -> Self { + // TODO deduplicate with Default impl let mut center = Vertex2D::default(); let mut width: f32 = 25.0; let mut height: f32 = 20.0; diff --git a/src/vpx/material.rs b/src/vpx/material.rs index cb6827d..62c55c4 100644 --- a/src/vpx/material.rs +++ b/src/vpx/material.rs @@ -1,6 +1,6 @@ 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}; @@ -44,7 +44,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 +153,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 +170,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 +186,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 +229,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 +245,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,7 +401,7 @@ fn get_padding_3_validate(bytes: &mut BytesMut) { #[derive(Dummy, Debug, PartialEq)] pub struct Material { - name: String, + pub name: String, // shading properties type_: MaterialType, @@ -412,7 +412,7 @@ pub struct Material { edge: f32, edge_alpha: f32, opacity: f32, - base_color: Color, + pub base_color: ColorNoAlpha, glossy_color: Color, clearcoat_color: Color, opacity_active: bool, @@ -437,7 +437,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 +460,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 +482,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 +506,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 +524,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, @@ -590,7 +590,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()), @@ -624,7 +624,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 +697,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, 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) From f995b0834c7bc1c34df599fb9b6cbd785587c4f6 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 19 Apr 2024 21:27:21 +0200 Subject: [PATCH 02/10] add plunger code --- basic.ini | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 basic.ini 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 From 0f38a0bf3827733142efe496032b39f99c7a16be Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 19 Apr 2024 21:29:23 +0200 Subject: [PATCH 03/10] add plunger code --- basic.vpx | Bin 20480 -> 20480 bytes examples/basic.vbs | 6 ++ src/vpx/gameitem/plunger.rs | 154 +++++++++--------------------------- 3 files changed, 45 insertions(+), 115 deletions(-) diff --git a/basic.vpx b/basic.vpx index 7c621ff286f45de133f219ac19982bdcaba1ae5b..ec0e097a54326b2ebe630d4576272b88f71d480a 100644 GIT binary patch delta 360 zcmZozz}T>WaYKay6NA9yDupaw%T&3J{zEVj z4#XNjtO>+gK&%bKIzSAP*8|e}Kx_cz8v$u!AT|MFQy?}2Vsjt{skH>sRzPeG#5O=| z3&eIn43Yz3DA@ep;3wZEhe@oAotr1S-e;W5?JlShkW-qMo?7IcTB%Tyk($S)00mHS zy@1l39H+$O?9FNJdSD%j*mQ8hl)GgXrB3efFx`B>qk&BzdsCpdbNkME1;L++Zrg9X I2L^=!0D-r5Hvj+t delta 259 zcmZozz}T>WaYKayQv?6xDupcG_aX+eWs`0(g6YX06+ZLsu!4wy>CGP%*%=vCCjV5b zW>lVBs$9>gJ^3q;)Sq0cQqO2M`KwAjt1SZqgTQ1)Cil$;R8Mf_{`>#`zY0)D6^PY< zSRIHpfLIfVwSX8TuLGoYfmjd9HvrOxKx_oW#z1TW#HK(DQfm&REr8e(h^>Iw8i;Lx z7$o;^v!DPs|0ah?tc+}%C%fKf+`QdgiE*;Hr{-o`&t^6O&YYezRzmx Deserialize<'de> for Plunger { impl BiffRead for Plunger { fn biff_read(reader: &mut BiffReader<'_>) -> Self { - // TODO deduplicate with Default impl - 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; - + let mut plunger = Plunger::default(); loop { reader.next(biff::WARN); if reader.is_eof() { @@ -376,114 +337,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!( @@ -495,44 +456,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 } } From 434a94e3f67986d16fc3f0ab6f2879db5c64777d Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 19 Apr 2024 21:30:18 +0200 Subject: [PATCH 04/10] add plunger code --- src/vpx/gameitem/plunger.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vpx/gameitem/plunger.rs b/src/vpx/gameitem/plunger.rs index 8ec7107..113ae9f 100644 --- a/src/vpx/gameitem/plunger.rs +++ b/src/vpx/gameitem/plunger.rs @@ -1,5 +1,4 @@ use crate::vpx::biff::{self, BiffRead, BiffReader, BiffWrite}; -use crate::vpx::gameitem::GameItemEnum::Plunger; use fake::Dummy; use serde::{Deserialize, Deserializer, Serialize, Serializer}; From 5e29fdfd9205ee2f6e6e17de33f5d1175a4929fe Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 19 Apr 2024 21:55:29 +0200 Subject: [PATCH 05/10] unknown alignment found during tests --- README.md | 4 + src/vpx/gameitem.rs | 1 + src/vpx/gameitem/flasher.rs | 112 +--------------------- src/vpx/gameitem/ramp.rs | 85 +---------------- src/vpx/gameitem/ramp_image_alignment.rs | 116 +++++++++++++++++++++++ 5 files changed, 126 insertions(+), 192 deletions(-) create mode 100644 src/vpx/gameitem/ramp_image_alignment.rs 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/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/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..7a01265 --- /dev/null +++ b/src/vpx/gameitem/ramp_image_alignment.rs @@ -0,0 +1,116 @@ +use fake::Dummy; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, PartialEq, Clone, Dummy)] +pub enum RampImageAlignment { + World = 0, + Wrap = 1, + Unknown = 2, // non-official, found in Andromeda (Game Plan 1985) v4.vpx +} + +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(); + } +} From 2010ccce5693c08df1d88f5c738ad2fc966fbdbb Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 19 Apr 2024 22:23:38 +0200 Subject: [PATCH 06/10] fix default for reading --- src/vpx/gameitem/plunger.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vpx/gameitem/plunger.rs b/src/vpx/gameitem/plunger.rs index 113ae9f..6018a82 100644 --- a/src/vpx/gameitem/plunger.rs +++ b/src/vpx/gameitem/plunger.rs @@ -326,7 +326,11 @@ impl<'de> Deserialize<'de> for Plunger { impl BiffRead for Plunger { fn biff_read(reader: &mut BiffReader<'_>) -> Self { - let mut plunger = Plunger::default(); + // 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() { From 87f9a4a4cf55ac1a4a9db040b367f383bdac1dd3 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 19 Apr 2024 22:41:21 +0200 Subject: [PATCH 07/10] more unknown enum types --- src/vpx/gameitem/plunger.rs | 8 ++++++++ src/vpx/gameitem/ramp_image_alignment.rs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vpx/gameitem/plunger.rs b/src/vpx/gameitem/plunger.rs index 6018a82..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), diff --git a/src/vpx/gameitem/ramp_image_alignment.rs b/src/vpx/gameitem/ramp_image_alignment.rs index 7a01265..d929f1b 100644 --- a/src/vpx/gameitem/ramp_image_alignment.rs +++ b/src/vpx/gameitem/ramp_image_alignment.rs @@ -5,7 +5,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub enum RampImageAlignment { World = 0, Wrap = 1, - Unknown = 2, // non-official, found in Andromeda (Game Plan 1985) v4.vpx + /// non-official, found in Andromeda (Game Plan 1985) v4.vpx + /// This is not in the official VPX documentation + Unknown = 2, } impl From for RampImageAlignment { From 9f756c2a5cf79ba797f37100d2e8f074dface090 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Sat, 20 Apr 2024 00:44:55 +0200 Subject: [PATCH 08/10] delete by accident added vpx --- basic.vpx | Bin 20480 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 basic.vpx diff --git a/basic.vpx b/basic.vpx deleted file mode 100644 index ec0e097a54326b2ebe630d4576272b88f71d480a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeHNU2Ggz6~0cIv@xM1C6xX^n3&QgrIep4%+Am5)H6H7%y<*~0a$xy?N!#h ztIZmcK0u3*3Mx_sLJ_GD3aSL6cib?zN8Z**j{wqf4BF|AO8HFtL(aRoZZ5%t?p(!)Ak|Ml6cb|#GH$9?2q**;0tx|zfI>hapb$_9Cy)5Kssx1QY@a0fm4f#xoJZNTr=;Dwz09{4{D*&QIVA}cR)YsrSDYo>Zd&U zaT)-r-+}f7Iq>i%4cM3JzjYJ(KLy!;ri0~r{bO6yPwwRP|1*$%JJoZ&{=-|;ziSiv z$;Yp);^^@;Z^4vZNp|7Xdxzb z{@yp1RQeVED*yTjY&>Fau7nzYYW(%HFW>reGc!=-pUOX#e|-dQ=Ge;9*Lxj*^1VNu zpXp3S=Quh`(Rqf>6tu6W^W6tPyFniW-46N?i1x61L3e;Y4B7|!2y)5Kssx1QY`Q z4+zki-zu90R9h;O@c6#0r`@t#6hs6@wTPC#^{McjKtrqd2AL~-a zDI2cjU454OK{B>qK|d1(;!nA8)+hK`#@^D?_BqB%za>60T*y5AM9M=Z;~^t>=(VLT z=*@WO&3NbqFZJ5!^NrL78+~57EcDvw^HoFWvQO3PKT7r5vsa&bS@M})KMVt@x2RX* zul(dHm$5@5e&ND}W7Lo3>JjWbE_ge`C$J;_dQ<4qGn~t25&Xpy)b0gEqlQ}WX68#! z@-l2g%gT7u>SONP?*DvVR8$P4_8qZwde_KEM}A=ne3J z8^zx{f1cY=Fg+h#a2txIZVaV-0W$UapLjo>9yugKW7gAS>(8PNNq0OOSg7~|y|V6E zYn(snIvKK|Ka=rAhq6Q%7nh7_fFd%|ETTivZs{(fE_grC;h5kHdSsn%j|-z+pMc=J z;rBB&jLR`*V%~1yW+@{wUdUUnojGrZ&YFyuvm7}ZS!PVXIUkix*mn?lx-&M)>sy zsKFZB!kX@F;qpIW?>wD_Im-@px{VY^rDC2UZ~8!NgI%)2DUcB7lz2wxhM@47w0VCC;cDuz}5^)p{IuYGqrpYBhZaS3ZY-)EGZ zBKV7(pVQ4JaSNY)N}PO|^ekFC zU*dgwmI1v&mmav;jO_*C^iN-UkS`lSWK^V#=X=>6DE9npO^rP#<110Yl{R2cP?mf& z6Y}+y-u=)lc{}O=-!-n_tHMS8jUi2IKMsTOFX)~Q6L?NV(*yhs zVp2dvmRW~KWQ^Zo=oLpsu@F@v$#3jC=_G0C|L5<0&bYL6RqWUG2xwj6{$K!s4>)hT z0r+ivzi4A=U;6ek-!16nBCz-y_Zd7Q@GZ{!mHysWU?lKb4^x?klm!wadf!MnNxA95 z^E$ruw2_+);2@CTIWyBoE{sdrt}raoPFbKEVpvN)io+s%dKVw}SGB$WIAH(s4}TS{`TzeL+Ey?y From c7ca4a2b9563212d3035b3646f65714020c45378 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Sat, 20 Apr 2024 09:07:33 +0200 Subject: [PATCH 09/10] improve material type --- src/vpx/material.rs | 116 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 24 deletions(-) diff --git a/src/vpx/material.rs b/src/vpx/material.rs index 62c55c4..dc85aee 100644 --- a/src/vpx/material.rs +++ b/src/vpx/material.rs @@ -6,34 +6,85 @@ 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 { + Basic = 0, + Metal = 1, } -impl MaterialType { - fn from_i32(i: i32) -> Self { - match i { +impl From for MaterialType { + fn from(value: u32) -> Self { + match value { 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 u32 { + fn from(value: &MaterialType) -> Self { + match value { 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::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() { + "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) */ @@ -404,18 +455,19 @@ pub struct Material { 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, + 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, - glossy_color: Color, - clearcoat_color: Color, - opacity_active: bool, + pub glossy_color: Color, + pub clearcoat_color: Color, + // Transparency active in the UI + pub opacity_active: bool, // physic properties elasticity: f32, @@ -581,7 +633,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_u32().into(), "NAME" => material.name = reader.get_string(), "WLIG" => material.wrap_lighting = reader.get_f32(), "ROUG" => material.roughness = reader.get_f32(), @@ -615,7 +667,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_u32("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); @@ -713,4 +765,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(); + } } From c75101eca298df6ec1357bcb68f8ba2a6a61c329 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Sun, 21 Apr 2024 10:16:01 +0200 Subject: [PATCH 10/10] fix for unknown material --- .gitignore | 3 +++ src/vpx/gameitem/gate.rs | 30 +++++++++++++++++------------- src/vpx/material.rs | 15 ++++++++++----- 3 files changed, 30 insertions(+), 18 deletions(-) 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/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/material.rs b/src/vpx/material.rs index dc85aee..c16e9ae 100644 --- a/src/vpx/material.rs +++ b/src/vpx/material.rs @@ -14,13 +14,15 @@ const MAX_NAME_BUFFER: usize = 32; #[derive(Dummy, Debug, Clone, PartialEq)] pub enum MaterialType { + Unknown = -1, // found in Hot Line (Williams 1966) SG1bsoN.vpx Basic = 0, Metal = 1, } -impl From for MaterialType { - fn from(value: u32) -> Self { +impl From for MaterialType { + fn from(value: i32) -> Self { match value { + -1 => MaterialType::Unknown, 0 => MaterialType::Basic, 1 => MaterialType::Metal, _ => panic!("Invalid MaterialType {}", value), @@ -28,9 +30,10 @@ impl From for MaterialType { } } -impl From<&MaterialType> for u32 { +impl From<&MaterialType> for i32 { fn from(value: &MaterialType) -> Self { match value { + MaterialType::Unknown => -1, MaterialType::Basic => 0, MaterialType::Metal => 1, } @@ -44,6 +47,7 @@ impl Serialize for MaterialType { S: Serializer, { match self { + MaterialType::Unknown => serializer.serialize_str("unknown"), MaterialType::Basic => serializer.serialize_str("basic"), MaterialType::Metal => serializer.serialize_str("metal"), } @@ -71,6 +75,7 @@ impl<'de> Deserialize<'de> for MaterialType { 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( @@ -633,7 +638,7 @@ impl BiffRead for Material { let tag = reader.tag(); let tag_str = tag.as_str(); match tag_str { - "TYPE" => material.type_ = reader.get_u32().into(), + "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(), @@ -667,7 +672,7 @@ impl BiffRead for Material { impl BiffWrite for Material { fn biff_write(&self, writer: &mut BiffWriter) { - writer.write_tagged_u32("TYPE", (&self.type_).into()); + 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);