diff --git a/rc_core/src/data/game_mode.rs b/rc_core/src/data/game_mode.rs index 082f439..da1f5aa 100644 --- a/rc_core/src/data/game_mode.rs +++ b/rc_core/src/data/game_mode.rs @@ -194,4 +194,22 @@ impl MapVisibility { Self::Bad => oj_rc_database::schema::multiplayer_game::MapVisibility::Bad, } } + + #[inline] + pub fn from_u8(num: u8) -> Option { + match num { + 0 => Some(Self::Good), + 1 => Some(Self::Poor), + 2 => Some(Self::Bad), + _ => None, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Good => "Good", + Self::Poor => "Poor", + Self::Bad => "VeryPoor", + } + } } diff --git a/rc_core/src/data/player_data.rs b/rc_core/src/data/player_data.rs index badc67f..62394e9 100644 --- a/rc_core/src/data/player_data.rs +++ b/rc_core/src/data/player_data.rs @@ -112,3 +112,16 @@ impl PlayerDatas { Typed::Bytes(buf.into()) } } + +pub struct AvatarInfo { + pub avatar_id: Option, +} + +impl AvatarInfo { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.avatar_id.is_none())), + (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id.unwrap_or(0))), + ].into()) + } +} diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index bc8d8ad..cad31f7 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -1331,6 +1331,37 @@ impl super::User for UserData { Ok(()) } + async fn list_avatar_info(&self, public_ids: &[String]) -> Result, polariton_server::operations::SimpleOpError> { + let users = self.db.users_by_public_id(public_ids.iter()).await + .map_err(|e| { + log::error!("Failed to retrieve friend avatars for user {} : {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to retrieve friend avatars: {}", e), + ) + })?; + let user_ids = users.iter().map(|user| user.id); + let user_avatars = self.db.user_auxs_by_user_ids_and_descriptor(user_ids, oj_rc_database::schema::user_aux::Descriptor::AvatarId).await + .map_err(|e| { + log::error!("Failed to retrieve friend avatars for user {} : {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to retrieve friend avatars: {}", e), + ) + })?; + let avatar_map: std::collections::HashMap = user_avatars.iter() + .filter_map(|avatar| avatar.data.parse().ok().map(|avatar_id| (avatar.user_id, avatar_id))) + .collect(); + Ok(users.iter() + .map(|user| super::SocialInfo { + public_id: user.public_id.clone(), + display_name: user.display_name.clone(), + avatar_id: avatar_map.get(&user.id).and_then(|&avatar_id| if avatar_id == u32::MAX { None } else { Some(avatar_id as i32) }), + }) + .collect() + ) + } + fn current_game_event_setter(&self) -> Box { Box::new(GameEventSetterImpl { db: self.db.clone(), diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 434062e..1b6bb8d 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -95,6 +95,7 @@ pub trait User: ChatUser + SocialUser + SocialUserC + LobbyUser + Multipla async fn last_seen(&self) -> Result; async fn get_avatar_info(&self) -> Result, i16>; async fn set_avatar_info(&self, info: AvatarInfo) -> Result<(), i16>; + async fn list_avatar_info(&self, public_ids: &[String]) -> Result, polariton_server::operations::SimpleOpError>; fn current_game_event_setter(&self) -> Box; async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result; async fn currency_debit(&self, ty: CurrencyType, to_sub: u64) -> Result<(), polariton_server::operations::SimpleOpError>; diff --git a/rc_plugins/src/vehicle_validation/plugin.rs b/rc_plugins/src/vehicle_validation/plugin.rs index 2fe3387..0b70361 100644 --- a/rc_plugins/src/vehicle_validation/plugin.rs +++ b/rc_plugins/src/vehicle_validation/plugin.rs @@ -1,4 +1,5 @@ #[repr(u8)] +#[derive(Debug)] pub enum ValidationResultCode { Invalid = 0, Ok = 1, diff --git a/rc_services_room/src/custom_game_tracker.rs b/rc_services_room/src/custom_game_tracker.rs new file mode 100644 index 0000000..0ffc3b6 --- /dev/null +++ b/rc_services_room/src/custom_game_tracker.rs @@ -0,0 +1,613 @@ +use crate::data::custom_games::*; + +fn custom_game_key(creator: &str) -> String { + let now = chrono::Utc::now().timestamp(); + format!("{}_{}_cg", creator, now) +} + +pub struct UserInfo { + pub public_id: String, + pub is_invited: bool, + pub team: u8, + pub state: PlayerSessionStatus, +} + +pub struct SessionInfo { + pub session_id: String, + pub config: std::collections::HashMap, + pub users: Vec, +} + +pub struct KickInfo { + pub session_id: String, + pub was_invited: bool, +} + +pub struct CustomGameMesh { + games: tokio::sync::RwLock>, + user_to_game: tokio::sync::RwLock>, +} + +struct GameHandle { + users: Vec, + config: GameConfig, +} + +struct UserHandle { + public_id: String, + is_invited: std::sync::atomic::AtomicBool, + team: u8, + status: std::sync::atomic::AtomicU8, +} + +impl CustomGameMesh { + pub fn new() -> Self { + Self { + games: tokio::sync::RwLock::new(std::collections::HashMap::new()), + user_to_game: tokio::sync::RwLock::new(std::collections::HashMap::new()), + } + } + + pub async fn create_game(&self, public_id: &str) -> Result { + let mut user_lock = self.user_to_game.write().await; + if user_lock.contains_key(public_id) { + log::debug!("Rejected custom game session create; user {} is already in a session", public_id); + return Err(SessionCreateResponseCode::AlreadyInSession); + } + let game_id = custom_game_key(public_id); + let owner_handle = UserHandle { + public_id: public_id.to_owned(), + is_invited: std::sync::atomic::AtomicBool::new(false), + team: 0, + status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Ready.to_u8()), + }; + let game_handle = GameHandle { + users: vec![owner_handle], + config: GameConfig::default(), + }; + let mut games_lock = self.games.write().await; + user_lock.insert(public_id.to_owned(), game_id.clone()); + games_lock.insert(game_id.clone(), game_handle); + log::debug!("Custom game session {} created", game_id); + Ok(game_id) + } + + pub async fn leave_game(&self, public_id: &str) -> (SessionLeaveResponseCode, Option) { + if let Some(game_id) = { self.user_to_game.write().await.remove(public_id) } { + let mut games_lock = self.games.write().await; + if let Some(game) = games_lock.get_mut(&game_id) { + let session = if game.users.len() == 1 || game.users.iter().all(|u| u.public_id == public_id || u.is_invited.load(std::sync::atomic::Ordering::Relaxed)) { + log::debug!("User {} has disbanded custom game {}", public_id, game_id); + if game.users.len() != 1 { + let mut user_lock = self.user_to_game.write().await; + for user in game.users.iter() { + user_lock.remove(&user.public_id); + } + log::debug!("Removed {} invited stragglers from custom game {}", game.users.len() - 1, game_id); + } + let session = Self::session_from_game(&game_id, game); + games_lock.remove(&game_id); + session + } else { + log::debug!("User {} has left custom game {}", public_id, game_id); + game.users.retain(|user| user.public_id != public_id); + Self::session_from_game(&game_id, game) + }; + return (SessionLeaveResponseCode::Success, Some(session)); + } + } + (SessionLeaveResponseCode::NotInSession, None) + } + + pub async fn kick_from_game(&self, kicker: &str, kickee: &str) -> (KickResponseCode, Option<(KickInfo, SessionInfo)>) { + if let Some(game_id) = { self.user_to_game.write().await.remove(kickee) } { + let mut games_lock = self.games.write().await; + if let Some(game) = games_lock.get_mut(&game_id) { + let leader = game.users.first().unwrap(); + if leader.public_id != kicker { + return (KickResponseCode::UserIsNotSessionLeader, None); + } + let target = game.users.iter().find(|mem| mem.public_id == kickee).unwrap(); + let kick_info = KickInfo { + session_id: game_id.clone(), + was_invited: target.is_invited.load(std::sync::atomic::Ordering::Relaxed), + }; + game.users.retain(|user| user.public_id != kickee); + let session = Self::session_from_game(&game_id, game); + return (KickResponseCode::UserRemovedFromSession, Some((kick_info, session))); + } + } + (KickResponseCode::KickTargetIsNotInsession, None) + } + + pub async fn get_user_game(&self, public_id: &str) -> Option { + if let Some(game_id) = self.user_to_game.read().await.get(public_id) { + if let Some(game) = self.games.read().await.get(game_id) { + return Some(SessionInfo { + session_id: game_id.to_owned(), + config: game.config.as_map(), + users: game.users.iter() + .map(|user| UserInfo { + public_id: user.public_id.clone(), + is_invited: user.is_invited.load(std::sync::atomic::Ordering::Relaxed), + team: user.team, + state: PlayerSessionStatus::from_u8(user.status.load(std::sync::atomic::Ordering::Relaxed)).unwrap(), + }) + .collect() + }); + } + } + None + } + + pub async fn invite_user(&self, inviter: &str, invitee: &str, is_team_a: bool) -> (InviteToCustomGameResponseCode, Option) { + if let Some(game_id) = { self.user_to_game.read().await.get(inviter).cloned() } { + if let Some(game) = self.games.write().await.get_mut(&game_id) { + if game.users.iter().find(|x| x.public_id == invitee).is_some() { + let session = Self::session_from_game(&game_id, game); + (InviteToCustomGameResponseCode::InviteeHasAlreadyBeenInvited, Some(session)) + } else { + let invitee_handle = UserHandle { + public_id: invitee.to_owned(), + is_invited: std::sync::atomic::AtomicBool::new(true), + team: if is_team_a { 0 } else { 1 }, + status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Unknown.to_u8()) + }; + game.users.push(invitee_handle); + let session = Self::session_from_game(&game_id, game); + self.user_to_game.write().await.insert(invitee.to_owned(), game_id); + log::debug!("User {} invited to custom game", invitee); + (InviteToCustomGameResponseCode::UserInvited, Some(session)) + } + } else { + (InviteToCustomGameResponseCode::UserIsNotInSession, None) + } + } else { + (InviteToCustomGameResponseCode::UserIsNotInSession, None) + } + } + + pub async fn update_invite_user(&self, invitee: &str, is_accept: bool) -> (InviteReplyCustomGameResponseCode, Option) { + if let Some(game_id) = { self.user_to_game.read().await.get(invitee).cloned() } { + if is_accept { + // fully join custom game + if let Some(game) = self.games.read().await.get(&game_id) { + let invitee_handle_opt = game.users.iter().find(|user| user.public_id == invitee); + if let Some(invitee_handle) = invitee_handle_opt { + invitee_handle.is_invited.store(false, std::sync::atomic::Ordering::Relaxed); + invitee_handle.status.store(PlayerSessionStatus::Ready.to_u8(), std::sync::atomic::Ordering::Relaxed); + let session = Self::session_from_game(&game_id, game); + (InviteReplyCustomGameResponseCode::Success, Some(session)) + } else { + (InviteReplyCustomGameResponseCode::UserIsNoLongerInvited, None) + } + } else { + (InviteReplyCustomGameResponseCode::UserIsNoLongerInvited, None) + } + } else { + // leave custom game + if let Some(game) = self.games.write().await.get_mut(&game_id) { + game.users.retain(|user| user.public_id != invitee); + self.user_to_game.write().await.remove(invitee); + let session = Self::session_from_game(&game_id, game); + (InviteReplyCustomGameResponseCode::Success, Some(session)) + } else { + (InviteReplyCustomGameResponseCode::UserIsNoLongerInvited, None) + } + } + + } else { + (InviteReplyCustomGameResponseCode::UserIsNotInSession, None) + } + } + + pub async fn update_user_status(&self, public_id: &str, status: PlayerSessionStatus) -> Option { + if let Some(game_id) = self.user_to_game.read().await.get(public_id) { + if let Some(game) = self.games.read().await.get(game_id) { + let target = game.users.iter() + .find(|mem| mem.public_id == public_id) + .unwrap(); + target.status.store(status.to_u8(), std::sync::atomic::Ordering::Relaxed); + let session = Self::session_from_game(&game_id, game); + return Some(session); + } + } + None + } + + pub async fn set_config_field(&self, public_id: &str, field: &str, value: &str) -> (AdjustCustomGameConfigResponseCode, Option) { + if let Some(game_id) = self.user_to_game.read().await.get(public_id) { + if let Some(game) = self.games.write().await.get_mut(game_id) { + if game.users[0].public_id != public_id { + log::debug!("Update custom game session {} config rejected (not leader)", game_id); + return (AdjustCustomGameConfigResponseCode::AdjustmentRejected, None); + } + if let Ok(_) = game.config.set_field(field, value) { + log::debug!("Update custom game session {} config {} to {}", game_id, field, value); + let session = Self::session_from_game(&game_id, game); + return (AdjustCustomGameConfigResponseCode::Success, Some(session)); + } + } + return (AdjustCustomGameConfigResponseCode::AdjustmentRejected, None); + } else { + (AdjustCustomGameConfigResponseCode::NotInSession, None) + } + } + + fn session_from_game(game_id: &str, game: &GameHandle) -> SessionInfo { + SessionInfo { + session_id: game_id.to_owned(), + config: game.config.as_map(), + users: game.users.iter() + .map(|user| UserInfo { + public_id: user.public_id.clone(), + is_invited: user.is_invited.load(std::sync::atomic::Ordering::Relaxed), + team: user.team, + state: PlayerSessionStatus::from_u8(user.status.load(std::sync::atomic::Ordering::Relaxed)) + .unwrap_or(PlayerSessionStatus::Unknown), + }) + .collect() + } + } +} + +// multipliers are percents (as in, 100 is no change; 200 is 2x original, 10 is 0.1x) + +struct GameConfig { + game_mode: oj_rc_core::data::game_mode::GameMode, + map: String, // TODO maybe this should be validated + map_visibility: oj_rc_core::data::game_mode::MapVisibility, + health_regen: bool, + capture_segment_memory: bool, + base_shields_go_down: bool, + damage_mult: i32, + health_mult: i32, + power_mult: i32, + game_time: i32, // minutes? + capture_speed: i32, // this is two things in one (seconds?) + points_kill_streak: bool, + points_total_required: i32, + number_of_kills_to_win: i32, + respawn_time: i32, // this is three things in one + core_appear_frequency: i32, + core_health_multiplier: i32, + core_destroy_time: i32, + protonium_harvest: i32, + ceiling_multiplier: i32, + min_cpu: i32, + max_cpu: i32, +} + +impl core::default::Default for GameConfig { + fn default() -> Self { + Self { + game_mode: oj_rc_core::data::game_mode::GameMode::BattleArena, + map: oj_rc_core::data::game_mode::GameMap::Earth2.as_str().to_owned(), + map_visibility: oj_rc_core::data::game_mode::MapVisibility::Good, + health_regen: true, + capture_segment_memory: true, + base_shields_go_down: true, + damage_mult: 100, + health_mult: 100, + power_mult: 100, + game_time: 10, + capture_speed: 120, + points_kill_streak: false, + points_total_required: 1, + number_of_kills_to_win: 1, + respawn_time: 1, + core_appear_frequency: 1, + core_health_multiplier: 100, + core_destroy_time: 10, + protonium_harvest: 10, + ceiling_multiplier: 100, + min_cpu: 200, + max_cpu: 100_000, + } + } +} + +enum ConfigSetError { + ValueParseError, + InvalidField, +} + +impl GameConfig { + fn as_map(&self) -> std::collections::HashMap { + let mut map = std::collections::HashMap::with_capacity(10); + map.insert("GameMode".to_owned(), self.game_mode.as_str().to_owned()); + map.insert("MapChoice".to_owned(), self.map.clone()); + map.insert("MapVisibility".to_owned(), self.map_visibility.as_str().to_owned()); + map.insert("HealthRegen".to_owned(), if self.health_regen { "True".to_owned() } else { "False".to_owned() }); + map.insert("CaptureSegmentMemory".to_owned(), if self.capture_segment_memory { "True".to_owned() } else { "False".to_owned() }); + map.insert("BaseShieldsGoDown".to_owned(), if self.base_shields_go_down { "True".to_owned() } else { "False".to_owned() }); + map.insert("DamageMultiplier".to_owned(), self.damage_mult.to_string()); + map.insert("HealthMultiplier".to_owned(), self.health_mult.to_string()); + map.insert("PowerMultiplier".to_owned(), self.power_mult.to_string()); + map.insert("GameTime".to_owned(), self.game_time.to_string()); + map.insert("CaptureSpeedElimination".to_owned(), self.capture_speed.to_string()); + map.insert("PointsKillStreakOnOff".to_owned(), if self.points_kill_streak { "True".to_owned() } else { "False".to_owned() }); + map.insert("PointsTotalRequired".to_owned(), self.points_total_required.to_string()); + map.insert("NumberOfKillsToWin".to_owned(), self.number_of_kills_to_win.to_string()); + map.insert("RespawnTimeBA".to_owned(), self.respawn_time.to_string()); + map.insert("RespawnTimeTDM".to_owned(), self.respawn_time.to_string()); + map.insert("RespawnTimePit".to_owned(), self.respawn_time.to_string()); + map.insert("CoreAppearFrequency".to_owned(), self.core_appear_frequency.to_string()); + map.insert("CoreHealthMultiplier".to_owned(), self.core_health_multiplier.to_string()); + map.insert("CoreDestroyTimeValue".to_owned(), self.core_destroy_time.to_string()); + map.insert("CaptureSpeedBA".to_owned(), self.capture_speed.to_string()); + map.insert("ProtoniumHarvestBA".to_owned(), self.protonium_harvest.to_string()); + map.insert("CeilingMultiplier".to_owned(), self.ceiling_multiplier.to_string()); + map.insert("MinCPU".to_owned(), self.min_cpu.to_string()); + map.insert("MaxCPU".to_owned(), self.max_cpu.to_string()); + map + } + + fn set_field(&mut self, field: &str, value: &str) -> Result<(), ConfigSetError> { + match field { + "GameMode" => { + let val = match value { + "TeamDeathmatch" => Ok(oj_rc_core::data::game_mode::GameMode::TeamDeathmatch), + "BattleArena" => Ok(oj_rc_core::data::game_mode::GameMode::BattleArena), + "Pit" => Ok(oj_rc_core::data::game_mode::GameMode::Pit), + "SuddenDeath" => Ok(oj_rc_core::data::game_mode::GameMode::SuddenDeath), + idk_val => { + log::warn!("Unrecognized game mode {}", idk_val); + Err(ConfigSetError::ValueParseError) + } + }?; + self.game_mode = val; + Ok(()) + }, + "MapChoice" => { + self.map = value.to_owned(); + Ok(()) + }, + "MapVisibility" => { + let val = match value { + "VeryPoor" => Ok(oj_rc_core::data::game_mode::MapVisibility::Bad), + "Poor" => Ok(oj_rc_core::data::game_mode::MapVisibility::Poor), + "Good" => Ok(oj_rc_core::data::game_mode::MapVisibility::Good), + idk_val => { + log::warn!("Unrecognized map visibility {}", idk_val); + Err(ConfigSetError::ValueParseError) + } + }?; + self.map_visibility = val; + Ok(()) + }, + "HealthRegen" => { + match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False" + Err(e) => { + log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.health_regen = val; + Ok(()) + } + } + }, + "CaptureSegmentMemory" => { + match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False" + Err(e) => { + log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.capture_segment_memory = val; + Ok(()) + } + } + }, + "BaseShieldsGoDown" => { + match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False" + Err(e) => { + log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.base_shields_go_down = val; + Ok(()) + } + } + }, + "DamageMultiplier" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.damage_mult = val; + Ok(()) + } + } + }, + "HealthMultiplier" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.health_mult = val; + Ok(()) + } + } + }, + "PowerMultiplier" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.power_mult = val; + Ok(()) + } + } + }, + "GameTime" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.game_time = val; + Ok(()) + } + } + }, + "CaptureSpeedElimination" | "CaptureSpeedBA" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.capture_speed = val; + Ok(()) + } + } + }, + "PointsKillStreakOnOff" => { + match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False" + Err(e) => { + log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.points_kill_streak = val; + Ok(()) + } + } + }, + "PointsTotalRequired" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.points_total_required = val; + Ok(()) + } + } + }, + "NumberOfKillsToWin" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.number_of_kills_to_win = val; + Ok(()) + } + } + }, + "RespawnTimeBA" | "RespawnTimeTDM" | "RespawnTimePit" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.respawn_time = val; + Ok(()) + } + } + }, + "CoreAppearFrequency" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.core_appear_frequency = val; + Ok(()) + } + } + }, + "CoreHealthMultiplier" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.core_health_multiplier = val; + Ok(()) + } + } + }, + "CoreDestroyTimeValue" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.core_destroy_time = val; + Ok(()) + } + } + }, + "ProtoniumHarvestBA" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.protonium_harvest = val; + Ok(()) + } + } + }, + "CeilingMultiplier" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.ceiling_multiplier = val; + Ok(()) + } + } + }, + "MinCPU" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.min_cpu = val; + Ok(()) + } + } + }, + "MaxCPU" => { + match value.parse() { + Err(e) => { + log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e); + Err(ConfigSetError::ValueParseError) + }, + Ok(val) => { + self.max_cpu = val; + Ok(()) + } + } + }, + _ => { + log::warn!("Unrecognized custom game config field {} (val: {})", field, value); + Err(ConfigSetError::InvalidField) + } + } + } +} diff --git a/rc_services_room/src/data/custom_games.rs b/rc_services_room/src/data/custom_games.rs index d40780f..5f754a1 100644 --- a/rc_services_room/src/data/custom_games.rs +++ b/rc_services_room/src/data/custom_games.rs @@ -1,6 +1,5 @@ #![allow(dead_code)] - -pub use oj_rc_core::data::game_mode::GameMode; +use polariton::operation::Typed; #[repr(u8)] #[derive(Copy, Clone)] @@ -8,3 +7,192 @@ pub enum CustomGameInviteCode { NoInvite = 0, PendingInvite = 1, } + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum SessionCreateResponseCode { + SessionCreated = 0, + AlreadyInSession = 1, + SessionCreateError = 2, +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum AdjustCustomGameConfigResponseCode { + Success = 0, + NotInSession = 1, + AdjustmentRejected = 2, + // 3, 4, 5 also exist but seem equivalent to 2 +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum SessionLeaveResponseCode { + Success = 0, + NotInSession = 1, +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum KickResponseCode { + SessionNoLongerExists = 0, + KickTargetIsNotInsession = 1, + UserIsNotSessionLeader = 2, + UserRemovedFromSession = 3, + ErrorKickingFromSession = 4 +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum SessionRetrieveResponse { + UserNotInAnySession = 0, + SessionRetrieved = 1, + PlayerIsInvitedOnly = 2, + ErrorRetrievingSession = 3, +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum InviteToCustomGameResponseCode { + UserIsNotInSession = 0, + UserIsNotSessionLeader = 1, + InviteeHasAlreadyBeenInvited = 2, + UserIsNotOnline = 3, + UserInvited = 4, + ErrorDispatchingMessage = 5, + InviteeIsInAnotherCustomGame = 6, + InviteeIsAlreadyInvitedToAnotherCustomGame = 7, + UserDoesNotExist = 8, + UserOnlyAcceptsInvitesFromFriendsAndClanmates = 9, + UserBlockedYou = 10, +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum InviteReplyCustomGameResponseCode { + Success = 1, + Failure2 = 2, + UserIsNotInSession = 4, + UserIsNoLongerInvited = 5, +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum CheckCanJoinQueueResponseCode { + UserNotInSession0 = 0, + OnlyOneAllowed = 1, + Unbalanced = 2, + AlreadyInBattle = 3, + Ok = 4, + UserNotInSession5 = 5, +} + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum PlayerSessionStatus { + Unknown = 0, + Ready = 1, + Queuing = 2, + InBattle = 3, +} + +impl PlayerSessionStatus { + #[inline] + pub fn from_u8(num: u8) -> Option { + match num { + 0 => Some(Self::Unknown), + 1 => Some(Self::Ready), + 2 => Some(Self::Queuing), + 3 => Some(Self::InBattle), + _ => None, + } + } + + #[inline] + pub fn to_u8(self) -> u8 { + self as u8 + } +} + +pub struct Session { + pub leader: String, + pub session: String, + pub members: Vec, + pub members_display_name: Vec, + pub invited: Vec, + pub team_b_members: Vec, + pub config: std::collections::HashMap, + pub avatar_info: std::collections::HashMap, + pub player_session_state: std::collections::HashMap, +} + +impl Session { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("Leader".into()), Typed::Str(self.leader.clone().into())), + (Typed::Str("SessionID".into()), Typed::Str(self.session.clone().into())), + (Typed::Str("Members".into()), Typed::HashMap(self.members.iter() + .enumerate() + .map(|(i, pub_id)| (Typed::Int(i as _), Typed::Str(pub_id.into()))) + .collect::>() + .into() + )), + (Typed::Str("MembersDisplayName".into()), Typed::HashMap(self.members_display_name.iter() + .enumerate() + .map(|(i, display_name)| (Typed::Int(i as _), Typed::Str(display_name.into()))) + .collect::>() + .into() + )), + (Typed::Str("Invited".into()), Typed::HashMap(self.invited.iter() + .enumerate() + .map(|(i, pub_id)| (Typed::Int(i as _), Typed::Str(pub_id.into()))) + .collect::>() + .into() + )), + (Typed::Str("TeamBMembers".into()), Typed::HashMap(self.team_b_members.iter() + .enumerate() + .map(|(i, pub_id)| (Typed::Int(i as _), Typed::Str(pub_id.into()))) + .collect::>() + .into() + )), + (Typed::Str("Config".into()), Typed::HashMap(self.config.iter() + .map(|(key, val)| (Typed::Str(key.into()), Typed::Str(val.into()))) + .collect::>() + .into() + )), + (Typed::Str("AvatarInfo".into()), Typed::HashMap(self.avatar_info.iter() + .map(|(key, val)| (Typed::Str(key.into()), val.as_transmissible())) + .collect::>() + .into() + )), + (Typed::Str("PlayerSessionState".into()), Typed::HashMap(self.player_session_state.iter() + .map(|(key, val)| (Typed::Str(key.into()), Typed::Int(*val as _))) + .collect::>() + .into() + )), + ].into()) + } +} + +/// not to be confused with the event which contains the same data +/// ... just serialized differently (to be difficult?) +pub struct CustomGameInvite { + pub inviter_public_id: String, + pub inviter_display_name: String, + pub session: String, + pub avatar_id: Option, + pub invited_to_team_b: bool, +} + +impl CustomGameInvite { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (polariton::operation::Typed::Str("SenderName".into()), polariton::operation::Typed::Str(self.inviter_public_id.clone().into())), + (polariton::operation::Typed::Str("SenderDisplayName".into()), polariton::operation::Typed::Str(self.inviter_display_name.clone().into())), + (polariton::operation::Typed::Str("SessionGUID".into()), polariton::operation::Typed::Str(self.session.clone().into())), + (polariton::operation::Typed::Str("UseCustomAvatar".into()), polariton::operation::Typed::Bool(self.avatar_id.is_none())), + (polariton::operation::Typed::Str("AvatarID".into()), polariton::operation::Typed::Int(self.avatar_id.unwrap_or(0))), + (polariton::operation::Typed::Str("IsInvitedToTeamB".into()), polariton::operation::Typed::Bool(self.invited_to_team_b)), + ].into()) + } +} diff --git a/rc_services_room/src/events/custom_game_config.rs b/rc_services_room/src/events/custom_game_config.rs new file mode 100644 index 0000000..4408105 --- /dev/null +++ b/rc_services_room/src/events/custom_game_config.rs @@ -0,0 +1,25 @@ +use polariton::operation::Typed; + +#[derive(Clone)] +pub struct CustomGameConfigRefresh { + pub field: String, + pub value: String, +} + +impl polariton_server::events::IntoEvent for CustomGameConfigRefresh { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + let mut params = polariton::operation::ParameterTable::with_capacity(1); + params.insert(181, polariton::operation::Typed::HashMap(vec![ + (Typed::Str("Field".into()), Typed::Str(self.field.into())), + (Typed::Str("Value".into()), Typed::Str(self.value.into())), + ].into())); + polariton::operation::Event { + code: 9, + params, + } + } +} diff --git a/rc_services_room/src/events/custom_game_invite.rs b/rc_services_room/src/events/custom_game_invite.rs new file mode 100644 index 0000000..d61d03e --- /dev/null +++ b/rc_services_room/src/events/custom_game_invite.rs @@ -0,0 +1,30 @@ +//#[derive(Clone)] +pub struct CustomGameInvite { + pub inviter_public_id: String, + pub inviter_display_name: String, + pub session: String, + pub avatar_id: Option, + pub invited_to_team_a: bool, +} + +impl polariton_server::events::IntoEvent for CustomGameInvite { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + let mut params = polariton::operation::ParameterTable::with_capacity(1); + params.insert(172, polariton::operation::Typed::HashMap(vec![ + (polariton::operation::Typed::Str("Inviter".into()), polariton::operation::Typed::Str(self.inviter_public_id.into())), + (polariton::operation::Typed::Str("DisplayName".into()), polariton::operation::Typed::Str(self.inviter_display_name.into())), + (polariton::operation::Typed::Str("SessionID".into()), polariton::operation::Typed::Str(self.session.into())), + (polariton::operation::Typed::Str("UseCustomAvatar".into()), polariton::operation::Typed::Bool(self.avatar_id.is_none())), + (polariton::operation::Typed::Str("AvatarID".into()), polariton::operation::Typed::Int(self.avatar_id.unwrap_or(0))), + (polariton::operation::Typed::Str("InvitedToTeamA".into()), polariton::operation::Typed::Bool(self.invited_to_team_a)), + ].into())); + polariton::operation::Event { + code: 7, + params, + } + } +} diff --git a/rc_services_room/src/events/custom_game_invite_decline.rs b/rc_services_room/src/events/custom_game_invite_decline.rs new file mode 100644 index 0000000..49982fa --- /dev/null +++ b/rc_services_room/src/events/custom_game_invite_decline.rs @@ -0,0 +1,23 @@ +use polariton::operation::Typed; + +#[derive(Clone)] +pub struct CustomGameInviteDecline { + pub public_id: String, +} + +impl polariton_server::events::IntoEvent for CustomGameInviteDecline { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + let mut params = polariton::operation::ParameterTable::with_capacity(1); + params.insert(190, polariton::operation::Typed::HashMap(vec![ + (Typed::Str("UserName".into()), Typed::Str(self.public_id.into())) + ].into())); + polariton::operation::Event { + code: 12, + params, + } + } +} diff --git a/rc_services_room/src/events/custom_game_kick.rs b/rc_services_room/src/events/custom_game_kick.rs new file mode 100644 index 0000000..68be020 --- /dev/null +++ b/rc_services_room/src/events/custom_game_kick.rs @@ -0,0 +1,25 @@ +use polariton::operation::Typed; + +//#[derive(Clone)] +pub struct CustomGameKick { + pub session: String, + pub was_invited: bool, +} + +impl polariton_server::events::IntoEvent for CustomGameKick { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + let mut params = polariton::operation::ParameterTable::with_capacity(2); + params.insert(184, polariton::operation::Typed::HashMap(vec![ + (Typed::Str("Session".into()), Typed::Str(self.session.into())), + (Typed::Str("WasInvited".into()), Typed::Bool(self.was_invited)), + ].into())); + polariton::operation::Event { + code: 11, + params, + } + } +} diff --git a/rc_services_room/src/events/custom_game_refresh.rs b/rc_services_room/src/events/custom_game_refresh.rs new file mode 100644 index 0000000..07d5cd6 --- /dev/null +++ b/rc_services_room/src/events/custom_game_refresh.rs @@ -0,0 +1,19 @@ +#[derive(Clone)] +pub struct CustomGameRefresh { + pub session: String, +} + +impl polariton_server::events::IntoEvent for CustomGameRefresh { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + let mut params = polariton::operation::ParameterTable::with_capacity(1); + params.insert(172, polariton::operation::Typed::Str(self.session.into())); + polariton::operation::Event { + code: 8, + params, + } + } +} diff --git a/rc_services_room/src/events/handler.rs b/rc_services_room/src/events/handler.rs index e7aed0a..c129c9b 100644 --- a/rc_services_room/src/events/handler.rs +++ b/rc_services_room/src/events/handler.rs @@ -4,7 +4,7 @@ use oj_rc_core::persist::user::intercom::IntercomWebServiceUserMessage; pub struct IntercomHandler { listener: IntercomListener, user: std::sync::Weak + Send + Sync>>, - emitter: polariton_server::events::EventEmitter<()>, + emitter: polariton_server::events::WeakEventEmitter<()>, } impl IntercomHandler { @@ -16,14 +16,14 @@ impl IntercomHandler { Self { listener, user: std::sync::Arc::downgrade(user), - emitter: emitter.to_owned(), + emitter: emitter.to_owned().downgrade(), } } async fn run_loop( listener: IntercomListener, user: std::sync::Weak + Send + Sync>>, - emitter: polariton_server::events::EventEmitter<()> + emitter: polariton_server::events::WeakEventEmitter<()> ) { use futures::StreamExt; let mut listener = listener.listen().await; diff --git a/rc_services_room/src/events/mod.rs b/rc_services_room/src/events/mod.rs index f296372..9b5b217 100644 --- a/rc_services_room/src/events/mod.rs +++ b/rc_services_room/src/events/mod.rs @@ -6,3 +6,18 @@ pub use dev_message::DevMessage; mod maintenance_mode; pub use maintenance_mode::MaintenanceMode; + +mod custom_game_invite; +pub use custom_game_invite::CustomGameInvite; + +mod custom_game_refresh; +pub use custom_game_refresh::CustomGameRefresh; + +mod custom_game_config; +pub use custom_game_config::CustomGameConfigRefresh; + +mod custom_game_invite_decline; +pub use custom_game_invite_decline::CustomGameInviteDecline; + +mod custom_game_kick; +pub use custom_game_kick::CustomGameKick; diff --git a/rc_services_room/src/main.rs b/rc_services_room/src/main.rs index 1112531..79b8d2a 100644 --- a/rc_services_room/src/main.rs +++ b/rc_services_room/src/main.rs @@ -5,6 +5,8 @@ mod data; mod events; mod operations; mod vehicle_validators; +mod custom_game_tracker; +mod user_service; use oj_polariton_auth::Handshake; use tokio::net; @@ -25,6 +27,8 @@ pub struct InitConfig { pub factory: std::sync::Arc, pub parsers: oj_rc_core::cubes::CubeParsers, pub vehicle_validators: vehicle_validators::InitedVehicleValidators, + pub custom_games: std::sync::Arc, + pub user_mesh: std::sync::Arc, } #[tokio::main] @@ -48,7 +52,9 @@ async fn main() -> std::io::Result<()> { users, factory, parsers, - vehicle_validators + vehicle_validators, + custom_games: std::sync::Arc::new(custom_game_tracker::CustomGameMesh::new()), + user_mesh: std::sync::Arc::new(user_service::UserMesh::new()), }); let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(&init_ctx), polariton_server::events::EventsHandler::new())); @@ -77,7 +83,7 @@ async fn main() -> std::io::Result<()> { } async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc>, init_ctx: std::sync::Arc) { - let login_num = std::sync::Arc::new(LOGINS.fetch_add(1, std::sync::atomic::Ordering::SeqCst)); + let login_num = LOGINS.fetch_add(1, std::sync::atomic::Ordering::SeqCst); log::debug!("Accepting connection from address {} (login #{})", address, login_num); let enc = match do_connect_handshake(&mut socket).await { Some(x) => x, @@ -86,31 +92,25 @@ async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAdd return; } }; - { - if let Ok(mut handles) = USER_HANDLES.lock() { - handles.push(std::sync::Arc::downgrade(&login_num)); - ONLINE_USERS.store(handles.len() as u64, std::sync::atomic::Ordering::SeqCst); - } else { - // this should never happen - log::warn!("USER_HANDLES lock is poisoned, cannot track online users anymore (please restart)"); - } - } let (socket_r, socket_w) = socket.into_split(); let (chann_tx, chann_rx) = tokio::sync::mpsc::unbounded_channel(); let user_state = std::sync::Arc::new(oj_rc_core::UserState::<()>::new(init_ctx.users.clone(), chann_tx.clone())); let ctx = polariton::packet::SerdesContext::from_boxed(Default::default(), enc); server.handle_async_with_channel_join(socket_r, socket_w, user_state.clone(), ctx, chann_tx, chann_rx).await; log::debug!("Goodbye connection from address {} (login #{})", address, login_num); - drop(login_num); // explicit for good measure - { - if let Ok(mut handles) = USER_HANDLES.lock() { - handles.retain(|x| x.strong_count() != 0); - ONLINE_USERS.store(handles.len() as u64, std::sync::atomic::Ordering::SeqCst); - } else { - // this should never happen - ONLINE_USERS.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); - log::warn!("USER_HANDLES lock is poisoned, cannot track online users anymore (please restart)"); + if let Ok(user) = user_state.user() { + let pub_id = user.public_id(); + init_ctx.user_mesh.remove_user(pub_id.to_owned()).await; + if let Some(session) = init_ctx.custom_games.leave_game(pub_id).await.1 { + let session_members = session.users.iter() + .filter(|mem| !mem.is_invited) + .map(|mem| &mem.public_id as &str); + let update_event = crate::events::CustomGameRefresh { + session: session.session_id, + }; + init_ctx.user_mesh.broadcast_event_to(session_members, update_event).await; } + ONLINE_USERS.store(init_ctx.user_mesh.user_count().await as u64, std::sync::atomic::Ordering::SeqCst); } if let Ok(user_info) = user_state.user() { update_status(user_info.as_ref().as_ref()).await; diff --git a/rc_services_room/src/operations/custom_game_adjust.rs b/rc_services_room/src/operations/custom_game_adjust.rs new file mode 100644 index 0000000..b22c349 --- /dev/null +++ b/rc_services_room/src/operations/custom_game_adjust.rs @@ -0,0 +1,50 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 149; + +const FIELD_PARAM_KEY: u8 = 179; // str; in +const VALUE_PARAM_KEY: u8 = 180; // any; in +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out + +pub(super) struct CustomGameConfigChanger { + games: std::sync::Arc, + mesh: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameConfigChanger { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(field_name)) = params.remove(&FIELD_PARAM_KEY) { + if let Some(Typed::Str(value)) = params.remove(&VALUE_PARAM_KEY) { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let (resp_code, session_opt) = self.games.set_config_field(my_pub_id, &field_name.string, &value.string).await; + if let Some(session) = session_opt { + let event = crate::events::CustomGameConfigRefresh { + field: field_name.string.clone(), + value: value.string.clone(), + }; + let session_members_iter = session.users.iter() + .filter(|mem| !mem.is_invited && mem.public_id != my_pub_id) + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(session_members_iter, event).await; + } + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); + } + } else { + log::warn!("Missing custom game field name parameter string"); + } + Ok(params) + } +} + +pub(super) fn game_adjust_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameConfigChanger { + games: init_ctx.custom_games.clone(), + mesh: init_ctx.user_mesh.clone(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_can_join_queue.rs b/rc_services_room/src/operations/custom_game_can_join_queue.rs new file mode 100644 index 0000000..dfa9c72 --- /dev/null +++ b/rc_services_room/src/operations/custom_game_can_join_queue.rs @@ -0,0 +1,43 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 153; + +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out + +pub(super) struct CustomGameCanJoinQueue { + games: std::sync::Arc, + validator: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameCanJoinQueue { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let resp_code = if let Some(session) = self.games.get_user_game(my_pub_id).await { + let vehicle = user_info.selected_vehicle_data().await?; + match self.validator.validate(&vehicle.robot_data, &vehicle.colour_data) { + oj_rc_plugins::vehicle_validation::ValidationResultCode::Ok => crate::data::custom_games::CheckCanJoinQueueResponseCode::Ok, + err_code => { + log::debug!("Failed to validate user {} vehicle for custom game {}: {:?}", my_pub_id, session.session_id, err_code); + crate::data::custom_games::CheckCanJoinQueueResponseCode::Unbalanced + } + } + } else { + crate::data::custom_games::CheckCanJoinQueueResponseCode::UserNotInSession0 + }; + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); + Ok(params) + } +} + +pub(super) fn game_can_queue_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameCanJoinQueue { + games: init_ctx.custom_games.clone(), + validator: init_ctx.vehicle_validators.custom_game.clone(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_create.rs b/rc_services_room/src/operations/custom_game_create.rs new file mode 100644 index 0000000..fb1d59c --- /dev/null +++ b/rc_services_room/src/operations/custom_game_create.rs @@ -0,0 +1,38 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 143; + +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out +const IDK_PARAM_KEY: u8 = 169; // ???; out (let's send back the game id) + +pub(super) struct CustomGameCreator { + games: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameCreator { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let result = self.games.create_game(user_info.public_id()).await; + match result { + Err(e) => { + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(e as _)); + }, + Ok(game_id) => { + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(crate::data::custom_games::SessionCreateResponseCode::SessionCreated as _)); + params.insert(IDK_PARAM_KEY, Typed::Str(game_id.into())); + } + } + Ok(params) + } +} + +pub(super) fn game_create_provider(games: &std::sync::Arc) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameCreator { + games: games.to_owned(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_invite_respond.rs b/rc_services_room/src/operations/custom_game_invite_respond.rs new file mode 100644 index 0000000..d63ad4e --- /dev/null +++ b/rc_services_room/src/operations/custom_game_invite_respond.rs @@ -0,0 +1,53 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 148; + +const ACCEPT_PARAM_KEY: u8 = 173; // bool; in +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out + +pub(super) struct CustomGameInviteResponder { + games: std::sync::Arc, + mesh: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameInviteResponder { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Bool(is_accept)) = params.remove(&ACCEPT_PARAM_KEY) { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let (resp_code, session_opt) = self.games.update_invite_user(my_pub_id, is_accept).await; + if let Some(session) = session_opt { + if !is_accept { + let event = crate::events::CustomGameInviteDecline { + public_id: my_pub_id.to_owned(), + }; + let session_members_iter = session.users.iter() + .filter(|mem| !mem.is_invited && mem.public_id != my_pub_id) + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(session_members_iter, event).await; + } + let event = crate::events::CustomGameRefresh { + session: session.session_id, + }; + let other_session_members_iter = session.users.iter() + .filter(|mem| !mem.is_invited && mem.public_id != my_pub_id) + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(other_session_members_iter, event).await; + } + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); + } + Ok(params) + } +} + +pub(super) fn game_invite_respond_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameInviteResponder { + games: init_ctx.custom_games.clone(), + mesh: init_ctx.user_mesh.clone(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_invite_to.rs b/rc_services_room/src/operations/custom_game_invite_to.rs new file mode 100644 index 0000000..eadf61d --- /dev/null +++ b/rc_services_room/src/operations/custom_game_invite_to.rs @@ -0,0 +1,70 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 147; + +const INVITEE_PARAM_KEY: u8 = 171; // str; in +const IS_TEAM_A_PARAM_KEY: u8 = 175; // bool; in +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out + +pub(super) struct CustomGameInviter { + games: std::sync::Arc, + mesh: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameInviter { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(invitee_id)) = params.remove(&INVITEE_PARAM_KEY) { + if let Some(Typed::Bool(is_team_a)) = params.remove(&IS_TEAM_A_PARAM_KEY) { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let avatars = user_info.list_avatar_info(&[my_pub_id.to_owned(), invitee_id.string.clone()]).await?; + // TODO resolve public_id of invitee by provided display name + let resp_code = if avatars.iter().find(|x| x.public_id == invitee_id.string).is_none() { + crate::data::custom_games::InviteToCustomGameResponseCode::UserDoesNotExist + } else { + let (resp_code, session_opt) = self.games.invite_user(my_pub_id, &invitee_id.string, is_team_a).await; + if let Some(session) = session_opt { + if self.mesh.is_user_online(&invitee_id.string).await { + let my_avatar = avatars.iter().find(|x| x.public_id == my_pub_id).unwrap(); + log::debug!("User {} invited {} to custom game {}", my_pub_id, invitee_id.string, session.session_id); + let event = crate::events::CustomGameInvite { + inviter_public_id: my_pub_id.to_owned(), + inviter_display_name: user_info.display_name().to_owned(), + session: session.session_id.clone(), + avatar_id: my_avatar.avatar_id, + invited_to_team_a: is_team_a, + }; + self.mesh.send_event_to(&invitee_id.string, event).await; + let event = crate::events::CustomGameRefresh { + session: session.session_id, + }; + let other_members = session.users.iter() + .filter(|mem| !mem.is_invited) + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(other_members, event).await; + resp_code + } else { + crate::data::custom_games::InviteToCustomGameResponseCode::UserIsNotOnline + } + } else { + resp_code + } + }; + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); + } + } + Ok(params) + } +} + +pub(super) fn game_invite_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameInviter { + games: init_ctx.custom_games.clone(), + mesh: init_ctx.user_mesh.clone(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_kick.rs b/rc_services_room/src/operations/custom_game_kick.rs new file mode 100644 index 0000000..19bfb35 --- /dev/null +++ b/rc_services_room/src/operations/custom_game_kick.rs @@ -0,0 +1,49 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 150; + +const TO_KICK_PARAM_KEY: u8 = 183; // str; in +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out + +pub(super) struct CustomGameMemberKicker { + games: std::sync::Arc, + mesh: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameMemberKicker { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(user_to_kick)) = params.remove(&TO_KICK_PARAM_KEY) { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let (resp_code, session_opt) = self.games.kick_from_game(my_pub_id, &user_to_kick.string).await; + if let Some((kick_info, session)) = session_opt { + log::debug!("User {} kicked {} from custom game {}", my_pub_id, user_to_kick.string, session.session_id); + let kick_event = crate::events::CustomGameKick { + session: kick_info.session_id, + was_invited: kick_info.was_invited, + }; + self.mesh.send_event_to(&user_to_kick.string, kick_event).await; + let update_event = crate::events::CustomGameRefresh { + session: session.session_id, + }; + let members_iter = session.users.iter() + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(members_iter, update_event).await; + } + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); + } + Ok(params) + } +} + +pub(super) fn game_kick_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameMemberKicker { + games: init_ctx.custom_games.clone(), + mesh: init_ctx.user_mesh.clone(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_leave.rs b/rc_services_room/src/operations/custom_game_leave.rs new file mode 100644 index 0000000..1474387 --- /dev/null +++ b/rc_services_room/src/operations/custom_game_leave.rs @@ -0,0 +1,39 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 145; + +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out + +pub(super) struct CustomGameLeaver { + games: std::sync::Arc, + mesh: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameLeaver { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let (resp_code, session_opt) = self.games.leave_game(user_info.public_id()).await; + if let Some(session) = session_opt { + let event = crate::events::CustomGameRefresh { + session: session.session_id, + }; + let session_members = session.users.iter() + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(session_members, event).await; + } + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); + Ok(params) + } +} + +pub(super) fn game_leave_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameLeaver { + games: init_ctx.custom_games.clone(), + mesh: init_ctx.user_mesh.clone(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_player_state.rs b/rc_services_room/src/operations/custom_game_player_state.rs new file mode 100644 index 0000000..344ba41 --- /dev/null +++ b/rc_services_room/src/operations/custom_game_player_state.rs @@ -0,0 +1,46 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 152; + +const NEW_STATE_PARAM_KEY: u8 = 188; // int enum; in + +pub(super) struct CustomGameMemberStateUpdate { + games: std::sync::Arc, + mesh: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameMemberStateUpdate { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Int(new_state)) = params.remove(&NEW_STATE_PARAM_KEY) { + let new_status_enum = crate::data::custom_games::PlayerSessionStatus::from_u8(new_state as _) + .ok_or_else(|| SimpleOpError::with_message( + oj_rc_core::data::error_codes::WebServicesError::UnexpectedError as _, + format!("Unrecognized PlayerSessionStatus {}", new_state), + ))?; + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let session_opt = self.games.update_user_status(my_pub_id, new_status_enum).await; + if let Some(session) = session_opt { + let update_event = crate::events::CustomGameRefresh { + session: session.session_id, + }; + let members_iter = session.users.iter() + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(members_iter, update_event).await; + } + } + Ok(params) + } +} + +pub(super) fn game_player_status_update_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameMemberStateUpdate { + games: init_ctx.custom_games.clone(), + mesh: init_ctx.user_mesh.clone(), + }) +} diff --git a/rc_services_room/src/operations/custom_game_session.rs b/rc_services_room/src/operations/custom_game_session.rs index 316b1de..bf85ebf 100644 --- a/rc_services_room/src/operations/custom_game_session.rs +++ b/rc_services_room/src/operations/custom_game_session.rs @@ -1,13 +1,71 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed}; -const RESPONSE_CODE_PARAM_KEY: u8 = 168; -//const CUSTOM_GAME_DATA_PARAM_KEY: u8 = 169; +const CODE: u8 = 144; -pub(super) fn get_custom_session_provider() -> SimpleFunc<144, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(0 /* Not in any session */)); - Ok(params.into()) +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out +const RESPONSE_DATA_PARAM_KEY: u8 = 169; // hashtable; out + +pub(super) struct CustomGameRetriever { + games: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameRetriever { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let game_opt = self.games.get_user_game(my_pub_id).await; + if let Some(game) = game_opt { + log::debug!("User {} retrieved their custom game session {} info", my_pub_id, game.session_id); + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(crate::data::custom_games::SessionRetrieveResponse::SessionRetrieved as _)); + let pub_ids: Vec = game.users.iter() + .map(|member| member.public_id.clone()) + .collect(); + let avatars = user_info.list_avatar_info(&pub_ids).await?; + let avatar_map: std::collections::HashMap<_, _> = avatars.into_iter() + .map(|avatar| (avatar.public_id.clone(), avatar)).collect(); + let resp = crate::data::custom_games::Session { + leader: game.users.first().map(|leader| leader.public_id.clone()).unwrap_or_default(), + session: game.session_id, + members: game.users.iter().map(|mem| mem.public_id.clone()).collect(), + members_display_name: game.users.iter() + .filter_map(|mem| avatar_map.get(&mem.public_id)) + .map(|mem| mem.display_name.clone()) + .collect(), + invited: game.users.iter() + .filter(|mem| mem.is_invited) + .map(|mem| mem.public_id.clone()) + .collect(), + team_b_members: game.users.iter() + .filter(|mem| mem.team == 1) + .map(|mem| mem.public_id.clone()) + .collect(), + config: game.config, + avatar_info: avatar_map.iter() + .map(|(pub_id, avatar)| (pub_id.to_owned(), oj_rc_core::data::player_data::AvatarInfo { + avatar_id: avatar.avatar_id, + })) + .collect(), + player_session_state: game.users.iter() + .map(|mem| (mem.public_id.clone(), mem.state)) + .collect(), + }; + params.insert(RESPONSE_DATA_PARAM_KEY, resp.as_transmissible()); + } else { + log::debug!("User {} is not in any custom game session", my_pub_id); + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(crate::data::custom_games::SessionRetrieveResponse::UserNotInAnySession as _)); + } + Ok(params) + } +} + +pub(super) fn custom_session_provider(games: &std::sync::Arc) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameRetriever { + games: games.to_owned(), }) } + diff --git a/rc_services_room/src/operations/custom_games_invite.rs b/rc_services_room/src/operations/custom_games_invite.rs index 4b40e1a..86c109c 100644 --- a/rc_services_room/src/operations/custom_games_invite.rs +++ b/rc_services_room/src/operations/custom_games_invite.rs @@ -1,15 +1,56 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed}; use crate::data::custom_games::*; -const PARAM_KEY: u8 = 168; -//const INVITE_PARAM_KEY: u8 = 189; // hashtable (refer to C# CheckIfHasBeenInvitedToCustomGameSessionRequest) +const CODE: u8 = 0; -pub(super) fn pending_invite_provider() -> SimpleFunc<0, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(PARAM_KEY, Typed::Int(CustomGameInviteCode::NoInvite as _)); - Ok(params.into()) +const RESULT_CODE_PARAM_KEY: u8 = 168; // int enum; out +const INVITE_PARAM_KEY: u8 = 189; // hashtable (refer to C# CheckIfHasBeenInvitedToCustomGameSessionRequest) + +pub(super) struct CustomGamePendingInvites { + games: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGamePendingInvites { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + if let Some(session) = self.games.get_user_game(my_pub_id).await { + let myself = session.users.iter().find(|u| u.public_id == my_pub_id).unwrap(); + if myself.is_invited { + log::debug!("User {} has checked and is invited to custom game {}", my_pub_id, session.session_id); + params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(CustomGameInviteCode::PendingInvite as _)); + // build invite data + let leader = session.users.first().unwrap(); + let leader_avatar = user_info.list_avatar_info(&[leader.public_id.clone()]).await?; + let resp = CustomGameInvite { + inviter_public_id: leader_avatar[0].public_id.clone(), + inviter_display_name: leader_avatar[0].display_name.clone(), + session: session.session_id, + avatar_id: leader_avatar[0].avatar_id, + invited_to_team_b: myself.team == 1, + }; + params.insert(INVITE_PARAM_KEY, resp.as_transmissible()); + } else { + log::debug!("User {} has already accepted invite to custom game {}", my_pub_id, session.session_id); + params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(CustomGameInviteCode::NoInvite as _)); + } + } else { + log::debug!("User {} is not invited to any custom game", my_pub_id); + params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(CustomGameInviteCode::NoInvite as _)); + } + Ok(params) + } +} + +pub(super) fn pending_invite_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGamePendingInvites { + games: init_ctx.custom_games.clone(), }) } + diff --git a/rc_services_room/src/operations/custom_games_maps.rs b/rc_services_room/src/operations/custom_games_maps.rs index 74df9c1..857e79a 100644 --- a/rc_services_room/src/operations/custom_games_maps.rs +++ b/rc_services_room/src/operations/custom_games_maps.rs @@ -1,11 +1,20 @@ use polariton_server::operations::SimpleFunc; use polariton::{operation::{Dict, ParameterTable, Typed}, serdes::TypePrefix}; -use crate::data::custom_games::*; - const MODE_MAP_PARAM_KEY: u8 = 170; const MAP_NAMES_PARAM_KEY: u8 = 178; +const ALL_MAPS: &[&str] = &[ + "RC_Planet_Mars_01_CTF", // og flat mars + "RC_Planet_Mars_02_BA", // the one with the bridge in the middle + "RC_Planet_Mars_03_BA", // tharsis rift without the rift + "RC_Planet_Neptune_01_CTF", // og flat GJ1214b gliese lake without the lake + "RC_Planet_Neptune_02_BA", // the one with the cave + "RC_Planet_Neptune_03_BA", // spitzer dam + "RC_Planet_Earth_01_BA", // birmingham power station + "RC_Planet_Earth_02_BA", // vanguard +]; + pub(super) fn allowed_maps_provider() -> SimpleFunc<146, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { SimpleFunc::new(|params, _| { let mut params = params.to_dict(); @@ -13,19 +22,33 @@ pub(super) fn allowed_maps_provider() -> SimpleFunc<146, crate::UserTy, impl (Fn key_ty: TypePrefix::Str, // str val_ty: TypePrefix::ObjArr, // obj arr items: vec![ - (Typed::Str(GameMode::BattleArena.as_str().into()), Typed::ObjArr(vec![ - Typed::Str("RC_Planet_Neptune_02_BA".into()), - Typed::Str("RC_Planet_Mars_03_BA".into()), - Typed::Str("RC_Planet_Mars_02_BA".into()), - Typed::Str("RC_Planet_Earth_02_BA".into()), - Typed::Str("RC_Planet_Earth_01_BA".into()), - Typed::Str("RC_Planet_Neptune_03_BA".into()), - ].into()) + (Typed::Str(oj_rc_core::data::game_mode::GameMode::BattleArena.as_str().into()), Typed::ObjArr( + ALL_MAPS.iter() + .map(|x| Typed::Str(x.into())) + .collect::>() + .into() + ) ), - (Typed::Str(GameMode::TeamDeathmatch.as_str().into()), Typed::ObjArr(vec![ - Typed::Str("RC_Planet_Neptune_01_CTF".into()), - Typed::Str("RC_Planet_Mars_01_CTF".into()), - ].into()) + (Typed::Str(oj_rc_core::data::game_mode::GameMode::TeamDeathmatch.as_str().into()), Typed::ObjArr( + ALL_MAPS.iter() + .map(|x| Typed::Str(x.into())) + .collect::>() + .into() + ) + ), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::Pit.as_str().into()), Typed::ObjArr( + ALL_MAPS.iter() + .map(|x| Typed::Str(x.into())) + .collect::>() + .into() + ) + ), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::SuddenDeath.as_str().into()), Typed::ObjArr( + ALL_MAPS.iter() + .map(|x| Typed::Str(x.into())) + .collect::>() + .into() + ) ), ], })); diff --git a/rc_services_room/src/operations/custom_games_team.rs b/rc_services_room/src/operations/custom_games_team.rs index d705b80..54c9434 100644 --- a/rc_services_room/src/operations/custom_games_team.rs +++ b/rc_services_room/src/operations/custom_games_team.rs @@ -1,8 +1,6 @@ use polariton_server::operations::SimpleFunc; use polariton::{operation::{Dict, ParameterTable, Typed}, serdes::TypePrefix}; -use crate::data::custom_games::*; - const PARAM_KEY: u8 = 168; pub(super) fn team_setup_provider() -> SimpleFunc<162, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { @@ -12,13 +10,13 @@ pub(super) fn team_setup_provider() -> SimpleFunc<162, crate::UserTy, impl (Fn(P key_ty: TypePrefix::Str, // str val_ty: TypePrefix::Int, // int items: vec![ - (Typed::Str(GameMode::BattleArena.as_str().into()), Typed::Int(10)), - (Typed::Str(GameMode::SuddenDeath.as_str().into()), Typed::Int(10)), - (Typed::Str(GameMode::Pit.as_str().into()), Typed::Int(10)), - (Typed::Str(GameMode::TestMode.as_str().into()), Typed::Int(10)), - (Typed::Str(GameMode::SinglePlayer.as_str().into()), Typed::Int(1)), - (Typed::Str(GameMode::TeamDeathmatch.as_str().into()), Typed::Int(10)), - (Typed::Str(GameMode::Campaign.as_str().into()), Typed::Int(6)), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::BattleArena.as_str().into()), Typed::Int(10)), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::SuddenDeath.as_str().into()), Typed::Int(10)), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::Pit.as_str().into()), Typed::Int(20)), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::TestMode.as_str().into()), Typed::Int(10)), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::SinglePlayer.as_str().into()), Typed::Int(1)), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::TeamDeathmatch.as_str().into()), Typed::Int(10)), + (Typed::Str(oj_rc_core::data::game_mode::GameMode::Campaign.as_str().into()), Typed::Int(1)), ] })); Ok(params.into()) }) diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index e1e16f9..a3c615b 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -108,6 +108,14 @@ mod crf_make_featured; mod crf_unmake_featured; mod tech_tree_unlock_cube; mod crf_remove; +mod custom_game_create; +mod custom_game_adjust; +mod custom_game_leave; +mod custom_game_invite_to; +mod custom_game_invite_respond; +mod custom_game_kick; +mod custom_game_player_state; +mod custom_game_can_join_queue; use polariton_server::operations::OperationsHandler; @@ -115,7 +123,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler OperationsHandler::new() .modify(oj_rc_core::polariton::RcOpModifier) .add(eac::EacChallengeIgnorer) - .add(more_auth::MoreLobbyAuth) + .add(more_auth::more_auth_provider(&init_ctx.user_mesh)) .add(versioner::version_teller(&init_ctx.cubes)) .add(maintenancer::maintenace_teller(&init_ctx.cubes)) .add(game_quality::QualityConfigTeller) @@ -154,7 +162,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(dev_message::dev_message_provider(&init_ctx.cubes)) .add(custom_games_maps::allowed_maps_provider()) .add(avatar_info::avatar_get_provider()) - .add(custom_game_session::get_custom_session_provider()) + .add(custom_game_session::custom_session_provider(&init_ctx.custom_games)) .add(user_xp::get_user_xp_provider()) .add(garage_upgrades::garage_upgrades_provider(&init_ctx.cubes)) .add(game_event_params::event_system_params_provider(&init_ctx.cubes)) @@ -174,8 +182,8 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(robot_mastery_settings::robot_mastery_settings_provider()) .add(player_started_purchase::started_purchase_provider()) .add(custom_games_team::team_setup_provider()) - .add(polariton_server::operations::Ack::<152, _>::default()) // custom game player state changed (188 is desired state) - .add(custom_games_invite::pending_invite_provider()) + //.add(polariton_server::operations::Ack::<152, _>::default()) // custom game player state changed (188 is desired state) + .add(custom_games_invite::pending_invite_provider(init_ctx)) .add(chat_settings::chat_settings_provider()) .add(polariton_server::operations::Ack::<19, _>::default()) // save chat settings .add(prebuilt_robots::garage_robot_data_provider()) @@ -240,4 +248,12 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(tech_tree_unlock_cube::tech_tree_cube_unlock_provider(&init_ctx.cubes)) .add(crf_remove::factory_remove_provider(&init_ctx.factory)) .add(polariton_server::operations::Ack::<94, _>::default()) // ReportCommunityShopItemRequest FIXME: log reports + .add(custom_game_create::game_create_provider(&init_ctx.custom_games)) + .add(custom_game_adjust::game_adjust_provider(init_ctx)) + .add(custom_game_leave::game_leave_provider(init_ctx)) + .add(custom_game_invite_to::game_invite_provider(init_ctx)) + .add(custom_game_invite_respond::game_invite_respond_provider(init_ctx)) + .add(custom_game_kick::game_kick_provider(init_ctx)) + .add(custom_game_player_state::game_player_status_update_provider(init_ctx)) + .add(custom_game_can_join_queue::game_can_queue_provider(init_ctx)) } diff --git a/rc_services_room/src/operations/more_auth.rs b/rc_services_room/src/operations/more_auth.rs index 8b2b014..82247f5 100644 --- a/rc_services_room/src/operations/more_auth.rs +++ b/rc_services_room/src/operations/more_auth.rs @@ -1,7 +1,15 @@ use polariton::operation::Typed; use polariton_server::operations::{Operation, OperationCode}; -pub struct MoreLobbyAuth; +pub struct MoreLobbyAuth { + mesh: std::sync::Arc, +} + +pub fn more_auth_provider(mesh: &std::sync::Arc) -> MoreLobbyAuth { + MoreLobbyAuth { + mesh: mesh.to_owned() + } +} impl MoreLobbyAuth { const AUTH_PAYLOAD_KEY: u8 = 245; @@ -27,6 +35,11 @@ impl Operation for MoreLobbyAuth { } else { match user_info.webservice_listener().await { Ok(listener) => { + self.mesh.add_user( + user_info.public_id().to_owned(), + user.event_sender().to_owned().downgrade(), + ).await; + crate::ONLINE_USERS.store(self.mesh.user_count().await as u64, std::sync::atomic::Ordering::SeqCst); crate::update_status(user_info.as_ref().as_ref()).await; let mut resp_params = std::collections::HashMap::with_capacity(1); resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); @@ -49,7 +62,8 @@ impl Operation for MoreLobbyAuth { }, } } - + } else { + log::debug!("Authentication failed for user"); } } polariton::operation::OperationResponse { diff --git a/rc_services_room/src/user_service.rs b/rc_services_room/src/user_service.rs new file mode 100644 index 0000000..ef10233 --- /dev/null +++ b/rc_services_room/src/user_service.rs @@ -0,0 +1,82 @@ +pub struct UserMesh { + online_users: tokio::sync::RwLock>, +} + +struct UserHandle { + emitter: polariton_server::events::WeakEventEmitter, + is_alive: std::sync::atomic::AtomicBool, +} + +impl UserMesh { + pub fn new() -> Self { + Self { + online_users: tokio::sync::RwLock::new(std::collections::HashMap::new()), + } + } + + pub async fn user_count(&self) -> usize { + self.online_users.read().await.values() + .filter(|u| u.is_alive.load(std::sync::atomic::Ordering::Relaxed)) + .count() + } + + /// returns whether the user was replaced (true) or new (false) + pub async fn add_user( + &self, + public_id: String, + emitter: polariton_server::events::WeakEventEmitter, + ) -> bool { + let handle = UserHandle { + emitter, + is_alive: std::sync::atomic::AtomicBool::new(true), + }; + self.online_users.write().await + .insert(public_id, handle) + .is_some() + } + + /// returns whether the user existing (true) or not (false) + pub async fn remove_user( + &self, + public_id: String, + ) -> bool { + self.online_users.write().await + .remove(&public_id) + .is_some() + } + + pub async fn broadcast_event_to(&self, public_ids: impl std::iter::Iterator, event: impl polariton_server::events::IntoEvent<()> + Clone) -> bool { + let user_lock = self.online_users.read().await; + let mut total_success = true; + for public_id in public_ids { + let is_success = if let Some(user_handle) = user_lock.get(public_id) { + let is_success = user_handle.emitter.emit(event.clone()); + user_handle.is_alive.swap(is_success, std::sync::atomic::Ordering::SeqCst); + is_success + } else { + false + }; + total_success &= is_success; + } + total_success + } + + pub async fn send_event_to(&self, public_id: &str, event: impl polariton_server::events::IntoEvent<()>) -> bool { + let user_lock = self.online_users.read().await; + if let Some(user_handle) = user_lock.get(public_id) { + let is_success = user_handle.emitter.emit(event); + user_handle.is_alive.swap(is_success, std::sync::atomic::Ordering::SeqCst); + is_success + } else { + false + } + } + + pub async fn is_user_online(&self, public_id: &str) -> bool { + if let Some(user) = self.online_users.read().await.get(public_id) { + user.is_alive.load(std::sync::atomic::Ordering::Relaxed) + } else { + false + } + } +} diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index f5f18d7..95ae04a 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -36,6 +36,7 @@ mod clan_invite_decline_all; mod clan_invite_cancel; mod clan_member_rerank; mod clan_experience_poll; +mod user_can_be_custom_gamed; use polariton_server::operations::OperationsHandler; @@ -66,6 +67,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler for PlatoonInviter { } } else { // create new platoon - let platoon_id = format!("{}-{}", user_info.public_id(), chrono::Utc::now().timestamp()); - log::debug!("Creating new platoon {} for user {} to invite {}", platoon_id, user_info.public_id(), username.string); let social_infos = user_info.list_social_info(&[ username.string.clone(), user_info.public_id().to_owned(), ]).await?; if social_infos.len() != 2 { - log::debug!("User {} info could not be retrieved while creating platoon {}", username.string, platoon_id); + log::debug!("User {} info could not be retrieved while creating new platoon", username.string); return Err(SimpleOpError::with_message( oj_rc_core::data::error_codes::SocialErrorCode::UserDoesNotExist as i16, "User's info could not be retrieved".to_owned(), )); } - if self.social.create_platoon(&platoon_id, user_info.public_id()).await.is_none() { + let platoon_id = if let Some((_, platoon_key)) = self.social.create_platoon(user_info.public_id()).await { + log::debug!("Created new platoon {} for user {} to invite {}", platoon_key, user_info.public_id(), username.string); + platoon_key + } else { return Err(SimpleOpError::with_message( oj_rc_core::data::error_codes::SocialErrorCode::UserNotInPlatoon as i16, "Failed to create platoon".to_owned(), )); - } + }; if let Some(timestamp) = self.social.add_user_to_platoon(&username.string, &platoon_id, crate::data::platoon::MemberStatus::Invited).await { (platoon_id, social_infos, timestamp) } else { diff --git a/rc_social_room/src/operations/user_can_be_custom_gamed.rs b/rc_social_room/src/operations/user_can_be_custom_gamed.rs new file mode 100644 index 0000000..3c0a958 --- /dev/null +++ b/rc_social_room/src/operations/user_can_be_custom_gamed.rs @@ -0,0 +1,42 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 58; + +const USERNAME_PARAM_KEY: u8 = 65; // str; in +const RESPONSE_CODE_PARAM_KEY: u8 = 66; // bool; out + +pub(super) struct UserCanBeInvitedToCustoGameGetter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for UserCanBeInvitedToCustoGameGetter { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(username)) = params.remove(&USERNAME_PARAM_KEY) { + let _user_info = user.user()?; // just to validate request is authenticated + let mut set = std::collections::HashSet::with_capacity(1); + set.insert(username.string.clone()); + self.social.filter_online_only(&mut set).await; + if set.is_empty() { + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Bool(false)); + return Ok(params); + } + if self.social.platoon_of_user(&username.string).await.is_some() { + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Bool(false)); + return Ok(params); + } + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Bool(true)); + } + Ok(params) + } +} + +pub(super) fn can_invite_to_custom_game_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(UserCanBeInvitedToCustoGameGetter { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/social_services.rs b/rc_social_room/src/social_services.rs index 2345b66..7bd2d3b 100644 --- a/rc_social_room/src/social_services.rs +++ b/rc_social_room/src/social_services.rs @@ -27,6 +27,11 @@ pub struct PlatoonMemberInfo { pub timestamp: i64, } +fn platoon_key(creator: &str) -> String { + let now = chrono::Utc::now().timestamp(); + format!("{}_{}_p", creator, now) +} + impl SocialMesh { #[allow(clippy::new_without_default)] pub fn new() -> Self { @@ -121,13 +126,14 @@ impl SocialMesh { } } - pub async fn create_platoon(&self, platoon_id: &str, public_id: &str) -> Option { + pub async fn create_platoon(&self, public_id: &str) -> Option<(i64, String)> { /*let user_handle = if let Some(handle) = self.users.read().await.get(public_id) { handle.to_owned() } else { return None; };*/ - if self.platoons.platoon_by_id.read().await.contains_key(platoon_id) { + let platoon_id = platoon_key(public_id); + if self.platoons.platoon_by_id.read().await.contains_key(&platoon_id) { return None; } let mut platoon_members = Vec::with_capacity(5); @@ -140,7 +146,7 @@ impl SocialMesh { }); self.platoons.platoon_by_id.write().await.insert(platoon_id.to_owned(), platoon_members); self.platoons.platoon_by_user.write().await.insert(public_id.to_owned(), platoon_id.to_owned()); - Some(timestamp) + Some((timestamp, platoon_id)) } pub async fn remove_user_from_platoon(&self, public_id: &str) -> bool {