From 9230a212efeed20a06bb636871b4ac17bd960547 Mon Sep 17 00:00:00 2001 From: WiseDev <83840010+wisedevik@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:03:26 +0300 Subject: [PATCH] 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. --- crates/game-service/src/battle_session.rs | 8 +++ crates/game-service/src/service.rs | 9 ++- crates/logic/src/battle/logic_component.rs | 4 ++ crates/logic/src/battle/logic_simulation.rs | 76 +++++++++++++++++++-- 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs index 09c2d34..e565d52 100644 --- a/crates/game-service/src/battle_session.rs +++ b/crates/game-service/src/battle_session.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20; +pub const MAX_CATCH_UP_TICKS: i32 = 40; use std::time::Instant; use logic::battle::{ LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP, @@ -105,6 +106,12 @@ impl BattleSession { fn snapshot_message(&mut self) -> Option { let snapshot = self.mode.snapshot().ok()?; 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() { tracing::error!( bytes = snapshot.len(), @@ -174,6 +181,7 @@ impl BattleSession { self.queued.push((at_tick.max(self.tick + 1), entries)); } pub fn advance_to(&mut self, tick: i32) { + let tick = tick.min(self.tick + MAX_CATCH_UP_TICKS); while self.tick < tick { self.tick += 1; self.mode.time.tick = self.tick; diff --git a/crates/game-service/src/service.rs b/crates/game-service/src/service.rs index b5468e8..69d4f7a 100644 --- a/crates/game-service/src/service.rs +++ b/crates/game-service/src/service.rs @@ -456,12 +456,17 @@ impl GameApi for GameService { Matched::Waiting => Ok(Vec::new()), }; } - Ok(self + let messages = self .running_battles .tick(account, |card, position, owner, instance| { self.battles.summon(card, position, owner, 0, instance) }) - .await) + .await; + if !messages.is_empty() { + let kinds: Vec = 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) -> RpcResult<()> { let Ok(sent) = SendBattleEventMessage::from_bytes(&payload) else { diff --git a/crates/logic/src/battle/logic_component.rs b/crates/logic/src/battle/logic_component.rs index 960949d..3783ef5 100644 --- a/crates/logic/src/battle/logic_component.rs +++ b/crates/logic/src/battle/logic_component.rs @@ -65,6 +65,8 @@ pub struct LogicMovementComponent { pub pushback_speed: i32, pub move_timer: i32, pub jump_distance: i32, + pub goal: Option<(i32, i32)>, + pub route: Vec<(i32, i32)>, } impl Default for LogicMovementComponent { fn default() -> Self { @@ -89,6 +91,8 @@ impl Default for LogicMovementComponent { pushback_speed: 0, move_timer: 0, jump_distance: 0, + goal: None, + route: Vec::new(), } } } diff --git a/crates/logic/src/battle/logic_simulation.rs b/crates/logic/src/battle/logic_simulation.rs index 298f5db..3618164 100644 --- a/crates/logic/src/battle/logic_simulation.rs +++ b/crates/logic/src/battle/logic_simulation.rs @@ -23,6 +23,7 @@ pub const COLUMN_TARGET_ONLY_BUILDINGS: &str = "TargetOnlyBuildings"; pub const COLUMN_FLYING_HEIGHT: &str = "FlyingHeight"; pub const COLUMN_PROJECTILE: &str = "Projectile"; pub const DISTANCE_SATURATION: i32 = 46340; +pub const WAYPOINT_REACHED: i32 = 250; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LogicCharacterStats { pub speed: i32, @@ -130,6 +131,23 @@ impl LogicGameObjectEntry { 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> { match self.components.get_mut(COMPONENT_COMBAT)? { Some(LogicComponent::Combat(component)) => Some(component), @@ -239,6 +257,57 @@ impl LogicBattle { } 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) { let positions: Vec<(i32, i32)> = self .objects @@ -272,12 +341,7 @@ impl LogicBattle { if straight <= stand_off as i64 || straight == 0 { continue; } - let waypoint = self - .tilemap - .as_ref() - .map(|tilemap| crate::battle::find_path(tilemap, from, goal, stats.flying)) - .and_then(|path| path.first().copied()) - .unwrap_or(goal); + let waypoint = self.next_waypoint(index, from, goal, stats.flying); let dx = (waypoint.0 - from.0) as i64; let dy = (waypoint.1 - from.1) as i64; let leg = integer_sqrt(dx * dx + dy * dy);