From 7a0ba1b65242e8ada4b62d52ce33e8fb0bcf9517 Mon Sep 17 00:00:00 2001 From: WiseDev <83840010+wisedevik@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:01:26 +0300 Subject: [PATCH] give the server its own battle clock and end condition LogicBattle::isEndConditionMatched, transcribed: the battle is over when end_counter is positive, when either king is dead, when tick/20 seconds reach MatchLength + OvertimeSeconds, or - once past MatchLength - when the crowns differ. the divisor is the 0x66666667/2^35 multiply in the client, which is a divide by twenty, so the battle runs at 20 ticks a second. crowns come from LogicSummoner::getStars: three when the enemy king is down, otherwise two minus the enemy princess towers still standing, which is exactly what leader_towers holds. BattleRegistry keeps the LogicGameMode we built for each account and advances it on the tick the client reports in its turn message, so the server now tracks the clock, the crowns and whether the battle is over, and drops the session when the player goes home. the two isSummoner guards in the overtime branch are left out: they only fire for an object that is not a summoner, which a king tower always is. --- crates/game-service/src/battle_session.rs | 65 ++++++++++++++++++++ crates/game-service/src/lib.rs | 2 + crates/game-service/src/service.rs | 22 ++++++- crates/logic/src/battle/logic_battle.rs | 61 ++++++++++++++++++ crates/logic/src/battle/logic_game_object.rs | 15 +++++ crates/logic/src/battle/mod.rs | 4 +- 6 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 crates/game-service/src/battle_session.rs diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs new file mode 100644 index 0000000..5ae66aa --- /dev/null +++ b/crates/game-service/src/battle_session.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; +use logic::battle::{LogicGameMode, BATTLE_TICKS_PER_SECOND}; +use service_rpc::AccountRef; +use tokio::sync::Mutex; +pub struct BattleSession { + mode: LogicGameMode, + tick: i32, +} +impl BattleSession { + pub fn new(mode: LogicGameMode) -> Self { + Self { mode, tick: 0 } + } + pub fn mode(&self) -> &LogicGameMode { + &self.mode + } + pub fn tick(&self) -> i32 { + self.tick + } + pub fn seconds(&self) -> i32 { + self.tick / BATTLE_TICKS_PER_SECOND + } + pub fn advance_to(&mut self, tick: i32) { + if tick > self.tick { + self.tick = tick; + self.mode.time.tick = tick; + } + } + pub fn is_finished(&self) -> bool { + self.mode.battle.is_end_condition_matched(self.tick) + } + pub fn stars(&self) -> (i32, i32) { + (self.mode.battle.stars(0), self.mode.battle.stars(1)) + } +} +#[derive(Default)] +pub struct BattleRegistry { + sessions: Mutex>, +} +impl BattleRegistry { + pub async fn start(&self, account: AccountRef, mode: LogicGameMode) { + self.sessions + .lock() + .await + .insert(account, BattleSession::new(mode)); + } + pub async fn finish(&self, account: AccountRef) -> Option { + self.sessions.lock().await.remove(&account) + } + pub async fn advance(&self, account: AccountRef, tick: i32) -> Option { + let mut sessions = self.sessions.lock().await; + let session = sessions.get_mut(&account)?; + session.advance_to(tick); + Some(BattleProgress { + seconds: session.seconds(), + finished: session.is_finished(), + stars: session.stars(), + }) + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BattleProgress { + pub seconds: i32, + pub finished: bool, + pub stars: (i32, i32), +} diff --git a/crates/game-service/src/lib.rs b/crates/game-service/src/lib.rs index febc3cb..0569391 100644 --- a/crates/game-service/src/lib.rs +++ b/crates/game-service/src/lib.rs @@ -1,4 +1,5 @@ pub mod battle; +pub mod battle_session; pub mod bot; pub mod catalog; pub mod config; @@ -10,6 +11,7 @@ pub mod shop; pub mod store; pub mod time; pub use battle::BattleBuilder; +pub use battle_session::{BattleProgress, BattleRegistry, BattleSession}; pub use bot::BotPlayer; pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE}; pub use config::{CardRef, DataSelector, GameConfig, StarterProfile}; diff --git a/crates/game-service/src/service.rs b/crates/game-service/src/service.rs index 5ebba46..461bb8a 100644 --- a/crates/game-service/src/service.rs +++ b/crates/game-service/src/service.rs @@ -11,6 +11,7 @@ use service_rpc::{ }; use titan::{Message, MessageMeta, Payload}; use crate::battle::BattleBuilder; +use crate::battle_session::BattleRegistry; use crate::catalog::Catalog; use crate::config::GameConfig; use crate::bot::BotPlayer; @@ -33,6 +34,7 @@ pub struct GameService { catalog: Arc, shop: Arc, battles: Arc, + running_battles: BattleRegistry, sessions: HomeModeRegistry, } impl GameService { @@ -63,6 +65,7 @@ impl GameService { catalog, shop, battles, + running_battles: BattleRegistry::default(), sessions: HomeModeRegistry::default(), })) } @@ -150,6 +153,7 @@ impl GameService { format!("{} trailing byte(s)", report.trailing) }))); } + self.running_battles.start(account, battle).await; Ok(vec![encode(&SectorStateMessage::new(snapshot))?]) } async fn roller(&self, account: AccountRef) -> RpcResult { @@ -197,6 +201,7 @@ impl GameApi for GameService { .sessions .get_or_open(account, &profile, unix_seconds() as i32) .await; + let mut came_back_from_battle = false; let (home_data, checksum, cycle, on_sale): ( OwnHomeDataMessage, i32, @@ -205,8 +210,8 @@ impl GameApi for GameService { ) = { let mut home = session.lock().await; if kind == HomeRequestKind::GoHome && home.is_home_logic_stopped() { - tracing::info!(%account, "client came back from the battle, resuming home logic"); home.resume_home_logic(); + came_back_from_battle = true; } ( home.own_home_data(self.config.random_seed), @@ -219,6 +224,16 @@ impl GameApi for GameService { .collect(), ) }; + if came_back_from_battle { + let battle = self.running_battles.finish(account).await; + tracing::info!( + %account, + seconds = battle.as_ref().map(|state| state.seconds()), + finished = battle.as_ref().map(|state| state.is_finished()), + stars = ?battle.as_ref().map(|state| state.stars()), + "client came back from the battle, resuming home logic" + ); + } tracing::info!( %account, ?kind, @@ -291,10 +306,13 @@ impl GameApi for GameService { (home.end_client_turn(&turn, &roller, &self.shop), in_battle) }; if in_battle { + let progress = self.running_battles.advance(account, turn.tick).await; tracing::info!( %account, tick = turn.tick, - checksum = turn.checksum, + seconds = progress.map(|state| state.seconds), + finished = progress.map(|state| state.finished), + stars = ?progress.map(|state| state.stars), commands = ?turn .commands .iter() diff --git a/crates/logic/src/battle/logic_battle.rs b/crates/logic/src/battle/logic_battle.rs index b1540d3..3efbf6c 100644 --- a/crates/logic/src/battle/logic_battle.rs +++ b/crates/logic/src/battle/logic_battle.rs @@ -1,4 +1,5 @@ use titan::{ByteStreamReader, ByteStreamWriter, LogicLong, Payload, Result}; +use crate::battle::logic_game_object::LogicGameObjectEntry; use crate::battle::logic_game_object_manager::LogicGameObjectManager; use crate::battle::logic_game_object_ref::LogicGameObjectRef; use crate::data::LogicDataRef; @@ -7,6 +8,11 @@ pub const BATTLE_TYPE_PVP: i32 = 0; pub const BATTLE_TYPE_NPC: i32 = 1; pub const BATTLE_TYPE_REPLAY: i32 = 3; pub const BATTLE_INT_ARRAY: usize = 8; +pub const BATTLE_TICKS_PER_SECOND: i32 = 20; +pub const LEADER_TOWER_COUNT: i32 = 2; +pub const CROWNS_FOR_LEADER: i32 = 3; +pub const LOCATION_MATCH_LENGTH_COLUMN: &str = "MatchLength"; +pub const LOCATION_OVERTIME_COLUMN: &str = "OvertimeSeconds"; pub const BATTLE_TRAILING_INTS: usize = 6; #[derive(Debug, Clone, PartialEq, Eq)] pub struct LogicBattle { @@ -67,6 +73,61 @@ impl LogicBattle { pub fn is_npc_battle(&self) -> bool { self.battle_type == BATTLE_TYPE_NPC && !self.npc.is_none() } + pub fn match_length_seconds(&self) -> i32 { + self.location + .data() + .map(|row| row.int(LOCATION_MATCH_LENGTH_COLUMN)) + .unwrap_or(0) + } + pub fn overtime_length_seconds(&self) -> i32 { + self.location + .data() + .map(|row| row.int(LOCATION_OVERTIME_COLUMN)) + .unwrap_or(0) + } + pub fn leader(&self, index: usize) -> Option<&LogicGameObjectEntry> { + let id = self.leaders.get(index)?; + self.objects + .objects + .iter() + .find(|entry| entry.global_id == *id) + } + pub fn is_leader_alive(&self, index: usize) -> bool { + self.leader(index).map(|entry| entry.is_alive()).unwrap_or(false) + } + pub fn stars(&self, index: usize) -> i32 { + let other = 1 - index.min(1); + if !self.is_leader_alive(other) { + return CROWNS_FOR_LEADER; + } + let standing = self + .leader_towers + .get(other) + .map(|towers| towers.len() as i32) + .unwrap_or(0) + .min(LEADER_TOWER_COUNT); + LEADER_TOWER_COUNT - standing + } + pub fn is_end_condition_matched(&self, tick: i32) -> bool { + if self.end_counter > 0 { + return true; + } + if !self.is_leader_alive(0) || !self.is_leader_alive(1) { + return true; + } + let match_length = self.match_length_seconds(); + if match_length < 1 { + return false; + } + let seconds = tick / BATTLE_TICKS_PER_SECOND; + if seconds >= match_length + self.overtime_length_seconds() { + return true; + } + if seconds < match_length { + return false; + } + self.stars(0) != self.stars(1) + } } impl Payload for LogicBattle { fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { diff --git a/crates/logic/src/battle/logic_game_object.rs b/crates/logic/src/battle/logic_game_object.rs index 41e6876..7fed6a3 100644 --- a/crates/logic/src/battle/logic_game_object.rs +++ b/crates/logic/src/battle/logic_game_object.rs @@ -58,6 +58,21 @@ pub struct LogicGameObjectEntry { pub components: [Option; COMPONENT_PASSES], } impl LogicGameObjectEntry { + pub fn hitpoints(&self) -> Option { + match self + .components + .get(crate::battle::logic_component::COMPONENT_HITPOINT)? + .as_ref()? + { + crate::battle::logic_component::LogicComponent::Hitpoint(component) => { + Some(component.hitpoints) + } + _ => None, + } + } + pub fn is_alive(&self) -> bool { + self.hitpoints().map(|value| value > 0).unwrap_or(false) + } pub fn new( data: LogicDataRef, global_id: LogicGameObjectRef, diff --git a/crates/logic/src/battle/mod.rs b/crates/logic/src/battle/mod.rs index ba3a5e4..40dc47c 100644 --- a/crates/logic/src/battle/mod.rs +++ b/crates/logic/src/battle/mod.rs @@ -11,8 +11,8 @@ mod logic_time; mod logic_tutorial_manager; mod verify; pub use logic_battle::{ - LogicBattle, BATTLE_INT_ARRAY, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, BATTLE_TYPE_PVP, - BATTLE_TYPE_REPLAY, + LogicBattle, BATTLE_INT_ARRAY, BATTLE_TICKS_PER_SECOND, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, + BATTLE_TYPE_PVP, BATTLE_TYPE_REPLAY, CROWNS_FOR_LEADER, LEADER_TOWER_COUNT, }; pub use logic_character::{ LogicCharacter, LogicSummoner, LogicSummonerDeck, DEFAULT_SIZE, DIRECTION_BOTTOM, DIRECTION_TOP,