From 41eca5b620b2a74d3e61d62582906b37540b5851 Mon Sep 17 00:00:00 2001 From: WiseDev <83840010+wisedevik@users.noreply.github.com> Date: Sun, 23 Aug 2026 10:00:16 +0300 Subject: [PATCH] battle model: LogicBattle, the object manager and BattleResultMessage field order taken straight off LogicBattle::encode. the object manager is columnar, not object major: 6 counters, a count, then all data refs, then all global ids, then all objects, then four component passes. global ids must be ascending, the client binary searches them. LogicRandom moved out of the shop into its own module. its abs was checked_abs().unwrap_or(0), the binary uses NEGS so i32::MIN stays i32::MIN. one seed in four billion, but it would have desynced the card shop. per object subclass encodes and the component payloads are still missing, so no snapshot can be sent yet. --- crates/logic/src/battle/logic_battle.rs | 201 ++++++++++++++++++ crates/logic/src/battle/logic_game_object.rs | 39 ++++ .../src/battle/logic_game_object_manager.rs | 81 +++++++ .../logic/src/battle/logic_game_object_ref.rs | 32 +++ crates/logic/src/battle/logic_time.rs | 11 + .../src/battle/logic_tutorial_manager.rs | 10 + crates/logic/src/battle/mod.rs | 18 ++ crates/logic/src/home/mod.rs | 3 +- crates/logic/src/home/shop.rs | 26 +-- crates/logic/src/lib.rs | 5 +- crates/logic/src/logic_random.rs | 41 ++++ crates/logic/src/messages/battle_result.rs | 49 +++++ crates/logic/src/messages/mod.rs | 4 + 13 files changed, 492 insertions(+), 28 deletions(-) create mode 100644 crates/logic/src/battle/logic_battle.rs create mode 100644 crates/logic/src/battle/logic_game_object.rs create mode 100644 crates/logic/src/battle/logic_game_object_manager.rs create mode 100644 crates/logic/src/battle/logic_game_object_ref.rs create mode 100644 crates/logic/src/battle/logic_time.rs create mode 100644 crates/logic/src/battle/logic_tutorial_manager.rs create mode 100644 crates/logic/src/battle/mod.rs create mode 100644 crates/logic/src/logic_random.rs create mode 100644 crates/logic/src/messages/battle_result.rs diff --git a/crates/logic/src/battle/logic_battle.rs b/crates/logic/src/battle/logic_battle.rs new file mode 100644 index 0000000..b1540d3 --- /dev/null +++ b/crates/logic/src/battle/logic_battle.rs @@ -0,0 +1,201 @@ +use titan::{ByteStreamReader, ByteStreamWriter, LogicLong, Payload, Result}; +use crate::battle::logic_game_object_manager::LogicGameObjectManager; +use crate::battle::logic_game_object_ref::LogicGameObjectRef; +use crate::data::LogicDataRef; +use crate::model::LogicSpellDeck; +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_TRAILING_INTS: usize = 6; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogicBattle { + pub location: LogicDataRef, + pub npc: LogicDataRef, + pub arena: LogicDataRef, + pub account_ids: [LogicLong; 2], + pub unknown_20: i32, + pub unknown_24: i32, + pub counters: [i32; BATTLE_INT_ARRAY], + pub battle_type: i32, + pub end_counter: i32, + pub winner_index: i32, + pub battle_ended_called: bool, + pub battle_ended_with_timeout: bool, + pub unknown_182: bool, + pub player_finished_npc_level: bool, + pub show_start_hud: bool, + pub is_on_overtime: bool, + pub objects: LogicGameObjectManager, + pub decks: [Option; 2], + pub leaders: [LogicGameObjectRef; 2], + pub leader_towers: [Vec; 2], + pub winner_score_change: i32, + pub loser_score_change: i32, + pub trailing: [i32; BATTLE_TRAILING_INTS], +} +impl Default for LogicBattle { + fn default() -> Self { + Self { + location: LogicDataRef::None, + npc: LogicDataRef::None, + arena: LogicDataRef::None, + account_ids: [LogicLong::default(); 2], + unknown_20: 0, + unknown_24: 0, + counters: [0; BATTLE_INT_ARRAY], + battle_type: BATTLE_TYPE_NPC, + end_counter: 0, + winner_index: 0, + battle_ended_called: false, + battle_ended_with_timeout: false, + unknown_182: false, + player_finished_npc_level: false, + show_start_hud: true, + is_on_overtime: false, + objects: LogicGameObjectManager::default(), + decks: [None, None], + leaders: [LogicGameObjectRef::NONE; 2], + leader_towers: [Vec::new(), Vec::new()], + winner_score_change: 0, + loser_score_change: 0, + trailing: [0; BATTLE_TRAILING_INTS], + } + } +} +impl LogicBattle { + pub fn is_npc_battle(&self) -> bool { + self.battle_type == BATTLE_TYPE_NPC && !self.npc.is_none() + } +} +impl Payload for LogicBattle { + fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { + self.location.encode(writer)?; + self.npc.encode(writer)?; + self.arena.encode(writer)?; + for account in &self.account_ids { + writer.write_vlong(account.high, account.low); + } + writer.write_vint(self.unknown_20); + writer.write_vint(self.unknown_24); + for value in self.counters { + writer.write_vint(value); + } + writer.write_vint(self.battle_type); + writer.write_vint(self.end_counter); + writer.write_vint(self.winner_index); + writer.write_boolean(self.battle_ended_called); + writer.write_boolean(self.battle_ended_with_timeout); + writer.write_boolean(self.unknown_182); + writer.write_boolean(self.player_finished_npc_level); + writer.write_boolean(self.show_start_hud); + writer.write_boolean(self.is_on_overtime); + self.objects.encode(writer)?; + for deck in &self.decks { + match deck { + None => writer.write_boolean(false), + Some(deck) => { + writer.write_boolean(true); + deck.encode(writer)?; + } + } + } + for leader in &self.leaders { + leader.encode(writer)?; + } + for towers in &self.leader_towers { + writer.write_vint(towers.len() as i32); + for tower in towers { + tower.encode(writer)?; + } + } + if self.battle_ended_called { + writer.write_vint(self.winner_score_change); + writer.write_vint(self.loser_score_change); + } + for value in self.trailing { + writer.write_vint(value); + } + Ok(()) + } + fn decode(reader: &mut ByteStreamReader<'_>) -> Result { + let location = LogicDataRef::decode(reader)?; + let npc = LogicDataRef::decode(reader)?; + let arena = LogicDataRef::decode(reader)?; + let account_ids = { + let first = reader.read_vlong()?; + let second = reader.read_vlong()?; + [ + LogicLong::new(first.0, first.1), + LogicLong::new(second.0, second.1), + ] + }; + let unknown_20 = reader.read_vint()?; + let unknown_24 = reader.read_vint()?; + let mut counters = [0; BATTLE_INT_ARRAY]; + for value in counters.iter_mut() { + *value = reader.read_vint()?; + } + let battle_type = reader.read_vint()?; + let end_counter = reader.read_vint()?; + let winner_index = reader.read_vint()?; + let battle_ended_called = reader.read_boolean()?; + let battle_ended_with_timeout = reader.read_boolean()?; + let unknown_182 = reader.read_boolean()?; + let player_finished_npc_level = reader.read_boolean()?; + let show_start_hud = reader.read_boolean()?; + let is_on_overtime = reader.read_boolean()?; + let objects = LogicGameObjectManager::decode(reader)?; + let mut decks = [None, None]; + for deck in decks.iter_mut() { + if reader.read_boolean()? { + *deck = Some(LogicSpellDeck::decode(reader)?); + } + } + let leaders = [ + LogicGameObjectRef::decode(reader)?, + LogicGameObjectRef::decode(reader)?, + ]; + let mut leader_towers = [Vec::new(), Vec::new()]; + for towers in leader_towers.iter_mut() { + let count = reader.read_vint()?.max(0) as usize; + for _ in 0..count { + towers.push(LogicGameObjectRef::decode(reader)?); + } + } + let (winner_score_change, loser_score_change) = if battle_ended_called { + (reader.read_vint()?, reader.read_vint()?) + } else { + (0, 0) + }; + let mut trailing = [0; BATTLE_TRAILING_INTS]; + for value in trailing.iter_mut() { + *value = reader.read_vint()?; + } + Ok(Self { + location, + npc, + arena, + account_ids, + unknown_20, + unknown_24, + counters, + battle_type, + end_counter, + winner_index, + battle_ended_called, + battle_ended_with_timeout, + unknown_182, + player_finished_npc_level, + show_start_hud, + is_on_overtime, + objects, + decks, + leaders, + leader_towers, + winner_score_change, + loser_score_change, + trailing, + }) + } +} diff --git a/crates/logic/src/battle/logic_game_object.rs b/crates/logic/src/battle/logic_game_object.rs new file mode 100644 index 0000000..993c3d5 --- /dev/null +++ b/crates/logic/src/battle/logic_game_object.rs @@ -0,0 +1,39 @@ +use titan::Payload; +use crate::battle::logic_game_object_ref::LogicGameObjectRef; +use crate::data::LogicDataRef; +pub const COMPONENT_PASSES: usize = 4; +pub const OBJECT_TYPE_COUNT: usize = 6; +pub const CHARACTER_OBJECT_TYPE: i32 = 5; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Payload)] +pub struct LogicVector2 { + #[codec(vint)] + pub x: i32, + #[codec(vint)] + pub y: i32, +} +impl LogicVector2 { + pub fn new(x: i32, y: i32) -> Self { + Self { x, y } + } +} +#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)] +pub struct LogicGameObject { + #[codec(vint)] + pub hitpoints: i32, + #[codec(vint)] + pub owner_index: i32, + pub position: LogicVector2, + #[codec(vint)] + pub state: i32, +} +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct LogicGameObjectEntry { + pub data: LogicDataRef, + pub global_id: LogicGameObjectRef, + pub object: LogicGameObject, +} +impl LogicGameObjectEntry { + pub fn object_type(&self) -> i32 { + self.global_id.0.map(|id| id.class_id - 1).unwrap_or(-1) + } +} diff --git a/crates/logic/src/battle/logic_game_object_manager.rs b/crates/logic/src/battle/logic_game_object_manager.rs new file mode 100644 index 0000000..15c4914 --- /dev/null +++ b/crates/logic/src/battle/logic_game_object_manager.rs @@ -0,0 +1,81 @@ +use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result}; +use crate::battle::logic_game_object::{ + LogicGameObject, LogicGameObjectEntry, COMPONENT_PASSES, OBJECT_TYPE_COUNT, +}; +use crate::battle::logic_game_object_ref::LogicGameObjectRef; +use crate::data::LogicDataRef; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogicGameObjectManager { + pub instance_counters: [i32; OBJECT_TYPE_COUNT], + pub objects: Vec, +} +impl Default for LogicGameObjectManager { + fn default() -> Self { + Self { + instance_counters: [0; OBJECT_TYPE_COUNT], + objects: Vec::new(), + } + } +} +impl LogicGameObjectManager { + pub fn push(&mut self, entry: LogicGameObjectEntry) { + let object_type = entry.object_type(); + if let Ok(index) = usize::try_from(object_type) { + if let Some(counter) = self.instance_counters.get_mut(index) { + let instance = entry.global_id.0.map(|id| id.instance_id).unwrap_or(0); + *counter = (*counter).max(instance + 1); + } + } + self.objects.push(entry); + self.objects + .sort_by_key(|entry| entry.global_id.0.map(|id| (id.class_id, id.instance_id))); + } +} +impl Payload for LogicGameObjectManager { + fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { + for counter in self.instance_counters { + writer.write_vint(counter); + } + writer.write_vint(self.objects.len() as i32); + for entry in &self.objects { + entry.data.encode(writer)?; + } + for entry in &self.objects { + entry.global_id.encode(writer)?; + } + for entry in &self.objects { + entry.object.encode(writer)?; + } + for _ in 0..COMPONENT_PASSES { + for _ in &self.objects {} + } + Ok(()) + } + fn decode(reader: &mut ByteStreamReader<'_>) -> Result { + let mut instance_counters = [0; OBJECT_TYPE_COUNT]; + for counter in instance_counters.iter_mut() { + *counter = reader.read_vint()?; + } + let count = reader.read_vint()?.max(0) as usize; + let mut data = Vec::with_capacity(count); + for _ in 0..count { + data.push(LogicDataRef::decode(reader)?); + } + let mut ids = Vec::with_capacity(count); + for _ in 0..count { + ids.push(LogicGameObjectRef::decode(reader)?); + } + let mut objects = Vec::with_capacity(count); + for index in 0..count { + objects.push(LogicGameObjectEntry { + data: data[index].clone(), + global_id: ids[index], + object: LogicGameObject::decode(reader)?, + }); + } + Ok(Self { + instance_counters, + objects, + }) + } +} diff --git a/crates/logic/src/battle/logic_game_object_ref.rs b/crates/logic/src/battle/logic_game_object_ref.rs new file mode 100644 index 0000000..ba1d7b7 --- /dev/null +++ b/crates/logic/src/battle/logic_game_object_ref.rs @@ -0,0 +1,32 @@ +use titan::{ByteStreamReader, ByteStreamWriter, GlobalId, Payload, Result}; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct LogicGameObjectRef(pub Option); +impl LogicGameObjectRef { + pub const NONE: Self = Self(None); + pub fn of(class_id: i32, instance_id: i32) -> Self { + Self(Some(GlobalId::new(class_id, instance_id))) + } + pub fn is_none(&self) -> bool { + self.0.is_none() + } +} +impl Payload for LogicGameObjectRef { + fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { + match self.0 { + None => writer.write_vint(0), + Some(id) => { + writer.write_vint(id.class_id); + writer.write_vint(id.instance_id); + } + } + Ok(()) + } + fn decode(reader: &mut ByteStreamReader<'_>) -> Result { + let class_id = reader.read_vint()?; + if class_id == 0 { + return Ok(Self(None)); + } + let instance_id = reader.read_vint()?; + Ok(Self::of(class_id, instance_id)) + } +} diff --git a/crates/logic/src/battle/logic_time.rs b/crates/logic/src/battle/logic_time.rs new file mode 100644 index 0000000..b19f66c --- /dev/null +++ b/crates/logic/src/battle/logic_time.rs @@ -0,0 +1,11 @@ +use titan::Payload; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Payload)] +pub struct LogicTime { + #[codec(vint)] + pub tick: i32, +} +impl LogicTime { + pub fn new(tick: i32) -> Self { + Self { tick } + } +} diff --git a/crates/logic/src/battle/logic_tutorial_manager.rs b/crates/logic/src/battle/logic_tutorial_manager.rs new file mode 100644 index 0000000..0019a62 --- /dev/null +++ b/crates/logic/src/battle/logic_tutorial_manager.rs @@ -0,0 +1,10 @@ +use titan::Payload; +use crate::battle::logic_game_object_ref::LogicGameObjectRef; +use crate::data::LogicDataRef; +#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)] +pub struct LogicTutorialManager { + pub tutorial: LogicDataRef, + #[codec(vint)] + pub countdown: i32, + pub target: LogicGameObjectRef, +} diff --git a/crates/logic/src/battle/mod.rs b/crates/logic/src/battle/mod.rs new file mode 100644 index 0000000..d884159 --- /dev/null +++ b/crates/logic/src/battle/mod.rs @@ -0,0 +1,18 @@ +mod logic_battle; +mod logic_game_object; +mod logic_game_object_manager; +mod logic_game_object_ref; +mod logic_time; +mod logic_tutorial_manager; +pub use logic_battle::{ + LogicBattle, BATTLE_INT_ARRAY, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, BATTLE_TYPE_PVP, + BATTLE_TYPE_REPLAY, +}; +pub use logic_game_object::{ + LogicGameObject, LogicGameObjectEntry, LogicVector2, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES, + OBJECT_TYPE_COUNT, +}; +pub use logic_game_object_manager::LogicGameObjectManager; +pub use logic_game_object_ref::LogicGameObjectRef; +pub use logic_time::LogicTime; +pub use logic_tutorial_manager::LogicTutorialManager; diff --git a/crates/logic/src/home/mod.rs b/crates/logic/src/home/mod.rs index 2396c9e..aa1bdd8 100644 --- a/crates/logic/src/home/mod.rs +++ b/crates/logic/src/home/mod.rs @@ -2,6 +2,5 @@ mod logic_home_mode; mod shop; pub use logic_home_mode::LogicHomeMode; pub use shop::{ - cards_on_sale, randomize_shop_items, LogicRandom, CARD_TABLES, EPIC_SUNDAY_WEEKDAY, - RARITIES_ON_SALE, + cards_on_sale, randomize_shop_items, CARD_TABLES, EPIC_SUNDAY_WEEKDAY, RARITIES_ON_SALE, }; diff --git a/crates/logic/src/home/shop.rs b/crates/logic/src/home/shop.rs index fe49094..7eb346c 100644 --- a/crates/logic/src/home/shop.rs +++ b/crates/logic/src/home/shop.rs @@ -1,4 +1,5 @@ use crate::data::{table, LogicDataRef, LogicDataTables, RARITY_COMMON, RARITY_EPIC, RARITY_RARE}; +use crate::logic_random::LogicRandom; use crate::model::LogicDataSlot; pub const EPIC_SUNDAY_WEEKDAY: i32 = 1; pub const RARITIES_ON_SALE: [&str; 3] = [RARITY_COMMON, RARITY_RARE, RARITY_EPIC]; @@ -7,31 +8,6 @@ pub const CARD_TABLES: [i32; 3] = [ table::SPELLS_BUILDINGS, table::SPELLS_OTHER, ]; -pub struct LogicRandom { - seed: i32, -} -impl LogicRandom { - pub fn new(seed: i32) -> Self { - Self { seed } - } - pub fn seed(&self) -> i32 { - self.seed - } - pub fn next(&mut self, bound: i32) -> i32 { - if bound <= 0 { - return 0; - } - if self.seed == 0 { - self.seed = -1; - } - let mut state = self.seed; - state ^= state.wrapping_shl(13); - state ^= state >> 17; - state ^= state.wrapping_shl(5); - self.seed = state; - state.checked_abs().unwrap_or(0) % bound - } -} pub fn cards_on_sale(rarity: &str, arena: &LogicDataRef) -> Vec { let tables = LogicDataTables::instance(); let mut pool = Vec::new(); diff --git a/crates/logic/src/lib.rs b/crates/logic/src/lib.rs index ed707a7..906dc3d 100644 --- a/crates/logic/src/lib.rs +++ b/crates/logic/src/lib.rs @@ -1,8 +1,10 @@ extern crate self as logic; +pub mod battle; pub mod commands; pub mod data; pub mod factory; pub mod home; +pub mod logic_random; pub mod messages; pub mod model; pub use commands::{ @@ -22,7 +24,8 @@ pub use data::{ TABLE_COUNT, }; pub use factory::{scroll_message_registry, LogicScrollMessageFactory}; -pub use home::{randomize_shop_items, LogicHomeMode, LogicRandom}; +pub use home::{randomize_shop_items, LogicHomeMode}; +pub use logic_random::LogicRandom; pub use messages::*; pub use model::*; pub use titan::{GlobalId, LogicLong}; diff --git a/crates/logic/src/logic_random.rs b/crates/logic/src/logic_random.rs new file mode 100644 index 0000000..d1358d9 --- /dev/null +++ b/crates/logic/src/logic_random.rs @@ -0,0 +1,41 @@ +use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result}; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct LogicRandom { + seed: i32, +} +impl LogicRandom { + pub fn new(seed: i32) -> Self { + Self { seed } + } + pub fn seed(&self) -> i32 { + self.seed + } + pub fn set_seed(&mut self, seed: i32) { + self.seed = seed; + } + pub fn next(&mut self, bound: i32) -> i32 { + if bound < 1 { + return 0; + } + if self.seed == 0 { + self.seed = -1; + } + let mut state = self.seed; + state ^= state.wrapping_shl(13); + state ^= state >> 17; + state ^= state.wrapping_shl(5); + self.seed = state; + state.wrapping_abs() % bound + } +} +impl Payload for LogicRandom { + fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { + writer.write_int(self.seed); + Ok(()) + } + fn decode(reader: &mut ByteStreamReader<'_>) -> Result { + Ok(Self { + seed: reader.read_int()?, + }) + } +} diff --git a/crates/logic/src/messages/battle_result.rs b/crates/logic/src/messages/battle_result.rs new file mode 100644 index 0000000..c595296 --- /dev/null +++ b/crates/logic/src/messages/battle_result.rs @@ -0,0 +1,49 @@ +use titan::Message; +use crate::battle::LogicGameObjectRef; +pub const BATTLE_RESULT_WIN: i32 = 1; +pub const BATTLE_RESULT_LOSE: i32 = 2; +pub const BATTLE_RESULT_DRAW: i32 = 3; +#[derive(Debug, Default, Clone, PartialEq, Eq, Message)] +#[message(id = 20225, direction = "server", name = "BattleResultMessage")] +pub struct BattleResultMessage { + #[codec(vint)] + pub result: i32, + #[codec(vint)] + pub score_change: i32, + #[codec(vint)] + pub unknown_56: i32, + #[codec(vint)] + pub unknown_60: i32, + #[codec(vint)] + pub unknown_64: i32, + #[codec(vint)] + pub unknown_68: i32, + #[codec(vint)] + pub gold_reward: i32, + #[codec(vint)] + pub exp_reward: i32, + #[codec(vint)] + pub own_stars: i32, + #[codec(vint)] + pub opponent_stars: i32, + pub treasure_chest: LogicGameObjectRef, + #[codec(bytes)] + pub full_update: Option>, + #[codec(bool)] + pub unknown_100: bool, + pub unknown_104: LogicGameObjectRef, + pub unknown_108: LogicGameObjectRef, + pub unknown_112: LogicGameObjectRef, + pub unknown_116: LogicGameObjectRef, +} +impl BattleResultMessage { + pub fn npc_win(exp_reward: i32, gold_reward: i32, own_stars: i32) -> Self { + Self { + result: BATTLE_RESULT_WIN, + exp_reward, + gold_reward, + own_stars, + ..Self::default() + } + } +} diff --git a/crates/logic/src/messages/mod.rs b/crates/logic/src/messages/mod.rs index c32b18b..7d3cde4 100644 --- a/crates/logic/src/messages/mod.rs +++ b/crates/logic/src/messages/mod.rs @@ -1,4 +1,5 @@ mod available_server_command; +mod battle_result; mod client_capabilities; mod client_requests; mod end_client_turn; @@ -12,6 +13,9 @@ mod own_home_data; mod server_error; mod start_mission; pub use available_server_command::AvailableServerCommandMessage; +pub use battle_result::{ + BattleResultMessage, BATTLE_RESULT_DRAW, BATTLE_RESULT_LOSE, BATTLE_RESULT_WIN, +}; pub use client_capabilities::ClientCapabilitiesMessage; pub use client_requests::{ AskForAvatarStreamMessage, AskForBattleReplayStreamMessage,