~starkingdoms/starkingdoms

1e40dcdf4975c404a4d697c0d9c88fc7954f8908 — ghostlyzsh 2 years ago 84d27d1 + fe715d0
idk
108 files changed, 3465 insertions(+), 1309 deletions(-)

M Cargo.lock
M Cargo.toml
M Jenkinsfile
A ansible/infra
M api/src/config.rs
M api/src/error.rs
M api/src/main.rs
M api/src/routes/beamin.rs
M api/src/routes/beamout.rs
M api/src/routes/callback.rs
M api/src/routes/mod.rs
M api/src/routes/select_realm.rs
M api/starkingdoms_api_entities/src/lib.rs
M api/starkingdoms_api_migration/src/m20230417_162824_create_table_users.rs
M api/starkingdoms_api_migration/src/m20230417_164240_create_table_user_auth_realms.rs
M api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs
M assets/dist/spritesheet-125.json
M assets/dist/spritesheet-125.png
M assets/dist/spritesheet-375.json
M assets/dist/spritesheet-375.png
M assets/dist/spritesheet-full.json
M assets/dist/spritesheet-full.png
M assets/final/125/earth.png
M assets/final/125/moon.png
M assets/final/375/earth.png
M assets/final/375/moon.png
M assets/final/full/earth.png
M assets/final/full/moon.png
R assets/src/{autoplr_cfg.ink => autoplr_cfg}.svg
R assets/src/{autoplr_error.ink => autoplr_error}.svg
R assets/src/{autoplr_on.ink => autoplr_on}.svg
R assets/src/{cargo_off.ink => cargo_off}.svg
R assets/src/{cargo_on.ink => cargo_on}.svg
R assets/src/{earth.ink => earth}.svg
R assets/src/{ecothruster_on.ink => ecothruster_on}.svg
R assets/src/{hearty.ink => hearty}.svg
R assets/src/{hub_off.ink => hub_off}.svg
R assets/src/{hub_on.ink => hub_on}.svg
R assets/src/{landingleg.ink => landingleg}.svg
R assets/src/{landingthruster_off.ink => landingthruster_off}.svg
R assets/src/{landingthruster_on.ink => landingthruster_on}.svg
R assets/src/{moon.ink => moon}.svg
R assets/src/{powerhub_off.ink => powerhub_off}.svg
R assets/src/{powerhub_on.ink => powerhub_on}.svg
R assets/src/{starfield.ink => starfield}.svg
R assets/src/{superthruster_off.ink => superthruster_off}.svg
R assets/src/{superthruster_on.ink => superthruster_on}.svg
R assets/src/{thruster_off.ink => thruster_off}.svg
R assets/src/{thruster_on.ink => thruster_on}.svg
M client/package.json
M client/src/gateway.ts
M client/src/index.ts
M client/src/protocol/goodbye_reason.ts
A client/src/protocol/input.ts
M client/src/protocol/message_c2s.ts
M client/src/protocol/message_s2c.ts
M client/src/protocol/module.ts
M client/src/protocol/planet.ts
M client/src/protocol/player.ts
M client/src/protocol/starkingdoms-protocol.ts
M client/src/protocol/state.ts
M client/vite.config.ts
A docker/README.md
M protocol/build.rs
M protocol/src/api.rs
M protocol/src/legacy.rs
M protocol/src/lib.rs
M protocol/src/pbuf/goodbye_reason.proto
A protocol/src/pbuf/input.proto
M protocol/src/pbuf/message_c2s.proto
M protocol/src/pbuf/module.proto
M protocol/src/pbuf/planet.proto
M protocol/src/pbuf/player.proto
M protocol/src/pbuf/starkingdoms-protocol.proto
M protocol/src/pbuf/state.proto
M server/build.rs
M server/src/api.rs
M server/src/entity.rs
M server/src/handler.rs
M server/src/macros.rs
M server/src/main.rs
M server/src/manager.rs
M server/src/orbit/constants.rs
M server/src/orbit/kepler.rs
M server/src/orbit/mod.rs
M server/src/orbit/newtonian.rs
M server/src/orbit/orbit.rs
M server/src/orbit/vis_viva.rs
M server/src/planet.rs
M server/src/timer.rs
M spacetime
A spacetime_old
A spacetime_rs/Cargo.toml
A spacetime_rs/src/cmd.rs
A spacetime_rs/src/commands/api.rs
A spacetime_rs/src/commands/assets.rs
A spacetime_rs/src/commands/clean.rs
A spacetime_rs/src/commands/client.rs
A spacetime_rs/src/commands/docker.rs
A spacetime_rs/src/commands/mod.rs
A spacetime_rs/src/commands/server.rs
A spacetime_rs/src/config.rs
A spacetime_rs/src/configure/asset.rs
A spacetime_rs/src/configure/client.rs
A spacetime_rs/src/configure/mod.rs
A spacetime_rs/src/configure/rust.rs
A spacetime_rs/src/main.rs
A spacetime_rs/src/ninja.rs
M Cargo.lock => Cargo.lock +17 -0
@@ 3164,6 3164,14 @@ dependencies = [
]

[[package]]
name = "spacetime"
version = "0.1.0"
dependencies = [
 "tabwriter",
 "which",
]

[[package]]
name = "spade"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 3400,6 3408,15 @@ dependencies = [
]

[[package]]
name = "tabwriter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36205cfc997faadcc4b0b87aaef3fbedafe20d38d4959a7ca6ff803564051111"
dependencies = [
 "unicode-width",
]

[[package]]
name = "tempfile"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +2 -1
@@ 4,7 4,8 @@ members = [
    "protocol",
    "api",
    "api/starkingdoms_api_entities",
    "api/starkingdoms_api_migration"
    "api/starkingdoms_api_migration",
    "spacetime_rs"
]

[profile.dev.package.rapier2d-f64]

M Jenkinsfile => Jenkinsfile +2 -2
@@ 26,8 26,8 @@ pipeline {
        }
        stage('Deploy') {
            steps {
                sh 'sshpass -p ${INFRA_KEY} ./spacetime infra update-bleeding'
                sh 'sshpass -p ${INFRA_KEY} ./spacetime infra restart-bleeding'
                sh 'sshpass -p ${INFRA_KEY} ./ansible/infra update-bleeding'
                sh 'sshpass -p ${INFRA_KEY} ./ansible/infra restart-bleeding'
            }
        }
    }

A ansible/infra => ansible/infra +3 -0
@@ 0,0 1,3 @@
#!/bin/bash
echo "[*] Connecting to infrastructure manager server. If you are prompted for a password, enter your infrastructure key. You may be prompted several times."
ssh team@10.16.1.3 /home/team/run_ansible.sh "$1"
\ No newline at end of file

M api/src/config.rs => api/src/config.rs +22 -12
@@ 1,9 1,9 @@
use log::error;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use log::error;
use once_cell::sync::Lazy;
use serde::{Serialize, Deserialize};

pub static CONFIG: Lazy<StarkingdomsApiConfig> = Lazy::new(|| {
    let config_str = match fs::read_to_string("/etc/starkingdoms/config.toml") {


@@ 31,7 31,7 @@ pub struct StarkingdomsApiConfig {
    pub jwt_signing_secret: String,
    pub base: String,
    pub game: String,
    pub realms: HashMap<String, StarkingdomsApiConfigRealm>
    pub realms: HashMap<String, StarkingdomsApiConfigRealm>,
}

#[derive(Serialize, Deserialize, Debug)]


@@ 50,13 50,13 @@ pub struct StarkingdomsApiConfigDatabase {
    #[serde(default = "time_defaults")]
    pub max_lifetime: u64,
    #[serde(default = "sqlx_logging_default")]
    pub sqlx_logging: bool
    pub sqlx_logging: bool,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct StarkingdomsApiConfigServer {
    #[serde(default = "socketaddr_8080")]
    pub bind: SocketAddr
    pub bind: SocketAddr,
}

/*


@@ 69,11 69,21 @@ issuer = "https://api.e3t.cc"
pub struct StarkingdomsApiConfigRealm {
    pub authorize_url: String,
    pub public_key: String,
    pub issuer: String
    pub issuer: String,
}

fn max_connections_default() -> u32 { 100 }
fn min_connections_default() -> u32 { 5 }
fn time_defaults() -> u64 { 8 }
fn sqlx_logging_default() -> bool { true }
fn socketaddr_8080() -> SocketAddr { SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([0, 0, 0, 0]), 8080)) }
\ No newline at end of file
fn max_connections_default() -> u32 {
    100
}
fn min_connections_default() -> u32 {
    5
}
fn time_defaults() -> u64 {
    8
}
fn sqlx_logging_default() -> bool {
    true
}
fn socketaddr_8080() -> SocketAddr {
    SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([0, 0, 0, 0]), 8080))
}

M api/src/error.rs => api/src/error.rs +45 -57
@@ 1,9 1,9 @@
use actix_web::error::{JsonPayloadError, PayloadError};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct APIErrorsResponse {
    pub errors: Vec<APIError>
    pub errors: Vec<APIError>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct APIError {


@@ 11,10 11,12 @@ pub struct APIError {
    pub message: String,
    #[serde(skip_serializing_if = "is_none")]
    #[serde(default)]
    pub path: Option<String>
    pub path: Option<String>,
}

fn is_none<T>(o: &Option<T>) -> bool { o.is_none() }
fn is_none<T>(o: &Option<T>) -> bool {
    o.is_none()
}

impl From<&JsonPayloadError> for APIError {
    fn from(value: &JsonPayloadError) -> Self {


@@ 71,58 73,44 @@ impl From<&JsonPayloadError> for APIError {
impl From<&PayloadError> for APIError {
    fn from(value: &PayloadError) -> Self {
        match value {
            PayloadError::Incomplete(e) => {
                APIError {
                    code: "ERR_UNEXPECTED_EOF".to_string(),
                    message: match e {
                        None => "Payload reached EOF but was incomplete".to_string(),
                        Some(e) => format!("Payload reached EOF but was incomplete: {}", e)
                    },
                    path: None,
                }
            }
            PayloadError::EncodingCorrupted => {
                APIError {
                    code: "ERR_CORRUPTED_PAYLOAD".to_string(),
                    message: "Payload content encoding corrupted".to_string(),
                    path: None,
                }
            }
            PayloadError::Overflow => {
                APIError {
                    code: "ERR_PAYLOAD_OVERFLOW".to_string(),
                    message: "Payload reached size limit".to_string(),
                    path: None,
                }
            }
            PayloadError::UnknownLength => {
                APIError {
                    code: "ERR_PAYLOAD_UNKNOWN_LENGTH".to_string(),
                    message: "Unable to determine payload length".to_string(),
                    path: None,
                }
            }
            PayloadError::Http2Payload(e) => {
                APIError {
                    code: "ERR_HTTP2_ERROR".to_string(),
                    message: format!("HTTP/2 error: {}", e),
                    path: None,
                }
            }
            PayloadError::Io(e) => {
                APIError {
                    code: "ERR_IO_ERROR".to_string(),
                    message: format!("I/O error: {}", e),
                    path: None,
                }
            }
            _ => {
                APIError {
                    code: "ERR_UNKNOWN_ERROR".to_string(),
                    message: "An unknown error has occured".to_string(),
                    path: None,
                }
            }
            PayloadError::Incomplete(e) => APIError {
                code: "ERR_UNEXPECTED_EOF".to_string(),
                message: match e {
                    None => "Payload reached EOF but was incomplete".to_string(),
                    Some(e) => format!("Payload reached EOF but was incomplete: {}", e),
                },
                path: None,
            },
            PayloadError::EncodingCorrupted => APIError {
                code: "ERR_CORRUPTED_PAYLOAD".to_string(),
                message: "Payload content encoding corrupted".to_string(),
                path: None,
            },
            PayloadError::Overflow => APIError {
                code: "ERR_PAYLOAD_OVERFLOW".to_string(),
                message: "Payload reached size limit".to_string(),
                path: None,
            },
            PayloadError::UnknownLength => APIError {
                code: "ERR_PAYLOAD_UNKNOWN_LENGTH".to_string(),
                message: "Unable to determine payload length".to_string(),
                path: None,
            },
            PayloadError::Http2Payload(e) => APIError {
                code: "ERR_HTTP2_ERROR".to_string(),
                message: format!("HTTP/2 error: {}", e),
                path: None,
            },
            PayloadError::Io(e) => APIError {
                code: "ERR_IO_ERROR".to_string(),
                message: format!("I/O error: {}", e),
                path: None,
            },
            _ => APIError {
                code: "ERR_UNKNOWN_ERROR".to_string(),
                message: "An unknown error has occured".to_string(),
                path: None,
            },
        }
    }
}
\ No newline at end of file
}

M api/src/main.rs => api/src/main.rs +21 -17
@@ 1,23 1,23 @@
use std::error::Error;
use std::time::Duration;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use actix_request_identifier::RequestIdentifier;
use actix_web::{App, HttpResponse, HttpServer};
use actix_web::http::header::HeaderValue;
use actix_web::web::{Data, JsonConfig};
use actix_web::{App, HttpResponse, HttpServer};
use log::{info, Level};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use tera::Tera;
use starkingdoms_api_migration::{Migrator, MigratorTrait};
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use std::error::Error;
use std::time::Duration;
use tera::Tera;

pub mod config;
pub mod routes;
pub mod error;
pub mod routes;

pub struct AppState {
    pub conn: DatabaseConnection,
    pub templates: Tera
    pub templates: Tera,
}

#[actix_web::main]


@@ 46,7 46,7 @@ async fn main() -> Result<(), Box<dyn Error>> {

    let data = Data::new(AppState {
        conn: db,
        templates: tera
        templates: tera,
    });

    HttpServer::new(move || {


@@ 57,19 57,23 @@ async fn main() -> Result<(), Box<dyn Error>> {
                actix_web::error::InternalError::from_response(
                    err,
                    HttpResponse::BadRequest().json(APIErrorsResponse {
                        errors: vec![
                            api_error
                        ],
                    })
                ).into()
                        errors: vec![api_error],
                    }),
                )
                .into()
            }))
            .wrap(RequestIdentifier::with_generator(|| {
                HeaderValue::from_str(&ulid::Ulid::new().to_string()).unwrap()
            }))
            .wrap(RequestIdentifier::with_generator(|| HeaderValue::from_str(&ulid::Ulid::new().to_string()).unwrap()))
            .service(routes::select_realm::select_realm)
            .service(routes::callback::callback)
            .service(routes::beamin::beam_in)
            .service(routes::beamout::beam_out)
            .service(actix_files::Files::new("/static", "static"))
    }).bind(CONFIG.server.bind)?.run().await?;
    })
    .bind(CONFIG.server.bind)?
    .run()
    .await?;

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

M api/src/routes/beamin.rs => api/src/routes/beamin.rs +49 -54
@@ 1,6 1,8 @@
use std::collections::BTreeMap;
use actix_web::{HttpResponse, post};
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::AppState;
use actix_web::web::{Data, Json};
use actix_web::{post, HttpResponse};
use hmac::digest::KeyInit;
use hmac::Hmac;
use jwt::VerifyWithKey;


@@ 9,35 11,31 @@ use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use starkingdoms_protocol::api::APISavedPlayerData;
use crate::AppState;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use std::collections::BTreeMap;

#[derive(Serialize, Deserialize)]
pub struct BeaminRequest {
    pub api_token: String,
    pub user_auth_realm_id: String,
    pub user_auth_token: String
    pub user_auth_token: String,
}

#[derive(Serialize, Deserialize)]
pub struct BeaminResponse {
    pub save_id: String,
    pub save: APISavedPlayerData
    pub save: APISavedPlayerData,
}

#[post("/beamin")]
pub async fn beam_in(data: Json<BeaminRequest>, state: Data<AppState>) -> HttpResponse {
    if !CONFIG.internal_tokens.contains(&data.api_token) {
        return HttpResponse::Unauthorized().json(APIErrorsResponse {
            errors: vec![
                APIError {
                    code: "ERR_BAD_TOKEN".to_string(),
                    message: "Missing or invalid api token".to_string(),
                    path: None,
                }
            ],
        })
            errors: vec![APIError {
                code: "ERR_BAD_TOKEN".to_string(),
                message: "Missing or invalid api token".to_string(),
                path: None,
            }],
        });
    }

    let key: Hmac<Sha256> = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap();


@@ 46,67 44,64 @@ pub async fn beam_in(data: Json<BeaminRequest>, state: Data<AppState>) -> HttpRe
        Err(e) => {
            error!("verifying error: {}", e);
            return HttpResponse::Unauthorized().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_BAD_TOKEN".to_string(),
                        message: "Missing or invalid user token".to_string(),
                        path: None,
                    }
                ],
            })
                errors: vec![APIError {
                    code: "ERR_BAD_TOKEN".to_string(),
                    message: "Missing or invalid user token".to_string(),
                    path: None,
                }],
            });
        }
    };

    if !token.contains_key("user") || !token.contains_key("nonce") {
        return HttpResponse::Unauthorized().json(APIErrorsResponse {
            errors: vec![
                APIError {
                    code: "ERR_BAD_TOKEN".to_string(),
                    message: "Missing or invalid user token (missing scopes)".to_string(),
                    path: None,
                }
            ],
            errors: vec![APIError {
                code: "ERR_BAD_TOKEN".to_string(),
                message: "Missing or invalid user token (missing scopes)".to_string(),
                path: None,
            }],
        });
    }

    let user_id = token.get("user").unwrap();

    let user_savefile: Vec<starkingdoms_api_entities::entity::user_savefile::Model> = match starkingdoms_api_entities::entity::user_savefile::Entity::find().filter(
        starkingdoms_api_entities::entity::user_savefile::Column::User.eq(user_id))
        .order_by_desc(starkingdoms_api_entities::entity::user_savefile::Column::Timestamp).all(&state.conn).await {
        Ok(sf) => sf,
        Err(e) => {
            error!("database error: {}", e);
            return HttpResponse::InternalServerError().json(APIErrorsResponse {
                errors: vec![
                    APIError {
    let user_savefile: Vec<starkingdoms_api_entities::entity::user_savefile::Model> =
        match starkingdoms_api_entities::entity::user_savefile::Entity::find()
            .filter(starkingdoms_api_entities::entity::user_savefile::Column::User.eq(user_id))
            .order_by_desc(starkingdoms_api_entities::entity::user_savefile::Column::Timestamp)
            .all(&state.conn)
            .await
        {
            Ok(sf) => sf,
            Err(e) => {
                error!("database error: {}", e);
                return HttpResponse::InternalServerError().json(APIErrorsResponse {
                    errors: vec![APIError {
                        code: "ERR_DB_ERROR".to_string(),
                        message: "Unable to fetch user savefiles".to_string(),
                        path: None,
                    }
                ],
            });
        }
    };
                    }],
                });
            }
        };
    if user_savefile.is_empty() {
        return HttpResponse::NoContent().json(APIErrorsResponse {
            errors: vec![
                APIError {
                    code: "ERR_NO_SAVES".to_string(),
                    message: "This user has no savefiles".to_string(),
                    path: None,
                }
            ],
            errors: vec![APIError {
                code: "ERR_NO_SAVES".to_string(),
                message: "This user has no savefiles".to_string(),
                path: None,
            }],
        });
    }

    let save = &user_savefile[0];
    let save_id = &save.id;
    let save_data_str = &save.data;
    let save_data: APISavedPlayerData = toml::from_str(save_data_str).expect("database contained corrupted player save data");
    let save_data: APISavedPlayerData =
        toml::from_str(save_data_str).expect("database contained corrupted player save data");

    HttpResponse::Ok().json(BeaminResponse {
        save_id: save_id.clone(),
        save: save_data
        save: save_data,
    })
}
\ No newline at end of file
}

M api/src/routes/beamout.rs => api/src/routes/beamout.rs +35 -40
@@ 1,7 1,8 @@
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{HttpResponse, post};
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::AppState;
use actix_web::web::{Data, Json};
use actix_web::{post, HttpResponse};
use hmac::digest::KeyInit;
use hmac::Hmac;
use jwt::VerifyWithKey;


@@ 9,18 10,17 @@ use log::error;
use sea_orm::{ActiveModelTrait, IntoActiveModel};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use ulid::Ulid;
use starkingdoms_protocol::api::APISavedPlayerData;
use crate::AppState;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use ulid::Ulid;

#[derive(Serialize, Deserialize)]
pub struct BeamoutRequest {
    pub api_token: String,
    pub user_auth_realm_id: String,
    pub user_auth_token: String,
    pub data: APISavedPlayerData
    pub data: APISavedPlayerData,
}

#[derive(Serialize, Deserialize)]


@@ 30,14 30,12 @@ pub struct BeamoutResponse {}
pub async fn beam_out(data: Json<BeamoutRequest>, state: Data<AppState>) -> HttpResponse {
    if !CONFIG.internal_tokens.contains(&data.api_token) {
        return HttpResponse::Unauthorized().json(APIErrorsResponse {
            errors: vec![
                APIError {
                    code: "ERR_BAD_TOKEN".to_string(),
                    message: "Missing or invalid api token".to_string(),
                    path: None,
                }
            ],
        })
            errors: vec![APIError {
                code: "ERR_BAD_TOKEN".to_string(),
                message: "Missing or invalid api token".to_string(),
                path: None,
            }],
        });
    }

    let key: Hmac<Sha256> = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap();


@@ 46,26 44,22 @@ pub async fn beam_out(data: Json<BeamoutRequest>, state: Data<AppState>) -> Http
        Err(e) => {
            error!("verifying error: {}", e);
            return HttpResponse::Unauthorized().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_BAD_TOKEN".to_string(),
                        message: "Missing or invalid user token".to_string(),
                        path: None,
                    }
                ],
            })
                errors: vec![APIError {
                    code: "ERR_BAD_TOKEN".to_string(),
                    message: "Missing or invalid user token".to_string(),
                    path: None,
                }],
            });
        }
    };

    if !token.contains_key("user") || !token.contains_key("nonce") {
        return HttpResponse::Unauthorized().json(APIErrorsResponse {
            errors: vec![
                APIError {
                    code: "ERR_BAD_TOKEN".to_string(),
                    message: "Missing or invalid user token (missing scopes)".to_string(),
                    path: None,
                }
            ],
            errors: vec![APIError {
                code: "ERR_BAD_TOKEN".to_string(),
                message: "Missing or invalid user token (missing scopes)".to_string(),
                path: None,
            }],
        });
    }



@@ 74,7 68,10 @@ pub async fn beam_out(data: Json<BeamoutRequest>, state: Data<AppState>) -> Http
        id: format!("save-{}", Ulid::new().to_string()),
        user: token.get("user").unwrap().clone(),
        data: saved_data,
        timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64,
        timestamp: SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64,
    };
    let savefile_active_model = savefile_model.into_active_model();
    match savefile_active_model.insert(&state.conn).await {


@@ 82,16 79,14 @@ pub async fn beam_out(data: Json<BeamoutRequest>, state: Data<AppState>) -> Http
        Err(e) => {
            error!("database error: {}", e);
            return HttpResponse::InternalServerError().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_DB_ERROR".to_string(),
                        message: "database failure".to_string(),
                        path: None,
                    }
                ],
                errors: vec![APIError {
                    code: "ERR_DB_ERROR".to_string(),
                    message: "database failure".to_string(),
                    path: None,
                }],
            });
        }
    }

    HttpResponse::Ok().json(BeamoutResponse {})
}
\ No newline at end of file
}

M api/src/routes/callback.rs => api/src/routes/callback.rs +72 -63
@@ 1,7 1,8 @@
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{get, HttpResponse};
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::AppState;
use actix_web::web::{Data, Query};
use actix_web::{get, HttpResponse};
use hmac::digest::KeyInit;
use hmac::Hmac;
use jwt::{PKeyWithDigest, SignWithKey, VerifyWithKey};


@@ 11,16 12,15 @@ use openssl::pkey::PKey;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::Deserialize;
use sha2::Sha256;
use ulid::Ulid;
use starkingdoms_api_entities::entity;
use crate::AppState;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use ulid::Ulid;

#[derive(Deserialize)]
pub struct CallbackQueryParams {
    pub token: String,
    pub realm: String
    pub realm: String,
}

#[get("/callback")]


@@ 33,20 33,18 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 
        None => {
            error!("realm not found");
            return HttpResponse::Unauthorized().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_UNKNOWN_REALM".to_string(),
                        message: "Unknown realm".to_string(),
                        path: None,
                    }
                ],
            })
                errors: vec![APIError {
                    code: "ERR_UNKNOWN_REALM".to_string(),
                    message: "Unknown realm".to_string(),
                    path: None,
                }],
            });
        }
    };

    let rs256_pub_key = PKeyWithDigest {
        digest: MessageDigest::sha256(),
        key: PKey::public_key_from_pem(realm.public_key.as_bytes()).unwrap()
        key: PKey::public_key_from_pem(realm.public_key.as_bytes()).unwrap(),
    };

    let token: BTreeMap<String, String> = match query.token.verify_with_key(&rs256_pub_key) {


@@ 59,35 57,35 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 

    let realm = match token.get("realm").ok_or(generic_unauthorized()) {
        Ok(r) => r,
        Err(e) => return e
        Err(e) => return e,
    };
    let realm_local_id = match token.get("realm_native_id").ok_or(generic_unauthorized()) {
        Ok(r) => r,
        Err(e) => return e
        Err(e) => return e,
    };

    debug!("got authenticated realm native authorization: authenticated as {}:{}", realm, realm_local_id);
    debug!(
        "got authenticated realm native authorization: authenticated as {}:{}",
        realm, realm_local_id
    );

    // see if a user on this realm already exists
    let maybe_user = match entity::user_auth_realm::Entity::find()
        .filter(
            entity::user_auth_realm::Column::RealmNativeId.eq(realm_local_id.clone())
        )
        .filter(
            entity::user_auth_realm::Column::Realm.eq(realm.clone())
        ).one(&state.conn).await {
        .filter(entity::user_auth_realm::Column::RealmNativeId.eq(realm_local_id.clone()))
        .filter(entity::user_auth_realm::Column::Realm.eq(realm.clone()))
        .one(&state.conn)
        .await
    {
        Ok(r) => r,
        Err(e) => {
            error!("database error: {}", e);
            return HttpResponse::InternalServerError().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_DB_ERROR".to_string(),
                        message: "Database error".to_string(),
                        path: None,
                    }
                ],
            })
                errors: vec![APIError {
                    code: "ERR_DB_ERROR".to_string(),
                    message: "Database error".to_string(),
                    path: None,
                }],
            });
        }
    };



@@ 98,17 96,27 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 
        claims.insert("nonce", Ulid::new().to_string());
        let token_str = claims.sign_with_key(&key).unwrap();
        let auth_url = format!("{}/?token={}&user={}", CONFIG.game, token_str, user.id);
        return HttpResponse::Found().append_header(("Location", auth_url)).finish();
        return HttpResponse::Found()
            .append_header(("Location", auth_url))
            .finish();
    }

    // create the user
    let new_user = entity::user::Model {
        id: format!("user-{}", Ulid::new().to_string()),
        username: Ulid::new().to_string(),
        created_on: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64,
        created_on: SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64,
    };
    let new_user_realm = entity::user_auth_realm::Model {
        id: format!("{}-{}:{}", new_user.id.clone(), realm.clone(), realm_local_id.clone()),
        id: format!(
            "{}-{}:{}",
            new_user.id.clone(),
            realm.clone(),
            realm_local_id.clone()
        ),
        realm: realm.clone(),
        realm_native_id: realm_local_id.clone(),
        user: new_user.id.clone(),


@@ 119,7 127,12 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 
    claims.insert("user", new_user.id.clone());
    claims.insert("nonce", Ulid::new().to_string());
    let token_str = claims.sign_with_key(&key).unwrap();
    let auth_url = format!("{}/?token={}&user={}", CONFIG.game, token_str, new_user.id.clone());
    let auth_url = format!(
        "{}/?token={}&user={}",
        CONFIG.game,
        token_str,
        new_user.id.clone()
    );

    let new_user_active_model = new_user.into_active_model();
    let new_user_realm_active_model = new_user_realm.into_active_model();


@@ 129,14 142,12 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 
        Err(e) => {
            error!("database error: {}", e);
            return HttpResponse::InternalServerError().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_DB_ERROR".to_string(),
                        message: "Database error".to_string(),
                        path: None,
                    }
                ],
            })
                errors: vec![APIError {
                    code: "ERR_DB_ERROR".to_string(),
                    message: "Database error".to_string(),
                    path: None,
                }],
            });
        }
    }



@@ 145,28 156,26 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 
        Err(e) => {
            error!("database error: {}", e);
            return HttpResponse::InternalServerError().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_DB_ERROR".to_string(),
                        message: "Database error".to_string(),
                        path: None,
                    }
                ],
            })
                errors: vec![APIError {
                    code: "ERR_DB_ERROR".to_string(),
                    message: "Database error".to_string(),
                    path: None,
                }],
            });
        }
    }

    HttpResponse::Found().append_header(("Location", auth_url)).finish()
    HttpResponse::Found()
        .append_header(("Location", auth_url))
        .finish()
}

fn generic_unauthorized() -> HttpResponse {
    HttpResponse::Unauthorized().json(APIErrorsResponse {
        errors: vec![
            APIError {
                code: "ERR_INVALID_STATE".to_string(),
                message: "Unknown/invalid login state".to_string(),
                path: None,
            }
        ],
        errors: vec![APIError {
            code: "ERR_INVALID_STATE".to_string(),
            message: "Unknown/invalid login state".to_string(),
            path: None,
        }],
    })
}
\ No newline at end of file
}

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

M api/src/routes/select_realm.rs => api/src/routes/select_realm.rs +21 -23
@@ 1,17 1,17 @@
use std::collections::HashMap;
use crate::config::{StarkingdomsApiConfigRealm, CONFIG};
use crate::error::{APIError, APIErrorsResponse};
use crate::AppState;
use actix_web::web::Data;
use actix_web::{get, HttpResponse};
use actix_web::web::{Data};
use log::error;
use serde::{Serialize};
use serde::Serialize;
use std::collections::HashMap;
use tera::Context;
use crate::AppState;
use crate::config::{CONFIG, StarkingdomsApiConfigRealm};
use crate::error::{APIError, APIErrorsResponse};

#[derive(Serialize)]
pub struct RealmsListTemplateContext {
    pub realms: HashMap<String, StarkingdomsApiConfigRealm>,
    pub back_to: String
    pub back_to: String,
}

#[get("/select-realm")]


@@ 24,14 24,13 @@ pub async fn select_realm(state: Data<AppState>) -> HttpResponse {
        Err(e) => {
            error!("[context] error creating render context: {}", e);
            return HttpResponse::InternalServerError().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_INTERNAL_SERVER_ERROR".to_string(),
                        message: "There was an error processing your request. Please try again later.".to_string(),
                        path: None,
                    }
                ],
            })
                errors: vec![APIError {
                    code: "ERR_INTERNAL_SERVER_ERROR".to_string(),
                    message: "There was an error processing your request. Please try again later."
                        .to_string(),
                    path: None,
                }],
            });
        }
    };
    match state.templates.render("select_realm.tera", &context) {


@@ 39,14 38,13 @@ pub async fn select_realm(state: Data<AppState>) -> HttpResponse {
        Err(e) => {
            error!("[context] error creating render context: {}", e);
            HttpResponse::InternalServerError().json(APIErrorsResponse {
                errors: vec![
                    APIError {
                        code: "ERR_INTERNAL_SERVER_ERROR".to_string(),
                        message: "There was an error processing your request. Please try again later.".to_string(),
                        path: None,
                    }
                ],
                errors: vec![APIError {
                    code: "ERR_INTERNAL_SERVER_ERROR".to_string(),
                    message: "There was an error processing your request. Please try again later."
                        .to_string(),
                    path: None,
                }],
            })
        }
    }
}
\ No newline at end of file
}

M api/starkingdoms_api_entities/src/lib.rs => api/starkingdoms_api_entities/src/lib.rs +1 -1
@@ 1,1 1,1 @@
pub mod entity;
\ No newline at end of file
pub mod entity;

M api/starkingdoms_api_migration/src/m20230417_162824_create_table_users.rs => api/starkingdoms_api_migration/src/m20230417_162824_create_table_users.rs +16 -9
@@ 6,14 6,21 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager.create_table(
            Table::create()
                .table(User::Table)
                .col(ColumnDef::new(User::Id).string().primary_key().not_null())
                .col(ColumnDef::new(User::Username).string().unique_key().not_null())
                .col(ColumnDef::new(User::CreatedOn).big_unsigned().not_null())
                .to_owned()
        ).await
        manager
            .create_table(
                Table::create()
                    .table(User::Table)
                    .col(ColumnDef::new(User::Id).string().primary_key().not_null())
                    .col(
                        ColumnDef::new(User::Username)
                            .string()
                            .unique_key()
                            .not_null(),
                    )
                    .col(ColumnDef::new(User::CreatedOn).big_unsigned().not_null())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {


@@ 29,5 36,5 @@ pub enum User {
    Table,
    Id,
    Username,
    CreatedOn
    CreatedOn,
}

M api/starkingdoms_api_migration/src/m20230417_164240_create_table_user_auth_realms.rs => api/starkingdoms_api_migration/src/m20230417_164240_create_table_user_auth_realms.rs +27 -15
@@ 1,5 1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230417_162824_create_table_users::User;
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;


@@ 7,19 7,31 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager.create_table(
            Table::create()
                .table(UserAuthRealm::Table)
                .col(ColumnDef::new(UserAuthRealm::Id).string().not_null().primary_key())
                .col(ColumnDef::new(UserAuthRealm::Realm).string().not_null())
                .col(ColumnDef::new(UserAuthRealm::RealmNativeId).string().not_null())
                .col(ColumnDef::new(UserAuthRealm::User).string().not_null())
                .foreign_key(
                    ForeignKey::create()
                        .from(UserAuthRealm::Table, UserAuthRealm::User)
                        .to(User::Table, User::Id)
                ).to_owned()
        ).await
        manager
            .create_table(
                Table::create()
                    .table(UserAuthRealm::Table)
                    .col(
                        ColumnDef::new(UserAuthRealm::Id)
                            .string()
                            .not_null()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(UserAuthRealm::Realm).string().not_null())
                    .col(
                        ColumnDef::new(UserAuthRealm::RealmNativeId)
                            .string()
                            .not_null(),
                    )
                    .col(ColumnDef::new(UserAuthRealm::User).string().not_null())
                    .foreign_key(
                        ForeignKey::create()
                            .from(UserAuthRealm::Table, UserAuthRealm::User)
                            .to(User::Table, User::Id),
                    )
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {


@@ 36,5 48,5 @@ pub enum UserAuthRealm {
    Id,
    Realm,
    RealmNativeId,
    User
    User,
}

M api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs => api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs +28 -16
@@ 1,5 1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230417_164240_create_table_user_auth_realms::UserAuthRealm;
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;


@@ 7,20 7,32 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager.create_table(
            Table::create()
                .table(UserSavefile::Table)
                .col(ColumnDef::new(UserSavefile::Id).string().not_null().primary_key())
                .col(ColumnDef::new(UserSavefile::User).string().not_null())
                .col(ColumnDef::new(UserSavefile::Data).string().not_null())
                .col(ColumnDef::new(UserSavefile::Timestamp).big_unsigned().not_null().unique_key())
                .foreign_key(
                    ForeignKey::create()
                        .from(UserSavefile::Table, UserSavefile::User)
                        .to(UserAuthRealm::Table, UserAuthRealm::Id)
                )
                .to_owned()
        ).await
        manager
            .create_table(
                Table::create()
                    .table(UserSavefile::Table)
                    .col(
                        ColumnDef::new(UserSavefile::Id)
                            .string()
                            .not_null()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(UserSavefile::User).string().not_null())
                    .col(ColumnDef::new(UserSavefile::Data).string().not_null())
                    .col(
                        ColumnDef::new(UserSavefile::Timestamp)
                            .big_unsigned()
                            .not_null()
                            .unique_key(),
                    )
                    .foreign_key(
                        ForeignKey::create()
                            .from(UserSavefile::Table, UserSavefile::User)
                            .to(UserAuthRealm::Table, UserAuthRealm::Id),
                    )
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {


@@ 37,5 49,5 @@ pub enum UserSavefile {
    Id,
    User,
    Data,
    Timestamp
    Timestamp,
}

M assets/dist/spritesheet-125.json => assets/dist/spritesheet-125.json +34 -34
@@ 1,25 1,25 @@
{
  "frames": {
    "moon.png": {
      "frame": { "x": 0, "y": 0, "w": 256, "h": 256 },
    "starfield.png": {
      "frame": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "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 }
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "earth.png": {
      "frame": { "x": 0, "y": 256, "w": 256, "h": 256 },
    "moon.png": {
      "frame": { "x": 0, "y": 64, "w": 64, "h": 64 },
      "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 }
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "starfield.png": {
      "frame": { "x": 0, "y": 512, "w": 64, "h": 64 },
    "earth.png": {
      "frame": { "x": 0, "y": 128, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 27,8 27,8 @@
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 576, "w": 64, "h": 64 },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 192, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 36,8 36,8 @@
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 640, "w": 64, "h": 64 },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 256, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


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


@@ 55,7 55,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "superthruster_on.png": {
      "frame": { "x": 0, "y": 768, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 384, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 64,7 64,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 0, "y": 832, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 448, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 73,7 73,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 0, "y": 896, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 512, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 82,7 82,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "thruster_on.png": {
      "frame": { "x": 0, "y": 960, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 576, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 91,7 91,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingleg.png": {
      "frame": { "x": 0, "y": 1024, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 640, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 100,7 100,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_on.png": {
      "frame": { "x": 0, "y": 1088, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 704, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 109,7 109,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hub_on.png": {
      "frame": { "x": 0, "y": 1152, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 768, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 118,7 118,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "powerhub_on.png": {
      "frame": { "x": 0, "y": 1216, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 832, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 127,7 127,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "superthruster_off.png": {
      "frame": { "x": 0, "y": 1280, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 896, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 136,7 136,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 0, "y": 1344, "w": 64, "h": 64 },
      "frame": { "x": 0, "y": 960, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },


@@ 145,7 145,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "thruster_off.png": {
      "frame": { "x": 0, "y": 1408, "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 },


@@ 154,7 154,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "cargo_on.png": {
      "frame": { "x": 0, "y": 1472, "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 },


@@ 163,7 163,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "cargo_off.png": {
      "frame": { "x": 0, "y": 1536, "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 },


@@ 172,7 172,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "powerhub_off.png": {
      "frame": { "x": 0, "y": 1600, "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 },


@@ 181,7 181,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hub_off.png": {
      "frame": { "x": 0, "y": 1664, "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 },

M assets/dist/spritesheet-125.png => assets/dist/spritesheet-125.png +0 -0
M assets/dist/spritesheet-375.json => assets/dist/spritesheet-375.json +34 -34
@@ 1,25 1,25 @@
{
  "frames": {
    "moon.png": {
      "frame": { "x": 0, "y": 0, "w": 768, "h": 768 },
    "starfield.png": {
      "frame": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "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 }
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "earth.png": {
      "frame": { "x": 0, "y": 768, "w": 768, "h": 768 },
    "moon.png": {
      "frame": { "x": 0, "y": 192, "w": 192, "h": 192 },
      "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 }
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "starfield.png": {
      "frame": { "x": 0, "y": 1536, "w": 192, "h": 192 },
    "earth.png": {
      "frame": { "x": 0, "y": 384, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 27,8 27,8 @@
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 1728, "w": 192, "h": 192 },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 576, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 36,8 36,8 @@
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 1920, "w": 192, "h": 192 },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 768, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


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


@@ 55,7 55,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "superthruster_on.png": {
      "frame": { "x": 0, "y": 2304, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 1152, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 64,7 64,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 0, "y": 2496, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 1344, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 73,7 73,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 0, "y": 2688, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 1536, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 82,7 82,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "thruster_on.png": {
      "frame": { "x": 0, "y": 2880, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 1728, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 91,7 91,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingleg.png": {
      "frame": { "x": 0, "y": 3072, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 1920, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 100,7 100,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_on.png": {
      "frame": { "x": 0, "y": 3264, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 2112, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 109,7 109,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hub_on.png": {
      "frame": { "x": 0, "y": 3456, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 2304, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 118,7 118,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "powerhub_on.png": {
      "frame": { "x": 0, "y": 3648, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 2496, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 127,7 127,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 0, "y": 3840, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 2688, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 136,7 136,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "superthruster_off.png": {
      "frame": { "x": 192, "y": 1536, "w": 192, "h": 192 },
      "frame": { "x": 0, "y": 2880, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },


@@ 145,7 145,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "thruster_off.png": {
      "frame": { "x": 192, "y": 1728, "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 },


@@ 154,7 154,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "cargo_on.png": {
      "frame": { "x": 192, "y": 1920, "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 },


@@ 163,7 163,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "cargo_off.png": {
      "frame": { "x": 192, "y": 2112, "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 },


@@ 172,7 172,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "powerhub_off.png": {
      "frame": { "x": 192, "y": 2304, "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 },


@@ 181,7 181,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hub_off.png": {
      "frame": { "x": 192, "y": 2496, "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 },

M assets/dist/spritesheet-375.png => assets/dist/spritesheet-375.png +0 -0
M assets/dist/spritesheet-full.json => assets/dist/spritesheet-full.json +34 -34
@@ 1,25 1,25 @@
{
  "frames": {
    "moon.png": {
      "frame": { "x": 0, "y": 0, "w": 2048, "h": 2048 },
    "starfield.png": {
      "frame": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "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 }
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "earth.png": {
      "frame": { "x": 0, "y": 2048, "w": 2048, "h": 2048 },
    "moon.png": {
      "frame": { "x": 0, "y": 512, "w": 512, "h": 512 },
      "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 }
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "starfield.png": {
      "frame": { "x": 2048, "y": 0, "w": 512, "h": 512 },
    "earth.png": {
      "frame": { "x": 0, "y": 1024, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 27,8 27,8 @@
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 2560, "y": 0, "w": 512, "h": 512 },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 1536, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 36,8 36,8 @@
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "autoplr_error.png": {
      "frame": { "x": 3072, "y": 0, "w": 512, "h": 512 },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 2048, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 46,7 46,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "hearty.png": {
      "frame": { "x": 3584, "y": 0, "w": 512, "h": 512 },
      "frame": { "x": 0, "y": 2560, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 55,7 55,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "superthruster_on.png": {
      "frame": { "x": 2048, "y": 512, "w": 512, "h": 512 },
      "frame": { "x": 0, "y": 3072, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 64,7 64,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 2560, "y": 512, "w": 512, "h": 512 },
      "frame": { "x": 0, "y": 3584, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 73,7 73,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 3072, "y": 512, "w": 512, "h": 512 },
      "frame": { "x": 512, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 82,7 82,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "thruster_on.png": {
      "frame": { "x": 3584, "y": 512, "w": 512, "h": 512 },
      "frame": { "x": 1024, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 91,7 91,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "landingleg.png": {
      "frame": { "x": 2048, "y": 1024, "w": 512, "h": 512 },
      "frame": { "x": 1536, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


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


@@ 109,7 109,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "hub_on.png": {
      "frame": { "x": 3072, "y": 1024, "w": 512, "h": 512 },
      "frame": { "x": 2560, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 118,7 118,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "powerhub_on.png": {
      "frame": { "x": 3584, "y": 1024, "w": 512, "h": 512 },
      "frame": { "x": 3072, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 127,7 127,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "superthruster_off.png": {
      "frame": { "x": 2048, "y": 1536, "w": 512, "h": 512 },
      "frame": { "x": 3584, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 136,7 136,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 2560, "y": 1536, "w": 512, "h": 512 },
      "frame": { "x": 512, "y": 512, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 145,7 145,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "thruster_off.png": {
      "frame": { "x": 3072, "y": 1536, "w": 512, "h": 512 },
      "frame": { "x": 512, "y": 1024, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 154,7 154,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "cargo_on.png": {
      "frame": { "x": 3584, "y": 1536, "w": 512, "h": 512 },
      "frame": { "x": 512, "y": 1536, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 163,7 163,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "cargo_off.png": {
      "frame": { "x": 2048, "y": 2048, "w": 512, "h": 512 },
      "frame": { "x": 512, "y": 2048, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 172,7 172,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "powerhub_off.png": {
      "frame": { "x": 2048, "y": 2560, "w": 512, "h": 512 },
      "frame": { "x": 512, "y": 2560, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },


@@ 181,7 181,7 @@
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "hub_off.png": {
      "frame": { "x": 2048, "y": 3072, "w": 512, "h": 512 },
      "frame": { "x": 512, "y": 3072, "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
M assets/final/125/earth.png => assets/final/125/earth.png +0 -0
M assets/final/125/moon.png => assets/final/125/moon.png +0 -0
M assets/final/375/earth.png => assets/final/375/earth.png +0 -0
M assets/final/375/moon.png => assets/final/375/moon.png +0 -0
M assets/final/full/earth.png => assets/final/full/earth.png +0 -0
M assets/final/full/moon.png => assets/final/full/moon.png +0 -0
R assets/src/autoplr_cfg.ink.svg => assets/src/autoplr_cfg.svg +0 -0
R assets/src/autoplr_error.ink.svg => assets/src/autoplr_error.svg +0 -0
R assets/src/autoplr_on.ink.svg => assets/src/autoplr_on.svg +0 -0
R assets/src/cargo_off.ink.svg => assets/src/cargo_off.svg +0 -0
R assets/src/cargo_on.ink.svg => assets/src/cargo_on.svg +0 -0
R assets/src/earth.ink.svg => assets/src/earth.svg +0 -0
R assets/src/ecothruster_on.ink.svg => assets/src/ecothruster_on.svg +0 -0
R assets/src/hearty.ink.svg => assets/src/hearty.svg +0 -0
R assets/src/hub_off.ink.svg => assets/src/hub_off.svg +0 -0
R assets/src/hub_on.ink.svg => assets/src/hub_on.svg +0 -0
R assets/src/landingleg.ink.svg => assets/src/landingleg.svg +0 -0
R assets/src/landingthruster_off.ink.svg => assets/src/landingthruster_off.svg +0 -0
R assets/src/landingthruster_on.ink.svg => assets/src/landingthruster_on.svg +0 -0
R assets/src/moon.ink.svg => assets/src/moon.svg +0 -0
R assets/src/powerhub_off.ink.svg => assets/src/powerhub_off.svg +0 -0
R assets/src/powerhub_on.ink.svg => assets/src/powerhub_on.svg +0 -0
R assets/src/starfield.ink.svg => assets/src/starfield.svg +0 -0
R assets/src/superthruster_off.ink.svg => assets/src/superthruster_off.svg +0 -0
R assets/src/superthruster_on.ink.svg => assets/src/superthruster_on.svg +0 -0
R assets/src/thruster_off.ink.svg => assets/src/thruster_off.svg +0 -0
R assets/src/thruster_on.ink.svg => assets/src/thruster_on.svg +0 -0
M client/package.json => client/package.json +1 -1
@@ 7,7 7,7 @@
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "protobuf": "mkdir -p ./src/protocol && protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/protocol -I ../protocol/src/pbuf ../protocol/src/pbuf/starkingdoms-protocol.proto"
    "protobuf": "mkdir -p ./src/protocol && protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/protocol -I ../protocol/src/pbuf ../protocol/src/pbuf/starkingdoms-protocol.proto && sed -i 's/\\/\\* eslint-disable \\*\\//\\/\\/@ts-nocheck/' src/protocol/*.ts"
  },
  "devDependencies": {
    "ts-proto": "^1.146.0",

M client/src/gateway.ts => client/src/gateway.ts +2 -2
@@ 74,7 74,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: 3,
            version: 4,
            requestedUsername: username,
            nextState: State.Play,
            user: window.localStorage.getItem("user")!,


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

M client/src/index.ts => client/src/index.ts +70 -1
@@ 7,9 7,10 @@ import {
    MessageC2SAuthenticateAndBeamOut,
    MessageC2SAuthenticateAndBeamOut_packetInfo,
    MessageC2SInput,
    MessageC2SInput_packetInfo
    MessageC2SInput_packetInfo, MessageC2SMouseInput, MessageC2SMouseInput_packetInfo
} from "./protocol/message_c2s";
import {encode} from "./serde";
import {InputType} from "./protocol/input";

logSetup();
const logger = new Logger("client");


@@ 110,6 111,71 @@ async function client_main(server: string, username: string, texture_quality: st
    }

    global.canvas.style.setProperty("background-image", `url("/assets/final/${texture_quality}/starfield.png")`);
    let canvas = document.getElementById("canvas")!;

    document.onmousedown = (e) => {
        let canvasLeft = canvas.offsetLeft + canvas.clientLeft;
        let canvasTop = canvas.offsetTop + canvas.clientTop;

        let screenspaceX = e.pageX - canvasLeft;
        let screenspaceY = e.pageY - canvasTop;

        // convert screenspace to worldspace
        if (global.me !== null) {
            let worldX = screenspaceX + global.me?.x;
            let worldY = screenspaceY + global.me?.y;

            let button: InputType;
            if (e.button == 0) {
                button = InputType.Left;
            } else if (e.button == 1) {
                button = InputType.Middle;
            } else if (e.button == 2) {
                button = InputType.Right;
            }

            let msg = MessageC2SMouseInput.encode({
                worldposX: worldX,
                worldposY: worldY,
                released: false,
                button: button!
            }).finish();

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

    document.onmouseup = (e) => {
        let canvasLeft = canvas.offsetLeft + canvas.clientLeft;
        let canvasTop = canvas.offsetTop + canvas.clientTop;

        let screenspaceX = e.pageX - canvasLeft;
        let screenspaceY = e.pageY - canvasTop;

        // convert screenspace to worldspace
        if (global.me !== null) {
            let worldX = screenspaceX + global.me?.x;
            let worldY = screenspaceY + global.me?.y;

            let button: InputType;
            if (e.button == 0) {
                button = InputType.Left;
            } else if (e.button == 1) {
                button = InputType.Middle;
            } else if (e.button == 2) {
                button = InputType.Right;
            }

            let msg = MessageC2SMouseInput.encode({
                worldposX: worldX,
                worldposY: worldY,
                released: true,
                button: button!
            }).finish();

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

    document.onkeydown = (e) => {
        if (e.code == "ArrowLeft" || e.code == "KeyA") { // arrow-left


@@ 213,6 279,9 @@ async function client_main(server: string, username: string, texture_quality: st

            global.context.save();

            // x_{screen} = x_{world} - player_{x_{world}}
            // x_{world} = x_{screen} + player_{x_{world}}

            global.context.translate(module.x - global.me!.x, module.y - global.me!.y);

            global.context.rotate(module.rotation);

M client/src/protocol/goodbye_reason.ts => client/src/protocol/goodbye_reason.ts +18 -12
@@ 1,35 1,39 @@
/* eslint-disable */
//@ts-nocheck

export const protobufPackage = "protocol.goodbye_reason";

export enum GoodbyeReason {
  UnsupportedProtocol = 0,
  UnexpectedPacket = 1,
  UnexpectedNextState = 2,
  UsernameTaken = 3,
  PingPongTimeout = 4,
  Done = 5,
  UNKNOWN = 0,
  UnsupportedProtocol = 1,
  UnexpectedPacket = 2,
  UnexpectedNextState = 3,
  UsernameTaken = 4,
  PingPongTimeout = 5,
  Done = 6,
  UNRECOGNIZED = -1,
}

export function goodbyeReasonFromJSON(object: any): GoodbyeReason {
  switch (object) {
    case 0:
    case "UNKNOWN":
      return GoodbyeReason.UNKNOWN;
    case 1:
    case "UnsupportedProtocol":
      return GoodbyeReason.UnsupportedProtocol;
    case 1:
    case 2:
    case "UnexpectedPacket":
      return GoodbyeReason.UnexpectedPacket;
    case 2:
    case 3:
    case "UnexpectedNextState":
      return GoodbyeReason.UnexpectedNextState;
    case 3:
    case 4:
    case "UsernameTaken":
      return GoodbyeReason.UsernameTaken;
    case 4:
    case 5:
    case "PingPongTimeout":
      return GoodbyeReason.PingPongTimeout;
    case 5:
    case 6:
    case "Done":
      return GoodbyeReason.Done;
    case -1:


@@ 41,6 45,8 @@ export function goodbyeReasonFromJSON(object: any): GoodbyeReason {

export function goodbyeReasonToJSON(object: GoodbyeReason): string {
  switch (object) {
    case GoodbyeReason.UNKNOWN:
      return "UNKNOWN";
    case GoodbyeReason.UnsupportedProtocol:
      return "UnsupportedProtocol";
    case GoodbyeReason.UnexpectedPacket:

A client/src/protocol/input.ts => client/src/protocol/input.ts +48 -0
@@ 0,0 1,48 @@
//@ts-nocheck

export const protobufPackage = "protocol.input";

export enum InputType {
  UNKNOWN = 0,
  Left = 1,
  Middle = 2,
  Right = 3,
  UNRECOGNIZED = -1,
}

export function inputTypeFromJSON(object: any): InputType {
  switch (object) {
    case 0:
    case "UNKNOWN":
      return InputType.UNKNOWN;
    case 1:
    case "Left":
      return InputType.Left;
    case 2:
    case "Middle":
      return InputType.Middle;
    case 3:
    case "Right":
      return InputType.Right;
    case -1:
    case "UNRECOGNIZED":
    default:
      return InputType.UNRECOGNIZED;
  }
}

export function inputTypeToJSON(object: InputType): string {
  switch (object) {
    case InputType.UNKNOWN:
      return "UNKNOWN";
    case InputType.Left:
      return "Left";
    case InputType.Middle:
      return "Middle";
    case InputType.Right:
      return "Right";
    case InputType.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

M client/src/protocol/message_c2s.ts => client/src/protocol/message_c2s.ts +141 -4
@@ 1,7 1,7 @@
// @ts-nocheck

//@ts-nocheck
import * as _m0 from "protobufjs/minimal";
import { GoodbyeReason, goodbyeReasonFromJSON, goodbyeReasonToJSON } from "./goodbye_reason";
import { InputType, inputTypeFromJSON, inputTypeToJSON } from "./input";
import { State, stateFromJSON, stateToJSON } from "./state";

export const protobufPackage = "protocol.message_c2s";


@@ 13,8 13,8 @@ export interface MessageC2SHello {
  requestedUsername: string;
  /** The state the connection will go into after the handshake. */
  nextState: State;
  token?: string | undefined;
  user?: string | undefined;
  token: string;
  user: string;
}

export enum MessageC2SHello_packetInfo {


@@ 246,6 246,46 @@ export function messageC2SAuthenticateAndBeamOut_packetInfoToJSON(
  }
}

export interface MessageC2SMouseInput {
  worldposX: number;
  worldposY: number;
  button: InputType;
  released: boolean;
}

export enum MessageC2SMouseInput_packetInfo {
  unknown = 0,
  type = 13,
  UNRECOGNIZED = -1,
}

export function messageC2SMouseInput_packetInfoFromJSON(object: any): MessageC2SMouseInput_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageC2SMouseInput_packetInfo.unknown;
    case 13:
    case "type":
      return MessageC2SMouseInput_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageC2SMouseInput_packetInfo.UNRECOGNIZED;
  }
}

export function messageC2SMouseInput_packetInfoToJSON(object: MessageC2SMouseInput_packetInfo): string {
  switch (object) {
    case MessageC2SMouseInput_packetInfo.unknown:
      return "unknown";
    case MessageC2SMouseInput_packetInfo.type:
      return "type";
    case MessageC2SMouseInput_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

function createBaseMessageC2SHello(): MessageC2SHello {
  return { version: 0, requestedUsername: "", nextState: 0, token: "", user: "" };
}


@@ 684,6 724,103 @@ export const MessageC2SAuthenticateAndBeamOut = {
  },
};

function createBaseMessageC2SMouseInput(): MessageC2SMouseInput {
  return { worldposX: 0, worldposY: 0, button: 0, released: false };
}

export const MessageC2SMouseInput = {
  encode(message: MessageC2SMouseInput, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.worldposX !== 0) {
      writer.uint32(9).double(message.worldposX);
    }
    if (message.worldposY !== 0) {
      writer.uint32(17).double(message.worldposY);
    }
    if (message.button !== 0) {
      writer.uint32(24).int32(message.button);
    }
    if (message.released === true) {
      writer.uint32(32).bool(message.released);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageC2SMouseInput {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageC2SMouseInput();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 9) {
            break;
          }

          message.worldposX = reader.double();
          continue;
        case 2:
          if (tag != 17) {
            break;
          }

          message.worldposY = reader.double();
          continue;
        case 3:
          if (tag != 24) {
            break;
          }

          message.button = reader.int32() as any;
          continue;
        case 4:
          if (tag != 32) {
            break;
          }

          message.released = reader.bool();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageC2SMouseInput {
    return {
      worldposX: isSet(object.worldposX) ? Number(object.worldposX) : 0,
      worldposY: isSet(object.worldposY) ? Number(object.worldposY) : 0,
      button: isSet(object.button) ? inputTypeFromJSON(object.button) : 0,
      released: isSet(object.released) ? Boolean(object.released) : false,
    };
  },

  toJSON(message: MessageC2SMouseInput): unknown {
    const obj: any = {};
    message.worldposX !== undefined && (obj.worldposX = message.worldposX);
    message.worldposY !== undefined && (obj.worldposY = message.worldposY);
    message.button !== undefined && (obj.button = inputTypeToJSON(message.button));
    message.released !== undefined && (obj.released = message.released);
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageC2SMouseInput>, I>>(base?: I): MessageC2SMouseInput {
    return MessageC2SMouseInput.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageC2SMouseInput>, I>>(object: I): MessageC2SMouseInput {
    const message = createBaseMessageC2SMouseInput();
    message.worldposX = object.worldposX ?? 0;
    message.worldposY = object.worldposY ?? 0;
    message.button = object.button ?? 0;
    message.released = object.released ?? false;
    return message;
  },
};

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T

M client/src/protocol/message_s2c.ts => client/src/protocol/message_s2c.ts +1 -1
@@ 1,4 1,4 @@
/* eslint-disable */
//@ts-nocheck
import * as _m0 from "protobufjs/minimal";
import { GoodbyeReason, goodbyeReasonFromJSON, goodbyeReasonToJSON } from "./goodbye_reason";
import { Module } from "./module";

M client/src/protocol/module.ts => client/src/protocol/module.ts +21 -15
@@ 1,24 1,28 @@
/* eslint-disable */
//@ts-nocheck
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "protocol.module";

export enum ModuleType {
  Cargo = 0,
  LandingThruster = 1,
  Hub = 2,
  UNKNOWN = 0,
  Cargo = 1,
  LandingThruster = 2,
  Hub = 3,
  UNRECOGNIZED = -1,
}

export function moduleTypeFromJSON(object: any): ModuleType {
  switch (object) {
    case 0:
    case "UNKNOWN":
      return ModuleType.UNKNOWN;
    case 1:
    case "Cargo":
      return ModuleType.Cargo;
    case 1:
    case 2:
    case "LandingThruster":
      return ModuleType.LandingThruster;
    case 2:
    case 3:
    case "Hub":
      return ModuleType.Hub;
    case -1:


@@ 30,6 34,8 @@ export function moduleTypeFromJSON(object: any): ModuleType {

export function moduleTypeToJSON(object: ModuleType): string {
  switch (object) {
    case ModuleType.UNKNOWN:
      return "UNKNOWN";
    case ModuleType.Cargo:
      return "Cargo";
    case ModuleType.LandingThruster:


@@ 59,13 65,13 @@ export const Module = {
      writer.uint32(8).int32(message.moduleType);
    }
    if (message.rotation !== 0) {
      writer.uint32(21).float(message.rotation);
      writer.uint32(17).double(message.rotation);
    }
    if (message.x !== 0) {
      writer.uint32(29).float(message.x);
      writer.uint32(25).double(message.x);
    }
    if (message.y !== 0) {
      writer.uint32(37).float(message.y);
      writer.uint32(33).double(message.y);
    }
    return writer;
  },


@@ 85,25 91,25 @@ export const Module = {
          message.moduleType = reader.int32() as any;
          continue;
        case 2:
          if (tag != 21) {
          if (tag != 17) {
            break;
          }

          message.rotation = reader.float();
          message.rotation = reader.double();
          continue;
        case 3:
          if (tag != 29) {
          if (tag != 25) {
            break;
          }

          message.x = reader.float();
          message.x = reader.double();
          continue;
        case 4:
          if (tag != 37) {
          if (tag != 33) {
            break;
          }

          message.y = reader.float();
          message.y = reader.double();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {

M client/src/protocol/planet.ts => client/src/protocol/planet.ts +19 -13
@@ 1,20 1,24 @@
/* eslint-disable */
//@ts-nocheck
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "protocol.planet";

export enum PlanetType {
  Earth = 0,
  Moon = 1,
  UNKNOWN = 0,
  Earth = 1,
  Moon = 2,
  UNRECOGNIZED = -1,
}

export function planetTypeFromJSON(object: any): PlanetType {
  switch (object) {
    case 0:
    case "UNKNOWN":
      return PlanetType.UNKNOWN;
    case 1:
    case "Earth":
      return PlanetType.Earth;
    case 1:
    case 2:
    case "Moon":
      return PlanetType.Moon;
    case -1:


@@ 26,6 30,8 @@ export function planetTypeFromJSON(object: any): PlanetType {

export function planetTypeToJSON(object: PlanetType): string {
  switch (object) {
    case PlanetType.UNKNOWN:
      return "UNKNOWN";
    case PlanetType.Earth:
      return "Earth";
    case PlanetType.Moon:


@@ 57,13 63,13 @@ export const Planet = {
      writer.uint32(8).int32(message.planetType);
    }
    if (message.x !== 0) {
      writer.uint32(21).float(message.x);
      writer.uint32(17).double(message.x);
    }
    if (message.y !== 0) {
      writer.uint32(29).float(message.y);
      writer.uint32(25).double(message.y);
    }
    if (message.radius !== 0) {
      writer.uint32(37).float(message.radius);
      writer.uint32(33).double(message.radius);
    }
    return writer;
  },


@@ 83,25 89,25 @@ export const Planet = {
          message.planetType = reader.int32() as any;
          continue;
        case 2:
          if (tag != 21) {
          if (tag != 17) {
            break;
          }

          message.x = reader.float();
          message.x = reader.double();
          continue;
        case 3:
          if (tag != 29) {
          if (tag != 25) {
            break;
          }

          message.y = reader.float();
          message.y = reader.double();
          continue;
        case 4:
          if (tag != 37) {
          if (tag != 33) {
            break;
          }

          message.radius = reader.float();
          message.radius = reader.double();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {

M client/src/protocol/player.ts => client/src/protocol/player.ts +10 -10
@@ 1,4 1,4 @@
/* eslint-disable */
//@ts-nocheck
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "protocol.player";


@@ 21,13 21,13 @@ function createBasePlayer(): Player {
export const Player = {
  encode(message: Player, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.rotation !== 0) {
      writer.uint32(13).float(message.rotation);
      writer.uint32(9).double(message.rotation);
    }
    if (message.x !== 0) {
      writer.uint32(21).float(message.x);
      writer.uint32(17).double(message.x);
    }
    if (message.y !== 0) {
      writer.uint32(29).float(message.y);
      writer.uint32(25).double(message.y);
    }
    if (message.username !== "") {
      writer.uint32(34).string(message.username);


@@ 43,25 43,25 @@ export const Player = {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 13) {
          if (tag != 9) {
            break;
          }

          message.rotation = reader.float();
          message.rotation = reader.double();
          continue;
        case 2:
          if (tag != 21) {
          if (tag != 17) {
            break;
          }

          message.x = reader.float();
          message.x = reader.double();
          continue;
        case 3:
          if (tag != 29) {
          if (tag != 25) {
            break;
          }

          message.y = reader.float();
          message.y = reader.double();
          continue;
        case 4:
          if (tag != 34) {

M client/src/protocol/starkingdoms-protocol.ts => client/src/protocol/starkingdoms-protocol.ts +1 -1
@@ 1,4 1,4 @@
// @ts-nocheck
//@ts-nocheck
import * as Long from "long";
import * as _m0 from "protobufjs/minimal";


M client/src/protocol/state.ts => client/src/protocol/state.ts +10 -4
@@ 1,19 1,23 @@
/* eslint-disable */
//@ts-nocheck

export const protobufPackage = "protocol.state";

export enum State {
  Handshake = 0,
  Play = 1,
  UNKNOWN = 0,
  Handshake = 1,
  Play = 2,
  UNRECOGNIZED = -1,
}

export function stateFromJSON(object: any): State {
  switch (object) {
    case 0:
    case "UNKNOWN":
      return State.UNKNOWN;
    case 1:
    case "Handshake":
      return State.Handshake;
    case 1:
    case 2:
    case "Play":
      return State.Play;
    case -1:


@@ 25,6 29,8 @@ export function stateFromJSON(object: any): State {

export function stateToJSON(object: State): string {
  switch (object) {
    case State.UNKNOWN:
      return "UNKNOWN";
    case State.Handshake:
      return "Handshake";
    case State.Play:

M client/vite.config.ts => client/vite.config.ts +1 -0
@@ 3,6 3,7 @@ import { defineConfig } from "vite";
export default defineConfig({
    build: {
        lib: {
            formats: ["es"],
            entry: {
                play: "play.html",
                index: "index.html",

A docker/README.md => docker/README.md +1 -0
@@ 0,0 1,1 @@
These files contain essential configuration files used by Ansible to manage and deploy StarKingdoms servers. **DO NOT TOUCH THESE FILES.**
\ No newline at end of file

M protocol/build.rs => protocol/build.rs +1 -0
@@ 10,5 10,6 @@ fn main() {
        .input("src/pbuf/state.proto")
        .input("src/pbuf/goodbye_reason.proto")
        .input("src/pbuf/module.proto")
        .input("src/pbuf/input.proto")
        .run_from_script();
}

M protocol/src/api.rs => protocol/src/api.rs +1 -3
@@ 2,6 2,4 @@ use serde::{Deserialize, Serialize};

// ALL FIELDS **MUST** BE WRAPPED IN Option<>
#[derive(Serialize, Deserialize, Default, Clone, Debug)]
pub struct APISavedPlayerData {

}
\ No newline at end of file
pub struct APISavedPlayerData {}

M protocol/src/legacy.rs => protocol/src/legacy.rs +14 -15
@@ 5,7 5,7 @@ pub const PROTOCOL_VERSION: u32 = 1;
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum State {
    Handshake,
    Play
    Play,
}

#[derive(Serialize, Deserialize, Debug, Clone)]


@@ 13,15 13,15 @@ pub enum MessageC2S {
    Hello {
        version: u32,
        requested_username: String,
        next_state: State
        next_state: State,
    },

    Goodbye {
        reason: GoodbyeReason
        reason: GoodbyeReason,
    },

    Chat {
        message: String
        message: String,
    },

    Ping {},


@@ 32,28 32,27 @@ pub enum MessageS2C {
    Hello {
        version: u32,
        given_username: String,
        next_state: State
        next_state: State,
    },

    Goodbye {
        reason: GoodbyeReason
        reason: GoodbyeReason,
    },

    Chat {
        from: String,
        message: String
        message: String,
    },

    Pong {},

    PlayersUpdate {
        players: Vec<ProtocolPlayer>
        players: Vec<ProtocolPlayer>,
    },


    PlanetData {
        planets: Vec<ProtocolPlanet>
    }
        planets: Vec<ProtocolPlanet>,
    },
}

#[derive(Serialize, Deserialize, Debug, Clone)]


@@ 61,7 60,7 @@ pub struct ProtocolPlayer {
    pub rotation: f64,
    pub x: f64,
    pub y: f64,
    pub username: String
    pub username: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]


@@ 79,18 78,18 @@ pub struct ProtocolPlanet {
    pub planet_type: PlanetType,
    pub x: f64,
    pub y: f64,
    pub radius: f64
    pub radius: f64,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PlanetType {
    Earth
    Earth,
}

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

M protocol/src/lib.rs => protocol/src/lib.rs +116 -67
@@ 1,12 1,18 @@
use std::error::Error;
use protobuf::{Enum, Message};
use crate::message_c2s::{MessageC2SAuthenticateAndBeamOut, MessageC2SChat, MessageC2SGoodbye, MessageC2SHello, MessageC2SInput, MessageC2SPing};
use crate::message_s2c::{MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CPlanetData, MessageS2CPlayersUpdate, MessageS2CPong, MessageS2CModulesUpdate};
use crate::message_c2s::{
    MessageC2SAuthenticateAndBeamOut, MessageC2SChat, MessageC2SGoodbye, MessageC2SHello,
    MessageC2SInput, MessageC2SMouseInput, MessageC2SPing,
};
use crate::message_s2c::{
    MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CModulesUpdate,
    MessageS2CPlanetData, MessageS2CPlayersUpdate, MessageS2CPong,
};
use crate::planet::PlanetType;
use crate::starkingdoms_protocol::PacketWrapper;
use protobuf::{Enum, Message};
use std::error::Error;
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));

pub const PROTOCOL_VERSION: u32 = 3;
pub const PROTOCOL_VERSION: u32 = 4;

pub mod api;



@@ 17,7 23,8 @@ pub enum MessageC2S {
    Chat(MessageC2SChat),
    Ping(MessageC2SPing),
    Input(MessageC2SInput),
    AuthenticateAndBeamOut(MessageC2SAuthenticateAndBeamOut)
    AuthenticateAndBeamOut(MessageC2SAuthenticateAndBeamOut),
    MouseInput(MessageC2SMouseInput),
}

#[derive(Debug)]


@@ 40,23 47,35 @@ impl TryFrom<&[u8]> for MessageC2S {
        let deser_pkt = match pkt.packet_id {
            _id if _id == message_c2s::message_c2shello::Packet_info::type_.value() as i64 => {
                MessageC2S::Hello(MessageC2SHello::parse_from_bytes(&pkt.packet_data)?)
            },
            }
            _id if _id == message_c2s::message_c2sgoodbye::Packet_info::type_.value() as i64 => {
                MessageC2S::Goodbye(MessageC2SGoodbye::parse_from_bytes(&pkt.packet_data)?)
            },
            }
            _id if _id == message_c2s::message_c2schat::Packet_info::type_.value() as i64 => {
                MessageC2S::Chat(MessageC2SChat::parse_from_bytes(&pkt.packet_data)?)
            },
            }
            _id if _id == message_c2s::message_c2sping::Packet_info::type_.value() as i64 => {
                MessageC2S::Ping(MessageC2SPing::parse_from_bytes(&pkt.packet_data)?)
            },
            }
            _id if _id == message_c2s::message_c2sinput::Packet_info::type_.value() as i64 => {
                MessageC2S::Input(MessageC2SInput::parse_from_bytes(&pkt.packet_data)?)
            },
            _id if _id == message_c2s::message_c2sauthenticate_and_beam_out::Packet_info::type_.value() as i64 => {
                MessageC2S::AuthenticateAndBeamOut(MessageC2SAuthenticateAndBeamOut::parse_from_bytes(&pkt.packet_data)?)
            }
            _id => { return Err(format!("Unrecognized C2S packet {}", _id).into()); }
            _id if _id
                == message_c2s::message_c2sauthenticate_and_beam_out::Packet_info::type_.value()
                    as i64 =>
            {
                MessageC2S::AuthenticateAndBeamOut(
                    MessageC2SAuthenticateAndBeamOut::parse_from_bytes(&pkt.packet_data)?,
                )
            }
            _id if _id
                == message_c2s::message_c2smouse_input::Packet_info::type_.value() as i64 =>
            {
                MessageC2S::MouseInput(MessageC2SMouseInput::parse_from_bytes(&pkt.packet_data)?)
            }
            _id => {
                return Err(format!("Unrecognized C2S packet {}", _id).into());
            }
        };

        Ok(deser_pkt)


@@ 68,24 87,34 @@ impl TryInto<Vec<u8>> for MessageC2S {

    fn try_into(self) -> Result<Vec<u8>, Self::Error> {
        let (pkt_id, pkt_bytes) = match self {
            MessageC2S::Hello(p) => {
                (message_c2s::message_c2shello::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageC2S::Goodbye(p) => {
                (message_c2s::message_c2sgoodbye::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageC2S::Chat(p) => {
                (message_c2s::message_c2schat::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageC2S::Ping(p) => {
                (message_c2s::message_c2sping::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageC2S::Input(p) => {
                (message_c2s::message_c2sping::Packet_info::type_.value(), p.write_to_bytes()?)
            },
            MessageC2S::AuthenticateAndBeamOut(p) => {
                (message_c2s::message_c2sauthenticate_and_beam_out::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageC2S::Hello(p) => (
                message_c2s::message_c2shello::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageC2S::Goodbye(p) => (
                message_c2s::message_c2sgoodbye::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageC2S::Chat(p) => (
                message_c2s::message_c2schat::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageC2S::Ping(p) => (
                message_c2s::message_c2sping::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageC2S::Input(p) => (
                message_c2s::message_c2sping::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageC2S::AuthenticateAndBeamOut(p) => (
                message_c2s::message_c2sauthenticate_and_beam_out::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageC2S::MouseInput(p) => (
                message_c2s::message_c2smouse_input::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
        };

        let pkt = PacketWrapper {


@@ 107,26 136,38 @@ impl TryFrom<&[u8]> for MessageS2C {
        let deser_pkt = match pkt.packet_id {
            _id if _id == message_s2c::message_s2chello::Packet_info::type_.value() as i64 => {
                MessageS2C::Hello(MessageS2CHello::parse_from_bytes(&pkt.packet_data)?)
            },
            }
            _id if _id == message_s2c::message_s2cgoodbye::Packet_info::type_.value() as i64 => {
                MessageS2C::Goodbye(MessageS2CGoodbye::parse_from_bytes(&pkt.packet_data)?)
            },
            }
            _id if _id == message_s2c::message_s2cchat::Packet_info::type_.value() as i64 => {
                MessageS2C::Chat(MessageS2CChat::parse_from_bytes(&pkt.packet_data)?)
            },
            }
            _id if _id == message_s2c::message_s2cpong::Packet_info::type_.value() as i64 => {
                MessageS2C::Pong(MessageS2CPong::parse_from_bytes(&pkt.packet_data)?)
            },
            _id if _id == message_s2c::message_s2cplayers_update::Packet_info::type_.value() as i64 => {
                MessageS2C::PlayersUpdate(MessageS2CPlayersUpdate::parse_from_bytes(&pkt.packet_data)?)
            },
            _id if _id == message_s2c::message_s2cplanet_data::Packet_info::type_.value() as i64 => {
            }
            _id if _id
                == message_s2c::message_s2cplayers_update::Packet_info::type_.value() as i64 =>
            {
                MessageS2C::PlayersUpdate(MessageS2CPlayersUpdate::parse_from_bytes(
                    &pkt.packet_data,
                )?)
            }
            _id if _id
                == message_s2c::message_s2cplanet_data::Packet_info::type_.value() as i64 =>
            {
                MessageS2C::PlanetData(MessageS2CPlanetData::parse_from_bytes(&pkt.packet_data)?)
            },
            _id if _id == message_s2c::message_s2cmodules_update::Packet_info::type_.value() as i64 => {
                MessageS2C::ModulesUpdate(MessageS2CModulesUpdate::parse_from_bytes(&pkt.packet_data)?)
            },
            _ => { return Err("Not a S2C packet".into()); }
            }
            _id if _id
                == message_s2c::message_s2cmodules_update::Packet_info::type_.value() as i64 =>
            {
                MessageS2C::ModulesUpdate(MessageS2CModulesUpdate::parse_from_bytes(
                    &pkt.packet_data,
                )?)
            }
            _ => {
                return Err("Not a S2C packet".into());
            }
        };

        Ok(deser_pkt)


@@ 138,27 179,34 @@ impl TryInto<Vec<u8>> for MessageS2C {

    fn try_into(self) -> Result<Vec<u8>, Self::Error> {
        let (pkt_id, pkt_bytes) = match self {
            MessageS2C::Hello(p) => {
                (message_s2c::message_s2chello::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageS2C::Goodbye(p) => {
                (message_s2c::message_s2cgoodbye::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageS2C::Chat(p) => {
                (message_s2c::message_s2cchat::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageS2C::Pong(p) => {
                (message_s2c::message_s2cpong::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageS2C::PlayersUpdate(p) => {
                (message_s2c::message_s2cplayers_update::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageS2C::PlanetData(p) => {
                (message_s2c::message_s2cplanet_data::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageS2C::ModulesUpdate(p) => {
                (message_s2c::message_s2cmodules_update::Packet_info::type_.value(), p.write_to_bytes()?)
            }
            MessageS2C::Hello(p) => (
                message_s2c::message_s2chello::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageS2C::Goodbye(p) => (
                message_s2c::message_s2cgoodbye::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageS2C::Chat(p) => (
                message_s2c::message_s2cchat::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageS2C::Pong(p) => (
                message_s2c::message_s2cpong::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageS2C::PlayersUpdate(p) => (
                message_s2c::message_s2cplayers_update::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageS2C::PlanetData(p) => (
                message_s2c::message_s2cplanet_data::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
            MessageS2C::ModulesUpdate(p) => (
                message_s2c::message_s2cmodules_update::Packet_info::type_.value(),
                p.write_to_bytes()?,
            ),
        };

        let pkt = PacketWrapper {


@@ 175,7 223,8 @@ impl planet::PlanetType {
    pub fn as_texture_id(&self) -> String {
        match self {
            PlanetType::Earth => "earth".to_string(),
            PlanetType::Moon => "moon".to_string()
            PlanetType::Moon => "moon".to_string(),
            PlanetType::UNKNOWN => "missing".to_string(),
        }
    }
}

M protocol/src/pbuf/goodbye_reason.proto => protocol/src/pbuf/goodbye_reason.proto +7 -6
@@ 2,10 2,11 @@ syntax = "proto3";
package protocol.goodbye_reason;

enum GoodbyeReason {
  UnsupportedProtocol = 0;
  UnexpectedPacket = 1;
  UnexpectedNextState = 2;
  UsernameTaken = 3;
  PingPongTimeout = 4;
  Done = 5;
  UNKNOWN = 0;
  UnsupportedProtocol = 1;
  UnexpectedPacket = 2;
  UnexpectedNextState = 3;
  UsernameTaken = 4;
  PingPongTimeout = 5;
  Done = 6;
}
\ No newline at end of file

A protocol/src/pbuf/input.proto => protocol/src/pbuf/input.proto +9 -0
@@ 0,0 1,9 @@
syntax = "proto3";
package protocol.input;

enum InputType {
  UNKNOWN = 0;
  Left = 1;
  Middle = 2;
  Right = 3;
}
\ No newline at end of file

M protocol/src/pbuf/message_c2s.proto => protocol/src/pbuf/message_c2s.proto +12 -0
@@ 3,6 3,7 @@ package protocol.message_c2s;

import "state.proto";
import "goodbye_reason.proto";
import "input.proto";

message MessageC2SHello {
  enum packet_info { unknown = 0; type = 0x01; }


@@ 38,6 39,8 @@ message MessageC2SInput {
  bool down_pressed = 2;
  bool left_pressed = 3;
  bool right_pressed = 4;


}

message MessageC2SAuthenticateAndBeamOut {


@@ 45,4 48,13 @@ message MessageC2SAuthenticateAndBeamOut {

  string user_id = 1; // The user ID that the client is authenticating as
  string token = 2;   // The token from the authentication server that the user is authenticating as
}

message MessageC2SMouseInput {
  enum packet_info { unknown = 0; type = 0x0d; }

  double worldpos_x = 1;
  double worldpos_y = 2;
  protocol.input.InputType button = 3;
  bool released = 4;
}
\ No newline at end of file

M protocol/src/pbuf/module.proto => protocol/src/pbuf/module.proto +7 -6
@@ 3,13 3,14 @@ package protocol.module;

message Module {
    ModuleType module_type = 1;
    float rotation = 2;
    float x = 3;
    float y = 4;
    double rotation = 2;
    double x = 3;
    double y = 4;
}

enum ModuleType {
    Cargo = 0;
    LandingThruster = 1;
    Hub = 2;
    UNKNOWN = 0;
    Cargo = 1;
    LandingThruster = 2;
    Hub = 3;
}

M protocol/src/pbuf/planet.proto => protocol/src/pbuf/planet.proto +6 -5
@@ 3,12 3,13 @@ package protocol.planet;

message Planet {
  PlanetType planet_type = 1;  // Type of the planet
  float x = 2;          // Translation on the X axis, in game units
  float y = 3;          // Translation on the Y axis, in game units
  float radius = 4;     // The radius of the planet extending out from (x, y)
  double x = 2;          // Translation on the X axis, in game units
  double y = 3;          // Translation on the Y axis, in game units
  double radius = 4;     // The radius of the planet extending out from (x, y)
}

enum PlanetType {
  Earth = 0;
  Moon = 1;
  UNKNOWN = 0;
  Earth = 1;
  Moon = 2;
}
\ No newline at end of file

M protocol/src/pbuf/player.proto => protocol/src/pbuf/player.proto +3 -3
@@ 2,8 2,8 @@ syntax = "proto3";
package protocol.player;

message Player {
  float rotation = 1;   // The rotation, clockwise, in degrees, of the player
  float x = 2;          // The translation on the X axis, in game units, of the player
  float y = 3;          // The translation on the Y axis, in game units, of the player
  double rotation = 1;   // The rotation, clockwise, in degrees, of the player
  double x = 2;          // The translation on the X axis, in game units, of the player
  double y = 3;          // The translation on the Y axis, in game units, of the player
  string username = 4;  // The username of the player
}
\ No newline at end of file

M protocol/src/pbuf/starkingdoms-protocol.proto => protocol/src/pbuf/starkingdoms-protocol.proto +1 -0
@@ 7,6 7,7 @@ import public "goodbye_reason.proto";
import public "message_s2c.proto";
import public "player.proto";
import public "planet.proto";
import public "input.proto";

message PacketWrapper {
  int64 packet_id = 1;    // What is the Packet ID of this packet?

M protocol/src/pbuf/state.proto => protocol/src/pbuf/state.proto +3 -2
@@ 2,6 2,7 @@ syntax = "proto3";
package protocol.state;

enum State {
  Handshake = 0;
  Play = 1;
  UNKNOWN = 0;
  Handshake = 1;
  Play = 2;
}
\ No newline at end of file

M server/build.rs => server/build.rs +19 -6
@@ 1,5 1,5 @@
use std::process::Command;
use cargo_metadata::MetadataCommand;
use std::process::Command;

fn main() {
    let path = std::env::var("CARGO_MANIFEST_DIR").unwrap();


@@ 14,16 14,29 @@ fn main() {

    let version = root.version.to_string();
    let version_name = root.metadata["version-name"].to_string().replace('"', "");
    let description = root.metadata["slp-description"].to_string().replace('"', "");
    let description = root.metadata["slp-description"]
        .to_string()
        .replace('"', "");

    let output = Command::new("git").args(["rev-parse", "--short", "HEAD"]).output().unwrap();
    let output = Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .unwrap();
    let git_hash = String::from_utf8(output.stdout).unwrap();

    println!("cargo:rustc-env=STK_VERSION={}", version);
    println!("cargo:rustc-env=STK_VERSION_NAME={}", version_name);
    println!("cargo:rustc-env=STK_SLP_DESCRIPTION={}", description);
    println!("cargo:rustc-env=STK_CHANNEL={}", std::env::var("STK_CHANNEL").unwrap_or("dev".to_string()));
    println!("cargo:rustc-env=STK_BUILD={}-{}-{}", std::env::var("STK_CHANNEL").unwrap_or("dev".to_string()), std::env::var("STK_BUILD_NUM").unwrap_or("local".to_string()), git_hash);
    println!(
        "cargo:rustc-env=STK_CHANNEL={}",
        std::env::var("STK_CHANNEL").unwrap_or("dev".to_string())
    );
    println!(
        "cargo:rustc-env=STK_BUILD={}-{}-{}",
        std::env::var("STK_CHANNEL").unwrap_or("dev".to_string()),
        std::env::var("STK_BUILD_NUM").unwrap_or("local".to_string()),
        git_hash
    );
    println!("cargo:rerun-if-changed=Cargo.toml");
    println!("cargo:rerun-if-env-changed=STK_BUILD_NUM");
}
\ No newline at end of file
}

M server/src/api.rs => server/src/api.rs +44 -17
@@ 1,40 1,53 @@
use std::error::Error;
use log::error;
use reqwest::StatusCode;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use starkingdoms_protocol::api::APISavedPlayerData;
use std::error::Error;

#[derive(Serialize, Deserialize)]
pub struct BeaminRequest {
    pub api_token: String,
    pub user_auth_realm_id: String,
    pub user_auth_token: String
    pub user_auth_token: String,
}

#[derive(Serialize, Deserialize)]
pub struct BeaminResponse {
    pub save_id: String,
    pub save: APISavedPlayerData
    pub save: APISavedPlayerData,
}

pub async fn load_player_data_from_api(token: &str, user_id: &str, internal_token: &str) -> Result<APISavedPlayerData, Box<dyn Error>> {
pub async fn load_player_data_from_api(
    token: &str,
    user_id: &str,
    internal_token: &str,
) -> Result<APISavedPlayerData, Box<dyn Error>> {
    let client = reqwest::Client::new();

    let req_body = BeaminRequest {
        api_token: internal_token.to_owned(),
        user_auth_realm_id: user_id.to_owned(),
        user_auth_token: token.to_owned()
        user_auth_token: token.to_owned(),
    };

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

    if res.status() == StatusCode::NO_CONTENT {
        return Ok(APISavedPlayerData::default())
        return Ok(APISavedPlayerData::default());
    }

    if res.status() != StatusCode::OK {
        error!("error with API call (status: {}, body: {})", res.status(), res.text().await?);
        return Err("Error with API call".into())
        error!(
            "error with API call (status: {}, body: {})",
            res.status(),
            res.text().await?
        );
        return Err("Error with API call".into());
    }

    let resp: BeaminResponse = serde_json::from_str(&res.text().await?)?;


@@ 47,25 60,39 @@ pub struct BeamoutRequest {
    pub api_token: String,
    pub user_auth_realm_id: String,
    pub user_auth_token: String,
    pub data: APISavedPlayerData
    pub data: APISavedPlayerData,
}

pub async fn save_player_data_to_api(data: &APISavedPlayerData, token: &str, user_id: &str, internal_token: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
pub async fn save_player_data_to_api(
    data: &APISavedPlayerData,
    token: &str,
    user_id: &str,
    internal_token: &str,
) -> Result<(), Box<dyn Error + Send + Sync>> {
    let client = reqwest::Client::new();

    let req_body = BeamoutRequest {
        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.to_owned(),
    };

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

    if res.status() != StatusCode::OK {
        error!("error with API call (status: {}, body: {})", res.status(), res.text().await?);
        return Err("Error with API call".into())
        error!(
            "error with API call (status: {}, body: {})",
            res.status(),
            res.text().await?
        );
        return Err("Error with API call".into());
    }

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

M server/src/entity.rs => server/src/entity.rs +22 -24
@@ 1,9 1,13 @@
use std::{sync::atomic::AtomicU32, collections::HashMap, net::SocketAddr};
use std::{collections::HashMap, net::SocketAddr, sync::atomic::AtomicU32};

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

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

pub type EntityId = u32;
pub type Entities = HashMap<EntityId, Entity>;


@@ 11,7 15,9 @@ 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") };
    if id > 4_147_483_600 {
        panic!("No remaining entity ids")
    };
    id
}



@@ 23,7 29,7 @@ pub struct EntityHandler {
impl EntityHandler {
    pub fn new() -> EntityHandler {
        EntityHandler {
            entities: Entities::new()
            entities: Entities::new(),
        }
    }
    pub fn get_planets(&self) -> Vec<Planet> {


@@ 58,12 64,11 @@ impl EntityHandler {
        players
    }
    pub fn get_player_from_id(&self, id: EntityId) -> Option<Player> {
        if let Some(entity) = self.entities.get(&id) {
            if let Entity::Player(player) = entity {
                return Some(player.clone());
            }
        if let Some(Entity::Player(player)) = self.entities.get(&id) {
            Some(player.clone())
        } else {
            None
        }
        None
    }
    pub fn get_player_id(&self, addr: SocketAddr) -> Option<EntityId> {
        for (id, entity) in self.entities.iter() {


@@ 106,10 111,8 @@ impl EntityHandler {
        module_count
    }
    pub fn get_module_from_id(&self, id: EntityId) -> Option<Module> {
        if let Some(entity) = self.entities.get(&id) {
            if let Entity::Module(module) = entity {
                return Some(module.clone());
            }
        if let Some(Entity::Module(module)) = self.entities.get(&id) {
            return Some(module.clone());
        }
        None
    }


@@ 133,10 136,8 @@ impl EntityHandler {
        modules
    }
    pub fn get_attached_from_id(&self, id: EntityId) -> Option<AttachedModule> {
        if let Some(entity) = self.entities.get(&id) {
            if let Entity::AttachedModule(module) = entity {
                return Some(module.clone());
            }
        if let Some(Entity::AttachedModule(module)) = self.entities.get(&id) {
            return Some(module.clone());
        }
        None
    }


@@ 156,19 157,16 @@ impl EntityHandler {
        let mut planets = vec![];

        for planet in self.get_planets() {
            // TODO: Adjust codegen to use f64
            planets.push(starkingdoms_protocol::planet::Planet {
                planet_type: planet.planet_type.into(),
                x: (planet.position.0 * SCALE) as f32,
                y: (planet.position.1 * SCALE) as f32,
                radius: planet.radius as f32, // DO NOT * SCALE
                x: planet.position.0 * SCALE,
                y: planet.position.1 * SCALE,
                radius: planet.radius, // DO NOT * SCALE. THIS VALUE IS NOT SCALED!
                special_fields: Default::default(),
            });
        }

        ClientHandlerMessage::PlanetData {
            planets
        }
        ClientHandlerMessage::PlanetData { planets }
    }
}


M server/src/handler.rs => server/src/handler.rs +241 -124
@@ 1,32 1,44 @@
use crate::api::{load_player_data_from_api, save_player_data_to_api};
use crate::entity::{get_entity_id, Entity, EntityHandler};
use crate::manager::{
    AttachedModule, ClientHandlerMessage, ClientManager, ModuleTemplate, PhysicsData, Player,
};
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};
use nalgebra::{point, vector};
use rand::Rng;
use rapier2d_f64::prelude::{
    Collider, ColliderBuilder, MassProperties, RigidBodyBuilder, RigidBodyType,
};
use starkingdoms_protocol::goodbye_reason::GoodbyeReason;
use starkingdoms_protocol::message_s2c::{
    MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CModulesUpdate,
    MessageS2CPlanetData, MessageS2CPlayersUpdate, MessageS2CPong,
};
use starkingdoms_protocol::module::ModuleType;
use starkingdoms_protocol::state::State;
use starkingdoms_protocol::{MessageC2S, MessageS2C, PROTOCOL_VERSION};
use std::error::Error;
use std::f64::consts::PI;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use futures::stream::{SplitSink, SplitStream};
use futures::{FutureExt, SinkExt, StreamExt};
use log::{error, info, debug, warn};
use nalgebra::{vector, point};
use rand::Rng;
use rapier2d_f64::prelude::{RigidBodyBuilder, RigidBodyType, ColliderBuilder, MassProperties, Collider};
use starkingdoms_protocol::module::ModuleType;
use tungstenite::Message;
use starkingdoms_protocol::goodbye_reason::GoodbyeReason;
use starkingdoms_protocol::message_s2c::{MessageS2CChat, MessageS2CGoodbye, MessageS2CHello, MessageS2CPlanetData, MessageS2CPlayersUpdate, MessageS2CPong, MessageS2CModulesUpdate};
use starkingdoms_protocol::{MessageS2C, MessageC2S, PROTOCOL_VERSION};
use starkingdoms_protocol::state::State;
use crate::entity::{EntityHandler, get_entity_id, Entity};
use crate::manager::{ClientHandlerMessage, ClientManager, PhysicsData, Player, AttachedModule, ModuleTemplate};
use crate::{send, recv, SCALE};
use async_std::{sync::RwLock, channel::Receiver};
use async_std::net::TcpStream;
use async_tungstenite::WebSocketStream;
use crate::api::{load_player_data_from_api, save_player_data_to_api};

pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandler>>, data: Arc<RwLock<PhysicsData>>,
                           remote_addr: SocketAddr, rx: Receiver<ClientHandlerMessage>,
                           mut client_tx: SplitSink<WebSocketStream<TcpStream>, Message>, mut client_rx: SplitStream<WebSocketStream<TcpStream>>
                          ) -> Result<(), Box<dyn Error>> {
pub async fn handle_client(
    mgr: ClientManager,
    entities: Arc<RwLock<EntityHandler>>,
    data: Arc<RwLock<PhysicsData>>,
    remote_addr: SocketAddr,
    rx: Receiver<ClientHandlerMessage>,
    mut client_tx: SplitSink<WebSocketStream<TcpStream>, Message>,
    mut client_rx: SplitStream<WebSocketStream<TcpStream>>,
) -> Result<(), Box<dyn Error>> {
    let mut state = State::Handshake;
    let mut username = String::new();
    let mut ping_timeout = SystemTime::now() + Duration::from_secs(10);


@@ 41,7 53,8 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                            from,
                            message,
                            special_fields: Default::default(),
                        }).try_into()?;
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                    }
                }


@@ 50,7 63,8 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                        let msg = MessageS2C::PlayersUpdate(MessageS2CPlayersUpdate {
                            players,
                            special_fields: Default::default(),
                        }).try_into()?;
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                    }
                }


@@ 59,7 73,8 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                        let msg = MessageS2C::PlanetData(MessageS2CPlanetData {
                            planets,
                            special_fields: Default::default(),
                        }).try_into()?;
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                    }
                }


@@ 68,7 83,8 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                        let msg = MessageS2C::ModulesUpdate(MessageS2CModulesUpdate {
                            modules,
                            special_fields: Default::default(),
                        }).try_into()?;
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                    }
                }


@@ 83,46 99,66 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
            let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                reason: GoodbyeReason::PingPongTimeout.into(),
                special_fields: Default::default(),
            }).try_into()?;
            })
            .try_into()?;
            send!(client_tx, msg).await?;
            break;
        }

        if let Some(pkt) = recv!(client_rx)? {
            match state {
                State::UNKNOWN => unreachable!(),
                State::Handshake => {
                    match pkt {
                        MessageC2S::Hello(pkt) => {
                            info!("client sent hello");
                            if !matches!(pkt.next_state.unwrap(), State::Play) {
                                error!("client sent unexpected state {:?} (expected: Play)", pkt.next_state);
                                error!(
                                    "client sent unexpected state {:?} (expected: Play)",
                                    pkt.next_state
                                );
                                let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                    reason: GoodbyeReason::UnexpectedNextState.into(),
                                    special_fields: Default::default(),
                                }).try_into()?;
                                })
                                .try_into()?;
                                send!(client_tx, msg).await?;
                                break;
                            }

                            // check version
                            if pkt.version != PROTOCOL_VERSION {
                                error!("client sent incompatible version {} (expected: {})", pkt.version, PROTOCOL_VERSION);
                                error!(
                                    "client sent incompatible version {} (expected: {})",
                                    pkt.version, PROTOCOL_VERSION
                                );
                                let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                    reason: GoodbyeReason::UnsupportedProtocol.into(),
                                    special_fields: Default::default(),
                                }).try_into()?;
                                })
                                .try_into()?;
                                send!(client_tx, msg).await?;
                                break;
                            }

                            // determine if we can give them that username
                            {
                                if mgr.usernames.read().await.values().any(|u| *u == pkt.requested_username) {
                                    error!("client requested username {} but it is in use", pkt.requested_username);
                                if mgr
                                    .usernames
                                    .read()
                                    .await
                                    .values()
                                    .any(|u| *u == pkt.requested_username)
                                {
                                    error!(
                                        "client requested username {} but it is in use",
                                        pkt.requested_username
                                    );
                                    let msg: Vec<u8> = MessageS2C::Goodbye(MessageS2CGoodbye {
                                        reason: GoodbyeReason::UsernameTaken.into(),
                                        special_fields: Default::default(),
                                    }).try_into()?;
                                    })
                                    .try_into()?;
                                    send!(client_tx, msg).await?;
                                    break;
                                }


@@ 130,7 166,10 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle

                            // username is fine
                            {
                                mgr.usernames.write().await.insert(remote_addr, pkt.requested_username.clone());
                                mgr.usernames
                                    .write()
                                    .await
                                    .insert(remote_addr, pkt.requested_username.clone());
                            }

                            let msg = MessageS2C::Hello(MessageS2CHello {


@@ 138,7 177,8 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                                given_username: pkt.requested_username.clone(),
                                special_fields: Default::default(),
                                next_state: pkt.next_state,
                            }).try_into()?;
                            })
                            .try_into()?;

                            send!(client_tx, msg).await?;



@@ 157,16 197,27 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                                    rng.gen::<f64>() * PI * 2.
                                };
                                let player_body = RigidBodyBuilder::new(RigidBodyType::Dynamic)
                                    .translation(vector![angle.cos() * 2050. / SCALE, angle.sin() * 2050.0/SCALE])
                                    .translation(vector![
                                        angle.cos() * 2050. / SCALE,
                                        angle.sin() * 2050.0 / SCALE
                                    ])
                                    .rotation(angle + PI / 2.)
                                    .build();
                                let player_collider: Collider = ColliderBuilder::cuboid(25.0 / SCALE, 25.0 / SCALE)
                                    .mass_properties(MassProperties::new(point![0.0, 0.0], 120.0, 122500.0))
                                    .build();
                                let player_collider: Collider =
                                    ColliderBuilder::cuboid(25.0 / SCALE, 25.0 / SCALE)
                                        .mass_properties(MassProperties::new(
                                            point![0.0, 0.0],
                                            120.0,
                                            122500.0,
                                        ))
                                        .build();
                                let player_handle = rigid_body_set.insert(player_body);

                                collider_set.insert_with_parent(player_collider, player_handle, &mut rigid_body_set);

                                collider_set.insert_with_parent(
                                    player_collider,
                                    player_handle,
                                    &mut rigid_body_set,
                                );

                                let mut player = Player {
                                    handle: player_handle,


@@ 182,125 233,191 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                                if !pkt.user.is_empty() && !pkt.token.is_empty() {
                                    player.auth_token = Some(pkt.token.clone());
                                    player.auth_user = Some(pkt.user.clone());
                                    info!("[{}] * Beamin: beaming in {} as {} with token {}", remote_addr, username, pkt.user, pkt.token);
                                    info!(
                                        "[{}] * Beamin: beaming in {} as {} with token {}",
                                        remote_addr, username, pkt.user, pkt.token
                                    );

                                    let player_data = match load_player_data_from_api(&pkt.token, &pkt.user, &std::env::var("STK_API_KEY").unwrap()).await {
                                    let player_data = match load_player_data_from_api(
                                        &pkt.token,
                                        &pkt.user,
                                        &std::env::var("STK_API_KEY").unwrap(),
                                    )
                                    .await
                                    {
                                        Ok(d) => d,
                                        Err(e) => {
                                            warn!("[{}] * Beamin: ABORTED. API returned error: {}", remote_addr, e);
                                            e_write_handle.entities.insert(get_entity_id(), Entity::Player(player));
                                            warn!(
                                                "[{}] * Beamin: ABORTED. API returned error: {}",
                                                remote_addr, e
                                            );
                                            e_write_handle
                                                .entities
                                                .insert(get_entity_id(), Entity::Player(player));
                                            continue;
                                        }
                                    };

                                    info!("[{}] Beamin: loaded player data! {:?}", remote_addr, player_data);
                                    info!(
                                        "[{}] Beamin: loaded player data! {:?}",
                                        remote_addr, player_data
                                    );

                                    player.load_api_data(&player_data);
                                }

                                let player_id = get_entity_id();
                                e_write_handle.entities.insert(player_id, Entity::Player(player));
                                AttachedModule::attach_new(&mut data_handle, &mut e_write_handle,
                                                           player_id, player_id,
                                                           ModuleTemplate {
                                                               translation: vector![0.0, 50.0],
                                                               heading: 0.0,
                                                               mass_properties: MassProperties::new(point![0.0, 0.0], 120.0, 122500.0),
                                                               module_type: ModuleType::Cargo,
                                                           }, 0, angle);
                                e_write_handle
                                    .entities
                                    .insert(player_id, Entity::Player(player));
                                AttachedModule::attach_new(
                                    &mut data_handle,
                                    &mut e_write_handle,
                                    player_id,
                                    player_id,
                                    ModuleTemplate {
                                        translation: vector![0.0, 50.0],
                                        heading: 0.0,
                                        mass_properties: MassProperties::new(
                                            point![0.0, 0.0],
                                            120.0,
                                            122500.0,
                                        ),
                                        module_type: ModuleType::Cargo,
                                    },
                                    0,
                                    angle,
                                );
                                data_handle.rigid_body_set = rigid_body_set;
                                data_handle.collider_set = collider_set;
                                debug!("running");
                            }
                        },
                        }
                        MessageC2S::Goodbye(pkt) => {
                            info!("client sent goodbye: {:?}", pkt.reason);
                            break;
                        },
                        }
                        _ => {
                            error!("client sent unexpected packet {:?} for state {:?}", pkt, state);
                            error!(
                                "client sent unexpected packet {:?} for state {:?}",
                                pkt, state
                            );
                            let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                reason: GoodbyeReason::UnexpectedPacket.into(),
                                special_fields: Default::default(),
                            }).try_into()?;
                            })
                            .try_into()?;
                            send!(client_tx, msg).await?;
                            break;
                        }
                    }
                }
                State::Play => {
                    match pkt {
                        MessageC2S::Hello { .. } => {
                            error!("client sent unexpected packet {:?} for state {:?}", pkt, state);
                            let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                reason: GoodbyeReason::UnexpectedPacket.into(),
                                special_fields: Default::default(),
                            }).try_into()?;
                            send!(client_tx, msg).await?;
                            break;
                        },
                        MessageC2S::Goodbye(pkt) => {
                            info!("client sent goodbye: {:?}", pkt.reason);
                            break;
                        },
                        MessageC2S::Chat(pkt) => {
                            info!("[{}] CHAT: [{}] {}", remote_addr, username, pkt.message);
                State::Play => match pkt {
                    MessageC2S::Hello { .. } => {
                        error!(
                            "client sent unexpected packet {:?} for state {:?}",
                            pkt, state
                        );
                        let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                            reason: GoodbyeReason::UnexpectedPacket.into(),
                            special_fields: Default::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                        break;
                    }
                    MessageC2S::Goodbye(pkt) => {
                        info!("client sent goodbye: {:?}", pkt.reason);
                        break;
                    }
                    MessageC2S::Chat(pkt) => {
                        info!("[{}] CHAT: [{}] {}", remote_addr, username, pkt.message);

                            for (_addr, client_thread) in mgr.handlers.read().await.iter() {
                                match client_thread.tx.send(ClientHandlerMessage::ChatMessage { from: username.clone(), message: pkt.message.clone() }).await {
                                    Ok(_) => (),
                                    Err(e) => {
                                        error!("unable to update a client thread: {}", e);
                                    }
                        for (_addr, client_thread) in mgr.handlers.read().await.iter() {
                            match client_thread
                                .tx
                                .send(ClientHandlerMessage::ChatMessage {
                                    from: username.clone(),
                                    message: pkt.message.clone(),
                                })
                                .await
                            {
                                Ok(_) => (),
                                Err(e) => {
                                    error!("unable to update a client thread: {}", e);
                                }
                            }
                        },
                        MessageC2S::Ping(_) => {
                            let msg = MessageS2C::Pong(MessageS2CPong {
                                special_fields: Default::default(),
                            }).try_into()?;
                            send!(client_tx, msg).await?;
                            ping_timeout = SystemTime::now() + Duration::from_secs(10);
                        },
                        MessageC2S::Input(p) => {
                            let mut handle = entities.write().await;
                            let id = handle.get_player_id(remote_addr)
                                .expect("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") {
                                me.input.up = p.up_pressed;
                                me.input.down = p.down_pressed;
                                me.input.left = p.left_pressed;
                                me.input.right = p.right_pressed;
                            }
                        },
                        MessageC2S::AuthenticateAndBeamOut(p) => {
                            info!("[{}] * Beaming out {} as {} with realm token {}", remote_addr, username, p.user_id, p.token);
                        }
                    }
                    MessageC2S::Ping(_) => {
                        let msg = MessageS2C::Pong(MessageS2CPong {
                            special_fields: Default::default(),
                        })
                        .try_into()?;
                        send!(client_tx, msg).await?;
                        ping_timeout = SystemTime::now() + Duration::from_secs(10);
                    }
                    MessageC2S::Input(p) => {
                        let mut handle = entities.write().await;
                        let id = handle
                            .get_player_id(remote_addr)
                            .expect("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")
                        {
                            me.input.up = p.up_pressed;
                            me.input.down = p.down_pressed;
                            me.input.left = p.left_pressed;
                            me.input.right = p.right_pressed;
                        }
                    }
                    MessageC2S::AuthenticateAndBeamOut(p) => {
                        info!(
                            "[{}] * Beaming out {} as {} with realm token {}",
                            remote_addr, username, p.user_id, p.token
                        );

                            let player = entities.read().await.get_player(remote_addr).expect("Player sending messages after disconnect");
                        let player = entities
                            .read()
                            .await
                            .get_player(remote_addr)
                            .expect("Player sending messages after disconnect");

                            if Some(p.token) != player.auth_token || Some(p.user_id) != player.auth_user {
                                warn!("[{}] invalid beamout packet, ignoring", remote_addr);
                                continue;
                            }
                        if Some(p.token) != player.auth_token || Some(p.user_id) != player.auth_user
                        {
                            warn!("[{}] invalid beamout packet, ignoring", remote_addr);
                            continue;
                        }

                            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()).await {
                                Ok(_) => {
                                    info!("[{}] * Beamed out successfully", remote_addr);
                                    let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                        reason: GoodbyeReason::Done.into(),
                                        special_fields: Default::default(),
                                    }).try_into()?;
                                    send!(client_tx, msg).await?;
                                    break;
                                }
                                Err(e) => {
                                    error!("[{}] error beaming out: {}", remote_addr, e);
                                }
                        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(),
                        )
                        .await
                        {
                            Ok(_) => {
                                info!("[{}] * Beamed out successfully", remote_addr);
                                let msg = MessageS2C::Goodbye(MessageS2CGoodbye {
                                    reason: GoodbyeReason::Done.into(),
                                    special_fields: Default::default(),
                                })
                                .try_into()?;
                                send!(client_tx, msg).await?;
                                break;
                            }
                            Err(e) => {
                                error!("[{}] error beaming out: {}", remote_addr, e);
                            }
                        }
                    }
                }
                    MessageC2S::MouseInput(p) => {
                        debug!("[{}] player input: {:?}", remote_addr, p);
                    }
                },
            }
        }
    }

M server/src/macros.rs => server/src/macros.rs +39 -43
@@ 13,49 13,13 @@ pub fn _generic_pkt_into(p: Vec<u8>) -> Message {

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

#[macro_export]
macro_rules! recv_now {
    ($reader:expr) => {
        {
            if let Some(msg) = $reader.next().await {
    ($reader:expr) => {{
        if let Some(future_result) = $reader.next().now_or_never() {
            if let Some(msg) = future_result {
                match msg {
                    Ok(msg) => {
                        if msg.is_binary() {
                            match MessageC2S::try_from(&msg.into_data()) {
                            match MessageC2S::try_from(msg.into_data().as_slice()) {
                                Ok(d) => Ok(Some(d)),
                                Err(e) => {
                                    log::error!("error deserializing message: {}", e);


@@ 65,7 29,7 @@ macro_rules! recv_now {
                        } else {
                            Ok(None)
                        }
                    },
                    }
                    Err(e) => {
                        log::error!("error receiving message: {}", e);
                        Ok(None)


@@ 75,6 39,38 @@ macro_rules! recv_now {
                log::error!("pipe closed");
                Err("Pipe closed")
            }
        } else {
            Ok(None)
        }
    };
}
\ No newline at end of file
    }};
}

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

M server/src/main.rs => server/src/main.rs +92 -39
@@ 1,37 1,45 @@
use std::error::Error;
use std::net::SocketAddr;
use async_std::io::WriteExt;
use async_std::sync::Arc;
use async_std::net::{TcpListener, TcpStream};
use entity::{EntityHandler};
use manager::PhysicsData;
use nalgebra::vector;
use rapier2d_f64::prelude::{MultibodyJointSet, ImpulseJointSet, ColliderSet, RigidBodySet, NarrowPhase, BroadPhase, IslandManager, CCDSolver, IntegrationParameters};
use lazy_static::lazy_static;
use log::{error, info, Level, warn};
use serde::{Deserialize, Serialize};
use crate::entity::Entity;
use crate::handler::handle_client;
use crate::manager::{ClientHandler, ClientManager};
use crate::timer::timer_main;
use async_std::io::WriteExt;
use async_std::net::{TcpListener, TcpStream};
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};
use manager::PhysicsData;
use nalgebra::vector;
use rapier2d_f64::prelude::{
    BroadPhase, CCDSolver, ColliderSet, ImpulseJointSet, IntegrationParameters, IslandManager,
    MultibodyJointSet, NarrowPhase, RigidBodySet,
};
use serde::{Deserialize, Serialize};
use starkingdoms_protocol::PROTOCOL_VERSION;
use crate::handler::handle_client;
use std::error::Error;
use std::net::SocketAddr;

pub mod handler;
pub mod manager;
pub mod timer;
#[macro_use]
pub mod macros;
pub mod planet;
pub mod orbit;
pub mod entity;
pub mod api;
pub mod entity;
pub mod orbit;
pub mod planet;

const SCALE: f64 = 1.0;

async fn handle_request(conn: TcpStream, remote_addr: SocketAddr, mgr: ClientManager,
                        entities: Arc<RwLock<EntityHandler>>, physics_data: Arc<RwLock<PhysicsData>>) {
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) => {


@@ 40,8 48,13 @@ async fn handle_request(conn: TcpStream, remote_addr: SocketAddr, mgr: ClientMan
    }
}

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


@@ 56,20 69,29 @@ async fn _handle_request(mut conn: TcpStream, remote_addr: SocketAddr, mgr: Clie
        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
                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()
                build: env!("STK_BUILD").to_string(),
            },
            players: CMGR.usernames.read().await.len() as u32,
            description: env!("STK_SLP_DESCRIPTION").to_string(),
        }).unwrap();
        })
        .unwrap();

        let resp_str = format!("HTTP/1.0 200 OK\nAccess-Control-Allow-Origin: *\nContent-Length: {}\n\n{}", ping_resp.len(), ping_resp);
        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());
        info!(
            "[{}] sent ping response (200 OK {} bytes)",
            remote_addr,
            ping_resp.len()
        );
        return Ok(());
    }
    info!("[{}] incoming websocket connection", remote_addr);


@@ 91,16 113,28 @@ async fn _handle_request(mut conn: TcpStream, remote_addr: SocketAddr, mgr: Clie
    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 {
    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();
            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);
        }


@@ 125,8 159,14 @@ async fn _handle_request(mut conn: TcpStream, remote_addr: SocketAddr, mgr: Clie
            }
        };
        if let Entity::Player(player) = entities.read().await.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);
            rigid_body_set.remove(
                player.handle,
                &mut island_manager,
                &mut collider_set,
                &mut impulse_joint_set,
                &mut multibody_joint_set,
                true,
            );
        }
        data.rigid_body_set = rigid_body_set;
        data.collider_set = collider_set;


@@ 144,11 184,12 @@ lazy_static! {
        handlers: Arc::new(RwLock::new(Default::default())),
        usernames: Arc::new(RwLock::new(Default::default())),
    };
    static ref DATA: Arc<RwLock<PhysicsData>> = Arc::new(RwLock::new(PhysicsData { 
    static ref DATA: Arc<RwLock<PhysicsData>> = Arc::new(RwLock::new(PhysicsData {
        gravity: vector![0.0, 0.0],
        integration_parameters: IntegrationParameters {
            dt: 1.0 / 20.0,
            ..Default::default() },
            ..Default::default()
        },
        island_manager: IslandManager::new(),
        broad_phase: BroadPhase::new(),
        narrow_phase: NarrowPhase::new(),


@@ 156,7 197,8 @@ lazy_static! {
        collider_set: ColliderSet::new(),
        impulse_joint_set: ImpulseJointSet::new(),
        multibody_joint_set: MultibodyJointSet::new(),
        ccd_solver: CCDSolver::new(), }));
        ccd_solver: CCDSolver::new(),
    }));
    static ref ENTITIES: Arc<RwLock<EntityHandler>> = Arc::new(RwLock::new(EntityHandler::new()));
}



@@ 164,10 206,16 @@ lazy_static! {
async fn main() {
    simple_logger::init_with_level(Level::Debug).expect("Unable to start logging service");

    info!("StarKingdoms server (v: {}, build {}) - initializing", env!("STK_VERSION"), env!("STK_BUILD"));
    info!(
        "StarKingdoms server (v: {}, build {}) - initializing",
        env!("STK_VERSION"),
        env!("STK_BUILD")
    );

    if std::env::var("STK_API_KEY").is_err() {
        error!("Unable to read the API key from STK_API_KEY. Ensure it is set, and has a valid value.");
        error!(
            "Unable to read the API key from STK_API_KEY. Ensure it is set, and has a valid value."
        );
        std::process::exit(1);
    }
    if std::env::var("STK_API_URL").is_err() {


@@ 197,8 245,13 @@ async fn main() {
    };

    while let Ok((stream, peer_addr)) = listener.accept().await {
        async_std::task::spawn(handle_request(stream, peer_addr, CMGR.clone(),
                                ENTITIES.clone(), DATA.clone()));
        async_std::task::spawn(handle_request(
            stream,
            peer_addr,
            CMGR.clone(),
            ENTITIES.clone(),
            DATA.clone(),
        ));
    }
}



@@ 206,7 259,7 @@ async fn main() {
pub struct ServerPingResponse {
    pub version: ServerPingResponseVersion,
    pub players: u32,
    pub description: String
    pub description: String,
}

#[derive(Serialize, Deserialize)]


@@ 215,5 268,5 @@ pub struct ServerPingResponseVersion {
    pub number: String,
    pub protocol: u32,
    pub channel: String,
    pub build: String
    pub build: String,
}

M server/src/manager.rs => server/src/manager.rs +124 -82
@@ 1,17 1,21 @@
use std::collections::HashMap;
use std::f64::consts::PI;
use std::net::SocketAddr;
use std::sync::Arc;
use async_std::channel::Sender;
use async_std::sync::RwLock;
use nalgebra::point;
use rapier2d_f64::na::Vector2;
use rapier2d_f64::prelude::{IntegrationParameters, PhysicsPipeline, IslandManager, BroadPhase, NarrowPhase, ImpulseJointSet, MultibodyJointSet, CCDSolver, RigidBodySet, ColliderSet, RigidBodyHandle, ImpulseJointHandle, RigidBodyBuilder, ColliderBuilder, FixedJointBuilder, Real, MassProperties, Isometry, PrismaticJointBuilder};
use async_std::sync::RwLock;
use async_std::channel::Sender;
use rapier2d_f64::prelude::{
    BroadPhase, CCDSolver, ColliderBuilder, ColliderSet, FixedJointBuilder, ImpulseJointHandle,
    ImpulseJointSet, IntegrationParameters, IslandManager, Isometry, MassProperties,
    MultibodyJointSet, NarrowPhase, PhysicsPipeline, Real, RigidBodyBuilder, RigidBodyHandle,
    RigidBodySet,
};
use starkingdoms_protocol::api::APISavedPlayerData;
use starkingdoms_protocol::module::ModuleType;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;

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

#[derive(Clone)]
pub struct ClientManager {


@@ 34,16 38,14 @@ impl Player {
        APISavedPlayerData {}
    }

    pub fn load_api_data(&mut self, _data: &APISavedPlayerData) {

    }
    pub fn load_api_data(&mut self, _data: &APISavedPlayerData) {}
    pub fn search_modules(&self, entities: &EntityHandler) -> Vec<AttachedModule> {
        let mut modules = Vec::new();
        for child in &self.children {
            if let Some(attachment) = child {
                if let Entity::AttachedModule(child_module) = entities.entities.get(&attachment.child).unwrap() {
                    modules.append(&mut child_module.search_modules(entities));
                }
        for attachment in self.children.iter().flatten() {
            if let Entity::AttachedModule(child_module) =
                entities.entities.get(&attachment.child).unwrap()
            {
                modules.append(&mut child_module.search_modules(entities));
            }
        }
        modules


@@ 73,26 75,35 @@ pub struct AttachedModule {
    pub children: [Option<Attachment>; 4],
}
impl AttachedModule {
    pub fn attach(data: &mut PhysicsData, entities: &mut EntityHandler, parent: EntityId,
                  player_id: EntityId, module: Module, attachment_slot: usize) {
    pub fn attach(
        data: &mut PhysicsData,
        entities: &mut EntityHandler,
        parent: EntityId,
        player_id: EntityId,
        module: Module,
        attachment_slot: usize,
    ) {
        let mut entity_map = entities.entities.clone();

        let loose_id = entities.get_from_module(&module).expect("loose module does not exist");
        let loose_body = data.rigid_body_set.get(module.handle).expect("loose module does not exist");
        let mut parent_entity = entity_map.get_mut(&parent).expect("parent id does not exist");
        let loose_id = entities
            .get_from_module(&module)
            .expect("loose module does not exist");
        let loose_body = data
            .rigid_body_set
            .get(module.handle)
            .expect("loose module does not exist");
        let parent_entity = entity_map
            .get_mut(&parent)
            .expect("parent id does not exist");
        let parent_handle = match parent_entity {
            Entity::Player(player)  => {
                player.handle
            },
            Entity::AttachedModule(module)  => {
                module.handle
            },
            Entity::Player(player) => player.handle,
            Entity::AttachedModule(module) => module.handle,
            _ => {
                panic!("unexpected parent");
            }
        };
        let parent_body = data.rigid_body_set
            .get(parent_handle).unwrap();
        //let parent_body = data.rigid_body_set
        //    .get(parent_handle).unwrap();
        // create attachment module
        let module_collider = ColliderBuilder::cuboid(25.0 / SCALE, 25.0 / SCALE)
            .mass_properties(loose_body.mass_properties().local_mprops)


@@ 102,12 113,18 @@ impl AttachedModule {
            .rotation(loose_body.rotation().angle())
            .build();
        let attached_handle = data.rigid_body_set.insert(module_body);
        data.collider_set.insert_with_parent(module_collider, attached_handle, &mut data.rigid_body_set);
        data.collider_set.insert_with_parent(
            module_collider,
            attached_handle,
            &mut data.rigid_body_set,
        );
        let attach_joint = FixedJointBuilder::new()
            .local_anchor1(point![0.0, 0.0])
            .local_anchor2(point![0.0, 0.0])
            .build();
        let attach_joint_handle = data.impulse_joint_set.insert(parent_handle, attached_handle, attach_joint, true);
        let attach_joint_handle =
            data.impulse_joint_set
                .insert(parent_handle, attached_handle, attach_joint, true);
        let attached_module = AttachedModule {
            handle: attached_handle,
            module_type: module.module_type,


@@ 121,40 138,48 @@ impl AttachedModule {
                    child: attached_id,
                    connection: attach_joint_handle,
                });
            },
            Entity::AttachedModule(ref mut module)  => {
            }
            Entity::AttachedModule(ref mut module) => {
                module.children[attachment_slot] = Some(Attachment {
                    child: attached_id,
                    connection: attach_joint_handle,
                });
            },
            }
            _ => {
                panic!("unexpected parent");
            }
        };
        entity_map.insert(attached_id, Entity::AttachedModule(attached_module));
        // delete loose module
        data.rigid_body_set.remove(module.handle,
                                   &mut data.island_manager,
                                   &mut data.collider_set,
                                   &mut data.impulse_joint_set,
                                   &mut data.multibody_joint_set, true);
        data.rigid_body_set.remove(
            module.handle,
            &mut data.island_manager,
            &mut data.collider_set,
            &mut data.impulse_joint_set,
            &mut data.multibody_joint_set,
            true,
        );
        entities.entities.remove(&loose_id);
    }
    pub fn attach_new(data: &mut PhysicsData, entities: &mut EntityHandler, parent: EntityId,
                  player_id: EntityId, module: ModuleTemplate, attachment_slot: usize, rotation: f64) {
    pub fn attach_new(
        data: &mut PhysicsData,
        entities: &mut EntityHandler,
        parent: EntityId,
        player_id: EntityId,
        module: ModuleTemplate,
        attachment_slot: usize,
        rotation: f64,
    ) {
        let mut entity_map = entities.entities.clone();

        //let loose_id = entities.get_from_module(&module).expect("loose module does not exist");
        //let loose_body = data.rigid_body_set.get(module.handle).expect("loose module does not exist");
        let mut parent_entity = entity_map.get_mut(&parent).expect("parent id does not exist");
        let parent_entity = entity_map
            .get_mut(&parent)
            .expect("parent id does not exist");
        let parent_handle = match parent_entity {
            Entity::Player(player)  => {
                player.handle
            },
            Entity::AttachedModule(module)  => {
                module.handle
            },
            Entity::Player(player) => player.handle,
            Entity::AttachedModule(module) => module.handle,
            _ => {
                panic!("unexpected parent");
            }


@@ 168,17 193,23 @@ impl AttachedModule {
            .rotation(module.heading)
            .build();
        let attached_handle = data.rigid_body_set.insert(module_body);
        data.collider_set.insert_with_parent(module_collider, attached_handle, &mut data.rigid_body_set);
        data.collider_set.insert_with_parent(
            module_collider,
            attached_handle,
            &mut data.rigid_body_set,
        );
        let anchor = point![
            -0. / SCALE * rotation.cos() +100. / SCALE * rotation.sin(), 
            -0. / SCALE * rotation.sin() -100. / SCALE * rotation.cos()
            -0. / SCALE * rotation.cos() + 100. / SCALE * rotation.sin(),
            -0. / SCALE * rotation.sin() - 100. / SCALE * rotation.cos()
        ];
        let attach_joint = PrismaticJointBuilder::new(Vector2::x_axis())
            .local_anchor1(anchor)
            .local_anchor2(point![0.0, 0.0 / SCALE])
            //.local_frame2(Isometry::rotation(rotation))
            .build();
        let attach_joint_handle = data.impulse_joint_set.insert(parent_handle, attached_handle, attach_joint, true);
        let attach_joint_handle =
            data.impulse_joint_set
                .insert(parent_handle, attached_handle, attach_joint, true);
        let attached_module = AttachedModule {
            handle: attached_handle,
            module_type: module.module_type,


@@ 192,13 223,13 @@ impl AttachedModule {
                    child: attached_id,
                    connection: attach_joint_handle,
                });
            },
            Entity::AttachedModule(ref mut module)  => {
            }
            Entity::AttachedModule(ref mut module) => {
                module.children[attachment_slot] = Some(Attachment {
                    child: attached_id,
                    connection: attach_joint_handle,
                });
            },
            }
            _ => {
                panic!("unexpected parent");
            }


@@ 214,14 245,14 @@ impl AttachedModule {
            lifetime: 10.,
        }
    }

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


@@ 239,19 270,20 @@ pub struct PlayerInput {
    pub up: bool,
    pub left: bool,
    pub right: bool,
    pub down: bool
    pub down: bool,
}

#[derive(Clone)]
pub struct ClientHandler {
    pub tx: Sender<ClientHandlerMessage>
    pub tx: Sender<ClientHandlerMessage>,
}

#[derive(Clone, Default)]
pub struct PhysicsData {
    pub gravity: Vector2<f64>,
    pub integration_parameters: IntegrationParameters,
    pub island_manager: IslandManager, pub broad_phase: BroadPhase,
    pub island_manager: IslandManager,
    pub broad_phase: BroadPhase,
    pub narrow_phase: NarrowPhase,
    pub rigid_body_set: RigidBodySet,
    pub collider_set: ColliderSet,


@@ 261,28 293,38 @@ pub struct PhysicsData {
}
impl PhysicsData {
    pub fn tick(&mut self, pipeline: &mut PhysicsPipeline) {
        pipeline.step(&self.gravity,
                           &self.integration_parameters,
                           &mut self.island_manager,
                           &mut self.broad_phase,
                           &mut self.narrow_phase,
                           &mut self.rigid_body_set,
                           &mut self.collider_set,
                           &mut self.impulse_joint_set,
                           &mut self.multibody_joint_set,
                           &mut self.ccd_solver,
                           None,
                           &(),
                           &()
                        );
        pipeline.step(
            &self.gravity,
            &self.integration_parameters,
            &mut self.island_manager,
            &mut self.broad_phase,
            &mut self.narrow_phase,
            &mut self.rigid_body_set,
            &mut self.collider_set,
            &mut self.impulse_joint_set,
            &mut self.multibody_joint_set,
            &mut self.ccd_solver,
            None,
            &(),
            &(),
        );
    }
}

#[derive(Debug, Clone)]
pub enum ClientHandlerMessage {
    Tick,
    ChatMessage { from: String, message: String },
    PlayersUpdate { players: Vec<starkingdoms_protocol::player::Player> },
    PlanetData { planets: Vec<starkingdoms_protocol::planet::Planet> },
    ModulesUpdate { modules: Vec<starkingdoms_protocol::module::Module> },
    ChatMessage {
        from: String,
        message: String,
    },
    PlayersUpdate {
        players: Vec<starkingdoms_protocol::player::Player>,
    },
    PlanetData {
        planets: Vec<starkingdoms_protocol::planet::Planet>,
    },
    ModulesUpdate {
        modules: Vec<starkingdoms_protocol::module::Module>,
    },
}

M server/src/orbit/constants.rs => server/src/orbit/constants.rs +2 -2
@@ 22,6 22,6 @@ pub const MOON_RADIUS: f64 = MOON_RADIUS_RL * GAME_SCALE_DISTANCE * MOON_RADIUS_
pub const MOON_MASS_RL: f64 = 73476730900000000000000.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_APOAPSIS: f64 = 405400000.0 * GAME_SCALE_DISTANCE * MOON_APOAPSIS_BIAS;
pub const MOON_ORBIT_TIME_RL: f64 = 2332800.0;
pub const MOON_ORBIT_TIME: f64 = MOON_ORBIT_TIME_RL * GAME_SCALE_TIME * MOON_ORBIT_TIME_BIAS;
\ No newline at end of file
pub const MOON_ORBIT_TIME: f64 = MOON_ORBIT_TIME_RL * GAME_SCALE_TIME * MOON_ORBIT_TIME_BIAS;

M server/src/orbit/kepler.rs => server/src/orbit/kepler.rs +1 -1
@@ 4,4 4,4 @@
/// 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()
}
\ No newline at end of file
}

M server/src/orbit/mod.rs => server/src/orbit/mod.rs +3 -3
@@ 1,6 1,6 @@
pub mod constants;
pub mod kepler;
pub mod newtonian;
#[allow(clippy::module_inception)]
pub mod orbit;
pub mod newtonian;
pub mod kepler;
pub mod vis_viva;
\ No newline at end of file
pub mod vis_viva;

M server/src/orbit/newtonian.rs => server/src/orbit/newtonian.rs +8 -3
@@ 3,7 3,11 @@ use crate::orbit::kepler::kepler_equation;
pub const NEWTONIAN_STEP_SIZE: f64 = 0.0001;
pub const NEWTONIAN_ACCEPTABLE_ERROR: f64 = 0.00000001;

pub fn solve_kepler_with_newtonian(mean_anomaly: f64, eccentricity: f64, max_iterations: u64) -> f64 {
pub fn solve_kepler_with_newtonian(
    mean_anomaly: f64,
    eccentricity: f64,
    max_iterations: u64,
) -> f64 {
    let mut guess = mean_anomaly;

    for _ in 0..max_iterations {


@@ 15,10 19,11 @@ pub fn solve_kepler_with_newtonian(mean_anomaly: f64, eccentricity: f64, max_ite
        }

        // otherwise, update guess
        let slope = (kepler_equation(guess + NEWTONIAN_STEP_SIZE, mean_anomaly, eccentricity) - y) / NEWTONIAN_STEP_SIZE;
        let slope = (kepler_equation(guess + NEWTONIAN_STEP_SIZE, mean_anomaly, eccentricity) - y)
            / NEWTONIAN_STEP_SIZE;
        let step = y / slope;
        guess -= step;
    }

    guess
}
\ No newline at end of file
}

M server/src/orbit/orbit.rs => server/src/orbit/orbit.rs +23 -6
@@ 1,15 1,28 @@
// Mostly stolen from SebLague's plane game
// thanks

use nalgebra::{vector, Vector2};
use crate::orbit::newtonian::solve_kepler_with_newtonian;
use nalgebra::{vector, Vector2};

#[allow(clippy::too_many_arguments)]
pub fn calculate_vector_of_orbit(periapsis: f64, apoapsis: f64, t: f64, current_x: f64, current_y: f64, orbiting_x: f64, orbiting_y: f64, mass: f64, step: f64) -> Vector2<f64> {
pub fn calculate_vector_of_orbit(
    periapsis: f64,
    apoapsis: f64,
    t: f64,
    current_x: f64,
    current_y: f64,
    orbiting_x: f64,
    orbiting_y: f64,
    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

    let target = calculate_world_position_of_orbit(calculate_point_on_orbit(periapsis, apoapsis, t), vector![orbiting_x, orbiting_y]);
    let target = calculate_world_position_of_orbit(
        calculate_point_on_orbit(periapsis, apoapsis, t),
        vector![orbiting_x, orbiting_y],
    );
    let target_x = target[0];
    let target_y = target[1];



@@ 32,7 45,8 @@ 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 * 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);


@@ 44,8 58,11 @@ pub fn calculate_point_on_orbit(periapsis: f64, apoapsis: f64, t: f64) -> Vector
    vector![point_x, point_y]
}

pub fn calculate_world_position_of_orbit(point: Vector2<f64>, orbiting_on: Vector2<f64>) -> Vector2<f64> {
pub fn calculate_world_position_of_orbit(
    point: Vector2<f64>,
    orbiting_on: Vector2<f64>,
) -> Vector2<f64> {
    // i have no idea if this is actually right or not
    // we'll find out
    vector![point[0] + orbiting_on[0], point[1] + orbiting_on[1]]
}
\ No newline at end of file
}

M server/src/orbit/vis_viva.rs => server/src/orbit/vis_viva.rs +7 -2
@@ 1,3 1,8 @@
pub fn vis_viva(distance_between_centers: f64, semi_major: f64, g: f64, mass_of_bigger: f64) -> f64 {
pub fn vis_viva(
    distance_between_centers: f64,
    semi_major: f64,
    g: f64,
    mass_of_bigger: f64,
) -> f64 {
    (g * mass_of_bigger * (2.0 / distance_between_centers - 1.0 / semi_major)).sqrt()
}
\ No newline at end of file
}

M server/src/planet.rs => server/src/planet.rs +54 -34
@@ 1,12 1,16 @@
use std::collections::HashMap;
use nalgebra::{Vector2, vector};
use rapier2d_f64::prelude::{RigidBodyHandle, RigidBodySet, ColliderBuilder, RigidBodyBuilder, ColliderSet};
use nalgebra::{vector, Vector2};
use rapier2d_f64::prelude::{
    ColliderBuilder, ColliderSet, RigidBodyBuilder, RigidBodyHandle, RigidBodySet,
};
use starkingdoms_protocol::planet::PlanetType;
use std::collections::HashMap;

use crate::entity::{Entities, get_entity_id, Entity, EntityId};
use crate::{SCALE, manager::ClientHandlerMessage};
use crate::orbit::constants::{EARTH_MASS, EARTH_RADIUS, MOON_APOAPSIS, MOON_MASS, MOON_PERIAPSIS, MOON_RADIUS};
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::orbit::{calculate_point_on_orbit, calculate_world_position_of_orbit};
use crate::{manager::ClientHandlerMessage, SCALE};

//const GRAVITY: f64 = 0.001;
pub const GRAVITY: f64 = 12.6674;


@@ 17,14 21,17 @@ pub struct Planet {
    pub body_handle: RigidBodyHandle,
    pub position: (f64, f64),
    pub radius: f64,
    pub mass: f64
    pub mass: f64,
}

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).powi(2)
            + (position.1 - self.position.1).powi(2))
        .sqrt();
        let force = GRAVITY * ((self.mass * mass) / (distance * distance));
        let mut direction = Vector2::new(self.position.0 - position.0, self.position.1 - position.1);
        let mut direction =
            Vector2::new(self.position.0 - position.0, self.position.1 - position.1);
        direction.set_magnitude(force);
        (direction.x, direction.y)
    }


@@ 44,12 51,16 @@ impl Planets {
        self.planets.get_mut(planet_id)
    }

    pub async fn make_planet(_planet_id: &str,
                             planet_type: PlanetType, mass: f64, radius: f64,
                             position: (f64, f64), rigid_body_set: &mut RigidBodySet, collider_set: &mut ColliderSet
                            ) -> (EntityId, Entity) {
        let collider = ColliderBuilder::ball(radius / SCALE)
            .build();
    pub async fn make_planet(
        _planet_id: &str,
        planet_type: PlanetType,
        mass: f64,
        radius: f64,
        position: (f64, f64),
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
    ) -> (EntityId, Entity) {
        let collider = ColliderBuilder::ball(radius / SCALE).build();
        let body = RigidBodyBuilder::kinematic_position_based()
            .translation(vector![position.0 / SCALE, position.1 / SCALE])
            .dominance_group(127)


@@ 59,17 70,23 @@ impl Planets {
        collider_set.insert_with_parent(collider, body_handle, rigid_body_set);

        let entity_id = get_entity_id();
        (entity_id, Entity::Planet(Planet {
            planet_type,
            body_handle,
            position,
            radius,
            mass,
        }))
        (
            entity_id,
            Entity::Planet(Planet {
                planet_type,
                body_handle,
                position,
                radius,
                mass,
            }),
        )
    }

    pub async fn create_planets(rigid_body_set: &mut RigidBodySet, collider_set: &mut ColliderSet,
                                entities: &mut Entities) -> Vec<EntityId> {
    pub async 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(
            "earth",


@@ 79,13 96,17 @@ impl Planets {
            (100.0, 100.0),
            rigid_body_set,
            collider_set,
        ).await;
        )
        .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() {
            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]);
            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],
            );
        } else {
            moon_start_point = vector![0., 0.];
        }


@@ 97,8 118,9 @@ impl Planets {
            MOON_RADIUS,
            (moon_start_point[0], moon_start_point[1]),
            rigid_body_set,
            collider_set
        ).await;
            collider_set,
        )
        .await;
        entities.insert(moon_id, moon);
        planet_ids.push(moon_id);
        planet_ids


@@ 111,16 133,14 @@ impl Planets {
            // TODO: Adjust codegen to use f64
            planets.push(starkingdoms_protocol::planet::Planet {
                planet_type: planet.planet_type.into(),
                x: (planet.position.0 * SCALE) as f32,
                y: (planet.position.1 * SCALE) as f32,
                radius: planet.radius as f32, // DO NOT * SCALE
                x: planet.position.0 * SCALE,
                y: planet.position.1 * SCALE,
                radius: planet.radius, // DO NOT * SCALE - THIS VALUE IS NOT SCALED!
                special_fields: Default::default(),
            });
        }

        ClientHandlerMessage::PlanetData {
            planets
        }
        ClientHandlerMessage::PlanetData { planets }
    }

    pub fn gravity(&self, position: (f64, f64), mass: f64) -> (f64, f64) {

M server/src/timer.rs => server/src/timer.rs +134 -67
@@ 1,22 1,35 @@
use std::{time::Duration, sync::Arc, f64::consts::PI};
use log::{debug, warn, info};
use nalgebra::{vector, point};
use rand::Rng;
use rapier2d_f64::prelude::{PhysicsPipeline, ColliderBuilder, RigidBodyBuilder, MassProperties, RigidBodyHandle};
use async_std::sync::RwLock;
use async_std::task::sleep;
use starkingdoms_protocol::{player::Player, planet::PlanetType, module::ModuleType};
use crate::{manager::{ClientHandlerMessage, ClientManager, PhysicsData, Module}, SCALE, planet::{Planets, Planet}, entity::{get_entity_id, Entity}};
use crate::entity::EntityHandler;
use crate::orbit::constants::{GAME_ORBITS_ENABLED, MOON_APOAPSIS, MOON_ORBIT_TIME, MOON_PERIAPSIS};
use crate::orbit::constants::{
    GAME_ORBITS_ENABLED, 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, Module, PhysicsData},
    planet::{Planet, Planets},
    SCALE,
};
use async_std::sync::RwLock;
use async_std::task::sleep;
use log::{info, warn};
use nalgebra::{point, vector};
use rand::Rng;
use rapier2d_f64::prelude::{
    ColliderBuilder, MassProperties, PhysicsPipeline, RigidBodyBuilder, RigidBodyHandle,
};
use starkingdoms_protocol::{module::ModuleType, planet::PlanetType, player::Player};
use std::{f64::consts::PI, sync::Arc, time::Duration};

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

pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<PhysicsData>>, entities: Arc<RwLock<EntityHandler>>) {
pub async fn timer_main(
    mgr: ClientManager,
    physics_data_orig: Arc<RwLock<PhysicsData>>,
    entities: Arc<RwLock<EntityHandler>>,
) {
    let mut pipeline = PhysicsPipeline::new();

    let mut time = 0.0;


@@ 29,7 42,12 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
        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(&mut rigid_body_set, &mut collider_set, &mut entities.write().await.entities).await;
        _planet_ids = 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;


@@ 55,10 73,19 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic

            // 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();
            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);
            moon.position = (
                moon_body.translation()[0] / SCALE,
                moon_body.translation()[1] / SCALE,
            );
        }

        physics_data.tick(&mut pipeline);


@@ 66,7 93,8 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
        let mut protocol_players = vec![];

        {
            if module_timer > MODULE_SPAWN && entities.read().await.get_module_count() < MODULE_MAX {
            if module_timer > MODULE_SPAWN && entities.read().await.get_module_count() < MODULE_MAX
            {
                module_timer = 0.;

                let mut rigid_body_set = physics_data.rigid_body_set.clone();


@@ 81,10 109,17 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
                    rng.gen::<f64>() * PI * 2.
                };
                let module_body = RigidBodyBuilder::dynamic()
                    .translation(vector![angle.cos() * 2050. / SCALE, angle.sin() * 2050.0/SCALE])
                    .translation(vector![
                        angle.cos() * 2050. / SCALE,
                        angle.sin() * 2050.0 / SCALE
                    ])
                    .build();
                let module_handler = rigid_body_set.insert(module_body);
                collider_set.insert_with_parent(module_collider, module_handler, &mut rigid_body_set);
                collider_set.insert_with_parent(
                    module_collider,
                    module_handler,
                    &mut rigid_body_set,
                );

                physics_data.rigid_body_set = rigid_body_set;
                physics_data.collider_set = collider_set;


@@ 94,19 129,26 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
                    module_type: ModuleType::Cargo,
                    lifetime: 0.0,
                };
                entities.write().await.entities.insert(get_entity_id(), Entity::Module(module));
                entities
                    .write()
                    .await
                    .entities
                    .insert(get_entity_id(), Entity::Module(module));
            }
            let mut entities = entities.write().await;
            for mut module in entities.get_modules().iter_mut() {
            for module in entities.get_modules().iter_mut() {
                let module_handle = module.handle;
                let module_body = physics_data.rigid_body_set.get_mut(module_handle).unwrap();
                module_body.reset_forces(true);
                module_body.reset_torques(true);
                let grav_force = entities.gravity((module_body.translation().x, module_body.translation().y), module_body.mass());
                //module_body.apply_impulse(vector![grav_force.0, grav_force.1], true);
                let grav_force = entities.gravity(
                    (module_body.translation().x, module_body.translation().y),
                    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() {
                    p_module.lifetime += 5./1000.;
                    p_module.lifetime += 5. / 1000.;
                }
                if module.lifetime > 80. {
                    let mut rigid_body_set = physics_data.rigid_body_set.clone();


@@ 114,8 156,14 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
                    let mut collider_set = physics_data.collider_set.clone();
                    let mut impulse_joint_set = physics_data.impulse_joint_set.clone();
                    let mut multibody_joint_set = physics_data.multibody_joint_set.clone();
                    rigid_body_set.remove(module.handle, &mut island_manager, &mut collider_set,
                                            &mut impulse_joint_set, &mut multibody_joint_set, true);
                    rigid_body_set.remove(
                        module.handle,
                        &mut island_manager,
                        &mut collider_set,
                        &mut impulse_joint_set,
                        &mut multibody_joint_set,
                        true,
                    );
                    physics_data.rigid_body_set = rigid_body_set;
                    physics_data.collider_set = collider_set;
                    physics_data.island_manager = island_manager;


@@ 133,8 181,11 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
                player_body.reset_forces(true);
                player_body.reset_torques(true);
                let planets = entities.read().await;
                let grav_force = planets.gravity((player_body.translation().x, player_body.translation().y), player_body.mass());
                //player_body.apply_impulse(vector![grav_force.0, grav_force.1], true);
                let grav_force = planets.gravity(
                    (player_body.translation().x, player_body.translation().y),
                    player_body.mass(),
                );
                player_body.apply_impulse(vector![grav_force.0, grav_force.1], true);

                let mut left_top_thruster: f64 = 0.0;
                let mut right_top_thruster: f64 = 0.0;


@@ 182,35 233,26 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
                ];
                let scale = SCALE;
                let top_left_point = point![
                    -25. / scale * rotation.cos() +25. / scale * rotation.sin(), 
                    -25. / scale * rotation.sin() -25. / scale * rotation.cos()
                    -25. / scale * rotation.cos() + 25. / scale * rotation.sin(),
                    -25. / scale * 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 * rotation.cos() + 25. / scale * rotation.sin(),
                    25. / scale * 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 * rotation.cos() - 25. / scale * rotation.sin(),
                    -25. / scale * 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 * rotation.cos() - 25. / scale * rotation.sin(),
                    25. / scale * rotation.sin() + 25. / scale * rotation.cos()
                ] + player_body.translation();

                player_body.add_force_at_point(
                    left_top_thruster,
                    top_left_point, true);
                player_body.add_force_at_point(
                    right_top_thruster,
                    top_right_point, true);
                player_body.add_force_at_point(
                    left_bottom_thruster,
                    bottom_left_point, true);
                player_body.add_force_at_point(
                    right_bottom_thruster,
                    bottom_right_point, true);

                player_body.add_force_at_point(left_top_thruster, top_left_point, true);
                player_body.add_force_at_point(right_top_thruster, top_right_point, true);
                player_body.add_force_at_point(left_bottom_thruster, bottom_left_point, true);
                player_body.add_force_at_point(right_bottom_thruster, bottom_right_point, true);

                let translation = player_body.translation();



@@ 222,9 264,9 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic

                // TODO: Figure out how to adjust codegen to use f64
                protocol_players.push(Player {
                    rotation: rotation as f32,
                    x: (translation.x * SCALE) as f32,
                    y: (translation.y * SCALE) as f32,
                    rotation,
                    x: (translation.x * SCALE),
                    y: (translation.y * SCALE),
                    username,
                    special_fields: Default::default(),
                });


@@ 239,7 281,13 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
        for (addr, client_thread) in mgr_r.iter() {
            match client_thread.tx.send(ClientHandlerMessage::Tick).await {
                Ok(_) => {
                    match client_thread.tx.send(ClientHandlerMessage::PlayersUpdate {players: protocol_players.clone()}).await {
                    match client_thread
                        .tx
                        .send(ClientHandlerMessage::PlayersUpdate {
                            players: protocol_players.clone(),
                        })
                        .await
                    {
                        Ok(_) => (),
                        Err(e) => {
                            warn!("unable to send position packet: {}", e);


@@ 247,31 295,50 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
                    };
                    let mut modules = entities.read().await.get_modules();
                    let attached_modules = entities.read().await.get_all_attached();
                    let attached_handles: Vec<RigidBodyHandle> = attached_modules.iter().map(|m| {
                        m.handle
                    }).collect();
                    modules.append(&mut attached_modules.iter().map(|m| {
                        let module = m.to_module();
                        info!("{:?}", module);
                        return module;
                    }).collect());
                    let attached_handles: Vec<RigidBodyHandle> =
                        attached_modules.iter().map(|m| m.handle).collect();
                    modules.append(
                        &mut attached_modules
                            .iter()
                            .map(|m| {
                                let module = m.to_module();
                                info!("{:?}", module);
                                module
                            })
                            .collect(),
                    );
                    modules.iter().for_each(|module| {
                        if attached_handles.contains(&module.handle) {
                            info!("{:?}", physics_data.rigid_body_set.get(module.handle).unwrap().translation());
                            info!(
                                "{:?}",
                                physics_data
                                    .rigid_body_set
                                    .get(module.handle)
                                    .unwrap()
                                    .translation()
                            );
                        }
                    });
                    let protocol_modules: Vec<starkingdoms_protocol::module::Module> = modules.iter()
                    let protocol_modules: Vec<starkingdoms_protocol::module::Module> = modules
                        .iter()
                        .map(|module| {
                            let body = physics_data.rigid_body_set.get(module.handle).unwrap();
                            return starkingdoms_protocol::module::Module {
                                module_type: module.module_type.into(),
                                rotation: body.rotation().angle() as f32,
                                x: (body.translation().x * SCALE) as f32,
                                y: (body.translation().y * SCALE) as f32,
                                rotation: body.rotation().angle(),
                                x: body.translation().x * SCALE,
                                y: body.translation().y * SCALE,
                                special_fields: Default::default(),
                            };
                        }).collect();
                    match client_thread.tx.send(ClientHandlerMessage::ModulesUpdate { modules: protocol_modules.clone() }).await {
                        })
                        .collect();
                    match client_thread
                        .tx
                        .send(ClientHandlerMessage::ModulesUpdate {
                            modules: protocol_modules.clone(),
                        })
                        .await
                    {
                        Ok(_) => (),
                        Err(e) => {
                            warn!("unable to send module position packet: {}", e);

M spacetime => spacetime +2 -271
@@ 4,276 4,7 @@ set -e

SCRIPT_PATH=$(readlink -f "${BASH_SOURCE:-$0}")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
SERVER_MODS=${STK_SERVER_MODULES:=""}

exec_spacetime() {
  # args: target, environment, build root, modules
  echo "[*] Running configure command: 'python3 $SCRIPT_DIR/spacetime_py/spacetime.py $1 $2 $SCRIPT_DIR $SPACETIME_VERBOSE'"
  python3 "$SCRIPT_DIR/spacetime_py/spacetime.py" "$1" "$2" "$SCRIPT_DIR" "$SPACETIME_VERBOSE" "$4"
}
echo "[*] Running configure command 'spacetime $* $SCRIPT_DIR'"

exec_ninja() {
  # args: target
  echo "[*] Running build command: 'ninja -C $SCRIPT_DIR $1'"
  ninja -C "$SCRIPT_DIR" "$1"
}

sub_help() {
  echo "Spacetime - StarKingdoms build utility"
  echo "Spacetime is a small utility program to generate Ninja build manifests for compiling StarKingdoms."
  echo "Available targets:"
  echo "    help - Show this help screen" # done
  echo "    run_http - Compile the client and run a development http server for testing it" # done
  echo "    run_server (default) - Compile and run the game server" # done
  echo "    build_server - Compile the game server" # done
  echo "    run_server_prod - Compile and run the game server with optimizations enabled" # done
  echo "    build_server_prod - Compile the game server with optimizations enabled" # done
  echo "    run_api - Compile and run the API server" # done
  echo "    build_api - Compile the API server" # done
  echo "    run_api_prod - Compile and run the API server with optimizations enabled" # done
  echo "    build_api_prod - Compile the API server with optimizations enabled" # done
  echo "    install_tooling - Install the compilation utilities required for compiling StarKingdoms" # done
  echo "    build_assets - Compile spritesheets in all three texture sizes for textures-fast" # done
  echo "    build_assets_full - Compile spritesheets in full size for textures-fast" # done
  echo "    build_assets_375 - Commpile 37.5% spritesheets for textures-fast" # done
  echo "    build_assets_125 - Compile 12.5% spritesheets for textures-fast" # done
  echo "    clean - Remove all generated files" # done
  echo "    build_docker_api - Build the API dockerfile" # done
  echo "    build_docker_server - Build the server dockerfile" # done
  echo "    build_docker_web - Build the web dockerfile" # done
  echo "    build_docker - Build the API, web and server containers" # done
  echo "    build_docker_api_stable - Build the API container and push it as api-stable" # done
  echo "    build_docker_server_stable - Build the server container and push it as server-stable" # done
  echo "    build_docker_web_stable - Build the web dockerfile and push it as web-stable" # done
  echo "    build_docker_stable - Build the stable api, web and server containers" # done
  echo "    infra [action] - Run an infrastructure command. Requires an infrastructure key" # done
}

check_install_cargo() {
  echo "[*] Checking for $1"
  if ! command -v "$1" &> /dev/null
  then
    echo "[+] $1 was not found, installing via Cargo..."
    cargo install "$2" $3
  fi
}

check() {
  echo "[*] Checking for $1"
  if ! command -v "$1" &> /dev/null
  then
    echo "[x] $1 was not found but is required for the build process to continue. Install it with your system package manager, or, if supported, 'st install_tooling'"
    exit 1
  fi
}

check_all() {
  check inkscape
  check protoc
  check atlasify
}

sub_clean() {
  rm -rf web/dist
  rm -rf assets/dist
  rm -rf assets/final
  rm -rf target
  rm -rf client/pkg
}

sub_install_tooling() {
  check inkscape
  check protoc
  check atlasify
  echo "[*] All required tools are installed"
}

sub_run_http() {
  check_all
  exec_spacetime client prod "$SCRIPT_DIR" "$CLIENT_MODS"
  exec_ninja asset
  cd client && yarn && yarn run dev
}

sub_build_server() {
  check_all
  exec_spacetime server dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
}
sub_run_server() {
  check_all
  exec_spacetime server dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
  exec "$SCRIPT_DIR/target/debug/starkingdoms-server"
}

sub_build_server_prod() {
  check_all
  exec_spacetime server prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
}
sub_run_server_prod() {
  check_all
  exec_spacetime server prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
  exec "$SCRIPT_DIR/target/release/starkingdoms-server"
}

sub_build_api() {
  check_all
  exec_spacetime api dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
}
sub_run_api() {
  check_all
  exec_spacetime api dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
  cd api && exec "$SCRIPT_DIR/target/debug/starkingdoms-api"
}

sub_build_api_prod() {
  check_all
  exec_spacetime api prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
}
sub_run_api_prod() {
  check_all
  exec_spacetime api prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
  cd api && exec "$SCRIPT_DIR/target/release/starkingdoms-api"
}

sub_build_assets() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset
}

sub_build_assets_full() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset-full
}

sub_build_assets_375() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset-375
}

sub_build_assets_125() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset-125
}


build_docker() {
  docker buildx build -f "$SCRIPT_DIR/$1".Dockerfile -t registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-$(git rev-parse --short HEAD) "$SCRIPT_DIR"
  docker buildx build -f "$SCRIPT_DIR/$1".Dockerfile -t registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-"$2" "$SCRIPT_DIR"
  docker push registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-$(git rev-parse --short HEAD)
  docker push registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-"$2"
}

swap_out_server_for() {
  echo "[*] Swapping out API server"
  sed -i'orig' "s/let api_server = \"http:\\/\\/localhost:8080\";/let api_server = \"https:\\/\\/api.${1}.${2}\";/" "$SCRIPT_DIR/client/index.html"
  echo "[*] Swapping out game server"
  sed -i "s/let servers = \[\"localhost:3000\"\];/let servers = [\"${1}.${2}\"];/" "$SCRIPT_DIR/client/index.html"
}

sub_swap_server() {
  swap_out_server_for "$1" "$2"
}
sub_reset_server() {
  mv client/index.htmlorig client/index.html
}

sub_build_docker_api() {
  sub_build_api_prod
  build_docker "api" "bleeding"
}

sub_build_docker_server() {
  sub_build_server_prod
  build_docker "server" "bleeding"
}

sub_build_docker_web() {
  swap_out_server_for "bleeding" "starkingdoms.io"
  build_docker "web" "bleeding"
  mv "$SCRIPT_DIR/client/index.htmlorig" "$SCRIPT_DIR/client/index.html"
}

sub_build_docker_web_stable() {
  swap_out_server_for "starkingdoms" "io"
  build_docker "web" "stable"
  mv "$SCRIPT_DIR/client/index.htmlorig" "$SCRIPT_DIR/client/index.html"
}

sub_build_docker_api_stable() {
  sub_build_api_prod
  build_docker "api" "stable"
}

sub_build_docker_server_stable() {
  sub_build_server_prod
  build_docker "server" "stable"
}


sub_build_docker_api_beta() {
  sub_build_api_prod
  build_docker "api" "beta"
}

sub_build_docker_server_beta() {
  sub_build_server_prod
  build_docker "server" "beta"
}

sub_build_docker_web_beta() {
  swap_out_server_for "beta" "starkingdoms.io"
  build_docker "web" "beta"
  mv "$SCRIPT_DIR/client/index.htmlorig" "$SCRIPT_DIR/client/index.html"
}

sub_build_docker() {
  sub_build_docker_api
  sub_build_docker_server
  sub_build_docker_web
}

sub_build_docker_beta() {
  sub_build_docker_api_beta
  sub_build_docker_server_beta
  sub_build_docker_web_beta
}

sub_build_docker_stable() {
  sub_build_docker_api_stable
  sub_build_docker_server_stable
  sub_build_docker_web_stable
}

sub_infra() {
  echo "[*] Connecting to infrastructure manager server. If you are prompted for a password, enter your infrastructure key. You may be prompted several times."
  ssh team@10.16.1.3 /home/team/run_ansible.sh "$1"
}

subcommand=$1
case $subcommand in
    "" | "-h" | "--help" | "help")
        sub_help
        ;;
    *)
        echo "[*] Running build command $subcommand"
        shift
        sub_${subcommand} $@
        if [ $? = 127 ]; then
            echo "Error: '$subcommand' is not a known subcommand." >&2
            echo "       Run 'st --help' for a list of known subcommands." >&2
            exit 1
        fi
        ;;
esac
\ No newline at end of file
cd "$SCRIPT_DIR" && cargo run --release --bin spacetime -- "$@" "$SCRIPT_DIR"
\ No newline at end of file

A spacetime_old => spacetime_old +284 -0
@@ 0,0 1,284 @@
#!/bin/bash

set -e

SCRIPT_PATH=$(readlink -f "${BASH_SOURCE:-$0}")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
SERVER_MODS=${STK_SERVER_MODULES:=""}

exec_spacetime() {
  # args: target, environment, build root, modules
  echo "[*] Running configure command: 'python3 $SCRIPT_DIR/spacetime_py/spacetime.py $1 $2 $SCRIPT_DIR $SPACETIME_VERBOSE'"
  python3 "$SCRIPT_DIR/spacetime_py/spacetime.py" "$1" "$2" "$SCRIPT_DIR" "$SPACETIME_VERBOSE" "$4"
}

exec_ninja() {
  # args: target
  echo "[*] Running build command: 'ninja -C $SCRIPT_DIR $1'"
  ninja -C "$SCRIPT_DIR" "$1"
}

sub_help() {
  echo "Spacetime - StarKingdoms build utility"
  echo "Spacetime is a small utility program to generate Ninja build manifests for compiling StarKingdoms."
  echo "Available targets:"
  echo "    help - Show this help screen" # done
  echo "    run_http - Compile the client and run a development http server for testing it" # done
  echo "    run_server (default) - Compile and run the game server" # done
  echo "    build_server - Compile the game server" # done
  echo "    run_server_prod - Compile and run the game server with optimizations enabled" # done
  echo "    build_server_prod - Compile the game server with optimizations enabled" # done
  echo "    run_api - Compile and run the API server" # done
  echo "    build_api - Compile the API server" # done
  echo "    run_api_prod - Compile and run the API server with optimizations enabled" # done
  echo "    build_api_prod - Compile the API server with optimizations enabled" # done
  echo "    install_tooling - Install the compilation utilities required for compiling StarKingdoms" # done
  echo "    build_assets - Compile spritesheets in all three texture sizes for textures-fast" # done
  echo "    build_assets_full - Compile spritesheets in full size for textures-fast" # done
  echo "    build_assets_375 - Commpile 37.5% spritesheets for textures-fast" # done
  echo "    build_assets_125 - Compile 12.5% spritesheets for textures-fast" # done
  echo "    clean - Remove all generated files" # done
  echo "    build_docker_api - Build the API dockerfile" # done
  echo "    build_docker_server - Build the server dockerfile" # done
  echo "    build_docker_web - Build the web dockerfile" # done
  echo "    build_docker - Build the API, web and server containers" # done
  echo "    build_docker_api_stable - Build the API container and push it as api-stable" # done
  echo "    build_docker_server_stable - Build the server container and push it as server-stable" # done
  echo "    build_docker_web_stable - Build the web dockerfile and push it as web-stable" # done
  echo "    build_docker_stable - Build the stable api, web and server containers" # done
  echo "    infra [action] - Run an infrastructure command. Requires an infrastructure key" # done
  echo "    client_protobuf - Rebuild the client protocol bindings" # done
}

check_install_cargo() {
  echo "[*] Checking for $1"
  if ! command -v "$1" &> /dev/null
  then
    echo "[+] $1 was not found, installing via Cargo..."
    cargo install "$2" $3
  fi
}

check() {
  echo "[*] Checking for $1"
  if ! command -v "$1" &> /dev/null
  then
    echo "[x] $1 was not found but is required for the build process to continue. Install it with your system package manager, or, if supported, 'st install_tooling'"
    exit 1
  fi
}

check_all() {
  check inkscape
  check protoc
  check atlasify
}

sub_clean() {
  rm -rf web/dist
  rm -rf assets/dist
  rm -rf assets/final
  rm -rf target
  rm -rf client/pkg
}

sub_install_tooling() {
  check inkscape
  check protoc
  check atlasify
  echo "[*] All required tools are installed"
}

sub_run_http() {
  check_all
  exec_spacetime client prod "$SCRIPT_DIR" "$CLIENT_MODS"
  exec_ninja asset
  cd client && yarn && yarn run dev
}

sub_client_protobuf() {
  cd client && yarn && yarn protobuf
}

sub_build_server() {
  check_all
  exec_spacetime server dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
}
sub_run_server() {
  check_all
  exec_spacetime server dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
  exec "$SCRIPT_DIR/target/debug/starkingdoms-server"
}

sub_build_server_prod() {
  check_all
  exec_spacetime server prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
}
sub_run_server_prod() {
  check_all
  exec_spacetime server prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja server
  exec "$SCRIPT_DIR/target/release/starkingdoms-server"
}

sub_build_api() {
  check_all
  exec_spacetime api dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
}
sub_run_api() {
  check_all
  exec_spacetime api dev "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
  cd api && exec "$SCRIPT_DIR/target/debug/starkingdoms-api"
}

sub_build_api_prod() {
  check_all
  exec_spacetime api prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
}
sub_run_api_prod() {
  check_all
  exec_spacetime api prod "$SCRIPT_DIR" "$SERVER_MODS"
  exec_ninja api
  cd api && exec "$SCRIPT_DIR/target/release/starkingdoms-api"
}

sub_build_assets() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset
}

sub_build_assets_full() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset-full
}

sub_build_assets_375() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset-375
}

sub_build_assets_125() {
  check_all
  exec_spacetime asset dev "$SCRIPT_DIR"
  exec_ninja asset-125
}


build_docker() {
  docker buildx build -f "$SCRIPT_DIR/$1".Dockerfile -t registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-$(git rev-parse --short HEAD) "$SCRIPT_DIR"
  docker buildx build -f "$SCRIPT_DIR/$1".Dockerfile -t registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-"$2" "$SCRIPT_DIR"
  docker push registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-$(git rev-parse --short HEAD)
  docker push registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:"$1"-"$2"
}

swap_out_server_for() {
  echo "[*] Swapping out API server"
  sed -i'orig' "s/let api_server = \"http:\\/\\/localhost:8080\";/let api_server = \"https:\\/\\/api.${1}.${2}\";/" "$SCRIPT_DIR/client/index.html"
  echo "[*] Swapping out game server"
  sed -i "s/let servers = \[\"localhost:3000\"\];/let servers = [\"${1}.${2}\"];/" "$SCRIPT_DIR/client/index.html"
}

sub_swap_server() {
  swap_out_server_for "$1" "$2"
}
sub_reset_server() {
  mv client/index.htmlorig client/index.html
}

sub_build_docker_api() {
  sub_build_api_prod
  build_docker "api" "bleeding"
}

sub_build_docker_server() {
  sub_build_server_prod
  build_docker "server" "bleeding"
}

sub_build_docker_web() {
  swap_out_server_for "bleeding" "starkingdoms.io"
  build_docker "web" "bleeding"
  mv "$SCRIPT_DIR/client/index.htmlorig" "$SCRIPT_DIR/client/index.html"
}

sub_build_docker_web_stable() {
  swap_out_server_for "starkingdoms" "io"
  build_docker "web" "stable"
  mv "$SCRIPT_DIR/client/index.htmlorig" "$SCRIPT_DIR/client/index.html"
}

sub_build_docker_api_stable() {
  sub_build_api_prod
  build_docker "api" "stable"
}

sub_build_docker_server_stable() {
  sub_build_server_prod
  build_docker "server" "stable"
}


sub_build_docker_api_beta() {
  sub_build_api_prod
  build_docker "api" "beta"
}

sub_build_docker_server_beta() {
  sub_build_server_prod
  build_docker "server" "beta"
}

sub_build_docker_web_beta() {
  swap_out_server_for "beta" "starkingdoms.io"
  build_docker "web" "beta"
  mv "$SCRIPT_DIR/client/index.htmlorig" "$SCRIPT_DIR/client/index.html"
}

sub_build_docker() {
  sub_build_docker_api
  sub_build_docker_server
  sub_build_docker_web
}

sub_build_docker_beta() {
  sub_build_docker_api_beta
  sub_build_docker_server_beta
  sub_build_docker_web_beta
}

sub_build_docker_stable() {
  sub_build_docker_api_stable
  sub_build_docker_server_stable
  sub_build_docker_web_stable
}

sub_infra() {
  echo "[*] Connecting to infrastructure manager server. If you are prompted for a password, enter your infrastructure key. You may be prompted several times."
  ssh team@10.16.1.3 /home/team/run_ansible.sh "$1"
}

subcommand=$1
case $subcommand in
    "" | "-h" | "--help" | "help")
        sub_help
        ;;
    *)
        echo "[*] Running build command $subcommand"
        shift
        sub_${subcommand} $@
        if [ $? = 127 ]; then
            echo "Error: '$subcommand' is not a known subcommand." >&2
            echo "       Run 'st --help' for a list of known subcommands." >&2
            exit 1
        fi
        ;;
esac
\ No newline at end of file

A spacetime_rs/Cargo.toml => spacetime_rs/Cargo.toml +10 -0
@@ 0,0 1,10 @@
[package]
name = "spacetime"
version = "0.1.0"
edition = "2021"

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

[dependencies]
tabwriter = "1.2.1"
which = "4.4.0"
\ No newline at end of file

A spacetime_rs/src/cmd.rs => spacetime_rs/src/cmd.rs +20 -0
@@ 0,0 1,20 @@
use which::which;

pub fn enforce_commands() {
    println!("[spacetime] checking for required tooling");
    _enforce_command("cargo");
    _enforce_command("ninja");
    _enforce_command("yarn");
    _enforce_command("inkscape");
    _enforce_command("atlasify");
    println!("[spacetime] all required tools present");
}

fn _enforce_command(cmd: &str) {
    if which(cmd).is_err() {
        eprintln!(
            "[!] Unable to find required binary {}. Please install it to continue.",
            cmd
        );
    }
}

A spacetime_rs/src/commands/api.rs => spacetime_rs/src/commands/api.rs +59 -0
@@ 0,0 1,59 @@
use crate::configure::create_writer;
use crate::configure::rust::configure_rust_target;
use crate::ninja::{exec, exec_ninja};
use std::error::Error;
use std::path::PathBuf;

pub fn build_api(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("api", "dev", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["api".to_string()])?;

    Ok(())
}

pub fn build_api_prod(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("api", "prod", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["api".to_string()])?;

    Ok(())
}

pub fn run_api(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("api", "dev", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["api".to_string()])?;

    exec(
        root.join("target/debug/starkingdoms-api").to_str().unwrap(),
        &root,
        args,
    )?;

    Ok(())
}

pub fn run_api_prod(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("api", "prod", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["api".to_string()])?;

    exec(
        root.join("target/release/starkingdoms-api")
            .to_str()
            .unwrap(),
        &root,
        args,
    )?;

    Ok(())
}

A spacetime_rs/src/commands/assets.rs => spacetime_rs/src/commands/assets.rs +26 -0
@@ 0,0 1,26 @@
use crate::configure::asset::configure_assets;
use crate::configure::create_writer;
use crate::ninja::exec_ninja;
use std::error::Error;
use std::path::PathBuf;
use std::time::SystemTime;

pub fn build_assets(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    let start = SystemTime::now();

    configure_assets(&mut config_file_writer, &root)?;

    let end = SystemTime::now();
    let duration = end.duration_since(start).unwrap();

    println!(
        "[spacetime] configure completed in {} seconds",
        duration.as_secs_f32()
    );

    exec_ninja(&root, vec!["asset".to_string()])?;

    Ok(())
}

A spacetime_rs/src/commands/clean.rs => spacetime_rs/src/commands/clean.rs +11 -0
@@ 0,0 1,11 @@
use crate::ninja::exec;
use std::error::Error;
use std::fs;
use std::path::PathBuf;

pub fn clean(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    exec("cargo", &root, vec!["clean".to_string()])?;
    fs::remove_dir_all(root.join("assets").join("dist"))?;
    fs::remove_dir_all(root.join("assets").join("final"))?;
    Ok(())
}

A spacetime_rs/src/commands/client.rs => spacetime_rs/src/commands/client.rs +32 -0
@@ 0,0 1,32 @@
use crate::configure::client::configure_client;
use crate::configure::create_writer;
use crate::ninja::{exec, exec_ninja};
use std::error::Error;
use std::path::PathBuf;

pub fn run_http(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_client(&mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["asset".to_string()])?;

    exec("yarn", &root.join("client"), vec![])?;
    exec(
        "yarn",
        &root.join("client"),
        vec!["run".to_string(), "dev".to_string()],
    )?;

    Ok(())
}

pub fn client_protobuf(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    exec("yarn", &root.join("client"), vec![])?;
    exec("yarn", &root.join("client"), vec!["protobuf".to_string()])
}

pub fn build_client_prod(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    exec("yarn", &root.join("client"), vec![])?;
    exec("yarn", &root.join("client"), vec!["build".to_string()])
}

A spacetime_rs/src/commands/docker.rs => spacetime_rs/src/commands/docker.rs +143 -0
@@ 0,0 1,143 @@
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 std::error::Error;
use std::path::PathBuf;
use std::process::Command;

fn _build(img: &str, channel: &str, root: &PathBuf) -> Result<(), Box<dyn Error>> {
    // compile the various thingies
    if img == "server" {
        build_server_prod(vec![], root.clone())?;
    } else if img == "api" {
        build_api_prod(vec![], root.clone())?;
    } else {
        build_client_prod(vec![], root.clone())?
    }

    let git_commit_id = String::from_utf8(
        Command::new("git")
            .args(["rev-parse", "--short", "HEAD"])
            .current_dir(root)
            .output()
            .unwrap()
            .stdout,
    )
    .unwrap()
    .replace('\n', "");
    exec(
        "docker",
        root,
        vec![
            "buildx",
            "build",
            "-f",
            root.join(format!("{}.Dockerfile", img)).to_str().unwrap(),
            "-t",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, git_commit_id
            ),
            root.to_str().unwrap(),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;
    exec(
        "docker",
        root,
        vec![
            "buildx",
            "build",
            "-f",
            root.join(format!("{}.Dockerfile", img)).to_str().unwrap(),
            "-t",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, channel
            ),
            root.to_str().unwrap(),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;

    exec(
        "docker",
        root,
        vec![
            "push",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, git_commit_id
            ),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;
    exec(
        "docker",
        root,
        vec![
            "push",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, channel
            ),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;

    Ok(())
}

pub fn build_docker_api(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("api", "bleeding", &root)
}
pub fn build_docker_server(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("server", "bleeding", &root)
}
pub fn build_docker_web(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("web", "bleeding", &root)
}
pub fn build_docker(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    build_docker_api(_a.clone(), root.clone())?;
    build_docker_server(_a.clone(), root.clone())?;
    build_docker_web(_a, root)
}

pub fn build_docker_api_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("api", "beta", &root)
}
pub fn build_docker_server_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("server", "beta", &root)
}
pub fn build_docker_web_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("web", "beta", &root)
}
pub fn build_docker_beta(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    build_docker_api_beta(_a.clone(), root.clone())?;
    build_docker_server_beta(_a.clone(), root.clone())?;
    build_docker_web_beta(_a, root)
}

pub fn build_docker_api_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("api", "stable", &root)
}
pub fn build_docker_server_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("server", "stable", &root)
}
pub fn build_docker_web_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("web", "stable", &root)
}
pub fn build_docker_stable(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    build_docker_api_stable(_a.clone(), root.clone())?;
    build_docker_server_stable(_a.clone(), root.clone())?;
    build_docker_web_stable(_a, root)
}

A spacetime_rs/src/commands/mod.rs => spacetime_rs/src/commands/mod.rs +6 -0
@@ 0,0 1,6 @@
pub mod api;
pub mod assets;
pub mod clean;
pub mod client;
pub mod docker;
pub mod server;

A spacetime_rs/src/commands/server.rs => spacetime_rs/src/commands/server.rs +61 -0
@@ 0,0 1,61 @@
use crate::configure::create_writer;
use crate::configure::rust::configure_rust_target;
use crate::ninja::{exec, exec_ninja};
use std::error::Error;
use std::path::PathBuf;

pub fn build_server(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("server", "dev", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["server".to_string()])?;

    Ok(())
}

pub fn build_server_prod(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("server", "prod", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["server".to_string()])?;

    Ok(())
}

pub fn run_server(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("server", "dev", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["server".to_string()])?;

    exec(
        root.join("target/debug/starkingdoms-server")
            .to_str()
            .unwrap(),
        &root,
        args,
    )?;

    Ok(())
}

pub fn run_server_prod(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;

    configure_rust_target("server", "prod", &mut config_file_writer, &root)?;

    exec_ninja(&root, vec!["server".to_string()])?;

    exec(
        root.join("target/release/starkingdoms-server")
            .to_str()
            .unwrap(),
        &root,
        args,
    )?;

    Ok(())
}

A spacetime_rs/src/config.rs => spacetime_rs/src/config.rs +4 -0
@@ 0,0 1,4 @@
pub const ASSET_DIR: &str = "assets/";
pub const ASSETS_DIST_SUBDIR: &str = "dist/";
pub const ASSETS_SRC_SUBDIR: &str = "src/";
pub const ASSETS_FINAL_SUBDIR: &str = "final/";

A spacetime_rs/src/configure/asset.rs => spacetime_rs/src/configure/asset.rs +419 -0
@@ 0,0 1,419 @@
use crate::config::{ASSETS_DIST_SUBDIR, ASSETS_FINAL_SUBDIR, ASSETS_SRC_SUBDIR, ASSET_DIR};
use crate::ninja::NinjaWriter;
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::fs::File;
use std::path::{Path, PathBuf};

pub fn configure_assets(writer: &mut NinjaWriter<File>, root: &Path) -> Result<(), Box<dyn Error>> {
    // scan for assets
    let asset_src_dir = root.join(ASSET_DIR).join(ASSETS_SRC_SUBDIR);

    let mut found_assets = vec![];

    let files_in_src_dir = fs::read_dir(asset_src_dir)?;
    for maybe_asset in files_in_src_dir {
        let maybe_asset = maybe_asset?;
        if maybe_asset.file_name().to_str().unwrap().ends_with(".svg") {
            found_assets.push(maybe_asset.path());
        }
    }

    println!(
        "[spacetime] asset scan: found {} assets",
        found_assets.len()
    );

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

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

    gen_inkscape_rule(default_asset_size, writer, &mut written_rules_for)?;

    for size in asset_overrides.values() {
        gen_inkscape_rule(*size, writer, &mut written_rules_for)?;
    }

    println!(
        "[spacetime] generated {} image conversion rules",
        written_rules_for.len() * 3
    );

    let mut files_375 = vec![];
    let mut files_125 = vec![];
    let mut files_full = vec![];

    for asset in &found_assets {
        gen_convert_rule(
            asset,
            root,
            writer,
            &mut files_375,
            &mut files_full,
            &mut files_125,
            asset_size(
                asset.to_str().unwrap(),
                &asset_overrides,
                default_asset_size,
            ),
        )?;
    }

    println!(
        "[spacetime] generated {} image conversion steps",
        files_full.len() + files_125.len() + files_375.len()
    );

    gen_packer_rule(root, writer, &files_375, &files_full, &files_125)?;

    println!("[spacetime] generated asset build commands");

    Ok(())
}

fn gen_packer_rule(
    root: &Path,
    writer: &mut NinjaWriter<File>,
    files_375: &[PathBuf],
    files_full: &[PathBuf],
    files_125: &[PathBuf],
) -> Result<(), Box<dyn Error>> {
    writer.rule(
        "pack",
        &format!(
            "cd {} && atlasify -m 4096,4096 -o $out $in && touch $out",
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .to_string_lossy()
        ),
        None,
        None,
        None,
        Some("console"),
        None,
        None,
        None,
        None,
    )?;

    writer.build(
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-full")
            .to_str()
            .unwrap()
            .to_string()],
        "pack".to_string(),
        files_full
            .iter()
            .map(|u| u.to_str().unwrap().to_string())
            .collect(),
        vec![],
        vec![],
        HashMap::new(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-full.json")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-full.png")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        None,
        None,
    )?;
    writer.build(
        vec!["asset-full".to_string()],
        "phony".to_string(),
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-full")
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    writer.build(
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-125")
            .to_str()
            .unwrap()
            .to_string()],
        "pack".to_string(),
        files_125
            .iter()
            .map(|u| u.to_str().unwrap().to_string())
            .collect(),
        vec![],
        vec![],
        HashMap::new(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-125.json")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-125.png")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        None,
        None,
    )?;
    writer.build(
        vec!["asset-125".to_string()],
        "phony".to_string(),
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-125")
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    writer.build(
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-375")
            .to_str()
            .unwrap()
            .to_string()],
        "pack".to_string(),
        files_375
            .iter()
            .map(|u| u.to_str().unwrap().to_string())
            .collect(),
        vec![],
        vec![],
        HashMap::new(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-375.json")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-375.png")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        None,
        None,
    )?;
    writer.build(
        vec!["asset-375".to_string()],
        "phony".to_string(),
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-375")
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    writer.build(
        vec!["asset".to_string()],
        "phony".to_string(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-375")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-full")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-125")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    Ok(())
}

fn gen_convert_rule(
    asset: &Path,
    root: &Path,
    writer: &mut NinjaWriter<File>,
    files_375: &mut Vec<PathBuf>,
    files_full: &mut Vec<PathBuf>,
    files_125: &mut Vec<PathBuf>,
    size: i32,
) -> Result<(), Box<dyn Error>> {
    let out_full = root
        .join(ASSET_DIR)
        .join(ASSETS_FINAL_SUBDIR)
        .join("full/")
        .join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    files_full.push(out_full.clone());
    let rule_full = format!("inkscape_{}_px_full", size);
    writer.build(
        vec![out_full.to_str().unwrap().to_string()],
        rule_full,
        vec![asset.to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    let out_375 = root
        .join(ASSET_DIR)
        .join(ASSETS_FINAL_SUBDIR)
        .join("375/")
        .join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    files_375.push(out_375.clone());
    let rule_375 = format!("inkscape_{}_px_375", size);
    writer.build(
        vec![out_375.to_str().unwrap().to_string()],
        rule_375,
        vec![asset.to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    let out_125 = root
        .join(ASSET_DIR)
        .join(ASSETS_FINAL_SUBDIR)
        .join("125/")
        .join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    files_125.push(out_125.clone());
    let rule_125 = format!("inkscape_{}_px_125", size);
    writer.build(
        vec![out_125.to_str().unwrap().to_string()],
        rule_125,
        vec![asset.to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    Ok(())
}

fn asset_size(asset: &str, overrides: &HashMap<&str, i32>, default: i32) -> i32 {
    *overrides.get(asset).unwrap_or(&default)
}

fn gen_inkscape_rule(
    size: i32,
    writer: &mut NinjaWriter<File>,
    written: &mut Vec<i32>,
) -> Result<(), Box<dyn Error>> {
    if written.contains(&size) {
        return Ok(());
    }

    writer.rule(
        &format!("inkscape_{}_px_full", size),
        &format!("inkscape -w {} -h {} $in -o $out", size, size),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
    )?;
    writer.rule(
        &format!("inkscape_{}_px_375", size),
        &format!(
            "inkscape -w {} -h {} $in -o $out",
            (size as f64 * 0.375) as i32,
            (size as f64 * 0.375) as i32
        ),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
    )?;
    writer.rule(
        &format!("inkscape_{}_px_125", size),
        &format!(
            "inkscape -w {} -h {} $in -o $out",
            (size as f64 * 0.125) as i32,
            (size as f64 * 0.125) as i32
        ),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
    )?;

    written.push(size);

    Ok(())
}

A spacetime_rs/src/configure/client.rs => spacetime_rs/src/configure/client.rs +11 -0
@@ 0,0 1,11 @@
use crate::configure::asset::configure_assets;
use crate::ninja::NinjaWriter;
use std::error::Error;
use std::fs::File;
use std::path::Path;

pub fn configure_client(writer: &mut NinjaWriter<File>, root: &Path) -> Result<(), Box<dyn Error>> {
    configure_assets(writer, root)?;

    Ok(())
}

A spacetime_rs/src/configure/mod.rs => spacetime_rs/src/configure/mod.rs +15 -0
@@ 0,0 1,15 @@
use crate::ninja::NinjaWriter;
use std::fs::File;
use std::io;
use std::path::Path;

pub mod asset;
pub mod client;
pub mod rust;

pub fn create_writer(root: &Path) -> Result<NinjaWriter<File>, io::Error> {
    let mut w = NinjaWriter::new(File::create(root.join("build.ninja"))?);
    w.comment("Generated by spacetime")?;
    w.comment("Do not edit manually")?;
    Ok(w)
}

A spacetime_rs/src/configure/rust.rs => spacetime_rs/src/configure/rust.rs +103 -0
@@ 0,0 1,103 @@
/*
def gen_rules_for_api(root, env, writer, modules):
    if env == 'dev':
        out_dir = 'debug'
        writer.rule('cargo-api', f'cargo build --bin starkingdoms-api --features "{modules}"',
                    depfile=f'{root}/target/debug/starkingdoms-api.d', pool='console')
    elif env == 'prod':
        out_dir = 'release'
        writer.rule('cargo-api', f'cargo build --bin starkingdoms-api --release --features "{modules}"',
                    depfile=f'{root}/target/release/starkingdoms-api.d', pool='console')

    writer.build([f'{root}/target/{out_dir}/starkingdoms-api'], 'cargo-api', ['server/Cargo.toml'])
    writer.build(['api'], 'phony', [f'{root}/target/{out_dir}/starkingdoms-api'])
 */

use crate::ninja::NinjaWriter;
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::path::Path;

pub fn configure_rust_target(
    rust_target: &str,
    rust_env: &str,
    writer: &mut NinjaWriter<File>,
    root: &Path,
) -> Result<(), Box<dyn Error>> {
    let out_dir;
    if rust_env == "dev" {
        out_dir = "debug";
        writer.rule(
            &format!("cargo-{}", rust_target),
            &format!("cargo build --bin starkingdoms-{}", rust_target),
            None,
            Some(
                root.join("target/debug/")
                    .join(format!("starkingdoms-{}.d", rust_target))
                    .to_str()
                    .unwrap(),
            ),
            None,
            Some("console"),
            None,
            None,
            None,
            None,
        )?;
    } else {
        out_dir = "release";
        writer.rule(
            &format!("cargo-{}", rust_target),
            &format!("cargo build --bin starkingdoms-{} --release", rust_target),
            None,
            Some(
                root.join("target/release/")
                    .join(format!("starkingdoms-{}.d", rust_target))
                    .to_str()
                    .unwrap(),
            ),
            None,
            Some("console"),
            None,
            None,
            None,
            None,
        )?;
    }

    writer.build(
        vec![root
            .join(format!("target/{}/", out_dir))
            .join(format!("starkingdoms-{}", rust_target))
            .to_str()
            .unwrap()
            .to_string()],
        format!("cargo-{}", rust_target),
        vec![root.join("server/Cargo.toml").to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;
    writer.build(
        vec![rust_target.to_string()],
        "phony".to_string(),
        vec![root
            .join(format!("target/{}/", out_dir))
            .join(format!("starkingdoms-{}", rust_target))
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    Ok(())
}

A spacetime_rs/src/main.rs => spacetime_rs/src/main.rs +263 -0
@@ 0,0 1,263 @@
use crate::cmd::enforce_commands;
use crate::commands::api::{build_api, build_api_prod, run_api, run_api_prod};
use crate::commands::assets::build_assets;
use crate::commands::clean::clean;
use crate::commands::client::{build_client_prod, client_protobuf, run_http};
use crate::commands::docker::{
    build_docker, build_docker_api, build_docker_api_beta, build_docker_api_stable,
    build_docker_beta, build_docker_server, build_docker_server_beta, build_docker_server_stable,
    build_docker_stable, build_docker_web, build_docker_web_beta, build_docker_web_stable,
};
use crate::commands::server::{build_server, build_server_prod, run_server, run_server_prod};
use std::collections::HashMap;
use std::error::Error;
use std::io::Write;
use std::path::PathBuf;
use std::time::SystemTime;
use tabwriter::TabWriter;

pub mod cmd;
pub mod commands;
pub mod config;
pub mod configure;
pub mod ninja;

fn main() {
    let mut bcm = BuildCommandManager::new();

    bcm.register(
        "run_http",
        Box::new(run_http),
        "Compile the client and then run a development HTTP server",
        false,
    );
    bcm.register(
        "build_assets",
        Box::new(build_assets),
        "Compile the asset source files into spritesheets",
        false,
    );
    bcm.register(
        "client_protobuf",
        Box::new(client_protobuf),
        "Update the client protocol bindings",
        false,
    );
    bcm.register(
        "clean",
        Box::new(clean),
        "Remove all compilation artifacts",
        false,
    );

    bcm.register(
        "build_api",
        Box::new(build_api),
        "Compile the API server",
        false,
    );
    bcm.register("run_api", Box::new(run_api), "Run the API server", false);
    bcm.register(
        "build_api_prod",
        Box::new(build_api_prod),
        "Compile the API server with optimizations",
        false,
    );
    bcm.register(
        "run_api_prod",
        Box::new(run_api_prod),
        "Run the API server with optimizations",
        false,
    );

    bcm.register(
        "build_server",
        Box::new(build_server),
        "Compile the game server",
        false,
    );
    bcm.register(
        "run_server",
        Box::new(run_server),
        "Run the game server",
        false,
    );
    bcm.register(
        "build_server_prod",
        Box::new(build_server_prod),
        "Compile the game server with optimizations",
        false,
    );
    bcm.register(
        "run_server_prod",
        Box::new(run_server_prod),
        "Run the game server with optimizations",
        false,
    );

    bcm.register(
        "build_docker_beta",
        Box::new(build_docker_beta),
        "Build all three docker images for the beta channel",
        false,
    );
    bcm.register(
        "build_docker_api_beta",
        Box::new(build_docker_api_beta),
        "Build the API docker image for the beta channel",
        false,
    );
    bcm.register(
        "build_docker_server_beta",
        Box::new(build_docker_server_beta),
        "Build the main docker image for the beta channel",
        false,
    );
    bcm.register(
        "build_docker_web_beta",
        Box::new(build_docker_web_beta),
        "Build the webserver docker image for the beta channel",
        false,
    );

    bcm.register(
        "build_docker_stable",
        Box::new(build_docker_stable),
        "Build all three docker images for the stable channel",
        false,
    );
    bcm.register(
        "build_docker_api_stable",
        Box::new(build_docker_api_stable),
        "Build the API docker image for the stable channel",
        false,
    );
    bcm.register(
        "build_docker_server_stable",
        Box::new(build_docker_server_stable),
        "Build the main docker image for the stable channel",
        false,
    );
    bcm.register(
        "build_docker_web_stable",
        Box::new(build_docker_web_stable),
        "Build the webserver docker image for the stable channel",
        false,
    );

    bcm.register(
        "build_docker",
        Box::new(build_docker),
        "Build all three docker images for the bleeding channel",
        false,
    );
    bcm.register(
        "build_docker_api",
        Box::new(build_docker_api),
        "Build the API docker image for the bleeding channel",
        false,
    );
    bcm.register(
        "build_docker_server",
        Box::new(build_docker_server),
        "Build the main docker image for the bleeding channel",
        false,
    );
    bcm.register(
        "build_docker_web",
        Box::new(build_docker_web),
        "Build the webserver docker image for the bleeding channel",
        false,
    );

    bcm.register(
        "build_client_prod",
        Box::new(build_client_prod),
        "Build the production-ready client bundle",
        false,
    );

    let start = SystemTime::now();

    let args: Vec<String> = std::env::args().collect();
    match bcm.exec(args) {
        Ok(_) => (),
        Err(e) => {
            let end = SystemTime::now();
            let duration = end.duration_since(start).unwrap();

            println!("[spacetime] Done in {} seconds", duration.as_secs_f32());

            eprintln!("[!] Error executing build command: {}", e);
            std::process::exit(-1);
        }
    }

    let end = SystemTime::now();
    let duration = end.duration_since(start).unwrap();

    println!("[spacetime] Done in {} seconds", duration.as_secs_f32());
}

type BuildCommandCallback = Box<dyn Fn(Vec<String>, PathBuf) -> Result<(), Box<dyn Error>>>;
#[derive(Default)]
pub struct BuildCommandManager {
    commands: HashMap<String, (BuildCommandCallback, String, bool)>,
}
impl BuildCommandManager {
    pub fn new() -> Self {
        Self {
            commands: HashMap::new(),
        }
    }

    pub fn register(
        &mut self,
        name: &str,
        command: BuildCommandCallback,
        description: &str,
        hidden: bool,
    ) {
        self.commands
            .insert(name.to_string(), (command, description.to_string(), hidden));
    }

    pub fn help(&self) {
        println!("Spacetime - StarKingdoms build utility");
        println!("Spacetime is a small Rust utility to assist in compiling StarKingdoms.");
        println!("Available targets:");

        let mut tw = TabWriter::new(vec![]);

        for (target, (_, description, hidden)) in &self.commands {
            if *hidden {
                continue;
            };

            writeln!(tw, "\t{}\t{}", target, description).unwrap();
        }

        std::io::stdout()
            .write_all(&tw.into_inner().unwrap())
            .unwrap();
    }

    pub fn exec(&self, args: Vec<String>) -> Result<(), Box<dyn Error>> {
        if args.len() < 2 || args[1] == "help" || args[1] == "h" {
            self.help();
            return Ok(());
        }

        enforce_commands();

        let main_path = PathBuf::from(args[2].to_string());

        if let Some((callback, _, _)) = self.commands.get(&args[1]) {
            callback(args[2..].to_owned(), main_path)
        } else {
            eprintln!("Unrecognized build command {}", args[1]);
            self.help();
            Ok(())
        }
    }
}

A spacetime_rs/src/ninja.rs => spacetime_rs/src/ninja.rs +194 -0
@@ 0,0 1,194 @@
use std::collections::HashMap;
use std::error::Error;
use std::io;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};

fn escape_path(word: &str) -> String {
    word.replace("$ ", "$$ ")
        .replace(' ', "$ ")
        .replace(':', "$:")
}

pub struct NinjaWriter<T: Write> {
    output: T,
}
impl<T: Write> NinjaWriter<T> {
    pub fn new(output: T) -> Self {
        Self { output }
    }

    pub fn newline(&mut self) -> Result<(), io::Error> {
        writeln!(self.output)
    }

    pub fn comment(&mut self, text: &str) -> Result<(), io::Error> {
        writeln!(self.output, "# {}", text)
    }

    pub fn variable(&mut self, key: &str, value: &str, indent: usize) -> Result<(), io::Error> {
        if value.is_empty() {
            return Ok(());
        }
        writeln!(self.output, "{}{} = {}", self._indent(indent), key, value)
    }

    pub fn pool(&mut self, name: &str, depth: usize) -> Result<(), io::Error> {
        writeln!(self.output, "pool {}", name)?;
        self.variable("depth", &depth.to_string(), 1)
    }

    #[allow(clippy::too_many_arguments)]
    pub fn rule(
        &mut self,
        name: &str,
        command: &str,
        description: Option<&str>,
        depfile: Option<&str>,
        generator: Option<bool>,
        pool: Option<&str>,
        restat: Option<bool>,
        rspfile: Option<&str>,
        rspfile_content: Option<&str>,
        deps: Option<&str>,
    ) -> Result<(), io::Error> {
        writeln!(self.output, "rule {}", name)?;
        self.variable("command", command, 1)?;
        if let Some(desc) = description {
            self.variable("description", desc, 1)?;
        }
        if let Some(depfile) = depfile {
            self.variable("depfile", depfile, 1)?;
        }
        if let Some(gen) = generator {
            if gen {
                self.variable("generator", "1", 1)?;
            }
        }
        if let Some(pool) = pool {
            self.variable("pool", pool, 1)?;
        }
        if let Some(restat) = restat {
            if restat {
                self.variable("restat", "1", 1)?;
            }
        }
        if let Some(rspfile) = rspfile {
            self.variable("rspfile", rspfile, 1)?;
        }
        if let Some(rspfile_content) = rspfile_content {
            self.variable("rspfile_content", rspfile_content, 1)?;
        }
        if let Some(deps) = deps {
            self.variable("deps", deps, 1)?;
        }

        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    pub fn build(
        &mut self,
        outputs: Vec<String>,
        rule: String,
        inputs: Vec<String>,
        mut implicit: Vec<String>,
        mut order_only: Vec<String>,
        variables: HashMap<String, String>,
        mut implicit_outputs: Vec<String>,
        pool: Option<String>,
        dyndep: Option<String>,
    ) -> Result<(), io::Error> {
        let mut out_outputs: Vec<String> = outputs.iter().map(|u| escape_path(u)).collect();
        let mut all_inputs: Vec<String> = inputs.iter().map(|u| escape_path(u)).collect();

        if !implicit.is_empty() {
            all_inputs.push("|".to_string());
            all_inputs.append(&mut implicit.iter_mut().map(|u| escape_path(u)).collect());
        }
        if !order_only.is_empty() {
            all_inputs.push("||".to_string());
            all_inputs.append(&mut order_only.iter_mut().map(|u| escape_path(u)).collect());
        }
        if !implicit_outputs.is_empty() {
            out_outputs.push("|".to_string());
            out_outputs.append(
                &mut implicit_outputs
                    .iter_mut()
                    .map(|u| escape_path(u))
                    .collect(),
            );
        }

        all_inputs.insert(0, rule);

        writeln!(
            self.output,
            "build {}: {}",
            out_outputs.join(" "),
            all_inputs.join(" ")
        )?;

        if let Some(pool) = pool {
            self.variable("pool", &pool, 1)?;
        }
        if let Some(dyndep) = dyndep {
            self.variable("dyndep", &dyndep, 1)?;
        }

        for (key, value) in variables {
            self.variable(&key, &value, 1)?;
        }

        Ok(())
    }

    pub fn include(&mut self, path: &str) -> Result<(), io::Error> {
        writeln!(self.output, "include {}", path)
    }

    pub fn subninja(&mut self, path: &str) -> Result<(), io::Error> {
        writeln!(self.output, "subninja {}", path)
    }

    pub fn default(&mut self, paths: Vec<String>) -> Result<(), io::Error> {
        writeln!(self.output, "default {}", paths.join(" "))
    }

    fn _indent(&self, num: usize) -> String {
        "  ".repeat(num)
    }
}

pub fn exec_ninja(root: &PathBuf, args: Vec<String>) -> Result<(), Box<dyn Error>> {
    println!("[exec] cmd: ninja {:?}", args);
    let stat = Command::new("ninja")
        .args(args)
        .current_dir(root)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()?
        .wait()?;
    if stat.success() {
        Ok(())
    } else {
        Err(format!("ninja exited with error: {}", stat.code().unwrap()).into())
    }
}

pub fn exec(cmd: &str, dir: &PathBuf, args: Vec<String>) -> Result<(), Box<dyn Error>> {
    println!("[exec] cmd: {} {:?}", cmd, args);
    let stat = Command::new(cmd)
        .args(args)
        .current_dir(dir)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()?
        .wait()?;
    if stat.success() {
        Ok(())
    } else {
        Err(format!("{} exited with error: {}", cmd, stat.code().unwrap()).into())
    }
}