diff --git a/Cargo.lock b/Cargo.lock index 8191806..d7dd632 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 = "4.2.3" @@ -80,6 +89,7 @@ version = "0.8.0" dependencies = [ "binrw", "bitflags", + "itertools", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aed3a45..d564eeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,13 @@ [package] name = "strafesnet_roblox_bot_file" version = "0.8.0" -edition = "2021" +edition = "2024" [dependencies] binrw = "0.15.0" bitflags = "2.6.0" +itertools = { version = "0.14.0", optional = true } [features] +default = ["itertools"] +itertools = ["dep:itertools"] diff --git a/src/tests.rs b/src/tests.rs index cdbd2da..9274458 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -27,4 +27,15 @@ fn deserialize_all()->Result<(),v0::Error>{ Ok(()) } -// TODO: file serialization test +#[test] +#[cfg(feature="itertools")] +fn serialize_round_trip()->Result<(),binrw::Error>{ + let file=std::fs::read("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap(); + let block=v0::read_all_to_block(std::io::Cursor::new(file.as_slice())).unwrap(); + + let mut data=Vec::with_capacity(file.len()); + v0::serialize(&block,&mut std::io::Cursor::new(&mut data))?; + + // TODO: It encodes, but is it equal? Test something! PartialEq? + Ok(()) +} diff --git a/src/v0.rs b/src/v0.rs index e5a5507..9d3c4e1 100644 --- a/src/v0.rs +++ b/src/v0.rs @@ -11,20 +11,36 @@ fn read_trey_float(bits:u32)->f32{ let m=(bits>>(1+8))&((1<<23)-1); f32::from_bits(m|(e<<23)|(s<<31)) } +fn write_trey_float(value:&f32)->u32{ + let bits=value.to_bits(); + let s=(bits>>31)&1; + let e=(bits>>23)&((1<<8)-1); + let m=bits&((1<<23)-1); + m<<(1+8)|(e<<1)|s +} fn read_trey_double(bits:u64)->f64{ let s=bits&1; let e=(bits>>1)&((1<<11)-1); let m=(bits>>(1+11))&((1<<52)-1); f64::from_bits(m|(e<<52)|(s<<63)) } +fn write_trey_double(value:&f64)->u64{ + let bits=value.to_bits(); + let s=(bits>>63)&1; + let e=(bits>>52)&((1<<11)-1); + let m=bits&((1<<52)-1); + m<<(1+11)|(e<<1)|s +} #[binrw] #[brw(little)] #[derive(Debug,Clone)] pub struct Vector2{ #[br(map=read_trey_float)] + #[bw(map=write_trey_float)] pub x:f32, #[br(map=read_trey_float)] + #[bw(map=write_trey_float)] pub y:f32, } #[binrw] @@ -32,10 +48,13 @@ pub struct Vector2{ #[derive(Debug,Clone)] pub struct Vector3{ #[br(map=read_trey_float)] + #[bw(map=write_trey_float)] pub x:f32, #[br(map=read_trey_float)] + #[bw(map=write_trey_float)] pub y:f32, #[br(map=read_trey_float)] + #[bw(map=write_trey_float)] pub z:f32, } @@ -85,6 +104,7 @@ pub struct Timed E:for<'a>binrw::BinWrite=()>, { #[br(map=read_trey_double)] + #[bw(map=write_trey_double)] pub time:f64, pub event:E, } @@ -214,6 +234,7 @@ pub struct WorldEventButton{ #[derive(Debug,Clone)] pub struct WorldEventSetTime{ #[br(map=read_trey_double)] + #[bw(map=write_trey_double)] pub time:f64, #[br(temp)] #[bw(ignore)] @@ -430,6 +451,7 @@ pub enum SettingType{ pub struct SettingEvent{ pub setting_type:SettingType, #[br(map=read_trey_double)] + #[bw(map=write_trey_double)] pub value:f64, } @@ -449,6 +471,7 @@ pub struct Block{ #[binrw] #[brw(little)] +#[derive(Clone,Copy)] enum EventType{ #[brw(magic=1u32)] Input, @@ -564,11 +587,19 @@ impl std::error::Error for Error{} #[binrw] #[brw(little)] #[derive(Debug,Clone,Copy)] -pub struct BlockId(#[br(map=|i:u32|i-1)]u32); +pub struct BlockId( + #[br(map=|i:u32|i-1)] + #[bw(map=|&i:&u32|i+1)] + u32 +); #[binrw] #[brw(little)] #[derive(Debug,Clone)] -struct BlockPosition(#[br(map=|i:u32|i-1)]u32); +struct BlockPosition( + #[br(map=|i:u32|i-1)] + #[bw(map=|&i:&u32|i+1)] + u32 +); #[derive(Debug)] pub struct InvalidBlockId(pub BlockId); @@ -698,3 +729,241 @@ pub fn read_all_to_block(mut data:R)->Result{ block.extend_from_block_id_iter(&mut data,&block_timelines,block_timelines.realtime_blocks())?; Ok(block) } + +#[cfg(feature="itertools")] +pub fn serialize(block:&Block,writer:&mut W)->Result<(),BinrwError>{ + use std::ops::Range; + const MAX_BLOCK_SIZE:usize=1<<14; + const FILE_VERSION:u32=0; + const EVENT_TYPES:[EventType;8]=[ + EventType::Input, + EventType::Output, + EventType::Sound, + EventType::World, + EventType::Gravity, + EventType::Run, + EventType::Camera, + EventType::Setting, + ]; + const EVENT_SIZE:[usize;8]=[ + 8+4+2*4, // Input + 8+4+4*3*4, // Output + 8+4+4, // Sound + 8+4+12, // World + 8+3*4, // Gravity + 8+4+4+4, // Run + 8+4+3*4, // Camera + 8+4+8, // Setting + ]; + #[derive(Clone,Default)] + struct Plan([T;8]); + // A plan of how many events of each type to include in a data block. + impl Plan{ + /// Predict the size increment from adding a new event. + fn size_increase(&self,event_type:EventType)->usize{ + let new_chunk_header=self.0[event_type as usize]==0; + let mask=(-(new_chunk_header as isize)) as usize; + EVENT_SIZE[event_type as usize]+(mask&size_of::()) + } + /// Add the new event. + fn accumulate(&mut self,event_type:EventType){ + self.0[event_type as usize]+=1; + } + fn range(&self,end:&Plan)->Plan>{ + Plan(core::array::from_fn(|i|self.0[i]..end.0[i])) + } + } + // A plan of what range of events to include in a data block. + impl Plan>{ + /// Calculate the predicted size of the planned block. + fn size(&self)->usize{ + self.0.iter() + .zip(EVENT_SIZE) + .filter_map(|(range,event_size)|match range.len(){ + 0=>None, + other=>Some(other*event_size+size_of::()), + }) + .sum() + } + } + // compare an event at the head of the plan to the best event collected so far. + fn collect_event( + best:&mut Option<(f64,EventType)>, + list:&[Timed], + plan:&Plan, + event_type:EventType, + ) + where + E:for<'a>binrw::BinRead=()>, + E:for<'a>binrw::BinWrite=()>, + { + if let Some(event)=list.get(plan.0[event_type as usize]) + &&best.is_none_or(|(time,_)|event.time,next_event:impl Fn(&Plan)->Option<(f64,EventType)>)->Option{ + let mut size=0; + let (start_time,first_event)=next_event(plan)?; + + size+=plan.size_increase(first_event); + if MAX_BLOCK_SIZE>, + } + fn plan_timeline(next_event:F)->std::collections::VecDeque + where + F:Copy, + F:Fn(&Plan)->Option<(f64,EventType)> + { + let mut timeline=std::collections::VecDeque::new(); + let mut plan=Plan::default(); + let mut last_plan=plan.clone(); + let mut index=0; + while let Some(time)=plan_block(&mut plan,next_event){ + timeline.push_back(PlannedBlock{ + index, + time, + plan:last_plan.range(&plan), + }); + last_plan=plan.clone(); + index+=1; + } + timeline + } + // plan events into segments without spilling over max size threshold + // each plan describes the range of events included in the block. + let mut plan_offline=plan_timeline(|plan|{ + let mut next_event=None; + collect_event(&mut next_event,&block.world_events,plan,EventType::World); + collect_event(&mut next_event,&block.gravity_events,plan,EventType::Gravity); + collect_event(&mut next_event,&block.run_events,plan,EventType::Run); + collect_event(&mut next_event,&block.camera_events,plan,EventType::Camera); + collect_event(&mut next_event,&block.setting_events,plan,EventType::Setting); + next_event + }); + let mut plan_realtime=plan_timeline(|plan|{ + let mut next_event=None; + collect_event(&mut next_event,&block.input_events,plan,EventType::Input); + collect_event(&mut next_event,&block.output_events,plan,EventType::Output); + collect_event(&mut next_event,&block.sound_events,plan,EventType::Sound); + next_event + }); + + let file_header=FileHeader{ + file_version:FILE_VERSION, + num_offline_blocks:plan_offline.len() as u32, + num_realtime_blocks:plan_realtime.len() as u32, + }; + + let mut plan_order=Vec::with_capacity(plan_offline.len()+plan_realtime.len()); + let mut block_positions=Vec::with_capacity(file_header.block_position_count() as usize); + // Fill the timelines with dummy values, we don't know the block ids yet. + // This can be done with Vec::spare_capacity_mut and unsafe, but whatever. + const DUMMY_BLOCK:Timed=Timed{time:0.0,event:BlockId(0)}; + let mut offline_blocks_timeline=vec![DUMMY_BLOCK;plan_offline.len()]; + let mut realtime_blocks_timeline=vec![DUMMY_BLOCK;plan_realtime.len()]; + + { + // position starts after the *predicted* end of the BlockTimelines + let mut position=file_header.block_timelines_info().end; + let mut block_id=0; + let mut push_block=|timeline:&mut Vec>,planned:PlannedBlock|{ + block_positions.push(BlockPosition(position)); + position+=planned.plan.size() as u32; + + // write the block id to the correct index + timeline[planned.index]=Timed{ + time:planned.time, + event:BlockId(block_id), + }; + block_id+=1; + + plan_order.push(planned.plan); + }; + // the first block in the file is an offline block to + // initialize the state of things like the current style + if let Some(plan)=plan_offline.pop_front(){ + push_block(&mut offline_blocks_timeline,plan); + } + // the second block is the first realtime block which + // includes the starting position of the replay + if let Some(plan)=plan_realtime.pop_front(){ + push_block(&mut realtime_blocks_timeline,plan); + } + // the third block is the last realtime block which + // is used by the game client to determine the duration + if let Some(plan)=plan_realtime.pop_back(){ + push_block(&mut realtime_blocks_timeline,plan); + } + // push the remaining blocks in chronological order + for either_plan in itertools::merge_join_by( + plan_offline, + plan_realtime, + |offline,realtime|offline.time<=realtime.time, + ){ + match either_plan{ + itertools::Either::Left(offline)=>push_block(&mut offline_blocks_timeline,offline), + itertools::Either::Right(realtime)=>push_block(&mut realtime_blocks_timeline,realtime), + } + } + // final position + block_positions.push(BlockPosition(position)); + } + + let block_timelines=BlockTimelines{ + block_positions, + offline_blocks_timeline, + realtime_blocks_timeline, + }; + + use binrw::BinWrite; + file_header.write_le(writer)?; + block_timelines.write_le(writer)?; + for plan in plan_order{ + for (range,event_type) in plan.0.into_iter().zip(EVENT_TYPES){ + let num_events=range.len(); + if num_events==0{ + continue; + } + let event_chunk_header=EventChunkHeader{ + event_type, + num_events:num_events as u32, + }; + event_chunk_header.write_le(writer)?; + match event_type{ + EventType::Input=>block.input_events[range].write_le(writer)?, + EventType::Output=>block.output_events[range].write_le(writer)?, + EventType::Sound=>block.sound_events[range].write_le(writer)?, + EventType::World=>block.world_events[range].write_le(writer)?, + EventType::Gravity=>block.gravity_events[range].write_le(writer)?, + EventType::Run=>block.run_events[range].write_le(writer)?, + EventType::Camera=>block.camera_events[range].write_le(writer)?, + EventType::Setting=>block.setting_events[range].write_le(writer)?, + } + } + } + + Ok(()) +}