diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index e76d7b7..7b7a9f7 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -259,13 +259,13 @@ fn default_game_modes() -> GameModes { pit: GameMode { respawn_heal_duration: 20.0, respawn_full_heal_duration: 20.0, - kill_limit: 15, + kill_limit: 0, game_time_m: 15, }, team_deathmatch: GameMode { respawn_heal_duration: 10.0, respawn_full_heal_duration: 0.5, - kill_limit: 10, + kill_limit: 2, game_time_m: 10, }, } @@ -378,7 +378,7 @@ fn default_rotation() -> GameEventSequence { multiplayer: GameEvent { map: GameMap::Earth1, visibility: GameVisibility::Good, - mode: GameType::Pit, + mode: GameType::TeamDeathmatch, auto_heal: true, }, duration_s: 5*60, // 5 minutes @@ -515,6 +515,7 @@ fn default_multiplayer() -> super::MultiplayerConfig { fakes: super::multiplayer::default_fake_users(), battle_arena: super::multiplayer::default_ba_conf(), pit_config: super::multiplayer::default_pit_conf(), + team_death_match: super::multiplayer::default_tdm_conf(), } } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index febdb6a..747e0f2 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -476,4 +476,8 @@ impl super::ConfigProvider for CubeConfig { fn pit_settings(&self) -> super::PitSettings { self.battle.multiplayer.pit_config.clone().into() } + + fn tdm_settings(&self) -> super::TeamDeathMatchSettings { + self.battle.multiplayer.team_death_match.clone().into() + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index 76dcfe6..fedb2c4 100644 --- a/rc_core/src/persist/config/mod.rs +++ b/rc_core/src/persist/config/mod.rs @@ -2,7 +2,7 @@ mod cubes_json; pub use cubes_json::CubeConfig; mod traits; -pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition}; +pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings}; pub type ConfigImpl = CubeConfig; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 03a43ce..493b242 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -37,6 +37,7 @@ pub trait ConfigProvider { fn energy(&self) -> EnergyConfig; fn ba_settings(&self) -> BattleArenaResolver; fn pit_settings(&self) -> PitSettings; + fn tdm_settings(&self) -> TeamDeathMatchSettings; } pub struct CompleteCampaignProvider { @@ -445,3 +446,9 @@ pub enum PitWinCondition { Damage(u32), Time, } + +#[derive(Debug)] +pub struct TeamDeathMatchSettings { + pub respawn_time_seconds: u64, + pub self_destruct_is_kill: bool, +} diff --git a/rc_core/src/persist/multiplayer.rs b/rc_core/src/persist/multiplayer.rs index fef4b68..63c1311 100644 --- a/rc_core/src/persist/multiplayer.rs +++ b/rc_core/src/persist/multiplayer.rs @@ -12,6 +12,8 @@ pub struct MultiplayerConfig { pub battle_arena: BattleArenaConfig, #[serde(default = "default_pit_conf")] pub pit_config: PitConfig, + #[serde(default = "default_tdm_conf")] + pub team_death_match: TeamDeathMatchConfig, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -235,3 +237,25 @@ fn default_pit_win_conditions() -> Vec { PitWinCondition::Time, ] } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TeamDeathMatchConfig { + pub respawn_time_s: u64, + pub self_destruct_is_kill: bool, +} + +impl std::convert::From for crate::persist::config::TeamDeathMatchSettings { + fn from(value: TeamDeathMatchConfig) -> Self { + Self { + respawn_time_seconds: value.respawn_time_s, + self_destruct_is_kill: value.self_destruct_is_kill, + } + } +} + +pub(super) fn default_tdm_conf() -> TeamDeathMatchConfig { + TeamDeathMatchConfig { + respawn_time_s: default_respawn_time(), + self_destruct_is_kill: true, + } +} diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index d660504..bd0c2c5 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -246,6 +246,7 @@ mod _broadcast_impls { impl Broadcastable for rlnl::events::sync::FusionShieldState {} impl Broadcastable for rlnl::events::sync::EqualizerNotification {} impl Broadcastable for rlnl::events::sync::SpawnPoint {} + impl Broadcastable for rlnl::events::sync::UpdateTeamDeathmatchSettings {} impl Broadcastable for rlnl::events::GameTime {} impl Broadcastable for rlnl::events::ingame::TeamBaseState {} impl Broadcastable for rlnl::events::ingame::GameEnd {} diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index 7eaf7db..730baca 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -7,6 +7,7 @@ pub struct GameMatches { cube_parsers: std::sync::Arc, ba_settings: std::sync::Arc, pit_settings: std::sync::Arc, + tdm_settings: std::sync::Arc, factory: std::sync::Arc, } @@ -24,6 +25,7 @@ impl GameMatches { cube_parsers, ba_settings: std::sync::Arc::new(>::ba_settings(conf)), pit_settings: std::sync::Arc::new(>::pit_settings(conf)), + tdm_settings: std::sync::Arc::new(>::tdm_settings(conf)), factory, } } @@ -128,7 +130,25 @@ impl GameMatches { let engine = super::GenericGamemodeEngine::new( game_info, map_config, - &self.mode_configs.battle_arena, + &self.mode_configs.the_pit, + players, + inner, + fakes_handler, + ); + Ok(engine.spawn()) + }, + oj_rc_core::data::game_mode::GameMode::TeamDeathmatch => { + log::warn!("Game {}: Team Death Match is experimental", guid); + let inner = super::modes::TeamDeathMatchLogic::new( + &self.mode_configs.team_deathmatch, + &map_config, + &players, + self.tdm_settings.clone(), + ); + let engine = super::GenericGamemodeEngine::new( + game_info, + map_config, + &self.mode_configs.team_deathmatch, players, inner, fakes_handler, @@ -139,7 +159,7 @@ impl GameMatches { // 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), + message: format!("Game mode {:?} is not supported in multiplayer", mode), }) } } diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 89b65b6..3fffd04 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -225,7 +225,7 @@ impl GenericGamemodeEngine { users: tokio::sync::RwLock::new(std::collections::HashMap::new()), user_id_map: tokio::sync::RwLock::new(std::collections::HashMap::new()), is_complete: std::sync::atomic::AtomicBool::new(false), - game_start: std::sync::atomic::AtomicI64::new(-1), + game_start: std::sync::atomic::AtomicI64::new(i64::MIN), map_config: std::sync::Arc::new(map), game_descriptor: game, game_duration: std::time::Duration::from_secs((config.game_time_minutes as u64) * 60), @@ -322,6 +322,12 @@ impl GenericGamemodeEngine { self.game_start.load(std::sync::atomic::Ordering::Relaxed) + self.game_duration.as_secs() as i64 } + pub(super) fn is_game_past_end_time(&self) -> bool { + let game_start = self.game_start.load(std::sync::atomic::Ordering::Relaxed); + let game_end = self.game_end(); + game_start != i64::MIN && game_end <= chrono::Utc::now().timestamp() + } + /*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( @@ -1214,8 +1220,6 @@ impl GenericGamemodeEngine { literustlib::packet::Property::ReliableOrdered, &connection.connection) .await?;*/ - - // TODO Ok(()) } diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index ca33b48..5c6b61a 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -882,7 +882,6 @@ enum WinMode { } pub struct BattleArenaLogic { - game_end: std::sync::atomic::AtomicI64, respawn_full_heal_duration: f32, respawn_heal_duration: f32, timer_task: tokio::sync::Mutex>>, @@ -900,14 +899,11 @@ impl BattleArenaLogic { const CLASP_ID: u32 = 606866102; pub fn new(config: &oj_rc_core::data::game_mode::GameModeConfig, map: &oj_rc_core::persist::config::MapConfig, parsers: &oj_rc_core::cubes::CubeParsers, ba_config: oj_rc_core::data::battle_arena_config::BattleArenaData) -> Self { - let dur = std::time::Duration::from_secs((config.game_time_minutes as u64) * 60); - let fake_end = (chrono::Utc::now() + dur).timestamp(); let cube_parser = parsers.locations_of(); let crystals = cube_parser.locations_of_by_distance_to_first(&mut std::io::Cursor::new(&ba_config.base_machine_map), Self::CRYSTAL_ID, Self::CLASP_ID); Self { 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), player_tracking: PlayerTracker::new(), capture_tracking: PointTracker::new(map.capture_points.iter().map(|(_, speed)| *speed)), @@ -955,7 +951,7 @@ impl BattleArenaLogic { } async fn check_if_match_time_is_done(&self, generic: &crate::matches::GenericGamemodeEngine) -> bool { - if self.game_end.load(std::sync::atomic::Ordering::Relaxed) <= chrono::Utc::now().timestamp() { + if generic.is_game_past_end_time() { // find winning team let mut winning_team = None; for (base, tracking) in self.base_tracking.bases.iter() { @@ -1063,35 +1059,13 @@ impl BattleArenaLogic { z: 10.0 * (player_team as f32) + 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)); + let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); + tokio::task::spawn(super::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); } else { log::error!("Player {} cannot respawn because they are not in a team!?", 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 fn do_team_base_stealing(&self, cube_damage: &rlnl::events::ingame::DestroyCubeNoEffect, generic: &crate::matches::GenericGamemodeEngine, actual_damage_data: impl FnOnce(Vec) -> crate::matches::RlnlPacket) { let base_id = cube_damage.hit_machine_id as u8; let mut total_destroyed = 0; @@ -1175,6 +1149,9 @@ impl CustomGameLogic for BattleArenaLogic { } async fn on_player_end(&self, generic: &crate::matches::GenericGamemodeEngine, player: &crate::matches::generic::UserConnection) -> bool { + if generic.is_game_done() { + return true; + } let player_id = player.descriptor.player_id; self.do_destruct_tasks(generic, player_id).await; self.player_tracking.disconnect_player(player_id).await; @@ -1372,7 +1349,6 @@ impl CustomGameLogic for BattleArenaLogic { timer_t.abort(); } *timer_lock = Some(new_timer_task); - self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed); generic.broadcast( rlnl::event_code::NetworkEvent::SetSurrenderTimes, literustlib::packet::Property::ReliableOrdered, @@ -1466,7 +1442,7 @@ impl CustomGameLogic for BattleArenaLogic { async fn on_motion(&self, generic: &crate::matches::GenericGamemodeEngine, motion: &rlnl::machine_motion::MachineMotion, location: (f32, f32, f32)) -> bool { let game_start = generic.game_start.load(std::sync::atomic::Ordering::Relaxed); - if generic.game_start.load(std::sync::atomic::Ordering::Relaxed) == -1 || chrono::Utc::now().timestamp() < game_start { + if generic.game_start.load(std::sync::atomic::Ordering::Relaxed) == i64::MIN || chrono::Utc::now().timestamp() < game_start { // game is not in progress, ignore motion event log::debug!("Ignoring early motion event from player {}", motion.player_id); return true; @@ -1548,7 +1524,6 @@ impl CustomGameLogic for BattleArenaLogic { ).await; }, (rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => { - // TODO let maybe_init_surr = ::downcast_ref::(data.as_ref()); if let Some(init_surr) = maybe_init_surr { if let Some(team) = self.player_tracking.team(init_surr.player_id).await { @@ -1576,7 +1551,6 @@ impl CustomGameLogic for BattleArenaLogic { } } } - }, (rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest, literustlib::packet::Property::ReliableOrdered) => { let maybe_crystal_bonus = ::downcast_ref::(data.as_ref()); diff --git a/rc_multiplayer/src/matches/modes/elimination.rs b/rc_multiplayer/src/matches/modes/elimination.rs index 714d4e4..e28b01a 100644 --- a/rc_multiplayer/src/matches/modes/elimination.rs +++ b/rc_multiplayer/src/matches/modes/elimination.rs @@ -535,6 +535,11 @@ impl CustomGameLogic for EliminationLogic { self.abort_timer_sync().await; return true; } + if generic.is_game_past_end_time() { + generic.game_done(); + self.abort_timer_sync().await; + return true; + } if self.bases.is_baseless { return true; // don't bother trying to track whether players are in bases since there are no bases } diff --git a/rc_multiplayer/src/matches/modes/mod.rs b/rc_multiplayer/src/matches/modes/mod.rs index 3da7825..b176606 100644 --- a/rc_multiplayer/src/matches/modes/mod.rs +++ b/rc_multiplayer/src/matches/modes/mod.rs @@ -11,4 +11,29 @@ pub use battle_arena::BattleArenaLogic; mod pit; pub use pit::PitLogic; +mod team_death_match; +pub use team_death_match::TeamDeathMatchLogic; + mod trackers; + +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); + } +} diff --git a/rc_multiplayer/src/matches/modes/pit.rs b/rc_multiplayer/src/matches/modes/pit.rs index dca1b38..7d70ed4 100644 --- a/rc_multiplayer/src/matches/modes/pit.rs +++ b/rc_multiplayer/src/matches/modes/pit.rs @@ -325,29 +325,7 @@ impl PitLogic { ).await; let spawn_point = Self::choose_spawn_point(&generic.map_config, player_id).1; 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); - } + tokio::task::spawn(super::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); } } @@ -467,6 +445,11 @@ impl CustomGameLogic for PitLogic { self.abort_timer_sync().await; return true; } + if generic.is_game_past_end_time() { + generic.game_done(); + self.abort_timer_sync().await; + return true; + } self.win_tracking.tick(generic, self).await; true } diff --git a/rc_multiplayer/src/matches/modes/team_death_match.rs b/rc_multiplayer/src/matches/modes/team_death_match.rs new file mode 100644 index 0000000..d8329e6 --- /dev/null +++ b/rc_multiplayer/src/matches/modes/team_death_match.rs @@ -0,0 +1,452 @@ +use crate::matches::CustomGameLogic; + +struct ScoreTracker { + ticker: super::trackers::TickTracker<{Self::TICK_MS}>, + scores: std::collections::HashMap, // team -> score + teams: std::collections::HashMap, // player_id -> team, + self_destruct_is_kill: bool, + is_sudden_death: std::sync::atomic::AtomicBool, // triggered if time runs out and both sides are tied +} + +impl ScoreTracker { + const TICK_MS: i64 = 50; + + fn new(players: &[oj_rc_core::persist::user::PlayerDescriptor], self_destruct_is_kill: bool) -> Self { + Self { + ticker: super::trackers::TickTracker::new(), + scores: players.iter().map(|p| (p.player_id, std::sync::atomic::AtomicU32::new(0))).collect(), + teams: players.iter().map(|p| (p.player_id, p.team as u8)).collect(), + self_destruct_is_kill, + is_sudden_death: std::sync::atomic::AtomicBool::new(false), + } + } + + fn on_destruction(&self, killer: u8, victim: u8) { + if let Some(killer_team) = self.teams.get(&killer) { + if killer == victim { + if !self.self_destruct_is_kill { return; } + let other_teams: Vec = self.scores.keys().copied().filter(|x| x != killer_team).collect(); + let team_to_award = if other_teams.is_empty() { + log::warn!("Only one team in team death match, cannot award self-destruct point to other team"); + return; + } else if other_teams.len() == 1 { + other_teams[0] + } else { + use rand::Rng; + let index = rand::rng().random_range(0..other_teams.len()); + other_teams[index] + }; + self.scores[&team_to_award].fetch_add(1, std::sync::atomic::Ordering::Relaxed); + log::debug!("Team {} awarded 1 point", team_to_award); + } else { + self.scores[killer_team].fetch_add(1, std::sync::atomic::Ordering::Relaxed); + log::debug!("Team {} awarded 1 point", killer_team); + } + } + } + + fn leading_team(&self) -> Option { + let mut max = None; + for (team, score) in self.scores.iter() { + let score = score.load(std::sync::atomic::Ordering::Relaxed); + if let Some((_existing_team, existing_score)) = max { + if existing_score < score { + max = Some((*team, score)); + } + } else { + max = Some((*team, score)); + } + } + max.map(|(team, _score)| team) + } + + async fn update_team_scores(&self, generic: &crate::matches::GenericGamemodeEngine) { + let packet = rlnl::events::sync::UpdateTeamDeathMatch { + num_teams: self.scores.len() as i32, + team_scores: self.scores.iter().map(|(team, score)| rlnl::events::sync::TeamScore { + team_id: *team as i32, + score: score.load(std::sync::atomic::Ordering::Relaxed) as i32, + }).collect(), + time_expired: if generic.is_game_done() { 1 } else { 0 }, + }; + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamDeathMatchState, + literustlib::packet::Property::ReliableOrdered, + &packet, + true, + ).await; + } + + async fn do_timeout_win(&self, generic: &crate::matches::GenericGamemodeEngine) { + if let Some(winning_team) = self.leading_team() { + generic.game_done(); + let was_sudden_death = self.is_sudden_death.load(std::sync::atomic::Ordering::Relaxed); + let data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason: if was_sudden_death { rlnl::types::GameEndReason::TeamDeathMatchTimeExpiredSuddenDeath } else { rlnl::types::GameEndReason::TeamDeathMatchTimeExpiredMostKills }, + }; + 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::GameWonBaseDestroyed + } else { + rlnl::event_code::NetworkEvent::GameLostBaseDestroyed + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + } else { + self.is_sudden_death.store(true, std::sync::atomic::Ordering::Relaxed); + } + } + + async fn do_objective_win(&self, generic: &crate::matches::GenericGamemodeEngine, winning_team: u8) { + generic.game_done(); + let data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason: rlnl::types::GameEndReason::TeamDeathMatchMaxKillsAchieved, + }; + 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::GameWonBaseDestroyed + } else { + rlnl::event_code::NetworkEvent::GameLostBaseDestroyed + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + } + + async fn check_win(&self, generic: &crate::matches::GenericGamemodeEngine, kill_limit: u32) { + for (team, score) in self.scores.iter() { + if score.load(std::sync::atomic::Ordering::Relaxed) >= kill_limit { + self.do_objective_win(generic, *team).await; + break; + } + } + } + + async fn tick(&self, generic: &crate::matches::GenericGamemodeEngine, kill_limit: u32) { + let delta = self.ticker.tick(); + if delta == 0 { return; } + self.update_team_scores(generic).await; + self.check_win(generic, kill_limit).await; + } +} + +struct PlayerTracker { + respawns: std::collections::HashMap, + teams: std::collections::HashMap, // player_id -> team, + team_members: std::collections::HashMap>, // team -> set of player_id, +} + +impl PlayerTracker { + fn new(players: &[oj_rc_core::persist::user::PlayerDescriptor]) -> Self { + Self { + respawns: players.iter().map(|p| (p.player_id, std::sync::atomic::AtomicI64::new(i64::MIN))).collect(), + teams: players.iter().map(|p| (p.player_id, p.team as u8)).collect(), + team_members: Self::generate_team_members(players), + } + } + + fn generate_team_members(players: &[oj_rc_core::persist::user::PlayerDescriptor]) -> std::collections::HashMap> { + let mut team_map = std::collections::HashMap::>::new(); + for user in players.iter() { + if let Some(member_set) = team_map.get_mut(&(user.team as u8)) { + member_set.push(user.player_id); + } else { + let member_set = vec![user.player_id]; + team_map.insert(user.team as u8, member_set); + } + } + team_map + } + + async fn single_remaining_team(generic: &crate::matches::GenericGamemodeEngine) -> Option { + let mut first_remaining_team = None; + for conn in generic.users.read().await.values() { + let mode = crate::matches::generic::ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)); + if matches!(mode, crate::matches::generic::ConnectionMode::InGame) { + if let Some(first_remaining_team) = first_remaining_team { + if first_remaining_team != (conn.descriptor.team as u8) { + return None; + } + } else { + first_remaining_team = Some(conn.descriptor.team as u8); + } + } + } + first_remaining_team + } +} + +enum WinReason { + OutOfPlayers, + Surrender, +} + +#[allow(dead_code)] +pub struct TeamDeathMatchLogic { + respawn_heal_duration: f32, + respawn_full_heal_duration: f32, + kill_limit: u32, + settings: std::sync::Arc, + timer_task: tokio::sync::Mutex>>, + score_tracking: ScoreTracker, + player_tracking: PlayerTracker, + surrender_tracking: super::trackers::SurrenderGameTracker, +} + +impl TeamDeathMatchLogic { + 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], tdm_settings: std::sync::Arc) -> Self { + let self_destruct_is_kill = tdm_settings.self_destruct_is_kill; + TeamDeathMatchLogic { + respawn_heal_duration: config.respawn_heal_duration, + respawn_full_heal_duration: config.respawn_full_heal_duration, + kill_limit: config.kill_limit as u32, + settings: tdm_settings, + timer_task: tokio::sync::Mutex::new(None), + score_tracking: ScoreTracker::new(players, self_destruct_is_kill), + player_tracking: PlayerTracker::new(players), + surrender_tracking: super::trackers::SurrenderGameTracker::new(), + } + } + + 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_win(&self, reason: WinReason, generic: &crate::matches::GenericGamemodeEngine, winning_team: u8) { + generic.game_done(); + let end_reason = match reason { + WinReason::OutOfPlayers => rlnl::types::GameEndReason::OneTeamRemaining, + WinReason::Surrender => rlnl::types::GameEndReason::Surrendered, + }; + let data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason, + }; + 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::GameWonBaseDestroyed + } else { + rlnl::event_code::NetworkEvent::GameLostBaseDestroyed + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).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(self.settings.respawn_time_seconds as u64); + 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: self.settings.respawn_time_seconds as i16, + }; + generic.broadcast( + rlnl::event_code::NetworkEvent::SetRespawnWaitingTime, + literustlib::packet::Property::ReliableOrdered, + &respawn_payload, + true + ).await; + if let Some(user) = generic.users.read().await.get(&player_id) { + let player_team = user.descriptor.team as u8; + let spawn_point = if let Some(team_spawns) = generic.map_config.spawns.get(&player_team) { + if team_spawns.is_empty() { + oj_rc_core::persist::config::Point { + x: 10.0 * (player_id as f32), + y: 100.0, + z: 10.0 * (player_team as f32) + 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 * (player_team as f32) + 10.0, + } + }; + let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); + tokio::task::spawn(super::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); + } else { + log::error!("Player {} cannot respawn because they are not connected!?", player_id); + } + } +} + +#[async_trait::async_trait] +impl CustomGameLogic for TeamDeathMatchLogic { + 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 { + if generic.is_game_done() { + return true; + } + if let Some(winning_team) = PlayerTracker::single_remaining_team(generic).await { + self.do_win(WinReason::OutOfPlayers, generic, winning_team).await; + } + true + } + + async fn on_vehicle_destroyed(&self, generic: &crate::matches::GenericGamemodeEngine, killer: u8, victim: u8) -> bool { + self.do_respawn_tasks(generic, victim).await; + self.score_tracking.on_destruction(killer, victim); + true + } + + async fn on_vehicle_self_destruct(&self, generic: &crate::matches::GenericGamemodeEngine, user: u8, _is_classic: bool) -> bool { + self.do_respawn_tasks(generic, user).await; + self.score_tracking.on_destruction(user, user); + 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![ + crate::matches::RlnlPacket { + event: rlnl::event_code::NetworkEvent::GameModeSettings, + property: literustlib::packet::Property::ReliableOrdered, + data: Box::new(rlnl::events::sync::UpdateTeamDeathmatchSettings { + settings: rlnl::types::GameModeSettings { + game_time_minutes: (generic.game_duration.as_secs() / 60) as i32, + kill_limit: self.kill_limit as i32, + 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 end_packets = vec![ + crate::matches::RlnlPacket { + event: rlnl::event_code::NetworkEvent::EndGame, + property: literustlib::packet::Property::ReliableOrdered, + data: Box::new(rlnl::events::ingame::GameEnd { + reason: rlnl::types::GameEndReason::TimeOut, + }), + } + ];*/ + 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 team death match mode suggests an assumption was wrong"); + timer_t.abort(); + } + *timer_lock = Some(new_timer_task); + generic.broadcast( + rlnl::event_code::NetworkEvent::SetSurrenderTimes, + literustlib::packet::Property::ReliableOrdered, + &super::trackers::SurrenderGameTracker::surrender_times(), + false, + ).await; + 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 { + if generic.is_game_done() { + self.abort_timer_sync().await; + return true; + } + if generic.is_game_past_end_time() { + self.score_tracking.do_timeout_win(generic).await; + self.abort_timer_sync().await; + return true; + } + self.score_tracking.tick(generic, self.kill_limit).await; + // handle surrender vote tick + self.surrender_tracking.tick(generic).await; + true + } + + async fn on_custom(&self, generic: &crate::matches::GenericGamemodeEngine, user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box) { + match (event, property) { + (rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => { + let maybe_init_surr = ::downcast_ref::(data.as_ref()); + if let Some(init_surr) = maybe_init_surr { + if let Some(&team) = self.player_tracking.teams.get(&init_surr.player_id) { + let team_members = self.player_tracking.team_members[&team].clone(); + let result = self.surrender_tracking.request_new(team, init_surr.player_id, team_members.into_iter(), generic).await; + if matches!(result, super::trackers::SurrenderVoteResult::Succeeded) { + let winning_team = if team == 0 { 1 } else { 0 }; + self.do_win(WinReason::Surrender, generic, winning_team).await; + } + } + } else { + log::warn!("Bad SurrenderRequest data"); + } + }, + (rlnl::event_code::NetworkEvent::SurrenderVoteCast, literustlib::packet::Property::ReliableOrdered) => { + if let Some(player_id) = generic.user_key_by_user_id(user_id).await { + if let Some(&team) = self.player_tracking.teams.get(&player_id) { + let maybe_vote = ::downcast_ref::(data.as_ref()); + if let Some(vote) = maybe_vote { + let result = self.surrender_tracking.vote(team, player_id, vote.surrender != 0, generic).await; + if matches!(result, super::trackers::SurrenderVoteResult::Succeeded) { + let winning_team = if team == 0 { 1 } else { 0 }; + self.do_win(WinReason::Surrender, generic, winning_team).await; + } + } else { + log::warn!("Bad SurrenderVoteCast data"); + } + } + } + }, + _ => {} + } + } + + async fn on_spot_vehicle(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _remote_player: u8) -> bool { + true + } +}