From a452d37d225a2418dc5ed2963493aa23b38b9f1a Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Mon, 21 Jul 2025 22:33:12 -0400 Subject: [PATCH] Add some semblance of match end --- rc_core/src/persist/user/traits.rs | 1 + .../src/events/client_unregister.rs | 30 ++++ rc_multiplayer/src/events/mod.rs | 92 +++++++++- .../src/handlers/ingame_broadcast.rs | 6 +- rc_multiplayer/src/main.rs | 2 +- rc_multiplayer/src/matches/aggregate.rs | 62 +++++-- rc_multiplayer/src/matches/engine.rs | 17 +- rc_multiplayer/src/matches/generic.rs | 148 +++++++++------- rc_multiplayer/src/matches/messages.rs | 8 +- rc_multiplayer/src/matches/mod.rs | 4 +- .../src/matches/modes/elimination.rs | 162 ++++++++++++++++++ rc_multiplayer/src/matches/modes/mod.rs | 6 + rc_multiplayer/src/matches/modes/no_op.rs | 22 +++ rc_multiplayer/src/traits.rs | 2 + 14 files changed, 471 insertions(+), 91 deletions(-) create mode 100644 rc_multiplayer/src/events/client_unregister.rs create mode 100644 rc_multiplayer/src/matches/modes/elimination.rs create mode 100644 rc_multiplayer/src/matches/modes/mod.rs create mode 100644 rc_multiplayer/src/matches/modes/no_op.rs diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index b0622cd..3fc48dc 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -251,6 +251,7 @@ pub struct PlayerLobbyDescriptor { pub group: Option, } +#[derive(Clone)] pub struct PlayerDescriptor { pub user_id: i32, pub player_id: u8, diff --git a/rc_multiplayer/src/events/client_unregister.rs b/rc_multiplayer/src/events/client_unregister.rs new file mode 100644 index 0000000..3f88ebd --- /dev/null +++ b/rc_multiplayer/src/events/client_unregister.rs @@ -0,0 +1,30 @@ +pub struct ClientUnregisterer { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::Dataless { + crate::handlers::Dataless::new(ClientUnregisterer::new(init_ctx)) +} + +impl ClientUnregisterer { + fn new(init_ctx: &crate::InitConfig) -> Self { + Self { + msg_router: init_ctx.matches_chann.clone(), + } + } +} + +#[async_trait::async_trait] +impl crate::handlers::DatalessEventCodeHandler for ClientUnregisterer { + const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::ClientUnregistered; + + async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { + if let Some(user_info) = user.user().await { + super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::EndConnection { + user_id: user_info.user_id(), + }).await); + } else { + log::error!("Failed to handle sync loading request for unknown user"); + } + } +} diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index 072609f..a361fea 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -7,6 +7,7 @@ mod loading_done; //mod player_input; mod spot_player; mod kill_player; +mod client_unregister; pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHandler { crate::handler::LnlEventHandler::new(init_ctx.users.clone(), crate::vehicle_motion::handler(init_ctx)) @@ -44,13 +45,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::< + .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))*/ + {rlnl::event_code::NetworkEvent::MultipleFireMisses as i16}, + {rlnl::event_code::NetworkEvent::MultipleFireMisses as i16}, + {literustlib::packet::Property::Unreliable as u8}, + rlnl::events::ingame::MultipleFireMisses, + >::handler(init_ctx)) .add(spot_player::handler(init_ctx)) .add(crate::handlers::Broadcaster::< false, @@ -89,6 +90,63 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa rlnl::events::ingame::Kill, // FIXME this is not a player ID -- it's actually sending 2 bytes not 1 >::handler(init_ctx)) .add(kill_player::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::EnergyModuleActivated as i16}, + {rlnl::event_code::NetworkEvent::EnergyModuleActivated as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::PlayerId, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::RadarModuleActivated as i16}, + {rlnl::event_code::NetworkEvent::RemoteRadarModuleActivated as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::PlayerId, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::ActivateTeleportEffect as i16}, + {rlnl::event_code::NetworkEvent::ActivateTeleportEffect as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::PlayerId, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::SpawnEmpLocator as i16}, + {rlnl::event_code::NetworkEvent::SpawnEmpLocator as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::SpawnEmpLocator, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::SpawnEmpMachineEffect as i16}, + {rlnl::event_code::NetworkEvent::SpawnEmpMachineEffect as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::NetworkStunnedMachineEffect, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::SpawnShield as i16}, + {rlnl::event_code::NetworkEvent::SpawnShield as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::ShieldModuleEvent, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::Taunt as i16}, + {rlnl::event_code::NetworkEvent::Taunt as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::Taunt, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::CosmeticAction as i16}, + {rlnl::event_code::NetworkEvent::CosmeticAction as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::CosmeticAction, + >::handler(init_ctx)) + .add(client_unregister::handler(init_ctx)) } #[inline] @@ -104,3 +162,25 @@ pub fn log_lnl_send_failure(result: std::io::Result) { log::error!("Failed to send packet: {}", e); } } + +mod _broadcast_impls { + use crate::Broadcastable; + + impl Broadcastable for rlnl::events::ingame::MultiPlayerInputChanged {} + impl Broadcastable for rlnl::events::ingame::WeaponFireEffect {} + impl Broadcastable for rlnl::events::ingame::FireMiss {} + impl Broadcastable for rlnl::events::ingame::MultipleFireMisses {} + impl Broadcastable for rlnl::events::ingame::DestroyCubesFull {} + impl Broadcastable for rlnl::events::ingame::DestroyCubeNoEffect {} + impl Broadcastable for rlnl::events::ingame::DestroyCubeEffectOnly {} + impl Broadcastable for rlnl::events::HealedCubes {} + impl Broadcastable for rlnl::events::ingame::PlayerId {} + impl Broadcastable for rlnl::events::ingame::SpawnEmpLocator {} + impl Broadcastable for rlnl::events::ingame::NetworkStunnedMachineEffect {} + impl Broadcastable for rlnl::events::ingame::ShieldModuleEvent {} + impl Broadcastable for rlnl::events::ingame::Taunt {} + impl Broadcastable for rlnl::events::ingame::CosmeticAction {} + + impl Broadcastable for rlnl::events::sync::UpdateGameModeSettings {} + impl Broadcastable for rlnl::events::GameTime {} +} diff --git a/rc_multiplayer/src/handlers/ingame_broadcast.rs b/rc_multiplayer/src/handlers/ingame_broadcast.rs index c04187e..33b91c7 100644 --- a/rc_multiplayer/src/handlers/ingame_broadcast.rs +++ b/rc_multiplayer/src/handlers/ingame_broadcast.rs @@ -1,11 +1,11 @@ -pub struct Broadcaster + byteserde::ser_heap::ByteSerializeHeap + Send + Sync + 'static> { +pub struct Broadcaster + crate::Broadcastable> { msg_router: tokio::sync::mpsc::Sender, code_out: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, _in: std::marker::PhantomData, } -impl + byteserde::ser_heap::ByteSerializeHeap + Send + Sync + 'static> Broadcaster { +impl + crate::Broadcastable> Broadcaster { pub fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::simple_typed::SimpleRlnl { crate::handlers::simple_typed::SimpleRlnl::new(Broadcaster::new(init_ctx)) } @@ -21,7 +21,7 @@ impl + byteserde::ser_heap::ByteSerializeHeap + Send + Sync + 'static> crate::handlers::simple_typed::RlnlEventCodeHandler for Broadcaster { +impl + crate::Broadcastable> crate::handlers::simple_typed::RlnlEventCodeHandler for Broadcaster { type In = InOut; const CODE: rlnl::event_code::NetworkEvent = crate::handler::i16_to_event_or_panic(CODE_IN); diff --git a/rc_multiplayer/src/main.rs b/rc_multiplayer/src/main.rs index 0caf6bc..e28663c 100644 --- a/rc_multiplayer/src/main.rs +++ b/rc_multiplayer/src/main.rs @@ -1,7 +1,7 @@ mod cli; mod handler; mod traits; -pub use traits::{EventCodeHandler, UserData, PacketData, EventCode, RobotMotionHandler}; +pub use traits::{EventCodeHandler, UserData, PacketData, EventCode, RobotMotionHandler, Broadcastable}; mod data; mod events; mod handlers; diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index 3e7762d..37a54a2 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -19,10 +19,43 @@ impl GameMatches { async fn start_new_match_engine(&self, _user: &Box, guid: &str) -> tokio::sync::mpsc::Sender { // TODO figure out gamemode and act accordingly - let engine = super::GenericGamemodeEngine::new(guid.to_owned()); + let engine = super::GenericGamemodeEngine::new(guid.to_owned(), super::modes::EliminationLogic::new()); engine.spawn() } + // create a new match + async fn create_new_game(&mut self, + user: std::sync::Arc>, + game_guid: String, + connection: std::sync::Arc>, + response: tokio::sync::oneshot::Sender>, + sender: std::sync::Arc>, + ) { + log::info!("Creating new game {}", game_guid); + let tx = self.start_new_match_engine(&user, &game_guid).await; + self.matches.insert(game_guid.clone(), tx.clone()); + 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 new match"); + } + } + + /*fn do_full_cleanup(&mut self) { + let mut games_to_remove = std::collections::HashSet::new(); + for (game_guid, tx) in self.matches.iter() { + if tx.is_closed() { + games_to_remove.insert(game_guid.to_owned()); + } + } + self.matches.retain(|key, _| !games_to_remove.contains(key)); + self.routing.retain(|_, val| !games_to_remove.contains(val)); + }*/ + + fn do_game_cleanup(&mut self, game_guid: &String) { + self.routing.retain(|_, game_guid2| game_guid2 != game_guid); + self.matches.remove(game_guid); + } + async fn run(mut self, mut rx: tokio::sync::mpsc::Receiver) { log::info!("Match message router has started"); while !rx.is_closed() { @@ -31,28 +64,26 @@ 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"); + if tx.is_closed() { + self.do_game_cleanup(&game_guid); + self.create_new_game(user, game_guid, connection, response, sender).await; + } else { + 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"); + } } } else { - // create a new match - log::info!("Creating new game {}", game_guid); - let tx = self.start_new_match_engine(&user, &game_guid).await; - self.matches.insert(game_guid.clone(), tx.clone()); - 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 new match"); - } + self.create_new_game(user, game_guid, connection, response, sender).await; } } msg => { let user_id = msg.user_id(); + let mut to_clean = None; if let Some(guid) = self.routing.get(&user_id) { if let Some(tx) = self.matches.get(guid) { if tx.is_closed() { - self.matches.remove(guid); - self.routing.remove(&user_id); + to_clean = Some(guid.to_owned()); } else { if tx.send(msg).await.is_err() { log::error!("Failed to route game message from user {} to match {}", user_id, guid); @@ -64,6 +95,9 @@ impl GameMatches { } else { log::warn!("Got unroutable user {}", user_id); } + if let Some(game_guid) = to_clean { + self.do_game_cleanup(&game_guid); + } } } } diff --git a/rc_multiplayer/src/matches/engine.rs b/rc_multiplayer/src/matches/engine.rs index 77b4370..cf293f4 100644 --- a/rc_multiplayer/src/matches/engine.rs +++ b/rc_multiplayer/src/matches/engine.rs @@ -1,3 +1,16 @@ -#[allow(dead_code)] -pub trait GamemodeEngine: Send + Sync { +pub struct RlnlPacket { + pub event: rlnl::event_code::NetworkEvent, + pub property: literustlib::packet::Property, + pub data: Box, } + +/// Functions returning a boolean indicate if the generic engine should also perform the default behaviour +/// (true = yes, do default behaviour) +#[async_trait::async_trait] +pub trait CustomGameLogic: Sized + Send + Sync + 'static { + async fn on_player_join(&self, generic: &super::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection, others: &[oj_rc_core::persist::user::PlayerDescriptor]) -> bool; + async fn on_player_end(&self, generic: &super::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> bool; + async fn on_vehicle_destroyed(&self, generic: &super::GenericGamemodeEngine, killer: u8, victim: u8) -> bool; + async fn extra_sync_events(&self, generic: &super::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> Vec; +} + diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 8d9c174..e830602 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -3,6 +3,7 @@ pub(super) struct UserConnection { pub(super) connection: UserSender, pub(super) state: std::sync::Arc, pub(super) machine: MachineState, + pub(super) descriptor: oj_rc_core::persist::user::PlayerDescriptor, } #[derive(Clone)] @@ -65,6 +66,7 @@ pub(super) enum ConnectionMode { Sync = 2, WaitingToStart = 3, InGame = 4, + Disconnected = 5, } impl ConnectionMode { @@ -76,6 +78,7 @@ impl ConnectionMode { 2 => Self::Sync, 3 => Self::WaitingToStart, 4 => Self::InGame, + 5 => Self::Disconnected, x => panic!("Unrecognized ConnectionMode {}", x), } } @@ -86,22 +89,23 @@ impl ConnectionMode { } } -pub(super) struct GenericGamemodeEngine { +pub(super) struct GenericGamemodeEngine { pub users: tokio::sync::RwLock>, pub user_id_map: tokio::sync::RwLock>, //pub recv: tokio::sync::Mutex>, //pub send: tokio::sync::mpsc::Sender, pub game_guid: String, - pub is_complete: std::sync::atomic::AtomicBool, + is_complete: std::sync::atomic::AtomicBool, pub game_start: std::sync::atomic::AtomicI64, pub player_count: std::sync::atomic::AtomicU8, + pub custom_logic_handler: L, } -impl GenericGamemodeEngine { +impl GenericGamemodeEngine { const END_OF_SYNC_DELAY: std::time::Duration = std::time::Duration::from_millis(100); const COUNTDOWN_DURATION: std::time::Duration = std::time::Duration::from_secs(5); - pub fn new(guid: String) -> Self { + pub fn new(guid: String, custom: L) -> Self { Self { users: tokio::sync::RwLock::new(std::collections::HashMap::new()), @@ -110,6 +114,7 @@ impl GenericGamemodeEngine { is_complete: std::sync::atomic::AtomicBool::new(false), game_start: std::sync::atomic::AtomicI64::new(-1), player_count: std::sync::atomic::AtomicU8::new(0), + custom_logic_handler: custom, } } @@ -188,7 +193,8 @@ impl GenericGamemodeEngine { } pub(super) async fn run(self, mut recv: tokio::sync::mpsc::Receiver) { - while !recv.is_closed() { + let mut is_engaged = true; + while !recv.is_closed() && is_engaged { if let Some(msg) = recv.recv().await { match msg { super::GameMessage::NewConnection { user, game_guid, connection, response, sender } => { @@ -201,28 +207,32 @@ impl GenericGamemodeEngine { return; } else { let mut users = self.users.write().await; - let new_user = UserConnection { - user, - connection: UserSender { - connection, - sender, - }, - 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; - match new_user.user.game_players(&game_guid).await { + match 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); + let user_id = user.user_id(); + let player_info = players.iter().filter(|p| p.user_id == user_id).next().unwrap(); + let id = player_info.player_id; + let new_user = UserConnection { + user, + connection: UserSender { + connection, + sender, + }, + state: std::sync::Arc::new(UserState::new()), + machine: MachineState::new(), + descriptor: player_info.to_owned(), + }; + if self.custom_logic_handler.on_player_join(&self, &new_user, &players).await { + 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) => { @@ -236,6 +246,34 @@ impl GenericGamemodeEngine { } }, + super::GameMessage::EndConnection { user_id } => { + if let Some(player_id) = self.user_key_by_user_id(user_id).await { + let conn_opt = self.users.write().await.remove(&player_id); + if let Some(conn) = conn_opt { + if self.custom_logic_handler.on_player_end(&self, &conn).await { + conn.state.mode.store(ConnectionMode::Disconnected.to_u8(), std::sync::atomic::Ordering::Relaxed); + if !self.is_complete.load(std::sync::atomic::Ordering::Relaxed) { + self.rebroadcast( + user_id, + rlnl::event_code::NetworkEvent::OnAnotherClientDisconnected, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::PlayerId { player: player_id }, + true, + ).await; + } else { + let mut has_active_connections = false; + for user in self.users.read().await.values() { + let mode = ConnectionMode::from_u8(user.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + has_active_connections |= !matches!(mode, ConnectionMode::Disconnected); + } + is_engaged = has_active_connections; + } + conn.connection.connection.disconnect(); + } + } + + } + }, super::GameMessage::LoadingProgress { user_id, user_name, progress } => { let progress_data = rlnl::events::loading::LoadingProgress { user_name: rlnl::types::BinaryWriterString(user_name), @@ -255,8 +293,8 @@ impl GenericGamemodeEngine { } let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); match mode { - ConnectionMode::Loading | ConnectionMode::WaitingForSync => {}, - ConnectionMode::Sync | ConnectionMode::WaitingToStart => { + 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); @@ -357,7 +395,8 @@ impl GenericGamemodeEngine { 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); + let extra_packets = self.custom_logic_handler.extra_sync_events(&self, conn).await; + self.spawn_send_sync_events(conn, conn.user.user_id(), *user_key, total_users, extra_packets); } } }, @@ -417,6 +456,7 @@ impl GenericGamemodeEngine { ).await; }, super::GameMessage::DestroyVehicle { user_id, remote_player, killer_player } => { + // FIXME allow custom_logic_handler to override the MachineDestroyedConfirmed send self.rebroadcast( user_id, rlnl::event_code::NetworkEvent::MachineDestroyedConfirmed, @@ -424,7 +464,8 @@ impl GenericGamemodeEngine { &rlnl::events::ingame::Kill { killee_player_id: remote_player, killer_player_id: killer_player }, true, ).await; - log::warn!("Player {} was destroyed by {} ({}) !!!implement this!!!", remote_player, killer_player, user_id); + log::info!("Player {} was destroyed by {} ({}) in game {}", remote_player, killer_player, user_id, self.game_guid); + self.custom_logic_handler.on_vehicle_destroyed(&self, killer_player, remote_player).await; } super::GameMessage::BroadcastRlnl { user_id: _, event, property, data } => { if let Some(data) = data { @@ -456,7 +497,7 @@ impl GenericGamemodeEngine { } } } - self.is_complete.store(true, std::sync::atomic::Ordering::Relaxed); + log::info!("Game {} has exited", self.game_guid); } fn spawn_send_loading_events(&self, user: &UserConnection, player_id: u8, players: Vec) { @@ -488,18 +529,6 @@ impl GenericGamemodeEngine { 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()), - display_name: rlnl::types::BinaryWriterString("NGniusness".to_owned()), - }, - rlnl::events::loading::PlayerIDAndName { - player_id: 1, - name: rlnl::types::BinaryWriterString("NGniusness2".to_owned()), - display_name: rlnl::types::BinaryWriterString("NGniusness2".to_owned()), - }, - ],*/ }, rlnl::event_code::NetworkEvent::PlayerIDs, literustlib::packet::Property::ReliableOrdered, @@ -517,42 +546,35 @@ impl GenericGamemodeEngine { Ok(()) } - fn spawn_send_sync_events(&self, user: &UserConnection, user_id: i32, player_id: u8, num_players: u8) { + fn spawn_send_sync_events(&self, user: &UserConnection, user_id: i32, player_id: u8, num_players: u8, extra_packets: Vec) { let connection = user.connection.clone(); - tokio::spawn(Self::send_sync_events_wrapper(connection, user_id, player_id, num_players)); + tokio::spawn(Self::send_sync_events_wrapper(connection, user_id, player_id, num_players, extra_packets)); user.state.mode.store(ConnectionMode::Sync.to_u8(), std::sync::atomic::Ordering::Relaxed); } - 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 { + async fn send_sync_events_wrapper(connection: UserSender, user_id: i32, player_id: u8, num_players: u8, extra_packets: Vec) { + if let Err(e) = Self::send_sync_events(connection, player_id, num_players, extra_packets).await { log::error!("Failed to send Sync events for user {}: {}", user_id, e); } } - async fn send_sync_events(connection: UserSender, _player_id: u8, num_players: u8) -> std::io::Result<()> { + async fn send_sync_events(connection: UserSender, _player_id: u8, num_players: u8, extra_packets: Vec) -> std::io::Result<()> { let sender = connection.rlnl(); sender.send_empty( rlnl::event_code::NetworkEvent::BeginSync, literustlib::packet::Property::ReliableOrdered, &connection.connection) .await?; - // sudden death - sender.send_data( - &rlnl::events::sync::UpdateGameModeSettings { // FIXME use value from config - respawn_heal_duration: 10.0, - respawn_full_heal_duration: 10.0, - }, - rlnl::event_code::NetworkEvent::GameModeSettings, - literustlib::packet::Property::ReliableOrdered, - &connection.connection) - .await?; - sender.send_data( - &rlnl::events::GameTime(300.0), // FIXME use value from config - rlnl::event_code::NetworkEvent::CurrentGameTime, - literustlib::packet::Property::ReliableOrdered, - &connection.connection) - .await?; - // generic + + for packet in extra_packets { + sender.send_data( + &*packet.data, + packet.event, + packet.property, + &connection.connection) + .await?; + } + sender.send_data( &rlnl::events::sync::InitialiseGameStats { num_players, @@ -628,6 +650,8 @@ impl GenericGamemodeEngine { // TODO Ok(()) } -} -impl super::GamemodeEngine for GenericGamemodeEngine {} + pub(super) fn game_done(&self) { + self.is_complete.store(true, std::sync::atomic::Ordering::SeqCst); + } +} diff --git a/rc_multiplayer/src/matches/messages.rs b/rc_multiplayer/src/matches/messages.rs index ead1c61..ca6cb6e 100644 --- a/rc_multiplayer/src/matches/messages.rs +++ b/rc_multiplayer/src/matches/messages.rs @@ -6,6 +6,9 @@ pub enum GameMessage { response: tokio::sync::oneshot::Sender>, sender: std::sync::Arc>, }, + EndConnection { + user_id: i32, + }, LoadingProgress { user_id: i32, user_name: String, @@ -39,13 +42,13 @@ pub enum GameMessage { user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, - data: Option>, + data: Option>, }, RebroadcastRlnl { skip_user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, - data: Option>, + data: Option>, }, Motion { user_id: i32, @@ -60,6 +63,7 @@ impl GameMessage { Self::NewConnection { user, .. } => { user.user_id() } + Self::EndConnection { user_id, .. } => *user_id, Self::LoadingProgress { user_id, .. } => *user_id, Self::RequestLoadingProgress { user_id, .. } => *user_id, Self::WeaponSelect { user_id, .. } => *user_id, diff --git a/rc_multiplayer/src/matches/mod.rs b/rc_multiplayer/src/matches/mod.rs index 78da3ff..1d29a09 100644 --- a/rc_multiplayer/src/matches/mod.rs +++ b/rc_multiplayer/src/matches/mod.rs @@ -1,5 +1,5 @@ mod engine; -pub use engine::GamemodeEngine; +pub(self) use engine::{CustomGameLogic, RlnlPacket}; mod messages; pub use messages::GameMessage; @@ -12,4 +12,6 @@ pub use aggregate::GameMatches; mod countdown; +pub mod modes; + pub const CHANNEL_BOUND: usize = 16; diff --git a/rc_multiplayer/src/matches/modes/elimination.rs b/rc_multiplayer/src/matches/modes/elimination.rs new file mode 100644 index 0000000..03e19f9 --- /dev/null +++ b/rc_multiplayer/src/matches/modes/elimination.rs @@ -0,0 +1,162 @@ +use crate::matches::CustomGameLogic; + +struct PlayerTracker { + alive: tokio::sync::Mutex>>, // team -> set of player_id +} + +impl PlayerTracker { + async fn track_vehicle(&self, player: &oj_rc_core::persist::user::PlayerDescriptor) { + let mut alive_lock = self.alive.lock().await; + if let Some(team) = alive_lock.get_mut(&(player.team as u8)) { + team.insert(player.player_id); + } else { + let mut new_team = std::collections::HashSet::new(); + new_team.insert(player.player_id); + alive_lock.insert(player.team as u8, new_team); + } + } + + async fn destroy_vehicle(&self, player: &oj_rc_core::persist::user::PlayerDescriptor) { + let mut alive_lock = self.alive.lock().await; + if let Some(team) = alive_lock.get_mut(&(player.team as u8)) { + team.remove(&player.player_id); + } else { + log::warn!("Destroyed player's vehicle for previously-unseen team"); + } + } + + async fn winner_team(&self) -> Option { + let alive_lock = self.alive.lock().await; + let mut only_alive_team = None; + for (team, players) in alive_lock.iter() { + if !players.is_empty() { + if only_alive_team.is_some() { + // more than one team is alive, game is not over + return None; + } else { + only_alive_team = Some(*team); + } + } + } + only_alive_team + } +} + +pub struct EliminationLogic { + tracked: PlayerTracker +} + +impl EliminationLogic { + pub fn new() -> Self { + Self { + tracked: PlayerTracker { + alive: tokio::sync::Mutex::new(std::collections::HashMap::new()), + }, + } + } +} + +#[async_trait::async_trait] +impl CustomGameLogic for EliminationLogic { + async fn on_player_join(&self, _generic: &crate::matches::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection, _others: &[oj_rc_core::persist::user::PlayerDescriptor]) -> bool { + self.tracked.track_vehicle(&player.descriptor).await; + true + } + + async fn on_player_end(&self, generic: &crate::matches::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> bool { + self.tracked.destroy_vehicle(&player.descriptor).await; + if let Some(winning_team) = self.tracked.winner_team().await { + log::info!("Team {} has won sudden death game {} because player {} left", winning_team, generic.game_guid, player.descriptor.player_id); + let data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason: rlnl::types::GameEndReason::OneTeamRemaining, + }; + let winning_team_i32 = winning_team as i32; + for conn in generic.users.read().await.values() { + let event = if conn.descriptor.team == winning_team_i32 { + rlnl::event_code::NetworkEvent::GameWon + } else { + rlnl::event_code::NetworkEvent::GameLost + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + generic.game_done(); + } + true + } + + async fn on_vehicle_destroyed(&self, generic: &crate::matches::GenericGamemodeEngine, _killer: u8, victim: u8) -> bool { + if let Some(conn) = generic.users.read().await.get(&victim) { + self.tracked.destroy_vehicle(&conn.descriptor).await; + let final_score = rlnl::events::ingame::SetFinalGameScore { + player_id: victim, + score: 42, + }; + generic.broadcast( + rlnl::event_code::NetworkEvent::SetFinalGameScore, + literustlib::packet::Property::ReliableOrdered, + &final_score, + true, + ).await; + if let Some(winning_team) = self.tracked.winner_team().await { + log::info!("Team {} has won sudden death game {}", winning_team, generic.game_guid); + let data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason: rlnl::types::GameEndReason::OneTeamRemaining, + }; + let winning_team_i32 = winning_team as i32; + for conn in generic.users.read().await.values() { + let event = if conn.descriptor.team == winning_team_i32 { + rlnl::event_code::NetworkEvent::GameWon + } else { + rlnl::event_code::NetworkEvent::GameLost + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + generic.game_done(); + } else { + log::info!("Player {} has been destroyed in sudden death game {}", victim, generic.game_guid); + let data = rlnl::events::ingame::GameLoseWin { + winning_team: if conn.descriptor.team == 0 { 1 } else { 0 }, // always the other team + end_reason: rlnl::types::GameEndReason::NoPlayersRemaining, + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + rlnl::event_code::NetworkEvent::GameLost, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + + } + true + } + + async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { + vec![ + crate::matches::RlnlPacket { + event: rlnl::event_code::NetworkEvent::GameModeSettings, + property: literustlib::packet::Property::ReliableOrdered, + data: Box::new(rlnl::events::sync::UpdateGameModeSettings { // FIXME use value from config + respawn_heal_duration: 10.0, + respawn_full_heal_duration: 10.0, + }), + }, + crate::matches::RlnlPacket { + event: rlnl::event_code::NetworkEvent::CurrentGameTime, + property: literustlib::packet::Property::ReliableOrdered, + data: Box::new(rlnl::events::GameTime(300.0)), // FIXME use value from config + }, + ] + } +} diff --git a/rc_multiplayer/src/matches/modes/mod.rs b/rc_multiplayer/src/matches/modes/mod.rs new file mode 100644 index 0000000..a92a7b7 --- /dev/null +++ b/rc_multiplayer/src/matches/modes/mod.rs @@ -0,0 +1,6 @@ +mod no_op; +#[allow(unused_imports)] +pub use no_op::NoOpLogic; + +mod elimination; +pub use elimination::EliminationLogic; diff --git a/rc_multiplayer/src/matches/modes/no_op.rs b/rc_multiplayer/src/matches/modes/no_op.rs new file mode 100644 index 0000000..82c963b --- /dev/null +++ b/rc_multiplayer/src/matches/modes/no_op.rs @@ -0,0 +1,22 @@ +use crate::matches::CustomGameLogic; + +pub struct NoOpLogic; + +#[async_trait::async_trait] +impl CustomGameLogic for NoOpLogic { + async fn on_player_join(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection, _others: &[oj_rc_core::persist::user::PlayerDescriptor]) -> bool { + true + } + + async fn on_player_end(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> bool { + true + } + + async fn on_vehicle_destroyed(&self, _generic: &crate::matches::GenericGamemodeEngine, _killer: u8, _victim: u8) -> bool { + true + } + + async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { + Vec::default() + } +} diff --git a/rc_multiplayer/src/traits.rs b/rc_multiplayer/src/traits.rs index 036a2d6..24f1ba5 100644 --- a/rc_multiplayer/src/traits.rs +++ b/rc_multiplayer/src/traits.rs @@ -14,3 +14,5 @@ pub trait EventCode: EventCodeHandler { pub trait RobotMotionHandler: Send + Sync { async fn handle(&self, data: &bytes::Bytes, user: &UserData); } + +pub trait Broadcastable: byteserde::ser_heap::ByteSerializeHeap + core::any::Any + Send + Sync + 'static {}