13 Commits

Author SHA1 Message Date
8162fa90bc simplify BlockHeader 2026-02-13 15:57:42 -08:00
607929a471 reuse types from v0 2026-02-13 10:51:55 -08:00
167d89a2f0 zstd decode 2026-02-13 10:10:12 -08:00
b1ed436fe0 zstd encode 2026-02-13 10:10:12 -08:00
46b5568b4f zstd dep 2026-02-13 10:10:12 -08:00
04f5b4f881 v1 2026-02-13 10:10:12 -08:00
080dba4c75 16x bigger blocks 2026-02-13 10:10:12 -08:00
6aea99c06c zero-indexed Ids 2026-02-13 10:10:12 -08:00
616b32a958 fixed block header and event order 2026-02-13 10:10:12 -08:00
cf7184cd3d style setting 2026-02-13 10:10:12 -08:00
e29124edfd u32 Time 2026-02-13 10:09:48 -08:00
c86645542f remove trey float 2026-02-13 10:09:48 -08:00
465cfbf052 copy v0 2026-02-13 10:08:38 -08:00
5 changed files with 963 additions and 2 deletions

114
Cargo.lock generated
View File

@@ -44,12 +44,48 @@ version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "cc"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -59,12 +95,34 @@ dependencies = [
"either",
]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "owo-colors"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro2"
version = "1.0.103"
@@ -83,6 +141,18 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strafesnet_roblox_bot_file"
version = "0.8.1"
@@ -90,6 +160,7 @@ dependencies = [
"binrw",
"bitflags",
"itertools",
"zstd",
]
[[package]]
@@ -108,3 +179,46 @@ name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
]

View File

@@ -7,6 +7,7 @@ edition = "2024"
binrw = "0.15.0"
bitflags = "2.6.0"
itertools = { version = "0.14.0", optional = true }
zstd = "0.13.3"
[features]
default = ["itertools"]

View File

@@ -1,6 +1,7 @@
pub use binrw::Error as BinrwError;
pub mod v0;
pub mod v1;
#[cfg(test)]
mod tests;

View File

@@ -89,7 +89,7 @@ impl std::fmt::Display for GameControlsError{
}
impl std::error::Error for GameControlsError{}
impl GameControls{
fn try_from_bits(bits:u32)->Result<Self,GameControlsError>{
pub fn try_from_bits(bits:u32)->Result<Self,GameControlsError>{
Self::from_bits(bits).ok_or(GameControlsError)
}
}
@@ -162,7 +162,7 @@ impl std::fmt::Display for TickInfoError{
}
impl std::error::Error for TickInfoError{}
impl TickInfo{
fn try_from_bits(bits:u32)->Result<Self,TickInfoError>{
pub fn try_from_bits(bits:u32)->Result<Self,TickInfoError>{
Self::from_bits(bits).ok_or(TickInfoError)
}
}

845
src/v1.rs Normal file
View File

@@ -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(())
}