From 705fae29b3a23ed6d582313726aa97db80e9b760 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 1 Jun 2021 20:50:23 -0400 Subject: [PATCH] Implement rudementary Techblox save parsing --- .gitignore | 1 + Cargo.toml | 11 +- parsable_macro_derive/Cargo.toml | 16 +++ parsable_macro_derive/src/lib.rs | 49 ++++++++ src/cardlife/mod.rs | 2 +- src/cardlife_simple/mod.rs | 2 +- src/lib.rs | 12 +- src/robocraft/cubes.rs | 6 +- src/robocraft/mod.rs | 2 +- src/techblox/block_group_entity.rs | 44 +++++++ src/techblox/blocks/block_entity.rs | 58 +++++++++ src/techblox/blocks/common_components.rs | 113 +++++++++++++++++ src/techblox/blocks/lookup_tables.rs | 16 +++ src/techblox/blocks/mod.rs | 12 ++ src/techblox/entity_header.rs | 54 ++++++++ src/techblox/entity_traits.rs | 26 ++++ src/techblox/gamesave.rs | 114 +++++++++++++++++ src/techblox/mod.rs | 19 +++ src/techblox/murmur.rs | 74 +++++++++++ src/techblox/parsing_tools.rs | 151 +++++++++++++++++++++++ src/techblox/unity_types.rs | 46 +++++++ test.sh | 4 + tests/GameSave.Techblox | Bin 0 -> 207376 bytes tests/cardlife_live.rs | 9 +- tests/clre_server.rs | 7 +- tests/robocraft_factory.rs | 10 ++ tests/robocraft_factory_simple.rs | 16 +-- tests/techblox_parsing.rs | 67 ++++++++++ 28 files changed, 918 insertions(+), 23 deletions(-) create mode 100644 parsable_macro_derive/Cargo.toml create mode 100644 parsable_macro_derive/src/lib.rs create mode 100644 src/techblox/block_group_entity.rs create mode 100644 src/techblox/blocks/block_entity.rs create mode 100644 src/techblox/blocks/common_components.rs create mode 100644 src/techblox/blocks/lookup_tables.rs create mode 100644 src/techblox/blocks/mod.rs create mode 100644 src/techblox/entity_header.rs create mode 100644 src/techblox/entity_traits.rs create mode 100644 src/techblox/gamesave.rs create mode 100644 src/techblox/mod.rs create mode 100644 src/techblox/murmur.rs create mode 100644 src/techblox/parsing_tools.rs create mode 100644 src/techblox/unity_types.rs create mode 100755 test.sh create mode 100644 tests/GameSave.Techblox create mode 100644 tests/techblox_parsing.rs diff --git a/.gitignore b/.gitignore index 2de3917..ccb41cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock /.idea +/parsable_macro_derive/target diff --git a/Cargo.toml b/Cargo.toml index 2b74aef..1fa37e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfj" -version = "0.4.1" +version = "0.5.1" authors = ["NGnius (Graham) "] edition = "2018" description = "An unofficial collection of APIs used in FreeJam games and mods" @@ -14,14 +14,21 @@ readme = "README.md" [dependencies] serde = { version = "^1", features = ["derive"]} serde_json = "^1" -reqwest = { version = "^0.11", features = ["json"]} +reqwest = { version = "^0.11", features = ["json"], optional = true} url = "^2.2" ureq = { version = "^2", features = ["json"], optional = true} base64 = "^0.13" num_enum = "^0.5" +chrono = {version = "^0.4", optional = true} +fasthash = {version = "^0.4", optional = true} +libfj_parsable_macro_derive = {version = "0.5.3", optional = true} +#libfj_parsable_macro_derive = {path = "./parsable_macro_derive", optional = true} [dev-dependencies] tokio = { version = "1.4.0", features = ["macros"]} [features] simple = ["ureq"] +robocraft = ["reqwest"] +cardlife = ["reqwest"] +techblox = ["chrono", "fasthash", "libfj_parsable_macro_derive"] diff --git a/parsable_macro_derive/Cargo.toml b/parsable_macro_derive/Cargo.toml new file mode 100644 index 0000000..e6eac35 --- /dev/null +++ b/parsable_macro_derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "libfj_parsable_macro_derive" +version = "0.5.3" +authors = ["NGnius (Graham) "] +edition = "2018" +description = "An unofficial collection of APIs used in FreeJam games and mods" +license = "MIT" +homepage = "https://github.com/NGnius/libfj" +repository = "https://github.com/NGnius/libfj" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" diff --git a/parsable_macro_derive/src/lib.rs b/parsable_macro_derive/src/lib.rs new file mode 100644 index 0000000..89e377b --- /dev/null +++ b/parsable_macro_derive/src/lib.rs @@ -0,0 +1,49 @@ +//! Macro implementation for using #[derive(Parsable)] +extern crate proc_macro; + +use proc_macro::{TokenStream}; +use syn::{DeriveInput, Data}; +use quote::quote; + +/// Macro generator +#[proc_macro_derive(Parsable)] +pub fn derive_parsable(struc: TokenStream) -> TokenStream { + let ast: &DeriveInput = &syn::parse(struc).unwrap(); + let name = &ast.ident; + if let Data::Struct(data_struct) = &ast.data { + let mut p_fields_gen = vec![]; + let mut d_fields_gen = vec![]; + for field in &data_struct.fields { + let field_ident = &field.ident.clone().expect("Expected named field"); + let field_type = &field.ty; + p_fields_gen.push( + quote! { + #field_ident: <#field_type>::parse(data)? + } + ); + d_fields_gen.push( + quote! { + self.#field_ident.dump(data)?; + } + ); + } + let final_gen = quote! { + impl Parsable for #name { + fn parse(data: &mut dyn std::io::Read) -> std::io::Result { + Ok(Self{ + #(#p_fields_gen),* + }) + } + + fn dump(&self, data: &mut dyn std::io::Write) -> std::io::Result { + let mut write_count: usize = 0; + #(write_count += #d_fields_gen;)* + Ok(write_count) + } + } + }; + return final_gen.into(); + } else { + panic!("Expected Parsable auto-trait to be applied to struct"); + } +} diff --git a/src/cardlife/mod.rs b/src/cardlife/mod.rs index 897e6dd..f934e04 100644 --- a/src/cardlife/mod.rs +++ b/src/cardlife/mod.rs @@ -1,4 +1,4 @@ -//! Cardlife vanilla and modded (CLre) APIs (WIP) +//! Cardlife vanilla and modded (CLre) APIs (WIP). //! LiveAPI and CLreServer are mostly complete, but some other APIs are missing. mod client; diff --git a/src/cardlife_simple/mod.rs b/src/cardlife_simple/mod.rs index 165ab2c..686198e 100644 --- a/src/cardlife_simple/mod.rs +++ b/src/cardlife_simple/mod.rs @@ -1,3 +1,3 @@ -//! Simple, blocking Cardlife API (WIP) +//! Simple, blocking Cardlife API (WIP). //! Nothing is here yet, sorry! // TODO diff --git a/src/lib.rs b/src/lib.rs index bc7406d..667fb6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,13 @@ //! An unofficial collection of APIs used in Robocraft and Cardlife. //! //! This crate is WIP, but the available APIs are tested and very usable. -#![warn(missing_docs)] - +#[cfg(feature = "cardlife")] pub mod cardlife; +#[cfg(all(feature = "simple", feature = "cardlife"))] +pub mod cardlife_simple; +#[cfg(feature = "robocraft")] pub mod robocraft; -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] pub mod robocraft_simple; -#[cfg(feature = "simple")] -pub mod cardlife_simple; +#[cfg(feature = "techblox")] +pub mod techblox; diff --git a/src/robocraft/cubes.rs b/src/robocraft/cubes.rs index 46b8816..b3307f4 100644 --- a/src/robocraft/cubes.rs +++ b/src/robocraft/cubes.rs @@ -18,7 +18,7 @@ impl Cubes { /// Process the raw bytes containing block data from a Robocraft CRF bot /// /// `cube_data` and `colour_data` correspond to the `cube_data` and `colour_data` fields of FactoryRobotGetInfo. - /// In generally, you should use `Cubes::from(data)` instead of this lower-level function. + /// In general, you should use `Cubes::from(data)` instead of this lower-level function. pub fn parse(cube_data: &mut Vec, colour_data: &mut Vec) -> Result { // read first 4 bytes (cube count) from both arrays and make sure they match let mut cube_buf = [0; 4]; @@ -133,7 +133,7 @@ pub struct Cube { } impl Cube { - fn parse_cube_data(&mut self, reader: &mut &[u8]) -> Result { + fn parse_cube_data(&mut self, reader: &mut dyn Read) -> Result { let mut buf = [0; 4]; // read cube id if let Ok(len) = reader.read(&mut buf) { @@ -159,7 +159,7 @@ impl Cube { Ok(8) } - fn parse_colour_data(&mut self, reader: &mut &[u8]) -> Result { + fn parse_colour_data(&mut self, reader: &mut dyn Read) -> Result { let mut buf = [0; 4]; if let Ok(len) = reader.read(&mut buf) { if len != 4 { diff --git a/src/robocraft/mod.rs b/src/robocraft/mod.rs index aaa1ba6..11b4102 100644 --- a/src/robocraft/mod.rs +++ b/src/robocraft/mod.rs @@ -1,4 +1,4 @@ -//! Robocraft APIs for the CRF and leaderboards (WIP) +//! Robocraft APIs for the CRF and leaderboards (WIP). //! FactoryAPI is mostly complete, but many other APIs are missing. mod factory; diff --git a/src/techblox/block_group_entity.rs b/src/techblox/block_group_entity.rs new file mode 100644 index 0000000..e55c093 --- /dev/null +++ b/src/techblox/block_group_entity.rs @@ -0,0 +1,44 @@ +use crate::techblox::{UnityFloat3, UnityQuaternion, SerializedEntityComponent, SerializedEntityDescriptor, Parsable}; +use libfj_parsable_macro_derive::*; + +/// Block group entity descriptor. +#[derive(Clone, Copy, Parsable)] +pub struct BlockGroupEntity { + /// Block group identifier + pub saved_block_group_id: SavedBlockGroupIdComponent, + /// Block group location information + pub block_group_transform: BlockGroupTransformEntityComponent, +} + +impl BlockGroupEntity {} + +impl SerializedEntityDescriptor for BlockGroupEntity { + fn serialized_components() -> u8 { + 2 + } + + fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> { + vec![&self.saved_block_group_id, + &self.block_group_transform] + } +} + +/// Saved block group identifier entity component. +#[derive(Clone, Copy, Parsable)] +pub struct SavedBlockGroupIdComponent { + /// Block group identifier + pub saved_block_group_id: i32, +} + +impl SerializedEntityComponent for SavedBlockGroupIdComponent {} + +/// Block group entity component for storing position and rotation. +#[derive(Clone, Copy, Parsable)] +pub struct BlockGroupTransformEntityComponent { + /// Block group position + pub block_group_grid_position: UnityFloat3, + /// Block group rotation + pub block_group_grid_rotation: UnityQuaternion, +} + +impl SerializedEntityComponent for BlockGroupTransformEntityComponent {} diff --git a/src/techblox/blocks/block_entity.rs b/src/techblox/blocks/block_entity.rs new file mode 100644 index 0000000..c2b4fb6 --- /dev/null +++ b/src/techblox/blocks/block_entity.rs @@ -0,0 +1,58 @@ +use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent}; +use crate::techblox::blocks::{DBEntityStruct, PositionEntityStruct, ScalingEntityStruct, RotationEntityStruct, +SkewComponent, GridRotationStruct, SerializedGridConnectionsEntityStruct, SerializedBlockPlacementInfoStruct, +SerializedCubeMaterialStruct, SerializedUniformBlockScaleEntityStruct, SerializedColourParameterEntityStruct, +BlockGroupEntityComponent}; +use libfj_parsable_macro_derive::*; + +/// Block entity descriptor. +#[derive(Copy, Clone, Parsable)] +pub struct BlockEntity { + /// Database component + pub db_component: DBEntityStruct, + /// Position component + pub pos_component: PositionEntityStruct, + /// Scale component + pub scale_component: ScalingEntityStruct, + /// Rotation component + pub rot_component: RotationEntityStruct, + /// Skew matrix component + pub skew_component: SkewComponent, + /// Grid component + pub grid_component: GridRotationStruct, + // GridConnectionsEntityStruct is not serialized to disk + /// No-op serializer (this has no data!) + pub grid_conn_component: SerializedGridConnectionsEntityStruct, + // BlockPlacementInfoStruct has a disk serializer that does nothing (?) + /// No-op serializer (this has no data!) + pub placement_component: SerializedBlockPlacementInfoStruct, + /// Cube material component + pub material_component: SerializedCubeMaterialStruct, + /// Uniform scale component + pub uscale_component: SerializedUniformBlockScaleEntityStruct, + /// Colour component + pub colour_component: SerializedColourParameterEntityStruct, + /// Group component + pub group_component: BlockGroupEntityComponent, +} + +impl SerializedEntityDescriptor for BlockEntity { + fn serialized_components() -> u8 { + 12 + } + + fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> { + vec![&self.db_component, + &self.pos_component, + &self.scale_component, + &self.rot_component, + &self.skew_component, + &self.grid_component, + &self.grid_conn_component, + &self.placement_component, + &self.material_component, + &self.uscale_component, + &self.colour_component, + &self.group_component] + } +} diff --git a/src/techblox/blocks/common_components.rs b/src/techblox/blocks/common_components.rs new file mode 100644 index 0000000..9e87d43 --- /dev/null +++ b/src/techblox/blocks/common_components.rs @@ -0,0 +1,113 @@ +use crate::techblox::{Parsable, SerializedEntityComponent, UnityFloat3, +UnityQuaternion, UnityFloat4x4}; +use libfj_parsable_macro_derive::*; + +/// Database entity component. +#[derive(Copy, Clone, Parsable)] +pub struct DBEntityStruct { + /// Database identifier + pub dbid: u32, +} + +impl SerializedEntityComponent for DBEntityStruct {} + +/// Position entity component. +#[derive(Copy, Clone, Parsable)] +pub struct PositionEntityStruct { + /// Entity position + pub position: UnityFloat3, +} + +impl SerializedEntityComponent for PositionEntityStruct {} + +/// Scaling entity component. +#[derive(Copy, Clone, Parsable)] +pub struct ScalingEntityStruct { + /// Entity position + pub scale: UnityFloat3, +} + +impl SerializedEntityComponent for ScalingEntityStruct {} + +/// Scaling entity component. +#[derive(Copy, Clone, Parsable)] +pub struct RotationEntityStruct { + /// Entity position + pub rotation: UnityQuaternion, +} + +impl SerializedEntityComponent for RotationEntityStruct {} + +/// Block skew component. +#[derive(Copy, Clone, Parsable)] +pub struct SkewComponent { + /// Block skew matrix + pub skew_matrix: UnityFloat4x4, +} + +impl SerializedEntityComponent for SkewComponent {} + +/// Block placement grid rotation component. +#[derive(Copy, Clone, Parsable)] +pub struct GridRotationStruct { + /// Grid rotation + pub rotation: UnityQuaternion, + /// Grid position + pub position: UnityFloat3, +} + +impl SerializedEntityComponent for GridRotationStruct {} + +// *** These don't contain anything but the game thinks they do *** +// GridConnectionsEntityStruct is not serialized to disk +// BlockPlacementInfoStruct has a disk serializer that does nothing (?) + +/// Empty, basically useless except that Techblox says it exists while serializing. +#[derive(Copy, Clone, Parsable)] +pub struct SerializedGridConnectionsEntityStruct {} + +impl SerializedEntityComponent for SerializedGridConnectionsEntityStruct {} + +/// Empty, basically useless except that Techblox says it exists while serializing. +#[derive(Copy, Clone, Parsable)] +pub struct SerializedBlockPlacementInfoStruct {} + +impl SerializedEntityComponent for SerializedBlockPlacementInfoStruct {} + +// *** These do contain data again *** + +/// Block material component. +#[derive(Copy, Clone, Parsable)] +pub struct SerializedCubeMaterialStruct { + /// Material identifier + pub material_id: u8, +} + +impl SerializedEntityComponent for SerializedCubeMaterialStruct {} + +/// Block uniform scale component. +#[derive(Copy, Clone, Parsable)] +pub struct SerializedUniformBlockScaleEntityStruct { + /// Uniform scale factor + pub scale_factor: u8, +} + +impl SerializedEntityComponent for SerializedUniformBlockScaleEntityStruct {} + +/// Block colour component. +#[derive(Copy, Clone, Parsable)] +pub struct SerializedColourParameterEntityStruct { + /// Index of colour in Techblox palette + pub index_in_palette: u8, +} + +impl SerializedEntityComponent for SerializedColourParameterEntityStruct {} + +/// Block group component. +#[derive(Copy, Clone, Parsable)] +pub struct BlockGroupEntityComponent { + /// Index of colour in Techblox palette + pub current_block_group: i32, +} + +impl SerializedEntityComponent for BlockGroupEntityComponent {} diff --git a/src/techblox/blocks/lookup_tables.rs b/src/techblox/blocks/lookup_tables.rs new file mode 100644 index 0000000..79ba0dc --- /dev/null +++ b/src/techblox/blocks/lookup_tables.rs @@ -0,0 +1,16 @@ +use std::io::Read; + +use crate::techblox::{Parsable, SerializedEntityDescriptor}; +#[cfg(debug_assertions)] +use crate::techblox::blocks::*; + +pub fn lookup_hashname(hash: u32, data: &mut dyn Read) -> std::io::Result> { + Ok(match hash { + 1357220432 /*StandardBlockEntityDescriptorV4*/ => Box::new(BlockEntity::parse(data)?), + _ => { + #[cfg(debug_assertions)] + println!("Unknown hash ID {}", hash); + return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Unrecognised hash {}", hash))) + } + }) +} diff --git a/src/techblox/blocks/mod.rs b/src/techblox/blocks/mod.rs new file mode 100644 index 0000000..e174b6e --- /dev/null +++ b/src/techblox/blocks/mod.rs @@ -0,0 +1,12 @@ +//! A (mostly) complete collection of Techblox blocks for serialization + +mod block_entity; +mod common_components; +mod lookup_tables; + +pub use block_entity::{BlockEntity}; +pub use common_components::{DBEntityStruct, PositionEntityStruct, ScalingEntityStruct, RotationEntityStruct, +SkewComponent, GridRotationStruct, SerializedGridConnectionsEntityStruct, SerializedBlockPlacementInfoStruct, +SerializedCubeMaterialStruct, SerializedUniformBlockScaleEntityStruct, SerializedColourParameterEntityStruct, +BlockGroupEntityComponent}; +pub(crate) use lookup_tables::*; diff --git a/src/techblox/entity_header.rs b/src/techblox/entity_header.rs new file mode 100644 index 0000000..229f193 --- /dev/null +++ b/src/techblox/entity_header.rs @@ -0,0 +1,54 @@ +use crate::techblox::{hashname, brute_force, Parsable}; +use libfj_parsable_macro_derive::*; + +/// An entity's header information. +/// +/// This holds entity data common to all entities, such as entity type and ID. +#[derive(Clone, Copy, Parsable)] +pub struct EntityHeader { + /// Entity type hash + pub hash: u32, + /// Entity identifier + pub entity_id: u32, + /// Entity group identifier + pub group_id: u32, + /// Count of serialized components after this header (this is not the size in bytes) + pub component_count: u8, +} + +impl EntityHeader { + /// Guess the original name from the hashed value by brute-force. + /// + /// This is slow and cannot guarantee a correct result. Use is discouraged. + pub fn guess_name(&self) -> String { + brute_force(self.hash) + } + + /// Create an entity header using the hash of `name`. + pub fn from_name(name: &str, entity_id: u32, group_id: u32, component_count: u8) -> Self { + Self { + hash: hashname(name), + entity_id, + group_id, + component_count, + } + } +} + +impl std::convert::Into for EntityHeader { + fn into(self) -> EntityGroupID { + EntityGroupID { + entity_id: self.entity_id, + group_id: self.group_id, + } + } +} + +/// Entity identifier common among all components in the same entity +#[derive(Clone, Copy, Parsable)] +pub struct EntityGroupID { + /// Entity identifier + pub entity_id: u32, + /// Entity group identifier + pub group_id: u32 +} diff --git a/src/techblox/entity_traits.rs b/src/techblox/entity_traits.rs new file mode 100644 index 0000000..56c05b5 --- /dev/null +++ b/src/techblox/entity_traits.rs @@ -0,0 +1,26 @@ +use std::io::{Read, Write}; + +/// Standard trait for parsing Techblox game save data. +pub trait Parsable { + /// Process information from raw data. + fn parse(reader: &mut dyn Read) -> std::io::Result where Self: Sized; + /// Convert struct data back into raw bytes + fn dump(&self, writer: &mut dyn Write) -> std::io::Result; +} + +/// Entity descriptor containing serialized components. +pub trait SerializedEntityDescriptor: Parsable { + /// Count of entity components that this descriptor contains + fn serialized_components() -> u8 where Self: Sized; + /// Components that this entity is comprised of + fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent>; +} + +/// Serializable entity component. +/// Components are the atomic unit of entities. +pub trait SerializedEntityComponent: Parsable { + /// Raw size of struct, in bytes. + fn size() -> usize where Self: Sized { + std::mem::size_of::() + } +} diff --git a/src/techblox/gamesave.rs b/src/techblox/gamesave.rs new file mode 100644 index 0000000..942d1e4 --- /dev/null +++ b/src/techblox/gamesave.rs @@ -0,0 +1,114 @@ +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; + +/// A collection of cubes and other data from a GameSave.techblox file +//#[derive(Clone)] +pub struct GameSave { + /// Game version that this save was created by. + /// This may affect how the rest of the save file was parsed. + pub version: NaiveDate, + + /// Unused magic value in file header. + pub magic1: 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, + + /// Amount of block groups, as claimed by the file header. + pub group_len: u32, + + /// Entity group descriptors for block group entities. + pub group_headers: Vec, + + /// Block group entities. + pub cube_groups: Vec, + + /// Entity group descriptors for block entities. + pub cube_headers: Vec, + + /// Blocks + pub cube_entities: Vec> +} + +impl Parsable for GameSave { + /// Process a Techblox save file from raw bytes. + fn parse(data: &mut dyn Read) -> std::io::Result { + // parse version + let year = parse_u32(data)?; // parsed as i32 in-game for some reason + 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 cube_count = parse_u32(data)?; // parsed as i32 in-game for some reason + let magic_val2 = 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); + let mut groups_e = Vec::::with_capacity(group_count as usize); + for _i in 0..group_count { + groups_h.push(EntityHeader::parse(data)?); + groups_e.push(BlockGroupEntity::parse(data)?); + } + + // parse cube data + let mut cubes_h = Vec::::with_capacity(cube_count as usize); + let mut cubes_e = Vec::>::with_capacity(cube_count as usize); + for _i in 0..cube_count { + let header = EntityHeader::parse(data)?; + let hash = header.hash; + cubes_h.push(header); + cubes_e.push(lookup_hashname(hash, data)?); + } + // TODO + Ok(Self { + version: date, + magic1: magic_val1, + cube_len: cube_count, + magic2: magic_val2, + group_len: group_count, + group_headers: groups_h, + cube_groups: groups_e, + cube_headers: cubes_h, + cube_entities: cubes_e, + }) + } + + fn dump(&self, writer: &mut dyn Write) -> std::io::Result { + let mut write_count: usize = 0; + // version + 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)?; + write_count += self.cube_len.dump(writer)?; + // magic separator + write_count += self.magic2.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)?; + write_count += self.cube_groups[i].dump(writer)?; + } + + // dump cube data + for i in 0..self.cube_len as usize { + write_count += self.cube_headers[i].dump(writer)?; + write_count += self.cube_entities[i].dump(writer)?; + } + // TODO + 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) + } +} diff --git a/src/techblox/mod.rs b/src/techblox/mod.rs new file mode 100644 index 0000000..1bda602 --- /dev/null +++ b/src/techblox/mod.rs @@ -0,0 +1,19 @@ +//! Techblox APIs and functionality (WIP). + +pub mod blocks; +mod gamesave; +mod entity_header; +mod entity_traits; +mod block_group_entity; +mod unity_types; +#[allow(dead_code)] +mod parsing_tools; +mod murmur; + +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(crate) use parsing_tools::*; +pub(crate) use murmur::*; diff --git a/src/techblox/murmur.rs b/src/techblox/murmur.rs new file mode 100644 index 0000000..cb727d0 --- /dev/null +++ b/src/techblox/murmur.rs @@ -0,0 +1,74 @@ +use fasthash::murmur3::hash32_with_seed; +use std::sync::mpsc::{channel, Sender}; +use std::thread; + +const HASH_SEED: u32 = 4919; + +const ASCII_LETTERS: &[u8] = &[65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90, // A..Z +97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122]; // a..z + +const ASCII_NUMBERS: &[u8] = &[48, 49, 50, 51, 52, 53, 54, 55, 56, 57]; // 0..9 + +const HASHNAME_ENDING: &[u8] = &[69, 110, 116, 105, 116, 121, // Entity +68, 101, 115, 99, 114, 105, 112, 116, 111, 114, // Descriptor +86, 42]; // EntityDescriptorV0 + +const MAX_LENGTH: usize = 10; + +pub fn hashname(name: &str) -> u32 { + hash32_with_seed(name, HASH_SEED) +} + +pub fn brute_force(hash: u32) -> String { + let (tx, rx) = channel::(); + let mut start = Vec::::new(); + thread::spawn(move || brute_force_letter(hash, &mut start, &tx, 1)); + //println!("All brute force possibilities explored"); + if let Ok(res) = rx.recv_timeout(std::time::Duration::from_secs(30)) { + return res; + } else { + return "".to_string(); + } +} + +fn brute_force_letter(hash: u32, start: &mut Vec, tx: &Sender, threadity: usize) { + if start.len() > 0 { + brute_force_endings(hash, start, tx); + } + if start.len() >= MAX_LENGTH { // do not continue extending forever + //handles.pop().unwrap().join().unwrap(); + return; + } + let mut handles = Vec::>::new(); + start.push(65); // add letter + let last_elem = start.len()-1; + for letter in ASCII_LETTERS { + start[last_elem] = *letter; + if threadity > 0 { + //thread::sleep(std::time::Duration::from_millis(50)); + let mut new_start = start.clone(); + let new_tx = tx.clone(); + handles.push(thread::spawn(move || brute_force_letter(hash, &mut new_start, &new_tx, threadity-1))); + } else { + brute_force_letter(hash, start, tx, threadity); + } + } + for handle in handles { + handle.join().unwrap() + } + start.truncate(last_elem); +} + +fn brute_force_endings(hash: u32, start: &mut Vec, tx: &Sender) { + start.extend(HASHNAME_ENDING); // add ending + let last_elem = start.len()-1; + for num in ASCII_NUMBERS { + start[last_elem] = *num; + if hash32_with_seed(&start, HASH_SEED) == hash { + let result = String::from_utf8(start.clone()).unwrap(); + println!("Found match `{}`", result); + tx.send(result).unwrap(); + } + } + start.truncate(start.len()-HASHNAME_ENDING.len()); // remove ending +} diff --git a/src/techblox/parsing_tools.rs b/src/techblox/parsing_tools.rs new file mode 100644 index 0000000..e23ebe3 --- /dev/null +++ b/src/techblox/parsing_tools.rs @@ -0,0 +1,151 @@ +use std::io::{Read, Write}; +use crate::techblox::Parsable; + +// reading + +pub fn parse_header_u32(reader: &mut dyn Read) -> std::io::Result { + // this is possibly wrong + let mut u32_buf = [0; 4]; + //u32_buf[3] = parse_u8(reader)?; + //u32_buf[2] = parse_u8(reader)?; + //u32_buf[1] = parse_u8(reader)?; + //u32_buf[0] = parse_u8(reader)?; + reader.read(&mut u32_buf)?; + Ok(u32::from_le_bytes(u32_buf)) +} + +pub fn parse_u8(reader: &mut dyn Read) -> std::io::Result { + let mut u8_buf = [0; 1]; + reader.read(&mut u8_buf)?; + Ok(u8_buf[0]) +} + +pub fn parse_u32(reader: &mut dyn Read) -> std::io::Result { + let mut u32_buf = [0; 4]; + reader.read(&mut u32_buf)?; + Ok(u32::from_le_bytes(u32_buf)) +} + +pub fn parse_i32(reader: &mut dyn Read) -> std::io::Result { + let mut i32_buf = [0; 4]; + reader.read(&mut i32_buf)?; + Ok(i32::from_le_bytes(i32_buf)) +} + +pub fn parse_u64(reader: &mut dyn Read) -> std::io::Result { + let mut u64_buf = [0; 8]; + reader.read(&mut u64_buf)?; + Ok(u64::from_le_bytes(u64_buf)) +} + +pub fn parse_i64(reader: &mut dyn Read) -> std::io::Result { + let mut i64_buf = [0; 8]; + reader.read(&mut i64_buf)?; + Ok(i64::from_le_bytes(i64_buf)) +} + +pub fn parse_f32(reader: &mut dyn Read) -> std::io::Result { + let mut f32_buf = [0; 4]; + reader.read(&mut f32_buf)?; + Ok(f32::from_le_bytes(f32_buf)) +} + +// writing + +pub fn dump_u8(data: u8, writer: &mut dyn Write) -> std::io::Result { + writer.write(&data.to_le_bytes()) +} + +pub fn dump_u32(data: u32, writer: &mut dyn Write) -> std::io::Result { + writer.write(&data.to_le_bytes()) +} + +pub fn dump_i32(data: i32, writer: &mut dyn Write) -> std::io::Result { + writer.write(&data.to_le_bytes()) +} + +pub fn dump_u64(data: u64, writer: &mut dyn Write) -> std::io::Result { + writer.write(&data.to_le_bytes()) +} + +pub fn dump_i64(data: i64, writer: &mut dyn Write) -> std::io::Result { + writer.write(&data.to_le_bytes()) +} + +pub fn dump_f32(data: f32, writer: &mut dyn Write) -> std::io::Result { + writer.write(&data.to_le_bytes()) +} + +// trait implementations + +impl Parsable for u8 { + fn parse(reader: &mut dyn Read) -> std::io::Result { + let mut buf = [0; 1]; + 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()) + } +} + +impl Parsable for u32 { + fn parse(reader: &mut dyn Read) -> std::io::Result { + let mut buf = [0; 4]; + 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()) + } +} + +impl Parsable for i32 { + fn parse(reader: &mut dyn Read) -> std::io::Result { + let mut buf = [0; 4]; + 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()) + } +} + +impl Parsable for u64 { + fn parse(reader: &mut dyn Read) -> std::io::Result { + let mut buf = [0; 8]; + 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()) + } +} + +impl Parsable for i64 { + fn parse(reader: &mut dyn Read) -> std::io::Result { + let mut buf = [0; 8]; + 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()) + } +} + +impl Parsable for f32 { + fn parse(reader: &mut dyn Read) -> std::io::Result { + let mut buf = [0; 4]; + 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 new file mode 100644 index 0000000..f62d44a --- /dev/null +++ b/src/techblox/unity_types.rs @@ -0,0 +1,46 @@ +use crate::techblox::{Parsable}; +use libfj_parsable_macro_derive::*; + +/// Unity-like floating-point vector for 3-dimensional space. +#[derive(Clone, Copy, Parsable)] +pub struct UnityFloat3 { + /// x coordinate + pub x: f32, + /// y coordinate + pub y: f32, + /// z coordinate + pub z: f32, +} + +/// Unity-like floating-point vector for 4-dimensional space. +#[derive(Clone, Copy, Parsable)] +pub struct UnityFloat4 { + /// x coordinate + pub x: f32, + /// y coordinate + pub y: f32, + /// z coordinate + pub z: f32, + /// w coordinate + pub w: f32, +} + +/// Unity-like floating-point vector matrix for 4-dimensional space. +#[derive(Clone, Copy, Parsable)] +pub struct UnityFloat4x4 { + /// c0 row(?) + pub c0: UnityFloat4, + /// c1 row(?) + pub c1: UnityFloat4, + /// c2 row(?) + pub c2: UnityFloat4, + /// c3 row(?) + pub c3: UnityFloat4, +} + +/// Unity-like floating-point quaternion for rotation in 3-dimensional space. +#[derive(Clone, Copy, Parsable)] +pub struct UnityQuaternion { + /// Rotational orientation + pub value: UnityFloat4, +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..5f9f80e --- /dev/null +++ b/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +RUST_BACKTRACE=1 cargo test --all-features -- --nocapture +# RUST_BACKTRACE=1 cargo test --features techblox -- --nocapture +exit $? diff --git a/tests/GameSave.Techblox b/tests/GameSave.Techblox new file mode 100644 index 0000000000000000000000000000000000000000..cbc3c82edf19f9902a6c0f7fc4882895ae3abfb2 GIT binary patch literal 207376 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 XLjD6f<-^b1eE4Z|M_>Evz25%^B@Vl` literal 0 HcmV?d00001 diff --git a/tests/cardlife_live.rs b/tests/cardlife_live.rs index 84ecb54..4acebd1 100644 --- a/tests/cardlife_live.rs +++ b/tests/cardlife_live.rs @@ -1,14 +1,19 @@ +#[cfg(feature = "cardlife")] use libfj::cardlife; +#[cfg(feature = "cardlife")] const EMAIL: &str = ""; +#[cfg(feature = "cardlife")] const PASSWORD: &str = ""; +#[cfg(feature = "cardlife")] #[test] fn live_api_init() -> Result<(), ()> { cardlife::LiveAPI::new(); Ok(()) } +#[cfg(feature = "cardlife")] #[tokio::test] async fn live_api_init_auth() -> Result<(), ()> { let live = cardlife::LiveAPI::login_email(EMAIL, PASSWORD).await; @@ -16,6 +21,7 @@ async fn live_api_init_auth() -> Result<(), ()> { Ok(()) } +#[cfg(feature = "cardlife")] #[tokio::test] async fn live_api_authenticate() -> Result<(), ()> { let mut live = cardlife::LiveAPI::new(); @@ -30,6 +36,7 @@ async fn live_api_authenticate() -> Result<(), ()> { Ok(()) } +#[cfg(feature = "cardlife")] #[tokio::test] async fn live_api_lobbies() -> Result<(), ()> { //let live = cardlife::LiveAPI::login_email(EMAIL, PASSWORD).await.unwrap(); @@ -43,4 +50,4 @@ async fn live_api_lobbies() -> Result<(), ()> { println!("LiveGameInfo.to_string() -> `{}`", game.to_string()); }*/ Ok(()) -} \ No newline at end of file +} diff --git a/tests/clre_server.rs b/tests/clre_server.rs index 25a5272..95f08a1 100644 --- a/tests/clre_server.rs +++ b/tests/clre_server.rs @@ -1,12 +1,16 @@ +#[cfg(feature = "cardlife")] use libfj::cardlife; +#[cfg(feature = "cardlife")] #[test] fn clre_server_init() -> Result<(), ()> { assert!(cardlife::CLreServer::new("http://localhost:5030").is_ok()); Ok(()) } -/*#[tokio::test] +/* +#[cfg(feature = "cardlife")] +#[tokio::test] async fn clre_server_game() -> Result<(), ()> { let server = cardlife::CLreServer::new("http://localhost:5030").unwrap(); let result = server.game_info().await; @@ -18,6 +22,7 @@ async fn clre_server_game() -> Result<(), ()> { Ok(()) } +#[cfg(feature = "cardlife")] #[tokio::test] async fn clre_server_status() -> Result<(), ()> { let server = cardlife::CLreServer::new("http://localhost:5030").unwrap(); diff --git a/tests/robocraft_factory.rs b/tests/robocraft_factory.rs index 02f1c3e..31f2e85 100644 --- a/tests/robocraft_factory.rs +++ b/tests/robocraft_factory.rs @@ -1,12 +1,16 @@ +#[cfg(feature = "robocraft")] use libfj::robocraft; +#[cfg(feature = "robocraft")] use std::convert::From; +#[cfg(feature = "robocraft")] #[test] fn robocraft_factory_api_init() -> Result<(), ()> { robocraft::FactoryAPI::new(); Ok(()) } +#[cfg(feature = "robocraft")] #[tokio::test] async fn robocraft_factory_default_query() -> Result<(), ()> { let api = robocraft::FactoryAPI::new(); @@ -25,10 +29,12 @@ async fn robocraft_factory_default_query() -> Result<(), ()> { Ok(()) } +#[cfg(feature = "robocraft")] fn builder() -> robocraft::FactorySearchBuilder { robocraft::FactoryAPI::new().list_builder() } +#[cfg(feature = "robocraft")] fn assert_factory_list(robo_info: robocraft::FactoryInfo) -> Result<(), ()> { assert_ne!(robo_info.response.roboshop_items.len(), 0); assert_eq!(robo_info.status_code, 200); @@ -42,6 +48,7 @@ fn assert_factory_list(robo_info: robocraft::FactoryInfo Result<(), ()> { let api = robocraft::FactoryAPI::new(); @@ -67,6 +74,7 @@ async fn robocraft_factory_custom_query() -> Result<(), ()> { Ok(()) } +#[cfg(feature = "robocraft")] #[tokio::test] async fn robocraft_factory_player_query() -> Result<(), ()> { let result = builder() @@ -78,6 +86,7 @@ async fn robocraft_factory_player_query() -> Result<(), ()> { assert_factory_list(result.unwrap()) } +#[cfg(feature = "robocraft")] #[tokio::test] async fn robocraft_factory_robot_query() -> Result<(), ()> { let api = robocraft::FactoryAPI::new(); @@ -91,6 +100,7 @@ async fn robocraft_factory_robot_query() -> Result<(), ()> { Ok(()) } +#[cfg(feature = "robocraft")] #[tokio::test] async fn robocraft_factory_robot_cubes() -> Result<(), ()> { let api = robocraft::FactoryAPI::new(); diff --git a/tests/robocraft_factory_simple.rs b/tests/robocraft_factory_simple.rs index 5e60334..1b1a6bc 100644 --- a/tests/robocraft_factory_simple.rs +++ b/tests/robocraft_factory_simple.rs @@ -1,21 +1,21 @@ -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] use libfj::robocraft_simple; -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] use libfj::robocraft; -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] #[test] fn robocraft_factory_api_init_simple() -> Result<(), ()> { robocraft_simple::FactoryAPI::new(); Ok(()) } -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] fn builder() -> robocraft_simple::FactorySearchBuilder { robocraft_simple::FactoryAPI::new().list_builder() } -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] fn assert_factory_list(robo_info: robocraft::FactoryInfo) -> Result<(), ()> { assert_ne!(robo_info.response.roboshop_items.len(), 0); assert_eq!(robo_info.status_code, 200); @@ -30,7 +30,7 @@ fn assert_factory_list(robo_info: robocraft::FactoryInfo Result<(), ()> { let result = builder() .movement_or(robocraft::FactoryMovementType::Wheels) @@ -55,7 +55,7 @@ fn robocraft_factory_custom_query_simple() -> Result<(), ()> { } #[test] -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] fn robocraft_factory_player_query() -> Result<(), ()> { let result = builder() .text("Baerentoeter".to_string()) @@ -67,7 +67,7 @@ fn robocraft_factory_player_query() -> Result<(), ()> { } #[test] -#[cfg(feature = "simple")] +#[cfg(all(feature = "simple", feature = "robocraft"))] fn robocraft_factory_robot_query() -> Result<(), ()> { let api = robocraft_simple::FactoryAPI::new(); let result = api.get(6478345 /* featured robot id*/); diff --git a/tests/techblox_parsing.rs b/tests/techblox_parsing.rs new file mode 100644 index 0000000..3c81964 --- /dev/null +++ b/tests/techblox_parsing.rs @@ -0,0 +1,67 @@ +#[cfg(feature = "techblox")] +use libfj::techblox; +#[cfg(feature = "techblox")] +use libfj::techblox::{SerializedEntityDescriptor, Parsable, blocks}; +#[cfg(feature = "techblox")] +use std::io::Read; +#[cfg(feature = "techblox")] +use std::fs::File; + +#[cfg(feature = "techblox")] +const GAMESAVE_PATH: &str = "tests/GameSave.Techblox"; + +#[cfg(feature = "techblox")] +const HASHNAMES: &[&str] = &[ + "StandardBlockEntityDescriptorV4", +]; + +#[cfg(feature = "techblox")] +#[test] +fn techblox_gamesave_parse() -> Result<(), ()> { + let mut f = File::open(GAMESAVE_PATH).map_err(|_| ())?; + let mut buf = Vec::new(); + f.read_to_end(&mut buf).map_err(|_| ())?; + let gs = techblox::GameSave::parse(&mut buf.as_slice()).map_err(|_| ())?; + for i in 1..(gs.group_len as usize) { + assert_eq!(gs.group_headers[i-1].hash, gs.group_headers[i].hash); + //println!("#{} count {} vs {}", i, gs.group_headers[i-1].component_count, gs.group_headers[i].component_count); + assert_eq!(gs.group_headers[i-1].component_count, gs.group_headers[i].component_count); + } + for i in 0..(gs.group_len as usize) { + assert_eq!(gs.group_headers[i].component_count, techblox::BlockGroupEntity::serialized_components()); + } + for i in 1..(gs.cube_len as usize) { + //assert_eq!(gs.cube_headers[i-1].hash, gs.cube_headers[i].hash); + //println!("#{} count {} vs {}", i, gs.cube_headers[i-1].component_count, gs.cube_headers[i].component_count); + if gs.cube_headers[i-1].hash == gs.cube_headers[i].hash { + assert_eq!(gs.group_headers[i-1].component_count, gs.group_headers[i].component_count); + } + } + for i in 0..(gs.cube_len as usize) { + assert!(gs.cube_headers[i].component_count >= blocks::BlockEntity::serialized_components()); + //println!("#{} components: {}", i, gs.cube_headers[i].component_count); + } + println!("{}", gs.to_string()); + Ok(()) +} + +#[allow(dead_code)] +#[cfg(feature = "techblox")] +//#[test] +fn techblox_gamesave_brute_force() -> Result<(), ()> { + // this is slow and not very important, so it's probably better to not test this + let mut f = File::open(GAMESAVE_PATH).map_err(|_| ())?; + let mut buf = Vec::new(); + f.read_to_end(&mut buf).map_err(|_| ())?; + let gs = techblox::GameSave::parse(&mut buf.as_slice()).map_err(|_| ())?; + println!("murmurhash3: {} -> {}", gs.group_headers[0].guess_name(), gs.group_headers[0].hash); + Ok(()) +} + +#[cfg(feature = "techblox")] +#[test] +fn hash_tb_name() { + for name in HASHNAMES { + println!("MurmurHash3: {} -> {}", name, crate::techblox::EntityHeader::from_name(name, 0, 0, 0).hash); + } +}