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 0000000..cbc3c82 Binary files /dev/null and b/tests/GameSave.Techblox differ 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); + } +}