~starkingdoms/starkingdoms

23bf3e09b7adf56c13b54ffcd16549553f008ac3 — c0repwn3r 2 years ago b7815fc + 38bf8de
resolve asset merge conflict
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 assets/dist/spritesheet-125.json => assets/dist/spritesheet-125.json +32 -23
@@ 9,7 9,7 @@
      "pivot": { "x": 128, "y": 128 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 256, "h": 256 }
    },
    "earth.png": {
    "mars.png": {
      "frame": { "x": 0, "y": 256, "w": 256, "h": 256 },
      "rotated": false,
      "trimmed": false,


@@ 18,7 18,7 @@
      "pivot": { "x": 128, "y": 128 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 256, "h": 256 }
    },
    "moon.png": {
    "earth.png": {
      "frame": { "x": 0, "y": 512, "w": 256, "h": 256 },
      "rotated": false,
      "trimmed": false,


@@ 27,8 27,17 @@
      "pivot": { "x": 128, "y": 128 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 256, "h": 256 }
    },
    "moon.png": {
      "frame": { "x": 0, "y": 768, "w": 256, "h": 256 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 256, "h": 256 },
      "sourceSize": { "w": 256, "h": 256 },
      "pivot": { "x": 128, "y": 128 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 256, "h": 256 }
    },
    "hearty_party.png": {
      "frame": { "x": 0, "y": 768, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1024, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 37,7 46,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "trackindicator.png": {
      "frame": { "x": 0, "y": 832, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1088, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 46,7 55,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hearty.png": {
      "frame": { "x": 0, "y": 896, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1152, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 55,7 64,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 960, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1216, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 64,7 73,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 1024, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1280, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 73,7 82,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hearty_ferris.png": {
      "frame": { "x": 0, "y": 1088, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1344, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 82,7 91,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "superthruster_on.png": {
      "frame": { "x": 0, "y": 1152, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1408, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 91,7 100,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 0, "y": 1216, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1472, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 100,7 109,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 0, "y": 1280, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1536, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 109,7 118,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "thruster_on.png": {
      "frame": { "x": 0, "y": 1344, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1600, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 118,7 127,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingleg.png": {
      "frame": { "x": 0, "y": 1408, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1664, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 127,7 136,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hub_on.png": {
      "frame": { "x": 0, "y": 1472, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1728, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 136,7 145,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "powerhub_on.png": {
      "frame": { "x": 0, "y": 1536, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1792, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 145,7 154,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_on.png": {
      "frame": { "x": 0, "y": 1600, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1856, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 154,7 163,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "superthruster_off.png": {
      "frame": { "x": 0, "y": 1664, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1920, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 163,7 172,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 0, "y": 1728, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 1984, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 172,7 181,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "thruster_off.png": {
      "frame": { "x": 0, "y": 1792, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 2048, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 181,7 190,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "cargo_on.png": {
      "frame": { "x": 0, "y": 1856, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 2112, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 190,7 199,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "cargo_off.png": {
      "frame": { "x": 0, "y": 1920, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 2176, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 199,7 208,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hub_off.png": {
      "frame": { "x": 0, "y": 1984, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 2240, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 208,7 217,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "powerhub_off.png": {
      "frame": { "x": 0, "y": 2048, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 2304, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },

M assets/dist/spritesheet-125.png => assets/dist/spritesheet-125.png +0 -0
M assets/dist/spritesheet-375.json => assets/dist/spritesheet-375.json +32 -23
@@ 9,7 9,7 @@
      "pivot": { "x": 384, "y": 384 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 768, "h": 768 }
    },
    "earth.png": {
    "mars.png": {
      "frame": { "x": 0, "y": 768, "w": 768, "h": 768 },
      "rotated": false,
      "trimmed": false,


@@ 18,7 18,7 @@
      "pivot": { "x": 384, "y": 384 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 768, "h": 768 }
    },
    "moon.png": {
    "earth.png": {
      "frame": { "x": 0, "y": 1536, "w": 768, "h": 768 },
      "rotated": false,
      "trimmed": false,


@@ 27,8 27,17 @@
      "pivot": { "x": 384, "y": 384 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 768, "h": 768 }
    },
    "moon.png": {
      "frame": { "x": 0, "y": 2304, "w": 768, "h": 768 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 768 },
      "sourceSize": { "w": 768, "h": 768 },
      "pivot": { "x": 384, "y": 384 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 768, "h": 768 }
    },
    "hearty_party.png": {
      "frame": { "x": 0, "y": 2304, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 3072, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 37,7 46,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "trackindicator.png": {
      "frame": { "x": 0, "y": 2496, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 3264, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 46,7 55,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hearty.png": {
      "frame": { "x": 0, "y": 2688, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 3456, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 55,7 64,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 2880, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 3648, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 64,7 73,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 3072, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 3840, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 73,7 82,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hearty_ferris.png": {
      "frame": { "x": 0, "y": 3264, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 4032, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 82,7 91,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "superthruster_on.png": {
      "frame": { "x": 0, "y": 3456, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 4224, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 91,7 100,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 0, "y": 3648, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 4416, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 100,7 109,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 0, "y": 3840, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 4608, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 109,7 118,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "thruster_on.png": {
      "frame": { "x": 0, "y": 4032, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 4800, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 118,7 127,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingleg.png": {
      "frame": { "x": 0, "y": 4224, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 4992, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 127,7 136,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hub_on.png": {
      "frame": { "x": 0, "y": 4416, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 5184, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 136,7 145,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "powerhub_on.png": {
      "frame": { "x": 0, "y": 4608, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 5376, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 145,7 154,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_on.png": {
      "frame": { "x": 0, "y": 4800, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 5568, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 154,7 163,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 0, "y": 4992, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 5760, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 163,7 172,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "superthruster_off.png": {
      "frame": { "x": 0, "y": 5184, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 5952, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 172,7 181,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "thruster_off.png": {
      "frame": { "x": 0, "y": 5376, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 6144, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 181,7 190,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "cargo_on.png": {
      "frame": { "x": 0, "y": 5568, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 6336, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 190,7 199,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "cargo_off.png": {
      "frame": { "x": 0, "y": 5760, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 6528, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 199,7 208,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hub_off.png": {
      "frame": { "x": 0, "y": 5952, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 6720, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 208,7 217,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "powerhub_off.png": {
      "frame": { "x": 0, "y": 6144, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 6912, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },

M assets/dist/spritesheet-375.png => assets/dist/spritesheet-375.png +0 -0
M assets/dist/spritesheet-full.json => assets/dist/spritesheet-full.json +22 -13
@@ 9,7 9,7 @@
      "pivot": { "x": 1024, "y": 1024 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 2048, "h": 2048 }
    },
    "earth.png": {
    "mars.png": {
      "frame": { "x": 0, "y": 2048, "w": 2048, "h": 2048 },
      "rotated": false,
      "trimmed": false,


@@ 18,7 18,7 @@
      "pivot": { "x": 1024, "y": 1024 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 2048, "h": 2048 }
    },
    "moon.png": {
    "earth.png": {
      "frame": { "x": 0, "y": 4096, "w": 2048, "h": 2048 },
      "rotated": false,
      "trimmed": false,


@@ 27,6 27,15 @@
      "pivot": { "x": 1024, "y": 1024 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 2048, "h": 2048 }
    },
    "moon.png": {
      "frame": { "x": 2048, "y": 0, "w": 2048, "h": 2048 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 2048, "h": 2048 },
      "sourceSize": { "w": 2048, "h": 2048 },
      "pivot": { "x": 1024, "y": 1024 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 2048, "h": 2048 }
    },
    "hearty_party.png": {
      "frame": { "x": 0, "y": 6144, "w": 512, "h": 512 },
      "rotated": false,


@@ 136,7 145,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "powerhub_on.png": {
      "frame": { "x": 2048, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 4096, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 145,7 154,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "autoplr_on.png": {
      "frame": { "x": 2560, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 4608, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 154,7 163,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "superthruster_off.png": {
      "frame": { "x": 3072, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 5120, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 163,7 172,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 3584, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 5632, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 172,7 181,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "thruster_off.png": {
      "frame": { "x": 4096, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 6144, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 181,7 190,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "cargo_on.png": {
      "frame": { "x": 4608, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 6656, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 190,7 199,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "cargo_off.png": {
      "frame": { "x": 5120, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 7168, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 198,8 207,8 @@
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "hub_off.png": {
      "frame": { "x": 5632, "y": 0, "w": 512, "h": 512 },
    "powerhub_off.png": {
      "frame": { "x": 4096, "y": 512, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 207,8 216,8 @@
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "powerhub_off.png": {
      "frame": { "x": 6144, "y": 0, "w": 512, "h": 512 },
    "hub_off.png": {
      "frame": { "x": 4608, "y": 512, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },

M assets/dist/spritesheet-full.png => assets/dist/spritesheet-full.png +0 -0
A assets/src/mars.svg => assets/src/mars.svg +116 -0
@@ 0,0 1,116 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="2048"
   height="2048"
   viewBox="0 0 541.86664 541.86668"
   version="1.1"
   id="svg5"
   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
   sodipodi:docname="mars.svg"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <sodipodi:namedview
     id="namedview7"
     pagecolor="#000000"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     inkscape:document-units="px"
     showgrid="false"
     inkscape:zoom="0.38796647"
     inkscape:cx="778.41778"
     inkscape:cy="989.77625"
     inkscape:window-width="1918"
     inkscape:window-height="1057"
     inkscape:window-x="0"
     inkscape:window-y="21"
     inkscape:window-maximized="1"
     inkscape:current-layer="layer1" />
  <defs
     id="defs2">
    <clipPath
       clipPathUnits="userSpaceOnUse"
       id="clipPath1088">
      <path
         id="path1090"
         style="fill:#da8555;stroke-width:0.0921874"
         d="M 541.86663,270.93333 A 270.93333,270.93333 0 0 1 270.93333,541.86663 270.93333,270.93333 0 0 1 0,270.93333 270.93333,270.93333 0 0 1 270.93333,0 270.93333,270.93333 0 0 1 541.86663,270.93333 Z" />
    </clipPath>
    <clipPath
       clipPathUnits="userSpaceOnUse"
       id="clipPath1092">
      <path
         id="path1094"
         style="fill:#da8555;stroke-width:0.0921874"
         d="M 541.86663,270.93333 A 270.93333,270.93333 0 0 1 270.93333,541.86663 270.93333,270.93333 0 0 1 0,270.93333 270.93333,270.93333 0 0 1 270.93333,0 270.93333,270.93333 0 0 1 541.86663,270.93333 Z" />
    </clipPath>
    <clipPath
       clipPathUnits="userSpaceOnUse"
       id="clipPath1096">
      <path
         id="path1098"
         style="fill:#da8555;stroke-width:0.0921874"
         d="M 541.86663,270.93333 A 270.93333,270.93333 0 0 1 270.93333,541.86663 270.93333,270.93333 0 0 1 0,270.93333 270.93333,270.93333 0 0 1 270.93333,0 270.93333,270.93333 0 0 1 541.86663,270.93333 Z" />
    </clipPath>
    <clipPath
       clipPathUnits="userSpaceOnUse"
       id="clipPath1100">
      <path
         id="path1102"
         style="fill:#da8555;stroke-width:0.0921874"
         d="M 541.86663,270.93333 A 270.93333,270.93333 0 0 1 270.93333,541.86663 270.93333,270.93333 0 0 1 0,270.93333 270.93333,270.93333 0 0 1 270.93333,0 270.93333,270.93333 0 0 1 541.86663,270.93333 Z" />
    </clipPath>
  </defs>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1">
    <path
       id="path788"
       style="fill:#da8555;stroke-width:0.0921874"
       d="M 541.86664,270.93332 A 270.93332,270.93332 0 0 1 270.93332,541.86664 270.93332,270.93332 0 0 1 0,270.93332 270.93332,270.93332 0 0 1 270.93332,0 270.93332,270.93332 0 0 1 541.86664,270.93332 Z" />
    <path
       style="fill:#ffffff;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="m 150.27466,13.881843 c 35.72958,11.992447 73.4399,4.96055 73.4399,4.96055 0,0 29.59864,-7.188602 43.63356,5.224678 14.03493,12.413282 26.90074,5.535843 54.50303,-3.573312 27.60228,-9.109154 34.02573,-16.9096246 34.02573,-16.9096246 L 253.70247,-7.2584793 Z"
       id="path1000"
       clip-path="url(#clipPath1100)" />
    <path
       style="fill:#c4724b;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
       d="m 290.74464,82.230319 c 40.92916,19.820031 42.26131,36.944871 42.26131,36.944871 0,0 8.58153,23.3741 5.10284,32.68735 -3.47869,9.31326 10.77857,38.26533 22.08763,41.19945 11.30906,2.93413 30.41634,41.50267 42.63379,32.01685 12.21743,-9.48581 33.62497,-28.44722 30.93135,-42.99275 -2.69359,-14.54554 -0.63502,-67.31568 -28.18307,-68.20141 C 378.03044,112.99896 392.72125,82.028254 371.28487,78.709225 349.84851,75.390197 340.64477,46.653706 318.18712,57.397628 295.7295,68.14155 275.47443,74.442076 290.74464,82.230319 Z"
       id="path1004" />
    <path
       style="fill:#c4724b;fill-opacity:1;stroke:none;stroke-width:0.238163px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="m 204.90853,419.16259 c 73.33571,23.92056 133.85944,17.25742 133.85944,17.25742 l -88.31974,16.88834 c 0,0 73.71257,30.85138 63.70542,41.1298 -10.00716,10.27845 -89.56448,16.823 -98.81147,5.52201 -9.24699,-11.30097 1.35794,-21.1439 -24.82973,-23.56829 -26.18764,-2.42438 -92.058752,-7.47467 -80.5049,-18.22357 11.55383,-10.74892 84.6916,6.94821 83.99168,-8.04389 -0.69994,-14.99208 -15.89188,-21.17089 -8.06113,-27.32 7.83073,-6.14914 18.97043,-3.64182 18.97043,-3.64182 z"
       id="path1006" />
    <path
       style="fill:#c4724b;fill-opacity:1;stroke:none;stroke-width:0.260459px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="m 218.88032,288.22208 c 62.66042,18.30099 72.3813,34.9639 70.78597,39.83545 -1.59533,4.87156 21.5758,28.36271 68.05737,27.30698 46.48162,-1.05575 116.3579,2.84383 111.4476,25.18047 -4.91028,22.33666 -33.14862,86.4198 -43.36723,85.66296 -10.21862,-0.75684 -70.16394,58.18309 -93.32451,57.10993 -23.16057,-1.07316 -7.36497,23.37388 -7.36497,23.37388 l 188.4428,-91.08357 65.76135,-120.32555 c 0,0 -34.43207,-20.2762 -43.7216,-13.19755 -9.2895,7.07861 -9.36013,-68.7778 -18.53767,-64.99857 -9.17756,3.77921 -18.04556,93.67885 -39.12149,93.54711 -21.07593,-0.13174 -141.24953,-28.44658 -141.24953,-28.44658 0,0 -34.34662,-7.80951 -12.38659,-87.93631 21.96004,-80.12681 -49.23158,53.45259 -49.23158,53.45259 z"
       id="path1008"
       clip-path="url(#clipPath1096)" />
    <path
       style="fill:#ffffff;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="m 160.88427,533.86862 c 52.36478,-16.63456 42.57143,-3.76547 80.89575,-8.99583 38.32432,-5.23036 75.06333,5.63404 75.06333,5.63404 0,0 24.33757,10.91726 34.97718,8.73268 10.63964,-2.18456 -42.85583,13.40416 -42.85583,13.40416 z"
       id="path1002"
       clip-path="url(#clipPath1092)" />
    <path
       style="fill:#c4724b;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
       d="M 116.30533,94.068849 C 106.99078,149.35756 98.713053,145.54002 89.239957,158.36081 79.76686,171.18159 78.033286,183.47029 65.7813,176.776 53.529314,170.08171 38.622104,193.16637 34.308647,177.71794 29.995193,162.26951 17.27797,159.13746 17.27797,159.13746 L 41.378975,112.7075 62.182036,66.617889 122.5311,36.735467 137.26652,29.082648 c 0,0 4.30996,9.489217 -4.01468,19.34859 -8.32463,9.859369 -22.42483,13.084765 -22.84053,22.557485 -0.4157,9.472724 5.89402,23.080126 5.89402,23.080126 z"
       id="path1064"
       clip-path="url(#clipPath1088)" />
    <path
       style="fill:#a46748;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
       d="m 300.17516,273.48989 c 0,0 -5.09789,23.3929 -13.42544,23.13498 -8.32754,-0.25789 -18.03794,-0.85711 -13.00204,2.58945 5.03589,3.4466 26.76075,22.30097 36.49384,26.09596 9.73307,3.79498 20.09556,22.82955 45.60697,22.0032 25.51142,-0.82632 100.65684,8.94218 85.80774,5.3616 -14.84911,-3.58055 -80.19884,-7.61606 -89.63181,-11.36383 -9.43298,-3.74779 -33.74943,-17.75211 -33.74943,-20.77992 0,-3.02782 -16.29849,-9.02327 -11.09562,-34.39343 5.2029,-25.37018 -7.00421,-12.64801 -7.00421,-12.64801 z"
       id="path1066" />
    <path
       style="fill:#a46748;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
       d="m 309.99157,78.247006 c 0,0 41.40814,26.653604 41.47225,37.189874 0.0641,10.53627 -7.38243,51.00181 7.90434,58.45479 15.28681,7.45298 33.74872,39.54567 34.89741,32.79277 1.14866,-6.7529 -26.19896,-41.5386 -31.3154,-44.70177 -5.11643,-3.16316 -5.61239,-45.33077 4.29985,-46.71905 9.91222,-1.38828 -57.25845,-37.016614 -57.25845,-37.016614 z"
       id="path1068" />
  </g>
</svg>

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

        .w-90 {
            width: 90%;
        }

    </style>
</html>

M client/src/gateway.ts => client/src/gateway.ts +3 -2
@@ 90,7 90,7 @@ export async function gateway_connect(gateway_url: string, username: string): Pr
    let handshake_start_msg;
    if (global.can_beam_out) {
        handshake_start_msg = MessageC2SHello.encode({
            version: 5,
            version: 6,
            requestedUsername: username,
            nextState: State.Play,
            user: window.localStorage.getItem("user")!,


@@ 98,7 98,7 @@ export async function gateway_connect(gateway_url: string, username: string): Pr
        }).finish();
    } else {
        handshake_start_msg = MessageC2SHello.encode({
            version: 5,
            version: 6,
            requestedUsername: username,
            nextState: State.Play,
            // @ts-ignore


@@ 196,6 196,7 @@ export async function gateway_connect(gateway_url: string, username: string): Pr
                    children: pkt.module!.children,
                };
                global.tree.set(pkt.module!.id, module);
                global.clicked = null;
            } else if (pkt_id == MessageS2CModuleRemove_packetInfo.type) {
                let pkt = MessageS2CModuleRemove.decode(pkt_data);
                global.clicked = pkt.module!.id;

M client/src/index.ts => client/src/index.ts +39 -9
@@ 1,5 1,5 @@
import {Logger, logSetup} from "./logger";
import {gateway_connect, GatewayClient, AttachedModule} from "./gateway";
import {AttachedModule, gateway_connect, GatewayClient} from "./gateway";
import {Player} from "./protocol/player";
import {Planet, PlanetType} from "./protocol/planet";
import {Module, ModuleType} from "./protocol/module";


@@ 7,7 7,15 @@ import {
    MessageC2SAuthenticateAndBeamOut,
    MessageC2SAuthenticateAndBeamOut_packetInfo,
    MessageC2SInput,
    MessageC2SInput_packetInfo, MessageC2SModuleDetach, MessageC2SModuleDetach_packetInfo, MessageC2SModuleGrabBegin, MessageC2SModuleGrabBegin_packetInfo, MessageC2SModuleGrabEnd, MessageC2SModuleGrabEnd_packetInfo, MessageC2SMouseInput, MessageC2SMouseInput_packetInfo
    MessageC2SInput_packetInfo,
    MessageC2SModuleDetach,
    MessageC2SModuleDetach_packetInfo,
    MessageC2SModuleGrabBegin,
    MessageC2SModuleGrabBegin_packetInfo,
    MessageC2SModuleGrabEnd,
    MessageC2SModuleGrabEnd_packetInfo,
    MessageC2SMouseInput,
    MessageC2SMouseInput_packetInfo
} from "./protocol/message_c2s";
import {encode} from "./serde";
import {InputType} from "./protocol/input";


@@ 148,8 156,12 @@ async function client_main(server: string, username: string, texture_quality: st
                let rot = -global.modules[i].rotation;
                relativeX = relativeX*Math.cos(rot) - relativeY*Math.sin(rot);
                relativeY = relativeX*Math.sin(rot) + relativeY*Math.cos(rot);
                if (-25 < relativeX && relativeX < 25) {
                    if (-25 < relativeY && relativeY < 25) {
                let bound = [-25, 25, -25, 25];
                if (global.modules[i].moduleType == ModuleType.Cargo) {
                    bound = [-18.75, 18.75, -25, 21.875];
                }
                if (bound[0] < relativeX && relativeX < bound[1]) {
                    if (bound[2] < relativeY && relativeY < bound[3]) {
                        let msg = MessageC2SModuleGrabBegin.encode({
                            moduleId: global.modules[i].id,
                            worldposX: worldX,


@@ 164,10 176,16 @@ async function client_main(server: string, username: string, texture_quality: st
                let relativeX = value.x - worldX;
                let relativeY = value.y - worldY;
                let rot = -value.rotation;
                relativeX = relativeX*Math.cos(rot) - relativeY*Math.sin(rot);
                relativeY = relativeX*Math.sin(rot) + relativeY*Math.cos(rot);
                if (-25 < relativeX && relativeX < 25) {
                    if (-25 < relativeY && relativeY < 25) {
                let adjustedX = relativeX*Math.cos(rot) - relativeY*Math.sin(rot);
                let adjustedY = relativeX*Math.sin(rot) + relativeY*Math.cos(rot);
                let bound = [-25, 25, -25, 25];
                if (value.module_type == ModuleType.Cargo) {
                    bound = [-18.75, 18.75, -25, 21.875];
                }
                if (bound[0] < adjustedX && adjustedX < bound[1]) {
                    if (bound[2] < adjustedY && adjustedY < bound[3]) {
                        console.log("relative: " + relativeX + ", " + relativeY);
                        console.log("adjusted: " + adjustedX + ", " + adjustedY);
                        let msg = MessageC2SModuleDetach.encode({
                            moduleId: key,
                        }).finish();


@@ 211,7 229,6 @@ async function client_main(server: string, username: string, texture_quality: st
            
            for (let i = 0; i < global.modules.length; i++) {
                if(global.clicked === global.modules[i].id) {
                    global.clicked = null;
                    let msg = MessageC2SModuleGrabEnd.encode({
                        moduleId: global.modules[i].id,
                        worldposX: worldX,


@@ 220,6 237,7 @@ async function client_main(server: string, username: string, texture_quality: st
                    global.client?.socket.send(encode(MessageC2SModuleGrabEnd_packetInfo.type, msg))
                }
            }
            global.clicked = null;

            global.client?.socket.send(encode(MessageC2SMouseInput_packetInfo.type, msg))
        }


@@ 331,6 349,7 @@ async function client_main(server: string, username: string, texture_quality: st
                    global.context.moveTo(global.me!.x - global.me!.x, global.me!.y - global.me!.y);
                    global.context.lineTo(planet.x - global.me!.x, planet.y - global.me!.y);
                    global.context.stroke();
                    console.log("moon: " + planet.x + ", " + planet.y);

                    document.getElementById("pos-moon")!.innerText = `Relative to Moon: ${Math.trunc(global.me!.x - planet.x)}, ${Math.trunc(global.me!.y - planet.y)}`
                }


@@ 343,6 362,15 @@ async function client_main(server: string, username: string, texture_quality: st
                    global.context.lineTo(planet.x - global.me!.x, planet.y - global.me!.y);
                    global.context.stroke();
                }
            } else if (planet.planetType == PlanetType.Mars) {
                if (global.me !== null) {
                    global.context.beginPath();
                    global.context.strokeStyle = "orange";
                    global.context.lineWidth = 5;
                    global.context.moveTo(global.me!.x - global.me!.x, global.me!.y - global.me!.y);
                    global.context.lineTo(planet.x - global.me!.x, planet.y - global.me!.y);
                    global.context.stroke();
                }
            }
        }



@@ 484,6 512,8 @@ function planet_type_to_tex_id(ty: PlanetType): string {
        return "earth.png"
    } else if (ty == PlanetType.Moon) {
        return "moon.png"
    } else if (ty == PlanetType.Mars) {
        return "mars.png"
    }
    return "unknown.png"
}

M client/src/protocol/planet.ts => client/src/protocol/planet.ts +6 -0
@@ 7,6 7,7 @@ export enum PlanetType {
  UNKNOWN = 0,
  Earth = 1,
  Moon = 2,
  Mars = 3,
  UNRECOGNIZED = -1,
}



@@ 21,6 22,9 @@ export function planetTypeFromJSON(object: any): PlanetType {
    case 2:
    case "Moon":
      return PlanetType.Moon;
    case 3:
    case "Mars":
      return PlanetType.Mars;
    case -1:
    case "UNRECOGNIZED":
    default:


@@ 36,6 40,8 @@ export function planetTypeToJSON(object: PlanetType): string {
      return "Earth";
    case PlanetType.Moon:
      return "Moon";
    case PlanetType.Mars:
      return "Mars";
    case PlanetType.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";

M protocol/src/lib.rs => protocol/src/lib.rs +20 -16
@@ 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;


@@ 14,7 14,7 @@ use protobuf::{Enum, Message};
use std::error::Error;
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));

pub const PROTOCOL_VERSION: u32 = 5;
pub const PROTOCOL_VERSION: u32 = 6;

pub mod api;



@@ 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,11 290,12 @@ 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(),
            PlanetType::Moon => "moon".to_string(),
            PlanetType::Mars => "mars".to_string(),
            PlanetType::UNKNOWN => "missing".to_string(),
        }
    }

M protocol/src/pbuf/planet.proto => protocol/src/pbuf/planet.proto +1 -0
@@ 12,4 12,5 @@ enum PlanetType {
  UNKNOWN = 0;
  Earth = 1;
  Moon = 2;
  Mars = 3;
}
\ No newline at end of file

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 +32 -26
@@ 1,12 1,13 @@
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 16,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 26,8 @@ pub struct EntityHandler {
}

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


@@ 42,16 41,17 @@ impl EntityHandler {
        ids
    }
    pub fn get_planet(&self, planet_type: PlanetType) -> Option<Planet> {
        let mut planets = self.get_planets();
        for i in 0..planets.len() {
            if planets[i].planet_type == planet_type {
                planets.remove(i);
        self.get_planets().iter().find(|u| u.planet_type == planet_type).cloned()
    }
    pub fn get_planet_id(&self, planet_type: PlanetType) -> Option<EntityId> {
        for (id, entity) in &self.entities {
            if let Entity::Planet(planet) = entity {
                if planet.planet_type == planet_type {
                    return Some(*id);
                }
            }
        }
        if planets.is_empty() {
            return None;
        }
        Some(planets[0].clone())
        None
    }

    pub fn get_players(&self) -> Vec<(SocketAddr, Player)> {


@@ 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);


@@ 140,14 137,23 @@ impl EntityHandler {
        }
        modules
    }
    pub fn get_all_attached_id(&self) -> Vec<(EntityId, AttachedModule)> {
        let mut modules = Vec::new();
        for (id, entity) in self.entities.clone() {
            if let Entity::AttachedModule(module) = entity {
                modules.push((id, module.clone()));
            }
        }
        modules
    }
    pub fn get_attached_from_id(&self, id: EntityId) -> Option<AttachedModule> {
        if let Some(Entity::AttachedModule(module)) = self.entities.get(&id) {
            return Some(module.clone());
        }
        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 183,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 +205 -131
@@ 1,27 1,27 @@
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::module::{AttachedModule, ModuleTemplate, Module, Attachment};
use crate::manager::{ClientHandlerMessage, ClientManager, PhysicsData, Player, PlayerInput};
use crate::module::{AttachedModule, Module};
use crate::{recv, send, SCALE};
use async_std::net::TcpStream;
use async_std::{channel::Receiver, sync::RwLock};
use async_tungstenite::WebSocketStream;
use futures::stream::{SplitSink, SplitStream};
use futures::{FutureExt, SinkExt, StreamExt};
use log::{debug, error, info, warn, trace};
use nalgebra::{point, vector, Matrix, Vector2};
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::module::ModuleType;

use protobuf::SpecialFields;
use starkingdoms_protocol::state::State;
use starkingdoms_protocol::{MessageC2S, MessageS2C, PROTOCOL_VERSION};
use std::error::Error;


@@ 53,7 53,7 @@ pub async fn handle_client(
                        let msg = MessageS2C::Chat(MessageS2CChat {
                            from,
                            message,
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;


@@ 63,7 63,7 @@ pub async fn handle_client(
                    if matches!(state, State::Play) {
                        let msg = MessageS2C::PlayersUpdate(MessageS2CPlayersUpdate {
                            players,
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;


@@ 73,7 73,7 @@ pub async fn handle_client(
                    if matches!(state, State::Play) {
                        let msg = MessageS2C::PlanetData(MessageS2CPlanetData {
                            planets,
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;


@@ 83,7 83,7 @@ pub async fn handle_client(
                    if matches!(state, State::Play) {
                        let msg = MessageS2C::ModulesUpdate(MessageS2CModulesUpdate {
                            modules,
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;


@@ 93,7 93,7 @@ pub async fn handle_client(
                    if matches!(state, State::Play) {
                        let msg = MessageS2C::ModuleTreeUpdate(MessageS2CModuleTreeUpdate {
                            tree: modules,
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;


@@ 109,7 109,7 @@ pub async fn handle_client(
            error!("[{}] ping timeout", remote_addr);
            let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                reason: GoodbyeReason::PingPongTimeout.into(),
                special_fields: Default::default(),
                special_fields: SpecialFields::default(),
            })
            .try_into()?;
            send!(client_tx, msg).await?;


@@ 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?;


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


@@ 167,7 169,7 @@ pub async fn handle_client(
                                    );
                                    let msg: Vec<u8> = MessageS2C::Goodbye(MessageS2CGoodbye {
                                        reason: GoodbyeReason::UsernameTaken.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()?;


@@ 232,7 234,7 @@ pub async fn handle_client(

                                let mut player = Player {
                                    handle: player_handle,
                                    input: Default::default(),
                                    input: PlayerInput::default(),
                                    addr: remote_addr,
                                    auth_token: None,
                                    auth_user: None,


@@ 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
                                    {


@@ 284,40 286,6 @@ pub async fn handle_client(

                                data_handle.rigid_body_set = rigid_body_set;
                                data_handle.collider_set = collider_set;

                                /*let module_id = AttachedModule::attach_new(
                                    &mut data_handle,
                                    &mut e_write_handle,
                                    player_id,
                                    player_id,
                                    ModuleTemplate {
                                        translation: vector![0.0, 50.0],
                                        mass_properties: MassProperties::new(
                                            point![0.0, 0.0],
                                            0.0001,
                                            0.005,
                                        ),
                                        module_type: ModuleType::Cargo,
                                    },
                                    1,
                                );
                                let module_id = AttachedModule::attach_new(
                                    &mut data_handle,
                                    &mut e_write_handle,
                                    module_id,
                                    player_id,
                                    ModuleTemplate {
                                        translation: vector![0.0, 50.0],
                                        mass_properties: MassProperties::new(
                                            point![0.0, 0.0],
                                            0.0001,
                                            0.005,
                                        ),
                                        module_type: ModuleType::Cargo,
                                    },
                                    2,
                                );*/

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


@@ 331,7 299,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 315,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?;


@@ 360,7 328,10 @@ pub async fn handle_client(
                    MessageC2S::Chat(pkt) => {
                        info!("[{}] CHAT: [{}] {}", remote_addr, username, pkt.message);

                        for (_addr, client_thread) in mgr.handlers.read().await.iter() {
                        let read_handle = mgr.handlers.read().await;

                        #[allow(clippy::significant_drop_in_scrutinee)] // i know
                        for (_addr, client_thread) in read_handle.iter() {
                            match client_thread
                                .tx
                                .send(ClientHandlerMessage::ChatMessage {


@@ 378,21 349,24 @@ pub async fn handle_client(
                    }
                    MessageC2S::Ping(_) => {
                        let msg = MessageS2C::Pong(MessageS2CPong {
                            special_fields: Default::default(),
                            special_fields: SpecialFields::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                        ping_timeout = SystemTime::now() + Duration::from_secs(10);
                    }
                    #[allow(clippy::significant_drop_tightening)]
                    // TODO: Remove when this lint is developed more
                    MessageC2S::Input(p) => {
                        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 384,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 394,13 @@ 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
                        {


@@ 430,7 408,7 @@ pub async fn handle_client(
                                info!("[{}] * Beamed out successfully", remote_addr);
                                let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                    reason: GoodbyeReason::Done.into(),
                                    special_fields: Default::default(),
                                    special_fields: SpecialFields::default(),
                                })
                                .try_into()?;
                                send!(client_tx, msg).await?;


@@ 441,35 419,44 @@ pub async fn handle_client(
                            }
                        }
                    }
                    MessageC2S::MouseInput(p) => {
                    MessageC2S::MouseInput(_p) => {
                        //debug!("[{}] player input: {:?}", remote_addr, p);
                    }
                    #[allow(clippy::significant_drop_tightening)]
                    // TODO: Remove when this lint is developed more
                    MessageC2S::ModuleDetach(p) => {
                        let mut entities = entities.write().await;
                        let mut data_handle = data.write().await;
                        let mut module: Option<AttachedModule> = None;
                        debug!("[{}] detach: {:?}", remote_addr, p);
                        debug!("[{}] {:?}", remote_addr, entities.entities);

                        let module: Option<AttachedModule>;
                        //debug!("[{}] detach: {:?}", remote_addr, p);
                        //debug!("[{}] {:?}", remote_addr, entities.entities);


                        // START: MY CHANGES
                        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;
                        }
                        // END: MY CHANGES



                        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(),


@@ 477,94 464,181 @@ 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);
                        }
                    }
                    #[allow(clippy::significant_drop_tightening)]
                    // TODO: Remove when this lint is developed more
                    MessageC2S::ModuleGrabEnd(p) => {
                        let mut entities = entities.write().await;
                        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);
                            //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 +60 -198
@@ 1,28 1,40 @@
use crate::entity::Entity;
use crate::handler::handle_client;
use crate::manager::{ClientHandler, ClientManager};
// 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::manager::{ClientManager};
use crate::timer::timer_main;
use async_std::io::WriteExt;
use async_std::net::{TcpListener, TcpStream};
use async_std::net::{TcpListener};
use async_std::sync::Arc;
use async_std::sync::RwLock;
use entity::EntityHandler;
use futures::StreamExt;
use lazy_static::lazy_static;
use log::{error, info, warn, Level, debug};
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, Real,
    MultibodyJointSet, NarrowPhase, RigidBodySet,
};
use serde::{Deserialize, Serialize};
use starkingdoms_protocol::PROTOCOL_VERSION;
use std::error::Error;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::thread;
use std::time::Duration;
use parking_lot::deadlock::check_deadlock;
use crate::tcp_handler::handle_request;

pub mod handler;
pub mod manager;


@@ 31,175 43,17 @@ pub mod timer;
pub mod macros;
pub mod api;
pub mod entity;
pub mod module;
pub mod orbit;
pub mod planet;
pub mod module;
pub mod tcp_handler;

const SCALE: f64 = 10.0;

async fn handle_request(
    conn: TcpStream,
    remote_addr: SocketAddr,
    mgr: ClientManager,
    entities: Arc<RwLock<EntityHandler>>,
    physics_data: Arc<RwLock<PhysicsData>>,
) {
    match _handle_request(conn, remote_addr, mgr, entities, physics_data).await {
        Ok(_) => (),
        Err(e) => {
            error!("[{}] error in handler thread: {}", remote_addr, e);
        }
    }
}

async fn _handle_request(
    mut conn: TcpStream,
    remote_addr: SocketAddr,
    mgr: ClientManager,
    entities: Arc<RwLock<EntityHandler>>,
    physics_data: Arc<RwLock<PhysicsData>>,
) -> Result<(), Box<dyn Error>> {
    let mut peek_buf = [0u8; 9];

    loop {
        let read = conn.peek(&mut peek_buf).await?;
        if read == 9 {
            break;
        }
    }

    if peek_buf == *b"GET /ping" {
        info!("[{}] incoming http connection", remote_addr);
        let ping_resp = serde_json::to_string(&ServerPingResponse {
            version: ServerPingResponseVersion {
                name: env!("STK_VERSION_NAME").to_string(), // Set by build.rs
                number: env!("STK_VERSION").to_string(),    // Set by build.rs
                protocol: PROTOCOL_VERSION,
                channel: env!("STK_CHANNEL").to_string(),
                build: env!("STK_BUILD").to_string(),
            },
            players: CMGR.usernames.read().await.len() as u32,
            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{}",
            ping_resp.len(),
            ping_resp
        );
        let http_resp = resp_str.as_bytes();

        conn.write_all(http_resp).await?;
        info!(
            "[{}] sent ping response (200 OK {} bytes)",
            remote_addr,
            ping_resp.len()
        );
        return Ok(());
    }
    info!("[{}] incoming websocket connection", remote_addr);

    // if its not GET /ping, assume its websocket and attempt to handshake with them
    let ws_stream = async_tungstenite::accept_async(conn).await?;

    let (ws_write, ws_read) = ws_stream.split();

    let (tx, rx) = async_std::channel::unbounded();

    let client = ClientHandler { tx };

    // Acquire the write lock in a small scope, so it's dropped as quickly as possible
    {
        mgr.handlers.write().await.insert(remote_addr, client);
    }

    info!("[{}] passing to client handler", remote_addr);

    //forward the stream to the sink to achieve echo
    match handle_client(
        mgr.clone(),
        entities.clone(),
        physics_data.clone(),
        remote_addr,
        rx,
        ws_write,
        ws_read,
    )
    .await
    {
        Ok(_) => (),
        Err(e) if e.is::<async_tungstenite::tungstenite::error::Error>() => {
            let e = e
                .downcast::<async_tungstenite::tungstenite::error::Error>()
                .unwrap();
            if matches!(*e, async_tungstenite::tungstenite::Error::ConnectionClosed) {
                info!("[{}] connection closed normally", remote_addr);
            } else {
                error!("[{}] error in client thread: {}", remote_addr, e);
            }
        }
        Err(e) => {
            error!("[{}] error in client thread: {}", remote_addr, e);
        }
    }

    // clean up values left over
    {
        mgr.handlers.write().await.remove(&remote_addr);
        mgr.usernames.write().await.remove(&remote_addr);
        // remove player physics body
        let mut entity_write = entities.write().await;
        let mut data = physics_data.write().await;
        let mut rigid_body_set = data.rigid_body_set.clone();
        let mut island_manager = data.island_manager.clone();
        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 => {
                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() {
            rigid_body_set.remove(
                player.handle,
                &mut island_manager,
                &mut collider_set,
                &mut impulse_joint_set,
                &mut multibody_joint_set,
                true,
            );
            for module in player.search_modules(&entity_write) {
                rigid_body_set.remove(
                    module.handle,
                    &mut island_manager,
                    &mut collider_set,
                    &mut impulse_joint_set,
                    &mut multibody_joint_set,
                    true,
                );
                let module_id = entity_write.get_id_from_attached(module).unwrap();
                entity_write.entities.remove(&module_id);
            }
        }
        data.rigid_body_set = rigid_body_set;
        data.collider_set = collider_set;
        data.island_manager = island_manager;
        data.impulse_joint_set = impulse_joint_set;
        data.multibody_joint_set = multibody_joint_set;
        entity_write.entities.remove(&player_id);
    }

    Ok(())
}

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],


@@ 207,7 61,7 @@ lazy_static! {
            dt: 1.0 / 20.0,
            joint_erp: 0.2,
            erp: 0.5,
            max_stabilization_iterations: 8,
            max_stabilization_iterations: 16,
            ..Default::default()
        },
        island_manager: IslandManager::new(),


@@ 224,9 78,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 105,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));


@@ 282,7 138,13 @@ async fn main() {
    let physics_data = DATA.clone();
    let entities_timer = ENTITIES.clone();
    let _timer_thread = async_std::task::spawn(async move {
        timer_main(mgr_timer, physics_data, entities_timer).await;
        match timer_main(mgr_timer, physics_data, entities_timer).await {
            Ok(_) => (),
            Err(e) => {
                error!("timer thread exited with error: {}", e);
                std::process::exit(1);
            }
        }
    });

    let try_socket = TcpListener::bind(&addr).await;

M server/src/manager.rs => server/src/manager.rs +25 -21
@@ 1,24 1,21 @@
use async_std::channel::Sender;
use async_std::sync::RwLock;
use log::debug;
use nalgebra::{point, vector};

use rapier2d_f64::na::Vector2;
use rapier2d_f64::prelude::{
    BroadPhase, CCDSolver, ColliderBuilder, ColliderSet, FixedJointBuilder, ImpulseJointHandle,
    ImpulseJointSet, IntegrationParameters, IslandManager, Isometry, MassProperties,
    MultibodyJointSet, NarrowPhase, PhysicsPipeline, Real, RigidBodyBuilder, RigidBodyHandle,
    RigidBodySet,
    BroadPhase, CCDSolver, ColliderSet, ImpulseJointSet, IntegrationParameters, IslandManager,
    MultibodyJointSet, NarrowPhase, PhysicsPipeline, RigidBodyHandle, RigidBodySet,
};
use starkingdoms_protocol::api::APISavedPlayerData;
use starkingdoms_protocol::module::ModuleType;

use std::collections::HashMap;
use std::f64::consts::PI;

use std::net::SocketAddr;
use std::sync::Arc;

use crate::entity::{get_entity_id, Entity, EntityHandler, EntityId};
use crate::SCALE;
use crate::module::{Attachment, AttachedModule};
use crate::entity::{Entity, EntityHandler, EntityId};

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

#[derive(Clone)]
pub struct ClientManager {


@@ 37,6 34,7 @@ pub struct Player {
}

impl Player {
    #[allow(clippy::missing_const_for_fn)] // This will eventually do something, but for now it doesn't
    pub fn as_api_data(&self) -> APISavedPlayerData {
        APISavedPlayerData {}
    }


@@ 45,27 43,32 @@ 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)?
                {
                    debug!("player child: {:?}", *child_module);
                    debug!("player target: {:?}", module);
                    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 {
                    let parent = child_module.find_parent(module, entities);
                    if parent.is_some() {
                        return parent;
                    }
                }


@@ 76,6 79,7 @@ impl Player {
}

#[derive(Default, Clone, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct PlayerInput {
    pub up: bool,
    pub left: bool,

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

use log::debug;
use nalgebra::{Vector2, point, vector, Isometry, Isometry2, Complex, Unit};
use rapier2d_f64::prelude::{RigidBodyHandle, Real, MassProperties, ColliderBuilder, RigidBodyBuilder, FixedJointBuilder, ImpulseJointHandle, RigidBodySet};
use nalgebra::{point, vector, Isometry2, Unit, Vector2};
use protobuf::SpecialFields;
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 {


@@ 22,7 30,7 @@ pub struct ModuleTemplate {
    pub module_type: ModuleType,
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachedModule {
    pub handle: RigidBodyHandle,
    pub module_type: ModuleType,


@@ 35,137 43,122 @@ impl AttachedModule {
        entities: &mut EntityHandler,
        parent: EntityId,
        player_id: EntityId,
        module: Module,
        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.),
        };

        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 module_pos = parent_pos + relative_pos;
        let module_body = data.rigid_body_set.get_mut(module.handle).unwrap();
        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);
        module_body.set_angvel(parent_angvel, true);
        if let Some(id) = entities.get_from_module(module) {
            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)
                .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);
            module_body.set_angvel(parent_angvel, true);

        let attach_joint = FixedJointBuilder::new()
            .local_anchor1(anchor)
            .local_anchor2(point![0.0, 0.0 / SCALE])
            .local_frame2(Isometry2::rotation(rotation))
            .build();
        let attach_joint_handle =
            data.impulse_joint_set
                .insert(parent_handle, module.handle, attach_joint, true);
        let attached_module = AttachedModule {
            handle: module.handle,
            module_type: module.module_type,
            player_id,
            children: [None, None, None, None],
        };
        let attached_id = get_entity_id();
        match parent_entity {
            Entity::Player(ref mut player) => {
                player.children[attachment_slot] = Some(Attachment {
                    child: attached_id,
                    connection: attach_joint_handle,
                });
            }
            Entity::AttachedModule(ref mut module) => {
                module.children[attachment_slot] = Some(Attachment {
                    child: attached_id,
                    connection: attach_joint_handle,
                });
            }
            _ => {
                panic!("unexpected parent");
            }
        };
        entity_map.remove(&entities.get_from_module(&module).unwrap());
        entity_map.insert(attached_id, Entity::AttachedModule(attached_module));
        entities.entities = entity_map;
        Ok(attached_id)
            let attach_joint = FixedJointBuilder::new()
                .local_anchor1(anchor)
                .local_anchor2(point![0.0, 0.0 / SCALE])
                .local_frame2(Isometry2::rotation(rotation))
                .build();
            let attach_joint_handle =
                data.impulse_joint_set
                    .insert(parent_handle, module.handle, attach_joint, true);
            let attached_module = Self {
                handle: module.handle,
                module_type: module.module_type,
                player_id,
                children: [None, None, None, None],
            };
            let attached_id = get_entity_id();
            match parent_entity {
                Entity::Player(ref mut player) => {
                    player.children[attachment_slot] = Some(Attachment {
                        child: attached_id,
                        connection: attach_joint_handle,
                    });
                }
                Entity::AttachedModule(ref mut module) => {
                    module.children[attachment_slot] = Some(Attachment {
                        child: attached_id,
                        connection: attach_joint_handle,
                    });
                }
                _ => {
                    panic!("unexpected parent");
                }
            };
            entity_map.remove(&id);
            entity_map.insert(attached_id, Entity::AttachedModule(attached_module));
            entities.entities = entity_map;
            return Ok(attached_id);
        }
        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 parent_entity = entity_map
            .get_mut(&parent_id)
            .expect("parent id does not exist");
        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)?;

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


@@ 192,43 185,38 @@ 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 {
                    debug!("child: {:?}", child);
                    data.impulse_joint_set.remove(child.connection, true);
                    let child_body = entities.get_attached_from_id(child.child).unwrap();
                    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());
                    let attached_id = get_entity_id();
                    entity_map.insert(attached_id, Entity::Module(new_module));
                }
            for child in element.clone().children.into_iter().flatten() {
                data.impulse_joint_set.remove(child.connection, true);
                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)?);
                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,
        entities: &mut EntityHandler,
        parent: EntityId,
        player_id: EntityId,
        module: ModuleTemplate,
        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");
        let parent_entity = entity_map.get_mut(&parent)?;
        let parent_handle = match parent_entity {
            Entity::Player(player) => player.handle,
            Entity::AttachedModule(module) => module.handle,


@@ 237,47 225,27 @@ 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


@@ 303,7 271,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,


@@ 329,10 297,10 @@ 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 {
    pub const fn to_module(&self) -> Module {
        Module {
            handle: self.handle,
            module_type: self.module_type,


@@ 340,45 308,53 @@ impl AttachedModule {
            flags: 0,
        }
    }

    // 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() {
            if let Some(Some(child)) = children.get(i) {
                prot_children.push(starkingdoms_protocol::module::Attachment {
                    id: child.child,
                    slot: i as u32,
                    special_fields: Default::default(),
                    slot: u32::try_from(i).ok()?,
                    special_fields: SpecialFields::default(),
                });
            }
        }
        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(),
        }
            special_fields: SpecialFields::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));
            }


@@ 386,19 362,20 @@ 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)?
                {
                    debug!("child: {:?}", *child_module);
                    debug!("target: {:?}", module);
                    if *child_module == module {
                        return Some((slot as u8, entities.get_id_from_attached(self.clone()).unwrap()));
                    if *child_module == *module {
                        return Some((
                            u8::try_from(slot).ok()?,
                            entities.get_id_from_attached(self)?,
                        ));
                    }
                    let parent = child_module.find_parent(module.clone(), entities);
                    if let Some(_) = parent {
                    let parent = child_module.find_parent(module, entities);
                    if parent.is_some() {
                        return parent;
                    }
                }


@@ 408,7 385,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 +24 -9
@@ 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,20 8,35 @@ 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;
pub const MOON_MASS_BIAS: f64 = 10.0;
pub const MOON_MASS_BIAS: f64 = 2.0;
pub const MOON_PERIAPSIS_BIAS: f64 = 0.1;
pub const MOON_APOAPSIS_BIAS: f64 = 0.1;
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;

pub const MARS_RADIUS_BIAS: f64 = 1.0;
pub const MARS_MASS_BIAS: f64 = 10.0;
pub const MARS_PERIHELION_BIAS: f64 = 0.5;
pub const MARS_APHELION_BIAS: f64 = 0.5;
pub const MARS_ORBIT_TIME_BIAS: f64 = 0.2;

pub const MARS_RADIUS_RL: f64 = 3_390_000.0;
pub const MARS_RADIUS: f64 = MARS_RADIUS_RL * GAME_SCALE_DISTANCE * MARS_RADIUS_BIAS;
pub const MARS_MASS_RL: f64 = 63_900_000_000_000_000_000_000.0;
pub const MARS_MASS: f64 = MARS_MASS_RL * GAME_SCALE_MASS * MARS_MASS_BIAS;
pub const MARS_PERIHELION: f64 = 206_700_000.0 * GAME_SCALE_DISTANCE * MARS_PERIHELION_BIAS;
pub const MARS_APHELION: f64 = 249_200_000.0 * GAME_SCALE_DISTANCE * MARS_APHELION_BIAS;
pub const MARS_ORBIT_TIME_RL: f64 = 59_360_000.0;
pub const MARS_ORBIT_TIME: f64 = MARS_ORBIT_TIME_RL * GAME_SCALE_TIME * MARS_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 +36 -17
@@ 1,5 1,5 @@
use log::debug;
use nalgebra::{vector, Vector2};
use protobuf::SpecialFields;
use rapier2d_f64::prelude::{
    ColliderBuilder, ColliderSet, RigidBodyBuilder, RigidBodyHandle, RigidBodySet,
};


@@ 7,9 7,7 @@ use starkingdoms_protocol::planet::PlanetType;
use std::collections::HashMap;

use crate::entity::{get_entity_id, Entities, Entity, EntityId};
use crate::orbit::constants::{
    EARTH_MASS, EARTH_RADIUS, MOON_APOAPSIS, MOON_MASS, MOON_PERIAPSIS, MOON_RADIUS,
};
use crate::orbit::constants::{EARTH_MASS, EARTH_RADIUS, MARS_APHELION, MARS_MASS, MARS_PERIHELION, MARS_RADIUS, MOON_APOAPSIS, MOON_MASS, MOON_PERIAPSIS, MOON_RADIUS};
use crate::orbit::orbit::{calculate_point_on_orbit, calculate_world_position_of_orbit};
use crate::{manager::ClientHandlerMessage, SCALE};



@@ 26,9 24,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);


@@ 51,7 47,7 @@ impl Planets {
        self.planets.get_mut(planet_id)
    }

    pub async fn make_planet(
    pub fn make_planet(
        _planet_id: &str,
        planet_type: PlanetType,
        mass: f64,


@@ 82,13 78,13 @@ impl Planets {
        )
    }

    pub async fn create_planets(
    pub fn create_planets(
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
        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,


@@ 96,13 92,13 @@ impl Planets {
            (100.0, 100.0),
            rigid_body_set,
            collider_set,
        )
        .await;
        );
        entities.insert(earth_id, entity);
        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 107,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,


@@ 119,10 115,33 @@ impl Planets {
            (moon_start_point[0], moon_start_point[1]),
            rigid_body_set,
            collider_set,
        )
        .await;
        );
        entities.insert(moon_id, moon);
        planet_ids.push(moon_id);

        let mars_start_point;
        #[allow(clippy::expect_used)]
        if let Entity::Planet(moon) = entities.get(&moon_id).expect("moon does not exist") {
            mars_start_point = calculate_world_position_of_orbit(
                calculate_point_on_orbit(MARS_PERIHELION, MARS_APHELION, 0.0),
                vector![moon.position.0, moon.position.1],
            );
        } else {
            mars_start_point = vector![0., 0.];
        }

        let (mars_id, mars) = Self::make_planet(
            "mars",
            PlanetType::Mars,
            MARS_MASS,
            MARS_RADIUS,
            (mars_start_point[0], mars_start_point[1]),
            rigid_body_set,
            collider_set,
        );
        entities.insert(mars_id, mars);
        planet_ids.push(mars_id);

        planet_ids
    }



@@ 136,7 155,7 @@ impl Planets {
                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(),
            });
        }


A server/src/tcp_handler.rs => server/src/tcp_handler.rs +176 -0
@@ 0,0 1,176 @@
use std::error::Error;
use std::net::SocketAddr;
use std::sync::Arc;
use async_std::io::WriteExt;
use async_std::net::TcpStream;
use async_std::sync::RwLock;
use log::{error, info, warn};
use starkingdoms_protocol::PROTOCOL_VERSION;
use crate::entity::{Entity, EntityHandler};
use crate::manager::{ClientHandler, ClientManager, PhysicsData};
use crate::{CMGR, ServerPingResponse, ServerPingResponseVersion};
use crate::handler::handle_client;
use futures::StreamExt;

pub async fn handle_request(
    conn: TcpStream,
    remote_addr: SocketAddr,
    mgr: ClientManager,
    entities: Arc<RwLock<EntityHandler>>,
    physics_data: Arc<RwLock<PhysicsData>>,
) {
    match _handle_request(conn, remote_addr, mgr, entities, physics_data).await {
        Ok(_) => (),
        Err(e) => {
            error!("[{}] error in handler thread: {}", remote_addr, e);
        }
    }
}

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

    loop {
        let read = conn.peek(&mut peek_buf).await?;
        if read == 9 {
            break;
        }
    }

    if peek_buf == *b"GET /ping" {
        info!("[{}] incoming http connection", remote_addr);
        let ping_resp = serde_json::to_string(&ServerPingResponse {
            version: ServerPingResponseVersion {
                name: env!("STK_VERSION_NAME").to_string(), // Set by build.rs
                number: env!("STK_VERSION").to_string(),    // Set by build.rs
                protocol: PROTOCOL_VERSION,
                channel: env!("STK_CHANNEL").to_string(),
                build: env!("STK_BUILD").to_string(),
            },
            players: u32::try_from(CMGR.usernames.read().await.len())?,
            description: env!("STK_SLP_DESCRIPTION").to_string(),
        })?;

        let resp_str = format!(
            "HTTP/1.0 200 OK\nAccess-Control-Allow-Origin: *\nContent-Length: {}\n\n{}",
            ping_resp.len(),
            ping_resp
        );
        let http_resp = resp_str.as_bytes();

        conn.write_all(http_resp).await?;
        info!(
            "[{}] sent ping response (200 OK {} bytes)",
            remote_addr,
            ping_resp.len()
        );
        return Ok(());
    }
    info!("[{}] incoming websocket connection", remote_addr);

    // if its not GET /ping, assume its websocket and attempt to handshake with them
    let ws_stream = async_tungstenite::accept_async(conn).await?;

    let (ws_write, ws_read) = ws_stream.split();

    let (tx, rx) = async_std::channel::unbounded();

    let client = ClientHandler { tx };

    // Acquire the write lock in a small scope, so it's dropped as quickly as possible
    {
        mgr.handlers.write().await.insert(remote_addr, client);
    }

    info!("[{}] passing to client handler", remote_addr);

    //forward the stream to the sink to achieve echo
    match handle_client(
        mgr.clone(),
        entities.clone(),
        physics_data.clone(),
        remote_addr,
        rx,
        ws_write,
        ws_read,
    )
        .await
    {
        Ok(_) => (),
        Err(e) if e.is::<async_tungstenite::tungstenite::error::Error>() => {
            #[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 {
                error!("[{}] error in client thread: {}", remote_addr, e);
            }
        }
        Err(e) => {
            error!("[{}] error in client thread: {}", remote_addr, e);
        }
    }

    // clean up values left over
    {
        mgr.handlers.write().await.remove(&remote_addr);
        mgr.usernames.write().await.remove(&remote_addr);
        // remove player physics body
        let mut entity_write = entities.write().await;
        let mut data = physics_data.write().await;
        let mut rigid_body_set = data.rigid_body_set.clone();
        let mut island_manager = data.island_manager.clone();
        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 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 Some(Entity::Player(player)) = entity_write.entities.get(&player_id) {
            rigid_body_set.remove(
                player.handle,
                &mut island_manager,
                &mut collider_set,
                &mut impulse_joint_set,
                &mut multibody_joint_set,
                true,
            );
            for module in player.search_modules(&entity_write) {
                rigid_body_set.remove(
                    module.handle,
                    &mut island_manager,
                    &mut collider_set,
                    &mut impulse_joint_set,
                    &mut multibody_joint_set,
                    true,
                );
                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;
        data.island_manager = island_manager;
        data.impulse_joint_set = impulse_joint_set;
        data.multibody_joint_set = multibody_joint_set;
        entity_write.entities.remove(&player_id);
    }

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

M server/src/timer.rs => server/src/timer.rs +224 -89
@@ 1,40 1,38 @@
use crate::entity::EntityHandler;
use crate::module::Module;
use crate::orbit::constants::{
    GAME_ORBITS_ENABLED, MOON_APOAPSIS, MOON_ORBIT_TIME, MOON_PERIAPSIS,
};
use crate::module::{Module, AttachedModule};
use crate::orbit::constants::{GAME_ORBITS_ENABLED, MARS_APHELION, MARS_ORBIT_TIME, MARS_PERIHELION, MOON_APOAPSIS, MOON_ORBIT_TIME, MOON_PERIAPSIS};
use crate::orbit::orbit::{calculate_point_on_orbit, calculate_world_position_of_orbit};
use crate::{
    entity::{get_entity_id, Entity},
    manager::{ClientHandlerMessage, ClientManager, PhysicsData},
    planet::{Planet, Planets},
    planet::Planets,
    SCALE,
};
use async_std::sync::RwLock;
use async_std::task::sleep;
use log::{info, warn, debug};
use log::warn;
use nalgebra::{point, vector};
use protobuf::SpecialFields;
use rand::Rng;
use rapier2d_f64::prelude::{
    ColliderBuilder, MassProperties, PhysicsPipeline, RigidBodyBuilder, RigidBodyHandle,
};
use rapier2d_f64::prelude::{ColliderBuilder, MassProperties, PhysicsPipeline, RigidBodyBuilder};
use starkingdoms_protocol::{module::ModuleType, planet::PlanetType, player::Player};
use std::error::Error;
use std::{f64::consts::PI, sync::Arc, time::Duration};

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;
    let mut module_timer = 0.0;
    let _planet_ids;

    {
        let mut data_handle = physics_data_orig.write().await;


@@ 42,17 40,18 @@ pub async fn timer_main(
        let mut rigid_body_set = data_handle.rigid_body_set.clone();
        let mut collider_set = data_handle.collider_set.clone();

        _planet_ids = Planets::create_planets(
        let _ = Planets::create_planets(
            &mut rigid_body_set,
            &mut collider_set,
            &mut entities.write().await.entities,
        )
        .await;
        );

        data_handle.rigid_body_set = rigid_body_set;
        data_handle.collider_set = collider_set;
    }

    #[allow(clippy::significant_drop_tightening)]
    // underdeveloped lint, TODO remove when its better
    loop {
        sleep(Duration::from_millis(5)).await;
        time += 5.0 / 1000.0; // 5ms, time is in seconds


@@ 65,27 64,59 @@ pub async fn timer_main(
        // IT MAY ALWAYS BE TRUE
        // THATS FINE
        if GAME_ORBITS_ENABLED {
            let planets = entities.write().await;
            let mut planets = entities.write().await;
            let mut map = planets.entities.clone();

            // update earth (nothing changes, yet)
            let earth = planets.get_planet(PlanetType::Earth).unwrap();
            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 new_moon_position = calculate_world_position_of_orbit(
                calculate_point_on_orbit(MOON_PERIAPSIS, MOON_APOAPSIS, time / MOON_ORBIT_TIME),
                new_earth_position,
            );
            let moon_body = physics_data
                .rigid_body_set
                .get_mut(moon.body_handle)
                .unwrap();
            moon_body.set_next_kinematic_position(new_moon_position.into());
            moon.position = (
                moon_body.translation()[0] / SCALE,
                moon_body.translation()[1] / SCALE,
            );
            let earth_id = planets.get_planet_id(PlanetType::Earth).ok_or("earth does not exist")?;
            if let Entity::Planet(earth) = map
                .get(&earth_id)
                .ok_or("earth does not exist")? {
                let new_earth_position = vector![earth.position.0, earth.position.1];

                // update moon
                let moon_id = planets.get_planet_id(PlanetType::Moon).ok_or("moon does not exist")?;
                if let Entity::Planet(moon) = map
                    .get_mut(&moon_id)
                    .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,
                    );
                    let new_moon_position = new_moon_position / SCALE;
                    let moon_body = physics_data
                        .rigid_body_set
                        .get_mut(moon.body_handle)
                        .ok_or("moon does not exist")?;
                    moon_body.set_next_kinematic_translation(new_moon_position);
                    moon.position = (
                        moon_body.translation()[0],
                        moon_body.translation()[1],
                    );

                    // update mars
                    let mars_id = planets.get_planet_id(PlanetType::Mars).ok_or("mars does not exist")?;
                    if let Entity::Planet(mars) = map
                        .get_mut(&mars_id)
                        .ok_or("mars does not exist")? {
                        let new_mars_position = calculate_world_position_of_orbit(
                            calculate_point_on_orbit(MARS_PERIHELION, MARS_APHELION, time / MARS_ORBIT_TIME),
                            new_moon_position,
                        );
                        let new_mars_position = new_mars_position/SCALE;
                        let mars_body = physics_data
                            .rigid_body_set
                            .get_mut(mars.body_handle)
                            .ok_or("mars does not exist")?;
                        mars_body.set_next_kinematic_translation(new_mars_position);
                        mars.position = (
                            mars_body.translation()[0],
                            mars_body.translation()[1],
                        );
                    }
                }
            }
            planets.entities = map;
        }

        physics_data.tick(&mut pipeline);


@@ 102,7 133,7 @@ pub async fn timer_main(

                let module_collider = ColliderBuilder::cuboid(18.75 / SCALE, 23.4375 / SCALE)
                    .translation(vector![0.0, 1.5625 / SCALE])
                    .mass_properties(MassProperties::new(point![0.0, 0.0], 0.0001, 0.005))
                    .mass_properties(MassProperties::new(point![0.0, 0.0], 0.000075, 0.005))
                    .build();
                let angle: f64 = {
                    let mut rng = rand::thread_rng();


@@ 137,9 168,12 @@ 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 181,14 @@ 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 213,12 @@ 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(


@@ 183,17 226,45 @@ pub async fn timer_main(
                    module_body.mass(),
                );
                module_body.apply_impulse(vector![grav_force.0, grav_force.1], true);

                for child in module.children.clone() {
                    if let Some(child) = child {
                        let joint = physics_data.impulse_joint_set
                            .get(child.connection).ok_or("module joint does not exist")?;
                        if joint.impulses.magnitude() > 0.00012 {
                            let module: Option<AttachedModule>;
                            if let Some(Entity::AttachedModule(p_module)) =
                                entities.entities.get_mut(&child.child)
                            {
                                module = Some(p_module.clone());
                            } else {
                                warn!("attempted to detach nonexistent module");
                                continue;
                            }
                            let player_id = module.as_ref().ok_or("cannot detach module that doesn't exist")?.player_id;
                            AttachedModule::detach(
                                &mut physics_data,
                                &mut entities,
                                player_id,
                                &module.ok_or("cannot detach module that doesn't exist")?,
                            );
                        }
                    }
                }
            }
        }

        {
            for (player_id, player) in entities.read().await.get_players().iter() {
            let mut entities = entities.write().await;
            for (player_id, player) in &entities.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;
                let grav_force = planets.gravity(
                let grav_force = entities.gravity(
                    (player_body.translation().x, player_body.translation().y),
                    player_body.mass(),
                );


@@ 245,20 316,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,17 342,47 @@ 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),
                    y: (translation.y * SCALE),
                    username,
                    special_fields: Default::default(),
                    special_fields: SpecialFields::default(),
                });

                for child in player.children.clone() {
                    if let Some(child) = child {
                        let joint = physics_data.impulse_joint_set
                            .get(child.connection).ok_or("module joint does not exist")?;
                        // displays impulse on joint * 10000 so its visible, use to tune breaking
                        // force
                        //debug!("impulse: {}", joint.impulses.magnitude() * 10000.);
                        if joint.impulses.magnitude() > 0.00012 {
                            let module: Option<AttachedModule>;
                            if let Some(Entity::AttachedModule(p_module)) =
                                entities.entities.get_mut(&child.child)
                            {
                                module = Some(p_module.clone());
                            } else {
                                warn!("attempted to detach nonexistent module");
                                continue;
                            }
                            let player_id = module.as_ref().ok_or("cannot detach module that doesn't exist")?.player_id;
                            AttachedModule::detach(
                                &mut physics_data,
                                &mut entities,
                                player_id,
                                &module.ok_or("cannot detach module that doesn't exist")?,
                            );
                        }
                    }
                }
            }
        }



@@ 290,7 391,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 409,74 @@ 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));
                                    if m.player_id == id {
                                        #[allow(clippy::expect_used)]
                                        {
                                            attached_modules.push(
                                                m.to_protocol(
                                                    &entities,
                                                    &physics_data.rigid_body_set,
                                                )
                                                .expect("module does not exist"),
                                            );
                                        }
                                        false
                                    } else {
                                        true
                                    }
                                }
                                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");
                                }
                            }
                        })
                        .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();
                                //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: SpecialFields::default(),
                                }
                            })
                            .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(),


@@ 353,7 484,7 @@ pub async fn timer_main(
                                y: body.translation().y * SCALE,
                                id: *id,
                                flags: module.flags,
                                special_fields: Default::default(),
                                special_fields: SpecialFields::default(),
                            }
                        })
                        .collect();


@@ 371,9 502,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 +6 -1
@@ 27,7 27,12 @@ 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),
        ("mars.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;