@@ -1,3 +1,4 @@ | |||
/target | |||
Cargo.lock | |||
/.idea | |||
/parsable_macro_derive/target |
@@ -1,6 +1,6 @@ | |||
[package] | |||
name = "libfj" | |||
version = "0.4.1" | |||
version = "0.5.1" | |||
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] | |||
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"] |
@@ -0,0 +1,16 @@ | |||
[package] | |||
name = "libfj_parsable_macro_derive" | |||
version = "0.5.3" | |||
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] | |||
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" |
@@ -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<Self> { | |||
Ok(Self{ | |||
#(#p_fields_gen),* | |||
}) | |||
} | |||
fn dump(&self, data: &mut dyn std::io::Write) -> std::io::Result<usize> { | |||
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"); | |||
} | |||
} |
@@ -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; | |||
@@ -1,3 +1,3 @@ | |||
//! Simple, blocking Cardlife API (WIP) | |||
//! Simple, blocking Cardlife API (WIP). | |||
//! Nothing is here yet, sorry! | |||
// TODO |
@@ -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; |
@@ -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<FactoryRobotGetInfo>(data)` instead of this lower-level function. | |||
/// In general, you should use `Cubes::from<FactoryRobotGetInfo>(data)` instead of this lower-level function. | |||
pub fn parse(cube_data: &mut Vec<u8>, colour_data: &mut Vec<u8>) -> Result<Self, ()> { | |||
// 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<usize, ()> { | |||
fn parse_cube_data(&mut self, reader: &mut dyn Read) -> Result<usize, ()> { | |||
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<usize, ()> { | |||
fn parse_colour_data(&mut self, reader: &mut dyn Read) -> Result<usize, ()> { | |||
let mut buf = [0; 4]; | |||
if let Ok(len) = reader.read(&mut buf) { | |||
if len != 4 { | |||
@@ -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; | |||
@@ -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 {} |
@@ -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] | |||
} | |||
} |
@@ -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 {} |
@@ -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<Box<dyn SerializedEntityDescriptor>> { | |||
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))) | |||
} | |||
}) | |||
} |
@@ -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::*; |
@@ -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<EntityGroupID> 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 | |||
} |
@@ -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<Self> where Self: Sized; | |||
/// Convert struct data back into raw bytes | |||
fn dump(&self, writer: &mut dyn Write) -> std::io::Result<usize>; | |||
} | |||
/// 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::<Self>() | |||
} | |||
} |
@@ -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<EntityHeader>, | |||
/// Block group entities. | |||
pub cube_groups: Vec<BlockGroupEntity>, | |||
/// Entity group descriptors for block entities. | |||
pub cube_headers: Vec<EntityHeader>, | |||
/// Blocks | |||
pub cube_entities: Vec<Box<dyn SerializedEntityDescriptor>> | |||
} | |||
impl Parsable for GameSave { | |||
/// Process a Techblox save file from raw bytes. | |||
fn parse(data: &mut dyn Read) -> std::io::Result<Self> { | |||
// 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::<EntityHeader>::with_capacity(group_count as usize); | |||
let mut groups_e = Vec::<BlockGroupEntity>::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::<EntityHeader>::with_capacity(cube_count as usize); | |||
let mut cubes_e = Vec::<Box<dyn SerializedEntityDescriptor>>::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<usize> { | |||
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) | |||
} | |||
} |
@@ -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::*; |
@@ -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::<String>(); | |||
let mut start = Vec::<u8>::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<u8>, tx: &Sender<String>, 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::<thread::JoinHandle::<_>>::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<u8>, tx: &Sender<String>) { | |||
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 | |||
} |
@@ -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<u32> { | |||
// 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<u8> { | |||
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<u32> { | |||
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<i32> { | |||
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<u64> { | |||
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<i64> { | |||
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<f32> { | |||
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<usize> { | |||
writer.write(&data.to_le_bytes()) | |||
} | |||
pub fn dump_u32(data: u32, writer: &mut dyn Write) -> std::io::Result<usize> { | |||
writer.write(&data.to_le_bytes()) | |||
} | |||
pub fn dump_i32(data: i32, writer: &mut dyn Write) -> std::io::Result<usize> { | |||
writer.write(&data.to_le_bytes()) | |||
} | |||
pub fn dump_u64(data: u64, writer: &mut dyn Write) -> std::io::Result<usize> { | |||
writer.write(&data.to_le_bytes()) | |||
} | |||
pub fn dump_i64(data: i64, writer: &mut dyn Write) -> std::io::Result<usize> { | |||
writer.write(&data.to_le_bytes()) | |||
} | |||
pub fn dump_f32(data: f32, writer: &mut dyn Write) -> std::io::Result<usize> { | |||
writer.write(&data.to_le_bytes()) | |||
} | |||
// trait implementations | |||
impl Parsable for u8 { | |||
fn parse(reader: &mut dyn Read) -> std::io::Result<Self> { | |||
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<usize> { | |||
writer.write(&self.to_le_bytes()) | |||
} | |||
} | |||
impl Parsable for u32 { | |||
fn parse(reader: &mut dyn Read) -> std::io::Result<Self> { | |||
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<usize> { | |||
writer.write(&self.to_le_bytes()) | |||
} | |||
} | |||
impl Parsable for i32 { | |||
fn parse(reader: &mut dyn Read) -> std::io::Result<Self> { | |||
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<usize> { | |||
writer.write(&self.to_le_bytes()) | |||
} | |||
} | |||
impl Parsable for u64 { | |||
fn parse(reader: &mut dyn Read) -> std::io::Result<Self> { | |||
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<usize> { | |||
writer.write(&self.to_le_bytes()) | |||
} | |||
} | |||
impl Parsable for i64 { | |||
fn parse(reader: &mut dyn Read) -> std::io::Result<Self> { | |||
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<usize> { | |||
writer.write(&self.to_le_bytes()) | |||
} | |||
} | |||
impl Parsable for f32 { | |||
fn parse(reader: &mut dyn Read) -> std::io::Result<Self> { | |||
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<usize> { | |||
writer.write(&self.to_le_bytes()) | |||
} | |||
} |
@@ -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, | |||
} |
@@ -0,0 +1,4 @@ | |||
#!/bin/bash | |||
RUST_BACKTRACE=1 cargo test --all-features -- --nocapture | |||
# RUST_BACKTRACE=1 cargo test --features techblox -- --nocapture | |||
exit $? |
@@ -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(()) | |||
} | |||
} |
@@ -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(); | |||
@@ -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<robocraft::RoboShopItemsInfo>) -> 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<robocraft::RoboShopItem | |||
Ok(()) | |||
} | |||
#[cfg(feature = "robocraft")] | |||
#[tokio::test] | |||
async fn robocraft_factory_custom_query() -> 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(); | |||
@@ -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<robocraft::RoboShopItemsInfo>) -> 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<robocraft::RoboShopItem | |||
} | |||
#[test] | |||
#[cfg(feature = "simple")] | |||
#[cfg(all(feature = "simple", feature = "robocraft"))] | |||
fn robocraft_factory_custom_query_simple() -> 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*/); | |||
@@ -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); | |||
} | |||
} |