~starkingdoms/starkingdoms

29a5457af664e7f51c1f89ad04f472bd242b3f98 — core 2 years ago e591c22
code cleanup pt1
M Cargo.lock => Cargo.lock +3 -2
@@ 511,9 511,9 @@ dependencies = [

[[package]]
name = "async-tungstenite"
version = "0.21.0"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2c69d237cf761215175f390021344f5530101cca8164d69878b8a1779e80c"
checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58"
dependencies = [
 "futures-io",
 "futures-util",


@@ 3436,6 3436,7 @@ dependencies = [
 "log",
 "nalgebra",
 "parking_lot 0.12.1",
 "protobuf 3.2.0",
 "rand",
 "rapier2d-f64",
 "reqwest",

M api/src/config.rs => api/src/config.rs +1 -1
@@ 32,7 32,7 @@ pub struct StarkingdomsApiConfig {
    pub base: String,
    pub game: String,
    pub realms: HashMap<String, StarkingdomsApiConfigRealm>,
    pub servers: Vec<String>
    pub servers: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]

M api/src/main.rs => api/src/main.rs +1 -1
@@ 1,5 1,6 @@
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use actix_cors::Cors;
use actix_request_identifier::RequestIdentifier;
use actix_web::http::header::HeaderValue;
use actix_web::web::{Data, JsonConfig};


@@ 9,7 10,6 @@ use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use starkingdoms_api_migration::{Migrator, MigratorTrait};
use std::error::Error;
use std::time::Duration;
use actix_cors::Cors;
use tera::Tera;

pub mod config;

M api/src/routes/mod.rs => api/src/routes/mod.rs +1 -1
@@ 2,4 2,4 @@ pub mod beamin;
pub mod beamout;
pub mod callback;
pub mod select_realm;
pub mod server_list;
\ No newline at end of file
pub mod server_list;

M api/src/routes/select_realm.rs => api/src/routes/select_realm.rs +1 -1
@@ 16,7 16,7 @@ pub struct RealmsListTemplateContext {

#[get("/select-realm")]
pub async fn select_realm(state: Data<AppState>) -> HttpResponse {
    let context = match Context::from_serialize(&RealmsListTemplateContext {
    let context = match Context::from_serialize(RealmsListTemplateContext {
        back_to: format!("{}/callback", CONFIG.base),
        realms: CONFIG.realms.clone(),
    }) {

M api/src/routes/server_list.rs => api/src/routes/server_list.rs +4 -4
@@ 1,11 1,11 @@
use actix_web::{get};
use crate::config::CONFIG;
use actix_web::get;
use actix_web::web::Json;
use serde::Serialize;
use crate::config::CONFIG;

#[derive(Serialize)]
pub struct ServerListResponse {
    pub servers: Vec<String>
    pub servers: Vec<String>,
}

#[get("server-list")]


@@ 13,4 13,4 @@ pub async fn server_list() -> Json<ServerListResponse> {
    Json(ServerListResponse {
        servers: CONFIG.servers.clone(),
    })
}
\ No newline at end of file
}

M client/index.html => client/index.html +0 -4
@@ 115,9 115,5 @@
            width: 100%;
        }

        .w-90 {
            width: 90%;
        }

    </style>
</html>

M protocol/src/lib.rs => protocol/src/lib.rs +18 -15
@@ 1,11 1,11 @@
use crate::message_c2s::{
    MessageC2SAuthenticateAndBeamOut, MessageC2SChat, MessageC2SGoodbye, MessageC2SHello,
    MessageC2SInput, MessageC2SModuleDetach, MessageC2SModuleGrabBegin, MessageC2SModuleGrabEnd,
    MessageC2SMouseInput, MessageC2SPing
    MessageC2SMouseInput, MessageC2SPing,
};
use crate::message_s2c::{
    MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CModulesUpdate,
    MessageS2CModuleAdd, MessageS2CModuleRemove, MessageS2CModuleTreeUpdate,
    MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CModuleAdd,
    MessageS2CModuleRemove, MessageS2CModuleTreeUpdate, MessageS2CModulesUpdate,
    MessageS2CPlanetData, MessageS2CPlayersUpdate, MessageS2CPong,
};
use crate::planet::PlanetType;


@@ 50,7 50,7 @@ impl TryFrom<&[u8]> for MessageC2S {
    type Error = Box<dyn Error>;

    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
        let pkt = starkingdoms_protocol::PacketWrapper::parse_from_bytes(value)?;
        let pkt = PacketWrapper::parse_from_bytes(value)?;

        let deser_pkt = match pkt.packet_id {
            _id if _id == message_c2s::message_c2shello::Packet_info::type_.value() as i64 => {


@@ 84,17 84,23 @@ impl TryFrom<&[u8]> for MessageC2S {
            _id if _id
                == message_c2s::message_c2smodule_grab_begin::Packet_info::type_.value() as i64 =>
            {
                MessageC2S::ModuleGrabBegin(MessageC2SModuleGrabBegin::parse_from_bytes(&pkt.packet_data)?)
                MessageC2S::ModuleGrabBegin(MessageC2SModuleGrabBegin::parse_from_bytes(
                    &pkt.packet_data,
                )?)
            }
            _id if _id
                == message_c2s::message_c2smodule_grab_end::Packet_info::type_.value() as i64 =>
            {
                MessageC2S::ModuleGrabEnd(MessageC2SModuleGrabEnd::parse_from_bytes(&pkt.packet_data)?)
                MessageC2S::ModuleGrabEnd(MessageC2SModuleGrabEnd::parse_from_bytes(
                    &pkt.packet_data,
                )?)
            }
            _id if _id
                == message_c2s::message_c2smodule_detach::Packet_info::type_.value() as i64 =>
            {
                MessageC2S::ModuleDetach(MessageC2SModuleDetach::parse_from_bytes(&pkt.packet_data)?)
                MessageC2S::ModuleDetach(MessageC2SModuleDetach::parse_from_bytes(
                    &pkt.packet_data,
                )?)
            }
            _id => {
                return Err(format!("Unrecognized C2S packet {}", _id).into());


@@ 201,18 207,15 @@ impl TryFrom<&[u8]> for MessageS2C {
                )?)
            }
            _id if _id
                == message_s2c::message_s2cmodule_tree_update::Packet_info::type_.value() as i64 =>
                == message_s2c::message_s2cmodule_tree_update::Packet_info::type_.value()
                    as i64 =>
            {
                MessageS2C::ModuleTreeUpdate(MessageS2CModuleTreeUpdate::parse_from_bytes(
                    &pkt.packet_data,
                )?)
            }
            _id if _id
                == message_s2c::message_s2cmodule_add::Packet_info::type_.value() as i64 =>
            {
                MessageS2C::ModuleAdd(MessageS2CModuleAdd::parse_from_bytes(
                    &pkt.packet_data,
                )?)
            _id if _id == message_s2c::message_s2cmodule_add::Packet_info::type_.value() as i64 => {
                MessageS2C::ModuleAdd(MessageS2CModuleAdd::parse_from_bytes(&pkt.packet_data)?)
            }
            _id if _id
                == message_s2c::message_s2cmodule_remove::Packet_info::type_.value() as i64 =>


@@ 287,7 290,7 @@ impl TryInto<Vec<u8>> for MessageS2C {
    }
}

impl planet::PlanetType {
impl PlanetType {
    pub fn as_texture_id(&self) -> String {
        match self {
            PlanetType::Earth => "earth".to_string(),

M server/Cargo.toml => server/Cargo.toml +3 -1
@@ 17,7 17,7 @@ serde_json = "1"
futures = { version = "0.3", default-features = false }

tungstenite = { version = "0.19.0", default-features = false }
async-tungstenite = "0.21.0"
async-tungstenite = "0.22.1"

log = "0.4"
simple_logger = "4.1"


@@ 28,6 28,8 @@ nalgebra = "0.32.2"
rand = "0.8.5"
reqwest = "0.11.16"

protobuf = "3.2.0"

parking_lot = { version = "0.12.1", features = ["deadlock_detection"] }

[build-dependencies]

M server/src/api.rs => server/src/api.rs +3 -3
@@ 31,7 31,7 @@ pub async fn load_player_data_from_api(
    };

    let res = client
        .post(format!("{}/beamin", std::env::var("STK_API_URL").unwrap()))
        .post(format!("{}/beamin", std::env::var("STK_API_URL")?))
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&req_body)?)
        .send()


@@ 75,11 75,11 @@ pub async fn save_player_data_to_api(
        api_token: internal_token.to_owned(),
        user_auth_realm_id: user_id.to_owned(),
        user_auth_token: token.to_owned(),
        data: data.to_owned(),
        data: data.clone(),
    };

    let res = client
        .post(format!("{}/beamout", std::env::var("STK_API_URL").unwrap()))
        .post(format!("{}/beamout", std::env::var("STK_API_URL")?))
        .header("Content-Type", "application/json")
        .body(serde_json::to_string(&req_body)?)
        .send()

M server/src/entity.rs => server/src/entity.rs +14 -17
@@ 1,12 1,14 @@
use std::{collections::HashMap, net::SocketAddr, sync::atomic::AtomicU32};

use nalgebra::Vector2;
use protobuf::SpecialFields;
use starkingdoms_protocol::planet::PlanetType;

use crate::{
    manager::{ClientHandlerMessage, Player},
    module::{AttachedModule, Module},
    planet::Planet,
    SCALE, module::{Module, AttachedModule},
    SCALE,
};

pub type EntityId = u32;


@@ 15,9 17,7 @@ static mut ENTITY_ID_COUNT: AtomicU32 = AtomicU32::new(0);
pub fn get_entity_id() -> EntityId {
    let last_entity_id = unsafe { &ENTITY_ID_COUNT };
    let id = last_entity_id.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
    if id > 4_147_483_600 {
        panic!("No remaining entity ids")
    };
    assert!(id <= 4_147_483_600, "No remaining entity ids");
    id
}



@@ 27,8 27,8 @@ pub struct EntityHandler {
}

impl EntityHandler {
    pub fn new() -> EntityHandler {
        EntityHandler {
    pub fn new() -> Self {
        Self {
            entities: Entities::new(),
        }
    }


@@ 71,7 71,7 @@ impl EntityHandler {
        }
    }
    pub fn get_player_id(&self, addr: SocketAddr) -> Option<EntityId> {
        for (id, entity) in self.entities.iter() {
        for (id, entity) in &self.entities {
            if let Entity::Player(player) = entity {
                if player.addr == addr {
                    return Some(*id);


@@ 83,10 83,7 @@ impl EntityHandler {
    pub fn get_player(&self, addr: SocketAddr) -> Option<Player> {
        let players = self.get_players();
        let player = players.iter().find(|p| p.0 == addr);
        match player {
            Some(p) => Some(p.1.clone()),
            None => None,
        }
        player.map(|p| p.1.clone())
    }
    pub fn get_modules(&self) -> Vec<Module> {
        let mut modules = Vec::new();


@@ 99,9 96,9 @@ impl EntityHandler {
    }
    pub fn get_modules_id(&self) -> Vec<(EntityId, Module)> {
        let mut modules = Vec::new();
        for (id, entity) in self.entities.clone().into_iter() {
        for (id, entity) in self.entities.clone() {
            if let Entity::Module(module) = entity {
                modules.push((id as u32, module.clone()));
                modules.push((id, module.clone()));
            }
        }
        modules


@@ 122,7 119,7 @@ impl EntityHandler {
        None
    }
    pub fn get_from_module(&self, p_module: &Module) -> Option<EntityId> {
        for (id, entity) in self.entities.iter() {
        for (id, entity) in &self.entities {
            if let Entity::Module(module) = entity {
                if module.handle == p_module.handle {
                    return Some(*id);


@@ 146,8 143,8 @@ impl EntityHandler {
        }
        None
    }
    pub fn get_id_from_attached(&self, p_module: AttachedModule) -> Option<EntityId> {
        for (id, entity) in self.entities.iter() {
    pub fn get_id_from_attached(&self, p_module: &AttachedModule) -> Option<EntityId> {
        for (id, entity) in &self.entities {
            if let Entity::AttachedModule(module) = entity {
                if module.handle == p_module.handle {
                    return Some(*id);


@@ 177,7 174,7 @@ impl EntityHandler {
                x: planet.position.0 * SCALE,
                y: planet.position.1 * SCALE,
                radius: planet.radius * SCALE, // DO NOT * SCALE. THIS VALUE IS NOT SCALED! YES IT IS
                special_fields: Default::default(),
                special_fields: SpecialFields::default(),
            });
        }


M server/src/handler.rs => server/src/handler.rs +148 -71
@@ 1,8 1,6 @@
use crate::api::{load_player_data_from_api, save_player_data_to_api};
use crate::entity::{get_entity_id, Entity, EntityHandler};
use crate::manager::{
    ClientHandlerMessage, ClientManager, Player, PhysicsData,
};
use crate::manager::{ClientHandlerMessage, ClientManager, PhysicsData, Player};
use crate::module::{AttachedModule, Module};
use crate::{recv, send, SCALE};
use async_std::net::TcpStream;


@@ 14,12 12,13 @@ use log::{error, info, warn};
use nalgebra::{point, vector, Vector2};
use rand::Rng;
use rapier2d_f64::prelude::{
    Collider, ColliderBuilder, MassProperties, RigidBodyBuilder, RigidBodyType, Isometry,
    Collider, ColliderBuilder, Isometry, MassProperties, RigidBodyBuilder, RigidBodyType,
};
use starkingdoms_protocol::goodbye_reason::GoodbyeReason;
use starkingdoms_protocol::message_s2c::{
    MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CModulesUpdate,
    MessageS2CPlanetData, MessageS2CPlayersUpdate, MessageS2CPong, MessageS2CModuleAdd, MessageS2CModuleTreeUpdate, MessageS2CModuleRemove,
    MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CModuleAdd,
    MessageS2CModuleRemove, MessageS2CModuleTreeUpdate, MessageS2CModulesUpdate,
    MessageS2CPlanetData, MessageS2CPlayersUpdate, MessageS2CPong,
};

use starkingdoms_protocol::state::State;


@@ 29,6 28,7 @@ use std::f64::consts::PI;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use protobuf::SpecialFields;
use tungstenite::Message;

pub async fn handle_client(


@@ 123,6 123,8 @@ pub async fn handle_client(
                    match pkt {
                        MessageC2S::Hello(pkt) => {
                            info!("client sent hello");
                            // there is no way to not use unwrap here :/
                            #[allow(clippy::unwrap_used)]
                            if !matches!(pkt.next_state.unwrap(), State::Play) {
                                error!(
                                    "client sent unexpected state {:?} (expected: Play)",


@@ 130,7 132,7 @@ pub async fn handle_client(
                                );
                                let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                    reason: GoodbyeReason::UnexpectedNextState.into(),
                                    special_fields: Default::default(),
                                    special_fields: SpecialFields::default(),
                                })
                                .try_into()?;
                                send!(client_tx, msg).await?;


@@ 186,7 188,7 @@ pub async fn handle_client(
                            let msg = MessageS2C::Hello(MessageS2CHello {
                                version: pkt.version,
                                given_username: pkt.requested_username.clone(),
                                special_fields: Default::default(),
                                special_fields: SpecialFields::default(),
                                next_state: pkt.next_state,
                            })
                            .try_into()?;


@@ 252,7 254,7 @@ pub async fn handle_client(
                                    let player_data = match load_player_data_from_api(
                                        &pkt.token,
                                        &pkt.user,
                                        &std::env::var("STK_API_KEY").unwrap(),
                                        &std::env::var("STK_API_KEY")?,
                                    )
                                    .await
                                    {


@@ 317,7 319,6 @@ pub async fn handle_client(
                                    },
                                    2,
                                );*/

                            }
                        }
                        MessageC2S::Goodbye(pkt) => {


@@ 331,7 332,7 @@ pub async fn handle_client(
                            );
                            let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                reason: GoodbyeReason::UnexpectedPacket.into(),
                                special_fields: Default::default(),
                                special_fields: SpecialFields::default(),
                            })
                            .try_into()?;
                            send!(client_tx, msg).await?;


@@ 347,7 348,7 @@ pub async fn handle_client(
                        );
                        let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                            reason: GoodbyeReason::UnexpectedPacket.into(),
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;


@@ 388,11 389,12 @@ pub async fn handle_client(
                        let mut handle = entities.write().await;
                        let id = handle
                            .get_player_id(remote_addr)
                            .expect("could not get player id");
                            .ok_or("could not get player id")?;

                        if let Entity::Player(ref mut me) = handle
                            .entities
                            .get_mut(&id)
                            .expect("player disconnected but continued to send packets")
                            .ok_or("player disconnected but continued to send packets")?
                        {
                            me.input.up = p.up_pressed;
                            me.input.down = p.down_pressed;


@@ 410,7 412,7 @@ pub async fn handle_client(
                            .read()
                            .await
                            .get_player(remote_addr)
                            .expect("Player sending messages after disconnect");
                            .ok_or("Player sending messages after disconnect")?;

                        if Some(p.token) != player.auth_token || Some(p.user_id) != player.auth_user
                        {


@@ 420,9 422,9 @@ pub async fn handle_client(

                        match save_player_data_to_api(
                            &player.as_api_data(),
                            &player.auth_token.unwrap(),
                            &player.auth_user.unwrap(),
                            &std::env::var("STK_API_KEY").unwrap(),
                            &player.auth_token.ok_or("Tried to beamout without an auth token")?,
                            &player.auth_user.ok_or("Tried to beamout without setting a user")?,
                            &std::env::var("STK_API_KEY")?,
                        )
                        .await
                        {


@@ 451,19 453,24 @@ pub async fn handle_client(
                        //debug!("[{}] detach: {:?}", remote_addr, p);
                        //debug!("[{}] {:?}", remote_addr, entities.entities);

                        if let Some(Entity::AttachedModule(p_module)) = entities.entities.get_mut(&p.module_id) {
                        if let Some(Entity::AttachedModule(p_module)) =
                            entities.entities.get_mut(&p.module_id)
                        {
                            module = Some(p_module.clone());
                        } else {
                            warn!("[{}] attempted to detach nonexistent module", remote_addr);
                            continue;
                        }

                        let player_id = entities.get_player_id(remote_addr).unwrap();
                        let module_id = AttachedModule::detach(&mut data_handle, &mut entities,
                                               player_id,
                                               module.unwrap());
                        let module = entities.get_module_from_id(module_id).unwrap();
                        let body = data_handle.rigid_body_set.get(module.handle).unwrap();
                        let player_id = entities.get_player_id(remote_addr).ok_or("player does not exist")?;
                        let module_id = AttachedModule::detach(
                            &mut data_handle,
                            &mut entities,
                            player_id,
                            module.ok_or("cannot detach module that doesn't exist")?,
                        ).ok_or("detach failed")?;
                        let module = entities.get_module_from_id(module_id).ok_or("player does not exist")?;
                        let body = data_handle.rigid_body_set.get(module.handle).ok_or("module rigidbody does not exist")?;
                        let prot_module = starkingdoms_protocol::module::Module {
                            module_type: module.module_type.into(),
                            rotation: body.rotation().angle(),


@@ 471,17 478,23 @@ pub async fn handle_client(
                            y: body.translation().y * SCALE,
                            id: module_id,
                            flags: module.flags,
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        };
                        let msg = MessageS2C::ModuleRemove(MessageS2CModuleRemove {
                            module: Some(prot_module).into(),
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                    }
                    MessageC2S::ModuleGrabBegin(p) => {
                        if let Entity::Module(_module) = entities.write().await.entities.get_mut(&p.module_id).unwrap() {
                        if let Entity::Module(_module) = entities
                            .write()
                            .await
                            .entities
                            .get_mut(&p.module_id)
                            .ok_or("module does not exist")?
                        {
                            //debug!("[{}] grab begin: {:?}, flags: {}", remote_addr, p, module.flags);
                        }
                    }


@@ 490,75 503,139 @@ pub async fn handle_client(
                        let mut module: Option<Module> = None;
                        let mut did_attach = false;
                        let mut attached_id = None;
                        if let Entity::Module(p_module) = entities.entities.get_mut(&p.module_id).unwrap() {
                        if let Entity::Module(p_module) =
                            entities.entities.get_mut(&p.module_id).ok_or("module does not exist")?
                        {
                            module = Some(p_module.clone());
                            //debug!("[{}] grab end: {:?}", remote_addr, p);
                        }
                        let mut data_handle = data.write().await;
                        let player_id = entities.get_player_id(remote_addr).unwrap();
                        let player = entities.get_player(remote_addr).unwrap();
                        let body = data_handle.rigid_body_set.get(player.handle).unwrap();
                        let (x, y) = (body.translation().x - p.worldpos_x/SCALE, body.translation().y - p.worldpos_y/SCALE);
                        let player_id = entities.get_player_id(remote_addr).ok_or("player entity does not exist")?;
                        let player = entities.get_player(remote_addr).ok_or("player does not exist")?;
                        let body = data_handle.rigid_body_set.get(player.handle).ok_or("player rigidbody does not exist")?;
                        let (x, y) = (
                            body.translation().x - p.worldpos_x / SCALE,
                            body.translation().y - p.worldpos_y / SCALE,
                        );
                        let angle = -body.rotation().angle();
                        let (x, y) = (x*angle.cos() - y*angle.sin(), x*angle.sin() + y*angle.cos());
                        let (x, y) = (
                            x.mul_add(angle.cos(), -y * angle.sin()),
                            x.mul_add(angle.sin(), y * angle.cos()),
                        );
                        if 1.5 < y && y < 3. && -2. < x && x < 2. {
                            attached_id = Some(AttachedModule::attach(&mut data_handle, &mut entities, 
                                                   player_id,
                                                   player_id, module.clone().unwrap(), 2));
                            attached_id = Some(AttachedModule::attach(
                                &mut data_handle,
                                &mut entities,
                                player_id,
                                player_id,
                                module.clone().ok_or("module is None")?,
                                2,
                            ));
                            did_attach = true;
                        } else if -3. < y && y < -1.5 && -2. < x && x < 2. {
                            attached_id = Some(AttachedModule::attach(&mut data_handle, &mut entities, 
                                                   player_id,
                                                   player_id, module.clone().unwrap(), 0));
                            attached_id = Some(AttachedModule::attach(
                                &mut data_handle,
                                &mut entities,
                                player_id,
                                player_id,
                                module.clone().ok_or("module is None")?,
                                0,
                            ));
                            did_attach = true;
                        } else if -3. < x && x < -1.5 && -2. < y && y < 2. {
                            attached_id = Some(AttachedModule::attach(&mut data_handle, &mut entities, 
                                                   player_id,
                                                   player_id, module.clone().unwrap(), 3));
                            attached_id = Some(AttachedModule::attach(
                                &mut data_handle,
                                &mut entities,
                                player_id,
                                player_id,
                                module.clone().ok_or("module is None")?,
                                3,
                            ));
                            did_attach = true;
                        } else if 1.5 < x && x < 3. && -2. < y && y < 2. {
                            attached_id = Some(AttachedModule::attach(&mut data_handle, &mut entities, 
                                                   player_id,
                                                   player_id, module.clone().unwrap(), 1));
                            attached_id = Some(AttachedModule::attach(
                                &mut data_handle,
                                &mut entities,
                                player_id,
                                player_id,
                                module.clone().ok_or("module is None")?,
                                1,
                            ));
                            did_attach = true;
                        }
                        let modules = player.search_modules(&entities);
                        for attached in modules {
                            let body = data_handle.rigid_body_set.get(attached.handle).unwrap();
                            let (x, y) = (body.translation().x - p.worldpos_x/SCALE, body.translation().y - p.worldpos_y/SCALE);
                            let body = data_handle.rigid_body_set.get(attached.handle).ok_or("attached module rigidbody does not exist")?;
                            let (x, y) = (
                                body.translation().x - p.worldpos_x / SCALE,
                                body.translation().y - p.worldpos_y / SCALE,
                            );
                            let angle = -body.rotation().angle();
                            let (x, y) = (x*angle.cos() - y*angle.sin(), x*angle.sin() + y*angle.cos());
                            let parent_id = entities.get_id_from_attached(attached).unwrap();
                            let (x, y) = (
                                x.mul_add(angle.cos(), -y * angle.sin()),
                                x.mul_add(angle.sin(), y * angle.cos()),
                            );
                            let parent_id = entities.get_id_from_attached(&attached).ok_or("attached module does not exist")?;

                            // ghostly: this is cursed as hell
                            // please find a better way in the future lmao
                            if 1.5 < y && y < 3. && -2. < x && x < 2. {
                                attached_id = Some(AttachedModule::attach(&mut data_handle, &mut entities, 
                                                       parent_id,
                                                       player_id, module.clone().unwrap(), 2));
                                attached_id = Some(AttachedModule::attach(
                                    &mut data_handle,
                                    &mut entities,
                                    parent_id,
                                    player_id,
                                    module.clone().ok_or("module does not exist")?,
                                    2,
                                ));
                                did_attach = true;
                            } else if -3. < x && x < -1.5 && -2. < y && y < 2. {
                                attached_id = Some(AttachedModule::attach(&mut data_handle, &mut entities, 
                                                       parent_id,
                                                       player_id, module.clone().unwrap(), 3));
                                attached_id = Some(AttachedModule::attach(
                                    &mut data_handle,
                                    &mut entities,
                                    parent_id,
                                    player_id,
                                    module.clone().ok_or("module does not exist")?,
                                    3,
                                ));
                                did_attach = true;
                            } else if 1.5 < x && x < 3. && -2. < y && y < 2. {
                                attached_id = Some(AttachedModule::attach(&mut data_handle, &mut entities, 
                                                       parent_id,
                                                       player_id, module.clone().unwrap(), 1));
                                attached_id = Some(AttachedModule::attach(
                                    &mut data_handle,
                                    &mut entities,
                                    parent_id,
                                    player_id,
                                    module.clone().ok_or("module does not exist")?,
                                    1,
                                ));
                                did_attach = true;
                            }
                        }
                        if did_attach == false {
                            let body = data_handle.rigid_body_set.get_mut(module.unwrap().handle).unwrap();
                            body.set_position(Isometry::new(Vector2::new(p.worldpos_x/SCALE, p.worldpos_y/SCALE),body.rotation().angle()), true);
                        if !did_attach {
                            let body = data_handle
                                .rigid_body_set
                                .get_mut(module.ok_or("module does not exist")?.handle)
                                .ok_or("module rigidbody does not exist")?;
                            body.set_position(
                                Isometry::new(
                                    Vector2::new(p.worldpos_x / SCALE, p.worldpos_y / SCALE),
                                    body.rotation().angle(),
                                ),
                                true,
                            );
                        } else if let Some(Ok(id)) = attached_id {
                            let prot_module = entities
                                .get_attached_from_id(id)
                                .ok_or("attached module does not exist")?
                                .to_protocol(&entities, &data_handle.rigid_body_set);
                            let msg = MessageS2C::ModuleAdd(MessageS2CModuleAdd {
                                module: Some(prot_module.ok_or("attached module does not exist")?).into(),
                                special_fields: SpecialFields::default(),
                            })
                            .try_into()?;
                            send!(client_tx, msg).await?;
                        } else {
                            if let Some(Ok(id)) = attached_id {
                                let prot_module = entities.get_attached_from_id(id).unwrap().to_protocol(&entities, &mut data_handle.rigid_body_set);
                                let msg = MessageS2C::ModuleAdd(MessageS2CModuleAdd {
                                    module: Some(prot_module).into(),
                                    special_fields: Default::default(),
                                })
                                .try_into()?;
                                send!(client_tx, msg).await?;
                            }
                            warn!("attached ID does not exist");
                        }
                    }
                },

M server/src/main.rs => server/src/main.rs +62 -40
@@ 1,3 1,19 @@
// StarKingdoms.IO, an open source browser game
//      Copyright (C) 2023 ghostly_zsh (and contributors, depending on the license you choose)
//
//      <license disclaimer here>

#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::missing_panics_doc)]

use crate::entity::Entity;
use crate::handler::handle_client;
use crate::manager::{ClientHandler, ClientManager};


@@ 12,17 28,18 @@ use lazy_static::lazy_static;
use log::{error, info, warn, Level};
use manager::PhysicsData;
use nalgebra::vector;
use parking_lot::deadlock::check_deadlock;
use rapier2d_f64::prelude::{
    BroadPhase, CCDSolver, ColliderSet, ImpulseJointSet, IntegrationParameters, IslandManager,
    MultibodyJointSet, NarrowPhase, RigidBodySet,
};
use serde::{Deserialize, Serialize};
use starkingdoms_protocol::PROTOCOL_VERSION;
use std::collections::HashMap;
use std::error::Error;
use std::net::SocketAddr;
use std::thread;
use std::time::Duration;
use parking_lot::deadlock::check_deadlock;

pub mod handler;
pub mod manager;


@@ 31,9 48,9 @@ pub mod timer;
pub mod macros;
pub mod api;
pub mod entity;
pub mod module;
pub mod orbit;
pub mod planet;
pub mod module;

const SCALE: f64 = 10.0;



@@ 58,7 75,7 @@ async fn _handle_request(
    mgr: ClientManager,
    entities: Arc<RwLock<EntityHandler>>,
    physics_data: Arc<RwLock<PhysicsData>>,
) -> Result<(), Box<dyn Error>> {
) -> Result<(), Box<dyn Error + Sync + Send>> {
    let mut peek_buf = [0u8; 9];

    loop {


@@ 78,10 95,9 @@ async fn _handle_request(
                channel: env!("STK_CHANNEL").to_string(),
                build: env!("STK_BUILD").to_string(),
            },
            players: CMGR.usernames.read().await.len() as u32,
            players: u32::try_from(CMGR.usernames.read().await.len())?,
            description: env!("STK_SLP_DESCRIPTION").to_string(),
        })
        .unwrap();
        })?;

        let resp_str = format!(
            "HTTP/1.0 200 OK\nAccess-Control-Allow-Origin: *\nContent-Length: {}\n\n{}",


@@ 130,9 146,11 @@ async fn _handle_request(
    {
        Ok(_) => (),
        Err(e) if e.is::<async_tungstenite::tungstenite::error::Error>() => {
            let e = e
                .downcast::<async_tungstenite::tungstenite::error::Error>()
                .unwrap();
            #[allow(clippy::expect_used)]
            let e = {
                e.downcast::<async_tungstenite::tungstenite::error::Error>().expect("unable to convert between types safely")
            };

            if matches!(*e, async_tungstenite::tungstenite::Error::ConnectionClosed) {
                info!("[{}] connection closed normally", remote_addr);
            } else {


@@ 156,14 174,12 @@ async fn _handle_request(
        let mut collider_set = data.collider_set.clone();
        let mut impulse_joint_set = data.impulse_joint_set.clone();
        let mut multibody_joint_set = data.multibody_joint_set.clone();
        let player_id = match entity_write.get_player_id(remote_addr) {
            Some(s) => s,
            None => {
        let Some(player_id) = entity_write.get_player_id(remote_addr) else {
                warn!("[{}] player missing from entities.players", remote_addr);
                return Err("Player missing from entities.players".into());
            }
        };
        if let Entity::Player(player) = entity_write.entities.get(&player_id).unwrap() {

        if let Some(Entity::Player(player)) = entity_write.entities.get(&player_id) {
            rigid_body_set.remove(
                player.handle,
                &mut island_manager,


@@ 181,9 197,13 @@ async fn _handle_request(
                    &mut multibody_joint_set,
                    true,
                );
                let module_id = entity_write.get_id_from_attached(module).unwrap();
                let module_id = entity_write
                    .get_id_from_attached(&module)
                    .ok_or("Tried to remove nonexistent module")?;
                entity_write.entities.remove(&module_id);
            }
        } else {
            warn!("tried to remove player that doesnt exist: #{}", player_id);
        }
        data.rigid_body_set = rigid_body_set;
        data.collider_set = collider_set;


@@ 198,8 218,8 @@ async fn _handle_request(

lazy_static! {
    static ref CMGR: ClientManager = ClientManager {
        handlers: Arc::new(RwLock::new(Default::default())),
        usernames: Arc::new(RwLock::new(Default::default())),
        handlers: Arc::new(RwLock::new(HashMap::default())),
        usernames: Arc::new(RwLock::new(HashMap::default())),
    };
    static ref DATA: Arc<RwLock<PhysicsData>> = Arc::new(RwLock::new(PhysicsData {
        gravity: vector![0.0, 0.0],


@@ 224,9 244,13 @@ lazy_static! {

pub const PANIC_ON_DEADLOCK: bool = true;

//noinspection ALL
#[async_std::main]
async fn main() {
    simple_logger::init_with_level(Level::Debug).expect("Unable to start logging service");
    #[allow(clippy::expect_used)]
    {
        simple_logger::init_with_level(Level::Debug).expect("Unable to start logging service");
    }

    info!(
        "StarKingdoms server (v: {}, build {}) - initializing",


@@ 247,31 271,29 @@ async fn main() {

    info!("Starting deadlock detector...");

    thread::spawn(move || {
        loop {
            thread::sleep(Duration::from_secs(10));
            let deadlocks = check_deadlock();
            if deadlocks.is_empty() {
                continue;
            }
    thread::spawn(move || loop {
        thread::sleep(Duration::from_secs(10));
        let deadlocks = check_deadlock();
        if deadlocks.is_empty() {
            continue;
        }

            error!("---- DEADLOCK DETECTED ----");
            error!("{} deadlocks were detected.", deadlocks.len());
            for (i, threads) in deadlocks.iter().enumerate() {
                error!("-= Deadlock #{}", i);
                for t in threads {
                    error!("-= Thread ID = {:#?}", t.thread_id());
                    error!("-= Backtrace:\n{:#?}", t.backtrace());
                }
            }
            if PANIC_ON_DEADLOCK {
                error!("StarKingdoms is configured to panic when deadlocks are detected.");
                error!("Bye!");
                panic!("Deadlock detected on one or more threads");
            } else {
                error!("StarKingdoms is not configured to panic when deadlocks are detected.");
        error!("---- DEADLOCK DETECTED ----");
        error!("{} deadlocks were detected.", deadlocks.len());
        for (i, threads) in deadlocks.iter().enumerate() {
            error!("-= Deadlock #{}", i);
            for t in threads {
                error!("-= Thread ID = {:#?}", t.thread_id());
                error!("-= Backtrace:\n{:#?}", t.backtrace());
            }
        }
        if PANIC_ON_DEADLOCK {
            error!("StarKingdoms is configured to panic when deadlocks are detected.");
            error!("Bye!");
            panic!("Deadlock detected on one or more threads");
        } else {
            error!("StarKingdoms is not configured to panic when deadlocks are detected.");
        }
    });

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));

M server/src/manager.rs => server/src/manager.rs +14 -13
@@ 1,13 1,10 @@
use async_std::channel::Sender;
use async_std::sync::RwLock;


use rapier2d_f64::na::Vector2;
use rapier2d_f64::prelude::{
    BroadPhase, CCDSolver, ColliderSet,
    ImpulseJointSet, IntegrationParameters, IslandManager,
    MultibodyJointSet, NarrowPhase, PhysicsPipeline, RigidBodyHandle,
    RigidBodySet,
    BroadPhase, CCDSolver, ColliderSet, ImpulseJointSet, IntegrationParameters, IslandManager,
    MultibodyJointSet, NarrowPhase, PhysicsPipeline, RigidBodyHandle, RigidBodySet,
};
use starkingdoms_protocol::api::APISavedPlayerData;



@@ 18,7 15,7 @@ use std::sync::Arc;

use crate::entity::{Entity, EntityHandler, EntityId};

use crate::module::{Attachment, AttachedModule};
use crate::module::{AttachedModule, Attachment};

#[derive(Clone)]
pub struct ClientManager {


@@ 45,25 42,29 @@ impl Player {
    pub fn search_modules(&self, entities: &EntityHandler) -> Vec<AttachedModule> {
        let mut modules = Vec::new();
        for attachment in self.children.iter().flatten() {
            if let Entity::AttachedModule(child_module) =
                entities.entities.get(&attachment.child).unwrap()
            if let Some(Entity::AttachedModule(child_module)) =
                entities.entities.get(&attachment.child)
            {
                modules.append(&mut child_module.search_modules(entities));
            }
        }
        modules
    }
    pub fn find_parent(&self, module: AttachedModule, entities: &EntityHandler) -> Option<(u8, EntityId)> {
    pub fn find_parent(
        &self,
        module: &AttachedModule,
        entities: &EntityHandler,
    ) -> Option<(u8, EntityId)> {
        for (slot, attachment) in self.children.iter().enumerate() {
            if let Some(attachment) = attachment {
                if let Entity::AttachedModule(child_module) =
                    entities.entities.get(&attachment.child).unwrap()
                    entities.entities.get(&attachment.child)?
                {
                    if *child_module == module {
                        return Some((slot as u8, entities.get_player_id(self.addr).unwrap()));
                    if *child_module == *module {
                        return Some((u8::try_from(slot).ok()?, entities.get_player_id(self.addr)?));
                    }
                    let parent = child_module.find_parent(module.clone(), entities);
                    if let Some(_) = parent {
                    if parent.is_some() {
                        return parent;
                    }
                }

M server/src/module.rs => server/src/module.rs +93 -127
@@ 1,11 1,18 @@
use std::error::Error;
use std::f64::consts::PI;


use nalgebra::{Vector2, point, vector, Isometry2, Unit};
use rapier2d_f64::prelude::{RigidBodyHandle, Real, MassProperties, ColliderBuilder, RigidBodyBuilder, FixedJointBuilder, ImpulseJointHandle, RigidBodySet};
use nalgebra::{point, vector, Isometry2, Unit, Vector2};
use rapier2d_f64::prelude::{
    ColliderBuilder, FixedJointBuilder, ImpulseJointHandle, MassProperties, Real, RigidBodyBuilder,
    RigidBodyHandle, RigidBodySet,
};
use starkingdoms_protocol::module::ModuleType;

use crate::{entity::{EntityId, EntityHandler, Entity, get_entity_id}, manager::PhysicsData, SCALE};
use crate::{
    entity::{get_entity_id, Entity, EntityHandler, EntityId},
    manager::PhysicsData,
    SCALE,
};

#[derive(Debug, Clone)]
pub struct Module {


@@ 37,79 44,53 @@ impl AttachedModule {
        player_id: EntityId,
        module: Module,
        attachment_slot: usize,
    ) -> Result<EntityId, ()> {
    ) -> Result<EntityId, Box<dyn Error + Send + Sync>> {
        let mut entity_map = entities.entities.clone();

        let parent_entity = entity_map
            .get_mut(&parent)
            .expect("parent id does not exist");
            .ok_or("parent id does not exist")?;
        let parent_handle = match parent_entity {
            Entity::Player(player) => {
                if let Some(_child) = &player.children[attachment_slot] {
                    return Err(());
                } else {
                    player.handle
                if player.children[attachment_slot].is_some() {
                    return Err("already attached".into());
                }
            },
                player.handle
            }
            Entity::AttachedModule(module) => {
                if let Some(_child) = &module.children[attachment_slot] {
                    return Err(());
                } else {
                    module.handle
                if module.children[attachment_slot].is_some() {
                    return Err("already attached".into());
                }
            },
                module.handle
            }
            _ => {
                panic!("unexpected parent");
            }
        };

        let parent_body = data.rigid_body_set.get(parent_handle).expect("Parent body does not exist");
        let parent_body = data
            .rigid_body_set
            .get(parent_handle).ok_or("parent body does not exist")?;
        let parent_pos = vector![parent_body.translation().x, parent_body.translation().y];
        let parent_angle = parent_body.rotation().angle();
        let parent_linvel = parent_body.linvel().clone();
        let parent_linvel = *parent_body.linvel();
        let parent_angvel = parent_body.angvel();

        let (anchor, rotation) = match attachment_slot {
            0 => {
                (point![
                    0. / SCALE,
                    53. / SCALE
                ], PI)
            }
            1 => {
                (point![
                    -53. / SCALE,
                     0. / SCALE
                ], -PI/2.)
            }
            2 => {
                (point![
                     0. / SCALE,
                    -53. / SCALE
                ], 0.)
            }
            3 => {
                (point![
                    53. / SCALE,
                    0. / SCALE
                ], PI/2.)
            }
            _ => {
                (point![
                    0. / SCALE,
                    53. / SCALE
                ], 0.)
            }
            0 => (point![0. / SCALE, 53. / SCALE], PI),
            1 => (point![-53. / SCALE, 0. / SCALE], -PI / 2.),
            2 => (point![0. / SCALE, -53. / SCALE], 0.),
            3 => (point![53. / SCALE, 0. / SCALE], PI / 2.),
            _ => (point![0. / SCALE, 53. / SCALE], 0.),
        };

        if let Some(id) = entities.get_from_module(&module) {
            let relative_pos = 
                vector![anchor.x *  (parent_body.rotation().angle()).cos() + 
                        anchor.y * -(parent_body.rotation().angle()).sin(),
                        anchor.x *  (parent_body.rotation().angle()).sin() +
                        anchor.y *  (parent_body.rotation().angle()).cos()];
            let relative_pos = vector![
                anchor.x.mul_add((parent_body.rotation().angle()).cos(), anchor.y * -(parent_body.rotation().angle()).sin()),
                anchor.x.mul_add((parent_body.rotation().angle()).sin(), anchor.y * (parent_body.rotation().angle()).cos())
            ];
            let module_pos = parent_pos + relative_pos;
            let module_body = data.rigid_body_set.get_mut(module.handle).unwrap();
            let module_body = data.rigid_body_set.get_mut(module.handle).ok_or("module body does not exist")?;
            module_body.set_translation(module_pos, true);
            module_body.set_rotation(Unit::from_angle(parent_angle + rotation), true);
            module_body.set_linvel(parent_linvel, true);


@@ 123,7 104,7 @@ impl AttachedModule {
            let attach_joint_handle =
                data.impulse_joint_set
                    .insert(parent_handle, module.handle, attach_joint, true);
            let attached_module = AttachedModule {
            let attached_module = Self {
                handle: module.handle,
                module_type: module.module_type,
                player_id,


@@ 152,23 133,22 @@ impl AttachedModule {
            entities.entities = entity_map;
            return Ok(attached_id);
        }
        Err(())
        Err("entity does not exist".into())
    }
    pub fn detach(
        data: &mut PhysicsData,
        entities: &mut EntityHandler,
        player_id: EntityId,
        module: AttachedModule,
    ) -> EntityId {
        module: Self,
    ) -> Option<EntityId> {
        let mut entity_map = entities.entities.clone();

        // player not in parent search
        // also no parents included in parent search
        let player = entities.get_player_from_id(player_id).unwrap();
        let (slot, parent_id) = player.find_parent(module.clone(), entities).unwrap();
        let player = entities.get_player_from_id(player_id)?;
        let (slot, parent_id) = player.find_parent(&module, entities)?;
        let parent_entity = entity_map
            .get_mut(&parent_id)
            .expect("parent id does not exist");
            .get_mut(&parent_id)?;

        match parent_entity {
            Entity::Player(ref mut player) => {


@@ 195,28 175,28 @@ impl AttachedModule {
            lifetime: 0.,
            flags: 0,
        };
        entity_map.remove(&entities.get_id_from_attached(module.clone()).unwrap());
        entity_map.remove(&entities.get_id_from_attached(&module)?);
        let id = get_entity_id();
        entity_map.insert(id, Entity::Module(new_module));
        for element in tree {
            for child in element.clone().children {
                if let Some(child) = child {
                    data.impulse_joint_set.remove(child.connection, true);
                    let child_body = entities.get_attached_from_id(child.child).unwrap();
                    let child_body = entities.get_attached_from_id(child.child)?;
                    let new_module = Module {
                        handle: child_body.handle,
                        module_type: child_body.module_type,
                        lifetime: 0.,
                        flags: 0,
                    };
                    entity_map.remove(&entities.get_id_from_attached(child_body.clone()).unwrap());
                    entity_map.remove(&entities.get_id_from_attached(&child_body)?);
                    let attached_id = get_entity_id();
                    entity_map.insert(attached_id, Entity::Module(new_module));
                }
            }
        }
        entities.entities = entity_map;
        id
        Some(id)
    }
    pub fn attach_new(
        data: &mut PhysicsData,


@@ 225,12 205,11 @@ impl AttachedModule {
        player_id: EntityId,
        module: ModuleTemplate,
        attachment_slot: usize,
    ) -> EntityId {
    ) -> Option<EntityId> {
        let mut entity_map = entities.entities.clone();

        let parent_entity = entity_map
            .get_mut(&parent)
            .expect("parent id does not exist");
            .get_mut(&parent)?;
        let parent_handle = match parent_entity {
            Entity::Player(player) => player.handle,
            Entity::AttachedModule(module) => module.handle,


@@ 239,47 218,23 @@ impl AttachedModule {
            }
        };

        let parent_body = data.rigid_body_set.get(parent_handle).expect("Parent body does not exist");
        let parent_body = data
            .rigid_body_set
            .get(parent_handle)?;
        let parent_pos = vector![parent_body.translation().x, parent_body.translation().y];

        let (anchor, rotation) = match attachment_slot {
            0 => {
                (point![
                    0. / SCALE,
                    53. / SCALE
                ], PI)
            }
            1 => {
                (point![
                    -53. / SCALE,
                     0. / SCALE
                ], -PI/2.)
            }
            2 => {
                (point![
                     0. / SCALE,
                    -53. / SCALE
                ], 0.)
            }
            3 => {
                (point![
                    53. / SCALE,
                    0. / SCALE
                ], PI/2.)
            }
            _ => {
                (point![
                    0. / SCALE,
                    53. / SCALE
                ], 0.)
            }
            0 => (point![0. / SCALE, 53. / SCALE], PI),
            1 => (point![-53. / SCALE, 0. / SCALE], -PI / 2.),
            2 => (point![0. / SCALE, -53. / SCALE], 0.),
            3 => (point![53. / SCALE, 0. / SCALE], PI / 2.),
            _ => (point![0. / SCALE, 53. / SCALE], 0.),
        };

        let relative_pos = 
            vector![anchor.x *  (parent_body.rotation().angle()).cos() + 
                    anchor.y * -(parent_body.rotation().angle()).sin(),
                    anchor.x *  (parent_body.rotation().angle()).sin() +
                    anchor.y *  (parent_body.rotation().angle()).cos()];
        let relative_pos = vector![
            anchor.x.mul_add((parent_body.rotation().angle()).cos(), anchor.y * -(parent_body.rotation().angle()).sin()),
            anchor.x.mul_add((parent_body.rotation().angle()).sin(), anchor.y * (parent_body.rotation().angle()).cos())
        ];
        let module_pos = parent_pos + relative_pos;

        // create attachment module


@@ 305,7 260,7 @@ impl AttachedModule {
        let attach_joint_handle =
            data.impulse_joint_set
                .insert(parent_handle, attached_handle, attach_joint, true);
        let attached_module = AttachedModule {
        let attached_module = Self {
            handle: attached_handle,
            module_type: module.module_type,
            player_id,


@@ 331,7 286,7 @@ impl AttachedModule {
        };
        entity_map.insert(attached_id, Entity::AttachedModule(attached_module));
        entities.entities = entity_map;
        attached_id
        Some(attached_id)
    }
    // TODO: remove this function
    pub fn to_module(&self) -> Module {


@@ 343,17 298,24 @@ impl AttachedModule {
        }
    }
    // TODO: this one too
    pub fn to_module_id(&self, entities: &EntityHandler) -> (EntityId, Module) {
        (entities.get_id_from_attached(self.clone()).unwrap(), Module {
            handle: self.handle,
            module_type: self.module_type,
            lifetime: 10.,
            flags: 1,
        })
    pub fn to_module_id(&self, entities: &EntityHandler) -> Option<(EntityId, Module)> {
        Some((
            entities.get_id_from_attached(self)?,
            Module {
                handle: self.handle,
                module_type: self.module_type,
                lifetime: 10.,
                flags: 1,
            },
        ))
    }

    pub fn to_protocol(&self, entities: &EntityHandler, data: &RigidBodySet) -> starkingdoms_protocol::module::AttachedModule {
        let body = data.get(self.handle).unwrap();
    pub fn to_protocol(
        &self,
        entities: &EntityHandler,
        data: &RigidBodySet,
    ) -> Option<starkingdoms_protocol::module::AttachedModule> {
        let body = data.get(self.handle)?;
        let children = self.children.to_vec();
        let mut prot_children = Vec::new();
        for i in 1..children.len() {


@@ 365,22 327,22 @@ impl AttachedModule {
                });
            }
        }
        starkingdoms_protocol::module::AttachedModule {
        Some(starkingdoms_protocol::module::AttachedModule {
            module_type: self.module_type.into(),
            rotation: body.rotation().angle(),
            x: body.translation().x * SCALE,
            y: body.translation().y * SCALE,
            id: entities.get_id_from_attached(self.clone()).unwrap(),
            id: entities.get_id_from_attached(self)?,
            children: prot_children,
            special_fields: Default::default(),
        }
        })
    }

    pub fn search_modules(&self, entities: &EntityHandler) -> Vec<AttachedModule> {
    pub fn search_modules(&self, entities: &EntityHandler) -> Vec<Self> {
        let mut modules = vec![self.clone()];
        for attachment in self.children.iter().flatten() {
            if let Entity::AttachedModule(child_module) =
                entities.entities.get(&attachment.child).unwrap()
            if let Some(Entity::AttachedModule(child_module)) =
                entities.entities.get(&attachment.child)
            {
                modules.append(&mut child_module.search_modules(entities));
            }


@@ 388,17 350,21 @@ impl AttachedModule {
        modules
    }

    pub fn find_parent(&self, module: AttachedModule, entities: &EntityHandler) -> Option<(u8, EntityId)> {
    pub fn find_parent(
        &self,
        module: Self,
        entities: &EntityHandler,
    ) -> Option<(u8, EntityId)> {
        for (slot, attachment) in self.children.iter().enumerate() {
            if let Some(attachment) = attachment {
                if let Entity::AttachedModule(child_module) =
                    entities.entities.get(&attachment.child).unwrap()
                    entities.entities.get(&attachment.child)?
                {
                    if *child_module == module {
                        return Some((slot as u8, entities.get_id_from_attached(self.clone()).unwrap()));
                        return Some((slot as u8, entities.get_id_from_attached(self)?));
                    }
                    let parent = child_module.find_parent(module.clone(), entities);
                    if let Some(_) = parent {
                    if parent.is_some() {
                        return parent;
                    }
                }


@@ 408,7 374,7 @@ impl AttachedModule {
    }
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attachment {
    pub child: EntityId,
    pub connection: ImpulseJointHandle,

M server/src/orbit/constants.rs => server/src/orbit/constants.rs +8 -8
@@ 1,6 1,6 @@
pub const GAME_SCALE_DISTANCE: f64 = 0.0001567865; // 1000 / EARTH_RADIUS_RL
pub const GAME_SCALE_MASS: f64 = 0.0000000000000000000006697923643670463; // 4000 / EARTH_MASS_RL
pub const GAME_SCALE_TIME: f64 = 0.00051440329218107; // 1200 / MOON_ORBIT_TIME_RL
pub const GAME_SCALE_DISTANCE: f64 = 0.000_156_786_5; // 1000 / EARTH_RADIUS_RL
pub const GAME_SCALE_MASS: f64 = 0.000_000_000_000_000_000_000_669_792_364_367_046_3; // 4000 / EARTH_MASS_RL
pub const GAME_SCALE_TIME: f64 = 0.000_514_403_292_181_07; // 1200 / MOON_ORBIT_TIME_RL
pub const GAME_ORBITS_ENABLED: bool = false;

pub const EARTH_RADIUS_BIAS: f64 = 1.0;


@@ 8,7 8,7 @@ pub const EARTH_MASS_BIAS: f64 = 1.0;

pub const EARTH_RADIUS_RL: f64 = 6_378_100.0;
pub const EARTH_RADIUS: f64 = EARTH_RADIUS_RL * GAME_SCALE_DISTANCE * EARTH_RADIUS_BIAS;
pub const EARTH_MASS_RL: f64 = 5972000000000000000000000.0;
pub const EARTH_MASS_RL: f64 = 5_972_000_000_000_000_000_000_000.0;
pub const EARTH_MASS: f64 = EARTH_MASS_RL * GAME_SCALE_MASS * EARTH_MASS_BIAS;

pub const MOON_RADIUS_BIAS: f64 = 1.0;


@@ 19,9 19,9 @@ pub const MOON_ORBIT_TIME_BIAS: f64 = 1.0;

pub const MOON_RADIUS_RL: f64 = 1_737_400.0;
pub const MOON_RADIUS: f64 = MOON_RADIUS_RL * GAME_SCALE_DISTANCE * MOON_RADIUS_BIAS;
pub const MOON_MASS_RL: f64 = 73476730900000000000000.0;
pub const MOON_MASS_RL: f64 = 73_476_730_900_000_000_000_000.0;
pub const MOON_MASS: f64 = MOON_MASS_RL * GAME_SCALE_MASS * MOON_MASS_BIAS;
pub const MOON_PERIAPSIS: f64 = 363228900.0 * GAME_SCALE_DISTANCE * MOON_PERIAPSIS_BIAS;
pub const MOON_APOAPSIS: f64 = 405400000.0 * GAME_SCALE_DISTANCE * MOON_APOAPSIS_BIAS;
pub const MOON_ORBIT_TIME_RL: f64 = 2332800.0;
pub const MOON_PERIAPSIS: f64 = 363_228_900.0 * GAME_SCALE_DISTANCE * MOON_PERIAPSIS_BIAS;
pub const MOON_APOAPSIS: f64 = 405_400_000.0 * GAME_SCALE_DISTANCE * MOON_APOAPSIS_BIAS;
pub const MOON_ORBIT_TIME_RL: f64 = 2_332_800.0;
pub const MOON_ORBIT_TIME: f64 = MOON_ORBIT_TIME_RL * GAME_SCALE_TIME * MOON_ORBIT_TIME_BIAS;

M server/src/orbit/kepler.rs => server/src/orbit/kepler.rs +1 -1
@@ 3,5 3,5 @@
/// E is the Eccentric Anomaly (angle to where the body is on the ellipse)
/// e is the eccentricity of the orbit (0 = perfect circle, and up to 1 is increasingly elliptical)
pub fn kepler_equation(eccentric_anomaly: f64, mean_anomaly: f64, eccentricity: f64) -> f64 {
    mean_anomaly - eccentric_anomaly + eccentricity * eccentric_anomaly.sin()
    eccentricity.mul_add(eccentric_anomaly.sin(), mean_anomaly - eccentric_anomaly)
}

M server/src/orbit/newtonian.rs => server/src/orbit/newtonian.rs +1 -1
@@ 1,7 1,7 @@
use crate::orbit::kepler::kepler_equation;

pub const NEWTONIAN_STEP_SIZE: f64 = 0.0001;
pub const NEWTONIAN_ACCEPTABLE_ERROR: f64 = 0.00000001;
pub const NEWTONIAN_ACCEPTABLE_ERROR: f64 = 0.000_000_01;

pub fn solve_kepler_with_newtonian(
    mean_anomaly: f64,

M server/src/orbit/orbit.rs => server/src/orbit/orbit.rs +12 -5
@@ 16,8 16,9 @@ pub fn calculate_vector_of_orbit(
    mass: f64,
    step: f64,
) -> Vector2<f64> {
    let semi_major_length = (apoapsis + periapsis) / 2.0;
    let _linear_eccentricity = semi_major_length - periapsis; // distance between center and focus
    // this doesnt actually do anything
    //let semi_major_length = (apoapsis + periapsis) / 2.0;
    //let linear_eccentricity = semi_major_length - periapsis; // distance between center and focus

    let target = calculate_world_position_of_orbit(
        calculate_point_on_orbit(periapsis, apoapsis, t),


@@ 45,14 46,20 @@ pub fn calculate_point_on_orbit(periapsis: f64, apoapsis: f64, t: f64) -> Vector
    let semi_major_length = (apoapsis + periapsis) / 2.0;
    let linear_eccentricity = semi_major_length - periapsis; // distance between center and focus
    let eccentricity = linear_eccentricity / semi_major_length; // 0: circle. 1: parabola. in between: ellipse
    let semi_minor_length =
        (semi_major_length * semi_major_length - linear_eccentricity * linear_eccentricity).sqrt();
    let semi_minor_length = semi_major_length
        .mul_add(
            semi_major_length,
            -linear_eccentricity * linear_eccentricity,
        )
        .sqrt();

    let mean_anomaly = t * std::f64::consts::PI * 2.0;
    let eccentric_anomaly = solve_kepler_with_newtonian(mean_anomaly, eccentricity, 100);

    let ellipse_center_x = -linear_eccentricity;
    let point_x = eccentric_anomaly.cos() * semi_major_length + ellipse_center_x;
    let point_x = eccentric_anomaly
        .cos()
        .mul_add(semi_major_length, ellipse_center_x);
    let point_y = eccentric_anomaly.sin() * semi_minor_length;

    vector![point_x, point_y]

M server/src/planet.rs => server/src/planet.rs +5 -7
@@ 1,4 1,3 @@

use nalgebra::{vector, Vector2};
use rapier2d_f64::prelude::{
    ColliderBuilder, ColliderSet, RigidBodyBuilder, RigidBodyHandle, RigidBodySet,


@@ 26,9 25,7 @@ pub struct Planet {

impl Planet {
    pub fn gravity(&self, position: (f64, f64), mass: f64) -> (f64, f64) {
        let distance = ((position.0 - self.position.0).powi(2)
            + (position.1 - self.position.1).powi(2))
        .sqrt();
        let distance = (position.0 - self.position.0).hypot(position.1 - self.position.1);
        let force = GRAVITY * ((self.mass * mass) / (distance * distance));
        let mut direction =
            Vector2::new(self.position.0 - position.0, self.position.1 - position.1);


@@ 88,7 85,7 @@ impl Planets {
        entities: &mut Entities,
    ) -> Vec<EntityId> {
        let mut planet_ids: Vec<EntityId> = Vec::new();
        let (earth_id, entity) = Planets::make_planet(
        let (earth_id, entity) = Self::make_planet(
            "earth",
            PlanetType::Earth,
            EARTH_MASS,


@@ 102,7 99,8 @@ impl Planets {
        planet_ids.push(earth_id);

        let moon_start_point;
        if let Entity::Planet(earth) = entities.get(&earth_id).unwrap() {
        #[allow(clippy::expect_used)]
        if let Entity::Planet(earth) = entities.get(&earth_id).expect("earth does not exist") {
            moon_start_point = calculate_world_position_of_orbit(
                calculate_point_on_orbit(MOON_PERIAPSIS, MOON_APOAPSIS, 0.0),
                vector![earth.position.0, earth.position.1],


@@ 111,7 109,7 @@ impl Planets {
            moon_start_point = vector![0., 0.];
        }

        let (moon_id, moon) = Planets::make_planet(
        let (moon_id, moon) = Self::make_planet(
            "moon",
            PlanetType::Moon,
            MOON_MASS,

M server/src/timer.rs => server/src/timer.rs +68 -56
@@ 12,24 12,24 @@ use crate::{
};
use async_std::sync::RwLock;
use async_std::task::sleep;
use log::{warn};
use log::warn;
use nalgebra::{point, vector};
use rand::Rng;
use rapier2d_f64::prelude::{
    ColliderBuilder, MassProperties, PhysicsPipeline, RigidBodyBuilder,
};
use rapier2d_f64::prelude::{ColliderBuilder, MassProperties, PhysicsPipeline, RigidBodyBuilder};
use starkingdoms_protocol::{module::ModuleType, planet::PlanetType, player::Player};
use std::{f64::consts::PI, sync::Arc, time::Duration};
use std::error::Error;

pub const LATERAL_FORCE: f64 = 0.0002;
pub const MODULE_SPAWN: f64 = 3.0;
pub const MODULE_MAX: u32 = 10;

//noinspection ALL
pub async fn timer_main(
    mgr: ClientManager,
    physics_data_orig: Arc<RwLock<PhysicsData>>,
    entities: Arc<RwLock<EntityHandler>>,
) {
) -> Result<(), Box<dyn Error>> {
    let mut pipeline = PhysicsPipeline::new();

    let mut time = 0.0;


@@ 68,11 68,11 @@ pub async fn timer_main(
            let planets = entities.write().await;

            // update earth (nothing changes, yet)
            let earth = planets.get_planet(PlanetType::Earth).unwrap();
            let earth = planets.get_planet(PlanetType::Earth).ok_or("earth does not exist")?;
            let new_earth_position = vector![earth.position.0, earth.position.1];

            // update moon
            let moon: &mut Planet = &mut planets.get_planet(PlanetType::Moon).unwrap();
            let moon: &mut Planet = &mut planets.get_planet(PlanetType::Moon).ok_or("moon does not exist")?;
            let new_moon_position = calculate_world_position_of_orbit(
                calculate_point_on_orbit(MOON_PERIAPSIS, MOON_APOAPSIS, time / MOON_ORBIT_TIME),
                new_earth_position,


@@ 80,7 80,7 @@ pub async fn timer_main(
            let moon_body = physics_data
                .rigid_body_set
                .get_mut(moon.body_handle)
                .unwrap();
                .ok_or("moon does not exist")?;
            moon_body.set_next_kinematic_position(new_moon_position.into());
            moon.position = (
                moon_body.translation()[0] / SCALE,


@@ 137,9 137,9 @@ pub async fn timer_main(
                    .insert(get_entity_id(), Entity::Module(module));
            }
            let mut entities = entities.write().await;
            for module in entities.get_modules().iter_mut() {
            for module in &mut entities.get_modules() {
                let module_handle = module.handle;
                let module_body = physics_data.rigid_body_set.get_mut(module_handle).unwrap();
                let module_body = physics_data.rigid_body_set.get_mut(module_handle).ok_or("module does not exist")?;
                module_body.reset_forces(true);
                module_body.reset_torques(true);
                let grav_force = entities.gravity(


@@ 147,8 147,8 @@ pub async fn timer_main(
                    module_body.mass(),
                );
                module_body.apply_impulse(vector![grav_force.0, grav_force.1], true);
                let id = entities.get_from_module(module).unwrap();
                if let Entity::Module(p_module) = entities.entities.get_mut(&id).unwrap() {
                let id = entities.get_from_module(module).ok_or("module entity does not exist")?;
                if let Entity::Module(p_module) = entities.entities.get_mut(&id).ok_or("module does not exist")? {
                    p_module.lifetime += 5. / 1000.;
                }
                if module.lifetime > 80. {


@@ 173,9 173,9 @@ pub async fn timer_main(
                    entities.entities.remove(&id);
                }
            }
            for module in entities.get_all_attached().iter_mut() {
            for module in &mut entities.get_all_attached() {
                let module_handle = module.handle;
                let module_body = physics_data.rigid_body_set.get_mut(module_handle).unwrap();
                let module_body = physics_data.rigid_body_set.get_mut(module_handle).ok_or("module does not exist")?;
                module_body.reset_forces(true);
                module_body.reset_torques(true);
                let grav_force = entities.gravity(


@@ 187,9 187,9 @@ pub async fn timer_main(
        }

        {
            for (player_id, player) in entities.read().await.get_players().iter() {
            for (player_id, player) in &entities.read().await.get_players() {
                let player_handle = player.handle;
                let player_body = physics_data.rigid_body_set.get_mut(player_handle).unwrap();
                let player_body = physics_data.rigid_body_set.get_mut(player_handle).ok_or("player body does not exist")?;
                player_body.reset_forces(true);
                player_body.reset_torques(true);
                let planets = entities.read().await;


@@ 245,20 245,20 @@ pub async fn timer_main(
                    right_bottom_thruster / scale * rotation.cos()
                ];
                let top_left_point = point![
                    -25. / scale * rotation.cos() + 25. / scale * rotation.sin(),
                    -25. / scale * rotation.sin() - 25. / scale * rotation.cos()
                    (-25. / scale).mul_add(rotation.cos(), 25. / scale * rotation.sin()),
                    (-25. / scale).mul_add(rotation.sin(), -25. / scale * rotation.cos())
                ] + player_body.translation();
                let top_right_point = point![
                    25. / scale * rotation.cos() + 25. / scale * rotation.sin(),
                    25. / scale * rotation.sin() - 25. / scale * rotation.cos()
                    (25. / scale).mul_add(rotation.cos(), 25. / scale * rotation.sin()),
                    (25. / scale).mul_add(rotation.sin(), -25. / scale * rotation.cos())
                ] + player_body.translation();
                let bottom_left_point = point![
                    -25. / scale * rotation.cos() - 25. / scale * rotation.sin(),
                    -25. / scale * rotation.sin() + 25. / scale * rotation.cos()
                    (-25. / scale).mul_add(rotation.cos(), -25. / scale * rotation.sin()),
                    (-25. / scale).mul_add(rotation.sin(), 25. / scale * rotation.cos())
                ] + player_body.translation();
                let bottom_right_point = point![
                    25. / scale * rotation.cos() - 25. / scale * rotation.sin(),
                    25. / scale * rotation.sin() + 25. / scale * rotation.cos()
                    (25. / scale).mul_add(rotation.cos(), -25. / scale * rotation.sin()),
                    (25. / scale).mul_add(rotation.sin(), 25. / scale * rotation.cos())
                ] + player_body.translation();

                player_body.add_force_at_point(left_top_thruster, top_left_point, true);


@@ 271,10 271,9 @@ pub async fn timer_main(
                let username;
                {
                    let usernames = mgr.usernames.read().await;
                    username = usernames.get(player_id).unwrap().clone();
                    username = usernames.get(player_id).ok_or("username does not exist")?.clone();
                }

                // TODO: Figure out how to adjust codegen to use f64
                protocol_players.push(Player {
                    rotation,
                    x: (translation.x * SCALE),


@@ 290,7 289,7 @@ pub async fn timer_main(
        let mut mgr_w = mgr.handlers.write().await;
        let mgr_r = mgr_w.clone();

        for (addr, client_thread) in mgr_r.iter() {
        for (addr, client_thread) in &mgr_r {
            match client_thread.tx.send(ClientHandlerMessage::Tick).await {
                Ok(_) => {
                    match client_thread


@@ 308,44 307,53 @@ pub async fn timer_main(
                    let modules = entities.read().await.get_modules_id();
                    let entities = entities.read().await;
                    let this_player = entities.get_player_id(*addr);
                    let mut attached_modules: Vec<starkingdoms_protocol::module::AttachedModule> = Vec::new();
                    let mut attached_modules: Vec<starkingdoms_protocol::module::AttachedModule> =
                        Vec::new();
                    let unattached_modules = entities.get_all_attached();
                    let mut unattached_modules: Vec<starkingdoms_protocol::module::Module> = unattached_modules
                        .iter()
                        .filter(|m| {
                            match this_player {
                    let mut unattached_modules: Vec<starkingdoms_protocol::module::Module> =
                        unattached_modules
                            .iter()
                            .filter(|m| match this_player {
                                Some(id) => {
                                    if m.player_id != id {
                                        true
                                    } else {
                                        attached_modules.push(m.to_protocol(&entities, &physics_data.rigid_body_set));
                                        #[allow(clippy::expect_used)] {
                                            attached_modules.push(
                                                m.to_protocol(&entities, &physics_data.rigid_body_set).expect("module does not exist"),
                                            );
                                        }
                                        false
                                    }
                                }
                                None => {
                                    true
                                None => true,
                            })
                            .map(|m| {
                                let id;
                                let module;
                                #[allow(clippy::expect_used)] { (id, module) = m.to_module_id(&entities).expect("unable to get module id"); }
                                //info!("{:?}", module);
                                let body;
                                #[allow(clippy::expect_used)] { body = physics_data.rigid_body_set.get(module.handle).expect("module body does not exist"); }

                                starkingdoms_protocol::module::Module {
                                    module_type: module.module_type.into(),
                                    rotation: body.rotation().angle(),
                                    x: body.translation().x * SCALE,
                                    y: body.translation().y * SCALE,
                                    id,
                                    flags: module.flags,
                                    special_fields: Default::default(),
                                }
                            }
                        })
                        .map(|m| {
                            let (id, module) = m.to_module_id(&entities);
                            //info!("{:?}", module);
                            let body = physics_data.rigid_body_set.get(module.handle).unwrap();
                            starkingdoms_protocol::module::Module {
                                module_type: module.module_type.into(),
                                rotation: body.rotation().angle(),
                                x: body.translation().x * SCALE,
                                y: body.translation().y * SCALE,
                                id,
                                flags: module.flags,
                                special_fields: Default::default(),
                            }
                        })
                        .collect();
                            })
                            .collect();
                    let mut protocol_modules: Vec<starkingdoms_protocol::module::Module> = modules
                        .iter()
                        .map(|(id, module)| {
                            let body = physics_data.rigid_body_set.get(module.handle).unwrap();
                            let body;

                            #[allow(clippy::expect_used)] { body = physics_data.rigid_body_set.get(module.handle).expect("module body does not exist"); }

                            starkingdoms_protocol::module::Module {
                                module_type: module.module_type.into(),
                                rotation: body.rotation().angle(),


@@ 371,9 379,13 @@ pub async fn timer_main(
                        }
                    };

                    match client_thread.tx.send(ClientHandlerMessage::ModuleTreeUpdate {
                        modules: attached_modules
                    }).await {
                    match client_thread
                        .tx
                        .send(ClientHandlerMessage::ModuleTreeUpdate {
                            modules: attached_modules,
                        })
                        .await
                    {
                        Ok(_) => (),
                        Err(e) => {
                            warn!("unable to send module tree update packet: {}", e);

M spacetime_rs/src/commands/docker.rs => spacetime_rs/src/commands/docker.rs +6 -3
@@ 2,11 2,11 @@ use crate::commands::api::build_api_prod;
use crate::commands::client::build_client_prod;
use crate::commands::server::build_server_prod;
use crate::ninja::exec;
use sedregex::find_and_replace;
use std::error::Error;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use sedregex::find_and_replace;

fn _build(img: &str, channel: &str, root: &PathBuf) -> Result<(), Box<dyn Error>> {
    let mut patched = "".to_string();


@@ 18,7 18,7 @@ fn _build(img: &str, channel: &str, root: &PathBuf) -> Result<(), Box<dyn Error>
    } else if img == "web" {
        let (a, b) = match channel {
            "stable" => ("starkingdoms", "tk"),
            _ => (channel, "starkingdoms.io")
            _ => (channel, "starkingdoms.io"),
        };

        let index_html_path = root.clone().join("client/").join("index.html");


@@ 30,7 30,10 @@ fn _build(img: &str, channel: &str, root: &PathBuf) -> Result<(), Box<dyn Error>

        fs::write(&index_html_path, index_html_patched.as_bytes())?;

        fs::write(root.clone().join("client/").join("index.html.orig"), index_html_src.clone())?;
        fs::write(
            root.clone().join("client/").join("index.html.orig"),
            index_html_src.clone(),
        )?;

        patched = index_html_src.clone();


M spacetime_rs/src/configure/asset.rs => spacetime_rs/src/configure/asset.rs +5 -1
@@ 27,7 27,11 @@ pub fn configure_assets(writer: &mut NinjaWriter<File>, root: &Path) -> Result<(
    );

    let default_asset_size = 512;
    let asset_overrides = HashMap::from([("earth.svg", 2048), ("moon.svg", 2048), ("starfield.svg", 2048)]);
    let asset_overrides = HashMap::from([
        ("earth.svg", 2048),
        ("moon.svg", 2048),
        ("starfield.svg", 2048),
    ]);

    // generate an inkscape rule for all required asset sizes
    let mut written_rules_for = vec![];

M web/play.html => web/play.html +1 -1
@@ 2,7 2,7 @@
<html lang="en-US">
<head>
    <title>StarKingdoms.TK</title>
    <link rel="stylesheet" href="/static/css/play.css"></link>
    <link rel="stylesheet" href="/static/css/play.css"/>
    <meta charset="utf-8">
</head>


M web/static/css/index.css => web/static/css/index.css +0 -3
@@ 11,6 11,3 @@
    width: 100%;
}

.w-90 {
    width: 90%;
}
\ No newline at end of file

M web/static/css/play.css => web/static/css/play.css +0 -5
@@ 1,8 1,3 @@
.texturebox {
    display: inline;
    margin: 5px;
}

#canvas {
    position: absolute;
    top: 0;