diff --git a/Cargo.lock b/Cargo.lock index 59ab4aa..45f4f38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2476,6 +2476,16 @@ dependencies = [ "num-traits 0.2.19", ] +[[package]] +name = "num-quaternion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d7fa2cef9166a2dcb9f1aa92e0ce2c6ddef7d664cc6d4af93e7f210bfb8a19b" +dependencies = [ + "num-integer", + "num-traits 0.2.19", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -3350,6 +3360,7 @@ dependencies = [ "literustlib", "literustlib_server", "log", + "num-quaternion", "oj_rc_core", "rlnl", "tokio", diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index e4a128b..0f3f30e 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -391,14 +391,14 @@ impl super::ConfigProvider for CubeConfig { spawns.insert(point.team, list); } } - let bases = conf.bases.iter().map(|base| (base.team, super::Sphere { + let bases = conf.bases.iter().map(|base| (base.team, (super::Sphere { radius: base.radius, center: super::Point { x: base.x, y: base.y, z: base.z, }, - })).collect(); + }, base.percent_per_second))).collect(); let map_conf = super::MapConfig { spawns, bases, diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index b44d46f..4b71c41 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -347,5 +347,5 @@ pub struct Sphere { #[derive(Clone, Debug)] pub struct MapConfig { pub spawns: std::collections::HashMap>, // team -> points - pub bases: std::collections::HashMap, // team -> base + pub bases: std::collections::HashMap, // team -> base } diff --git a/rc_core/src/persist/maps.rs b/rc_core/src/persist/maps.rs index d498287..e567a6d 100644 --- a/rc_core/src/persist/maps.rs +++ b/rc_core/src/persist/maps.rs @@ -27,12 +27,13 @@ pub struct CaptureBase { pub y: f32, pub z: f32, pub radius: f32, + pub percent_per_second: f32, } pub(super) fn default_map() -> std::collections::HashMap { let mut map = std::collections::HashMap::with_capacity(9); - let coords_t0 = corner_to_center((6.60, 4.09, 20.3), 10.0); - let coords_t1 = corner_to_center((364.60, 10.63, 372.20), 10.0); + //let coords_t0 = corner_to_center((6.60, 4.09, 20.3), 10.0); + //let coords_t1 = corner_to_center((364.60, 10.63, 372.20), 10.0); map.insert(super::combat::GameMap::Mars1, MapConfig { spawn_points: vec![ // team 0 @@ -161,17 +162,19 @@ pub(super) fn default_map() -> std::collections::HashMap std::collections::HashMap (f32, f32, f32) { +/*const fn corner_to_center(corner: (f32, f32, f32), radius: f32) -> (f32, f32, f32) { (corner.0 + radius, corner.1, corner.2 + radius) -} +}*/ diff --git a/rc_multiplayer/Cargo.toml b/rc_multiplayer/Cargo.toml index 30dc702..27662c7 100644 --- a/rc_multiplayer/Cargo.toml +++ b/rc_multiplayer/Cargo.toml @@ -18,6 +18,7 @@ bytes = "1.10" byteserde = "0.6.2" chrono.workspace = true atomic_float = "1.1" +num-quaternion = "1.0" literustlib_server = { version = "0.1", path = "../../LiteRustLib/server" } literustlib = { version = "0.1", path = "../../LiteRustLib" } diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index ef6e1f7..c93b62c 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -45,11 +45,12 @@ impl GameMatches { } match game_info.mode { oj_rc_core::data::game_mode::GameMode::SuddenDeath => { + let inner = super::modes::EliminationLogic::new(&self.mode_configs.elimination, &map_config); let engine = super::GenericGamemodeEngine::new( game_info, map_config, players, - super::modes::EliminationLogic::new(&self.mode_configs.elimination) + inner, ); Ok(engine.spawn()) } diff --git a/rc_multiplayer/src/matches/engine.rs b/rc_multiplayer/src/matches/engine.rs index bcfc44a..1ea7259 100644 --- a/rc_multiplayer/src/matches/engine.rs +++ b/rc_multiplayer/src/matches/engine.rs @@ -16,6 +16,6 @@ pub trait CustomGameLogic: Sized + Send + Sync + 'static { 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; + async fn on_motion(&self, generic: &super::GenericGamemodeEngine, motion: &rlnl::machine_motion::MachineMotion, location: (f32, f32, f32)) -> bool; } diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 649da9a..6aee65a 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -554,31 +554,38 @@ impl GenericGamemodeEngine { } }, 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); + let (x, y, z) = motion.rb_state.rb_pos_rot.pos.into(); + let (x2, y2, z2) = motion.rb_state.center_of_mass.into(); + let (w3, x3, y3, z3) = motion.rb_state.rb_pos_rot.rot.into(); + let quat = num_quaternion::Quaternion::new(w3, x3, y3, z3); + if let Some(unit_quat) = quat.normalize() { + let coords = unit_quat.rotate_vector([x2, y2, z2]); + let (x4, y4, z4) = (x + coords[0], y + coords[1], z + coords[2]); + //log::debug!("Player {} world CoM is at (x, y, z) ({}, {}, {})", motion.player_id, x4, y4, z4); + if self.custom_logic_handler.on_motion(&self, &motion, (x4, y4, z4)).await { + if let Some(conn) = self.users.read().await.get(&motion.player_id) { + conn.machine.location.x.store(x4, std::sync::atomic::Ordering::Relaxed); + conn.machine.location.y.store(y4, std::sync::atomic::Ordering::Relaxed); + conn.machine.location.z.store(z4, std::sync::atomic::Ordering::Relaxed); + 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); } - } else { - log::warn!("Received machine motion with unknown player id {} from user {}", motion.player_id, user_id); } } }, @@ -798,4 +805,15 @@ impl GenericGamemodeEngine { pub(super) fn is_game_done(&self) -> bool { self.is_complete.load(std::sync::atomic::Ordering::SeqCst) } + + #[inline] + pub(super) fn is_in(loc: &(f32, f32, f32), sphere: &oj_rc_core::persist::config::Sphere) -> bool { + let distance = ( + (loc.0 - sphere.center.x).powi(2) + + (loc.1 - sphere.center.y).powi(2) + + (loc.2 - sphere.center.z).powi(2) + ).sqrt(); + //log::info!("{} away from sphere", distance); + distance < sphere.radius + } } diff --git a/rc_multiplayer/src/matches/modes/elimination.rs b/rc_multiplayer/src/matches/modes/elimination.rs index 38d45ba..4dfcca6 100644 --- a/rc_multiplayer/src/matches/modes/elimination.rs +++ b/rc_multiplayer/src/matches/modes/elimination.rs @@ -2,6 +2,7 @@ use crate::matches::CustomGameLogic; struct PlayerTracker { alive: tokio::sync::Mutex>>, // team -> set of player_id + in_base: tokio::sync::RwLock>, // player_id -> in base state (if base > u8::MAX then not in a base) } impl PlayerTracker { @@ -14,6 +15,7 @@ impl PlayerTracker { new_team.insert(player.player_id); alive_lock.insert(player.team as u8, new_team); } + self.in_base.write().await.insert(player.player_id, std::sync::atomic::AtomicU16::new(u16::MAX)); } async fn destroy_vehicle(&self, player: &oj_rc_core::persist::user::PlayerDescriptor) { @@ -52,13 +54,291 @@ impl PlayerTracker { alive_players } - async fn teams(&self) -> std::collections::HashSet { + /*async fn teams(&self) -> std::collections::HashSet { self.alive.lock().await.keys().map(|x| *x).collect() + }*/ + + async fn player_team(&self, player_id: u8) -> Option { + for (team, players) in self.alive.lock().await.iter() { + if players.contains(&player_id) { + return Some(*team); + } + } + None + } + + async fn swap_is_in_base(&self, player_id: u8, base: Option) -> Option { + self.in_base.read().await.get(&player_id).and_then(|x| { + let base = x.swap(base.map(|x| x as u16).unwrap_or(u16::MAX), std::sync::atomic::Ordering::Relaxed); + if base > u8::MAX as u16 { + None + } else { + Some(base as u8) + } + }) + } +} + +struct BaseCounters { + enemies: std::sync::atomic::AtomicI16, + friendlies: std::sync::atomic::AtomicI16, + capture: atomic_float::AtomicF32, + percent_per_second: f32, +} + +impl BaseCounters { + fn new(percent_per_second: f32) -> Self { + Self { + enemies: std::sync::atomic::AtomicI16::new(0), + friendlies: std::sync::atomic::AtomicI16::new(0), + capture: atomic_float::AtomicF32::new(0.0), + percent_per_second, + } + } +} + +struct BaseTracker { + bases: std::collections::HashMap, + last_tick: std::sync::atomic::AtomicI64, + is_baseless: bool, +} + +impl BaseTracker { + const TICK_MS: i64 = 50; + //const CAPTURE_PER_TICK: f32 = 0.007; + + async fn on_enter(&self, generic: &crate::matches::GenericGamemodeEngine, base_id: u8, is_friendly: bool, player_id: u8) { + if let Some(base) = self.bases.get(&base_id) { + if let Some(conn) = generic.users.read().await.get(&player_id) { + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &rlnl::events::ingame::TeamBaseBoolean { + team: base_id, + value: 1, + }, + rlnl::event_code::NetworkEvent::PlayerInsideBase, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection, + ).await); + } else { return; } + if is_friendly { + let friendlies = base.friendlies.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + if friendlies == 0 { + // newly contesting + if base.enemies.load(std::sync::atomic::Ordering::SeqCst) > 0 { + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseContested, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseBoolean { + team: base_id, + value: 1, + }, + true + ).await; + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseCaptureStop, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: base_id, + current_progress: rlnl::types::ByteFloat::from(base.capture.load(std::sync::atomic::Ordering::SeqCst)), + max_progress: rlnl::types::ByteFloat::from(4.0), + }, + true + ).await; + } + } + } else { + let enemies = base.enemies.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + if enemies == 0 { + // newly capturing + if base.friendlies.load(std::sync::atomic::Ordering::SeqCst) > 0 { + // contested + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseContested, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseBoolean { + team: base_id, + value: 1, + }, + true + ).await; + } else { + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseCaptureStart, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: base_id, + current_progress: rlnl::types::ByteFloat::from(base.capture.load(std::sync::atomic::Ordering::SeqCst)), + max_progress: rlnl::types::ByteFloat::from(4.0), + }, + true + ).await; + } + } + } + } + } + + async fn on_exit(&self, generic: &crate::matches::GenericGamemodeEngine, base_id: u8, is_friendly: bool, player_id: u8) { + if let Some(base) = self.bases.get(&base_id) { + if let Some(conn) = generic.users.read().await.get(&player_id) { + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &rlnl::events::ingame::TeamBaseBoolean { + team: base_id, + value: 0, + }, + rlnl::event_code::NetworkEvent::PlayerInsideBase, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection, + ).await); + } else { return; } + if is_friendly { + let friendlies = base.friendlies.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); + if friendlies <= 1 { + // no longer contesting + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseContested, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseBoolean { + team: base_id, + value: 0, + }, + true + ).await; + if base.enemies.load(std::sync::atomic::Ordering::SeqCst) > 0 { + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseCaptureStart, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: base_id, + current_progress: rlnl::types::ByteFloat::from(base.capture.load(std::sync::atomic::Ordering::SeqCst)), + max_progress: rlnl::types::ByteFloat::from(4.0), + }, + true + ).await; + } + } + } else { + let enemies = base.enemies.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); + if enemies <= 1 { + // no longer capturing + let last_state = base.capture.load(std::sync::atomic::Ordering::SeqCst); + let rounded_state = last_state.floor(); + base.capture.store(rounded_state, std::sync::atomic::Ordering::SeqCst); + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseCaptureStop, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: base_id, + current_progress: rlnl::types::ByteFloat::from(last_state), + max_progress: rlnl::types::ByteFloat::from(4.0), + }, + true + ).await; + /*generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseCaptureReset, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: base_id, + current_progress: rlnl::types::ByteFloat::from(rounded_state), + max_progress: rlnl::types::ByteFloat::from(4.0), + }, + true + ).await;*/ + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseState, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: base_id, + current_progress: rlnl::types::ByteFloat::from(rounded_state + f32::EPSILON), + max_progress: rlnl::types::ByteFloat::from(4.0), + }, + true + ).await; + } + } + } + } + + async fn tick(&self, generic: &crate::matches::GenericGamemodeEngine) { + let now = chrono::Utc::now().timestamp_millis(); + let last_tick = self.last_tick.load(std::sync::atomic::Ordering::SeqCst); + let delta = if last_tick == i64::MIN { + // first tick + self.last_tick.store(now, std::sync::atomic::Ordering::SeqCst); + 1 + } else { + let delta = (now - last_tick) / Self::TICK_MS; + if delta == 0 { return; } + self.last_tick.store(last_tick + (delta * Self::TICK_MS), std::sync::atomic::Ordering::SeqCst); + delta + }; + for (team, base) in self.bases.iter() { + if base.friendlies.load(std::sync::atomic::Ordering::SeqCst) > 0 { continue; } + if base.enemies.load(std::sync::atomic::Ordering::SeqCst) <= 0 { continue; } + let to_add = (delta as f32) * (Self::TICK_MS as f32) * base.percent_per_second * 4.0 / (100.0 * 1000.0); + let pre_add = base.capture.fetch_add(to_add, std::sync::atomic::Ordering::SeqCst); + let post_add = pre_add + to_add; + if post_add >= 4.0 { + log::info!("Team {}'s base was captured in game {}", team, generic.game_guid()); + base.capture.store(4.0, std::sync::atomic::Ordering::SeqCst); + let data = rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: *team, + current_progress: rlnl::types::ByteFloat::from(4.0), + max_progress: rlnl::types::ByteFloat::from(4.0), + }; + generic.broadcast( + rlnl::event_code::NetworkEvent::TeamBaseFinalSectionComplete, + literustlib::packet::Property::ReliableOrdered, + &data, + true + ).await; + let winning_team = if *team == 0 { 1u8 } else { 0u8 }; + let winning_team_i32 = winning_team as i32; + let win_data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason: rlnl::types::GameEndReason::BaseCaptured, + }; + 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( + &win_data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + generic.game_done(); + break; + } else { + let data = rlnl::events::ingame::TeamBaseState { + base_team_or_mining_point_index: *team, + current_progress: rlnl::types::ByteFloat::from(post_add), + max_progress: rlnl::types::ByteFloat::from(4.0), + }; + let is_section_complete = pre_add.floor() < post_add.floor(); + generic.broadcast( + if is_section_complete { rlnl::event_code::NetworkEvent::TeamBaseSectionComplete } else { rlnl::event_code::NetworkEvent::TeamBaseState }, + literustlib::packet::Property::ReliableOrdered, + &data, + true + ).await; + } + + } + } + + fn teams(&self) -> std::collections::HashSet { + self.bases.keys().map(|x| *x).collect() } } pub struct EliminationLogic { tracked: PlayerTracker, + bases: BaseTracker, game_duration: std::time::Duration, respawn_full_heal_duration: f32, respawn_heal_duration: f32, @@ -67,12 +347,18 @@ pub struct EliminationLogic { } impl EliminationLogic { - pub fn new(config: &oj_rc_core::data::game_mode::GameModeConfig) -> Self { + 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()), + in_base: tokio::sync::RwLock::new(std::collections::HashMap::new()), + }, + bases: BaseTracker { + bases: map.bases.iter().map(|(team, base)| (*team, BaseCounters::new(base.1))).collect(), + last_tick: std::sync::atomic::AtomicI64::new(i64::MIN), + is_baseless: map.bases.is_empty(), }, game_duration: dur, respawn_full_heal_duration: config.respawn_full_heal_duration, @@ -90,6 +376,45 @@ impl EliminationLogic { } *lock = None; } + + async fn on_last_player_gone(&self, generic: &crate::matches::GenericGamemodeEngine, conn: &crate::matches::generic::UserConnection) { + log::debug!("Everyone is dead, so long and thanks for all the fish"); + generic.game_done(); + self.abort_timer_sync().await; + let data = rlnl::events::ingame::GameLoseWin { + winning_team: if conn.descriptor.team == 0 { 1 } else { 0 }, // always the other team + end_reason: rlnl::types::GameEndReason::NoPlayersRemaining, + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + rlnl::event_code::NetworkEvent::GameLost, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + + async fn send_win_info(&self, generic: &crate::matches::GenericGamemodeEngine, winning_team: u8) { + generic.game_done(); + self.abort_timer_sync().await; + let data = rlnl::events::ingame::GameLoseWin { + winning_team, + end_reason: rlnl::types::GameEndReason::OneTeamRemaining, + }; + let winning_team_i32 = winning_team as i32; + for conn in generic.users.read().await.values() { + let event = if conn.descriptor.team == winning_team_i32 { + rlnl::event_code::NetworkEvent::GameWon + } else { + rlnl::event_code::NetworkEvent::GameLost + }; + crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( + &data, + event, + literustlib::packet::Property::ReliableOrdered, + &conn.connection.connection + ).await); + } + } } #[async_trait::async_trait] @@ -111,30 +436,9 @@ impl CustomGameLogic for EliminationLogic { self.tracked.destroy_vehicle(&player.descriptor).await; if let Some(winning_team) = self.tracked.winner_team().await { log::info!("Team {} has won sudden death game {} because player {} left", winning_team, generic.game_guid(), player.descriptor.player_id); - let data = rlnl::events::ingame::GameLoseWin { - winning_team, - end_reason: rlnl::types::GameEndReason::OneTeamRemaining, - }; - let winning_team_i32 = winning_team as i32; - for conn in generic.users.read().await.values() { - let event = if conn.descriptor.team == winning_team_i32 { - rlnl::event_code::NetworkEvent::GameWon - } else { - rlnl::event_code::NetworkEvent::GameLost - }; - crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( - &data, - event, - literustlib::packet::Property::ReliableOrdered, - &conn.connection.connection - ).await); - } - generic.game_done(); - self.abort_timer_sync().await; + self.send_win_info(generic, winning_team).await; } else if self.tracked.alive_count().await.is_empty() { - log::debug!("Everyone is dead, so long and thanks for all the fish"); - generic.game_done(); - self.abort_timer_sync().await; + self.on_last_player_gone(generic, player).await; } true } @@ -154,42 +458,13 @@ impl CustomGameLogic for EliminationLogic { ).await; if let Some(winning_team) = self.tracked.winner_team().await { log::info!("Team {} has won sudden death game {}", winning_team, generic.game_guid()); - let data = rlnl::events::ingame::GameLoseWin { - winning_team, - end_reason: rlnl::types::GameEndReason::OneTeamRemaining, - }; - let winning_team_i32 = winning_team as i32; - for conn in generic.users.read().await.values() { - let event = if conn.descriptor.team == winning_team_i32 { - rlnl::event_code::NetworkEvent::GameWon - } else { - rlnl::event_code::NetworkEvent::GameLost - }; - crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( - &data, - event, - literustlib::packet::Property::ReliableOrdered, - &conn.connection.connection - ).await); - } - generic.game_done(); + self.send_win_info(generic, winning_team).await; } else { log::info!("Player {} has been destroyed in sudden death game {}", victim, generic.game_guid()); - let data = rlnl::events::ingame::GameLoseWin { - winning_team: if conn.descriptor.team == 0 { 1 } else { 0 }, // always the other team - end_reason: rlnl::types::GameEndReason::NoPlayersRemaining, - }; - crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data( - &data, - rlnl::event_code::NetworkEvent::GameLost, - literustlib::packet::Property::ReliableOrdered, - &conn.connection.connection - ).await); + if self.tracked.alive_count().await.is_empty() { + self.on_last_player_gone(generic, conn).await; + } } - } else if self.tracked.alive_count().await.is_empty() { - log::debug!("Everyone is dead, this must be the West Seth was talking about!"); - generic.game_done(); - self.abort_timer_sync().await; } true } @@ -227,7 +502,7 @@ impl CustomGameLogic for EliminationLogic { senders.push((conn.connection.clone(), conn.state.clone())); } let game_end = game_start + self.game_duration; - let teams = self.tracked.teams().await; + let teams = self.bases.teams(); let extra_packets = teams.iter().map(|team| crate::matches::RlnlPacket { event: rlnl::event_code::NetworkEvent::TeamBaseInitialise, property: literustlib::packet::Property::ReliableOrdered, @@ -257,7 +532,48 @@ impl CustomGameLogic for EliminationLogic { true } - async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine, _motion: &rlnl::machine_motion::MachineMotion) -> 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() { + 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 + } + // ignore dead or invalid players + if let Some(player_team) = self.tracked.player_team(motion.player_id).await { + let mut now_in_base = None; + for (&team, base) in generic.map_config.bases.iter() { + if crate::matches::GenericGamemodeEngine::::is_in(&location, &base.0) { + now_in_base = Some(team); + break; + } + } + let in_base = self.tracked.swap_is_in_base(motion.player_id, now_in_base).await; + if let Some(team) = in_base { + // was in a base + if let Some(now_team) = now_in_base { + if now_team != team { + // changed bases !? + self.bases.on_exit(generic, team, player_team == team, motion.player_id).await; + self.bases.on_enter(generic, now_team, player_team == now_team, motion.player_id).await; + } + // still in same base + } else { + // player has left the base + self.bases.on_exit(generic, team, player_team == team, motion.player_id).await; + } + } else { + if let Some(now_team) = now_in_base { + // player entered a base + self.bases.on_enter(generic, now_team, player_team == now_team, motion.player_id).await; + } + // still outside of base + } + } + self.bases.tick(generic).await; + if generic.is_game_done() { + self.abort_timer_sync().await; + } true } } diff --git a/rc_multiplayer/src/matches/modes/no_op.rs b/rc_multiplayer/src/matches/modes/no_op.rs index ed1bbf2..1247206 100644 --- a/rc_multiplayer/src/matches/modes/no_op.rs +++ b/rc_multiplayer/src/matches/modes/no_op.rs @@ -36,7 +36,7 @@ impl CustomGameLogic for NoOpLogic { true } - async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine, _motion: &rlnl::machine_motion::MachineMotion) -> bool { + async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine, _motion: &rlnl::machine_motion::MachineMotion, _location: (f32, f32, f32)) -> bool { true } }