~starkingdoms/starkingdoms

afeb7c8562239af02974fe4fa7cd9d5f962117c0 — ghostlyzsh 2 years ago 4698679 + 652ff55
merged because oops
M Cargo.lock => Cargo.lock +26 -24
@@ 3291,6 3291,31 @@ dependencies = [
]

[[package]]
name = "starkingdoms-api"
version = "0.1.0"
dependencies = [
 "actix-files",
 "actix-request-identifier",
 "actix-web",
 "hmac",
 "jwt",
 "log",
 "once_cell",
 "openssl",
 "reqwest",
 "sea-orm",
 "serde",
 "sha2",
 "simple_logger",
 "starkingdoms-protocol",
 "starkingdoms_api_entities",
 "starkingdoms_api_migration",
 "tera",
 "toml 0.7.3",
 "ulid",
]

[[package]]
name = "starkingdoms-protocol"
version = "0.1.0"
dependencies = [


@@ 3313,6 3338,7 @@ dependencies = [
 "nalgebra",
 "rand",
 "rapier2d-f64",
 "reqwest",
 "serde",
 "serde_json",
 "simple_logger",


@@ 3321,30 3347,6 @@ dependencies = [
]

[[package]]
name = "starkingdoms-server-api"
version = "0.1.0"
dependencies = [
 "actix-files",
 "actix-request-identifier",
 "actix-web",
 "hmac",
 "jwt",
 "log",
 "once_cell",
 "openssl",
 "reqwest",
 "sea-orm",
 "serde",
 "sha2",
 "simple_logger",
 "starkingdoms_api_entities",
 "starkingdoms_api_migration",
 "tera",
 "toml 0.7.3",
 "ulid",
]

[[package]]
name = "starkingdoms_api_entities"
version = "0.1.0"
dependencies = [

M api/Cargo.toml => api/Cargo.toml +4 -2
@@ 1,5 1,5 @@
[package]
name = "starkingdoms-server-api"
name = "starkingdoms-api"
version = "0.1.0"
edition = "2021"



@@ 30,4 30,6 @@ jwt = { version = "0.16", features = ["openssl"] }  # Auth
openssl = "0.10"                                    # Auth
reqwest = "0.11"                                    # Auth
hmac = "0.12.1"                                     # Auth
sha2 = "0.10.6"                                     # Auth
\ No newline at end of file
sha2 = "0.10.6"                                     # Auth

starkingdoms-protocol = { version = "0.1.0", path = "../protocol" }
\ No newline at end of file

M api/src/main.rs => api/src/main.rs +2 -0
@@ 66,6 66,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
            .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?;


D api/src/routes/auth.rs => api/src/routes/auth.rs +0 -0
A api/src/routes/beamin.rs => api/src/routes/beamin.rs +112 -0
@@ 0,0 1,112 @@
use std::collections::BTreeMap;
use actix_web::{HttpResponse, post};
use actix_web::web::{Data, Json};
use hmac::digest::KeyInit;
use hmac::Hmac;
use jwt::VerifyWithKey;
use log::error;
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};

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

#[derive(Serialize, Deserialize)]
pub struct BeaminResponse {
    pub save_id: String,
    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,
                }
            ],
        })
    }

    let key: Hmac<Sha256> = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap();
    let token: BTreeMap<String, String> = match data.user_auth_token.verify_with_key(&key) {
        Ok(t) => t,
        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,
                    }
                ],
            })
        }
    };

    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,
                }
            ],
        });
    }

    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 {
                        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,
                }
            ],
        });
    }

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

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

A api/src/routes/beamout.rs => api/src/routes/beamout.rs +97 -0
@@ 0,0 1,97 @@
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{HttpResponse, post};
use actix_web::web::{Data, Json};
use hmac::digest::KeyInit;
use hmac::Hmac;
use jwt::VerifyWithKey;
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};

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

#[derive(Serialize, Deserialize)]
pub struct BeamoutResponse {}

#[post("/beamout")]
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,
                }
            ],
        })
    }

    let key: Hmac<Sha256> = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap();
    let token: BTreeMap<String, String> = match data.user_auth_token.verify_with_key(&key) {
        Ok(t) => t,
        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,
                    }
                ],
            })
        }
    };

    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,
                }
            ],
        });
    }

    let saved_data = toml::to_string(&data.data).unwrap();
    let savefile_model = starkingdoms_api_entities::entity::user_savefile::Model {
        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,
    };
    let savefile_active_model = savefile_model.into_active_model();
    match savefile_active_model.insert(&state.conn).await {
        Ok(_) => (),
        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,
                    }
                ],
            });
        }
    }

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

M api/src/routes/callback.rs => api/src/routes/callback.rs +2 -0
@@ 95,6 95,7 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 
        let key: Hmac<Sha256> = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap();
        let mut claims = BTreeMap::new();
        claims.insert("user", 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, user.id);
        return HttpResponse::Found().append_header(("Location", auth_url)).finish();


@@ 116,6 117,7 @@ pub async fn callback(query: Query<CallbackQueryParams>, state: Data<AppState>) 
    let key: Hmac<Sha256> = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap();
    let mut claims = BTreeMap::new();
    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());


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

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

M api/starkingdoms_api_entities/src/entity/mod.rs => api/starkingdoms_api_entities/src/entity/mod.rs +1 -0
@@ 4,3 4,4 @@ pub mod prelude;

pub mod user;
pub mod user_auth_realm;
pub mod user_savefile;

M api/starkingdoms_api_entities/src/entity/prelude.rs => api/starkingdoms_api_entities/src/entity/prelude.rs +1 -0
@@ 2,3 2,4 @@

pub use super::user::Entity as User;
pub use super::user_auth_realm::Entity as UserAuthRealm;
pub use super::user_savefile::Entity as UserSavefile;

M api/starkingdoms_api_entities/src/entity/user_auth_realm.rs => api/starkingdoms_api_entities/src/entity/user_auth_realm.rs +8 -0
@@ 22,6 22,8 @@ pub enum Relation {
        on_delete = "NoAction"
    )]
    User,
    #[sea_orm(has_many = "super::user_savefile::Entity")]
    UserSavefile,
}

impl Related<super::user::Entity> for Entity {


@@ 30,4 32,10 @@ impl Related<super::user::Entity> for Entity {
    }
}

impl Related<super::user_savefile::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::UserSavefile.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

A api/starkingdoms_api_entities/src/entity/user_savefile.rs => api/starkingdoms_api_entities/src/entity/user_savefile.rs +34 -0
@@ 0,0 1,34 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user_savefile")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: String,
    pub user: String,
    pub data: String,
    #[sea_orm(unique)]
    pub timestamp: i64,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(
        belongs_to = "super::user_auth_realm::Entity",
        from = "Column::User",
        to = "super::user_auth_realm::Column::Id",
        on_update = "NoAction",
        on_delete = "NoAction"
    )]
    UserAuthRealm,
}

impl Related<super::user_auth_realm::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::UserAuthRealm.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

M api/starkingdoms_api_migration/src/lib.rs => api/starkingdoms_api_migration/src/lib.rs +4 -2
@@ 1,7 1,8 @@
pub use sea_orm_migration::prelude::*;

mod m20230417_162824_create_table_users;
mod m20230417_164240_create_table_user_auth_realms;
pub mod m20230417_162824_create_table_users;
pub mod m20230417_164240_create_table_user_auth_realms;
pub mod m20230420_144333_create_table_user_data;

pub struct Migrator;



@@ 11,6 12,7 @@ impl MigratorTrait for Migrator {
        vec![
            Box::new(m20230417_162824_create_table_users::Migration),
            Box::new(m20230417_164240_create_table_user_auth_realms::Migration),
            Box::new(m20230420_144333_create_table_user_data::Migration),
        ]
    }
}

A api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs => api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs +41 -0
@@ 0,0 1,41 @@
use sea_orm_migration::prelude::*;
use crate::m20230417_164240_create_table_user_auth_realms::UserAuthRealm;

#[derive(DeriveMigrationName)]
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
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(UserSavefile::Table).to_owned())
            .await
    }
}

/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum UserSavefile {
    Table,
    Id,
    User,
    Data,
    Timestamp
}

M client/index.html => client/index.html +16 -1
@@ 28,11 28,15 @@
        <input class="m-5px" type="text" name="username" id="username" required />
        <br>
        <button class="m-5px w-full">Launch!</button>
        <br>
        <p id="loginstatus">You are not logged in.</p>
        <button style="display: none;" id="logout">Log out</button>
        <a href="http://localhost:8080/select-realm" id="login">Click here to log in or change accounts.</a>
    </form>
</fieldset>

<script>

    let api_server = "http://localhost:8080";
    let servers = ["localhost:3000"];

    function server_url_to_ping_url(server) { return "http://" + server + "/ping" }


@@ 60,6 64,17 @@
        window.localStorage.setItem("token", query.get("token"));
        window.localStorage.setItem("user", query.get("user"));
    }

    if (window.localStorage.getItem("token") !== null && window.localStorage.getItem("user") !== null) {
        document.getElementById("logout").style.setProperty("display", "block");
        document.getElementById("logout").addEventListener("click", () => {
            window.localStorage.clear();
            window.location.reload();
        })
        document.getElementById("loginstatus").innerText = `Logged in! (you are ${window.localStorage.getItem("user")})`;
    }

    document.getElementById("login").href = `${api_server}/select-realm`;
</script>
</body>
</html>

M client/src/gateway.ts => client/src/gateway.ts +20 -5
@@ 69,11 69,26 @@ export async function gateway_connect(gateway_url: string, username: string): Ga
    }
    client.ping_timeout = setTimeout(ping_fn, 5 * 1000);

    let handshake_start_msg = MessageC2SHello.encode({
        version: 3,
        requestedUsername: username,
        nextState: State.Play
    }).finish();
    let handshake_start_msg;
    if (global.can_beam_out) {
        handshake_start_msg = MessageC2SHello.encode({
            version: 2,
            requestedUsername: username,
            nextState: State.Play,
            user: window.localStorage.getItem("user")!,
            token: window.localStorage.getItem("token")!
        }).finish();
    } else {
        handshake_start_msg = MessageC2SHello.encode({
            version: 2,
            requestedUsername: username,
            nextState: State.Play,
            // @ts-ignore
            user: null,
            // @ts-ignore
            token: null
        }).finish();
    }
    client.socket.send(encode(MessageC2SHello_packetInfo.type, handshake_start_msg));

    client.socket.addEventListener('message', async (msg) => {

M client/src/index.ts => client/src/index.ts +1 -1
@@ 159,7 159,7 @@ async function client_main(server: string, username: string, texture_quality: st
        let viewer_size_x = global.canvas.width;
        let viewer_size_y = global.canvas.height;

        global.canvas.style.setProperty("background-position", `${-global.me?.x!}px ${-global.me?.y!}px`);
        global.canvas.style.setProperty("background-position", `${-global.me?.x!/5}px ${-global.me?.y!/5}px`);

        global.context.setTransform(1, 0, 0, 1, 0, 0);
        global.context.clearRect(0, 0, viewer_size_x, viewer_size_y);

M client/src/protocol/message_c2s.ts => client/src/protocol/message_c2s.ts +7 -7
@@ 246,7 246,7 @@ export function messageC2SAuthenticateAndBeamOut_packetInfoToJSON(
}

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

export const MessageC2SHello = {


@@ 260,10 260,10 @@ export const MessageC2SHello = {
    if (message.nextState !== 0) {
      writer.uint32(24).int32(message.nextState);
    }
    if (message.token !== undefined) {
    if (message.token !== "") {
      writer.uint32(34).string(message.token);
    }
    if (message.user !== undefined) {
    if (message.user !== "") {
      writer.uint32(42).string(message.user);
    }
    return writer;


@@ 325,8 325,8 @@ export const MessageC2SHello = {
      version: isSet(object.version) ? Number(object.version) : 0,
      requestedUsername: isSet(object.requestedUsername) ? String(object.requestedUsername) : "",
      nextState: isSet(object.nextState) ? stateFromJSON(object.nextState) : 0,
      token: isSet(object.token) ? String(object.token) : undefined,
      user: isSet(object.user) ? String(object.user) : undefined,
      token: isSet(object.token) ? String(object.token) : "",
      user: isSet(object.user) ? String(object.user) : "",
    };
  },



@@ 349,8 349,8 @@ export const MessageC2SHello = {
    message.version = object.version ?? 0;
    message.requestedUsername = object.requestedUsername ?? "";
    message.nextState = object.nextState ?? 0;
    message.token = object.token ?? undefined;
    message.user = object.user ?? undefined;
    message.token = object.token ?? "";
    message.user = object.user ?? "";
    return message;
  },
};

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

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

}
\ No newline at end of file

M server/Cargo.toml => server/Cargo.toml +1 -0
@@ 26,6 26,7 @@ lazy_static = "1.4.0"
rapier2d-f64 = { version = "0.17.2", features = [ "simd-stable" ] }
nalgebra = "0.32.2"
rand = "0.8.5"
reqwest = "0.11.16"

[build-dependencies]
cargo_metadata = "0.15"

M server/src/api.rs => server/src/api.rs +62 -3
@@ 1,12 1,71 @@
use std::error::Error;
use log::error;
use reqwest::StatusCode;
use serde::{Serialize, Deserialize};
use starkingdoms_protocol::api::APISavedPlayerData;

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

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

pub async fn load_player_data_from_api(token: &str, user_id: &str, internal_token: &str) -> Result<APISavedPlayerData, Box<dyn Error>> {
    // TODO
    Ok(APISavedPlayerData {})
    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()
    };

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

    if res.status() != StatusCode::OK {
        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?)?;

    Ok(resp.save)
}

#[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 async fn save_player_data_to_api(data: &APISavedPlayerData, token: &str, user_id: &str, internal_token: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
    // TODO
    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()
    };

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

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

M server/src/entity.rs => server/src/entity.rs +5 -4
@@ 15,6 15,7 @@ pub fn get_entity_id() -> EntityId {
    id
}

#[derive(Default)]
pub struct EntityHandler {
    pub entities: Entities,
}


@@ 41,7 42,7 @@ impl EntityHandler {
                planets.remove(i);
            }
        }
        if planets.len() == 0 {
        if planets.is_empty() {
            return None;
        }
        Some(planets[0].clone())


@@ 73,7 74,7 @@ impl EntityHandler {
                players.remove(i);
            }
        }
        if players.len() == 0 {
        if players.is_empty() {
            return None;
        }
        Some(players[0].clone().1)


@@ 100,7 101,7 @@ impl EntityHandler {
    pub fn gravity(&self, position: (f64, f64), mass: f64) -> (f64, f64) {
        let mut direction = Vector2::zeros();
        let planets = self.get_planets();
        for planet in planets.clone() {
        for planet in planets {
            let planet_grav = planet.gravity(position, mass);
            direction.x += planet_grav.0;
            direction.y += planet_grav.1;


@@ 111,7 112,7 @@ impl EntityHandler {
    pub fn to_protocol(&self) -> ClientHandlerMessage {
        let mut planets = vec![];

        for planet in self.get_planets().clone() {
        for planet in self.get_planets() {
            // TODO: Adjust codegen to use f64
            planets.push(starkingdoms_protocol::planet::Planet {
                planet_type: planet.planet_type.into(),

M server/src/handler.rs => server/src/handler.rs +17 -15
@@ 173,27 173,29 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc<RwLock<EntityHandle
                                    handle: player_handle,
                                    input: Default::default(),
                                    addr: remote_addr,
                                    auth_token: pkt.token.clone(),
                                    auth_user: pkt.user.clone()
                                    auth_token: None,
                                    auth_user: None
                                };

                                let mut e_write_handle = entities.write().await;

                                if let Some(user) = pkt.user {
                                    if let Some(token) = pkt.token {
                                        info!("[{}] * Beamin: beaming in {} as {} with token {}", remote_addr, username, user, token);
                                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);

                                        let player_data = match load_player_data_from_api(&token, &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));
                                                continue;
                                            }
                                        };
                                    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));
                                            continue;
                                        }
                                    };

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

                                    player.load_api_data(&player_data);
                                }

                                e_write_handle.entities.insert(get_entity_id(), Entity::Player(player));

M server/src/main.rs => server/src/main.rs +5 -2
@@ 3,10 3,9 @@ use std::net::SocketAddr;
use async_std::io::WriteExt;
use async_std::sync::Arc;
use async_std::net::{TcpListener, TcpStream};
use entity::{Entities, EntityHandler};
use entity::{EntityHandler};
use manager::PhysicsData;
use nalgebra::vector;
use planet::Planets;
use rapier2d_f64::prelude::{MultibodyJointSet, ImpulseJointSet, ColliderSet, RigidBodySet, NarrowPhase, BroadPhase, IslandManager, CCDSolver, IntegrationParameters};
use lazy_static::lazy_static;
use log::{error, info, Level, warn};


@@ 167,6 166,10 @@ async fn main() {
        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() {
        error!("Unable to read the API server URL from STK_API_URL. Ensure it is set, and has a valid value.");
        std::process::exit(1);
    }

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


M server/src/manager.rs => server/src/manager.rs +2 -1
@@ 28,7 28,8 @@ impl Player {
        APISavedPlayerData {}
    }

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

    }
}


M server/src/orbit/mod.rs => server/src/orbit/mod.rs +1 -0
@@ 1,4 1,5 @@
pub mod constants;
#[allow(clippy::module_inception)]
pub mod orbit;
pub mod newtonian;
pub mod kepler;

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

use log::debug;
use nalgebra::{vector, Vector2};
use crate::orbit::newtonian::solve_kepler_with_newtonian;
use crate::orbit::vis_viva::vis_viva;
use crate::planet::GRAVITY;

#[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> {
    let semi_major_length = (apoapsis + periapsis) / 2.0;
    let linear_eccentricity = semi_major_length - periapsis; // distance between center and focus
    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_x = target[0];

M server/src/planet.rs => server/src/planet.rs +4 -6
@@ 1,11 1,9 @@
use std::collections::HashMap;
use std::sync::Arc;
use async_std::sync::RwLock;
use nalgebra::{Vector2, vector};
use rapier2d_f64::prelude::{RigidBodyHandle, RigidBodySet, ColliderBuilder, RigidBodyBuilder, ColliderSet};
use starkingdoms_protocol::planet::PlanetType;

use crate::entity::{Entities, get_entity_id, Entity, EntityId, EntityHandler};
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::orbit::orbit::{calculate_point_on_orbit, calculate_world_position_of_orbit};


@@ 46,7 44,7 @@ impl Planets {
        self.planets.get_mut(planet_id)
    }

    pub async fn make_planet(planet_id: &str,
    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) {


@@ 70,8 68,8 @@ impl Planets {
        }))
    }

    pub async fn new(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",

M server/src/timer.rs => server/src/timer.rs +8 -8
@@ 6,9 6,10 @@ use rapier2d_f64::prelude::{PhysicsPipeline, ColliderBuilder, RigidBodyBuilder};
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::{Entities, Entity, EntityHandler, get_entity_id}};
use crate::orbit::constants::{EARTH_MASS, GAME_ORBITS_ENABLED, MOON_APOAPSIS, MOON_MASS, MOON_ORBIT_TIME, MOON_PERIAPSIS};
use crate::orbit::orbit::{calculate_point_on_orbit, calculate_vector_of_orbit, calculate_world_position_of_orbit};
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::orbit::{calculate_point_on_orbit, calculate_world_position_of_orbit};

pub const ROTATIONAL_FORCE: f64 = 100.0;
pub const LATERAL_FORCE: f64 = 100.0;


@@ 20,7 21,7 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic

    let mut time = 0.0;
    let mut module_timer = 0.0;
    let planet_ids;
    let _planet_ids;

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


@@ 28,7 29,7 @@ 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::new(&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;


@@ 46,12 47,11 @@ pub async fn timer_main(mgr: ClientManager, physics_data_orig: Arc<RwLock<Physic
        // IT MAY ALWAYS BE TRUE
        // THATS FINE
        if GAME_ORBITS_ENABLED {
            let mut planets = entities.write().await;
            let planets = entities.write().await;

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

            // update moon
            let moon: &mut Planet = &mut planets.get_planet(PlanetType::Moon).unwrap();

M spacetime => spacetime +28 -0
@@ 28,6 28,10 @@ sub_help() {
  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


@@ 106,6 110,30 @@ sub_run_server_prod() {
  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"

M spacetime_py/spacetime.py => spacetime_py/spacetime.py +14 -0
@@ 75,6 75,18 @@ def gen_rules_for_server(root, env, writer, modules):
    writer.build([f'{root}/target/{out_dir}/starkingdoms-server'], 'cargo-server', ['server/Cargo.toml'])
    writer.build(['server'], 'phony', [f'{root}/target/{out_dir}/starkingdoms-server'])

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'])

def gen_inkscape(root, assets, writer, files_375, files_full, files_125):
    gen_inkscape_rules_for_asset_sizes(writer)


@@ 144,6 156,8 @@ def main():
            generate_assets_build_command(root, assets, writer)
        elif target == 'server':
            gen_rules_for_server(root, env, writer, modules)
        elif target == 'api':
            gen_rules_for_api(root, env, writer, modules)

    print(f'[spacetime] Configured build')