From 94d247f8f40284effc50b788dae1b90ee164689a Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sat, 16 Aug 2025 15:52:06 -0400 Subject: [PATCH] Minor refactor for fake server players --- rc_core/src/persist/maps.rs | 9 +- rc_core/src/persist/user/account_json.rs | 203 +----------------- rc_core/src/persist/user/lobby.rs | 102 +++++++++ rc_core/src/persist/user/mod.rs | 5 +- rc_core/src/persist/user/multiplayer.rs | 140 ++++++++++++ rc_core/src/persist/user/traits.rs | 10 +- .../m20250816_000001_add_fake_players.rs | 154 +++++++++++++ rc_database/src/migration/mod.rs | 2 + .../src/schema/multiplayer_game_player.rs | 4 +- rc_database/src/wrapper.rs | 43 +++- rc_lobby_room/src/lobby.rs | 6 +- rc_lobby_room/src/operations/mod.rs | 2 +- rc_multiplayer/src/matches/generic.rs | 85 ++++++-- rc_multiplayer/src/matches/modes/no_op.rs | 1 + 14 files changed, 530 insertions(+), 236 deletions(-) create mode 100644 rc_core/src/persist/user/lobby.rs create mode 100644 rc_core/src/persist/user/multiplayer.rs create mode 100644 rc_database/src/migration/m20250816_000001_add_fake_players.rs diff --git a/rc_core/src/persist/maps.rs b/rc_core/src/persist/maps.rs index 952f403..43ac8df 100644 --- a/rc_core/src/persist/maps.rs +++ b/rc_core/src/persist/maps.rs @@ -28,6 +28,13 @@ impl SpawnPoint { self } + const fn scale(mut self, scale: f32) -> Self { + self.x *= scale; + self.y *= scale; + self.z *= scale; + self + } + fn rotated_from(mut self, x: f32, y: f32, z: f32, rot: num_quaternion::Quaternion) -> Self { if let Some(unit_rot) = rot.normalize() { let rotated = unit_rot.rotate_vector([self.x, self.y, self.z]); @@ -974,7 +981,7 @@ pub(super) fn default_map() -> std::collections::HashMap Result { + pub(super) async fn user_player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result { let current_slot = self.db.garage_selected(self.account.id).await.map_err(|e| { log::error!("Failed to retrieve selected vehicle for user_id {} (user_player_data): {}", self.account.id, e); polariton_server::operations::SimpleOpError::with_message(DATABASE_ERR, format!("Could not retrieve selected garage: {}", e)) @@ -556,7 +556,7 @@ impl UserData { Ok(players) } - async fn get_players_in_current_game(&self) -> Result, polariton_server::operations::SimpleOpError> { + async fn get_players_in_current_game(&self) -> Result, polariton_server::operations::SimpleOpError> { let current_game = self.db.game_by_user_id_and_completion(self.account.id, false).await .map_err(|e| { log::error!("Failed to retrieve ongoing game for user {}: {}", self.account.id, e); @@ -567,7 +567,7 @@ impl UserData { })?; if let Some(current_game) = current_game { let guid = current_game.guid; - self.db.players_by_game_guid_and_completion_heavy(current_game.guid, false).await + self.db.players_by_game_guid_and_completion(current_game.guid, false).await .map_err(|e| { log::error!("Failed to retrieve players for game {} for user {}: {}", guid, self.account.id, e); polariton_server::operations::SimpleOpError::with_message( @@ -580,7 +580,7 @@ impl UserData { } } - async fn get_teammates_in_current_game(&self) -> Result, polariton_server::operations::SimpleOpError> { + async fn get_teammates_in_current_game(&self) -> Result, polariton_server::operations::SimpleOpError> { let current_game_info = self.db.game_and_player_by_user_id_and_completion(self.account.id, false).await .map_err(|e| { log::error!("Failed to retrieve ongoing game for user {}: {}", self.account.id, e); @@ -591,7 +591,7 @@ impl UserData { })?; if let Some((current_game, current_player)) = current_game_info { let guid = current_game.guid; - self.db.players_by_game_guid_and_completion_and_team_heavy(current_game.guid, current_player.team, false).await + self.db.players_by_game_guid_and_completion_and_team(current_game.guid, current_player.team, false).await .map_err(|e| { log::error!("Failed to retrieve players for game {} for user {}: {}", guid, self.account.id, e); polariton_server::operations::SimpleOpError::with_message( @@ -1352,7 +1352,7 @@ impl super::ChatUser for UserData { Ok( self.get_teammates_in_current_game().await? .into_iter() - .map(|player| player.1.public_id) + .map(|player| player.public_id) .collect() ) } @@ -1361,197 +1361,8 @@ impl super::ChatUser for UserData { Ok( self.get_players_in_current_game().await? .into_iter() - .map(|player| player.1.public_id) + .map(|player| player.public_id) .collect() ) } } - -#[async_trait::async_trait] -impl super::LobbyUser for UserData { - fn user_id(&self) -> i32 { - self.account.id - } - - async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result { - self.user_player_data(cpu_counter).await.map_err(|e| { - if let Some(msg) = e.error_msg() { - polariton_server::operations::SimpleOpError::with_message(crate::data::error_codes::LobbyReasonCode::from_service_error(e.error_code()) as i16, msg.to_owned()) - } else { - polariton_server::operations::SimpleOpError::with_code(crate::data::error_codes::LobbyReasonCode::from_service_error(e.error_code()) as i16) - } - - }) - } - - async fn start_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 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), - }; - 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); - polariton_server::operations::SimpleOpError::with_message( - crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16, - format!("Failed to create 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(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), - } - }) - .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(()) - } -} - -#[async_trait::async_trait] -impl super::MultiplayerUser for UserData { - fn user_id(&self) -> i32 { - self.account.id - } - - fn user_name(&self) -> &'_ str { - &self.account.public_id - } - - fn display_name(&self) -> &'_ str { - &self.account.display_name - } - - async fn current_game(&self) -> Result, super::MultiplayerError> { - Ok(self.db.game_by_user_id_and_completion(self.account.id, false).await - .map_err(|e| { - log::error!("Failed to retrieve ongoing game for user {}: {}", self.account.id, e); - super::MultiplayerError { - code: super::MultiplayerErrorCode::CustomString, - message: format!("Failed to retrieve ongoing game: {}", e), - } - })? - .map(|game| super::GameDescriptor { - guid: crate::persist::user::i64_as_uuid_str(game.guid), - map: game.map, - mode: crate::data::game_mode::GameMode::from_db(game.mode), - visibility: crate::data::game_mode::MapVisibility::from_db(game.visibility), - auto_heal: game.auto_heal, - 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, - })) - } - - async fn game_players(&self, guid: &str) -> Result, super::MultiplayerError> { - if let Some(guid) = crate::persist::user::str_to_i64(guid) { - let players = self.db.players_by_game_guid_and_completion_heavy(guid, false).await - .map_err(|e| { - log::error!("Failed to retrieve players for game {} for user {}: {}", guid, self.account.id, e); - super::MultiplayerError { - code: super::MultiplayerErrorCode::CustomString, - message: format!("Failed to retrieve players for game {}: {}", guid, e), - } - })?; - Ok(players.into_iter() - .map(|(player, user)| super::PlayerDescriptor { - user_id: player.user_id, - player_id: player.player_id as u8, - team: player.team, - group: player.group, - is_rewards_claimed: player.is_claimed, - display_name: user.display_name, - public_id: user.public_id, - }) - .collect()) - } else { - Err(super::MultiplayerError { - code: super::MultiplayerErrorCode::IncorrectGameGuid, - message: format!("Failed to parse game GUID {}", guid), - }) - } - } - - async fn complete_game(&self, guid: &str) -> Result<(), super::MultiplayerError> { - if let Some(guid) = crate::persist::user::str_to_i64(guid) { - self.db.update_complete_game_by_game_guid(guid).await - .map_err(|e| { - log::error!("Failed to complete ongoing game with user {}: {}", self.account.id, e); - super::MultiplayerError { - code: super::MultiplayerErrorCode::CustomString, - message: format!("Failed to complete ongoing game: {}", e), - } - }) - } else { - Err(super::MultiplayerError { - code: super::MultiplayerErrorCode::IncorrectGameGuid, - message: format!("Failed to parse game GUID {}", guid), - }) - } - } - - async fn game_info(&self, guid: &str) -> Result, super::MultiplayerError> { - if let Some(guid) = crate::persist::user::str_to_i64(guid) { - let game_opt = self.db.game_by_guid(guid.to_owned()).await - .map_err(|e| { - log::error!("Failed to retrieve game {} with user {}: {}", guid, self.account.id, e); - super::MultiplayerError { - code: super::MultiplayerErrorCode::CustomString, - message: format!("Failed to retrieve game {}: {}", guid, e), - } - })?; - Ok(game_opt.map(|game| super::GameDescriptor { - guid: crate::persist::user::i64_as_uuid_str(game.guid), - map: game.map, - mode: crate::data::game_mode::GameMode::from_db(game.mode), - visibility: crate::data::game_mode::MapVisibility::from_db(game.visibility), - auto_heal: game.auto_heal, - 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, - })) - } else { - Err(super::MultiplayerError { - code: super::MultiplayerErrorCode::IncorrectGameGuid, - message: format!("Failed to parse game GUID {}", guid), - }) - } - } -} diff --git a/rc_core/src/persist/user/lobby.rs b/rc_core/src/persist/user/lobby.rs new file mode 100644 index 0000000..eb94338 --- /dev/null +++ b/rc_core/src/persist/user/lobby.rs @@ -0,0 +1,102 @@ +use super::account_json::UserData; + +#[async_trait::async_trait] +impl super::LobbyUser for UserData { + fn user_id(&self) -> i32 { + self.account.id + } + + async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result { + self.user_player_data(cpu_counter).await.map_err(|e| { + if let Some(msg) = e.error_msg() { + polariton_server::operations::SimpleOpError::with_message(crate::data::error_codes::LobbyReasonCode::from_service_error(e.error_code()) as i16, msg.to_owned()) + } else { + polariton_server::operations::SimpleOpError::with_code(crate::data::error_codes::LobbyReasonCode::from_service_error(e.error_code()) as i16) + } + + }) + } + + async fn start_game(&self, game: super::GameDescriptor, players: Vec) -> Result { + 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 fake_players = self.generate_fake_players_data(guid).await; + + 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), + }; + 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); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16, + format!("Failed to create game {}: {}", game.guid, e), + ) + })?; + + let players_len = players.len(); + + 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), + } + }) + .chain(fake_players.iter() + .enumerate() + .map(|(i, fake)| { + 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(None), + 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 + players_len) as u8) as _), + team: oj_rc_database::sea_orm::ActiveValue::Set(fake.team), + group: oj_rc_database::sea_orm::ActiveValue::Set(None), + is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(true), + public_id: oj_rc_database::sea_orm::ActiveValue::Set(fake.name.clone()), + display_name: oj_rc_database::sea_orm::ActiveValue::Set(fake.display_name.clone()), + } + }) + ) + .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(super::FakePlayers { players: fake_players }) + } +} diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 2537a61..404620c 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -11,11 +11,14 @@ mod inventory; pub use inventory::UnlockedParts; mod traits; -pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser}; +pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers}; mod intercom; pub use intercom::generate_token as generate_intercom_token; +mod multiplayer; +mod lobby; + pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; pub const USERS_DIR: &str = "accounts"; diff --git a/rc_core/src/persist/user/multiplayer.rs b/rc_core/src/persist/user/multiplayer.rs new file mode 100644 index 0000000..9df5ef3 --- /dev/null +++ b/rc_core/src/persist/user/multiplayer.rs @@ -0,0 +1,140 @@ +use super::account_json::UserData; + +impl UserData { + pub(super) async fn generate_fake_players_data(&self, _guid: i64) -> Vec { + vec![ + crate::data::player_data::PlayerData { + name: "FakeUser".to_owned(), + display_name: "Server".to_owned(), + mastery: 1, + tier: 1, + robot_name: "Very bad but very good".to_owned(), + robot_map: crate::persist::VALID_ROBOT.into(), + group: None, + team: 2, + has_premium: true, + robot_uuid: "1234_1234".to_owned(), + cpu: 42, + avatar_id: Some(0), + weapon_order: vec![0,0,0], + colour_map: crate::persist::VALID_COLOUR.into(), + is_ai: false, + spawn_effect: "Spawn".into(), + death_effect: "Explosion".into(), + player_rank: 1, + weapon_rank: Default::default(), + } + ] + } +} + +#[async_trait::async_trait] +impl super::MultiplayerUser for UserData { + fn user_id(&self) -> i32 { + self.account.id + } + + fn user_name(&self) -> &'_ str { + &self.account.public_id + } + + fn display_name(&self) -> &'_ str { + &self.account.display_name + } + + async fn current_game(&self) -> Result, super::MultiplayerError> { + Ok(self.db.game_by_user_id_and_completion(self.account.id, false).await + .map_err(|e| { + log::error!("Failed to retrieve ongoing game for user {}: {}", self.account.id, e); + super::MultiplayerError { + code: super::MultiplayerErrorCode::CustomString, + message: format!("Failed to retrieve ongoing game: {}", e), + } + })? + .map(|game| super::GameDescriptor { + guid: crate::persist::user::i64_as_uuid_str(game.guid), + map: game.map, + mode: crate::data::game_mode::GameMode::from_db(game.mode), + visibility: crate::data::game_mode::MapVisibility::from_db(game.visibility), + auto_heal: game.auto_heal, + 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, + })) + } + + async fn game_players(&self, guid: &str) -> Result, super::MultiplayerError> { + if let Some(guid) = crate::persist::user::str_to_i64(guid) { + let players = self.db.players_by_game_guid_and_completion(guid, false).await + .map_err(|e| { + log::error!("Failed to retrieve players for game {} for user {}: {}", guid, self.account.id, e); + super::MultiplayerError { + code: super::MultiplayerErrorCode::CustomString, + message: format!("Failed to retrieve players for game {}: {}", guid, e), + } + })?; + Ok(players.into_iter() + .map(|player| super::PlayerDescriptor { + user_id: player.user_id, + player_id: player.player_id as u8, + team: player.team, + group: player.group, + is_rewards_claimed: player.is_claimed, + display_name: player.display_name, + public_id: player.public_id, + }) + .collect()) + } else { + Err(super::MultiplayerError { + code: super::MultiplayerErrorCode::IncorrectGameGuid, + message: format!("Failed to parse game GUID {}", guid), + }) + } + } + + async fn complete_game(&self, guid: &str) -> Result<(), super::MultiplayerError> { + if let Some(guid) = crate::persist::user::str_to_i64(guid) { + self.db.update_complete_game_by_game_guid(guid).await + .map_err(|e| { + log::error!("Failed to complete ongoing game with user {}: {}", self.account.id, e); + super::MultiplayerError { + code: super::MultiplayerErrorCode::CustomString, + message: format!("Failed to complete ongoing game: {}", e), + } + }) + } else { + Err(super::MultiplayerError { + code: super::MultiplayerErrorCode::IncorrectGameGuid, + message: format!("Failed to parse game GUID {}", guid), + }) + } + } + + async fn game_info(&self, guid: &str) -> Result, super::MultiplayerError> { + if let Some(guid) = crate::persist::user::str_to_i64(guid) { + let game_opt = self.db.game_by_guid(guid.to_owned()).await + .map_err(|e| { + log::error!("Failed to retrieve game {} with user {}: {}", guid, self.account.id, e); + super::MultiplayerError { + code: super::MultiplayerErrorCode::CustomString, + message: format!("Failed to retrieve game {}: {}", guid, e), + } + })?; + Ok(game_opt.map(|game| super::GameDescriptor { + guid: crate::persist::user::i64_as_uuid_str(game.guid), + map: game.map, + mode: crate::data::game_mode::GameMode::from_db(game.mode), + visibility: crate::data::game_mode::MapVisibility::from_db(game.visibility), + auto_heal: game.auto_heal, + 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, + })) + } else { + Err(super::MultiplayerError { + code: super::MultiplayerErrorCode::IncorrectGameGuid, + message: format!("Failed to parse game GUID {}", guid), + }) + } + } +} diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 68401b8..5e07051 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -249,7 +249,11 @@ impl SanctionType { pub trait LobbyUser { fn user_id(&self) -> i32; async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result; - async fn start_game(&self, game: GameDescriptor, players: Vec) -> Result<(), polariton_server::operations::SimpleOpError>; + async fn start_game(&self, game: GameDescriptor, players: Vec) -> Result; +} + +pub struct FakePlayers { + pub players: Vec } pub struct CurrentGameEvent { @@ -275,12 +279,14 @@ pub struct GameDescriptor { pub struct PlayerLobbyDescriptor { pub user_id: i32, pub team: i32, + pub public_id: String, + pub display_name: String, pub group: Option, } #[derive(Clone)] pub struct PlayerDescriptor { - pub user_id: i32, + pub user_id: Option, pub player_id: u8, pub team: i32, pub group: Option, diff --git a/rc_database/src/migration/m20250816_000001_add_fake_players.rs b/rc_database/src/migration/m20250816_000001_add_fake_players.rs new file mode 100644 index 0000000..8c9b997 --- /dev/null +++ b/rc_database/src/migration/m20250816_000001_add_fake_players.rs @@ -0,0 +1,154 @@ +use sea_orm::{DbBackend, Statement}; +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20250816_000001_add_fake_players" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Make user_id column nullable/optional + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + match manager.get_database_backend() { + DbBackend::Sqlite => { + // cannot modify existing column, let's just drop it since this is supposed to be only for dev work + manager.drop_table( + Table::drop() + .table(crate::schema::multiplayer_game_player::Entity) + .to_owned() + ).await?; + manager + .create_table( + Table::create() + .table(crate::schema::multiplayer_game_player::Entity) + .col( + ColumnDef::new(crate::schema::multiplayer_game_player::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::UserId).integer().null()) + .foreign_key( + ForeignKey::create() + .name("fk-players-user_id") + .from(crate::schema::multiplayer_game_player::Entity, crate::schema::multiplayer_game_player::Column::UserId) + .to(crate::schema::user::Entity, crate::schema::user::Column::Id), + ) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::GameId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-players-game_id") + .from(crate::schema::multiplayer_game_player::Entity, crate::schema::multiplayer_game_player::Column::GameId) + .to(crate::schema::multiplayer_game::Entity, crate::schema::multiplayer_game::Column::Id), + ) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::PlayerId).small_integer().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::Team).integer().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::Group).integer().null()) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::IsClaimed).boolean().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::PublicId).string().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game_player::Column::DisplayName).string().not_null()) + .to_owned(), + ) + .await + }, + _ => { + manager + .alter_table( + Table::alter() + .table(crate::schema::multiplayer_game_player::Entity) + .modify_column(ColumnDef::new(crate::schema::multiplayer_game_player::Column::UserId).integer().null()) + .add_column(ColumnDef::new(crate::schema::multiplayer_game_player::Column::PublicId).string().not_null().default("???")) + .add_column(ColumnDef::new(crate::schema::multiplayer_game_player::Column::DisplayName).string().not_null().default("???")) + .to_owned() + ) + .await + } + } + + } + + // Define how to rollback this migration: Makes the colum not nullable. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // this will always fail in sqlite, but why are you rolling back migrations anyway? + match manager.get_database_backend() { + DbBackend::Sqlite => { + // cannot modify existing column, just do it in raw sqlite for simplicity + let statements = [ + r#"CREATE TEMPORARY TABLE temp AS + SELECT + id, + user_id, + game_id, + creation_time, + player_id, + team, + "group", + is_claimed + FROM players;"#, + + r#"DROP TABLE players;"#, + + r#"CREATE TABLE players ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + game_id INTEGER NOT NULL, + creation_time BIGINT NOT NULL, + player_id SMALLINT NOT NULL, + team INTEGER NOT NULL, + "group" INTEGER, + is_claimed BOOLEAN NOT NULL, + CONSTRAINT FK_players_games FOREIGN KEY (game_id) REFERENCES games(id), + CONSTRAINT FK_players_users FOREIGN KEY (user_id) REFERENCES users(id) + );"#, + + r#"INSERT INTO players + (id, + user_id, + game_id, + creation_time, + player_id, + team, + "group", + is_claimed) + SELECT + id, + user_id, + game_id, + creation_time, + player_id, + team, + "group", + is_claimed + FROM temp + WHERE user_id IS NOT NULL;"#, + + r#"DROP TABLE temp;"#, + ]; + for sql in statements { + let statement = Statement::from_sql_and_values(DbBackend::Sqlite, sql, []); + manager.get_connection().execute(statement).await?; + } + Ok(()) + } + _ => { + manager + .alter_table( + Table::alter() + .table(crate::schema::multiplayer_game_player::Entity) + .modify_column(ColumnDef::new(crate::schema::multiplayer_game_player::Column::UserId).integer().not_null()) + .drop_column(crate::schema::multiplayer_game_player::Column::PublicId) + .drop_column(crate::schema::multiplayer_game_player::Column::DisplayName) + .to_owned() + ) + .await + } + } + + } +} diff --git a/rc_database/src/migration/mod.rs b/rc_database/src/migration/mod.rs index f0889cb..48b7b72 100644 --- a/rc_database/src/migration/mod.rs +++ b/rc_database/src/migration/mod.rs @@ -10,6 +10,7 @@ mod m20250529_000001_create_sanction_table; mod m20250713_000001_create_game_table; mod m20250713_000002_create_player_table; mod m20250722_000001_create_game_event_table; +mod m20250816_000001_add_fake_players; pub struct Migrator; @@ -27,6 +28,7 @@ impl MigratorTrait for Migrator { Box::new(m20250713_000001_create_game_table::Migration), Box::new(m20250713_000002_create_player_table::Migration), Box::new(m20250722_000001_create_game_event_table::Migration), + Box::new(m20250816_000001_add_fake_players::Migration), ] } } diff --git a/rc_database/src/schema/multiplayer_game_player.rs b/rc_database/src/schema/multiplayer_game_player.rs index 046d916..f5b3983 100644 --- a/rc_database/src/schema/multiplayer_game_player.rs +++ b/rc_database/src/schema/multiplayer_game_player.rs @@ -5,13 +5,15 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: i32, - pub user_id: i32, + pub user_id: Option, pub game_id: i32, pub creation_time: i64, // seconds since unix epoch pub player_id: i16, // actually u8 pub team: i32, pub group: Option, // probably a user id pub is_claimed: bool, + pub public_id: String, + pub display_name: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index b1e60ba..1e95f40 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -1,5 +1,5 @@ use sea_orm_migration::MigratorTrait; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, RelationTrait}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait}; pub struct Database { orm: sea_orm::DatabaseConnection, @@ -330,16 +330,19 @@ impl Database { } pub async fn players_by_game_guid_and_completion(&self, game_guid: i64, is_complete: bool) -> Result, sea_orm::DbErr> { - crate::schema::multiplayer_game_player::Entity::find() - .join(sea_orm::JoinType::InnerJoin, crate::schema::multiplayer_game::Relation::Player.def()) + Ok(crate::schema::multiplayer_game_player::Entity::find() + .find_also_related(crate::schema::multiplayer_game::Entity) .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) - .into_model::() + //.into_model::() .all(&self.orm) - .await + .await? + .into_iter() + .map(|(player, _)| player) + .collect()) } - pub async fn players_by_game_guid_and_completion_heavy(&self, game_guid: i64, is_complete: bool) -> Result, sea_orm::DbErr> { + /*pub async fn players_by_game_guid_and_completion_heavy(&self, game_guid: i64, is_complete: bool) -> Result, sea_orm::DbErr> { Ok(crate::schema::multiplayer_game_player::Entity::find() .find_also_related(crate::schema::user::Entity) .find_also_related(crate::schema::multiplayer_game::Entity) @@ -352,9 +355,9 @@ impl Database { .into_iter() .filter_map(|(player, user, _)| user.map(|user| (player, user))) .collect()) - } + }*/ - pub async fn players_by_game_guid_and_completion_and_team_heavy(&self, game_guid: i64, team: i32, is_complete: bool) -> Result, sea_orm::DbErr> { + /*pub async fn players_by_game_guid_and_completion_and_team_heavy(&self, game_guid: i64, team: i32, is_complete: bool) -> Result, sea_orm::DbErr> { Ok(crate::schema::multiplayer_game_player::Entity::find() .find_also_related(crate::schema::user::Entity) .find_also_related(crate::schema::multiplayer_game::Entity) @@ -368,14 +371,32 @@ impl Database { .into_iter() .filter_map(|(player, user, _)| user.map(|user| (player, user))) .collect()) + }*/ + + pub async fn players_by_game_guid_and_completion_and_team(&self, game_guid: i64, team: i32, is_complete: bool) -> Result, sea_orm::DbErr> { + Ok(crate::schema::multiplayer_game_player::Entity::find() + .find_also_related(crate::schema::multiplayer_game::Entity) + //.join(sea_orm::JoinType::InnerJoin, crate::schema::multiplayer_game::Relation::Player.def()) + //.join(sea_orm::JoinType::InnerJoin, crate::schema::user::Relation::Player.def()) + .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) + .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) + .filter(crate::schema::multiplayer_game_player::Column::Team.eq(team)) + .all(&self.orm) + .await? + .into_iter() + .map(|(player, _)| player) + .collect()) } pub async fn players_by_game_id_and_completion(&self, game_id: i32) -> Result, sea_orm::DbErr> { - crate::schema::multiplayer_game_player::Entity::find() - .join(sea_orm::JoinType::InnerJoin, crate::schema::multiplayer_game::Relation::Player.def()) + Ok(crate::schema::multiplayer_game_player::Entity::find() + .find_also_related(crate::schema::multiplayer_game::Entity) .filter(crate::schema::multiplayer_game::Column::Id.eq(game_id)) .all(&self.orm) - .await + .await? + .into_iter() + .map(|(player, _)| player) + .collect()) } pub async fn insert_players(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index ff84c08..5492f5f 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -68,6 +68,8 @@ impl QueueHandler { user_id: x.user_id, team: x.player.team, group: None, // TODO support platoons + public_id: x.player.name.clone(), + display_name: x.player.display_name.clone(), }).collect(); let game_desc = oj_rc_core::persist::user::GameDescriptor { guid: guid_str.clone(), @@ -80,8 +82,8 @@ impl QueueHandler { is_complete: false, }; match user.start_game(game_desc, player_descs).await { - Ok(_) => { - let player_datas = players.iter().map(|x| x.player.clone()).collect(); + Ok(fakes) => { + let player_datas = players.iter().map(|x| x.player.clone()).chain(fakes.players.into_iter()).collect(); let enter_battle_ev = crate::events::battle_enter::BattleEnter { host: self.hostname.clone(), port: self.hostport, diff --git a/rc_lobby_room/src/operations/mod.rs b/rc_lobby_room/src/operations/mod.rs index cf83a61..8d1d001 100644 --- a/rc_lobby_room/src/operations/mod.rs +++ b/rc_lobby_room/src/operations/mod.rs @@ -1,5 +1,5 @@ mod more_auth; -mod eac; +//mod eac; mod no_quit; mod join_queue; diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 4006692..0ffb35e 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -7,6 +7,28 @@ pub(super) struct UserConnection { pub(super) counters: UserData, } +#[allow(dead_code)] +pub(super) struct FakeUser { + pub(super) state: std::sync::Arc, + pub(super) machine: MachineState, + pub(super) descriptor: oj_rc_core::persist::user::PlayerDescriptor, + pub(super) counters: UserData, +} + +impl FakeUser { + fn new(descriptor: oj_rc_core::persist::user::PlayerDescriptor) -> Self { + Self { + state: std::sync::Arc::new(UserState { + mode: std::sync::atomic::AtomicU8::new(ConnectionMode::InGame.to_u8()), + progress: std::sync::atomic::AtomicU8::new(100), + }), + machine: MachineState::new(), + descriptor, + counters: UserData::new(), + } + } +} + #[derive(Clone)] pub(super) struct UserSender { pub(super) connection: std::sync::Arc>, @@ -171,6 +193,7 @@ pub(super) struct GenericGamemodeEngine { pub game_descriptor: oj_rc_core::persist::user::GameDescriptor, pub players_info: std::sync::Arc>, pub custom_logic_handler: L, + pub fake_users: std::collections::HashMap, } impl GenericGamemodeEngine { @@ -184,6 +207,10 @@ impl GenericGamemodeEngine { custom: L ) -> Self { + let fake_users = players.iter() + .filter(|player| player.user_id.is_none()) + .map(|player| (player.team as u8, FakeUser::new(player.to_owned()))) + .collect(); Self { users: tokio::sync::RwLock::new(std::collections::HashMap::new()), user_id_map: tokio::sync::RwLock::new(std::collections::HashMap::new()), @@ -193,6 +220,7 @@ impl GenericGamemodeEngine { game_descriptor: game, players_info: std::sync::Arc::new(players), custom_logic_handler: custom, + fake_users, } } @@ -293,7 +321,7 @@ impl GenericGamemodeEngine { //tokio::time::sleep(std::time::Duration::from_secs(1)).await; //let id = users.len() as u8; let user_id = user.user_id(); - let player_info = self.players_info.iter().filter(|p| p.user_id == user_id).next().unwrap(); + let player_info = self.players_info.iter().filter(|p| p.user_id == Some(user_id)).next().unwrap(); let id = player_info.player_id; let new_user = UserConnection { user, @@ -371,10 +399,12 @@ impl GenericGamemodeEngine { match mode { ConnectionMode::Loading | ConnectionMode::Disconnected => {}, ConnectionMode::WaitingForSync | ConnectionMode::Sync | ConnectionMode::WaitingToStart => { - if user_id != conn.user.user_id() { - crate::events::log_lnl_send_failure(conn.connection.rlnl() - .send_data(&progress_data, rlnl::event_code::NetworkEvent::BroadcastLoadingProgress, literustlib::packet::Property::ReliableOrdered, &conn.connection.connection).await); - } + self.broadcast( + rlnl::event_code::NetworkEvent::BroadcastLoadingProgress, + literustlib::packet::Property::ReliableOrdered, + &progress_data, + false, + ).await; /*if progress > 0.95 { log::info!("User {} is ready, ending sync", user_id); crate::events::log_lnl_send_failure(crate::handlers::simple_typed::RlnlSender::new(&conn.sender) @@ -421,12 +451,34 @@ impl GenericGamemodeEngine { let sender = crate::handlers::RlnlSender::new(&user_info.0.sender); for conn in self.users.read().await.values() { if user_id == conn.user.user_id() { continue; } - crate::events::log_lnl_send_failure(sender.send_data( + /*crate::events::log_lnl_send_failure(sender.send_data( &user_info.1, rlnl::event_code::NetworkEvent::BroadcastLoadingProgress, literustlib::packet::Property::ReliableOrdered, &user_info.0.connection, - ).await); + ).await);*/ + let event = rlnl::events::loading::LoadingProgress { + user_name: rlnl::types::BinaryWriterString(conn.user.user_name().to_owned()), + progress: (conn.state.progress.load(std::sync::atomic::Ordering::Relaxed) as f32) / 100.0, + }; + crate::events::log_lnl_send_failure(sender.send_data( + &event, + rlnl::event_code::NetworkEvent::BroadcastLoadingProgress, + literustlib::packet::Property::ReliableOrdered, + &user_info.0.connection, + ).await) + } + for fake in self.fake_users.values() { + let event = rlnl::events::loading::LoadingProgress { + user_name: rlnl::types::BinaryWriterString("FakeUser".to_owned()), + progress: (fake.state.progress.load(std::sync::atomic::Ordering::Relaxed) as f32) / 100.0, + }; + crate::events::log_lnl_send_failure(sender.send_data( + &event, + rlnl::event_code::NetworkEvent::BroadcastLoadingProgress, + literustlib::packet::Property::ReliableOrdered, + &user_info.0.connection, + ).await) } } else { log::error!("Failed to find user {} in connected users for match {}", user_id, self.game_guid()); @@ -466,7 +518,7 @@ impl GenericGamemodeEngine { } } } - let player_count = self.players_info.len(); + let player_count = self.players_info.iter().filter(|x| x.user_id.is_some()).count(); if ready_count == player_count { log::info!("All players ({}) awaiting sync for game {}", player_count, self.game_guid()); for (user_key, conn) in self.users.read().await.iter() { @@ -480,18 +532,9 @@ impl GenericGamemodeEngine { if let Some(conn) = self.users.read().await.get(&user_key) { log::info!("Loading complete for game {}, user {} ({})", self.game_guid(), user_id, user_key); conn.state.progress.store(100, std::sync::atomic::Ordering::Relaxed); - conn.state.mode.store(ConnectionMode::WaitingToStart.to_u8(), std::sync::atomic::Ordering::Relaxed); - /*let game_start = chrono::DateTime::from_timestamp(self.game_start.load(std::sync::atomic::Ordering::Relaxed), 0).unwrap(); - let payload = super::countdown::time_to_game_start_payload(game_start); - let sender = conn.connection.rlnl(); - if let Err(e) = sender.send_data( - &payload, - rlnl::event_code::NetworkEvent::TimeToGameStart, - literustlib::packet::Property::ReliableOrdered, - &conn.connection.connection) - .await { - log::error!("Failed to send updated TimeToGameStart to a user: {}", e); - }*/ + if matches!(ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)), ConnectionMode::Sync) { + conn.state.mode.store(ConnectionMode::WaitingToStart.to_u8(), std::sync::atomic::Ordering::Relaxed); + } self.spawn_initial_ingame_events(conn, user_id); } else { log::warn!("Invalid LoadComplete user key {} for game {}", user_key, self.game_guid()); @@ -509,7 +552,7 @@ impl GenericGamemodeEngine { } // trigger game start if all_users_loading_complete { - let player_count = self.players_info.len(); + let player_count = self.players_info.iter().filter(|x| x.user_id.is_some()).count(); log::info!("All players ({}) are ready for game {}", player_count, self.game_guid()); tokio::time::sleep(Self::END_OF_SYNC_DELAY).await; let game_start = chrono::Utc::now() + Self::COUNTDOWN_DURATION; diff --git a/rc_multiplayer/src/matches/modes/no_op.rs b/rc_multiplayer/src/matches/modes/no_op.rs index 1247206..c9d92c8 100644 --- a/rc_multiplayer/src/matches/modes/no_op.rs +++ b/rc_multiplayer/src/matches/modes/no_op.rs @@ -1,5 +1,6 @@ use crate::matches::CustomGameLogic; +#[allow(dead_code)] pub struct NoOpLogic; #[async_trait::async_trait]