forked from StrafesNET/roblox-bot-player
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
102dd7fa6f
|
|||
|
f8996c958c
|
|||
|
f91fcf6b6f
|
|||
|
4593514954
|
|||
|
31a3e31e70
|
|||
|
4873e0298c
|
|||
|
637fb38131
|
|||
|
ae624f90dc
|
|||
|
1d17e6acf0
|
|||
|
a53cf8a8c7
|
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -858,6 +858,15 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "integration-tests"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"strafesnet_common",
|
||||
"strafesnet_roblox_bot_file",
|
||||
"strafesnet_roblox_bot_player",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -1883,9 +1892,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",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"integration-tests",
|
||||
"lib",
|
||||
"native-player",
|
||||
"video-encoder",
|
||||
|
||||
9
integration-tests/Cargo.toml
Normal file
9
integration-tests/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "integration-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_roblox_bot_file.workspace = true
|
||||
strafesnet_roblox_bot_player.workspace = true
|
||||
25
integration-tests/src/main.rs
Normal file
25
integration-tests/src/main.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use strafesnet_roblox_bot_file::v0;
|
||||
use strafesnet_roblox_bot_player::{bot,bvh,head};
|
||||
use head::Time as PlaybackTime;
|
||||
use strafesnet_common::session::Time as SessionTime;
|
||||
|
||||
fn main(){
|
||||
let bot=include_bytes!("../../web-demo/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d.qbot");
|
||||
let timelines=v0::read_all_to_block(std::io::Cursor::new(bot)).unwrap();
|
||||
let bot=bot::CompleteBot::new(timelines);
|
||||
let bvh=bvh::Bvh::new(&bot);
|
||||
|
||||
// sample the position at 0.24s
|
||||
let mut playback0=head::PlaybackHead::new(&bot,SessionTime::ZERO);
|
||||
for i in 0..10{
|
||||
let sample_time=PlaybackTime::from_millis(6543+1*i);
|
||||
playback0.set_time(&bot,SessionTime::ZERO,sample_time);
|
||||
let pos=playback0.get_position(&bot,SessionTime::ZERO);
|
||||
|
||||
// get the closest time on the timeline (convert to PlaybackTime which starts at 0)
|
||||
let closest_time=bot.playback_time(bvh.closest_time_to_point(&bot,pos).unwrap());
|
||||
println!("time={sample_time} closest_time={closest_time}");
|
||||
}
|
||||
// let mut playback1=head::PlaybackHead::new(&bot,SessionTime::ZERO);
|
||||
// playback1.set_time(&bot,SessionTime::ZERO,sample_time);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
162
lib/src/bvh.rs
Normal file
162
lib/src/bvh.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
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,Planar64};
|
||||
use strafesnet_common::physics::Time as PhysicsTime;
|
||||
use crate::bot::CompleteBot;
|
||||
use strafesnet_roblox_bot_file::v0;
|
||||
|
||||
const MAX_SLICE_LEN:usize=16;
|
||||
struct EventSlice{
|
||||
slice:Range<usize>,
|
||||
inclusive:bool,
|
||||
}
|
||||
|
||||
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==event1.event.position.x
|
||||
&&event0.event.position.y==event1.event.position.y
|
||||
&&event0.event.position.z==event1.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=MAX_SLICE_LEN;
|
||||
bvh_nodes.reserve(count);
|
||||
// 0123456789
|
||||
// split into groups of MAX_SLICE_LEN=4
|
||||
// [0123][4567][89]
|
||||
let mut push_slice=|slice:Range<usize>,inclusive:bool|{
|
||||
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());
|
||||
}
|
||||
if inclusive{
|
||||
let event=&output_events[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,inclusive},aabb));
|
||||
};
|
||||
// push fixed-size groups
|
||||
for i in 0..count-1{
|
||||
push_slice((last_index+i*slice_len)..(last_index+(i+1)*slice_len),true);
|
||||
}
|
||||
// push last group which may be shorter
|
||||
push_slice((last_index+(count-1)*slice_len)..index,false);
|
||||
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<'a>(&self,bot:&'a CompleteBot,point:glam::Vec3)->Option<PhysicsTime>{
|
||||
let point=point+bot.world_offset();
|
||||
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=|event_slice:&EventSlice|{
|
||||
// calculate the distance to the leaf contents
|
||||
let mut best_distance=output_events[event_slice.slice.start..event_slice.slice.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 mut prev_event=&output_events[event_slice.slice.start];
|
||||
let mut f=|event:&'a v0::Timed<v0::OutputEvent>|{
|
||||
let p0=vec3::try_from_f32_array([prev_event.event.position.x,prev_event.event.position.y,prev_event.event.position.z]).unwrap();
|
||||
let p1=vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap();
|
||||
let d=p1-p0;
|
||||
let d0=p0.dot(d);
|
||||
let d1=p1.dot(d);
|
||||
let sp_d=start_point.dot(d);
|
||||
// must be on the segment
|
||||
if d0<sp_d&&sp_d<d1{
|
||||
let t0=d1-sp_d;
|
||||
let t1=sp_d-d0;
|
||||
let dt=d1-d0;
|
||||
let distance=(((p0*t0+p1*t1)/dt).divide().wrap_1()-start_point).length_squared();
|
||||
if distance<best_distance{
|
||||
best_distance=distance;
|
||||
}
|
||||
}
|
||||
prev_event=event;
|
||||
};
|
||||
for event in &output_events[event_slice.slice.start+1..event_slice.slice.end]{
|
||||
f(event);
|
||||
}
|
||||
if event_slice.inclusive{
|
||||
f(&output_events[event_slice.slice.end]);
|
||||
}
|
||||
Some(best_distance)
|
||||
};
|
||||
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 (_,event_slice)=self.bvh.traverse(start_point,Fixed::ZERO,Fixed::MAX,intersect_leaf,intersect_aabb)?;
|
||||
|
||||
// find time at the closest point
|
||||
let (best_time,mut best_distance)=output_events[event_slice.slice.start..event_slice.slice.end].iter().map(|event|{
|
||||
let p=event.event.position;
|
||||
let p=vec3::try_from_f32_array([p.x,p.y,p.z]).unwrap();
|
||||
(event.time,(start_point-p).length_squared())
|
||||
}).min_by_key(|&(_,distance)|distance)?;
|
||||
let mut best_time=crate::time::from_float(best_time).unwrap();
|
||||
let mut prev_event=&output_events[event_slice.slice.start];
|
||||
let mut f=|event:&'a v0::Timed<v0::OutputEvent>|{
|
||||
let p0=vec3::try_from_f32_array([prev_event.event.position.x,prev_event.event.position.y,prev_event.event.position.z]).unwrap();
|
||||
let p1=vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap();
|
||||
let d=p1-p0;
|
||||
let d0=p0.dot(d);
|
||||
let d1=p1.dot(d);
|
||||
let sp_d=start_point.dot(d);
|
||||
// must be on the segment
|
||||
if d0<sp_d&&sp_d<d1{
|
||||
let t0=d1-sp_d;
|
||||
let t1=sp_d-d0;
|
||||
let dt=d1-d0;
|
||||
let distance=(((p0*t0+p1*t1)/dt).divide().wrap_1()-start_point).length_squared();
|
||||
if distance<best_distance{
|
||||
best_distance=distance;
|
||||
let p0:Planar64=prev_event.time.try_into().unwrap();
|
||||
let p1:Planar64=event.time.try_into().unwrap();
|
||||
best_time=((p0*t0+p1*t1)/dt).into();
|
||||
}
|
||||
}
|
||||
prev_event=event;
|
||||
};
|
||||
for event in &output_events[event_slice.slice.start+1..event_slice.slice.end]{
|
||||
f(event);
|
||||
}
|
||||
if event_slice.inclusive{
|
||||
f(&output_events[event_slice.slice.end]);
|
||||
}
|
||||
Some(best_time)
|
||||
}
|
||||
}
|
||||
@@ -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()-bot.world_offset()
|
||||
}
|
||||
pub fn get_velocity(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{
|
||||
let interp=self.interpolate_output(bot,time);
|
||||
interp.velocity()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod bot;
|
||||
pub mod bvh;
|
||||
pub mod head;
|
||||
pub mod time;
|
||||
pub mod state;
|
||||
|
||||
@@ -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,12 +193,11 @@ impl PlaybackHead{
|
||||
pub fn get_game_controls(&self)->u32{
|
||||
self.head.state().get_controls().bits()
|
||||
}
|
||||
/// Returns an array of [pitch, yaw, roll] in radians. Yaw is not restricted to any particular range.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_angles(&self,bot:&CompleteBot,time:f64)->Vec<f32>{
|
||||
pub fn get_position(&self,bot:&CompleteBot,time:f64)->Vector3{
|
||||
let time=time::from_float(time).unwrap();
|
||||
let angles=self.head.get_angles(&bot.bot,time);
|
||||
angles.to_array().to_vec()
|
||||
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]
|
||||
@@ -206,3 +205,31 @@ impl PlaybackHead{
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
<div class="hud">
|
||||
<div id="hud_duration" class="timer">00:00:00</div>
|
||||
<div id="hud_timer" class="timer">00:00:00</div>
|
||||
<div id="diff_velocity" class="timer">-0.000 u/s</div>
|
||||
<div id="diff_time" class="timer">-0.000s</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="control_reset">↪️</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import init, {
|
||||
CompleteBot,
|
||||
CompleteMap,
|
||||
PlaybackHead,
|
||||
Bvh,
|
||||
} from "./pkg/strafesnet_roblox_bot_player_wasm_module.js";
|
||||
|
||||
// Loading
|
||||
@@ -17,12 +18,16 @@ 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(bot, 0);
|
||||
const bvh_wr = new Bvh(bot);
|
||||
const playback_wr = new PlaybackHead(bot, 0);
|
||||
|
||||
graphics.change_map(map);
|
||||
|
||||
// HUD
|
||||
const hud_timer = document.getElementById("hud_timer");
|
||||
const hud_duration = document.getElementById("hud_duration");
|
||||
const diff_velocity = document.getElementById("diff_velocity");
|
||||
const diff_time = document.getElementById("diff_time");
|
||||
const MODE_MAIN = 0;
|
||||
|
||||
function timer_text(t) {
|
||||
@@ -106,6 +111,22 @@ function animate(now) {
|
||||
const time = playback.get_run_time(bot, elapsedSec, MODE_MAIN);
|
||||
hud_timer.textContent = timer_text(time);
|
||||
|
||||
// show diff
|
||||
const pos = playback.get_position(bot, elapsedSec);
|
||||
const wr_playback_time = bvh_wr.closest_time_to_point(bot, pos);
|
||||
playback_wr.set_head_time(bot, elapsedSec, wr_playback_time);
|
||||
const wr_time = playback_wr.get_run_time(bot, elapsedSec, MODE_MAIN);
|
||||
const run_speed = playback.get_speed(bot, elapsedSec);
|
||||
const wr_speed = playback_wr.get_speed(bot, elapsedSec);
|
||||
const v_diff = run_speed - wr_speed;
|
||||
const wholespeed = Math.floor(Math.abs(v_diff));
|
||||
const millispeed = Math.floor((Math.abs(v_diff) % 1) * 1000);
|
||||
diff_velocity.textContent = `${v_diff<0?"-":"+"}${String(wholespeed)}.${String(millispeed).padStart(3, "0")} u/s`;
|
||||
const t_diff = time - wr_time;
|
||||
const s = Math.floor(Math.abs(t_diff));
|
||||
const ms = Math.floor((Math.abs(t_diff) % 1) * 1000);
|
||||
diff_time.textContent = `${t_diff<0?"-":"+"}${String(s)}.${String(ms).padStart(3, "0")}s`;
|
||||
|
||||
// Render the frame that the bot is at that time
|
||||
graphics.render(bot, playback, elapsedSec);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user