diff --git a/Cargo.lock b/Cargo.lock index c9fc5e7..ae51f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,9 +140,20 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" name = "logic" version = "0.1.0" dependencies = [ + "logic-derive", "serde", "thiserror", "titan", + "tracing", +] + +[[package]] +name = "logic-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5d172f3..c2fe70f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "titan/titan", "titan/titan-derive", "logic/logic", + "logic/logic-derive", "services/service-rpc", "services/auth-service", "services/game-service", @@ -21,6 +22,7 @@ license = "MIT" titan = { path = "titan/titan" } titan-derive = { path = "titan/titan-derive" } logic = { path = "logic/logic" } +logic-derive = { path = "logic/logic-derive" } service-rpc = { path = "services/service-rpc" } auth-service = { path = "services/auth-service" } game-service = { path = "services/game-service" } diff --git a/logic/logic-derive/Cargo.toml b/logic/logic-derive/Cargo.toml new file mode 100644 index 0000000..c90dfcb --- /dev/null +++ b/logic/logic-derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "logic-derive" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description = "Derive macro that turns an annotated struct into a self registering logic command" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } diff --git a/logic/logic-derive/src/lib.rs b/logic/logic-derive/src/lib.rs new file mode 100644 index 0000000..7781efa --- /dev/null +++ b/logic/logic-derive/src/lib.rs @@ -0,0 +1,46 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, Error, Expr, LitStr, Result}; +#[proc_macro_derive(Command, attributes(command))] +pub fn derive_command(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand(&input) + .unwrap_or_else(Error::into_compile_error) + .into() +} +fn expand(input: &DeriveInput) -> Result { + let attr = input + .attrs + .iter() + .find(|attr| attr.path().is_ident("command")) + .ok_or_else(|| { + Error::new_spanned(&input.ident, "missing #[command(id = ...)] attribute") + })?; + let mut id: Option = None; + let mut name: Option = None; + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("id") { + let value: Expr = meta.value()?.parse()?; + id = Some(quote!(#value)); + return Ok(()); + } + if meta.path.is_ident("name") { + let value: LitStr = meta.value()?.parse()?; + name = Some(value.value()); + return Ok(()); + } + Err(meta.error("unsupported #[command(...)] key")) + })?; + let id = id.ok_or_else(|| Error::new_spanned(attr, "#[command(...)] requires `id`"))?; + let ident = &input.ident; + let name = name.unwrap_or_else(|| ident.to_string()); + Ok(quote! { + impl ::logic::commands::CommandMeta for #ident { + const COMMAND_TYPE: i32 = #id; + const NAME: &'static str = #name; + } + ::titan::inventory::submit! { + ::logic::commands::CommandRegistryEntry::of::<#ident>() + } + }) +} diff --git a/logic/logic/Cargo.toml b/logic/logic/Cargo.toml index 9370078..a2ee0a7 100644 --- a/logic/logic/Cargo.toml +++ b/logic/logic/Cargo.toml @@ -12,5 +12,7 @@ serde = ["dep:serde"] [dependencies] titan = { workspace = true } +logic-derive = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } serde = { workspace = true, optional = true } diff --git a/logic/logic/src/commands/chest.rs b/logic/logic/src/commands/chest.rs new file mode 100644 index 0000000..50f704b --- /dev/null +++ b/logic/logic/src/commands/chest.rs @@ -0,0 +1,104 @@ +use logic_derive::Command; +use titan::Payload; +use crate::commands::command::{CommandOutcome, Execute}; +use crate::commands::header::LogicCommandHeader; +use crate::commands::reward::LogicReward; +use crate::commands::{chest_source, command_type}; +use crate::home::LogicHomeMode; +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::CLAIM_REWARD)] +pub struct LogicClaimRewardCommand { + pub reward: Option, + #[codec(vint)] + pub chest_id: i32, + #[codec(vint)] + pub chest_source: i32, + #[codec(vint)] + pub server_command_id: i32, + pub header: LogicCommandHeader, +} +impl LogicClaimRewardCommand { + pub fn new(reward: LogicReward, chest_id: i32, chest_source: i32) -> Self { + Self { + reward: Some(reward), + chest_id, + chest_source, + server_command_id: 0, + header: LogicCommandHeader::default(), + } + } +} +impl Execute for LogicClaimRewardCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + match &self.reward { + None => CommandOutcome::Rejected("the claim carries no reward"), + Some(reward) => { + mode.apply_reward(reward); + CommandOutcome::Applied + } + } + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::START_REWARD_CLAIM)] +pub struct LogicStartRewardClaimCommand { + pub header: LogicCommandHeader, + #[codec(vint)] + pub chest_id: i32, +} +impl Execute for LogicStartRewardClaimCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + if mode.is_claiming_reward() { + return CommandOutcome::Rejected("a reward claim is already in progress"); + } + match mode.chest_with_id(self.chest_id) { + None => CommandOutcome::Rejected("no chest with that id"), + Some(chest) if !chest.unlocked => CommandOutcome::Rejected("the chest is still locked"), + Some(chest) if chest.claimed => CommandOutcome::Rejected("the chest is already open"), + Some(_) => { + mode.begin_claim(); + CommandOutcome::ClaimStarted { + source: chest_source::SLOT, + chest_id: self.chest_id, + } + } + } + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::COLLECT_FREE_CHEST)] +pub struct LogicCollectFreeChestCommand { + pub header: LogicCommandHeader, +} +impl Execute for LogicCollectFreeChestCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + if mode.is_claiming_reward() { + return CommandOutcome::Rejected("a reward claim is already in progress"); + } + if mode.avatar().arena.is_none() { + return CommandOutcome::Rejected("the avatar has no arena"); + } + mode.begin_claim(); + CommandOutcome::ClaimStarted { + source: chest_source::FREE, + chest_id: 0, + } + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::COLLECT_MULTI_WIN_CHEST)] +pub struct LogicCollectMultiWinChestCommand { + pub header: LogicCommandHeader, +} +impl Execute for LogicCollectMultiWinChestCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + if mode.is_claiming_reward() { + return CommandOutcome::Rejected("a reward claim is already in progress"); + } + mode.begin_claim(); + CommandOutcome::ClaimStarted { + source: chest_source::CROWN, + chest_id: 0, + } + } +} diff --git a/logic/logic/src/commands/client.rs b/logic/logic/src/commands/client.rs deleted file mode 100644 index 5ccde35..0000000 --- a/logic/logic/src/commands/client.rs +++ /dev/null @@ -1,47 +0,0 @@ -use titan::Payload; -use crate::commands::chest_source; -use crate::commands::command_type; -use crate::commands::header::LogicCommandHeader; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Payload)] -#[codec(tag = vint)] -pub enum ClientCommand { - #[codec(id = command_type::START_REWARD_CLAIM)] - StartRewardClaim { - header: LogicCommandHeader, - #[codec(vint)] - chest_id: i32, - }, - #[codec(id = command_type::COLLECT_FREE_CHEST)] - CollectFreeChest { header: LogicCommandHeader }, - #[codec(id = command_type::COLLECT_MULTI_WIN_CHEST)] - CollectMultiWinChest { header: LogicCommandHeader }, -} -impl ClientCommand { - pub fn command_type(&self) -> i32 { - match self { - ClientCommand::StartRewardClaim { .. } => command_type::START_REWARD_CLAIM, - ClientCommand::CollectFreeChest { .. } => command_type::COLLECT_FREE_CHEST, - ClientCommand::CollectMultiWinChest { .. } => command_type::COLLECT_MULTI_WIN_CHEST, - } - } - pub fn header(&self) -> &LogicCommandHeader { - match self { - ClientCommand::StartRewardClaim { header, .. } - | ClientCommand::CollectFreeChest { header } - | ClientCommand::CollectMultiWinChest { header } => header, - } - } - pub fn claimed_chest_source(&self) -> Option { - match self { - ClientCommand::StartRewardClaim { .. } => Some(chest_source::SLOT), - ClientCommand::CollectFreeChest { .. } => Some(chest_source::FREE), - ClientCommand::CollectMultiWinChest { .. } => Some(chest_source::CROWN), - } - } - pub fn claimed_chest_id(&self) -> i32 { - match self { - ClientCommand::StartRewardClaim { chest_id, .. } => *chest_id, - _ => 0, - } - } -} diff --git a/logic/logic/src/commands/command.rs b/logic/logic/src/commands/command.rs index c383843..d4022d8 100644 --- a/logic/logic/src/commands/command.rs +++ b/logic/logic/src/commands/command.rs @@ -1,106 +1,92 @@ -use titan::Payload; -use crate::commands::chest_source; -use crate::commands::command_type; -use crate::commands::header::LogicCommandHeader; -use crate::commands::reward::LogicReward; +use std::any::Any; +use std::fmt::Debug; +use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result}; use crate::data::LogicDataRef; -#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)] -pub struct LogicClaimRewardCommand { - pub reward: Option, - #[codec(vint)] - pub chest_id: i32, - #[codec(vint)] - pub chest_source: i32, - #[codec(vint)] - pub server_command_id: i32, - pub header: LogicCommandHeader, -} -impl LogicClaimRewardCommand { - pub fn new(reward: LogicReward, chest_id: i32, chest_source: i32) -> Self { - Self { - reward: Some(reward), - chest_id, - chest_source, - server_command_id: 0, - header: LogicCommandHeader::default(), - } - } -} -#[derive(Debug, Clone, PartialEq, Eq, Payload)] -#[codec(tag = vint)] -pub enum LogicCommand { - #[codec(id = command_type::CLAIM_REWARD)] - ClaimReward { command: LogicClaimRewardCommand }, - #[codec(id = command_type::START_REWARD_CLAIM)] - StartRewardClaim { - header: LogicCommandHeader, - #[codec(vint)] +use crate::home::LogicHomeMode; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CommandOutcome { + ClaimStarted { + source: i32, chest_id: i32, }, - #[codec(id = command_type::FUSE_SPELLS)] - FuseSpells { - header: LogicCommandHeader, + Upgraded { spell: LogicDataRef, + level: i32, + gold_spent: i32, }, - #[codec(id = command_type::COLLECT_FREE_CHEST)] - CollectFreeChest { header: LogicCommandHeader }, - #[codec(id = command_type::COLLECT_MULTI_WIN_CHEST)] - CollectMultiWinChest { header: LogicCommandHeader }, - #[codec(id = command_type::SORT_COLLECTION)] - SortCollection { - header: LogicCommandHeader, - #[codec(vint)] - sort_mode: i32, - }, - #[codec(id = command_type::HELP_OPENED)] - HelpOpened { header: LogicCommandHeader }, - #[codec(id = command_type::SHOP_OPENED)] - ShopOpened { header: LogicCommandHeader }, - #[codec(id = command_type::REFRESH_ACHIEVEMENTS)] - RefreshAchievements { header: LogicCommandHeader }, - #[codec(id = command_type::PAGE_OPENED)] - PageOpened { - header: LogicCommandHeader, - #[codec(vint)] - page: i32, - }, + Applied, + Rejected(&'static str), + Ignored, } -impl LogicCommand { - pub fn command_type(&self) -> i32 { - match self { - LogicCommand::ClaimReward { .. } => command_type::CLAIM_REWARD, - LogicCommand::StartRewardClaim { .. } => command_type::START_REWARD_CLAIM, - LogicCommand::FuseSpells { .. } => command_type::FUSE_SPELLS, - LogicCommand::CollectFreeChest { .. } => command_type::COLLECT_FREE_CHEST, - LogicCommand::CollectMultiWinChest { .. } => command_type::COLLECT_MULTI_WIN_CHEST, - LogicCommand::SortCollection { .. } => command_type::SORT_COLLECTION, - LogicCommand::HelpOpened { .. } => command_type::HELP_OPENED, - LogicCommand::ShopOpened { .. } => command_type::SHOP_OPENED, - LogicCommand::RefreshAchievements { .. } => command_type::REFRESH_ACHIEVEMENTS, - LogicCommand::PageOpened { .. } => command_type::PAGE_OPENED, - } +pub trait CommandMeta: Payload + Execute + Debug + Send + Sync + 'static { + const COMMAND_TYPE: i32; + const NAME: &'static str; +} +pub trait Execute { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome; +} +pub trait LogicCommand: Debug + Send + Sync + 'static { + fn command_type(&self) -> i32; + fn name(&self) -> &'static str; + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome; + fn encode_body(&self, writer: &mut ByteStreamWriter) -> Result<()>; + fn as_any(&self) -> &dyn Any; +} +impl LogicCommand for T { + fn command_type(&self) -> i32 { + T::COMMAND_TYPE } + fn name(&self) -> &'static str { + T::NAME + } + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + Execute::execute(self, mode) + } + fn encode_body(&self, writer: &mut ByteStreamWriter) -> Result<()> { + Payload::encode(self, writer) + } + fn as_any(&self) -> &dyn Any { + self + } +} +impl dyn LogicCommand { pub fn is_server_command(&self) -> bool { (200..500).contains(&self.command_type()) } - pub fn claimed_chest_source(&self) -> Option { - match self { - LogicCommand::StartRewardClaim { .. } => Some(chest_source::SLOT), - LogicCommand::CollectFreeChest { .. } => Some(chest_source::FREE), - LogicCommand::CollectMultiWinChest { .. } => Some(chest_source::CROWN), - _ => None, - } - } - pub fn claimed_chest_id(&self) -> i32 { - match self { - LogicCommand::StartRewardClaim { chest_id, .. } => *chest_id, - _ => 0, - } - } - pub fn upgraded_spell(&self) -> Option<&LogicDataRef> { - match self { - LogicCommand::FuseSpells { spell, .. } => Some(spell), - _ => None, - } + pub fn downcast_ref(&self) -> Option<&T> { + self.as_any().downcast_ref::() } } +type DecodeFn = fn(&mut ByteStreamReader<'_>) -> Result>; +#[derive(Clone, Copy)] +pub struct CommandRegistryEntry { + pub command_type: i32, + pub name: &'static str, + decode: DecodeFn, +} +impl Debug for CommandRegistryEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CommandRegistryEntry") + .field("command_type", &self.command_type) + .field("name", &self.name) + .finish() + } +} +impl CommandRegistryEntry { + pub const fn of() -> Self { + Self { + command_type: T::COMMAND_TYPE, + name: T::NAME, + decode: decode_into_box::, + } + } + pub fn decode(&self, reader: &mut ByteStreamReader<'_>) -> Result> { + (self.decode)(reader) + } +} +fn decode_into_box( + reader: &mut ByteStreamReader<'_>, +) -> Result> { + Ok(Box::new(T::decode(reader)?)) +} +titan::inventory::collect!(CommandRegistryEntry); diff --git a/logic/logic/src/commands/manager.rs b/logic/logic/src/commands/manager.rs new file mode 100644 index 0000000..3e35e1d --- /dev/null +++ b/logic/logic/src/commands/manager.rs @@ -0,0 +1,45 @@ +use std::collections::HashMap; +use std::sync::{Arc, OnceLock}; +use titan::{ByteStreamReader, ByteStreamWriter, Error, Payload, Result}; +use crate::commands::command::{CommandRegistryEntry, LogicCommand}; +static REGISTRY: OnceLock>> = OnceLock::new(); +pub struct LogicCommandManager; +impl LogicCommandManager { + pub fn registry() -> Arc> { + Arc::clone(REGISTRY.get_or_init(|| { + let mut entries = HashMap::new(); + for entry in titan::inventory::iter:: { + if let Some(previous) = entries.insert(entry.command_type, *entry) { + tracing::warn!( + command_type = entry.command_type, + previous = previous.name, + replacement = entry.name, + "two commands declare the same type" + ); + } + } + tracing::debug!(commands = entries.len(), "command registry collected"); + Arc::new(entries) + })) + } + pub fn lookup(command_type: i32) -> Option { + Self::registry().get(&command_type).copied() + } + pub fn decode_command(reader: &mut ByteStreamReader<'_>) -> Result> { + let command_type = reader.read_vint()?; + let entry = Self::lookup(command_type).ok_or(Error::UnknownCommandType(command_type))?; + entry.decode(reader) + } + pub fn encode_command(writer: &mut ByteStreamWriter, command: &dyn LogicCommand) -> Result<()> { + writer.write_vint(command.command_type()); + command.encode_body(writer) + } +} +impl Payload for Box { + fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { + LogicCommandManager::encode_command(writer, self.as_ref()) + } + fn decode(reader: &mut ByteStreamReader<'_>) -> Result { + LogicCommandManager::decode_command(reader) + } +} diff --git a/logic/logic/src/commands/mod.rs b/logic/logic/src/commands/mod.rs index 41359f6..e8eeff1 100644 --- a/logic/logic/src/commands/mod.rs +++ b/logic/logic/src/commands/mod.rs @@ -1,9 +1,23 @@ +mod chest; mod command; mod header; +mod manager; mod reward; -pub use command::{LogicClaimRewardCommand, LogicCommand}; +mod spells; +mod ui; +pub use chest::{ + LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, + LogicStartRewardClaimCommand, +}; +pub use command::{CommandMeta, CommandOutcome, CommandRegistryEntry, Execute, LogicCommand}; pub use header::LogicCommandHeader; +pub use manager::LogicCommandManager; pub use reward::LogicReward; +pub use spells::{LogicFuseSpellsCommand, LogicSortCollectionCommand}; +pub use ui::{ + LogicHelpOpenedCommand, LogicPageOpenedCommand, LogicRefreshAchievementsCommand, + LogicShopOpenedCommand, +}; pub mod command_type { pub const CLAIM_REWARD: i32 = 213; pub const ADD_CHEST: i32 = 214; diff --git a/logic/logic/src/commands/server.rs b/logic/logic/src/commands/server.rs deleted file mode 100644 index f34e139..0000000 --- a/logic/logic/src/commands/server.rs +++ /dev/null @@ -1,39 +0,0 @@ -use titan::Payload; -use crate::commands::command_type; -use crate::commands::header::LogicCommandHeader; -use crate::commands::reward::LogicReward; -#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)] -pub struct LogicClaimRewardCommand { - pub reward: Option, - #[codec(vint)] - pub chest_id: i32, - #[codec(vint)] - pub chest_source: i32, - #[codec(vint)] - pub server_command_id: i32, - pub header: LogicCommandHeader, -} -impl LogicClaimRewardCommand { - pub fn new(reward: LogicReward, chest_id: i32, chest_source: i32) -> Self { - Self { - reward: Some(reward), - chest_id, - chest_source, - server_command_id: 0, - header: LogicCommandHeader::default(), - } - } -} -#[derive(Debug, Clone, PartialEq, Eq, Payload)] -#[codec(tag = vint)] -pub enum ServerCommand { - #[codec(id = command_type::CLAIM_REWARD)] - ClaimReward { command: LogicClaimRewardCommand }, -} -impl ServerCommand { - pub fn command_type(&self) -> i32 { - match self { - ServerCommand::ClaimReward { .. } => command_type::CLAIM_REWARD, - } - } -} diff --git a/logic/logic/src/commands/spells.rs b/logic/logic/src/commands/spells.rs new file mode 100644 index 0000000..48a6945 --- /dev/null +++ b/logic/logic/src/commands/spells.rs @@ -0,0 +1,31 @@ +use logic_derive::Command; +use titan::Payload; +use crate::commands::command::{CommandOutcome, Execute}; +use crate::commands::command_type; +use crate::commands::header::LogicCommandHeader; +use crate::data::LogicDataRef; +use crate::home::LogicHomeMode; +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::FUSE_SPELLS)] +pub struct LogicFuseSpellsCommand { + pub header: LogicCommandHeader, + pub spell: LogicDataRef, +} +impl Execute for LogicFuseSpellsCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + mode.upgrade_spell(&self.spell) + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::SORT_COLLECTION)] +pub struct LogicSortCollectionCommand { + pub header: LogicCommandHeader, + #[codec(vint)] + pub sort_mode: i32, +} +impl Execute for LogicSortCollectionCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + mode.home_mut().spell_collection.current_sort = self.sort_mode; + CommandOutcome::Applied + } +} diff --git a/logic/logic/src/commands/ui.rs b/logic/logic/src/commands/ui.rs new file mode 100644 index 0000000..0684d46 --- /dev/null +++ b/logic/logic/src/commands/ui.rs @@ -0,0 +1,51 @@ +use logic_derive::Command; +use titan::Payload; +use crate::commands::command::{CommandOutcome, Execute}; +use crate::commands::command_type; +use crate::commands::header::LogicCommandHeader; +use crate::home::LogicHomeMode; +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::HELP_OPENED)] +pub struct LogicHelpOpenedCommand { + pub header: LogicCommandHeader, +} +impl Execute for LogicHelpOpenedCommand { + fn execute(&self, _mode: &mut LogicHomeMode) -> CommandOutcome { + CommandOutcome::Applied + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::SHOP_OPENED)] +pub struct LogicShopOpenedCommand { + pub header: LogicCommandHeader, +} +impl Execute for LogicShopOpenedCommand { + fn execute(&self, _mode: &mut LogicHomeMode) -> CommandOutcome { + CommandOutcome::Applied + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::REFRESH_ACHIEVEMENTS)] +pub struct LogicRefreshAchievementsCommand { + pub header: LogicCommandHeader, +} +impl Execute for LogicRefreshAchievementsCommand { + fn execute(&self, _mode: &mut LogicHomeMode) -> CommandOutcome { + CommandOutcome::Applied + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::PAGE_OPENED)] +pub struct LogicPageOpenedCommand { + pub header: LogicCommandHeader, + #[codec(vint)] + pub page: i32, +} +impl Execute for LogicPageOpenedCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + if (0..32).contains(&self.page) { + mode.home_mut().opened_pages |= 1 << self.page; + } + CommandOutcome::Applied + } +} diff --git a/logic/logic/src/data/logic_data_table.rs b/logic/logic/src/data/logic_data_table.rs index cb9cefc..da81060 100644 --- a/logic/logic/src/data/logic_data_table.rs +++ b/logic/logic/src/data/logic_data_table.rs @@ -13,7 +13,13 @@ impl LogicDataTable { pub fn load(table_index: i32, csv_table: CsvTable) -> Self { let csv_table = Arc::new(csv_table); let items: Vec> = (0..csv_table.row_count()) - .map(|row_index| Arc::new(LogicData::new(Arc::clone(&csv_table), row_index, table_index))) + .map(|row_index| { + Arc::new(LogicData::new( + Arc::clone(&csv_table), + row_index, + table_index, + )) + }) .collect(); let by_name = items .iter() @@ -44,7 +50,9 @@ impl LogicDataTable { self.items.get(index) } pub fn get_by_name(&self, name: &str) -> Option<&Arc> { - self.by_name.get(name).and_then(|index| self.items.get(*index)) + self.by_name + .get(name) + .and_then(|index| self.items.get(*index)) } pub fn iter(&self) -> impl Iterator> { self.items.iter() diff --git a/logic/logic/src/data/logic_data_tables.rs b/logic/logic/src/data/logic_data_tables.rs index 7658e90..2d620cd 100644 --- a/logic/logic/src/data/logic_data_tables.rs +++ b/logic/logic/src/data/logic_data_tables.rs @@ -32,7 +32,10 @@ pub const DATA_TABLE_RESOURCES: &[LogicDataTableResource] = &[ LogicDataTableResource::new("csv_logic/locations.csv", table::LOCATIONS), LogicDataTableResource::new("csv_logic/npcs.csv", table::NPCS), LogicDataTableResource::new("csv_logic/treasure_chests.csv", table::TREASURE_CHESTS), - LogicDataTableResource::new("csv_logic/area_effect_objects.csv", table::AREA_EFFECT_OBJECTS), + LogicDataTableResource::new( + "csv_logic/area_effect_objects.csv", + table::AREA_EFFECT_OBJECTS, + ), LogicDataTableResource::new("csv_logic/spells_characters.csv", table::SPELLS_CHARACTERS), LogicDataTableResource::new("csv_logic/spells_buildings.csv", table::SPELLS_BUILDINGS), LogicDataTableResource::new("csv_logic/spells_other.csv", table::SPELLS_OTHER), @@ -66,16 +69,22 @@ pub const DATA_TABLE_RESOURCES: &[LogicDataTableResource] = &[ LogicDataTableResource::new("csv_client/hints.csv", table::HINTS), ]; pub const COMBINED_TABLES: &[(i32, &[i32])] = &[ - (table::SPELLS, &[ - table::SPELLS_CHARACTERS, - table::SPELLS_BUILDINGS, - table::SPELLS_OTHER, - ]), - (table::CHARACTERS_COMBINED, &[table::CHARACTERS, table::BUILDINGS]), - (table::TUTORIALS_COMBINED, &[ - table::TUTORIALS_HOME, - table::TUTORIALS_NPC, - ]), + ( + table::SPELLS, + &[ + table::SPELLS_CHARACTERS, + table::SPELLS_BUILDINGS, + table::SPELLS_OTHER, + ], + ), + ( + table::CHARACTERS_COMBINED, + &[table::CHARACTERS, table::BUILDINGS], + ), + ( + table::TUTORIALS_COMBINED, + &[table::TUTORIALS_HOME, table::TUTORIALS_NPC], + ), ]; #[derive(Debug, thiserror::Error)] pub enum DataError { @@ -197,7 +206,9 @@ impl LogicDataTables { self.data_by_name(table::TREASURE_CHESTS, name) } pub fn exp_level_count(&self) -> usize { - self.table(table::EXP_LEVELS).map(|table| table.count()).unwrap_or(0) + self.table(table::EXP_LEVELS) + .map(|table| table.count()) + .unwrap_or(0) } pub fn globals(&self) -> Option<&Arc> { self.table(table::GLOBALS) diff --git a/logic/logic/src/home/logic_home_mode.rs b/logic/logic/src/home/logic_home_mode.rs index 1dea917..43892a9 100644 --- a/logic/logic/src/home/logic_home_mode.rs +++ b/logic/logic/src/home/logic_home_mode.rs @@ -1,17 +1,9 @@ -use crate::commands::{chest_source, LogicCommand, LogicReward}; +use crate::commands::{CommandOutcome, LogicCommand, LogicReward}; use crate::data::{LogicDataRef, LogicRarityData}; use crate::model::{ - LogicClientAvatar, LogicClientHome, LogicSpell, LogicTimer, TICKS_PER_SECOND, + LogicChest, LogicClientAvatar, LogicClientHome, LogicSpell, LogicTimer, TICKS_PER_SECOND, }; #[derive(Debug, Clone, PartialEq, Eq)] -pub enum CommandOutcome { - ClaimStarted { source: i32, chest_id: i32 }, - Upgraded { spell: LogicDataRef, level: i32, gold_spent: i32 }, - Applied, - Rejected(&'static str), - Ignored, -} -#[derive(Debug, Clone, PartialEq, Eq)] pub struct LogicHomeMode { home: LogicClientHome, avatar: LogicClientAvatar, @@ -102,36 +94,18 @@ impl LogicHomeMode { ] .into_iter() } - pub fn execute(&mut self, command: &LogicCommand) -> CommandOutcome { - match command { - LogicCommand::ClaimReward { .. } => CommandOutcome::Ignored, - LogicCommand::CollectFreeChest { .. } => self.start_claim(chest_source::FREE, 0), - LogicCommand::CollectMultiWinChest { .. } => self.start_claim(chest_source::CROWN, 0), - LogicCommand::StartRewardClaim { chest_id, .. } => { - self.start_claim(chest_source::SLOT, *chest_id) - } - LogicCommand::FuseSpells { spell, .. } => self.upgrade_spell(spell), - LogicCommand::SortCollection { sort_mode, .. } => { - self.home.spell_collection.current_sort = *sort_mode; - CommandOutcome::Applied - } - LogicCommand::PageOpened { page, .. } => { - if (0..32).contains(page) { - self.home.opened_pages |= 1 << page; - } - CommandOutcome::Applied - } - LogicCommand::HelpOpened { .. } - | LogicCommand::ShopOpened { .. } - | LogicCommand::RefreshAchievements { .. } => CommandOutcome::Applied, - } + pub fn execute(&mut self, command: &dyn LogicCommand) -> CommandOutcome { + command.execute(self) } - fn start_claim(&mut self, source: i32, chest_id: i32) -> CommandOutcome { - if self.claiming_reward { - return CommandOutcome::Rejected("a reward claim is already in progress"); - } + pub fn chest_with_id(&self, chest_id: i32) -> Option<&LogicChest> { + self.home + .chest_slots + .iter() + .flatten() + .find(|chest| chest.chest_id == chest_id) + } + pub fn begin_claim(&mut self) { self.claiming_reward = true; - CommandOutcome::ClaimStarted { source, chest_id } } pub fn apply_reward(&mut self, reward: &LogicReward) { for spell in reward.spells.iter().flatten() { @@ -161,7 +135,7 @@ impl LogicHomeMode { .iter_mut() .find(|slot| slot.is_none()) } - fn upgrade_spell(&mut self, data: &LogicDataRef) -> CommandOutcome { + pub fn upgrade_spell(&mut self, data: &LogicDataRef) -> CommandOutcome { let rarity = rarity_of(data); let Some(spell) = self.find_spell_mut(data) else { return CommandOutcome::Rejected("the avatar does not own this card"); diff --git a/logic/logic/src/home/mod.rs b/logic/logic/src/home/mod.rs index d0e0a23..6180112 100644 --- a/logic/logic/src/home/mod.rs +++ b/logic/logic/src/home/mod.rs @@ -1,2 +1,2 @@ mod logic_home_mode; -pub use logic_home_mode::{CommandOutcome, LogicHomeMode}; +pub use logic_home_mode::LogicHomeMode; diff --git a/logic/logic/src/lib.rs b/logic/logic/src/lib.rs index ce961aa..ddc0ba7 100644 --- a/logic/logic/src/lib.rs +++ b/logic/logic/src/lib.rs @@ -1,20 +1,24 @@ +extern crate self as logic; pub mod commands; pub mod data; -pub mod home; pub mod factory; +pub mod home; pub mod messages; pub mod model; +pub use commands::{ + chest_source, command_type, CommandMeta, CommandOutcome, Execute, LogicClaimRewardCommand, + LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, LogicCommand, + LogicCommandHeader, LogicCommandManager, LogicFuseSpellsCommand, LogicHelpOpenedCommand, + LogicPageOpenedCommand, LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand, + LogicSortCollectionCommand, LogicStartRewardClaimCommand, +}; pub use data::{ table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable, LogicDataTableResource, LogicDataTables, LogicRarityData, LogicResourceData, LogicSpellData, LogicTreasureChestData, DATA_TABLE_RESOURCES, TABLE_COUNT, }; pub use factory::{scroll_message_registry, LogicScrollMessageFactory}; -pub use commands::{ - chest_source, command_type, LogicClaimRewardCommand, LogicCommand, LogicCommandHeader, - LogicReward, -}; -pub use home::{CommandOutcome, LogicHomeMode}; +pub use home::LogicHomeMode; pub use messages::*; pub use model::*; pub use titan::{GlobalId, LogicLong}; diff --git a/logic/logic/src/messages/available_server_command.rs b/logic/logic/src/messages/available_server_command.rs index f36142a..282fee5 100644 --- a/logic/logic/src/messages/available_server_command.rs +++ b/logic/logic/src/messages/available_server_command.rs @@ -1,14 +1,18 @@ use titan::Message; use crate::commands::{LogicClaimRewardCommand, LogicCommand}; -#[derive(Debug, Clone, PartialEq, Eq, Message)] -#[message(id = 24111, direction = "server", name = "AvailableServerCommandMessage")] +#[derive(Debug, Message)] +#[message( + id = 24111, + direction = "server", + name = "AvailableServerCommandMessage" +)] pub struct AvailableServerCommandMessage { - pub command: LogicCommand, + pub command: Box, } impl AvailableServerCommandMessage { pub fn claim_reward(command: LogicClaimRewardCommand) -> Self { Self { - command: LogicCommand::ClaimReward { command }, + command: Box::new(command), } } } diff --git a/logic/logic/src/messages/client_requests.rs b/logic/logic/src/messages/client_requests.rs index 85e1bbf..51f56a5 100644 --- a/logic/logic/src/messages/client_requests.rs +++ b/logic/logic/src/messages/client_requests.rs @@ -6,7 +6,11 @@ pub struct SetDeviceTokenMessage { pub device_token: Option>, } #[derive(Debug, Default, Clone, PartialEq, Eq, Message)] -#[message(id = 10513, direction = "client", name = "AskForPlayingFacebookFriendsMessage")] +#[message( + id = 10513, + direction = "client", + name = "AskForPlayingFacebookFriendsMessage" +)] pub struct AskForPlayingFacebookFriendsMessage { #[codec(string, null_list)] pub facebook_ids: Option>>, @@ -18,7 +22,11 @@ pub struct AskForTVContentMessage {} #[message(id = 14405, direction = "client", name = "AskForAvatarStreamMessage")] pub struct AskForAvatarStreamMessage {} #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Message)] -#[message(id = 14406, direction = "client", name = "AskForBattleReplayStreamMessage")] +#[message( + id = 14406, + direction = "client", + name = "AskForBattleReplayStreamMessage" +)] pub struct AskForBattleReplayStreamMessage { #[codec(long)] pub avatar_id: LogicLong, diff --git a/logic/logic/src/messages/end_client_turn.rs b/logic/logic/src/messages/end_client_turn.rs index 6430c28..d7a9294 100644 --- a/logic/logic/src/messages/end_client_turn.rs +++ b/logic/logic/src/messages/end_client_turn.rs @@ -1,6 +1,6 @@ use titan::Message; use crate::commands::LogicCommand; -#[derive(Debug, Default, Clone, PartialEq, Eq, Message)] +#[derive(Debug, Default, Message)] #[message(id = 14102, direction = "client", name = "EndClientTurnMessage")] #[codec(partial)] pub struct EndClientTurnMessage { @@ -8,7 +8,7 @@ pub struct EndClientTurnMessage { pub tick: i32, #[codec(vint)] pub checksum: i32, - pub commands: Vec, + pub commands: Vec>, #[codec(bytes, stop_if_eof)] pub trailing: Option>, } diff --git a/logic/logic/src/messages/mod.rs b/logic/logic/src/messages/mod.rs index cd14d9d..c32b18b 100644 --- a/logic/logic/src/messages/mod.rs +++ b/logic/logic/src/messages/mod.rs @@ -14,8 +14,8 @@ mod start_mission; pub use available_server_command::AvailableServerCommandMessage; pub use client_capabilities::ClientCapabilitiesMessage; pub use client_requests::{ - AskForAvatarStreamMessage, AskForBattleReplayStreamMessage, AskForPlayingFacebookFriendsMessage, - AskForTVContentMessage, SetDeviceTokenMessage, + AskForAvatarStreamMessage, AskForBattleReplayStreamMessage, + AskForPlayingFacebookFriendsMessage, AskForTVContentMessage, SetDeviceTokenMessage, }; pub use end_client_turn::EndClientTurnMessage; pub use go_home::GoHomeMessage; diff --git a/logic/logic/src/model/mod.rs b/logic/logic/src/model/mod.rs index a6fb352..8a43d09 100644 --- a/logic/logic/src/model/mod.rs +++ b/logic/logic/src/model/mod.rs @@ -5,7 +5,9 @@ mod data_slot; mod spell; mod timer; pub use chest::{ChestSource, LogicChest}; -pub use client_avatar::{LogicAllianceInfo, LogicClientAvatar, LogicCommodityStore, COMMODITY_TYPE_COUNT}; +pub use client_avatar::{ + LogicAllianceInfo, LogicClientAvatar, LogicCommodityStore, COMMODITY_TYPE_COUNT, +}; pub use client_home::{LogicClientHome, TUTORIAL_BITSET_WORDS}; pub use data_slot::LogicDataSlot; pub use spell::{LogicSpell, LogicSpellCollection, LogicSpellDeck, DECK_SLOT_COUNT}; diff --git a/services/auth-service/src/config.rs b/services/auth-service/src/config.rs index 361760d..ef4e904 100644 --- a/services/auth-service/src/config.rs +++ b/services/auth-service/src/config.rs @@ -28,8 +28,10 @@ impl AuthConfig { store_path: env_string("SCROLL_AUTH_STORE") .map(PathBuf::from) .unwrap_or(defaults.store_path), - min_client_build: env_i32("SCROLL_MIN_CLIENT_BUILD").unwrap_or(defaults.min_client_build), - max_client_build: env_i32("SCROLL_MAX_CLIENT_BUILD").unwrap_or(defaults.max_client_build), + min_client_build: env_i32("SCROLL_MIN_CLIENT_BUILD") + .unwrap_or(defaults.min_client_build), + max_client_build: env_i32("SCROLL_MAX_CLIENT_BUILD") + .unwrap_or(defaults.max_client_build), content_fingerprint: env_string("SCROLL_CONTENT_FINGERPRINT"), content_url: env_string("SCROLL_CONTENT_URL"), } diff --git a/services/auth-service/src/service.rs b/services/auth-service/src/service.rs index 39cb856..1753f5d 100644 --- a/services/auth-service/src/service.rs +++ b/services/auth-service/src/service.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use service_rpc::{ - AccountRef, AuthApi, AuthRequest, AuthResponse, DeviceInfo, LoginOutcome, RpcResult, RpcService, - Session, + AccountRef, AuthApi, AuthRequest, AuthResponse, DeviceInfo, LoginOutcome, RpcResult, + RpcService, Session, }; use crate::config::AuthConfig; use crate::store::AccountStore; diff --git a/services/auth-service/src/store.rs b/services/auth-service/src/store.rs index b6c9094..04505c5 100644 --- a/services/auth-service/src/store.rs +++ b/services/auth-service/src/store.rs @@ -74,7 +74,9 @@ impl AccountStore { play_time_seconds: 0, banned: false, }; - state.accounts.insert(account.account_ref(), account.clone()); + state + .accounts + .insert(account.account_ref(), account.clone()); account }; self.persist().await?; diff --git a/services/game-service/src/home.rs b/services/game-service/src/home.rs index fba7497..7af49c1 100644 --- a/services/game-service/src/home.rs +++ b/services/game-service/src/home.rs @@ -71,7 +71,10 @@ pub fn build_avatar(profile: &PlayerProfile) -> LogicClientAvatar { let mut commodities = LogicCommodityStore::default(); commodities.set( COMMODITY_RESOURCES, - vec![LogicDataSlot::new(profile.gold_resource.clone(), profile.gold)], + vec![LogicDataSlot::new( + profile.gold_resource.clone(), + profile.gold, + )], ); LogicClientAvatar { avatar_id: account, diff --git a/services/game-service/src/home_mode.rs b/services/game-service/src/home_mode.rs index 2a903ec..0e24d79 100644 --- a/services/game-service/src/home_mode.rs +++ b/services/game-service/src/home_mode.rs @@ -55,7 +55,7 @@ impl HomeMode { } let mut pending = Vec::new(); for command in &turn.commands { - match self.logic.execute(command) { + match self.logic.execute(command.as_ref()) { CommandOutcome::ClaimStarted { source, chest_id } => { pending.push((source, chest_id)); } @@ -90,11 +90,9 @@ impl HomeMode { let rolled = roller.roll(reward); self.logic.apply_reward(&rolled); result.changed = true; - result.claims.push(LogicClaimRewardCommand::new( - rolled, - chest_id, - source, - )); + result + .claims + .push(LogicClaimRewardCommand::new(rolled, chest_id, source)); } result } @@ -150,11 +148,12 @@ impl HomeModeRegistry { return Arc::clone(session); } let mut sessions = self.sessions.write().await; - Arc::clone( - sessions - .entry(account) - .or_insert_with(|| Arc::new(Mutex::new(HomeMode::from_profile(profile, current_timestamp)))), - ) + Arc::clone(sessions.entry(account).or_insert_with(|| { + Arc::new(Mutex::new(HomeMode::from_profile( + profile, + current_timestamp, + ))) + })) } pub async fn close(&self, account: AccountRef) -> Option>> { self.sessions.write().await.remove(&account) diff --git a/services/game-service/src/lib.rs b/services/game-service/src/lib.rs index 938c5e1..9ad906a 100644 --- a/services/game-service/src/lib.rs +++ b/services/game-service/src/lib.rs @@ -2,14 +2,14 @@ pub mod catalog; pub mod config; pub mod home; pub mod home_mode; -pub mod rewards; -pub mod time; pub mod profile; +pub mod rewards; pub mod service; +pub mod time; pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE}; pub use config::{CardRef, ChestRewardConfig, DataSelector, GameConfig, StarterProfile}; pub use home::{build_avatar, build_home}; pub use home_mode::{HomeMode, HomeModeRegistry, TurnResult}; -pub use rewards::RewardRoller; pub use profile::{PlayerProfile, ProfileStore}; +pub use rewards::RewardRoller; pub use service::GameService; diff --git a/services/game-service/src/profile.rs b/services/game-service/src/profile.rs index 845dc7d..50aa449 100644 --- a/services/game-service/src/profile.rs +++ b/services/game-service/src/profile.rs @@ -58,7 +58,11 @@ impl PlayerProfile { arena: catalog.resolve_arena(&starter.arena), chest_slot_count: starter.chest_slot_count, deck: starter.deck.iter().filter_map(card_of).collect(), - collection: starter.extra_collection.iter().filter_map(card_of).collect(), + collection: starter + .extra_collection + .iter() + .filter_map(card_of) + .collect(), battle_count: 0, win_count: 0, lose_count: 0, diff --git a/services/game-service/src/service.rs b/services/game-service/src/service.rs index 87204ea..77ce75b 100644 --- a/services/game-service/src/service.rs +++ b/services/game-service/src/service.rs @@ -1,5 +1,7 @@ use std::sync::Arc; -use logic::{EndClientTurnMessage, LogicDataRef, OutOfSyncMessage, OwnHomeDataMessage}; +use logic::{ + EndClientTurnMessage, LogicCommandManager, LogicDataRef, OutOfSyncMessage, OwnHomeDataMessage, +}; use service_rpc::{ AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult, RpcService, WireMessage, @@ -25,6 +27,7 @@ impl GameService { profiles = profiles.len().await, store = %config.store_path.display(), catalog = catalog.is_loaded(), + commands = LogicCommandManager::registry().len(), "profile store ready" ); Ok(Arc::new(Self { diff --git a/services/gateway/src/backend.rs b/services/gateway/src/backend.rs index c1ca4db..91d53a8 100644 --- a/services/gateway/src/backend.rs +++ b/services/gateway/src/backend.rs @@ -46,13 +46,17 @@ impl AuthApi for RemoteAuth { .await? { AuthResponse::Login(outcome) => Ok(outcome), - other => Err(RpcError::Rejected(format!("unexpected auth reply {other:?}"))), + other => Err(RpcError::Rejected(format!( + "unexpected auth reply {other:?}" + ))), } } async fn resolve(&self, account: AccountRef) -> RpcResult { match self.client.call(&AuthRequest::Resolve { account }).await? { AuthResponse::Resolved { known } => Ok(known), - other => Err(RpcError::Rejected(format!("unexpected auth reply {other:?}"))), + other => Err(RpcError::Rejected(format!( + "unexpected auth reply {other:?}" + ))), } } } diff --git a/services/gateway/src/bin/scroll-probe.rs b/services/gateway/src/bin/scroll-probe.rs index f8d5253..87f232c 100644 --- a/services/gateway/src/bin/scroll-probe.rs +++ b/services/gateway/src/bin/scroll-probe.rs @@ -1,8 +1,8 @@ use std::time::Duration; use logic::{ message_type, AvailableServerCommandMessage, EndClientTurnMessage, KeepAliveMessage, - LoginMessage, LoginOkMessage, LogicCommand, LogicCommandHeader, LogicDataTables, - OwnHomeDataMessage, + LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCommandHeader, LogicDataTables, + LoginMessage, LoginOkMessage, OwnHomeDataMessage, }; use titan::crypto::SessionCipher; use titan::{FrameCodec, FrameHeader, MessageFrame, MessageMeta, Payload, HEADER_LEN}; @@ -38,14 +38,20 @@ impl Probe { .await??; let header = FrameHeader::parse(&header_bytes); let mut payload = vec![0u8; header.payload_len]; - tokio::time::timeout(Duration::from_secs(10), self.stream.read_exact(&mut payload)).await??; + tokio::time::timeout( + Duration::from_secs(10), + self.stream.read_exact(&mut payload), + ) + .await??; let plain = self.cipher.decrypt_inbound(&payload).expect("decrypt"); Ok((header.message_type, plain)) } } #[tokio::main] async fn main() -> Result<(), Box> { - let endpoint = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1:9339".to_owned()); + let endpoint = std::env::args() + .nth(1) + .unwrap_or_else(|| "127.0.0.1:9339".to_owned()); let account_high: i32 = std::env::args() .nth(2) .and_then(|value| value.parse().ok()) @@ -67,8 +73,9 @@ async fn main() -> Result<(), Box> { Err(error) => println!("no data tables ({error}), printing raw ids"), } println!( - "message registry: {} type(s)", - logic::scroll_message_registry().len() + "message registry: {} type(s), command registry: {} type(s)", + logic::scroll_message_registry().len(), + logic::LogicCommandManager::registry().len() ); println!("connecting to {endpoint}"); let mut probe = Probe::connect(&endpoint).await?; @@ -95,7 +102,10 @@ async fn main() -> Result<(), Box> { println!("<- 20104 LoginOkMessage"); println!(" account {}", ok.account_id); println!(" home {}", ok.home_id); - println!(" pass token {}", ok.pass_token.as_deref().unwrap_or("-")); + println!( + " pass token {}", + ok.pass_token.as_deref().unwrap_or("-") + ); println!( " server {}.{}.{} content {}", ok.server_major_version, @@ -104,7 +114,10 @@ async fn main() -> Result<(), Box> { ok.content_version ); println!(" sessions {}", ok.session_count); - println!(" created {}", ok.account_created_date.as_deref().unwrap_or("-")); + println!( + " created {}", + ok.account_created_date.as_deref().unwrap_or("-") + ); } message_type::LOGIN_FAILED => { let failed = logic::LoginFailedMessage::from_bytes(&payload)?; @@ -168,9 +181,9 @@ async fn main() -> Result<(), Box> { checksum: home.home.chest_id_counter + ((home.home.spell_collection.spells.len() as i32) << 16) + i32::from(desync), - commands: vec![LogicCommand::CollectFreeChest { + commands: vec![Box::new(LogicCollectFreeChestCommand { header: LogicCommandHeader::for_account(home.avatar.account_id), - }], + })], trailing: None, }; probe.send(&turn).await?; @@ -189,14 +202,17 @@ async fn main() -> Result<(), Box> { } message_type::AVAILABLE_SERVER_COMMAND => { let envelope = AvailableServerCommandMessage::from_bytes(&payload)?; - println!("<- 24111 AvailableServerCommandMessage ({} bytes)", payload.len()); - if let LogicCommand::ClaimReward { command: claim } = envelope.command { + println!( + "<- 24111 AvailableServerCommandMessage ({} bytes)", + payload.len() + ); + if let Some(claim) = envelope.command.downcast_ref::() { println!(" chest source {}", claim.chest_source); println!(" chest id {}", claim.chest_id); - if let Some(reward) = claim.reward { + if let Some(reward) = &claim.reward { println!(" gold {}", reward.gold); println!(" diamonds {}", reward.diamonds); - for spell in reward.spells.unwrap_or_default() { + for spell in reward.spells.iter().flatten() { println!(" card {} x{}", spell.data, spell.count); } } diff --git a/services/service-rpc/src/auth.rs b/services/service-rpc/src/auth.rs index 918fbc6..1b1817d 100644 --- a/services/service-rpc/src/auth.rs +++ b/services/service-rpc/src/auth.rs @@ -38,7 +38,10 @@ pub struct Session { #[serde(tag = "outcome", rename_all = "snake_case")] pub enum LoginOutcome { Accepted(Session), - Rejected { error_code: i32, message: Option }, + Rejected { + error_code: i32, + message: Option, + }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "op", rename_all = "snake_case")] diff --git a/services/service-rpc/src/game.rs b/services/service-rpc/src/game.rs index 6584e8f..a06b1b2 100644 --- a/services/service-rpc/src/game.rs +++ b/services/service-rpc/src/game.rs @@ -1,7 +1,9 @@ use serde::{Deserialize, Serialize}; use crate::error::RpcResult; use crate::wire::WireMessage; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, +)] pub struct AccountRef { pub high: i32, pub low: i32, @@ -70,6 +72,10 @@ pub trait GameApi: Send + Sync + 'static { kind: HomeRequestKind, ) -> RpcResult>; async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()>; - async fn end_client_turn(&self, account: AccountRef, payload: Vec) -> RpcResult>; + async fn end_client_turn( + &self, + account: AccountRef, + payload: Vec, + ) -> RpcResult>; async fn disconnect(&self, account: AccountRef) -> RpcResult<()>; } diff --git a/titan/titan-derive/src/codec.rs b/titan/titan-derive/src/codec.rs index 12c2043..c777470 100644 --- a/titan/titan-derive/src/codec.rs +++ b/titan/titan-derive/src/codec.rs @@ -35,9 +35,6 @@ impl Default for FieldSpec { } } pub fn expand_payload(input: &DeriveInput) -> Result { - if let Data::Enum(data) = &input.data { - return expand_tagged_enum(input, data); - } let ident = &input.ident; let partial = has_partial(input)?; let fields = match &input.data { @@ -52,10 +49,16 @@ pub fn expand_payload(input: &DeriveInput) -> Result { } }, Data::Enum(data) => { - return Err(Error::new_spanned(data.enum_token, "unreachable enum branch")) + return Err(Error::new_spanned( + data.enum_token, + "enums are not supported", + )) } Data::Union(data) => { - return Err(Error::new_spanned(data.union_token, "unions are not supported")) + return Err(Error::new_spanned( + data.union_token, + "unions are not supported", + )) } }; let mut encode_body = Vec::new(); @@ -121,123 +124,6 @@ pub fn expand_payload(input: &DeriveInput) -> Result { } }) } -fn expand_tagged_enum(input: &DeriveInput, data: &syn::DataEnum) -> Result { - let ident = &input.ident; - let tag = tag_codec(input)?; - let mut encode_arms = Vec::new(); - let mut decode_arms = Vec::new(); - for variant in &data.variants { - let variant_ident = &variant.ident; - let id = variant_id(variant)?; - let named = match &variant.fields { - Fields::Named(named) => named.named.iter().collect::>(), - Fields::Unit => Vec::new(), - Fields::Unnamed(unnamed) => { - return Err(Error::new_spanned( - unnamed, - "tuple variants are not supported, use named fields", - )) - } - }; - let mut bindings = Vec::new(); - let mut writes = Vec::new(); - let mut reads = Vec::new(); - let mut inits = Vec::new(); - for field in &named { - let spec = parse_field_spec(&field.attrs)?; - let name = field.ident.as_ref().expect("named field"); - let ty = &field.ty; - let codec = codec_for_spec(ty, &spec)?; - bindings.push(quote!(#name)); - writes.push(quote! { - <#codec as ::titan::codec::Codec<#ty>>::write(writer, #name)?; - }); - let binding = format_ident!("field_{}", name); - reads.push(quote! { - let #binding = <#codec as ::titan::codec::Codec<#ty>>::read(reader)?; - }); - inits.push(quote!(#name: #binding)); - } - encode_arms.push(quote! { - Self::#variant_ident { #(#bindings),* } => { - <#tag as ::titan::codec::Codec>::write(writer, &#id)?; - #(#writes)* - } - }); - decode_arms.push(quote! { - #id => { - #(#reads)* - ::core::result::Result::Ok(Self::#variant_ident { #(#inits),* }) - } - }); - } - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - Ok(quote! { - impl #impl_generics ::titan::message::Payload for #ident #ty_generics #where_clause { - fn encode(&self, writer: &mut ::titan::io::ByteStreamWriter) -> ::titan::error::Result<()> { - match self { - #(#encode_arms)* - } - ::core::result::Result::Ok(()) - } - fn decode(reader: &mut ::titan::io::ByteStreamReader<'_>) -> ::titan::error::Result { - let tag = <#tag as ::titan::codec::Codec>::read(reader)?; - match tag { - #(#decode_arms)* - other => ::core::result::Result::Err(::titan::error::Error::UnknownCommandType(other)), - } - } - } - }) -} -fn tag_codec(input: &DeriveInput) -> Result { - let mut tag = None; - for attr in &input.attrs { - if !attr.path().is_ident("codec") { - continue; - } - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("tag") { - let value: syn::Ident = meta.value()?.parse()?; - tag = Some(match value.to_string().as_str() { - "vint" => quote!(::titan::codec::VInt), - "int" => quote!(::titan::codec::Int), - other => { - return Err(Error::new_spanned( - &value, - format!("unsupported tag codec `{other}`, expected vint or int"), - )) - } - }); - return Ok(()); - } - Err(meta.error("unsupported enum level #[codec(...)] key")) - })?; - } - tag.ok_or_else(|| { - Error::new_spanned( - &input.ident, - "tagged enums need #[codec(tag = vint)] on the enum", - ) - }) -} -fn variant_id(variant: &syn::Variant) -> Result { - let mut id = None; - for attr in &variant.attrs { - if !attr.path().is_ident("codec") { - continue; - } - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("id") { - let value: syn::Expr = meta.value()?.parse()?; - id = Some(quote!(#value)); - return Ok(()); - } - Err(meta.error("unsupported variant level #[codec(...)] key")) - })?; - } - id.ok_or_else(|| Error::new_spanned(&variant.ident, "each variant needs #[codec(id = ...)]")) -} fn has_partial(input: &DeriveInput) -> Result { let mut partial = false; for attr in &input.attrs { diff --git a/titan/titan/src/codec.rs b/titan/titan/src/codec.rs index 3ae7d64..745d0a7 100644 --- a/titan/titan/src/codec.rs +++ b/titan/titan/src/codec.rs @@ -216,10 +216,12 @@ impl, const N: usize> Codec<[T; N]> for Arr { for _ in 0..N { items.push(C::read(reader)?); } - items.try_into().map_err(|items: Vec| Error::ArityMismatch { - expected: N, - actual: items.len(), - }) + items + .try_into() + .map_err(|items: Vec| Error::ArityMismatch { + expected: N, + actual: items.len(), + }) } } impl, const N: usize> Codec<[Option; N]> for SplitArr { @@ -239,7 +241,11 @@ impl, const N: usize> Codec<[Option; N]> for SplitArr { } let mut items = Vec::with_capacity(N); for occupied in present { - items.push(if occupied { Some(C::read(reader)?) } else { None }); + items.push(if occupied { + Some(C::read(reader)?) + } else { + None + }); } items .try_into() diff --git a/titan/titan/src/crypto/rc4.rs b/titan/titan/src/crypto/rc4.rs index 6ea5329..1445286 100644 --- a/titan/titan/src/crypto/rc4.rs +++ b/titan/titan/src/crypto/rc4.rs @@ -19,9 +19,7 @@ impl Rc4Encrypter { } let mut j: u8 = 0; for i in 0..256usize { - j = j - .wrapping_add(state[i]) - .wrapping_add(seed[i % seed.len()]); + j = j.wrapping_add(state[i]).wrapping_add(seed[i % seed.len()]); state.swap(i, j as usize); } let mut cipher = Self { state, i: 0, j: 0 }; diff --git a/titan/titan/src/csv/reader.rs b/titan/titan/src/csv/reader.rs index 744d042..a579674 100644 --- a/titan/titan/src/csv/reader.rs +++ b/titan/titan/src/csv/reader.rs @@ -27,16 +27,12 @@ impl CsvReader { let mut lines = source .lines() .map(|line| line.strip_suffix('\r').unwrap_or(line)); - let name_line = lines - .next() - .ok_or_else(|| CsvError::MissingColumnNames { - file: file_name.clone(), - })?; - let type_line = lines - .next() - .ok_or_else(|| CsvError::MissingColumnTypes { - file: file_name.clone(), - })?; + let name_line = lines.next().ok_or_else(|| CsvError::MissingColumnNames { + file: file_name.clone(), + })?; + let type_line = lines.next().ok_or_else(|| CsvError::MissingColumnTypes { + file: file_name.clone(), + })?; let names = split_line(name_line); let types = split_line(type_line); let mut columns = Vec::with_capacity(names.len()); diff --git a/titan/titan/src/csv/row.rs b/titan/titan/src/csv/row.rs index 61a5817..58ef14f 100644 --- a/titan/titan/src/csv/row.rs +++ b/titan/titan/src/csv/row.rs @@ -26,7 +26,9 @@ impl CsvRow { } } pub fn value_at(&self, column: usize, index: usize) -> Option<&CsvValue> { - self.columns.get(column).and_then(|values| values.get(index)) + self.columns + .get(column) + .and_then(|values| values.get(index)) } pub fn value(&self, column: usize) -> Option<&CsvValue> { self.value_at(column, 0) @@ -35,13 +37,17 @@ impl CsvRow { self.columns.get(column).map(Vec::len).unwrap_or_default() } pub fn string_at(&self, column: usize, index: usize) -> &str { - self.value_at(column, index).map(CsvValue::as_str).unwrap_or("") + self.value_at(column, index) + .map(CsvValue::as_str) + .unwrap_or("") } pub fn string(&self, column: usize) -> &str { self.string_at(column, 0) } pub fn int_at(&self, column: usize, index: usize) -> i32 { - self.value_at(column, index).map(CsvValue::as_int).unwrap_or(0) + self.value_at(column, index) + .map(CsvValue::as_int) + .unwrap_or(0) } pub fn int(&self, column: usize) -> i32 { self.int_at(column, 0) diff --git a/titan/titan/src/csv/table.rs b/titan/titan/src/csv/table.rs index 29df91d..d812ebe 100644 --- a/titan/titan/src/csv/table.rs +++ b/titan/titan/src/csv/table.rs @@ -68,7 +68,9 @@ impl CsvTable { &self.rows } pub fn row_by_name(&self, name: &str) -> Option<&CsvRow> { - self.row_index.get(name).and_then(|index| self.rows.get(*index)) + self.row_index + .get(name) + .and_then(|index| self.rows.get(*index)) } pub fn row_index_of(&self, name: &str) -> Option { self.row_index.get(name).copied() diff --git a/titan/titan/src/factory.rs b/titan/titan/src/factory.rs index 1764290..68b83c7 100644 --- a/titan/titan/src/factory.rs +++ b/titan/titan/src/factory.rs @@ -45,7 +45,12 @@ where inventory::collect!(RegistryEntry); static GLOBAL: OnceLock> = OnceLock::new(); pub trait MessageFactory: Send + Sync { - fn create(&self, message_type: u16, message_version: u16, payload: &[u8]) -> Result>; + fn create( + &self, + message_type: u16, + message_version: u16, + payload: &[u8], + ) -> Result>; fn lookup(&self, message_type: u16) -> Option<&RegistryEntry>; } #[derive(Debug, Default)] @@ -91,7 +96,12 @@ impl MessageRegistry { } } impl MessageFactory for MessageRegistry { - fn create(&self, message_type: u16, _message_version: u16, payload: &[u8]) -> Result> { + fn create( + &self, + message_type: u16, + _message_version: u16, + payload: &[u8], + ) -> Result> { let entry = self .entries .get(&message_type) diff --git a/titan/titan/src/frame.rs b/titan/titan/src/frame.rs index d559236..065dae8 100644 --- a/titan/titan/src/frame.rs +++ b/titan/titan/src/frame.rs @@ -12,7 +12,9 @@ impl FrameHeader { pub fn parse(bytes: &[u8; HEADER_LEN]) -> Self { Self { message_type: u16::from_be_bytes([bytes[0], bytes[1]]), - payload_len: ((bytes[2] as usize) << 16) | ((bytes[3] as usize) << 8) | bytes[4] as usize, + payload_len: ((bytes[2] as usize) << 16) + | ((bytes[3] as usize) << 8) + | bytes[4] as usize, message_version: u16::from_be_bytes([bytes[5], bytes[6]]), } } diff --git a/titan/titan/src/json.rs b/titan/titan/src/json.rs index 390bc78..eb22de8 100644 --- a/titan/titan/src/json.rs +++ b/titan/titan/src/json.rs @@ -46,13 +46,19 @@ impl JsonValue { } } pub fn int_or(&self, key: &str, fallback: i32) -> i32 { - self.get(key).and_then(JsonValue::as_i32).unwrap_or(fallback) + self.get(key) + .and_then(JsonValue::as_i32) + .unwrap_or(fallback) } pub fn bool_or(&self, key: &str, fallback: bool) -> bool { - self.get(key).and_then(JsonValue::as_bool).unwrap_or(fallback) + self.get(key) + .and_then(JsonValue::as_bool) + .unwrap_or(fallback) } pub fn str_or<'a>(&'a self, key: &str, fallback: &'a str) -> &'a str { - self.get(key).and_then(JsonValue::as_str).unwrap_or(fallback) + self.get(key) + .and_then(JsonValue::as_str) + .unwrap_or(fallback) } pub fn object(pairs: impl IntoIterator) -> Self { JsonValue::Object( @@ -255,7 +261,10 @@ impl<'a> Parser<'a> { .and_then(|text| u32::from_str_radix(text, 16).ok()) .ok_or(JsonError::InvalidEscape(self.offset))?; self.offset += 4; - out.push(char::from_u32(code).ok_or(JsonError::InvalidEscape(self.offset))?); + out.push( + char::from_u32(code) + .ok_or(JsonError::InvalidEscape(self.offset))?, + ); } _ => return Err(JsonError::InvalidEscape(self.offset - 1)), } @@ -263,7 +272,10 @@ impl<'a> Parser<'a> { _ => { let start = self.offset - 1; let mut end = self.offset; - while end < self.bytes.len() && self.bytes[end] != b'"' && self.bytes[end] != b'\\' { + while end < self.bytes.len() + && self.bytes[end] != b'"' + && self.bytes[end] != b'\\' + { end += 1; } let chunk = std::str::from_utf8(&self.bytes[start..end]) diff --git a/titan/titan/src/lib.rs b/titan/titan/src/lib.rs index 1374c69..2f270d3 100644 --- a/titan/titan/src/lib.rs +++ b/titan/titan/src/lib.rs @@ -1,7 +1,7 @@ pub mod checksum; -pub mod csv; pub mod codec; pub mod crypto; +pub mod csv; pub mod data_ref; pub mod error; pub mod factory; @@ -12,11 +12,11 @@ pub mod logic_long; pub mod message; pub mod net; pub use checksum::ChecksumEncoder; -pub use csv::{ColumnType, CsvError, CsvNode, CsvReader, CsvRow, CsvTable, CsvValue}; pub use codec::{ Arr, Bool, Bytes, Codec, Int, List, Long, Nested, NullList, Opt, SplitArr, Str, StrRef, VInt, VLong, }; +pub use csv::{ColumnType, CsvError, CsvNode, CsvReader, CsvRow, CsvTable, CsvValue}; pub use data_ref::GlobalId; pub use error::{Error, Result}; pub use factory::{MessageFactory, MessageRegistry}; @@ -25,7 +25,9 @@ pub use io::{ByteStreamReader, ByteStreamWriter}; pub use json::JsonValue; pub use logic_long::LogicLong; pub use message::{Direction, Message, MessageMeta, Payload}; -pub use net::{Incoming, Messaging, MessagingConfig, MessagingError, MessagingSender, OutboundMessage}; +pub use net::{ + Incoming, Messaging, MessagingConfig, MessagingError, MessagingSender, OutboundMessage, +}; pub use inventory; pub use titan_derive::{Message, Payload}; pub mod prelude { @@ -39,5 +41,5 @@ pub mod prelude { pub use crate::logic_long::LogicLong; pub use crate::message::{Direction, Message, MessageMeta, Payload}; pub use inventory; -pub use titan_derive::{Message, Payload}; + pub use titan_derive::{Message, Payload}; } diff --git a/titan/titan/src/net/messaging.rs b/titan/titan/src/net/messaging.rs index 114eb15..a989f31 100644 --- a/titan/titan/src/net/messaging.rs +++ b/titan/titan/src/net/messaging.rs @@ -140,9 +140,7 @@ impl Messaging { .await { Err(_) => return Err(MessagingError::IdleTimeout), - Ok(Err(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => { - return Ok(None) - } + Ok(Err(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), Ok(Err(error)) => return Err(error.into()), Ok(Ok(_)) => {} } @@ -161,22 +159,23 @@ impl Messaging { .await .map_err(|_| MessagingError::IdleTimeout)??; let payload = self.inbound.decrypt(&cipher_text)?; - let message = match self - .factory - .create(header.message_type, header.message_version, &payload) - { - Ok(message) => Some(message), - Err(error) => { - tracing::warn!( - message_type = header.message_type, - bytes = payload.len(), - %error, - "ignoring message of unknown type {}", - header.message_type - ); - None - } - }; + let message = + match self + .factory + .create(header.message_type, header.message_version, &payload) + { + Ok(message) => Some(message), + Err(error) => { + tracing::warn!( + message_type = header.message_type, + bytes = payload.len(), + %error, + "ignoring message of unknown type {}", + header.message_type + ); + None + } + }; Ok(Some(Incoming { header, payload,