From 37cbc8d85fbb350bdba369a9f415f1290a9a934b Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 20 Jul 2025 18:31:08 -0400 Subject: [PATCH] Allow two or more people to load into the same multiplayer match #30 --- rc_core/src/data/game_mode.rs | 44 +++ rc_core/src/data/player_data.rs | 4 +- rc_core/src/persist/combat.rs | 2 +- rc_core/src/persist/user/account_json.rs | 140 +++++++++ rc_core/src/persist/user/mod.rs | 2 +- rc_core/src/persist/user/traits.rs | 65 ++++- .../m20250713_000001_create_game_table.rs | 45 +++ .../m20250713_000002_create_player_table.rs | 56 ++++ rc_database/src/migration/mod.rs | 4 + rc_database/src/schema/mod.rs | 2 + rc_database/src/schema/multiplayer_game.rs | 58 ++++ .../src/schema/multiplayer_game_player.rs | 45 +++ rc_database/src/schema/user.rs | 8 + rc_database/src/wrapper.rs | 75 ++++- rc_lobby_room/src/lobby.rs | 83 ++++-- rc_multiplayer/src/events/mod.rs | 7 + .../src/events/validate_game_guid.rs | 94 +++++- rc_multiplayer/src/handler.rs | 10 +- rc_multiplayer/src/handlers/simple_typed.rs | 14 +- rc_multiplayer/src/main.rs | 1 + rc_multiplayer/src/matches/aggregate.rs | 1 + rc_multiplayer/src/matches/countdown.rs | 22 +- rc_multiplayer/src/matches/engine.rs | 1 + rc_multiplayer/src/matches/generic.rs | 268 ++++++++++++------ 24 files changed, 902 insertions(+), 149 deletions(-) create mode 100644 rc_database/src/migration/m20250713_000001_create_game_table.rs create mode 100644 rc_database/src/migration/m20250713_000002_create_player_table.rs create mode 100644 rc_database/src/schema/multiplayer_game.rs create mode 100644 rc_database/src/schema/multiplayer_game_player.rs diff --git a/rc_core/src/data/game_mode.rs b/rc_core/src/data/game_mode.rs index ecc5b81..71a64ff 100644 --- a/rc_core/src/data/game_mode.rs +++ b/rc_core/src/data/game_mode.rs @@ -118,6 +118,32 @@ impl GameMode { crate::persist::config::GameType::Campaign => Self::Campaign, } } + + #[inline] + pub(crate) fn from_db(mode: oj_rc_database::schema::multiplayer_game::GameMode) -> Self { + match mode { + oj_rc_database::schema::multiplayer_game::GameMode::BattleArena => Self::BattleArena, + oj_rc_database::schema::multiplayer_game::GameMode::SuddenDeath => Self::SuddenDeath, + oj_rc_database::schema::multiplayer_game::GameMode::Pit => Self::Pit, + oj_rc_database::schema::multiplayer_game::GameMode::TestMode => Self::TestMode, + oj_rc_database::schema::multiplayer_game::GameMode::SinglePlayer => Self::SinglePlayer, + oj_rc_database::schema::multiplayer_game::GameMode::TeamDeathmatch => Self::TeamDeathmatch, + oj_rc_database::schema::multiplayer_game::GameMode::Campaign => Self::Campaign, + } + } + + #[inline] + pub(crate) fn to_db(&self) -> oj_rc_database::schema::multiplayer_game::GameMode { + match self { + Self::BattleArena => oj_rc_database::schema::multiplayer_game::GameMode::BattleArena, + Self::SuddenDeath => oj_rc_database::schema::multiplayer_game::GameMode::SuddenDeath, + Self::Pit => oj_rc_database::schema::multiplayer_game::GameMode::Pit, + Self::TestMode => oj_rc_database::schema::multiplayer_game::GameMode::TestMode, + Self::SinglePlayer => oj_rc_database::schema::multiplayer_game::GameMode::SinglePlayer, + Self::TeamDeathmatch => oj_rc_database::schema::multiplayer_game::GameMode::TeamDeathmatch, + Self::Campaign => oj_rc_database::schema::multiplayer_game::GameMode::Campaign, + } + } } #[repr(u8)] @@ -137,4 +163,22 @@ impl MapVisibility { crate::persist::config::GameVisibility::Bad => Self::Bad, } } + + #[inline] + pub(crate) fn from_db(mode: oj_rc_database::schema::multiplayer_game::MapVisibility) -> Self { + match mode { + oj_rc_database::schema::multiplayer_game::MapVisibility::Good => Self::Good, + oj_rc_database::schema::multiplayer_game::MapVisibility::Poor => Self::Poor, + oj_rc_database::schema::multiplayer_game::MapVisibility::Bad => Self::Bad, + } + } + + #[inline] + pub(crate) fn to_db(&self) -> oj_rc_database::schema::multiplayer_game::MapVisibility { + match self { + Self::Good => oj_rc_database::schema::multiplayer_game::MapVisibility::Good, + Self::Poor => oj_rc_database::schema::multiplayer_game::MapVisibility::Poor, + Self::Bad => oj_rc_database::schema::multiplayer_game::MapVisibility::Bad, + } + } } diff --git a/rc_core/src/data/player_data.rs b/rc_core/src/data/player_data.rs index e290de6..33c30a2 100644 --- a/rc_core/src/data/player_data.rs +++ b/rc_core/src/data/player_data.rs @@ -8,7 +8,7 @@ pub struct PlayerData { pub tier: i32, pub robot_name: String, pub robot_map: Vec, - // -- unused i32 here -- + pub group: Option, // unused i32 too??? pub team: i32, pub has_premium: bool, pub robot_uuid: String, @@ -70,7 +70,7 @@ impl PlayerData { (Typed::Str("spawnEffect".into()), Typed::Str(self.spawn_effect.clone().into())), (Typed::Str("deathEffect".into()), Typed::Str(self.death_effect.clone().into())), //(Typed::Str("groupId".into()), Typed::Int(self.group)), // FIXME - (Typed::Str("groupId".into()), Typed::Int(self.team)), // not strongly enforced in EnterBattleEventListener + (Typed::Str("groupId".into()), Typed::Str(self.group.clone().unwrap_or_default().into())), (Typed::Str("team".into()), Typed::Int(self.team)), // not strongly enforced in EnterBattleEventListener (but has to be parsable into an i32) (Typed::Str("hasPremium".into()), Typed::Bool(self.has_premium)), (Typed::Str("weaponOrder".into()), Typed::IntArr(self.weapon_order.clone().into())), diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index b380abe..6b9a7ed 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -477,7 +477,7 @@ fn default_rotation() -> GameEventSequence { fn default_multiplayer() -> super::MultiplayerConfig { super::MultiplayerConfig { - players_per_game: 1, + players_per_game: 2, enabled: true, network: super::multiplayer::default_net_conf(), } diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index b320e42..014cac3 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -27,6 +27,10 @@ impl AccountProvider { }) } + pub async fn multiplayer_init(&self) -> Result<(), oj_rc_database::sea_orm::DbErr> { + self.db.complete_all_games().await + } + /*pub fn fake_user(&self) -> Box + Send + Sync> { Box::new(UserData { token: super::UserToken { uuid: "fake user!".to_owned(), token: "".to_owned(), refresh_token: "".to_owned() }, @@ -306,6 +310,7 @@ impl UserData { tier: 1, // FIXME robot_name: current_slot.name, robot_map: current_slot.robot_data.clone(), + group: None, // no platoon team: 0, has_premium: false, // FIXME robot_uuid: super::i64_as_uuid_str(current_slot.uuid).into(), @@ -358,6 +363,7 @@ impl UserData { tier: 1, // FIXME robot_name: vehicle.name.clone().unwrap_or_else(|| factory_vehicle.1.name.clone()), robot_map: factory_vehicle.0.cube_data, + group: None, team: team_num, has_premium: false, robot_uuid: uuid_str, @@ -392,6 +398,7 @@ impl UserData { tier: 1, // FIXME robot_name: vehicle.name.clone().unwrap_or_else(|| db_vehicle.name.clone()), robot_map: db_vehicle.robot_data, + group: None, team: team_num, has_premium: false, robot_uuid: uuid_str, @@ -430,6 +437,7 @@ impl UserData { tier: 1, // FIXME robot_name: vehicle.name.clone().unwrap_or_else(|| "Raw Robot".to_owned()), robot_map: cube_data.to_owned(), + group: None, team: team_num, has_premium: false, robot_uuid: uuid_str, @@ -1109,6 +1117,10 @@ impl super::ChatUser for UserData { #[async_trait::async_trait] impl super::LobbyUser for UserData { + fn user_id(&self) -> i32 { + self.account.id + } + async fn player_data(&self) -> Result { self.user_player_data().await.map_err(|e| { if let Some(msg) = e.error_msg() { @@ -1119,6 +1131,66 @@ impl super::LobbyUser for UserData { }) } + + 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] @@ -1135,4 +1207,72 @@ impl super::MultiplayerUser for UserData { 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), + }) + } + } } diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index c87bb14..a54d0a8 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; 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, MultiplayerUser}; +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}; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 3142a6c..40e4b4a 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -229,13 +229,76 @@ impl SanctionType { #[async_trait::async_trait] pub trait LobbyUser { + fn user_id(&self) -> i32; async fn player_data(&self) -> Result; + async fn start_game(&self, game: GameDescriptor, players: Vec) -> Result<(), polariton_server::operations::SimpleOpError>; +} + +pub struct GameDescriptor { + pub guid: String, + pub map: String, + pub mode: crate::data::game_mode::GameMode, + pub visibility: crate::data::game_mode::MapVisibility, + pub auto_heal: bool, + pub is_ranked: bool, + pub is_custom: bool, + pub is_complete: bool, +} + +pub struct PlayerLobbyDescriptor { + pub user_id: i32, + pub team: i32, + pub group: Option, +} + +pub struct PlayerDescriptor { + pub user_id: i32, + pub player_id: u8, + pub team: i32, + pub group: Option, + pub public_id: String, + pub display_name: String, + pub is_rewards_claimed: bool, +} + +#[derive(Debug)] +pub struct MultiplayerError { + pub code: MultiplayerErrorCode, + pub message: String, +} + +impl core::fmt::Display for MultiplayerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}: {}", self.code, self.message) + } +} + +impl core::error::Error for MultiplayerError {} + +#[repr(u8)] +#[derive(Debug)] +pub enum MultiplayerErrorCode { + HaxSpeed = 0, + HaxException = 1, + HaxTeleport = 2, + HaxEacViolation = 6, + HaxAfk = 7, + HaxFirerange = 8, + HaxFiredamage = 9, + HaxFirerate = 10, + HaxFireposition = 11, + IncorrectGameGuid = 12, + CustomString = 13, + TimedOut = 14, + GameEnded = 15, } #[async_trait::async_trait] pub trait MultiplayerUser { - // TODO fn user_id(&self) -> i32; fn user_name(&self) -> &'_ str; fn display_name(&self) -> &'_ str; + async fn current_game(&self) -> Result, MultiplayerError>; + async fn game_players(&self, guid: &str) -> Result, MultiplayerError>; + async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>; } diff --git a/rc_database/src/migration/m20250713_000001_create_game_table.rs b/rc_database/src/migration/m20250713_000001_create_game_table.rs new file mode 100644 index 0000000..dfa8506 --- /dev/null +++ b/rc_database/src/migration/m20250713_000001_create_game_table.rs @@ -0,0 +1,45 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20250713_000001_create_game_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Permissions table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(crate::schema::multiplayer_game::Entity) + .col( + ColumnDef::new(crate::schema::multiplayer_game::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::Guid).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::Map).string().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::Mode).string().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::Visibility).string().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::AutoHeal).boolean().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::Variant).string().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game::Column::IsComplete).boolean().not_null()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Permissions table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(crate::schema::multiplayer_game::Entity).to_owned()) + .await + } +} diff --git a/rc_database/src/migration/m20250713_000002_create_player_table.rs b/rc_database/src/migration/m20250713_000002_create_player_table.rs new file mode 100644 index 0000000..f040744 --- /dev/null +++ b/rc_database/src/migration/m20250713_000002_create_player_table.rs @@ -0,0 +1,56 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20250713_000002_create_player_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Permissions table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + 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().not_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()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Permissions table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(crate::schema::multiplayer_game_player::Entity).to_owned()) + .await + } +} diff --git a/rc_database/src/migration/mod.rs b/rc_database/src/migration/mod.rs index 2026aa8..f3fcb90 100644 --- a/rc_database/src/migration/mod.rs +++ b/rc_database/src/migration/mod.rs @@ -7,6 +7,8 @@ mod m20250424_000004_create_user_aux_table; mod m20250424_000005_create_campaign_tables; mod m20250526_000001_add_garage_customisation; mod m20250529_000001_create_sanction_table; +mod m20250713_000001_create_game_table; +mod m20250713_000002_create_player_table; pub struct Migrator; @@ -21,6 +23,8 @@ impl MigratorTrait for Migrator { Box::new(m20250424_000005_create_campaign_tables::Migration), Box::new(m20250526_000001_add_garage_customisation::Migration), Box::new(m20250529_000001_create_sanction_table::Migration), + Box::new(m20250713_000001_create_game_table::Migration), + Box::new(m20250713_000002_create_player_table::Migration), ] } } diff --git a/rc_database/src/schema/mod.rs b/rc_database/src/schema/mod.rs index 44a02fd..953e7f3 100644 --- a/rc_database/src/schema/mod.rs +++ b/rc_database/src/schema/mod.rs @@ -6,6 +6,8 @@ pub mod campaign; pub mod campaign_difficulty_completion; pub mod common_query; pub mod sanction; +pub mod multiplayer_game; +pub mod multiplayer_game_player; pub fn parse_int_csv(s: &str) -> Vec { s.split(',').filter_map(|i_as_s| { diff --git a/rc_database/src/schema/multiplayer_game.rs b/rc_database/src/schema/multiplayer_game.rs new file mode 100644 index 0000000..a2b76be --- /dev/null +++ b/rc_database/src/schema/multiplayer_game.rs @@ -0,0 +1,58 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "games")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub creation_time: i64, // seconds since unix epoch + pub guid: i64, + pub map: String, + pub mode: GameMode, + pub visibility: MapVisibility, + pub auto_heal: bool, + pub variant: GameType, + pub is_complete: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::multiplayer_game_player::Entity")] + Player, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Player.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "PascalCase")] +pub enum GameMode { + BattleArena, + SuddenDeath, + Pit, + TestMode, + SinglePlayer, + TeamDeathmatch, + Campaign, +} + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "PascalCase")] +pub enum MapVisibility { + Good, + Poor, + Bad, +} + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "PascalCase")] +pub enum GameType { + Standard, + Ranked, + Custom, +} diff --git a/rc_database/src/schema/multiplayer_game_player.rs b/rc_database/src/schema/multiplayer_game_player.rs new file mode 100644 index 0000000..046d916 --- /dev/null +++ b/rc_database/src/schema/multiplayer_game_player.rs @@ -0,0 +1,45 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "players")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + 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, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::multiplayer_game::Entity", + from = "Column::GameId", + to = "super::multiplayer_game::Column::Id" + )] + Game, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Game.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_database/src/schema/user.rs b/rc_database/src/schema/user.rs index 80ddf3c..d6d291c 100644 --- a/rc_database/src/schema/user.rs +++ b/rc_database/src/schema/user.rs @@ -23,6 +23,8 @@ pub enum Relation { Aux, #[sea_orm(has_many = "super::campaign::Entity")] Campaigns, + #[sea_orm(has_many = "super::multiplayer_game_player::Entity")] + Player, } impl Related for Entity { @@ -49,4 +51,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Player.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index e06d5e3..a9d6c06 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}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, RelationTrait}; pub struct Database { orm: sea_orm::DatabaseConnection, @@ -272,4 +272,77 @@ impl Database { pub async fn insert_sanction(&self, entity: crate::schema::sanction::ActiveModel) -> Result { entity.insert(&self.orm).await } + + pub async fn game_by_user_id_and_completion(&self, user_id: i32, is_complete: bool) -> Result, sea_orm::DbErr> { + Ok(crate::schema::multiplayer_game::Entity::find() + .find_also_related(crate::schema::multiplayer_game_player::Entity) + //.join(sea_orm::JoinType::InnerJoin, crate::schema::multiplayer_game_player::Relation::Game.def()) + .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) + .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) + .order_by_asc(crate::schema::multiplayer_game::Column::CreationTime) + //.into_model() + .one(&self.orm) + .await? + .map(|(x, _)| x)) + } + + pub async fn update_complete_game_by_game_guid(&self, game_guid: i64) -> Result<(), sea_orm::DbErr> { + crate::schema::multiplayer_game::Entity::update_many() + .col_expr(crate::schema::multiplayer_game::Column::IsComplete, sea_orm::sea_query::Expr::value(true)) + .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) + .exec(&self.orm) + .await?; + Ok(()) + } + + pub async fn complete_all_games(&self) -> Result<(), sea_orm::DbErr> { + crate::schema::multiplayer_game::Entity::update_many() + .col_expr(crate::schema::multiplayer_game::Column::IsComplete, sea_orm::sea_query::Expr::value(true)) + .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(false)) + .exec(&self.orm) + .await?; + Ok(()) + } + + pub async fn insert_game(&self, entity: crate::schema::multiplayer_game::ActiveModel) -> Result { + entity.insert(&self.orm).await + } + + 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()) + .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) + .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) + .into_model::() + .all(&self.orm) + .await + } + + 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) + //.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)) + .all(&self.orm) + .await? + .into_iter() + .filter_map(|(player, user, _)| user.map(|user| (player, user))) + .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()) + .filter(crate::schema::multiplayer_game::Column::Id.eq(game_id)) + .all(&self.orm) + .await + } + + pub async fn insert_players(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { + crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(&self.orm).await?; + Ok(()) + } } diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index 2d070b2..f0bed4a 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -11,10 +11,11 @@ struct QueueKey { struct QueueUser { emitter: polariton_server::events::EventEmitter, player: oj_rc_core::data::player_data::PlayerData, + user_id: i32, } pub struct QueueHandler { - users_in_queue: std::sync::Mutex>>, + users_in_queue: tokio::sync::Mutex>>, users_per_game: usize, is_enabled: bool, hostname: String, @@ -26,7 +27,7 @@ impl QueueHandler { pub fn new(conf: &oj_rc_core::ConfigImpl, game_host: &str) -> Self { let (domain, port_str) = game_host.split_once(':').expect("Invalid redirect address (must be domain:port)"); Self { - users_in_queue: std::sync::Mutex::new(HashMap::new()), + users_in_queue: tokio::sync::Mutex::new(HashMap::new()), users_per_game: oj_rc_core::ConfigProvider::<()>::players_per_game(conf), is_enabled: oj_rc_core::ConfigProvider::<()>::is_multiplayer_enabled(conf), hostname: domain.to_owned(), @@ -35,30 +36,58 @@ impl QueueHandler { } } - fn enter_match(&self, key: &QueueKey, players: &Vec) { + async fn enter_match(&self, key: QueueKey, players: Vec, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { use std::hash::Hasher; let mut hasher = std::hash::DefaultHasher::new(); key.hash(&mut hasher); let guid = oj_rc_core::persist::user::uuid_sanitize(hasher.finish() as i64); let guid_str = oj_rc_core::persist::user::i64_as_uuid_str(guid); - let player_datas = players.iter().map(|x| x.player.clone()).collect(); - let enter_battle_ev = crate::events::battle_enter::BattleEnter { - host: self.hostname.clone(), - port: self.hostport, + let player_descs = players.iter().map(|x| oj_rc_core::persist::user::PlayerLobbyDescriptor { + user_id: x.user_id, + team: x.player.team, + group: None, // TODO support platoons + }).collect(); + let game_desc = oj_rc_core::persist::user::GameDescriptor { + guid: guid_str.clone(), map: key.map.clone(), - mode: key.mode, - guid: guid_str, + mode: key.mode.clone(), + visibility: key.visibility.clone(), + auto_heal: key.auto_heal, is_ranked: false, is_custom: false, - visibility: Some(key.visibility), - auto_heal: key.auto_heal, - player_datas, - network_config: self.network_conf.clone(), + is_complete: false, }; - let arc_event = std::sync::Arc::new(enter_battle_ev); - for player in players.iter() { - tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone())); + match user.start_game(game_desc, player_descs).await { + Ok(_) => { + let player_datas = players.iter().map(|x| x.player.clone()).collect(); + let enter_battle_ev = crate::events::battle_enter::BattleEnter { + host: self.hostname.clone(), + port: self.hostport, + map: key.map.clone(), + mode: key.mode, + guid: guid_str, + is_ranked: false, + is_custom: false, + 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 player in players.iter() { + tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone())); + } + }, + 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) { @@ -69,7 +98,7 @@ impl QueueHandler { } } - 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: &(dyn oj_rc_core::persist::user::User<()> + Send + Sync), event_emitter: polariton_server::events::EventEmitter) { + 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: &(dyn oj_rc_core::persist::user::LobbyUser + 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, @@ -82,21 +111,25 @@ impl QueueHandler { }; match user.player_data().await { Ok(player_data) => { - let new_player = QueueUser { + let mut new_player = QueueUser { emitter: event_emitter, player: player_data, + user_id: user.user_id(), }; - let mut lock = self.users_in_queue.lock().unwrap(); - let players = if let Some(players) = lock.get_mut(&key) { + let mut lock = self.users_in_queue.lock().await; + let players_len = if let Some(players) = lock.get_mut(&key) { + new_player.player.team = (players.len() % 2) as _; // alternate teams players.push(new_player); - players + players.len() } else { lock.insert(key.clone(), vec![new_player]); - lock.get(&key).unwrap() + 1 }; - if players.len() >= self.users_per_game { - self.enter_match(&key, players); - lock.remove(&key); + let game_ready = players_len >= self.users_per_game; + let players = if game_ready { lock.remove(&key) } else { None }; + drop(lock); + if let Some(players) = players { + self.enter_match(key, players, user).await; } }, Err(e) => { diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index 6968d53..f90a3dd 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -42,6 +42,13 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa {literustlib::packet::Property::Unreliable as u8}, rlnl::events::ingame::FireMiss, >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::EnemySpotted as i16}, + {rlnl::event_code::NetworkEvent::EnemySpotted as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::SpottingIds, + >::handler(init_ctx)) } #[inline] diff --git a/rc_multiplayer/src/events/validate_game_guid.rs b/rc_multiplayer/src/events/validate_game_guid.rs index c3e46ee..939b839 100644 --- a/rc_multiplayer/src/events/validate_game_guid.rs +++ b/rc_multiplayer/src/events/validate_game_guid.rs @@ -24,21 +24,91 @@ impl crate::handlers::RlnlEventCodeHandler for AuthUserGame { let game_guid = data.game_guid.0.clone(); if user.authenticate(data).await { let user_info = user.user().await.unwrap(); - let (tx, rx) = tokio::sync::oneshot::channel(); - super::log_channel_send_failure(self.matches.send(crate::matches::GameMessage::NewConnection { - user: user_info.clone(), - game_guid, - connection: peer.to_owned(), - response: tx, - sender: sender.to_owned(), - }).await); - log::debug!("Sent NewConnection message to matches handler"); - if let Ok(Some(e)) = rx.await { - log::error!("Failed {:?} event: {}", Self::CODE, e); + match user_info.current_game().await { + Ok(Some(current_game)) => { + if current_game.guid == game_guid { + let (tx, rx) = tokio::sync::oneshot::channel(); + super::log_channel_send_failure(self.matches.send(crate::matches::GameMessage::NewConnection { + user: user_info.clone(), + game_guid, + connection: peer.to_owned(), + response: tx, + sender: sender.to_owned(), + }).await); + log::debug!("Sent NewConnection message to matches handler"); + if let Ok(Some(e)) = rx.await { + log::error!("Failed {:?} event: {} [disconnecting...]", Self::CODE, e); + super::log_lnl_send_failure(crate::handlers::RlnlSender::new(&sender) + .send_data(&rlnl::types::StringCode { + ty: rlnl::types::GameServerErrorCodes::StrErrCustomString, + custom: Some(rlnl::types::BinaryWriterString(e.message)), + }, + rlnl::event_code::NetworkEvent::OnFailedToConnectToServer, + literustlib::packet::Property::ReliableOrdered, + &peer).await); + peer.disconnect(); + } + } else { + log::error!("Registered game GUID does not match sent GUID (got: {}, expected: {}) [disconnecting...]", game_guid, current_game.guid); + super::log_lnl_send_failure(crate::handlers::RlnlSender::new(&sender) + .send_data(&rlnl::types::StringCode { + ty: rlnl::types::GameServerErrorCodes::StrErrIncorrectGameGuid, + custom: Some(rlnl::types::BinaryWriterString(format!("Send game guid does not equal expected guid; {} != {}", game_guid, current_game.guid))), + }, + rlnl::event_code::NetworkEvent::OnFailedToConnectToServer, + literustlib::packet::Property::ReliableOrdered, + &peer).await); + peer.disconnect(); + } + }, + Ok(None) => { + log::warn!("Cannot validate game guid for user {} with no ongoing game [disconnecting...]", user_info.user_id()); + super::log_lnl_send_failure(crate::handlers::RlnlSender::new(&sender) + .send_data(&rlnl::types::StringCode { + ty: rlnl::types::GameServerErrorCodes::StrErrIncorrectGameGuid, + custom: None, + }, + rlnl::event_code::NetworkEvent::OnFailedToConnectToServer, + literustlib::packet::Property::ReliableOrdered, + &peer).await); + peer.disconnect(); + }, + Err(e) => { + log::error!("Failed to get current game for user {}: {} [disconnecting...]", user_info.user_id(), e.message); + super::log_lnl_send_failure(crate::handlers::RlnlSender::new(&sender) + .send_data(&rlnl::types::StringCode { + ty: core_to_rlnl_mp_error_code(e.code), + custom: Some(rlnl::types::BinaryWriterString(e.message)), + }, + rlnl::event_code::NetworkEvent::OnFailedToConnectToServer, + literustlib::packet::Property::ReliableOrdered, + &peer).await); + peer.disconnect(); + }, } + } else { - log::error!("Failed to validate game guid for user {} (other packets will probably be ignored)", username); + log::error!("Failed to validate game guid for user {} [disconnecting...]", username); + peer.disconnect(); } } } + +fn core_to_rlnl_mp_error_code(core_: oj_rc_core::persist::user::MultiplayerErrorCode) -> rlnl::types::GameServerErrorCodes { + match core_ { + oj_rc_core::persist::user::MultiplayerErrorCode::HaxSpeed => rlnl::types::GameServerErrorCodes::StrErrHaxSpeed, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxException => rlnl::types::GameServerErrorCodes::StrErrHaxException, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxTeleport => rlnl::types::GameServerErrorCodes::StrErrHaxTeleport, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxEacViolation => rlnl::types::GameServerErrorCodes::StrErrHaxEacViolation, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxAfk => rlnl::types::GameServerErrorCodes::StrErrHaxAfk, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxFirerange => rlnl::types::GameServerErrorCodes::StrErrHaxFirerange, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxFiredamage => rlnl::types::GameServerErrorCodes::StrErrHaxFiredamage, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxFirerate => rlnl::types::GameServerErrorCodes::StrErrHaxFirerate, + oj_rc_core::persist::user::MultiplayerErrorCode::HaxFireposition => rlnl::types::GameServerErrorCodes::StrErrHaxFireposition, + oj_rc_core::persist::user::MultiplayerErrorCode::IncorrectGameGuid => rlnl::types::GameServerErrorCodes::StrErrIncorrectGameGuid, + oj_rc_core::persist::user::MultiplayerErrorCode::CustomString => rlnl::types::GameServerErrorCodes::StrErrCustomString, + oj_rc_core::persist::user::MultiplayerErrorCode::TimedOut => rlnl::types::GameServerErrorCodes::StrErrTimedOut, + oj_rc_core::persist::user::MultiplayerErrorCode::GameEnded => rlnl::types::GameServerErrorCodes::StrErrGameEnded, + } +} diff --git a/rc_multiplayer/src/handler.rs b/rc_multiplayer/src/handler.rs index 3bde74a..5f796d1 100644 --- a/rc_multiplayer/src/handler.rs +++ b/rc_multiplayer/src/handler.rs @@ -63,7 +63,7 @@ impl literustlib_server::EventHandler for LnlEventHandler { Some(crate::UserData::new(self.user_provider.clone())) } - async fn on_connect_done(&self, peer: &std::sync::Arc< literustlib_server::Connection>, _user: &Self::UserData, sender: &std::sync::Arc>) { + async fn on_connect_done(&self, peer: &std::sync::Arc>, _user: &Self::UserData, sender: &std::sync::Arc>) { log::debug!("New connection completed (id:{})", peer.id()); let data = EventData::without_data( crate::data::MessageType::ServerMsg, @@ -72,6 +72,14 @@ impl literustlib_server::EventHandler for LnlEventHandler { if let Err(e) = sender.send_data(data, literustlib::packet::Property::Reliable, peer).await { log::error!("Failed to send rlnl OnConnectedToGameServer event: {}", e); } + } + + async fn on_disconnect(&self, peer: &std::sync::Arc>, user: &Self::UserData) { + if let Some(user_info) = user.user().await { + log::info!("Disconnect from user {} ({})", user_info.user_id(), peer.id()); + } else { + log::debug!("Disconnect from connection {}", peer.id()); + } } } diff --git a/rc_multiplayer/src/handlers/simple_typed.rs b/rc_multiplayer/src/handlers/simple_typed.rs index 49a258e..7dd463f 100644 --- a/rc_multiplayer/src/handlers/simple_typed.rs +++ b/rc_multiplayer/src/handlers/simple_typed.rs @@ -22,15 +22,21 @@ pub trait RlnlEventCodeHandler: Sync + Send { } #[async_trait::async_trait] -impl , H: RlnlEventCodeHandler> crate::EventCodeHandler for SimpleRlnl { +impl + Send, H: RlnlEventCodeHandler> crate::EventCodeHandler for SimpleRlnl { async fn handle(&self, data: &bytes::Bytes, peer: &std::sync::Arc>, user: &crate::UserData, sender: &std::sync::Arc>) { let mut des = byteserde::des_slice::ByteDeserializerSlice::new(&data); - let rlnl_data = In::byte_deserialize(&mut des).expect("Bad deserialization"); - self.handler.handle(rlnl_data, peer, user, sender).await; + match In::byte_deserialize(&mut des) { + Ok(rlnl_data) => { + self.handler.handle(rlnl_data, peer, user, sender).await; + }, + Err(e) => { + log::error!("Bad deserialization: {}", e); + } + } } } -impl , H: RlnlEventCodeHandler> crate::EventCode for SimpleRlnl { +impl + Send, H: RlnlEventCodeHandler> crate::EventCode for SimpleRlnl { const CODE: i16 = H::CODE as i16; } diff --git a/rc_multiplayer/src/main.rs b/rc_multiplayer/src/main.rs index d2ebe4f..0caf6bc 100644 --- a/rc_multiplayer/src/main.rs +++ b/rc_multiplayer/src/main.rs @@ -24,6 +24,7 @@ async fn main() -> std::io::Result<()> { let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data")); + users.multiplayer_init().await.expect("Multiplayer init task failed"); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); let matches = matches::GameMatches::new(); let matches_chann = matches.spawn(); diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index 4a59e63..3e7762d 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -31,6 +31,7 @@ impl GameMatches { match msg { super::GameMessage::NewConnection { user, game_guid, connection, response, sender } => { if let Some(tx) = self.matches.get(&game_guid) { + self.routing.insert(user.user_id(), game_guid.clone()); if tx.send(super::GameMessage::NewConnection { user, game_guid, connection, response, sender }).await.is_err() { log::error!("Failed to send NewConnection game message to existing match"); } diff --git a/rc_multiplayer/src/matches/countdown.rs b/rc_multiplayer/src/matches/countdown.rs index d0ae60b..d00657b 100644 --- a/rc_multiplayer/src/matches/countdown.rs +++ b/rc_multiplayer/src/matches/countdown.rs @@ -1,4 +1,4 @@ -pub fn match_countdown(players: Vec, game_start: chrono::DateTime) { +pub fn match_countdown(players: Vec<(super::generic::UserSender, std::sync::Arc)>, game_start: chrono::DateTime) { tokio::spawn(do_match_countdown_async(players, game_start)); } @@ -9,37 +9,43 @@ pub fn time_to_game_start_payload(game_start: chrono::DateTime) -> rlnl::events::GameTime(time_until_start_f32) } -async fn do_match_countdown_async(players: Vec, game_start: chrono::DateTime) { +async fn do_match_countdown_async(players: Vec<(super::generic::UserSender, std::sync::Arc)>, game_start: chrono::DateTime) { let now = chrono::Utc::now(); let time_until_start = game_start.signed_duration_since(now); let payload = time_to_game_start_payload(game_start); for player in players.iter() { - let sender = player.rlnl(); + let sender = player.0.rlnl(); if let Err(e) = sender.send_data( &payload, rlnl::event_code::NetworkEvent::TimeToGameStart, literustlib::packet::Property::ReliableOrdered, - &player.connection) + &player.0.connection) .await { log::error!("Failed to send TimeToGameStart to a user: {}", e); } } tokio::time::sleep(time_until_start.to_std().unwrap_or_default()).await; - log::debug!("Sending starting game event"); + log::info!("Sending starting game event"); let payload = rlnl::events::ingame::GameStart { is_reconnecting: 0, }; - for player in players { - let sender = player.rlnl(); + for player in players.iter() { + let sender = player.0.rlnl(); if let Err(e) = sender.send_data( &payload, rlnl::event_code::NetworkEvent::GameStarted, literustlib::packet::Property::ReliableOrdered, - &player.connection) + &player.0.connection) .await { log::error!("Failed to send GameStarted event to a user: {}", e); } } + + tokio::time::sleep(std::time::Duration::ZERO).await; // is this necessary? + + for player in players { + player.1.mode.store(super::generic::ConnectionMode::InGame.to_u8(), std::sync::atomic::Ordering::Relaxed); + } } diff --git a/rc_multiplayer/src/matches/engine.rs b/rc_multiplayer/src/matches/engine.rs index 036ceca..77b4370 100644 --- a/rc_multiplayer/src/matches/engine.rs +++ b/rc_multiplayer/src/matches/engine.rs @@ -1,2 +1,3 @@ +#[allow(dead_code)] pub trait GamemodeEngine: Send + Sync { } diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index af579a2..cfacc10 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -1,7 +1,7 @@ pub(super) struct UserConnection { pub(super) user: std::sync::Arc>, pub(super) connection: UserSender, - pub(super) state: UserState, + pub(super) state: std::sync::Arc, pub(super) machine: MachineState, } @@ -20,7 +20,6 @@ impl UserSender { pub(super) struct UserState { pub(super) mode: std::sync::atomic::AtomicU8, pub(super) progress: std::sync::atomic::AtomicU8, // percent - _x: (), } impl UserState { @@ -28,21 +27,18 @@ impl UserState { Self { mode: std::sync::atomic::AtomicU8::new(ConnectionMode::Loading.to_u8()), progress: std::sync::atomic::AtomicU8::new(0), - _x: (), } } } pub(super) struct MachineState { pub(super) selected_weapon: WeaponInfo, - _x: (), } impl MachineState { fn new() -> Self { Self { selected_weapon: WeaponInfo::new(), - _x: (), } } } @@ -65,23 +61,27 @@ impl WeaponInfo { #[derive(Debug, Copy, Clone)] pub(super) enum ConnectionMode { Loading = 0, - Sync = 1, - InGame = 2, + WaitingForSync = 1, + Sync = 2, + WaitingToStart = 3, + InGame = 4, } impl ConnectionMode { #[inline] - fn from_u8(num: u8) -> Self { + pub(super) fn from_u8(num: u8) -> Self { match num { 0 => Self::Loading, - 1 => Self::Sync, - 2 => Self::InGame, + 1 => Self::WaitingForSync, + 2 => Self::Sync, + 3 => Self::WaitingToStart, + 4 => Self::InGame, x => panic!("Unrecognized ConnectionMode {}", x), } } #[inline] - fn to_u8(self) -> u8 { + pub(super) fn to_u8(self) -> u8 { self as u8 } } @@ -94,6 +94,7 @@ pub(super) struct GenericGamemodeEngine { pub game_guid: String, pub is_complete: std::sync::atomic::AtomicBool, pub game_start: std::sync::atomic::AtomicI64, + pub player_count: std::sync::atomic::AtomicU8, } impl GenericGamemodeEngine { @@ -108,6 +109,7 @@ impl GenericGamemodeEngine { game_guid: guid, is_complete: std::sync::atomic::AtomicBool::new(false), game_start: std::sync::atomic::AtomicI64::new(-1), + player_count: std::sync::atomic::AtomicU8::new(0), } } @@ -115,9 +117,13 @@ impl GenericGamemodeEngine { self.user_id_map.read().await.get(&user_id).map(|x| *x) } - pub(super) async fn rebroadcast(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T) { + pub(super) async fn rebroadcast(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T, in_game: bool) { for conn in self.users.read().await.values() { if user_id == conn.user.user_id() { continue; } + if in_game { + let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + if !matches!(mode, ConnectionMode::InGame) { continue; } + } let sender = crate::handlers::RlnlSender::new(&conn.connection.sender); crate::events::log_lnl_send_failure(sender.send_data( data, @@ -128,9 +134,13 @@ impl GenericGamemodeEngine { } } - pub(super) async fn rebroadcast_dataless(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property) { + pub(super) async fn rebroadcast_dataless(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, in_game: bool) { for conn in self.users.read().await.values() { if user_id == conn.user.user_id() { continue; } + if in_game { + let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + if !matches!(mode, ConnectionMode::InGame) { continue; } + } let sender = crate::handlers::RlnlSender::new(&conn.connection.sender); crate::events::log_lnl_send_failure(sender.send_empty( code, @@ -140,8 +150,12 @@ impl GenericGamemodeEngine { } } - pub(super) async fn broadcast(&self, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T) { + pub(super) async fn broadcast(&self, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T, in_game: bool) { for conn in self.users.read().await.values() { + if in_game { + let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + if !matches!(mode, ConnectionMode::InGame) { continue; } + } let sender = conn.connection.rlnl(); crate::events::log_lnl_send_failure(sender.send_data( data, @@ -152,8 +166,12 @@ impl GenericGamemodeEngine { } } - pub(super) async fn broadcast_dataless(&self, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property) { + pub(super) async fn broadcast_dataless(&self, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, in_game: bool) { for conn in self.users.read().await.values() { + if in_game { + let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + if !matches!(mode, ConnectionMode::InGame) { continue; } + } let sender = conn.connection.rlnl(); crate::events::log_lnl_send_failure(sender.send_empty( code, @@ -175,8 +193,9 @@ impl GenericGamemodeEngine { match msg { super::GameMessage::NewConnection { user, game_guid, connection, response, sender } => { if self.game_guid != game_guid { + log::error!("Game guid does not match (got: {}, expected: {})", game_guid, self.game_guid); response.send(Some(super::messages::ErrorMessage { - message: "Game guid does not match".to_owned(), + message: format!("Game guid does not match (got: {}, expected: {})", game_guid, self.game_guid), inner: None, })).unwrap_or_default(); return; @@ -188,22 +207,33 @@ impl GenericGamemodeEngine { connection, sender, }, - state: UserState::new(), + state: std::sync::Arc::new(UserState::new()), machine: MachineState::new(), }; //tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let id = users.len() as u8; - if let Err(e) = self.send_loading_events(&new_user.connection, id).await { - response.send(Some(super::messages::ErrorMessage { - message: "Failed to send GameGuidValidated response".to_owned(), - inner: Some(Box::new(e)), - })).unwrap_or_default(); - return; + //let id = users.len() as u8; + match new_user.user.game_players(&game_guid).await { + Ok(players) => { + if self.player_count.load(std::sync::atomic::Ordering::Relaxed) == 0 { + self.player_count.store(players.len() as _, std::sync::atomic::Ordering::Relaxed); + } + let user_id = new_user.user.user_id(); + let id = players.iter().filter(|p| p.user_id == user_id).next().map(|p| p.player_id).unwrap(); + self.spawn_send_loading_events(&new_user, id, players); + log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid); + self.user_id_map.write().await.insert(new_user.user.user_id(), id); + users.insert(id, new_user); + response.send(None).unwrap_or_default(); + }, + Err(e) => { + log::error!("Failed to retrieve players for game {}: {}", game_guid, e); + response.send(Some(super::messages::ErrorMessage { + message: "Failed to retrieve players for game".to_owned(), + inner: Some(Box::new(e)), + })).unwrap_or_default(); + } } - log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid); - self.user_id_map.write().await.insert(new_user.user.user_id(), id); - users.insert(id, new_user); - response.send(None).unwrap_or_default(); + } }, super::GameMessage::LoadingProgress { user_id, user_name, progress } => { @@ -214,17 +244,19 @@ impl GenericGamemodeEngine { let mut all_users_loading_complete = true; for conn in self.users.read().await.values() { if user_id == conn.user.user_id() { - let progress_percent = (progress * 100.0).ceil() as u8; + let progress_percent = ((progress * 100.0).ceil() as u8).clamp(0, 100); log::debug!("User {} is loaded {}% into game {}", user_id, progress_percent, self.game_guid); conn.state.progress.store(progress_percent, std::sync::atomic::Ordering::Relaxed); - all_users_loading_complete &= progress_percent == 100; + if progress_percent != 100 { + all_users_loading_complete = false; + } } else { all_users_loading_complete &= conn.state.progress.load(std::sync::atomic::Ordering::Relaxed) == 100; } let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); match mode { - ConnectionMode::Loading - | ConnectionMode::Sync => { + ConnectionMode::Loading | 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); @@ -245,24 +277,17 @@ impl GenericGamemodeEngine { log::warn!("Got loading progress for user {} who is supposed to be already in-game", user_id); }, } - if !matches!(mode, ConnectionMode::Sync) { - all_users_loading_complete = false; - } } - // trigger game start if all_users_loading_complete { - log::info!("All players are ready for game {}", self.game_guid); - tokio::time::sleep(Self::END_OF_SYNC_DELAY).await; - let mut senders = Vec::new(); - for conn in self.users.read().await.values() { - crate::events::log_lnl_send_failure(conn.connection.rlnl() - .send_empty(rlnl::event_code::NetworkEvent::EndOfSync, literustlib::packet::Property::ReliableOrdered, &conn.connection.connection).await); - - senders.push(conn.connection.clone()); + for (id, conn) in self.users.read().await.iter() { + if let Err(e) = conn.connection.rlnl().send_empty( + rlnl::event_code::NetworkEvent::EndOfSync, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await { + log::error!("Failed to send EndOfSync event to user {}: {}", id, e); + } } - let game_start = chrono::Utc::now() + Self::COUNTDOWN_DURATION; - self.game_start.store(game_start.timestamp(), std::sync::atomic::Ordering::Relaxed); - super::countdown::match_countdown(senders, game_start); } } super::GameMessage::RequestLoadingProgress { user_id } => { @@ -310,21 +335,39 @@ impl GenericGamemodeEngine { rlnl::event_code::NetworkEvent::BroadcastWeaponSelect, literustlib::packet::Property::ReliableOrdered, &data, + false ).await; } }, super::GameMessage::RequestLoadingSync { user_id } => { - if let Some(user_key) = self.user_key_by_user_id(user_id).await { - if let Some(conn) = self.users.read().await.get(&user_key) { - self.spawn_send_sync_events(conn, user_id); + // wait for all users to be ready before transitioning to loading sync + let mut ready_count = 0; + for user in self.users.read().await.values() { + if user.user.user_id() == user_id { + user.state.mode.store(ConnectionMode::WaitingForSync.to_u8(), std::sync::atomic::Ordering::Relaxed); + ready_count += 1; + } else { + if matches!(ConnectionMode::from_u8(user.state.mode.load(std::sync::atomic::Ordering::Relaxed)), ConnectionMode::WaitingForSync) { + ready_count += 1; + } + } + } + let player_count = self.player_count.load(std::sync::atomic::Ordering::Relaxed) as usize; + if ready_count == player_count { + log::info!("All players ({}) awaiting sync for game {}", player_count, self.game_guid); + let total_users = self.users.read().await.len() as u8; + for (user_key, conn) in self.users.read().await.iter() { + self.spawn_send_sync_events(conn, conn.user.user_id(), *user_key, total_users); } } }, super::GameMessage::LoadComplete { user_id } => { if let Some(user_key) = self.user_key_by_user_id(user_id).await { if let Some(conn) = self.users.read().await.get(&user_key) { - log::debug!("Loading complete for game {}, user {} ({})", self.game_guid, user_id, user_key); - let game_start = chrono::DateTime::from_timestamp(self.game_start.load(std::sync::atomic::Ordering::Relaxed), 0).unwrap(); + 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( @@ -334,24 +377,48 @@ impl GenericGamemodeEngine { &conn.connection.connection) .await { log::error!("Failed to send updated TimeToGameStart to a user: {}", e); - } + }*/ self.spawn_initial_ingame_events(conn, user_id); + } else { + log::warn!("Invalid LoadComplete user key {} for game {}", user_key, self.game_guid); + continue; } + } else { + log::warn!("Unknown LoadComplete user id {} for game {}", user_id, self.game_guid); + continue; + } + // wait for all users to be ready for starting game start countdown + let mut all_users_loading_complete = true; + for conn in self.users.read().await.values() { + let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + all_users_loading_complete &= matches!(mode, ConnectionMode::WaitingToStart); + } + // trigger game start + if all_users_loading_complete { + let player_count = self.player_count.load(std::sync::atomic::Ordering::Relaxed) as usize; + log::info!("All players ({}) are ready for game {}", player_count, self.game_guid); + tokio::time::sleep(Self::END_OF_SYNC_DELAY).await; + let mut senders = Vec::new(); + for conn in self.users.read().await.values() { + senders.push((conn.connection.clone(), conn.state.clone())); + } + let game_start = chrono::Utc::now() + Self::COUNTDOWN_DURATION; + self.game_start.store(game_start.timestamp(), std::sync::atomic::Ordering::Relaxed); + super::countdown::match_countdown(senders, game_start); } } super::GameMessage::BroadcastRlnl { user_id: _, event, property, data } => { if let Some(data) = data { - self.broadcast(event, property, &*data).await; + self.broadcast(event, property, &*data, true).await; } else { - self.broadcast_dataless(event, property).await; + self.broadcast_dataless(event, property, true).await; } - } super::GameMessage::RebroadcastRlnl { skip_user_id, event, property, data } => { if let Some(data) = data { - self.rebroadcast(skip_user_id, event, property, &*data).await; + self.rebroadcast(skip_user_id, event, property, &*data, true).await; } else { - self.rebroadcast_dataless(skip_user_id, event, property).await; + self.rebroadcast_dataless(skip_user_id, event, property, true).await; } } @@ -373,18 +440,36 @@ impl GenericGamemodeEngine { self.is_complete.store(true, std::sync::atomic::Ordering::Relaxed); } - async fn send_loading_events(&self, user: &UserSender, player_id: u8) -> std::io::Result<()> { + fn spawn_send_loading_events(&self, user: &UserConnection, player_id: u8, players: Vec) { + let connection = user.connection.clone(); + let user_id = user.user.user_id(); + tokio::spawn(Self::send_loading_events_wrapper(connection, player_id, user_id, players)); + } + + async fn send_loading_events_wrapper(connection: UserSender, player_id: u8, user_id: i32, players: Vec) { + if let Err(e) = Self::send_loading_events(&connection, player_id, players).await { + log::error!("Failed to send Loading events for user {} ({}): {}", user_id, player_id, e); + } + } + + async fn send_loading_events(user: &UserSender, player_id: u8, players: Vec) -> std::io::Result<()> { let sender = user.rlnl(); sender.send_data( - &rlnl::events::loading::PlayerID { owner: player_id }, + &rlnl::events::ingame::PlayerId { player: player_id }, rlnl::event_code::NetworkEvent::GameGuidValidated, literustlib::packet::Property::ReliableOrdered, &user.connection ).await?; sender.send_data( &rlnl::events::loading::PlayerIDsAndNames { - num_players: 2, - players: vec![ // FIXME + num_players: players.len() as _, + players: players.into_iter().map(|player| rlnl::events::loading::PlayerIDAndName { + player_id: player.player_id as _, + name: rlnl::types::BinaryWriterString(player.public_id), + display_name: rlnl::types::BinaryWriterString(player.display_name), + }) + .collect(), + /*players: vec![ // FIXME rlnl::events::loading::PlayerIDAndName { player_id: 0, name: rlnl::types::BinaryWriterString("NGniusness".to_owned()), @@ -392,10 +477,10 @@ impl GenericGamemodeEngine { }, rlnl::events::loading::PlayerIDAndName { player_id: 1, - name: rlnl::types::BinaryWriterString("NGniusness_echo".to_owned()), - display_name: rlnl::types::BinaryWriterString("NGniusness_echo".to_owned()), + name: rlnl::types::BinaryWriterString("NGniusness2".to_owned()), + display_name: rlnl::types::BinaryWriterString("NGniusness2".to_owned()), }, - ], + ],*/ }, rlnl::event_code::NetworkEvent::PlayerIDs, literustlib::packet::Property::ReliableOrdered, @@ -413,19 +498,19 @@ impl GenericGamemodeEngine { Ok(()) } - fn spawn_send_sync_events(&self, user: &UserConnection, user_id: i32) { + fn spawn_send_sync_events(&self, user: &UserConnection, user_id: i32, player_id: u8, num_players: u8) { let connection = user.connection.clone(); - tokio::spawn(Self::send_sync_events_wrapper(connection, user_id)); + tokio::spawn(Self::send_sync_events_wrapper(connection, user_id, player_id, num_players)); user.state.mode.store(ConnectionMode::Sync.to_u8(), std::sync::atomic::Ordering::Relaxed); } - async fn send_sync_events_wrapper(connection: UserSender, user_id: i32) { - if let Err(e) = Self::send_sync_events(connection).await { + async fn send_sync_events_wrapper(connection: UserSender, user_id: i32, player_id: u8, num_players: u8) { + if let Err(e) = Self::send_sync_events(connection, player_id, num_players).await { log::error!("Failed to send Sync events for user {}: {}", user_id, e); } } - async fn send_sync_events(connection: UserSender) -> std::io::Result<()> { + async fn send_sync_events(connection: UserSender, _player_id: u8, num_players: u8) -> std::io::Result<()> { let sender = connection.rlnl(); sender.send_empty( rlnl::event_code::NetworkEvent::BeginSync, @@ -451,36 +536,33 @@ impl GenericGamemodeEngine { // generic sender.send_data( &rlnl::events::sync::InitialiseGameStats { - num_players: 2, - stats: vec![ // FIXME generate one per connection - rlnl::types::IngamePlayerStats { - player_name: 0, + num_players, + stats: (0..num_players).into_iter() + .map(|i| rlnl::types::IngamePlayerStats { + player_name: i, num_stats: 0, stats: vec![], - }, - rlnl::types::IngamePlayerStats { - player_name: 1, - num_stats: 0, - stats: vec![], - }, - ], + }).collect(), }, rlnl::event_code::NetworkEvent::InitialiseGameStats, literustlib::packet::Property::ReliableOrdered, &connection.connection) .await?; - sender.send_data( - &rlnl::events::sync::SpawnPoint { - pos: rlnl::types::PosQuatPair { - pos: rlnl::types::CompressedVec3 { x: 0, y: 42, z: 0 }, - rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + for i in 0..num_players { + sender.send_data( + &rlnl::events::sync::SpawnPoint { + pos: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3 { x: i as _, y: 42, z: i as _ }, + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + owner: i, }, - owner: 0, - }, - rlnl::event_code::NetworkEvent::FreeSpawnPoint, - literustlib::packet::Property::ReliableOrdered, - &connection.connection) - .await?; + rlnl::event_code::NetworkEvent::FreeSpawnPoint, + literustlib::packet::Property::ReliableOrdered, + &connection.connection) + .await?; + } + // seems to be for reconnecting /*sender.send_data( &rlnl::events::sync::SyncMachineCubes { @@ -506,7 +588,7 @@ impl GenericGamemodeEngine { fn spawn_initial_ingame_events(&self, user: &UserConnection, user_id: i32) { let connection = user.connection.clone(); tokio::spawn(Self::send_initial_ingame_events_wrapper(connection, user_id)); - user.state.mode.store(ConnectionMode::InGame.to_u8(), std::sync::atomic::Ordering::Relaxed); + //user.state.mode.store(ConnectionMode::InGame.to_u8(), std::sync::atomic::Ordering::Relaxed); } async fn send_initial_ingame_events_wrapper(connection: UserSender, user_id: i32) {