diff --git a/README.md b/README.md index 296e87a..16f288a 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,11 @@ rustup target add wasm32-unknown-unknown 3. `trunk build --release` in `/client` 4. `cargo run --release` in `/server` -## HTTPS - -If you build the server in `--release` mode, it will force HTTPS using a self-signed certificate. - -Optionally, specify `--certificate-path` and `--private-key-path` to use a trusted CA certificate (e.g. acquired via [Let's Encrypt](https://letsencrypt.org/)). The server will periodically check for and load renewed certificates. - ## Official Server(s) To avoid potential visibility-cheating, you are prohibited from using the open-source -client to play on official Kiomet server(s). \ No newline at end of file +client to play on official Kiomet server(s). + +## Trademark + +Kiomet is a trademark of Softbear, Inc. \ No newline at end of file diff --git a/assets/branding/app.png b/assets/branding/app.png new file mode 100644 index 0000000..7d1f6f9 Binary files /dev/null and b/assets/branding/app.png differ diff --git a/client/Cargo.toml b/client/Cargo.toml index d4a6183..04c062d 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -25,7 +25,7 @@ sprite_sheet = { path = "../engine/sprite_sheet" } strum = { version = "0.24.1" } stylist = { version = "0.12.1", default-features = false } yew = "0.20" -yew_frontend = { path = "../engine/yew_frontend" } +yew_frontend = { path = "../engine/yew_frontend", features = ["audio"] } yew_icons = { version = "0.7", features = [ "BootstrapExclamationTriangleFill", "FontAwesomeSolidLocationCrosshairs", diff --git a/common/src/protocol.rs b/common/src/protocol.rs index 3125861..80c7e2c 100644 --- a/common/src/protocol.rs +++ b/common/src/protocol.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; pub use diff::Diff; -#[derive(Clone, Encode, Decode)] +#[derive(Clone, Debug, Encode, Decode)] pub enum Command { Alliance { with: PlayerId, diff --git a/engine/core_protocol/src/plasma.rs b/engine/core_protocol/src/plasma.rs index 03c2c12..ca7d6fb 100644 --- a/engine/core_protocol/src/plasma.rs +++ b/engine/core_protocol/src/plasma.rs @@ -167,7 +167,10 @@ pub enum PlasmaRequestV1 { #[cfg_attr(feature = "server", rtype(result = "()"))] pub enum PlasmaUpdate { /// Version 1 protocol. - V1(Box<[PlasmaUpdateV1]>), + V1( + #[serde(deserialize_with = "crate::serde_util::box_slice_skip_invalid")] + Box<[PlasmaUpdateV1]>, + ), } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/engine/core_protocol/src/serde_util.rs b/engine/core_protocol/src/serde_util.rs index 25bc6f2..9cd30d9 100644 --- a/engine/core_protocol/src/serde_util.rs +++ b/engine/core_protocol/src/serde_util.rs @@ -4,6 +4,7 @@ use serde::de::Visitor; use serde::{de, Deserialize, Deserializer}; use std::fmt; +use std::marker::PhantomData; pub fn is_default(x: &T) -> bool { x == &T::default() @@ -168,3 +169,43 @@ impl<'de> Visitor<'de> for StrVisitor { Ok(String::from(value)) } } + +pub fn box_slice_skip_invalid<'de, T: Deserialize<'de>, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(serde::Deserialize)] + #[serde(untagged, bound(deserialize = "T: Deserialize<'de>"))] + enum Item { + Valid(T), + Skip(serde::de::IgnoredAny), + } + + struct SeqVisitor(PhantomData); + + impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for SeqVisitor { + type Value = Box<[T]>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut values = Vec::with_capacity(seq.size_hint().unwrap_or_default().min(32)); + while let Some(value) = seq.next_element()? { + if let Item::::Valid(value) = value { + values.push(value); + } + } + Ok(values.into_boxed_slice()) + } + } + + let visitor = SeqVisitor::(PhantomData); + deserializer.deserialize_seq(visitor) +} diff --git a/engine/game_server/Cargo.toml b/engine/game_server/Cargo.toml index 1c0bceb..cbacb50 100644 --- a/engine/game_server/Cargo.toml +++ b/engine/game_server/Cargo.toml @@ -2,7 +2,7 @@ name = "game_server" workspace = ".." version = "0.1.0" -edition = "2018" +edition = "2021" authors = ["Softbear, Inc."] license = "AGPL-3.0-or-later" diff --git a/engine/game_server/src/bot.rs b/engine/game_server/src/bot.rs index efd2534..824cca2 100644 --- a/engine/game_server/src/bot.rs +++ b/engine/game_server/src/bot.rs @@ -20,7 +20,7 @@ impl BotData { Self { bot: G::Bot::default(), player_tuple: Arc::new(player_tuple), - action_buffer: BotAction::None, + action_buffer: BotAction::default(), } } } @@ -72,7 +72,8 @@ impl BotRepo { update, bot_data.player_tuple.player.borrow().player_id, players, - ) + ); + //println!("{:?}", bot_data.action_buffer); }); } @@ -83,7 +84,7 @@ impl BotRepo { BotAction::Some(command) => { let _ = service.player_command(command, &bot_data.player_tuple, players); } - BotAction::None => {} + BotAction::None(_) => {} BotAction::Quit => { // Recycle. service.player_left(&bot_data.player_tuple, players); diff --git a/engine/game_server/src/client.rs b/engine/game_server/src/client.rs index 0b63e27..bd39f29 100644 --- a/engine/game_server/src/client.rs +++ b/engine/game_server/src/client.rs @@ -128,7 +128,7 @@ impl ClientRepo { let player_tuple = match players.get(player_id) { Some(player_tuple) => player_tuple, None => { - debug_assert!(false, "client gone in register"); + debug_assert!(false, "client {player_id:?} gone in register"); return; } }; @@ -972,6 +972,7 @@ impl Handler, Updat _ctx: &mut Self::Context, ) { let Some(context_service) = self.arenas.get_mut(msg.realm_name) else { + error!("missing realm {:?}", msg.realm_name); match msg.body { ObserverMessageBody::Register { observer, .. } => { let _ = observer.send(ObserverUpdate::Close); @@ -995,7 +996,7 @@ impl Handler, Updat #[cfg(feature = "teams")] &mut context_service.context.teams, &context_service.context.chat, - &self.leaderboard, + &context_service.context.leaderboard, &context_service.context.liveboard, &mut self.metrics, &self.system, @@ -1103,6 +1104,7 @@ impl Handler for Infrastructure { let realm_name = msg.realm_name; let Some(context_service) = self.arenas.get_mut(realm_name) else { + log::warn!("no arena {realm_name:?}"); return Err("no such arena"); }; let arena_token = context_service.context.token; diff --git a/engine/game_server/src/context.rs b/engine/game_server/src/context.rs index be7d8f1..bda5b53 100644 --- a/engine/game_server/src/context.rs +++ b/engine/game_server/src/context.rs @@ -1,13 +1,13 @@ // SPDX-FileCopyrightText: 2021 Softbear, Inc. // SPDX-License-Identifier: AGPL-3.0-or-later -use crate::bot::BotRepo; use crate::chat::ChatRepo; use crate::game_service::GameArenaService; use crate::liveboard::LiveboardRepo; use crate::player::PlayerRepo; #[cfg(feature = "teams")] use crate::team::TeamRepo; +use crate::{bot::BotRepo, leaderboard::LeaderboardRepo}; use core_protocol::ArenaToken; use rand::{thread_rng, Rng}; @@ -20,6 +20,7 @@ pub struct Context { #[cfg(feature = "teams")] pub teams: TeamRepo, pub(crate) liveboard: LiveboardRepo, + pub(crate) leaderboard: LeaderboardRepo, } impl Context { @@ -32,6 +33,7 @@ impl Context { teams: TeamRepo::default(), chat: ChatRepo::new(chat_log), liveboard: LiveboardRepo::default(), + leaderboard: LeaderboardRepo::default(), } } } diff --git a/engine/game_server/src/context_service.rs b/engine/game_server/src/context_service.rs index 8ab4c2a..769d6cc 100644 --- a/engine/game_server/src/context_service.rs +++ b/engine/game_server/src/context_service.rs @@ -6,7 +6,6 @@ use crate::client::ClientRepo; use crate::context::Context; use crate::game_service::GameArenaService; use crate::invitation::InvitationRepo; -use crate::leaderboard::LeaderboardRepo; use crate::metric::MetricRepo; use crate::plasma::PlasmaClient; use core_protocol::dto::ServerDto; @@ -39,7 +38,6 @@ impl ContextService { pub(crate) fn update( &mut self, clients: &mut ClientRepo, - leaderboard: &mut LeaderboardRepo, invitations: &mut InvitationRepo, metrics: &mut MetricRepo, server_delta: &Option<(Arc<[ServerDto]>, Arc<[ServerNumber]>)>, @@ -78,18 +76,22 @@ impl ContextService { #[cfg(feature = "teams")] &mut self.context.teams, &mut self.context.liveboard, - leaderboard, + &self.context.leaderboard, server_delta, ); self.context .bots .update(&self.service, &self.context.players); - leaderboard.process(&self.context.liveboard, &self.context.players); + self.context + .leaderboard + .process(&self.context.liveboard, &self.context.players); // Post-update game logic. self.service.post_update(&mut self.context); + self.context.leaderboard.clear_deltas(); + // Bot commands/joining/leaving, postponed because no commands should be issued between // `GameService::tick` and `GameService::post_update`. self.context diff --git a/engine/game_server/src/entry_point.rs b/engine/game_server/src/entry_point.rs index 854cadc..7f7f4ed 100644 --- a/engine/game_server/src/entry_point.rs +++ b/engine/game_server/src/entry_point.rs @@ -37,15 +37,20 @@ use server_util::observer::{ObserverMessage, ObserverMessageBody, ObserverUpdate use server_util::os::set_open_file_limit; use server_util::rate_limiter::{RateLimiterProps, RateLimiterState}; use server_util::user_agent::UserAgent; -use std::convert::TryInto; -use std::fs::File; -use std::io::Write; -use std::net::{IpAddr, SocketAddr}; -use std::num::NonZeroU64; -use std::str::FromStr; -use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant}; +use std::{ + collections::HashMap, + convert::TryInto, + fs::File, + io::Write, + net::{IpAddr, SocketAddr}, + num::NonZeroU64, + str::FromStr, + sync::{ + atomic::{AtomicU64, AtomicU8, Ordering}, + Arc, Mutex, RwLock, + }, + time::{Duration, Instant}, +}; use structopt::StructOpt; use tower::ServiceBuilder; use tower_http::cors::CorsLayer; @@ -57,6 +62,7 @@ static REDIRECT_TO_SERVER_ID: AtomicU8 = AtomicU8::new(0); static SERVER_TOKEN: AtomicU64 = AtomicU64::new(0); lazy_static::lazy_static! { + static ref REALM_ROUTES: Mutex> = Mutex::default(); // Will be overwritten first thing. static ref HTTP_RATE_LIMITER: Mutex = Mutex::new(IpRateLimiter::new_bandwidth_limiter(1, 0)); } @@ -117,6 +123,61 @@ where } } +struct ExtractRealmName(RealmName); + +impl ExtractRealmName { + fn parse(domain: &str) -> Option { + if domain.bytes().filter(|&b| b == b'.').count() < 2 { + return None; + } + domain + .split('.') + .next() + .filter(|&host| usize::from_str(host).is_err() && host != "www") + .and_then(|host| RealmName::from_str(host).ok()) + } +} + +enum ExtractRealmNameError { + Missing, + Invalid, +} + +impl IntoResponse for ExtractRealmNameError { + fn into_response(self) -> Response { + ( + StatusCode::UNAUTHORIZED, + match self { + Self::Missing => "missing realm name", + Self::Invalid => "invalid realm name", + }, + ) + .into_response() + } +} + +#[async_trait::async_trait] +impl FromRequestParts for ExtractRealmName +where + S: Send + Sync, +{ + type Rejection = ExtractRealmNameError; + + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + state: &S, + ) -> Result { + let host = TypedHeader::::from_request_parts(parts, state) + .await + .map_err(|_| ExtractRealmNameError::Missing)?; + if let Some(realm_name) = Self::parse(host.hostname()) { + Ok(Self(realm_name)) + } else { + Err(ExtractRealmNameError::Invalid) + } + } +} + pub fn entry_point(game_client: MiniCdn, browser_router: bool) where ::GameUpdate: std::fmt::Debug, @@ -168,12 +229,12 @@ where }; let game_client = Arc::new(RwLock::new(game_client)); - let domain = options.domain.map(|domain| &*Box::leak(domain.into_boxed_str())); let srv = Infrastructure::::start( Infrastructure::new( server_id, &REDIRECT_TO_SERVER_ID, + &REALM_ROUTES, static_hash, ip_address.and_then(|ip| if let IpAddr::V4(ipv4_address) = ip { Some(ipv4_address) @@ -216,20 +277,6 @@ where let plasma_srv = srv.to_owned(); let system_srv = srv.to_owned(); - #[cfg(not(debug_assertions))] - let domain_clone_cors = domain.as_ref().map(|d| { - [ - format!("://{}", d), - format!(".{}", d), - String::from("http://localhost:8080"), - String::from("https://localhost:8443"), - String::from("http://localhost:80"), - String::from("https://localhost:443"), - String::from("https://softbear.com"), - String::from("https://www.softbear.com"), - ] - }); - let admin_router = post( move |_: Authenticated, request: Json| { let srv_clone_admin = admin_srv.clone(); @@ -252,7 +299,7 @@ where let app = Router::new() .fallback_service(get(StaticFilesHandler{cdn: game_client, prefix: "", browser_router})) - .route("/ws", axum::routing::get(async move |upgrade: WebSocketUpgrade, ConnectInfo(addr): ConnectInfo, user_agent: Option>, Query(query): Query| { + .route("/ws", axum::routing::get(async move |upgrade: WebSocketUpgrade, ConnectInfo(addr): ConnectInfo, user_agent: Option>, realm_name: Option, Query(query): Query| { let user_agent_id = user_agent .map(|h| UserAgent::new(h.as_str())) .and_then(UserAgent::into_id); @@ -263,7 +310,7 @@ where ip_address: addr.ip(), referrer: query.referrer, user_agent_id, - realm_name: None, // TODO + realm_name: realm_name.map(|e| e.0), player_id_token: query.player_id.zip(query.token), session_token: query.session_token, date_created: query.date_created.filter(|&d| d > 1680570365768 && d <= now).unwrap_or(now), @@ -280,7 +327,7 @@ where Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()), Ok(result) => match result { // Currently, if authentication fails, it was due to rate limit. - Err(_) => Err(StatusCode::TOO_MANY_REQUESTS.into_response()), + Err(e) => Err((StatusCode::TOO_MANY_REQUESTS, e).into_response()), Ok((realm_name, player_id)) => Ok(upgrade .max_frame_size(MAX_MESSAGE_SIZE) .max_message_size(MAX_MESSAGE_SIZE) @@ -494,18 +541,22 @@ where .get("host") .and_then(|host| host.to_str().ok()) - .and_then(|host| host.split('.').next()) - .filter(|host| usize::from_str(host).is_err()) - .and_then(|host| RealmName::from_str(host).ok()); - if let Some(realm_name) = realm_name { - info!("connected to realm {realm_name:?}"); - } - if let Some((domain, server_id)) = domain - .as_ref() - .zip(ServerNumber::new(REDIRECT_TO_SERVER_ID.load(Ordering::Relaxed))) + .and_then(ExtractRealmName::parse); + + if let Some(server_number) = + realm_name + .and_then(|realm_name| + REALM_ROUTES + .lock() + .unwrap() + .get(&realm_name) + .copied() + .filter(|&server_number| server_number != server_id.number || server_id.kind.is_local()) + ) + .or(ServerNumber::new(REDIRECT_TO_SERVER_ID.load(Ordering::Relaxed))) { let scheme = request.uri().scheme().cloned().unwrap_or(Scheme::HTTPS); - if let Ok(authority) = Authority::from_str(&format!("{}.{}", server_id.0.get(), domain)) { + if let Ok(authority) = Authority::from_str(&format!("{}.{}", server_number.0.get(), G::GAME_ID.domain())) { let mut builder = Uri::builder() .scheme(scheme) .authority(authority); @@ -541,19 +592,26 @@ where .layer(ServiceBuilder::new() .layer(CorsLayer::new() .allow_origin(tower_http::cors::AllowOrigin::predicate(move |origin, _parts| { - #[cfg(debug_assertions)] - { - let _ = origin; - true + if cfg!(debug_assertions) { + true + } else { + let Ok(origin) = std::str::from_utf8(origin.as_bytes()) else { + return false; + }; + + let origin = origin + .trim_start_matches("http://") + .trim_start_matches("https://"); + + for domain in [G::GAME_ID.domain(), "localhost:8080", "localhost:8443", "localhost:80", "localhost:443", "softbear.com"] { + if let Some(prefix) = origin.strip_suffix(domain) { + if prefix.is_empty() || prefix.ends_with('.') { + return true; + } + } } - #[cfg(not(debug_assertions))] - if let Some(domains) = domain_clone_cors.as_ref() { - domains.iter().any(|domain| { - origin.as_bytes().ends_with(domain.as_bytes()) - }) - } else { - true + false } })) .allow_headers(tower_http::cors::Any) diff --git a/engine/game_server/src/game_service.rs b/engine/game_server/src/game_service.rs index 17d844a..f10be6c 100644 --- a/engine/game_server/src/game_service.rs +++ b/engine/game_server/src/game_service.rs @@ -38,7 +38,7 @@ pub trait GameArenaService: 'static + Unpin + Sized + Send + Sync { type Bot: 'static + Bot; type ClientData: 'static + Default + Debug + Unpin + Send + Sync; type GameUpdate: 'static + Sync + Send + Encode + Decode; - type GameRequest: 'static + Decode + Send + Unpin; + type GameRequest: 'static + Debug + Decode + Send + Unpin; type PlayerData: 'static + Default + Unpin + Send + Sync + Debug; type PlayerExtension: 'static + Default + Unpin + Send + Sync; @@ -174,13 +174,13 @@ pub trait Bot: Default + Unpin + Sized + Send { #[derive(Debug)] pub enum BotAction { Some(GR), - None, + None(&'static str), Quit, } impl Default for BotAction { fn default() -> Self { - Self::None + Self::None("default") } } @@ -204,7 +204,7 @@ impl Bot for () { _player_id: PlayerId, _players: &'a PlayerRepo, ) -> BotAction { - BotAction::None + BotAction::default() } } @@ -233,7 +233,7 @@ impl Bot for MockGameBot { _player_id: PlayerId, _players: &PlayerRepo, ) -> BotAction<::GameRequest> { - BotAction::None + BotAction::default() } } diff --git a/engine/game_server/src/infrastructure.rs b/engine/game_server/src/infrastructure.rs index 3a3c058..a708d5d 100644 --- a/engine/game_server/src/infrastructure.rs +++ b/engine/game_server/src/infrastructure.rs @@ -14,17 +14,18 @@ use crate::system::SystemRepo; use actix::AsyncContext; use actix::{Actor, Context as ActorContext}; use core_protocol::id::{ClientHash, RegionId, ServerId}; -use core_protocol::{PlasmaRequestV1, PlasmaUpdate}; +use core_protocol::{PlasmaRequestV1, PlasmaUpdate, RealmName, ServerNumber}; use futures::stream::FuturesUnordered; use log::{error, info}; use minicdn::MiniCdn; use server_util::health::Health; use server_util::rate_limiter::RateLimiterProps; +use std::collections::HashMap; use std::future::Future; use std::net::Ipv4Addr; use std::pin::Pin; use std::sync::atomic::{AtomicU64, AtomicU8}; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant}; /// An entire game server. @@ -46,8 +47,6 @@ pub struct Infrastructure { pub(crate) invitations: InvitationRepo, /// Shared admin interface. pub(crate) admin: AdminRepo, - /// Shared leaderboard. - pub(crate) leaderboard: LeaderboardRepo, /// Shared metrics. pub(crate) metrics: MetricRepo, @@ -103,6 +102,7 @@ impl Infrastructure { pub async fn new( server_id: ServerId, redirect_server_number: &'static AtomicU8, + realm_routes: &'static Mutex>, client_hash: ClientHash, ipv4_address: Option, region_id: Option, @@ -120,7 +120,7 @@ impl Infrastructure { ipv4_address, region_id, clients: ClientRepo::new(trace_log, client_authenticate), - plasma: PlasmaClient::new(redirect_server_number, server_token), + plasma: PlasmaClient::new(redirect_server_number, realm_routes, server_token), system: SystemRepo::new(), admin: AdminRepo::new(game_client, client_hash), arenas: ArenaRepo::new(ContextService::new( @@ -131,7 +131,6 @@ impl Infrastructure { )), health: Health::default(), invitations: InvitationRepo::default(), - leaderboard: LeaderboardRepo::default(), metrics: MetricRepo::new(), last_update: Instant::now(), } @@ -151,7 +150,6 @@ impl Infrastructure { for (_, context_service) in self.arenas.iter_mut() { context_service.update( &mut self.clients, - &mut self.leaderboard, &mut self.invitations, &mut self.metrics, &server_delta, @@ -159,7 +157,7 @@ impl Infrastructure { &self.plasma, ); } - self.leaderboard.clear_deltas(); + self.health.record_tick(G::TICK_PERIOD_SECS); // These are all rate-limited internally. diff --git a/engine/game_server/src/leaderboard.rs b/engine/game_server/src/leaderboard.rs index 401bc60..44c34b9 100644 --- a/engine/game_server/src/leaderboard.rs +++ b/engine/game_server/src/leaderboard.rs @@ -116,15 +116,17 @@ impl LeaderboardRepo { } pub fn update_to_plasma(infrastructure: &mut Infrastructure) { - if let Some(scores) = infrastructure.leaderboard.take_pending() { - infrastructure - .plasma - .do_request(PlasmaRequestV1::UpdateLeaderboards { - game_id: G::GAME_ID, - server_id: infrastructure.server_id, - realm_name: None, - scores, - }); + for (realm_name, context_service) in infrastructure.arenas.iter_mut() { + if let Some(scores) = context_service.context.leaderboard.take_pending() { + infrastructure + .plasma + .do_request(PlasmaRequestV1::UpdateLeaderboards { + game_id: G::GAME_ID, + server_id: infrastructure.server_id, + realm_name, + scores, + }); + } } } diff --git a/engine/game_server/src/options.rs b/engine/game_server/src/options.rs index 90d800e..4dc4afa 100644 --- a/engine/game_server/src/options.rs +++ b/engine/game_server/src/options.rs @@ -55,6 +55,7 @@ pub struct Options { pub region_id: Option, /// Domain (without server id prepended). #[allow(dead_code)] + #[deprecated = "now from game id"] #[structopt(long)] pub domain: Option, /// Certificate chain path. diff --git a/engine/game_server/src/plasma.rs b/engine/game_server/src/plasma.rs index c7784dc..91d92d9 100644 --- a/engine/game_server/src/plasma.rs +++ b/engine/game_server/src/plasma.rs @@ -3,13 +3,15 @@ use actix::{Handler, Recipient}; use axum::http::Method; use core_protocol::{ ArenaToken, ClientHash, GameId, PlasmaRequest, PlasmaRequestV1, PlasmaUpdate, PlasmaUpdateV1, - RegionId, ServerId, ServerRole, + RealmName, RegionId, ServerId, ServerNumber, ServerRole, }; use log::{info, warn}; use reqwest::Client; +use std::collections::HashMap; use std::future::Future; use std::net::Ipv4Addr; use std::sync::atomic::AtomicU8; +use std::sync::Mutex; use std::time::Instant; use std::{ sync::atomic::{AtomicU64, Ordering}, @@ -18,7 +20,8 @@ use std::{ pub(crate) struct PlasmaClient { redirect_server_number: &'static AtomicU8, - pub token: &'static AtomicU64, + realm_routes: &'static Mutex>, + pub server_token: &'static AtomicU64, pub role: ServerRole, client: Client, infrastructure: Option>, @@ -31,11 +34,13 @@ pub(crate) struct PlasmaClient { impl PlasmaClient { pub(crate) fn new( redirect_server_number: &'static AtomicU8, + realm_routes: &'static Mutex>, server_token: &'static AtomicU64, ) -> Self { Self { redirect_server_number, - token: server_token, + realm_routes, + server_token, role: ServerRole::Unlisted, client: Client::builder() .timeout(Duration::from_secs(15)) @@ -115,7 +120,7 @@ impl PlasmaClient { &self, request: PlasmaRequestV1, ) -> impl Future> + Send { - Self::request_impl(request, &self.client, self.token) + Self::request_impl(request, &self.client, self.server_token) } pub(crate) fn request_impl( @@ -179,7 +184,7 @@ impl PlasmaClient { pub(crate) fn do_requests(&self, requests: Vec) { let client = self.client.clone(); - let token = self.token; + let token = self.server_token; let infrastructure = self.infrastructure.clone(); tokio::spawn(async move { @@ -220,7 +225,9 @@ impl Handler for Infrastructure { match update { PlasmaUpdateV1::ConfigServer { token, role } => { if let Some(token) = token { - self.plasma.token.store(token.0.get(), Ordering::Relaxed); + self.plasma + .server_token + .store(token.0.get(), Ordering::Relaxed); } if let Some(role) = role { self.plasma.set_role(role); @@ -263,9 +270,9 @@ impl Handler for Infrastructure { leaderboards, realm_name, } => { - if realm_name.is_none() { - for (period_id, leaderboard) in Vec::from(leaderboards) { - self.leaderboard.put_leaderboard(period_id, leaderboard); + for (period_id, scores) in Vec::from(leaderboards) { + if let Some(realm) = self.arenas.get_mut(realm_name) { + realm.context.leaderboard.put_leaderboard(period_id, scores); } } } @@ -274,13 +281,24 @@ impl Handler for Infrastructure { scores, realm_name, } => { - if realm_name.is_none() { - self.leaderboard.put_leaderboard(period_id, scores); + if let Some(realm) = self.arenas.get_mut(realm_name) { + realm.context.leaderboard.put_leaderboard(period_id, scores); } } PlasmaUpdateV1::Servers { servers } => { self.system.servers = servers; } + PlasmaUpdateV1::Realms { added, removed } => { + let mut routes = self.plasma.realm_routes.lock().unwrap(); + for removed in removed.iter() { + routes.remove(removed); + } + for added in added.iter() { + if let Some(server_number) = added.server_number { + routes.insert(added.realm_name, server_number); + } + } + } _ => {} } } diff --git a/engine/game_terraform/server_init.sh b/engine/game_terraform/server_init.sh index 6afabb8..6ed446e 100644 --- a/engine/game_terraform/server_init.sh +++ b/engine/game_terraform/server_init.sh @@ -256,7 +256,6 @@ WorkingDirectory=~ ExecStart=/root/server \ --server-id $SERVER_ID \ --ip-address $IP_ADDRESS \ - --domain $DOMAIN \ --chat-log /root/chat.log \ --trace-log /root/trace.log \ --certificate-path /etc/letsencrypt/live/$DOMAIN/fullchain.pem \ diff --git a/server/src/bot.rs b/server/src/bot.rs index 7674d56..977b1df 100644 --- a/server/src/bot.rs +++ b/server/src/bot.rs @@ -86,7 +86,7 @@ impl Bot for TowerBot { players: &'a PlayerRepo, ) -> BotAction<::GameRequest> { let Some(input) = input else { - return BotAction::None; + return BotAction::None("no input"); }; let player = match players.borrow_player(player_id) { Some(player) => player, @@ -101,17 +101,27 @@ impl Bot for TowerBot { return BotAction::Some(Command::Spawn); } - // Don't crash if ruler is on the run and enemy is hot on it's tail. - if player.towers.is_empty() { - return BotAction::None; - } + let Some((random_tower_id, random_tower)) + = player + .towers + .iter() + .filter_map(|&tower_id| + input + .world + .chunk + .get(tower_id) + .filter(|tower| !tower.force_units().is_empty()) + .map(|tower| (tower_id, tower)) + ) + .choose(&mut rng) else { + // Don't crash if ruler is on the run and enemy is hot on it's tail. + return BotAction::None("no towers"); + }; let world_player = input.world.player(player_id); - let random_tower_id = *player.towers.iter().choose(&mut rng).unwrap(); - - self.before_quit = if let Some(before_quit) = self.before_quit.checked_sub(Ticks::ONE) { - before_quit + if let Some(before_quit) = self.before_quit.checked_sub(Ticks::ONE) { + self.before_quit = before_quit; } else { // We are close to the world center, so leave and make room for real players. println!("bot quitting with {} towers", player.towers.len()); @@ -133,218 +143,224 @@ impl Bot for TowerBot { }) }); - if let Some(random_tower) = input.world.chunk.get(random_tower_id) { - // Check if can upgrade. Require more shield if in war. - let min_shield = random_tower.tower_type.raw_unit_capacity(Unit::Shield) - / (1 + self.war.is_none() as usize); + // Check if can upgrade. Require more shield if in war. + let min_shield = random_tower.tower_type.raw_unit_capacity(Unit::Shield) + / (1 + self.war.is_none() as usize); - if random_tower.units.available(Unit::Shield) >= min_shield { - if let Some(tower_type) = random_tower - .tower_type - .upgrades() - .filter(|u| { - u.has_prerequisites(&player.tower_counts) - && !matches!(u, TowerType::Helipad) - }) - .choose(&mut rng) - { - return BotAction::Some(Command::Upgrade { - tower_id: random_tower_id, - tower_type, - }); - } + if random_tower.units.available(Unit::Shield) >= min_shield { + if let Some(tower_type) = random_tower + .tower_type + .upgrades() + .filter(|u| { + u.has_prerequisites(&player.tower_counts) && !matches!(u, TowerType::Helipad) + }) + .choose(&mut rng) + { + return BotAction::Some(Command::Upgrade { + tower_id: random_tower_id, + tower_type, + }); + } + } + + // Recompute war. + if self.war.is_none() && rng.gen_bool(0.005) { + #[derive(PartialEq)] + struct WarTarget { + player_id: PlayerId, + alias: PlayerAlias, + towers: u32, } - // Recompute war. - if self.war.is_none() && rng.gen_bool(0.005) { - #[derive(PartialEq)] - struct WarTarget { - player_id: PlayerId, - alias: PlayerAlias, - towers: u32, + impl PartialOrd for WarTarget { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.towers.cmp(&other.towers)) } + } - impl PartialOrd for WarTarget { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.towers.cmp(&other.towers)) + let mut best_target = Option::::None; + for (_, tower) in input.world.chunk.iter_towers_square(random_tower_id, 5) { + if let Some(enemy_id) = tower.player_id { + if enemy_id == player_id || enemy_id.is_bot() { + // Not enemy. + continue; } - } - let mut best_target = Option::::None; - for (_, tower) in input.world.chunk.iter_towers_square(random_tower_id, 5) { - if let Some(enemy_id) = tower.player_id { - if enemy_id == player_id || enemy_id.is_bot() { - // Not enemy. + if let Some(enemy) = players.borrow_player(enemy_id) { + if enemy.towers.len() / 8 <= self.territorial_ambition as usize { continue; } - if let Some(enemy) = players.borrow_player(enemy_id) { - if enemy.towers.len() / 8 <= self.territorial_ambition as usize { - continue; - } - - let target = Some(WarTarget { - player_id: enemy_id, - alias: enemy.alias(), - towers: enemy.towers.len() as u32, - }); - if target > best_target { - best_target = target; - } - } - } - } - if let Some(best_target) = best_target { - println!( - "BOT {} ({:?}) declaring WAR on {} ({:?})", - player.alias(), - player_id, - best_target.alias, - best_target.player_id - ); - self.war = Some(War { - against: best_target.player_id, - remaining: Ticks::from_whole_secs(if best_target.towers > 500 { - 360 - } else { - 180 - }), - }); - if world_player.allies.contains(&best_target.player_id) { - return BotAction::Some(Command::Alliance { - with: best_target.player_id, - break_alliance: true, + let target = Some(WarTarget { + player_id: enemy_id, + alias: enemy.alias(), + towers: enemy.towers.len() as u32, }); + if target > best_target { + best_target = target; + } } } } - - // Contemplate entering an alliance. - if rng.gen_bool(0.0025) { - let with = input - .world - .chunk - .iter_towers_square(random_tower_id, 4) - .find_map(|(_, candidate_destination_tower)| { - candidate_destination_tower - .player_id - .and_then(|player_id| players.borrow_player(player_id)) - .filter(|player| { - /* !player.is_bot() - && */ - player.towers.len() / 8 <= self.territorial_ambition as usize - && ((player.player_id.0.get() ^ player_id.0.get()) & 0b1 == 0) - && !world_player.allies.contains(&player.player_id) - }) - .map(|player| player.player_id) - }); - if let Some(with) = with { + if let Some(best_target) = best_target { + println!( + "BOT {} ({:?}) declaring WAR on {} ({:?})", + player.alias(), + player_id, + best_target.alias, + best_target.player_id + ); + self.war = Some(War { + against: best_target.player_id, + remaining: Ticks::from_whole_secs(if best_target.towers > 500 { + 360 + } else { + 180 + }), + }); + if world_player.allies.contains(&best_target.player_id) { return BotAction::Some(Command::Alliance { - with, - break_alliance: false, + with: best_target.player_id, + break_alliance: true, }); } } + } - // Contemplate dispatching a force. - let strength = random_tower.force_units(); - if !strength.is_empty() { - // Whether ruler would be part of force. - let sending_ruler = strength.contains(Unit::Ruler); + // Contemplate entering an alliance. + if rng.gen_bool(0.0025) { + let with = input + .world + .chunk + .iter_towers_square(random_tower_id, 4) + .find_map(|(_, candidate_destination_tower)| { + candidate_destination_tower + .player_id + .and_then(|player_id| players.borrow_player(player_id)) + .filter(|player| { + /* !player.is_bot() + && */ + player.towers.len() / 8 <= self.territorial_ambition as usize + && ((player.player_id.0.get() ^ player_id.0.get()) & 0b1 == 0) + && !world_player.allies.contains(&player.player_id) + }) + .map(|player| player.player_id) + }); + if let Some(with) = with { + return BotAction::Some(Command::Alliance { + with, + break_alliance: false, + }); + } + } - // Whether this force will do significant damage as opposed to bouncing. - let formidable = { - let mut total_damage = 0u32; - for unit_damage in strength.iter().map(|(unit, count)| { - Unit::damage_to_finite( - unit.damage(unit.field(false, true, false), Field::Surface), - ) - .saturating_mul(count as u32) - }) { - total_damage = total_damage.saturating_add(unit_damage); - } - total_damage >= 5 - }; + // Contemplate dispatching a force. + let strength = random_tower.force_units(); + if !strength.is_empty() { + // Whether ruler would be part of force. + let sending_ruler = strength.contains(Unit::Ruler); - let destination = input - .world - .chunk - .iter_towers_square(random_tower_id, 5) - .filter(|&(_, candidate_destination_tower)| { - if candidate_destination_tower.player_id == Some(player_id) { - // Can shuffle units if not at war or to protect ruler while at war. - self.war.is_none() - || (sending_ruler - && candidate_destination_tower.player_id.is_some() - && candidate_destination_tower.units.available(Unit::Shield) - > Unit::damage_to_finite( - candidate_destination_tower - .tower_type - .max_ranged_damage(), - ) as usize) - } else if sending_ruler - || candidate_destination_tower - .player_id - .map(|p| - input - .world - .player(p) - .allies - .contains(&player_id) - && world_player.allies.contains(&p) - ).unwrap_or(false) { - // Cannot send ruler to an unowned tower or forces to an allied tower. - false - } else if let Some(War { against, .. }) = self.war { - // Focus on the adversary (only). - formidable && candidate_destination_tower.player_id == Some(against) - } else { - candidate_destination_tower - .player_id - .and_then(|player_id| players.borrow_player(player_id)) - .map_or( - true, // player.towers.len() < self.territorial_ambition as usize, - |enemy| { + // Whether this force will do significant damage as opposed to bouncing. + let formidable = { + let mut total_damage = 0u32; + for unit_damage in strength.iter().map(|(unit, count)| { + Unit::damage_to_finite( + unit.damage(unit.field(false, true, false), Field::Surface), + ) + .saturating_mul(count as u32) + }) { + total_damage = total_damage.saturating_add(unit_damage); + } + total_damage >= 5 + }; + + let destination = input + .world + .chunk + .iter_towers_square(random_tower_id, 5) + .filter(|&(_, candidate_destination_tower)| { + if candidate_destination_tower.player_id == Some(player_id) { + // Can shuffle units if not at war or to protect ruler while at war. + self.war.is_none() + || (sending_ruler + && candidate_destination_tower.player_id.is_some() + && candidate_destination_tower.units.available(Unit::Shield) + > Unit::damage_to_finite( + candidate_destination_tower + .tower_type + .max_ranged_damage(), + ) as usize) + } else if sending_ruler + || candidate_destination_tower + .player_id + .map(|p| + input + .world + .player(p) + .allies + .contains(&player_id) + && world_player.allies.contains(&p) + ).unwrap_or(false) { + // Cannot send ruler to an unowned tower or forces to an allied tower. + false + } else if let Some(War { against, .. }) = self.war { + // Focus on the adversary (or securing more unclaimed towers). + (formidable && candidate_destination_tower.player_id == Some(against)) || candidate_destination_tower.player_id.is_none() + } else { + candidate_destination_tower + .player_id + .and_then(|player_id| players.borrow_player(player_id)) + .map_or( + true, // player.towers.len() < self.territorial_ambition as usize, + |enemy| { + // They're big; get em! + enemy.towers.len() / 4 > self.territorial_ambition as usize // They're big; get em! - enemy.towers.len() / 4 > self.territorial_ambition as usize - // They're big; get em! - || enemy.score > 1000 - // Don't do too much damage to smol's. - || !formidable - // Recently changed hands? - || candidate_destination_tower.units.available(Unit::Shield) < 5 - }, - ) - } - }) - .choose(&mut rng); + || enemy.score > 1000 + // Don't do too much damage to smol's. + || !formidable + // Recently changed hands? + || candidate_destination_tower.units.available(Unit::Shield) < 5 + }, + ) + } + }) + .choose(&mut rng); - if let Some((destination, _)) = destination { - let max_edge_distance = strength.max_edge_distance(); - let path = input.world.find_best_path( - random_tower_id, - destination, - max_edge_distance, - player_id, - |_| true, - ); + if let Some((destination, _)) = destination { + let max_edge_distance = strength.max_edge_distance(); + let path = input.world.find_best_path( + random_tower_id, + destination, + max_edge_distance, + player_id, + |_| true, + ); - if let Some(path) = path { - return BotAction::Some(if true || rng.gen() { + if let Some(path) = path { + return BotAction::Some( + if sending_ruler + || !random_tower.generates_mobile_units() + || rng.gen_bool(0.75) + { Command::deploy_force_from_path(path) } else { Command::SetSupplyLine { tower_id: path[0], path: Some(Path::new(path)), } - }); - } + }, + ); + } else { + return BotAction::None("no path"); } + } else { + return BotAction::None("no destination"); } } else { - debug_assert!(false, "missing random owned tower"); + return BotAction::None("empty force"); } - BotAction::None + //BotAction::None("no action") } }