diff --git a/strafe-client/src/file.rs b/strafe-client/src/file.rs index 3adc397..2f5263c 100644 --- a/strafe-client/src/file.rs +++ b/strafe-client/src/file.rs @@ -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(input:R)->Result{ +pub fn read(input:R)->Result{ 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"Ok(DataStructure::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)), + b"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>(path:P)->Result{ +pub enum Format2{ + #[cfg(feature="snf")] + Map(strafesnet_common::map::CompleteMap), + #[cfg(feature="snf")] + Bot(strafesnet_snf::bot::Segment), +} + +pub fn load>(path:P)->Result{ //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>(path:P)->Result{ + 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>(path:P)->Result), 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)); + } } }) } diff --git a/strafe-client/src/session.rs b/strafe-client/src/session.rs index 4503aa1..b5b8ba3 100644 --- a/strafe-client/src/session.rs +++ b/strafe-client/src/session.rs @@ -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,7 @@ pub enum SessionControlInstruction{ // copy the current session simulation recording into a replay and view it CopyRecordingIntoReplayAndSpectate, StopSpectate, + SaveReplay, } pub enum SessionPlaybackInstruction{ SkipForward, @@ -87,6 +89,11 @@ pub struct Recording{ instructions:Vec>, } impl Recording{ + fn new( + instructions:Vec>, + )->Self{ + Self{instructions} + } fn clear(&mut self){ self.instructions.clear(); } @@ -281,6 +288,16 @@ impl InstructionConsumer> for Session{ } _=self.simulation.timer.set_paused(ins.time,false); }, + Instruction::Control(SessionControlInstruction::SaveReplay)=>{ + 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(); + }, + } + }, Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{ match &self.view_state{ ViewState::Play=>{ @@ -339,6 +356,30 @@ impl InstructionConsumer> 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 diff --git a/strafe-client/src/setup.rs b/strafe-client/src/setup.rs index 5049881..acb51b3 100644 --- a/strafe-client/src/setup.rs +++ b/strafe-client/src/setup.rs @@ -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, diff --git a/strafe-client/src/window.rs b/strafe-client/src/window.rs index 00f52a2..6af6911 100644 --- a/strafe-client/src/window.rs +++ b/strafe-client/src/window.rs @@ -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,7 @@ 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), _=>None, }, _=>None,