diff --git a/rc_chat_room/src/operations/chat_ignores.rs b/rc_chat_room/src/operations/chat_ignores.rs index c9e2233..535c2da 100644 --- a/rc_chat_room/src/operations/chat_ignores.rs +++ b/rc_chat_room/src/operations/chat_ignores.rs @@ -8,6 +8,7 @@ pub(super) fn ignores_provider() -> SimpleFunc<8, crate::UserTy, impl (Fn(Parame let mut params = params.to_dict(); params.insert(PARAM_KEY, Typed::Arr(Arr { ty: polariton::serdes::TypePrefix::Str, + custom_ty: None, items: vec![ Typed::Str("Pluto".into()), ], diff --git a/rc_core/src/data/channel.rs b/rc_core/src/data/channel.rs index 7a4ed41..d29e4a1 100644 --- a/rc_core/src/data/channel.rs +++ b/rc_core/src/data/channel.rs @@ -12,6 +12,7 @@ impl ChatChannelInfo { (Typed::Str("channelName".into()), Typed::Str(self.channel_name.clone().into())), (Typed::Str("members".into()), Typed::Arr(Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashtable + custom_ty: None, items: self.members.iter().map(|x| x.as_transmissible()).collect(), })), (Typed::Str("channelType".into()), Typed::Int(self.channel_ty as _)), diff --git a/rc_core/src/data/garage_bay.rs b/rc_core/src/data/garage_bay.rs index a9dbc71..4114f8b 100644 --- a/rc_core/src/data/garage_bay.rs +++ b/rc_core/src/data/garage_bay.rs @@ -32,6 +32,7 @@ impl GarageSlotInfo { (Typed::Str("wasRated".into()), Typed::Bool(self.was_rated)), (Typed::Str("movementCategories".into()), Typed::Arr(Arr { ty: TypePrefix::Int, // int + custom_ty: None, items: self.movement_categories.iter().map(|x| Typed::Int(x.but_bigger())).collect(), })), (Typed::Str("uniqueId1".into()), Typed::Int(self.uuid.0 as i32)), @@ -49,6 +50,7 @@ impl GarageSlotInfo { (Typed::Str("baySkinId".into()), Typed::Str(self.bay_skin_id.clone().into())), (Typed::Str("weaponOrder".into()), Typed::Arr(Arr { ty: TypePrefix::Int, // int + custom_ty: None, items: self.weapon_order.iter().map(|x| Typed::Int(*x)).collect(), })), ].into()) @@ -85,6 +87,7 @@ impl ControlOptions { pub fn as_transmissible(&self) -> Typed { Typed::Arr(Arr { ty: TypePrefix::Bool, // bool + custom_ty: None, items: vec![ Typed::Bool(self.vertical_strafing), Typed::Bool(self.sideways_driving), diff --git a/rc_core/src/data/tech_tree.rs b/rc_core/src/data/tech_tree.rs index 49fb2ea..1df381c 100644 --- a/rc_core/src/data/tech_tree.rs +++ b/rc_core/src/data/tech_tree.rs @@ -21,6 +21,7 @@ impl TechTreeNode { (Typed::Str("tp".into()), Typed::Int(self.tech_points as i32)), (Typed::Str("neighbours".into()), Typed::Arr(Arr { ty: TypePrefix::Str, // str + custom_ty: None, items: self.neighbours.iter().map(|cube_id| Typed::Str(hex::encode(cube_id.to_be_bytes()).into())).collect(), })), ].into()) diff --git a/rc_core/src/data/weapon_list.rs b/rc_core/src/data/weapon_list.rs index a9e37c1..2b868ad 100644 --- a/rc_core/src/data/weapon_list.rs +++ b/rc_core/src/data/weapon_list.rs @@ -117,6 +117,7 @@ impl WeaponData { let typed_arr: Vec> = self.group_fire_scales.iter().map(|x| Typed::Float(*x)).collect(); out.push((Typed::Str("groupFireScales".into()), Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Float, + custom_ty: None, items: typed_arr, }))); } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 17bc887..5bbe960 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -284,6 +284,7 @@ impl super::ConfigProvider for CubeConfig { fn public_channels(&self) -> Typed { Typed::Arr(polariton::operation::Arr { ty: TypePrefix::Str, + custom_ty: None, items: self.chat.public_channels.iter().map(|s| Typed::Str(s.into())).collect(), }) } diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index d3b718f..f89710f 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -193,6 +193,7 @@ impl GameEventSequence { GameEventTransmissible { maps: Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Str, + custom_ty: None, items: vec![ Typed::Str(crate::data::game_mode::GameMap::from_persist(item_now.singleplayer.map).as_str().into()), Typed::Str(crate::data::game_mode::GameMap::from_persist(item_now.multiplayer.map).as_str().into()), @@ -200,6 +201,7 @@ impl GameEventSequence { }), visibilities: Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Int, + custom_ty: None, items: vec![ Typed::Int(crate::data::game_mode::MapVisibility::from_persist(item_now.singleplayer.visibility) as _), Typed::Int(crate::data::game_mode::MapVisibility::from_persist(item_now.multiplayer.visibility) as _), @@ -207,6 +209,7 @@ impl GameEventSequence { }), modes: Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Int, + custom_ty: None, items: vec![ Typed::Int(crate::data::game_mode::GameMode::from_persist(item_now.singleplayer.mode) as _), Typed::Int(crate::data::game_mode::GameMode::from_persist(item_now.multiplayer.mode) as _), @@ -214,6 +217,7 @@ impl GameEventSequence { }), auto_heals: Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Bool, + custom_ty: None, items: vec![ Typed::Bool(item_now.singleplayer.auto_heal), Typed::Bool(item_now.multiplayer.auto_heal), diff --git a/rc_core/src/persist/user/chat.rs b/rc_core/src/persist/user/chat.rs index 9ba069b..3eb0a70 100644 --- a/rc_core/src/persist/user/chat.rs +++ b/rc_core/src/persist/user/chat.rs @@ -7,6 +7,7 @@ impl super::ChatUser for UserData { log::info!("User is subscribed to channels {:?}", channels); Ok(polariton::operation::Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashtable + custom_ty: None, items: channels.into_iter().map(|name| crate::data::channel::ChatChannelInfo { channel_name: name, members: vec![ @@ -103,6 +104,7 @@ impl super::ChatUser for UserData { })?; Ok(polariton::operation::Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Str, + custom_ty: None, items: sanctions.into_iter().map(|x| { let data = crate::data::sanction::SanctionJson { type_: crate::data::sanction::SanctionType::from_db(x.descriptor), diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 813c80c..bda478f 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -11,7 +11,7 @@ mod inventory; pub use inventory::{UnlockedParts, UnlockOverride}; mod traits; -pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus}; +pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo}; pub mod intercom; pub use intercom::generate_token as generate_intercom_token; diff --git a/rc_core/src/persist/user/social.rs b/rc_core/src/persist/user/social.rs index fc45d46..46f3e92 100644 --- a/rc_core/src/persist/user/social.rs +++ b/rc_core/src/persist/user/social.rs @@ -57,7 +57,7 @@ impl super::SocialUser for UserData { format!("Failed to retrieve friends: {}", e), ) })?; - let friend_ids: Vec = friends.iter().map(|(_, user)| user.id).collect(); + let friend_ids = friends.iter().map(|(_, user)| user.id); let friend_avatars = self.db.user_auxs_by_user_ids_and_descriptor(friend_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); @@ -81,6 +81,37 @@ impl super::SocialUser for UserData { ) } + async fn list_social_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::SocialErrorCode::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::SocialErrorCode::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() + ) + } + async fn has_unclaimed_match_rewards(&self) -> Result { let count = self.db.count_score_by_user_id_and_claimed(self.account.id, false).await .map_err(|e| { diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 68537a1..0a6f666 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -458,6 +458,7 @@ pub trait SocialUser: Send + Sync { async fn cancel_friend(&self, username: String) -> Result; async fn remove_friend(&self, username: String) -> Result; async fn list_friends(&self) -> Result, polariton_server::operations::SimpleOpError>; + async fn list_social_info(&self, public_ids: &[String]) -> Result, polariton_server::operations::SimpleOpError>; async fn has_unclaimed_match_rewards(&self) -> Result; async fn get_unclaimed_match_rewards(&self) -> Result; async fn claim_match_rewards(&self) -> Result; @@ -498,6 +499,13 @@ pub struct FriendData { pub avatar_id: u32, } +#[derive(Clone)] +pub struct SocialInfo { + pub public_id: String, + pub display_name: String, + pub avatar_id: Option, +} + pub enum FriendInviteStatus { InviteSent, InvitePending, diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index ccc1f52..d7030ed 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -46,6 +46,13 @@ impl Database { .await } + pub async fn users_by_public_id<'a>(&self, public_ids: impl std::iter::Iterator) -> Result, sea_orm::DbErr> { + crate::schema::user::Entity::find() + .filter(crate::schema::user::Column::PublicId.is_in(public_ids)) + .all(self.orm.as_ref()) + .await + } + pub async fn user_by_some_social_id(&self, public_id: String) -> Result, sea_orm::DbErr> { let lower_public_id = public_id.to_lowercase(); crate::schema::user::Entity::find() @@ -113,7 +120,7 @@ impl Database { .await } - pub async fn user_auxs_by_user_ids_and_descriptor(&self, user_ids: impl std::iter::IntoIterator, descriptor: crate::schema::user_aux::Descriptor) -> Result, sea_orm::DbErr> { + pub async fn user_auxs_by_user_ids_and_descriptor(&self, user_ids: impl std::iter::Iterator, descriptor: crate::schema::user_aux::Descriptor) -> Result, sea_orm::DbErr> { crate::schema::user_aux::Entity::find() .filter(crate::schema::user_aux::Column::UserId.is_in(user_ids)) .filter(crate::schema::user_aux::Column::Descriptor.eq(descriptor)) diff --git a/rc_lobby_room/src/events/battle_enter.rs b/rc_lobby_room/src/events/battle_enter.rs index e80f5c4..a0b0abb 100644 --- a/rc_lobby_room/src/events/battle_enter.rs +++ b/rc_lobby_room/src/events/battle_enter.rs @@ -30,6 +30,7 @@ impl BattleEnter { fn as_transmissible(&self) -> Vec<(u8, polariton::operation::Typed)> { let player_datas = polariton::operation::Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::HashMap, + custom_ty: None, items: self.player_datas.iter().map(|x| x.as_transmissible()).collect() }); let mut vec = Vec::with_capacity(11); diff --git a/rc_services_room/src/data/player_rank.rs b/rc_services_room/src/data/player_rank.rs index fba3e71..a5b76ff 100644 --- a/rc_services_room/src/data/player_rank.rs +++ b/rc_services_room/src/data/player_rank.rs @@ -15,6 +15,7 @@ impl PlayerRankStaticInfo { (Typed::Str("subRankCount".into()), Typed::Int(self.sub_rank_count)), (Typed::Str("subRankThresholds".into()), Typed::Arr(Arr { ty: TypePrefix::Int, // int + custom_ty: None, items: self.sub_rank_thresholds.iter().map(|x| Typed::Int(*x)).collect(), })), ], diff --git a/rc_services_room/src/operations/all_customisations_info.rs b/rc_services_room/src/operations/all_customisations_info.rs index d4f70f1..13c56e7 100644 --- a/rc_services_room/src/operations/all_customisations_info.rs +++ b/rc_services_room/src/operations/all_customisations_info.rs @@ -192,14 +192,17 @@ pub(super) fn all_customisations_provider() -> SimpleFunc<216, crate::UserTy, im let mut params = params.to_dict(); params.insert(SKINS_KEY, Typed::Arr(Arr { ty: TypePrefix::HashMap, // hashtable + custom_ty: None, items: all_skins().into_iter().map(|x| x.as_transmissible()).collect(), })); params.insert(SPAWNS_KEY, Typed::Arr(Arr { ty: TypePrefix::HashMap, // hashtable + custom_ty: None, items: all_spawns().into_iter().map(|x| x.as_transmissible()).collect(), })); params.insert(DEATHS_KEY, Typed::Arr(Arr { ty: TypePrefix::HashMap, // hashtable + custom_ty: None, items: all_deaths().into_iter().map(|x| x.as_transmissible()).collect(), })); diff --git a/rc_services_room/src/operations/cube_awards.rs b/rc_services_room/src/operations/cube_awards.rs index 3f1bc83..bb132fd 100644 --- a/rc_services_room/src/operations/cube_awards.rs +++ b/rc_services_room/src/operations/cube_awards.rs @@ -8,6 +8,7 @@ pub(super) fn cube_awards_provider() -> SimpleFunc<206, crate::UserTy, impl (Fn( let mut params = params.to_dict(); params.insert(PARAM_KEY, Typed::Arr(Arr { ty: TypePrefix::Str, // str + custom_ty: None, items: Vec::default(), })); Ok(params.into()) diff --git a/rc_services_room/src/operations/item_shop_purchase.rs b/rc_services_room/src/operations/item_shop_purchase.rs index 61708a5..b9d6cfa 100644 --- a/rc_services_room/src/operations/item_shop_purchase.rs +++ b/rc_services_room/src/operations/item_shop_purchase.rs @@ -30,6 +30,7 @@ impl SimpleOperation<()> for ItemBundleBuyer { let purchase_result = user_info.apply_purchase(transaction).await?; let new_cubes = polariton::operation::Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Str, + custom_ty: None, items: purchase_result.cube_awards.into_keys() .map(|k| polariton::operation::Typed::Str(k.into())) .collect(), diff --git a/rc_services_room/src/operations/owned_cosmetics.rs b/rc_services_room/src/operations/owned_cosmetics.rs index b46efa3..a4b6723 100644 --- a/rc_services_room/src/operations/owned_cosmetics.rs +++ b/rc_services_room/src/operations/owned_cosmetics.rs @@ -8,6 +8,7 @@ pub(super) fn owned_cosmetics_provider() -> SimpleFunc<23, crate::UserTy, impl ( let mut params = params.to_dict(); params.insert(PARAM_KEY, Typed::Arr(Arr { ty: TypePrefix::Str, // str + custom_ty: None, items: vec![Typed::Str("1".into())], })); Ok(params.into()) @@ -19,6 +20,7 @@ pub(super) fn selected_cosmetics_provider() -> SimpleFunc<21, crate::UserTy, imp let mut params = params.to_dict(); params.insert(PARAM_KEY, Typed::Arr(Arr { ty: TypePrefix::Str, // str + custom_ty: None, items: vec![Typed::Str("1".into())], })); Ok(params.into()) diff --git a/rc_services_room/src/operations/player_data.rs b/rc_services_room/src/operations/player_data.rs index dc5baf4..8fe319c 100644 --- a/rc_services_room/src/operations/player_data.rs +++ b/rc_services_room/src/operations/player_data.rs @@ -23,12 +23,14 @@ pub(super) fn player_data_provider() -> SimpleFunc<61, crate::UserTy, impl (Fn(P params.insert(CONTROL_OPTIONS_KEY, ControlOptions { vertical_strafing: false, sideways_driving: false, tracks_turn_on_spot: false, }.as_transmissible()); params.insert(WEAPON_ORDER_KEY, Typed::Arr(Arr { ty: TypePrefix::Int, // int + custom_ty: None, items: vec![ Typed::Int(0), ], })); params.insert(ITEM_CATEGORY_KEY, Typed::Arr(Arr { ty: TypePrefix::Int, // int + custom_ty: None, items: vec![ Typed::Int(ItemCategory::Wheel.but_bigger()), ], diff --git a/rc_services_room/src/operations/robot_sanction.rs b/rc_services_room/src/operations/robot_sanction.rs index d9f6267..41cc287 100644 --- a/rc_services_room/src/operations/robot_sanction.rs +++ b/rc_services_room/src/operations/robot_sanction.rs @@ -12,6 +12,7 @@ pub(super) fn robot_sanction_provider() -> SimpleFunc<174, crate::UserTy, impl ( } params.insert(SANCTION_JSONS_PARAM_KEY, Typed::Arr(Arr { ty: TypePrefix::Str, // str + custom_ty: None, items: Vec::default(), })); Ok(params.into()) @@ -23,6 +24,7 @@ pub(super) fn all_robot_sanctions_provider() -> SimpleFunc<176, crate::UserTy, i let mut params = params.to_dict(); params.insert(SANCTION_JSONS_PARAM_KEY, Typed::Arr(Arr { ty: TypePrefix::Str, // str + custom_ty: None, items: Vec::default(), })); Ok(params.into()) diff --git a/rc_services_room/src/operations/steam_promo.rs b/rc_services_room/src/operations/steam_promo.rs index 28f3157..dc0c2f8 100644 --- a/rc_services_room/src/operations/steam_promo.rs +++ b/rc_services_room/src/operations/steam_promo.rs @@ -12,6 +12,7 @@ pub(super) fn steam_promos_provider() -> Immediate { let mut params = std::collections::HashMap::with_capacity(4); params.insert(PROMO_LIST_PARAM_KEY, Typed::Arr(polariton::operation::Arr { ty: polariton::serdes::TypePrefix::Str, + custom_ty: None, items: Vec::default(), })); params.insert(STEAM_PROMOS_PARAM_KEY, Typed::Str("{}".into())); diff --git a/rc_social_room/src/data/custom.rs b/rc_social_room/src/data/custom.rs index a3b7aa2..c945562 100644 --- a/rc_social_room/src/data/custom.rs +++ b/rc_social_room/src/data/custom.rs @@ -1,6 +1,7 @@ #[derive(Debug, Clone)] pub enum CustomType { - FriendInfo(super::friend::FriendInfo), // TODO actually serialise + FriendInfo(super::friend::FriendInfo), + PlatoonMember(super::platoon::PlatoonMemberInfo), Unknown, } @@ -8,7 +9,8 @@ impl CustomType { fn custom_ty(&self) -> u8 { match self { Self::FriendInfo(_) => 0, - Self::Unknown => 1, + Self::PlatoonMember(_) => 1, + Self::Unknown => u8::MAX, } } } @@ -16,31 +18,27 @@ impl CustomType { pub struct CustomTypeSerdes; impl polariton::serdes::CustomSerdes for CustomTypeSerdes { - fn dump(c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result { - w.write_all(&[c.custom_ty()])?; - let mut buf = Vec::new(); + fn custom_ty(c: &CustomType) -> u8 { + c.custom_ty() + } + + fn dump_inner(c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result { let total_written_len = match c { CustomType::FriendInfo(friend) => { - friend.dump(&mut std::io::Cursor::new(&mut buf))? + friend.dump(w)? + }, + CustomType::PlatoonMember(member) => { + member.dump(w)? }, CustomType::Unknown => 0, }; - w.write_all(&(total_written_len as i16).to_be_bytes())?; - w.write_all(&buf)?; - Ok(3 + total_written_len) - /*let payload = vec![ // FIXME don't manually serialize - 0u8, // byte custom type - 0u8, 5u8, // short custom object size - 3u8, 0u8, 0u8, 0u8, 0u8, // content - ];*/ + Ok(total_written_len) } - fn parse(r: &mut dyn std::io::Read) -> std::io::Result { - let mut buf = [0u8; 3]; - r.read_exact(&mut buf)?; - // TODO only read up up to size - match buf[0] { + fn parse_inner(ty: u8, r: &mut dyn std::io::Read) -> std::io::Result { + match ty { 0 => super::friend::FriendInfo::parse(r).map(CustomType::FriendInfo), + 1 => super::platoon::PlatoonMemberInfo::parse(r).map(CustomType::PlatoonMember), _ => Ok(CustomType::Unknown), } } diff --git a/rc_social_room/src/data/mod.rs b/rc_social_room/src/data/mod.rs index 5714350..f873a3c 100644 --- a/rc_social_room/src/data/mod.rs +++ b/rc_social_room/src/data/mod.rs @@ -2,3 +2,4 @@ pub mod friend; pub mod clan_invite; pub mod clan; pub mod custom; +pub mod platoon; diff --git a/rc_social_room/src/data/platoon.rs b/rc_social_room/src/data/platoon.rs new file mode 100644 index 0000000..d99af92 --- /dev/null +++ b/rc_social_room/src/data/platoon.rs @@ -0,0 +1,72 @@ +#[derive(Debug, Clone)] +pub struct PlatoonMemberInfo { + pub public_id: String, + pub display_name: String, + pub status: MemberStatus, + pub added: i64, // timestamp + pub avatar_id: i32, + pub use_custom_avatar: bool, +} + +impl PlatoonMemberInfo { + pub(super) fn dump(&self, w: &mut dyn std::io::Write) -> std::io::Result { + let mut total = 1 + 8 + 4 + 1; + total += oj_rc_core::data::write_str_for_binreader(&self.public_id, w)?; + total += oj_rc_core::data::write_str_for_binreader(&self.display_name, w)?; + w.write_all(&[self.status.as_u8()])?; + w.write_all(&self.added.to_be_bytes())?; + w.write_all(&self.avatar_id.to_be_bytes())?; + w.write_all(&[self.use_custom_avatar as u8])?; + Ok(total) + } + + pub(super) fn parse(r: &mut dyn std::io::Read) -> std::io::Result { + let public_id = oj_rc_core::data::read_str_for_binwriter(r)?; + let display_name = oj_rc_core::data::read_str_for_binwriter(r)?; + let mut buf = [0u8; 1 + 8 + 4 + 1]; + r.read_exact(&mut buf)?; + let status = MemberStatus::from_u8(buf[0]).ok_or_else(|| std::io::Error::other(format!("Invalid platoon member status {}", buf[0])))?; + let added = i64::from_be_bytes([ + buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], + ]); + let avatar_id = i32::from_be_bytes([ + buf[9], buf[10], buf[11], buf[12], + ]); + let use_custom_avatar = buf[13] != 0; + Ok(Self { + public_id, + display_name, + status, + added, + avatar_id, + use_custom_avatar, + }) + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy)] +pub enum MemberStatus { + Invited = 0, + Ready = 1, + InQueue = 2, + InBattle = 3, +} + +impl MemberStatus { + #[inline] + pub fn from_u8(num: u8) -> Option { + match num { + 0 => Some(Self::Invited), + 1 => Some(Self::Ready), + 2 => Some(Self::InQueue), + 3 => Some(Self::InBattle), + _ => None, + } + } + + #[inline] + pub fn as_u8(&self) -> u8 { + *self as u8 + } +} diff --git a/rc_social_room/src/events/mod.rs b/rc_social_room/src/events/mod.rs index 3696761..2a08c19 100644 --- a/rc_social_room/src/events/mod.rs +++ b/rc_social_room/src/events/mod.rs @@ -4,3 +4,11 @@ pub mod friend_invite_declined; pub mod friend_invite_cancelled; pub mod friend_removed; pub mod friend_status; +pub mod platoon_invite_received; +pub mod platoon_member_added; +pub mod platoon_member_left; +pub mod platoon_member_update; +pub mod platoon_removed; +pub mod platoon_leader_changed; +pub mod platoon_member_kick; +pub mod platoon_member_avatar_update; diff --git a/rc_social_room/src/events/platoon_invite_received.rs b/rc_social_room/src/events/platoon_invite_received.rs new file mode 100644 index 0000000..1781985 --- /dev/null +++ b/rc_social_room/src/events/platoon_invite_received.rs @@ -0,0 +1,32 @@ +#[derive(Clone)] +pub struct PlatoonInviteReceived { + pub inviter_public_id: String, + pub inviter_display_name: String, + pub avatar_id: Option, // None when using custom avatar +} + +impl PlatoonInviteReceived { + pub const CODE: u8 = 10; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(4); + params.insert(19, polariton::operation::Typed::Str(self.inviter_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.inviter_display_name.clone().into())); + params.insert(13, polariton::operation::Typed::Bool(self.avatar_id.is_none())); + params.insert(14, polariton::operation::Typed::Int(self.avatar_id.unwrap_or_default())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for PlatoonInviteReceived { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/platoon_leader_changed.rs b/rc_social_room/src/events/platoon_leader_changed.rs new file mode 100644 index 0000000..143051c --- /dev/null +++ b/rc_social_room/src/events/platoon_leader_changed.rs @@ -0,0 +1,29 @@ +#[derive(Clone)] +pub struct PlatoonLeaderChanged { + pub leader_public_id: String, + pub leader_display_name: String, +} + +impl PlatoonLeaderChanged { + pub const CODE: u8 = 16; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(4); + params.insert(17, polariton::operation::Typed::Str(self.leader_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.leader_display_name.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for PlatoonLeaderChanged { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/platoon_member_added.rs b/rc_social_room/src/events/platoon_member_added.rs new file mode 100644 index 0000000..ea19a3b --- /dev/null +++ b/rc_social_room/src/events/platoon_member_added.rs @@ -0,0 +1,27 @@ +#[derive(Clone)] +pub struct PlatoonNewMember { + pub new_member: crate::data::platoon::PlatoonMemberInfo, +} + +impl PlatoonNewMember { + pub const CODE: u8 = 17; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(1); + params.insert(15, polariton::operation::Typed::Custom(crate::data::custom::CustomType::PlatoonMember(self.new_member.clone()))); + params.into() + } +} + +impl polariton_server::events::IntoEvent for PlatoonNewMember { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/platoon_member_avatar_update.rs b/rc_social_room/src/events/platoon_member_avatar_update.rs new file mode 100644 index 0000000..61f9183 --- /dev/null +++ b/rc_social_room/src/events/platoon_member_avatar_update.rs @@ -0,0 +1,30 @@ +#[derive(Clone)] +pub struct PlatoonMemberAvatarChanged { + pub member_public_id: String, + pub avatar_id: Option, +} + +impl PlatoonMemberAvatarChanged { + pub const CODE: u8 = 14; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(2); + params.insert(1, polariton::operation::Typed::Str(self.member_public_id.clone().into())); + params.insert(13, polariton::operation::Typed::Bool(self.avatar_id.is_none())); + params.insert(14, polariton::operation::Typed::Int(self.avatar_id.unwrap_or_default())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for PlatoonMemberAvatarChanged { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/platoon_member_kick.rs b/rc_social_room/src/events/platoon_member_kick.rs new file mode 100644 index 0000000..d13fc2b --- /dev/null +++ b/rc_social_room/src/events/platoon_member_kick.rs @@ -0,0 +1,23 @@ +#[derive(Clone)] +pub struct PlatoonMemberKick; + +impl PlatoonMemberKick { + pub const CODE: u8 = 12; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + polariton::operation::ParameterTable::::with_capacity(0) + } +} + +impl polariton_server::events::IntoEvent for PlatoonMemberKick { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/platoon_member_left.rs b/rc_social_room/src/events/platoon_member_left.rs new file mode 100644 index 0000000..1a83a46 --- /dev/null +++ b/rc_social_room/src/events/platoon_member_left.rs @@ -0,0 +1,29 @@ +#[derive(Clone)] +pub struct PlatoonMemberLeft { + pub member_public_id: String, + pub member_display_name: String, +} + +impl PlatoonMemberLeft { + pub const CODE: u8 = 11; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(2); + params.insert(1, polariton::operation::Typed::Str(self.member_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.member_display_name.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for PlatoonMemberLeft { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/platoon_member_update.rs b/rc_social_room/src/events/platoon_member_update.rs new file mode 100644 index 0000000..4178550 --- /dev/null +++ b/rc_social_room/src/events/platoon_member_update.rs @@ -0,0 +1,31 @@ +#[derive(Clone)] +pub struct PlatoonMemberStatusUpdate { + pub member_public_id: String, + pub member_display_name: String, + pub member_status: crate::data::platoon::MemberStatus, +} + +impl PlatoonMemberStatusUpdate { + pub const CODE: u8 = 18; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(2); + params.insert(1, polariton::operation::Typed::Str(self.member_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.member_display_name.clone().into())); + params.insert(3, polariton::operation::Typed::Int(self.member_status.as_u8() as i32)); + params.into() + } +} + +impl polariton_server::events::IntoEvent for PlatoonMemberStatusUpdate { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/platoon_removed.rs b/rc_social_room/src/events/platoon_removed.rs new file mode 100644 index 0000000..2d4bc58 --- /dev/null +++ b/rc_social_room/src/events/platoon_removed.rs @@ -0,0 +1,27 @@ +#[derive(Clone)] +pub struct PlatoonDisbanded { + pub platoon_id: String, +} + +impl PlatoonDisbanded { + pub const CODE: u8 = 13; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(1); + params.insert(16, polariton::operation::Typed::Str(self.platoon_id.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for PlatoonDisbanded { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/main.rs b/rc_social_room/src/main.rs index 3cc6681..1a61516 100644 --- a/rc_social_room/src/main.rs +++ b/rc_social_room/src/main.rs @@ -85,6 +85,19 @@ async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAdd ONLINE_USERS.store(social.online_count_read().await - 1, std::sync::atomic::Ordering::SeqCst); if let Ok(user_info) = user_state.user() { update_status(user_info.as_ref().as_ref()).await; + if let Some(platoon_id) = social.platoon_of_user(user_info.public_id()).await { + // send platoon leave event to others in platoon + social.remove_user_from_platoon(user_info.public_id()).await; + let remaining_users = social.users_of_platoon(&platoon_id).await; + let event = crate::events::platoon_member_left::PlatoonMemberLeft { + member_public_id: user_info.public_id().to_owned(), + member_display_name: user_info.display_name().to_owned(), + }; + for rem_user in remaining_users { + social.send_event_to(&rem_user.public_id, event.clone()).await; + } + } + if let Ok(friends) = user_info.list_friends().await { for friend in friends { social.send_event_to(&friend.public_id, crate::events::friend_status::FriendStatus { diff --git a/rc_social_room/src/operations/clan_info.rs b/rc_social_room/src/operations/clan_info.rs index eb7b93f..8c54054 100644 --- a/rc_social_room/src/operations/clan_info.rs +++ b/rc_social_room/src/operations/clan_info.rs @@ -16,6 +16,7 @@ pub(super) fn clan_info_provider() -> SimpleFunc<33, crate::User log::debug!("Requested info on clan {}", clan_name.string); params.insert(MEMBERS_PARAM_KEY, Typed::Arr(Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, items: vec![ ClanMember { username: "RE_clan_user_idk0".to_owned(), diff --git a/rc_social_room/src/operations/clan_invite.rs b/rc_social_room/src/operations/clan_invite.rs index 54c0864..441cd4d 100644 --- a/rc_social_room/src/operations/clan_invite.rs +++ b/rc_social_room/src/operations/clan_invite.rs @@ -10,6 +10,7 @@ pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::U let mut params = params.to_dict(); params.insert(PARAM_KEY, Typed::::Arr(Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, /*items: vec![ ClanInviteInfo { username: "RE_user1".to_owned(), diff --git a/rc_social_room/src/operations/friend_list.rs b/rc_social_room/src/operations/friend_list.rs index f2912c9..3dfd1ce 100644 --- a/rc_social_room/src/operations/friend_list.rs +++ b/rc_social_room/src/operations/friend_list.rs @@ -44,6 +44,7 @@ impl SimpleOperation for FriendsLister { // Typed::Custom(crate::data::custom::CustomType::FriendInfo) params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr { ty: polariton::serdes::TypePrefix::Custom, // custom + custom_ty: Some(0), items: friends.iter().map(|friend| Typed::Custom(crate::data::custom::CustomType::FriendInfo(crate::data::friend::FriendInfo { status: crate::data::friend::InviteStatus::from_core(&friend.state), @@ -56,6 +57,7 @@ impl SimpleOperation for FriendsLister { })); params.insert(AVATAR_PARAM_KEY, Typed::Arr(Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, items: friends.iter() .map(|friend| AvatarInfo { diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index 8ad97ce..caf23b8 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -15,6 +15,14 @@ mod friend_accept; mod friend_decline; mod friend_cancel; mod friend_remove; +mod platoon_invite_to; +mod user_can_be_platooned; +mod platoon_invite_accept; +mod platoon_invite_decline; +mod platoon_leave; +mod platoon_kick; +mod platoon_invites; +mod platoon_status; use polariton_server::operations::OperationsHandler; @@ -27,7 +35,8 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler::default()) // get my clan info (this is equivalent to not being in a clan) .add(clan_invite::clan_invites_provider()) - .add(polariton_server::operations::Ack::<19, _>::default()) // get pending platoon invite (this is equivalent to having no pending invite) + //.add(polariton_server::operations::Ack::<19, _>::default()) // get pending platoon invite (this is equivalent to having no pending invite) + .add(platoon_invites::platoon_pending_provider(init_ctx)) .add(clan_info::clan_info_provider()) .add(search_clan::search_clans_provider()) .add(polariton_server::operations::Ack::<52, _>::default()) // validate pending season rewards (this just always needs to be ack-ed) @@ -44,4 +53,11 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler() -> SimpleFunc<19, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(INVITER_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter".into()))); - params.insert(INVITER_DISPLAY_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter_display".into()))); - params.insert(INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY, Typed::Bool(false.into()))); - params.insert(INVITER_AVATAR_ID_NAME_PARAM_KEY, Typed::Int(1))); - Ok(params.into()) - }) -} diff --git a/rc_social_room/src/operations/platoon_invite_accept.rs b/rc_social_room/src/operations/platoon_invite_accept.rs new file mode 100644 index 0000000..50a34d2 --- /dev/null +++ b/rc_social_room/src/operations/platoon_invite_accept.rs @@ -0,0 +1,93 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 11; + +const PLATOON_ID_PARAM_KEY: u8 = 16; // str; out +const PLATOON_LEADER_PARAM_KEY: u8 = 17; // str; out +const PLATOON_MEMBERS_PARAM_KEY: u8 = 7; // array of custom (platoon member); out + +pub(super) struct PlatoonInviteAccepter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for PlatoonInviteAccepter { + 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_public_id = user_info.public_id(); + if let Some(platoon_id) = self.social.platoon_of_user(my_public_id).await { + log::debug!("User {} accepted invite to platoon {}", my_public_id, platoon_id); + let members = self.social.users_of_platoon(&platoon_id).await; + let member_pub_ids: Vec = members.iter().map(|m| m.public_id.clone()).collect(); + let social_infos = user_info.list_social_info(&member_pub_ids).await?; + let plat_members: Vec = members.iter() + .map(|member| { + let soc = social_infos.iter() + .find(|soc| soc.public_id == member.public_id) + .unwrap(); + crate::data::platoon::PlatoonMemberInfo { + public_id: soc.public_id.clone(), + display_name: soc.display_name.clone(), + status: member.status, + added: member.timestamp, + avatar_id: 0, + use_custom_avatar: true, // the game client has a bug that makes it need a custom avatar here + } + }).collect(); + params.insert(PLATOON_ID_PARAM_KEY, Typed::Str(platoon_id.into())); + params.insert(PLATOON_LEADER_PARAM_KEY, Typed::Str( + members.first() + .map(|p| p.public_id.clone()) + .unwrap_or_default() + .into() + )); + params.insert(PLATOON_MEMBERS_PARAM_KEY, Typed::Arr(polariton::operation::Arr { + ty: polariton::serdes::TypePrefix::Custom, + custom_ty: Some(1), + items: plat_members.iter() + .map(|x| Typed::Custom(crate::data::custom::CustomType::PlatoonMember(x.to_owned()))) + .collect(), + })); + self.social.update_user_in_platoon(my_public_id, crate::data::platoon::MemberStatus::Ready).await; + let event = crate::events::platoon_member_update::PlatoonMemberStatusUpdate { + member_public_id: my_public_id.to_owned(), + member_display_name: user_info.display_name().to_owned(), + member_status: crate::data::platoon::MemberStatus::Ready, + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + // send the correct avatars later (to workaround the game client bug) + let social = self.social.clone(); + let my_id = my_public_id.to_owned(); + tokio::task::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(200)).await; // FIXME figure out a better way to run this shortly after the op has returned + for member in plat_members.iter() { + if member.public_id == my_id { continue; } + let social_info = social_infos.iter() + .find(|soc| soc.public_id == member.public_id) + .unwrap(); + if social_info.avatar_id.is_none() { continue; } + let event = crate::events::platoon_member_avatar_update::PlatoonMemberAvatarChanged { + member_public_id: member.public_id.clone(), + avatar_id: social_info.avatar_id, + }; + social.send_event_to(&my_id, event).await; + } + }); + + } + Ok(params) + } +} + +pub(super) fn platoon_accepter_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(PlatoonInviteAccepter { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/platoon_invite_decline.rs b/rc_social_room/src/operations/platoon_invite_decline.rs new file mode 100644 index 0000000..8f06d70 --- /dev/null +++ b/rc_social_room/src/operations/platoon_invite_decline.rs @@ -0,0 +1,52 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::ParameterTable; + +const CODE: u8 = 12; + +pub(super) struct PlatoonInviteDecliner { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for PlatoonInviteDecliner { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let my_public_id = user_info.public_id(); + if let Some(platoon_id) = self.social.platoon_of_user(my_public_id).await { + log::debug!("User {} declined invite to platoon {}", my_public_id, platoon_id); + let members = self.social.users_of_platoon(&platoon_id).await; + if members.len() <= 2 { + self.social.remove_platoon(&platoon_id).await; + let event = crate::events::platoon_removed::PlatoonDisbanded { + platoon_id: platoon_id.to_owned(), + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + log::debug!("Platoon {} removed", platoon_id); + } else { + self.social.remove_user_from_platoon(my_public_id).await; + let event = crate::events::platoon_member_left::PlatoonMemberLeft { + member_public_id: my_public_id.to_owned(), + member_display_name: user_info.display_name().to_owned(), + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + } + + } + Ok(params) + } +} + +pub(super) fn platoon_decliner_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(PlatoonInviteDecliner { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/platoon_invite_to.rs b/rc_social_room/src/operations/platoon_invite_to.rs new file mode 100644 index 0000000..6f70c16 --- /dev/null +++ b/rc_social_room/src/operations/platoon_invite_to.rs @@ -0,0 +1,158 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 10; + +const USERNAME_PARAM_KEY: u8 = 1; // str; in +const PLATOON_ID_PARAM_KEY: u8 = 16; // str; out +const PLATOON_INVITEE_PARAM_KEY: u8 = 15; // custom (PlatoonMember); out +const AVATAR_ID_PARAM_KEY: u8 = 14; // int; out +const USE_CUSTOM_AVATAR_PARAM_KEY: u8 = 13; // bool; out + +pub(super) struct PlatoonInviter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for PlatoonInviter { + 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) { + if let Some(platoon_id) = self.social.platoon_of_user(&username.string).await { + log::debug!("User {} is already in platoon {}", username.string, platoon_id); + return Err(SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::AlreadyInPlatoon as i16, + format!("User is already in platoon {}", platoon_id), + )); + } + let user_info = user.user()?; + let (platoon_id, social_infos, timestamp) = if let Some(platoon_id) = self.social.platoon_of_user(user_info.public_id()).await { + // add to existing platoon + log::debug!("Inviting {} to platoon {} by user {}", username.string, platoon_id, user_info.public_id()); + let members = self.social.users_of_platoon(&platoon_id).await; + let member_public_ids: Vec = members.iter() + .map(|m| m.public_id.clone()) + .chain([username.string.clone()]) + .collect(); + let social_infos = user_info.list_social_info(&member_public_ids).await?; + if social_infos.len() != members.len() + 1 { + log::debug!("Platoon members info could not be retrieved while inviting {} to platoon {}", username.string, platoon_id); + 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 let Some(timestamp) = self.social.add_user_to_platoon(&username.string, &platoon_id, crate::data::platoon::MemberStatus::Invited).await { + if social_infos.len() > 2 { + // send PlatoonMemberAdded event to other platoon members + let invitee_soc = social_infos.iter() + .find(|soc| soc.public_id == username.string) + .unwrap(); + let event = crate::events::platoon_member_added::PlatoonNewMember { + new_member: crate::data::platoon::PlatoonMemberInfo { + public_id: invitee_soc.public_id.clone(), + display_name: invitee_soc.display_name.clone(), + status: crate::data::platoon::MemberStatus::Invited, + added: timestamp, + avatar_id: invitee_soc.avatar_id.unwrap_or_default(), + use_custom_avatar: invitee_soc.avatar_id.is_none(), + }, + }; + // unlikely to be more than 3, so no need to run this in its own task + for social_info in social_infos.iter() { + if social_info.public_id == username.string { continue; } + if social_info.public_id == user_info.public_id() { continue; } + self.social.send_event_to(&social_info.public_id, event.clone()).await; + } + /*// send PlatoonMemberAdded event to invitee for other members + for social_info in social_infos.iter() { + if social_info.public_id == username.string { continue; } + if social_info.public_id == user_info.public_id() { continue; } + let member = members.iter() + .find(|mem| mem.public_id == social_info.public_id) + .unwrap(); + let event = crate::events::platoon_member_added::PlatoonNewMember { + new_member: crate::data::platoon::PlatoonMemberInfo { + public_id: social_info.public_id.clone(), + display_name: social_info.display_name.clone(), + status: member.status, + added: member.timestamp, + avatar_id: social_info.avatar_id.unwrap_or_default(), + use_custom_avatar: social_info.avatar_id.is_none(), + }, + }; + self.social.send_event_to(&username.string, event).await; + }*/ + } + (platoon_id, social_infos, timestamp) + } else { + log::debug!("Failed to add user {} to platoon {} (did the platoon disband?)", username.string, platoon_id); + return Err(SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::UserNotPlatoonFound as i16, + "Failed to invite user to platoon".to_owned(), + )); + } + } 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); + 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() { + 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 { + return Err(SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::UserNotPlatoonFound as i16, + "Failed to add user to new platoon".to_owned(), + )); + } + }; + let invitee = social_infos.iter() + .find(|soc| soc.public_id == username.string) + .unwrap(); + let inviter = social_infos.iter() + .find(|soc| soc.public_id == user_info.public_id()) + .unwrap(); + self.social.send_event_to(&invitee.public_id, crate::events::platoon_invite_received::PlatoonInviteReceived { + inviter_public_id: user_info.public_id().to_owned(), + inviter_display_name: user_info.display_name().to_owned(), + avatar_id: inviter.avatar_id, + }).await; + params.insert(PLATOON_ID_PARAM_KEY, Typed::Str(platoon_id.into())); + params.insert(PLATOON_INVITEE_PARAM_KEY, Typed::Custom(crate::data::custom::CustomType::PlatoonMember(crate::data::platoon::PlatoonMemberInfo { + public_id: invitee.public_id.clone(), + display_name: invitee.display_name.clone(), + status: crate::data::platoon::MemberStatus::Invited, + added: timestamp, + avatar_id: invitee.avatar_id.unwrap_or_default(), + use_custom_avatar: invitee.avatar_id.is_none(), + }))); + params.insert(AVATAR_ID_PARAM_KEY, Typed::Int(invitee.avatar_id.unwrap_or_default())); + params.insert(USE_CUSTOM_AVATAR_PARAM_KEY, Typed::Bool(invitee.avatar_id.is_none())); + } + Ok(params) + } +} + +pub(super) fn platoon_invite_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(PlatoonInviter { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/platoon_invites.rs b/rc_social_room/src/operations/platoon_invites.rs new file mode 100644 index 0000000..d8e3a3f --- /dev/null +++ b/rc_social_room/src/operations/platoon_invites.rs @@ -0,0 +1,42 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +//use crate::data::friend::*; + +const CODE: u8 = 19; + +const INVITER_NAME_PARAM_KEY: u8 = 19; +const INVITER_DISPLAY_NAME_PARAM_KEY: u8 = 75; +const INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY: u8 = 13; +const INVITER_AVATAR_ID_NAME_PARAM_KEY: u8 = 14; + +pub(super) struct PlatoonPendingInviter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for PlatoonPendingInviter { + 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_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 { + 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())); + params.insert(INVITER_DISPLAY_NAME_PARAM_KEY, Typed::Str(social_infos[0].display_name.clone().into())); + params.insert(INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY, Typed::Bool(social_infos[0].avatar_id.is_none())); + params.insert(INVITER_AVATAR_ID_NAME_PARAM_KEY, Typed::Int(social_infos[0].avatar_id.unwrap_or_default())); + } + Ok(params) + } +} + +pub(super) fn platoon_pending_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(PlatoonPendingInviter { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/platoon_kick.rs b/rc_social_room/src/operations/platoon_kick.rs new file mode 100644 index 0000000..14d1c02 --- /dev/null +++ b/rc_social_room/src/operations/platoon_kick.rs @@ -0,0 +1,65 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 14; + +const PUBLIC_ID_PARAM_KEY: u8 = 1; // str; in + +pub(super) struct PlatoonKicker { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for PlatoonKicker { + 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_public_id = user_info.public_id(); + if let Some(Typed::Str(username)) = params.remove(&PUBLIC_ID_PARAM_KEY) { + if let Some(platoon_id) = self.social.platoon_of_user(&username.string).await { + let members = self.social.users_of_platoon(&platoon_id).await; + if !members.is_empty() && members[0].public_id != my_public_id { + return Err(SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::TheyNotPlatoonLeader as i16, + "Only platoon leader can kick members".to_owned(), + )); + } + log::debug!("User {} kicked from platoon {}", username.string, platoon_id); + if members.len() <= 2 { + self.social.remove_platoon(&platoon_id).await; + let event = crate::events::platoon_removed::PlatoonDisbanded { + platoon_id: platoon_id.to_owned(), + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + log::debug!("Platoon {} removed", platoon_id); + } else { + let social_infos = user_info.list_social_info(std::slice::from_ref(&username.string)).await?; + self.social.remove_user_from_platoon(&username.string).await; + self.social.send_event_to(&username.string, crate::events::platoon_member_kick::PlatoonMemberKick).await; + let event = crate::events::platoon_member_left::PlatoonMemberLeft { + member_public_id: username.string.clone(), + member_display_name: social_infos.first().map(|soc| soc.display_name.clone()).unwrap_or_else(|| username.string.clone()), + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + } + } else { + log::warn!("Cannot kick {} from platoon because they are not in one", username.string); + } + } + Ok(params) + } +} + +pub(super) fn platoon_kick_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(PlatoonKicker { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/platoon_leave.rs b/rc_social_room/src/operations/platoon_leave.rs new file mode 100644 index 0000000..12a01f0 --- /dev/null +++ b/rc_social_room/src/operations/platoon_leave.rs @@ -0,0 +1,65 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::ParameterTable; + +const CODE: u8 = 13; + +pub(super) struct PlatoonLeaver { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for PlatoonLeaver { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let my_public_id = user_info.public_id(); + if let Some(platoon_id) = self.social.platoon_of_user(my_public_id).await { + log::debug!("User {} left platoon {}", my_public_id, platoon_id); + let members = self.social.users_of_platoon(&platoon_id).await; + if members.len() <= 2 { + self.social.remove_platoon(&platoon_id).await; + let event = crate::events::platoon_removed::PlatoonDisbanded { + platoon_id: platoon_id.to_owned(), + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + log::debug!("Platoon {} removed", platoon_id); + } else { + self.social.remove_user_from_platoon(my_public_id).await; + let event = crate::events::platoon_member_left::PlatoonMemberLeft { + member_public_id: my_public_id.to_owned(), + member_display_name: user_info.display_name().to_owned(), + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + if members[0].public_id == my_public_id { + // current user was the leader; there is now a new leader + log::debug!("User {} is now the leader of platoon {}", members[1].public_id, platoon_id); + let social_info = user_info.list_social_info(&[members[1].public_id.clone()]).await?; + let event = crate::events::platoon_leader_changed::PlatoonLeaderChanged { + leader_public_id: social_info[0].public_id.clone(), + leader_display_name: social_info[0].display_name.clone(), + }; + for member in members.iter() { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + } + } + + } + Ok(params) + } +} + +pub(super) fn platoon_leave_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(PlatoonLeaver { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/platoon_status.rs b/rc_social_room/src/operations/platoon_status.rs new file mode 100644 index 0000000..4e7fa14 --- /dev/null +++ b/rc_social_room/src/operations/platoon_status.rs @@ -0,0 +1,51 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 17; + +const STATUS_PARAM_KEY: u8 = 3; + +pub(super) struct PlatoonStatusUpdater { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for PlatoonStatusUpdater { + 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(status)) = params.remove(&STATUS_PARAM_KEY) { + let user_info = user.user()?; + let my_public_id = user_info.public_id(); + let member_status = crate::data::platoon::MemberStatus::from_u8(status as u8) + .ok_or_else(|| SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::UnexpectedError as i16, + "Invalid member status".to_owned(), + ))?; + if let Some(platoon_id) = self.social.platoon_of_user(my_public_id).await { + log::debug!("User {} platoon {} status updated to {:?}", my_public_id, platoon_id, member_status); + let members = self.social.users_of_platoon(&platoon_id).await; + self.social.update_user_in_platoon(my_public_id, member_status).await; + let event = crate::events::platoon_member_update::PlatoonMemberStatusUpdate { + member_public_id: my_public_id.to_owned(), + member_display_name: user_info.display_name().to_owned(), + member_status, + }; + for member in members { + if member.public_id == my_public_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + } + } + + + Ok(params) + } +} + +pub(super) fn platoon_update_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(PlatoonStatusUpdater { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/search_clan.rs b/rc_social_room/src/operations/search_clan.rs index e1265d7..359c2b4 100644 --- a/rc_social_room/src/operations/search_clan.rs +++ b/rc_social_room/src/operations/search_clan.rs @@ -23,6 +23,7 @@ pub(super) fn search_clans_provider() -> SimpleFunc<32, crate::U } params.insert(RESULTS_PARAM_KEY, Typed::Arr(Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, items: vec![ ClanInfo { clan_name: "RE_clan_search_name".to_owned(), diff --git a/rc_social_room/src/operations/user_can_be_platooned.rs b/rc_social_room/src/operations/user_can_be_platooned.rs new file mode 100644 index 0000000..7116b41 --- /dev/null +++ b/rc_social_room/src/operations/user_can_be_platooned.rs @@ -0,0 +1,52 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +#[allow(dead_code)] +#[repr(u8)] +enum ResponseCode { + Ok = 0, + InCustomGame = 1, + OutstandingCustomInvite = 2, + Error = 3, + PlayerDoesNotExist = 4, +} + +const CODE: u8 = 59; + +const USERNAME_PARAM_KEY: u8 = 65; // str; in +const RESPONSE_CODE_PARAM_KEY: u8 = 66; // bool; out + +pub(super) struct UserCanBeInvitedToPlatoonGetter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for UserCanBeInvitedToPlatoonGetter { + 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::Int(ResponseCode::Error as i32)); + return Ok(params); + } + /*if self.social.platoon_of_user(&username.string).await.is_some() { + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(ResponseCode::OutstandingCustomInvite as i32)); + return Ok(params); + }*/ + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(ResponseCode::Ok as i32)); + } + Ok(params) + } +} + +pub(super) fn can_invite_to_platoon_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(UserCanBeInvitedToPlatoonGetter { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/social_services.rs b/rc_social_room/src/social_services.rs index be3fbbf..2345b66 100644 --- a/rc_social_room/src/social_services.rs +++ b/rc_social_room/src/social_services.rs @@ -1,6 +1,7 @@ /// Primarily keeps track of who is online so events can be sent to them. pub struct SocialMesh { - users: tokio::sync::RwLock>, + users: tokio::sync::RwLock>>, + platoons: Platoons, } struct UserHandle { @@ -8,10 +9,33 @@ struct UserHandle { is_alive: std::sync::atomic::AtomicBool, } +struct Platoons { + platoon_by_id: tokio::sync::RwLock>>, + platoon_by_user: tokio::sync::RwLock>, // public_id -> platoon_id +} + +struct PlatoonMember { + public_id: String, + //handle: std::sync::Arc, + status: crate::data::platoon::MemberStatus, + timestamp: i64, +} + +pub struct PlatoonMemberInfo { + pub public_id: String, + pub status: crate::data::platoon::MemberStatus, + pub timestamp: i64, +} + impl SocialMesh { + #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { users: tokio::sync::RwLock::new(std::collections::HashMap::new()), + platoons: Platoons { + platoon_by_id: tokio::sync::RwLock::new(std::collections::HashMap::new()), + platoon_by_user: tokio::sync::RwLock::new(std::collections::HashMap::new()), + }, } } @@ -33,10 +57,10 @@ impl SocialMesh { ) { let mut user_lock = self.users.write().await; Self::cleanup_dead_users(&mut user_lock).await; - user_lock.insert(public_id, UserHandle { + user_lock.insert(public_id, std::sync::Arc::new(UserHandle { emitter, is_alive: std::sync::atomic::AtomicBool::new(true), - }); + })); } /// Filter out offline users @@ -46,7 +70,7 @@ impl SocialMesh { public_ids.retain(|public_id| user_lock.contains_key(public_id)); } - async fn cleanup_dead_users(users: &mut std::collections::HashMap) { + async fn cleanup_dead_users(users: &mut std::collections::HashMap>) { users.retain(|_public_id, handle| handle.is_alive.load(std::sync::atomic::Ordering::SeqCst)); } @@ -56,4 +80,112 @@ impl SocialMesh { .filter(|(_, handle)| handle.is_alive.load(std::sync::atomic::Ordering::SeqCst)) .count() as u64 } + + pub async fn platoon_of_user(&self, public_id: &str) -> Option { + self.platoons.platoon_by_user.read().await + .get(public_id) + .map(|x| x.to_owned()) + } + + pub async fn users_of_platoon(&self, platoon_id: &str) -> Vec { + self.platoons.platoon_by_id.read().await + .get(platoon_id) + .map(|plat| plat.iter() + .map(|member| PlatoonMemberInfo { + public_id: member.public_id.clone(), + status: member.status, + timestamp: member.timestamp, + }) + .collect() + ).unwrap_or_default() + } + + pub async fn add_user_to_platoon(&self, public_id: &str, platoon_id: &str, status: crate::data::platoon::MemberStatus) -> Option { + /*let user_handle = if let Some(handle) = self.users.read().await.get(public_id) { + handle.to_owned() + } else { + return None; + };*/ + if let Some(platoon) = self.platoons.platoon_by_id.write().await.get_mut(platoon_id) { + let timestamp = chrono::Utc::now().timestamp(); + platoon.push(PlatoonMember { + public_id: public_id.to_owned(), + //handle: user_handle, + status, + timestamp, + }); + self.platoons.platoon_by_user.write().await.insert(public_id.to_owned(), platoon_id.to_owned()); + Some(timestamp) + } else { + None + } + } + + pub async fn create_platoon(&self, platoon_id: &str, public_id: &str) -> Option { + /*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) { + return None; + } + let mut platoon_members = Vec::with_capacity(5); + let timestamp = chrono::Utc::now().timestamp(); + platoon_members.push(PlatoonMember { + public_id: public_id.to_owned(), + //handle: user_handle, + status: crate::data::platoon::MemberStatus::Ready, + timestamp, + }); + 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) + } + + 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_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() { + platoon_by_id_lock.remove(&platoon_id); + } + true + } else { + false + } + } else { + false + } + } + + pub async fn update_user_in_platoon(&self, public_id: &str, status: crate::data::platoon::MemberStatus) -> bool { + if let Some(platoon_id) = self.platoons.platoon_by_user.write().await.get_mut(public_id) { + if let Some(platoon) = self.platoons.platoon_by_id.write().await.get_mut(platoon_id) { + for member in platoon.iter_mut() { + if member.public_id != public_id { continue; } + member.status = status; + break; + } + true + } else { + false + } + } else { + false + } + } + + pub async fn remove_platoon(&self, platoon_id: &str) -> bool { + if let Some(platoon) = self.platoons.platoon_by_id.write().await.remove(platoon_id) { + let mut platoon_by_user_lock = self.platoons.platoon_by_user.write().await; + for member in platoon { + platoon_by_user_lock.remove(&member.public_id); + } + true + } else { + false + } + } }