use std::sync::Arc; use logic::{EndClientTurnMessage, LogicDataRef, OutOfSyncMessage, OwnHomeDataMessage}; use service_rpc::{ AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult, RpcService, WireMessage, }; use titan::{Message, MessageMeta, Payload}; use crate::catalog::Catalog; use crate::config::GameConfig; use crate::home_mode::{available_server_command, HomeModeRegistry}; use crate::profile::ProfileStore; use crate::rewards::RewardRoller; use crate::time::unix_seconds; pub struct GameService { config: GameConfig, profiles: Arc, catalog: Arc, sessions: HomeModeRegistry, } impl GameService { pub async fn bootstrap(config: GameConfig) -> std::io::Result> { let profiles = Arc::new(ProfileStore::open(&config.store_path).await?); let catalog = Arc::new(Catalog::load(config.csv_root.as_deref())); tracing::info!( profiles = profiles.len().await, store = %config.store_path.display(), catalog = catalog.is_loaded(), "profile store ready" ); Ok(Arc::new(Self { config, profiles, catalog, sessions: HomeModeRegistry::default(), })) } pub fn profiles(&self) -> &Arc { &self.profiles } pub fn catalog(&self) -> &Arc { &self.catalog } async fn roller(&self, account: AccountRef) -> RpcResult { let profile = self .profiles .get_or_create(account, &self.config.starter, &self.catalog) .await?; let fallback: Vec = profile .deck .iter() .chain(profile.collection.iter()) .map(|owned| owned.card.clone()) .collect(); Ok(RewardRoller::new(&self.catalog, fallback)) } async fn persist(&self, account: AccountRef) -> RpcResult<()> { let Some(session) = self.sessions.close(account).await else { return Ok(()); }; let mut profile = self .profiles .get_or_create(account, &self.config.starter, &self.catalog) .await?; session.lock().await.write_back(&mut profile); self.profiles.save(profile).await?; Ok(()) } } fn encode(message: &M) -> RpcResult where M: MessageMeta + Message, { let payload = message .to_bytes() .map_err(|error| RpcError::Rejected(error.to_string()))?; Ok(WireMessage::new( M::MESSAGE_TYPE, M::MESSAGE_VERSION, payload, )) } #[async_trait::async_trait] impl GameApi for GameService { async fn load_home( &self, account: AccountRef, kind: HomeRequestKind, ) -> RpcResult> { let profile = self .profiles .get_or_create(account, &self.config.starter, &self.catalog) .await?; let session = self .sessions .get_or_open(account, &profile, unix_seconds() as i32) .await; let (home_data, checksum): (OwnHomeDataMessage, i32) = { let home = session.lock().await; ( home.own_home_data(self.config.random_seed), home.logic().checksum(), ) }; tracing::info!( %account, ?kind, cards = home_data.home.total_spell_count(), trophies = home_data.avatar.score, checksum, "serving own home data" ); Ok(vec![encode(&home_data)?]) } async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()> { tracing::debug!(%account, ping_ms, "client capabilities"); Ok(()) } async fn end_client_turn( &self, account: AccountRef, payload: Vec, ) -> RpcResult> { let turn = match EndClientTurnMessage::from_bytes(&payload) { Ok(turn) => turn, Err(error) => { tracing::warn!(%account, bytes = payload.len(), %error, "undecodable client turn"); return Ok(Vec::new()); } }; let profile = self .profiles .get_or_create(account, &self.config.starter, &self.catalog) .await?; let session = self .sessions .get_or_open(account, &profile, unix_seconds() as i32) .await; let roller = self.roller(account).await?; let result = { let mut home = session.lock().await; home.end_client_turn(&turn, &self.config.chest_reward, &roller) }; tracing::debug!( %account, tick = turn.tick, checksum = turn.checksum, commands = turn.commands.len(), "end client turn" ); let mut replies = Vec::new(); if let Some(out_of_sync) = result.out_of_sync { tracing::warn!( %account, tick = out_of_sync.tick, server = out_of_sync.server_checksum, client = out_of_sync.client_checksum, "client and server home state diverged" ); replies.push(encode::(&out_of_sync)?); } for claim in result.claims { tracing::info!( %account, source = claim.chest_source, chest_id = claim.chest_id, gold = claim.reward.as_ref().map(|reward| reward.gold).unwrap_or(0), cards = claim.reward.as_ref().map(|reward| reward.spell_count()).unwrap_or(0), "granting a chest reward" ); replies.push(encode(&available_server_command(claim))?); } if result.changed { let mut stored = self .profiles .get_or_create(account, &self.config.starter, &self.catalog) .await?; session.lock().await.write_back(&mut stored); self.profiles.save(stored).await?; } Ok(replies) } async fn disconnect(&self, account: AccountRef) -> RpcResult<()> { self.persist(account).await?; let sessions = self.sessions.session_count().await; tracing::debug!(%account, sessions, "session closed"); Ok(()) } } #[async_trait::async_trait] impl RpcService for GameService { type Request = GameRequest; type Response = GameResponse; fn service_name(&self) -> &'static str { "game" } async fn call(&self, request: GameRequest) -> RpcResult { match request { GameRequest::LoadHome { account, kind } => { Ok(GameResponse::messages(self.load_home(account, kind).await?)) } GameRequest::ClientCapabilities { account, ping_ms } => { self.client_capabilities(account, ping_ms).await?; Ok(GameResponse::Empty) } GameRequest::EndClientTurn { account, payload } => Ok(GameResponse::messages( self.end_client_turn(account, payload).await?, )), GameRequest::Disconnect { account } => { self.disconnect(account).await?; Ok(GameResponse::Empty) } } } }