|
|
|
|
@@ -0,0 +1,845 @@
|
|
|
|
|
use std::io::{SeekFrom,Error as IoError};
|
|
|
|
|
use binrw::binrw;
|
|
|
|
|
use binrw::io::{TakeSeek,TakeSeekExt};
|
|
|
|
|
use binrw::BinReaderExt;
|
|
|
|
|
use crate::BinrwError;
|
|
|
|
|
|
|
|
|
|
pub use crate::v0::{FlagReason,GameControls,GameControlsError,ModeID,ModeSpec,SoundType,SoundEvent,Style,TickInfo,TickInfoError};
|
|
|
|
|
|
|
|
|
|
const EVENT_SIZE:[usize;8]=[
|
|
|
|
|
4+4+2*4, // Input
|
|
|
|
|
4+4+4*3*4, // Output
|
|
|
|
|
4+4+4, // Sound
|
|
|
|
|
4+4+12, // World
|
|
|
|
|
4+3*4, // Gravity
|
|
|
|
|
4+4+4+4, // Run
|
|
|
|
|
4+4+3*4, // Camera
|
|
|
|
|
4+4+8, // Setting
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum TimeFromFloatError{
|
|
|
|
|
Nan,
|
|
|
|
|
Overflow,
|
|
|
|
|
Underflow,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd)]
|
|
|
|
|
pub struct Time(u32);
|
|
|
|
|
impl Time{
|
|
|
|
|
const TIME_UNITS_PER_SECOND:f64=65536.0;
|
|
|
|
|
const TIME_SECONDS_PER_UNIT:f64=1.0/Self::TIME_UNITS_PER_SECOND;
|
|
|
|
|
pub const fn new(value:f64)->Result<Self,TimeFromFloatError>{
|
|
|
|
|
use core::num::FpCategory::*;
|
|
|
|
|
match value.classify(){
|
|
|
|
|
Nan=>Err(TimeFromFloatError::Nan),
|
|
|
|
|
Zero=>Ok(Time(0)),
|
|
|
|
|
Subnormal
|
|
|
|
|
|Normal
|
|
|
|
|
|Infinite=>{
|
|
|
|
|
if value<u32::MIN as f64*Self::TIME_SECONDS_PER_UNIT{
|
|
|
|
|
return Err(TimeFromFloatError::Underflow);
|
|
|
|
|
}
|
|
|
|
|
if u32::MAX as f64*Self::TIME_SECONDS_PER_UNIT<value{
|
|
|
|
|
return Err(TimeFromFloatError::Overflow);
|
|
|
|
|
}
|
|
|
|
|
Ok(Time((value*Self::TIME_UNITS_PER_SECOND) as u32))
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pub const fn get(self)->f64{
|
|
|
|
|
self.0 as f64*Self::TIME_SECONDS_PER_UNIT
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct Vector2{
|
|
|
|
|
pub x:f32,
|
|
|
|
|
pub y:f32,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct Vector3{
|
|
|
|
|
pub x:f32,
|
|
|
|
|
pub y:f32,
|
|
|
|
|
pub z:f32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// generic timed event
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct Timed<E>
|
|
|
|
|
where
|
|
|
|
|
E:for<'a>binrw::BinRead<Args<'a>=()>,
|
|
|
|
|
E:for<'a>binrw::BinWrite<Args<'a>=()>,
|
|
|
|
|
{
|
|
|
|
|
pub time:Time,
|
|
|
|
|
pub event:E,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<A,B> PartialEq<Timed<B>> for Timed<A>
|
|
|
|
|
where
|
|
|
|
|
A:for<'a>binrw::BinRead<Args<'a>=()>,
|
|
|
|
|
A:for<'a>binrw::BinWrite<Args<'a>=()>,
|
|
|
|
|
B:for<'a>binrw::BinRead<Args<'a>=()>,
|
|
|
|
|
B:for<'a>binrw::BinWrite<Args<'a>=()>,
|
|
|
|
|
{
|
|
|
|
|
fn eq(&self,other:&Timed<B>)->bool{
|
|
|
|
|
self.time.eq(&other.time)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl<A,B> PartialOrd<Timed<B>> for Timed<A>
|
|
|
|
|
where
|
|
|
|
|
A:for<'a>binrw::BinRead<Args<'a>=()>,
|
|
|
|
|
A:for<'a>binrw::BinWrite<Args<'a>=()>,
|
|
|
|
|
B:for<'a>binrw::BinRead<Args<'a>=()>,
|
|
|
|
|
B:for<'a>binrw::BinWrite<Args<'a>=()>,
|
|
|
|
|
{
|
|
|
|
|
fn partial_cmp(&self,other:&Timed<B>)->Option<core::cmp::Ordering>{
|
|
|
|
|
self.time.partial_cmp(&other.time)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// input
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct InputEvent{
|
|
|
|
|
#[br(try_map=GameControls::try_from_bits)]
|
|
|
|
|
#[bw(map=GameControls::bits)]
|
|
|
|
|
pub game_controls:GameControls,
|
|
|
|
|
pub mouse_pos:Vector2,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// output
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct OutputEvent{
|
|
|
|
|
#[br(try_map=TickInfo::try_from_bits)]
|
|
|
|
|
#[bw(map=TickInfo::bits)]
|
|
|
|
|
pub tick_info:TickInfo,
|
|
|
|
|
pub angles:Vector3,
|
|
|
|
|
pub position:Vector3,
|
|
|
|
|
pub velocity:Vector3,
|
|
|
|
|
pub acceleration:Vector3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// world
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct WorldEventReset{
|
|
|
|
|
pub position:Vector3,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct WorldEventButton{
|
|
|
|
|
pub button_id:u32,
|
|
|
|
|
// This field does not exist in the final struct and
|
|
|
|
|
// exists purely to de/serialize the magic number.
|
|
|
|
|
#[br(temp)]
|
|
|
|
|
#[bw(ignore)]
|
|
|
|
|
#[brw(magic=b"quatdata")]
|
|
|
|
|
_magic:(),
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct WorldEventSetTime{
|
|
|
|
|
pub time:Time,
|
|
|
|
|
#[br(temp)]
|
|
|
|
|
#[bw(ignore)]
|
|
|
|
|
#[brw(magic=b"quatdata")]
|
|
|
|
|
_magic:(),
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct WorldEventSetPaused{
|
|
|
|
|
#[br(map=|paused:u32|paused!=0)]
|
|
|
|
|
#[bw(map=|&paused:&bool|paused as u32)]
|
|
|
|
|
pub paused:bool,
|
|
|
|
|
#[br(temp)]
|
|
|
|
|
#[bw(ignore)]
|
|
|
|
|
#[brw(magic=b"quatdata")]
|
|
|
|
|
_magic:(),
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub enum WorldEvent{
|
|
|
|
|
#[brw(magic=0u32)]
|
|
|
|
|
Reset(WorldEventReset),
|
|
|
|
|
#[brw(magic=1u32)]
|
|
|
|
|
Button(WorldEventButton),
|
|
|
|
|
#[brw(magic=2u32)]
|
|
|
|
|
SetTime(WorldEventSetTime),
|
|
|
|
|
#[brw(magic=3u32)]
|
|
|
|
|
SetPaused(WorldEventSetPaused),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// gravity
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct GravityEvent{
|
|
|
|
|
pub gravity:Vector3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// run
|
|
|
|
|
/// Creates a new run when the player enters a start zone.
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct RunEventPrepare{
|
|
|
|
|
pub mode:ModeID,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct RunEventZone{
|
|
|
|
|
pub mode:ModeID,
|
|
|
|
|
#[br(temp)]
|
|
|
|
|
#[bw(ignore)]
|
|
|
|
|
#[brw(magic=b"data")]
|
|
|
|
|
_magic:(),
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct RunEventClear{
|
|
|
|
|
pub mode:ModeSpec,
|
|
|
|
|
#[br(temp)]
|
|
|
|
|
#[bw(ignore)]
|
|
|
|
|
#[brw(magic=b"data")]
|
|
|
|
|
_magic:(),
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct RunEventFlag{
|
|
|
|
|
pub mode:ModeSpec,
|
|
|
|
|
pub flag_reason:FlagReason,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct RunEventPractice{
|
|
|
|
|
pub mode:ModeSpec,
|
|
|
|
|
pub state_id:u32,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub enum RunEvent{
|
|
|
|
|
#[brw(magic=0u32)]
|
|
|
|
|
Prepare(RunEventPrepare),
|
|
|
|
|
#[brw(magic=1u32)]
|
|
|
|
|
Start(RunEventZone),
|
|
|
|
|
#[brw(magic=2u32)]
|
|
|
|
|
Finish(RunEventZone),
|
|
|
|
|
#[brw(magic=3u32)]
|
|
|
|
|
Clear(RunEventClear),
|
|
|
|
|
#[brw(magic=4u32)]
|
|
|
|
|
Flag(RunEventFlag),
|
|
|
|
|
#[brw(magic=5u32)]
|
|
|
|
|
LoadState(RunEventPractice),
|
|
|
|
|
#[brw(magic=6u32)]
|
|
|
|
|
SaveState(RunEventPractice),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// camera
|
|
|
|
|
/// Punches the camera when the player has an intense collision.
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct CameraEventCameraPunch{
|
|
|
|
|
pub rot_velocity:Vector3,
|
|
|
|
|
}
|
|
|
|
|
/// Rotates the camera when the player goes through a wormhole.
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct CameraEventTransform{
|
|
|
|
|
pub axis_angle:Vector3,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub enum CameraEvent{
|
|
|
|
|
#[brw(magic=0u32)]
|
|
|
|
|
CameraPunch(CameraEventCameraPunch),
|
|
|
|
|
#[brw(magic=1u32)]
|
|
|
|
|
Transform(CameraEventTransform),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setting
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct SettingEventFieldOfView{
|
|
|
|
|
pub fov:f64,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct SettingEventSensitivity{
|
|
|
|
|
pub sensitivity:f64,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct SettingEventTurnSpeed{
|
|
|
|
|
pub turn_speed:f64,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct SettingEventStyle{
|
|
|
|
|
pub style:Style,
|
|
|
|
|
#[br(temp)]
|
|
|
|
|
#[bw(ignore)]
|
|
|
|
|
#[brw(magic=b"data")]
|
|
|
|
|
_magic:(),
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub enum SettingEvent{
|
|
|
|
|
#[brw(magic=0u32)]
|
|
|
|
|
FieldOfView(SettingEventFieldOfView),
|
|
|
|
|
#[brw(magic=1u32)]
|
|
|
|
|
SensitivityX(SettingEventSensitivity),
|
|
|
|
|
#[brw(magic=2u32)]
|
|
|
|
|
SensitivityY(SettingEventSensitivity),
|
|
|
|
|
#[brw(magic=3u32)]
|
|
|
|
|
TurnSpeed(SettingEventTurnSpeed),
|
|
|
|
|
#[brw(magic=4u32)]
|
|
|
|
|
Style(SettingEventStyle),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A segment of event timelines.
|
|
|
|
|
/// Timelines are always be sorted.
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
pub struct Block{
|
|
|
|
|
pub input_events:Vec<Timed<InputEvent>>,
|
|
|
|
|
pub output_events:Vec<Timed<OutputEvent>>,
|
|
|
|
|
pub sound_events:Vec<Timed<SoundEvent>>,
|
|
|
|
|
pub world_events:Vec<Timed<WorldEvent>>,
|
|
|
|
|
pub gravity_events:Vec<Timed<GravityEvent>>,
|
|
|
|
|
pub run_events:Vec<Timed<RunEvent>>,
|
|
|
|
|
pub camera_events:Vec<Timed<CameraEvent>>,
|
|
|
|
|
pub setting_events:Vec<Timed<SettingEvent>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Clone,Copy)]
|
|
|
|
|
enum EventType{
|
|
|
|
|
#[brw(magic=1u32)]
|
|
|
|
|
Input,
|
|
|
|
|
#[brw(magic=2u32)]
|
|
|
|
|
Output,
|
|
|
|
|
#[brw(magic=3u32)]
|
|
|
|
|
Sound,
|
|
|
|
|
#[brw(magic=4u32)]
|
|
|
|
|
World,
|
|
|
|
|
#[brw(magic=5u32)]
|
|
|
|
|
Gravity,
|
|
|
|
|
#[brw(magic=6u32)]
|
|
|
|
|
Run,
|
|
|
|
|
#[brw(magic=7u32)]
|
|
|
|
|
Camera,
|
|
|
|
|
#[brw(magic=8u32)]
|
|
|
|
|
Setting,
|
|
|
|
|
}
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
struct BlockHeader{
|
|
|
|
|
num_events:[u16;8],
|
|
|
|
|
}
|
|
|
|
|
impl BlockHeader{
|
|
|
|
|
fn payload_size(&self)->usize{
|
|
|
|
|
self.num_events
|
|
|
|
|
.into_iter()
|
|
|
|
|
.zip(EVENT_SIZE)
|
|
|
|
|
.map(|(num_events,event_size)|
|
|
|
|
|
num_events as usize*event_size
|
|
|
|
|
)
|
|
|
|
|
.sum()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// binread args tech has been further refined
|
|
|
|
|
fn read_data_into_events<R,T,F>(
|
|
|
|
|
data:&mut R,
|
|
|
|
|
events:&mut Vec<T>,
|
|
|
|
|
num_events:usize,
|
|
|
|
|
reserve_fn:F,
|
|
|
|
|
)->Result<(),BinrwError>
|
|
|
|
|
where
|
|
|
|
|
R:BinReaderExt,
|
|
|
|
|
T:for<'a> binrw::BinRead<Args<'a>=()>,
|
|
|
|
|
F:Fn(&mut Vec<T>,usize),
|
|
|
|
|
{
|
|
|
|
|
reserve_fn(events,num_events);
|
|
|
|
|
for _ in 0..num_events{
|
|
|
|
|
events.push(data.read_le()?);
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Block{
|
|
|
|
|
pub fn from_reader<R:BinReaderExt>(data:R)->Result<Block,BinrwError>{
|
|
|
|
|
let mut block=Block::default();
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
/// 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<R:BinReaderExt>(&mut self,mut data:R)->Result<(),BinrwError>{
|
|
|
|
|
let block_header:BlockHeader=data.read_le()?;
|
|
|
|
|
let mut buffer=Vec::with_capacity(block_header.payload_size());
|
|
|
|
|
zstd::stream::copy_decode(data,&mut buffer)?;
|
|
|
|
|
let mut reader=std::io::Cursor::new(buffer);
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.input_events,block_header.num_events[EventType::Input as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.output_events,block_header.num_events[EventType::Output as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.sound_events,block_header.num_events[EventType::Sound as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.world_events,block_header.num_events[EventType::World as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.gravity_events,block_header.num_events[EventType::Gravity as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.run_events,block_header.num_events[EventType::Run as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.camera_events,block_header.num_events[EventType::Camera as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.setting_events,block_header.num_events[EventType::Setting as usize] as usize,Vec::reserve_exact)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
/// Read a complete data block and append the elements to the timelines in this block.
|
|
|
|
|
pub fn extend_from_reader<R:BinReaderExt>(&mut self,mut data:R)->Result<(),BinrwError>{
|
|
|
|
|
// sad code duplication
|
|
|
|
|
let block_header:BlockHeader=data.read_le()?;
|
|
|
|
|
let mut buffer=Vec::with_capacity(block_header.payload_size());
|
|
|
|
|
zstd::stream::copy_decode(data,&mut buffer)?;
|
|
|
|
|
let mut reader=std::io::Cursor::new(buffer);
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.input_events,block_header.num_events[EventType::Input as usize] as usize,Vec::reserve)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.output_events,block_header.num_events[EventType::Output as usize] as usize,Vec::reserve)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.sound_events,block_header.num_events[EventType::Sound as usize] as usize,Vec::reserve)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.world_events,block_header.num_events[EventType::World as usize] as usize,Vec::reserve)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.gravity_events,block_header.num_events[EventType::Gravity as usize] as usize,Vec::reserve)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.run_events,block_header.num_events[EventType::Run as usize] as usize,Vec::reserve)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.camera_events,block_header.num_events[EventType::Camera as usize] as usize,Vec::reserve)?;
|
|
|
|
|
read_data_into_events(&mut reader,&mut self.setting_events,block_header.num_events[EventType::Setting as usize] as usize,Vec::reserve)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
fn extend_from_block_id_iter<'a,R:BinReaderExt>(&mut self,mut data:R,block_timelines:&BlockTimelines,blocks:impl IntoIterator<Item=&'a Timed<BlockId>>)->Result<(),Error>{
|
|
|
|
|
for timed in blocks{
|
|
|
|
|
let take_seek=block_timelines
|
|
|
|
|
.block_info(timed.event)?
|
|
|
|
|
.take_seek(&mut data)
|
|
|
|
|
.map_err(Error::Seek)?;
|
|
|
|
|
self.extend_from_reader(take_seek).map_err(Error::InvalidData)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum Error{
|
|
|
|
|
InvalidBlockId(InvalidBlockId),
|
|
|
|
|
Seek(IoError),
|
|
|
|
|
InvalidData(BinrwError),
|
|
|
|
|
}
|
|
|
|
|
impl std::fmt::Display for Error{
|
|
|
|
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
|
|
|
|
write!(f,"{self:?}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl std::error::Error for Error{}
|
|
|
|
|
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone,Copy)]
|
|
|
|
|
pub struct BlockId(u32);
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
struct BlockPosition(u32);
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct InvalidBlockId(pub BlockId);
|
|
|
|
|
impl std::fmt::Display for InvalidBlockId{
|
|
|
|
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
|
|
|
|
write!(f,"{self:?}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl std::error::Error for InvalidBlockId{}
|
|
|
|
|
impl From<InvalidBlockId> for Error{
|
|
|
|
|
fn from(value:InvalidBlockId)->Self{
|
|
|
|
|
Self::InvalidBlockId(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The first 16 bytes of the file.
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[brw(magic=b"qbot")]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct FileHeader{
|
|
|
|
|
file_version:u32,
|
|
|
|
|
num_offline_blocks:u32,
|
|
|
|
|
num_realtime_blocks:u32,
|
|
|
|
|
}
|
|
|
|
|
impl FileHeader{
|
|
|
|
|
pub fn from_reader<R:BinReaderExt>(mut data:R)->Result<Self,BinrwError>{
|
|
|
|
|
data.read_le()
|
|
|
|
|
}
|
|
|
|
|
fn block_position_count(&self)->u32{
|
|
|
|
|
self.num_offline_blocks+self.num_realtime_blocks+1
|
|
|
|
|
}
|
|
|
|
|
/// Get BlockInfo for the BlockTimelines.
|
|
|
|
|
/// BlockTimelines is not really a "Block" per se, but BlockInfo is just a byte range.
|
|
|
|
|
pub fn block_timelines_info(&self)->BlockInfo{
|
|
|
|
|
const BLOCK_POSITION_SIZE:u32=size_of::<BlockPosition>() as u32;
|
|
|
|
|
const TIMED_BLOCKID_SIZE:u32=8+4;
|
|
|
|
|
let size=BLOCK_POSITION_SIZE*self.block_position_count()
|
|
|
|
|
+TIMED_BLOCKID_SIZE*self.num_offline_blocks
|
|
|
|
|
+TIMED_BLOCKID_SIZE*self.num_realtime_blocks;
|
|
|
|
|
let start=16;
|
|
|
|
|
let end=start+size;
|
|
|
|
|
BlockInfo(start..end)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// Information about "Blocks" of data. Appears immediately after FileHeader.
|
|
|
|
|
/// Contains all the information required to implement streaming download, decode, and playback.
|
|
|
|
|
#[binrw]
|
|
|
|
|
#[brw(little)]
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
#[br(import_raw(header:&FileHeader))]
|
|
|
|
|
pub struct BlockTimelines{
|
|
|
|
|
#[br(count=header.block_position_count())]
|
|
|
|
|
block_positions:Vec<BlockPosition>,
|
|
|
|
|
#[br(count=header.num_offline_blocks)]
|
|
|
|
|
offline_blocks_timeline:Vec<Timed<BlockId>>,
|
|
|
|
|
#[br(count=header.num_realtime_blocks)]
|
|
|
|
|
realtime_blocks_timeline:Vec<Timed<BlockId>>,
|
|
|
|
|
}
|
|
|
|
|
impl BlockTimelines{
|
|
|
|
|
pub fn from_reader<R:BinReaderExt>(header:&FileHeader,mut data:R)->Result<Self,BinrwError>{
|
|
|
|
|
data.read_le_args(header)
|
|
|
|
|
}
|
|
|
|
|
/// "Offline" blocks (containing World, Gravity, Run, Camera, and Setting events) in chronological order.
|
|
|
|
|
pub fn offline_blocks(&self)->&[Timed<BlockId>]{
|
|
|
|
|
&self.offline_blocks_timeline
|
|
|
|
|
}
|
|
|
|
|
/// "Realtime" blocks (containing Input, Output, and Sound events) in chronological order.
|
|
|
|
|
pub fn realtime_blocks(&self)->&[Timed<BlockId>]{
|
|
|
|
|
&self.realtime_blocks_timeline
|
|
|
|
|
}
|
|
|
|
|
/// Get BlockInfo for a specfic BlockId.
|
|
|
|
|
pub fn block_info(&self,block_id:BlockId)->Result<BlockInfo,InvalidBlockId>{
|
|
|
|
|
let BlockId(id)=block_id;
|
|
|
|
|
if self.block_positions.len() as u32<=id{
|
|
|
|
|
return Err(InvalidBlockId(block_id));
|
|
|
|
|
}
|
|
|
|
|
let BlockPosition(start)=self.block_positions[id as usize];
|
|
|
|
|
let BlockPosition(end)=self.block_positions[id as usize+1];
|
|
|
|
|
Ok(BlockInfo(start..end))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// The range of data for a specific Block, relative to the start of the file.
|
|
|
|
|
#[derive(Debug,Clone)]
|
|
|
|
|
pub struct BlockInfo(core::ops::Range<u32>);
|
|
|
|
|
impl BlockInfo{
|
|
|
|
|
/// Create an adapter which seeks to the block start and reads at most the block length.
|
|
|
|
|
pub fn take_seek<R:BinReaderExt>(&self,mut data:R)->Result<TakeSeek<R>,IoError>{
|
|
|
|
|
data.seek(SeekFrom::Start(self.start as u64))?;
|
|
|
|
|
Ok(data.take_seek(self.len() as u64))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl core::ops::Deref for BlockInfo{
|
|
|
|
|
type Target=core::ops::Range<u32>;
|
|
|
|
|
fn deref(&self)->&Self::Target{
|
|
|
|
|
&self.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read offline blocks 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.
|
|
|
|
|
pub fn read_offline_to_block<R:BinReaderExt>(mut data:R)->Result<Block,Error>{
|
|
|
|
|
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();
|
|
|
|
|
block.extend_from_block_id_iter(data,&block_timelines,block_timelines.offline_blocks())?;
|
|
|
|
|
Ok(block)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read realtime blocks 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.
|
|
|
|
|
pub fn read_realtime_to_block<R:BinReaderExt>(mut data:R)->Result<Block,Error>{
|
|
|
|
|
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();
|
|
|
|
|
block.extend_from_block_id_iter(data,&block_timelines,block_timelines.realtime_blocks())?;
|
|
|
|
|
Ok(block)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
|
pub fn read_all_to_block<R:BinReaderExt>(mut data:R)->Result<Block,Error>{
|
|
|
|
|
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();
|
|
|
|
|
block.extend_from_block_id_iter(&mut data,&block_timelines,block_timelines.offline_blocks())?;
|
|
|
|
|
block.extend_from_block_id_iter(&mut data,&block_timelines,block_timelines.realtime_blocks())?;
|
|
|
|
|
Ok(block)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature="itertools")]
|
|
|
|
|
pub fn serialize<W:binrw::BinWriterExt>(block:&Block,writer:&mut W)->Result<(),BinrwError>{
|
|
|
|
|
use std::ops::Range;
|
|
|
|
|
const MAX_BLOCK_SIZE:usize=1<<18;
|
|
|
|
|
const FILE_VERSION:u32=1;
|
|
|
|
|
const COMPRESSION_LEVEL:i32=19;
|
|
|
|
|
const EVENT_TYPES:[EventType;8]=[
|
|
|
|
|
EventType::Input,
|
|
|
|
|
EventType::Output,
|
|
|
|
|
EventType::Sound,
|
|
|
|
|
EventType::World,
|
|
|
|
|
EventType::Gravity,
|
|
|
|
|
EventType::Run,
|
|
|
|
|
EventType::Camera,
|
|
|
|
|
EventType::Setting,
|
|
|
|
|
];
|
|
|
|
|
#[derive(Clone,Default)]
|
|
|
|
|
struct Plan<T>([T;8]);
|
|
|
|
|
// A plan of how many events of each type to include in a data block.
|
|
|
|
|
impl Plan<usize>{
|
|
|
|
|
/// Predict the size increment from adding a new event.
|
|
|
|
|
fn size_increase(&self,event_type:EventType)->usize{
|
|
|
|
|
EVENT_SIZE[event_type as usize]
|
|
|
|
|
}
|
|
|
|
|
/// Add the new event.
|
|
|
|
|
fn accumulate(&mut self,event_type:EventType){
|
|
|
|
|
self.0[event_type as usize]+=1;
|
|
|
|
|
}
|
|
|
|
|
fn range(&self,end:&Plan<usize>)->Plan<Range<usize>>{
|
|
|
|
|
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<Range<usize>>{
|
|
|
|
|
/// Calculate the predicted size of the planned block.
|
|
|
|
|
fn size(&self)->usize{
|
|
|
|
|
self.0.iter()
|
|
|
|
|
.zip(EVENT_SIZE)
|
|
|
|
|
.map(|(range,event_size)|event_size*range.len())
|
|
|
|
|
.sum::<usize>()
|
|
|
|
|
+size_of::<BlockHeader>()
|
|
|
|
|
}
|
|
|
|
|
fn header(&self)->BlockHeader{
|
|
|
|
|
BlockHeader{
|
|
|
|
|
num_events:self.0.map(|events|events.len() as u16),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// compare an event at the head of the plan to the best event collected so far.
|
|
|
|
|
fn collect_event<E>(
|
|
|
|
|
best:&mut Option<(Time,EventType)>,
|
|
|
|
|
list:&[Timed<E>],
|
|
|
|
|
plan:&Plan<usize>,
|
|
|
|
|
event_type:EventType,
|
|
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
E:for<'a>binrw::BinRead<Args<'a>=()>,
|
|
|
|
|
E:for<'a>binrw::BinWrite<Args<'a>=()>,
|
|
|
|
|
{
|
|
|
|
|
if let Some(event)=list.get(plan.0[event_type as usize])
|
|
|
|
|
&&best.is_none_or(|(time,_)|event.time<time)
|
|
|
|
|
{
|
|
|
|
|
*best=Some((event.time,event_type));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// plan a single block: collect events until the block is full
|
|
|
|
|
fn plan_block(plan:&mut Plan<usize>,next_event:impl Fn(&Plan<usize>)->Option<(Time,EventType)>)->Option<Time>{
|
|
|
|
|
let mut size=0;
|
|
|
|
|
let (start_time,first_event)=next_event(plan)?;
|
|
|
|
|
|
|
|
|
|
size+=plan.size_increase(first_event);
|
|
|
|
|
if MAX_BLOCK_SIZE<size{
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
plan.accumulate(first_event);
|
|
|
|
|
|
|
|
|
|
while let Some((_,event_type))=next_event(plan){
|
|
|
|
|
size+=plan.size_increase(event_type);
|
|
|
|
|
if MAX_BLOCK_SIZE<size{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
plan.accumulate(event_type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(start_time)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct PlannedBlock{
|
|
|
|
|
// index is not the same as BlockId.
|
|
|
|
|
// It is list-local for both plan_offline and plan_realtime.
|
|
|
|
|
index:usize,
|
|
|
|
|
time:Time,
|
|
|
|
|
plan:Plan<Range<usize>>,
|
|
|
|
|
}
|
|
|
|
|
fn plan_timeline<F>(next_event:F)->std::collections::VecDeque<PlannedBlock>
|
|
|
|
|
where
|
|
|
|
|
F:Copy,
|
|
|
|
|
F:Fn(&Plan<usize>)->Option<(Time,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,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn create_block(block:&Block,plan:Plan<Range<usize>>)->Result<Vec<u8>,BinrwError>{
|
|
|
|
|
let allocation_size=plan.size();
|
|
|
|
|
let mut buffer=Vec::with_capacity(allocation_size);
|
|
|
|
|
let mut cursor=std::io::Cursor::new(&mut buffer);
|
|
|
|
|
|
|
|
|
|
let block_header=plan.header();
|
|
|
|
|
for (range,event_type) in plan.0.into_iter().zip(EVENT_TYPES){
|
|
|
|
|
let num_events=range.len();
|
|
|
|
|
if num_events==0{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
match event_type{
|
|
|
|
|
EventType::Input=>block.input_events[range].write_le(&mut cursor)?,
|
|
|
|
|
EventType::Output=>block.output_events[range].write_le(&mut cursor)?,
|
|
|
|
|
EventType::Sound=>block.sound_events[range].write_le(&mut cursor)?,
|
|
|
|
|
EventType::World=>block.world_events[range].write_le(&mut cursor)?,
|
|
|
|
|
EventType::Gravity=>block.gravity_events[range].write_le(&mut cursor)?,
|
|
|
|
|
EventType::Run=>block.run_events[range].write_le(&mut cursor)?,
|
|
|
|
|
EventType::Camera=>block.camera_events[range].write_le(&mut cursor)?,
|
|
|
|
|
EventType::Setting=>block.setting_events[range].write_le(&mut cursor)?,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allocate way too much space, whatever.
|
|
|
|
|
let mut output=Vec::with_capacity(allocation_size);
|
|
|
|
|
// Block includes header uncompressed, since the header unambiguously
|
|
|
|
|
// determines the size of the output data and that may be useful.
|
|
|
|
|
block_header.write_le(&mut std::io::Cursor::new(&mut output))?;
|
|
|
|
|
zstd::stream::copy_encode(buffer.as_slice(),&mut output,COMPRESSION_LEVEL)?;
|
|
|
|
|
|
|
|
|
|
Ok(output)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut blocks=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<BlockId>=Timed{time:Time(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<Timed<BlockId>>,planned:PlannedBlock|{
|
|
|
|
|
block_positions.push(BlockPosition(position));
|
|
|
|
|
let block=create_block(block,planned.plan).unwrap();
|
|
|
|
|
position+=block.len() as u32;
|
|
|
|
|
blocks.push(block);
|
|
|
|
|
|
|
|
|
|
// write the block id to the correct index
|
|
|
|
|
timeline[planned.index]=Timed{
|
|
|
|
|
time:planned.time,
|
|
|
|
|
event:BlockId(block_id),
|
|
|
|
|
};
|
|
|
|
|
block_id+=1;
|
|
|
|
|
};
|
|
|
|
|
// 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 block in blocks{
|
|
|
|
|
writer.write_all(&block)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|