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.
This commit is contained in:
WiseDev 2026-08-23 14:32:20 +03:00
parent e9894d9838
commit 8699a6884d
7 changed files with 46 additions and 1 deletions

View file

@ -114,7 +114,7 @@ impl BattleSession {
}
})
}
fn snapshot_message(&mut self) -> Option<WireMessage> {
pub fn snapshot_message(&mut self) -> Option<WireMessage> {
let snapshot = self.mode.snapshot().ok()?;
let report = verify_snapshot(&snapshot);
let unit = self
@ -344,6 +344,11 @@ impl BattleRegistry {
session.push_outbound(message);
}
}
pub async fn resend(&self, account: AccountRef) -> Option<WireMessage> {
let handle = self.session(account).await?;
let mut session = handle.lock().await;
session.snapshot_message()
}
pub async fn send(&self, account: AccountRef, message: WireMessage) {
if let Some(handle) = self.session(account).await {
handle.lock().await.push_outbound(message);

View file

@ -481,6 +481,9 @@ impl GameApi for GameService {
self.running_battles.answer_emote(account).await;
Ok(())
}
async fn request_sector_state(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>> {
Ok(self.running_battles.resend(account).await.into_iter().collect())
}
async fn sector_command(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
let Ok(sector) = SectorCommandMessage::from_bytes(&payload) else {
return Ok(());
@ -567,6 +570,9 @@ impl RpcService for GameService {
GameRequest::BattleTick { account } => Ok(GameResponse::messages(
self.battle_tick(account).await?,
)),
GameRequest::RequestSectorState { account } => Ok(GameResponse::messages(
self.request_sector_state(account).await?,
)),
GameRequest::SectorCommand { account, payload } => {
self.sector_command(account, payload).await?;
Ok(GameResponse::Empty)

View file

@ -147,6 +147,13 @@ impl GameApi for RemoteGame {
.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 })

View file

@ -99,6 +99,19 @@ impl MessageManager {
self.stop_battle_ticker();
self.push_home(HomeRequestKind::GoHome).await
}
message_type::REQUEST_SECTOR_STATE => {
let Some(account) = self.account else {
return Ok(());
};
tracing::info!(peer = %self.peer, "client asked for the sector state");
match self.backends.game.request_sector_state(account).await {
Ok(replies) => self.push_wire_messages(replies).await?,
Err(error) => {
tracing::warn!(peer = %self.peer, %error, "could not serve the sector state")
}
}
Ok(())
}
message_type::SECTOR_COMMAND => {
let Some(account) = self.account else {
return Ok(());

View file

@ -12,6 +12,7 @@ mod matchmake;
mod out_of_sync;
mod own_home_data;
mod battle_event;
mod request_sector_state;
mod sector_command;
mod sector_state;
mod server_error;
@ -35,6 +36,7 @@ pub use matchmake::{CancelMatchmakeDoneMessage, HomeLogicStoppedMessage, StopHom
pub use out_of_sync::OutOfSyncMessage;
pub use own_home_data::OwnHomeDataMessage;
pub use battle_event::{BattleEventMessage, SendBattleEventMessage};
pub use request_sector_state::RequestSectorStateMessage;
pub use sector_command::SectorCommandMessage;
pub use sector_state::SectorStateMessage;
pub use server_error::ServerErrorMessage;
@ -53,6 +55,7 @@ pub mod message_type {
pub const START_MISSION: u16 = 14104;
pub const HOME_LOGIC_STOPPED: u16 = 14105;
pub const CANCEL_MATCHMAKE: u16 = 14107;
pub const REQUEST_SECTOR_STATE: u16 = 12903;
pub const SECTOR_COMMAND: u16 = 12904;
pub const SEND_BATTLE_EVENT: u16 = 12951;
pub const BATTLE_EVENT: u16 = 22952;

View file

@ -0,0 +1,7 @@
use titan::Message;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Message)]
#[message(id = 12903, direction = "client", name = "RequestSectorStateMessage")]
pub struct RequestSectorStateMessage {
#[codec(vint)]
pub client_tick: i32,
}

View file

@ -56,6 +56,9 @@ pub enum GameRequest {
BattleTick {
account: AccountRef,
},
RequestSectorState {
account: AccountRef,
},
SectorCommand {
account: AccountRef,
#[serde(with = "crate::base64::serde_bytes")]
@ -117,5 +120,6 @@ pub trait GameApi: Send + Sync + 'static {
async fn battle_event(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()>;
async fn cancel_matchmake(&self, account: AccountRef) -> RpcResult<()>;
async fn sector_command(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()>;
async fn request_sector_state(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>>;
async fn disconnect(&self, account: AccountRef) -> RpcResult<()>;
}