diff --git a/rc_multiplayer/src/matches/engine.rs b/rc_multiplayer/src/matches/engine.rs index 802c2f4..5b9599b 100644 --- a/rc_multiplayer/src/matches/engine.rs +++ b/rc_multiplayer/src/matches/engine.rs @@ -8,15 +8,27 @@ pub struct RlnlPacket { /// (true = yes, do default behaviour) #[async_trait::async_trait] pub trait CustomGameLogic: Sized + Send + Sync + 'static { + /// Called when player joins the game server (after authentication). async fn on_player_join(&self, generic: &super::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection, others: &[oj_rc_core::persist::user::PlayerDescriptor]) -> bool; + /// Called when player leaves the game server (i.e. player disconnects). async fn on_player_end(&self, generic: &super::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> bool; + /// Called when a player's vehicle is destroyed by another player. async fn on_vehicle_destroyed(&self, generic: &super::GenericGamemodeEngine, killer: u8, victim: u8) -> bool; + /// Called when a player's vehicle self-destructs. async fn on_vehicle_self_destruct(&self, generic: &super::GenericGamemodeEngine, user: u8, is_classic: bool) -> bool; + /// Called during loading, before the sync stage is entered. + /// + /// Roughly, loading is as follows: `Loading -> WaitingForSync -> Sync -> WaitingToStart -> InGame`. async fn extra_sync_events(&self, generic: &super::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> Vec; + /// Called when loading completes and the countdown is starting async fn on_countdown_start(&self, generic: &super::GenericGamemodeEngine, game_start: chrono::DateTime) -> bool; + /// Called when the game is marked as complete async fn on_game_completed(&self, generic: &super::GenericGamemodeEngine) -> bool; + /// Called when various network events are broadcast from one client but before they are sent to the rest of the clients async fn on_broadcast(&self, generic: &super::GenericGamemodeEngine, user_id: i32, event_out: rlnl::event_code::NetworkEvent, event_in: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &Option>, skip_user: bool) -> bool; + /// Called when a vehicle motion event is received from a client async fn on_motion(&self, generic: &super::GenericGamemodeEngine, motion: &rlnl::machine_motion::MachineMotion, location: (f32, f32, f32)) -> bool; + /// Called when a gamemode-specific network event is received from a client async fn on_custom(&self, generic: &super::GenericGamemodeEngine, user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box); } diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 7f727aa..68295ee 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -390,6 +390,16 @@ impl GenericGamemodeEngine { super::GameMessage::RequestLeave { user_id } => { log::info!("User {} wants to leave game {}", user_id, self.game_guid()); if let Some(player_id) = self.user_key_by_user_id(user_id).await { + self.rebroadcast( + user_id, + rlnl::event_code::NetworkEvent::MachineDestroyedConfirmed, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::Kill { + killee_player_id: player_id, + killer_player_id: player_id, + }, + true, + ).await; if let Some(conn) = self.users.read().await.get(&player_id) { crate::events::log_lnl_send_failure(conn.connection.rlnl().send_empty( rlnl::event_code::NetworkEvent::PlayerQuitRequestComplete, @@ -498,6 +508,11 @@ impl GenericGamemodeEngine { let mut ready_count = 0; for user in self.users.read().await.values() { if user.user.user_id() == user_id { + if !matches!(ConnectionMode::from_u8(user.state.mode.load(std::sync::atomic::Ordering::Relaxed)), ConnectionMode::Loading | ConnectionMode::Disconnected) { + log::warn!("Got RequestLoadingSync after user {} was already in/past WaitingForSync stage", user_id); + continue; + } + log::info!("User {} is awaiting sync", user_id); user.state.mode.store(ConnectionMode::WaitingForSync.to_u8(), std::sync::atomic::Ordering::Relaxed); ready_count += 1; } else { @@ -520,9 +535,11 @@ impl GenericGamemodeEngine { if let Some(conn) = self.users.read().await.get(&user_key) { log::info!("Loading complete for game {}, user {} (player {})", self.game_guid(), user_id, user_key); conn.state.progress.store(100, std::sync::atomic::Ordering::Relaxed); - 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); + let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + if !matches!(mode, ConnectionMode::Sync) { + log::warn!("Player {} completed loading but is in mode {:?} (should be Sync)", conn.descriptor.player_id, mode); } + 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()); @@ -800,7 +817,7 @@ impl GenericGamemodeEngine { }, literustlib::packet::Property::Unreliable, &conn.connection.connection).await); } } - if self.game_start.load(std::sync::atomic::Ordering::Relaxed) == -1 { + /*if self.game_start.load(std::sync::atomic::Ordering::Relaxed) == -1 { 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)); @@ -819,7 +836,7 @@ impl GenericGamemodeEngine { } } } - } + }*/ } else { log::warn!("Received machine motion with unknown player id {} from user {}", motion.player_id, user_id); } @@ -1009,6 +1026,14 @@ impl GenericGamemodeEngine { literustlib::packet::Property::ReliableOrdered, &user.connection) .await?;*/ + + tokio::time::sleep(GenericGamemodeEngine::::END_OF_SYNC_DELAY).await; + + sender.send_empty( + rlnl::event_code::NetworkEvent::EndOfSync, + literustlib::packet::Property::ReliableOrdered, + &connection.connection + ).await?; Ok(()) } diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index 0d8d851..cb2b18d 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -47,6 +47,28 @@ impl PlayerTracker { self.in_point.write().await.insert(player.player_id, std::sync::atomic::AtomicU16::new(u16::MAX)); self.respawning.write().await.insert(player.player_id, std::sync::atomic::AtomicI64::new(i64::MIN)); } + + async fn disconnect_player(&self, player_id: u8) { + let mut conn_lock = self.connected.lock().await; + for team_members in conn_lock.values_mut() { + team_members.remove(&player_id); + } + } + + async fn single_remaining_team(&self) -> Option { + let conn_lock = self.connected.lock().await; + let mut first_remaining_team = None; + for (team, team_members) in conn_lock.iter() { + if !team_members.is_empty() { + if first_remaining_team.is_some() { + return None; + } else { + first_remaining_team = Some(*team) + } + } + } + first_remaining_team + } } struct PointInfo { @@ -430,6 +452,7 @@ impl BaseInfo { enum WinMode { BaseFull, OutOfTime, + OutOfPlayers, } pub struct BattleArenaLogic { @@ -535,7 +558,8 @@ impl BattleArenaLogic { async fn do_win(&self, winning_team: u8, ty: WinMode, generic: &crate::matches::GenericGamemodeEngine) { generic.game_done(); let end_reason = match ty { - WinMode::BaseFull => rlnl::types::GameEndReason::BaseDestroyed, + WinMode::BaseFull + | WinMode::OutOfPlayers => rlnl::types::GameEndReason::BaseDestroyed, WinMode::OutOfTime => rlnl::types::GameEndReason::TimeOut, }; let payload = rlnl::events::ingame::GameLoseWin { @@ -545,7 +569,8 @@ impl BattleArenaLogic { for player in generic.users.read().await.values() { let is_winner = player.descriptor.team == winning_team as i32; let net_event = match ty { - WinMode::BaseFull => { + WinMode::BaseFull + | WinMode::OutOfPlayers => { if is_winner { rlnl::event_code::NetworkEvent::GameWonBaseDestroyed } else { rlnl::event_code::NetworkEvent::GameLostBaseDestroyed } }, WinMode::OutOfTime => { @@ -563,6 +588,15 @@ impl BattleArenaLogic { ); } } + + async fn do_destruct_tasks(&self, generic: &crate::matches::GenericGamemodeEngine, player_id: u8) { + if let Some(player_team) = self.player_tracking.team(player_id).await { + let was_in_point = self.player_tracking.swap_is_in_point(player_id, None).await; + if let Some(was_in_point) = was_in_point { + self.capture_tracking.on_exit(generic, was_in_point, player_id, player_team as i8, self.config.num_segments as f32).await; + } + } + } } #[async_trait::async_trait] @@ -573,22 +607,25 @@ impl CustomGameLogic for BattleArenaLogic { true } - async fn on_player_end(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> bool { + async fn on_player_end(&self, generic: &crate::matches::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> bool { + let player_id = player.descriptor.player_id; + self.do_destruct_tasks(generic, player_id).await; + self.player_tracking.disconnect_player(player_id).await; + if let Some(winning_team) = self.player_tracking.single_remaining_team().await { + self.do_win(winning_team, WinMode::OutOfPlayers, generic).await; + } true } async fn on_vehicle_destroyed(&self, generic: &crate::matches::GenericGamemodeEngine, _killer: u8, victim: u8) -> bool { - if let Some(player_team) = self.player_tracking.team(victim).await { - let was_in_point = self.player_tracking.swap_is_in_point(victim, None).await; - if let Some(was_in_point) = was_in_point { - self.capture_tracking.on_exit(generic, was_in_point, victim, player_team as i8, self.config.num_segments as f32).await; - } - } + self.do_destruct_tasks(generic, victim).await; // TODO handle respawn true } - async fn on_vehicle_self_destruct(&self, _generic: &crate::matches::GenericGamemodeEngine, _user: u8, _is_classic: bool) -> bool { + async fn on_vehicle_self_destruct(&self, generic: &crate::matches::GenericGamemodeEngine, user: u8, _is_classic: bool) -> bool { + self.do_destruct_tasks(generic, user).await; + // TODO handle respawn true }