@@ -3,3 +3,4 @@ Cargo.lock | |||
/.idea | |||
/parsable_macro_derive/target | |||
/tests/test-*.obj | |||
/tests/GameSave2.Techblox |
@@ -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"] |
@@ -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}; |
@@ -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 {} |
@@ -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 {} |
@@ -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<EntityHeader>, | |||
/// Blocks | |||
pub cube_entities: Vec<Box<dyn SerializedEntityDescriptor>> | |||
pub cube_entities: Vec<Box<dyn SerializedEntityDescriptor>>, | |||
/// 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<EntityHeader>, | |||
/// Wires | |||
pub wire_entities: Vec<SerializedWireEntity>, | |||
/// 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::<EntityHeader>::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::<EntityHeader>::with_capacity(wire_count as usize); | |||
let mut wires_e = Vec::<SerializedWireEntity>::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) | |||
} | |||
} |
@@ -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::*; |
@@ -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<Self> { | |||
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<usize> { | |||
writer.write(&self.to_le_bytes()) | |||
} | |||
} |
@@ -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 { | |||
@@ -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(()) | |||
} |