never take a leader off the board

the client crashed in LogicBattle::resetSimulatedManaTimers again, and
this time it was ours: remove_dead was dropping a king tower once it
fell, while battle.leaders still pointed at it. that function reads both
leaders straight out of the battle and calls a virtual on each without a
null check, so the next state to arrive killed the client.

the client's own rule is one line:

  LogicCharacter::shouldDestruct() { if (this[209]) return 0; ... }

isLeader, never destructs. remove_dead now keeps them the same way, and
the harness watches every tick of a full match for a leader that has
gone missing - it caught this one at tick 693.
This commit is contained in:
WiseDev 2026-08-23 17:37:02 +03:00
parent f5a8c39ab2
commit 14786cbc92
2 changed files with 54 additions and 2 deletions

View file

@ -122,6 +122,19 @@ fn the_bot_deploys_in_front_of_its_own_towers() {
} }
} }
session.advance_to(tick); session.advance_to(tick);
for (side, leader) in session.mode().battle.leaders.iter().enumerate() {
let alive = session
.mode()
.battle
.objects
.objects
.iter()
.any(|entry| entry.global_id == *leader);
assert!(
alive,
"t={tick}: side {side}'s leader {leader:?} is no longer in the battle"
);
}
if session.is_finished() || session.towers_standing().0 == 0 { if session.is_finished() || session.towers_standing().0 == 0 {
println!("battle over at t={tick} after {plays} bot plays"); println!("battle over at t={tick} after {plays} bot plays");
break; break;
@ -205,3 +218,39 @@ fn a_bot_troop_walks_at_the_player_towers() {
"the bot troop walked away from the player towers to {end:?}" "the bot troop walked away from the player towers to {end:?}"
); );
} }
#[test]
fn both_leaders_resolve_to_a_summoner() {
let root = assets();
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
LogicDataTables::install(Arc::new(tables));
}
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");
println!("leaders: {:?}", mode.battle.leaders);
for (side, leader) in mode.battle.leaders.iter().enumerate() {
assert!(
!leader.is_none(),
"side {side} has no leader; LogicBattle::resetSimulatedManaTimers reads both \
without checking for null and takes the client down with it"
);
let found = mode
.battle
.objects
.objects
.iter()
.find(|entry| entry.global_id == *leader);
assert!(
found.is_some(),
"side {side}'s leader {leader:?} matches no object in the battle"
);
}
}

View file

@ -514,17 +514,20 @@ impl LogicBattle {
} }
} }
fn remove_dead(&mut self) { fn remove_dead(&mut self) {
let leaders = self.leaders;
let dead: Vec<crate::battle::logic_game_object_ref::LogicGameObjectRef> = self let dead: Vec<crate::battle::logic_game_object_ref::LogicGameObjectRef> = self
.objects .objects
.objects .objects
.iter() .iter()
.filter(|entry| !entry.is_alive()) .filter(|entry| !entry.is_alive() && !leaders.contains(&entry.global_id))
.map(|entry| entry.global_id) .map(|entry| entry.global_id)
.collect(); .collect();
if dead.is_empty() { if dead.is_empty() {
return; return;
} }
self.objects.objects.retain(LogicGameObjectEntry::is_alive); self.objects
.objects
.retain(|entry| entry.is_alive() || leaders.contains(&entry.global_id));
for towers in self.leader_towers.iter_mut() { for towers in self.leader_towers.iter_mut() {
towers.retain(|tower| !dead.contains(tower)); towers.retain(|tower| !dead.contains(tower));
} }