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,