use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use tokio::sync::mpsc::UnboundedSender;
use tracing::{debug, error, info};
use crate::client_runner::MultiClientStats;
use crate::event_bus::{EventEnvelope, EventType, SystemEvent};
use gromnie_events::{ProtocolEvent, S2CEvent, SimpleClientAction};
use serenity::http::Http;
use serenity::model::id::ChannelId;
fn format_uptime(duration: Duration) -> (u64, u64, u64) {
let secs = duration.as_secs();
(secs / 3600, (secs % 3600) / 60, secs % 60)
}
fn log_common_protocol_event(event: &ProtocolEvent) -> bool {
match event {
ProtocolEvent::S2C(S2CEvent::LoginCharacterSet {
account,
characters,
num_slots,
}) => {
let names = characters
.iter()
.map(|c| format!("{} ({})", c.name, c.character_id.0))
.collect::<Vec<_>>()
.join(", ");
info!(target: "events", "CharacterList -- Account: {}, Slots: {}, Number of Chars: {}, Chars: {}", account, num_slots, characters.len(), names);
true
}
ProtocolEvent::S2C(S2CEvent::CharacterError {
error_code,
error_message,
}) => {
error!(target: "events", "Character error (code {}): {}", error_code, error_message);
true
}
_ => false,
}
}
fn log_common_system_event(event: &SystemEvent) -> bool {
match event {
SystemEvent::ConnectingProgress { progress, .. } => {
debug!(target: "events", "Connecting progress: {:.1}%", progress * 100.0);
true
}
SystemEvent::UpdatingProgress { progress, .. } => {
debug!(target: "events", "Updating progress: {:.1}%", progress * 100.0);
true
}
SystemEvent::AuthenticationSucceeded { .. } => {
info!(target: "events", "Authentication succeeded - connected to server");
true
}
SystemEvent::AuthenticationFailed { reason, .. } => {
error!(target: "events", "Authentication failed: {}", reason);
true
}
SystemEvent::Disconnected {
will_reconnect,
reconnect_attempt,
delay_secs,
..
} => {
info!(
target: "events",
"Disconnected (will_reconnect={}, attempt={}, delay={}s)",
will_reconnect, reconnect_attempt, delay_secs
);
true
}
SystemEvent::Reconnecting {
attempt,
delay_secs,
..
} => {
info!(target: "events", "Reconnecting (attempt={}, delay={}s)", attempt, delay_secs);
true
}
_ => false,
}
}
pub use gromnie_events::EventConsumer;
pub struct LoggingConsumer {
_action_tx: UnboundedSender<SimpleClientAction>,
}
impl LoggingConsumer {
pub fn new(action_tx: UnboundedSender<SimpleClientAction>) -> Self {
Self {
_action_tx: action_tx,
}
}
pub fn from_factory() -> impl crate::client_runner_builder::ConsumerFactory {
LoggingConsumerFactory
}
}
struct LoggingConsumerFactory;
impl crate::client_runner_builder::ConsumerFactory for LoggingConsumerFactory {
fn create(
&self,
ctx: &crate::client_runner_builder::ConsumerContext,
) -> Box<dyn EventConsumer> {
Box::new(LoggingConsumer::new(ctx.action_tx.clone()))
}
}
impl EventConsumer for LoggingConsumer {
fn handle_event(&mut self, envelope: EventEnvelope) {
match envelope.event {
EventType::Protocol(protocol_event) => {
if log_common_protocol_event(&protocol_event) {
return;
}
if let Some(chat) = protocol_event.as_chat_message() {
info!(target: "events", "CHAT [{}]: {}", chat.message_type, chat.text);
return;
}
if let ProtocolEvent::S2C(s2c) = &protocol_event {
match s2c {
S2CEvent::LoginCreatePlayer { character_id } => {
info!(target: "events", "CREATE PLAYER: Character ID {}", character_id);
}
S2CEvent::ItemCreateObject {
object_id,
name,
item_type,
container_id,
burden,
value,
items_capacity: _,
container_capacity: _,
} => {
info!(target: "events", "ITEM CREATE: {} (ID: {}, Type: {}, Container: {:?}, Burden: {}, Value: {})",
name, object_id, item_type, container_id, burden, value);
}
S2CEvent::ItemOnViewContents {
container_id,
items,
} => {
info!(target: "events", "ITEM VIEW CONTENTS: Container {} has {} items", container_id, items.len());
}
S2CEvent::PlayerContainersReceived {
player_id,
containers,
} => {
info!(target: "events", "PLAYER CONTAINERS: Player {} has {} containers", player_id, containers.len());
}
S2CEvent::ItemDeleteObject { object_id } => {
info!(target: "events", "ITEM DELETE: Object ID {}", object_id);
}
S2CEvent::QualitiesPrivateUpdateInt { property, value } => {
info!(target: "events", "QUALITY UPDATE: property {} = {}", property, value);
}
S2CEvent::ItemSetState { object_id, state } => {
info!(target: "events", "ITEM SET STATE: Object {} state = {}", object_id, state);
}
_ => {}
}
}
}
EventType::State(state_event) => {
info!(target: "events", "STATE CHANGE: {:?}", state_event);
}
EventType::System(system_event) => {
if log_common_system_event(&system_event) {
return;
}
match system_event {
SystemEvent::LoginSucceeded {
character_id,
character_name,
} => {
info!(target: "events", "LoginSucceeded -- Character: {} (ID: {})", character_name, character_id);
}
SystemEvent::ConnectingStarted { .. } => {
info!(target: "events", "Connecting started");
}
SystemEvent::ConnectingDone { .. } => {
info!(target: "events", "Connecting done");
}
SystemEvent::UpdatingStarted { .. } => {
info!(target: "events", "Updating started");
}
SystemEvent::UpdatingDone { .. } => {
info!(target: "events", "Updating done");
}
SystemEvent::ReloadScripts { .. } => {
info!(target: "events", "Reloading scripts");
}
SystemEvent::LogScriptMessage { script_id, message } => {
info!(target: "events", "Script [{}]: {}", script_id, message);
}
SystemEvent::Shutdown => {
info!(target: "events", "System shutdown");
}
_ => {}
}
}
}
}
}
pub struct TuiConsumer {
_action_tx: UnboundedSender<SimpleClientAction>,
tui_event_tx: UnboundedSender<crate::event_bus::TuiEvent>,
}
impl TuiConsumer {
pub fn new(
action_tx: UnboundedSender<SimpleClientAction>,
tui_event_tx: UnboundedSender<crate::event_bus::TuiEvent>,
) -> Self {
Self {
_action_tx: action_tx,
tui_event_tx,
}
}
pub fn from_factory(
tui_event_tx: UnboundedSender<crate::event_bus::TuiEvent>,
) -> impl crate::client_runner_builder::ConsumerFactory {
TuiConsumerFactory { tui_event_tx }
}
}
struct TuiConsumerFactory {
tui_event_tx: UnboundedSender<crate::event_bus::TuiEvent>,
}
impl crate::client_runner_builder::ConsumerFactory for TuiConsumerFactory {
fn create(
&self,
ctx: &crate::client_runner_builder::ConsumerContext,
) -> Box<dyn EventConsumer> {
Box::new(TuiConsumer::new(
ctx.action_tx.clone(),
self.tui_event_tx.clone(),
))
}
}
impl EventConsumer for TuiConsumer {
fn handle_event(&mut self, envelope: EventEnvelope) {
match envelope.event {
EventType::Protocol(protocol_event) => {
tracing::info!(target: "tui_consumer", "TuiConsumer forwarding ProtocolEvent: {:?}", std::mem::discriminant(&protocol_event));
let _ = self.tui_event_tx.send(protocol_event.into());
}
EventType::System(system_event) => {
tracing::info!(target: "tui_consumer", "TuiConsumer forwarding SystemEvent: {:?}", std::mem::discriminant(&system_event));
let _ = self.tui_event_tx.send(system_event.into());
}
EventType::State(state_event) => {
tracing::info!(target: "tui_consumer", "TuiConsumer forwarding StateEvent: {:?}", std::mem::discriminant(&state_event));
let _ = self.tui_event_tx.send(state_event.into());
}
}
}
}
#[derive(Clone)]
pub struct UptimeData {
pub bot_start: Instant,
pub ingame_start: Option<Instant>,
}
impl UptimeData {
pub fn format_bot_uptime(&self) -> String {
let (h, m, s) = format_uptime(self.bot_start.elapsed());
format!("{:02}:{:02}:{:02}", h, m, s)
}
pub fn format_ingame_uptime(&self) -> Option<String> {
self.ingame_start.map(|start| {
let (h, m, s) = format_uptime(start.elapsed());
format!("{:02}:{:02}:{:02}", h, m, s)
})
}
}
pub struct DiscordConsumer {
_action_tx: UnboundedSender<SimpleClientAction>,
http: Arc<Http>,
channel_id: ChannelId,
bot_start_time: Instant,
ingame_start_time: Option<Instant>,
uptime_data: Option<Arc<tokio::sync::RwLock<UptimeData>>>,
}
impl DiscordConsumer {
pub fn new(
action_tx: UnboundedSender<SimpleClientAction>,
http: Arc<Http>,
channel_id: ChannelId,
) -> Self {
Self {
_action_tx: action_tx,
http,
channel_id,
bot_start_time: Instant::now(),
ingame_start_time: None,
uptime_data: None,
}
}
pub fn new_with_uptime(
action_tx: UnboundedSender<SimpleClientAction>,
http: Arc<Http>,
channel_id: ChannelId,
uptime_data: Arc<tokio::sync::RwLock<UptimeData>>,
) -> Self {
Self {
_action_tx: action_tx,
http,
channel_id,
bot_start_time: Instant::now(),
ingame_start_time: None,
uptime_data: Some(uptime_data),
}
}
fn handle_login_succeeded(&mut self, character_id: u32, character_name: &str) {
let now = Instant::now();
self.ingame_start_time = Some(now);
if let Some(ref uptime_data) = self.uptime_data {
let uptime_data_clone = uptime_data.clone();
tokio::spawn(async move {
let mut data = uptime_data_clone.write().await;
data.ingame_start = Some(now);
});
}
let (h, m, s) = format_uptime(self.bot_start_time.elapsed());
info!(target: "events", "LoginSucceeded -- Character: {} (ID: {})", character_name, character_id);
info!(target: "events", "Bot uptime: {:02}:{:02}:{:02} | Now tracking in-game time", h, m, s);
}
pub fn from_factory(
http: Arc<Http>,
channel_id: ChannelId,
) -> impl crate::client_runner_builder::ConsumerFactory {
DiscordConsumerFactory {
http,
channel_id,
uptime_data: None,
}
}
pub fn from_factory_with_uptime(
http: Arc<Http>,
channel_id: ChannelId,
uptime_data: Arc<tokio::sync::RwLock<UptimeData>>,
) -> impl crate::client_runner_builder::ConsumerFactory {
DiscordConsumerFactory {
http,
channel_id,
uptime_data: Some(uptime_data),
}
}
}
struct DiscordConsumerFactory {
http: Arc<Http>,
channel_id: ChannelId,
uptime_data: Option<Arc<tokio::sync::RwLock<UptimeData>>>,
}
impl crate::client_runner_builder::ConsumerFactory for DiscordConsumerFactory {
fn create(
&self,
ctx: &crate::client_runner_builder::ConsumerContext,
) -> Box<dyn EventConsumer> {
if let Some(ref uptime_data) = self.uptime_data {
Box::new(DiscordConsumer::new_with_uptime(
ctx.action_tx.clone(),
self.http.clone(),
self.channel_id,
uptime_data.clone(),
))
} else {
Box::new(DiscordConsumer::new(
ctx.action_tx.clone(),
self.http.clone(),
self.channel_id,
))
}
}
}
impl EventConsumer for DiscordConsumer {
fn handle_event(&mut self, envelope: EventEnvelope) {
match envelope.event {
EventType::Protocol(protocol_event) => {
if log_common_protocol_event(&protocol_event) {
return;
}
if let Some(chat) = protocol_event.as_chat_message() {
if let Some(ingame_start) = self.ingame_start_time {
let (h, m, s) = format_uptime(ingame_start.elapsed());
info!(target: "events", "CHAT [{}]: {} | In-game: {:02}:{:02}:{:02}", chat.message_type, chat.text, h, m, s);
} else {
info!(target: "events", "CHAT [{}]: {}", chat.message_type, chat.text);
}
let discord_message = format!("[{}] {}", chat.message_type, chat.text);
let http = self.http.clone();
let channel_id = self.channel_id;
tokio::spawn(async move {
if let Err(e) = channel_id.say(&http, &discord_message).await {
error!("Failed to send Discord message: {}", e);
}
});
}
}
EventType::State(state_event) => {
info!(target: "events", "STATE CHANGE: {:?}", state_event);
}
EventType::System(system_event) => {
if log_common_system_event(&system_event) {
return;
}
if let SystemEvent::LoginSucceeded {
character_id,
character_name,
} = system_event
{
self.handle_login_succeeded(character_id, &character_name);
}
}
}
}
}
pub struct StatsConsumer {
client_id: u32,
stats: Arc<MultiClientStats>,
verbose: bool,
}
impl StatsConsumer {
pub fn new(client_id: u32, stats: Arc<MultiClientStats>) -> Self {
Self {
client_id,
stats,
verbose: false,
}
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn from_factory(
stats: Arc<MultiClientStats>,
verbose: bool,
) -> impl crate::client_runner_builder::ConsumerFactory {
StatsConsumerFactory { stats, verbose }
}
}
struct StatsConsumerFactory {
stats: Arc<MultiClientStats>,
verbose: bool,
}
impl crate::client_runner_builder::ConsumerFactory for StatsConsumerFactory {
fn create(
&self,
ctx: &crate::client_runner_builder::ConsumerContext,
) -> Box<dyn EventConsumer> {
Box::new(StatsConsumer::new(ctx.client_id, self.stats.clone()).with_verbose(self.verbose))
}
}
impl EventConsumer for StatsConsumer {
fn handle_event(&mut self, envelope: EventEnvelope) {
match envelope.event {
EventType::Protocol(event) => {
if let ProtocolEvent::S2C(S2CEvent::CharacterError { .. }) = event {
self.stats.errors.fetch_add(1, Ordering::SeqCst);
if self.verbose {
error!("[Client {}] Character error", self.client_id);
}
}
}
EventType::System(event) => match event {
SystemEvent::LoginSucceeded { .. } => {
self.stats.logged_in.fetch_add(1, Ordering::SeqCst);
if self.verbose {
info!("[Client {}] Login succeeded", self.client_id);
}
}
SystemEvent::AuthenticationSucceeded { .. } => {
self.stats.authenticated.fetch_add(1, Ordering::SeqCst);
if self.verbose {
info!("[Client {}] Authentication succeeded", self.client_id);
}
}
SystemEvent::AuthenticationFailed { .. } => {
self.stats.errors.fetch_add(1, Ordering::SeqCst);
if self.verbose {
error!("[Client {}] Authentication failed", self.client_id);
}
}
_ => {}
},
EventType::State(_) => {}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum AutoLoginState {
WaitingForCharList,
CharacterCreationInProgress,
CharacterFound,
}
pub struct AutoLoginConsumer {
client_id: u32,
character_name: String,
action_tx: UnboundedSender<SimpleClientAction>,
state: AutoLoginState,
verbose: bool,
}
impl AutoLoginConsumer {
pub fn new(
client_id: u32,
character_name: String,
action_tx: UnboundedSender<SimpleClientAction>,
) -> Self {
Self {
client_id,
character_name,
action_tx,
state: AutoLoginState::WaitingForCharList,
verbose: false,
}
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn state(&self) -> &AutoLoginState {
&self.state
}
pub fn from_factory(
character_name: String,
verbose: bool,
) -> impl crate::client_runner_builder::ConsumerFactory {
AutoLoginConsumerFactory {
character_name,
verbose,
}
}
}
struct AutoLoginConsumerFactory {
character_name: String,
verbose: bool,
}
impl crate::client_runner_builder::ConsumerFactory for AutoLoginConsumerFactory {
fn create(
&self,
ctx: &crate::client_runner_builder::ConsumerContext,
) -> Box<dyn EventConsumer> {
Box::new(
AutoLoginConsumer::new(
ctx.client_id,
self.character_name.clone(),
ctx.action_tx.clone(),
)
.with_verbose(self.verbose),
)
}
}
impl EventConsumer for AutoLoginConsumer {
fn handle_event(&mut self, envelope: EventEnvelope) {
if let Some(ProtocolEvent::S2C(S2CEvent::LoginCharacterSet {
characters,
account,
..
})) = envelope.extract_protocol_event()
{
if self.verbose {
info!(
"[Client {}] Got character list for {}: {} chars",
self.client_id,
account,
characters.len()
);
}
match self.state {
AutoLoginState::WaitingForCharList
| AutoLoginState::CharacterCreationInProgress => {
if let Some(char_info) =
characters.iter().find(|c| c.name == self.character_name)
{
if self.verbose {
info!(
"[Client {}] Found character: {} (ID: {})",
self.client_id, char_info.name, char_info.character_id.0
);
}
self.state = AutoLoginState::CharacterFound;
if let Err(e) = self.action_tx.send(SimpleClientAction::LoginCharacter {
character_id: char_info.character_id.0,
character_name: char_info.name.clone(),
account: account.clone(),
}) {
error!(
"[Client {}] Failed to send login action: {}",
self.client_id, e
);
}
} else if self.state == AutoLoginState::WaitingForCharList {
if self.verbose {
info!(
"[Client {}] Creating character: {}",
self.client_id, self.character_name
);
}
self.state = AutoLoginState::CharacterCreationInProgress;
if self.verbose {
info!(
"[Client {}] Would create character: {} in account {}",
self.client_id, self.character_name, account
);
}
}
}
AutoLoginState::CharacterFound => {
if self.verbose {
info!(
"[Client {}] Already processing login, ignoring character list update",
self.client_id
);
}
}
}
}
}
}
pub struct CompositeConsumer {
consumers: Vec<Box<dyn EventConsumer>>,
}
impl CompositeConsumer {
pub fn new(consumers: Vec<Box<dyn EventConsumer>>) -> Self {
Self { consumers }
}
pub fn with_consumer(mut self, consumer: Box<dyn EventConsumer>) -> Self {
self.consumers.push(consumer);
self
}
}
impl EventConsumer for CompositeConsumer {
fn handle_event(&mut self, envelope: EventEnvelope) {
for consumer in &mut self.consumers {
consumer.handle_event(envelope.clone());
}
}
}