diff options
Diffstat (limited to 'src/net.rs')
-rw-r--r-- | src/net.rs | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 0000000..323fe06 --- /dev/null +++ b/src/net.rs @@ -0,0 +1,252 @@ +use crate::{GfxEvent, NetEvent}; +use cgmath::{Deg, Point3, Vector3}; +use futures::future::OptionFuture; +use mt_net::{CltSender, ReceiverExt, SenderExt, ToCltPkt, ToSrvPkt}; +use rand::RngCore; +use sha2::Sha256; +use srp::{client::SrpClient, groups::G_2048}; +use std::{future::Future, time::Duration}; +use tokio::{ + sync::mpsc, + time::{interval, Instant, Interval}, +}; +use winit::event_loop::EventLoopProxy; + +enum AuthState { + Init(Interval), + Verify(Vec<u8>, SrpClient<'static, Sha256>), + Done, +} + +struct Conn { + tx: CltSender, + auth: AuthState, + send_pos_iv: Option<Interval>, + username: String, + password: String, + pos: Point3<f32>, + pitch: Deg<f32>, + yaw: Deg<f32>, + events: EventLoopProxy<GfxEvent>, +} + +fn maybe_tick(iv: Option<&mut Interval>) -> OptionFuture<impl Future<Output = Instant> + '_> { + OptionFuture::from(iv.map(Interval::tick)) +} + +pub(crate) async fn run( + evt_out: EventLoopProxy<GfxEvent>, + mut evt_in: mpsc::UnboundedReceiver<NetEvent>, +) { + let (tx, mut rx, worker) = mt_net::connect("localhost:30000").await.unwrap(); + + let mut conn = Conn { + tx, + auth: AuthState::Init(interval(Duration::from_millis(100))), + send_pos_iv: None, + username: "shrek".into(), // shrek is love, shrek is life <3 + password: "boobies".into(), + pos: Point3::new(0.0, 0.0, 0.0), + pitch: Deg(0.0), + yaw: Deg(0.0), + events: evt_out, + }; + + let init_pkt = ToSrvPkt::Init { + serialize_version: 29, + proto_version: 40..=40, + player_name: conn.username.clone(), + send_full_item_meta: false, + }; + + let worker_thread = tokio::spawn(worker.run()); + + loop { + tokio::select! { + pkt = rx.recv() => match pkt { + None => break, + Some(Err(e)) => eprintln!("{e}"), + Some(Ok(v)) => conn.handle_pkt(v).await, + }, + Some(_) = maybe_tick(match &mut conn.auth { + AuthState::Init(iv) => Some(iv), + _ => None, + }) => { + conn.tx.send(&init_pkt).await.unwrap(); + } + Some(_) = maybe_tick(conn.send_pos_iv.as_mut()) => { + conn.tx + .send(&ToSrvPkt::PlayerPos(mt_net::PlayerPos { + pos: conn.pos, + vel: Vector3::new(0.0, 0.0, 0.0), + pitch: conn.pitch, + yaw: conn.yaw, + keys: mt_net::enumset::EnumSet::empty(), + fov: Deg(90.0).into(), + wanted_range: 12, + })) + .await + .unwrap(); + } + evt = evt_in.recv() => { + match evt { + Some(NetEvent::PlayerPos(pos, yaw, pitch)) => { + conn.pos = pos; + conn.yaw = yaw; + conn.pitch = pitch; + }, + Some(NetEvent::Ready) => { + conn.tx + .send(&ToSrvPkt::CltReady { + major: 0, + minor: 1, + patch: 0, + reserved: 0, + version: format!("Minetest Rust {}", env!("CARGO_PKG_VERSION")), + formspec: 4, + }) + .await + .unwrap(); + } + None => conn.tx.close(), + } + } + _ = tokio::signal::ctrl_c() => { + conn.tx.close(); + } + } + } + + conn.events.send_event(GfxEvent::Close).ok(); // TODO: make sure to send this on panic + worker_thread.await.unwrap(); +} + +impl Conn { + async fn handle_pkt(&mut self, pkt: ToCltPkt) { + use ToCltPkt::*; + + match pkt { + Hello { + auth_methods, + username: name, + .. + } => { + use mt_net::AuthMethod; + + if !matches!(self.auth, AuthState::Init(_)) { + return; + } + + let srp = SrpClient::<Sha256>::new(&G_2048); + + let mut rand_bytes = vec![0; 32]; + rand::thread_rng().fill_bytes(&mut rand_bytes); + + if self.username != name { + panic!("username changed"); + } + + if auth_methods.contains(AuthMethod::FirstSrp) { + let verifier = srp.compute_verifier( + self.username.to_lowercase().as_bytes(), + self.password.as_bytes(), + &rand_bytes, + ); + + self.tx + .send(&ToSrvPkt::FirstSrp { + salt: rand_bytes, + verifier, + empty_passwd: self.password.is_empty(), + }) + .await + .unwrap(); + + self.auth = AuthState::Done; + } else if auth_methods.contains(AuthMethod::Srp) { + let a = srp.compute_public_ephemeral(&rand_bytes); + + self.tx + .send(&ToSrvPkt::SrpBytesA { a, no_sha1: true }) + .await + .unwrap(); + + self.auth = AuthState::Verify(rand_bytes, srp); + } else { + panic!("unsupported auth methods: {auth_methods:?}"); + } + } + SrpBytesSaltB { salt, b } => { + if let AuthState::Verify(a, srp) = &self.auth { + let m = srp + .process_reply( + a, + self.username.to_lowercase().as_bytes(), + self.password.as_bytes(), + &salt, + &b, + ) + .unwrap() + .proof() + .into(); + + self.tx.send(&ToSrvPkt::SrpBytesM { m }).await.unwrap(); + + self.auth = AuthState::Done; + } + } + NodeDefs(defs) => { + self.events.send_event(GfxEvent::NodeDefs(defs.0)).ok(); + } + Kick(reason) => { + println!("kicked: {reason}"); + } + AcceptAuth { player_pos, .. } => { + self.tx + .send(&ToSrvPkt::Init2 { + lang: "en_US".into(), // localization is unironically overrated + }) + .await + .unwrap(); + + self.pos = player_pos; + self.send_pos_iv = Some(interval(Duration::from_millis(100))); + } + MovePlayer { pos, pitch, yaw } => { + self.pos = pos; + self.pitch = pitch; + self.yaw = yaw; + + self.events + .send_event(GfxEvent::PlayerPos(self.pos, self.pitch, self.yaw)) + .ok(); + } + BlockData { pos, block } => { + self.events.send_event(GfxEvent::MapBlock(pos, block)).ok(); + self.tx + .send(&ToSrvPkt::GotBlocks { + blocks: Vec::from([pos]), + }) + .await + .unwrap(); + } + AnnounceMedia { files, .. } => { + self.tx + .send(&ToSrvPkt::RequestMedia { + filenames: files.into_keys().collect(), // TODO: cache + }) + .await + .ok(); + } + Media { files, n, i } => { + self.events + .send_event(GfxEvent::Media(files, i + 1 == n)) + .ok(); + } + ChatMsg { text, .. } => { + println!("{text}"); + } + _ => {} + } + } +} |