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<ClientData>,
pub planets: Vec<Planet>,
pub x: f64,
pub y: f64
}
#[derive(Debug)]
pub struct ClientData {
pub state: State,
pub tx: SplitSink<WsStream, WsMessage>,
pub rx: SplitStream<WsStream>,
pub pong_timeout: u64
}
pub const PONG_MAX_TIMEOUT: u64 = 5;
lazy_static! {
pub static ref CLIENT: Arc<RwLock<Client>> = 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<dyn Error>> {
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<MessageS2C> = 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
}