Implement Serializer (#3)

Add an algorithm to generate a bot file from a set of timelines.

Reviewed-on: #3
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
This commit was merged in pull request #3.
This commit is contained in:
2025-12-15 23:09:21 +00:00
committed by Rhys Lloyd
parent b9aaf9d30f
commit f9c5ef7b44
4 changed files with 297 additions and 4 deletions

10
Cargo.lock generated
View File

@@ -50,6 +50,15 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "owo-colors"
version = "4.2.3"
@@ -80,6 +89,7 @@ version = "0.8.0"
dependencies = [
"binrw",
"bitflags",
"itertools",
]
[[package]]

View File

@@ -1,10 +1,13 @@
[package]
name = "strafesnet_roblox_bot_file"
version = "0.8.0"
edition = "2021"
edition = "2024"
[dependencies]
binrw = "0.15.0"
bitflags = "2.6.0"
itertools = { version = "0.14.0", optional = true }
[features]
default = ["itertools"]
itertools = ["dep:itertools"]

View File

@@ -27,4 +27,15 @@ fn deserialize_all()->Result<(),v0::Error>{
Ok(())
}
// TODO: file serialization test
#[test]
#[cfg(feature="itertools")]
fn serialize_round_trip()->Result<(),binrw::Error>{
let file=std::fs::read("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap();
let block=v0::read_all_to_block(std::io::Cursor::new(file.as_slice())).unwrap();
let mut data=Vec::with_capacity(file.len());
v0::serialize(&block,&mut std::io::Cursor::new(&mut data))?;
// TODO: It encodes, but is it equal? Test something! PartialEq?
Ok(())
}

273
src/v0.rs
View File

@@ -11,20 +11,36 @@ 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)]
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]
@@ -32,10 +48,13 @@ pub struct Vector2{
#[derive(Debug,Clone)]
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,
}
@@ -85,6 +104,7 @@ pub struct Timed<E>
E:for<'a>binrw::BinWrite<Args<'a>=()>,
{
#[br(map=read_trey_double)]
#[bw(map=write_trey_double)]
pub time:f64,
pub event:E,
}
@@ -214,6 +234,7 @@ pub struct WorldEventButton{
#[derive(Debug,Clone)]
pub struct WorldEventSetTime{
#[br(map=read_trey_double)]
#[bw(map=write_trey_double)]
pub time:f64,
#[br(temp)]
#[bw(ignore)]
@@ -430,6 +451,7 @@ pub enum SettingType{
pub struct SettingEvent{
pub setting_type:SettingType,
#[br(map=read_trey_double)]
#[bw(map=write_trey_double)]
pub value:f64,
}
@@ -449,6 +471,7 @@ pub struct Block{
#[binrw]
#[brw(little)]
#[derive(Clone,Copy)]
enum EventType{
#[brw(magic=1u32)]
Input,
@@ -564,11 +587,19 @@ 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)]
struct BlockPosition(#[br(map=|i:u32|i-1)]u32);
struct BlockPosition(
#[br(map=|i:u32|i-1)]
#[bw(map=|&i:&u32|i+1)]
u32
);
#[derive(Debug)]
pub struct InvalidBlockId(pub BlockId);
@@ -698,3 +729,241 @@ pub fn read_all_to_block<R:BinReaderExt>(mut data:R)->Result<Block,Error>{
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<<14;
const FILE_VERSION:u32=0;
const EVENT_TYPES:[EventType;8]=[
EventType::Input,
EventType::Output,
EventType::Sound,
EventType::World,
EventType::Gravity,
EventType::Run,
EventType::Camera,
EventType::Setting,
];
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 (range,event_type) in plan.0.into_iter().zip(EVENT_TYPES){
let num_events=range.len();
if num_events==0{
continue;
}
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(())
}