diff --git a/Cargo.lock b/Cargo.lock index 0451fac..93dcea8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "owo-colors" version = "3.5.0" @@ -80,6 +89,7 @@ version = "0.3.1" dependencies = [ "binrw", "bitflags", + "itertools", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3723d15..4456a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,8 @@ edition = "2021" [dependencies] binrw = "0.14.1" bitflags = "2.6.0" +itertools = { version = "0.14.0", optional = true } + +[features] +default = ["itertools"] +itertools = ["dep:itertools"] diff --git a/README.md b/README.md index ebdf7a7..9ae6896 100644 --- a/README.md +++ b/README.md @@ -3,30 +3,37 @@ Roblox Bhop/Surf Bot File Format ## Example +Read the whole file with the itertools feature enabled: ```rust -use strafesnet_roblox_bot_file::{File,TimedBlockId}; +use strafesnet_roblox_bot_file::v0::read_all_to_block; -let file=std::fs::File::open("bot_file")?; -let input=std::io::BufReader::new(file); -let mut bot_file=File::new(input)?; +let file=std::fs::read("bot_file")?; +let mut input=std::io::Cursor::new(file); -// read the whole file -let block=bot_file.read_all()?; +let block=read_all_to_block(&mut input)?; +``` +Or decode individual blocks using block location info: +```rust +use strafesnet_roblox_bot_file::v0::{Block,BlockTimelines,FileHeader}; -// or do data streaming block by block -for &TimedBlockId{time,block_id} in &bot_file.header.offline_blocks_timeline{ - // header is immutably borrowed - // while data is mutably borrowed - let block_info=bot_file.header.block_info(block_id)?; - let block=bot_file.data.read_block_info(block_info)?; - // offline blocks include the following event types: - // World, Gravity, Run, Camera, Setting +let file=std::fs::read("bot_file")?; +let mut input=std::io::Cursor::new(file); + +let header=FileHeader::from_reader(&mut input)?; +let timelines=BlockTimelines::from_reader(&header,&mut input)?; + +// offline blocks include the following event types: +// World, Gravity, Run, Camera, Setting +for timed in timelines.offline_blocks(){ + let block_info=timelines.block_info(timed.event)?; + let block=Block::from_reader(block_info.take_seek(&mut input)?)?; } -for &TimedBlockId{time,block_id} in &bot_file.header.realtime_blocks_timeline{ - let block_info=bot_file.header.block_info(block_id)?; - let block=bot_file.data.read_block_info(block_info)?; - // realtime blocks include the following event types: - // Input, Output, Sound + +// realtime blocks include the following event types: +// Input, Output, Sound +for timed in timelines.realtime_blocks(){ + let block_info=timelines.block_info(timed.event)?; + let block=Block::from_reader(block_info.take_seek(&mut input)?)?; } ``` diff --git a/src/tests.rs b/src/tests.rs index 5bcc349..3d3c310 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,39 +1,38 @@ -use crate::v0::{Error,File,TimedBlockId}; +use crate::v0::{Block,BlockTimelines,FileHeader,Timed}; #[test] -fn _1()->Result<(),Error>{ - let file=std::fs::File::open("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap(); - let input=std::io::BufReader::new(file); - let mut bot_file=File::new(input).unwrap(); - println!("header={:?}",bot_file.header); - for &TimedBlockId{time,block_id} in &bot_file.header.offline_blocks_timeline{ +fn _1(){ + let file=std::fs::read("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap(); + let mut input=std::io::Cursor::new(file); + let header=FileHeader::from_reader(&mut input).unwrap(); + let timelines=BlockTimelines::from_reader(&header,&mut input).unwrap(); + println!("header={:?}",header); + for &Timed{time,event:block_id} in timelines.offline_blocks(){ println!("offline time={} block_id={:?}",time,block_id); - let block_info=bot_file.header.block_info(block_id)?; - let _block=bot_file.data.read_block_info(block_info)?; + let take_seek=timelines.block_info(block_id).unwrap().take_seek(&mut input).unwrap(); + let _block=Block::from_reader(take_seek).unwrap(); // offline blocks include the following event types: // World, Gravity, Run, Camera, Setting } - for &TimedBlockId{time,block_id} in &bot_file.header.realtime_blocks_timeline{ + for &Timed{time,event:block_id} in timelines.realtime_blocks(){ println!("realtime time={} block_id={:?}",time,block_id); - let block_info=bot_file.header.block_info(block_id)?; - let _block=bot_file.data.read_block_info(block_info)?; + let take_seek=timelines.block_info(block_id).unwrap().take_seek(&mut input).unwrap(); + let _block=Block::from_reader(take_seek).unwrap(); // realtime blocks include the following event types: // Input, Output, Sound } - - Ok(()) } +#[cfg(feature="itertools")] +use crate::v0::{read_all_to_block,Error}; #[test] +#[cfg(feature="itertools")] fn _2()->Result<(),Error>{ - let file=std::fs::File::open("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap(); - let input=std::io::BufReader::new(file); + let file=std::fs::read("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap(); let t0=std::time::Instant::now(); - let mut bot_file=File::new(input).unwrap(); - - let _block=bot_file.read_all()?; + let _block=read_all_to_block(std::io::Cursor::new(file))?; println!("{:?}",t0.elapsed()); diff --git a/src/v0.rs b/src/v0.rs index ee754f5..ec8f4b0 100644 --- a/src/v0.rs +++ b/src/v0.rs @@ -1,4 +1,7 @@ -use binrw::{binrw,BinReaderExt,io::TakeSeekExt}; +use std::io::{SeekFrom,Error as IoError}; +use binrw::binrw; +use binrw::io::{TakeSeek,TakeSeekExt}; +use binrw::BinReaderExt; // the bit chunks are deposited in reverse fn read_trey_float(bits:u32)->f32{ @@ -69,6 +72,20 @@ impl GameControls{ } } +// generic timed event +#[binrw] +#[brw(little)] +#[derive(Debug)] +pub struct Timed + where + E:for<'a>binrw::BinRead=()>, + E:for<'a>binrw::BinWrite=()>, +{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:E, +} + // input #[binrw] #[brw(little)] @@ -78,13 +95,6 @@ pub struct InputEvent{ pub game_controls:GameControls, pub mouse_pos:Vector2, } -#[binrw] -#[brw(little)] -pub struct TimedInputEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:InputEvent, -} // output bitflags::bitflags!{ @@ -119,13 +129,6 @@ pub struct OutputEvent{ pub velocity:Vector3, pub acceleration:Vector3, } -#[binrw] -#[brw(little)] -pub struct TimedOutputEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:OutputEvent, -} // sound #[binrw] @@ -156,13 +159,6 @@ pub struct SoundEvent{ /// Roblox enum pub material:u32, } -#[binrw] -#[brw(little)] -pub struct TimedSoundEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:SoundEvent, -} // world #[binrw] @@ -214,13 +210,6 @@ pub enum WorldEvent{ #[brw(magic=3u32)] SetPaused(WorldEventSetPaused), } -#[binrw] -#[brw(little)] -pub struct TimedWorldEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:WorldEvent, -} // gravity #[binrw] @@ -228,13 +217,6 @@ pub struct TimedWorldEvent{ pub struct GravityEvent{ pub gravity:Vector3, } -#[binrw] -#[brw(little)] -pub struct TimedGravityEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:GravityEvent, -} // run #[binrw] @@ -305,13 +287,6 @@ pub struct RunEvent{ pub mode:Mode, pub flag_reason:FlagReason, } -#[binrw] -#[brw(little)] -pub struct TimedRunEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:RunEvent, -} // camera #[binrw] @@ -329,13 +304,6 @@ pub struct CameraEvent{ pub camera_event_type:CameraEventType, pub value:Vector3, } -#[binrw] -#[brw(little)] -pub struct TimedCameraEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:CameraEvent, -} // setting #[binrw] @@ -360,24 +328,17 @@ pub struct SettingEvent{ #[br(map=read_trey_double)] pub value:f64, } -#[binrw] -#[brw(little)] -pub struct TimedSettingEvent{ - #[br(map=read_trey_double)] - pub time:f64, - pub event:SettingEvent, -} #[derive(Default)] pub struct Block{ - pub input_events:Vec, - pub output_events:Vec, - pub sound_events:Vec, - pub world_events:Vec, - pub gravity_events:Vec, - pub run_events:Vec, - pub camera_events:Vec, - pub setting_events:Vec, + pub input_events:Vec>, + pub output_events:Vec>, + pub sound_events:Vec>, + pub world_events:Vec>, + pub gravity_events:Vec>, + pub run_events:Vec>, + pub camera_events:Vec>, + pub setting_events:Vec>, } #[binrw] @@ -409,17 +370,18 @@ struct EventChunkHeader{ } // binread args tech has been further refined -fn read_data_into_events<'a,R:BinReaderExt,T:binrw::BinRead=()>>(data:&mut R,events:&mut Vec,num_events:usize)->binrw::BinResult<()>{ - // there is only supposed to be at most one of each type of event chunk per block, so no need to amortize. - events.reserve_exact(num_events); - for _ in 0..num_events{ - events.push(data.read_le()?); - } - Ok(()) -} -fn read_data_into_events_amortized<'a,R:BinReaderExt,T:binrw::BinRead=()>>(data:&mut R,events:&mut Vec,num_events:usize)->binrw::BinResult<()>{ - // this is used when reading multiple blocks into a single object, so amortize the allocation cost. - events.reserve(num_events); +fn read_data_into_events<'a,R,T,F>( + data:&mut R, + events:&mut Vec, + num_events:usize, + reserve_fn:F, +)->binrw::BinResult<()> + where + R:BinReaderExt, + T:binrw::BinRead=()>, + F:Fn(&mut Vec,usize), +{ + reserve_fn(events,num_events); for _ in 0..num_events{ events.push(data.read_le()?); } @@ -427,39 +389,44 @@ fn read_data_into_events_amortized<'a,R:BinReaderExt,T:binrw::BinRead=( } impl Block{ - fn read(data:R)->binrw::BinResult{ + pub fn from_reader(data:R)->binrw::BinResult{ let mut block=Block::default(); - Block::read_into(data,&mut block)?; + // there is only supposed to be at most one of each type + // of event chunk per block, so allocate the size exactly. + block.extend_from_reader_exact(data)?; Ok(block) } - fn read_into(mut data:R,block:&mut Block)->binrw::BinResult<()>{ + /// Read a complete data block and append the elements to the timelines in this block. + /// Reserves exactly enough information for the new data. + pub fn extend_from_reader_exact(&mut self,mut data:R)->binrw::BinResult<()>{ // well... this looks error prone while let Ok(event_chunk_header)=data.read_le::(){ match event_chunk_header.event_type{ - EventType::Input=>read_data_into_events(&mut data,&mut block.input_events,event_chunk_header.num_events as usize)?, - EventType::Output=>read_data_into_events(&mut data,&mut block.output_events,event_chunk_header.num_events as usize)?, - EventType::Sound=>read_data_into_events(&mut data,&mut block.sound_events,event_chunk_header.num_events as usize)?, - EventType::World=>read_data_into_events(&mut data,&mut block.world_events,event_chunk_header.num_events as usize)?, - EventType::Gravity=>read_data_into_events(&mut data,&mut block.gravity_events,event_chunk_header.num_events as usize)?, - EventType::Run=>read_data_into_events(&mut data,&mut block.run_events,event_chunk_header.num_events as usize)?, - EventType::Camera=>read_data_into_events(&mut data,&mut block.camera_events,event_chunk_header.num_events as usize)?, - EventType::Setting=>read_data_into_events(&mut data,&mut block.setting_events,event_chunk_header.num_events as usize)?, + EventType::Input=>read_data_into_events(&mut data,&mut self.input_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, + EventType::Output=>read_data_into_events(&mut data,&mut self.output_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, + EventType::Sound=>read_data_into_events(&mut data,&mut self.sound_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, + EventType::World=>read_data_into_events(&mut data,&mut self.world_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, + EventType::Gravity=>read_data_into_events(&mut data,&mut self.gravity_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, + EventType::Run=>read_data_into_events(&mut data,&mut self.run_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, + EventType::Camera=>read_data_into_events(&mut data,&mut self.camera_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, + EventType::Setting=>read_data_into_events(&mut data,&mut self.setting_events,event_chunk_header.num_events as usize,Vec::reserve_exact)?, } } Ok(()) } - fn read_into_amortized(mut data:R,block:&mut Block)->binrw::BinResult<()>{ + /// Read a complete data block and append the elements to the timelines in this block. + pub fn extend_from_reader(&mut self,mut data:R)->binrw::BinResult<()>{ // sad code duplication while let Ok(event_chunk_header)=data.read_le::(){ match event_chunk_header.event_type{ - EventType::Input=>read_data_into_events_amortized(&mut data,&mut block.input_events,event_chunk_header.num_events as usize)?, - EventType::Output=>read_data_into_events_amortized(&mut data,&mut block.output_events,event_chunk_header.num_events as usize)?, - EventType::Sound=>read_data_into_events_amortized(&mut data,&mut block.sound_events,event_chunk_header.num_events as usize)?, - EventType::World=>read_data_into_events_amortized(&mut data,&mut block.world_events,event_chunk_header.num_events as usize)?, - EventType::Gravity=>read_data_into_events_amortized(&mut data,&mut block.gravity_events,event_chunk_header.num_events as usize)?, - EventType::Run=>read_data_into_events_amortized(&mut data,&mut block.run_events,event_chunk_header.num_events as usize)?, - EventType::Camera=>read_data_into_events_amortized(&mut data,&mut block.camera_events,event_chunk_header.num_events as usize)?, - EventType::Setting=>read_data_into_events_amortized(&mut data,&mut block.setting_events,event_chunk_header.num_events as usize)?, + EventType::Input=>read_data_into_events(&mut data,&mut self.input_events,event_chunk_header.num_events as usize,Vec::reserve)?, + EventType::Output=>read_data_into_events(&mut data,&mut self.output_events,event_chunk_header.num_events as usize,Vec::reserve)?, + EventType::Sound=>read_data_into_events(&mut data,&mut self.sound_events,event_chunk_header.num_events as usize,Vec::reserve)?, + EventType::World=>read_data_into_events(&mut data,&mut self.world_events,event_chunk_header.num_events as usize,Vec::reserve)?, + EventType::Gravity=>read_data_into_events(&mut data,&mut self.gravity_events,event_chunk_header.num_events as usize,Vec::reserve)?, + EventType::Run=>read_data_into_events(&mut data,&mut self.run_events,event_chunk_header.num_events as usize,Vec::reserve)?, + EventType::Camera=>read_data_into_events(&mut data,&mut self.camera_events,event_chunk_header.num_events as usize,Vec::reserve)?, + EventType::Setting=>read_data_into_events(&mut data,&mut self.setting_events,event_chunk_header.num_events as usize,Vec::reserve)?, } } Ok(()) @@ -469,7 +436,7 @@ impl Block{ #[derive(Debug)] pub enum Error{ InvalidBlockId(BlockId), - Seek(std::io::Error), + Seek(IoError), InvalidData(binrw::Error), } impl std::fmt::Display for Error{ @@ -488,20 +455,12 @@ pub struct BlockId(#[br(map=|i:u32|i-1)]u32); #[derive(Debug,Clone,Copy)] pub struct BlockPosition(#[br(map=|i:u32|i-1)]u32); -#[binrw] -#[brw(little)] -#[derive(Debug,Clone,Copy)] -pub struct TimedBlockId{ - #[br(map=read_trey_double)] - pub time:f64, - pub block_id:BlockId, -} -impl PartialEq for TimedBlockId{ +impl PartialEq for Timed{ fn eq(&self,other:&Self)->bool{ self.time.eq(&other.time) } } -impl PartialOrd for TimedBlockId{ +impl PartialOrd for Timed{ fn partial_cmp(&self,other:&Self)->Option{ self.time.partial_cmp(&other.time) } @@ -512,21 +471,34 @@ impl PartialOrd for TimedBlockId{ #[derive(Debug)] pub struct FileHeader{ #[brw(magic=b"qbot")] - pub file_version:u32, - pub num_offline_blocks:u32, - pub num_realtime_blocks:u32, - #[br(count=num_offline_blocks+num_realtime_blocks+1)] - pub block_positions:Vec, - #[br(count=num_offline_blocks)] - pub offline_blocks_timeline:Vec, - #[br(count=num_realtime_blocks)] - pub realtime_blocks_timeline:Vec, -} -pub struct BlockInfo{ - start:u32, - length:u32, + file_version:u32, + num_offline_blocks:u32, + num_realtime_blocks:u32, } impl FileHeader{ + pub fn from_reader(mut data:R)->binrw::BinResult{ + data.read_le() + } +} +#[binrw] +#[brw(little)] +#[derive(Debug)] +#[br(import(num_offline_blocks:u32,num_realtime_blocks:u32))] +pub struct BlockTimelines{ + #[br(count=num_offline_blocks+num_realtime_blocks+1)] + block_positions:Vec, + #[br(count=num_offline_blocks)] + offline_blocks_timeline:Vec>, + #[br(count=num_realtime_blocks)] + realtime_blocks_timeline:Vec>, +} +impl BlockTimelines{ + pub fn offline_blocks(&self)->&[Timed]{ + &self.offline_blocks_timeline + } + pub fn realtime_blocks(&self)->&[Timed]{ + &self.realtime_blocks_timeline + } pub fn block_info(&self,BlockId(block_id):BlockId)->Result{ if self.block_positions.len() as u32<=block_id{ return Err(Error::InvalidBlockId(BlockId(block_id))); @@ -536,86 +508,42 @@ impl FileHeader{ Ok(BlockInfo{start,length:end-start}) } } - -struct MergeIter,It1:Iterator>{ - it0:It0, - it1:It1, - item0:Option, - item1:Option, -} -impl,It1:Iterator> MergeIter{ - fn new(mut it0:It0,mut it1:It1)->Self{ - Self{ - item0:it0.next(), - item1:it1.next(), - it0, - it1, - } +impl BlockTimelines{ + pub fn from_reader(header:&FileHeader,mut data:R)->binrw::BinResult{ + data.read_le_args((header.num_offline_blocks,header.num_realtime_blocks)) } } -impl,It1:Iterator> Iterator for MergeIter{ - type Item=T; - fn next(&mut self)->Option{ - match (&self.item0,&self.item1){ - (None,None)=>None, - (Some(_),None)=>core::mem::replace(&mut self.item0,self.it0.next()), - (None,Some(_))=>core::mem::replace(&mut self.item1,self.it1.next()), - (Some(item0),Some(item1))=>match item0.partial_cmp(item1){ - Some(core::cmp::Ordering::Less) - |Some(core::cmp::Ordering::Equal) - |None - =>core::mem::replace(&mut self.item0,self.it0.next()), - Some(core::cmp::Ordering::Greater) - =>core::mem::replace(&mut self.item1,self.it1.next()), - }, - } +pub struct BlockInfo{ + start:u32, + length:u32, +} +impl BlockInfo{ + pub fn start(&self)->u32{ + self.start + } + pub fn length(&self)->u32{ + self.length + } + /// Create an adapter which seeks to the block start and reads at most the block length. + pub fn take_seek(&self,mut data:R)->Result,IoError>{ + data.seek(SeekFrom::Start(self.start() as u64))?; + Ok(data.take_seek(self.length() as u64)) } } -pub struct File{ - pub header:FileHeader, - pub data:FileData, -} -impl File{ - pub fn new(mut data:R)->Result,binrw::Error>{ - Ok(File{ - header:data.read_le()?, - data:FileData{data}, - }) - } - pub fn read_all(&mut self)->Result{ - let block_iter=MergeIter::new( - self.header.offline_blocks_timeline.iter(), - self.header.realtime_blocks_timeline.iter(), - ); - let mut big_block=Block::default(); - for &TimedBlockId{time:_,block_id} in block_iter{ - let block_info=self.header.block_info(block_id)?; - self.data.read_block_info_into_block(block_info,&mut big_block)?; - } - Ok(big_block) - } -} - -pub struct FileData{ - data:R, -} -impl FileData{ - fn data_mut(&mut self)->&mut R{ - &mut self.data - } - fn block_reader(&mut self,block_info:BlockInfo)->Result,Error>{ - self.data.seek(std::io::SeekFrom::Start(block_info.start as u64)).map_err(Error::Seek)?; - Ok(self.data_mut().take_seek(block_info.length as u64)) - } - pub fn read_block_info(&mut self,block_info:BlockInfo)->Result{ - let data=self.block_reader(block_info)?; - let block=Block::read(data).map_err(Error::InvalidData)?; - Ok(block) - } - pub fn read_block_info_into_block(&mut self,block_info:BlockInfo,block:&mut Block)->Result<(),Error>{ - let data=self.block_reader(block_info)?; - Block::read_into_amortized(data,block).map_err(Error::InvalidData)?; - Ok(()) - } +/// Read the entire file and combine the timelines into a single Block. +/// Note that this reads the blocks in chronological order, not the order they appear in the file, so there is some seeking involved. +#[cfg(feature="itertools")] +pub fn read_all_to_block(mut data:R)->Result{ + let header=FileHeader::from_reader(&mut data).map_err(Error::InvalidData)?; + let block_timelines=BlockTimelines::from_reader(&header,&mut data).map_err(Error::InvalidData)?; + let mut block=Block::default(); + for timed in itertools::merge(block_timelines.offline_blocks(),block_timelines.realtime_blocks()){ + let take_seek=block_timelines + .block_info(timed.event)? + .take_seek(&mut data) + .map_err(Error::Seek)?; + block.extend_from_reader(take_seek).map_err(Error::InvalidData)?; + } + Ok(block) }