diff --git a/.gitignore b/.gitignore index e504091..cd8f466 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock /.idea /parsable_macro_derive/target /tests/test-*.obj +/tests/GameSave2.Techblox diff --git a/Cargo.toml b/Cargo.toml index ca5fb10..edcac2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ base64 = "^0.13" num_enum = "^0.5" chrono = {version = "^0.4", optional = true} fasthash = {version = "^0.4", optional = true} +half = {version = "^1.7", optional = true} libfj_parsable_macro_derive = {version = "0.5.3", optional = true} #libfj_parsable_macro_derive = {path = "./parsable_macro_derive", optional = true} obj = {version = "^0.10", optional = true} @@ -33,5 +34,5 @@ tokio = { version = "1.4.0", features = ["macros"]} simple = ["ureq"] robocraft = ["reqwest"] cardlife = ["reqwest"] -techblox = ["chrono", "fasthash", "libfj_parsable_macro_derive"] +techblox = ["chrono", "fasthash", "half", "libfj_parsable_macro_derive"] convert = ["obj", "genmesh"] diff --git a/src/techblox/blocks/mod.rs b/src/techblox/blocks/mod.rs index e174b6e..b0ff2a3 100644 --- a/src/techblox/blocks/mod.rs +++ b/src/techblox/blocks/mod.rs @@ -3,6 +3,7 @@ mod block_entity; mod common_components; mod lookup_tables; +mod wire_entity; pub use block_entity::{BlockEntity}; pub use common_components::{DBEntityStruct, PositionEntityStruct, ScalingEntityStruct, RotationEntityStruct, @@ -10,3 +11,4 @@ SkewComponent, GridRotationStruct, SerializedGridConnectionsEntityStruct, Serial SerializedCubeMaterialStruct, SerializedUniformBlockScaleEntityStruct, SerializedColourParameterEntityStruct, BlockGroupEntityComponent}; pub(crate) use lookup_tables::*; +pub use wire_entity::{SerializedWireEntity, WireSaveDataStruct, SerializedGlobalWireSettingsEntity, GlobalWireSettingsEntityStruct}; diff --git a/src/techblox/blocks/wire_entity.rs b/src/techblox/blocks/wire_entity.rs new file mode 100644 index 0000000..e761c3f --- /dev/null +++ b/src/techblox/blocks/wire_entity.rs @@ -0,0 +1,61 @@ +use crate::techblox::{SerializedEntityComponent, SerializedEntityDescriptor, Parsable}; + +use libfj_parsable_macro_derive::*; + +/// Wire save data +#[derive(Copy, Clone, Parsable)] +pub struct SerializedWireEntity { + /// Wiring save data component + pub save_data_component: WireSaveDataStruct, +} + +impl SerializedEntityDescriptor for SerializedWireEntity { + fn serialized_components() -> u8 { + 1 + } + + fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> { + vec![&self.save_data_component] + } +} + +/// Wire connection information that is saved. +#[derive(Copy, Clone, Parsable)] +pub struct WireSaveDataStruct { + /// Wire source block index in save + pub source_block_index: u32, + /// Wire destination block index in save + pub destination_block_index: u32, + /// Wire source port index + pub source_port_usage: u8, + /// Wire destination port index + pub destination_port_usage: u8, +} + +impl SerializedEntityComponent for WireSaveDataStruct {} + +/// Wire settings data for a game +#[derive(Copy, Clone, Parsable)] +pub struct SerializedGlobalWireSettingsEntity { + /// Global wire settings + pub settings_component: GlobalWireSettingsEntityStruct, +} + +impl SerializedEntityDescriptor for SerializedGlobalWireSettingsEntity { + fn serialized_components() -> u8 { + 1 + } + + fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> { + vec![&self.settings_component] + } +} + +/// Wire settings applied to the whole game save +#[derive(Copy, Clone, Parsable)] +pub struct GlobalWireSettingsEntityStruct { + /// Is using obsolete wiring system? (bool) + pub obsolete: u8, +} + +impl SerializedEntityComponent for GlobalWireSettingsEntityStruct {} diff --git a/src/techblox/camera.rs b/src/techblox/camera.rs new file mode 100644 index 0000000..bc6052d --- /dev/null +++ b/src/techblox/camera.rs @@ -0,0 +1,57 @@ +use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent, UnityFloat3, UnityHalf3}; + +use libfj_parsable_macro_derive::*; + +/// Player editing camera entity descriptor. +#[derive(Copy, Clone, Parsable)] +pub struct SerializedFlyCamEntity { + /// Player camera in-game location + pub rb_component: SerializedRigidBodyEntityStruct, +} + +impl SerializedEntityDescriptor for SerializedFlyCamEntity { + fn serialized_components() -> u8 { + 2 + } + + fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> { + vec![&self.rb_component] + } +} + +/// Physical object info for simulation +#[derive(Copy, Clone, Parsable)] +pub struct SerializedRigidBodyEntityStruct { + /// Rigid body location + pub position: UnityFloat3, +} + +impl SerializedEntityComponent for SerializedRigidBodyEntityStruct {} + +/// Player simulation camera entity descriptor. +#[derive(Copy, Clone, Parsable)] +pub struct SerializedPhysicsCameraEntity { + /// In-game camera location information + pub cam_component: SerializedCameraEntityStruct, +} + +impl SerializedEntityDescriptor for SerializedPhysicsCameraEntity { + fn serialized_components() -> u8 { + 1 + } + + fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> { + vec![&self.cam_component] + } +} + +/// Physics camera component +#[derive(Copy, Clone, Parsable)] +pub struct SerializedCameraEntityStruct { + /// Camera position in game world + pub position: UnityHalf3, + /// Camera euler rotation in game world + pub rotation: UnityHalf3, +} + +impl SerializedEntityComponent for SerializedCameraEntityStruct {} diff --git a/src/techblox/gamesave.rs b/src/techblox/gamesave.rs index 942d1e4..e2458ca 100644 --- a/src/techblox/gamesave.rs +++ b/src/techblox/gamesave.rs @@ -1,8 +1,9 @@ use chrono::{naive::NaiveDate, Datelike}; use std::io::{Read, Write}; -use crate::techblox::{EntityHeader, BlockGroupEntity, parse_i64, parse_u32, Parsable, SerializedEntityDescriptor}; -use crate::techblox::blocks::lookup_hashname; +use crate::techblox::{EntityHeader, BlockGroupEntity, parse_i64, parse_u32, Parsable, SerializedEntityDescriptor, +SerializedFlyCamEntity, SerializedPhysicsCameraEntity}; +use crate::techblox::blocks::{lookup_hashname, SerializedWireEntity, SerializedGlobalWireSettingsEntity}; /// A collection of cubes and other data from a GameSave.techblox file //#[derive(Clone)] @@ -11,14 +12,17 @@ pub struct GameSave { /// This may affect how the rest of the save file was parsed. pub version: NaiveDate, - /// Unused magic value in file header. - pub magic1: i64, + /// Time when file was saved, corresponding to ticks since 0 AD + /// https://docs.microsoft.com/en-us/dotnet/api/system.datetime.ticks?view=netframework-4.7.2 + /// Not used for deserialization so not required to be sensible. + pub ticks: i64, /// Amount of cubes present in the save data, as claimed by the file header. pub cube_len: u32, - /// Unused magic value in file header. - pub magic2: u32, + /// Maximum block entity identifier in the game save. + /// Not used for deserialization so not required to be correct. + pub max_entity_id: u32, /// Amount of block groups, as claimed by the file header. pub group_len: u32, @@ -33,7 +37,34 @@ pub struct GameSave { pub cube_headers: Vec, /// Blocks - pub cube_entities: Vec> + pub cube_entities: Vec>, + + /// Amount of wires in the save data, as claimed by the file. + pub wire_len: u32, + + /// Entity group descriptor for wire entities. + pub wire_headers: Vec, + + /// Wires + pub wire_entities: Vec, + + /// Entity group descriptor for wire settings + pub wire_settings_header: EntityHeader, + + /// Wire settings + pub wire_settings_entity: SerializedGlobalWireSettingsEntity, + + /// Entity group descriptor for player fly camera + pub flycam_header: EntityHeader, + + /// Player edit mode fly camera + pub flycam_entity: SerializedFlyCamEntity, + + /// Entity group descriptor for player simulation mode camera + pub phycam_header: EntityHeader, + + /// Player simulation mode camera + pub phycam_entity: SerializedPhysicsCameraEntity, } impl Parsable for GameSave { @@ -44,9 +75,9 @@ impl Parsable for GameSave { let month = parse_u32(data)?; let day = parse_u32(data)?; let date = NaiveDate::from_ymd(year as i32, month, day); - let magic_val1 = parse_i64(data)?; // unused + let ticks = parse_i64(data)?; // unused let cube_count = parse_u32(data)?; // parsed as i32 in-game for some reason - let magic_val2 = parse_u32(data)?; // unused + let max_e_id = parse_u32(data)?; // unused let group_count = parse_u32(data)?; // parsed as i32 in-game for some reason // parse block groups let mut groups_h = Vec::::with_capacity(group_count as usize); @@ -65,17 +96,47 @@ impl Parsable for GameSave { cubes_h.push(header); cubes_e.push(lookup_hashname(hash, data)?); } - // TODO + + // parse wire data + let wire_count = parse_u32(data)?; + let mut wires_h = Vec::::with_capacity(wire_count as usize); + let mut wires_e = Vec::::with_capacity(wire_count as usize); + for _i in 0..wire_count { + wires_h.push(EntityHeader::parse(data)?); + wires_e.push(SerializedWireEntity::parse(data)?); + } + + // parse global wire settings + let wire_settings_h = EntityHeader::parse(data)?; + let wire_settings_e = SerializedGlobalWireSettingsEntity::parse(data)?; + + // parse player cameras + let flycam_h = EntityHeader::parse(data)?; + let flycam_e = SerializedFlyCamEntity::parse(data)?; + + let phycam_h = EntityHeader::parse(data)?; + let phycam_e = SerializedPhysicsCameraEntity::parse(data)?; + + // build struct Ok(Self { version: date, - magic1: magic_val1, + ticks: ticks, cube_len: cube_count, - magic2: magic_val2, + max_entity_id: max_e_id, group_len: group_count, group_headers: groups_h, cube_groups: groups_e, cube_headers: cubes_h, cube_entities: cubes_e, + wire_len: wire_count, + wire_headers: wires_h, + wire_entities: wires_e, + wire_settings_header: wire_settings_h, + wire_settings_entity: wire_settings_e, + flycam_header: flycam_h, + flycam_entity: flycam_e, + phycam_header: phycam_h, + phycam_entity: phycam_e, }) } @@ -85,12 +146,13 @@ impl Parsable for GameSave { write_count += self.version.year().dump(writer)?; write_count += self.version.month().dump(writer)?; write_count += self.version.day().dump(writer)?; - // magic separator - write_count += self.magic1.dump(writer)?; + // unused separator \/ + write_count += self.ticks.dump(writer)?; write_count += self.cube_len.dump(writer)?; - // magic separator - write_count += self.magic2.dump(writer)?; + // unused separator \/ + write_count += self.max_entity_id.dump(writer)?; write_count += self.group_len.dump(writer)?; + // dump block groups for i in 0..self.group_len as usize { write_count += self.group_headers[i].dump(writer)?; @@ -102,13 +164,30 @@ impl Parsable for GameSave { write_count += self.cube_headers[i].dump(writer)?; write_count += self.cube_entities[i].dump(writer)?; } - // TODO + + // dump wire data + write_count += self.wire_len.dump(writer)?; + for i in 0..self.wire_len as usize { + write_count += self.wire_headers[i].dump(writer)?; + write_count += self.wire_entities[i].dump(writer)?; + } + + // dump global wire settings + write_count += self.wire_settings_header.dump(writer)?; + write_count += self.wire_settings_entity.dump(writer)?; + + // dump player cameras + write_count += self.flycam_header.dump(writer)?; + write_count += self.flycam_entity.dump(writer)?; + + write_count += self.phycam_header.dump(writer)?; + write_count += self.phycam_entity.dump(writer)?; Ok(write_count) } } impl std::string::ToString for GameSave { fn to_string(&self) -> String { - format!("{}g {}c (v{})", self.group_len, self.cube_len, self.version) + format!("{}g {}c {}w (v{})", self.group_len, self.cube_len, self.wire_len, self.version) } } diff --git a/src/techblox/mod.rs b/src/techblox/mod.rs index 1bda602..d54948c 100644 --- a/src/techblox/mod.rs +++ b/src/techblox/mod.rs @@ -1,6 +1,7 @@ //! Techblox APIs and functionality (WIP). pub mod blocks; +mod camera; mod gamesave; mod entity_header; mod entity_traits; @@ -10,10 +11,12 @@ mod unity_types; mod parsing_tools; mod murmur; +pub use camera::{SerializedFlyCamEntity, SerializedRigidBodyEntityStruct, +SerializedPhysicsCameraEntity, SerializedCameraEntityStruct}; pub use gamesave::{GameSave}; pub use entity_header::{EntityHeader, EntityGroupID}; pub use entity_traits::{Parsable, SerializedEntityComponent, SerializedEntityDescriptor}; pub use block_group_entity::{BlockGroupEntity, BlockGroupTransformEntityComponent, SavedBlockGroupIdComponent}; -pub use unity_types::{UnityFloat3, UnityFloat4, UnityQuaternion, UnityFloat4x4}; +pub use unity_types::{UnityFloat3, UnityHalf3, UnityFloat4, UnityQuaternion, UnityFloat4x4}; pub(crate) use parsing_tools::*; pub(crate) use murmur::*; diff --git a/src/techblox/parsing_tools.rs b/src/techblox/parsing_tools.rs index e23ebe3..86fa4b1 100644 --- a/src/techblox/parsing_tools.rs +++ b/src/techblox/parsing_tools.rs @@ -1,5 +1,6 @@ use std::io::{Read, Write}; use crate::techblox::Parsable; +use half::f16; // reading @@ -149,3 +150,16 @@ impl Parsable for f32 { writer.write(&self.to_le_bytes()) } } + + +impl Parsable for f16 { + fn parse(reader: &mut dyn Read) -> std::io::Result { + let mut buf = [0; 2]; + reader.read(&mut buf)?; + Ok(Self::from_le_bytes(buf)) + } + + fn dump(&self, writer: &mut dyn Write) -> std::io::Result { + writer.write(&self.to_le_bytes()) + } +} diff --git a/src/techblox/unity_types.rs b/src/techblox/unity_types.rs index f62d44a..6e32f1a 100644 --- a/src/techblox/unity_types.rs +++ b/src/techblox/unity_types.rs @@ -1,5 +1,6 @@ use crate::techblox::{Parsable}; use libfj_parsable_macro_derive::*; +use half::f16; /// Unity-like floating-point vector for 3-dimensional space. #[derive(Clone, Copy, Parsable)] @@ -12,6 +13,17 @@ pub struct UnityFloat3 { pub z: f32, } +/// Unity-like half-precision vector for 3-dimensional space. +#[derive(Clone, Copy, Parsable)] +pub struct UnityHalf3 { + /// x coordinate + pub x: f16, + /// y coordinate + pub y: f16, + /// z coordinate + pub z: f16, +} + /// Unity-like floating-point vector for 4-dimensional space. #[derive(Clone, Copy, Parsable)] pub struct UnityFloat4 { diff --git a/tests/techblox_parsing.rs b/tests/techblox_parsing.rs index 3c81964..9453363 100644 --- a/tests/techblox_parsing.rs +++ b/tests/techblox_parsing.rs @@ -1,18 +1,24 @@ #[cfg(feature = "techblox")] use libfj::techblox; #[cfg(feature = "techblox")] -use libfj::techblox::{SerializedEntityDescriptor, Parsable, blocks}; +use libfj::techblox::{SerializedEntityDescriptor, Parsable, blocks, EntityHeader}; #[cfg(feature = "techblox")] -use std::io::Read; +use std::io::{Read, Seek}; #[cfg(feature = "techblox")] -use std::fs::File; +use std::fs::{File, OpenOptions}; #[cfg(feature = "techblox")] const GAMESAVE_PATH: &str = "tests/GameSave.Techblox"; +#[cfg(feature = "techblox")] +const GAMESAVE_PATH2: &str = "tests/GameSave2.Techblox"; #[cfg(feature = "techblox")] const HASHNAMES: &[&str] = &[ "StandardBlockEntityDescriptorV4", + "WireEntityDescriptorMock", + "GlobalWireSettingsEntityDescriptor", + "FlyCamEntityDescriptorV0", + "CharacterCameraEntityDescriptorV1", ]; #[cfg(feature = "techblox")] @@ -41,6 +47,15 @@ fn techblox_gamesave_parse() -> Result<(), ()> { assert!(gs.cube_headers[i].component_count >= blocks::BlockEntity::serialized_components()); //println!("#{} components: {}", i, gs.cube_headers[i].component_count); } + + //println!("Parsed wire settings hash: {} obsolete? {}", gs.wire_settings_header.hash, gs.wire_settings_entity.settings_component.obsolete != 0); + assert_eq!(gs.wire_settings_header.hash, EntityHeader::from_name("GlobalWireSettingsEntityDescriptor", 0, 0, 0).hash); + + //println!("Parsed Flycam hash: {}", gs.flycam_header.hash); + assert_eq!(gs.flycam_header.hash, EntityHeader::from_name("FlyCamEntityDescriptorV0", 0, 0, 0).hash); + + //println!("Parsed Phycam hash: {}", gs.phycam_header.hash); + assert_eq!(gs.phycam_header.hash, EntityHeader::from_name("CharacterCameraEntityDescriptorV1", 0, 0, 0).hash); println!("{}", gs.to_string()); Ok(()) } @@ -65,3 +80,21 @@ fn hash_tb_name() { println!("MurmurHash3: {} -> {}", name, crate::techblox::EntityHeader::from_name(name, 0, 0, 0).hash); } } + +#[cfg(feature = "techblox")] +#[test] +fn techblox_gamesave_perfect_parse() -> Result<(), ()> { + let mut in_file = File::open(GAMESAVE_PATH).map_err(|_| ())?; + let mut buf = Vec::new(); + in_file.read_to_end(&mut buf).map_err(|_| ())?; + let gs = techblox::GameSave::parse(&mut buf.as_slice()).map_err(|_| ())?; + let mut out_file = OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(GAMESAVE_PATH2) + .map_err(|_| ())?; + gs.dump(&mut out_file).map_err(|_| ())?; + assert_eq!(in_file.stream_position().unwrap(), out_file.stream_position().unwrap()); + Ok(()) +}