214 lines
7.3 KiB
Rust
214 lines
7.3 KiB
Rust
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<ProfileStore>,
|
|
catalog: Arc<Catalog>,
|
|
sessions: HomeModeRegistry,
|
|
}
|
|
impl GameService {
|
|
pub async fn bootstrap(config: GameConfig) -> std::io::Result<Arc<Self>> {
|
|
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<ProfileStore> {
|
|
&self.profiles
|
|
}
|
|
pub fn catalog(&self) -> &Arc<Catalog> {
|
|
&self.catalog
|
|
}
|
|
async fn roller(&self, account: AccountRef) -> RpcResult<RewardRoller> {
|
|
let profile = self
|
|
.profiles
|
|
.get_or_create(account, &self.config.starter, &self.catalog)
|
|
.await?;
|
|
let fallback: Vec<LogicDataRef> = 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<M>(message: &M) -> RpcResult<WireMessage>
|
|
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<Vec<WireMessage>> {
|
|
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<u8>,
|
|
) -> RpcResult<Vec<WireMessage>> {
|
|
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::<OutOfSyncMessage>(&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<GameResponse> {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|