give every object a new id once the client is live

the client never asks for the state, so the objects it decoded before
the battle screen installed its listener were stuck without models for
the whole battle - and every later snapshot matched them by global id
and reused them, so the "newly created" flag the visual depends on was
never set again.

the first sector command is the client telling us it is up. at that
point every object is handed a fresh id: nothing matches, so the client
builds them all from scratch and they get models. the leaders and the
tower lists are remapped with them and the column is re-sorted, since
the client binary searches it.

the battle itself is complete as of this run: a princess tower fell and
the first crown was scored, towers (2, 1) and stars (1, 0) in the log.
This commit is contained in:
WiseDev 2026-08-23 14:36:16 +03:00
parent 8699a6884d
commit eb0e59f6f4
2 changed files with 53 additions and 1 deletions

View file

@ -4,7 +4,8 @@ pub const SNAPSHOT_INTERVAL_TICKS: i32 = 4;
pub const MAX_CATCH_UP_TICKS: i32 = 40; pub const MAX_CATCH_UP_TICKS: i32 = 40;
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, LogicGameObjectRef, LogicVector2,
BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP, CHARACTER_OBJECT_TYPE,
}; };
use logic::battle::{verify_snapshot, LogicBattleEvent}; use logic::battle::{verify_snapshot, LogicBattleEvent};
use logic::SectorStateMessage; use logic::SectorStateMessage;
@ -25,6 +26,7 @@ pub struct BattleSession {
next_bot_play: i32, next_bot_play: i32,
pub pushed_snapshots: u32, pub pushed_snapshots: u32,
next_instance: i32, next_instance: i32,
announced: bool,
pending_bot_play: Option<(LogicDataRef, LogicVector2, i32)>, pending_bot_play: Option<(LogicDataRef, LogicVector2, i32)>,
} }
impl BattleSession { impl BattleSession {
@ -53,9 +55,43 @@ impl BattleSession {
next_bot_play: crate::bot::BOT_OPENING_DELAY_SECONDS * BATTLE_TICKS_PER_SECOND, next_bot_play: crate::bot::BOT_OPENING_DELAY_SECONDS * BATTLE_TICKS_PER_SECOND,
pushed_snapshots: 0, pushed_snapshots: 0,
next_instance: first_instance, next_instance: first_instance,
announced: false,
pending_bot_play: None, pending_bot_play: None,
} }
} }
pub fn reannounce(&mut self) {
let mut remap: Vec<(LogicGameObjectRef, LogicGameObjectRef)> = Vec::new();
for entry in self.mode.battle.objects.objects.iter_mut() {
let fresh = LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, self.next_instance);
self.next_instance += 1;
remap.push((entry.global_id, fresh));
entry.global_id = fresh;
}
let translate = |old: &LogicGameObjectRef| -> Option<LogicGameObjectRef> {
remap
.iter()
.find(|(was, _)| was == old)
.map(|(_, now)| *now)
};
for leader in self.mode.battle.leaders.iter_mut() {
if let Some(now) = translate(leader) {
*leader = now;
}
}
for towers in self.mode.battle.leader_towers.iter_mut() {
for tower in towers.iter_mut() {
if let Some(now) = translate(tower) {
*tower = now;
}
}
}
self.mode
.battle
.objects
.objects
.sort_by_key(|entry| entry.global_id.0.map(|id| (id.class_id, id.instance_id)));
self.next_snapshot = self.tick;
}
pub fn take_bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> { pub fn take_bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> {
self.pending_bot_play.take() self.pending_bot_play.take()
} }
@ -347,8 +383,21 @@ impl BattleRegistry {
pub async fn resend(&self, account: AccountRef) -> Option<WireMessage> { pub async fn resend(&self, account: AccountRef) -> Option<WireMessage> {
let handle = self.session(account).await?; let handle = self.session(account).await?;
let mut session = handle.lock().await; let mut session = handle.lock().await;
session.reannounce();
session.snapshot_message() session.snapshot_message()
} }
pub async fn announce_once(&self, account: AccountRef) -> bool {
let Some(handle) = self.session(account).await else {
return false;
};
let mut session = handle.lock().await;
if session.announced {
return false;
}
session.announced = true;
session.reannounce();
true
}
pub async fn send(&self, account: AccountRef, message: WireMessage) { pub async fn send(&self, account: AccountRef, message: WireMessage) {
if let Some(handle) = self.session(account).await { if let Some(handle) = self.session(account).await {
handle.lock().await.push_outbound(message); handle.lock().await.push_outbound(message);

View file

@ -488,6 +488,9 @@ impl GameApi for GameService {
let Ok(sector) = SectorCommandMessage::from_bytes(&payload) else { let Ok(sector) = SectorCommandMessage::from_bytes(&payload) else {
return Ok(()); return Ok(());
}; };
if self.running_battles.announce_once(account).await {
tracing::info!(%account, "client is live, announcing the objects again");
}
let progress = self let progress = self
.running_battles .running_battles
.advance(account, sector.client_tick) .advance(account, sector.client_tick)