scroll.server/crates/gateway/src/message_manager.rs
WiseDev 774e7c60cf build and send the battle sector state
npc missions used to get a ServerErrorMessage back. now StartMission builds a
LogicGameMode snapshot off the arena tilemap and answers 21903.

towers come from assets/locations/*.csv the way initDefaultSector does it: tile
coordinates times 500, leader index decided by which half of the map the tower
sits in. the two king towers must be there, the client dereferences them without
a null check. they live in buildings.csv, not characters.csv.

LogicCharacter puts the base object fields fourth, not first. the buff component
writes a fixed array sized by the character_buffs row count even with no buffs.

training_arena parses to 2 kings, 4 princess towers, 36x64 subtiles. snapshot is
602 bytes over 6 objects. the real client has not seen it yet.
2026-08-23 10:24:20 +03:00

240 lines
8.9 KiB
Rust

use std::net::SocketAddr;
use std::sync::Arc;
use logic::{
message_type, 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 struct MessageManager {
peer: SocketAddr,
config: Arc<GatewayConfig>,
backends: Arc<Backends>,
sender: MessagingSender,
account: Option<AccountRef>,
}
impl MessageManager {
pub fn new(
peer: SocketAddr,
config: Arc<GatewayConfig>,
backends: Arc<Backends>,
sender: MessagingSender,
) -> Self {
Self {
peer,
config,
backends,
sender,
account: 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.push_home(HomeRequestKind::GoHome).await
}
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?,
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
}
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(())
}
}