current state
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
//! Bad-internet simulator: a UDP proxy between game client and server that adds
|
||||
//! delay, jitter and packet loss, per direction. Dev tool only — the game itself
|
||||
//! never knows it exists.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run -p netsim -- [--listen 7778] [--upstream 127.0.0.1:7777]
|
||||
//! [--delay MS] [--jitter MS] [--loss PERCENT]
|
||||
//! [--up-delay MS] [--up-jitter MS] [--up-loss PERCENT]
|
||||
//! [--down-delay MS] [--down-jitter MS] [--down-loss PERCENT]
|
||||
//!
|
||||
//! `--delay/--jitter/--loss` set both directions; the `--up-*` (client → server) and
|
||||
//! `--down-*` (server → client) variants override one direction. Jitter is uniform in
|
||||
//! ±MS around the delay; reordering emerges from jitter naturally, as on a real link.
|
||||
//! Point the client at the listen port (e.g. `client 127.0.0.1:7778`).
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct LinkParams {
|
||||
delay_ms: f32,
|
||||
jitter_ms: f32,
|
||||
loss_pct: f32,
|
||||
}
|
||||
|
||||
/// Xorshift64* — plenty for impairment dice; avoids pulling in a rand dependency.
|
||||
struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
fn new() -> Self {
|
||||
let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64;
|
||||
Rng(seed | 1)
|
||||
}
|
||||
|
||||
fn next(&mut self) -> u64 {
|
||||
let mut x = self.0;
|
||||
x ^= x >> 12;
|
||||
x ^= x << 25;
|
||||
x ^= x >> 27;
|
||||
self.0 = x;
|
||||
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
|
||||
}
|
||||
|
||||
/// Uniform in [0, 1).
|
||||
fn unit(&mut self) -> f32 {
|
||||
(self.next() >> 40) as f32 / (1u64 << 24) as f32
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkParams {
|
||||
/// Roll the dice for one datagram: `None` = lost, `Some(d)` = deliver after `d`.
|
||||
fn impair(&self, rng: &mut Rng) -> Option<Duration> {
|
||||
if rng.unit() * 100.0 < self.loss_pct {
|
||||
return None;
|
||||
}
|
||||
let jitter = (rng.unit() * 2.0 - 1.0) * self.jitter_ms;
|
||||
Some(Duration::from_secs_f32((self.delay_ms + jitter).max(0.0) / 1000.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// A datagram waiting for its delivery time. `Up` = client → server.
|
||||
enum Dir {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
struct Pending {
|
||||
due: Instant,
|
||||
dir: Dir,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
// BinaryHeap ordering: only `due` matters (earliest first via Reverse).
|
||||
impl PartialEq for Pending {
|
||||
fn eq(&self, other: &Self) -> bool { self.due == other.due }
|
||||
}
|
||||
impl Eq for Pending {}
|
||||
impl PartialOrd for Pending {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
|
||||
}
|
||||
impl Ord for Pending {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.due.cmp(&other.due) }
|
||||
}
|
||||
|
||||
fn parse_args() -> (u16, SocketAddr, LinkParams, LinkParams) {
|
||||
let mut listen: u16 = 7778;
|
||||
let mut upstream: SocketAddr = "127.0.0.1:7777".parse().unwrap();
|
||||
let mut both = LinkParams { delay_ms: 0.0, jitter_ms: 0.0, loss_pct: 0.0 };
|
||||
let mut up_over = [None::<f32>; 3]; // delay, jitter, loss
|
||||
let mut down_over = [None::<f32>; 3];
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(flag) = args.next() {
|
||||
let val = args.next().unwrap_or_else(|| panic!("missing value for {flag}"));
|
||||
let num = || val.parse::<f32>().unwrap_or_else(|_| panic!("bad number for {flag}: {val}"));
|
||||
match flag.as_str() {
|
||||
"--listen" => listen = val.parse().expect("bad listen port"),
|
||||
"--upstream" => upstream = val.parse().expect("bad upstream address"),
|
||||
"--delay" => both.delay_ms = num(),
|
||||
"--jitter" => both.jitter_ms = num(),
|
||||
"--loss" => both.loss_pct = num(),
|
||||
"--up-delay" => up_over[0] = Some(num()),
|
||||
"--up-jitter" => up_over[1] = Some(num()),
|
||||
"--up-loss" => up_over[2] = Some(num()),
|
||||
"--down-delay" => down_over[0] = Some(num()),
|
||||
"--down-jitter" => down_over[1] = Some(num()),
|
||||
"--down-loss" => down_over[2] = Some(num()),
|
||||
_ => panic!("unknown flag {flag}"),
|
||||
}
|
||||
}
|
||||
|
||||
let apply = |over: [Option<f32>; 3]| LinkParams {
|
||||
delay_ms: over[0].unwrap_or(both.delay_ms),
|
||||
jitter_ms: over[1].unwrap_or(both.jitter_ms),
|
||||
loss_pct: over[2].unwrap_or(both.loss_pct),
|
||||
};
|
||||
(listen, upstream, apply(up_over), apply(down_over))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (listen, upstream, up, down) = parse_args();
|
||||
|
||||
let client_sock = UdpSocket::bind(("127.0.0.1", listen)).expect("bind listen port");
|
||||
let server_sock = UdpSocket::bind("127.0.0.1:0").expect("bind upstream socket");
|
||||
server_sock.connect(upstream).expect("connect upstream");
|
||||
client_sock.set_nonblocking(true).unwrap();
|
||||
server_sock.set_nonblocking(true).unwrap();
|
||||
|
||||
println!("netsim: 127.0.0.1:{listen} ⇄ {upstream}");
|
||||
println!(" up: {:.0} ms ±{:.0} ms, {:.1}% loss", up.delay_ms, up.jitter_ms, up.loss_pct);
|
||||
println!(" down: {:.0} ms ±{:.0} ms, {:.1}% loss", down.delay_ms, down.jitter_ms, down.loss_pct);
|
||||
|
||||
let mut rng = Rng::new();
|
||||
let mut queue: BinaryHeap<Reverse<Pending>> = BinaryHeap::new();
|
||||
// The one game client, learned from its first datagram (re-learned on address change).
|
||||
let mut client_addr: Option<SocketAddr> = None;
|
||||
let mut buf = [0u8; 2048];
|
||||
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
|
||||
while let Ok((n, from)) = client_sock.recv_from(&mut buf) {
|
||||
client_addr = Some(from);
|
||||
if let Some(d) = up.impair(&mut rng) {
|
||||
queue.push(Reverse(Pending { due: now + d, dir: Dir::Up, data: buf[..n].to_vec() }));
|
||||
}
|
||||
}
|
||||
while let Ok(n) = server_sock.recv(&mut buf) {
|
||||
if let Some(d) = down.impair(&mut rng) {
|
||||
queue.push(Reverse(Pending { due: now + d, dir: Dir::Down, data: buf[..n].to_vec() }));
|
||||
}
|
||||
}
|
||||
|
||||
while queue.peek().is_some_and(|Reverse(p)| p.due <= now) {
|
||||
let Reverse(p) = queue.pop().unwrap();
|
||||
match p.dir {
|
||||
Dir::Up => { server_sock.send(&p.data).ok(); }
|
||||
Dir::Down => {
|
||||
if let Some(addr) = client_addr {
|
||||
client_sock.send_to(&p.data, addr).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user