This commit is contained in:
2026-03-13 10:40:50 -07:00
parent a53cf8a8c7
commit 1d17e6acf0
6 changed files with 147 additions and 3 deletions

4
Cargo.lock generated
View File

@@ -1883,9 +1883,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strafesnet_common" name = "strafesnet_common"
version = "0.8.6" version = "0.8.7"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "fb31424f16d189979d9f5781067ff29169a258c11da6ff46a4196bffd96d61dc" checksum = "ac4eb613a8d86986b61aa6b52bd74ef605d370c149778fe96cfab16dc4377636"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags 2.11.0", "bitflags 2.11.0",

View File

@@ -34,6 +34,10 @@ impl CompleteBot{
pub fn time(&self,time:PlaybackTime)->PhysicsTime{ pub fn time(&self,time:PlaybackTime)->PhysicsTime{
self.timer.time(time) 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{ pub const fn duration(&self)->PhysicsTime{
self.duration self.duration
} }

101
lib/src/bvh.rs Normal file
View File

@@ -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<usize>);
pub struct Bvh{
bvh:strafesnet_common::bvh::BvhNode<EventSlice>,
}
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<usize>|{
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<PhysicsTime>{
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()
}
}

View File

@@ -119,6 +119,10 @@ impl PlaybackHead{
(p-bot.world_offset()+CompleteBot::CAMERA_OFFSET,a.yx()) (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{ pub fn get_velocity(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{
let interp=self.interpolate_output(bot,time); let interp=self.interpolate_output(bot,time);
interp.velocity() interp.velocity()

View File

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

View File

@@ -1,7 +1,7 @@
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsError; use wasm_bindgen::JsError;
use strafesnet_roblox_bot_file::v0; 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}; use strafesnet_graphics::{setup,surface};
// Hack to keep the code compiling, // Hack to keep the code compiling,
@@ -193,9 +193,43 @@ impl PlaybackHead{
pub fn get_game_controls(&self)->u32{ pub fn get_game_controls(&self)->u32{
self.head.state().get_controls().bits() 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. /// Returns the camera angles yaw delta between the last game tick and the most recent game tick.
#[wasm_bindgen] #[wasm_bindgen]
pub fn get_angles_yaw_delta(&self)->f32{ pub fn get_angles_yaw_delta(&self)->f32{
self.head.state().get_angles_delta().y 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<f32>{
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<f64>{
Some(bot.bot.playback_time(self.bvh.closest_time_to_point(&bot.bot,point.0)?).into())
}
}