send the SectorHeartbeat the type-0 client is built around
audit finding #16. steady state was a full SectorState (21903) every four ticks - a hard client reset each time - and the per-turn channel the client is actually written around was never driven. added SectorHeartbeatMessage (21902): wire is vint serverTurn, vint checksum, with optional command and server-tick-data blocks the client's decode treats as absent at end of stream. the session now emits one per turn (every 10 ticks); the client reads serverTick as 10*serverTurn and verifies its predicted checksum for that tick against ours via LogicGameMode::endTurnReceivedFromServer. a match keeps it in sync with no reset; a mismatch flips it out of sync, which makes it request a sector state - which request_sector_state already serves. now that the idle divergence is fixed the early-game turns verify cleanly. format confirmed against SectorHeartbeatMessage::decode/encode and getMessageType (21902) in the client. the full snapshots still go out too; routing the bot's plays as commands inside the heartbeat and then dropping the periodic snapshot is the next step, but this stands up the channel and the checksum verification. harness: the session emits heartbeats at ticks 10 and 20, and the first decodes to (turn 1, checksum_at(10)).
This commit is contained in:
parent
2933d2ff7e
commit
7ead67d219
4 changed files with 65 additions and 2 deletions
|
|
@ -10,13 +10,14 @@ pub fn snapshot_interval_ticks() -> i32 {
|
||||||
}
|
}
|
||||||
pub const MAX_CATCH_UP_TICKS: i32 = 40;
|
pub const MAX_CATCH_UP_TICKS: i32 = 40;
|
||||||
pub const CHECKSUM_HISTORY: usize = 256;
|
pub const CHECKSUM_HISTORY: usize = 256;
|
||||||
|
pub const TICKS_PER_TURN: i32 = 10;
|
||||||
pub const BOT_DEPLOY_AHEAD: i32 = 2000;
|
pub const BOT_DEPLOY_AHEAD: i32 = 2000;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use logic::battle::{
|
use logic::battle::{
|
||||||
LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP,
|
LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP,
|
||||||
};
|
};
|
||||||
use logic::battle::{verify_snapshot, LogicBattleEvent};
|
use logic::battle::{verify_snapshot, LogicBattleEvent};
|
||||||
use logic::SectorStateMessage;
|
use logic::{SectorHeartbeatMessage, SectorStateMessage};
|
||||||
use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
|
use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
|
||||||
use titan::LogicLong;
|
use titan::LogicLong;
|
||||||
use service_rpc::{AccountRef, WireMessage};
|
use service_rpc::{AccountRef, WireMessage};
|
||||||
|
|
@ -238,12 +239,23 @@ impl BattleSession {
|
||||||
self.mode.time.tick = self.tick;
|
self.mode.time.tick = self.tick;
|
||||||
self.release_queued(self.tick);
|
self.release_queued(self.tick);
|
||||||
self.mode.battle.tick(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 {
|
if self.recent_checksums.len() >= CHECKSUM_HISTORY {
|
||||||
self.recent_checksums.pop_front();
|
self.recent_checksums.pop_front();
|
||||||
}
|
}
|
||||||
self.recent_checksums.push_back((self.tick, checksum));
|
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 {
|
if self.pushes_snapshots() && self.tick >= self.next_snapshot {
|
||||||
self.next_snapshot = self.tick + snapshot_interval_ticks();
|
self.next_snapshot = self.tick + snapshot_interval_ticks();
|
||||||
if let Some(message) = self.snapshot_message() {
|
if let Some(message) = self.snapshot_message() {
|
||||||
|
|
|
||||||
|
|
@ -478,3 +478,34 @@ fn a_troop_in_range_damages_the_tower_with_the_client_hit_model() {
|
||||||
.expect("tower still present");
|
.expect("tower still present");
|
||||||
assert!(hp < start_hp, "the knight's attacks should reduce tower hp ({hp} !< {start_hp})");
|
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));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ mod own_home_data;
|
||||||
mod battle_event;
|
mod battle_event;
|
||||||
mod request_sector_state;
|
mod request_sector_state;
|
||||||
mod sector_command;
|
mod sector_command;
|
||||||
|
mod sector_heartbeat;
|
||||||
mod sector_state;
|
mod sector_state;
|
||||||
mod server_error;
|
mod server_error;
|
||||||
mod start_mission;
|
mod start_mission;
|
||||||
|
|
@ -38,6 +39,7 @@ pub use own_home_data::OwnHomeDataMessage;
|
||||||
pub use battle_event::{BattleEventMessage, SendBattleEventMessage};
|
pub use battle_event::{BattleEventMessage, SendBattleEventMessage};
|
||||||
pub use request_sector_state::RequestSectorStateMessage;
|
pub use request_sector_state::RequestSectorStateMessage;
|
||||||
pub use sector_command::SectorCommandMessage;
|
pub use sector_command::SectorCommandMessage;
|
||||||
|
pub use sector_heartbeat::SectorHeartbeatMessage;
|
||||||
pub use sector_state::SectorStateMessage;
|
pub use sector_state::SectorStateMessage;
|
||||||
pub use server_error::ServerErrorMessage;
|
pub use server_error::ServerErrorMessage;
|
||||||
pub use start_mission::StartMissionMessage;
|
pub use start_mission::StartMissionMessage;
|
||||||
|
|
@ -56,6 +58,7 @@ pub mod message_type {
|
||||||
pub const HOME_LOGIC_STOPPED: u16 = 14105;
|
pub const HOME_LOGIC_STOPPED: u16 = 14105;
|
||||||
pub const CANCEL_MATCHMAKE: u16 = 14107;
|
pub const CANCEL_MATCHMAKE: u16 = 14107;
|
||||||
pub const REQUEST_SECTOR_STATE: u16 = 12903;
|
pub const REQUEST_SECTOR_STATE: u16 = 12903;
|
||||||
|
pub const SECTOR_HEARTBEAT: u16 = 21902;
|
||||||
pub const SECTOR_COMMAND: u16 = 12904;
|
pub const SECTOR_COMMAND: u16 = 12904;
|
||||||
pub const SEND_BATTLE_EVENT: u16 = 12951;
|
pub const SEND_BATTLE_EVENT: u16 = 12951;
|
||||||
pub const BATTLE_EVENT: u16 = 22952;
|
pub const BATTLE_EVENT: u16 = 22952;
|
||||||
|
|
|
||||||
17
crates/logic/src/messages/sector_heartbeat.rs
Normal file
17
crates/logic/src/messages/sector_heartbeat.rs
Normal file
|
|
@ -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<u8>,
|
||||||
|
}
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue