From 6b40eb938ca9c7f693ddce31039774049717017c Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sat, 6 Sep 2025 19:13:47 -0400 Subject: [PATCH] Add basic surrendering for #32 --- rc_core/src/persist/combat.rs | 2 +- rc_multiplayer/src/events/mod.rs | 6 + rc_multiplayer/src/matches/aggregate.rs | 2 + rc_multiplayer/src/matches/generic.rs | 11 + .../src/matches/modes/battle_arena.rs | 86 ++++--- .../src/matches/modes/elimination.rs | 8 +- rc_multiplayer/src/matches/modes/mod.rs | 2 + .../src/matches/modes/trackers/mod.rs | 2 + .../src/matches/modes/trackers/surrender.rs | 228 ++++++++++++++++++ 9 files changed, 314 insertions(+), 33 deletions(-) create mode 100644 rc_multiplayer/src/matches/modes/trackers/mod.rs create mode 100644 rc_multiplayer/src/matches/modes/trackers/surrender.rs diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 868bd59..9d59ac3 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -248,7 +248,7 @@ fn default_game_modes() -> GameModes { respawn_heal_duration: 10.0, respawn_full_heal_duration: 0.5, kill_limit: 0, - game_time_m: 5, + game_time_m: 20, }, elimination: GameMode { respawn_heal_duration: 10.0, diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index e9ff639..28dbe95 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -187,6 +187,11 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa {literustlib::packet::Property::ReliableOrdered as u8}, rlnl::events::ingame::InitiateSurrender, >::handler(init_ctx)) + .add(crate::handlers::GamemodeSpecific::< + {rlnl::event_code::NetworkEvent::SurrenderVoteCast as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::SurrenderVoteCast, + >::handler(init_ctx)) .add(crate::handlers::GamemodeSpecific::< {rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest as i16}, {literustlib::packet::Property::ReliableOrdered as u8}, @@ -231,6 +236,7 @@ mod _broadcast_impls { impl Broadcastable for rlnl::events::ingame::MapPing {} impl Broadcastable for rlnl::events::ingame::DamagedByEnemyShield {} impl Broadcastable for rlnl::events::ingame::InitiateSurrender {} + impl Broadcastable for rlnl::events::ingame::SurrenderVoteCast {} impl Broadcastable for rlnl::events::ingame::AwardProtoniumDestroyedCubes {} impl Broadcastable for rlnl::events::sync::UpdateGameModeSettings {} diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index 879ffa4..837d2ba 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -80,6 +80,7 @@ impl GameMatches { let engine = super::GenericGamemodeEngine::new( game_info, map_config, + &self.mode_configs.elimination, players, inner, fakes_handler, @@ -101,6 +102,7 @@ impl GameMatches { let engine = super::GenericGamemodeEngine::new( game_info, map_config, + &self.mode_configs.battle_arena, players, inner, fakes_handler, diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 183e4d1..872a9e8 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -197,6 +197,7 @@ pub(super) struct GenericGamemodeEngine { pub game_start: std::sync::atomic::AtomicI64, pub map_config: std::sync::Arc, pub game_descriptor: oj_rc_core::persist::user::GameDescriptor, + pub game_duration: std::time::Duration, pub players_info: std::sync::Arc>, pub custom_logic_handler: L, pub fake_users: std::collections::HashMap, @@ -210,6 +211,7 @@ impl GenericGamemodeEngine { pub fn new( game: oj_rc_core::persist::user::GameDescriptor, map: oj_rc_core::persist::config::MapConfig, + config: &oj_rc_core::data::game_mode::GameModeConfig, players: Vec, custom: L, fakes_handler: super::fake::Handler, @@ -226,6 +228,7 @@ impl GenericGamemodeEngine { game_start: std::sync::atomic::AtomicI64::new(-1), map_config: std::sync::Arc::new(map), game_descriptor: game, + game_duration: std::time::Duration::from_secs((config.game_time_minutes as u64) * 60), players_info: std::sync::Arc::new(players), custom_logic_handler: custom, fake_users, @@ -306,6 +309,14 @@ impl GenericGamemodeEngine { } } + pub(super) fn elapsed_game_time(&self) -> f32 { + let game_start = self.game_start.load(std::sync::atomic::Ordering::Relaxed); + //let game_end = (game_start as u64) + self.game_duration.as_secs(); + let duration = self.game_duration.as_secs(); + let now = chrono::Utc::now().timestamp(); + ((now - game_start) as f32) / duration as f32 + } + /*pub(super) async fn send_to_player(&self, player_id: u8, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T) { if let Some(player) = self.users.read().await.get(&player_id) { crate::events::log_lnl_send_failure(player.connection.rlnl().send_data( diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index 74b8ef6..dd35942 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -1,4 +1,4 @@ -use crate::matches::CustomGameLogic; +use crate::matches::{modes::trackers::SurrenderGameTracker, CustomGameLogic}; struct PlayerTracker { connected: tokio::sync::Mutex>>, // team -> set of player_id @@ -69,6 +69,14 @@ impl PlayerTracker { } first_remaining_team } + + async fn players_on_team(&self, team: u8) -> Vec { + if let Some(members) = self.connected.lock().await.get(&team) { + members.iter().map(|x| *x).collect() + } else { + Vec::default() + } + } } struct PointInfo { @@ -517,10 +525,10 @@ enum WinMode { BaseFull, OutOfTime, OutOfPlayers, + Surrender, } pub struct BattleArenaLogic { - game_duration: std::time::Duration, game_end: std::sync::atomic::AtomicI64, respawn_full_heal_duration: f32, respawn_heal_duration: f32, @@ -528,6 +536,7 @@ pub struct BattleArenaLogic { player_tracking: PlayerTracker, capture_tracking: PointTracker, base_tracking: BaseTracker, + surrender_tracking: super::trackers::SurrenderGameTracker, //cube_parser: std::sync::Arc, crystals: Vec, config: oj_rc_core::data::battle_arena_config::BattleArenaData, @@ -543,13 +552,13 @@ impl BattleArenaLogic { let cube_parser = parsers.locations_of(); let crystals = cube_parser.locations_of_by_distance_to_first(&mut std::io::Cursor::new(&ba_config.base_machine_map), Self::CRYSTAL_ID, Self::CLASP_ID); Self { - game_duration: dur, respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_heal_duration: config.respawn_heal_duration, game_end: std::sync::atomic::AtomicI64::new(fake_end), timer_task: tokio::sync::Mutex::new(None), player_tracking: PlayerTracker::new(), capture_tracking: PointTracker::new(map.capture_points.iter().map(|(_, speed)| *speed)), + surrender_tracking: super::trackers::SurrenderGameTracker::new(), base_tracking: BaseTracker::new(map.bases.keys(), &crystals), //cube_parser, //ba_base: teambase, @@ -622,8 +631,9 @@ impl BattleArenaLogic { async fn do_win(&self, winning_team: u8, ty: WinMode, generic: &crate::matches::GenericGamemodeEngine) { generic.game_done(); let end_reason = match ty { - WinMode::BaseFull - | WinMode::OutOfPlayers => rlnl::types::GameEndReason::BaseDestroyed, + WinMode::BaseFull => rlnl::types::GameEndReason::BaseDestroyed, + WinMode::OutOfPlayers => rlnl::types::GameEndReason::OneTeamRemaining, + WinMode::Surrender => rlnl::types::GameEndReason::Surrendered, WinMode::OutOfTime => rlnl::types::GameEndReason::TimeOut, }; let payload = rlnl::events::ingame::GameLoseWin { @@ -634,7 +644,8 @@ impl BattleArenaLogic { let is_winner = player.descriptor.team == winning_team as i32; let net_event = match ty { WinMode::BaseFull - | WinMode::OutOfPlayers => { + | WinMode::OutOfPlayers + | WinMode::Surrender => { if is_winner { rlnl::event_code::NetworkEvent::GameWonBaseDestroyed } else { rlnl::event_code::NetworkEvent::GameLostBaseDestroyed } }, WinMode::OutOfTime => { @@ -902,7 +913,7 @@ impl CustomGameLogic for BattleArenaLogic { Some(crate::matches::RlnlPacket { event: rlnl::event_code::NetworkEvent::CurrentGameTime, property: literustlib::packet::Property::ReliableOrdered, - data: Box::new(rlnl::events::GameTime(self.game_duration.as_millis() as f32 / 1000.0)), + data: Box::new(rlnl::events::GameTime(generic.game_duration.as_millis() as f32 / 1000.0)), }), // SyncTeamBaseCubes // TODO ??? @@ -978,7 +989,7 @@ impl CustomGameLogic for BattleArenaLogic { senders.push((conn.connection.clone(), conn.state.clone())); } drop(read_lock); - let game_end = game_start + self.game_duration; + let game_end = game_start + generic.game_duration; let extra_packets = Vec::default(); let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, extra_packets, Vec::default()); let mut timer_lock = self.timer_task.lock().await; @@ -988,6 +999,12 @@ impl CustomGameLogic for BattleArenaLogic { } *timer_lock = Some(new_timer_task); self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed); + generic.broadcast( + rlnl::event_code::NetworkEvent::SetSurrenderTimes, + literustlib::packet::Property::ReliableOrdered, + &SurrenderGameTracker::surrender_times(), + false, + ).await; true } @@ -1013,21 +1030,6 @@ impl CustomGameLogic for BattleArenaLogic { true } }, - /*(rlnl::event_code::NetworkEvent::DamageCubeEffectOnly, Some(data)) => { - let maybe_cube_dmg = ::downcast_ref::(data.as_ref()); - if let Some(cube_damage) = maybe_cube_dmg { - if matches!(cube_damage.target_type, rlnl::types::TargetType::TeamBase) { - log::info!("Got team base DamageCubeEffectOnly for pos ({}, {}, {})", cube_damage.hit_cube.x, cube_damage.hit_cube.y, cube_damage.hit_cube.z); - false - } else { - log::info!("Got non-base DamageCubeEffectOnly {:?}", cube_damage.target_type); - true - } - } else { - log::warn!("Got DamageCubeEffectOnly event with bad serialization type"); - true - } - },*/ _ => true } } @@ -1099,7 +1101,7 @@ impl CustomGameLogic for BattleArenaLogic { if let Some(owned_points) = tick_info.owned.get(base_id) { if let Some(tracked_base) = self.base_tracking.bases.get(base_id) { let one_tick = (self.crystals.len() as f32) - * ((PointTracker::TICK_MS as f32) / (self.game_duration.as_millis() as f32)) + * ((PointTracker::TICK_MS as f32) / (generic.game_duration.as_millis() as f32)) * ((self.base_tracking.bases.len() as f32) / (self.capture_tracking.points.len() as f32)); let increment = tick_info.delta as f32 * (*owned_points as f32) * one_tick; let old_float_index = tracked_base.cube_index.fetch_add(increment, std::sync::atomic::Ordering::SeqCst); @@ -1164,11 +1166,14 @@ impl CustomGameLogic for BattleArenaLogic { } } } + + // handle surrender vote tick + self.surrender_tracking.tick(generic).await; } true } - async fn on_custom(&self, generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box) { + async fn on_custom(&self, generic: &crate::matches::GenericGamemodeEngine, user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box) { match (event, property) { (rlnl::event_code::NetworkEvent::SendDamagedByEnemyShield, literustlib::packet::Property::ReliableOrdered) => { generic.broadcast( @@ -1180,8 +1185,35 @@ impl CustomGameLogic for BattleArenaLogic { }, (rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => { // TODO - log::warn!("Ignoring SurrenderRequest because it's not implemented (yet)"); - } + let maybe_init_surr = ::downcast_ref::(data.as_ref()); + if let Some(init_surr) = maybe_init_surr { + if let Some(team) = self.player_tracking.team(init_surr.player_id).await { + let team_members = self.player_tracking.players_on_team(team).await; + let result = self.surrender_tracking.request_new(team, init_surr.player_id, team_members.into_iter(), generic).await; + if matches!(result, super::trackers::SurrenderVoteResult::Succeeded) { + let winning_team = if team == 0 { 1 } else { 0 }; + self.do_win(winning_team, WinMode::Surrender, generic).await; + } + } + } else { + log::warn!("Bad SurrenderRequest data"); + } + }, + (rlnl::event_code::NetworkEvent::SurrenderVoteCast, literustlib::packet::Property::ReliableOrdered) => { + if let Some(player_id) = generic.user_key_by_user_id(user_id).await { + if let Some(team) = self.player_tracking.team(player_id).await { + let maybe_vote = ::downcast_ref::(data.as_ref()); + if let Some(vote) = maybe_vote { + let result = self.surrender_tracking.vote(team, player_id, vote.surrender != 0, generic).await; + if matches!(result, super::trackers::SurrenderVoteResult::Succeeded) { + let winning_team = if team == 0 { 1 } else { 0 }; + self.do_win(winning_team, WinMode::Surrender, generic).await; + } + } + } + } + + }, (rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest, literustlib::packet::Property::ReliableOrdered) => { let maybe_crystal_bonus = ::downcast_ref::(data.as_ref()); if let Some(crystal_destroyed) = maybe_crystal_bonus { diff --git a/rc_multiplayer/src/matches/modes/elimination.rs b/rc_multiplayer/src/matches/modes/elimination.rs index d09b029..a55da32 100644 --- a/rc_multiplayer/src/matches/modes/elimination.rs +++ b/rc_multiplayer/src/matches/modes/elimination.rs @@ -339,7 +339,6 @@ impl BaseTracker { pub struct EliminationLogic { tracked: PlayerTracker, bases: BaseTracker, - game_duration: std::time::Duration, respawn_full_heal_duration: f32, respawn_heal_duration: f32, game_end: std::sync::atomic::AtomicI64, @@ -360,7 +359,6 @@ impl EliminationLogic { last_tick: std::sync::atomic::AtomicI64::new(i64::MIN), is_baseless: map.bases.is_empty(), }, - game_duration: dur, respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_heal_duration: config.respawn_heal_duration, game_end: std::sync::atomic::AtomicI64::new(fake_end), @@ -478,7 +476,7 @@ impl CustomGameLogic for EliminationLogic { } } - async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { + async fn extra_sync_events(&self, generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { vec![ crate::matches::RlnlPacket { event: rlnl::event_code::NetworkEvent::GameModeSettings, @@ -491,7 +489,7 @@ impl CustomGameLogic for EliminationLogic { crate::matches::RlnlPacket { event: rlnl::event_code::NetworkEvent::CurrentGameTime, property: literustlib::packet::Property::ReliableOrdered, - data: Box::new(rlnl::events::GameTime(self.game_duration.as_millis() as f32 / 1000.0)), + data: Box::new(rlnl::events::GameTime(generic.game_duration.as_millis() as f32 / 1000.0)), }, ] } @@ -503,7 +501,7 @@ impl CustomGameLogic for EliminationLogic { senders.push((conn.connection.clone(), conn.state.clone())); } drop(read_lock); - let game_end = game_start + self.game_duration; + let game_end = game_start + generic.game_duration; let teams = self.bases.teams(); let extra_packets = teams.iter().map(|team| crate::matches::RlnlPacket { event: rlnl::event_code::NetworkEvent::TeamBaseInitialise, diff --git a/rc_multiplayer/src/matches/modes/mod.rs b/rc_multiplayer/src/matches/modes/mod.rs index ecbd8ea..7984302 100644 --- a/rc_multiplayer/src/matches/modes/mod.rs +++ b/rc_multiplayer/src/matches/modes/mod.rs @@ -7,3 +7,5 @@ pub use elimination::EliminationLogic; mod battle_arena; pub use battle_arena::BattleArenaLogic; + +mod trackers; diff --git a/rc_multiplayer/src/matches/modes/trackers/mod.rs b/rc_multiplayer/src/matches/modes/trackers/mod.rs new file mode 100644 index 0000000..4ee6f05 --- /dev/null +++ b/rc_multiplayer/src/matches/modes/trackers/mod.rs @@ -0,0 +1,2 @@ +mod surrender; +pub use surrender::{SurrenderGameTracker, SurrenderVoteResult}; diff --git a/rc_multiplayer/src/matches/modes/trackers/surrender.rs b/rc_multiplayer/src/matches/modes/trackers/surrender.rs new file mode 100644 index 0000000..a99d793 --- /dev/null +++ b/rc_multiplayer/src/matches/modes/trackers/surrender.rs @@ -0,0 +1,228 @@ +#[repr(u8)] +#[derive(Copy, Clone)] +enum VoteStatus { + NoEntry = 0, + Yes = 1, + No = 2, +} + +impl VoteStatus { + fn from_u8(num: u8) -> Self { + match num { + 0 => Self::NoEntry, + 1 => Self::Yes, + 2 => Self::No, + _ => panic!("Invalid vote status variant {}", num), + } + } + + #[inline] + fn to_u8(self) -> u8 { + self as u8 + } +} + +struct CurrentVoteInfo { + current_votes: rlnl::events::ingame::CurrentSurrenderVotes, + declined: Option, + succeeded: bool, +} + +struct SurrenderTracker { + ends_at: chrono::DateTime, + players: std::collections::HashMap, +} + +impl SurrenderTracker { + const DURATION: std::time::Duration = std::time::Duration::from_secs(30); + + fn new(players: impl std::iter::Iterator, initiator: u8) -> Self { + let now = chrono::Utc::now(); + Self { + ends_at: now + Self::DURATION, + players: players + .map(|player_id| (player_id, if initiator == player_id { + std::sync::atomic::AtomicU8::new(VoteStatus::Yes.to_u8()) + } else { + std::sync::atomic::AtomicU8::new(VoteStatus::NoEntry.to_u8()) + })) + .collect(), + } + } + + fn current_votes(&self, player_id: Option, game_time_elapsed: f32) -> CurrentVoteInfo { + let mut votes = Vec::with_capacity(self.players.len()); + let mut total_no_votes = 0; + let mut total_yes_votes = 0; + for voter in self.players.values() { + let vote = VoteStatus::from_u8(voter.load(std::sync::atomic::Ordering::Relaxed)); + match vote { + VoteStatus::NoEntry => {}, + VoteStatus::Yes => { + total_yes_votes += 1; + votes.push(1); + }, + VoteStatus::No => { + total_no_votes += 1; + votes.push(0); + }, + } + } + let threshold = self.players.len() / 2; + let declined = if total_no_votes > threshold { + Some(rlnl::events::ingame::SurrenderDeclined { + surrendering_player_id: player_id.or_else(|| self.players.keys().next().map(|x| *x)).unwrap_or(0) as i32, + game_time_elapsed, + }) + } else { + None + }; + CurrentVoteInfo { + current_votes: rlnl::events::ingame::CurrentSurrenderVotes { + players_on_team: self.players.len() as i32, + num_votes: votes.len() as _, + votes, + }, + declined, + succeeded: total_yes_votes > threshold, + } + } + + fn vote(&self, player_id: u8, is_surrender: bool, game_time_elapsed: f32) -> Option { + if let Some(voter) = self.players.get(&player_id) { + let vote = if is_surrender { VoteStatus::Yes } else { VoteStatus::No }; + voter.store(vote.to_u8(), std::sync::atomic::Ordering::Relaxed); + Some(self.current_votes(Some(player_id), game_time_elapsed)) + } else { + None + } + } + + fn tick(&self, game_time_elapsed: f32) -> Option { + let now = chrono::Utc::now(); + if now > self.ends_at { + Some(rlnl::events::ingame::SurrenderDeclined { + surrendering_player_id: 0, + game_time_elapsed, + }) + } else { + None + } + } +} + +pub struct SurrenderGameTracker { + surrenders: tokio::sync::RwLock>, +} + +pub enum SurrenderVoteResult { + Succeeded, + Declined, + NoChange, +} + +impl SurrenderGameTracker { + pub fn new() -> Self { + Self { + surrenders: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(2)), + } + } + + pub async fn vote(&self, team: u8, player_id: u8, is_surrender: bool, generic: &crate::matches::GenericGamemodeEngine) -> SurrenderVoteResult { + let mut result = SurrenderVoteResult::NoChange; + if let Some(tracker) = self.surrenders.read().await.get(&team) { + if let Some(vote_info) = tracker.vote(player_id, is_surrender, generic.elapsed_game_time()) { + if vote_info.succeeded { + result = SurrenderVoteResult::Succeeded; + } else if vote_info.declined.is_some() { + result = SurrenderVoteResult::Declined; + } + Self::handle_vote_info(tracker, generic, vote_info, false).await; + } + } + match result { + SurrenderVoteResult::Succeeded | SurrenderVoteResult::Declined => { + self.surrenders.write().await.remove(&team); + }, + _ => {}, + } + return result; + } + + pub async fn request_new(&self, team: u8, initiator: u8, players: impl std::iter::Iterator, generic: &crate::matches::GenericGamemodeEngine) -> SurrenderVoteResult { + log::info!("Surrender vote for team {} initiated by player {} in game {}", team, initiator, generic.game_guid()); + let new_tracker = SurrenderTracker::new(players, initiator); + let vote_info = new_tracker.current_votes(None, generic.elapsed_game_time()); + let result = if vote_info.succeeded { + SurrenderVoteResult::Succeeded + } else { + SurrenderVoteResult::NoChange + }; + Self::handle_vote_info(&new_tracker, generic, vote_info, true).await; + self.surrenders.write().await.insert(team, new_tracker); + result + } + + pub const fn surrender_times() -> rlnl::events::ingame::SurrenderTimes { + // TODO make this configurable + rlnl::events::ingame::SurrenderTimes { + player_cooldown_seconds: 30, + team_cooldown_seconds: 30, + surrender_timeout_seconds: 10, + initial_surrender_timeout_seconds: 60, + } + } + + pub async fn tick(&self, generic: &crate::matches::GenericGamemodeEngine) { + let mut keys_to_remove = Vec::default(); + let generic_user_lock = generic.users.read().await; + for (team, tracker) in self.surrenders.read().await.iter() { + if let Some(declined) = tracker.tick(generic.elapsed_game_time()) { + log::info!("Surrender vote for team {} expired in game {}", team, generic.game_guid()); + for team_member in tracker.players.keys() { + if let Some(conn) = generic_user_lock.get(team_member) { + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &declined, + rlnl::event_code::NetworkEvent::SurrenderDeclined, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection, + ).await); + } + } + keys_to_remove.push(*team); + } + } + if !keys_to_remove.is_empty() { + let mut tracker_write_lock = self.surrenders.write().await; + for to_remove in keys_to_remove { + tracker_write_lock.remove(&to_remove); + } + } + } + + async fn handle_vote_info(tracker: &SurrenderTracker, generic: &crate::matches::GenericGamemodeEngine, vote_info: CurrentVoteInfo, is_new: bool) { + let generic_user_lock = generic.users.read().await; + for team_member in tracker.players.keys() { + if let Some(conn) = generic_user_lock.get(team_member) { + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &vote_info.current_votes, + if is_new { rlnl::event_code::NetworkEvent::SurrenderVoteStarted } else { rlnl::event_code::NetworkEvent::CurrentSurrenderVotes }, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection, + ).await); + } + } + if let Some(declined) = vote_info.declined { + for team_member in tracker.players.keys() { + if let Some(conn) = generic_user_lock.get(team_member) { + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &declined, + rlnl::event_code::NetworkEvent::SurrenderDeclined, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection, + ).await); + } + } + } + } +}