From 6beb6c5f9ab9a48a5b3e92b9a02b01150d89e4a1 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Thu, 16 Jan 2025 20:36:03 -0800 Subject: [PATCH] implement bot file --- lib/snf/src/bot.rs | 363 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 302 insertions(+), 61 deletions(-) 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; #[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 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, } -// mod instruction{ - // #[super::binrw] - // #[brw(little)] - // pub struct TimedInstruction{ - // time:u64, - // instruction:Instruction - // } -// } -// mod timeline{ - // #[super::binrw] - // #[brw(little)] - // pub struct Timeline{ - // #[bw(try_calc(u32::try_from(instructions.len())))] - // instruction_count:u32, - // #[br(count=instruction_count)] - // instructions:Vec> - // } -// } - -//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> + pub instructions:Vec +} - //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{ file:crate::file::File, - //timeline:timeline::Timeline, - segment_id_to_block_id:Vec, + segment_map:Vec, } impl StreamableBot{ - pub(crate) fn new(file:crate::file::File)->Result{ - Err(Error::InvalidHeader) + pub(crate) fn new(mut file:crate::file::File)->Result{ + //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{ - 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{ + 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&[SegmentInfo]{ + // // TODO: This is off by one, both should be one less + // let start=self.segment_map.partition_point(|segment_info|segment_info.timeResult<(),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{ + 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{ + 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(mut writer:W,instructions:impl IntoIterator)->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 + } + + 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