From 52493bf166baa8d01ed471fdeb85c194c0d95a3f Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Tue, 22 Jul 2025 18:27:41 -0400 Subject: [PATCH] Implement most of the occasional packets sent in elimination, expand interface for custom game logic #30 --- Cargo.lock | 8 + rc_lobby_room/Cargo.toml | 1 + rc_lobby_room/src/lobby.rs | 1 + rc_multiplayer/Cargo.toml | 1 + rc_multiplayer/src/events/flipper_start.rs | 30 ++++ rc_multiplayer/src/events/mod.rs | 43 ++++- .../src/events/self_destruct_elimination.rs | 31 ++++ .../src/handlers/ingame_broadcast.rs | 2 + .../src/handlers/ingame_broadcast_dataless.rs | 3 + rc_multiplayer/src/handlers/mod.rs | 1 + rc_multiplayer/src/matches/engine.rs | 5 + rc_multiplayer/src/matches/generic.rs | 150 ++++++++++++++---- rc_multiplayer/src/matches/messages.rs | 13 +- rc_multiplayer/src/matches/mod.rs | 2 + .../src/matches/modes/elimination.rs | 83 +++++++++- rc_multiplayer/src/matches/modes/no_op.rs | 20 +++ rc_multiplayer/src/matches/timer.rs | 71 +++++++++ rc_multiplayer/src/vehicle_motion.rs | 19 ++- rc_services_room/src/operations/mod.rs | 2 + .../src/operations/steam_promo.rs | 22 +++ .../src/operations/calculate_mmr.rs | 13 ++ rc_social_room/src/operations/mod.rs | 2 + 22 files changed, 482 insertions(+), 41 deletions(-) create mode 100644 rc_multiplayer/src/events/flipper_start.rs create mode 100644 rc_multiplayer/src/events/self_destruct_elimination.rs create mode 100644 rc_multiplayer/src/matches/timer.rs create mode 100644 rc_services_room/src/operations/steam_promo.rs create mode 100644 rc_social_room/src/operations/calculate_mmr.rs diff --git a/Cargo.lock b/Cargo.lock index a22e0dc..59ab4aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,6 +452,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic_float" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" + [[package]] name = "autocfg" version = "1.4.0" @@ -2704,6 +2710,7 @@ name = "oj_rc_lobby_room" version = "0.3.0" dependencies = [ "async-trait", + "chrono", "clap", "env_logger", "log", @@ -3334,6 +3341,7 @@ name = "rc_multiplayer" version = "0.3.0" dependencies = [ "async-trait", + "atomic_float", "bytes", "byteserde", "chrono", diff --git a/rc_lobby_room/Cargo.toml b/rc_lobby_room/Cargo.toml index f899496..dd80aa1 100644 --- a/rc_lobby_room/Cargo.toml +++ b/rc_lobby_room/Cargo.toml @@ -17,3 +17,4 @@ oj_polariton_auth = { version = "*", path = "../polariton_auth" } polariton_server.workspace = true oj_rc_core = { version = "*", path = "../rc_core" } async-trait.workspace = true +chrono.workspace = true diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index 3744201..100ce5a 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -42,6 +42,7 @@ impl QueueHandler { use std::hash::Hasher; let mut hasher = std::hash::DefaultHasher::new(); key.hash(&mut hasher); + chrono::Utc::now().timestamp().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_descs = players.iter().map(|x| oj_rc_core::persist::user::PlayerLobbyDescriptor { diff --git a/rc_multiplayer/Cargo.toml b/rc_multiplayer/Cargo.toml index 95719ce..30dc702 100644 --- a/rc_multiplayer/Cargo.toml +++ b/rc_multiplayer/Cargo.toml @@ -17,6 +17,7 @@ async-trait.workspace = true bytes = "1.10" byteserde = "0.6.2" chrono.workspace = true +atomic_float = "1.1" literustlib_server = { version = "0.1", path = "../../LiteRustLib/server" } literustlib = { version = "0.1", path = "../../LiteRustLib" } diff --git a/rc_multiplayer/src/events/flipper_start.rs b/rc_multiplayer/src/events/flipper_start.rs new file mode 100644 index 0000000..546678c --- /dev/null +++ b/rc_multiplayer/src/events/flipper_start.rs @@ -0,0 +1,30 @@ +pub struct RectifierStart { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::Dataless { + crate::handlers::Dataless::new(RectifierStart::new(init_ctx)) +} + +impl RectifierStart { + fn new(init_ctx: &crate::InitConfig) -> Self { + Self { + msg_router: init_ctx.matches_chann.clone(), + } + } +} + +#[async_trait::async_trait] +impl crate::handlers::DatalessEventCodeHandler for RectifierStart { + const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::AlignmentRectifierStarted; + + 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::FlippingStarted { + 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 6989739..05074ce 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -8,6 +8,8 @@ mod loading_done; mod spot_player; mod kill_player; mod client_unregister; +mod flipper_start; +mod self_destruct_elimination; 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)) @@ -25,12 +27,14 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa {literustlib::packet::Property::Unreliable as u8}, rlnl::events::ingame::PlayerIdAndInputData, >::handler(init_ctx)) - .add(crate::handlers::DatalessBroadcaster::< + .add(flipper_start::handler(init_ctx)) + /*.add(crate::handlers::Broadcaster::< true, {rlnl::event_code::NetworkEvent::AlignmentRectifierStarted as i16}, {rlnl::event_code::NetworkEvent::AlignmentRectifierStarted as i16}, {literustlib::packet::Property::Unreliable as u8}, - >::handler(init_ctx)) + rlnl::events::ingame::PlayerId, + >::handler(init_ctx))*/ .add(crate::handlers::Broadcaster::< true, {rlnl::event_code::NetworkEvent::FireWeaponEffect as i16}, @@ -106,28 +110,28 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa >::handler(init_ctx)) .add(crate::handlers::Broadcaster::< true, - {rlnl::event_code::NetworkEvent::ActivateTeleportEffect as i16}, + {rlnl::event_code::NetworkEvent::BroadcastActivateTeleportEffect as i16}, {rlnl::event_code::NetworkEvent::ActivateTeleportEffect as i16}, {literustlib::packet::Property::ReliableOrdered as u8}, - rlnl::events::ingame::PlayerId, + rlnl::events::ingame::TeleportActivateEffect, >::handler(init_ctx)) .add(crate::handlers::Broadcaster::< true, - {rlnl::event_code::NetworkEvent::SpawnEmpLocator as i16}, + {rlnl::event_code::NetworkEvent::BroadcastSpawnEmpLocator 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::BroadcastSpawnEmpMachineEffect 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::ShieldSpawned as i16}, {rlnl::event_code::NetworkEvent::SpawnShield as i16}, {literustlib::packet::Property::ReliableOrdered as u8}, rlnl::events::ingame::ShieldModuleEvent, @@ -147,6 +151,28 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa rlnl::events::ingame::CosmeticAction, >::handler(init_ctx)) .add(client_unregister::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::BroadcastInvisible as i16}, + {rlnl::event_code::NetworkEvent::MakeInvisible as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::PlayerId, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::BroadcastVisible as i16}, + {rlnl::event_code::NetworkEvent::MakeVisible as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::PlayerId, + >::handler(init_ctx)) + .add(crate::handlers::Broadcaster::< + true, + {rlnl::event_code::NetworkEvent::UpdateVotingAfterBattle as i16}, + {rlnl::event_code::NetworkEvent::UpdateVotingAfterBattle as i16}, + {literustlib::packet::Property::ReliableOrdered as u8}, + rlnl::events::ingame::UpdateVotingAfterBattle, + >::handler(init_ctx)) + .add(self_destruct_elimination::handler(init_ctx)) } #[inline] @@ -180,7 +206,10 @@ mod _broadcast_impls { 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::ingame::UpdateVotingAfterBattle {} + impl Broadcastable for rlnl::events::ingame::TeleportActivateEffect {} impl Broadcastable for rlnl::events::sync::UpdateGameModeSettings {} impl Broadcastable for rlnl::events::GameTime {} + impl Broadcastable for rlnl::events::ingame::TeamBaseState {} } diff --git a/rc_multiplayer/src/events/self_destruct_elimination.rs b/rc_multiplayer/src/events/self_destruct_elimination.rs new file mode 100644 index 0000000..55b9bc6 --- /dev/null +++ b/rc_multiplayer/src/events/self_destruct_elimination.rs @@ -0,0 +1,31 @@ +pub struct SelfDestructElim { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::Dataless { + crate::handlers::Dataless::new(SelfDestructElim::new(init_ctx)) +} + +impl SelfDestructElim { + fn new(init_ctx: &crate::InitConfig) -> Self { + Self { + msg_router: init_ctx.matches_chann.clone(), + } + } +} + +#[async_trait::async_trait] +impl crate::handlers::DatalessEventCodeHandler for SelfDestructElim { + const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::SelfDestructClassicMode; + + 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::SelfDestruct { + user_id: user_info.user_id(), + is_classic: true, + }).await); + } else { + log::error!("Failed to handle sync loading request for unknown user"); + } + } +} diff --git a/rc_multiplayer/src/handlers/ingame_broadcast.rs b/rc_multiplayer/src/handlers/ingame_broadcast.rs index 33b91c7..2b0595e 100644 --- a/rc_multiplayer/src/handlers/ingame_broadcast.rs +++ b/rc_multiplayer/src/handlers/ingame_broadcast.rs @@ -32,6 +32,7 @@ impl { msg_router: tokio::sync::mpsc::Sender, code_out: rlnl::event_code::NetworkEvent, @@ -28,6 +29,7 @@ impl , 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 on_vehicle_self_destruct(&self, generic: &super::GenericGamemodeEngine, user: u8, is_classic: bool) -> bool; async fn extra_sync_events(&self, generic: &super::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> Vec; + async fn on_countdown_start(&self, generic: &super::GenericGamemodeEngine, game_start: chrono::DateTime) -> bool; + async fn on_game_completed(&self, generic: &super::GenericGamemodeEngine) -> bool; + 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; + async fn on_motion(&self, generic: &super::GenericGamemodeEngine, motion: &rlnl::machine_motion::MachineMotion) -> bool; } diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 2e271f6..e1d9eb3 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -34,12 +34,14 @@ impl UserState { pub(super) struct MachineState { pub(super) selected_weapon: WeaponInfo, + pub(super) location: Location, } impl MachineState { fn new() -> Self { Self { selected_weapon: WeaponInfo::new(), + location: Location::new(), } } } @@ -58,6 +60,22 @@ impl WeaponInfo { } } +pub(super) struct Location { + pub x: atomic_float::AtomicF32, + pub y: atomic_float::AtomicF32, + pub z: atomic_float::AtomicF32, +} + +impl Location { + fn new() -> Self { + Self { + x: atomic_float::AtomicF32::new(0.0), + y: atomic_float::AtomicF32::new(0.0), + z: atomic_float::AtomicF32::new(0.0), + } + } +} + #[repr(u8)] #[derive(Debug, Copy, Clone)] pub(super) enum ConnectionMode { @@ -267,6 +285,13 @@ impl GenericGamemodeEngine { has_active_connections |= !matches!(mode, ConnectionMode::Disconnected); } is_engaged = has_active_connections; + if !has_active_connections { + if self.custom_logic_handler.on_game_completed(&self).await { + if let Err(e) = conn.user.complete_game(&self.game_guid).await { + log::error!("Failed to mark game {} as complete: {}", self.game_guid, e); + } + } + } } conn.connection.connection.goodbye(&conn.connection.sender).await; } @@ -437,13 +462,15 @@ impl GenericGamemodeEngine { 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); + if self.custom_logic_handler.on_countdown_start(&self, game_start).await { + let mut senders = Vec::new(); + for conn in self.users.read().await.values() { + senders.push((conn.connection.clone(), conn.state.clone())); + } + self.game_start.store(game_start.timestamp(), std::sync::atomic::Ordering::Relaxed); + super::countdown::match_countdown(senders, game_start); + } } }, super::GameMessage::SpotVehicle { user_id, remote_player } => { @@ -466,31 +493,96 @@ impl GenericGamemodeEngine { ).await; 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 { - self.broadcast(event, property, &*data, true).await; - } else { - 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, true).await; - } else { - self.rebroadcast_dataless(skip_user_id, event, property, true).await; + super::GameMessage::SelfDestruct { user_id, is_classic } => { + 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; + log::info!("Player {} ({}) self-destructed in game {} (elimination? {})", player_id, user_id, self.game_guid, is_classic); + if self.custom_logic_handler.on_vehicle_self_destruct(&self, player_id, is_classic).await { + if is_classic { + self.rebroadcast( + user_id, + rlnl::event_code::NetworkEvent::OnAnotherClientDisconnected, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::PlayerId { player: 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, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + conn.state.mode.store(ConnectionMode::Disconnected.to_u8(), std::sync::atomic::Ordering::Relaxed); + conn.connection.connection.disconnect(); + } + } + } } }, - super::GameMessage::Motion { user_id, data } => { - for conn in self.users.read().await.values() { - if conn.user.user_id() == user_id { continue; } // fun fact: the game hard crashes if you omit this - crate::events::log_lnl_send_failure(conn.connection.sender.send_data(crate::handler::EventData { - message_ty: crate::data::MessageType::RobotMotion, - variant: 0, - data_size: data.len() as _, - data: data.clone(), - }, literustlib::packet::Property::Unreliable, &conn.connection.connection).await); + super::GameMessage::FlippingStarted { user_id } => { + if let Some(user_key) = self.user_key_by_user_id(user_id).await { + self.rebroadcast( + user_id, + rlnl::event_code::NetworkEvent::AlignmentRectifierStarted, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::PlayerId { player: user_key }, + true, + ).await; + } + }, + super::GameMessage::BroadcastRlnl { user_id, event, event_in, property, data } => { + if self.custom_logic_handler.on_broadcast(&self, user_id, event, event_in, property, &data, false).await { + if let Some(data) = data { + self.broadcast(event, property, &*data, true).await; + } else { + self.broadcast_dataless(event, property, true).await; + } + } + }, + super::GameMessage::RebroadcastRlnl { skip_user_id, event, event_in, property, data } => { + if self.custom_logic_handler.on_broadcast(&self, skip_user_id, event, event_in, property, &data, true).await { + if let Some(data) = data { + self.rebroadcast(skip_user_id, event, property, &*data, true).await; + } else { + self.rebroadcast_dataless(skip_user_id, event, property, true).await; + } + } + }, + super::GameMessage::Motion { user_id, motion } => { + if self.custom_logic_handler.on_motion(&self, &motion).await { + if let Some(conn) = self.users.read().await.get(&motion.player_id) { + let (x, y, z) = motion.rb_state.rb_pos_rot.pos.into(); + conn.machine.location.x.store(x, std::sync::atomic::Ordering::Relaxed); + conn.machine.location.y.store(y, std::sync::atomic::Ordering::Relaxed); + conn.machine.location.z.store(z, std::sync::atomic::Ordering::Relaxed); + //log::debug!("Player {} is at (x, y, z) ({}, {}, {})", motion.player_id, x, y, z); + use byteserde::ser_heap::ByteSerializeHeap; + let mut ser = byteserde::ser_heap::ByteSerializerHeap::default(); + if let Err(e) = motion.byte_serialize_heap(&mut ser) { + log::error!("Failed to serialize motion data from user {}: {}", user_id, e); + } else { + let data = bytes::Bytes::copy_from_slice(ser.as_slice()); + for conn in self.users.read().await.values() { + if conn.user.user_id() == user_id { continue; } // fun fact: the game hard crashes if you omit this + crate::events::log_lnl_send_failure(conn.connection.sender.send_data(crate::handler::EventData { + message_ty: crate::data::MessageType::RobotMotion, + variant: 0, + data_size: data.len() as _, + data: data.clone(), + }, literustlib::packet::Property::Unreliable, &conn.connection.connection).await); + } + } + } else { + log::warn!("Received machine motion with unknown player id {} from user {}", motion.player_id, user_id); + } } }, super::GameMessage::NoOp => {}, @@ -654,4 +746,8 @@ impl GenericGamemodeEngine { pub(super) fn game_done(&self) { self.is_complete.store(true, std::sync::atomic::Ordering::SeqCst); } + + pub(super) fn is_game_done(&self) -> bool { + self.is_complete.load(std::sync::atomic::Ordering::SeqCst) + } } diff --git a/rc_multiplayer/src/matches/messages.rs b/rc_multiplayer/src/matches/messages.rs index ca6cb6e..1bd04e9 100644 --- a/rc_multiplayer/src/matches/messages.rs +++ b/rc_multiplayer/src/matches/messages.rs @@ -38,21 +38,30 @@ pub enum GameMessage { remote_player: u8, killer_player: u8, }, + SelfDestruct { + user_id: i32, + is_classic: bool, + }, + FlippingStarted { // flip yeah! + user_id: i32, + }, BroadcastRlnl { user_id: i32, event: rlnl::event_code::NetworkEvent, + event_in: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Option>, }, RebroadcastRlnl { skip_user_id: i32, event: rlnl::event_code::NetworkEvent, + event_in: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Option>, }, Motion { user_id: i32, - data: bytes::Bytes, + motion: rlnl::machine_motion::MachineMotion, }, NoOp, } @@ -71,6 +80,8 @@ impl GameMessage { Self::LoadComplete { user_id, .. } => *user_id, Self::SpotVehicle { user_id, .. } => *user_id, Self::DestroyVehicle { user_id, .. } => *user_id, + Self::SelfDestruct { user_id, .. } => *user_id, + Self::FlippingStarted { user_id, .. } => *user_id, Self::BroadcastRlnl { user_id, .. } => *user_id, Self::RebroadcastRlnl { skip_user_id, .. } => *skip_user_id, Self::Motion { user_id, .. } => *user_id, diff --git a/rc_multiplayer/src/matches/mod.rs b/rc_multiplayer/src/matches/mod.rs index 1d29a09..afaf41b 100644 --- a/rc_multiplayer/src/matches/mod.rs +++ b/rc_multiplayer/src/matches/mod.rs @@ -14,4 +14,6 @@ mod countdown; pub mod modes; +mod timer; + pub const CHANNEL_BOUND: usize = 16; diff --git a/rc_multiplayer/src/matches/modes/elimination.rs b/rc_multiplayer/src/matches/modes/elimination.rs index 03e19f9..aa74d05 100644 --- a/rc_multiplayer/src/matches/modes/elimination.rs +++ b/rc_multiplayer/src/matches/modes/elimination.rs @@ -40,20 +40,41 @@ impl PlayerTracker { } only_alive_team } + + async fn teams(&self) -> std::collections::HashSet { + self.alive.lock().await.keys().map(|x| *x).collect() + } } pub struct EliminationLogic { - tracked: PlayerTracker + tracked: PlayerTracker, + game_duration: std::time::Duration, + game_end: std::sync::atomic::AtomicI64, + timer_task: tokio::sync::Mutex>>, } impl EliminationLogic { pub fn new() -> Self { + let dur = std::time::Duration::from_secs(300); + let fake_end = (chrono::Utc::now() + dur).timestamp(); Self { tracked: PlayerTracker { alive: tokio::sync::Mutex::new(std::collections::HashMap::new()), }, + game_duration: dur, + game_end: std::sync::atomic::AtomicI64::new(fake_end), + timer_task: tokio::sync::Mutex::new(None), } } + + async fn abort_timer_sync(&self) { + let mut lock = self.timer_task.lock().await; + if let Some(timer_t) = &*lock { + timer_t.abort(); + log::debug!("Aborted elimination match timer task"); + } + *lock = None; + } } #[async_trait::async_trait] @@ -64,6 +85,14 @@ impl CustomGameLogic for EliminationLogic { } async fn on_player_end(&self, generic: &crate::matches::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> bool { + if generic.is_game_done() { + return true; + } + if chrono::Utc::now().timestamp() >= self.game_end.load(std::sync::atomic::Ordering::Relaxed) { + generic.game_done(); + self.abort_timer_sync().await; + return true; + } 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); @@ -86,6 +115,7 @@ impl CustomGameLogic for EliminationLogic { ).await); } generic.game_done(); + self.abort_timer_sync().await; } true } @@ -142,6 +172,15 @@ impl CustomGameLogic for EliminationLogic { true } + async fn on_vehicle_self_destruct(&self, generic: &crate::matches::GenericGamemodeEngine, user: u8, is_classic: bool) -> bool { + if is_classic { + self.on_vehicle_destroyed(generic, u8::MAX, user).await + } else { + log::warn!("Received non-elimination self destruct in elimination game mode"); + false + } + } + async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { vec![ crate::matches::RlnlPacket { @@ -155,8 +194,48 @@ impl CustomGameLogic for EliminationLogic { 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 + data: Box::new(rlnl::events::GameTime(self.game_duration.as_millis() as f32 / 1000.0)), // FIXME use value from config }, ] } + + async fn on_countdown_start(&self, generic: &crate::matches::GenericGamemodeEngine, game_start: chrono::DateTime) -> bool { + let mut senders = Vec::new(); + for conn in generic.users.read().await.values() { + senders.push((conn.connection.clone(), conn.state.clone())); + } + let game_end = game_start + self.game_duration; + let teams = self.tracked.teams().await; + let extra_packets = teams.iter().map(|team| crate::matches::RlnlPacket { + event: rlnl::event_code::NetworkEvent::TeamBaseInitialise, + property: literustlib::packet::Property::ReliableOrdered, + data: Box::new(rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: *team, + current_progress: rlnl::types::ByteFloat::from(0.0), + max_progress: rlnl::types::ByteFloat::from(4.0), + }), + }).collect(); + let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, extra_packets); + let mut timer_lock = self.timer_task.lock().await; + if let Some(timer_t) = &*timer_lock { // this is quite unlikely (i.e. impossible), but I've done it for completeness + log::warn!("Aborting an existing timer task for elimination mode suggests an assumption was wrong"); + timer_t.abort(); + } + *timer_lock = Some(new_timer_task); + self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed); + true + } + + async fn on_game_completed(&self, _generic: &crate::matches::GenericGamemodeEngine) -> bool { + self.abort_timer_sync().await; + true + } + + async fn on_broadcast(&self, _generic: &crate::matches::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 { + true + } + + async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine, _motion: &rlnl::machine_motion::MachineMotion) -> bool { + true + } } diff --git a/rc_multiplayer/src/matches/modes/no_op.rs b/rc_multiplayer/src/matches/modes/no_op.rs index 82c963b..ed1bbf2 100644 --- a/rc_multiplayer/src/matches/modes/no_op.rs +++ b/rc_multiplayer/src/matches/modes/no_op.rs @@ -16,7 +16,27 @@ impl CustomGameLogic for NoOpLogic { true } + async fn on_vehicle_self_destruct(&self, _generic: &crate::matches::GenericGamemodeEngine, _user: u8, _is_classic: bool) -> bool { + true + } + async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { Vec::default() } + + async fn on_countdown_start(&self, _generic: &crate::matches::GenericGamemodeEngine, _game_start: chrono::DateTime) -> bool { + true + } + + async fn on_game_completed(&self, _generic: &crate::matches::GenericGamemodeEngine) -> bool { + true + } + + async fn on_broadcast(&self, _generic: &crate::matches::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 { + true + } + + async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine, _motion: &rlnl::machine_motion::MachineMotion) -> bool { + true + } } diff --git a/rc_multiplayer/src/matches/timer.rs b/rc_multiplayer/src/matches/timer.rs new file mode 100644 index 0000000..37629fa --- /dev/null +++ b/rc_multiplayer/src/matches/timer.rs @@ -0,0 +1,71 @@ +const SLEEP_PERIOD: std::time::Duration = std::time::Duration::from_millis(250); + +pub fn match_time_syncer(players: Vec<(super::generic::UserSender, std::sync::Arc)>, game_start: chrono::DateTime, game_end: chrono::DateTime, extra_packets: Vec) -> tokio::task::JoinHandle<()> { + tokio::spawn(do_match_timer_async(players, game_start, game_end, extra_packets)) +} + +pub fn time_to_game_end_payload(game_end: chrono::DateTime) -> rlnl::events::GameTime { + let now = chrono::Utc::now(); + let time_until_end = game_end.signed_duration_since(now); + let time_until_end_f32 = (time_until_end.num_milliseconds().clamp(0, i64::MAX) as f32) / 1000.0; + rlnl::events::GameTime(time_until_end_f32 * 4.0) +} + +async fn do_match_timer_async(players: Vec<(super::generic::UserSender, std::sync::Arc)>, game_start: chrono::DateTime, game_end: chrono::DateTime, extra_packets: Vec) { + let now = chrono::Utc::now(); + let time_until_start_ms = game_start.signed_duration_since(now).num_milliseconds().clamp(0, i64::MAX) + SLEEP_PERIOD.as_millis() as i64; + tokio::time::sleep(std::time::Duration::from_millis(time_until_start_ms as u64)).await; + let extras_count = extra_packets.len(); + for packet in extra_packets { + for player in players.iter() { + let mode = super::generic::ConnectionMode::from_u8(player.1.mode.load(std::sync::atomic::Ordering::Relaxed)); + if matches!(mode, super::generic::ConnectionMode::InGame) { + let sender = player.0.rlnl(); + crate::events::log_lnl_send_failure(sender.send_data( + &*packet.data, + packet.event, + packet.property, + &player.0.connection + ).await); + } + } + } + if extras_count != 0 { + log::info!("Broadcast {} custom packets for game start", extras_count); + } + 'timer_loop: loop { + let payload = time_to_game_end_payload(game_end); + 'player_loop: for player in players.iter() { + if !player.0.connection.is_connected() { continue 'player_loop; } + let sender = player.0.rlnl(); + if let Err(e) = sender.send_data( + &payload, + rlnl::event_code::NetworkEvent::CurrentGameTime, + literustlib::packet::Property::Unreliable, + &player.0.connection) + .await { + log::error!("Failed to send CurrentGameTime event to a user: {}", e); + } + } + if payload.0 > f32::EPSILON { + tokio::time::sleep(SLEEP_PERIOD).await; + } else { + break 'timer_loop; + } + } + let payload = rlnl::events::ingame::GameEnd { + reason: rlnl::types::GameEndReason::TimeOut, + }; + for player in players.iter() { + let sender = player.0.rlnl(); + if let Err(e) = sender.send_data( + &payload, + rlnl::event_code::NetworkEvent::EndGame, + literustlib::packet::Property::ReliableOrdered, + &player.0.connection) + .await { + log::error!("Failed to send EndGame event to a user: {}", e); + } + } + log::debug!("Game timer (a)sync thread has completed"); +} diff --git a/rc_multiplayer/src/vehicle_motion.rs b/rc_multiplayer/src/vehicle_motion.rs index 9b72aca..6237bdf 100644 --- a/rc_multiplayer/src/vehicle_motion.rs +++ b/rc_multiplayer/src/vehicle_motion.rs @@ -1,3 +1,5 @@ +use byteserde::prelude::ByteDeserializeSlice; + pub struct VehicleMotionHandler { msg_router: tokio::sync::mpsc::Sender, } @@ -18,10 +20,19 @@ impl VehicleMotionHandler { impl crate::RobotMotionHandler for VehicleMotionHandler { async fn handle(&self, data: &bytes::Bytes, user: &crate::UserData) { if let Some(user_info) = user.user().await { - crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::Motion { - user_id: user_info.user_id(), - data: data.to_owned(), - }).await); + let mut des = byteserde::des_slice::ByteDeserializerSlice::new(&data); + match rlnl::machine_motion::MachineMotion::byte_deserialize(&mut des) { + Ok(motion_data) => { + crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::Motion { + user_id: user_info.user_id(), + motion: motion_data, + }).await); + }, + Err(e) => { + log::error!("Bad deserialization for machine motion, bytes {:?}: {}", &data[..], e); + } + } + } else { log::error!("Failed to handle motion unknown user"); } diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 48b8897..a64c280 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -97,6 +97,7 @@ mod garage_slot_controls; mod garage_slot_set_customisations; mod garage_slot_name; mod garage_slot_copy; +mod steam_promo; use polariton_server::operations::OperationsHandler; @@ -219,4 +220,5 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(garage_slot_copy::garage_slot_copy_provider()) .add(polariton_server::operations::Ack::<12, _>::default()) // TODO handle UpdatePlayerDailyQuestProgressRequest instead of ignoring it .add(polariton_server::operations::Ack::<90, _>::default()) // TODO handle SubmitCRFRatingRequest instead of ignoring it + .add(steam_promo::steam_promos_provider()) } diff --git a/rc_services_room/src/operations/steam_promo.rs b/rc_services_room/src/operations/steam_promo.rs new file mode 100644 index 0000000..28f3157 --- /dev/null +++ b/rc_services_room/src/operations/steam_promo.rs @@ -0,0 +1,22 @@ +use polariton_server::operations::Immediate; +use polariton::operation::Typed; + +const CODE: u8 = 52; +const PROMO_LIST_PARAM_KEY: u8 = 63; // list of str +const STEAM_PROMOS_PARAM_KEY: u8 = 121; // string (json) +const CUBES_AWARDED_PARAM_KEY: u8 = 128; // string (json) +const PASS_AWARDED_PARAM_KEY: u8 = 4; // bool + +pub(super) fn steam_promos_provider() -> Immediate { + Immediate::new(|| { + let mut params = std::collections::HashMap::with_capacity(4); + params.insert(PROMO_LIST_PARAM_KEY, Typed::Arr(polariton::operation::Arr { + ty: polariton::serdes::TypePrefix::Str, + items: Vec::default(), + })); + params.insert(STEAM_PROMOS_PARAM_KEY, Typed::Str("{}".into())); + params.insert(CUBES_AWARDED_PARAM_KEY, Typed::Str("{}".into())); + params.insert(PASS_AWARDED_PARAM_KEY, Typed::Bool(false)); + params.into() + }) +} diff --git a/rc_social_room/src/operations/calculate_mmr.rs b/rc_social_room/src/operations/calculate_mmr.rs new file mode 100644 index 0000000..35863fd --- /dev/null +++ b/rc_social_room/src/operations/calculate_mmr.rs @@ -0,0 +1,13 @@ +use polariton_server::operations::Immediate; +use polariton::operation::Typed; + +const CODE: u8 = 60; +const MMR_PARAM_KEY: u8 = 77; // f64 + +pub(super) fn mmr_provider() -> Immediate { + Immediate::new(|| { + let mut params = std::collections::HashMap::with_capacity(1); + params.insert(MMR_PARAM_KEY, Typed::Double(1.0)); + params.into() + }) +} diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index c213029..f3d34d2 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -7,6 +7,7 @@ mod search_clan; mod season_rewards; mod previous_battle_rewards; mod platoon_data; +mod calculate_mmr; use polariton_server::operations::OperationsHandler; @@ -27,4 +28,5 @@ pub fn handler() -> OperationsHandler::default()) // AvatarUpdatedRequest, sent on services_room avatar_set success (just needs to be ack-ed; no params) + .add(calculate_mmr::mmr_provider()) }