diff --git a/rc_core/src/persist/user/lobby.rs b/rc_core/src/persist/user/lobby.rs index 20505f0..240099d 100644 --- a/rc_core/src/persist/user/lobby.rs +++ b/rc_core/src/persist/user/lobby.rs @@ -2,25 +2,6 @@ use serde::{Serialize, Deserialize}; use super::account_json::UserData; -pub enum TeamChooser { - /// Alternating between team 0 and team 1 - Alternating, - /// All players will be put on the specified team - AllOn(u8), - /// Each player will be put on their own team (like in Pit mode) - OnePer, -} - -impl TeamChooser { - pub fn team(&self, index: usize) -> i32 { - match self { - Self::Alternating => (index % 2) as i32, - Self::AllOn(team) => *team as i32, - Self::OnePer => index as i32, - } - } -} - fn fake_impl_to_db(client_emu: &crate::persist::config::ClientEmulator) -> oj_rc_database::schema::multiplayer_game_player::ClientType { match client_emu { crate::persist::config::ClientEmulator::Experiment => oj_rc_database::schema::multiplayer_game_player::ClientType::ServerExperimental, @@ -45,10 +26,10 @@ impl super::LobbyUser for UserData { }) } - async fn team_chooser(&self, game: &super::GameDescriptor) -> TeamChooser { + async fn team_chooser(&self, game: &super::GameDescriptor) -> super::StandardTeamChooser { match game.mode { - crate::data::game_mode::GameMode::Pit => TeamChooser::OnePer, - _ => TeamChooser::Alternating, + crate::data::game_mode::GameMode::Pit => super::StandardTeamChooser::OnePer, + _ => super::StandardTeamChooser::alternating(), } } @@ -59,7 +40,7 @@ impl super::LobbyUser for UserData { factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, - chooser: &TeamChooser, + chooser: &dyn super::TeamChooser, missing_players: usize, ) -> Result { let now = chrono::Utc::now().timestamp(); @@ -76,8 +57,8 @@ impl super::LobbyUser for UserData { oj_rc_database::schema::multiplayer_game::GameType::Standard }; - let forced_fake_players = self.generate_forced_fake_players_data(guid, &players, factory, cpu_counter, weapon_lister, chooser).await?; - let filler_players = self.generate_filler_players_data(guid, &players, factory, cpu_counter, weapon_lister, chooser, missing_players).await?; + let forced_fake_players = self.generate_forced_fake_players_data(&game.guid, &players, factory, cpu_counter, weapon_lister, chooser).await?; + let filler_players = self.generate_filler_players_data(&game.guid, factory, cpu_counter, weapon_lister, chooser, missing_players, players.len() + forced_fake_players.len()).await?; let game_dbo = oj_rc_database::schema::multiplayer_game::ActiveModel { id: oj_rc_database::sea_orm::ActiveValue::NotSet, @@ -102,6 +83,17 @@ impl super::LobbyUser for UserData { let players_len = players.len(); let forced_fake_players_len = forced_fake_players.len(); + let mut group_map = std::collections::HashMap::new(); + let mut group_num = 1; + for player in players.iter() { + if let Some(group) = &player.group { + if !group_map.contains_key(group) { + group_map.insert(group.to_owned(), group_num); + group_num += 1; + } + } + } + let players: Vec = players.into_iter() .enumerate() .map(|(i, player)| { @@ -112,7 +104,7 @@ impl super::LobbyUser for UserData { creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _), team: oj_rc_database::sea_orm::ActiveValue::Set(player.team), - group: oj_rc_database::sea_orm::ActiveValue::Set(player.group), + group: oj_rc_database::sea_orm::ActiveValue::Set(player.group.as_ref().and_then(|x| group_map.get(x)).copied()), is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false), public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id), display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name), @@ -220,7 +212,7 @@ impl super::LobbyUser for UserData { creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _), team: oj_rc_database::sea_orm::ActiveValue::Set(player.team), - group: oj_rc_database::sea_orm::ActiveValue::Set(player.group), + group: oj_rc_database::sea_orm::ActiveValue::Set(None), is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false), public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id), display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name), diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 1c2f265..3d59c7e 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -18,13 +18,14 @@ pub use intercom::generate_token as generate_intercom_token; mod multiplayer; mod lobby; -pub use lobby::TeamChooser; mod common; mod chat; mod social; mod singleplayer; mod factory; mod userless; +mod team; +pub use team::{TeamChooser, StandardTeamChooser}; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; diff --git a/rc_core/src/persist/user/multiplayer.rs b/rc_core/src/persist/user/multiplayer.rs index 6c663f7..566cd0e 100644 --- a/rc_core/src/persist/user/multiplayer.rs +++ b/rc_core/src/persist/user/multiplayer.rs @@ -12,18 +12,24 @@ impl UserData { #[allow(clippy::too_many_arguments)] pub(super) async fn generate_fake_players_data( &self, - _guid: i64, - real_players: &[super::PlayerLobbyDescriptor], + guid_str: &str, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, - chooser: &super::TeamChooser, + chooser: &dyn super::TeamChooser, fake_players: &[crate::persist::config::FakePlayer], + total_offset: usize, ) -> Result, polariton_server::operations::SimpleOpError> { let mut fakes = Vec::with_capacity(fake_players.len()); - let mut fake_i = real_players.len(); - for fake in fake_players.iter() { + for (i, fake) in fake_players.iter().enumerate() { let vehicle = self.resolve_vehicle(&fake.vehicle, factory, weapon_lister, cpu_counter).await?; + let fake_lobby_desc = super::PlayerLobbyDescriptor { + user_id: -1, + team: -1, + public_id: fake.vehicle.username.clone(), + display_name: fake.vehicle.username.clone(), + group: None, + }; let out = ( crate::data::player_data::PlayerData { name: fake.vehicle.username.clone(), @@ -33,12 +39,7 @@ impl UserData { robot_name: vehicle.robot_name, robot_map: vehicle.robot_map, group: None, - team: fake.team.map(|t| t as i32) - .unwrap_or_else(|| { - let assigned_team = chooser.team(fake_i); - fake_i += 1; - assigned_team - }), + team: chooser.choose_team(guid_str, total_offset + i, &fake_lobby_desc), has_premium: true, robot_uuid: vehicle.robot_uuid, cpu: vehicle.cpu, @@ -61,43 +62,44 @@ impl UserData { #[allow(clippy::too_many_arguments)] pub(super) async fn generate_forced_fake_players_data( &self, - guid: i64, + guid_str: &str, real_players: &[super::PlayerLobbyDescriptor], factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, - chooser: &super::TeamChooser, + chooser: &dyn super::TeamChooser, ) -> Result, polariton_server::operations::SimpleOpError> { + let offset = real_players.len(); self.generate_fake_players_data( - guid, - real_players, + guid_str, factory, cpu_counter, weapon_lister, chooser, &self.fake_players, + offset, ).await } #[allow(clippy::too_many_arguments)] pub(super) async fn generate_filler_players_data( &self, - guid: i64, - real_players: &[super::PlayerLobbyDescriptor], + guid_str: &str, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, - chooser: &super::TeamChooser, + chooser: &dyn super::TeamChooser, count: usize, + total_offset: usize, ) -> Result, polariton_server::operations::SimpleOpError> { self.generate_fake_players_data( - guid, - real_players, + guid_str, factory, cpu_counter, weapon_lister, chooser, &self.filler_players[0..count], + total_offset ).await } } diff --git a/rc_core/src/persist/user/team.rs b/rc_core/src/persist/user/team.rs new file mode 100644 index 0000000..3d7090a --- /dev/null +++ b/rc_core/src/persist/user/team.rs @@ -0,0 +1,116 @@ +pub trait TeamChooser: Send + Sync { + fn choose_team(&self, game: &str, index: usize, player: &super::PlayerLobbyDescriptor) -> i32; +} + +pub enum StandardTeamChooser { + /// Alternating between team 0 and team 1 + Alternating(TwoTeamPartyAware), + /// All players will be put on the specified team + AllOn(u8), + /// Each player will be put on their own team (like in Pit mode) + OnePer, + //Custom(Box), +} + +impl StandardTeamChooser { + pub fn alternating() -> Self { + Self::Alternating(TwoTeamPartyAware::new()) + } +} + +impl TeamChooser for StandardTeamChooser { + fn choose_team(&self, game: &str, index: usize, player: &super::PlayerLobbyDescriptor) -> i32 { + match self { + Self::Alternating(t) => t.choose_team(game, index, player), + Self::AllOn(team) => *team as i32, + Self::OnePer => index as i32, + //Self::Custom(t) => t.choose_team(game, index, player), + } + } +} + +pub struct TwoTeamPartyAware { + tracker: std::sync::Mutex>, +} + +struct BalanceInfo { + overall: i64, + /// platoon ID -> team + platoons: std::collections::HashMap, + created_at: chrono::DateTime, +} + +impl TwoTeamPartyAware { + pub fn new() -> Self { + Self { + tracker: std::sync::Mutex::new(std::collections::HashMap::new()), + } + } + + /// Manually remove stale team infos + fn do_cleanup(tracker: &mut std::collections::HashMap) { + let max_ttl = chrono::TimeDelta::from_std(std::time::Duration::from_secs(10)).unwrap(); + let now = chrono::Utc::now(); + tracker.retain(|_, bal_info| (now - bal_info.created_at) < max_ttl); + } +} + +impl TeamChooser for TwoTeamPartyAware { + fn choose_team(&self, game: &str, _index: usize, player: &super::PlayerLobbyDescriptor) -> i32 { + let mut lock = self.tracker.lock().unwrap(); + // the algorithm is basically: + // if player is in platoon AND platoon is known platoon: + // use platoon's team assignment + // else if overall > 0: + // choose team 1 for player, remember team assignment for all future platoon members + // overall -= 1 + // else: + // choose team 0 for player, remember team assignment for all future platoon members + // overall += 1 + // + // this roughly balances player count between the two teams + // while ensuring players on the same platoon are put on the same team + if let Some(bal_info) = lock.get_mut(game) { + if let Some(platoon_id) = &player.group { + if let Some(&platoon_team) = bal_info.platoons.get(platoon_id) { + if platoon_team == 0 { + bal_info.overall += 1; + } else { + bal_info.overall -= 1; + } + return platoon_team as i32; + } + } + let chosen_team = if bal_info.overall > 0 { + bal_info.overall -= 1; + 1 + } else { + bal_info.overall += 1; + 0 + }; + if let Some(platoon_id) = &player.group { + bal_info.platoons.insert(platoon_id.to_owned(), chosen_team); + } + return chosen_team as i32; + } else { + Self::do_cleanup(&mut lock); + let init_info = if let Some(platoon) = &player.group { + let mut platoon_teams = std::collections::HashMap::new(); + platoon_teams.insert(platoon.to_owned(), 0); + BalanceInfo { + overall: 1, + platoons: platoon_teams, + created_at: chrono::Utc::now(), + } + } else { + BalanceInfo { + overall: 1, + platoons: std::collections::HashMap::new(), + created_at: chrono::Utc::now(), + } + }; + lock.insert(game.to_owned(), init_info); + 0 + } + } +} diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index d844139..009e3f9 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -279,9 +279,9 @@ pub enum UserRole { pub trait LobbyUser { fn user_id(&self) -> i32; async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result; - async fn team_chooser(&self, game: &GameDescriptor) -> super::TeamChooser; + async fn team_chooser(&self, game: &GameDescriptor) -> super::StandardTeamChooser; #[allow(clippy::too_many_arguments)] - async fn start_game(&self, game: GameDescriptor, players: Vec, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, team_chooser: &super::TeamChooser, missing_players: usize) -> Result; + async fn start_game(&self, game: GameDescriptor, players: Vec, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, team_chooser: &dyn super::TeamChooser, missing_players: usize) -> Result; #[allow(clippy::too_many_arguments)] async fn start_custom_game(&self, game: GameDescriptor, players: Vec) -> Result<(), polariton_server::operations::SimpleOpError>; } @@ -337,7 +337,7 @@ pub struct PlayerLobbyDescriptor { pub team: i32, pub public_id: String, pub display_name: String, - pub group: Option, + pub group: Option, } #[derive(Clone)] diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index 0f371d7..cc1dc60 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -1,5 +1,7 @@ use std::{collections::HashMap, hash::Hash}; +use oj_rc_core::persist::user::TeamChooser; + #[derive(Clone, Copy)] pub enum GamemodeChangeStrategy { Upgrade, // move enqueued players into newer gamemode @@ -55,6 +57,41 @@ impl QueueKey { } } +struct Queue { + users: Vec, + platoons: HashMap, +} + +struct Platoon { + total: u8, + members: Vec, +} + +/// A QueueUser wrapper for platoon members +struct PlatoonUser(QueueUser); + +impl PlatoonUser { + fn from_queue_user(qu: QueueUser) -> Self { + Self(qu) + } + + fn clone_for_queue(&self) -> QueueUser { + QueueUser { + emitter: self.0.emitter.clone(), + player: self.0.player.clone(), + user_id: self.0.user_id, + enqueued_at: self.0.enqueued_at, + user: self.0.user.clone(), + } + } +} + +pub struct PlatoonInfo { + pub total: u8, + pub platoon_id: String, + pub is_leader: bool, +} + struct QueueUser { emitter: polariton_server::events::EventEmitter, player: oj_rc_core::data::player_data::PlayerData, @@ -75,7 +112,7 @@ struct CustomGameQueueUser { } pub struct QueueHandler { - users_in_queue: std::sync::Arc>>>, + users_in_queue: std::sync::Arc>>, users_in_custom_games_queue: std::sync::Arc>>, custom_game_for_user: std::sync::Arc>>, users_per_game: usize, @@ -131,23 +168,24 @@ impl QueueHandler { tokio::spawn(async move { loop { let now = chrono::Utc::now(); - let mut to_start: Vec<(QueueKey, Vec)> = Vec::new(); + let mut to_start: Vec<(QueueKey, Queue)> = Vec::new(); let mut next_deadline: Option> = None; { let mut lock = users_in_queue.lock().await; - let mut empty_keys: Vec = Vec::new(); + //let mut empty_keys: Vec = Vec::new(); let mut expired_keys: Vec = Vec::new(); - for (key, users) in lock.iter() { - if users.is_empty() { + for (key, q_entry) in lock.iter() { + if q_entry.users.is_empty() { continue; } + /*if q_entry.users.is_empty() && q_entry.platoons.is_empty() { empty_keys.push(key.clone()); continue; - } + }*/ // first user is always oldest within a queue - let deadline = users[0].enqueued_at + autostart_after; + let deadline = q_entry.users[0].enqueued_at + autostart_after; if now >= deadline { expired_keys.push(key.clone()); } else { @@ -158,23 +196,23 @@ impl QueueHandler { } } - for k in empty_keys { + /*for k in empty_keys { lock.remove(&k); - } + }*/ for k in expired_keys { - if let Some(players) = lock.remove(&k) { - if !players.is_empty() { - to_start.push((k, players)); + if let Some(q_entry) = lock.remove(&k) { + if !q_entry.users.is_empty() { + to_start.push((k, q_entry)); } } } } // start expired queues outside the lock - for (key, players) in to_start { + for (key, q_entry) in to_start { // choose the oldest queued user's LobbyUser handle - let starter = match players.first() { + let starter = match q_entry.users.first() { Some(p) => p.user.clone(), None => continue, }; @@ -188,7 +226,7 @@ impl QueueHandler { weapon_guesser.clone(), users_per_game, key, - players, + q_entry, starter.as_ref().as_ref(), ).await; } @@ -208,7 +246,7 @@ impl QueueHandler { }); } - async fn enter_match(&self, key: QueueKey, players: Vec, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { + async fn enter_match(&self, key: QueueKey, q_entry: Queue, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { Self::enter_match_static( self.hostname.clone(), self.hostport, @@ -218,13 +256,13 @@ impl QueueHandler { self.weapon_guesser.clone(), self.users_per_game, key, - players, + q_entry, user, ).await } #[allow(clippy::too_many_arguments)] - async fn enter_match_static(hostname: String, hostport: u16, network_conf: crate::data::network::NetworkConfigData, factory: std::sync::Arc, cpu_counter: std::sync::Arc, weapon_guesser: std::sync::Arc, users_per_game: usize, key: QueueKey, mut players: Vec, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { + async fn enter_match_static(hostname: String, hostport: u16, network_conf: crate::data::network::NetworkConfigData, factory: std::sync::Arc, cpu_counter: std::sync::Arc, weapon_guesser: std::sync::Arc, users_per_game: usize, key: QueueKey, mut q_entry: Queue, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { let guid_str = key.unique_guid(); let game_desc = oj_rc_core::persist::user::GameDescriptor { guid: guid_str.clone(), @@ -242,22 +280,26 @@ impl QueueHandler { oj_rc_core::data::game_mode::GameMode::Pit => |i| i as i32, // each player is on a different team _ => |i| (i % 2) as i32, // alternate teams };*/ - for (i, player) in players.iter_mut().enumerate() { - player.player.team = team_picker.team(i); + let mut player_descs = Vec::with_capacity(q_entry.users.len()); + for (i, player) in q_entry.users.iter_mut().enumerate() { + let mut lobby_desc = oj_rc_core::persist::user::PlayerLobbyDescriptor { + user_id: player.user_id, + team: -1, + group: player.player.group.clone(), + public_id: player.player.name.clone(), + display_name: player.player.display_name.clone(), + }; + let team = team_picker.choose_team(&guid_str, i, &lobby_desc); + lobby_desc.team = team; + player.player.team = team; + player_descs.push(lobby_desc); } - let player_descs = players.iter().map(|x| oj_rc_core::persist::user::PlayerLobbyDescriptor { - user_id: x.user_id, - team: x.player.team, - group: None, // TODO support platoons - public_id: x.player.name.clone(), - display_name: x.player.display_name.clone(), - }).collect(); - let missing = users_per_game.saturating_sub(players.len()); + let missing = users_per_game.saturating_sub(q_entry.users.len()); match user.start_game(game_desc, player_descs, factory.as_ref(), &cpu_counter, &weapon_guesser, &team_picker, missing).await { Ok(fakes) => { - let player_datas = players.iter().map(|x| x.player.clone()) + let player_datas = q_entry.users.iter().map(|x| x.player.clone()) .chain(fakes.players.into_iter().map(|(desc, _emu)| desc),) .collect(); let enter_battle_ev = crate::events::battle_enter::BattleEnter { @@ -274,10 +316,10 @@ impl QueueHandler { network_config: network_conf.clone(), }; let arc_event = std::sync::Arc::new(enter_battle_ev); - for player in players.iter() { + for player in q_entry.users.iter() { tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone())); } - log::info!("{} players are entering match {}", players.len(), guid_str); + log::info!("{} players are entering match {}", q_entry.users.len(), guid_str); }, Err(e) => { if let Some(msg) = e.error_msg() { @@ -491,8 +533,55 @@ impl QueueHandler { } } + fn join_or_create_platoon(&self, new_player: QueueUser, platoon: PlatoonInfo, q_entry: &mut Queue) { + let new_player = PlatoonUser::from_queue_user(new_player); + log::debug!("Existing platoon count {}", q_entry.platoons.len()); + if let Some(p_entry) = q_entry.platoons.get_mut(&platoon.platoon_id) { + // update + log::debug!("Adding user {} to existing platoon {} ({}/{})", new_player.0.user_id, platoon.platoon_id, p_entry.members.len() + 1, p_entry.total); + if platoon.is_leader { + p_entry.total = platoon.total; + } + p_entry.members.push(new_player); + let wants_usize = p_entry.total as usize; + if wants_usize == p_entry.members.len() { + log::info!("All members of platoon {} are in queue, actually adding them to queue", platoon.platoon_id); + for member in p_entry.members.iter() { + q_entry.users.push(member.clone_for_queue()); + } + q_entry.users.sort_by_key(|u| u.enqueued_at); + } + } else { + // create + log::debug!("Adding user {} to new platoon {} (1/{})", new_player.0.user_id, platoon.platoon_id, platoon.total); + q_entry.platoons.insert(platoon.platoon_id.clone(), Platoon { + total: if platoon.is_leader { platoon.total } else { u8::MAX }, + members: vec![new_player], + }); + } + } - pub async fn join_queue(&self, map: String, mode: oj_rc_core::data::game_mode::GameMode, visibility: oj_rc_core::data::game_mode::MapVisibility, auto_heal: bool, user: std::sync::Arc + Send + Sync>>, event_emitter: polariton_server::events::EventEmitter) { + fn platoon_leave_queue(&self, user_id: i32, platoon_id: String, q_entry: &mut Queue) { + let needs_remove = if let Some(platoon) = q_entry.platoons.get_mut(&platoon_id) { + let wants_usize = platoon.total as usize; + if wants_usize == platoon.members.len() { + // other platoon members are in queue, they also need to be removed + let some_group = Some(platoon_id.clone()); + q_entry.users.retain(|q_user| q_user.player.group != some_group); + log::info!("Removed platoon {} from actual queue because user {} left queue", platoon_id, user_id); + } + platoon.members.retain(|p_mem| p_mem.0.user_id != user_id); + platoon.members.is_empty() + } else { + log::error!("Failed to find platoon {} to remove user {}: the lobby is in a bad state", platoon_id, user_id); + false + }; + if needs_remove { + q_entry.platoons.remove(&platoon_id); + } + } + + pub async fn join_queue(&self, map: String, mode: oj_rc_core::data::game_mode::GameMode, visibility: oj_rc_core::data::game_mode::MapVisibility, auto_heal: bool, user: std::sync::Arc + Send + Sync>>, event_emitter: polariton_server::events::EventEmitter, platoon: Option) { if !self.is_enabled { event_emitter.emit(crate::events::enqueue_error::QueueJoinError { code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16, @@ -508,7 +597,10 @@ impl QueueHandler { }; let lobby_user = user.as_ref().as_ref(); match lobby_user.player_data(&self.cpu_counter).await { - Ok(player_data) => { + Ok(mut player_data) => { + if let Some(platoon) = &platoon { + player_data.group = Some(platoon.platoon_id.clone()); + } let new_player = QueueUser { emitter: event_emitter, player: player_data, @@ -516,25 +608,30 @@ impl QueueHandler { enqueued_at: chrono::Utc::now(), user: user.clone(), }; + let mut lock = self.users_in_queue.lock().await; + for q_key in lock.keys() { + log::debug!("Existing queue for {} (matches joiner? {})", q_key.short(), q_key.short().to_string() == key.short().to_string()); + } // handle game event change - if !lock.contains_key(&key) && lock.len() == 1 { + if !lock.contains_key(&key) && !lock.is_empty() { log::debug!("Game event change detected"); match self.change_strategy { GamemodeChangeStrategy::Upgrade => { - let mut new_queue_map = std::collections::HashMap::>::with_capacity(lock.len()); + let mut new_queue_map = std::collections::HashMap::::with_capacity(lock.len()); let mut count = 0; - for (_key, mut users) in lock.drain() { - count += users.len(); + for (_key, mut q_entry) in lock.drain() { + count += q_entry.users.len(); if let Some(values) = new_queue_map.get_mut(&key) { - values.append(&mut users); + values.users.append(&mut q_entry.users); + values.platoons.extend(q_entry.platoons.into_iter()); } else { - new_queue_map.insert(key.clone(), users); + new_queue_map.insert(key.clone(), q_entry); } } - for users in new_queue_map.values_mut() { - users.sort_by_key(|u| u.enqueued_at); + for q_entry in new_queue_map.values_mut() { + q_entry.users.sort_by_key(|u| u.enqueued_at); } *lock = new_queue_map; if count != 0 { @@ -542,18 +639,31 @@ impl QueueHandler { } }, GamemodeChangeStrategy::Notify => { - let mut count = 0; - for (_key, users) in lock.drain() { - count += users.len(); - for player in users { + let mut seen = std::collections::HashSet::new(); + for (_key, q_entry) in lock.drain() { + for player in q_entry.users { + if seen.contains(&player.user_id) { continue; } + log::debug!("Notifying user {} of event expiry", player.user_id); player.emitter.emit(crate::events::enqueue_error::QueueJoinError { code: oj_rc_core::data::error_codes::LobbyReasonCode::EventSystemExpired as i16, text: "Please requeue".to_owned(), }); + seen.insert(player.user_id); + } + for platoon in q_entry.platoons.into_values() { + for player in platoon.members { + if seen.contains(&player.0.user_id) { continue; } + log::debug!("Notifying user {} of event expiry", player.0.user_id); + player.0.emitter.emit(crate::events::enqueue_error::QueueJoinError { + code: oj_rc_core::data::error_codes::LobbyReasonCode::EventSystemExpired as i16, + text: "Please requeue".to_owned(), + }); + seen.insert(player.0.user_id); + } } } - if count != 0 { - log::info!("Notified {} users in queue of new gamemode {}", count, key.short()); + if !seen.is_empty() { + log::info!("Notified {} users in queue of new gamemode {}", seen.len(), key.short()); } }, GamemodeChangeStrategy::Ignore => { @@ -561,21 +671,44 @@ impl QueueHandler { } } } - let players_len = if let Some(players) = lock.get_mut(&key) { - log::info!("User {} entered queue for existing match {}", new_player.user_id, key.short()); - players.push(new_player); - players.len() + let players_len = if let Some(q_entry) = lock.get_mut(&key) { + if let Some(platoon) = platoon { + log::info!("User {} platooned {} queue for existing match {}", new_player.user_id, platoon.platoon_id, key.short()); + self.join_or_create_platoon(new_player, platoon, q_entry); + } else { + log::info!("User {} entered queue for existing match {}", new_player.user_id, key.short()); + q_entry.users.push(new_player); + } + q_entry.users.len() } else { - log::info!("User {} entered queue for new match {}", new_player.user_id, key.short()); - lock.insert(key.clone(), vec![new_player]); - 1 + if let Some(platoon) = platoon { + log::info!("User {} platooned {} queue for new match {}", new_player.user_id, platoon.platoon_id, key.short()); + let mut q_entry = Queue { + users: Vec::default(), + platoons: HashMap::default(), + }; + self.join_or_create_platoon(new_player, platoon, &mut q_entry); + lock.insert(key.clone(), q_entry); + 0 + } else { + log::info!("User {} entered queue for new match {}", new_player.user_id, key.short()); + lock.insert(key.clone(), Queue { + users: vec![new_player], + platoons: HashMap::default(), + }); + 1 + } }; + for q_key in lock.keys() { + log::debug!("Now queue for {} (matches joiner? {})", q_key.short(), q_key.short().to_string() == key.short().to_string()); + } let game_ready = players_len >= self.users_per_game; let players = if game_ready { lock.remove(&key) } else { None }; drop(lock); if let Some(players) = players { self.enter_match(key, players, lobby_user).await; } + log::debug!("join_queue complete success"); }, Err(e) => { event_emitter.emit(crate::events::enqueue_error::QueueJoinError { @@ -598,13 +731,24 @@ impl QueueHandler { log::info!("User {} was removed from custom game {} queue", user_id, session_id); } else { // fallback to regular multiplayer - for (queue_key, queue) in self.users_in_queue.lock().await.iter_mut() { - if let Some((i, _)) = queue.iter().enumerate().find(|(_, user)| user.user_id == user_id) { - queue.remove(i); + let mut to_remove = Vec::new(); + let mut q_lock = self.users_in_queue.lock().await; + for (queue_key, queue) in q_lock.iter_mut() { + if let Some((i, _)) = queue.users.iter().enumerate().find(|(_, user)| user.user_id == user_id) { + let q_user = queue.users.remove(i); + if let Some(platoon_id) = q_user.player.group { + self.platoon_leave_queue(user_id, platoon_id, queue); + } + if queue.users.is_empty() && queue.platoons.is_empty() { + to_remove.push(queue_key.to_owned()); + } log::info!("User {} was removed from queue {}", user_id, queue_key.short()); break; } } + for key in to_remove { + q_lock.remove(&key); + } } } } diff --git a/rc_lobby_room/src/operations/join_queue.rs b/rc_lobby_room/src/operations/join_queue.rs index 07a9abf..f13a8e5 100644 --- a/rc_lobby_room/src/operations/join_queue.rs +++ b/rc_lobby_room/src/operations/join_queue.rs @@ -62,6 +62,15 @@ impl SimpleOperation for QueueJoinProvider { current_lobby.auto_heal, user_info.clone(), events.to_owned(), + if group_size > 1 { + Some(crate::lobby::PlatoonInfo { + total: group_size as _, + platoon_id: group_id.string, + is_leader, + }) + } else { + None + } ).await; } } else { diff --git a/rc_lobby_room/src/operations/mod.rs b/rc_lobby_room/src/operations/mod.rs index 8d1d001..a7b839f 100644 --- a/rc_lobby_room/src/operations/mod.rs +++ b/rc_lobby_room/src/operations/mod.rs @@ -10,7 +10,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .modify(oj_rc_core::polariton::RcOpModifier) .add(more_auth::MoreLobbyAuth) //.add(eac::EacChallengeIgnorer) - //.add(polariton_server::operations::Ack::<2, _>::default()) + .add(polariton_server::operations::Ack::<2, _>::default()) .add(no_quit::quit_blocker_provider()) .add(join_queue::join_queue_provider(&init_ctx.queue)) } diff --git a/rc_social_room/src/operations/platoon_invites.rs b/rc_social_room/src/operations/platoon_invites.rs index d8e3a3f..972426e 100644 --- a/rc_social_room/src/operations/platoon_invites.rs +++ b/rc_social_room/src/operations/platoon_invites.rs @@ -23,7 +23,7 @@ impl SimpleOperation for PlatoonPendingInviter let user_info = user.user()?; let my_public_id = user_info.public_id(); log::debug!("Checking for pending platoon invite of user {}", my_public_id); - if let Some(platoon_id) = self.social.platoon_of_user(my_public_id).await { + if let Some(platoon_id) = self.social.platoon_invite_of_user(my_public_id).await { let members = self.social.users_of_platoon(&platoon_id).await; let social_infos = user_info.list_social_info(&[members[0].public_id.clone()]).await?; params.insert(INVITER_NAME_PARAM_KEY, Typed::Str(social_infos[0].public_id.clone().into())); diff --git a/rc_social_room/src/social_services.rs b/rc_social_room/src/social_services.rs index 7bd2d3b..d6659f9 100644 --- a/rc_social_room/src/social_services.rs +++ b/rc_social_room/src/social_services.rs @@ -92,6 +92,20 @@ impl SocialMesh { .map(|x| x.to_owned()) } + pub async fn platoon_invite_of_user(&self, public_id: &str) -> Option { + if let Some(platoon_id) = self.platoons.platoon_by_user.read().await.get(public_id) { + if let Some(platoon) = self.platoons.platoon_by_id.read().await.get(platoon_id) { + let myself = platoon.iter() + .find(|mem| mem.public_id == public_id) + .unwrap(); + if matches!(myself.status, crate::data::platoon::MemberStatus::Invited) { + return Some(platoon_id.to_owned()); + } + } + } + None + } + pub async fn users_of_platoon(&self, platoon_id: &str) -> Vec { self.platoons.platoon_by_id.read().await .get(platoon_id) @@ -150,12 +164,14 @@ impl SocialMesh { } pub async fn remove_user_from_platoon(&self, public_id: &str) -> bool { - if let Some(platoon_id) = self.platoons.platoon_by_user.write().await.remove(public_id) { + let mut platoon_by_user_lock = self.platoons.platoon_by_user.write().await; + if let Some(platoon_id) = platoon_by_user_lock.remove(public_id) { let mut platoon_by_id_lock = self.platoons.platoon_by_id.write().await; if let Some(platoon) = platoon_by_id_lock.get_mut(&platoon_id) { platoon.retain(|member| member.public_id != public_id); - if platoon.is_empty() { + if platoon.len() <= 1 { platoon_by_id_lock.remove(&platoon_id); + platoon_by_user_lock.retain(|_pub_id, platoon_id_val| platoon_id_val != &platoon_id); } true } else {