diff --git a/Cargo.lock b/Cargo.lock index 3cfe48c..2681819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2576,6 +2576,7 @@ dependencies = [ "chrono", "clap", "env_logger", + "futures", "git-version", "log", "oj_polariton_auth", diff --git a/rc_auth/src/robocraft/intercom/lobby.rs b/rc_auth/src/robocraft/intercom/lobby.rs new file mode 100644 index 0000000..66e985c --- /dev/null +++ b/rc_auth/src/robocraft/intercom/lobby.rs @@ -0,0 +1,63 @@ +use actix_web::{rt, web::{Payload, Data, Json, Path}, Error, HttpRequest, HttpResponse, get, post}; +//use actix_ws::AggregatedMessage; +//use futures::StreamExt as _; + +#[get("/intercom/userless/.oj_lobby")] +pub async fn lobby_state_ws(req: HttpRequest, stream: Payload, auth: Data, reg: Data) -> Result { + auth.validate(&req, "state/.oj_lobby")?; + let (res, mut session, _stream) = actix_ws::handle(&req, stream)?; + + /*let mut stream = stream + .aggregate_continuations() + .max_continuation_size(2_usize.pow(20)); // aggregate continuation frames up to 1MiB + */ + + let (tx, mut rx) = tokio::sync::mpsc::channel(16); + reg.register_lobby_state_service(tx).await; + log::debug!("Registered lobby state intercom websocket"); + + // start task but don't wait for it + rt::spawn(async move { + let mut is_ok = false; + while let Some(op) = rx.recv().await { + match op { + super::IntercomOp::Message(msg) => { + if let Err(e) = session.text(serde_json::to_string(&msg).unwrap()).await { + log::warn!("Failed to send lobby state intercom message: {}", e); + break; + } + }, + super::IntercomOp::Info(info) => { + match info { + super::IntercomInfo::Close => { + is_ok = true; + break; + }, + } + } + } + + } + if !is_ok { + reg.remove_lobby_state_service().await; + } + rx.close(); + session.close(Some(actix_ws::CloseReason { + code: actix_ws::CloseCode::Normal, + description: Some("End of channel".to_owned()), + })).await.expect("Failed to close a services intercom websocket session"); + log::debug!("Lobby state intercom websocket closed"); + }); + + // respond immediately with response connected to WS session + Ok(res) +} + +#[post("/intercom/.oj_lobby/{name}/state")] +pub async fn lobby_state_msg(req: HttpRequest, body: Json, auth: Data, reg: Data, name: Path) -> Result { + log::debug!("Got lobby state intercom message"); + auth.validate(&req, &format!(".oj_lobby/{}/state", name))?; + log::debug!("Authenticated intercom lobby state message from {}", name); + reg.broadcast_lobby_message(body.0).await; + Ok(HttpResponse::NoContent().finish()) +} diff --git a/rc_auth/src/robocraft/intercom/mod.rs b/rc_auth/src/robocraft/intercom/mod.rs index 302525b..d7f50b4 100644 --- a/rc_auth/src/robocraft/intercom/mod.rs +++ b/rc_auth/src/robocraft/intercom/mod.rs @@ -10,11 +10,17 @@ pub use user_registry::Users; mod status; pub use status::{status_set, status_get}; -enum IntercomOp { - Message(oj_rc_core::persist::user::intercom::IntercomWebServiceUserMessage), +mod lobby; +pub use lobby::{lobby_state_ws, lobby_state_msg}; + +enum IntercomOp + 'static> { + Message(M), Info(IntercomInfo), } enum IntercomInfo { Close, } + +type WebServicesIntercomOp = IntercomOp; +type LobbyStateIntercomOp = IntercomOp; diff --git a/rc_auth/src/robocraft/intercom/services.rs b/rc_auth/src/robocraft/intercom/services.rs index bfbbd9d..d0574eb 100644 --- a/rc_auth/src/robocraft/intercom/services.rs +++ b/rc_auth/src/robocraft/intercom/services.rs @@ -13,7 +13,7 @@ pub async fn services_ws(req: HttpRequest, stream: Payload, auth: Data>>, + lobby_state: tokio::sync::RwLock>>, +} + pub struct Users { - service_listeners: tokio::sync::RwLock>>, + service_listeners: ServiceListeners, service_status: tokio::sync::RwLock>, } impl Users { pub fn new() -> Self { Self { - service_listeners: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(16)), + service_listeners: ServiceListeners { + web_services: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(16)), + lobby_state: tokio::sync::RwLock::new(None), + }, service_status: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(16)), } } - pub(super) async fn register_service(&self, public_id: String, sender: tokio::sync::mpsc::Sender) { - let mut write_lock = self.service_listeners.write().await; + pub(super) async fn register_web_service(&self, public_id: String, sender: tokio::sync::mpsc::Sender) { + let mut write_lock = self.service_listeners.web_services.write().await; if let Some(old_sender) = write_lock.insert(public_id.clone(), sender) { if !old_sender.is_closed() { log::warn!("Replaced web services intercom channel for user {} (why duplicate!?)", public_id); @@ -21,26 +29,43 @@ impl Users { } } - pub async fn remove_service(&self, public_id: String) { - let mut write_lock = self.service_listeners.write().await; + pub(super) async fn register_lobby_state_service(&self, sender: tokio::sync::mpsc::Sender) { + let mut write_lock = self.service_listeners.lobby_state.write().await; + if let Some(old_sender) = write_lock.replace(sender) { + if !old_sender.is_closed() { + log::warn!("Replaced lobby state intercom channel (why duplicate!?)"); + old_sender.send(super::IntercomOp::Info(super::IntercomInfo::Close)).await.unwrap_or_default() + } + } + } + + pub async fn remove_web_service(&self, public_id: String) { + let mut write_lock = self.service_listeners.web_services.write().await; if write_lock.remove(&public_id).is_none() { log::warn!("Tried to remove web services intercom channel for user {} without listener", public_id); } } - pub async fn broadcast_service_message(&self, msg: oj_rc_core::persist::user::intercom::IntercomWebServiceMessage) { - let read_lock = self.service_listeners.read().await; + pub async fn remove_lobby_state_service(&self) { + let mut write_lock = self.service_listeners.lobby_state.write().await; + if write_lock.take().is_none() { + log::warn!("Tried to remove lobby state intercom channel without listener"); + } + } + + pub async fn broadcast_web_service_message(&self, msg: oj_rc_core::persist::user::intercom::IntercomWebServiceMessage) { + let read_lock = self.service_listeners.web_services.read().await; if msg.everyone { if !msg.public_ids.is_empty() { return; } // invalid for (public_id, tx) in read_lock.iter() { - if let Err(e) = tx.send(super::IntercomOp::Message(msg.data.clone())).await { + if let Err(e) = tx.send(super::WebServicesIntercomOp::Message(msg.data.clone())).await { log::error!("Failed to send web service intercom message to {}: {}", public_id, e); } } } else { for public_id in msg.public_ids { if let Some(tx) = read_lock.get(&public_id) { - if let Err(e) = tx.send(super::IntercomOp::Message(msg.data.clone())).await { + if let Err(e) = tx.send(super::WebServicesIntercomOp::Message(msg.data.clone())).await { log::error!("Failed to send web service intercom message to {}: {}", public_id, e); } } else { @@ -50,6 +75,15 @@ impl Users { } } + pub async fn broadcast_lobby_message(&self, msg: oj_rc_core::persist::user::intercom::IntercomLobbyStateMessage) { + let read_lock = self.service_listeners.lobby_state.read().await; + if let Some(listener) = &*read_lock { + if let Err(e) = listener.send(super::LobbyStateIntercomOp::Message(msg)).await { + log::error!("Failed to send lobby state intercom message: {}", e); + } + } + } + pub async fn statuses(&self) -> std::collections::HashMap { self.service_status.read().await.clone() } diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index cad31f7..0dabdb5 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -13,8 +13,9 @@ pub struct AccountProvider { domain: std::sync::Arc, cdn: std::sync::Arc, auth: std::sync::Arc, - intercom: std::sync::Arc, - secret: std::sync::Arc>, + pub(super) intercom: std::sync::Arc, + pub(super) intercom_http_client: std::sync::Arc, + pub(super) secret: std::sync::Arc>, db: std::sync::Arc, } @@ -38,6 +39,7 @@ impl AccountProvider { cdn: std::sync::Arc::new(server_settings.cdn_url), auth: std::sync::Arc::new(server_settings.auth_url), intercom: std::sync::Arc::new(server_settings.intercom_url), + intercom_http_client: std::sync::Arc::new(reqwest::Client::new()), secret: std::sync::Arc::new(secret), db: std::sync::Arc::new(db), }) diff --git a/rc_core/src/persist/user/intercom.rs b/rc_core/src/persist/user/intercom.rs index cd2f6f6..4c1d1c7 100644 --- a/rc_core/src/persist/user/intercom.rs +++ b/rc_core/src/persist/user/intercom.rs @@ -116,6 +116,13 @@ impl super::IntercomUser for super::account_json::UserData { } } + async fn update_custom_game(&self, msg: IntercomLobbyCustomGameDataMessage) { + let data = IntercomLobbyStateMessage::CustomGame(msg); + if let Err(e) = self.post_to_intercom(&data, ".oj_lobby", "state").await { + log::error!("Failed to send intercom custom game state message: {}", e); + } + } + async fn update_status(&self, server_name: &str, msg: oj_serdes::ServerStatus) { if let Err(e) = self.post_to_intercom(&msg, ".status", server_name).await { log::error!("Failed to send intercom status message: {}", e); @@ -123,6 +130,91 @@ impl super::IntercomUser for super::account_json::UserData { } } +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "state")] +pub enum IntercomLobbyStateMessage { + CustomGame(IntercomLobbyCustomGameDataMessage), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct IntercomLobbyCustomGameDataMessage { + pub session_id: String, + pub config: IntercomLobbyCustomGameConfig, + pub users: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum CustomGameMode { + BattleArena, + TeamDeathmatch, + Pit, + SuddenDeath, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Copy)] +pub enum CustomGameVisibility { + Good, + Poor, + Bad, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct IntercomLobbyCustomGameConfig { + pub game_mode: CustomGameMode, + pub map: String, + pub map_visibility: CustomGameVisibility, + pub health_regen: bool, + pub capture_segment_memory: bool, + pub base_shields_go_down: bool, + pub damage_mult: i32, + pub health_mult: i32, + pub power_mult: i32, + pub game_time: i32, // minutes + pub capture_speed: i32, // seconds + pub points_kill_streak: bool, + pub points_total_required: i32, + pub number_of_kills_to_win: i32, + pub respawn_time: i32, + pub core_appear_frequency: i32, + pub core_health_multiplier: i32, + pub core_destroy_time: i32, + pub protonium_harvest: i32, + pub ceiling_multiplier: i32, + pub min_cpu: i32, + pub max_cpu: i32, +} + +impl IntercomLobbyCustomGameConfig { + pub fn as_core(&self) -> super::GameOverrides { + super::GameOverrides { + capture_segment_memory: self.capture_segment_memory, + base_shields_go_down: self.base_shields_go_down, + damage_mult: self.damage_mult, + health_mult: self.health_mult, + power_mult: self.power_mult, + game_time: self.game_time, + capture_speed: self.capture_speed, + points_kill_streak: self.points_kill_streak, + points_total_required: self.points_total_required, + number_of_kills_to_win: self.number_of_kills_to_win, + respawn_time: self.respawn_time, + core_appear_frequency: self.core_appear_frequency, + core_health_multiplier: self.core_health_multiplier, + core_destroy_time: self.core_destroy_time, + protonium_harvest: self.protonium_harvest, + ceiling_multiplier: self.ceiling_multiplier, + min_cpu: self.min_cpu, + max_cpu: self.max_cpu, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct IntercomLobbyCustomGameUserData { + pub public_id: String, + pub team: u8, +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct IntercomWebServiceMessage { pub public_ids: Vec, diff --git a/rc_core/src/persist/user/lobby.rs b/rc_core/src/persist/user/lobby.rs index cb72411..20505f0 100644 --- a/rc_core/src/persist/user/lobby.rs +++ b/rc_core/src/persist/user/lobby.rs @@ -1,3 +1,5 @@ +use serde::{Serialize, Deserialize}; + use super::account_json::UserData; pub enum TeamChooser { @@ -87,6 +89,7 @@ impl super::LobbyUser for UserData { auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(game.auto_heal), variant: oj_rc_database::sea_orm::ActiveValue::Set(variant), is_complete: oj_rc_database::sea_orm::ActiveValue::Set(false), + overrides: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()), }; let game_dbo = self.db.insert_game(game_dbo).await.map_err(|e| { log::error!("Failed to create game {} through user_id {}: {}", game.guid, self.account.id, e); @@ -165,4 +168,143 @@ impl super::LobbyUser for UserData { players: forced_fake_players.into_iter().chain(filler_players).collect(), }) } + + async fn start_custom_game(&self, game: super::GameDescriptor, players: Vec) -> Result<(), polariton_server::operations::SimpleOpError> { + let now = chrono::Utc::now().timestamp(); + let guid = crate::persist::user::str_to_i64(&game.guid) + .ok_or_else(|| polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16, "Invalid GUID".to_owned() + ) + )?; + let variant = if game.is_ranked { + oj_rc_database::schema::multiplayer_game::GameType::Ranked + } else if game.is_custom { + oj_rc_database::schema::multiplayer_game::GameType::Custom + } else { + oj_rc_database::schema::multiplayer_game::GameType::Standard + }; + + let conf_str = if let Some(config) = game.overrides { + serde_json::to_string_pretty(&CustomGameOverrides::from_user(&config)).unwrap() + } else { + "".to_owned() + }; + + let game_dbo = oj_rc_database::schema::multiplayer_game::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), + guid: oj_rc_database::sea_orm::ActiveValue::Set(guid), + map: oj_rc_database::sea_orm::ActiveValue::Set(game.map), + mode: oj_rc_database::sea_orm::ActiveValue::Set(game.mode.to_db()), + visibility: oj_rc_database::sea_orm::ActiveValue::Set(game.visibility.to_db()), + auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(game.auto_heal), + variant: oj_rc_database::sea_orm::ActiveValue::Set(variant), + is_complete: oj_rc_database::sea_orm::ActiveValue::Set(false), + overrides: oj_rc_database::sea_orm::ActiveValue::Set(conf_str), + }; + let game_dbo = self.db.insert_game(game_dbo).await.map_err(|e| { + log::error!("Failed to create custom game {} through user_id {}: {}", game.guid, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16, + format!("Failed to create custom game {}: {}", game.guid, e), + ) + })?; + + let players: Vec = players.into_iter() + .enumerate() + .map(|(i, player)| { + oj_rc_database::schema::multiplayer_game_player::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + user_id: oj_rc_database::sea_orm::ActiveValue::Set(Some(player.user_id)), + game_id: oj_rc_database::sea_orm::ActiveValue::Set(game_dbo.id), + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), + player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _), + team: oj_rc_database::sea_orm::ActiveValue::Set(player.team), + group: oj_rc_database::sea_orm::ActiveValue::Set(player.group), + is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false), + public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id), + display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name), + variant: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game_player::ClientType::Client), + } + }) + .collect(); + self.db.insert_players(players).await.map_err(|e| { + log::error!("Failed to create game players for {} through user_id {}: {}", game.guid, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16, + format!("Failed to create game players for {}: {}", game.guid, e), + ) + })?; + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(super) struct CustomGameOverrides { + pub capture_segment_memory: bool, + pub base_shields_go_down: bool, + pub damage_mult: i32, + pub health_mult: i32, + pub power_mult: i32, + pub game_time: i32, // minutes + pub capture_speed: i32, // seconds + pub points_kill_streak: bool, + pub points_total_required: i32, + pub number_of_kills_to_win: i32, + pub respawn_time: i32, + pub core_appear_frequency: i32, + pub core_health_multiplier: i32, + pub core_destroy_time: i32, + pub protonium_harvest: i32, + pub ceiling_multiplier: i32, + pub min_cpu: i32, + pub max_cpu: i32, +} + +impl CustomGameOverrides { + pub(super) fn from_user(intercom: &super::GameOverrides) -> Self { + Self { + capture_segment_memory: intercom.capture_segment_memory, + base_shields_go_down: intercom.base_shields_go_down, + damage_mult: intercom.damage_mult, + health_mult: intercom.health_mult, + power_mult: intercom.power_mult, + game_time: intercom.game_time, + capture_speed: intercom.capture_speed, + points_kill_streak: intercom.points_kill_streak, + points_total_required: intercom.points_total_required, + number_of_kills_to_win: intercom.number_of_kills_to_win, + respawn_time: intercom.respawn_time, + core_appear_frequency: intercom.core_appear_frequency, + core_health_multiplier: intercom.core_health_multiplier, + core_destroy_time: intercom.core_destroy_time, + protonium_harvest: intercom.protonium_harvest, + ceiling_multiplier: intercom.ceiling_multiplier, + min_cpu: intercom.min_cpu, + max_cpu: intercom.max_cpu, + } + } + + pub(super) fn to_user(&self) -> super::GameOverrides { + super::GameOverrides { + capture_segment_memory: self.capture_segment_memory, + base_shields_go_down: self.base_shields_go_down, + damage_mult: self.damage_mult, + health_mult: self.health_mult, + power_mult: self.power_mult, + game_time: self.game_time, + capture_speed: self.capture_speed, + points_kill_streak: self.points_kill_streak, + points_total_required: self.points_total_required, + number_of_kills_to_win: self.number_of_kills_to_win, + respawn_time: self.respawn_time, + core_appear_frequency: self.core_appear_frequency, + core_health_multiplier: self.core_health_multiplier, + core_destroy_time: self.core_destroy_time, + protonium_harvest: self.protonium_harvest, + ceiling_multiplier: self.ceiling_multiplier, + min_cpu: self.min_cpu, + max_cpu: self.max_cpu, + } + } } diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index e7539d0..1c2f265 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, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData}; +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, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides}; pub mod intercom; pub use intercom::generate_token as generate_intercom_token; @@ -24,6 +24,7 @@ mod chat; mod social; mod singleplayer; mod factory; +mod userless; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; diff --git a/rc_core/src/persist/user/multiplayer.rs b/rc_core/src/persist/user/multiplayer.rs index 8120a42..6c663f7 100644 --- a/rc_core/src/persist/user/multiplayer.rs +++ b/rc_core/src/persist/user/multiplayer.rs @@ -134,6 +134,15 @@ impl super::MultiplayerUser for UserData { is_ranked: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Ranked), is_custom: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Custom), is_complete: game.is_complete, + overrides: if game.overrides.is_empty() { None } else { + match serde_json::from_str::(&game.overrides) { + Ok(x) => Some(x.to_user()), + Err(e) => { + log::warn!("Failed to parse overrides JSON: {}\n{}", e, game.overrides); + None + } + } + } })) } @@ -204,6 +213,15 @@ impl super::MultiplayerUser for UserData { is_ranked: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Ranked), is_custom: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Custom), is_complete: game.is_complete, + overrides: if game.overrides.is_empty() { None } else { + match serde_json::from_str::(&game.overrides) { + Ok(x) => Some(x.to_user()), + Err(e) => { + log::warn!("Failed to parse overrides JSON: {}\n{}", e, game.overrides); + None + } + } + } })) } else { Err(super::MultiplayerError { diff --git a/rc_core/src/persist/user/singleplayer.rs b/rc_core/src/persist/user/singleplayer.rs index 04520bf..b7f6248 100644 --- a/rc_core/src/persist/user/singleplayer.rs +++ b/rc_core/src/persist/user/singleplayer.rs @@ -23,6 +23,7 @@ impl super::SingleplayerUser for UserData { auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(false), variant: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game::GameType::Standard), is_complete: oj_rc_database::sea_orm::ActiveValue::Set(true), + overrides: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()), }).await .map_err(|e| { log::error!("Failed to create singleplayer game {} for user {}: {}", guid, self.account.id, e); diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 1b6bb8d..d844139 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -282,6 +282,8 @@ pub trait LobbyUser { async fn team_chooser(&self, game: &GameDescriptor) -> super::TeamChooser; #[allow(clippy::too_many_arguments)] async fn start_game(&self, game: GameDescriptor, players: Vec, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, team_chooser: &super::TeamChooser, missing_players: usize) -> Result; + #[allow(clippy::too_many_arguments)] + async fn start_custom_game(&self, game: GameDescriptor, players: Vec) -> Result<(), polariton_server::operations::SimpleOpError>; } pub struct FakePlayers { @@ -306,6 +308,28 @@ pub struct GameDescriptor { pub is_ranked: bool, pub is_custom: bool, pub is_complete: bool, + pub overrides: Option, +} + +pub struct GameOverrides { + pub capture_segment_memory: bool, + pub base_shields_go_down: bool, + pub damage_mult: i32, + pub health_mult: i32, + pub power_mult: i32, + pub game_time: i32, // minutes + pub capture_speed: i32, // seconds + pub points_kill_streak: bool, + pub points_total_required: i32, + pub number_of_kills_to_win: i32, + pub respawn_time: i32, + pub core_appear_frequency: i32, + pub core_health_multiplier: i32, + pub core_destroy_time: i32, + pub protonium_harvest: i32, + pub ceiling_multiplier: i32, + pub min_cpu: i32, + pub max_cpu: i32, } pub struct PlayerLobbyDescriptor { @@ -394,6 +418,7 @@ pub trait IntercomUser: CommonUser { async fn webservice_listener(&self) -> Result, polariton_server::operations::SimpleOpError>; async fn show_dev_message(&self, msg: super::intercom::IntercomDevMessage, to: Vec); async fn enter_maintenance(&self, msg: super::intercom::IntercomMaintenanceMessage, to: Vec); + async fn update_custom_game(&self, msg: super::intercom::IntercomLobbyCustomGameDataMessage); async fn update_status(&self, server_name: &str, msg: oj_serdes::ServerStatus); } @@ -643,3 +668,8 @@ pub trait FactoryUser { async fn prepare_factory_upload(&self, vehicle: VehicleUploadData) -> Result; async fn rate_vehicle(&self, slot: i32, combat: i32, cosmetic: i32) -> Result, polariton_server::operations::SimpleOpError>; } + +#[async_trait::async_trait] +pub trait Userless: Send + Sync { + async fn lobby_state_listener(&self) -> Result, reqwest_websocket::Error>; +} diff --git a/rc_core/src/persist/user/userless.rs b/rc_core/src/persist/user/userless.rs new file mode 100644 index 0000000..dec2c25 --- /dev/null +++ b/rc_core/src/persist/user/userless.rs @@ -0,0 +1,29 @@ +//use serde::{Serialize, Deserialize}; + +impl super::AccountProvider { + async fn listen_on_websocket(&self, server_name: &str) -> Result, reqwest_websocket::Error> { + use reqwest_websocket::RequestBuilderExt; + let token = super::generate_intercom_token(format!("state/{}", server_name).as_bytes(), &self.secret); + let auth_header_val = format!("Internal {}", token); + let url = format!("{}/intercom/userless/{}", self.intercom, server_name); + log::debug!("Listening on websocket {}", url); + let websocket = self.intercom_http_client.get(url) + .header("Authorization", auth_header_val) + .upgrade() + .send() + .await? + .into_websocket() + .await?; + Ok(super::IntercomListener { + websocket, + _d: Default::default(), + }) + } +} + +#[async_trait::async_trait] +impl super::Userless for super::AccountProvider { + async fn lobby_state_listener(&self) -> Result, reqwest_websocket::Error> { + self.listen_on_websocket(".oj_lobby").await + } +} diff --git a/rc_database/src/migration/m20260317_000001_add_game_overrides.rs b/rc_database/src/migration/m20260317_000001_add_game_overrides.rs new file mode 100644 index 0000000..a69b020 --- /dev/null +++ b/rc_database/src/migration/m20260317_000001_add_game_overrides.rs @@ -0,0 +1,36 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20260317_000001_add_game_overrides" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Add game overrides columns + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(crate::schema::multiplayer_game::Entity) + .add_column(ColumnDef::new(crate::schema::multiplayer_game::Column::Overrides).string().not_null().default("".to_owned())) + .to_owned() + ) + .await + } + + // Define how to rollback this migration: Drop the added column + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(crate::schema::multiplayer_game::Entity) + .drop_column(crate::schema::multiplayer_game::Column::Overrides) + .to_owned() + ) + .await + } +} diff --git a/rc_database/src/migration/mod.rs b/rc_database/src/migration/mod.rs index b5feb71..db71424 100644 --- a/rc_database/src/migration/mod.rs +++ b/rc_database/src/migration/mod.rs @@ -18,6 +18,7 @@ mod m20260126_000001_create_factory_vehicle_table; mod m20260215_000001_create_friend_table; mod m20260221_000001_create_clan_table; mod m20260221_000002_create_clan_member_table; +mod m20260317_000001_add_game_overrides; pub struct Migrator; @@ -43,6 +44,7 @@ impl MigratorTrait for Migrator { Box::new(m20260215_000001_create_friend_table::Migration), Box::new(m20260221_000001_create_clan_table::Migration), Box::new(m20260221_000002_create_clan_member_table::Migration), + Box::new(m20260317_000001_add_game_overrides::Migration), ] } } diff --git a/rc_database/src/schema/multiplayer_game.rs b/rc_database/src/schema/multiplayer_game.rs index a2b76be..f041ccd 100644 --- a/rc_database/src/schema/multiplayer_game.rs +++ b/rc_database/src/schema/multiplayer_game.rs @@ -13,6 +13,7 @@ pub struct Model { pub auto_heal: bool, pub variant: GameType, pub is_complete: bool, + pub overrides: String, // JSON-encoded oj_rc_ore::persist::user::lobby::CustomGameConfig } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/rc_lobby_room/Cargo.toml b/rc_lobby_room/Cargo.toml index f28dd4e..64b0128 100644 --- a/rc_lobby_room/Cargo.toml +++ b/rc_lobby_room/Cargo.toml @@ -21,3 +21,4 @@ async-trait.workspace = true git-version.workspace = true chrono.workspace = true oj_serdes.workspace = true +futures.workspace = true diff --git a/rc_lobby_room/src/events/handler.rs b/rc_lobby_room/src/events/handler.rs new file mode 100644 index 0000000..c0b82c5 --- /dev/null +++ b/rc_lobby_room/src/events/handler.rs @@ -0,0 +1,74 @@ +//use oj_rc_core::persist::user::IntercomListener; +use oj_rc_core::persist::user::intercom::IntercomLobbyStateMessage; + +const RETRY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); + +pub struct IntercomHandler { + userless: std::sync::Arc, + lobby: std::sync::Arc, +} + +impl IntercomHandler { + pub fn new( + userless: std::sync::Arc, + lobby: std::sync::Arc, + ) -> Self { + Self { + userless, + lobby, + } + } + + async fn run_loop( + userless: std::sync::Arc, + lobby: std::sync::Arc, + ) { + loop { + let listener = match userless.lobby_state_listener().await { + Ok(listener) => { + log::debug!("Connected lobby state intercom listener"); + listener + }, + Err(e) => { + log::error!("Failed to connect to lobby state intercom: {} (retrying in {}s)", e, RETRY_TIMEOUT.as_secs()); + tokio::time::sleep(RETRY_TIMEOUT).await; + continue; + } + }; + use futures::StreamExt; + let mut listener = listener.listen().await; + while let Some(msg) = listener.next().await { + match msg { + Ok(msg) => { + match msg { + IntercomLobbyStateMessage::CustomGame(state) => { + if state.users.is_empty() { + // disband + lobby.remove_custom_queue(&state.session_id).await; + } else { + let is_create = lobby.update_custom_queue( + &state.session_id, + state.users.iter().map(|user| (user.public_id.clone(), user.team)), + state.config, + ).await; + if is_create { + log::debug!("Created custom game {} lobby data to {} members", state.session_id, state.users.len()); + } else { + log::debug!("Updated custom game {} lobby data to {} members", state.session_id, state.users.len()); + } + } + }, + } + }, + Err(e) => { + log::error!("Bad intercom message received: {}", e); + } + } + } + } + } + + pub fn run(self) -> tokio::task::JoinHandle<()> { + tokio::spawn(Self::run_loop(self.userless, self.lobby)) + } +} diff --git a/rc_lobby_room/src/events/mod.rs b/rc_lobby_room/src/events/mod.rs index cf0aa0b..03faa6e 100644 --- a/rc_lobby_room/src/events/mod.rs +++ b/rc_lobby_room/src/events/mod.rs @@ -1,3 +1,6 @@ pub mod battle_found; pub mod battle_enter; pub mod enqueue_error; + +mod handler; +pub use handler::IntercomHandler; diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index 63943d6..0f371d7 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -63,8 +63,21 @@ struct QueueUser { user: std::sync::Arc + Send + Sync>>, } +struct CustomGameQueue { + config: oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig, + users: Vec, +} + +struct CustomGameQueueUser { + public_id: String, + team: u8, + queue_user: Option, // if None, the user is not enqueued +} + pub struct QueueHandler { users_in_queue: std::sync::Arc>>>, + users_in_custom_games_queue: std::sync::Arc>>, + custom_game_for_user: std::sync::Arc>>, users_per_game: usize, is_enabled: bool, hostname: String, @@ -84,6 +97,8 @@ impl QueueHandler { let mp_settings = oj_rc_core::ConfigProvider::<()>::multiplayer_settings(conf); Self { users_in_queue: std::sync::Arc::new(tokio::sync::Mutex::new(HashMap::new())), + users_in_custom_games_queue: std::sync::Arc::new(tokio::sync::Mutex::new(HashMap::new())), + custom_game_for_user: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())), users_per_game: oj_rc_core::ConfigProvider::<()>::players_per_game(conf), is_enabled: mp_settings.is_enabled, hostname: domain.to_owned(), @@ -220,6 +235,7 @@ impl QueueHandler { is_ranked: false, is_custom: false, is_complete: false, + overrides: None, }; let team_picker = user.team_chooser(&game_desc).await; /*let team_picker = match key.mode { @@ -272,7 +288,88 @@ impl QueueHandler { } } + } + async fn enter_custom_match( + &self, + _session_id: &str, + session: &mut CustomGameQueue, + user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync), + ) { + let mode = match session.config.game_mode { + oj_rc_core::persist::user::intercom::CustomGameMode::BattleArena => oj_rc_core::data::game_mode::GameMode::BattleArena, + oj_rc_core::persist::user::intercom::CustomGameMode::TeamDeathmatch => oj_rc_core::data::game_mode::GameMode::TeamDeathmatch, + oj_rc_core::persist::user::intercom::CustomGameMode::Pit => oj_rc_core::data::game_mode::GameMode::Pit, + oj_rc_core::persist::user::intercom::CustomGameMode::SuddenDeath => oj_rc_core::data::game_mode::GameMode::SuddenDeath, + }; + let visibility = match session.config.map_visibility { + oj_rc_core::persist::user::intercom::CustomGameVisibility::Good => oj_rc_core::data::game_mode::MapVisibility::Good, + oj_rc_core::persist::user::intercom::CustomGameVisibility::Poor => oj_rc_core::data::game_mode::MapVisibility::Poor, + oj_rc_core::persist::user::intercom::CustomGameVisibility::Bad => oj_rc_core::data::game_mode::MapVisibility::Bad, + }; + let key = QueueKey { + map: session.config.map.clone(), + mode, + visibility, + auto_heal: session.config.health_regen, + }; + let guid_str = key.unique_guid(); + let game_desc = oj_rc_core::persist::user::GameDescriptor { + guid: guid_str.clone(), + map: session.config.map.clone(), + mode, + visibility, + auto_heal: session.config.health_regen, + is_ranked: false, + is_custom: true, + is_complete: false, + overrides: Some(session.config.as_core()), + }; + let player_descs = session.users.iter() + .map(|user| { + let q_user = user.queue_user.as_ref().unwrap(); + oj_rc_core::persist::user::PlayerLobbyDescriptor { + user_id: q_user.user_id, + team: user.team as i32, + group: None, + public_id: q_user.player.name.clone(), + display_name: q_user.player.display_name.clone(), + } + }) + .collect(); + match user.start_custom_game(game_desc, player_descs).await { + Ok(_) => { + let player_datas = session.users.iter() + .map(|x| x.queue_user.as_ref().unwrap().player.clone()) + .collect(); + let enter_battle_ev = crate::events::battle_enter::BattleEnter { + host: self.hostname.clone(), + port: self.hostport, + map: session.config.map.clone(), + mode, + guid: guid_str.clone(), + is_ranked: false, + is_custom: true, + visibility: Some(key.visibility), + auto_heal: key.auto_heal, + player_datas, + network_config: self.network_conf.clone(), + }; + let arc_event = std::sync::Arc::new(enter_battle_ev); + for user in session.users.iter_mut() { + let player = user.queue_user.take().unwrap(); + tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone())); + } + log::info!("{} players are entering custom match {}; {}", session.users.len(), guid_str, key.short()); + }, + Err(e) => { + if let Some(msg) = e.error_msg() { + log::error!("Cannot send enter battle events to players since LobbyUser.start_game(...) failed: {} ({})", msg, e.error_code()); + } else { + log::error!("Cannot send enter battle events to players since LobbyUser.start_game(...) failed ({})", e.error_code()); + } + } + } } async fn send_events_to_player(enter_event: std::sync::Arc, sender: polariton_server::events::EventEmitter) { @@ -283,6 +380,118 @@ impl QueueHandler { } } + pub async fn join_custom_queue(&self, user: std::sync::Arc + Send + Sync>>, event_emitter: polariton_server::events::EventEmitter) { + if !self.is_enabled { + event_emitter.emit(crate::events::enqueue_error::QueueJoinError { + code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16, + text: "Multiplayer is not enabled".to_owned(), + }); + return; + } + let public_id = user.public_id(); + if let Some(session_id) = self.custom_game_for_user.read().await.get(public_id) { + let mut lock = self.users_in_custom_games_queue.lock().await; + let session = lock.get_mut(session_id).unwrap(); + let lobby_user = user.as_ref().as_ref(); + match lobby_user.player_data(&self.cpu_counter).await { + Ok(mut player_data) => { + let target_queuer = session.users.iter_mut() + .find(|user| user.public_id == public_id) + .unwrap(); + player_data.team = target_queuer.team as i32; + let new_player = QueueUser { + emitter: event_emitter, + player: player_data, + user_id: oj_rc_core::persist::user::LobbyUser::user_id(lobby_user), + enqueued_at: chrono::Utc::now(), + user: user.clone(), + }; + target_queuer.queue_user = Some(new_player); + }, + Err(e) => { + event_emitter.emit(crate::events::enqueue_error::QueueJoinError { + code: e.error_code(), + text: e.error_msg().map(|x| x.to_owned()).unwrap_or_else(|| "Unknown queue join error".to_owned()), + }); + return; + } + } + let is_all_enqueued = session.users.iter().all(|user| user.queue_user.is_some()); + if is_all_enqueued { + self.enter_custom_match(session_id, session, lobby_user).await; + } + } else { + log::debug!("Rejecting join queue for unknown custom game for user {}", public_id); + event_emitter.emit(crate::events::enqueue_error::QueueJoinError { + code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16, + text: "User is not in a custom game".to_owned(), + }); + } + } + + pub async fn remove_custom_queue(&self, session_id: &str) -> bool { + log::debug!("Disbanding custom game {}", session_id); + self.custom_game_for_user.write().await.retain(|_key, val| val != session_id); + let mut lock = self.users_in_custom_games_queue.lock().await; + lock.remove(session_id).is_some() + } + + pub async fn update_custom_queue(&self, session_id: &str, members: impl std::iter::Iterator, config: oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig) -> bool { + let mut lock = self.users_in_custom_games_queue.lock().await; + let members: std::collections::HashMap<_, _> = members.collect(); + let mut user_map_lock = self.custom_game_for_user.write().await; + if let Some(session) = lock.get_mut(session_id) { + log::debug!("Updating existing custom game {}", session_id); + let member_ids: std::collections::HashSet<_> = members.keys().collect(); + // remove users who are no longer part of the custom game + user_map_lock.retain(|key, val| (members.contains_key(key) && val == session_id) || (!members.contains_key(key) && val != session_id)); + session.users.retain(|user| member_ids.contains(&user.public_id)); + // update team assignments + session.users.iter_mut() + .for_each(|user| { + let new_team = *members.get(&user.public_id).unwrap(); + user.team = new_team; + if let Some(q_user) = &mut user.queue_user { + q_user.player.team = new_team as i32; + } + }); + // collect users which are still members + let existing_ids: std::collections::HashSet<_> = session.users.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + // add new members + for (id, team) in members.iter() { + if !existing_ids.contains(id) { + session.users.push(CustomGameQueueUser { + public_id: id.to_owned(), + team: *team, + queue_user: None, + }); + user_map_lock.insert(id.to_owned(), session_id.to_owned()); + } + } + // update config overrides + session.config = config; + false + } else { + log::debug!("Creating new custom game {}", session_id); + lock.insert(session_id.to_owned(), CustomGameQueue { + config, + users: members.iter().map(|(mem_id, team)| { + user_map_lock.insert(mem_id.to_owned(), session_id.to_owned()); + CustomGameQueueUser { + public_id: mem_id.to_owned(), + team: *team, + queue_user: None, + } + }) + .collect() + }); + true + } + } + + pub async fn join_queue(&self, map: String, mode: oj_rc_core::data::game_mode::GameMode, visibility: oj_rc_core::data::game_mode::MapVisibility, auto_heal: bool, user: std::sync::Arc + Send + Sync>>, event_emitter: polariton_server::events::EventEmitter) { if !self.is_enabled { event_emitter.emit(crate::events::enqueue_error::QueueJoinError { @@ -378,12 +587,23 @@ impl QueueHandler { } pub async fn leave_queue(&self, user: std::sync::Arc + Send + Sync>>) { + let public_id = user.public_id(); let user_id = oj_rc_core::persist::user::LobbyUser::user_id(user.as_ref().as_ref()); - for (queue_key, queue) in self.users_in_queue.lock().await.iter_mut() { - if let Some((i, _)) = queue.iter().enumerate().find(|(_, user)| user.user_id == user_id) { - queue.remove(i); - log::info!("User {} was removed from queue {}", user_id, queue_key.short()); - break; + if let Some(session_id) = self.custom_game_for_user.read().await.get(public_id) { + // player is in custom game session + let mut lock = self.users_in_custom_games_queue.lock().await; + let session = lock.get_mut(session_id).unwrap(); + let target = session.users.iter_mut().find(|user| user.public_id == public_id).unwrap(); + target.queue_user = None; + log::info!("User {} was removed from custom game {} queue", user_id, session_id); + } else { + // fallback to regular multiplayer + for (queue_key, queue) in self.users_in_queue.lock().await.iter_mut() { + if let Some((i, _)) = queue.iter().enumerate().find(|(_, user)| user.user_id == user_id) { + queue.remove(i); + log::info!("User {} was removed from queue {}", user_id, queue_key.short()); + break; + } } } } diff --git a/rc_lobby_room/src/main.rs b/rc_lobby_room/src/main.rs index 8b71e2f..8e7bb55 100644 --- a/rc_lobby_room/src/main.rs +++ b/rc_lobby_room/src/main.rs @@ -46,6 +46,11 @@ async fn main() -> std::io::Result<()> { let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(&init_ctx), polariton_server::events::EventsHandler::new())); + crate::events::IntercomHandler::new( + init_ctx.users.clone(), + init_ctx.queue.clone(), + ).run(); + let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; diff --git a/rc_lobby_room/src/operations/join_queue.rs b/rc_lobby_room/src/operations/join_queue.rs index 2a9bc23..07a9abf 100644 --- a/rc_lobby_room/src/operations/join_queue.rs +++ b/rc_lobby_room/src/operations/join_queue.rs @@ -30,24 +30,49 @@ impl SimpleOperation for QueueJoinProvider { if let Some(Typed::Bool(is_leader)) = params.remove(&IS_GROUP_LEADER_PARAM_KEY) { if let Some(Typed::Int(lobby_ty)) = params.remove(&LOBBY_TY_PARAM_KEY) { let lobby_ty = oj_rc_core::data::lobby::LobbyType::from_int(lobby_ty)?; - if let Some(Typed::Str(event_to_join)) = params.remove(&EVENT_TO_JOIN_PARAM_KEY) { - log::debug!("Got lobby join queue request of platoon {} ({} players, is_leader:{}) slot {} lobby {:?} event {}", group_id.string, group_size, is_leader, slot_id, lobby_ty, event_to_join.string); - params.insert(ESTIMATED_QUEUE_TIME_PARAM_KEY, Typed::Int(42)); - params.insert(PERSONAL_RANKING_PARAM_KEY, Typed::Double(42.0)); - let events = user.event_sender(); - let user_info = user.user()?; - if let Some(current_lobby) = user_info.current_game_event_setter().get_multiplayer().await { - self.queue_handler.join_queue( - current_lobby.map, - current_lobby.mode, - current_lobby.visibility, - current_lobby.auto_heal, + log::debug!("Got lobby join queue request of platoon {} ({} players, is_leader:{}) slot {} lobby {:?}", group_id.string, group_size, is_leader, slot_id, lobby_ty); + match lobby_ty { + oj_rc_core::data::lobby::LobbyType::None => { + log::warn!("Cannot join queue for None mode (???)"); + }, + oj_rc_core::data::lobby::LobbyType::CustomGame => { + log::debug!("Joining platoon {} to custom game lobby", group_id.string); + params.insert(ESTIMATED_QUEUE_TIME_PARAM_KEY, Typed::Int(42)); + params.insert(PERSONAL_RANKING_PARAM_KEY, Typed::Double(42.0)); + let events = user.event_sender(); + let user_info = user.user()?; + self.queue_handler.join_custom_queue( user_info.clone(), events.to_owned(), ).await; - } - + }, + oj_rc_core::data::lobby::LobbyType::QuickPlay => { + // regular multiplayer + if let Some(Typed::Str(event_to_join)) = params.remove(&EVENT_TO_JOIN_PARAM_KEY) { + log::debug!("Joining platoon {} to multiplayer lobby with event {}", group_id.string, event_to_join.string); + params.insert(ESTIMATED_QUEUE_TIME_PARAM_KEY, Typed::Int(42)); + params.insert(PERSONAL_RANKING_PARAM_KEY, Typed::Double(42.0)); + let events = user.event_sender(); + let user_info = user.user()?; + if let Some(current_lobby) = user_info.current_game_event_setter().get_multiplayer().await { + self.queue_handler.join_queue( + current_lobby.map, + current_lobby.mode, + current_lobby.visibility, + current_lobby.auto_heal, + user_info.clone(), + events.to_owned(), + ).await; + } + } else { + log::warn!("Missing multiplayer event, not joining platoon {} to multiplayer lobby queue", group_id.string); + } + }, + oj_rc_core::data::lobby::LobbyType::Solo => { + log::warn!("Cannot join queue for solo mode (it's singleplayer!)"); + }, } + } } } diff --git a/rc_microtransactions/src/main.rs b/rc_microtransactions/src/main.rs index cd4a419..1060b12 100644 --- a/rc_microtransactions/src/main.rs +++ b/rc_microtransactions/src/main.rs @@ -24,6 +24,11 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() + .wrap_fn(|req, srv| { + use actix_web::dev::Service; + log::trace!("Request {} {}", req.method(), req.path()); + srv.call(req) + }) .app_data(cli_args2.clone()) .service(index) .service(robocraft::robopay_store) diff --git a/rc_services_room/src/custom_game_tracker.rs b/rc_services_room/src/custom_game_tracker.rs index 0ffc3b6..7319e56 100644 --- a/rc_services_room/src/custom_game_tracker.rs +++ b/rc_services_room/src/custom_game_tracker.rs @@ -15,6 +15,7 @@ pub struct UserInfo { pub struct SessionInfo { pub session_id: String, pub config: std::collections::HashMap, + pub config_core: oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig, pub users: Vec, } @@ -36,7 +37,7 @@ struct GameHandle { struct UserHandle { public_id: String, is_invited: std::sync::atomic::AtomicBool, - team: u8, + team: std::sync::atomic::AtomicU8, status: std::sync::atomic::AtomicU8, } @@ -48,7 +49,7 @@ impl CustomGameMesh { } } - pub async fn create_game(&self, public_id: &str) -> Result { + 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); @@ -58,18 +59,19 @@ impl CustomGameMesh { let owner_handle = UserHandle { public_id: public_id.to_owned(), is_invited: std::sync::atomic::AtomicBool::new(false), - team: 0, + team: std::sync::atomic::AtomicU8::new(0), status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Ready.to_u8()), }; let game_handle = GameHandle { users: vec![owner_handle], config: GameConfig::default(), }; + let session = Self::session_from_game(&game_id, &game_handle); 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) + Ok(session) } pub async fn leave_game(&self, public_id: &str) -> (SessionLeaveResponseCode, Option) { @@ -85,6 +87,7 @@ impl CustomGameMesh { } log::debug!("Removed {} invited stragglers from custom game {}", game.users.len() - 1, game_id); } + game.users.clear(); let session = Self::session_from_game(&game_id, game); games_lock.remove(&game_id); session @@ -123,18 +126,7 @@ impl CustomGameMesh { 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() - }); + return Some(Self::session_from_game(&game_id, game)); } } None @@ -150,7 +142,7 @@ impl CustomGameMesh { 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 }, + team: std::sync::atomic::AtomicU8::new(if is_team_a { 0 } else { 1 }), status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Unknown.to_u8()) }; game.users.push(invitee_handle); @@ -215,12 +207,45 @@ impl CustomGameMesh { None } + pub async fn update_user_team(&self, updater: &str, updatee: &str, is_team_b: bool) -> (ChangeTeamResponseCode, Option) { + if let Some(game_id) = self.user_to_game.read().await.get(updatee) { + if let Some(game) = self.games.read().await.get(game_id) { + let leader = game.users.first().unwrap(); + if leader.public_id != updater { + return (ChangeTeamResponseCode::UserIsNotSessionLeader, None); + } + let target = game.users.iter() + .find(|mem| mem.public_id == updatee) + .unwrap(); + let team = if is_team_b { 1 } else { 0 }; + target.team.store(team, std::sync::atomic::Ordering::Relaxed); + let session = Self::session_from_game(&game_id, game); + return (ChangeTeamResponseCode::Success, Some(session)); + } + } + (ChangeTeamResponseCode::UnknownFail0, 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); + // the first time a user joins/creates a custom game, the game client + // automatically tries to set these fields (regardless if the user is not the leader) + const STUPID_FIELDS: &[&str] = &[ + "DamageMultiplier", + "HealthMultiplier", + "PowerMultiplier", + "GameTime", + ]; + if STUPID_FIELDS.contains(&field /* &&str lol */) { + log::debug!("Update custom game session {} config ignored (not leader; client bug with field {})", game_id, field); + return (AdjustCustomGameConfigResponseCode::Success, None); + } else { + log::debug!("Update custom game session {} config rejected (not leader) {} = {}", game_id, field, value); + 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); @@ -238,11 +263,12 @@ impl CustomGameMesh { SessionInfo { session_id: game_id.to_owned(), config: game.config.as_map(), + config_core: game.config.as_core(), 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, + team: user.team.load(std::sync::atomic::Ordering::Relaxed), state: PlayerSessionStatus::from_u8(user.status.load(std::sync::atomic::Ordering::Relaxed)) .unwrap_or(PlayerSessionStatus::Unknown), }) @@ -610,4 +636,44 @@ impl GameConfig { } } } + + fn as_core(&self) -> oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig { + oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig { + game_mode: match self.game_mode { + oj_rc_core::data::game_mode::GameMode::BattleArena => oj_rc_core::persist::user::intercom::CustomGameMode::BattleArena, + oj_rc_core::data::game_mode::GameMode::TeamDeathmatch => oj_rc_core::persist::user::intercom::CustomGameMode::TeamDeathmatch, + oj_rc_core::data::game_mode::GameMode::Pit => oj_rc_core::persist::user::intercom::CustomGameMode::Pit, + oj_rc_core::data::game_mode::GameMode::SuddenDeath => oj_rc_core::persist::user::intercom::CustomGameMode::SuddenDeath, + invalid => { + log::warn!("Custom game set to invalid mode {:?} (using sudden death as fallback)", invalid); + oj_rc_core::persist::user::intercom::CustomGameMode::SuddenDeath + }, + }, + map: self.map.clone(), + map_visibility: match self.map_visibility { + oj_rc_core::data::game_mode::MapVisibility::Good => oj_rc_core::persist::user::intercom::CustomGameVisibility::Good, + oj_rc_core::data::game_mode::MapVisibility::Poor => oj_rc_core::persist::user::intercom::CustomGameVisibility::Poor, + oj_rc_core::data::game_mode::MapVisibility::Bad => oj_rc_core::persist::user::intercom::CustomGameVisibility::Bad, + }, + health_regen: self.health_regen, + capture_segment_memory: self.capture_segment_memory, + base_shields_go_down: self.base_shields_go_down, + damage_mult: self.damage_mult, + health_mult: self.health_mult, + power_mult: self.power_mult, + game_time: self.game_time, + capture_speed: self.capture_speed, + points_kill_streak: self.points_kill_streak, + points_total_required: self.points_total_required, + number_of_kills_to_win: self.number_of_kills_to_win, + respawn_time: self.respawn_time, + core_appear_frequency: self.core_appear_frequency, + core_health_multiplier: self.core_health_multiplier, + core_destroy_time: self.core_destroy_time, + protonium_harvest: self.protonium_harvest, + ceiling_multiplier: self.ceiling_multiplier, + min_cpu: self.min_cpu, + max_cpu: self.max_cpu, + } + } } diff --git a/rc_services_room/src/data/custom_games.rs b/rc_services_room/src/data/custom_games.rs index 5f754a1..b1860cb 100644 --- a/rc_services_room/src/data/custom_games.rs +++ b/rc_services_room/src/data/custom_games.rs @@ -87,6 +87,17 @@ pub enum CheckCanJoinQueueResponseCode { UserNotInSession5 = 5, } +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum ChangeTeamResponseCode { + UnknownFail0 = 0, + UserIsNotSessionLeader = 1, + UnknownFail2 = 2, + UnknownFail3 = 3, + Success = 4, + UnknownFail5 = 5, +} + #[repr(u8)] #[derive(Copy, Clone)] pub enum PlayerSessionStatus { diff --git a/rc_services_room/src/operations/custom_game_adjust.rs b/rc_services_room/src/operations/custom_game_adjust.rs index b22c349..efe1f8a 100644 --- a/rc_services_room/src/operations/custom_game_adjust.rs +++ b/rc_services_room/src/operations/custom_game_adjust.rs @@ -32,6 +32,17 @@ impl SimpleOperation for CustomGameConfigChanger { .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; + user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage { + session_id: session.session_id.clone(), + config: session.config_core, + users: session.users.iter() + .filter(|user| !user.is_invited) + .map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData { + public_id: user.public_id.clone(), + team: user.team, + }) + .collect(), + }).await; } params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); } diff --git a/rc_services_room/src/operations/custom_game_create.rs b/rc_services_room/src/operations/custom_game_create.rs index fb1d59c..69462ce 100644 --- a/rc_services_room/src/operations/custom_game_create.rs +++ b/rc_services_room/src/operations/custom_game_create.rs @@ -22,9 +22,19 @@ impl SimpleOperation for CustomGameCreator { Err(e) => { params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(e as _)); }, - Ok(game_id) => { + Ok(session) => { + user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage { + session_id: session.session_id.clone(), + config: session.config_core, + users: vec![ + oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData { + public_id: user_info.public_id().to_owned(), + team: 0, + } + ], + }).await; 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())); + params.insert(IDK_PARAM_KEY, Typed::Str(session.session_id.into())); } } Ok(params) diff --git a/rc_services_room/src/operations/custom_game_invite_respond.rs b/rc_services_room/src/operations/custom_game_invite_respond.rs index d63ad4e..0644373 100644 --- a/rc_services_room/src/operations/custom_game_invite_respond.rs +++ b/rc_services_room/src/operations/custom_game_invite_respond.rs @@ -30,6 +30,18 @@ impl SimpleOperation for CustomGameInviteResponder { .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; + } else { + user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage { + session_id: session.session_id.clone(), + config: session.config_core, + users: session.users.iter() + .filter(|user| !user.is_invited) + .map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData { + public_id: user.public_id.clone(), + team: user.team, + }) + .collect() + }).await; } let event = crate::events::CustomGameRefresh { session: session.session_id, diff --git a/rc_services_room/src/operations/custom_game_kick.rs b/rc_services_room/src/operations/custom_game_kick.rs index 19bfb35..5d194db 100644 --- a/rc_services_room/src/operations/custom_game_kick.rs +++ b/rc_services_room/src/operations/custom_game_kick.rs @@ -29,11 +29,22 @@ impl SimpleOperation for CustomGameMemberKicker { }; self.mesh.send_event_to(&user_to_kick.string, kick_event).await; let update_event = crate::events::CustomGameRefresh { - session: session.session_id, + session: session.session_id.clone(), }; let members_iter = session.users.iter() .map(|mem| &mem.public_id as &str); self.mesh.broadcast_event_to(members_iter, update_event).await; + user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage { + session_id: session.session_id, + config: session.config_core, + users: session.users.iter() + .filter(|user| !user.is_invited) + .map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData { + public_id: user.public_id.clone(), + team: user.team, + }) + .collect() + }).await; } params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); } diff --git a/rc_services_room/src/operations/custom_game_leave.rs b/rc_services_room/src/operations/custom_game_leave.rs index 1474387..9b80b9e 100644 --- a/rc_services_room/src/operations/custom_game_leave.rs +++ b/rc_services_room/src/operations/custom_game_leave.rs @@ -20,11 +20,22 @@ impl SimpleOperation for CustomGameLeaver { 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, + session: session.session_id.clone(), }; let session_members = session.users.iter() .map(|mem| &mem.public_id as &str); self.mesh.broadcast_event_to(session_members, event).await; + user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage { + session_id: session.session_id, + config: session.config_core, + users: session.users.iter() + .filter(|user| !user.is_invited) + .map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData { + public_id: user.public_id.clone(), + team: user.team, + }) + .collect() + }).await; } params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); Ok(params) diff --git a/rc_services_room/src/operations/custom_game_team.rs b/rc_services_room/src/operations/custom_game_team.rs new file mode 100644 index 0000000..e3b3e93 --- /dev/null +++ b/rc_services_room/src/operations/custom_game_team.rs @@ -0,0 +1,59 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 151; + +const USERNAME_PARAM_KEY: u8 = 185; // str; in +//const CHANGER_PARAM_KEY: u8 = 186; // always null?; in +const IS_TEAM_B_PARAM_KEY: u8 = 187; // bool; in +const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out + +pub(super) struct CustomGameTeamChanger { + games: std::sync::Arc, + mesh: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for CustomGameTeamChanger { + 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(Typed::Bool(is_team_b)) = params.remove(&IS_TEAM_B_PARAM_KEY) { + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + let (resp_code, session_opt) = self.games.update_user_team(my_pub_id, &username.string, is_team_b).await; + if let Some(session) = session_opt { + let event = crate::events::CustomGameRefresh { + session: session.session_id.clone(), + }; + let session_members_iter = session.users.iter() + .filter(|mem| !mem.is_invited) + .map(|mem| &mem.public_id as &str); + self.mesh.broadcast_event_to(session_members_iter, event).await; + user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage { + session_id: session.session_id, + config: session.config_core, + users: session.users.iter() + .filter(|user| !user.is_invited) + .map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData { + public_id: user.public_id.clone(), + team: user.team, + }) + .collect() + }).await; + } + params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _)); + } + } + Ok(params) + } +} + +pub(super) fn game_team_change_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(CustomGameTeamChanger { + games: init_ctx.custom_games.clone(), + mesh: init_ctx.user_mesh.clone(), + }) +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index a3c615b..d233e4e 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -116,6 +116,7 @@ mod custom_game_invite_respond; mod custom_game_kick; mod custom_game_player_state; mod custom_game_can_join_queue; +mod custom_game_team; use polariton_server::operations::OperationsHandler; @@ -256,4 +257,5 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .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)) + .add(custom_game_team::game_team_change_provider(init_ctx)) }