Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
b3d3900628 | |||
2c11e2ef8e | |||
d732ae7ab3 | |||
9d5c5c1548 | |||
e5c1b85edd | |||
56fc7ed1bf | |||
937d0f12ba | |||
2a7dbbe3e4 | |||
79a22ae303 | |||
6dbef74faa | |||
a44132840e | |||
808593ecbe | |||
bc59d65d15 |
lib/snf/src
strafe-client/src
@ -1,98 +1,339 @@
|
||||
use binrw::{BinReaderExt, binrw};
|
||||
use binrw::{binrw,BinReaderExt,BinWrite,BinWriterExt};
|
||||
|
||||
use crate::newtypes;
|
||||
use crate::file::BlockId;
|
||||
use strafesnet_common::physics::Time;
|
||||
|
||||
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
InvalidHeader,
|
||||
InvalidHeader(binrw::Error),
|
||||
InvalidSegment(binrw::Error),
|
||||
SegmentConvert(newtypes::integer::RatioError),
|
||||
InstructionConvert(newtypes::physics::InstructionConvert),
|
||||
InstructionWrite(binrw::Error),
|
||||
InvalidSegmentId(SegmentId),
|
||||
InvalidData(binrw::Error),
|
||||
IO(std::io::Error),
|
||||
File(crate::file::Error),
|
||||
}
|
||||
|
||||
// Bot files are simply the sequence of instructions that the physics received during the run.
|
||||
// The instructions are partitioned into timestamped blocks for ease of streaming.
|
||||
//
|
||||
// Keyframe information required for efficient seeking
|
||||
// is part of a different file, and is generated from this file.
|
||||
|
||||
/* block types
|
||||
|
||||
BLOCK_BOT_HEADER:
|
||||
u128 map_resource_uuid //which map is this bot running
|
||||
//don't include style info in bot header because it's in the simulation state
|
||||
//blocks are laid out in chronological order, but indices may jump around.
|
||||
u64 num_segments
|
||||
// Tegments are laid out in chronological order,
|
||||
// but block_id is not necessarily in ascending order.
|
||||
//
|
||||
// This is to place the final segment close to the start of the file,
|
||||
// which allows the duration of the bot to be conveniently calculated
|
||||
// from the first and last instruction timestamps.
|
||||
u32 num_segments
|
||||
for _ in 0..num_segments{
|
||||
i64 time //simulation_state timestamp
|
||||
u64 block_id
|
||||
i64 time
|
||||
u32 instruction_count
|
||||
u32 block_id
|
||||
}
|
||||
|
||||
BLOCK_BOT_SEGMENT:
|
||||
//format version indicates what version of these structures to use
|
||||
SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about.
|
||||
//to read, greedily decode instructions until eof
|
||||
loop{
|
||||
//delta encode as much as possible (time,mousepos)
|
||||
//strafe ticks are implied
|
||||
//physics can be implied in an input-only bot file
|
||||
TimedInstruction<SimulationInstruction> instruction
|
||||
// segments can potentially be losslessly compressed!
|
||||
for _ in 0..instruction_count{
|
||||
// TODO: delta encode as much as possible (time,mousepos)
|
||||
i64 time
|
||||
physics::Instruction instruction
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
//error hiding mock code
|
||||
mod simulation{
|
||||
#[super::binrw]
|
||||
#[brw(little)]
|
||||
pub struct State{}
|
||||
#[super::binrw]
|
||||
#[brw(little)]
|
||||
pub struct Instruction{}
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
struct SegmentHeader{
|
||||
time:i64,
|
||||
instruction_count:u32,
|
||||
block_id:BlockId,
|
||||
}
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
struct Header{
|
||||
num_segments:u32,
|
||||
#[br(count=num_segments)]
|
||||
segments:Vec<SegmentHeader>,
|
||||
}
|
||||
// mod instruction{
|
||||
// #[super::binrw]
|
||||
// #[brw(little)]
|
||||
// pub struct TimedInstruction<Instruction:binrw::BinRead+binrw::BinWrite>{
|
||||
// time:u64,
|
||||
// instruction:Instruction
|
||||
// }
|
||||
// }
|
||||
// mod timeline{
|
||||
// #[super::binrw]
|
||||
// #[brw(little)]
|
||||
// pub struct Timeline<Instruction:binrw::BinRead+binrw::BinWrite>{
|
||||
// #[bw(try_calc(u32::try_from(instructions.len())))]
|
||||
// instruction_count:u32,
|
||||
// #[br(count=instruction_count)]
|
||||
// instructions:Vec<super::instruction::TimedInstruction<Instruction>>
|
||||
// }
|
||||
// }
|
||||
|
||||
//serious code
|
||||
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
#[derive(Clone,Copy,Debug,id::Id)]
|
||||
pub struct SegmentId(u32);
|
||||
struct SegmentId(u32);
|
||||
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
pub struct Segment{
|
||||
state:simulation::State,
|
||||
//#[bw(try_calc(u32::try_from(instructions.len())))]
|
||||
//instruction_count:u32,
|
||||
//#[br(count=instruction_count)]
|
||||
//instructions:Vec<instruction::TimedInstruction<simulation::Instruction>>
|
||||
pub instructions:Vec<TimedPhysicsInstruction>
|
||||
}
|
||||
|
||||
//please remember that strafe ticks are implicit! 33% smaller bot files
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct SegmentInfo{
|
||||
/// time of the first instruction in this segment.
|
||||
time:Time,
|
||||
instruction_count:u32,
|
||||
/// How many total instructions in segments up to and including this segment
|
||||
/// Alternatively, the id of the first instruction be in the _next_ segment
|
||||
instructions_subtotal:u64,
|
||||
block_id:BlockId,
|
||||
}
|
||||
|
||||
pub struct StreamableBot<R:BinReaderExt>{
|
||||
file:crate::file::File<R>,
|
||||
//timeline:timeline::Timeline<SegmentId>,
|
||||
segment_id_to_block_id:Vec<crate::file::BlockId>,
|
||||
segment_map:Vec<SegmentInfo>,
|
||||
}
|
||||
impl<R:BinReaderExt> StreamableBot<R>{
|
||||
pub(crate) fn new(file:crate::file::File<R>)->Result<Self,Error>{
|
||||
Err(Error::InvalidHeader)
|
||||
pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{
|
||||
//assume the file seek is in the right place to start reading header
|
||||
let header:Header=file.data_mut().read_le().map_err(Error::InvalidHeader)?;
|
||||
let mut instructions_subtotal=0;
|
||||
let segment_map=header.segments.into_iter().map(|SegmentHeader{time,instruction_count,block_id}|{
|
||||
instructions_subtotal+=instruction_count as u64;
|
||||
SegmentInfo{
|
||||
time:Time::raw(time),
|
||||
instruction_count,
|
||||
instructions_subtotal,
|
||||
block_id,
|
||||
}
|
||||
}).collect();
|
||||
Ok(Self{
|
||||
file,
|
||||
segment_map,
|
||||
})
|
||||
}
|
||||
pub fn load_segment(&mut self,segment_id:SegmentId)->Result<Segment,Error>{
|
||||
let block_id=*self.segment_id_to_block_id.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?;
|
||||
let mut block=self.file.block_reader(block_id).map_err(Error::File)?;
|
||||
let segment=block.read_le().map_err(Error::InvalidSegment)?;
|
||||
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
|
||||
Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
|
||||
}
|
||||
pub fn find_segments_instruction_range(&self,start_instruction:u64,end_instruction:u64)->&[SegmentInfo]{
|
||||
let start=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<start_instruction);
|
||||
let end=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<end_instruction);
|
||||
&self.segment_map[start..=end]
|
||||
}
|
||||
// pub fn find_segments_time_range(&self,start_time:Time,end_time:Time)->&[SegmentInfo]{
|
||||
// // TODO: This is off by one, both should be one less
|
||||
// let start=self.segment_map.partition_point(|segment_info|segment_info.time<start_time);
|
||||
// let end=self.segment_map.partition_point(|segment_info|segment_info.time<end_time);
|
||||
// &self.segment_map[start..=end]
|
||||
// }
|
||||
fn append_to_segment(&mut self,segment_info:SegmentInfo,segment:&mut Segment)->Result<(),Error>{
|
||||
let mut block=self.file.block_reader(segment_info.block_id).map_err(Error::File)?;
|
||||
for _ in 0..segment_info.instruction_count{
|
||||
let instruction:newtypes::physics::TimedInstruction=block.read_le().map_err(Error::InvalidSegment)?;
|
||||
segment.instructions.push(instruction.try_into().map_err(Error::SegmentConvert)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn load_segment(&mut self,segment_info:SegmentInfo)->Result<Segment,Error>{
|
||||
let mut segment=Segment{
|
||||
instructions:Vec::with_capacity(segment_info.instruction_count as usize),
|
||||
};
|
||||
self.append_to_segment(segment_info,&mut segment)?;
|
||||
Ok(segment)
|
||||
}
|
||||
pub fn read_all(&mut self)->Result<Segment,Error>{
|
||||
let mut segment=Segment{
|
||||
instructions:Vec::new(),
|
||||
};
|
||||
for i in 0..self.segment_map.len(){
|
||||
let segment_info=self.segment_map[i];
|
||||
self.append_to_segment(segment_info,&mut segment)?;
|
||||
}
|
||||
Ok(segment)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
|
||||
pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
|
||||
// decide which instructions to put in which segment
|
||||
// write segment 1 to block 1
|
||||
// write segment N to block 2
|
||||
// write rest of segments
|
||||
// 1 2 3 4 5
|
||||
// becomes
|
||||
// [1 5] 2 3 4
|
||||
struct SegmentHeaderInfo{
|
||||
time:Time,
|
||||
instruction_count:u32,
|
||||
range:core::ops::Range<usize>
|
||||
}
|
||||
|
||||
let mut segment_header_infos=Vec::new();
|
||||
let mut raw_segments=std::io::Cursor::new(Vec::new());
|
||||
|
||||
// block info
|
||||
let mut start_time=Time::ZERO;
|
||||
let mut start_position=raw_segments.position() as usize;
|
||||
let mut instruction_count=0;
|
||||
|
||||
let mut last_position=start_position;
|
||||
|
||||
let mut iter=instructions.into_iter();
|
||||
|
||||
macro_rules! collect_instruction{
|
||||
($instruction:expr)=>{
|
||||
let time=$instruction.time;
|
||||
let instruction_writable:newtypes::physics::TimedInstruction=$instruction.try_into().map_err(Error::InstructionConvert)?;
|
||||
instruction_writable.write_le(&mut raw_segments).map_err(Error::InstructionWrite)?;
|
||||
instruction_count+=1;
|
||||
let position=raw_segments.position() as usize;
|
||||
// exceeds max block size
|
||||
if MAX_BLOCK_SIZE<position-last_position{
|
||||
segment_header_infos.push(SegmentHeaderInfo{
|
||||
time:start_time,
|
||||
instruction_count,
|
||||
range:start_position..last_position,
|
||||
});
|
||||
start_position=last_position;
|
||||
instruction_count=0;
|
||||
start_time=time;
|
||||
}
|
||||
last_position=position;
|
||||
}
|
||||
}
|
||||
|
||||
// unroll one loop iteration to grab the starting time
|
||||
if let Some(instruction)=iter.next(){
|
||||
start_time=instruction.time;
|
||||
collect_instruction!(instruction);
|
||||
}
|
||||
|
||||
for instruction in iter{
|
||||
collect_instruction!(instruction);
|
||||
}
|
||||
//last block, whatever size it happens to be
|
||||
{
|
||||
let final_position=raw_segments.position() as usize;
|
||||
segment_header_infos.push(SegmentHeaderInfo{
|
||||
time:start_time,
|
||||
instruction_count,
|
||||
range:start_position..final_position,
|
||||
});
|
||||
}
|
||||
// drop cursor
|
||||
let raw_segments=raw_segments.into_inner();
|
||||
|
||||
let num_segments=segment_header_infos.len();
|
||||
|
||||
// segments list is in chronological order
|
||||
let make_segment_header=|block_id,&SegmentHeaderInfo{time,instruction_count,range:ref _range}|SegmentHeader{
|
||||
time:time.get(),
|
||||
instruction_count,
|
||||
block_id,
|
||||
};
|
||||
let segments=if 2<num_segments{
|
||||
let mut segments=Vec::with_capacity(num_segments);
|
||||
// segment 1 is second block
|
||||
if let Some(seg)=segment_header_infos.first(){
|
||||
segments.push(make_segment_header(BlockId::new(1),seg));
|
||||
}
|
||||
// rest of segments start at fourth block
|
||||
for (i,seg) in segment_header_infos[1..num_segments-1].iter().enumerate(){
|
||||
make_segment_header(BlockId::new(3+i as u32),seg);
|
||||
}
|
||||
// last segment is third block
|
||||
if let Some(seg)=segment_header_infos.last(){
|
||||
segments.push(make_segment_header(BlockId::new(2),seg));
|
||||
}
|
||||
segments
|
||||
}else{
|
||||
// all segments in order
|
||||
segment_header_infos.iter().enumerate().map(|(i,seg)|
|
||||
make_segment_header(BlockId::new(1+i as u32),seg)
|
||||
).collect()
|
||||
};
|
||||
|
||||
let header=Header{
|
||||
num_segments:num_segments as u32,
|
||||
segments,
|
||||
};
|
||||
|
||||
// map header is +1
|
||||
let block_count=1+num_segments as u32;
|
||||
|
||||
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
|
||||
// block_location is one longer than block_count
|
||||
let mut block_location=Vec::with_capacity(1+block_count as usize);
|
||||
|
||||
//probe header length
|
||||
let mut bot_header_data=Vec::new();
|
||||
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
|
||||
|
||||
// the first block location is the map header
|
||||
block_location.push(offset);
|
||||
offset+=bot_header_data.len() as u64;
|
||||
block_location.push(offset);
|
||||
|
||||
// priming includes file header + first 3 blocks [bot header, first segment, last segment]
|
||||
let priming=if 2<num_segments{
|
||||
// segment 1 is block 2
|
||||
if let Some(seg)=segment_header_infos.first(){
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
// last segment is block 3
|
||||
if let Some(seg)=segment_header_infos.last(){
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
|
||||
let priming=offset;
|
||||
|
||||
// rest of segments
|
||||
for seg in &segment_header_infos[1..num_segments-1]{
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
priming
|
||||
}else{
|
||||
// all segments in order
|
||||
for seg in &segment_header_infos{
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
offset
|
||||
};
|
||||
|
||||
let file_header=crate::file::Header{
|
||||
fourcc:crate::file::FourCC::Bot,
|
||||
version:0,
|
||||
priming,
|
||||
resource:0,
|
||||
block_count,
|
||||
block_location,
|
||||
};
|
||||
|
||||
// write file header
|
||||
writer.write_le(&file_header).map_err(Error::InvalidData)?;
|
||||
// write bot header
|
||||
writer.write(&bot_header_data).map_err(Error::IO)?;
|
||||
|
||||
// write blocks
|
||||
if 2<num_segments{
|
||||
// segment 1 is block 2
|
||||
if let Some(seg)=segment_header_infos.first(){
|
||||
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
|
||||
}
|
||||
// last segment is block 3
|
||||
if let Some(seg)=segment_header_infos.last(){
|
||||
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
|
||||
}
|
||||
// rest of segments
|
||||
for seg in &segment_header_infos[1..num_segments-1]{
|
||||
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
|
||||
}
|
||||
}else{
|
||||
// all segments in order
|
||||
for seg in segment_header_infos{
|
||||
writer.write(&raw_segments[seg.range]).map_err(Error::IO)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,3 +1,9 @@
|
||||
pub const fn flag(b:bool,mask:u8)->u8{
|
||||
(-(b as i8) as u8)&mask
|
||||
}
|
||||
pub fn bool_from_u8(value:u8)->bool{
|
||||
value!=0
|
||||
}
|
||||
pub fn bool_into_u8(value:&bool)->u8{
|
||||
*value as u8
|
||||
}
|
||||
|
@ -38,6 +38,23 @@ pub struct Ratio64Vec2{
|
||||
pub x:Ratio64,
|
||||
pub y:Ratio64,
|
||||
}
|
||||
impl TryInto<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
|
||||
type Error=RatioError;
|
||||
fn try_into(self)->Result<strafesnet_common::integer::Ratio64Vec2,Self::Error>{
|
||||
Ok(strafesnet_common::integer::Ratio64Vec2{
|
||||
x:self.x.try_into()?,
|
||||
y:self.y.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl From<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
|
||||
fn from(value:strafesnet_common::integer::Ratio64Vec2)->Self{
|
||||
Self{
|
||||
x:value.x.into(),
|
||||
y:value.y.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Angle32=i32;
|
||||
pub type Planar64=i64;
|
||||
|
@ -1,7 +1,9 @@
|
||||
mod common;
|
||||
pub mod aabb;
|
||||
pub mod model;
|
||||
pub mod mouse;
|
||||
pub mod integer;
|
||||
pub mod physics;
|
||||
pub mod gameplay_modes;
|
||||
pub mod gameplay_style;
|
||||
pub mod gameplay_attributes;
|
||||
|
25
lib/snf/src/newtypes/mouse.rs
Normal file
25
lib/snf/src/newtypes/mouse.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use super::integer::Time;
|
||||
|
||||
#[binrw::binrw]
|
||||
#[brw(little)]
|
||||
pub struct MouseState{
|
||||
pub pos:[i32;2],
|
||||
pub time:Time,
|
||||
}
|
||||
|
||||
impl<T> Into<strafesnet_common::mouse::MouseState<T>> for MouseState{
|
||||
fn into(self)->strafesnet_common::mouse::MouseState<T>{
|
||||
strafesnet_common::mouse::MouseState{
|
||||
pos:self.pos.into(),
|
||||
time:strafesnet_common::integer::Time::raw(self.time),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> From<strafesnet_common::mouse::MouseState<T>> for MouseState{
|
||||
fn from(value:strafesnet_common::mouse::MouseState<T>)->Self{
|
||||
Self{
|
||||
pos:value.pos.to_array(),
|
||||
time:value.time.get(),
|
||||
}
|
||||
}
|
||||
}
|
156
lib/snf/src/newtypes/physics.rs
Normal file
156
lib/snf/src/newtypes/physics.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use super::integer::Time;
|
||||
use super::common::{bool_from_u8,bool_into_u8};
|
||||
|
||||
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
|
||||
|
||||
#[binrw::binrw]
|
||||
#[brw(little)]
|
||||
pub struct TimedInstruction{
|
||||
pub time:Time,
|
||||
pub instruction:Instruction,
|
||||
}
|
||||
|
||||
impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
|
||||
type Error=super::integer::RatioError;
|
||||
fn try_into(self)->Result<TimedPhysicsInstruction,Self::Error>{
|
||||
Ok(strafesnet_common::instruction::TimedInstruction{
|
||||
time:strafesnet_common::integer::Time::raw(self.time),
|
||||
instruction:self.instruction.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
|
||||
type Error=super::physics::InstructionConvert;
|
||||
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
|
||||
Ok(Self{
|
||||
time:value.time.get(),
|
||||
instruction:value.instruction.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[binrw::binrw]
|
||||
#[brw(little)]
|
||||
pub enum Instruction{
|
||||
#[brw(magic=0u8)]
|
||||
ReplaceMouse{
|
||||
m0:super::mouse::MouseState,
|
||||
m1:super::mouse::MouseState
|
||||
},
|
||||
#[brw(magic=1u8)]
|
||||
SetNextMouse(super::mouse::MouseState),
|
||||
#[brw(magic=2u8)]
|
||||
SetMoveRight(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=3u8)]
|
||||
SetMoveUp(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=4u8)]
|
||||
SetMoveBack(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=5u8)]
|
||||
SetMoveLeft(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=6u8)]
|
||||
SetMoveDown(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=7u8)]
|
||||
SetMoveForward(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=8u8)]
|
||||
SetJump(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=9u8)]
|
||||
SetZoom(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=10u8)]
|
||||
Reset,
|
||||
#[brw(magic=11u8)]
|
||||
Restart(super::gameplay_modes::ModeId),
|
||||
#[brw(magic=12u8)]
|
||||
Spawn(super::gameplay_modes::ModeId,super::gameplay_modes::StageId),
|
||||
#[brw(magic=13u8)]
|
||||
PracticeFly,
|
||||
#[brw(magic=14u8)]
|
||||
SetSensitivity(super::integer::Ratio64Vec2),
|
||||
#[brw(magic=255u8)]
|
||||
Idle,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum InstructionConvert{
|
||||
/// This is an instruction that can be dropped when serializing
|
||||
DropInstruction,
|
||||
}
|
||||
impl std::fmt::Display for InstructionConvert{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for InstructionConvert{}
|
||||
impl TryInto<strafesnet_common::physics::Instruction> for Instruction{
|
||||
type Error=super::integer::RatioError;
|
||||
fn try_into(self)->Result<strafesnet_common::physics::Instruction,Self::Error>{
|
||||
Ok(match self{
|
||||
Instruction::ReplaceMouse{m0,m1}=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
|
||||
Instruction::SetNextMouse(m)=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m.into())),
|
||||
Instruction::SetMoveRight(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state.into())),
|
||||
Instruction::SetMoveUp(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state.into())),
|
||||
Instruction::SetMoveBack(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state.into())),
|
||||
Instruction::SetMoveLeft(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state.into())),
|
||||
Instruction::SetMoveDown(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state.into())),
|
||||
Instruction::SetMoveForward(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state.into())),
|
||||
Instruction::SetJump(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state.into())),
|
||||
Instruction::SetZoom(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state.into())),
|
||||
Instruction::Reset=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset),
|
||||
Instruction::Restart(mode_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::new(mode_id))),
|
||||
Instruction::Spawn(mode_id,stage_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(
|
||||
strafesnet_common::gameplay_modes::ModeId::new(mode_id),
|
||||
strafesnet_common::gameplay_modes::StageId::new(stage_id),
|
||||
)),
|
||||
Instruction::PracticeFly=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly),
|
||||
Instruction::SetSensitivity(sensitivity)=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity.try_into()?)),
|
||||
Instruction::Idle=>strafesnet_common::physics::Instruction::Idle,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl TryFrom<strafesnet_common::physics::Instruction> for Instruction{
|
||||
type Error=InstructionConvert;
|
||||
fn try_from(value:strafesnet_common::physics::Instruction)->Result<Self,Self::Error>{
|
||||
match value{
|
||||
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0,m1})=>Ok(Instruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
|
||||
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m))=>Ok(Instruction::SetNextMouse(m.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state))=>Ok(Instruction::SetMoveRight(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state))=>Ok(Instruction::SetMoveUp(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state))=>Ok(Instruction::SetMoveBack(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state))=>Ok(Instruction::SetMoveLeft(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state))=>Ok(Instruction::SetMoveDown(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state))=>Ok(Instruction::SetMoveForward(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state))=>Ok(Instruction::SetJump(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state))=>Ok(Instruction::SetZoom(state.into())),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset)=>Ok(Instruction::Reset),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(mode_id))=>Ok(Instruction::Restart(mode_id.get())),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(mode_id,stage_id))=>Ok(Instruction::Spawn(
|
||||
mode_id.get(),
|
||||
stage_id.get(),
|
||||
)),
|
||||
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly)=>Ok(Instruction::PracticeFly),
|
||||
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity))=>Ok(Instruction::SetSensitivity(sensitivity.into())),
|
||||
strafesnet_common::physics::Instruction::Idle=>Ok(Instruction::Idle),
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,8 @@ pub enum ReadError{
|
||||
StrafesNET(strafesnet_snf::Error),
|
||||
#[cfg(feature="snf")]
|
||||
StrafesNETMap(strafesnet_snf::map::Error),
|
||||
#[cfg(feature="snf")]
|
||||
StrafesNETBot(strafesnet_snf::bot::Error),
|
||||
Io(std::io::Error),
|
||||
UnknownFileFormat,
|
||||
}
|
||||
@ -20,28 +22,35 @@ impl std::fmt::Display for ReadError{
|
||||
}
|
||||
impl std::error::Error for ReadError{}
|
||||
|
||||
pub enum DataStructure{
|
||||
enum Format{
|
||||
#[cfg(feature="roblox")]
|
||||
Roblox(strafesnet_rbx_loader::Model),
|
||||
#[cfg(feature="source")]
|
||||
Source(strafesnet_bsp_loader::Bsp),
|
||||
#[cfg(feature="snf")]
|
||||
StrafesNET(strafesnet_common::map::CompleteMap),
|
||||
SNFM(strafesnet_common::map::CompleteMap),
|
||||
#[cfg(feature="snf")]
|
||||
SNFB(strafesnet_snf::bot::Segment),
|
||||
}
|
||||
|
||||
pub fn read<R:Read+std::io::Seek>(input:R)->Result<DataStructure,ReadError>{
|
||||
pub fn read<R:Read+std::io::Seek>(input:R)->Result<Format,ReadError>{
|
||||
let mut buf=std::io::BufReader::new(input);
|
||||
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
|
||||
match &peek[0..4]{
|
||||
#[cfg(feature="roblox")]
|
||||
b"<rob"=>Ok(DataStructure::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)),
|
||||
b"<rob"=>Ok(Format::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)),
|
||||
#[cfg(feature="source")]
|
||||
b"VBSP"=>Ok(DataStructure::Source(strafesnet_bsp_loader::read(buf).map_err(ReadError::Source)?)),
|
||||
b"VBSP"=>Ok(Format::Source(strafesnet_bsp_loader::read(buf).map_err(ReadError::Source)?)),
|
||||
#[cfg(feature="snf")]
|
||||
b"SNFM"=>Ok(DataStructure::StrafesNET(
|
||||
b"SNFM"=>Ok(Format::SNFM(
|
||||
strafesnet_snf::read_map(buf).map_err(ReadError::StrafesNET)?
|
||||
.into_complete_map().map_err(ReadError::StrafesNETMap)?
|
||||
)),
|
||||
#[cfg(feature="snf")]
|
||||
b"SNFB"=>Ok(Format::SNFB(
|
||||
strafesnet_snf::read_bot(buf).map_err(ReadError::StrafesNET)?
|
||||
.read_all().map_err(ReadError::StrafesNETBot)?
|
||||
)),
|
||||
_=>Err(ReadError::UnknownFileFormat),
|
||||
}
|
||||
}
|
||||
@ -59,14 +68,23 @@ impl std::fmt::Display for LoadError{
|
||||
}
|
||||
impl std::error::Error for LoadError{}
|
||||
|
||||
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::CompleteMap,LoadError>{
|
||||
pub enum Format2{
|
||||
#[cfg(feature="snf")]
|
||||
Map(strafesnet_common::map::CompleteMap),
|
||||
#[cfg(feature="snf")]
|
||||
Bot(strafesnet_snf::bot::Segment),
|
||||
}
|
||||
|
||||
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<Format2,LoadError>{
|
||||
//blocking because it's simpler...
|
||||
let file=std::fs::File::open(path).map_err(LoadError::File)?;
|
||||
match read(file).map_err(LoadError::ReadError)?{
|
||||
#[cfg(feature="snf")]
|
||||
DataStructure::StrafesNET(map)=>Ok(map),
|
||||
Format::SNFB(bot)=>Ok(Format2::Bot(bot)),
|
||||
#[cfg(feature="snf")]
|
||||
Format::SNFM(map)=>Ok(Format2::Map(map)),
|
||||
#[cfg(feature="roblox")]
|
||||
DataStructure::Roblox(model)=>{
|
||||
Format::Roblox(model)=>{
|
||||
let mut place=model.into_place();
|
||||
place.run_scripts();
|
||||
|
||||
@ -99,10 +117,10 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::Co
|
||||
)
|
||||
);
|
||||
|
||||
Ok(map)
|
||||
Ok(Format2::Map(map))
|
||||
},
|
||||
#[cfg(feature="source")]
|
||||
DataStructure::Source(bsp)=>{
|
||||
Format::Source(bsp)=>{
|
||||
let mut loader=strafesnet_deferred_loader::source_legacy();
|
||||
|
||||
let (texture_loader,mesh_loader)=loader.get_inner_mut();
|
||||
@ -138,7 +156,7 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::Co
|
||||
),
|
||||
);
|
||||
|
||||
Ok(map)
|
||||
Ok(Format2::Map(map))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ pub enum Instruction{
|
||||
Render,
|
||||
Resize(winit::dpi::PhysicalSize<u32>),
|
||||
ChangeMap(strafesnet_common::map::CompleteMap),
|
||||
LoadReplay(strafesnet_snf::bot::Segment),
|
||||
}
|
||||
|
||||
pub fn new<'a>(
|
||||
@ -69,6 +70,9 @@ pub fn new<'a>(
|
||||
run_session_instruction!(ins.time,SessionInstruction::Input(SessionInputInstruction::Mode(crate::session::ImplicitModeInstruction::ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId::MAIN,strafesnet_common::gameplay_modes::StageId::FIRST))));
|
||||
run_graphics_worker_instruction!(GraphicsInstruction::ChangeMap(complete_map));
|
||||
},
|
||||
Instruction::LoadReplay(bot)=>{
|
||||
run_session_instruction!(ins.time,SessionInstruction::LoadReplay(bot));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ pub enum Instruction<'a>{
|
||||
Control(SessionControlInstruction),
|
||||
Playback(SessionPlaybackInstruction),
|
||||
ChangeMap(&'a strafesnet_common::map::CompleteMap),
|
||||
LoadReplay(strafesnet_snf::bot::Segment),
|
||||
Idle,
|
||||
}
|
||||
|
||||
@ -44,6 +45,8 @@ pub enum SessionControlInstruction{
|
||||
// copy the current session simulation recording into a replay and view it
|
||||
CopyRecordingIntoReplayAndSpectate,
|
||||
StopSpectate,
|
||||
SaveReplay,
|
||||
LoadIntoReplayState,
|
||||
}
|
||||
pub enum SessionPlaybackInstruction{
|
||||
SkipForward,
|
||||
@ -87,12 +90,17 @@ pub struct Recording{
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
}
|
||||
impl Recording{
|
||||
fn new(
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
)->Self{
|
||||
Self{instructions}
|
||||
}
|
||||
fn clear(&mut self){
|
||||
self.instructions.clear();
|
||||
}
|
||||
}
|
||||
pub struct Replay{
|
||||
last_instruction_id:usize,
|
||||
next_instruction_id:usize,
|
||||
recording:Recording,
|
||||
simulation:Simulation,
|
||||
}
|
||||
@ -102,7 +110,7 @@ impl Replay{
|
||||
simulation:Simulation,
|
||||
)->Self{
|
||||
Self{
|
||||
last_instruction_id:0,
|
||||
next_instruction_id:0,
|
||||
recording,
|
||||
simulation,
|
||||
}
|
||||
@ -110,16 +118,16 @@ impl Replay{
|
||||
pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){
|
||||
let mut time=self.simulation.timer.time(time_limit);
|
||||
loop{
|
||||
if let Some(ins)=self.recording.instructions.get(self.last_instruction_id){
|
||||
if let Some(ins)=self.recording.instructions.get(self.next_instruction_id){
|
||||
if ins.time<time{
|
||||
PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone());
|
||||
self.last_instruction_id+=1;
|
||||
self.next_instruction_id+=1;
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}else{
|
||||
// loop playback
|
||||
self.last_instruction_id=0;
|
||||
self.next_instruction_id=0;
|
||||
// No need to reset physics because the very first instruction is 'Reset'
|
||||
let new_time=self.recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
self.simulation.timer.set_time(time_limit,new_time);
|
||||
@ -243,7 +251,7 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
// what if they pause for 5ms lmao
|
||||
_=self.simulation.timer.set_paused(ins.time,paused);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=>{
|
||||
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
|
||||
// Bind: B
|
||||
|
||||
// pause simulation
|
||||
@ -281,6 +289,30 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
}
|
||||
_=self.simulation.timer.set_paused(ins.time,false);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::SaveReplay)=>{
|
||||
// Bind: N
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
let file=std::fs::File::create(format!("{}.snfb",ins.time)).unwrap();
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),replay.recording.instructions).unwrap();
|
||||
},
|
||||
}
|
||||
_=self.simulation.timer.set_paused(ins.time,false);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::LoadIntoReplayState)=>{
|
||||
// Bind: J
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
self.recording.instructions=replay.recording.instructions.into_iter().take(replay.next_instruction_id).collect();
|
||||
self.simulation=replay.simulation;
|
||||
},
|
||||
}
|
||||
// don't unpause -- use the replay timer state whether it is pasued or unpaused
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>{
|
||||
@ -323,7 +355,7 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
|
||||
replay.simulation.timer.set_time(ins.time,time);
|
||||
// resimulate the entire playback lol
|
||||
replay.last_instruction_id=0;
|
||||
replay.next_instruction_id=0;
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -339,6 +371,30 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
self.clear_recording();
|
||||
self.change_map(complete_map);
|
||||
},
|
||||
Instruction::LoadReplay(bot)=>{
|
||||
// pause simulation
|
||||
_=self.simulation.timer.set_paused(ins.time,true);
|
||||
|
||||
// create recording
|
||||
let recording=Recording::new(bot.instructions);
|
||||
|
||||
// create timer starting at first instruction (or zero if the list is empty)
|
||||
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
let timer=Timer::unpaused(ins.time,new_time);
|
||||
|
||||
// create default physics state
|
||||
let simulation=Simulation::new(timer,Default::default());
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
let bot_id=BotId(self.replays.len() as u32);
|
||||
self.replays.insert(bot_id,Replay::new(
|
||||
recording,
|
||||
simulation,
|
||||
));
|
||||
|
||||
// begin spectate
|
||||
self.view_state=ViewState::Replay(bot_id);
|
||||
},
|
||||
Instruction::Idle=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle);
|
||||
// this just refreshes the replays
|
||||
|
@ -215,7 +215,7 @@ pub fn setup_and_start(title:&str){
|
||||
setup_context,
|
||||
);
|
||||
|
||||
if let Some(arg)=std::env::args().nth(1){
|
||||
for arg in std::env::args().skip(1){
|
||||
let path=std::path::PathBuf::from(arg);
|
||||
window_thread.send(TimedInstruction{
|
||||
time:integer::Time::ZERO,
|
||||
|
@ -1,6 +1,7 @@
|
||||
use strafesnet_common::instruction::TimedInstruction;
|
||||
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
|
||||
use strafesnet_common::physics::{MiscInstruction,SetControlInstruction};
|
||||
use crate::file::Format2;
|
||||
use crate::physics_worker::Instruction as PhysicsWorkerInstruction;
|
||||
use crate::session::{SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction};
|
||||
|
||||
@ -29,8 +30,9 @@ impl WindowContext<'_>{
|
||||
match event{
|
||||
winit::event::WindowEvent::DroppedFile(path)=>{
|
||||
match crate::file::load(path.as_path()){
|
||||
Ok(map)=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(),
|
||||
Err(e)=>println!("Failed to load map: {e}"),
|
||||
Ok(Format2::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(),
|
||||
Ok(Format2::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}).unwrap(),
|
||||
Err(e)=>println!("Failed to load file: {e}"),
|
||||
}
|
||||
},
|
||||
winit::event::WindowEvent::Focused(state)=>{
|
||||
@ -153,6 +155,8 @@ impl WindowContext<'_>{
|
||||
"F"|"f"=>input_misc!(PracticeFly,s),
|
||||
"B"|"b"=>session_ctrl!(CopyRecordingIntoReplayAndSpectate,s),
|
||||
"X"|"x"=>session_ctrl!(StopSpectate,s),
|
||||
"N"|"n"=>session_ctrl!(SaveReplay,s),
|
||||
"J"|"j"=>session_ctrl!(LoadIntoReplayState,s),
|
||||
_=>None,
|
||||
},
|
||||
_=>None,
|
||||
|
Loading…
x
Reference in New Issue
Block a user