mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Add team death match support #33
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -476,4 +476,8 @@ impl <C: Clone + Send> super::ConfigProvider<C> 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ pub trait ConfigProvider<C: Clone> {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
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<TeamDeathMatchConfig> 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct GameMatches {
|
||||
cube_parsers: std::sync::Arc<oj_rc_core::cubes::CubeParsers>,
|
||||
ba_settings: std::sync::Arc<oj_rc_core::persist::config::BattleArenaResolver>,
|
||||
pit_settings: std::sync::Arc<oj_rc_core::persist::config::PitSettings>,
|
||||
tdm_settings: std::sync::Arc<oj_rc_core::persist::config::TeamDeathMatchSettings>,
|
||||
factory: std::sync::Arc<oj_rc_core::factory::Factory>,
|
||||
}
|
||||
|
||||
@@ -24,6 +25,7 @@ impl GameMatches {
|
||||
cube_parsers,
|
||||
ba_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::ba_settings(conf)),
|
||||
pit_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::pit_settings(conf)),
|
||||
tdm_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
|
||||
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 <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
|
||||
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<T: byteserde::ser_heap::ByteSerializeHeap + ?Sized>(&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 <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
|
||||
literustlib::packet::Property::ReliableOrdered,
|
||||
&connection.connection)
|
||||
.await?;*/
|
||||
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Option<tokio::task::JoinHandle<()>>>,
|
||||
@@ -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<Self>) -> 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<chrono::Utc>, players: Vec<crate::matches::generic::UserSender>, 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<Self>, actual_damage_data: impl FnOnce(Vec<rlnl::types::CubeState>) -> 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<Self>, 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<Self>, 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 = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::InitiateSurrender>(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 = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::AwardProtoniumDestroyedCubes>(data.as_ref());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<chrono::Utc>, players: Vec<crate::matches::generic::UserSender>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<chrono::Utc>, players: Vec<crate::matches::generic::UserSender>, 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
|
||||
}
|
||||
|
||||
452
rc_multiplayer/src/matches/modes/team_death_match.rs
Normal file
452
rc_multiplayer/src/matches/modes/team_death_match.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
use crate::matches::CustomGameLogic;
|
||||
|
||||
struct ScoreTracker {
|
||||
ticker: super::trackers::TickTracker<{Self::TICK_MS}>,
|
||||
scores: std::collections::HashMap<u8, std::sync::atomic::AtomicU32>, // team -> score
|
||||
teams: std::collections::HashMap<u8, u8>, // 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<u8> = 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<u8> {
|
||||
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<TeamDeathMatchLogic>) {
|
||||
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<TeamDeathMatchLogic>) {
|
||||
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<TeamDeathMatchLogic>, 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<TeamDeathMatchLogic>, 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<TeamDeathMatchLogic>, 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<u8, std::sync::atomic::AtomicI64>,
|
||||
teams: std::collections::HashMap<u8, u8>, // player_id -> team,
|
||||
team_members: std::collections::HashMap<u8, Vec<u8>>, // 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<u8, Vec<u8>> {
|
||||
let mut team_map = std::collections::HashMap::<u8, Vec<u8>>::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<TeamDeathMatchLogic>) -> Option<u8> {
|
||||
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<oj_rc_core::persist::config::TeamDeathMatchSettings>,
|
||||
timer_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
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<oj_rc_core::persist::config::TeamDeathMatchSettings>) -> 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<TeamDeathMatchLogic>, 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<Self>, 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<Self>, _player: &crate::matches::generic::UserConnection, _others: &[oj_rc_core::persist::user::PlayerDescriptor]) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn on_player_end(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _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<Self>, 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<Self>, 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<Self>, _killer: u8, _victim: u8) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn extra_sync_events(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> Vec<crate::matches::RlnlPacket> {
|
||||
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<Self>, game_start: chrono::DateTime<chrono::Utc>) -> 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<Self>) -> bool {
|
||||
self.abort_timer_sync().await;
|
||||
true
|
||||
}
|
||||
|
||||
async fn on_broadcast(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _user_id: i32, _event_out: rlnl::event_code::NetworkEvent, _event_in: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: &Option<Box<dyn crate::Broadcastable>>, _skip_user: bool) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn on_motion(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _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<Self>, user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box<dyn crate::Broadcastable>) {
|
||||
match (event, property) {
|
||||
(rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => {
|
||||
let maybe_init_surr = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::InitiateSurrender>(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 = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::SurrenderVoteCast>(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<Self>, _user_id: i32, _remote_player: u8) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user