use std::error::Error; use std::panic; use std::str::FromStr; use futures::stream::{SplitSink, SplitStream}; use futures::{StreamExt}; use log::{error, info, Level, trace, warn}; use wasm_bindgen::prelude::*; use ws_stream_wasm::{WsErr, WsMessage, WsMeta, WsStream}; 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::message_c2s::{MessageC2SGoodbye, MessageC2SHello, MessageC2SPing}; use starkingdoms_protocol::{MessageC2S, MessageS2C, PROTOCOL_VERSION}; use starkingdoms_protocol::goodbye_reason::GoodbyeReason; use starkingdoms_protocol::planet::Planet; use starkingdoms_protocol::player::Player; use starkingdoms_protocol::state::State; use crate::rendering::Renderer; use crate::rendering::renderer::WebRenderer; use crate::textures::loader::TextureLoader; use crate::textures::{TextureManager, TextureSize}; #[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, pub players: Vec } #[derive(Debug)] pub struct ClientData { pub state: State, pub tx: SplitSink, pub rx: SplitStream, pub pong_timeout: u64, pub textures: TextureLoader, pub renderer: WebRenderer, pub username: String } 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, players: vec![] })); //pub static ref BUTTONS: Arc = Arc::new(Buttons { up: false, left: false, right: false, down: false }); } pub const MAX_CONNECTION_TRIES: i32 = 10; #[wasm_bindgen] pub async fn rust_init(gateway: &str, username: &str, texture_size: &str) -> Result<(), JsValue> { panic::set_hook(Box::new(console_error_panic_hook::hook)); 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(TextureSize::from_str(texture_size).unwrap()).map_err(|e| e.to_string())?; match main(gateway, username, 1, textures, WebRenderer::get("canvas").await.unwrap()).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, textures: TextureLoader, renderer: WebRenderer) -> 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, textures, renderer).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, textures, renderer, username: username.to_string() }; trace!("Split stream, handshaking with server"); set_status("Handshaking with server..."); let msg = MessageC2S::Hello(MessageC2SHello { version: PROTOCOL_VERSION, requested_username: username.to_string(), next_state: State::Play.into(), special_fields: Default::default(), }).try_into()?; send!(client_data.tx, msg).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(pkt) => { info!("FAST CONNECT - connected to server protocol {} given username {}, switching to state {:?}", pkt.version, pkt.given_username, pkt.next_state); client_data.state = pkt.next_state.unwrap(); }, MessageS2C::Goodbye(pkt) => { error!("server disconnected before finishing handshake: {:?}", pkt.reason); return Err(format!("disconnected by server: {:?}", pkt.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 send_ping_pong() -> 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(); send!(client_data.tx, MessageC2S::Ping(MessageC2SPing {special_fields: Default::default()}).try_into().map_err(|_| JsError::new("I/O Error"))?).await?; 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"); let msg = MessageC2S::Goodbye(MessageC2SGoodbye { reason: GoodbyeReason::PingPongTimeout.into(), special_fields: Default::default(), }).try_into().map_err(|e: Box | JsError::new(&e.to_string()))?; send!(client_data.tx, msg).await?; client.client_data = None; set_status("Connection timed out. Reload to reconnect"); return Err(JsError::new("Connection timed out")); } 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(pkt) => { info!("server sent disconnect: {:?}", pkt.reason); client.client_data = None; return Err(JsError::new("disconnected by server")); } MessageS2C::Chat(pkt) => { info!("[CHAT] {}: {}", pkt.from, pkt.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!("**[{}]** {}", pkt.from, pkt.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(pkt) => { client.planets = pkt.planets; }, MessageS2C::PlayersUpdate(pkt) => { let me = pkt.players.iter().find(|i| i.username == client_data.username); if let Some(me) = me { client.x = me.x as f64; client.y = me.y as f64; } client.players = pkt.players; } _ => { 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 } #[wasm_bindgen] pub fn get_texture(texture_id: &str) -> Option { let client = CLIENT.read().unwrap(); if let Some(client_data) = &client.client_data { client_data.textures.get_texture(texture_id) } else { None } } #[wasm_bindgen] pub async fn render_frame(delta: f64) -> Result<(), JsError> { let client = CLIENT.read().unwrap(); if let Some(client_data) = &client.client_data { Ok(client_data.renderer.render_frame(delta).await.map_err(|e| JsError::new(&format!("{}", e)))?) } else { Err(JsError::new("Client not yet initialized")) } }