1
0
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:
NG (Graham)
2025-09-14 12:08:52 -04:00
parent d194e29ff7
commit 525fa8b87e
13 changed files with 565 additions and 65 deletions

View File

@@ -259,13 +259,13 @@ fn default_game_modes() -> GameModes {
pit: GameMode { pit: GameMode {
respawn_heal_duration: 20.0, respawn_heal_duration: 20.0,
respawn_full_heal_duration: 20.0, respawn_full_heal_duration: 20.0,
kill_limit: 15, kill_limit: 0,
game_time_m: 15, game_time_m: 15,
}, },
team_deathmatch: GameMode { team_deathmatch: GameMode {
respawn_heal_duration: 10.0, respawn_heal_duration: 10.0,
respawn_full_heal_duration: 0.5, respawn_full_heal_duration: 0.5,
kill_limit: 10, kill_limit: 2,
game_time_m: 10, game_time_m: 10,
}, },
} }
@@ -378,7 +378,7 @@ fn default_rotation() -> GameEventSequence {
multiplayer: GameEvent { multiplayer: GameEvent {
map: GameMap::Earth1, map: GameMap::Earth1,
visibility: GameVisibility::Good, visibility: GameVisibility::Good,
mode: GameType::Pit, mode: GameType::TeamDeathmatch,
auto_heal: true, auto_heal: true,
}, },
duration_s: 5*60, // 5 minutes duration_s: 5*60, // 5 minutes
@@ -515,6 +515,7 @@ fn default_multiplayer() -> super::MultiplayerConfig {
fakes: super::multiplayer::default_fake_users(), fakes: super::multiplayer::default_fake_users(),
battle_arena: super::multiplayer::default_ba_conf(), battle_arena: super::multiplayer::default_ba_conf(),
pit_config: super::multiplayer::default_pit_conf(), pit_config: super::multiplayer::default_pit_conf(),
team_death_match: super::multiplayer::default_tdm_conf(),
} }
} }

View File

@@ -476,4 +476,8 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
fn pit_settings(&self) -> super::PitSettings { fn pit_settings(&self) -> super::PitSettings {
self.battle.multiplayer.pit_config.clone().into() self.battle.multiplayer.pit_config.clone().into()
} }
fn tdm_settings(&self) -> super::TeamDeathMatchSettings {
self.battle.multiplayer.team_death_match.clone().into()
}
} }

View File

@@ -2,7 +2,7 @@ mod cubes_json;
pub use cubes_json::CubeConfig; pub use cubes_json::CubeConfig;
mod traits; 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; pub type ConfigImpl = CubeConfig;

View File

@@ -37,6 +37,7 @@ pub trait ConfigProvider<C: Clone> {
fn energy(&self) -> EnergyConfig; fn energy(&self) -> EnergyConfig;
fn ba_settings(&self) -> BattleArenaResolver; fn ba_settings(&self) -> BattleArenaResolver;
fn pit_settings(&self) -> PitSettings; fn pit_settings(&self) -> PitSettings;
fn tdm_settings(&self) -> TeamDeathMatchSettings;
} }
pub struct CompleteCampaignProvider { pub struct CompleteCampaignProvider {
@@ -445,3 +446,9 @@ pub enum PitWinCondition {
Damage(u32), Damage(u32),
Time, Time,
} }
#[derive(Debug)]
pub struct TeamDeathMatchSettings {
pub respawn_time_seconds: u64,
pub self_destruct_is_kill: bool,
}

View File

@@ -12,6 +12,8 @@ pub struct MultiplayerConfig {
pub battle_arena: BattleArenaConfig, pub battle_arena: BattleArenaConfig,
#[serde(default = "default_pit_conf")] #[serde(default = "default_pit_conf")]
pub pit_config: PitConfig, pub pit_config: PitConfig,
#[serde(default = "default_tdm_conf")]
pub team_death_match: TeamDeathMatchConfig,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@@ -235,3 +237,25 @@ fn default_pit_win_conditions() -> Vec<PitWinCondition> {
PitWinCondition::Time, 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,
}
}

View File

@@ -246,6 +246,7 @@ mod _broadcast_impls {
impl Broadcastable for rlnl::events::sync::FusionShieldState {} impl Broadcastable for rlnl::events::sync::FusionShieldState {}
impl Broadcastable for rlnl::events::sync::EqualizerNotification {} impl Broadcastable for rlnl::events::sync::EqualizerNotification {}
impl Broadcastable for rlnl::events::sync::SpawnPoint {} 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::GameTime {}
impl Broadcastable for rlnl::events::ingame::TeamBaseState {} impl Broadcastable for rlnl::events::ingame::TeamBaseState {}
impl Broadcastable for rlnl::events::ingame::GameEnd {} impl Broadcastable for rlnl::events::ingame::GameEnd {}

View File

@@ -7,6 +7,7 @@ pub struct GameMatches {
cube_parsers: std::sync::Arc<oj_rc_core::cubes::CubeParsers>, cube_parsers: std::sync::Arc<oj_rc_core::cubes::CubeParsers>,
ba_settings: std::sync::Arc<oj_rc_core::persist::config::BattleArenaResolver>, ba_settings: std::sync::Arc<oj_rc_core::persist::config::BattleArenaResolver>,
pit_settings: std::sync::Arc<oj_rc_core::persist::config::PitSettings>, 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>, factory: std::sync::Arc<oj_rc_core::factory::Factory>,
} }
@@ -24,6 +25,7 @@ impl GameMatches {
cube_parsers, cube_parsers,
ba_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::ba_settings(conf)), 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)), 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, factory,
} }
} }
@@ -128,7 +130,25 @@ impl GameMatches {
let engine = super::GenericGamemodeEngine::new( let engine = super::GenericGamemodeEngine::new(
game_info, game_info,
map_config, 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, players,
inner, inner,
fakes_handler, fakes_handler,
@@ -139,7 +159,7 @@ impl GameMatches {
// TODO support more gamemodes // TODO support more gamemodes
Err(oj_rc_core::persist::user::MultiplayerError { Err(oj_rc_core::persist::user::MultiplayerError {
code: oj_rc_core::persist::user::MultiplayerErrorCode::CustomString, 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),
}) })
} }
} }

View File

@@ -225,7 +225,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
users: tokio::sync::RwLock::new(std::collections::HashMap::new()), users: tokio::sync::RwLock::new(std::collections::HashMap::new()),
user_id_map: 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), 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), map_config: std::sync::Arc::new(map),
game_descriptor: game, game_descriptor: game,
game_duration: std::time::Duration::from_secs((config.game_time_minutes as u64) * 60), 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 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) { /*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) { if let Some(player) = self.users.read().await.get(&player_id) {
crate::events::log_lnl_send_failure(player.connection.rlnl().send_data( 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, literustlib::packet::Property::ReliableOrdered,
&connection.connection) &connection.connection)
.await?;*/ .await?;*/
// TODO
Ok(()) Ok(())
} }

View File

@@ -882,7 +882,6 @@ enum WinMode {
} }
pub struct BattleArenaLogic { pub struct BattleArenaLogic {
game_end: std::sync::atomic::AtomicI64,
respawn_full_heal_duration: f32, respawn_full_heal_duration: f32,
respawn_heal_duration: f32, respawn_heal_duration: f32,
timer_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>, timer_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
@@ -900,14 +899,11 @@ impl BattleArenaLogic {
const CLASP_ID: u32 = 606866102; 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 { 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 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); 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 { Self {
respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_full_heal_duration: config.respawn_full_heal_duration,
respawn_heal_duration: config.respawn_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), timer_task: tokio::sync::Mutex::new(None),
player_tracking: PlayerTracker::new(), player_tracking: PlayerTracker::new(),
capture_tracking: PointTracker::new(map.capture_points.iter().map(|(_, speed)| *speed)), 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 { 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 // find winning team
let mut winning_team = None; let mut winning_team = None;
for (base, tracking) in self.base_tracking.bases.iter() { for (base, tracking) in self.base_tracking.bases.iter() {
@@ -1063,35 +1059,13 @@ impl BattleArenaLogic {
z: 10.0 * (player_team as f32) + 10.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(); 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)); tokio::task::spawn(super::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id));
} else { } else {
log::error!("Player {} cannot respawn because they are not in a team!?", player_id); 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) { 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 base_id = cube_damage.hit_machine_id as u8;
let mut total_destroyed = 0; 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 { 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; let player_id = player.descriptor.player_id;
self.do_destruct_tasks(generic, player_id).await; self.do_destruct_tasks(generic, player_id).await;
self.player_tracking.disconnect_player(player_id).await; self.player_tracking.disconnect_player(player_id).await;
@@ -1372,7 +1349,6 @@ impl CustomGameLogic for BattleArenaLogic {
timer_t.abort(); timer_t.abort();
} }
*timer_lock = Some(new_timer_task); *timer_lock = Some(new_timer_task);
self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed);
generic.broadcast( generic.broadcast(
rlnl::event_code::NetworkEvent::SetSurrenderTimes, rlnl::event_code::NetworkEvent::SetSurrenderTimes,
literustlib::packet::Property::ReliableOrdered, 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 { 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); 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 // game is not in progress, ignore motion event
log::debug!("Ignoring early motion event from player {}", motion.player_id); log::debug!("Ignoring early motion event from player {}", motion.player_id);
return true; return true;
@@ -1548,7 +1524,6 @@ impl CustomGameLogic for BattleArenaLogic {
).await; ).await;
}, },
(rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => { (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()); 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(init_surr) = maybe_init_surr {
if let Some(team) = self.player_tracking.team(init_surr.player_id).await { 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) => { (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()); let maybe_crystal_bonus = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::AwardProtoniumDestroyedCubes>(data.as_ref());

View File

@@ -535,6 +535,11 @@ impl CustomGameLogic for EliminationLogic {
self.abort_timer_sync().await; self.abort_timer_sync().await;
return true; return true;
} }
if generic.is_game_past_end_time() {
generic.game_done();
self.abort_timer_sync().await;
return true;
}
if self.bases.is_baseless { if self.bases.is_baseless {
return true; // don't bother trying to track whether players are in bases since there are no bases return true; // don't bother trying to track whether players are in bases since there are no bases
} }

View File

@@ -11,4 +11,29 @@ pub use battle_arena::BattleArenaLogic;
mod pit; mod pit;
pub use pit::PitLogic; pub use pit::PitLogic;
mod team_death_match;
pub use team_death_match::TeamDeathMatchLogic;
mod trackers; 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);
}
}

View File

@@ -325,29 +325,7 @@ impl PitLogic {
).await; ).await;
let spawn_point = Self::choose_spawn_point(&generic.map_config, player_id).1; 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(); 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)); tokio::task::spawn(super::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);
}
} }
} }
@@ -467,6 +445,11 @@ impl CustomGameLogic for PitLogic {
self.abort_timer_sync().await; self.abort_timer_sync().await;
return true; 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; self.win_tracking.tick(generic, self).await;
true true
} }

View 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
}
}