From 7b7da8d59e8691e9d8030a6e1f95dec40e0e24a1 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Mon, 12 Jan 2026 18:01:23 -0500 Subject: [PATCH] Attempt to debounce kill bonus/counter --- .gitignore | 1 + rc_multiplayer/src/matches/generic.rs | 94 +++++++++++++------ .../src/matches/modes/battle_arena.rs | 14 ++- rc_multiplayer/src/matches/modes/mod.rs | 3 +- rc_multiplayer/src/matches/modes/pit.rs | 41 +++++--- .../src/matches/modes/team_death_match.rs | 14 ++- 6 files changed, 121 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 68f7d97..3b145a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +*/target *.zip # files generated by running servers diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 9569968..3f23021 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -73,6 +73,7 @@ impl UserState { pub(super) struct MachineState { pub(super) selected_weapon: WeaponInfo, pub(super) location: Location, + pub(super) is_alive: std::sync::Arc } impl MachineState { @@ -80,6 +81,7 @@ impl MachineState { Self { selected_weapon: WeaponInfo::new(), location: Location::new(), + is_alive: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)), } } } @@ -225,6 +227,35 @@ impl ConnectionMode { } } +struct UnclaimedStats { + kills: tokio::sync::Mutex>>, +} + +impl UnclaimedStats { + const DEBOUNCE_PERIOD: std::time::Duration = std::time::Duration::from_secs(2); + fn new() -> Self { + Self { + kills: tokio::sync::Mutex::new(std::collections::HashMap::new()), + } + } + + // returns true if is new + async fn debounce_kill(&self, attr: KillAttribution) -> bool { + let now = chrono::Utc::now(); + if let Some(time) = self.kills.lock().await.insert(attr, now) { + (now - time).to_std().unwrap_or_default() > Self::DEBOUNCE_PERIOD + } else { + true + } + } +} + +#[derive(Hash, Eq, PartialEq, Copy, Clone)] +struct KillAttribution { + killer: u8, + victim: u8, +} + pub(super) struct GenericGamemodeEngine { pub users: tokio::sync::RwLock>>, descriptors: std::collections::HashMap, @@ -241,6 +272,7 @@ pub(super) struct GenericGamemodeEngine { pub custom_logic_handler: L, //pub fake_users: std::collections::HashMap, pub fakes_handler: super::fake::Handler, + unclaimed: UnclaimedStats, } impl GenericGamemodeEngine { @@ -279,6 +311,7 @@ impl GenericGamemodeEngine { game_duration: std::time::Duration::from_secs((config.game_time_minutes as u64) * 60), custom_logic_handler: custom, fakes_handler, + unclaimed: UnclaimedStats::new(), } } @@ -872,14 +905,19 @@ impl GenericGamemodeEngine { 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.user_descriptor(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; + let was_killed = killed.machine.is_alive.swap(false, std::sync::atomic::Ordering::Relaxed); + if was_killed { + killed.counters.deaths.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + killed.machine.is_alive.store(false, 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; + //self.unclaimed.debounce_kill(KillAttribution { killer: killer_player, victim: remote_player }).await; + } } } } @@ -950,25 +988,27 @@ impl GenericGamemodeEngine { shooter: u8, ) { if self.custom_logic_handler.on_kill_bonus(self, shooter, shootee).await { - if let Some(to_reward) = self.users.read().await.get(&shooter) { - let to_reward_desc = self.user_descriptor(shooter).unwrap(); - to_reward_desc.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_desc.counters.get_generic_packet(shooter, rlnl::types::IngameStatId::Kill, None); - self.broadcast( - rlnl::event_code::NetworkEvent::UpdateGameStats, - literustlib::packet::Property::ReliableOrdered, - &data, - true, - ).await; + if self.unclaimed.debounce_kill(KillAttribution { killer: shooter, victim: shootee }).await { + if let Some(to_reward) = self.users.read().await.get(&shooter) { + let to_reward_desc = self.user_descriptor(shooter).unwrap(); + to_reward_desc.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_desc.counters.get_generic_packet(shooter, rlnl::types::IngameStatId::Kill, None); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + } } } } diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index f37fcea..ed15412 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -1130,8 +1130,18 @@ impl BattleArenaLogic { z: 10.0 * (player_team as f32) + 10.0, } }; - let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); - tokio::task::spawn(super::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); + if let Some(player_desc) = generic.user_descriptor(player_id) { + let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); + tokio::task::spawn(super::respawn_player_after( + respawn_timestamp, + connections, + spawn_point, + player_id, + player_desc.machine.is_alive.clone(), + )); + } else { + log::error!("Player {} cannot respawn because they are not in the game!?", player_id); + } } else { log::error!("Player {} cannot respawn because they are not in a team!?", player_id); } diff --git a/rc_multiplayer/src/matches/modes/mod.rs b/rc_multiplayer/src/matches/modes/mod.rs index b176606..a0b5ccb 100644 --- a/rc_multiplayer/src/matches/modes/mod.rs +++ b/rc_multiplayer/src/matches/modes/mod.rs @@ -16,7 +16,7 @@ pub use team_death_match::TeamDeathMatchLogic; mod trackers; -async fn respawn_player_after(after: chrono::DateTime, players: Vec, spawn: oj_rc_core::persist::config::Point, player_id: u8) { +async fn respawn_player_after(after: chrono::DateTime, players: Vec, spawn: oj_rc_core::persist::config::Point, player_id: u8, alive_flag: std::sync::Arc) { let sleep_dur = after.signed_duration_since(chrono::Utc::now()).to_std().expect("Respawn duration too long to sleep"); tokio::time::sleep(sleep_dur).await; let spawn_payload = rlnl::events::sync::SpawnPoint { @@ -36,4 +36,5 @@ async fn respawn_player_after(after: chrono::DateTime, players: Vec &player.connection, ).await); } + alive_flag.store(true, std::sync::atomic::Ordering::Relaxed); } diff --git a/rc_multiplayer/src/matches/modes/pit.rs b/rc_multiplayer/src/matches/modes/pit.rs index cd5c184..a8318c0 100644 --- a/rc_multiplayer/src/matches/modes/pit.rs +++ b/rc_multiplayer/src/matches/modes/pit.rs @@ -238,7 +238,12 @@ impl PitLogic { player_streak.store(0, std::sync::atomic::Ordering::Relaxed); } if let Some(player_streak) = self.player_tracking.streaks.get(&killer) { - player_streak.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + if let Some(victim_respawn) = self.player_tracking.respawns.get(&victim) { + let now = chrono::Utc::now().timestamp(); + if victim_respawn.load(std::sync::atomic::Ordering::Relaxed) <= now { + player_streak.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + } } self.do_leader_update(generic, killer, victim).await; } @@ -314,19 +319,27 @@ impl PitLogic { if let Some(player_respawn) = self.player_tracking.respawns.get(&player_id) { player_respawn.store(respawn_timestamp.timestamp(), std::sync::atomic::Ordering::Relaxed); } - let respawn_payload = rlnl::events::ingame::RespawnTime { - owner: player_id, - waiting_time: self.settings.respawn_time_seconds as i16, - }; - generic.broadcast( - rlnl::event_code::NetworkEvent::SetRespawnWaitingTime, - literustlib::packet::Property::ReliableOrdered, - &respawn_payload, - true - ).await; - let spawn_point = Self::choose_spawn_point(&generic.map_config, player_id).1; - let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); - tokio::task::spawn(super::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); + if let Some(player_desc) = generic.user_descriptor(player_id) { + let respawn_payload = rlnl::events::ingame::RespawnTime { + owner: player_id, + waiting_time: self.settings.respawn_time_seconds as i16, + }; + generic.broadcast( + rlnl::event_code::NetworkEvent::SetRespawnWaitingTime, + literustlib::packet::Property::ReliableOrdered, + &respawn_payload, + true + ).await; + let spawn_point = Self::choose_spawn_point(&generic.map_config, player_id).1; + let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); + tokio::task::spawn(super::respawn_player_after( + respawn_timestamp, + connections, + spawn_point, + player_id, + player_desc.machine.is_alive.clone(), + )); + } } } diff --git a/rc_multiplayer/src/matches/modes/team_death_match.rs b/rc_multiplayer/src/matches/modes/team_death_match.rs index a8ca2c1..4d1caf9 100644 --- a/rc_multiplayer/src/matches/modes/team_death_match.rs +++ b/rc_multiplayer/src/matches/modes/team_death_match.rs @@ -297,8 +297,18 @@ impl TeamDeathMatchLogic { z: 10.0 * (player_team as f32) + 10.0, } }; - let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); - tokio::task::spawn(super::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); + if let Some(player_desc) = generic.user_descriptor(player_id) { + let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); + tokio::task::spawn(super::respawn_player_after( + respawn_timestamp, + connections, + spawn_point, + player_id, + player_desc.machine.is_alive.clone(), + )); + } else { + log::error!("Player {} cannot respawn because they are not in the game!?", player_id); + } } else { log::error!("Player {} cannot respawn because they are not connected!?", player_id); }