~starkingdoms/starkingdoms

f7606751a9bff2eca82a1424cefb10ea11aa6465 — c0repwn3r 2 years ago 219a8aa
add typescript framework
11 files changed, 0 insertions(+), 903 deletions(-)

D client/Cargo.toml
D client/src/chat.rs
D client/src/lib.rs
D client/src/macros.rs
D client/src/rendering/mod.rs
D client/src/rendering/renderer_canvascentric.rs
D client/src/rendering/renderer_playercentric.rs
D client/src/rendering/util.rs
D client/src/textures/loader_fast.rs
D client/src/textures/loader_slow.rs
D client/src/textures/mod.rs
D client/Cargo.toml => client/Cargo.toml +0 -62
@@ 1,62 0,0 @@
[package]
name = "starkingdoms-client"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
console_log = { version = "1", features = ["color"] }
log = "0.4"
futures = { version = "0.3", default-features = false }
wasm-bindgen-futures = "0.4"
url = "2.3"
starkingdoms-protocol = { version = "0.1.0", path = "../protocol" }
rmp-serde = "1.1"
ws_stream_wasm = "0.7"
serde = { version = "1", features = ["derive"] }
lazy_static = "1.4"
markdown = "1.0.0-alpha.7" # DO NOT DOWNGRADE
async-recursion = "1"
console_error_panic_hook = "0.1"
async-trait = "0.1.68"
image = { version = "0.24.6", optional = true }
ron = { version = "0.8", optional = true }
base64 = { version = "0.21.0", optional = true }

[dependencies.web-sys]
version = "0.3.4"
features = [
    'Document',
    'Element',
    'HtmlCanvasElement',
    'Window',
    'CanvasRenderingContext2d',
    'HtmlImageElement',
    'SvgImageElement',
    'HtmlVideoElement',
    'ImageBitmap',
    'OffscreenCanvas',
    'VideoFrame',
    'CanvasWindingRule',
    'Path2d',
    'CanvasPattern',
    'CanvasGradient',
    'HitRegionOptions',
    'ImageData',
    'TextMetrics',
    'DomMatrix',
    'CssStyleDeclaration'
]

[features]
textures-slow = []
textures-fast = ['image', 'ron', 'base64']

renderer-canvascentric = []
renderer-playercentric = []
\ No newline at end of file

D client/src/chat.rs => client/src/chat.rs +0 -25
@@ 1,25 0,0 @@
use std::error::Error;
use wasm_bindgen::prelude::*;
use starkingdoms_protocol::MessageC2S;
use crate::CLIENT;
use futures::SinkExt;
use starkingdoms_protocol::message_c2s::MessageC2SChat;

#[wasm_bindgen]
// TODO: Switch to async-aware mutexes
#[allow(clippy::await_holding_lock)]
pub async fn send_chat(message: &str) -> Result<(), JsError> {
    let client_data = &mut CLIENT.write()?.client_data;

    if let Some(data) = client_data {
        let msg = MessageC2S::Chat(MessageC2SChat {
            message: message.to_string(),
            special_fields: Default::default(),
        }).try_into().map_err(|e: Box<dyn Error> | JsError::new(&e.to_string()))?;
        send!(data.tx, msg).await?;
    } else {
        return Err(JsError::new("Client not yet connected to server"));
    }

    Ok(())
}
\ No newline at end of file

D client/src/lib.rs => client/src/lib.rs +0 -327
@@ 1,327 0,0 @@
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<ClientData>,
    pub planets: Vec<Planet>,
    pub x: f64,
    pub y: f64,
    pub players: Vec<Player>
}

#[derive(Debug)]
pub struct ClientData {
    pub state: State,
    pub tx: SplitSink<WsStream, WsMessage>,
    pub rx: SplitStream<WsStream>,
    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<RwLock<Client>> = Arc::new(RwLock::new(Client {
        client_data: None,
        planets: vec![],
        x: 0f64,
        y: 0f64,
        players: vec![]
    }));
    //pub static ref BUTTONS: Arc<Buttons> = 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<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, 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<dyn Error> | 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<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(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<String> {
    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"))
    }
}

D client/src/macros.rs => client/src/macros.rs +0 -64
@@ 1,64 0,0 @@
use ws_stream_wasm::WsMessage;

#[macro_export]
macro_rules! send {
    ($writer:expr,$pkt:expr) => {
        $writer.send($crate::macros::__generic_packet_to_message($pkt))
    };
}

#[macro_export]
macro_rules! recv {
    ($reader:expr) => {
        {
            if let Some(future_result) = $reader.next().now_or_never() {
                if let Some(msg) = future_result {
                    if let WsMessage::Binary(msg) = msg {
                        match MessageS2C::try_from(msg.as_slice()) {
                            Ok(d) => Ok(Some(d)),
                            Err(e) => {
                                log::error!("error deserializing message: {}", e);
                                Ok(None)
                            }
                        }
                    } else {
                        Ok(None)
                    }
                } else {
                    log::error!("pipe closed");
                    Err("Pipe closed")
                }
            } else {
                Ok(None)
            }
        }
    }
}

#[macro_export]
macro_rules! recv_now {
    ($reader:expr) => {
        {
            if let Some(msg) = $reader.next().await {
                if let WsMessage::Binary(msg) = msg {
                    match MessageS2C::try_from(msg.as_slice()) {
                        Ok(d) => Ok(Some(d)),
                        Err(e) => {
                            log::error!("error deserializing message: {}", e);
                            Ok(None)
                        }
                    }
                } else {
                    Ok(None)
                }
            } else {
                log::error!("pipe closed");
                Err("Pipe closed")
            }
        }
    };
}

pub fn __generic_packet_to_message(pkt: Vec<u8>) -> WsMessage {
    WsMessage::from(pkt)
}
\ No newline at end of file

D client/src/rendering/mod.rs => client/src/rendering/mod.rs +0 -23
@@ 1,23 0,0 @@
use std::error::Error;
use async_trait::async_trait;

#[cfg(all(feature = "renderer-playercentric", feature = "renderer-canvascentric"))]
compile_error!("Mutually exclusive features renderer-playercentric and renderer-canvascentric selected");
#[cfg(not(any(feature = "renderer-playercentric", feature = "renderer-canvascentric")))]
compile_error!("Required feature renderer not selected");

#[cfg(feature = "renderer-canvascentric")]
#[path = "renderer_canvascentric.rs"]
pub mod renderer;

#[cfg(feature = "renderer-playercentric")]
#[path = "renderer_playercentric.rs"]
pub mod renderer;

pub mod util;

#[async_trait]
pub trait Renderer {
    async fn get(canvas_element_id: &str) -> Result<Self, Box<dyn Error>> where Self: Sized;
    async fn render_frame(&self, time_delta_ms: f64) -> Result<(), Box<dyn Error>>;
}
\ No newline at end of file

D client/src/rendering/renderer_canvascentric.rs => client/src/rendering/renderer_canvascentric.rs +0 -110
@@ 1,110 0,0 @@
use std::error::Error;
use async_trait::async_trait;
use log::debug;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlImageElement};
use crate::rendering::Renderer;
use wasm_bindgen::{JsCast, JsValue};
use crate::CLIENT;
use crate::rendering::util::texid_to_html_image_unchecked;
use crate::textures::TextureManager;

#[derive(Debug)]
pub struct WebRenderer {
    canvas_element_id: String
}

pub const USERNAME_TEXT_ALIGN: &str = "center";
pub const USERNAME_FONT: &str = "30px Segoe UI";
pub const USERNAME_COLOR: &str = "white";
pub const USERNAME_OFFSET_X: f64 = 0f64;
pub const USERNAME_OFFSET_Y: f64 = -35f64;

pub const HEARTY_OFFSET_X: f64 = -25f64;
pub const HEARTY_OFFSET_Y: f64 = -25f64;
pub const HEARTY_WIDTH: f64 = 50f64;
pub const HEARTY_HEIGHT: f64 = 50f64;

#[async_trait]
impl Renderer for WebRenderer {
    async fn get(canvas_element_id: &str) -> Result<Self, Box<dyn Error>> {
        Ok(Self {
            canvas_element_id: canvas_element_id.to_string()
        })
    }

    async fn render_frame(&self, _time_delta_ms: f64) -> Result<(), Box<dyn Error>> {
        // TODO - core is working on this, please no touchy without telling him
        // TODO - until this notice is removed
        // time_delta_ms is the delta, in ms, from when the last render_frame was called by the browser
        let window = web_sys::window().ok_or("window needs to exist")?;
        let document = window.document().ok_or("window.document needs to exist")?;
        let canvas_element = document.get_element_by_id(&self.canvas_element_id).ok_or("canvas element does not exist")?;
        let typed_canvas_element: HtmlCanvasElement = canvas_element.dyn_into::<web_sys::HtmlCanvasElement>().map_err(|_| ()).unwrap();
        let context = typed_canvas_element.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
        let client = CLIENT.read()?;
        if client.client_data.is_none() {
            return Err("client not yet initialized".into());
        }
        let client_data = client.client_data.as_ref().unwrap();

        context.set_transform(1f64, 0f64, 0f64, 1f64, 0f64, 0f64).map_err(|e: JsValue| e.as_string().unwrap())?;
        context.clear_rect(0f64, 0f64, typed_canvas_element.width() as f64, typed_canvas_element.height() as f64);

        let hearty = texid_to_html_image_unchecked("hearty");

        // draw players
        for player in &client.players {
            context.save(); // save current position

            // teleport to the player's location
            context.translate(-player.x, -player.y).map_err(|e| e.as_string().unwrap())?;

            debug!("[render] PL: ({}, {}) {}", player.x, player.y, player.username);

            // draw username
            context.set_text_align(USERNAME_TEXT_ALIGN);
            context.set_font(USERNAME_FONT);
            context.set_fill_style(&JsValue::from_str(USERNAME_COLOR)); // CssStyleColor
            context.fill_text(&player.username, USERNAME_OFFSET_X, USERNAME_OFFSET_Y).map_err(|e: JsValue| e.as_string().unwrap())?;

            // rotate the canvas so we can draw hearty
            context.rotate(player.rotation).map_err(|e| e.as_string().unwrap())?;

            // draw hearty
            context.draw_image_with_html_image_element_and_dw_and_dh(&hearty, HEARTY_OFFSET_X, HEARTY_OFFSET_Y, HEARTY_WIDTH, HEARTY_HEIGHT).map_err(|e| e.as_string().unwrap())?;

            context.restore(); // return to canvas base
        }

        // finally, translate to hearty
        context.translate(-client.x + ((typed_canvas_element.width() / 2) as f64), -client.y + ((typed_canvas_element.height() / 2) as f64)).map_err(|e| e.as_string().unwrap())?;

/*
        context.begin_path();

        // Draw the outer circle.
        context
            .arc(75.0, 75.0, 50.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the mouth.
        context.move_to(110.0, 75.0);
        context.arc(75.0, 75.0, 35.0, 0.0, std::f64::consts::PI).unwrap();

        // Draw the left eye.
        context.move_to(65.0, 65.0);
        context
            .arc(60.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the right eye.
        context.move_to(95.0, 65.0);
        context
            .arc(90.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        context.stroke();
*/
        Ok(())
    }
}

D client/src/rendering/renderer_playercentric.rs => client/src/rendering/renderer_playercentric.rs +0 -120
@@ 1,120 0,0 @@
use std::error::Error;
use async_trait::async_trait;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlImageElement};
use crate::rendering::Renderer;
use wasm_bindgen::{JsCast, JsValue};
use crate::CLIENT;

// TODO: Remove all the f32s

pub const STARFIELD_RENDER_SCALE: f64 = 1.0;

#[derive(Debug)]
pub struct WebRenderer {
    canvas_element_id: String
}

#[async_trait]
impl Renderer for WebRenderer {
    async fn get(canvas_element_id: &str) -> Result<Self, Box<dyn Error>> {
        Ok(Self {
            canvas_element_id: canvas_element_id.to_string()
        })
    }

    async fn render_frame(&self, _time_delta_ms: f64) -> Result<(), Box<dyn Error>> {
        // TODO - terra is working on this, please no touchy without telling him
        // TODO - until this notice is removed
        // time_delta_ms is the delta, in ms, from when the last render_frame was called by the browser
        let window = web_sys::window().ok_or("window needs to exist")?;
        let document = window.document().ok_or("window.document needs to exist")?;
        let canvas_element = document.get_element_by_id(&self.canvas_element_id).ok_or("canvas element does not exist")?;
        let typed_canvas_element: HtmlCanvasElement = canvas_element.dyn_into::<web_sys::HtmlCanvasElement>().map_err(|_| ()).unwrap();
        let context = typed_canvas_element.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
        let client = CLIENT.read()?;
        if client.client_data.is_none() {
            return Err("client not yet initialized".into());
        }
        let _client_data = client.client_data.as_ref().unwrap();

        //let camera_translate_x = -client.x + (typed_canvas_element.width() / 2) as f64;
        let viewer_size_x = typed_canvas_element.width() as f64;
        //let camera_translate_y = -client.y + (typed_canvas_element.height() / 2) as f64;
        let viewer_size_y = typed_canvas_element.height() as f64;

        typed_canvas_element.style().set_property("background-position", &format!("{}px {}px", -client.x / STARFIELD_RENDER_SCALE, -client.y / STARFIELD_RENDER_SCALE)).map_err(|e| e.as_string().unwrap())?;

        context.set_transform(1f64, 0f64, 0f64, 1f64, 0f64, 0f64).map_err(|e: JsValue| e.as_string().unwrap())?;
        context.clear_rect(0f64, 0f64, viewer_size_x, viewer_size_y);

        // *dont* translate the camera. we're movign everything else around us. cameracentrism.
        // only translation will be to center our core module.
        //context.translate(camera_translate_x, camera_translate_y).map_err(|e: JsValue| e.as_string().unwrap())?;
        context.translate(viewer_size_x / 2.0, viewer_size_y / 2.0).map_err(|e: JsValue| e.as_string().unwrap())?;

        for planet in &client.planets {
            //context.save();

            //context.set_transform(1f64, 0f64, 0f64, 1f64, 0f64, 0f64).map_err(|e: JsValue| e.as_string().unwrap())?;
            //context.translate(-planet.x, -planet.y).map_err(|e: JsValue| e.as_string().unwrap())?;

            let texture_image = document.get_element_by_id(&format!("tex-{}", planet.planet_type.unwrap().as_texture_id())).unwrap().dyn_into::<HtmlImageElement>().unwrap();
            // pos:
            //debug!("P {} {}", planet.x - planet.radius - client.x, planet.y - planet.radius - client.y);
            context.draw_image_with_html_image_element_and_dw_and_dh(&texture_image, (planet.x - planet.radius - client.x as f32) as f64, (planet.y - planet.radius - client.y as f32) as f64, planet.radius as f64 * 2f64, planet.radius as f64 * 2f64).map_err(|e: JsValue| e.as_string().unwrap())?;

            //context.restore();
        }

        for player in &client.players {
            context.save();

            //context.translate(player.x, player.y).map_err(|e: JsValue| e.as_string().unwrap())?;
            //gaah fuck why noo i didnt want this. godforsaken canvas rotation
            context.translate(player.x as f64 - client.x, player.y as f64 - client.y).map_err(|e: JsValue| e.as_string().unwrap())?; // fwip

            context.set_text_align("center");
            context.set_font("30px Segoe UI");
            context.set_fill_style(&JsValue::from_str("white")); // CssStyleColor
            context.fill_text(&player.username, 0f64, -35f64).map_err(|e: JsValue| e.as_string().unwrap())?;

            context.rotate(player.rotation as f64).map_err(|e: JsValue| e.as_string().unwrap())?; // fwip

            let texture_image = document.get_element_by_id("tex-hearty").unwrap().dyn_into::<HtmlImageElement>().unwrap();
            //context.draw_image_with_html_image_element_and_dw_and_dh(&texture_image, player.x - 25f64 - client.x, player.y - 25f64 - client.y, 50f64, 50f64).map_err(|e: JsValue| e.as_string().unwrap())?;
            context.draw_image_with_html_image_element_and_dw_and_dh(&texture_image, -25f64, -25f64, 50f64, 50f64).map_err(|e: JsValue| e.as_string().unwrap())?; // sktch

            //context.rotate(-player.rotation).map_err(|e: JsValue| e.as_string().unwrap())?; // fwoop

            context.restore();
        }

/*
        context.begin_path();

        // Draw the outer circle.
        context
            .arc(75.0, 75.0, 50.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the mouth.
        context.move_to(110.0, 75.0);
        context.arc(75.0, 75.0, 35.0, 0.0, std::f64::consts::PI).unwrap();

        // Draw the left eye.
        context.move_to(65.0, 65.0);
        context
            .arc(60.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the right eye.
        context.move_to(95.0, 65.0);
        context
            .arc(90.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        context.stroke();
*/
        Ok(())
    }
}

D client/src/rendering/util.rs => client/src/rendering/util.rs +0 -8
@@ 1,8 0,0 @@
use web_sys::HtmlImageElement;
use wasm_bindgen::JsCast;

pub fn texid_to_html_image_unchecked(tex: &str) -> HtmlImageElement {
    let window = web_sys::window().expect("window needs to exist");
    let document = window.document().expect("window.document needs to exist");
    document.get_element_by_id(&format!("tex-{}", tex)).unwrap().dyn_into::<HtmlImageElement>().unwrap()
}
\ No newline at end of file

D client/src/textures/loader_fast.rs => client/src/textures/loader_fast.rs +0 -96
@@ 1,96 0,0 @@
use std::collections::HashMap;
use std::error::Error;
use std::io::Cursor;
use base64::Engine;
use image::ImageOutputFormat;
use log::debug;
use serde::{Deserialize, Serialize};
use crate::textures::{TextureManager, TextureSize};

pub const SPRITESHEET_IMAGE_FILE_FULL: &[u8] = include_bytes!("../../../assets/dist/spritesheet-full.png");
pub const SPRITESHEET_DATA_FILE_FULL: &str = include_str!("../../../assets/dist/spritesheet-full.ron");

pub const SPRITESHEET_IMAGE_FILE_375: &[u8] = include_bytes!("../../../assets/dist/spritesheet-375.png");
pub const SPRITESHEET_DATA_FILE_375: &str = include_str!("../../../assets/dist/spritesheet-375.ron");

pub const SPRITESHEET_IMAGE_FILE_125: &[u8] = include_bytes!("../../../assets/dist/spritesheet-125.png");
pub const SPRITESHEET_DATA_FILE_125: &str = include_str!("../../../assets/dist/spritesheet-125.ron");

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SpritePosition {
    pub name: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub offsets: Option<[f32; 2]>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SerializedSpriteSheet {
    pub texture_width: f32,
    pub texture_height: f32,
    pub sprites: Vec<SpritePosition>,
}

#[derive(Debug)]
pub struct TextureLoader {
    pub sprites: HashMap<String, String>
}
impl TextureManager for TextureLoader {
    fn load(size: TextureSize) -> Result<Self, Box<dyn Error>> where Self: Sized {
        debug!("Loading textures - starting fast texture loader (size: {})", size.to_string());
        let start = js_sys::Date::now() as u64;
        // load the generated spritesheet data
        let spritesheet_data: SerializedSpriteSheet = ron::from_str(pick_data_file(size))?;

        // load the generated spritesheet image
        let spritesheet_image = image::load_from_memory(pick_image_file(size))?;

        if spritesheet_image.width() as f32 != spritesheet_data.texture_width {
            return Err("Image width mismatch between spritesheet and data file".into());
        }
        if spritesheet_image.height() as f32 != spritesheet_data.texture_height {
            return Err("Image height mismatch between spritesheet and data file".into());
        }

        let mut sprites = HashMap::new();

        for sprite in spritesheet_data.sprites {
            debug!("Loading texture {} ({}x{}, start at {}, {})", sprite.name, sprite.width, sprite.height, sprite.x, sprite.y);
            let sprite_img = spritesheet_image.crop_imm(sprite.x as u32, sprite.y as u32, sprite.width as u32, sprite.height as u32);
            let mut image_data: Vec<u8> = Vec::new();
            sprite_img.write_to(&mut Cursor::new(&mut image_data), ImageOutputFormat::Png)
                .unwrap();
            let res_base64 = base64::engine::general_purpose::STANDARD.encode(image_data);
            sprites.insert(sprite.name, format!("data:image/png;base64,{}", res_base64));
        }

        let end = js_sys::Date::now() as u64;
        debug!("Loaded {} sprites from spritesheet in {} ms", sprites.len(), end - start);

        Ok(Self {
            sprites,
        })
    }

    fn get_texture(&self, texture_id: &str) -> Option<String> {
        self.sprites.get(texture_id).map(|u| u.clone())
    }
}

fn pick_data_file(for_size: TextureSize) -> &'static str {
    match for_size {
        TextureSize::Full => SPRITESHEET_DATA_FILE_FULL,
        TextureSize::Scaled375 => SPRITESHEET_DATA_FILE_375,
        TextureSize::Scaled125 => SPRITESHEET_DATA_FILE_125
    }
}

fn pick_image_file(for_size: TextureSize) -> &'static [u8] {
    match for_size {
        TextureSize::Full => SPRITESHEET_IMAGE_FILE_FULL,
        TextureSize::Scaled375 => SPRITESHEET_IMAGE_FILE_375,
        TextureSize::Scaled125 => SPRITESHEET_IMAGE_FILE_125
    }
}
\ No newline at end of file

D client/src/textures/loader_slow.rs => client/src/textures/loader_slow.rs +0 -20
@@ 1,20 0,0 @@
use crate::textures::{TextureManager, TextureSize};
use std::error::Error;
use log::debug;

#[derive(Debug)]
pub struct TextureLoader {
    size: TextureSize
}
impl TextureManager for TextureLoader {
    fn load(size: TextureSize) -> Result<Self, Box<dyn Error>> where Self: Sized {
        debug!("Using slow texture loader, textures will be loaded on-the-fly");
        Ok(TextureLoader {
            size
        })
    }

    fn get_texture(&self, texture_id: &str) -> Option<String> {
        Some(format!("/assets/final/{}/{}.png", self.size.to_string(), texture_id))
    }
}
\ No newline at end of file

D client/src/textures/mod.rs => client/src/textures/mod.rs +0 -48
@@ 1,48 0,0 @@
use std::error::Error;
use std::str::FromStr;

#[cfg(all(feature = "textures-fast", feature = "textures-slow"))]
compile_error!("Mutually exclusive modules textures-fast and textures-slow selected.");
#[cfg(not(any(feature = "textures-fast", feature = "textures-slow")))]
compile_error!("Required feature textures not specified. Please specify one of textures-fast, textures-slow");

#[cfg(feature = "textures-fast")]
#[path = "loader_fast.rs"]
pub mod loader;

#[cfg(feature = "textures-slow")]
#[path = "loader_slow.rs"]
pub mod loader;

pub trait TextureManager {
    fn load(size: TextureSize) -> Result<Self, Box<dyn Error>> where Self: Sized;
    fn get_texture(&self, texture_id: &str) -> Option<String>;
}

#[derive(Debug, Copy, Clone)]
pub enum TextureSize {
    Full,
    Scaled375,
    Scaled125
}
impl ToString for TextureSize {
    fn to_string(&self) -> String {
        match self {
            TextureSize::Full => "full".to_string(),
            TextureSize::Scaled375 => "375".to_string(),
            TextureSize::Scaled125 => "125".to_string()
        }
    }
}
impl FromStr for TextureSize {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "full" => Ok(TextureSize::Full),
            "375" => Ok(TextureSize::Scaled375),
            "125" => Ok(TextureSize::Scaled125),
            _ => Err(())
        }
    }
}
\ No newline at end of file