diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs index 86a3e35..c407cc4 100644 --- a/crates/game-service/src/battle_session.rs +++ b/crates/game-service/src/battle_session.rs @@ -10,13 +10,14 @@ pub fn snapshot_interval_ticks() -> i32 { } pub const MAX_CATCH_UP_TICKS: i32 = 40; pub const CHECKSUM_HISTORY: usize = 256; +pub const TICKS_PER_TURN: i32 = 10; pub const BOT_DEPLOY_AHEAD: i32 = 2000; use std::time::Instant; use logic::battle::{ LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP, }; use logic::battle::{verify_snapshot, LogicBattleEvent}; -use logic::SectorStateMessage; +use logic::{SectorHeartbeatMessage, SectorStateMessage}; use logic::{BattleEventMessage, LogicDataRef, LogicRandom}; use titan::LogicLong; use service_rpc::{AccountRef, WireMessage}; @@ -238,12 +239,23 @@ impl BattleSession { self.mode.time.tick = self.tick; self.release_queued(self.tick); self.mode.battle.tick(self.tick); - if let Ok(checksum) = self.mode.calculate_checksum() { + let this_checksum = self.mode.calculate_checksum().ok(); + if let Some(checksum) = this_checksum { if self.recent_checksums.len() >= CHECKSUM_HISTORY { self.recent_checksums.pop_front(); } self.recent_checksums.push_back((self.tick, checksum)); } + if self.pushes_snapshots() && self.tick % TICKS_PER_TURN == 0 { + if let Some(checksum) = this_checksum { + let turn = self.tick / TICKS_PER_TURN; + if let Ok(message) = + crate::wire::encode(&SectorHeartbeatMessage::new(turn, checksum)) + { + self.outbound.push(message); + } + } + } if self.pushes_snapshots() && self.tick >= self.next_snapshot { self.next_snapshot = self.tick + snapshot_interval_ticks(); if let Some(message) = self.snapshot_message() { diff --git a/crates/game-service/tests/walk_to_tower.rs b/crates/game-service/tests/walk_to_tower.rs index 8556e69..372339e 100644 --- a/crates/game-service/tests/walk_to_tower.rs +++ b/crates/game-service/tests/walk_to_tower.rs @@ -478,3 +478,34 @@ fn a_troop_in_range_damages_the_tower_with_the_client_hit_model() { .expect("tower still present"); assert!(hp < start_hp, "the knight's attacks should reduce tower hp ({hp} !< {start_hp})"); } +#[test] +fn the_session_emits_a_heartbeat_every_turn() { + let root = assets(); + if let Ok(t) = LogicDataTables::load_from_dir(&root) { + LogicDataTables::install(Arc::new(t)); + } + let builder = BattleBuilder::new(&root); + let mode = builder + .build( + LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"), + LogicDataRef::None, + LogicDataRef::by_name(table::ARENAS, "Arena_T"), + vec![avatar(5), avatar(0)], + [None, None], + 1, + ) + .expect("battle"); + let mut session = game_service::battle_session::BattleSession::new(mode, Vec::new()); + session.advance_to(25); + let out = session.take_outbound(); + let heartbeats: Vec<&service_rpc::WireMessage> = out + .iter() + .filter(|m| m.message_type == 21902) + .collect(); + assert_eq!(heartbeats.len(), 2, "expected a heartbeat at ticks 10 and 20"); + let mut reader = titan::ByteStreamReader::new(&heartbeats[0].payload); + let turn = reader.read_vint().unwrap(); + let checksum = reader.read_vint().unwrap(); + assert_eq!(turn, 1); + assert_eq!(Some(checksum), session.checksum_at(10)); +} diff --git a/crates/logic/src/messages/mod.rs b/crates/logic/src/messages/mod.rs index d356991..3924ec8 100644 --- a/crates/logic/src/messages/mod.rs +++ b/crates/logic/src/messages/mod.rs @@ -14,6 +14,7 @@ mod own_home_data; mod battle_event; mod request_sector_state; mod sector_command; +mod sector_heartbeat; mod sector_state; mod server_error; mod start_mission; @@ -38,6 +39,7 @@ 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_heartbeat::SectorHeartbeatMessage; pub use sector_state::SectorStateMessage; pub use server_error::ServerErrorMessage; pub use start_mission::StartMissionMessage; @@ -56,6 +58,7 @@ pub mod message_type { pub const HOME_LOGIC_STOPPED: u16 = 14105; pub const CANCEL_MATCHMAKE: u16 = 14107; pub const REQUEST_SECTOR_STATE: u16 = 12903; + pub const SECTOR_HEARTBEAT: u16 = 21902; pub const SECTOR_COMMAND: u16 = 12904; pub const SEND_BATTLE_EVENT: u16 = 12951; pub const BATTLE_EVENT: u16 = 22952; diff --git a/crates/logic/src/messages/sector_heartbeat.rs b/crates/logic/src/messages/sector_heartbeat.rs new file mode 100644 index 0000000..ee4fdb2 --- /dev/null +++ b/crates/logic/src/messages/sector_heartbeat.rs @@ -0,0 +1,17 @@ +use titan::{ByteStreamWriter, Message}; +#[derive(Debug, Default, Clone, PartialEq, Eq, Message)] +#[message(id = 21902, direction = "server", name = "SectorHeartbeatMessage")] +#[codec(raw)] +pub struct SectorHeartbeatMessage { + pub body: Vec, +} +impl SectorHeartbeatMessage { + pub fn new(server_turn: i32, checksum: i32) -> Self { + let mut writer = ByteStreamWriter::new(); + writer.write_vint(server_turn); + writer.write_vint(checksum); + Self { + body: writer.into_inner(), + } + } +}