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:
parent
d8ed6ae2ee
commit
972c61dae7
46 changed files with 661 additions and 468 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
15
logic/logic-derive/Cargo.toml
Normal file
15
logic/logic-derive/Cargo.toml
Normal 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 }
|
||||
46
logic/logic-derive/src/lib.rs
Normal file
46
logic/logic-derive/src/lib.rs
Normal 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>()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
104
logic/logic/src/commands/chest.rs
Normal file
104
logic/logic/src/commands/chest.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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 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<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 {
|
||||
(200..500).contains(&self.command_type())
|
||||
}
|
||||
pub fn claimed_chest_source(&self) -> Option<i32> {
|
||||
match self {
|
||||
LogicCommand::StartRewardClaim { .. } => Some(chest_source::SLOT),
|
||||
LogicCommand::CollectFreeChest { .. } => Some(chest_source::FREE),
|
||||
LogicCommand::CollectMultiWinChest { .. } => Some(chest_source::CROWN),
|
||||
_ => None,
|
||||
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
|
||||
self.as_any().downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
pub fn claimed_chest_id(&self) -> i32 {
|
||||
match self {
|
||||
LogicCommand::StartRewardClaim { chest_id, .. } => *chest_id,
|
||||
_ => 0,
|
||||
type DecodeFn = fn(&mut ByteStreamReader<'_>) -> Result<Box<dyn LogicCommand>>;
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
pub fn upgraded_spell(&self) -> Option<&LogicDataRef> {
|
||||
match self {
|
||||
LogicCommand::FuseSpells { spell, .. } => Some(spell),
|
||||
_ => None,
|
||||
impl CommandRegistryEntry {
|
||||
pub const fn of<T: CommandMeta>() -> Self {
|
||||
Self {
|
||||
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);
|
||||
|
|
|
|||
45
logic/logic/src/commands/manager.rs
Normal file
45
logic/logic/src/commands/manager.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
31
logic/logic/src/commands/spells.rs
Normal file
31
logic/logic/src/commands/spells.rs
Normal 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
|
||||
}
|
||||
}
|
||||
51
logic/logic/src/commands/ui.rs
Normal file
51
logic/logic/src/commands/ui.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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();
|
||||
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<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>> {
|
||||
self.items.iter()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
&[
|
||||
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::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<LogicDataTable>> {
|
||||
self.table(table::GLOBALS)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
pub fn execute(&mut self, command: &dyn LogicCommand) -> CommandOutcome {
|
||||
command.execute(self)
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
mod logic_home_mode;
|
||||
pub use logic_home_mode::{CommandOutcome, LogicHomeMode};
|
||||
pub use logic_home_mode::LogicHomeMode;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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<dyn LogicCommand>,
|
||||
}
|
||||
impl AvailableServerCommandMessage {
|
||||
pub fn claim_reward(command: LogicClaimRewardCommand) -> Self {
|
||||
Self {
|
||||
command: LogicCommand::ClaimReward { command },
|
||||
command: Box::new(command),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ pub struct SetDeviceTokenMessage {
|
|||
pub device_token: Option<Vec<u8>>,
|
||||
}
|
||||
#[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<Vec<Option<String>>>,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<LogicCommand>,
|
||||
pub commands: Vec<Box<dyn LogicCommand>>,
|
||||
#[codec(bytes, stop_if_eof)]
|
||||
pub trailing: Option<Vec<u8>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Arc<Mutex<HomeMode>>> {
|
||||
self.sessions.write().await.remove(&account)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<bool> {
|
||||
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:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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()
|
||||
.nth(2)
|
||||
.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"),
|
||||
}
|
||||
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<dyn std::error::Error>> {
|
|||
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<dyn std::error::Error>> {
|
|||
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<dyn std::error::Error>> {
|
|||
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<dyn std::error::Error>> {
|
|||
}
|
||||
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::<LogicClaimRewardCommand>() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> },
|
||||
Rejected {
|
||||
error_code: i32,
|
||||
message: Option<String>,
|
||||
},
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "op", rename_all = "snake_case")]
|
||||
|
|
|
|||
|
|
@ -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<Vec<WireMessage>>;
|
||||
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<()>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,6 @@ impl Default for FieldSpec {
|
|||
}
|
||||
}
|
||||
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 partial = has_partial(input)?;
|
||||
let fields = match &input.data {
|
||||
|
|
@ -52,10 +49,16 @@ pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
|
|||
}
|
||||
},
|
||||
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<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> {
|
||||
let mut partial = false;
|
||||
for attr in &input.attrs {
|
||||
|
|
|
|||
|
|
@ -216,7 +216,9 @@ impl<T, C: Codec<T>, const N: usize> Codec<[T; N]> for Arr<C, N> {
|
|||
for _ in 0..N {
|
||||
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,
|
||||
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);
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -27,14 +27,10 @@ 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 {
|
||||
let name_line = lines.next().ok_or_else(|| CsvError::MissingColumnNames {
|
||||
file: file_name.clone(),
|
||||
})?;
|
||||
let type_line = lines
|
||||
.next()
|
||||
.ok_or_else(|| CsvError::MissingColumnTypes {
|
||||
let type_line = lines.next().ok_or_else(|| CsvError::MissingColumnTypes {
|
||||
file: file_name.clone(),
|
||||
})?;
|
||||
let names = split_line(name_line);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<usize> {
|
||||
self.row_index.get(name).copied()
|
||||
|
|
|
|||
|
|
@ -45,7 +45,12 @@ where
|
|||
inventory::collect!(RegistryEntry);
|
||||
static GLOBAL: OnceLock<Arc<MessageRegistry>> = OnceLock::new();
|
||||
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>;
|
||||
}
|
||||
#[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<Box<dyn Message>> {
|
||||
fn create(
|
||||
&self,
|
||||
message_type: u16,
|
||||
_message_version: u16,
|
||||
payload: &[u8],
|
||||
) -> Result<Box<dyn Message>> {
|
||||
let entry = self
|
||||
.entries
|
||||
.get(&message_type)
|
||||
|
|
|
|||
|
|
@ -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]]),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Item = (&'static str, JsonValue)>) -> 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])
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,7 +159,8 @@ impl Messaging {
|
|||
.await
|
||||
.map_err(|_| MessagingError::IdleTimeout)??;
|
||||
let payload = self.inbound.decrypt(&cipher_text)?;
|
||||
let message = match self
|
||||
let message =
|
||||
match self
|
||||
.factory
|
||||
.create(header.message_type, header.message_version, &payload)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue