implement file drag drop for native player

This commit is contained in:
2026-02-20 09:14:49 -08:00
parent c33daaf0c6
commit 2f584744c7
12 changed files with 41 additions and 113 deletions

2
Cargo.lock generated
View File

@@ -1678,7 +1678,6 @@ dependencies = [
"strafesnet_common", "strafesnet_common",
"strafesnet_graphics", "strafesnet_graphics",
"strafesnet_roblox_bot_file", "strafesnet_roblox_bot_file",
"strafesnet_snf",
"wgpu", "wgpu",
] ]
@@ -1705,6 +1704,7 @@ dependencies = [
"strafesnet_graphics", "strafesnet_graphics",
"strafesnet_roblox_bot_file", "strafesnet_roblox_bot_file",
"strafesnet_roblox_bot_player", "strafesnet_roblox_bot_player",
"strafesnet_snf",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",

View File

@@ -13,5 +13,6 @@ python3 -m http.server
How to run the native player: How to run the native player:
``` ```
cd native-player cd native-player
cargo run --release cargo run --release -- ../web-demo/bhop_marble_5692093612.snfm ../web-demo/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d.qbot
``` ```
You can drag and drop map files and bot files to load them.

View File

@@ -8,5 +8,4 @@ glam = "0.31.0"
strafesnet_common.workspace = true strafesnet_common.workspace = true
strafesnet_graphics.workspace = true strafesnet_graphics.workspace = true
strafesnet_roblox_bot_file.workspace = true strafesnet_roblox_bot_file.workspace = true
strafesnet_snf.workspace = true
wgpu = "28.0.0" wgpu = "28.0.0"

View File

@@ -11,16 +11,15 @@ pub struct CompleteBot{
impl CompleteBot{ impl CompleteBot{
pub(crate) const CAMERA_OFFSET:glam::Vec3=glam::vec3(0.0,2.0,0.0); pub(crate) const CAMERA_OFFSET:glam::Vec3=glam::vec3(0.0,2.0,0.0);
pub fn new( pub fn new(
data:&[u8], timelines:v0::Block,
)->Result<Self,v0::Error>{ )->Self{
let timelines=v0::read_all_to_block(std::io::Cursor::new(data))?;
let first=timelines.output_events.first().unwrap(); let first=timelines.output_events.first().unwrap();
let last=timelines.output_events.last().unwrap(); let last=timelines.output_events.last().unwrap();
Ok(Self{ Self{
time_base:crate::time::from_float(first.time).unwrap(), time_base:crate::time::from_float(first.time).unwrap(),
duration:crate::time::from_float(last.time-first.time).unwrap(), duration:crate::time::from_float(last.time-first.time).unwrap(),
timelines, timelines,
}) }
} }
pub const fn time_base(&self)->PhysicsTime{ pub const fn time_base(&self)->PhysicsTime{
self.time_base self.time_base

View File

@@ -18,9 +18,9 @@ impl Graphics{
config, config,
} }
} }
pub fn change_map(&mut self,map:&crate::map::CompleteMap){ pub fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
self.graphics.clear(); self.graphics.clear();
self.graphics.generate_models(&self.device,&self.queue,map.map()); self.graphics.generate_models(&self.device,&self.queue,map);
} }
pub fn resize(&mut self,surface:&wgpu::Surface<'_>,size:glam::UVec2){ pub fn resize(&mut self,surface:&wgpu::Surface<'_>,size:glam::UVec2){
self.config.width=size.x.max(1); self.config.width=size.x.max(1);

View File

@@ -1,5 +1,4 @@
pub mod bot; pub mod bot;
pub mod map;
pub mod head; pub mod head;
pub mod time; pub mod time;
pub mod state; pub mod state;

View File

@@ -1,29 +0,0 @@
#[derive(Debug)]
pub enum Error{
File(strafesnet_snf::Error),
Map(strafesnet_snf::map::Error),
}
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{}
pub struct CompleteMap{
complete_map:strafesnet_common::map::CompleteMap,
}
impl CompleteMap{
pub fn new(
data:&[u8],
)->Result<Self,Error>{
let complete_map=strafesnet_snf::read_map(std::io::Cursor::new(data))
.map_err(Error::File)?
.into_complete_map()
.map_err(Error::Map)?;
Ok(Self{complete_map})
}
pub const fn map(&self)->&strafesnet_common::map::CompleteMap{
&self.complete_map
}
}

View File

@@ -1,21 +1,11 @@
use std::io::Read; use std::io::Read;
#[cfg(any(feature="roblox",feature="source"))]
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
#[expect(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum ReadError{ pub enum ReadError{
#[cfg(feature="roblox")]
Roblox(strafesnet_rbx_loader::ReadError),
#[cfg(feature="source")]
Source(strafesnet_bsp_loader::ReadError),
#[cfg(feature="snf")]
StrafesNET(strafesnet_snf::Error), StrafesNET(strafesnet_snf::Error),
#[cfg(feature="snf")]
StrafesNETMap(strafesnet_snf::map::Error), StrafesNETMap(strafesnet_snf::map::Error),
#[cfg(feature="snf")] RobloxBot(strafesnet_roblox_bot_file::v0::Error),
StrafesNETBot(strafesnet_snf::bot::Error),
Io(std::io::Error), Io(std::io::Error),
UnknownFileFormat, UnknownFileFormat,
} }
@@ -27,14 +17,8 @@ impl std::fmt::Display for ReadError{
impl std::error::Error for ReadError{} impl std::error::Error for ReadError{}
pub enum ReadFormat{ pub enum ReadFormat{
#[cfg(feature="roblox")]
Roblox(strafesnet_rbx_loader::Model),
#[cfg(feature="source")]
Source(strafesnet_bsp_loader::Bsp),
#[cfg(feature="snf")]
SNFM(strafesnet_common::map::CompleteMap), SNFM(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")] QBOT(strafesnet_roblox_bot_file::v0::Block),
SNFB(strafesnet_snf::bot::Segment),
} }
pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
@@ -45,19 +29,12 @@ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?; buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?;
let cursor=std::io::Cursor::new(entire_file); let cursor=std::io::Cursor::new(entire_file);
match peek.as_slice(){ match peek.as_slice(){
#[cfg(feature="roblox")]
b"<rob"=>Ok(ReadFormat::Roblox(strafesnet_rbx_loader::read(cursor).map_err(ReadError::Roblox)?)),
#[cfg(feature="source")]
b"VBSP"=>Ok(ReadFormat::Source(strafesnet_bsp_loader::read(cursor).map_err(ReadError::Source)?)),
#[cfg(feature="snf")]
b"SNFM"=>Ok(ReadFormat::SNFM( b"SNFM"=>Ok(ReadFormat::SNFM(
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)? strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
.into_complete_map().map_err(ReadError::StrafesNETMap)? .into_complete_map().map_err(ReadError::StrafesNETMap)?
)), )),
#[cfg(feature="snf")] b"qbot"=>Ok(ReadFormat::QBOT(
b"SNFB"=>Ok(ReadFormat::SNFB( strafesnet_roblox_bot_file::v0::read_all_to_block(cursor).map_err(ReadError::RobloxBot)?
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
.read_all().map_err(ReadError::StrafesNETBot)?
)), )),
_=>Err(ReadError::UnknownFileFormat), _=>Err(ReadError::UnknownFileFormat),
} }
@@ -68,10 +45,6 @@ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
pub enum LoadError{ pub enum LoadError{
ReadError(ReadError), ReadError(ReadError),
File(std::io::Error), File(std::io::Error),
#[cfg(feature="roblox")]
LoadRoblox(strafesnet_rbx_loader::LoadError),
#[cfg(feature="source")]
LoadSource(strafesnet_bsp_loader::LoadError),
} }
impl std::fmt::Display for LoadError{ impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@@ -81,36 +54,15 @@ impl std::fmt::Display for LoadError{
impl std::error::Error for LoadError{} impl std::error::Error for LoadError{}
pub enum LoadFormat{ pub enum LoadFormat{
#[cfg(any(feature="snf",feature="roblox",feature="source"))]
Map(strafesnet_common::map::CompleteMap), Map(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")] Bot(strafesnet_roblox_bot_file::v0::Block),
Bot(strafesnet_snf::bot::Segment),
} }
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
//blocking because it's simpler... //blocking because it's simpler...
let file=std::fs::File::open(path).map_err(LoadError::File)?; let file=std::fs::File::open(path).map_err(LoadError::File)?;
match read(file).map_err(LoadError::ReadError)?{ match read(file).map_err(LoadError::ReadError)?{
#[cfg(feature="snf")] ReadFormat::QBOT(bot)=>Ok(LoadFormat::Bot(bot)),
ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
#[cfg(feature="snf")]
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)), ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")]
ReadFormat::Roblox(model)=>{
let mut place=strafesnet_rbx_loader::Place::from(model);
let script_errors=place.run_scripts().unwrap();
for error in script_errors{
println!("Script error: {error}");
}
let (map,errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?;
if errors.count()!=0{
print!("Errors encountered while loading the map:\n{}",errors);
}
Ok(LoadFormat::Map(map))
},
#[cfg(feature="source")]
ReadFormat::Source(bsp)=>Ok(LoadFormat::Map(
bsp.to_snf(LoadFailureMode::DefaultToNone,&[]).map_err(LoadError::LoadSource)?
)),
} }
} }

View File

@@ -17,25 +17,26 @@ pub enum Instruction{
SessionPlayback(SessionPlaybackInstruction), SessionPlayback(SessionPlaybackInstruction),
Render, Render,
Resize(winit::dpi::PhysicalSize<u32>), Resize(winit::dpi::PhysicalSize<u32>),
ChangeMap(strafesnet_common::map::CompleteMap),
LoadReplay(strafesnet_roblox_bot_file::v0::Block),
} }
pub struct PlayerWorker<'a>{ pub struct PlayerWorker<'a>{
surface:wgpu::Surface<'a>, surface:wgpu::Surface<'a>,
graphics_thread:Graphics, graphics_thread:Graphics,
bot:CompleteBot, bot:Option<CompleteBot>,
playback_head:PlaybackHead, playback_head:PlaybackHead,
} }
impl<'a> PlayerWorker<'a>{ impl<'a> PlayerWorker<'a>{
pub fn new( pub fn new(
surface:wgpu::Surface<'a>, surface:wgpu::Surface<'a>,
bot:CompleteBot,
graphics_thread:Graphics, graphics_thread:Graphics,
)->Self{ )->Self{
let playback_head=PlaybackHead::new(SessionTime::ZERO); let playback_head=PlaybackHead::new(SessionTime::ZERO);
Self{ Self{
surface, surface,
graphics_thread, graphics_thread,
bot, bot:None,
playback_head, playback_head,
} }
} }
@@ -43,14 +44,20 @@ impl<'a> PlayerWorker<'a>{
match ins.instruction{ match ins.instruction{
Instruction::SessionControl(session_control_instruction)=>{}, Instruction::SessionControl(session_control_instruction)=>{},
Instruction::SessionPlayback(session_playback_instruction)=>{}, Instruction::SessionPlayback(session_playback_instruction)=>{},
Instruction::Render=>{ Instruction::Render=>if let Some(bot)=&self.bot{
self.playback_head.advance_time(&self.bot,ins.time); self.playback_head.advance_time(bot,ins.time);
let (pos,angles)=self.playback_head.get_position_angles(&self.bot,ins.time); let (pos,angles)=self.playback_head.get_position_angles(bot,ins.time);
self.graphics_thread.render(&self.surface,pos,angles); self.graphics_thread.render(&self.surface,pos,angles);
}, },
Instruction::Resize(physical_size)=>{ Instruction::Resize(physical_size)=>{
self.graphics_thread.resize(&self.surface,glam::uvec2(physical_size.width,physical_size.height)); self.graphics_thread.resize(&self.surface,glam::uvec2(physical_size.width,physical_size.height));
}, },
Instruction::ChangeMap(complete_map)=>{
self.graphics_thread.change_map(&complete_map);
},
Instruction::LoadReplay(bot)=>{
self.bot=Some(CompleteBot::new(bot));
},
} }
} }
} }

View File

@@ -22,8 +22,8 @@ impl WindowContext<'_>{
match event{ match event{
winit::event::WindowEvent::DroppedFile(path)=>{ winit::event::WindowEvent::DroppedFile(path)=>{
match crate::file::load(path.as_path()){ match crate::file::load(path.as_path()){
// Ok(LoadFormat::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}), Ok(LoadFormat::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}),
// Ok(LoadFormat::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}), Ok(LoadFormat::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}),
Err(e)=>println!("Failed to load file: {e}"), Err(e)=>println!("Failed to load file: {e}"),
} }
}, },
@@ -149,12 +149,7 @@ impl WindowContext<'_>{
config:wgpu::SurfaceConfiguration, config:wgpu::SurfaceConfiguration,
)->WindowContext<'a>{ )->WindowContext<'a>{
let screen_size=glam::uvec2(config.width,config.height); let screen_size=glam::uvec2(config.width,config.height);
let bot=include_bytes!("../../web-demo/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d.qbot"); let graphics=strafesnet_roblox_bot_player::graphics::Graphics::new(device,queue,config);
let map=include_bytes!("../../web-demo/bhop_marble_5692093612.snfm");
let bot=strafesnet_roblox_bot_player::bot::CompleteBot::new(bot).unwrap();
let map=strafesnet_roblox_bot_player::map::CompleteMap::new(map).unwrap();
let mut graphics=strafesnet_roblox_bot_player::graphics::Graphics::new(device,queue,config);
graphics.change_map(&map);
WindowContext{ WindowContext{
simulation_paused:false, simulation_paused:false,
//make sure to update this!!!!! //make sure to update this!!!!!
@@ -162,7 +157,6 @@ impl WindowContext<'_>{
window, window,
physics_thread:crate::player::PlayerWorker::new( physics_thread:crate::player::PlayerWorker::new(
surface, surface,
bot,
graphics, graphics,
), ),
} }

View File

@@ -11,6 +11,7 @@ strafesnet_roblox_bot_player = { version = "0.1.0", path = "../lib" }
strafesnet_common.workspace = true strafesnet_common.workspace = true
strafesnet_graphics.workspace = true strafesnet_graphics.workspace = true
strafesnet_roblox_bot_file.workspace = true strafesnet_roblox_bot_file.workspace = true
strafesnet_snf.workspace = true
wasm-bindgen = "0.2.108" wasm-bindgen = "0.2.108"
wasm-bindgen-futures = "0.4.58" wasm-bindgen-futures = "0.4.58"
web-sys = { version = "0.3.85", features = ["HtmlCanvasElement"] } web-sys = { version = "0.3.85", features = ["HtmlCanvasElement"] }

View File

@@ -1,7 +1,7 @@
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use strafesnet_roblox_bot_file::v0; use strafesnet_roblox_bot_file::v0;
use strafesnet_roblox_bot_player::{bot,map,head,time,graphics}; use strafesnet_roblox_bot_player::{bot,head,time,graphics};
use strafesnet_graphics::setup; use strafesnet_graphics::setup;
use strafesnet_common::physics::Time as PhysicsTime; use strafesnet_common::physics::Time as PhysicsTime;
@@ -65,8 +65,9 @@ pub struct CompleteBot{
impl CompleteBot{ impl CompleteBot{
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(data:&[u8])->Result<Self,JsValue>{ pub fn new(data:&[u8])->Result<Self,JsValue>{
let timelines=v0::read_all_to_block(std::io::Cursor::new(data)).map_err(|e|JsValue::from_str(&e.to_string()))?;
Ok(Self{ Ok(Self{
bot:bot::CompleteBot::new(data).map_err(|e|JsValue::from_str(&e.to_string()))?, bot:bot::CompleteBot::new(timelines),
}) })
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -82,14 +83,18 @@ impl CompleteBot{
#[wasm_bindgen] #[wasm_bindgen]
pub struct CompleteMap{ pub struct CompleteMap{
map:map::CompleteMap, map:strafesnet_common::map::CompleteMap,
} }
#[wasm_bindgen] #[wasm_bindgen]
impl CompleteMap{ impl CompleteMap{
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(data:&[u8])->Result<Self,JsValue>{ pub fn new(data:&[u8])->Result<Self,JsValue>{
let map=strafesnet_snf::read_map(std::io::Cursor::new(data))
.map_err(|e|JsValue::from_str(&e.to_string()))?
.into_complete_map()
.map_err(|e|JsValue::from_str(&e.to_string()))?;
Ok(Self{ Ok(Self{
map:map::CompleteMap::new(data).map_err(|e|JsValue::from_str(&e.to_string()))?, map,
}) })
} }
} }