diff --git a/Cargo.lock b/Cargo.lock index 58149cb..72090f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1620,6 +1620,7 @@ dependencies = [ "hyper", "parking_lot 0.12.1", "planetwars-matchrunner", + "planetwars-rules", "prost", "rand 0.8.4", "rust-argon2", diff --git a/planetwars-server/Cargo.toml b/planetwars-server/Cargo.toml index 183bb90..735836e 100644 --- a/planetwars-server/Cargo.toml +++ b/planetwars-server/Cargo.toml @@ -34,6 +34,7 @@ serde_json = "1.0" base64 = "0.13.0" zip = "0.5" toml = "0.5" +planetwars-rules = { path = "../planetwars-rules" } planetwars-matchrunner = { path = "../planetwars-matchrunner" } config = { version = "0.12", features = ["toml"] } thiserror = "1.0.31" diff --git a/planetwars-server/src/lib.rs b/planetwars-server/src/lib.rs index 316458c..2045eda 100644 --- a/planetwars-server/src/lib.rs +++ b/planetwars-server/src/lib.rs @@ -137,7 +137,10 @@ fn api() -> Router { "/matches/:match_id/log", get(routes::matches::get_match_log), ) - .route("/maps", get(routes::maps::list_maps)) + .route( + "/maps", + get(routes::maps::list_maps).post(routes::maps::create_map), + ) .route("/leaderboard", get(routes::bots::get_ranking)) .route("/submit_bot", post(routes::demo::submit_bot)) .route("/save_bot", post(routes::bots::save_bot)) diff --git a/planetwars-server/src/routes/maps.rs b/planetwars-server/src/routes/maps.rs index 188089f..cef0b4a 100644 --- a/planetwars-server/src/routes/maps.rs +++ b/planetwars-server/src/routes/maps.rs @@ -1,5 +1,8 @@ -use crate::{db, DatabaseConnection}; -use axum::Json; +use std::{collections::HashSet, fs::File, path::PathBuf, sync::Arc}; + +use crate::{db, DatabaseConnection, GlobalConfig}; +use axum::{Extension, Json}; +use diesel::OptionalExtension; use hyper::StatusCode; use serde::{Deserialize, Serialize}; @@ -8,12 +11,119 @@ pub struct ApiMap { pub name: String, } +fn map_into_api_map(map: db::maps::Map) -> ApiMap { + ApiMap { name: map.name } +} + pub async fn list_maps(mut conn: DatabaseConnection) -> Result>, StatusCode> { let maps = db::maps::list_maps(&mut conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let api_maps = maps - .into_iter() - .map(|map| ApiMap { name: map.name }) - .collect(); + let api_maps = maps.into_iter().map(map_into_api_map).collect(); Ok(Json(api_maps)) } + +use planetwars_rules::config::Map as PlanetwarsMap; +use serde_json::json; + +#[derive(Serialize, Deserialize)] +pub struct CreateMapRequest { + name: String, + #[serde(flatten)] + map: PlanetwarsMap, +} + +pub async fn create_map( + mut conn: DatabaseConnection, + Extension(config): Extension>, + params: Json, +) -> Result, (StatusCode, String)> { + match db::maps::find_map_by_name(¶ms.name, &mut conn).optional() { + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + json!({ + "error": "internal error" + }) + .to_string(), + )) + } + Ok(Some(_)) => { + return Err(( + StatusCode::BAD_REQUEST, + json!({ + "error": "name taken", + }) + .to_string(), + )) + } + Ok(None) => {} + }; + + if let Err(error) = check_map_name(¶ms.name) { + return Err(( + StatusCode::BAD_REQUEST, + json!({ + "error": error, + }) + .to_string(), + )); + } + + if let Err(error) = check_map(¶ms.map) { + return Err(( + StatusCode::BAD_REQUEST, + json!({ + "error": error, + }) + .to_string(), + )); + } + + let rel_map_path = format!("{}.json", ¶ms.name); + + { + let full_map_path = PathBuf::from(&config.maps_directory).join(&rel_map_path); + let file = File::create(full_map_path).expect("failed to open file"); + serde_json::to_writer_pretty(file, ¶ms.map).expect("failed to write map"); + } + let map = db::maps::create_map( + db::maps::NewMap { + name: ¶ms.name, + file_path: &rel_map_path, + }, + &mut conn, + ) + .expect("failed to save map"); + + Ok(Json(map_into_api_map(map))) +} + +fn check_map(map: &PlanetwarsMap) -> Result<(), &str> { + let unique_names: HashSet = map.planets.iter().map(|p| p.name.clone()).collect(); + if unique_names.len() != map.planets.len() { + return Err("planet names not unique"); + } + let players: HashSet = map.planets.iter().filter_map(|p| p.owner).collect(); + + if players != HashSet::from([1, 2]) { + return Err("maps should have player 1 and 2"); + } + + Ok(()) +} + +// TODO: remove duplication (bot name, user name) +fn check_map_name(name: &str) -> Result<(), &str> { + if !name + .chars() + .all(|c| !c.is_uppercase() && (c.is_ascii_alphanumeric() || c == '_' || c == '-')) + { + return Err("Only [a-z-_] are allowed in map names"); + } + + if name.len() < 3 || name.len() > 32 { + return Err("map name should be between 3 and 32 characters"); + } + + Ok(()) +}