From f2de67b33888daf355cf57ebbb1d47a4d1253d7c Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Tue, 9 Sep 2025 23:36:34 -0400 Subject: [PATCH] Add minimal Pit implementation (without win conditions) #34 --- rc_core/src/persist/combat.rs | 2 +- rc_lobby_room/src/lobby.rs | 12 +- rc_multiplayer/src/events/mod.rs | 8 +- rc_multiplayer/src/events/player_input.rs | 14 +- rc_multiplayer/src/matches/aggregate.rs | 21 +- rc_multiplayer/src/matches/engine.rs | 5 + rc_multiplayer/src/matches/generic.rs | 80 +++-- rc_multiplayer/src/matches/messages.rs | 5 + .../src/matches/modes/battle_arena.rs | 8 + .../src/matches/modes/elimination.rs | 15 +- rc_multiplayer/src/matches/modes/mod.rs | 3 + rc_multiplayer/src/matches/modes/no_op.rs | 8 + rc_multiplayer/src/matches/modes/pit.rs | 295 ++++++++++++++++++ 13 files changed, 427 insertions(+), 49 deletions(-) create mode 100644 rc_multiplayer/src/matches/modes/pit.rs diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 5219424..44f6736 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -378,7 +378,7 @@ fn default_rotation() -> GameEventSequence { multiplayer: GameEvent { map: GameMap::Neptune3, visibility: GameVisibility::Good, - mode: GameType::BattleArena, + mode: GameType::Pit, auto_heal: true, }, duration_s: 5*60, // 5 minutes diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index d63667c..e9a774f 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -59,13 +59,20 @@ impl QueueHandler { } } - async fn enter_match(&self, key: QueueKey, players: Vec, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { + async fn enter_match(&self, key: QueueKey, mut 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); chrono::Utc::now().timestamp_micros().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 team_picker = match key.mode { + oj_rc_core::data::game_mode::GameMode::Pit => |i| i as i32, // each player is on a different team + _ => |i| (i % 2) as i32, // alternate teams + }; + for (i, player) in players.iter_mut().enumerate() { + player.player.team = team_picker(i); + } let player_descs = players.iter().map(|x| oj_rc_core::persist::user::PlayerLobbyDescriptor { user_id: x.user_id, team: x.player.team, @@ -137,7 +144,7 @@ impl QueueHandler { }; match user.player_data(&self.cpu_counter).await { Ok(player_data) => { - let mut new_player = QueueUser { + let new_player = QueueUser { emitter: event_emitter, player: player_data, user_id: user.user_id(), @@ -185,7 +192,6 @@ impl QueueHandler { } } 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.len() } else { diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index 28dbe95..12c3921 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -4,7 +4,7 @@ mod all_loading_progress; mod weapon_select; mod activate_sync; mod loading_done; -//mod player_input; +mod player_input; mod spot_player; mod kill_player; mod client_unregister; @@ -25,14 +25,14 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa .add(weapon_select::handler(init_ctx)) .add(activate_sync::handler(init_ctx)) .add(loading_done::handler(init_ctx)) - //.add(player_input::handler(init_ctx)) - .add(crate::handlers::Broadcaster::< + .add(player_input::handler(init_ctx)) + /*.add(crate::handlers::Broadcaster::< true, {rlnl::event_code::NetworkEvent::OnPlayerInputChanged as i16}, {rlnl::event_code::NetworkEvent::OnServerReceivedInputChange as i16}, {literustlib::packet::Property::Unreliable as u8}, rlnl::events::ingame::PlayerIdAndInputData, - >::handler(init_ctx)) + >::handler(init_ctx))*/ .add(flipper_start::handler(init_ctx)) /*.add(crate::handlers::Broadcaster::< true, diff --git a/rc_multiplayer/src/events/player_input.rs b/rc_multiplayer/src/events/player_input.rs index db87f07..0221e4d 100644 --- a/rc_multiplayer/src/events/player_input.rs +++ b/rc_multiplayer/src/events/player_input.rs @@ -2,8 +2,8 @@ pub struct PlayerInput { msg_router: tokio::sync::mpsc::Sender, } -pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::simple_typed::SimpleRlnl { - crate::handlers::simple_typed::SimpleRlnl::new(PlayerInput::new(init_ctx)) +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::SimpleRlnl { + crate::handlers::SimpleRlnl::new(PlayerInput::new(init_ctx)) } impl PlayerInput { @@ -15,17 +15,15 @@ impl PlayerInput { } #[async_trait::async_trait] -impl crate::handlers::simple_typed::RlnlEventCodeHandler for PlayerInput { - type In = rlnl::events::ingame::MultiPlayerInputChanged; +impl crate::handlers::RlnlEventCodeHandler for PlayerInput { + type In = rlnl::events::ingame::PlayerIdAndInputData; const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::OnPlayerInputChanged; async fn handle(&self, data: Self::In, _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::BroadcastRlnl { + super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::PlayerInputChanged { user_id: user_info.user_id(), - event: rlnl::event_code::NetworkEvent::OnServerReceivedInputChange, - property: literustlib::packet::Property::Unreliable, - data: Some(Box::new(data)), + data, }).await); } else { log::error!("Failed to rebroadcast OnPlayerInputChanged for user (no auth!)"); diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index 4889b07..b2f49f6 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -113,9 +113,26 @@ impl GameMatches { fakes_handler, ); Ok(engine.spawn()) - } + }, + oj_rc_core::data::game_mode::GameMode::Pit => { + log::warn!("Game {}: Pit is experimental", guid); + let inner = super::modes::PitLogic::new( + &self.mode_configs.the_pit, + &map_config, + &players, + ); + let engine = super::GenericGamemodeEngine::new( + game_info, + map_config, + &self.mode_configs.battle_arena, + players, + inner, + fakes_handler, + ); + Ok(engine.spawn()) + }, mode => { - // TODO support mode gamemodes + // TODO support more gamemodes Err(oj_rc_core::persist::user::MultiplayerError { code: oj_rc_core::persist::user::MultiplayerErrorCode::CustomString, message: format!("Game mode {:?} is not supported (yet)", mode), diff --git a/rc_multiplayer/src/matches/engine.rs b/rc_multiplayer/src/matches/engine.rs index 2daa3ec..9065c8f 100644 --- a/rc_multiplayer/src/matches/engine.rs +++ b/rc_multiplayer/src/matches/engine.rs @@ -16,6 +16,8 @@ pub trait CustomGameLogic: Sized + Send + Sync + 'static { 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 when a kill bonus event is received after a confirmed kill + async fn on_kill_bonus(&self, generic: &super::GenericGamemodeEngine, killer: u8, victim: u8) -> bool; /// Called during loading, before the sync stage is entered. /// /// Roughly, loading is as follows: `Loading -> WaitingForSync -> Sync -> WaitingToStart -> InGame`. @@ -31,5 +33,8 @@ pub trait CustomGameLogic: Sized + Send + Sync + 'static { 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); + + /// Called when an enemy vehicle is spotted + async fn on_spot_vehicle(&self, generic: &super::GenericGamemodeEngine, user_id: i32, remote_player: u8) -> bool; } diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 872a9e8..9681fe1 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -317,6 +317,11 @@ impl GenericGamemodeEngine { ((now - game_start) as f32) / duration as f32 } + /// seconds since Unix epoch + pub(super) fn game_end(&self) -> i64 { + self.game_start.load(std::sync::atomic::Ordering::Relaxed) + self.game_duration.as_secs() as i64 + } + /*pub(super) async fn send_to_player(&self, player_id: u8, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T) { if let Some(player) = self.users.read().await.get(&player_id) { crate::events::log_lnl_send_failure(player.connection.rlnl().send_data( @@ -364,13 +369,7 @@ impl GenericGamemodeEngine { self.on_load_complete(user_id).await; }, super::GameMessage::SpotVehicle { user_id, remote_player } => { - self.rebroadcast( - user_id, - rlnl::event_code::NetworkEvent::RemoteEnemySpotted, - literustlib::packet::Property::ReliableOrdered, - &rlnl::events::ingame::PlayerId { player: remote_player }, - true - ).await; + self.on_spot_vehicle(user_id, remote_player).await; }, super::GameMessage::DestroyVehicle { user_id, remote_player, killer_player } => { self.on_destroy_vehicle(user_id, remote_player, killer_player).await; @@ -405,6 +404,9 @@ impl GenericGamemodeEngine { super::GameMessage::CustomLogicRlnl { user_id, event, property, data } => { self.custom_logic_handler.on_custom(&self, user_id, event, property, data).await; }, + super::GameMessage::PlayerInputChanged { user_id, data } => { + self.on_player_input_changed(user_id, data).await; + }, super::GameMessage::Motion { user_id, motion } => { self.on_motion(user_id, motion).await; }, @@ -706,6 +708,18 @@ impl GenericGamemodeEngine { } } + async fn on_spot_vehicle(&self, user_id: i32, remote_player: u8) { + if self.custom_logic_handler.on_spot_vehicle(self, user_id, remote_player).await { + self.rebroadcast( + user_id, + rlnl::event_code::NetworkEvent::RemoteEnemySpotted, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::PlayerId { player: remote_player }, + true + ).await; + } + } + async fn on_destroy_vehicle(&self, user_id: i32, remote_player: u8, @@ -798,24 +812,26 @@ impl GenericGamemodeEngine { shootee: u8, shooter: u8, ) { - if let Some(to_reward) = self.users.read().await.get(&shooter) { - to_reward.counters.kills.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - crate::events::log_lnl_send_failure(to_reward.connection.rlnl().send_data( - &rlnl::events::ingame::Kill { - killee_player_id: shootee, - killer_player_id: shooter, - }, - rlnl::event_code::NetworkEvent::ConfirmedKill, - literustlib::packet::Property::ReliableOrdered, - &to_reward.connection.connection - ).await); - let data = to_reward.counters.get_generic_packet(shooter, rlnl::types::IngameStatId::Kill, None); - self.broadcast( - rlnl::event_code::NetworkEvent::UpdateGameStats, - literustlib::packet::Property::ReliableOrdered, - &data, - true, - ).await; + if self.custom_logic_handler.on_kill_bonus(self, shooter, shootee).await { + if let Some(to_reward) = self.users.read().await.get(&shooter) { + to_reward.counters.kills.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + crate::events::log_lnl_send_failure(to_reward.connection.rlnl().send_data( + &rlnl::events::ingame::Kill { + killee_player_id: shootee, + killer_player_id: shooter, + }, + rlnl::event_code::NetworkEvent::ConfirmedKill, + literustlib::packet::Property::ReliableOrdered, + &to_reward.connection.connection + ).await); + let data = to_reward.counters.get_generic_packet(shooter, rlnl::types::IngameStatId::Kill, None); + self.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + } } } @@ -936,6 +952,20 @@ impl GenericGamemodeEngine { } } + async fn on_player_input_changed(&self, user_id: i32, input: rlnl::events::ingame::PlayerIdAndInputData) { + let full_payload = rlnl::events::ingame::MultiPlayerInputChanged { + num_players: 1, + changes: vec![input], + }; + self.rebroadcast( + user_id, + rlnl::event_code::NetworkEvent::OnServerReceivedInputChange, + literustlib::packet::Property::ReliableOrdered, + &full_payload, + true, + ).await; + } + async fn on_motion(&self, user_id: i32, motion: rlnl::machine_motion::MachineMotion) { //let (looking_at_x, looking_at_y, looking_at_z) = motion.target_point.clone().into(); //log::info!("Player {} looking at ({}, {}, {})", motion.player_id, looking_at_x, looking_at_y, looking_at_z); diff --git a/rc_multiplayer/src/matches/messages.rs b/rc_multiplayer/src/matches/messages.rs index ee99427..1cae394 100644 --- a/rc_multiplayer/src/matches/messages.rs +++ b/rc_multiplayer/src/matches/messages.rs @@ -90,6 +90,10 @@ pub enum GameMessage { property: literustlib::packet::Property, data: Box, }, + PlayerInputChanged { + user_id: i32, + data: rlnl::events::ingame::PlayerIdAndInputData, + }, Motion { user_id: i32, motion: rlnl::machine_motion::MachineMotion, @@ -122,6 +126,7 @@ impl GameMessage { Self::BroadcastRlnl { user_id, .. } => *user_id, Self::RebroadcastRlnl { skip_user_id, .. } => *skip_user_id, Self::CustomLogicRlnl { user_id, .. } => *user_id, + Self::PlayerInputChanged { user_id, .. } => *user_id, Self::Motion { user_id, .. } => *user_id, Self::NoOp => unreachable!("NoOp is irrelevant for user ID"), } diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index bd72586..d717c7d 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -1195,6 +1195,10 @@ impl CustomGameLogic for BattleArenaLogic { true } + async fn on_kill_bonus(&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![ Some(crate::matches::RlnlPacket { @@ -1591,4 +1595,8 @@ impl CustomGameLogic for BattleArenaLogic { _ => {} } } + + async fn on_spot_vehicle(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _remote_player: u8) -> bool { + true + } } diff --git a/rc_multiplayer/src/matches/modes/elimination.rs b/rc_multiplayer/src/matches/modes/elimination.rs index 3e76d2e..714d4e4 100644 --- a/rc_multiplayer/src/matches/modes/elimination.rs +++ b/rc_multiplayer/src/matches/modes/elimination.rs @@ -331,14 +331,11 @@ pub struct EliminationLogic { bases: BaseTracker, respawn_full_heal_duration: f32, respawn_heal_duration: f32, - game_end: std::sync::atomic::AtomicI64, timer_task: tokio::sync::Mutex>>, } impl EliminationLogic { pub fn new(config: &oj_rc_core::data::game_mode::GameModeConfig, map: &oj_rc_core::persist::config::MapConfig) -> Self { - let dur = std::time::Duration::from_secs((config.game_time_minutes as u64) * 60); - let fake_end = (chrono::Utc::now() + dur).timestamp(); Self { tracked: PlayerTracker { alive: tokio::sync::Mutex::new(std::collections::HashMap::new()), @@ -351,7 +348,6 @@ impl EliminationLogic { }, respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_heal_duration: config.respawn_heal_duration, - game_end: std::sync::atomic::AtomicI64::new(fake_end), timer_task: tokio::sync::Mutex::new(None), } } @@ -416,7 +412,7 @@ impl CustomGameLogic for EliminationLogic { if generic.is_game_done() { return true; } - if chrono::Utc::now().timestamp() >= self.game_end.load(std::sync::atomic::Ordering::Relaxed) { + if chrono::Utc::now().timestamp() >= generic.game_end() { generic.game_done(); self.abort_timer_sync().await; return true; @@ -466,6 +462,10 @@ impl CustomGameLogic for EliminationLogic { } } + async fn on_kill_bonus(&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![ crate::matches::RlnlPacket { @@ -518,7 +518,6 @@ impl CustomGameLogic for EliminationLogic { timer_t.abort(); } *timer_lock = Some(new_timer_task); - self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed); true } @@ -563,6 +562,10 @@ impl CustomGameLogic for EliminationLogic { } async fn on_custom(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _event: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: Box) {} + + async fn on_spot_vehicle(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _remote_player: u8) -> bool { + true + } } // spawn points (best guess) diff --git a/rc_multiplayer/src/matches/modes/mod.rs b/rc_multiplayer/src/matches/modes/mod.rs index 7984302..3da7825 100644 --- a/rc_multiplayer/src/matches/modes/mod.rs +++ b/rc_multiplayer/src/matches/modes/mod.rs @@ -8,4 +8,7 @@ pub use elimination::EliminationLogic; mod battle_arena; pub use battle_arena::BattleArenaLogic; +mod pit; +pub use pit::PitLogic; + mod trackers; diff --git a/rc_multiplayer/src/matches/modes/no_op.rs b/rc_multiplayer/src/matches/modes/no_op.rs index cf18ef5..541d28e 100644 --- a/rc_multiplayer/src/matches/modes/no_op.rs +++ b/rc_multiplayer/src/matches/modes/no_op.rs @@ -21,6 +21,10 @@ impl CustomGameLogic for NoOpLogic { true } + async fn on_kill_bonus(&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() } @@ -42,4 +46,8 @@ impl CustomGameLogic for NoOpLogic { } async fn on_custom(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _event: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: Box) {} + + async fn on_spot_vehicle(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _remote_player: u8) -> bool { + true + } } diff --git a/rc_multiplayer/src/matches/modes/pit.rs b/rc_multiplayer/src/matches/modes/pit.rs new file mode 100644 index 0000000..38495a2 --- /dev/null +++ b/rc_multiplayer/src/matches/modes/pit.rs @@ -0,0 +1,295 @@ +use crate::matches::CustomGameLogic; + +struct PlayerTracker { + streaks: std::collections::HashMap, + respawns: std::collections::HashMap, +} + +impl PlayerTracker { + fn new(players: &[oj_rc_core::persist::user::PlayerDescriptor]) -> Self { + Self { + streaks: players.iter().map(|p| (p.player_id, std::sync::atomic::AtomicU32::new(0))).collect(), + respawns: players.iter().map(|p| (p.player_id, std::sync::atomic::AtomicI64::new(0))).collect(), + } + } + + fn streak_leader(&self) -> Option { + let mut max = None; + for (player_id, streak) in self.streaks.iter() { + if let Some((_, cur_max_streak)) = max { + let streak = streak.load(std::sync::atomic::Ordering::Relaxed); + if streak > cur_max_streak { + max = Some((*player_id, streak)); + } + } else { + max = Some((*player_id, streak.load(std::sync::atomic::Ordering::Relaxed))); + } + } + max.map(|(player_id, _streak)| player_id) + } + + fn pit_stats(&self, users: &std::collections::HashMap) -> rlnl::events::ingame::PitModeState { + let mut player_stats = Vec::with_capacity(self.streaks.len()); + for (player_id, streak) in self.streaks.iter() { + if let Some(user) = users.get(player_id) { + player_stats.push(rlnl::types::PitPlayerStats { + player_id: *player_id, + score: user.counters.generic_score(), + streak: streak.load(std::sync::atomic::Ordering::Relaxed), + }); + } + } + rlnl::events::ingame::PitModeState { + num_scores: player_stats.len() as u8, + scores: player_stats, + leader_id: self.streak_leader().map(|x| x as i32).unwrap_or(-1), + } + } +} + +pub struct PitLogic { + respawn_full_heal_duration: f32, + respawn_heal_duration: f32, + player_tracking: PlayerTracker, + timer_task: tokio::sync::Mutex>>, +} + +impl PitLogic { + pub fn new(config: &oj_rc_core::data::game_mode::GameModeConfig, _map: &oj_rc_core::persist::config::MapConfig, players: &[oj_rc_core::persist::user::PlayerDescriptor]) -> Self { + PitLogic { + respawn_full_heal_duration: config.respawn_full_heal_duration, + respawn_heal_duration: config.respawn_heal_duration, + player_tracking: PlayerTracker::new(players), + 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 fn do_leader_update(&self, generic: &crate::matches::GenericGamemodeEngine, killer: u8, victim: u8) { + let leader = if let Some(leader_id) = self.player_tracking.streak_leader() { + leader_id + } else { + log::warn!("Pit game {} has no leader", generic.game_guid()); + return; + }; + let killer_score = if let Some(user) = generic.users.read().await.get(&killer) { + user.counters.generic_score() + } else { + log::warn!("Player {} score not found", killer); + return; + }; + let killer_streak = if let Some(streak) = self.player_tracking.streaks.get(&killer) { + streak.load(std::sync::atomic::Ordering::Relaxed) + } else { + log::warn!("Player {} streak not found", killer); + return; + }; + let payload = rlnl::events::ingame::UpdatePitScore { + player_id: killer, + score: killer_score, + streak: killer_streak, + destroyed_id: victim, + leader_id: leader, + }; + generic.broadcast( + rlnl::event_code::NetworkEvent::PitLeaderBoardUpdate, + literustlib::packet::Property::ReliableOrdered, + &payload, + true, + ).await; + } + + async fn do_leaderboard_update(&self, generic: &crate::matches::GenericGamemodeEngine) { + let latest_stats = self.player_tracking.pit_stats(&*generic.users.read().await); + generic.broadcast( + rlnl::event_code::NetworkEvent::PitModeState, + literustlib::packet::Property::ReliableOrdered, + &latest_stats, + true, + ).await; + } + + async fn do_selfdestruct_tasks(&self, generic: &crate::matches::GenericGamemodeEngine, player_id: u8) { + if let Some(player_streak) = self.player_tracking.streaks.get(&player_id) { + player_streak.store(0, std::sync::atomic::Ordering::Relaxed); + } + self.do_leader_update(generic, player_id, player_id).await; + } + + async fn do_kill_tasks(&self, generic: &crate::matches::GenericGamemodeEngine, killer: u8, victim: u8) { + if let Some(player_streak) = self.player_tracking.streaks.get(&victim) { + player_streak.store(0, std::sync::atomic::Ordering::Relaxed); + } + if let Some(player_streak) = self.player_tracking.streaks.get(&killer) { + player_streak.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + self.do_leader_update(generic, killer, victim).await; + } + + async fn do_respawn_tasks(&self, generic: &crate::matches::GenericGamemodeEngine, player_id: u8) { + log::info!("Handling respawn player {} in game {}", player_id, generic.game_guid()); + let respawn_time = std::time::Duration::from_secs(10); + let now = chrono::Utc::now(); + let respawn_timestamp = now + respawn_time; + if let Some(player_respawn) = self.player_tracking.respawns.get(&player_id) { + player_respawn.store(respawn_timestamp.timestamp(), std::sync::atomic::Ordering::Relaxed); + } + let respawn_payload = rlnl::events::ingame::RespawnTime { + owner: player_id, + waiting_time: 10, + }; + generic.broadcast( + rlnl::event_code::NetworkEvent::SetRespawnWaitingTime, + literustlib::packet::Property::ReliableOrdered, + &respawn_payload, + true + ).await; + let spawn_point = if let Some(team_spawns) = generic.map_config.spawns.get(&0) { + if team_spawns.is_empty() { + oj_rc_core::persist::config::Point { + x: 10.0 * (player_id as f32), + y: 100.0, + z: 10.0, + } + } else { + let index = (player_id as usize) % team_spawns.len(); + team_spawns[index].clone() + } + } else { + oj_rc_core::persist::config::Point { + x: 10.0 * (player_id as f32), + y: 100.0, + z: 10.0, + } + }; + let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); + tokio::task::spawn(Self::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); + } + + async fn respawn_player_after(after: chrono::DateTime, players: Vec, spawn: oj_rc_core::persist::config::Point, player_id: u8) { + let sleep_dur = after.signed_duration_since(chrono::Utc::now()).to_std().expect("Respawn duration too long to sleep"); + tokio::time::sleep(sleep_dur).await; + let spawn_payload = rlnl::events::sync::SpawnPoint { + pos: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3::from((spawn.x, spawn.y, spawn.z)), + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + owner: player_id, + }; + log::debug!("Respawning player {} after {}ms", player_id, sleep_dur.as_millis()); + for player in players { + if !player.connection.is_connected() { continue; } + crate::events::log_lnl_send_failure(player.rlnl().send_data( + &spawn_payload, + rlnl::event_code::NetworkEvent::FreeRespawnPoint, + literustlib::packet::Property::ReliableOrdered, + &player.connection, + ).await); + } + } +} + +#[async_trait::async_trait] +impl CustomGameLogic for PitLogic { + 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 { + self.do_kill_tasks(generic, killer, victim).await; + self.do_respawn_tasks(generic, victim).await; + true + } + + async fn on_vehicle_self_destruct(&self, generic: &crate::matches::GenericGamemodeEngine, user: u8, _is_classic: bool) -> bool { + self.do_selfdestruct_tasks(generic, user).await; + self.do_respawn_tasks(generic, user).await; + true + } + + async fn on_kill_bonus(&self, generic: &crate::matches::GenericGamemodeEngine, killer: u8, victim: u8) -> bool { + if let Some(to_reward) = generic.users.read().await.get(&killer) { + to_reward.counters.kills.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + crate::events::log_lnl_send_failure(to_reward.connection.rlnl().send_data( + &rlnl::events::ingame::Kill { + killee_player_id: victim, + killer_player_id: killer, + }, + rlnl::event_code::NetworkEvent::ConfirmedKill, + literustlib::packet::Property::ReliableOrdered, + &to_reward.connection.connection + ).await); + let data = to_reward.counters.get_generic_packet(killer, rlnl::types::IngameStatId::Kill, None); + generic.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + self.do_leaderboard_update(generic).await; + } + false + } + + 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 { + respawn_heal_duration: self.respawn_heal_duration, + respawn_full_heal_duration: self.respawn_full_heal_duration, + }), + }, + ] + } + + async fn on_countdown_start(&self, generic: &crate::matches::GenericGamemodeEngine, game_start: chrono::DateTime) -> bool { + let read_lock = generic.users.read().await; + let game_end = game_start + generic.game_duration; + let mut senders = Vec::with_capacity(read_lock.len()); + for conn in read_lock.values() { + senders.push((conn.connection.clone(), conn.state.clone())); + } + drop(read_lock); + let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, Vec::default(), Vec::default()); + 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 pit mode suggests an assumption was wrong"); + timer_t.abort(); + } + *timer_lock = Some(new_timer_task); + 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, _location: (f32, f32, f32)) -> bool { + true + } + + async fn on_custom(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _event: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: Box) {} + + async fn on_spot_vehicle(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _remote_player: u8) -> bool { + false + } +}