17 Commits

11 changed files with 240 additions and 174 deletions

8
Cargo.lock generated
View File

@@ -1632,9 +1632,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strafesnet_common"
version = "0.8.1"
version = "0.8.5"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "cba84ffce9c31b70b5dcaab0b9e946fe34ca4d6613a4c83980745b47dfc86edf"
checksum = "28b5e3829a22fa11cbf7bde7ff248cc3562f80d571907c2957f9cba213e29fe5"
dependencies = [
"arrayvec",
"bitflags 2.10.0",
@@ -1661,9 +1661,9 @@ dependencies = [
[[package]]
name = "strafesnet_roblox_bot_file"
version = "0.9.0"
version = "0.9.3"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "3ee6a8c592794145fdbbb2fbdaedbbc5813373e2e2bbbb9bac6ab7944775cc0a"
checksum = "423d931e4f4f97406a86519a22172d1fc0d5b9d8c3b2d4553ae89b641bbd555c"
dependencies = [
"binrw",
"bitflags 2.10.0",

View File

@@ -12,7 +12,7 @@ strip = true
codegen-units = 1
[workspace.dependencies]
strafesnet_common = { version = "0.8.1", registry = "strafesnet" }
strafesnet_common = { version = "0.8.5", registry = "strafesnet" }
strafesnet_graphics = { version = "0.0.2", registry = "strafesnet" }
strafesnet_roblox_bot_file = { version = "0.9.0", registry = "strafesnet" }
strafesnet_roblox_bot_file = { version = "0.9.3", registry = "strafesnet" }
strafesnet_snf = { version = "0.3.2", registry = "strafesnet" }

View File

@@ -1,23 +1,29 @@
How to clone this repository:
### How to clone this repository:
- Install git
- Install git lfs (for test files)
```
git clone https://git.itzana.me/StrafesNET/roblox-bot-player
cd roblox-bot-player
git lfs pull
```
How to build the wasm module:
### How to build the wasm module:
- Install rust
- Install wasm-pack
```
cd wasm-module
wasm-pack build --target web --out-dir ../web-demo/pkg
```
How to serve the web demo (requires wasm module):
### How to serve the web demo (requires wasm module):
- Install python3 or use your favourite http server
```
cd web-demo
python3 -m http.server
```
How to run the native player:
### How to run the native player:
- Install rust
```
cd native-player
cargo run --release -- ../web-demo/bhop_marble_5692093612.snfm ../web-demo/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d.qbot

View File

@@ -1,11 +1,15 @@
use strafesnet_common::timer::{TimerFixed,Realtime,Unpaused};
use strafesnet_common::run::{Time as RunTime};
use strafesnet_common::physics::{Time as PhysicsTime,TimeInner as PhysicsTimeInner};
use strafesnet_roblox_bot_file::v0;
use strafesnet_common::physics::{Time as PhysicsTime};
use crate::head::{Time as PlaybackTime,TimeInner as PlaybackTimeInner};
/// A loaded bot file.
pub struct CompleteBot{
//Instructions
timelines:v0::Block,
time_base:PhysicsTime,
timer:TimerFixed<Realtime<PlaybackTimeInner,PhysicsTimeInner>,Unpaused>,
duration:PhysicsTime,
}
impl CompleteBot{
@@ -13,16 +17,16 @@ impl CompleteBot{
pub fn new(
timelines:v0::Block,
)->Self{
let first=timelines.output_events.first().unwrap();
let last=timelines.output_events.last().unwrap();
let start=crate::time::from_float(timelines.output_events.first().unwrap().time).unwrap();
let end=crate::time::from_float(timelines.output_events.last().unwrap().time).unwrap();
Self{
time_base:crate::time::from_float(first.time).unwrap(),
duration:crate::time::from_float(last.time-first.time).unwrap(),
timer:TimerFixed::new(PlaybackTime::ZERO,start),
duration:end-start,
timelines,
}
}
pub const fn time_base(&self)->PhysicsTime{
self.time_base
pub fn time(&self,time:PlaybackTime)->PhysicsTime{
self.timer.time(time)
}
pub const fn duration(&self)->PhysicsTime{
self.duration
@@ -30,7 +34,7 @@ impl CompleteBot{
pub const fn timelines(&self)->&v0::Block{
&self.timelines
}
pub fn run_duration(&self,mode_id:v0::ModeID)->Option<PhysicsTime>{
pub fn run_duration(&self,mode_id:v0::ModeID)->Option<RunTime>{
let mut it=self.timelines.run_events.iter().rev();
let end=it.find_map(|event|match &event.event{
v0::RunEvent::Finish(run_start_event) if run_start_event.mode==mode_id=>Some(event.time),

View File

@@ -9,8 +9,7 @@ pub struct Graphics{
}
impl Graphics{
pub fn new(device:wgpu::Device,queue:wgpu::Queue,config:wgpu::SurfaceConfiguration)->Self{
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&device,&queue,&config);
graphics.resize(&device,&config,glam::Vec2::ONE);
let graphics=strafesnet_graphics::graphics::GraphicsState::new(&device,&queue,&config);
Self{
graphics,
device,
@@ -22,11 +21,11 @@ impl Graphics{
self.graphics.clear();
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,fov:glam::Vec2){
self.config.width=size.x.max(1);
self.config.height=size.y.max(1);
surface.configure(&self.device,&self.config);
self.graphics.resize(&self.device,&self.config,glam::Vec2::ONE);
self.graphics.resize(&self.device,&self.config,fov);
}
pub fn render(&mut self,surface:&wgpu::Surface<'_>,pos:glam::Vec3,angles:glam::Vec2){
//this has to go deeper somehow

View File

@@ -1,7 +1,6 @@
use glam::Vec3Swizzles;
use strafesnet_common::timer::{Scaled,Timer,TimerState};
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::physics::{Time as PhysicsTime,TimeInner as PhysicsTimeInner};
use strafesnet_roblox_bot_file::v0::{EventType,Head,Timed};
use crate::bot::CompleteBot;
@@ -11,71 +10,65 @@ fn vector3_to_glam(v:&strafesnet_roblox_bot_file::v0::Vector3)->glam::Vec3{
glam::vec3(v.x,v.y,v.z)
}
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=strafesnet_common::integer::Time<TimeInner>;
/// A playback context. Advance time and then generate a camera position to pass to the renderer.
pub struct PlaybackHead{
head:Head,
loop_offset:PhysicsTime,
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
timer:Timer<Scaled<SessionTimeInner,TimeInner>>,
state:PlaybackState,
}
const HEAD_NO_CRASH:Head={
let mut head=Head::new();
// push one output event so that output-1 doesn't underflow
head.push(EventType::Output);
head
};
impl PlaybackHead{
pub fn new(time:SessionTime)->Self{
let timer=Timer::unpaused(time,PhysicsTime::ZERO);
pub fn new(bot:&CompleteBot,time:SessionTime)->Self{
let timer=Timer::unpaused(time,Time::ZERO);
let head=Head::after_time(bot.timelines(),bot.time(Time::ZERO).into());
let mut state=PlaybackState::new();
state.process_head(bot.timelines(),&head);
Self{
head:HEAD_NO_CRASH,
loop_offset:PhysicsTime::ZERO,
head,
timer,
state:PlaybackState::new(),
state,
}
}
pub const fn state(&self)->&PlaybackState{
&self.state
}
pub fn time(&self,bot:&CompleteBot,time:SessionTime)->PhysicsTime{
bot.time_base()+self.timer.time(time)+self.loop_offset
pub fn time(&self,time:SessionTime)->Time{
self.timer.time(time)
}
pub fn timer(&self)->&Timer<Scaled<SessionTimeInner,TimeInner>>{
&self.timer
}
pub fn set_paused(&mut self,time:SessionTime,paused:bool){
_=self.timer.set_paused(time,paused);
}
pub fn set_time(&mut self,bot:&CompleteBot,time:SessionTime,new_time:Time){
let new_time=new_time.rem_euclid(bot.duration().coerce());
self.timer.set_time(time,new_time);
// reset head
self.head=Head::after_time(bot.timelines(),bot.time(new_time).into());
self.state=PlaybackState::new();
self.state.process_head(bot.timelines(),&self.head);
}
pub fn get_scale(&self)->strafesnet_common::integer::Ratio64{
self.timer.get_scale()
}
pub fn set_scale(&mut self,time:SessionTime,new_scale:strafesnet_common::integer::Ratio64){
self.timer.set_scale(time,new_scale);
}
pub fn next_event(&self,bot:&CompleteBot)->Option<Timed<EventType>>{
self.head.next_event(bot.timelines())
}
pub fn process_event(&mut self,bot:&CompleteBot,event_type:EventType){
self.state.process_event(bot,event_type,self.head.get_event_index(event_type));
self.state.process_event(bot.timelines(),event_type,self.head.get_event_index(event_type));
self.head.push(event_type);
}
pub fn set_paused(&mut self,time:SessionTime,paused:bool){
_=self.timer.set_paused(time,paused);
}
pub fn seek_to(&mut self,time:SessionTime,new_time:PhysicsTime){
self.timer.set_time(time,new_time);
self.loop_offset=PhysicsTime::ZERO;
// reset head
self.head=HEAD_NO_CRASH;
}
pub fn seek_backward(&mut self,time_offset:SessionTime){
let (mut state,paused)=self.timer.clone().into_state();
let offset=state.get_time(-time_offset).coerce();
state.set_offset(offset);
self.timer=Timer::from_state(state,paused);
// reset head
self.head=HEAD_NO_CRASH;
}
pub fn seek_forward(&mut self,time_offset:SessionTime){
let (mut state,paused)=self.timer.clone().into_state();
let offset=state.get_time(time_offset).coerce();
state.set_offset(offset);
self.timer=Timer::from_state(state,paused);
}
pub fn set_scale(&mut self,time:SessionTime,new_scale:strafesnet_common::integer::Ratio64){
self.timer.set_scale(time,new_scale);
}
pub fn advance_time(&mut self,bot:&CompleteBot,time:SessionTime){
let mut simulation_time=self.time(bot,time);
let mut time_float=simulation_time.get() as f64/PhysicsTime::ONE_SECOND.get() as f64;
let mut simulation_time=bot.time(self.time(time));
let mut time_float=simulation_time.into();
loop{
match self.next_event(bot){
Some(next_event)=>{
@@ -87,16 +80,25 @@ impl PlaybackHead{
},
None=>{
//reset playback
self.head=HEAD_NO_CRASH;
self.loop_offset-=bot.duration();
self.head=Head::after_time(bot.timelines(),bot.time(Time::ZERO).into());
self.state=PlaybackState::new();
self.state.process_head(bot.timelines(),&self.head);
// hack to wind back timer offset without precise session timestamp
let (mut state,paused)=self.timer.clone().into_state();
let offset=state.get_offset()-bot.duration().coerce();
state.set_offset(offset);
self.timer=Timer::from_state(state,paused);
// update loop variables
simulation_time-=bot.duration();
time_float=simulation_time.get() as f64/PhysicsTime::ONE_SECOND.get() as f64;
time_float=simulation_time.into();
},
}
}
}
pub fn get_position_angles(&self,bot:&CompleteBot,time:SessionTime)->(glam::Vec3,glam::Vec2){
let time=self.time(bot,time);
let time=bot.time(self.time(time));
let event0=&bot.timelines().output_events[self.head.get_event_index(EventType::Output)-1];
let event1=&bot.timelines().output_events[self.head.get_event_index(EventType::Output)];
let p0=vector3_to_glam(&event0.event.position);
@@ -107,7 +109,7 @@ impl PlaybackHead{
// let a1=vector3_to_glam(&event1.event.acceleration);
let t0=event0.time;
let t1=event1.time;
let time_float=time.get() as f64/PhysicsTime::ONE_SECOND.get() as f64;
let time_float:f64=time.into();
let t=((time_float-t0)/(t1-t0)) as f32;
let p=p0.lerp(p1,t);
// let v=v0.lerp(v1,t);

View File

@@ -4,8 +4,6 @@ use strafesnet_common::run;
use strafesnet_common::physics::Time as PhysicsTime;
use strafesnet_roblox_bot_file::v0;
use crate::bot::CompleteBot;
pub struct Run{
run:run::RunState,
flag_reason:Option<v0::FlagReason>,
@@ -84,9 +82,9 @@ impl PlaybackState{
style:v0::Style::Autohop,
absolute_sensitivity_enabled:false,
fov_y:1.0,
sens_x:1.0,
sens_x:0.3,
vertical_sensitivity_multipler:1.0,
turn_speed:1.0,
turn_speed:core::f64::consts::TAU/0.715588,
}
}
pub fn get_run(&self,mode:v0::ModeID)->Option<&Run>{
@@ -111,13 +109,13 @@ impl PlaybackState{
self.style=run_event_prepare.style;
},
v0::RunEvent::Start(run_event_zone)=>{
let time=PhysicsTime::raw((event.time*PhysicsTime::ONE_SECOND.get() as f64) as i64);
let time=crate::time::from_float(event.time).unwrap();
if let Some(run)=self.runs.get_mut(&run_event_zone.mode){
_=run.run.start(time);
}
},
v0::RunEvent::Finish(run_event_zone)=>{
let time=PhysicsTime::raw((event.time*PhysicsTime::ONE_SECOND.get() as f64) as i64);
let time=crate::time::from_float(event.time).unwrap();
if let Some(run)=self.runs.get_mut(&run_event_zone.mode){
_=run.run.finish(time);
}
@@ -179,7 +177,7 @@ impl PlaybackState{
fn push_setting(&mut self,event:&v0::SettingEvent){
match event{
v0::SettingEvent::FieldOfView(setting_event_field_of_view)=>{
self.fov_y=setting_event_field_of_view.fov;
self.fov_y=(setting_event_field_of_view.fov*0.5).to_radians().tan();
},
v0::SettingEvent::Sensitivity(setting_event_sensitivity)=>{
self.sens_x=setting_event_sensitivity.sensitivity;
@@ -195,16 +193,57 @@ impl PlaybackState{
},
}
}
pub(crate) fn process_event(&mut self,bot:&CompleteBot,event_type:v0::EventType,event_index:usize){
pub(crate) fn process_head(&mut self,block:&v0::Block,head:&v0::Head){
// The whole point of this is to avoid running the realtime events!
/*
for event in &block.input_events[0..head.get_event_index(v0::EventType::Input)]{
self.push_input(&event.event);
}
for event in &block.output_events[0..head.get_event_index(v0::EventType::Output)]{
self.push_output(&event.event);
}
for event in &bot.sound_events[0..head.get_event_index(v0::EventType::Sound)]{
self.push_sound(&event.event);
}
*/
// for event in &bot.world_events[0..head.get_event_index(v0::EventType::World)]{
// self.push_world(&event.event);
// }
for event in &block.gravity_events[0..head.get_event_index(v0::EventType::Gravity)]{
self.push_gravity(&event.event);
}
for event in &block.run_events[0..head.get_event_index(v0::EventType::Run)]{
self.push_run(event);
}
// for event in &bot.camera_events[0..head.get_event_index(v0::EventType::Camera)]{
// self.push_camera(&event.event);
// }
for event in &block.setting_events[0..head.get_event_index(v0::EventType::Setting)]{
self.push_setting(&event.event);
}
}
pub(crate) fn process_event(&mut self,block:&v0::Block,event_type:v0::EventType,event_index:usize){
match event_type{
v0::EventType::Input=>self.push_input(&bot.timelines().input_events[event_index].event),
v0::EventType::Output=>self.push_output(&bot.timelines().output_events[event_index].event),
v0::EventType::Input=>self.push_input(&block.input_events[event_index].event),
v0::EventType::Output=>self.push_output(&block.output_events[event_index].event),
v0::EventType::Sound=>{},
v0::EventType::World=>{},
v0::EventType::Gravity=>self.push_gravity(&bot.timelines().gravity_events[event_index].event),
v0::EventType::Run=>self.push_run(&bot.timelines().run_events[event_index]),
v0::EventType::Gravity=>self.push_gravity(&block.gravity_events[event_index].event),
v0::EventType::Run=>self.push_run(&block.run_events[event_index]),
v0::EventType::Camera=>{},
v0::EventType::Setting=>self.push_setting(&bot.timelines().setting_events[event_index].event),
v0::EventType::Setting=>self.push_setting(&block.setting_events[event_index].event),
}
}
pub fn get_fov_y(&self)->f64{
let zoom_enabled=self.game_controls.contains(v0::GameControls::Zoom);
if zoom_enabled{self.fov_y*0.2}else{self.fov_y}
}
pub fn get_sensitivity(&self)->(f64,f64){
if self.absolute_sensitivity_enabled{
(self.sens_x,self.sens_x*self.vertical_sensitivity_multipler)
}else{
let sens_x=self.sens_x*self.get_fov_y()/128.0;
(sens_x,sens_x*self.vertical_sensitivity_multipler)
}
}
}

View File

@@ -1,7 +1,7 @@
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::Time as SessionTime;
use strafesnet_common::physics::Time as PhysicsTime;
use strafesnet_roblox_bot_player::{bot::CompleteBot,graphics::Graphics,head::PlaybackHead};
use strafesnet_common::timer::TimerState;
use strafesnet_roblox_bot_player::{bot::CompleteBot,graphics::Graphics,head::{PlaybackHead,Time as PlaybackTime}};
pub enum SessionControlInstruction{
SetPaused(bool),
@@ -28,62 +28,73 @@ fn speed_ratio(speed:i8)->strafesnet_common::integer::Ratio64{
}
}
struct Playback{
bot:CompleteBot,
playback_head:PlaybackHead,
playback_speed:i8,
}
pub struct PlayerWorker<'a>{
surface:wgpu::Surface<'a>,
graphics_thread:Graphics,
bot:Option<CompleteBot>,
playback_head:PlaybackHead,
playback_speed:i8,
playback:Option<Playback>,
}
impl<'a> PlayerWorker<'a>{
pub fn new(
surface:wgpu::Surface<'a>,
graphics_thread:Graphics,
)->Self{
let playback_head=PlaybackHead::new(SessionTime::ZERO);
Self{
surface,
graphics_thread,
bot:None,
playback_head,
playback_speed:0,
playback:None,
}
}
pub fn send(&mut self,ins:TimedInstruction<Instruction,SessionTime>){
match ins.instruction{
Instruction::SessionControl(SessionControlInstruction::SetPaused(paused))=>{
self.playback_head.set_paused(ins.time,paused);
Instruction::SessionControl(SessionControlInstruction::SetPaused(paused))=>if let Some(playback)=&mut self.playback{
playback.playback_head.set_paused(ins.time,paused);
},
Instruction::SessionControl(SessionControlInstruction::Restart)=>{
self.playback_head.seek_to(ins.time,PhysicsTime::ZERO);
Instruction::SessionControl(SessionControlInstruction::Restart)=>if let Some(playback)=&mut self.playback{
playback.playback_head.set_time(&playback.bot,ins.time,PlaybackTime::ZERO);
},
Instruction::SessionControl(SessionControlInstruction::SkipForward)=>{
self.playback_head.seek_forward(SessionTime::from_secs(2));
Instruction::SessionControl(SessionControlInstruction::SkipForward)=>if let Some(playback)=&mut self.playback{
let head_time=playback.playback_head.timer().clone().into_state().0.get_time(ins.time+SessionTime::from_secs(2));
playback.playback_head.set_time(&playback.bot,ins.time,head_time);
},
Instruction::SessionControl(SessionControlInstruction::SkipBack)=>{
self.playback_head.seek_backward(SessionTime::from_secs(2));
Instruction::SessionControl(SessionControlInstruction::SkipBack)=>if let Some(playback)=&mut self.playback{
let head_time=playback.playback_head.timer().clone().into_state().0.get_time(ins.time-SessionTime::from_secs(2));
playback.playback_head.set_time(&playback.bot,ins.time,head_time);
},
Instruction::SessionControl(SessionControlInstruction::DecreaseTimescale)=>{
self.playback_speed=self.playback_speed.saturating_sub(1).max(-27);
self.playback_head.set_scale(ins.time,speed_ratio(self.playback_speed));
Instruction::SessionControl(SessionControlInstruction::DecreaseTimescale)=>if let Some(playback)=&mut self.playback{
playback.playback_speed=playback.playback_speed.saturating_sub(1).max(-27);
playback.playback_head.set_scale(ins.time,speed_ratio(playback.playback_speed));
},
Instruction::SessionControl(SessionControlInstruction::IncreaseTimescale)=>{
self.playback_speed=self.playback_speed.saturating_add(1).min(27);
self.playback_head.set_scale(ins.time,speed_ratio(self.playback_speed));
Instruction::SessionControl(SessionControlInstruction::IncreaseTimescale)=>if let Some(playback)=&mut self.playback{
playback.playback_speed=playback.playback_speed.saturating_add(1).min(27);
playback.playback_head.set_scale(ins.time,speed_ratio(playback.playback_speed));
},
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);
Instruction::Render=>if let Some(playback)=&mut self.playback{
playback.playback_head.advance_time(&playback.bot,ins.time);
let (pos,angles)=playback.playback_head.get_position_angles(&playback.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::Resize(physical_size)=>if let Some(playback)=&self.playback{
let fov_y=playback.playback_head.state().get_fov_y();
let fov_x=fov_y*physical_size.width as f64/physical_size.height as f64;
self.graphics_thread.resize(&self.surface,glam::uvec2(physical_size.width,physical_size.height),glam::vec2(fov_x as f32,fov_y as f32));
},
Instruction::ChangeMap(complete_map)=>{
self.graphics_thread.change_map(&complete_map);
},
Instruction::LoadReplay(bot)=>{
self.bot=Some(CompleteBot::new(bot));
let bot=CompleteBot::new(bot);
let playback_head=PlaybackHead::new(&bot,SessionTime::ZERO);
self.playback=Some(Playback{
bot,
playback_head,
playback_speed:0,
});
},
}
}

View File

@@ -15,7 +15,7 @@ strafesnet_snf.workspace = true
wasm-bindgen = "0.2.108"
wasm-bindgen-futures = "0.4.58"
web-sys = { version = "0.3.85", features = ["HtmlCanvasElement"] }
wgpu = "28.0.0"
wgpu = { version = "28.0.0", features = ["web"], default-features = false }
[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Oz", "--enable-bulk-memory","--enable-nontrapping-float-to-int"]

View File

@@ -3,7 +3,6 @@ use wasm_bindgen::JsValue;
use strafesnet_roblox_bot_file::v0;
use strafesnet_roblox_bot_player::{bot,head,time,graphics};
use strafesnet_graphics::setup;
use strafesnet_common::physics::Time as PhysicsTime;
// Hack to keep the code compiling,
// SurfaceTarget::Canvas is not available in IDE for whatever reason.
@@ -48,8 +47,8 @@ impl Graphics{
self.graphics.render(&self.surface,pos,angles);
}
#[wasm_bindgen]
pub fn resize(&mut self,width:u32,height:u32){
self.graphics.resize(&self.surface,[width,height].into());
pub fn resize(&mut self,width:u32,height:u32,fov_slope_x:f32,fov_slope_y:f32){
self.graphics.resize(&self.surface,[width,height].into(),[fov_slope_x as f32,fov_slope_y as f32].into());
}
#[wasm_bindgen]
pub fn change_map(&mut self,map:&CompleteMap){
@@ -72,12 +71,12 @@ impl CompleteBot{
}
#[wasm_bindgen]
pub fn duration(&self)->f64{
self.bot.duration().get() as f64/PhysicsTime::ONE_SECOND.get() as f64
self.bot.duration().into()
}
#[wasm_bindgen]
pub fn run_duration(&self,mode_id:u32)->Option<f64>{
let mode=v0::ModeID(mode_id);
Some(self.bot.run_duration(mode)?.get() as f64/PhysicsTime::ONE_SECOND.get() as f64)
Some(self.bot.run_duration(mode)?.into())
}
}
@@ -106,10 +105,10 @@ pub struct PlaybackHead{
#[wasm_bindgen]
impl PlaybackHead{
#[wasm_bindgen(constructor)]
pub fn new(time:f64)->Result<Self,JsValue>{
pub fn new(bot:&CompleteBot,time:f64)->Result<Self,JsValue>{
let time=time::from_float(time).unwrap();
Ok(Self{
head:head::PlaybackHead::new(time),
head:head::PlaybackHead::new(&bot.bot,time),
})
}
#[wasm_bindgen]
@@ -118,12 +117,40 @@ impl PlaybackHead{
self.head.advance_time(&bot.bot,time);
}
#[wasm_bindgen]
pub fn set_paused(&mut self,time:f64,paused:bool){
let time=time::from_float(time).unwrap();
self.head.set_paused(time,paused);
}
#[wasm_bindgen]
pub fn get_scale(&mut self)->f64{
let scale=self.head.get_scale();
scale.num() as f64/scale.den() as f64
}
#[wasm_bindgen]
pub fn set_scale(&mut self,time:f64,scale:f64){
let time=time::from_float(time).unwrap();
self.head.set_scale(time,scale.try_into().unwrap());
}
#[wasm_bindgen]
pub fn get_head_time(&self,time:f64)->f64{
let time=time::from_float(time).unwrap();
let time=self.head.time(time);
time.into()
}
/// Set the playback head position to new_time.
#[wasm_bindgen]
pub fn set_head_time(&mut self,bot:&CompleteBot,time:f64,new_time:f64){
let time=time::from_float(time).unwrap();
let new_time=time::from_float(new_time).unwrap();
self.head.set_time(&bot.bot,time,new_time);
}
#[wasm_bindgen]
pub fn get_run_time(&self,bot:&CompleteBot,time:f64,mode_id:u32)->Option<f64>{
let time=time::from_float(time).unwrap();
let time=self.head.time(&bot.bot,time);
let time=bot.bot.time(self.head.time(time));
let mode=v0::ModeID(mode_id);
let run_time=self.head.state().get_run(mode)?.time(time);
Some(run_time.get() as f64/strafesnet_common::run::Time::ONE_SECOND.get() as f64)
Some(run_time.into())
}
#[wasm_bindgen]
pub fn is_run_in_progress(&self,mode_id:u32)->Option<bool>{
@@ -136,29 +163,7 @@ impl PlaybackHead{
Some(self.head.state().get_run(mode)?.is_finished())
}
#[wasm_bindgen]
pub fn set_paused(&mut self,time:f64,paused:bool){
let time=time::from_float(time).unwrap();
self.head.set_paused(time,paused);
}
#[wasm_bindgen]
pub fn set_scale(&mut self,time:f64,scale_num:i64,scale_den:u64){
let time=time::from_float(time).unwrap();
self.head.set_scale(time,strafesnet_common::integer::Ratio64::new(scale_num,scale_den).unwrap());
}
#[wasm_bindgen]
pub fn seek_to(&mut self,time:f64,new_time:f64){
let time=time::from_float(time).unwrap();
let new_time=time::from_float(new_time).unwrap();
self.head.seek_to(time,new_time);
}
#[wasm_bindgen]
pub fn seek_forward(&mut self,time_offset:f64){
let time_offset=time::from_float(time_offset).unwrap();
self.head.seek_forward(time_offset);
}
#[wasm_bindgen]
pub fn seek_backward(&mut self,time_offset:f64){
let time_offset=time::from_float(time_offset).unwrap();
self.head.seek_backward(time_offset);
pub fn get_fov_slope_y(&self)->f64{
self.head.state().get_fov_y()
}
}

View File

@@ -16,7 +16,7 @@ const canvas = document.getElementById("viewport");
const graphics = await setup_graphics(canvas);
const bot = new CompleteBot(new Uint8Array(await b.arrayBuffer()));
const map = new CompleteMap(new Uint8Array(await m.arrayBuffer()));
const playback = new PlaybackHead(0);
const playback = new PlaybackHead(bot, 0);
graphics.change_map(map);
@@ -44,56 +44,54 @@ function elapsed() {
const control_speed = document.getElementById("control_speed");
var paused = false;
var speed = 0;
function set_speed(new_speed) {
speed = new_speed;
var speed_num = null;
var speed_den = null;
if (new_speed < 0) {
speed_num = BigInt(4) ** BigInt(-new_speed);
speed_den = BigInt(5) ** BigInt(-new_speed);
} else {
speed_num = BigInt(5) ** BigInt(new_speed);
speed_den = BigInt(4) ** BigInt(new_speed);
}
playback.set_scale(elapsed(), speed_num, speed_den);
control_speed.value = `${((5 / 4) ** new_speed).toPrecision(3)}x`;
var scale = 1;
function set_scale(new_scale) {
scale = new_scale;
playback.set_scale(elapsed(), scale);
control_speed.value = `${scale.toPrecision(3)}x`;
}
const SEEK_DURATION = 1.0;
// Controls
document.getElementById("control_reset").addEventListener("click", (e) => {
playback.seek_to(elapsed(), 0.0);
playback.set_head_time(bot, elapsed(), 0.0);
});
document.getElementById("control_pause").addEventListener("click", (e) => {
paused = !paused;
playback.set_paused(elapsed(), paused);
});
document.getElementById("control_forward").addEventListener("click", (e) => {
playback.seek_forward(2.0);
const time_now = elapsed();
const playback_time = playback.get_head_time(time_now);
const time_offset = playback.get_scale() * SEEK_DURATION;
playback.set_head_time(bot, time_now, playback_time + time_offset);
});
document.getElementById("control_backward").addEventListener("click", (e) => {
playback.seek_backward(2.0);
const time_now = elapsed();
const playback_time = playback.get_head_time(time_now);
const time_offset = playback.get_scale() * SEEK_DURATION;
playback.set_head_time(bot, time_now, playback_time - time_offset);
});
document.getElementById("control_slower").addEventListener("click", (e) => {
set_speed(Math.max(speed - 1, -27));
set_scale((scale * 4) / 5);
});
const regex = new RegExp("^([^x]*)x?$");
control_speed.addEventListener("change", (e) => {
const parsed = regex.exec(e.target.value);
if (!parsed) {
set_speed(0);
set_scale(1);
return;
}
const input = Number(parsed.at(1));
if (Number.isNaN(input)) {
set_speed(0);
set_scale(1);
return;
}
const rounded = Math.round(Math.log(input) / Math.log(5 / 4));
set_speed(Math.max(-27, Math.min(27, rounded)));
set_scale(input);
});
document.getElementById("control_faster").addEventListener("click", (e) => {
set_speed(Math.min(speed + 1, 27));
set_scale((scale * 5) / 4);
});
// Rendering
@@ -121,7 +119,9 @@ requestAnimationFrame(animate);
function resize() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
graphics.resize(canvas.width, canvas.height);
const fov_y = playback.get_fov_slope_y();
const fov_x = (fov_y * canvas.width) / canvas.height;
graphics.resize(canvas.width, canvas.height, fov_x, fov_y);
}
window.addEventListener("resize", resize);
resize();