scroll.server/crates/gateway/src/message_manager.rs
WiseDev bab24f2295 send the battle state five times a second
the server was simulating correctly all along - the log shows a goblin
covering 120 units a tick and a knight 60, exactly their Speed columns,
in a straight line with no jitter. the teleporting was the correction
rate: one snapshot a second, while the client runs its own prediction in
between and diverges from ours. a unit gets more than a tile out of step
before our state arrives and drags it back.

the snapshot now goes every four ticks and the gateway ticks every fifty
milliseconds rather than two hundred, so a correction moves a unit a
fifth as far.

this makes the symptom smaller, not absent. it goes away when the two
simulations agree, which is what the checksum comparison is for and what
collisions and projectiles are still missing for.
2026-08-23 14:10:55 +03:00

358 lines
13 KiB
Rust

use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinHandle;
use logic::{
message_type, CancelMatchmakeDoneMessage, ClientCapabilitiesMessage, GoHomeMessage,
KeepAliveServerMessage, LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage,
};
use service_rpc::{
AccountRef, DeviceInfo, HomeRequestKind, LoginOutcome, Session as AuthSession, WireMessage,
};
use titan::{Incoming, LogicLong, MessagingSender, OutboundMessage};
use crate::backend::Backends;
use crate::config::GatewayConfig;
#[derive(Debug, thiserror::Error)]
pub enum RoutingError {
#[error("transport: {0}")]
Transport(#[from] titan::MessagingError),
#[error("backend: {0}")]
Backend(#[from] service_rpc::RpcError),
#[error("client sent message type {0} before authenticating")]
Unauthenticated(u16),
}
type RoutingResult<T> = Result<T, RoutingError>;
pub const BATTLE_TICK_INTERVAL: Duration = Duration::from_millis(50);
pub struct MessageManager {
peer: SocketAddr,
config: Arc<GatewayConfig>,
backends: Arc<Backends>,
sender: MessagingSender,
account: Option<AccountRef>,
battle_ticker: Option<JoinHandle<()>>,
}
impl Drop for MessageManager {
fn drop(&mut self) {
self.stop_battle_ticker();
}
}
impl MessageManager {
pub fn new(
peer: SocketAddr,
config: Arc<GatewayConfig>,
backends: Arc<Backends>,
sender: MessagingSender,
) -> Self {
Self {
peer,
config,
backends,
sender,
account: None,
battle_ticker: None,
}
}
pub fn account(&self) -> Option<AccountRef> {
self.account
}
pub async fn receive_message(&mut self, incoming: Incoming) -> RoutingResult<()> {
let message_type = incoming.message_type();
if self.account.is_none() && message_type != message_type::LOGIN {
return Err(RoutingError::Unauthenticated(message_type));
}
let Some(message) = incoming.message.as_ref() else {
return Ok(());
};
if !message.direction().accepts_from_client() {
tracing::warn!(
peer = %self.peer,
message = message.name(),
"client sent a server to client message"
);
return Ok(());
}
match message_type {
message_type::LOGIN => {
let login = incoming.downcast::<LoginMessage>().expect("login message");
self.handle_login(login).await
}
message_type::KEEP_ALIVE => {
self.sender
.send_message(&KeepAliveServerMessage::default())
.await?;
Ok(())
}
message_type::CLIENT_CAPABILITIES => {
let capabilities = incoming
.downcast::<ClientCapabilitiesMessage>()
.expect("client capabilities message");
if let Some(account) = self.account {
self.backends
.game
.client_capabilities(account, capabilities.ping_ms)
.await?;
}
Ok(())
}
message_type::GO_HOME => {
let _ = incoming.downcast::<GoHomeMessage>();
self.stop_battle_ticker();
self.push_home(HomeRequestKind::GoHome).await
}
message_type::SECTOR_COMMAND => {
let Some(account) = self.account else {
return Ok(());
};
if let Err(error) = self
.backends
.game
.sector_command(account, incoming.payload)
.await
{
tracing::warn!(peer = %self.peer, %error, "could not deliver the sector command");
}
Ok(())
}
message_type::SEND_BATTLE_EVENT => {
let Some(account) = self.account else {
return Ok(());
};
if let Err(error) = self
.backends
.game
.battle_event(account, incoming.payload)
.await
{
tracing::warn!(peer = %self.peer, %error, "could not deliver the battle event");
}
Ok(())
}
message_type::CANCEL_MATCHMAKE => {
tracing::info!(peer = %self.peer, "client cancelled matchmaking");
self.stop_battle_ticker();
if let Some(account) = self.account {
let _ = self.backends.game.cancel_matchmake(account).await;
}
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?;
self.start_battle_ticker();
}
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(());
};
match self
.backends
.game
.start_mission(account, incoming.payload)
.await
{
Ok(replies) => {
self.push_wire_messages(replies).await?;
self.start_battle_ticker();
}
Err(error) => {
tracing::warn!(peer = %self.peer, %error, "could not start the mission");
self.sender
.send_message(&ServerErrorMessage::new(
"this server could not build the battle",
))
.await?;
}
}
Ok(())
}
message_type::END_CLIENT_TURN => {
if let Some(account) = self.account {
let replies = self
.backends
.game
.end_client_turn(account, incoming.payload)
.await?;
self.push_wire_messages(replies).await?;
}
Ok(())
}
_ => {
tracing::debug!(
peer = %self.peer,
message = message.name(),
"no gateway route for this message"
);
Ok(())
}
}
}
pub async fn on_disconnect(&self) {
if let Some(account) = self.account {
if let Err(error) = self.backends.game.disconnect(account).await {
tracing::debug!(peer = %self.peer, %error, "disconnect notification failed");
}
}
}
async fn handle_login(&mut self, login: &LoginMessage) -> RoutingResult<()> {
let account = AccountRef::new(login.account_id.high, login.account_id.low);
let device = DeviceInfo {
udid: login.udid.clone(),
open_udid: login.open_udid.clone(),
device: login.device.clone(),
os_version: login.os_version.clone(),
android: login.android,
preferred_language: login.preferred_device_language.clone(),
client_major_version: login.client_major_version,
client_minor_version: login.client_minor_version,
client_build: login.client_build,
resource_sha: login.resource_sha.clone(),
};
tracing::info!(
peer = %self.peer,
%account,
version = format!(
"{}.{}.{}",
login.client_major_version, login.client_minor_version, login.client_build
),
device = login.device.as_deref().unwrap_or("unknown"),
"login attempt"
);
match self
.backends
.auth
.login(account, login.pass_token.clone(), device)
.await?
{
LoginOutcome::Rejected {
error_code,
message,
} => {
tracing::info!(
peer = %self.peer,
error_code,
reason = message.as_deref().unwrap_or("unspecified"),
"login rejected"
);
self.sender
.send_message(&LoginFailedMessage {
error_code,
fingerprint_json: None,
redirect_domain: None,
content_url: None,
remaining_seconds: 0,
})
.await?;
Ok(())
}
LoginOutcome::Accepted(session) => {
self.account = Some(session.account);
self.sender.send_message(&self.login_ok(&session)).await?;
self.push_home(HomeRequestKind::Login).await
}
}
}
fn login_ok(&self, session: &AuthSession) -> LoginOkMessage {
let account = LogicLong::new(session.account.high, session.account.low);
LoginOkMessage {
account_id: account,
home_id: account,
pass_token: Some(session.pass_token.clone()),
facebook_id: None,
gamecenter_id: None,
server_major_version: self.config.server_major_version,
server_minor_version: self.config.server_minor_version,
server_build: self.config.server_build,
content_version: self.config.content_version,
server_environment: Some(self.config.server_environment.clone()),
session_count: session.session_count,
play_time_seconds: session.play_time_seconds,
days_since_started_playing: session.days_since_started_playing,
facebook_app_id: self.config.facebook_app_id.clone(),
server_time: Some(session.server_time.clone()),
account_created_date: Some(session.account_created_date.clone()),
startup_cooldown_seconds: self.config.startup_cooldown_seconds,
}
}
async fn push_home(&self, kind: HomeRequestKind) -> RoutingResult<()> {
let Some(account) = self.account else {
return Ok(());
};
let messages = self.backends.game.load_home(account, kind).await?;
self.push_wire_messages(messages).await
}
fn start_battle_ticker(&mut self) {
let Some(account) = self.account else {
return;
};
self.stop_battle_ticker();
let backends = Arc::clone(&self.backends);
let sender = self.sender.clone();
let peer = self.peer;
self.battle_ticker = Some(tokio::spawn(async move {
let mut ticker = tokio::time::interval(BATTLE_TICK_INTERVAL);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
ticker.tick().await;
if sender.is_closed() {
return;
}
match backends.game.battle_tick(account).await {
Ok(messages) => {
for message in messages {
let outbound = OutboundMessage::raw(
message.message_type,
message.message_version,
message.payload,
);
if sender.send(outbound).await.is_err() {
return;
}
}
}
Err(error) => {
tracing::warn!(%peer, %error, "the battle tick failed, stopping the ticker");
return;
}
}
}
}));
}
fn stop_battle_ticker(&mut self) {
if let Some(handle) = self.battle_ticker.take() {
handle.abort();
}
}
async fn push_wire_messages(&self, messages: Vec<WireMessage>) -> RoutingResult<()> {
for message in messages {
self.sender
.send(OutboundMessage::raw(
message.message_type,
message.message_version,
message.payload,
))
.await?;
}
Ok(())
}
}