2 Commits

Author SHA1 Message Date
2ffce9d9ca adjust playback speed with arrow keys 2026-02-20 10:41:07 -08:00
8a55fbffd9 ai ratio 2026-02-20 10:41:07 -08:00
9 changed files with 164 additions and 2 deletions

1
.rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
hard_tabs=true

9
Cargo.lock generated
View File

@@ -1407,6 +1407,13 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde"
[[package]]
name = "ratio_from_float"
version = "0.1.0"
dependencies = [
"strafesnet_common",
]
[[package]]
name = "ratio_ops"
version = "0.1.1"
@@ -1687,6 +1694,7 @@ version = "0.1.0"
dependencies = [
"glam",
"pollster",
"ratio_from_float",
"strafesnet_common",
"strafesnet_graphics",
"strafesnet_roblox_bot_file",
@@ -1700,6 +1708,7 @@ dependencies = [
name = "strafesnet_roblox_bot_player_wasm_module"
version = "0.1.0"
dependencies = [
"ratio_from_float",
"strafesnet_common",
"strafesnet_graphics",
"strafesnet_roblox_bot_file",

View File

@@ -2,6 +2,7 @@
members = [
"lib",
"native-player",
"ratio_from_float",
"wasm-module"
]
resolver = "3"

View File

@@ -64,6 +64,9 @@ impl PlaybackHead{
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;

View File

@@ -4,8 +4,9 @@ version = "0.1.0"
edition = "2024"
[dependencies]
pollster = "0.4.0"
ratio_from_float = { version = "0.1.0", path = "../ratio_from_float" }
strafesnet_roblox_bot_player = { version = "0.1.0", path = "../lib" }
pollster = "0.4.0"
wgpu = "28.0.0"
winit = "0.30.12"
strafesnet_common.workspace = true

View File

@@ -23,6 +23,7 @@ pub struct PlayerWorker<'a>{
graphics_thread:Graphics,
bot:Option<CompleteBot>,
playback_head:PlaybackHead,
playback_speed:i8,
}
impl<'a> PlayerWorker<'a>{
pub fn new(
@@ -35,6 +36,7 @@ impl<'a> PlayerWorker<'a>{
graphics_thread,
bot:None,
playback_head,
playback_speed:0,
}
}
pub fn send(&mut self,ins:TimedInstruction<Instruction,SessionTime>){
@@ -48,7 +50,16 @@ impl<'a> PlayerWorker<'a>{
Instruction::SessionControl(SessionControlInstruction::SkipBack)=>{
self.playback_head.seek_backward(SessionTime::from_secs(5));
},
Instruction::SessionControl(session_control_instruction)=>{},
Instruction::SessionControl(SessionControlInstruction::DecreaseTimescale)=>{
self.playback_speed=self.playback_speed.saturating_sub(1).max(-48);
let speed=2.0f64.powf(self.playback_speed as f64/3.0);
self.playback_head.set_scale(ins.time,ratio_from_float::ratio_from_f64(speed).unwrap());
},
Instruction::SessionControl(SessionControlInstruction::IncreaseTimescale)=>{
self.playback_speed=self.playback_speed.saturating_add(1).min(48);
let speed=2.0f64.powf(self.playback_speed as f64/3.0);
self.playback_head.set_scale(ins.time,ratio_from_float::ratio_from_f64(speed).unwrap());
},
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);

View File

@@ -0,0 +1,7 @@
[package]
name = "ratio_from_float"
version = "0.1.0"
edition = "2024"
[dependencies]
strafesnet_common.workspace = true

128
ratio_from_float/src/lib.rs Normal file
View File

@@ -0,0 +1,128 @@
use strafesnet_common::integer::Ratio64;
/// Convert an `f64` to a `Ratio64`.
///
/// Returns `None` for NaN, infinities, or when the exact fraction would overflow `i64`/`u64`.
/// The result is always reduced to lowest terms.
pub fn ratio_from_f64(x: f64) -> Option<Ratio64> {
// Handle special values first
match x.classify() {
core::num::FpCategory::Nan | core::num::FpCategory::Infinite => return None,
core::num::FpCategory::Zero => return Ratio64::new(0, 1),
core::num::FpCategory::Subnormal | core::num::FpCategory::Normal => {
if x < i64::MIN as f64 {
return None;
}
if (i64::MAX as f64) < x {
return None;
}
}
}
// 2⃣ Pull out the raw bits
let bits: u64 = x.to_bits();
let sign: i64 = if (bits >> 63) != 0 { -1 } else { 1 };
let exp_raw: u32 = ((bits >> 52) & 0x7FF) as u32;
let mant: u64 = bits & 0xFFFFFFFFFFFFF; // 52 bits
// 3⃣ Normalise exponent and mantissa
let (exp, mant) = if exp_raw == 0 {
// subnormal
(1 - 1023, mant) // unbiased exponent = -1022
} else {
// normal
((exp_raw as i32) - 1023, mant | (1 << 52)) // implicit leading 1
};
// value = sign * mant * 2^(exp-52)
let shift = exp - 52; // may be negative
// 4⃣ Build numerator / denominator as 64bit values
// ────────────────────────────────────────
// If shift is positive → numerator = mant << shift
// If shift is negative → denominator = 1 << (-shift)
// We use the checked arithmetic helpers to catch overflow.
let (mut num, den) = if shift >= 0 {
// shift <= 63 because 53bit mantissa * 2^shift must fit in i64
let s = shift as u32;
let n = (mant as i64).checked_shl(s)?;
(n, 1)
} else {
// shift is negative
let s = (-shift) as u32;
if s > 63 {
// 2^s would not fit in a u64 → underflow
return Ratio64::new(0, 1);
}
(mant as i64, 1u64 << s)
};
// 5⃣ Apply the sign
num *= sign;
Ratio64::new(num, den)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let r = ratio_from_f64(1.5).unwrap();
assert_eq!(r.num(), 3);
assert_eq!(r.den(), 2);
let r = ratio_from_f64(0.1).unwrap();
// 0.1 = 3602879701896397 / 36028797018963968
assert_eq!(r.num(), 3602879701896397);
assert_eq!(r.den(), 36028797018963968);
let r = ratio_from_f64(-3.141592653589793).unwrap();
assert_eq!(r.num(), -884279719003555);
assert_eq!(r.den(), 281474976710656);
// NaN / Infinity → None
assert!(ratio_from_f64(f64::NAN).is_none());
assert!(ratio_from_f64(f64::INFINITY).is_none());
}
#[test]
fn overflow() {
// value that would need > 64bit numerator
let f = (i64::MAX as f64) * 2.0; // just above i64::MAX
assert!(ratio_from_f64(f).is_none());
// subnormal: denominator would need 2^1074 > u64::MAX
let sub = f64::MIN_POSITIVE / 2.0; // 2.22507e308 / 2 = 1.1125e308
assert_eq!(ratio_from_f64(sub).unwrap().num(), 0);
}
#[test]
fn test() {
let numbers = [
0.0,
-0.0,
1.0,
-1.0,
3.141592653589793,
1.5,
0.1,
2.225073858507201e-308, // subnormal
1.7976931348623157e308, // max normal
];
for f in numbers {
match ratio_from_f64(f) {
Some(r) => println!(
"{:>15}{:>15} / {:>15} (≈ {:.20})",
f,
r.num(),
r.den(),
f
),
None => println!("{:>15} → overflow / NaN / infinite", f),
}
}
}
}

View File

@@ -7,6 +7,7 @@ edition = "2024"
crate-type = ["cdylib"]
[dependencies]
ratio_from_float = { version = "0.1.0", path = "../ratio_from_float" }
strafesnet_roblox_bot_player = { version = "0.1.0", path = "../lib" }
strafesnet_common.workspace = true
strafesnet_graphics.workspace = true