diff --git a/Cargo.lock b/Cargo.lock index 4b3eee7..4be220e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1883,9 +1883,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strafesnet_common" -version = "0.8.6" +version = "0.8.7" source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" -checksum = "fb31424f16d189979d9f5781067ff29169a258c11da6ff46a4196bffd96d61dc" +checksum = "ac4eb613a8d86986b61aa6b52bd74ef605d370c149778fe96cfab16dc4377636" dependencies = [ "arrayvec", "bitflags 2.11.0", diff --git a/lib/src/bot.rs b/lib/src/bot.rs index f490401..a14cbf1 100644 --- a/lib/src/bot.rs +++ b/lib/src/bot.rs @@ -34,6 +34,10 @@ impl CompleteBot{ pub fn time(&self,time:PlaybackTime)->PhysicsTime{ self.timer.time(time) } + pub fn playback_time(&self,time:PhysicsTime)->PlaybackTime{ + use strafesnet_common::timer::TimerState; + time.coerce()-self.timer.clone().into_state().get_offset().coerce() + } pub const fn duration(&self)->PhysicsTime{ self.duration } diff --git a/lib/src/bvh.rs b/lib/src/bvh.rs new file mode 100644 index 0000000..d074d49 --- /dev/null +++ b/lib/src/bvh.rs @@ -0,0 +1,101 @@ +use core::ops::Range; +use strafesnet_common::aabb::Aabb; +use strafesnet_common::bvh::generate_bvh; +use strafesnet_common::integer::vec3; +use strafesnet_common::integer::Fixed; +use strafesnet_common::physics::Time as PhysicsTime; +use crate::bot::CompleteBot; + +const MAX_SLICE_LEN:usize=16; +struct EventSlice(Range); + +pub struct Bvh{ + bvh:strafesnet_common::bvh::BvhNode, +} + +impl Bvh{ + pub fn new(bot:&CompleteBot)->Self{ + let output_events=&bot.timelines().output_events; + // iterator over the event timeline and capture slices of contiguous output events. + // create an Aabb for each slice and then generate a BVH. + let mut bvh_nodes=Vec::new(); + let it=output_events + .array_windows() + .enumerate() + // find discontinuities + .filter(|&(_,[event0,event1])| + event0.time==event1.time&&!( + event0.event.position.x==event0.event.position.x + &&event0.event.position.y==event0.event.position.y + &&event0.event.position.z==event0.event.position.z + ) + ); + + let mut last_index=0; + let mut push_slices=|index:usize|{ + let len=index-last_index; + let count=len.div_ceil(MAX_SLICE_LEN); + let slice_len=len/count; + bvh_nodes.reserve(count); + // 0123456789 + // last_index=0 + // split_index=9 + // index=10 + // len=10 + // count=3 + // node_len=4 + // split into groups of 4 + // [0123][4567][89] + let mut push_slice=|slice:Range|{ + let mut aabb=Aabb::default(); + for event in &output_events[slice.start..slice.end]{ + aabb.grow(vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap()); + } + bvh_nodes.push((EventSlice(slice),aabb)); + }; + // push fixed-size groups + for i in 0..count-1{ + push_slice((last_index+i*slice_len)..(last_index+(i+1)*slice_len)); + } + // push last group which may be shorter + push_slice((last_index+(count-1)*slice_len)..index); + last_index=index; + }; + // find discontinuities (teleports) and avoid forming a bvh node across them + for (split_index,_) in it{ + // we want to use the index of event1 + push_slices(split_index+1); + } + // there are no more discontinuities, push the remaining slices + push_slices(output_events.len()); + let bvh=generate_bvh(bvh_nodes); + Self{bvh} + } + /// Find the exact timestamp on the bot timeline that is closest to the given point. + pub fn closest_time_to_point(&self,bot:&CompleteBot,point:glam::Vec3)->Option{ + let start_point=vec3::try_from_f32_array(point.to_array()).unwrap(); + let output_events=&bot.timelines().output_events; + // grow a sphere starting at start_point until we find the closest point on the bot output events + let intersect_leaf=|leaf:&EventSlice|{ + // calculate the distance to the leaf contents + output_events[leaf.0.start..leaf.0.end].iter().map(|event|{ + let p=event.event.position; + let p=vec3::try_from_f32_array([p.x,p.y,p.z]).unwrap(); + (start_point-p).length_squared() + }).min() + }; + let intersect_aabb=|aabb:&Aabb|{ + // calculate the distance to the aabb + let clamped_point=start_point.min(aabb.max()).max(aabb.min()); + Some((start_point-clamped_point).length_squared()) + }; + let (_,leaf)=self.bvh.traverse(start_point,Fixed::ZERO,Fixed::MAX,intersect_leaf,intersect_aabb)?; + let closest_event=output_events[leaf.0.start..leaf.0.end].iter().min_by_key(|&event|{ + let p=event.event.position; + let p=vec3::try_from_f32_array([p.x,p.y,p.z]).unwrap(); + (start_point-p).length_squared() + })?; + // TODO: project start_point onto the edges connected to the closest_event and return the true time + crate::time::from_float(closest_event.time).ok() + } +} diff --git a/lib/src/head.rs b/lib/src/head.rs index fb9606d..50f8296 100644 --- a/lib/src/head.rs +++ b/lib/src/head.rs @@ -119,6 +119,10 @@ impl PlaybackHead{ (p-bot.world_offset()+CompleteBot::CAMERA_OFFSET,a.yx()) } + pub fn get_position(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{ + let interp=self.interpolate_output(bot,time); + interp.position() + } pub fn get_velocity(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{ let interp=self.interpolate_output(bot,time); interp.velocity() diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 35cf582..5611a27 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,5 @@ pub mod bot; +pub mod bvh; pub mod head; pub mod time; pub mod state; diff --git a/wasm-module/src/lib.rs b/wasm-module/src/lib.rs index 6533e50..67552db 100644 --- a/wasm-module/src/lib.rs +++ b/wasm-module/src/lib.rs @@ -1,7 +1,7 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsError; use strafesnet_roblox_bot_file::v0; -use strafesnet_roblox_bot_player::{bot,head,time,graphics}; +use strafesnet_roblox_bot_player::{bot,bvh,head,time,graphics}; use strafesnet_graphics::{setup,surface}; // Hack to keep the code compiling, @@ -193,9 +193,43 @@ impl PlaybackHead{ pub fn get_game_controls(&self)->u32{ self.head.state().get_controls().bits() } + #[wasm_bindgen] + pub fn get_position(&self,bot:&CompleteBot,time:f64)->Vector3{ + let time=time::from_float(time).unwrap(); + let position=self.head.get_position(&bot.bot,time); + Vector3(position) + } /// Returns the camera angles yaw delta between the last game tick and the most recent game tick. #[wasm_bindgen] pub fn get_angles_yaw_delta(&self)->f32{ self.head.state().get_angles_delta().y } } + +#[wasm_bindgen] +pub struct Vector3(glam::Vec3); +#[wasm_bindgen] +impl Vector3{ + #[wasm_bindgen] + pub fn to_array(&self)->Vec{ + self.0.to_array().to_vec() + } +} + +#[wasm_bindgen] +pub struct Bvh{ + bvh:bvh::Bvh, +} +#[wasm_bindgen] +impl Bvh{ + #[wasm_bindgen(constructor)] + pub fn new(bot:&CompleteBot)->Self{ + Self{ + bvh:bvh::Bvh::new(&bot.bot), + } + } + #[wasm_bindgen] + pub fn closest_time_to_point(&self,bot:&CompleteBot,point:&Vector3)->Option{ + Some(bot.bot.playback_time(self.bvh.closest_time_to_point(&bot.bot,point.0)?).into()) + } +}