scroll.server/crates/gateway/src/backend.rs
WiseDev 8699a6884d answer the client when it asks for the sector state
RequestSectorStateMessage, 12903, one vint of client tick, sent through
sendUdpMessage and so arriving on tcp like everything else. we were
ignoring it.

it matters because of how the client builds models. the factory marks a
freshly created object at [obj+0x14], and only for those does
LogicGameObjectManager::decode call the listener at [mgr+0x28] that
builds the visual. every later snapshot matches the same object by
global id and reuses it, so the flag is never set again - an object that
was decoded before the battle screen installed its listener stays
invisible for the whole battle while still walking and fighting. that is
the tower archers and the invisible units; the knight shows because the
client creates that one itself, after the screen is up.

the client asks for the state when it is ready, and now it gets it.
2026-08-23 14:32:20 +03:00

163 lines
4.8 KiB
Rust

use std::sync::Arc;
use service_rpc::{
AccountRef, AuthApi, AuthRequest, AuthResponse, DeviceInfo, GameApi, GameRequest, GameResponse,
HomeRequestKind, LoginOutcome, RpcClient, RpcError, RpcResult, WireMessage,
};
pub struct Backends {
pub auth: Arc<dyn AuthApi>,
pub game: Arc<dyn GameApi>,
}
impl Backends {
pub fn new(auth: Arc<dyn AuthApi>, game: Arc<dyn GameApi>) -> Arc<Self> {
Arc::new(Self { auth, game })
}
pub fn remote(auth_endpoint: &str, game_endpoint: &str) -> Arc<Self> {
Self::new(
Arc::new(RemoteAuth::new(auth_endpoint)),
Arc::new(RemoteGame::new(game_endpoint)),
)
}
}
pub struct RemoteAuth {
client: RpcClient<AuthRequest, AuthResponse>,
}
impl RemoteAuth {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
client: RpcClient::new(endpoint),
}
}
}
#[async_trait::async_trait]
impl AuthApi for RemoteAuth {
async fn login(
&self,
account: AccountRef,
pass_token: Option<String>,
device: DeviceInfo,
) -> RpcResult<LoginOutcome> {
match self
.client
.call(&AuthRequest::Login {
account,
pass_token,
device,
})
.await?
{
AuthResponse::Login(outcome) => Ok(outcome),
other => Err(RpcError::Rejected(format!(
"unexpected auth reply {other:?}"
))),
}
}
async fn resolve(&self, account: AccountRef) -> RpcResult<bool> {
match self.client.call(&AuthRequest::Resolve { account }).await? {
AuthResponse::Resolved { known } => Ok(known),
other => Err(RpcError::Rejected(format!(
"unexpected auth reply {other:?}"
))),
}
}
}
pub struct RemoteGame {
client: RpcClient<GameRequest, GameResponse>,
}
impl RemoteGame {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
client: RpcClient::new(endpoint),
}
}
}
#[async_trait::async_trait]
impl GameApi for RemoteGame {
async fn load_home(
&self,
account: AccountRef,
kind: HomeRequestKind,
) -> RpcResult<Vec<WireMessage>> {
Ok(self
.client
.call(&GameRequest::LoadHome { account, kind })
.await?
.into_messages())
}
async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()> {
self.client
.call(&GameRequest::ClientCapabilities { account, ping_ms })
.await?;
Ok(())
}
async fn start_mission(
&self,
account: AccountRef,
payload: Vec<u8>,
) -> RpcResult<Vec<WireMessage>> {
Ok(self
.client
.call(&GameRequest::StartMission { account, payload })
.await?
.into_messages())
}
async fn home_logic_stopped(
&self,
account: AccountRef,
payload: Vec<u8>,
) -> RpcResult<Vec<WireMessage>> {
Ok(self
.client
.call(&GameRequest::HomeLogicStopped { account, payload })
.await?
.into_messages())
}
async fn end_client_turn(
&self,
account: AccountRef,
payload: Vec<u8>,
) -> RpcResult<Vec<WireMessage>> {
Ok(self
.client
.call(&GameRequest::EndClientTurn { account, payload })
.await?
.into_messages())
}
async fn battle_tick(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>> {
Ok(self
.client
.call(&GameRequest::BattleTick { account })
.await?
.into_messages())
}
async fn battle_event(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
self.client
.call(&GameRequest::BattleEvent { account, payload })
.await?;
Ok(())
}
async fn cancel_matchmake(&self, account: AccountRef) -> RpcResult<()> {
self.client
.call(&GameRequest::CancelMatchmake { account })
.await?;
Ok(())
}
async fn sector_command(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
self.client
.call(&GameRequest::SectorCommand { account, payload })
.await?;
Ok(())
}
async fn request_sector_state(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>> {
Ok(self
.client
.call(&GameRequest::RequestSectorState { account })
.await?
.into_messages())
}
async fn disconnect(&self, account: AccountRef) -> RpcResult<()> {
self.client
.call(&GameRequest::Disconnect { account })
.await?;
Ok(())
}
}