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_graphics",
"strafesnet_roblox_bot_file",
"strafesnet_snf",
"wgpu",
]
@@ -1705,6 +1704,7 @@ dependencies = [
"strafesnet_graphics",
"strafesnet_roblox_bot_file",
"strafesnet_roblox_bot_player",
"strafesnet_snf",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",

View File

@@ -13,5 +13,6 @@ python3 -m http.server
How to run the 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_graphics.workspace = true
strafesnet_roblox_bot_file.workspace = true
strafesnet_snf.workspace = true
wgpu = "28.0.0"

View File

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

View File

@@ -18,9 +18,9 @@ impl Graphics{
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.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){
self.config.width=size.x.max(1);

View File

@@ -1,5 +1,4 @@
pub mod bot;
pub mod map;
pub mod head;
pub mod time;
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;
#[cfg(any(feature="roblox",feature="source"))]
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
#[expect(dead_code)]
#[derive(Debug)]
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),
#[cfg(feature="snf")]
StrafesNETMap(strafesnet_snf::map::Error),
#[cfg(feature="snf")]
StrafesNETBot(strafesnet_snf::bot::Error),
RobloxBot(strafesnet_roblox_bot_file::v0::Error),
Io(std::io::Error),
UnknownFileFormat,
}
@@ -27,14 +17,8 @@ impl std::fmt::Display for ReadError{
impl std::error::Error for ReadError{}
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),
#[cfg(feature="snf")]
SNFB(strafesnet_snf::bot::Segment),
QBOT(strafesnet_roblox_bot_file::v0::Block),
}
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)?;
let cursor=std::io::Cursor::new(entire_file);
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(
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
.into_complete_map().map_err(ReadError::StrafesNETMap)?
)),
#[cfg(feature="snf")]
b"SNFB"=>Ok(ReadFormat::SNFB(
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
.read_all().map_err(ReadError::StrafesNETBot)?
b"qbot"=>Ok(ReadFormat::QBOT(
strafesnet_roblox_bot_file::v0::read_all_to_block(cursor).map_err(ReadError::RobloxBot)?
)),
_=>Err(ReadError::UnknownFileFormat),
}
@@ -68,10 +45,6 @@ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
pub enum LoadError{
ReadError(ReadError),
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{
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{}
pub enum LoadFormat{
#[cfg(any(feature="snf",feature="roblox",feature="source"))]
Map(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")]
Bot(strafesnet_snf::bot::Segment),
Bot(strafesnet_roblox_bot_file::v0::Block),
}
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,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")]
ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
#[cfg(feature="snf")]
ReadFormat::QBOT(bot)=>Ok(LoadFormat::Bot(bot)),
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),
Render,
Resize(winit::dpi::PhysicalSize<u32>),
ChangeMap(strafesnet_common::map::CompleteMap),
LoadReplay(strafesnet_roblox_bot_file::v0::Block),
}
pub struct PlayerWorker<'a>{
surface:wgpu::Surface<'a>,
graphics_thread:Graphics,
bot:CompleteBot,
bot:Option<CompleteBot>,
playback_head:PlaybackHead,
}
impl<'a> PlayerWorker<'a>{
pub fn new(
surface:wgpu::Surface<'a>,
bot:CompleteBot,
graphics_thread:Graphics,
)->Self{
let playback_head=PlaybackHead::new(SessionTime::ZERO);
Self{
surface,
graphics_thread,
bot,
bot:None,
playback_head,
}
}
@@ -43,14 +44,20 @@ impl<'a> PlayerWorker<'a>{
match ins.instruction{
Instruction::SessionControl(session_control_instruction)=>{},
Instruction::SessionPlayback(session_playback_instruction)=>{},
Instruction::Render=>{
self.playback_head.advance_time(&self.bot,ins.time);
let (pos,angles)=self.playback_head.get_position_angles(&self.bot,ins.time);
Instruction::Render=>if let Some(bot)=&self.bot{
self.playback_head.advance_time(bot,ins.time);
let (pos,angles)=self.playback_head.get_position_angles(bot,ins.time);
self.graphics_thread.render(&self.surface,pos,angles);
},
Instruction::Resize(physical_size)=>{
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{
winit::event::WindowEvent::DroppedFile(path)=>{
match crate::file::load(path.as_path()){
// 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::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)}),
Err(e)=>println!("Failed to load file: {e}"),
}
},
@@ -149,12 +149,7 @@ impl WindowContext<'_>{
config:wgpu::SurfaceConfiguration,
)->WindowContext<'a>{
let screen_size=glam::uvec2(config.width,config.height);
let bot=include_bytes!("../../web-demo/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d.qbot");
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);
let graphics=strafesnet_roblox_bot_player::graphics::Graphics::new(device,queue,config);
WindowContext{
simulation_paused:false,
//make sure to update this!!!!!
@@ -162,7 +157,6 @@ impl WindowContext<'_>{
window,
physics_thread:crate::player::PlayerWorker::new(
surface,
bot,
graphics,
),
}

View File

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

View File

@@ -1,7 +1,7 @@
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
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_common::physics::Time as PhysicsTime;
@@ -65,8 +65,9 @@ pub struct CompleteBot{
impl CompleteBot{
#[wasm_bindgen(constructor)]
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{
bot:bot::CompleteBot::new(data).map_err(|e|JsValue::from_str(&e.to_string()))?,
bot:bot::CompleteBot::new(timelines),
})
}
#[wasm_bindgen]
@@ -82,14 +83,18 @@ impl CompleteBot{
#[wasm_bindgen]
pub struct CompleteMap{
map:map::CompleteMap,
map:strafesnet_common::map::CompleteMap,
}
#[wasm_bindgen]
impl CompleteMap{
#[wasm_bindgen(constructor)]
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{
map:map::CompleteMap::new(data).map_err(|e|JsValue::from_str(&e.to_string()))?,
map,
})
}
}