From df017883c1a64ae3110adcd5f56412f667cb326a Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 3 Aug 2025 18:15:09 -0400 Subject: [PATCH] Add basic stats tracking #30 --- rc_core/src/persist/combat.rs | 2 +- rc_multiplayer/src/events/assist_bonus.rs | 31 ++++ rc_multiplayer/src/events/damage_bonus.rs | 30 ++++ rc_multiplayer/src/events/heal_bonus.rs | 30 ++++ rc_multiplayer/src/events/kill_bonus.rs | 31 ++++ rc_multiplayer/src/events/mod.rs | 16 +- rc_multiplayer/src/handlers/mod.rs | 1 + rc_multiplayer/src/handlers/stub.rs | 1 + rc_multiplayer/src/matches/generic.rs | 178 +++++++++++++++++++++- rc_multiplayer/src/matches/messages.rs | 22 +++ 10 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 rc_multiplayer/src/events/assist_bonus.rs create mode 100644 rc_multiplayer/src/events/damage_bonus.rs create mode 100644 rc_multiplayer/src/events/heal_bonus.rs create mode 100644 rc_multiplayer/src/events/kill_bonus.rs diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 8dbdbff..3498ab4 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -479,7 +479,7 @@ fn default_rotation() -> GameEventSequence { fn default_multiplayer() -> super::MultiplayerConfig { super::MultiplayerConfig { - players_per_game: 1, + players_per_game: 2, enabled: true, network: super::multiplayer::default_net_conf(), } diff --git a/rc_multiplayer/src/events/assist_bonus.rs b/rc_multiplayer/src/events/assist_bonus.rs new file mode 100644 index 0000000..fcdbf8e --- /dev/null +++ b/rc_multiplayer/src/events/assist_bonus.rs @@ -0,0 +1,31 @@ +pub struct AssistBonus { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::SimpleRlnl { + crate::handlers::SimpleRlnl::new(AssistBonus::new(init_ctx)) +} + +impl AssistBonus { + fn new(init_ctx: &crate::InitConfig) -> Self { + Self { + msg_router: init_ctx.matches_chann.clone(), + } + } +} + +#[async_trait::async_trait] +impl crate::handlers::RlnlEventCodeHandler for AssistBonus { + type In = rlnl::events::ingame::AssistBonus; + const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::AssistBonusRequest; + + async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { + if let Some(user_info) = user.user().await { + super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::AssistBonus { + user_id: user_info.user_id(), + shootee: data.requester_player_id as u8, + shooters: data.player_ids.into_iter().map(|x| x.player).collect(), + }).await); + } + } +} diff --git a/rc_multiplayer/src/events/damage_bonus.rs b/rc_multiplayer/src/events/damage_bonus.rs new file mode 100644 index 0000000..ee44f9f --- /dev/null +++ b/rc_multiplayer/src/events/damage_bonus.rs @@ -0,0 +1,30 @@ +pub struct DamageBonus { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::SimpleRlnl { + crate::handlers::SimpleRlnl::new(DamageBonus::new(init_ctx)) +} + +impl DamageBonus { + fn new(init_ctx: &crate::InitConfig) -> Self { + Self { + msg_router: init_ctx.matches_chann.clone(), + } + } +} + +#[async_trait::async_trait] +impl crate::handlers::RlnlEventCodeHandler for DamageBonus { + type In = rlnl::events::ingame::DestroyedHealedCubesBonus; + const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::DestroyCubesBonusRequest; + + async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { + if let Some(user_info) = user.user().await { + super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::DestroyCubesBonus { + user_id: user_info.user_id(), + info: data, + }).await); + } + } +} diff --git a/rc_multiplayer/src/events/heal_bonus.rs b/rc_multiplayer/src/events/heal_bonus.rs new file mode 100644 index 0000000..89b5a50 --- /dev/null +++ b/rc_multiplayer/src/events/heal_bonus.rs @@ -0,0 +1,30 @@ +pub struct HealBonus { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::SimpleRlnl { + crate::handlers::SimpleRlnl::new(HealBonus::new(init_ctx)) +} + +impl HealBonus { + fn new(init_ctx: &crate::InitConfig) -> Self { + Self { + msg_router: init_ctx.matches_chann.clone(), + } + } +} + +#[async_trait::async_trait] +impl crate::handlers::RlnlEventCodeHandler for HealBonus { + type In = rlnl::events::ingame::DestroyedHealedCubesBonus; + const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::HealCubesBonusRequest; + + async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { + if let Some(user_info) = user.user().await { + super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::DestroyCubesBonus { + user_id: user_info.user_id(), + info: data, + }).await); + } + } +} diff --git a/rc_multiplayer/src/events/kill_bonus.rs b/rc_multiplayer/src/events/kill_bonus.rs new file mode 100644 index 0000000..398903f --- /dev/null +++ b/rc_multiplayer/src/events/kill_bonus.rs @@ -0,0 +1,31 @@ +pub struct KillEnemyBonus { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::SimpleRlnl { + crate::handlers::SimpleRlnl::new(KillEnemyBonus::new(init_ctx)) +} + +impl KillEnemyBonus { + fn new(init_ctx: &crate::InitConfig) -> Self { + Self { + msg_router: init_ctx.matches_chann.clone(), + } + } +} + +#[async_trait::async_trait] +impl crate::handlers::RlnlEventCodeHandler for KillEnemyBonus { + type In = rlnl::events::ingame::Kill; + const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::KillBonusRequest; + + async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { + if let Some(user_info) = user.user().await { + super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::KillBonus { + user_id: user_info.user_id(), + shootee: data.killee_player_id, + shooter: data.killer_player_id, + }).await); + } + } +} diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index c385c22..da55765 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -11,6 +11,10 @@ mod client_unregister; mod flipper_start; mod self_destruct_elimination; mod map_ping; +mod kill_bonus; +mod assist_bonus; +mod damage_bonus; +mod heal_bonus; pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHandler { crate::handler::LnlEventHandler::new(init_ctx.users.clone(), crate::vehicle_motion::handler(init_ctx)) @@ -79,10 +83,8 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa {literustlib::packet::Property::Unreliable as u8}, rlnl::events::ingame::DestroyCubeEffectOnly, >::handler(init_ctx)) - .add(crate::handlers::Stub::< - {rlnl::event_code::NetworkEvent::DestroyCubesBonusRequest as i16}, - rlnl::events::ingame::DestroyedHealedCubesBonus, - >::handler(init_ctx)) + .add(damage_bonus::handler(init_ctx)) + .add(heal_bonus::handler(init_ctx)) .add(crate::handlers::Broadcaster::< false, {rlnl::event_code::NetworkEvent::HealSelf as i16}, @@ -90,10 +92,8 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa {literustlib::packet::Property::ReliableOrdered as u8}, rlnl::events::HealedCubes, >::handler(init_ctx)) - .add(crate::handlers::Stub::< - {rlnl::event_code::NetworkEvent::KillBonusRequest as i16}, - rlnl::events::ingame::Kill, // FIXME this is not a player ID -- it's actually sending 2 bytes not 1 - >::handler(init_ctx)) + .add(kill_bonus::handler(init_ctx)) + .add(assist_bonus::handler(init_ctx)) .add(kill_player::handler(init_ctx)) .add(crate::handlers::Broadcaster::< true, diff --git a/rc_multiplayer/src/handlers/mod.rs b/rc_multiplayer/src/handlers/mod.rs index 46e22e1..7f9cf26 100644 --- a/rc_multiplayer/src/handlers/mod.rs +++ b/rc_multiplayer/src/handlers/mod.rs @@ -12,4 +12,5 @@ mod ingame_broadcast_dataless; pub use ingame_broadcast_dataless::DatalessBroadcaster; mod stub; +#[allow(unused_imports)] pub use stub::Stub; diff --git a/rc_multiplayer/src/handlers/stub.rs b/rc_multiplayer/src/handlers/stub.rs index d7b1373..af98054 100644 --- a/rc_multiplayer/src/handlers/stub.rs +++ b/rc_multiplayer/src/handlers/stub.rs @@ -3,6 +3,7 @@ pub struct Stub + Send + Sync + 'static> Stub { + #[allow(dead_code)] pub fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::simple_typed::SimpleRlnl { crate::handlers::simple_typed::SimpleRlnl::new(Stub::new(init_ctx)) } diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 69b0ce0..c6f8d32 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -4,6 +4,7 @@ pub(super) struct UserConnection { pub(super) state: std::sync::Arc, pub(super) machine: MachineState, pub(super) descriptor: oj_rc_core::persist::user::PlayerDescriptor, + pub(super) counters: UserData, } #[derive(Clone)] @@ -76,6 +77,57 @@ impl Location { } } +pub(super) struct UserData { + pub kills: std::sync::atomic::AtomicU32, + pub deaths: std::sync::atomic::AtomicU32, + pub assists: std::sync::atomic::AtomicU32, + pub healed: std::sync::atomic::AtomicU32, + pub received_healed: std::sync::atomic::AtomicU32, + pub cubes: std::sync::atomic::AtomicU32, + pub received_cubes: std::sync::atomic::AtomicU32, // damage taken +} + +impl UserData { + fn new() -> Self { + Self { + kills: std::sync::atomic::AtomicU32::new(0), + deaths: std::sync::atomic::AtomicU32::new(0), + assists: std::sync::atomic::AtomicU32::new(0), + healed: std::sync::atomic::AtomicU32::new(0), + received_healed: std::sync::atomic::AtomicU32::new(0), + cubes: std::sync::atomic::AtomicU32::new(0), + received_cubes: std::sync::atomic::AtomicU32::new(0), + } + } + + pub(super) fn generic_score(&self) -> u32 { + self.kills.load(std::sync::atomic::Ordering::Relaxed) * 1_000 + + self.assists.load(std::sync::atomic::Ordering::Relaxed) * 100 + + self.healed.load(std::sync::atomic::Ordering::Relaxed) + + self.cubes.load(std::sync::atomic::Ordering::Relaxed) + } + + pub(super) fn get_generic_packet(&self, player_id: u8, stat: rlnl::types::IngameStatId, delta: Option) -> rlnl::events::ingame::UpdateGameStats { + let (stat_amount, backup_delta) = match stat { + rlnl::types::IngameStatId::DestroyedCubes + | rlnl::types::IngameStatId::DestroyedCubesInProtection + | rlnl::types::IngameStatId::DestroyedCubesDefendingTheBase => (self.cubes.load(std::sync::atomic::Ordering::SeqCst), 1), + rlnl::types::IngameStatId::Kill => (self.kills.load(std::sync::atomic::Ordering::Relaxed), 1_000), + rlnl::types::IngameStatId::KillAssist => (self.assists.load(std::sync::atomic::Ordering::Relaxed), 100), + rlnl::types::IngameStatId::HealCubes => (self.assists.load(std::sync::atomic::Ordering::SeqCst), 1), + rlnl::types::IngameStatId::RobotDestroyed => (self.deaths.load(std::sync::atomic::Ordering::Relaxed), 0), + s => panic!("Cannot generate game stat {:?}", s) + }; + rlnl::events::ingame::UpdateGameStats { + player_id, + stat_id: stat, + amount: stat_amount, + score: self.generic_score(), + delta_score: delta.unwrap_or(backup_delta), + } + } +} + #[repr(u8)] #[derive(Debug, Copy, Clone)] pub(super) enum ConnectionMode { @@ -252,6 +304,7 @@ impl GenericGamemodeEngine { state: std::sync::Arc::new(UserState::new()), machine: MachineState::new(), descriptor: player_info.to_owned(), + counters: UserData::new(), }; if self.custom_logic_handler.on_player_join(&self, &new_user, &self.players_info).await { self.spawn_send_loading_events(&new_user, id, self.players_info.clone()); @@ -489,7 +542,19 @@ impl GenericGamemodeEngine { true, ).await; log::info!("Player {} was destroyed by {} ({}) in game {}", remote_player, killer_player, user_id, self.game_guid()); - self.custom_logic_handler.on_vehicle_destroyed(&self, killer_player, remote_player).await; + if self.custom_logic_handler.on_vehicle_destroyed(&self, killer_player, remote_player).await { + // the kill tracking is initiated separately by the client with kill bonus event + if let Some(killed) = self.users.read().await.get(&remote_player) { + killed.counters.deaths.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let data = killed.counters.get_generic_packet(remote_player, rlnl::types::IngameStatId::RobotDestroyed, None); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + } + } }, super::GameMessage::SelfDestruct { user_id, is_classic } => { if let Some(player_id) = self.user_key_by_user_id(user_id).await { @@ -520,6 +585,16 @@ impl GenericGamemodeEngine { conn.connection.connection.disconnect(); } } + if let Some(killed) = self.users.read().await.get(&player_id) { + killed.counters.deaths.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let data = killed.counters.get_generic_packet(player_id, rlnl::types::IngameStatId::RobotDestroyed, None); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + } } } @@ -546,7 +621,106 @@ impl GenericGamemodeEngine { ).await); } } - } + }, + super::GameMessage::KillBonus { user_id: _, shootee, shooter } => { + if let Some(to_reward) = self.users.read().await.get(&shooter) { + to_reward.counters.kills.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + crate::events::log_lnl_send_failure(to_reward.connection.rlnl().send_data( + &rlnl::events::ingame::Kill { + killee_player_id: shootee, + killer_player_id: shooter, + }, + rlnl::event_code::NetworkEvent::ConfirmedKill, + literustlib::packet::Property::ReliableOrdered, + &to_reward.connection.connection + ).await); + let data = to_reward.counters.get_generic_packet(shooter, rlnl::types::IngameStatId::Kill, None); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + } + }, + super::GameMessage::AssistBonus { user_id: _, shootee, shooters } => { + let lock = self.users.read().await; + for shooter in shooters { + if let Some(to_reward) = lock.get(&shooter) { + to_reward.counters.assists.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + crate::events::log_lnl_send_failure(to_reward.connection.rlnl().send_data( + &rlnl::events::ingame::Kill { + killee_player_id: shootee, + killer_player_id: shooter, + }, + rlnl::event_code::NetworkEvent::ConfirmedAssist, + literustlib::packet::Property::ReliableOrdered, + &to_reward.connection.connection + ).await); + let data = to_reward.counters.get_generic_packet(shooter, rlnl::types::IngameStatId::KillAssist, None); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + } + } + }, + super::GameMessage::DestroyCubesBonus { user_id: _, info } => { + let lock = self.users.read().await; + for shooter in info.shooters { + if let Some(to_reward) = lock.get(&shooter.shooting_player_id) { + let mut total_cubes = 0; + for target in shooter.shooter_targets { + if let Some(to_punish) = lock.get(&target.target_player_id) { + let mut total_cubes_received = 0; + for cubes in target.cube_amounts { + // TODO use cube_id for something!? + total_cubes += cubes.cube_count; + total_cubes_received += cubes.cube_count; + } + to_punish.counters.received_cubes.fetch_add(total_cubes_received, std::sync::atomic::Ordering::SeqCst); + } + } + to_reward.counters.cubes.fetch_add(total_cubes, std::sync::atomic::Ordering::SeqCst); + let data = to_reward.counters.get_generic_packet(shooter.shooting_player_id, rlnl::types::IngameStatId::DestroyedCubes, Some(total_cubes)); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::Unreliable, + &data, + true, + ).await; + } + } + }, + super::GameMessage::HealCubesBonus { user_id: _, info } => { + let lock = self.users.read().await; + for shooter in info.shooters { + if let Some(to_reward) = lock.get(&shooter.shooting_player_id) { + let mut total_cubes = 0; + for target in shooter.shooter_targets { + if let Some(to_punish) = lock.get(&target.target_player_id) { + let mut total_cubes_received = 0; + for cubes in target.cube_amounts { + // TODO use cube_id for something!? + total_cubes += cubes.cube_count; + total_cubes_received += cubes.cube_count; + } + to_punish.counters.received_healed.fetch_add(total_cubes_received, std::sync::atomic::Ordering::SeqCst); + } + } + to_reward.counters.healed.fetch_add(total_cubes, std::sync::atomic::Ordering::SeqCst); + let data = to_reward.counters.get_generic_packet(shooter.shooting_player_id, rlnl::types::IngameStatId::HealCubes, Some(total_cubes)); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::Unreliable, + &data, + true, + ).await; + } + } + }, super::GameMessage::BroadcastRlnl { user_id, event, event_in, property, data } => { if self.custom_logic_handler.on_broadcast(&self, user_id, event, event_in, property, &data, false).await { if let Some(data) = data { diff --git a/rc_multiplayer/src/matches/messages.rs b/rc_multiplayer/src/matches/messages.rs index a909395..e45e858 100644 --- a/rc_multiplayer/src/matches/messages.rs +++ b/rc_multiplayer/src/matches/messages.rs @@ -49,6 +49,24 @@ pub enum GameMessage { user_id: i32, ping: rlnl::events::ingame::MapPing, }, + KillBonus { + user_id: i32, + shootee: u8, + shooter: u8, + }, + AssistBonus { + user_id: i32, + shootee: u8, + shooters: Vec, + }, + DestroyCubesBonus { + user_id: i32, + info: rlnl::events::ingame::DestroyedHealedCubesBonus, + }, + HealCubesBonus { + user_id: i32, + info: rlnl::events::ingame::DestroyedHealedCubesBonus, + }, BroadcastRlnl { user_id: i32, event: rlnl::event_code::NetworkEvent, @@ -87,6 +105,10 @@ impl GameMessage { Self::SelfDestruct { user_id, .. } => *user_id, Self::FlippingStarted { user_id, .. } => *user_id, Self::MapPing { user_id, .. } => *user_id, + Self::KillBonus { user_id, .. } => *user_id, + Self::AssistBonus { user_id, .. } => *user_id, + Self::DestroyCubesBonus { user_id, .. } => *user_id, + Self::HealCubesBonus { user_id, .. } => *user_id, Self::BroadcastRlnl { user_id, .. } => *user_id, Self::RebroadcastRlnl { skip_user_id, .. } => *skip_user_id, Self::Motion { user_id, .. } => *user_id,