switch the battle to type 0 and drive it from the server

type 0 turns out not to need a UDP transport. MessageManager::sendUdpMessage
checks for a socket and a valid connection, and falls straight back to
sendMessage when there is neither - we never send UdpConnectionInfoMessage,
so the client has no socket and the sector traffic arrives on the tcp
connection we already have.

so SectorCommandMessage, 12904, is decoded now: a client tick, a client
checksum and an optional command. a card played this way goes through the
same summon path as before. the client stops sending EndClientTurnMessage
in a battle - sendEndTurn asserts on isImmediateMessageExecution - so the
checksum comparison moves onto the sector command, which carries the same
two numbers.

with the type at 0 the client no longer simulates. it renders what the
snapshot says, which is why the snapshots start flowing again: the gate
on them was the battle type all along. the bot's cards reach the player
for the first time, because there is finally one simulation rather than
two arguing.
This commit is contained in:
WiseDev 2026-08-23 13:51:01 +03:00
parent ade049b29d
commit 498f2cb62d
7 changed files with 91 additions and 3 deletions

View file

@ -5,7 +5,7 @@ use logic::battle::{
LogicGameObjectRef, LogicHitpointComponent, LogicMovementComponent, LogicObjectBody,
LogicSummoner, LogicSummonerDeck,
LogicTilemap,
LogicTime, LogicVector2, BATTLE_TYPE_NPC, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES,
LogicTime, LogicVector2, BATTLE_TYPE_PVP, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES,
DIRECTION_BOTTOM,
DIRECTION_TOP, SUBTILE_UNITS,
};
@ -307,7 +307,7 @@ impl BattleBuilder {
npc,
arena,
account_ids,
battle_type: BATTLE_TYPE_NPC,
battle_type: BATTLE_TYPE_PVP,
decks,
objects,
leaders,

View file

@ -3,7 +3,7 @@ use logic::table;
use logic::{
AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef,
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage, SectorStateMessage,
SendBattleEventMessage,
SectorCommandMessage, SendBattleEventMessage,
StartMissionMessage, StopHomeLogicMessage,
};
use service_rpc::{
@ -476,6 +476,49 @@ impl GameApi for GameService {
self.running_battles.answer_emote(account).await;
Ok(())
}
async fn sector_command(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
let Ok(sector) = SectorCommandMessage::from_bytes(&payload) else {
return Ok(());
};
let progress = self
.running_battles
.advance(account, sector.client_tick)
.await;
let server_checksum = progress.and_then(|state| state.checksum);
let mut spawned = 0;
if let Some(played) = sector
.command
.as_ref()
.and_then(|command| command.as_any().downcast_ref::<logic::LogicDoSpellCommand>())
{
spawned = self
.running_battles
.play(
account,
played.header.executor_account_id,
played.deck_slot,
played.position,
played.header.execute_tick,
|card, position, owner, instance| {
self.battles.summon(card, position, owner, 0, instance)
},
)
.await;
}
tracing::info!(
%account,
tick = sector.client_tick,
checksum = sector.client_checksum,
server_checksum,
agrees = server_checksum == Some(sector.client_checksum),
objects = progress.map(|state| state.objects),
towers = ?progress.map(|state| state.towers),
stars = ?progress.map(|state| state.stars),
spawned,
"sector command"
);
Ok(())
}
async fn cancel_matchmake(&self, account: AccountRef) -> RpcResult<()> {
self.matchmaker.leave(account).await;
Ok(())
@ -516,6 +559,10 @@ impl RpcService for GameService {
GameRequest::BattleTick { account } => Ok(GameResponse::messages(
self.battle_tick(account).await?,
)),
GameRequest::SectorCommand { account, payload } => {
self.sector_command(account, payload).await?;
Ok(GameResponse::Empty)
}
GameRequest::CancelMatchmake { account } => {
self.cancel_matchmake(account).await?;
Ok(GameResponse::Empty)

View file

@ -141,6 +141,12 @@ impl GameApi for RemoteGame {
.await?;
Ok(())
}
async fn sector_command(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
self.client
.call(&GameRequest::SectorCommand { account, payload })
.await?;
Ok(())
}
async fn disconnect(&self, account: AccountRef) -> RpcResult<()> {
self.client
.call(&GameRequest::Disconnect { account })

View file

@ -99,6 +99,20 @@ impl MessageManager {
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(());

View file

@ -12,6 +12,7 @@ mod matchmake;
mod out_of_sync;
mod own_home_data;
mod battle_event;
mod sector_command;
mod sector_state;
mod server_error;
mod start_mission;
@ -34,6 +35,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 sector_command::SectorCommandMessage;
pub use sector_state::SectorStateMessage;
pub use server_error::ServerErrorMessage;
pub use start_mission::StartMissionMessage;
@ -51,6 +53,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 SECTOR_COMMAND: u16 = 12904;
pub const SEND_BATTLE_EVENT: u16 = 12951;
pub const BATTLE_EVENT: u16 = 22952;
pub const SERVER_HELLO: u16 = 20100;

View file

@ -0,0 +1,12 @@
use titan::Message;
use crate::commands::LogicCommand;
#[derive(Debug, Default, Message)]
#[message(id = 12904, direction = "client", name = "SectorCommandMessage")]
#[codec(partial)]
pub struct SectorCommandMessage {
#[codec(vint)]
pub client_tick: i32,
#[codec(vint)]
pub client_checksum: i32,
pub command: Option<Box<dyn LogicCommand>>,
}

View file

@ -56,6 +56,11 @@ pub enum GameRequest {
BattleTick {
account: AccountRef,
},
SectorCommand {
account: AccountRef,
#[serde(with = "crate::base64::serde_bytes")]
payload: Vec<u8>,
},
CancelMatchmake {
account: AccountRef,
},
@ -111,5 +116,6 @@ pub trait GameApi: Send + Sync + 'static {
async fn battle_tick(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>>;
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 disconnect(&self, account: AccountRef) -> RpcResult<()>;
}