63 Commits
stream ... peq

Author SHA1 Message Date
f22b0cdb27 expect fail 2025-12-14 19:20:08 -08:00
6b395b970c peq test 2025-12-14 18:57:48 -08:00
0ea00baeb5 v0.7.1-pre1 2025-12-14 18:56:06 -08:00
6b4e56ca82 PartialEq test 2025-12-14 18:55:16 -08:00
d153e8284b clippy fixes 2025-12-14 13:03:01 -08:00
f54af6600b move import to relevant section 2025-12-14 13:01:26 -08:00
fb4fb5b5b3 unplumb block and use closure capture 2025-12-14 12:59:07 -08:00
5dafa3971c generic timeline fn 2025-12-14 12:56:03 -08:00
a1cd627836 use mask 2025-12-14 12:45:42 -08:00
39049fa12e fix block size prediction 2025-12-14 12:34:01 -08:00
236e297cc1 use zip for funsies 2025-12-14 12:19:12 -08:00
838aecbda5 no test exact data 2025-12-14 12:13:27 -08:00
a5d5b30e0c fix chunk header size prediction 2025-12-14 12:06:56 -08:00
99fb761a1d fix lints without itertools 2025-12-13 13:16:59 -08:00
59fcc7c0fb fix tests without itertools 2025-12-13 13:13:55 -08:00
775c510ee6 tab group 2025-12-13 10:58:56 -08:00
22c01e5910 move and tweak comment 2025-12-13 10:58:56 -08:00
8c3e3c9463 skip empty event chunks 2025-12-13 10:58:56 -08:00
f315069f96 comments 2025-12-13 10:58:56 -08:00
881a3bad11 remove macro 2025-12-13 10:58:56 -08:00
bd8207ac2d simplify timeline construction 2025-12-13 10:58:56 -08:00
60ed1e2661 scary shadowing 2025-12-13 10:58:56 -08:00
680d49f6db fix timelines 2025-12-13 10:58:56 -08:00
504c3ff354 fix BlockId & BlockPosition write 2025-12-13 10:58:56 -08:00
89c5f94a28 change variable name 2025-12-13 10:58:56 -08:00
673255383d fix float serialization 2025-12-13 10:58:56 -08:00
677cb86987 fix VecDeque mistake 2025-12-13 10:58:56 -08:00
59b36c8821 fix event range serialization 2025-12-13 10:58:56 -08:00
0dc231d972 fix macro 2025-12-13 10:58:56 -08:00
4de222b1b6 test serializer 2025-12-13 10:58:55 -08:00
40e8f7c595 rename variable 2025-12-13 10:58:55 -08:00
6038241d7a v0 serializer 2025-12-13 10:58:55 -08:00
4ef063a474 merge impl blocks 2025-12-13 10:58:53 -08:00
03b5b8c239 v0.7.0 fix RunClear 2025-12-13 08:39:43 -08:00
488ce55990 fix RunClear 2025-12-13 08:37:56 -08:00
fe98210d10 v0.6.0 fix RunEvent 2025-12-13 08:05:25 -08:00
2eb282dfe2 update deps 2025-12-13 08:05:02 -08:00
c8ec986a2b Fix RunEvent (#2)
It was really wrong!

Reviewed-on: #2
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2025-12-13 15:57:26 +00:00
a6ce331682 un-nest example code 2025-12-09 16:05:09 -08:00
6a6849ec79 update readme 2025-11-06 13:09:14 -08:00
8b52f0765f tweak test to remove cfg macro 2025-11-06 10:39:06 -08:00
8030308310 update readme 2025-11-06 10:20:35 -08:00
67bb41fa5e FileHeader::block_timelines_info 2025-11-06 10:20:35 -08:00
7659e941b9 read_offline_to_block + read_realtime_to_block 2025-11-06 10:08:27 -08:00
fd9ddd9ca8 v0.5.4 asymmetric Timed Ord 2025-11-06 16:37:17 +01:00
7f86d0edaf allow Ord comparison between different inner Timed types 2025-11-06 16:36:25 +01:00
6021177364 reduce verbosity in readme (feature is enabled by default) 2025-11-05 16:10:14 -08:00
eb26ba0593 move FileHeader magic 2025-11-05 15:01:48 -08:00
d4e94ab0da binrw tech 2025-11-05 10:56:41 -08:00
cdbb5e6b01 v0.5.3 tweak inner types + add more documentation 2025-11-05 10:56:41 -08:00
240a8798cd document more 2025-11-05 10:56:40 -08:00
7e0cb86a32 BlockInfo extends Range 2025-11-05 10:36:54 -08:00
9fdaf34df3 BlockPosition struct never used in a public interface 2025-11-05 10:24:31 -08:00
cc67966743 unused derives 2025-11-05 10:24:31 -08:00
9af3f4e6cf v0.5.2 derive more traits + relax Timed Ord 2025-11-05 09:56:24 -08:00
95b8fb2d95 add #[derive(Debug,Clone)] to everything 2025-11-05 09:54:08 -08:00
503710aa95 impl PartialEq & PartialOrd for all Timed events 2025-11-05 09:49:46 -08:00
fd59928644 v0.5.1 update deps 2025-11-05 09:29:54 -08:00
873ea5e9da update deps 2025-11-05 09:29:19 -08:00
f3768af543 v0.5.0 refactor 2025-11-05 09:26:43 -08:00
91d1ba68d4 refactor 2025-11-05 09:04:10 -08:00
3a13ae1642 v0.4.0 support multiple bonuses 2025-11-03 18:59:42 +01:00
f21b5f8c40 support multiple bonuses 2025-11-03 18:57:14 +01:00
5 changed files with 541 additions and 106 deletions

38
Cargo.lock generated
View File

@@ -10,9 +10,9 @@ checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
[[package]]
name = "binrw"
version = "0.14.1"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b"
checksum = "81419ff39e6ed10a92a7f125290859776ced35d9a08a665ae40b23e7ca702f30"
dependencies = [
"array-init",
"binrw_derive",
@@ -21,9 +21,9 @@ dependencies = [
[[package]]
name = "binrw_derive"
version = "0.14.1"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c"
checksum = "376404e55ec40d0d6f8b4b7df3f87b87954bd987f0cf9a7207ea3b6ea5c9add4"
dependencies = [
"either",
"owo-colors",
@@ -34,15 +34,15 @@ dependencies = [
[[package]]
name = "bitflags"
version = "2.9.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bytemuck"
version = "1.22.0"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "either"
@@ -61,31 +61,31 @@ dependencies = [
[[package]]
name = "owo-colors"
version = "3.5.0"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strafesnet_roblox_bot_file"
version = "0.3.1"
version = "0.7.1-pre1"
dependencies = [
"binrw",
"bitflags",
@@ -94,9 +94,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.109"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
@@ -105,6 +105,6 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.18"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"

View File

@@ -1,10 +1,10 @@
[package]
name = "strafesnet_roblox_bot_file"
version = "0.3.1"
edition = "2021"
version = "0.7.1-pre1"
edition = "2024"
[dependencies]
binrw = "0.14.1"
binrw = "0.15.0"
bitflags = "2.6.0"
itertools = { version = "0.14.0", optional = true }

View File

@@ -3,7 +3,7 @@ Roblox Bhop/Surf Bot File Format
## Example
Read the whole file with the itertools feature enabled:
Read the whole file and print each position:
```rust
use strafesnet_roblox_bot_file::v0::read_all_to_block;
@@ -11,6 +11,10 @@ let file=std::fs::read("bot_file")?;
let mut input=std::io::Cursor::new(file);
let block=read_all_to_block(&mut input)?;
for output_event in &block.output_events{
println!("{:?}",output_event.event.position);
}
```
Or decode individual blocks using block location info:
```rust
@@ -19,21 +23,25 @@ use strafesnet_roblox_bot_file::v0::{Block,BlockTimelines,FileHeader};
let file=std::fs::read("bot_file")?;
let mut input=std::io::Cursor::new(file);
// FileHeader is the first 16 bytes of the file.
let header=FileHeader::from_reader(&mut input)?;
// BlockTimelines is an index of the blocks within the file.
let timelines=BlockTimelines::from_reader(&header,&mut input)?;
// offline blocks include the following event types:
// World, Gravity, Run, Camera, Setting
for timed in timelines.offline_blocks(){
let block_info=timelines.block_info(timed.event)?;
let block=Block::from_reader(block_info.take_seek(&mut input)?)?;
let block_reader=block_info.take_seek(&mut input)?;
let block=Block::from_reader(block_reader)?;
}
// realtime blocks include the following event types:
// Input, Output, Sound
for timed in timelines.realtime_blocks(){
let block_info=timelines.block_info(timed.event)?;
let block=Block::from_reader(block_info.take_seek(&mut input)?)?;
let block_reader=block_info.take_seek(&mut input)?;
let block=Block::from_reader(block_reader)?;
}
```

View File

@@ -1,7 +1,7 @@
use crate::v0::{Block,BlockTimelines,FileHeader,Timed};
#[test]
fn _1(){
fn deserialize_manual(){
let file=std::fs::read("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap();
let mut input=std::io::Cursor::new(file);
let header=FileHeader::from_reader(&mut input).unwrap();
@@ -23,20 +23,38 @@ fn _1(){
}
}
#[cfg(feature="itertools")]
use crate::v0::{read_all_to_block,Error};
#[test]
#[cfg(feature="itertools")]
fn _2()->Result<(),Error>{
fn deserialize_all()->Result<(),crate::v0::Error>{
let file=std::fs::read("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap();
let t0=std::time::Instant::now();
let _block=read_all_to_block(std::io::Cursor::new(file))?;
let _block=crate::v0::read_all_to_block(std::io::Cursor::new(file))?;
println!("{:?}",t0.elapsed());
Ok(())
}
// TODO: file serialization test
#[test]
#[cfg(feature="itertools")]
fn serialize_round_trip()->Result<(),binrw::Error>{
use crate::v0::serialize;
let file=std::fs::read("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap();
let mut block=crate::v0::read_all_to_block(std::io::Cursor::new(file.as_slice())).unwrap();
let mut data=Vec::with_capacity(file.len());
serialize(&block,&mut std::io::Cursor::new(&mut data))?;
let block_rt=crate::v0::read_all_to_block(std::io::Cursor::new(data.as_slice())).unwrap();
assert_eq!(block_rt,block);
block.output_events.pop();
assert_eq!(block_rt,block);
Ok(())
}

559
src/v0.rs
View File

@@ -10,29 +10,50 @@ 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,PartialEq)]
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]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
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,
}
@@ -75,20 +96,45 @@ impl GameControls{
// generic timed event
#[binrw]
#[brw(little)]
#[derive(Debug)]
#[derive(Debug,Clone)]
pub struct Timed<E>
where
E:for<'a>binrw::BinRead<Args<'a>=()>,
E:for<'a>binrw::BinWrite<Args<'a>=()>,
{
#[br(map=read_trey_double)]
#[bw(map=write_trey_double)]
pub time:f64,
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,PartialEq)]
pub struct InputEvent{
#[br(try_map=GameControls::try_from_bits)]
#[bw(map=GameControls::bits)]
@@ -98,6 +144,7 @@ pub struct InputEvent{
// output
bitflags::bitflags!{
#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq)]
pub struct TickInfo:u32{
const TickEnd=1<<0;
const Jump=1<<1;
@@ -120,6 +167,7 @@ impl TickInfo{
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct OutputEvent{
#[br(try_map=TickInfo::try_from_bits)]
#[bw(map=TickInfo::bits)]
@@ -154,6 +202,7 @@ pub enum SoundType{
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct SoundEvent{
pub sound_type:SoundType,
/// Roblox enum
@@ -163,11 +212,13 @@ pub struct SoundEvent{
// world
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct WorldEventReset{
pub position:Vector3,
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct WorldEventButton{
pub button_id:u32,
// This field does not exist in the final struct and
@@ -179,8 +230,10 @@ pub struct WorldEventButton{
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct WorldEventSetTime{
#[br(map=read_trey_double)]
#[bw(map=write_trey_double)]
pub time:f64,
#[br(temp)]
#[bw(ignore)]
@@ -189,6 +242,7 @@ pub struct WorldEventSetTime{
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct WorldEventSetPaused{
#[br(map=|paused:u32|paused!=0)]
#[bw(map=|&paused:&bool|paused as u32)]
@@ -200,6 +254,7 @@ pub struct WorldEventSetPaused{
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub enum WorldEvent{
#[brw(magic=0u32)]
Reset(WorldEventReset),
@@ -214,6 +269,7 @@ pub enum WorldEvent{
// gravity
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct GravityEvent{
pub gravity:Vector3,
}
@@ -222,30 +278,12 @@ pub struct GravityEvent{
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq)]
pub enum RunEventType{
#[brw(magic=0u32)]
Prepare,
#[brw(magic=1u32)]
Start,
#[brw(magic=2u32)]
Finish,
#[brw(magic=3u32)]
Clear,
#[brw(magic=4u32)]
Flag,
#[brw(magic=5u32)]
LoadState,
#[brw(magic=6u32)]
SaveState,
}
pub struct ModeID(pub u32);
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq)]
pub enum Mode{
#[brw(magic=0i32)]
Main,
#[brw(magic=1i32)]
Bonus,
pub enum ModeSpec{
Exactly(ModeID),
#[brw(magic=-1i32)]
All,
#[brw(magic=-2i32)]
@@ -256,6 +294,39 @@ pub enum Mode{
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq)]
pub enum Style{
#[brw(magic=1u32)]
Autohop,
#[brw(magic=2u32)]
Scroll,
#[brw(magic=3u32)]
Sideways,
#[brw(magic=4u32)]
HalfSideways,
#[brw(magic=5u32)]
WOnly,
#[brw(magic=6u32)]
AOnly,
#[brw(magic=7u32)]
Backwards,
#[brw(magic=8u32)]
Faste,
#[brw(magic=14u32)]
LowGravity,
#[brw(magic=501u32)]
Fly,
#[brw(magic=502u32)]
FlySustain,
#[brw(magic=503u32)]
Rocket,
#[brw(magic=504u32)]
Style3DStrafe,
#[brw(magic=505u32)]
RocketStrafe,
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq)]
pub enum FlagReason{
#[brw(magic=0u32)]
Anticheat,
@@ -277,16 +348,67 @@ pub enum FlagReason{
Teleport,
#[brw(magic=9u32)]
Practice,
#[brw(magic=b"data")]
None,
}
#[binrw]
#[brw(little)]
pub struct RunEvent{
pub run_event_type:RunEventType,
pub mode:Mode,
#[derive(Debug,Clone,PartialEq)]
pub struct RunEventPrepare{
pub mode:ModeID,
pub style:Style,
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct RunEventZone{
pub mode:ModeID,
#[br(temp)]
#[bw(ignore)]
#[brw(magic=b"data")]
_magic:(),
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct RunEventClear{
pub mode:ModeSpec,
#[br(temp)]
#[bw(ignore)]
#[brw(magic=b"data")]
_magic:(),
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct RunEventFlag{
pub mode:ModeSpec,
pub flag_reason:FlagReason,
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct RunEventPractice{
pub mode:ModeSpec,
pub state_id:u32,
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
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
#[binrw]
@@ -300,6 +422,7 @@ pub enum CameraEventType{
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct CameraEvent{
pub camera_event_type:CameraEventType,
pub value:Vector3,
@@ -323,13 +446,16 @@ pub enum SettingType{
}
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,PartialEq)]
pub struct SettingEvent{
pub setting_type:SettingType,
#[br(map=read_trey_double)]
#[bw(map=write_trey_double)]
pub value:f64,
}
#[derive(Default)]
/// A segment of event timelines.
#[derive(Debug,Default,PartialEq)]
pub struct Block{
pub input_events:Vec<Timed<InputEvent>>,
pub output_events:Vec<Timed<OutputEvent>>,
@@ -343,7 +469,7 @@ pub struct Block{
#[binrw]
#[brw(little)]
#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq)]
#[derive(Clone,Copy)]
enum EventType{
#[brw(magic=1u32)]
Input,
@@ -362,6 +488,22 @@ enum EventType{
#[brw(magic=8u32)]
Setting,
}
impl EventType{
// internal function meant for array indexing
fn from_usize(value:usize)->Self{
match value{
0=>Self::Input,
1=>Self::Output,
2=>Self::Sound,
3=>Self::World,
4=>Self::Gravity,
5=>Self::Run,
6=>Self::Camera,
7=>Self::Setting,
_=>panic!(),
}
}
}
#[binrw]
#[brw(little)]
struct EventChunkHeader{
@@ -449,28 +591,26 @@ 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,Copy)]
pub struct BlockPosition(#[br(map=|i:u32|i-1)]u32);
impl PartialEq for Timed<BlockId>{
fn eq(&self,other:&Self)->bool{
self.time.eq(&other.time)
}
}
impl PartialOrd for Timed<BlockId>{
fn partial_cmp(&self,other:&Self)->Option<core::cmp::Ordering>{
self.time.partial_cmp(&other.time)
}
}
#[derive(Debug,Clone)]
struct BlockPosition(
#[br(map=|i:u32|i-1)]
#[bw(map=|&i:&u32|i+1)]
u32
);
/// The first 16 bytes of the file.
#[binrw]
#[brw(little)]
#[derive(Debug)]
#[brw(magic=b"qbot")]
#[derive(Debug,Clone)]
pub struct FileHeader{
#[brw(magic=b"qbot")]
file_version:u32,
num_offline_blocks:u32,
num_realtime_blocks:u32,
@@ -479,66 +619,78 @@ impl FileHeader{
pub fn from_reader<R:BinReaderExt>(mut data:R)->binrw::BinResult<Self>{
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)]
#[br(import(num_offline_blocks:u32,num_realtime_blocks:u32))]
#[derive(Debug,Clone)]
#[br(import_raw(header:&FileHeader))]
pub struct BlockTimelines{
#[br(count=num_offline_blocks+num_realtime_blocks+1)]
#[br(count=header.block_position_count())]
block_positions:Vec<BlockPosition>,
#[br(count=num_offline_blocks)]
#[br(count=header.num_offline_blocks)]
offline_blocks_timeline:Vec<Timed<BlockId>>,
#[br(count=num_realtime_blocks)]
#[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)->binrw::BinResult<Self>{
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,BlockId(block_id):BlockId)->Result<BlockInfo,Error>{
if self.block_positions.len() as u32<=block_id{
return Err(Error::InvalidBlockId(BlockId(block_id)));
}
let BlockPosition(start)=self.block_positions[block_id as usize];
let BlockPosition(end)=self.block_positions[block_id as usize+1];
Ok(BlockInfo{start,length:end-start})
Ok(BlockInfo(start..end))
}
}
impl BlockTimelines{
pub fn from_reader<R:BinReaderExt>(header:&FileHeader,mut data:R)->binrw::BinResult<Self>{
data.read_le_args((header.num_offline_blocks,header.num_realtime_blocks))
}
}
pub struct BlockInfo{
start:u32,
length:u32,
}
/// 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{
pub fn start(&self)->u32{
self.start
}
pub fn length(&self)->u32{
self.length
}
/// 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.length() as u64))
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 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.
#[cfg(feature="itertools")]
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)?;
fn read_to_block<'a,R:BinReaderExt>(mut data:R,block_timelines:&BlockTimelines,blocks:impl IntoIterator<Item=&'a Timed<BlockId>>)->Result<Block,Error>{
let mut block=Block::default();
for timed in itertools::merge(block_timelines.offline_blocks(),block_timelines.realtime_blocks()){
for timed in blocks{
let take_seek=block_timelines
.block_info(timed.event)?
.take_seek(&mut data)
@@ -547,3 +699,260 @@ pub fn read_all_to_block<R:BinReaderExt>(mut data:R)->Result<Block,Error>{
}
Ok(block)
}
/// 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 block=read_to_block(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 block=read_to_block(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.
#[cfg(feature="itertools")]
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 block=read_to_block(data,&block_timelines,itertools::merge(block_timelines.offline_blocks(),block_timelines.realtime_blocks()))?;
Ok(block)
}
#[cfg(feature="itertools")]
pub fn serialize<W:binrw::BinWriterExt>(block:&Block,writer:&mut W)->Result<(),binrw::Error>{
use std::ops::Range;
const MAX_BLOCK_SIZE:usize=1<<14;
const FILE_VERSION:u32=0;
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>([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{
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::<EventChunkHeader>())
}
/// 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)
.filter_map(|(range,event_size)|match range.len(){
0=>None,
other=>Some(other*event_size+size_of::<EventChunkHeader>()),
})
.sum()
}
}
// compare an event at the head of the plan to the best event collected so far.
fn collect_event<E>(
best:&mut Option<(f64,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<(f64,EventType)>)->Option<f64>{
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:f64,
plan:Plan<Range<usize>>,
}
fn plan_timeline<F>(next_event:F)->std::collections::VecDeque<PlannedBlock>
where
F:Copy,
F:Fn(&Plan<usize>)->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<BlockId>=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<Timed<BlockId>>,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 (event_type_id,range) in plan.0.into_iter().enumerate(){
let num_events=range.len();
if num_events==0{
continue;
}
let event_type=EventType::from_usize(event_type_id);
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(())
}