keep the path instead of finding it again every tick

the tick was running A* over the whole 36x64 grid for every unit, every
tick. catching up ten seconds meant thousands of searches inside the
session lock, so the tick loop never finished, snapshots never went out,
and the client - which refuses to send a command while
isFullUpdatePending is true - sat there showing the connection icon and
would not spawn anything. ctrl-c looked like a hang for the same reason:
a task stuck in that loop.

the client does not do this either. LogicMovementComponent carries a
path array precisely so the route is found once and walked. we keep the
route and the goal it was found for, drop a node once we are within
250 units of it, and only search again when the goal moves or the route
runs out.

advance_to also refuses to simulate more than forty ticks in one call,
so a late tick can never turn into an unbounded loop under the lock.
This commit is contained in:
WiseDev 2026-08-23 14:03:26 +03:00
parent f51d76f780
commit 9230a212ef
4 changed files with 89 additions and 8 deletions

View file

@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20; pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20;
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, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP,
@ -105,6 +106,12 @@ impl BattleSession {
fn snapshot_message(&mut self) -> Option<WireMessage> { fn snapshot_message(&mut self) -> Option<WireMessage> {
let snapshot = self.mode.snapshot().ok()?; let snapshot = self.mode.snapshot().ok()?;
let report = verify_snapshot(&snapshot); let report = verify_snapshot(&snapshot);
tracing::debug!(
tick = self.tick,
bytes = snapshot.len(),
ok = report.is_ok(),
"built a battle snapshot"
);
if !report.is_ok() { if !report.is_ok() {
tracing::error!( tracing::error!(
bytes = snapshot.len(), bytes = snapshot.len(),
@ -174,6 +181,7 @@ impl BattleSession {
self.queued.push((at_tick.max(self.tick + 1), entries)); self.queued.push((at_tick.max(self.tick + 1), entries));
} }
pub fn advance_to(&mut self, tick: i32) { pub fn advance_to(&mut self, tick: i32) {
let tick = tick.min(self.tick + MAX_CATCH_UP_TICKS);
while self.tick < tick { while self.tick < tick {
self.tick += 1; self.tick += 1;
self.mode.time.tick = self.tick; self.mode.time.tick = self.tick;

View file

@ -456,12 +456,17 @@ impl GameApi for GameService {
Matched::Waiting => Ok(Vec::new()), Matched::Waiting => Ok(Vec::new()),
}; };
} }
Ok(self let messages = self
.running_battles .running_battles
.tick(account, |card, position, owner, instance| { .tick(account, |card, position, owner, instance| {
self.battles.summon(card, position, owner, 0, instance) self.battles.summon(card, position, owner, 0, instance)
}) })
.await) .await;
if !messages.is_empty() {
let kinds: Vec<u16> = messages.iter().map(|message| message.message_type).collect();
tracing::debug!(%account, sent = ?kinds, "battle tick");
}
Ok(messages)
} }
async fn battle_event(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> { async fn battle_event(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
let Ok(sent) = SendBattleEventMessage::from_bytes(&payload) else { let Ok(sent) = SendBattleEventMessage::from_bytes(&payload) else {

View file

@ -65,6 +65,8 @@ pub struct LogicMovementComponent {
pub pushback_speed: i32, pub pushback_speed: i32,
pub move_timer: i32, pub move_timer: i32,
pub jump_distance: i32, pub jump_distance: i32,
pub goal: Option<(i32, i32)>,
pub route: Vec<(i32, i32)>,
} }
impl Default for LogicMovementComponent { impl Default for LogicMovementComponent {
fn default() -> Self { fn default() -> Self {
@ -89,6 +91,8 @@ impl Default for LogicMovementComponent {
pushback_speed: 0, pushback_speed: 0,
move_timer: 0, move_timer: 0,
jump_distance: 0, jump_distance: 0,
goal: None,
route: Vec::new(),
} }
} }
} }

View file

@ -23,6 +23,7 @@ pub const COLUMN_TARGET_ONLY_BUILDINGS: &str = "TargetOnlyBuildings";
pub const COLUMN_FLYING_HEIGHT: &str = "FlyingHeight"; pub const COLUMN_FLYING_HEIGHT: &str = "FlyingHeight";
pub const COLUMN_PROJECTILE: &str = "Projectile"; pub const COLUMN_PROJECTILE: &str = "Projectile";
pub const DISTANCE_SATURATION: i32 = 46340; pub const DISTANCE_SATURATION: i32 = 46340;
pub const WAYPOINT_REACHED: i32 = 250;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LogicCharacterStats { pub struct LogicCharacterStats {
pub speed: i32, pub speed: i32,
@ -130,6 +131,23 @@ impl LogicGameObjectEntry {
component.hitpoints = (component.hitpoints - amount.max(0)).max(0); component.hitpoints = (component.hitpoints - amount.max(0)).max(0);
} }
} }
pub fn movement(&self) -> Option<&crate::battle::logic_component::LogicMovementComponent> {
match self.components.get(crate::battle::logic_component::COMPONENT_MOVEMENT)? {
Some(LogicComponent::Movement(component)) => Some(component),
_ => None,
}
}
pub fn movement_mut(
&mut self,
) -> Option<&mut crate::battle::logic_component::LogicMovementComponent> {
match self
.components
.get_mut(crate::battle::logic_component::COMPONENT_MOVEMENT)?
{
Some(LogicComponent::Movement(component)) => Some(component),
_ => None,
}
}
pub fn combat_mut(&mut self) -> Option<&mut crate::battle::logic_component::LogicCombatComponent> { pub fn combat_mut(&mut self) -> Option<&mut crate::battle::logic_component::LogicCombatComponent> {
match self.components.get_mut(COMPONENT_COMBAT)? { match self.components.get_mut(COMPONENT_COMBAT)? {
Some(LogicComponent::Combat(component)) => Some(component), Some(LogicComponent::Combat(component)) => Some(component),
@ -239,6 +257,57 @@ impl LogicBattle {
} }
best.map(|(_, other)| other) best.map(|(_, other)| other)
} }
fn next_waypoint(
&mut self,
index: usize,
from: (i32, i32),
goal: (i32, i32),
flying: bool,
) -> (i32, i32) {
let stale = match self.objects.objects[index].movement() {
Some(movement) => movement.goal != Some(goal) || movement.path.is_empty(),
None => return goal,
};
if stale {
let path = self
.tilemap
.as_ref()
.map(|tilemap| crate::battle::find_path(tilemap, from, goal, flying))
.unwrap_or_default();
if let Some(movement) = self.objects.objects[index].movement_mut() {
movement.goal = Some(goal);
movement.route = path;
}
}
let reached = self
.objects
.objects
.get(index)
.and_then(LogicGameObjectEntry::movement)
.and_then(|movement| movement.route.first().copied())
.map(|node| {
let close = distance_squared(from, node) <= WAYPOINT_REACHED * WAYPOINT_REACHED;
(node, close)
});
match reached {
Some((node, true)) => {
if let Some(movement) = self.objects.objects[index].movement_mut() {
if !movement.route.is_empty() {
movement.route.remove(0);
}
}
let _ = node;
self.objects
.objects
.get(index)
.and_then(LogicGameObjectEntry::movement)
.and_then(|movement| movement.route.first().copied())
.unwrap_or(goal)
}
Some((node, false)) => node,
None => goal,
}
}
fn move_objects(&mut self) { fn move_objects(&mut self) {
let positions: Vec<(i32, i32)> = self let positions: Vec<(i32, i32)> = self
.objects .objects
@ -272,12 +341,7 @@ impl LogicBattle {
if straight <= stand_off as i64 || straight == 0 { if straight <= stand_off as i64 || straight == 0 {
continue; continue;
} }
let waypoint = self let waypoint = self.next_waypoint(index, from, goal, stats.flying);
.tilemap
.as_ref()
.map(|tilemap| crate::battle::find_path(tilemap, from, goal, stats.flying))
.and_then(|path| path.first().copied())
.unwrap_or(goal);
let dx = (waypoint.0 - from.0) as i64; let dx = (waypoint.0 - from.0) as i64;
let dy = (waypoint.1 - from.1) as i64; let dy = (waypoint.1 - from.1) as i64;
let leg = integer_sqrt(dx * dx + dy * dy); let leg = integer_sqrt(dx * dx + dy * dy);