diff --git a/lib/snf/src/bot.rs b/lib/snf/src/bot.rs index ffdaee8..3712341 100644 --- a/lib/snf/src/bot.rs +++ b/lib/snf/src/bot.rs @@ -1,98 +1,339 @@ -use binrw::{BinReaderExt, binrw}; +use binrw::{binrw,BinReaderExt,BinWrite,BinWriterExt}; + +use crate::newtypes; +use crate::file::BlockId; +use strafesnet_common::physics::Time; + +type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>; #[derive(Debug)] pub enum Error{ - InvalidHeader, + InvalidHeader(binrw::Error), InvalidSegment(binrw::Error), + SegmentConvert(newtypes::integer::RatioError), + InstructionConvert(newtypes::physics::InstructionConvert), + InstructionWrite(binrw::Error), InvalidSegmentId(SegmentId), + InvalidData(binrw::Error), + IO(std::io::Error), File(crate::file::Error), } +// Bot files are simply the sequence of instructions that the physics received during the run. +// The instructions are partitioned into timestamped blocks for ease of streaming. +// +// Keyframe information required for efficient seeking +// is part of a different file, and is generated from this file. + /* block types BLOCK_BOT_HEADER: -u128 map_resource_uuid //which map is this bot running -//don't include style info in bot header because it's in the simulation state -//blocks are laid out in chronological order, but indices may jump around. -u64 num_segments +// Tegments are laid out in chronological order, +// but block_id is not necessarily in ascending order. +// +// This is to place the final segment close to the start of the file, +// which allows the duration of the bot to be conveniently calculated +// from the first and last instruction timestamps. +u32 num_segments for _ in 0..num_segments{ - i64 time //simulation_state timestamp - u64 block_id + i64 time + u32 instruction_count + u32 block_id } BLOCK_BOT_SEGMENT: -//format version indicates what version of these structures to use -SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about. -//to read, greedily decode instructions until eof -loop{ - //delta encode as much as possible (time,mousepos) - //strafe ticks are implied - //physics can be implied in an input-only bot file - TimedInstruction<SimulationInstruction> instruction +// segments can potentially be losslessly compressed! +for _ in 0..instruction_count{ + // TODO: delta encode as much as possible (time,mousepos) + i64 time + physics::Instruction instruction } */ -//error hiding mock code -mod simulation{ - #[super::binrw] - #[brw(little)] - pub struct State{} - #[super::binrw] - #[brw(little)] - pub struct Instruction{} +#[binrw] +#[brw(little)] +struct SegmentHeader{ + time:i64, + instruction_count:u32, + block_id:BlockId, +} +#[binrw] +#[brw(little)] +struct Header{ + num_segments:u32, + #[br(count=num_segments)] + segments:Vec<SegmentHeader>, } -// mod instruction{ - // #[super::binrw] - // #[brw(little)] - // pub struct TimedInstruction<Instruction:binrw::BinRead+binrw::BinWrite>{ - // time:u64, - // instruction:Instruction - // } -// } -// mod timeline{ - // #[super::binrw] - // #[brw(little)] - // pub struct Timeline<Instruction:binrw::BinRead+binrw::BinWrite>{ - // #[bw(try_calc(u32::try_from(instructions.len())))] - // instruction_count:u32, - // #[br(count=instruction_count)] - // instructions:Vec<super::instruction::TimedInstruction<Instruction>> - // } -// } - -//serious code #[binrw] #[brw(little)] #[derive(Clone,Copy,Debug,id::Id)] -pub struct SegmentId(u32); +struct SegmentId(u32); -#[binrw] -#[brw(little)] pub struct Segment{ - state:simulation::State, - //#[bw(try_calc(u32::try_from(instructions.len())))] - //instruction_count:u32, - //#[br(count=instruction_count)] - //instructions:Vec<instruction::TimedInstruction<simulation::Instruction>> + pub instructions:Vec<TimedPhysicsInstruction> +} - //please remember that strafe ticks are implicit! 33% smaller bot files +#[derive(Clone,Copy,Debug)] +pub struct SegmentInfo{ + /// time of the first instruction in this segment. + time:Time, + instruction_count:u32, + /// How many total instructions in segments up to and including this segment + /// Alternatively, the id of the first instruction be in the _next_ segment + instructions_subtotal:u64, + block_id:BlockId, } pub struct StreamableBot<R:BinReaderExt>{ file:crate::file::File<R>, - //timeline:timeline::Timeline<SegmentId>, - segment_id_to_block_id:Vec<crate::file::BlockId>, + segment_map:Vec<SegmentInfo>, } impl<R:BinReaderExt> StreamableBot<R>{ - pub(crate) fn new(file:crate::file::File<R>)->Result<Self,Error>{ - Err(Error::InvalidHeader) + pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{ + //assume the file seek is in the right place to start reading header + let header:Header=file.data_mut().read_le().map_err(Error::InvalidHeader)?; + let mut instructions_subtotal=0; + let segment_map=header.segments.into_iter().map(|SegmentHeader{time,instruction_count,block_id}|{ + instructions_subtotal+=instruction_count as u64; + SegmentInfo{ + time:Time::raw(time), + instruction_count, + instructions_subtotal, + block_id, + } + }).collect(); + Ok(Self{ + file, + segment_map, + }) } - pub fn load_segment(&mut self,segment_id:SegmentId)->Result<Segment,Error>{ - let block_id=*self.segment_id_to_block_id.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?; - let mut block=self.file.block_reader(block_id).map_err(Error::File)?; - let segment=block.read_le().map_err(Error::InvalidSegment)?; + fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{ + Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?) + } + pub fn find_segments_instruction_range(&self,start_instruction:u64,end_instruction:u64)->&[SegmentInfo]{ + let start=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<start_instruction); + let end=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<end_instruction); + &self.segment_map[start..=end] + } + // pub fn find_segments_time_range(&self,start_time:Time,end_time:Time)->&[SegmentInfo]{ + // // TODO: This is off by one, both should be one less + // let start=self.segment_map.partition_point(|segment_info|segment_info.time<start_time); + // let end=self.segment_map.partition_point(|segment_info|segment_info.time<end_time); + // &self.segment_map[start..=end] + // } + fn append_to_segment(&mut self,segment_info:SegmentInfo,segment:&mut Segment)->Result<(),Error>{ + let mut block=self.file.block_reader(segment_info.block_id).map_err(Error::File)?; + for _ in 0..segment_info.instruction_count{ + let instruction:newtypes::physics::TimedInstruction=block.read_le().map_err(Error::InvalidSegment)?; + segment.instructions.push(instruction.try_into().map_err(Error::SegmentConvert)?); + } + Ok(()) + } + pub fn load_segment(&mut self,segment_info:SegmentInfo)->Result<Segment,Error>{ + let mut segment=Segment{ + instructions:Vec::with_capacity(segment_info.instruction_count as usize), + }; + self.append_to_segment(segment_info,&mut segment)?; + Ok(segment) + } + pub fn read_all(&mut self)->Result<Segment,Error>{ + let mut segment=Segment{ + instructions:Vec::new(), + }; + for i in 0..self.segment_map.len(){ + let segment_info=self.segment_map[i]; + self.append_to_segment(segment_info,&mut segment)?; + } Ok(segment) } } + +const MAX_BLOCK_SIZE:usize=64*1024;//64 kB +pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{ + // decide which instructions to put in which segment + // write segment 1 to block 1 + // write segment N to block 2 + // write rest of segments + // 1 2 3 4 5 + // becomes + // [1 5] 2 3 4 + struct SegmentHeaderInfo{ + time:Time, + instruction_count:u32, + range:core::ops::Range<usize> + } + + let mut segment_header_infos=Vec::new(); + let mut raw_segments=std::io::Cursor::new(Vec::new()); + + // block info + let mut start_time=Time::ZERO; + let mut start_position=raw_segments.position() as usize; + let mut instruction_count=0; + + let mut last_position=start_position; + + let mut iter=instructions.into_iter(); + + macro_rules! collect_instruction{ + ($instruction:expr)=>{ + let time=$instruction.time; + let instruction_writable:newtypes::physics::TimedInstruction=$instruction.try_into().map_err(Error::InstructionConvert)?; + instruction_writable.write_le(&mut raw_segments).map_err(Error::InstructionWrite)?; + instruction_count+=1; + let position=raw_segments.position() as usize; + // exceeds max block size + if MAX_BLOCK_SIZE<position-last_position{ + segment_header_infos.push(SegmentHeaderInfo{ + time:start_time, + instruction_count, + range:start_position..last_position, + }); + start_position=last_position; + instruction_count=0; + start_time=time; + } + last_position=position; + } + } + + // unroll one loop iteration to grab the starting time + if let Some(instruction)=iter.next(){ + start_time=instruction.time; + collect_instruction!(instruction); + } + + for instruction in iter{ + collect_instruction!(instruction); + } + //last block, whatever size it happens to be + { + let final_position=raw_segments.position() as usize; + segment_header_infos.push(SegmentHeaderInfo{ + time:start_time, + instruction_count, + range:start_position..final_position, + }); + } + // drop cursor + let raw_segments=raw_segments.into_inner(); + + let num_segments=segment_header_infos.len(); + + // segments list is in chronological order + let make_segment_header=|block_id,&SegmentHeaderInfo{time,instruction_count,range:ref _range}|SegmentHeader{ + time:time.get(), + instruction_count, + block_id, + }; + let segments=if 2<num_segments{ + let mut segments=Vec::with_capacity(num_segments); + // segment 1 is second block + if let Some(seg)=segment_header_infos.first(){ + segments.push(make_segment_header(BlockId::new(1),seg)); + } + // rest of segments start at fourth block + for (i,seg) in segment_header_infos[1..num_segments-1].iter().enumerate(){ + make_segment_header(BlockId::new(3+i as u32),seg); + } + // last segment is third block + if let Some(seg)=segment_header_infos.last(){ + segments.push(make_segment_header(BlockId::new(2),seg)); + } + segments + }else{ + // all segments in order + segment_header_infos.iter().enumerate().map(|(i,seg)| + make_segment_header(BlockId::new(1+i as u32),seg) + ).collect() + }; + + let header=Header{ + num_segments:num_segments as u32, + segments, + }; + + // map header is +1 + let block_count=1+num_segments as u32; + + let mut offset=crate::file::Header::calculate_size(block_count) as u64; + // block_location is one longer than block_count + let mut block_location=Vec::with_capacity(1+block_count as usize); + + //probe header length + let mut bot_header_data=Vec::new(); + binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?; + + // the first block location is the map header + block_location.push(offset); + offset+=bot_header_data.len() as u64; + block_location.push(offset); + + // priming includes file header + first 3 blocks [bot header, first segment, last segment] + let priming=if 2<num_segments{ + // segment 1 is block 2 + if let Some(seg)=segment_header_infos.first(){ + offset+=seg.range.len() as u64; + block_location.push(offset); + } + // last segment is block 3 + if let Some(seg)=segment_header_infos.last(){ + offset+=seg.range.len() as u64; + block_location.push(offset); + } + + let priming=offset; + + // rest of segments + for seg in &segment_header_infos[1..num_segments-1]{ + offset+=seg.range.len() as u64; + block_location.push(offset); + } + priming + }else{ + // all segments in order + for seg in &segment_header_infos{ + offset+=seg.range.len() as u64; + block_location.push(offset); + } + offset + }; + + let file_header=crate::file::Header{ + fourcc:crate::file::FourCC::Bot, + version:0, + priming, + resource:0, + block_count, + block_location, + }; + + // write file header + writer.write_le(&file_header).map_err(Error::InvalidData)?; + // write bot header + writer.write(&bot_header_data).map_err(Error::IO)?; + + // write blocks + if 2<num_segments{ + // segment 1 is block 2 + if let Some(seg)=segment_header_infos.first(){ + writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?; + } + // last segment is block 3 + if let Some(seg)=segment_header_infos.last(){ + writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?; + } + // rest of segments + for seg in &segment_header_infos[1..num_segments-1]{ + writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?; + } + }else{ + // all segments in order + for seg in segment_header_infos{ + writer.write(&raw_segments[seg.range]).map_err(Error::IO)?; + } + } + Ok(()) +}