diff --git a/crates/game-service/src/home_mode.rs b/crates/game-service/src/home_mode.rs index f7034ba..e9efc73 100644 --- a/crates/game-service/src/home_mode.rs +++ b/crates/game-service/src/home_mode.rs @@ -26,6 +26,7 @@ fn diamonds() -> LogicDataRef { pub struct TurnResult { pub claims: Vec, pub purchases: Vec, + pub matchmake: bool, pub out_of_sync: Option, pub changed: bool, } @@ -214,6 +215,7 @@ impl HomeMode { } } } + CommandOutcome::MatchmakeStarted => result.matchmake = true, CommandOutcome::Applied => result.changed = true, CommandOutcome::Ignored => {} } diff --git a/crates/game-service/src/service.rs b/crates/game-service/src/service.rs index dc4bc82..50b67b5 100644 --- a/crates/game-service/src/service.rs +++ b/crates/game-service/src/service.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use logic::table; +use logic::{table, LogicDataTables}; use logic::{ AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef, LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage, SectorStateMessage, - StartMissionMessage, + StartMissionMessage, StopHomeLogicMessage, }; use service_rpc::{ AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult, @@ -77,6 +77,47 @@ impl GameService { pub fn shop(&self) -> &Arc { &self.shop } + async fn sector_state_for( + &self, + account: AccountRef, + npc: LogicDataRef, + ) -> RpcResult> { + let profile = self.profile(account).await?; + let avatar = build_avatar(&profile); + let location = npc + .data() + .map(|row| LogicDataRef::by_name(table::LOCATIONS, row.string("Location"))) + .unwrap_or_default(); + let battle = self + .battles + .build( + location.clone(), + npc.clone(), + profile.arena.clone(), + vec![avatar.clone(), avatar], + self.config.random_seed, + ) + .map_err(|error| RpcError::Rejected(error.to_string()))?; + let snapshot = battle + .snapshot() + .map_err(|error| RpcError::Rejected(error.to_string()))?; + tracing::info!( + %account, + npc = %npc, + location = %location, + objects = battle.battle.objects.objects.len(), + bytes = snapshot.len(), + "sending a battle sector state" + ); + Ok(vec![encode(&SectorStateMessage::new(snapshot))?]) + } + fn bot_opponent(&self) -> LogicDataRef { + LogicDataTables::instance() + .table(table::NPCS) + .and_then(|rows| rows.get_at(0).cloned()) + .map(LogicDataRef::from) + .unwrap_or_default() + } async fn roller(&self, account: AccountRef) -> RpcResult { let profile = self.profile(account).await?; let fallback: Vec = profile @@ -178,35 +219,21 @@ impl GameApi for GameService { ) -> RpcResult> { let mission = StartMissionMessage::from_bytes(&payload) .map_err(|error| RpcError::Rejected(error.to_string()))?; - let profile = self.profile(account).await?; - let avatar = build_avatar(&profile); - let location = mission - .npc - .data() - .map(|npc| LogicDataRef::by_name(table::LOCATIONS, npc.string("Location"))) - .unwrap_or_default(); - let battle = self - .battles - .build( - location.clone(), - mission.npc.clone(), - profile.arena.clone(), - vec![avatar.clone(), avatar], - self.config.random_seed, - ) - .map_err(|error| RpcError::Rejected(error.to_string()))?; - let snapshot = battle - .snapshot() - .map_err(|error| RpcError::Rejected(error.to_string()))?; - tracing::info!( - %account, - npc = %mission.npc, - location = %location, - objects = battle.battle.objects.objects.len(), - bytes = snapshot.len(), - "sending a battle sector state" - ); - Ok(vec![encode(&SectorStateMessage::new(snapshot))?]) + self.sector_state_for(account, mission.npc).await + } + async fn home_logic_stopped( + &self, + account: AccountRef, + _payload: Vec, + ) -> RpcResult> { + let npc = self.bot_opponent(); + if npc.is_none() { + return Err(RpcError::Rejected( + "npcs.csv has no bot to match against".into(), + )); + } + tracing::info!(%account, bot = %npc, "matchmaking against a bot"); + self.sector_state_for(account, npc).await } async fn end_client_turn( &self, @@ -238,6 +265,10 @@ impl GameApi for GameService { "end client turn" ); let mut replies = Vec::new(); + if result.matchmake { + tracing::info!(%account, "client started matchmaking, stopping home logic"); + replies.push(encode(&StopHomeLogicMessage::default())?); + } if let Some(out_of_sync) = result.out_of_sync { tracing::warn!( %account, @@ -304,6 +335,9 @@ impl RpcService for GameService { GameRequest::StartMission { account, payload } => Ok(GameResponse::messages( self.start_mission(account, payload).await?, )), + GameRequest::HomeLogicStopped { account, payload } => Ok(GameResponse::messages( + self.home_logic_stopped(account, payload).await?, + )), GameRequest::EndClientTurn { account, payload } => Ok(GameResponse::messages( self.end_client_turn(account, payload).await?, )), diff --git a/crates/gateway/src/backend.rs b/crates/gateway/src/backend.rs index aecb753..dd49138 100644 --- a/crates/gateway/src/backend.rs +++ b/crates/gateway/src/backend.rs @@ -100,6 +100,17 @@ impl GameApi for RemoteGame { .await? .into_messages()) } + async fn home_logic_stopped( + &self, + account: AccountRef, + payload: Vec, + ) -> RpcResult> { + Ok(self + .client + .call(&GameRequest::HomeLogicStopped { account, payload }) + .await? + .into_messages()) + } async fn end_client_turn( &self, account: AccountRef, diff --git a/crates/gateway/src/message_manager.rs b/crates/gateway/src/message_manager.rs index 0f0f377..de15b3d 100644 --- a/crates/gateway/src/message_manager.rs +++ b/crates/gateway/src/message_manager.rs @@ -1,8 +1,8 @@ use std::net::SocketAddr; use std::sync::Arc; use logic::{ - message_type, ClientCapabilitiesMessage, GoHomeMessage, KeepAliveServerMessage, - LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage, + message_type, CancelMatchmakeDoneMessage, ClientCapabilitiesMessage, GoHomeMessage, + KeepAliveServerMessage, LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage, }; use service_rpc::{ AccountRef, DeviceInfo, HomeRequestKind, LoginOutcome, Session as AuthSession, WireMessage, @@ -88,6 +88,33 @@ impl MessageManager { let _ = incoming.downcast::(); self.push_home(HomeRequestKind::GoHome).await } + message_type::CANCEL_MATCHMAKE => { + tracing::info!(peer = %self.peer, "client cancelled matchmaking"); + self.sender + .send_message(&CancelMatchmakeDoneMessage::default()) + .await?; + Ok(()) + } + message_type::HOME_LOGIC_STOPPED => { + let Some(account) = self.account else { + return Ok(()); + }; + match self + .backends + .game + .home_logic_stopped(account, incoming.payload) + .await + { + Ok(replies) => self.push_wire_messages(replies).await?, + Err(error) => { + tracing::warn!(peer = %self.peer, %error, "could not match the player"); + self.sender + .send_message(&CancelMatchmakeDoneMessage::default()) + .await?; + } + } + Ok(()) + } message_type::START_MISSION => { let Some(account) = self.account else { return Ok(()); diff --git a/crates/logic/src/commands/command.rs b/crates/logic/src/commands/command.rs index e899360..1900f11 100644 --- a/crates/logic/src/commands/command.rs +++ b/crates/logic/src/commands/command.rs @@ -18,6 +18,7 @@ pub enum CommandOutcome { data: LogicDataRef, count: i32, }, + MatchmakeStarted, Applied, Rejected(&'static str), Ignored, diff --git a/crates/logic/src/commands/matchmake.rs b/crates/logic/src/commands/matchmake.rs index d18ecf2..182190b 100644 --- a/crates/logic/src/commands/matchmake.rs +++ b/crates/logic/src/commands/matchmake.rs @@ -43,6 +43,6 @@ impl Execute for LogicStartMatchmakeCommand { if mode.is_claiming_reward() { return CommandOutcome::Rejected("a reward claim is in progress"); } - CommandOutcome::Ignored + CommandOutcome::MatchmakeStarted } } diff --git a/crates/logic/src/messages/matchmake.rs b/crates/logic/src/messages/matchmake.rs new file mode 100644 index 0000000..280715a --- /dev/null +++ b/crates/logic/src/messages/matchmake.rs @@ -0,0 +1,18 @@ +use titan::Message; +use crate::commands::LogicCommand; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Message)] +#[message(id = 24106, direction = "server", name = "StopHomeLogicMessage")] +pub struct StopHomeLogicMessage {} +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Message)] +#[message(id = 24125, direction = "server", name = "CancelMatchmakeDoneMessage")] +pub struct CancelMatchmakeDoneMessage {} +#[derive(Debug, Default, Message)] +#[message(id = 14105, direction = "client", name = "HomeLogicStoppedMessage")] +#[codec(partial)] +pub struct HomeLogicStoppedMessage { + #[codec(vint)] + pub tick: i32, + #[codec(vint)] + pub checksum: i32, + pub commands: Vec>, +} diff --git a/crates/logic/src/messages/mod.rs b/crates/logic/src/messages/mod.rs index 1160af0..8b54fd0 100644 --- a/crates/logic/src/messages/mod.rs +++ b/crates/logic/src/messages/mod.rs @@ -8,6 +8,7 @@ mod keep_alive; mod login; mod login_failed; mod login_ok; +mod matchmake; mod out_of_sync; mod own_home_data; mod sector_state; @@ -28,6 +29,7 @@ pub use keep_alive::{KeepAliveMessage, KeepAliveServerMessage}; pub use login::LoginMessage; pub use login_failed::{LoginFailedMessage, LoginFailureReason}; pub use login_ok::LoginOkMessage; +pub use matchmake::{CancelMatchmakeDoneMessage, HomeLogicStoppedMessage, StopHomeLogicMessage}; pub use out_of_sync::OutOfSyncMessage; pub use own_home_data::OwnHomeDataMessage; pub use sector_state::SectorStateMessage; @@ -45,6 +47,8 @@ pub mod message_type { pub const GO_HOME: u16 = 14101; pub const END_CLIENT_TURN: u16 = 14102; pub const START_MISSION: u16 = 14104; + pub const HOME_LOGIC_STOPPED: u16 = 14105; + pub const CANCEL_MATCHMAKE: u16 = 14107; pub const SERVER_HELLO: u16 = 20100; pub const LOGIN_FAILED: u16 = 20103; pub const LOGIN_OK: u16 = 20104; diff --git a/crates/service-rpc/src/game.rs b/crates/service-rpc/src/game.rs index 953a0b8..d1fa0f9 100644 --- a/crates/service-rpc/src/game.rs +++ b/crates/service-rpc/src/game.rs @@ -48,6 +48,11 @@ pub enum GameRequest { #[serde(with = "crate::base64::serde_bytes")] payload: Vec, }, + HomeLogicStopped { + account: AccountRef, + #[serde(with = "crate::base64::serde_bytes")] + payload: Vec, + }, Disconnect { account: AccountRef, }, @@ -82,6 +87,11 @@ pub trait GameApi: Send + Sync + 'static { account: AccountRef, payload: Vec, ) -> RpcResult>; + async fn home_logic_stopped( + &self, + account: AccountRef, + payload: Vec, + ) -> RpcResult>; async fn end_client_turn( &self, account: AccountRef,