Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3c31de9f9f
|
|||
|
513602e551
|
|||
|
b147c96240
|
|||
|
8f6b7bb8da
|
|||
|
2304f19239
|
|||
|
a2155f0864
|
|||
|
181f39ff37
|
|||
|
5f99a47cf2
|
|||
|
e44fd7cbb9
|
|||
|
60874fb769
|
|||
|
a2f1572281
|
|||
|
baf6f7e99b
|
|||
|
934be7a28a
|
|||
|
cdd2f5cd89
|
|||
|
2f76e5e63d
|
|||
|
5fa13de6cf
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/bots
|
||||
/replays
|
||||
/target
|
||||
|
||||
1125
Cargo.lock
generated
1125
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,9 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.31"
|
||||
strafesnet_common = { version = "0.8.7", registry = "strafesnet" }
|
||||
strafesnet_roblox_bot_file = { version = "0.9.4", registry = "strafesnet" }
|
||||
strafesnet_roblox_bot_player = { version = "0.4.0", registry = "strafesnet" }
|
||||
strafesnet_common = { version = "0.8.0", registry = "strafesnet" }
|
||||
strafesnet_physics = { version = "0.0.1", registry = "strafesnet" }
|
||||
strafesnet_roblox_bot_file = { version = "0.8.1", registry = "strafesnet" }
|
||||
strafesnet_snf = { version = "0.3.2", registry = "strafesnet" }
|
||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "fs"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
|
||||
272
src/main.rs
272
src/main.rs
@@ -1,6 +1,8 @@
|
||||
use strafesnet_roblox_bot_file::v0;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use strafesnet_common::integer::{Planar64,Planar64Vec3};
|
||||
use strafesnet_physics::physics::{PhysicsData,Time};
|
||||
use strafesnet_roblox_bot_file::v0;
|
||||
use futures::{StreamExt,TryStreamExt};
|
||||
|
||||
#[expect(dead_code)]
|
||||
@@ -8,9 +10,11 @@ use futures::{StreamExt,TryStreamExt};
|
||||
enum Error{
|
||||
InvalidArgs,
|
||||
Io(std::io::Error),
|
||||
DecodeMap(strafesnet_snf::Error),
|
||||
DecodeMap2(strafesnet_snf::map::Error),
|
||||
BotFile{
|
||||
path:PathBuf,
|
||||
err:PlaybackError,
|
||||
err:SimError,
|
||||
},
|
||||
Join(tokio::task::JoinError),
|
||||
}
|
||||
@@ -24,75 +28,243 @@ impl From<tokio::task::JoinError> for Error{
|
||||
Self::Join(value)
|
||||
}
|
||||
}
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum PlaybackError{
|
||||
Decode(v0::Error),
|
||||
Panic,
|
||||
|
||||
const fn fly_time(distance:Planar64)->Time{
|
||||
Time::raw(distance.to_raw()*Time::ONE_SECOND.get()/(80*Planar64::ONE.to_raw()))
|
||||
}
|
||||
|
||||
// #[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum SimError{
|
||||
Decode(v0::Error),
|
||||
}
|
||||
fn run_roblox_bot_in_strafe_client_physics(physics_data:&PhysicsData,block:&v0::Block,file_stem:std::ffi::OsString,offset:Planar64Vec3){
|
||||
use strafesnet_common::instruction::TimedInstruction;
|
||||
use strafesnet_common::mouse::MouseState;
|
||||
use strafesnet_common::physics::{Instruction,MouseInstruction,SetControlInstruction,ModeInstruction,MiscInstruction};
|
||||
use strafesnet_physics::physics::{PhysicsState,PhysicsContext};
|
||||
|
||||
fn vector3(v:&v0::Vector3)->Planar64Vec3{
|
||||
strafesnet_common::integer::vec3::try_from_f32_array([v.x,v.y,v.z]).unwrap()
|
||||
}
|
||||
|
||||
let mut input_it=block.input_events.iter();
|
||||
|
||||
let Some(input)=input_it.next()else{
|
||||
return;
|
||||
};
|
||||
let Some(output)=block.output_events.first()else{
|
||||
return;
|
||||
};
|
||||
let Some(setting)=block.setting_events.iter().find(|event|matches!(event.event.setting_type,v0::SettingType::Sensitivity))else{
|
||||
return;
|
||||
};
|
||||
|
||||
let mut recording=Vec::new();
|
||||
let mut physics=PhysicsState::default();
|
||||
let mut time=Time::ZERO;
|
||||
macro_rules! run{
|
||||
($time:expr,$ins:expr)=>{
|
||||
let ins=TimedInstruction{
|
||||
time:$time,
|
||||
instruction:$ins,
|
||||
};
|
||||
recording.push(ins.clone());
|
||||
PhysicsContext::run_input_instruction(&mut physics,physics_data,ins);
|
||||
};
|
||||
}
|
||||
|
||||
// === State Initialization ===
|
||||
|
||||
// Reset
|
||||
run!(time,Instruction::Mode(ModeInstruction::Reset));
|
||||
// generate an instruction to set the sensitivity (sens is also based on fov in rbhop, but pure in strafe client)
|
||||
let fov=1.0;//tan(90)
|
||||
let zoom=1.0;
|
||||
let pixels_to_radians=setting.event.value*fov*zoom/128.0;
|
||||
// convert radians to Angle32
|
||||
let sensitivity=(pixels_to_radians*strafesnet_common::integer::Angle32::FRAC_PI_2.get() as f64/core::f64::consts::FRAC_PI_2) as i64;
|
||||
let sensitivity_ratio=strafesnet_common::integer::Ratio64::new(sensitivity,1).unwrap();
|
||||
run!(time,Instruction::Misc(MiscInstruction::SetSensitivity(strafesnet_common::integer::Ratio64Vec2{
|
||||
x:sensitivity_ratio,
|
||||
y:sensitivity_ratio,
|
||||
})));
|
||||
run!(time,Instruction::Mode(ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::MAIN)));
|
||||
|
||||
// Fly!
|
||||
run!(time,Instruction::Misc(MiscInstruction::PracticeFly));
|
||||
|
||||
// NoClip!
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetSprint(true)));
|
||||
|
||||
// generate instructions to fly to the starting position
|
||||
let fly_offset=offset+vector3(&output.event.position)-physics.body().position;
|
||||
if fly_offset.x.is_negative(){
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveLeft(true)));
|
||||
time+=fly_time(-fly_offset.x);
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveLeft(false)));
|
||||
}else{
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveRight(true)));
|
||||
time+=fly_time(fly_offset.x);
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveRight(false)));
|
||||
}
|
||||
if fly_offset.y.is_negative(){
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveDown(true)));
|
||||
time+=fly_time(-fly_offset.y);
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveDown(false)));
|
||||
}else{
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveUp(true)));
|
||||
time+=fly_time(fly_offset.y);
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveUp(false)));
|
||||
}
|
||||
if fly_offset.z.is_negative(){
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveForward(true)));
|
||||
time+=fly_time(-fly_offset.z);
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveForward(false)));
|
||||
}else{
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveBack(true)));
|
||||
time+=fly_time(fly_offset.z);
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetMoveBack(false)));
|
||||
}
|
||||
|
||||
// Fly off
|
||||
run!(time,Instruction::Misc(MiscInstruction::PracticeFly));
|
||||
|
||||
// NoClip off
|
||||
run!(time,Instruction::SetControl(SetControlInstruction::SetSprint(false)));
|
||||
|
||||
// TODO: generate instructions to set up the velocity
|
||||
|
||||
let t0=Time::from_nanos((output.time*Time::ONE_SECOND.get() as f64) as i64);
|
||||
// generate an instruction that will rotate the camera to match output.angles
|
||||
let mut last_mouse=&input.event.mouse_pos;
|
||||
// generate an instruction for each pressed key
|
||||
let mut last_controls=v0::GameControls::empty();
|
||||
|
||||
// construct an iterator to detect key changes and mouse movement
|
||||
// run key changes first, then mouse movement
|
||||
let it=input_it.map(|event|{
|
||||
let time=Time::from_nanos((event.time*Time::ONE_SECOND.get() as f64) as i64)-t0+time;
|
||||
// detect controls changes
|
||||
let controls={
|
||||
let new_controls=event.event.game_controls;
|
||||
let mask=new_controls^last_controls;
|
||||
last_controls=new_controls;
|
||||
(mask,new_controls)
|
||||
};
|
||||
// detect mouse changes
|
||||
let mouse=(event.event.mouse_pos.x!=last_mouse.x||event.event.mouse_pos.y!=last_mouse.y).then(||{
|
||||
last_mouse=&event.event.mouse_pos;
|
||||
Instruction::Mouse(MouseInstruction::SetNextMouse(MouseState{
|
||||
pos:[event.event.mouse_pos.x as i32,event.event.mouse_pos.y as i32].into(),
|
||||
time,
|
||||
}))
|
||||
});
|
||||
(time,controls,mouse)
|
||||
});
|
||||
|
||||
// detect deviation from space radius and time range.
|
||||
// if the current position is not near any timeline position within +-1s, abort
|
||||
|
||||
for (time,(mask,controls),mouse) in it{
|
||||
macro_rules! run_control{
|
||||
($control:ident,$ins:ident)=>{
|
||||
if mask.contains(v0::GameControls::$control){
|
||||
let state=controls.contains(v0::GameControls::$control);
|
||||
let instruction=Instruction::SetControl(SetControlInstruction::$ins(state));
|
||||
run!(time,instruction);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
run_control!(MoveForward,SetMoveForward);
|
||||
run_control!(MoveLeft,SetMoveLeft);
|
||||
run_control!(MoveBack,SetMoveBack);
|
||||
run_control!(MoveRight,SetMoveRight);
|
||||
run_control!(MoveUp,SetMoveUp);
|
||||
run_control!(MoveDown,SetMoveDown);
|
||||
// run_control!(LookUp);
|
||||
// run_control!(LookLeft);
|
||||
// run_control!(LookDown);
|
||||
// run_control!(LookRight);
|
||||
run_control!(Jump,SetJump);
|
||||
// run_control!(Crouch);
|
||||
run_control!(Sprint,SetSprint);
|
||||
run_control!(Zoom,SetZoom);
|
||||
// run_control!(Use);
|
||||
// run_control!(Action1);
|
||||
// run_control!(Action2);
|
||||
|
||||
if let Some(instruction)=mouse{
|
||||
run!(time,instruction);
|
||||
}
|
||||
}
|
||||
// idle for 1s just in case we need to glide into the finish zone
|
||||
|
||||
// if the bot completed the map, write a .snfb file
|
||||
match physics.get_finish_time(){
|
||||
Some(time)=>println!("finish time:{}",time),
|
||||
None=>println!("simulation did not end in finished state"),
|
||||
}
|
||||
|
||||
let mut replays_path=std::env::current_dir().unwrap();
|
||||
let mut file_name=PathBuf::from(file_stem);
|
||||
file_name.set_extension("snfb");
|
||||
std::fs::create_dir_all(replays_path.as_path()).unwrap();
|
||||
replays_path.push(file_name);
|
||||
let file=std::fs::File::create(replays_path).unwrap();
|
||||
strafesnet_snf::bot::write_bot(
|
||||
std::io::BufWriter::new(file),
|
||||
strafesnet_physics::VERSION.get(),
|
||||
recording,
|
||||
).unwrap();
|
||||
println!("Finished writing bot file!");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main()->Result<(),Error>{
|
||||
const PREFETCH_QUEUE:usize=64;
|
||||
|
||||
let Some(dir)=std::env::args().skip(1).next()else{
|
||||
let mut args=std::env::args().skip(1);
|
||||
let (Some(map),Some(dir))=(args.next(),args.next())else{
|
||||
return Err(Error::InvalidArgs);
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct State{
|
||||
count:usize,
|
||||
errors:Vec<(PathBuf,strafesnet_roblox_bot_player::bot::Error)>,
|
||||
}
|
||||
println!("loading map file..");
|
||||
let data=tokio::fs::read(map).await?;
|
||||
let map=strafesnet_snf::read_map(std::io::Cursor::new(data))
|
||||
.map_err(Error::DecodeMap)?
|
||||
.into_complete_map()
|
||||
.map_err(Error::DecodeMap2)?;
|
||||
|
||||
// grab mapstart position
|
||||
let start_zone_model_id=map.modes.clone().denormalize().get_mode(strafesnet_common::gameplay_modes::ModeId::MAIN).unwrap().get_start();
|
||||
let offset=map.models[start_zone_model_id.get() as usize].transform.translation;
|
||||
|
||||
// create recording
|
||||
println!("generating models..");
|
||||
let physics_data:&_=Box::leak(Box::new(PhysicsData::new(&map)));
|
||||
|
||||
let available_parallelism=std::thread::available_parallelism()?.get();
|
||||
let read_dir=tokio::fs::read_dir(dir).await?;
|
||||
let tally=tokio_stream::wrappers::ReadDirStream::new(read_dir).map(|dir_entry|async move{
|
||||
tokio_stream::wrappers::ReadDirStream::new(read_dir).map(|dir_entry|async move{
|
||||
let entry=dir_entry?;
|
||||
let path=entry.path();
|
||||
let file=tokio::fs::read(path.as_path()).await?;
|
||||
Ok((path,file))
|
||||
})
|
||||
.buffer_unordered(PREFETCH_QUEUE)
|
||||
.map(|result:Result<_,Error>|async move{
|
||||
let (path,file)=result?;
|
||||
.try_for_each_concurrent(available_parallelism,|(path,file)|async move{
|
||||
let file_stem=path.file_stem().unwrap().to_owned();
|
||||
let result=tokio::task::spawn_blocking(move||{
|
||||
let block=v0::read_all_to_block(std::io::Cursor::new(file.as_slice())).map_err(PlaybackError::Decode)?;
|
||||
let bot=match strafesnet_roblox_bot_player::bot::CompleteBot::new(block){
|
||||
Ok(bot)=>bot,
|
||||
Err(e)=>return Ok(Some(e)),
|
||||
};
|
||||
let mut head=strafesnet_roblox_bot_player::head::PlaybackHead::new(&bot,strafesnet_common::session::Time::ZERO);
|
||||
if let Err(e)=std::panic::catch_unwind(move||{
|
||||
head.get_position_angles(&bot,strafesnet_common::session::Time::ZERO);
|
||||
head.set_time(&bot,strafesnet_common::session::Time::ZERO,bot.duration().coerce());
|
||||
head.get_position_angles(&bot,strafesnet_common::session::Time::ZERO);
|
||||
}){
|
||||
println!("{e:?}");
|
||||
return Err(PlaybackError::Panic);
|
||||
}
|
||||
Ok(None)
|
||||
let block=v0::read_all_to_block(std::io::Cursor::new(file.as_slice())).map_err(SimError::Decode)?;
|
||||
run_roblox_bot_in_strafe_client_physics(physics_data,&block,file_stem,offset);
|
||||
Ok(())
|
||||
}).await?;
|
||||
match result{
|
||||
Err(err)=>Err(Error::BotFile{path,err}),
|
||||
Ok(block)=>Ok((path,block)),
|
||||
Ok(())=>Ok(()),
|
||||
}
|
||||
})
|
||||
.buffer_unordered(available_parallelism)
|
||||
.try_fold(State::default(),async|mut state,(path,bot_error)|{
|
||||
state.count+=1;
|
||||
// state.accumulate(&path,&block);
|
||||
match bot_error{
|
||||
None=>println!("{:07} {:?}",state.count,path.file_name()),
|
||||
Some(e)=>{
|
||||
println!("{:07} {:?} {}",state.count,path.file_name(),e);
|
||||
state.errors.push((path,e));
|
||||
},
|
||||
};
|
||||
Ok(state)
|
||||
}).await?;
|
||||
println!("{}",tally.count);
|
||||
for (path,e) in tally.errors{
|
||||
println!("error: {:?} {}",path.file_name(),e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user