use crate::nex::user::User; use crate::rmc::protocols::notifications::{NotificationEvent, RemoteNotification}; use log::info; use rand::random; use rnex_core::kerberos::KerberosDateTime; use rnex_core::rmc::protocols::notifications::notification_types::{ HOST_CHANGED, OWNERSHIP_CHANGED, }; use rnex_core::rmc::response::ErrorCode; use rnex_core::rmc::response::ErrorCode::{Core_InvalidArgument, RendezVous_SessionVoid}; use rnex_core::rmc::structures::matchmake::gathering_flags::PERSISTENT_GATHERING; use rnex_core::rmc::structures::matchmake::{ Gathering, MatchmakeParam, MatchmakeSession, MatchmakeSessionSearchCriteria, }; use rnex_core::rmc::structures::variant::Variant; use std::collections::HashMap; use std::str::FromStr; use std::sync::atomic::AtomicU32; use std::sync::atomic::Ordering::Relaxed; use std::sync::{Arc, Weak}; use std::time::Duration; use tokio::sync::{Mutex, RwLock}; use tokio::time::sleep; pub struct MatchmakeManager { //pub gid_counter: AtomicU32, pub sessions: RwLock>>>, pub rv_cid_counter: AtomicU32, pub users: RwLock>>, } impl MatchmakeManager { pub fn next_gid(&self) -> u32 { random() //self.gid_counter.fetch_add(1, Relaxed) } pub fn next_cid(&self) -> u32 { self.rv_cid_counter.fetch_add(1, Relaxed) } pub async fn get_session( &self, gid: u32, ) -> Result>, ErrorCode> { let sessions = self.sessions.read().await; let Some(session) = sessions.get(&gid) else { return Err(RendezVous_SessionVoid); }; let session = session.clone(); drop(sessions); Ok(session) } async fn garbage_collect(&self) { info!("running rnex garbage collector over all sessions and users"); let mut idx = 0; let mut to_be_deleted_gids = Vec::new(); // i am very well aware of how inefficient doing it like this is but this is the only // way which i could think of to do this without potentially causing a deadlock of // the entire server while let Some((gid, session)) = { let sessions = self.sessions.read().await; let session_pair = sessions.iter().nth(idx).map(|s| (*s.0, s.1.clone())); drop(sessions); session_pair } { let session = session.lock().await; if !session.is_reachable() { to_be_deleted_gids.push(gid); } idx += 1; } let mut sessions = self.sessions.write().await; for gid in to_be_deleted_gids { sessions.remove(&gid); } } pub async fn initialize_garbage_collect_thread(this: Weak) { tokio::spawn(async move { while let Some(this) = this.upgrade() { this.garbage_collect().await; // every 30 minutes sleep(Duration::from_secs(60 * 30)).await; } }); } } #[derive(Default, Debug)] pub struct ExtendedMatchmakeSession { pub session: MatchmakeSession, pub connected_players: Vec>, } fn read_bounds_string(str: &str) -> Option<(T, T)> { let bounds = str.split_once(",")?; Some((T::from_str(bounds.0).ok()?, T::from_str(bounds.1).ok()?)) } fn check_bounds_str(compare: T, str: &str) -> Option { let bounds: (T, T) = read_bounds_string(str)?; Some(bounds.0 <= compare && compare <= bounds.1) } pub async fn broadcast_notification>( players: &[T], notification_event: &NotificationEvent, ) { for player in players { let player = player.as_ref(); player .remote .process_notification_event(notification_event.clone()) .await; } } impl ExtendedMatchmakeSession { #[inline(always)] pub fn get_active_players(&self) -> Vec> { self.connected_players .iter() .filter_map(|u| u.upgrade()) .collect() } #[inline(always)] pub async fn broadcast_notification(&self, notification_event: &NotificationEvent) { broadcast_notification(&self.get_active_players(), notification_event).await; } pub async fn from_matchmake_session( gid: u32, session: MatchmakeSession, host: &Weak, ) -> Self { let Some(host) = host.upgrade() else { return Default::default(); }; let mm_session = MatchmakeSession { gathering: Gathering { self_gid: gid, owner_pid: host.pid, host_pid: host.pid, ..session.gathering.clone() }, datetime: KerberosDateTime::now(), session_key: (0..32).map(|_| random()).collect(), matchmake_param: MatchmakeParam { params: vec![ ("@SR".to_owned(), Variant::Bool(true)), ("@GIR".to_owned(), Variant::SInt64(3)), ], }, system_password_enabled: false, ..session }; Self { session: mm_session, connected_players: Default::default(), } } pub async fn add_players(&mut self, conns: &[Weak], join_msg: String) { let Some(initiating_user) = conns[0].upgrade() else { return; }; let initiating_pid = initiating_user.pid; let old_particip = self.connected_players.clone(); for conn in conns { self.connected_players.push(conn.clone()); } self.session.participation_count = self.connected_players.len() as u32; for other_connection in &conns[1..] { let Some(other_conn) = other_connection.upgrade() else { continue; }; let other_pid = other_conn.pid; /*if other_pid == self.session.gathering.owner_pid && joining_pid == self.session.gathering.owner_pid{ continue; }*/ other_conn .remote .process_notification_event(NotificationEvent { pid_source: initiating_pid, notif_type: 122000, param_1: self.session.gathering.self_gid, param_2: other_pid, str_param: "".into(), param_3: 0, }) .await; } let list_of_connected_pids: Vec<_> = self .connected_players .iter() .filter_map(|p| p.upgrade()) .map(|p| p.pid) .collect(); for other_connection in conns { let Some(other_conn) = other_connection.upgrade() else { continue; }; // let other_pid = other_conn.pid; /*if other_pid == self.session.gathering.owner_pid && joining_pid == self.session.gathering.owner_pid{ continue; }*/ for pid in &list_of_connected_pids { other_conn .remote .process_notification_event(NotificationEvent { pid_source: initiating_pid, notif_type: 3001, param_1: self.session.gathering.self_gid, param_2: *pid, str_param: join_msg.clone(), param_3: self.connected_players.len() as _, }) .await; } } for old_conns in &old_particip { let Some(old_conns) = old_conns.upgrade() else { continue; }; let older_pid = old_conns.pid; initiating_user .remote .process_notification_event(NotificationEvent { pid_source: initiating_pid, notif_type: 3001, param_1: self.session.gathering.self_gid, param_2: older_pid, str_param: join_msg.clone(), param_3: self.connected_players.len() as _, }) .await; } } pub fn has_active_players(&self) -> bool { self.connected_players .iter() .filter(|v| v.upgrade().is_some()) .count() != 0 } #[inline] pub fn is_reachable(&self) -> bool { (if self.session.gathering.flags & PERSISTENT_GATHERING != 0 { if self.has_active_players() { true } else { self.session.open_participation } } else { self.has_active_players() }) & self.has_active_players() } #[inline] pub fn is_joinable(&self) -> bool { self.is_reachable() && self.session.open_participation } pub fn matches_criteria( &self, search_criteria: &MatchmakeSessionSearchCriteria, ) -> Result { // todo: implement the rest of the search criteria if search_criteria.vacant_only { if (self.connected_players.len() as u16 + search_criteria.vacant_participants) > self.session.gathering.maximum_participants { return Ok(false); } } if search_criteria.exclude_locked { if !self.session.open_participation { return Ok(false); } } if search_criteria.exclude_system_password_set { if self.session.system_password_enabled { return Ok(false); } } if search_criteria.exclude_user_password_set { if self.session.user_password_enabled { return Ok(false); } } if !check_bounds_str( self.session.gathering.minimum_participants, &search_criteria.minimum_participants, ) .ok_or(Core_InvalidArgument)? { return Ok(false); } if !check_bounds_str( self.session.gathering.maximum_participants, &search_criteria.maximum_participants, ) .ok_or(Core_InvalidArgument)? { return Ok(false); } let game_mode: u32 = search_criteria .game_mode .parse() .map_err(|_| Core_InvalidArgument)?; if self.session.gamemode != game_mode { return Ok(false); } let mm_sys_type: u32 = search_criteria .matchmake_system_type .parse() .map_err(|_| Core_InvalidArgument)?; if self.session.matchmake_system_type != mm_sys_type { return Ok(false); } if search_criteria .attribs .get(0) .map(|str| str.parse().ok()) .flatten() != self.session.attributes.get(0).map(|v| *v) { return Ok(false); } if search_criteria .attribs .get(2) .map(|str| str.parse().ok()) .flatten() != self.session.attributes.get(2).map(|v| *v) { return Ok(false); } if search_criteria .attribs .get(3) .map(|str| str.parse().ok()) .flatten() != self.session.attributes.get(3).map(|v| *v) { return Ok(false); } Ok(true) } pub async fn migrate_ownership(&mut self, initiator_pid: u32) -> Result<(), ErrorCode> { let players: Vec<_> = self .connected_players .iter() .filter_map(|p| p.upgrade()) .collect(); let Some(new_owner) = players .iter() .find(|p| p.pid != self.session.gathering.owner_pid) else { self.session.gathering.owner_pid = 0; return Ok(()); }; self.session.gathering.owner_pid = new_owner.pid; self.broadcast_notification(&NotificationEvent { pid_source: initiator_pid, notif_type: OWNERSHIP_CHANGED, param_1: self.session.gathering.self_gid, param_2: new_owner.pid, ..Default::default() }) .await; Ok(()) } pub async fn migrate_host(&mut self, initiator_pid: u32) -> Result<(), ErrorCode> { // let players: Vec<_> = self.connected_players.iter().filter_map(|p| p.upgrade()).collect(); self.session.gathering.host_pid = self.session.gathering.owner_pid; self.broadcast_notification(&NotificationEvent { pid_source: initiator_pid, notif_type: HOST_CHANGED, param_1: self.session.gathering.self_gid, ..Default::default() }) .await; Ok(()) } pub async fn remove_player_from_session( &mut self, pid: u32, message: &str, ) -> Result<(), ErrorCode> { self.connected_players .retain(|u| u.upgrade().is_some_and(|u| u.pid != pid)); self.session.participation_count = (self.connected_players.len() & u32::MAX as usize) as u32; if pid == self.session.gathering.owner_pid { self.migrate_ownership(pid).await?; } if pid == self.session.gathering.host_pid { self.migrate_host(pid).await?; } // todo: support DisconnectChangeOwner // todo: finish the rest of this for player in self.connected_players.iter().filter_map(|p| p.upgrade()) { player .remote .process_notification_event(NotificationEvent { notif_type: 3008, pid_source: pid, param_1: self.session.gathering.self_gid, param_2: pid, str_param: message.to_owned(), ..Default::default() }) .await; } Ok(()) } }