diff --git a/rc_chat_room/src/state/chat/config.rs b/rc_chat_room/src/state/chat/config.rs index e3a1231..ecc2869 100644 --- a/rc_chat_room/src/state/chat/config.rs +++ b/rc_chat_room/src/state/chat/config.rs @@ -121,7 +121,7 @@ impl BuiltIn { } } - fn prettify_re<'a>(regex: &'a str) -> &'a str { + fn prettify_re(regex: &str) -> &str { regex.trim_start_matches("\\") } diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 44f6736..53b2fcc 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -514,6 +514,7 @@ fn default_multiplayer() -> super::MultiplayerConfig { network: super::multiplayer::default_net_conf(), fakes: super::multiplayer::default_fake_users(), battle_arena: super::multiplayer::default_ba_conf(), + pit_config: super::multiplayer::default_pit_conf(), } } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 2eb16d9..10606a4 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -462,4 +462,8 @@ impl super::ConfigProvider for CubeConfig { data: self.battle.multiplayer.battle_arena.clone(), } } + + fn pit_settings(&self) -> super::PitSettings { + self.battle.multiplayer.pit_config.clone().into() + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index 8b7c5c0..76dcfe6 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}; +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 type ConfigImpl = CubeConfig; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 8c6e219..8eca126 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -36,6 +36,7 @@ pub trait ConfigProvider { fn fake_players(&self) -> Vec; fn energy(&self) -> EnergyConfig; fn ba_settings(&self) -> BattleArenaResolver; + fn pit_settings(&self) -> PitSettings; } pub struct CompleteCampaignProvider { @@ -428,3 +429,18 @@ impl BattleArenaResolver { })) } } + +#[derive(Debug)] +pub struct PitSettings { + pub wins: Vec, + pub respawn_time_seconds: u64, +} + +#[derive(Debug)] +pub enum PitWinCondition { + StreakKills(u32), + TotalKills(u32), + Score(u32), + Damage(u32), + Time, +} diff --git a/rc_core/src/persist/multiplayer.rs b/rc_core/src/persist/multiplayer.rs index 5a4663f..fef4b68 100644 --- a/rc_core/src/persist/multiplayer.rs +++ b/rc_core/src/persist/multiplayer.rs @@ -10,6 +10,8 @@ pub struct MultiplayerConfig { pub fakes: Vec, #[serde(default = "default_ba_conf")] pub battle_arena: BattleArenaConfig, + #[serde(default = "default_pit_conf")] + pub pit_config: PitConfig, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -169,3 +171,67 @@ fn default_equalizer_duration() -> u64 { fn default_segments() -> u16 { 3 } + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum PitWinCondition { + StreakKills { + streak: u32, + }, + TotalKills { + kills: u32, + }, + TotalScore { + score: u32, + }, + TotalDamage { + damage: u32, + }, + Time, +} + +impl std::convert::From for crate::persist::config::PitWinCondition { + fn from(value: PitWinCondition) -> Self { + match value { + PitWinCondition::StreakKills { streak } => crate::persist::config::PitWinCondition::StreakKills(streak), + PitWinCondition::TotalKills { kills } => crate::persist::config::PitWinCondition::TotalKills(kills), + PitWinCondition::TotalScore { score } => crate::persist::config::PitWinCondition::Score(score), + PitWinCondition::TotalDamage { damage } => crate::persist::config::PitWinCondition::Damage(damage), + PitWinCondition::Time => crate::persist::config::PitWinCondition::Time, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PitConfig { + pub wins: Vec, + pub respawn_time_s: u64, +} + +impl std::convert::From for crate::persist::config::PitSettings { + fn from(value: PitConfig) -> Self { + Self { + wins: value.wins.into_iter().map(|x| x.into()).collect(), + respawn_time_seconds: value.respawn_time_s, + } + } +} + +pub(super) fn default_pit_conf() -> PitConfig { + PitConfig { + wins: default_pit_win_conditions(), + respawn_time_s: default_respawn_time(), + } +} + +fn default_pit_win_conditions() -> Vec { + vec![ + PitWinCondition::StreakKills { + streak: 2, + }, + PitWinCondition::TotalKills { + kills: 5, + }, + PitWinCondition::Time, + ] +} diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index b2f49f6..49333c8 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -6,6 +6,7 @@ pub struct GameMatches { fake_players: Vec, cube_parsers: std::sync::Arc, ba_settings: std::sync::Arc, + pit_settings: std::sync::Arc, factory: std::sync::Arc, } @@ -22,6 +23,7 @@ impl GameMatches { fake_players: >::fake_players(conf), cube_parsers, ba_settings: std::sync::Arc::new(>::ba_settings(conf)), + pit_settings: std::sync::Arc::new(>::pit_settings(conf)), factory, } } @@ -120,6 +122,7 @@ impl GameMatches { &self.mode_configs.the_pit, &map_config, &players, + self.pit_settings.clone(), ); let engine = super::GenericGamemodeEngine::new( game_info, diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index d717c7d..ca33b48 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -72,7 +72,7 @@ impl PlayerTracker { async fn players_on_team(&self, team: u8) -> Vec { if let Some(members) = self.connected.lock().await.get(&team) { - members.iter().map(|x| *x).collect() + members.iter().copied().collect() } else { Vec::default() } @@ -434,12 +434,12 @@ impl EqualizerTracker { || ba_config.equalizer_model.is_empty() //|| ba_config.equalizer_trigger_time_seconds.is_empty() || ba_config.equalizer_duration_seconds.is_empty() - || ba_config.equalizer_duration_seconds.iter().any(|&x| x == 0); + || ba_config.equalizer_duration_seconds.contains(&0); if is_disabled { log::info!("Battle Arena equalizer is disabled by config (model ok? {}, health ok? {}, duration ok? {})", !ba_config.equalizer_model.is_empty(), ba_config.equalizer_health > 0, - !ba_config.equalizer_duration_seconds.is_empty() && !ba_config.equalizer_duration_seconds.iter().any(|&x| x == 0) + !ba_config.equalizer_duration_seconds.is_empty() && !ba_config.equalizer_duration_seconds.contains(&0) ); } Self { @@ -704,6 +704,7 @@ impl BaseTracker { // undo cube_index update tracked_base.cube_index.fetch_sub(increment, std::sync::atomic::Ordering::SeqCst); log::debug!("Skipping increment in favour of healing damaged/destroyed cube"); + #[allow(clippy::unnecessary_unwrap)] // have you seen the mess this would be with another if statement? let first_damaged = first_damaged.unwrap(); let healing = crystal_health - tracked_base.calculate_crystal_health(first_damaged, crystal_health); tracked_base.crystals_healths[first_damaged].store(u8::MAX, std::sync::atomic::Ordering::Relaxed); diff --git a/rc_multiplayer/src/matches/modes/pit.rs b/rc_multiplayer/src/matches/modes/pit.rs index 38495a2..6c009ee 100644 --- a/rc_multiplayer/src/matches/modes/pit.rs +++ b/rc_multiplayer/src/matches/modes/pit.rs @@ -1,5 +1,97 @@ use crate::matches::CustomGameLogic; +struct WinTracker { + ticker: super::trackers::TickTracker<{Self::TICK_MS}>, +} + +impl WinTracker { + const TICK_MS: i64 = 50; + + fn new() -> Self { + Self { + ticker: super::trackers::TickTracker::new(), + } + } + + async fn do_win(generic: &crate::matches::GenericGamemodeEngine, game: &PitLogic, winning_team: u8) { + generic.game_done(); + game.abort_timer_sync().await; + let data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason: rlnl::types::GameEndReason::PitMaxKillsAchieved, + }; + let winning_team_i32 = winning_team as i32; + for conn in generic.users.read().await.values() { + let event = if conn.descriptor.team == winning_team_i32 { + rlnl::event_code::NetworkEvent::GameWon + } else { + rlnl::event_code::NetworkEvent::GameLost + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + } + + async fn check_win(generic: &crate::matches::GenericGamemodeEngine, game: &PitLogic) { + for win_condition in game.settings.wins.iter() { + match win_condition { + oj_rc_core::persist::config::PitWinCondition::StreakKills(streak_threshold) => { + for (player_id, streak) in game.player_tracking.streaks.iter() { + let player_streak = streak.load(std::sync::atomic::Ordering::Relaxed); + if player_streak >= *streak_threshold { + if let Some(conn) = generic.users.read().await.get(player_id) { + log::info!("Player {} has reached the streak win condition in game {}", player_id, generic.game_guid()); + Self::do_win(generic, game, conn.descriptor.team as u8).await; + break; + } else { + log::warn!("Player {} has a winning kill streak but is not in game {}", player_id, generic.game_guid()); + } + } + } + }, + oj_rc_core::persist::config::PitWinCondition::TotalKills(kills_threshold) => { + for (player_id, conn) in generic.users.read().await.iter() { + if conn.counters.kills.load(std::sync::atomic::Ordering::Relaxed) >= *kills_threshold { + log::info!("Player {} has reached the total kills win condition in game {}", player_id, generic.game_guid()); + Self::do_win(generic, game, conn.descriptor.team as u8).await; + break; + } + } + }, + oj_rc_core::persist::config::PitWinCondition::Score(score_threshold) => { + for (player_id, conn) in generic.users.read().await.iter() { + if conn.counters.generic_score() >= *score_threshold { + log::info!("Player {} has reached the total score win condition in game {}", player_id, generic.game_guid()); + Self::do_win(generic, game, conn.descriptor.team as u8).await; + break; + } + } + }, + oj_rc_core::persist::config::PitWinCondition::Damage(dmg_threshold) => { + for (player_id, conn) in generic.users.read().await.iter() { + if conn.counters.cubes.load(std::sync::atomic::Ordering::Relaxed) >= *dmg_threshold { + log::info!("Player {} has reached the total damage win condition in game {}", player_id, generic.game_guid()); + Self::do_win(generic, game, conn.descriptor.team as u8).await; + break; + } + } + }, + oj_rc_core::persist::config::PitWinCondition::Time => { /* handled elsewhere */}, + } + } + } + + async fn tick(&self, generic: &crate::matches::GenericGamemodeEngine, game: &PitLogic) { + let delta = self.ticker.tick(); + if delta == 0 { return; } + Self::check_win(generic, game).await; + } +} + struct PlayerTracker { streaks: std::collections::HashMap, respawns: std::collections::HashMap, @@ -51,16 +143,20 @@ pub struct PitLogic { respawn_full_heal_duration: f32, respawn_heal_duration: f32, player_tracking: PlayerTracker, + win_tracking: WinTracker, + settings: std::sync::Arc, 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 { + 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], pit_settings: std::sync::Arc) -> Self { PitLogic { respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_heal_duration: config.respawn_heal_duration, player_tracking: PlayerTracker::new(players), + win_tracking: WinTracker::new(), timer_task: tokio::sync::Mutex::new(None), + settings: pit_settings, } } @@ -136,7 +232,7 @@ impl PitLogic { 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 respawn_time = std::time::Duration::from_secs(self.settings.respawn_time_seconds); let now = chrono::Utc::now(); let respawn_timestamp = now + respawn_time; if let Some(player_respawn) = self.player_tracking.respawns.get(&player_id) { @@ -144,7 +240,7 @@ impl PitLogic { } let respawn_payload = rlnl::events::ingame::RespawnTime { owner: player_id, - waiting_time: 10, + waiting_time: self.settings.respawn_time_seconds as i16, }; generic.broadcast( rlnl::event_code::NetworkEvent::SetRespawnWaitingTime, @@ -152,6 +248,7 @@ impl PitLogic { &respawn_payload, true ).await; + // FIXME use full map spawns instead of team base spawns 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 { @@ -264,7 +361,20 @@ impl CustomGameLogic for PitLogic { 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 end_packets = if self.settings.wins.iter().any(|cond| matches!(cond, oj_rc_core::persist::config::PitWinCondition::Time)) { + 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, + }), + } + ] + } else { + Vec::default() + }; + let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, Vec::default(), end_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 pit mode suggests an assumption was wrong"); @@ -283,7 +393,12 @@ impl CustomGameLogic for PitLogic { true } - async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine, _motion: &rlnl::machine_motion::MachineMotion, _location: (f32, f32, f32)) -> bool { + 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; + } + self.win_tracking.tick(generic, self).await; true } diff --git a/rc_multiplayer/src/matches/modes/trackers/surrender.rs b/rc_multiplayer/src/matches/modes/trackers/surrender.rs index e664283..1281465 100644 --- a/rc_multiplayer/src/matches/modes/trackers/surrender.rs +++ b/rc_multiplayer/src/matches/modes/trackers/surrender.rs @@ -71,7 +71,7 @@ impl SurrenderTracker { let threshold = self.players.len() / 2; let declined = if total_no_votes > threshold { Some(rlnl::events::ingame::SurrenderDeclined { - surrendering_player_id: player_id.or_else(|| self.players.keys().next().map(|x| *x)).unwrap_or(0) as i32, + surrendering_player_id: player_id.or_else(|| self.players.keys().next().copied()).unwrap_or(0) as i32, game_time_elapsed, }) } else { @@ -148,7 +148,7 @@ impl SurrenderGameTracker { }, _ => {}, } - return result; + result } pub async fn request_new(&self, team: u8, initiator: u8, players: impl std::iter::Iterator, generic: &crate::matches::GenericGamemodeEngine) -> SurrenderVoteResult { diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index 8655589..b05eb0e 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -30,4 +30,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()) .add(polariton_server::operations::Ack::<25, _>::default()) // save social settings, sent on escape menu settings save (should probably be saved someday...) + .add(polariton_server::operations::Ack::<0, _>::default()) // send friend request, can be sent from match leaderboard }