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