replace command enum with a dyn trait + registry

new logic-derive Command macro submits each command to inventory, manager decodes by vint type. titan-derive loses tagged enums since nothing uses them now. rest of the diff is rustfmt.
This commit is contained in:
WiseDev 2026-08-23 08:05:58 +03:00
parent d8ed6ae2ee
commit 972c61dae7
46 changed files with 661 additions and 468 deletions

11
Cargo.lock generated
View file

@ -140,9 +140,20 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
name = "logic" name = "logic"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"logic-derive",
"serde", "serde",
"thiserror", "thiserror",
"titan", "titan",
"tracing",
]
[[package]]
name = "logic-derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
] ]
[[package]] [[package]]

View file

@ -4,6 +4,7 @@ members = [
"titan/titan", "titan/titan",
"titan/titan-derive", "titan/titan-derive",
"logic/logic", "logic/logic",
"logic/logic-derive",
"services/service-rpc", "services/service-rpc",
"services/auth-service", "services/auth-service",
"services/game-service", "services/game-service",
@ -21,6 +22,7 @@ license = "MIT"
titan = { path = "titan/titan" } titan = { path = "titan/titan" }
titan-derive = { path = "titan/titan-derive" } titan-derive = { path = "titan/titan-derive" }
logic = { path = "logic/logic" } logic = { path = "logic/logic" }
logic-derive = { path = "logic/logic-derive" }
service-rpc = { path = "services/service-rpc" } service-rpc = { path = "services/service-rpc" }
auth-service = { path = "services/auth-service" } auth-service = { path = "services/auth-service" }
game-service = { path = "services/game-service" } game-service = { path = "services/game-service" }

View file

@ -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 }

View file

@ -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<proc_macro2::TokenStream> {
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<proc_macro2::TokenStream> = None;
let mut name: Option<String> = 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>()
}
})
}

View file

@ -12,5 +12,7 @@ serde = ["dep:serde"]
[dependencies] [dependencies]
titan = { workspace = true } titan = { workspace = true }
logic-derive = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }

View file

@ -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<LogicReward>,
#[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,
}
}
}

View file

@ -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<i32> {
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,
}
}
}

View file

@ -1,106 +1,92 @@
use titan::Payload; use std::any::Any;
use crate::commands::chest_source; use std::fmt::Debug;
use crate::commands::command_type; use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result};
use crate::commands::header::LogicCommandHeader;
use crate::commands::reward::LogicReward;
use crate::data::LogicDataRef; use crate::data::LogicDataRef;
#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)] use crate::home::LogicHomeMode;
pub struct LogicClaimRewardCommand { #[derive(Debug, Clone, PartialEq, Eq)]
pub reward: Option<LogicReward>, pub enum CommandOutcome {
#[codec(vint)] ClaimStarted {
pub chest_id: i32, source: 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)]
chest_id: i32, chest_id: i32,
}, },
#[codec(id = command_type::FUSE_SPELLS)] Upgraded {
FuseSpells {
header: LogicCommandHeader,
spell: LogicDataRef, spell: LogicDataRef,
level: i32,
gold_spent: i32,
}, },
#[codec(id = command_type::COLLECT_FREE_CHEST)] Applied,
CollectFreeChest { header: LogicCommandHeader }, Rejected(&'static str),
#[codec(id = command_type::COLLECT_MULTI_WIN_CHEST)] Ignored,
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,
},
} }
impl LogicCommand { pub trait CommandMeta: Payload + Execute + Debug + Send + Sync + 'static {
pub fn command_type(&self) -> i32 { const COMMAND_TYPE: i32;
match self { const NAME: &'static str;
LogicCommand::ClaimReward { .. } => command_type::CLAIM_REWARD, }
LogicCommand::StartRewardClaim { .. } => command_type::START_REWARD_CLAIM, pub trait Execute {
LogicCommand::FuseSpells { .. } => command_type::FUSE_SPELLS, fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome;
LogicCommand::CollectFreeChest { .. } => command_type::COLLECT_FREE_CHEST, }
LogicCommand::CollectMultiWinChest { .. } => command_type::COLLECT_MULTI_WIN_CHEST, pub trait LogicCommand: Debug + Send + Sync + 'static {
LogicCommand::SortCollection { .. } => command_type::SORT_COLLECTION, fn command_type(&self) -> i32;
LogicCommand::HelpOpened { .. } => command_type::HELP_OPENED, fn name(&self) -> &'static str;
LogicCommand::ShopOpened { .. } => command_type::SHOP_OPENED, fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome;
LogicCommand::RefreshAchievements { .. } => command_type::REFRESH_ACHIEVEMENTS, fn encode_body(&self, writer: &mut ByteStreamWriter) -> Result<()>;
LogicCommand::PageOpened { .. } => command_type::PAGE_OPENED, fn as_any(&self) -> &dyn Any;
}
impl<T: CommandMeta> 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 { pub fn is_server_command(&self) -> bool {
(200..500).contains(&self.command_type()) (200..500).contains(&self.command_type())
} }
pub fn claimed_chest_source(&self) -> Option<i32> { pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
match self { self.as_any().downcast_ref::<T>()
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 { type DecodeFn = fn(&mut ByteStreamReader<'_>) -> Result<Box<dyn LogicCommand>>;
match self { #[derive(Clone, Copy)]
LogicCommand::StartRewardClaim { chest_id, .. } => *chest_id, pub struct CommandRegistryEntry {
_ => 0, 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()
} }
} }
pub fn upgraded_spell(&self) -> Option<&LogicDataRef> { impl CommandRegistryEntry {
match self { pub const fn of<T: CommandMeta>() -> Self {
LogicCommand::FuseSpells { spell, .. } => Some(spell), Self {
_ => None, command_type: T::COMMAND_TYPE,
name: T::NAME,
decode: decode_into_box::<T>,
} }
} }
pub fn decode(&self, reader: &mut ByteStreamReader<'_>) -> Result<Box<dyn LogicCommand>> {
(self.decode)(reader)
} }
}
fn decode_into_box<T: CommandMeta>(
reader: &mut ByteStreamReader<'_>,
) -> Result<Box<dyn LogicCommand>> {
Ok(Box::new(T::decode(reader)?))
}
titan::inventory::collect!(CommandRegistryEntry);

View file

@ -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<Arc<HashMap<i32, CommandRegistryEntry>>> = OnceLock::new();
pub struct LogicCommandManager;
impl LogicCommandManager {
pub fn registry() -> Arc<HashMap<i32, CommandRegistryEntry>> {
Arc::clone(REGISTRY.get_or_init(|| {
let mut entries = HashMap::new();
for entry in titan::inventory::iter::<CommandRegistryEntry> {
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<CommandRegistryEntry> {
Self::registry().get(&command_type).copied()
}
pub fn decode_command(reader: &mut ByteStreamReader<'_>) -> Result<Box<dyn LogicCommand>> {
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<dyn LogicCommand> {
fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
LogicCommandManager::encode_command(writer, self.as_ref())
}
fn decode(reader: &mut ByteStreamReader<'_>) -> Result<Self> {
LogicCommandManager::decode_command(reader)
}
}

View file

@ -1,9 +1,23 @@
mod chest;
mod command; mod command;
mod header; mod header;
mod manager;
mod reward; 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 header::LogicCommandHeader;
pub use manager::LogicCommandManager;
pub use reward::LogicReward; pub use reward::LogicReward;
pub use spells::{LogicFuseSpellsCommand, LogicSortCollectionCommand};
pub use ui::{
LogicHelpOpenedCommand, LogicPageOpenedCommand, LogicRefreshAchievementsCommand,
LogicShopOpenedCommand,
};
pub mod command_type { pub mod command_type {
pub const CLAIM_REWARD: i32 = 213; pub const CLAIM_REWARD: i32 = 213;
pub const ADD_CHEST: i32 = 214; pub const ADD_CHEST: i32 = 214;

View file

@ -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<LogicReward>,
#[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,
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -13,7 +13,13 @@ impl LogicDataTable {
pub fn load(table_index: i32, csv_table: CsvTable) -> Self { pub fn load(table_index: i32, csv_table: CsvTable) -> Self {
let csv_table = Arc::new(csv_table); let csv_table = Arc::new(csv_table);
let items: Vec<Arc<LogicData>> = (0..csv_table.row_count()) let items: Vec<Arc<LogicData>> = (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(); .collect();
let by_name = items let by_name = items
.iter() .iter()
@ -44,7 +50,9 @@ impl LogicDataTable {
self.items.get(index) self.items.get(index)
} }
pub fn get_by_name(&self, name: &str) -> Option<&Arc<LogicData>> { pub fn get_by_name(&self, name: &str) -> Option<&Arc<LogicData>> {
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<Item = &Arc<LogicData>> { pub fn iter(&self) -> impl Iterator<Item = &Arc<LogicData>> {
self.items.iter() self.items.iter()

View file

@ -32,7 +32,10 @@ pub const DATA_TABLE_RESOURCES: &[LogicDataTableResource] = &[
LogicDataTableResource::new("csv_logic/locations.csv", table::LOCATIONS), LogicDataTableResource::new("csv_logic/locations.csv", table::LOCATIONS),
LogicDataTableResource::new("csv_logic/npcs.csv", table::NPCS), LogicDataTableResource::new("csv_logic/npcs.csv", table::NPCS),
LogicDataTableResource::new("csv_logic/treasure_chests.csv", table::TREASURE_CHESTS), 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_characters.csv", table::SPELLS_CHARACTERS),
LogicDataTableResource::new("csv_logic/spells_buildings.csv", table::SPELLS_BUILDINGS), LogicDataTableResource::new("csv_logic/spells_buildings.csv", table::SPELLS_BUILDINGS),
LogicDataTableResource::new("csv_logic/spells_other.csv", table::SPELLS_OTHER), 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), LogicDataTableResource::new("csv_client/hints.csv", table::HINTS),
]; ];
pub const COMBINED_TABLES: &[(i32, &[i32])] = &[ pub const COMBINED_TABLES: &[(i32, &[i32])] = &[
(table::SPELLS, &[ (
table::SPELLS,
&[
table::SPELLS_CHARACTERS, table::SPELLS_CHARACTERS,
table::SPELLS_BUILDINGS, table::SPELLS_BUILDINGS,
table::SPELLS_OTHER, table::SPELLS_OTHER,
]), ],
(table::CHARACTERS_COMBINED, &[table::CHARACTERS, table::BUILDINGS]), ),
(table::TUTORIALS_COMBINED, &[ (
table::TUTORIALS_HOME, table::CHARACTERS_COMBINED,
table::TUTORIALS_NPC, &[table::CHARACTERS, table::BUILDINGS],
]), ),
(
table::TUTORIALS_COMBINED,
&[table::TUTORIALS_HOME, table::TUTORIALS_NPC],
),
]; ];
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum DataError { pub enum DataError {
@ -197,7 +206,9 @@ impl LogicDataTables {
self.data_by_name(table::TREASURE_CHESTS, name) self.data_by_name(table::TREASURE_CHESTS, name)
} }
pub fn exp_level_count(&self) -> usize { 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<LogicDataTable>> { pub fn globals(&self) -> Option<&Arc<LogicDataTable>> {
self.table(table::GLOBALS) self.table(table::GLOBALS)

View file

@ -1,17 +1,9 @@
use crate::commands::{chest_source, LogicCommand, LogicReward}; use crate::commands::{CommandOutcome, LogicCommand, LogicReward};
use crate::data::{LogicDataRef, LogicRarityData}; use crate::data::{LogicDataRef, LogicRarityData};
use crate::model::{ use crate::model::{
LogicClientAvatar, LogicClientHome, LogicSpell, LogicTimer, TICKS_PER_SECOND, LogicChest, LogicClientAvatar, LogicClientHome, LogicSpell, LogicTimer, TICKS_PER_SECOND,
}; };
#[derive(Debug, Clone, PartialEq, Eq)] #[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 { pub struct LogicHomeMode {
home: LogicClientHome, home: LogicClientHome,
avatar: LogicClientAvatar, avatar: LogicClientAvatar,
@ -102,36 +94,18 @@ impl LogicHomeMode {
] ]
.into_iter() .into_iter()
} }
pub fn execute(&mut self, command: &LogicCommand) -> CommandOutcome { pub fn execute(&mut self, command: &dyn LogicCommand) -> CommandOutcome {
match command { command.execute(self)
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), pub fn chest_with_id(&self, chest_id: i32) -> Option<&LogicChest> {
LogicCommand::SortCollection { sort_mode, .. } => { self.home
self.home.spell_collection.current_sort = *sort_mode; .chest_slots
CommandOutcome::Applied .iter()
} .flatten()
LogicCommand::PageOpened { page, .. } => { .find(|chest| chest.chest_id == chest_id)
if (0..32).contains(page) {
self.home.opened_pages |= 1 << page;
}
CommandOutcome::Applied
}
LogicCommand::HelpOpened { .. }
| LogicCommand::ShopOpened { .. }
| LogicCommand::RefreshAchievements { .. } => CommandOutcome::Applied,
}
}
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 begin_claim(&mut self) {
self.claiming_reward = true; self.claiming_reward = true;
CommandOutcome::ClaimStarted { source, chest_id }
} }
pub fn apply_reward(&mut self, reward: &LogicReward) { pub fn apply_reward(&mut self, reward: &LogicReward) {
for spell in reward.spells.iter().flatten() { for spell in reward.spells.iter().flatten() {
@ -161,7 +135,7 @@ impl LogicHomeMode {
.iter_mut() .iter_mut()
.find(|slot| slot.is_none()) .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 rarity = rarity_of(data);
let Some(spell) = self.find_spell_mut(data) else { let Some(spell) = self.find_spell_mut(data) else {
return CommandOutcome::Rejected("the avatar does not own this card"); return CommandOutcome::Rejected("the avatar does not own this card");

View file

@ -1,2 +1,2 @@
mod logic_home_mode; mod logic_home_mode;
pub use logic_home_mode::{CommandOutcome, LogicHomeMode}; pub use logic_home_mode::LogicHomeMode;

View file

@ -1,20 +1,24 @@
extern crate self as logic;
pub mod commands; pub mod commands;
pub mod data; pub mod data;
pub mod home;
pub mod factory; pub mod factory;
pub mod home;
pub mod messages; pub mod messages;
pub mod model; 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::{ pub use data::{
table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable, table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable,
LogicDataTableResource, LogicDataTables, LogicRarityData, LogicResourceData, LogicSpellData, LogicDataTableResource, LogicDataTables, LogicRarityData, LogicResourceData, LogicSpellData,
LogicTreasureChestData, DATA_TABLE_RESOURCES, TABLE_COUNT, LogicTreasureChestData, DATA_TABLE_RESOURCES, TABLE_COUNT,
}; };
pub use factory::{scroll_message_registry, LogicScrollMessageFactory}; pub use factory::{scroll_message_registry, LogicScrollMessageFactory};
pub use commands::{ pub use home::LogicHomeMode;
chest_source, command_type, LogicClaimRewardCommand, LogicCommand, LogicCommandHeader,
LogicReward,
};
pub use home::{CommandOutcome, LogicHomeMode};
pub use messages::*; pub use messages::*;
pub use model::*; pub use model::*;
pub use titan::{GlobalId, LogicLong}; pub use titan::{GlobalId, LogicLong};

View file

@ -1,14 +1,18 @@
use titan::Message; use titan::Message;
use crate::commands::{LogicClaimRewardCommand, LogicCommand}; use crate::commands::{LogicClaimRewardCommand, LogicCommand};
#[derive(Debug, Clone, PartialEq, Eq, Message)] #[derive(Debug, Message)]
#[message(id = 24111, direction = "server", name = "AvailableServerCommandMessage")] #[message(
id = 24111,
direction = "server",
name = "AvailableServerCommandMessage"
)]
pub struct AvailableServerCommandMessage { pub struct AvailableServerCommandMessage {
pub command: LogicCommand, pub command: Box<dyn LogicCommand>,
} }
impl AvailableServerCommandMessage { impl AvailableServerCommandMessage {
pub fn claim_reward(command: LogicClaimRewardCommand) -> Self { pub fn claim_reward(command: LogicClaimRewardCommand) -> Self {
Self { Self {
command: LogicCommand::ClaimReward { command }, command: Box::new(command),
} }
} }
} }

View file

@ -6,7 +6,11 @@ pub struct SetDeviceTokenMessage {
pub device_token: Option<Vec<u8>>, pub device_token: Option<Vec<u8>>,
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Message)] #[derive(Debug, Default, Clone, PartialEq, Eq, Message)]
#[message(id = 10513, direction = "client", name = "AskForPlayingFacebookFriendsMessage")] #[message(
id = 10513,
direction = "client",
name = "AskForPlayingFacebookFriendsMessage"
)]
pub struct AskForPlayingFacebookFriendsMessage { pub struct AskForPlayingFacebookFriendsMessage {
#[codec(string, null_list)] #[codec(string, null_list)]
pub facebook_ids: Option<Vec<Option<String>>>, pub facebook_ids: Option<Vec<Option<String>>>,
@ -18,7 +22,11 @@ pub struct AskForTVContentMessage {}
#[message(id = 14405, direction = "client", name = "AskForAvatarStreamMessage")] #[message(id = 14405, direction = "client", name = "AskForAvatarStreamMessage")]
pub struct AskForAvatarStreamMessage {} pub struct AskForAvatarStreamMessage {}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Message)] #[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 { pub struct AskForBattleReplayStreamMessage {
#[codec(long)] #[codec(long)]
pub avatar_id: LogicLong, pub avatar_id: LogicLong,

View file

@ -1,6 +1,6 @@
use titan::Message; use titan::Message;
use crate::commands::LogicCommand; use crate::commands::LogicCommand;
#[derive(Debug, Default, Clone, PartialEq, Eq, Message)] #[derive(Debug, Default, Message)]
#[message(id = 14102, direction = "client", name = "EndClientTurnMessage")] #[message(id = 14102, direction = "client", name = "EndClientTurnMessage")]
#[codec(partial)] #[codec(partial)]
pub struct EndClientTurnMessage { pub struct EndClientTurnMessage {
@ -8,7 +8,7 @@ pub struct EndClientTurnMessage {
pub tick: i32, pub tick: i32,
#[codec(vint)] #[codec(vint)]
pub checksum: i32, pub checksum: i32,
pub commands: Vec<LogicCommand>, pub commands: Vec<Box<dyn LogicCommand>>,
#[codec(bytes, stop_if_eof)] #[codec(bytes, stop_if_eof)]
pub trailing: Option<Vec<u8>>, pub trailing: Option<Vec<u8>>,
} }

View file

@ -14,8 +14,8 @@ mod start_mission;
pub use available_server_command::AvailableServerCommandMessage; pub use available_server_command::AvailableServerCommandMessage;
pub use client_capabilities::ClientCapabilitiesMessage; pub use client_capabilities::ClientCapabilitiesMessage;
pub use client_requests::{ pub use client_requests::{
AskForAvatarStreamMessage, AskForBattleReplayStreamMessage, AskForPlayingFacebookFriendsMessage, AskForAvatarStreamMessage, AskForBattleReplayStreamMessage,
AskForTVContentMessage, SetDeviceTokenMessage, AskForPlayingFacebookFriendsMessage, AskForTVContentMessage, SetDeviceTokenMessage,
}; };
pub use end_client_turn::EndClientTurnMessage; pub use end_client_turn::EndClientTurnMessage;
pub use go_home::GoHomeMessage; pub use go_home::GoHomeMessage;

View file

@ -5,7 +5,9 @@ mod data_slot;
mod spell; mod spell;
mod timer; mod timer;
pub use chest::{ChestSource, LogicChest}; 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 client_home::{LogicClientHome, TUTORIAL_BITSET_WORDS};
pub use data_slot::LogicDataSlot; pub use data_slot::LogicDataSlot;
pub use spell::{LogicSpell, LogicSpellCollection, LogicSpellDeck, DECK_SLOT_COUNT}; pub use spell::{LogicSpell, LogicSpellCollection, LogicSpellDeck, DECK_SLOT_COUNT};

View file

@ -28,8 +28,10 @@ impl AuthConfig {
store_path: env_string("SCROLL_AUTH_STORE") store_path: env_string("SCROLL_AUTH_STORE")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or(defaults.store_path), .unwrap_or(defaults.store_path),
min_client_build: env_i32("SCROLL_MIN_CLIENT_BUILD").unwrap_or(defaults.min_client_build), min_client_build: env_i32("SCROLL_MIN_CLIENT_BUILD")
max_client_build: env_i32("SCROLL_MAX_CLIENT_BUILD").unwrap_or(defaults.max_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_fingerprint: env_string("SCROLL_CONTENT_FINGERPRINT"),
content_url: env_string("SCROLL_CONTENT_URL"), content_url: env_string("SCROLL_CONTENT_URL"),
} }

View file

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use service_rpc::{ use service_rpc::{
AccountRef, AuthApi, AuthRequest, AuthResponse, DeviceInfo, LoginOutcome, RpcResult, RpcService, AccountRef, AuthApi, AuthRequest, AuthResponse, DeviceInfo, LoginOutcome, RpcResult,
Session, RpcService, Session,
}; };
use crate::config::AuthConfig; use crate::config::AuthConfig;
use crate::store::AccountStore; use crate::store::AccountStore;

View file

@ -74,7 +74,9 @@ impl AccountStore {
play_time_seconds: 0, play_time_seconds: 0,
banned: false, banned: false,
}; };
state.accounts.insert(account.account_ref(), account.clone()); state
.accounts
.insert(account.account_ref(), account.clone());
account account
}; };
self.persist().await?; self.persist().await?;

View file

@ -71,7 +71,10 @@ pub fn build_avatar(profile: &PlayerProfile) -> LogicClientAvatar {
let mut commodities = LogicCommodityStore::default(); let mut commodities = LogicCommodityStore::default();
commodities.set( commodities.set(
COMMODITY_RESOURCES, COMMODITY_RESOURCES,
vec![LogicDataSlot::new(profile.gold_resource.clone(), profile.gold)], vec![LogicDataSlot::new(
profile.gold_resource.clone(),
profile.gold,
)],
); );
LogicClientAvatar { LogicClientAvatar {
avatar_id: account, avatar_id: account,

View file

@ -55,7 +55,7 @@ impl HomeMode {
} }
let mut pending = Vec::new(); let mut pending = Vec::new();
for command in &turn.commands { for command in &turn.commands {
match self.logic.execute(command) { match self.logic.execute(command.as_ref()) {
CommandOutcome::ClaimStarted { source, chest_id } => { CommandOutcome::ClaimStarted { source, chest_id } => {
pending.push((source, chest_id)); pending.push((source, chest_id));
} }
@ -90,11 +90,9 @@ impl HomeMode {
let rolled = roller.roll(reward); let rolled = roller.roll(reward);
self.logic.apply_reward(&rolled); self.logic.apply_reward(&rolled);
result.changed = true; result.changed = true;
result.claims.push(LogicClaimRewardCommand::new( result
rolled, .claims
chest_id, .push(LogicClaimRewardCommand::new(rolled, chest_id, source));
source,
));
} }
result result
} }
@ -150,11 +148,12 @@ impl HomeModeRegistry {
return Arc::clone(session); return Arc::clone(session);
} }
let mut sessions = self.sessions.write().await; let mut sessions = self.sessions.write().await;
Arc::clone( Arc::clone(sessions.entry(account).or_insert_with(|| {
sessions Arc::new(Mutex::new(HomeMode::from_profile(
.entry(account) profile,
.or_insert_with(|| Arc::new(Mutex::new(HomeMode::from_profile(profile, current_timestamp)))), current_timestamp,
) )))
}))
} }
pub async fn close(&self, account: AccountRef) -> Option<Arc<Mutex<HomeMode>>> { pub async fn close(&self, account: AccountRef) -> Option<Arc<Mutex<HomeMode>>> {
self.sessions.write().await.remove(&account) self.sessions.write().await.remove(&account)

View file

@ -2,14 +2,14 @@ pub mod catalog;
pub mod config; pub mod config;
pub mod home; pub mod home;
pub mod home_mode; pub mod home_mode;
pub mod rewards;
pub mod time;
pub mod profile; pub mod profile;
pub mod rewards;
pub mod service; pub mod service;
pub mod time;
pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE}; pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE};
pub use config::{CardRef, ChestRewardConfig, DataSelector, GameConfig, StarterProfile}; pub use config::{CardRef, ChestRewardConfig, DataSelector, GameConfig, StarterProfile};
pub use home::{build_avatar, build_home}; pub use home::{build_avatar, build_home};
pub use home_mode::{HomeMode, HomeModeRegistry, TurnResult}; pub use home_mode::{HomeMode, HomeModeRegistry, TurnResult};
pub use rewards::RewardRoller;
pub use profile::{PlayerProfile, ProfileStore}; pub use profile::{PlayerProfile, ProfileStore};
pub use rewards::RewardRoller;
pub use service::GameService; pub use service::GameService;

View file

@ -58,7 +58,11 @@ impl PlayerProfile {
arena: catalog.resolve_arena(&starter.arena), arena: catalog.resolve_arena(&starter.arena),
chest_slot_count: starter.chest_slot_count, chest_slot_count: starter.chest_slot_count,
deck: starter.deck.iter().filter_map(card_of).collect(), 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, battle_count: 0,
win_count: 0, win_count: 0,
lose_count: 0, lose_count: 0,

View file

@ -1,5 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use logic::{EndClientTurnMessage, LogicDataRef, OutOfSyncMessage, OwnHomeDataMessage}; use logic::{
EndClientTurnMessage, LogicCommandManager, LogicDataRef, OutOfSyncMessage, OwnHomeDataMessage,
};
use service_rpc::{ use service_rpc::{
AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult, AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult,
RpcService, WireMessage, RpcService, WireMessage,
@ -25,6 +27,7 @@ impl GameService {
profiles = profiles.len().await, profiles = profiles.len().await,
store = %config.store_path.display(), store = %config.store_path.display(),
catalog = catalog.is_loaded(), catalog = catalog.is_loaded(),
commands = LogicCommandManager::registry().len(),
"profile store ready" "profile store ready"
); );
Ok(Arc::new(Self { Ok(Arc::new(Self {

View file

@ -46,13 +46,17 @@ impl AuthApi for RemoteAuth {
.await? .await?
{ {
AuthResponse::Login(outcome) => Ok(outcome), 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<bool> { async fn resolve(&self, account: AccountRef) -> RpcResult<bool> {
match self.client.call(&AuthRequest::Resolve { account }).await? { match self.client.call(&AuthRequest::Resolve { account }).await? {
AuthResponse::Resolved { known } => Ok(known), AuthResponse::Resolved { known } => Ok(known),
other => Err(RpcError::Rejected(format!("unexpected auth reply {other:?}"))), other => Err(RpcError::Rejected(format!(
"unexpected auth reply {other:?}"
))),
} }
} }
} }

View file

@ -1,8 +1,8 @@
use std::time::Duration; use std::time::Duration;
use logic::{ use logic::{
message_type, AvailableServerCommandMessage, EndClientTurnMessage, KeepAliveMessage, message_type, AvailableServerCommandMessage, EndClientTurnMessage, KeepAliveMessage,
LoginMessage, LoginOkMessage, LogicCommand, LogicCommandHeader, LogicDataTables, LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCommandHeader, LogicDataTables,
OwnHomeDataMessage, LoginMessage, LoginOkMessage, OwnHomeDataMessage,
}; };
use titan::crypto::SessionCipher; use titan::crypto::SessionCipher;
use titan::{FrameCodec, FrameHeader, MessageFrame, MessageMeta, Payload, HEADER_LEN}; use titan::{FrameCodec, FrameHeader, MessageFrame, MessageMeta, Payload, HEADER_LEN};
@ -38,14 +38,20 @@ impl Probe {
.await??; .await??;
let header = FrameHeader::parse(&header_bytes); let header = FrameHeader::parse(&header_bytes);
let mut payload = vec![0u8; header.payload_len]; 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"); let plain = self.cipher.decrypt_inbound(&payload).expect("decrypt");
Ok((header.message_type, plain)) Ok((header.message_type, plain))
} }
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
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() let account_high: i32 = std::env::args()
.nth(2) .nth(2)
.and_then(|value| value.parse().ok()) .and_then(|value| value.parse().ok())
@ -67,8 +73,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(error) => println!("no data tables ({error}), printing raw ids"), Err(error) => println!("no data tables ({error}), printing raw ids"),
} }
println!( println!(
"message registry: {} type(s)", "message registry: {} type(s), command registry: {} type(s)",
logic::scroll_message_registry().len() logic::scroll_message_registry().len(),
logic::LogicCommandManager::registry().len()
); );
println!("connecting to {endpoint}"); println!("connecting to {endpoint}");
let mut probe = Probe::connect(&endpoint).await?; let mut probe = Probe::connect(&endpoint).await?;
@ -95,7 +102,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("<- 20104 LoginOkMessage"); println!("<- 20104 LoginOkMessage");
println!(" account {}", ok.account_id); println!(" account {}", ok.account_id);
println!(" home {}", ok.home_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!( println!(
" server {}.{}.{} content {}", " server {}.{}.{} content {}",
ok.server_major_version, ok.server_major_version,
@ -104,7 +114,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
ok.content_version ok.content_version
); );
println!(" sessions {}", ok.session_count); 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 => { message_type::LOGIN_FAILED => {
let failed = logic::LoginFailedMessage::from_bytes(&payload)?; let failed = logic::LoginFailedMessage::from_bytes(&payload)?;
@ -168,9 +181,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
checksum: home.home.chest_id_counter checksum: home.home.chest_id_counter
+ ((home.home.spell_collection.spells.len() as i32) << 16) + ((home.home.spell_collection.spells.len() as i32) << 16)
+ i32::from(desync), + i32::from(desync),
commands: vec![LogicCommand::CollectFreeChest { commands: vec![Box::new(LogicCollectFreeChestCommand {
header: LogicCommandHeader::for_account(home.avatar.account_id), header: LogicCommandHeader::for_account(home.avatar.account_id),
}], })],
trailing: None, trailing: None,
}; };
probe.send(&turn).await?; probe.send(&turn).await?;
@ -189,14 +202,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
message_type::AVAILABLE_SERVER_COMMAND => { message_type::AVAILABLE_SERVER_COMMAND => {
let envelope = AvailableServerCommandMessage::from_bytes(&payload)?; let envelope = AvailableServerCommandMessage::from_bytes(&payload)?;
println!("<- 24111 AvailableServerCommandMessage ({} bytes)", payload.len()); println!(
if let LogicCommand::ClaimReward { command: claim } = envelope.command { "<- 24111 AvailableServerCommandMessage ({} bytes)",
payload.len()
);
if let Some(claim) = envelope.command.downcast_ref::<LogicClaimRewardCommand>() {
println!(" chest source {}", claim.chest_source); println!(" chest source {}", claim.chest_source);
println!(" chest id {}", claim.chest_id); println!(" chest id {}", claim.chest_id);
if let Some(reward) = claim.reward { if let Some(reward) = &claim.reward {
println!(" gold {}", reward.gold); println!(" gold {}", reward.gold);
println!(" diamonds {}", reward.diamonds); 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); println!(" card {} x{}", spell.data, spell.count);
} }
} }

View file

@ -38,7 +38,10 @@ pub struct Session {
#[serde(tag = "outcome", rename_all = "snake_case")] #[serde(tag = "outcome", rename_all = "snake_case")]
pub enum LoginOutcome { pub enum LoginOutcome {
Accepted(Session), Accepted(Session),
Rejected { error_code: i32, message: Option<String> }, Rejected {
error_code: i32,
message: Option<String>,
},
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")] #[serde(tag = "op", rename_all = "snake_case")]

View file

@ -1,7 +1,9 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::error::RpcResult; use crate::error::RpcResult;
use crate::wire::WireMessage; 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 struct AccountRef {
pub high: i32, pub high: i32,
pub low: i32, pub low: i32,
@ -70,6 +72,10 @@ pub trait GameApi: Send + Sync + 'static {
kind: HomeRequestKind, kind: HomeRequestKind,
) -> RpcResult<Vec<WireMessage>>; ) -> RpcResult<Vec<WireMessage>>;
async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()>; async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()>;
async fn end_client_turn(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<Vec<WireMessage>>; async fn end_client_turn(
&self,
account: AccountRef,
payload: Vec<u8>,
) -> RpcResult<Vec<WireMessage>>;
async fn disconnect(&self, account: AccountRef) -> RpcResult<()>; async fn disconnect(&self, account: AccountRef) -> RpcResult<()>;
} }

View file

@ -35,9 +35,6 @@ impl Default for FieldSpec {
} }
} }
pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> { pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
if let Data::Enum(data) = &input.data {
return expand_tagged_enum(input, data);
}
let ident = &input.ident; let ident = &input.ident;
let partial = has_partial(input)?; let partial = has_partial(input)?;
let fields = match &input.data { let fields = match &input.data {
@ -52,10 +49,16 @@ pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
} }
}, },
Data::Enum(data) => { 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) => { 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(); let mut encode_body = Vec::new();
@ -121,123 +124,6 @@ pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
} }
}) })
} }
fn expand_tagged_enum(input: &DeriveInput, data: &syn::DataEnum) -> Result<TokenStream> {
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::<Vec<_>>(),
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<i32>>::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<Self> {
let tag = <#tag as ::titan::codec::Codec<i32>>::read(reader)?;
match tag {
#(#decode_arms)*
other => ::core::result::Result::Err(::titan::error::Error::UnknownCommandType(other)),
}
}
}
})
}
fn tag_codec(input: &DeriveInput) -> Result<TokenStream> {
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<TokenStream> {
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<bool> { fn has_partial(input: &DeriveInput) -> Result<bool> {
let mut partial = false; let mut partial = false;
for attr in &input.attrs { for attr in &input.attrs {

View file

@ -216,7 +216,9 @@ impl<T, C: Codec<T>, const N: usize> Codec<[T; N]> for Arr<C, N> {
for _ in 0..N { for _ in 0..N {
items.push(C::read(reader)?); items.push(C::read(reader)?);
} }
items.try_into().map_err(|items: Vec<T>| Error::ArityMismatch { items
.try_into()
.map_err(|items: Vec<T>| Error::ArityMismatch {
expected: N, expected: N,
actual: items.len(), actual: items.len(),
}) })
@ -239,7 +241,11 @@ impl<T, C: Codec<T>, const N: usize> Codec<[Option<T>; N]> for SplitArr<C, N> {
} }
let mut items = Vec::with_capacity(N); let mut items = Vec::with_capacity(N);
for occupied in present { 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 items
.try_into() .try_into()

View file

@ -19,9 +19,7 @@ impl Rc4Encrypter {
} }
let mut j: u8 = 0; let mut j: u8 = 0;
for i in 0..256usize { for i in 0..256usize {
j = j j = j.wrapping_add(state[i]).wrapping_add(seed[i % seed.len()]);
.wrapping_add(state[i])
.wrapping_add(seed[i % seed.len()]);
state.swap(i, j as usize); state.swap(i, j as usize);
} }
let mut cipher = Self { state, i: 0, j: 0 }; let mut cipher = Self { state, i: 0, j: 0 };

View file

@ -27,14 +27,10 @@ impl CsvReader {
let mut lines = source let mut lines = source
.lines() .lines()
.map(|line| line.strip_suffix('\r').unwrap_or(line)); .map(|line| line.strip_suffix('\r').unwrap_or(line));
let name_line = lines let name_line = lines.next().ok_or_else(|| CsvError::MissingColumnNames {
.next()
.ok_or_else(|| CsvError::MissingColumnNames {
file: file_name.clone(), file: file_name.clone(),
})?; })?;
let type_line = lines let type_line = lines.next().ok_or_else(|| CsvError::MissingColumnTypes {
.next()
.ok_or_else(|| CsvError::MissingColumnTypes {
file: file_name.clone(), file: file_name.clone(),
})?; })?;
let names = split_line(name_line); let names = split_line(name_line);

View file

@ -26,7 +26,9 @@ impl CsvRow {
} }
} }
pub fn value_at(&self, column: usize, index: usize) -> Option<&CsvValue> { 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> { pub fn value(&self, column: usize) -> Option<&CsvValue> {
self.value_at(column, 0) self.value_at(column, 0)
@ -35,13 +37,17 @@ impl CsvRow {
self.columns.get(column).map(Vec::len).unwrap_or_default() self.columns.get(column).map(Vec::len).unwrap_or_default()
} }
pub fn string_at(&self, column: usize, index: usize) -> &str { 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 { pub fn string(&self, column: usize) -> &str {
self.string_at(column, 0) self.string_at(column, 0)
} }
pub fn int_at(&self, column: usize, index: usize) -> i32 { 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 { pub fn int(&self, column: usize) -> i32 {
self.int_at(column, 0) self.int_at(column, 0)

View file

@ -68,7 +68,9 @@ impl CsvTable {
&self.rows &self.rows
} }
pub fn row_by_name(&self, name: &str) -> Option<&CsvRow> { 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<usize> { pub fn row_index_of(&self, name: &str) -> Option<usize> {
self.row_index.get(name).copied() self.row_index.get(name).copied()

View file

@ -45,7 +45,12 @@ where
inventory::collect!(RegistryEntry); inventory::collect!(RegistryEntry);
static GLOBAL: OnceLock<Arc<MessageRegistry>> = OnceLock::new(); static GLOBAL: OnceLock<Arc<MessageRegistry>> = OnceLock::new();
pub trait MessageFactory: Send + Sync { pub trait MessageFactory: Send + Sync {
fn create(&self, message_type: u16, message_version: u16, payload: &[u8]) -> Result<Box<dyn Message>>; fn create(
&self,
message_type: u16,
message_version: u16,
payload: &[u8],
) -> Result<Box<dyn Message>>;
fn lookup(&self, message_type: u16) -> Option<&RegistryEntry>; fn lookup(&self, message_type: u16) -> Option<&RegistryEntry>;
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -91,7 +96,12 @@ impl MessageRegistry {
} }
} }
impl MessageFactory for MessageRegistry { impl MessageFactory for MessageRegistry {
fn create(&self, message_type: u16, _message_version: u16, payload: &[u8]) -> Result<Box<dyn Message>> { fn create(
&self,
message_type: u16,
_message_version: u16,
payload: &[u8],
) -> Result<Box<dyn Message>> {
let entry = self let entry = self
.entries .entries
.get(&message_type) .get(&message_type)

View file

@ -12,7 +12,9 @@ impl FrameHeader {
pub fn parse(bytes: &[u8; HEADER_LEN]) -> Self { pub fn parse(bytes: &[u8; HEADER_LEN]) -> Self {
Self { Self {
message_type: u16::from_be_bytes([bytes[0], bytes[1]]), 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]]), message_version: u16::from_be_bytes([bytes[5], bytes[6]]),
} }
} }

View file

@ -46,13 +46,19 @@ impl JsonValue {
} }
} }
pub fn int_or(&self, key: &str, fallback: i32) -> i32 { 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 { 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 { 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<Item = (&'static str, JsonValue)>) -> Self { pub fn object(pairs: impl IntoIterator<Item = (&'static str, JsonValue)>) -> Self {
JsonValue::Object( JsonValue::Object(
@ -255,7 +261,10 @@ impl<'a> Parser<'a> {
.and_then(|text| u32::from_str_radix(text, 16).ok()) .and_then(|text| u32::from_str_radix(text, 16).ok())
.ok_or(JsonError::InvalidEscape(self.offset))?; .ok_or(JsonError::InvalidEscape(self.offset))?;
self.offset += 4; 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)), _ => return Err(JsonError::InvalidEscape(self.offset - 1)),
} }
@ -263,7 +272,10 @@ impl<'a> Parser<'a> {
_ => { _ => {
let start = self.offset - 1; let start = self.offset - 1;
let mut end = self.offset; 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; end += 1;
} }
let chunk = std::str::from_utf8(&self.bytes[start..end]) let chunk = std::str::from_utf8(&self.bytes[start..end])

View file

@ -1,7 +1,7 @@
pub mod checksum; pub mod checksum;
pub mod csv;
pub mod codec; pub mod codec;
pub mod crypto; pub mod crypto;
pub mod csv;
pub mod data_ref; pub mod data_ref;
pub mod error; pub mod error;
pub mod factory; pub mod factory;
@ -12,11 +12,11 @@ pub mod logic_long;
pub mod message; pub mod message;
pub mod net; pub mod net;
pub use checksum::ChecksumEncoder; pub use checksum::ChecksumEncoder;
pub use csv::{ColumnType, CsvError, CsvNode, CsvReader, CsvRow, CsvTable, CsvValue};
pub use codec::{ pub use codec::{
Arr, Bool, Bytes, Codec, Int, List, Long, Nested, NullList, Opt, SplitArr, Str, StrRef, VInt, Arr, Bool, Bytes, Codec, Int, List, Long, Nested, NullList, Opt, SplitArr, Str, StrRef, VInt,
VLong, VLong,
}; };
pub use csv::{ColumnType, CsvError, CsvNode, CsvReader, CsvRow, CsvTable, CsvValue};
pub use data_ref::GlobalId; pub use data_ref::GlobalId;
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use factory::{MessageFactory, MessageRegistry}; pub use factory::{MessageFactory, MessageRegistry};
@ -25,7 +25,9 @@ pub use io::{ByteStreamReader, ByteStreamWriter};
pub use json::JsonValue; pub use json::JsonValue;
pub use logic_long::LogicLong; pub use logic_long::LogicLong;
pub use message::{Direction, Message, MessageMeta, Payload}; 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 inventory;
pub use titan_derive::{Message, Payload}; pub use titan_derive::{Message, Payload};
pub mod prelude { pub mod prelude {

View file

@ -140,9 +140,7 @@ impl Messaging {
.await .await
{ {
Err(_) => return Err(MessagingError::IdleTimeout), Err(_) => return Err(MessagingError::IdleTimeout),
Ok(Err(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => { Ok(Err(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
return Ok(None)
}
Ok(Err(error)) => return Err(error.into()), Ok(Err(error)) => return Err(error.into()),
Ok(Ok(_)) => {} Ok(Ok(_)) => {}
} }
@ -161,7 +159,8 @@ impl Messaging {
.await .await
.map_err(|_| MessagingError::IdleTimeout)??; .map_err(|_| MessagingError::IdleTimeout)??;
let payload = self.inbound.decrypt(&cipher_text)?; let payload = self.inbound.decrypt(&cipher_text)?;
let message = match self let message =
match self
.factory .factory
.create(header.message_type, header.message_version, &payload) .create(header.message_type, header.message_version, &payload)
{ {