use std::error::Error; use futures::stream::{SplitSink, SplitStream}; use futures::StreamExt; use log::{debug, error, info, Level, trace, warn}; use wasm_bindgen::prelude::*; use ws_stream_wasm::{WsErr, WsMessage, WsMeta, WsStream}; use starkingdoms_protocol::{Planet, State}; use starkingdoms_protocol::PROTOCOL_VERSION; use starkingdoms_protocol::MessageS2C; use starkingdoms_protocol::MessageC2S; use futures::SinkExt; use lazy_static::lazy_static; use std::sync::Arc; use std::sync::RwLock; use async_recursion::async_recursion; use futures::FutureExt; use wasm_bindgen_futures::JsFuture; use web_sys::{Window}; use starkingdoms_protocol::GoodbyeReason::PingPongTimeout; use crate::rendering::renderer::WebRenderer; use crate::textures::loader::TextureLoader; use crate::textures::TextureManager; #[macro_use] pub mod macros; pub mod chat; pub mod rendering; pub mod textures; #[wasm_bindgen] extern { pub fn alert(s: &str); } #[derive(Debug)] pub struct Client { pub client_data: Option, pub planets: Vec, pub x: f64, pub y: f64 } #[derive(Debug)] pub struct ClientData { pub state: State, pub tx: SplitSink, pub rx: SplitStream, pub pong_timeout: u64 } pub const PONG_MAX_TIMEOUT: u64 = 5; lazy_static! { pub static ref CLIENT: Arc> = Arc::new(RwLock::new(Client { client_data: None, planets: vec![], x: 0f64, y: 0f64 })); } pub const MAX_CONNECTION_TRIES: i32 = 10; #[wasm_bindgen] pub async fn rust_init(gateway: &str, username: &str) -> Result<(), JsValue> { console_error_panic_hook::set_once(); set_status("Starting logger..."); console_log::init_with_level(Level::Debug).unwrap(); info!("Logger setup successfully"); info!("Loading sprites..."); set_status("Loading sprites.."); //let textures = TextureLoader::load().map_err(|e| e.to_string())?; match main(gateway, username, 1).await { Ok(c) => c, Err(e) => { error!("Error initializing gateway client: {}", e); return Err(JsValue::from_str(&e.to_string())); } }; info!("StarKingdoms client set up successfully"); Ok(()) } #[async_recursion(?Send)] pub async fn main(gateway: &str, username: &str, backoff: i32) -> Result<(), Box> { if backoff != 1 { info!("Backing off connection: waiting {} seconds", backoff * backoff); wait_for(sleep(backoff * backoff * 1000)).await; } if backoff > MAX_CONNECTION_TRIES { set_status("Connection to server failed"); return Err("Hit backoff limit during reconnection attempt".into()); } if backoff == 1 { set_status("Connecting to server..."); } else { set_status(&format!("Connection failed, retrying... (try {}/10)", backoff)); } info!("FAST CONNECT: {}", gateway); let gateway_url = url::Url::parse(gateway)?; trace!("Gateway URL parsed"); let (_ws, ws_stream) = match WsMeta::connect(gateway_url, None).await { Ok(r) => r, Err(e) => { return match e { WsErr::ConnectionFailed { .. } => { main(gateway, username, backoff + 1).await }, _ => { Err(e.into()) } } } }; trace!("Connected to gateway socket"); let (tx, rx) = ws_stream.split(); let mut client_data = ClientData { state: State::Handshake, tx, rx, pong_timeout: (js_sys::Date::now() as u64 / 1000) + 5 }; trace!("Split stream, handshaking with server"); set_status("Handshaking with server..."); send!(client_data.tx, &MessageC2S::Hello { next_state: State::Play, version: PROTOCOL_VERSION, requested_username: username.to_string() }).await?; trace!("Sent handshake start packet"); if let Some(msg) = recv_now!(client_data.rx)? { let typed_msg: MessageS2C = msg; match typed_msg { MessageS2C::Hello { version, given_username, next_state } => { info!("FAST CONNECT - connected to server protocol {} given username {}, switching to state {:?}", version, given_username, next_state); client_data.state = next_state; }, MessageS2C::Goodbye { reason } => { error!("server disconnected before finishing handshake: {:?}", reason); return Err(format!("disconnected by server: {:?}", reason).into()); }, _ => { warn!("received unexpected packet from server: {:?}", typed_msg); } } } else { error!("Server closed the connection") } CLIENT.write()?.client_data = Some(client_data); set_status(&format!("Connected! Username: {}", username)); Ok(()) } #[wasm_bindgen] pub async fn update_socket() -> Result<(), JsError> { let mut client = CLIENT.write()?; if client.client_data.is_none() { return Err(JsError::new("Client not yet initialized")); } let client_data = client.client_data.as_mut().unwrap(); if client_data.pong_timeout < (js_sys::Date::now() as u64 / 1000) { error!("Connection timed out"); send!(client_data.tx, &MessageC2S::Goodbye { reason: PingPongTimeout }).await?; client.client_data = None; set_status("Connection timed out. Reload to reconnect"); return Err(JsError::new("Connection timed out")); } if client_data.pong_timeout - 4 < (js_sys::Date::now() as u64 / 1000) { // send ping send!(client_data.tx, &MessageC2S::Ping {}).await?; } let maybe_msg: Option = match recv!(client_data.rx) { Ok(r) => r, Err(e) => { return Err(JsError::new(e)) } }; if let Some(msg) = maybe_msg { match msg { MessageS2C::Goodbye { reason } => { info!("server sent disconnect: {:?}", reason); client.client_data = None; return Err(JsError::new("disconnected by server")); } MessageS2C::Chat { from, message } => { info!("[CHAT] {}: {}", from, message); let window: Window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); let chatbox = document.get_element_by_id("chats").expect("chatbox does not exist"); let new_elem = document.create_element("div").expect("could not create element"); let msg_formatted = markdown::to_html(&format!("**[{}]** {}", from, message)); new_elem.set_inner_html(&msg_formatted); chatbox.append_child(&new_elem).unwrap(); }, MessageS2C::Pong {} => { client_data.pong_timeout = (js_sys::Date::now() as u64 / 1000) + PONG_MAX_TIMEOUT }, MessageS2C::PlanetData { planets } => { debug!("updated planet information {:?}", planets); client.planets = planets; }, MessageS2C::Position { x, y } => { client.x = x; client.y = y; } _ => { warn!("server sent unexpected packet {:?}, ignoring", msg); } } } Ok(()) } #[wasm_bindgen] pub fn set_status(new_status: &str) { let window: Window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); let status = document.get_element_by_id("status").expect("statusbox does not exist"); status.set_inner_html(new_status); } #[wasm_bindgen] pub fn sleep(ms: i32) -> js_sys::Promise { js_sys::Promise::new(&mut |resolve, _| { web_sys::window() .unwrap() .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms) .unwrap(); }) } pub async fn wait_for(promise: js_sys::Promise) -> JsFuture { wasm_bindgen_futures::JsFuture::from(promise) } #[wasm_bindgen] pub fn version() -> u32 { PROTOCOL_VERSION }