scroll.server/services/game-service/src/service.rs
scroll 20a75dbb25 nuke the tests and the dead code
data/ is runtime state, keep it out of git.
2026-08-23 07:51:04 +03:00

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)
}
}
}
}