1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00
Files
ojrc/rc_multiplayer/src/matches/modes/battle_arena.rs

1039 lines
49 KiB
Rust
Raw Normal View History

use crate::matches::CustomGameLogic;
struct PlayerTracker {
connected: tokio::sync::Mutex<std::collections::HashMap<u8, std::collections::HashSet<u8>>>, // team -> set of player_id
in_point: tokio::sync::RwLock<std::collections::HashMap<u8, std::sync::atomic::AtomicU16>>, // player_id -> in point state (if val > u8::MAX then not in a point)
respawning: tokio::sync::RwLock<std::collections::HashMap<u8, std::sync::atomic::AtomicI64>>, // player_id -> time when they'll spawn (time since unix epoch)
}
impl PlayerTracker {
fn new() -> Self {
Self {
connected: tokio::sync::Mutex::new(std::collections::HashMap::new()),
in_point: tokio::sync::RwLock::new(std::collections::HashMap::new()),
respawning: tokio::sync::RwLock::new(std::collections::HashMap::new()),
}
}
async fn team(&self, player_id: u8) -> Option<u8> {
for (team, players) in self.connected.lock().await.iter() {
if players.contains(&player_id) {
return Some(*team);
}
}
None
}
async fn swap_is_in_point(&self, player_id: u8, point: Option<u8>) -> Option<u8> {
self.in_point.read().await.get(&player_id).and_then(|x| {
let old_point = x.swap(point.map(|x| x as u16).unwrap_or(u16::MAX), std::sync::atomic::Ordering::Relaxed);
if old_point > u8::MAX as u16 {
None
} else {
Some(old_point as u8)
}
})
}
async fn track_player(&self, player: &oj_rc_core::persist::user::PlayerDescriptor) {
let mut conn_lock = self.connected.lock().await;
if let Some(team) = conn_lock.get_mut(&(player.team as u8)) {
team.insert(player.player_id);
} else {
let mut new_team = std::collections::HashSet::new();
new_team.insert(player.player_id);
conn_lock.insert(player.team as u8, new_team);
}
self.in_point.write().await.insert(player.player_id, std::sync::atomic::AtomicU16::new(u16::MAX));
self.respawning.write().await.insert(player.player_id, std::sync::atomic::AtomicI64::new(i64::MIN));
}
async fn disconnect_player(&self, player_id: u8) {
let mut conn_lock = self.connected.lock().await;
for team_members in conn_lock.values_mut() {
team_members.remove(&player_id);
}
}
async fn single_remaining_team(&self) -> Option<u8> {
let conn_lock = self.connected.lock().await;
let mut first_remaining_team = None;
for (team, team_members) in conn_lock.iter() {
if !team_members.is_empty() {
if first_remaining_team.is_some() {
return None;
} else {
first_remaining_team = Some(*team)
}
}
}
first_remaining_team
}
}
struct PointInfo {
team: std::sync::atomic::AtomicI8,
on_point: tokio::sync::RwLock<std::collections::HashMap<u8, std::sync::atomic::AtomicU8>>,
capture: atomic_float::AtomicF32,
percent_per_second: f32,
}
impl PointInfo {
fn new(percent_per_second: f32) -> Self {
Self {
team: std::sync::atomic::AtomicI8::new(-1),
on_point: tokio::sync::RwLock::new([
(0, std::sync::atomic::AtomicU8::new(0)),
(1, std::sync::atomic::AtomicU8::new(0)),
].into_iter().collect()),
capture: atomic_float::AtomicF32::new(0.0),
percent_per_second,
}
}
/*async fn friendlies_on_point(&self, team: u8) -> u8 {
if let Some(counter) = self.on_point.read().await.get(&team) {
counter.load(std::sync::atomic::Ordering::SeqCst)
} else {
0
}
}*/
async fn enemies_on_point(&self, team: u8) -> u8 {
let mut total = 0;
for (iter_team, counter) in self.on_point.read().await.iter() {
if team == *iter_team { continue; }
total += counter.load(std::sync::atomic::Ordering::SeqCst);
}
total
}
async fn owners_on_point(&self) -> u8 {
let team = self.team.load(std::sync::atomic::Ordering::SeqCst);
if team < 0 {
0
2025-09-03 22:33:26 -04:00
} else if let Some(counter) = self.on_point.read().await.get(&(team as u8)) {
counter.load(std::sync::atomic::Ordering::SeqCst)
} else {
2025-09-03 22:33:26 -04:00
0
}
}
async fn stealers_on_point(&self) -> u8 {
let owning_team = self.team.load(std::sync::atomic::Ordering::SeqCst);
let mut total = 0;
if owning_team < 0 {
for counter in self.on_point.read().await.values() {
total += counter.load(std::sync::atomic::Ordering::SeqCst);
}
} else {
for (team, counter) in self.on_point.read().await.iter() {
if (owning_team as u8) == *team { continue; }
total += counter.load(std::sync::atomic::Ordering::SeqCst);
}
}
total
}
async fn stealers_team(&self) -> Option<u8> {
let owning_team = self.team.load(std::sync::atomic::Ordering::SeqCst);
let mut stealing_team = None;
if owning_team < 0 {
for (team, counter) in self.on_point.read().await.iter() {
let count = counter.load(std::sync::atomic::Ordering::SeqCst);
if count != 0 {
if stealing_team.is_some() {
return None;
} else {
stealing_team = Some(*team);
}
}
}
} else {
let owning_team = owning_team as u8;
for (team, counter) in self.on_point.read().await.iter() {
if owning_team == *team { continue; }
let count = counter.load(std::sync::atomic::Ordering::SeqCst);
if count != 0 {
if stealing_team.is_some() {
return None;
} else {
stealing_team = Some(*team);
}
}
}
}
stealing_team
}
}
struct PointTracker {
points: Vec<PointInfo>,
last_tick: std::sync::atomic::AtomicI64,
}
struct PointTickInfo {
owned: std::collections::HashMap<u8, u8>, // team -> capture point count
captured_firsts: std::collections::HashSet<u8>, // team
lost_lasts: std::collections::HashSet<u8>, // team
delta: i64,
}
impl PointTracker {
const TICK_MS: i64 = 50;
fn new(points: impl Iterator<Item=f32>) -> Self {
Self {
points: points.map(PointInfo::new).collect(),
last_tick: std::sync::atomic::AtomicI64::new(i64::MIN),
}
}
async fn on_enter(&self, generic: &crate::matches::GenericGamemodeEngine<BattleArenaLogic>, point_i: u8, _player_id: u8, player_team: i8) {
if player_team < 0 {
return;
}
let player_team_u8 = player_team as u8;
if let Some(point) = self.points.get(point_i as usize) {
let point_team = point.team.load(std::sync::atomic::Ordering::SeqCst);
if !point.on_point.read().await.contains_key(&player_team_u8) {
point.on_point.write().await.insert(player_team_u8, std::sync::atomic::AtomicU8::new(0));
}
if point_team == player_team {
let old_friendlies = point.on_point.read().await[&player_team_u8].fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let current_enemies = point.enemies_on_point(player_team_u8).await;
if current_enemies != 0 && old_friendlies == 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureLocked,
id: point_i,
defending_team: point_team,
2025-09-03 22:33:26 -04:00
attacking_team: player_team,
},
true,
).await;
}
} else {
let old_enemies = point.on_point.read().await[&player_team_u8].fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let current_contesters = point.enemies_on_point(player_team_u8).await;
if old_enemies == 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureStarted,
id: point_i,
defending_team: point_team,
2025-09-03 22:33:26 -04:00
attacking_team: player_team,
},
true,
).await;
if current_contesters != 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureLocked,
id: point_i,
defending_team: point_team,
2025-09-03 22:33:26 -04:00
attacking_team: player_team,
},
true,
).await;
}
}
}
}
}
async fn on_exit(&self, generic: &crate::matches::GenericGamemodeEngine<BattleArenaLogic>, point_i: u8, _player_id: u8, player_team: i8, max_progress: f32) {
if player_team < 0 {
return;
}
let player_team_u8 = player_team as u8;
if let Some(point) = self.points.get(point_i as usize) {
let point_team = point.team.load(std::sync::atomic::Ordering::SeqCst);
if !point.on_point.read().await.contains_key(&player_team_u8) {
point.on_point.write().await.insert(player_team_u8, std::sync::atomic::AtomicU8::new(0));
}
if point_team == player_team {
let old_friendlies = point.on_point.read().await[&player_team_u8].fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
let current_enemies = point.enemies_on_point(player_team_u8).await;
if old_friendlies == 0 {
// something is out of sync, let's just ignore it and try to undo any underflow
log::warn!("Team {} players on point {} counting error", player_team, point_i);
point.on_point.read().await[&player_team_u8].store(0, std::sync::atomic::Ordering::SeqCst);
2025-09-03 22:33:26 -04:00
} else if old_friendlies == 1 && current_enemies != 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureUnlocked,
id: point_i,
defending_team: point_team,
attacking_team: player_team,
},
true,
).await;
}
} else {
let old_enemies = point.on_point.read().await[&player_team_u8].fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
//let current_friendlies = point.friendlies.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if old_enemies == 0 {
// something is out of sync, let's just ignore it and try to undo any underflow
log::warn!("Team {} players on point {} counting error", player_team, point_i);
point.on_point.read().await[&player_team_u8].store(0, std::sync::atomic::Ordering::SeqCst);
2025-09-03 22:33:26 -04:00
} else if old_enemies == 1 {
//log::info!("Enemy has left the capture point");
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureStoppedNoAttackers,
id: point_i,
defending_team: point_team,
attacking_team: player_team,
},
true,
).await;
let progress_now = point.capture.load(std::sync::atomic::Ordering::SeqCst).floor();
point.capture.store(progress_now, std::sync::atomic::Ordering::SeqCst);
let data = rlnl::events::ingame::TeamBaseState {
base_team_or_mining_point_index: point_i,
current_progress: rlnl::types::ByteFloat::from(progress_now),
max_progress: rlnl::types::ByteFloat::from(max_progress),
};
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointProgress,
literustlib::packet::Property::ReliableOrdered,
&data,
true
).await;
}
}
}
}
async fn tick(&self, generic: &crate::matches::GenericGamemodeEngine<BattleArenaLogic>, max_progress: f32) -> Option<PointTickInfo> {
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 None; }
self.last_tick.store(last_tick + (delta * Self::TICK_MS), std::sync::atomic::Ordering::SeqCst);
delta
};
let mut owned_points = std::collections::HashMap::with_capacity(2);
let mut captured_firsts = std::collections::HashSet::new();
let mut lost_lasts = std::collections::HashSet::new();
for (i, cap_point) in self.points.iter().enumerate() {
let point_owner = cap_point.team.load(std::sync::atomic::Ordering::SeqCst);
if point_owner >= 0 {
let point_owner = point_owner as u8;
if let Some(count) = owned_points.get_mut(&point_owner) {
*count += 1;
} else {
owned_points.insert(point_owner, 1);
}
}
let friendlies = cap_point.owners_on_point().await;
let enemies = cap_point.stealers_on_point().await;
if friendlies != 0 { continue; }
if enemies == 0 { continue; }
let stealing_team = cap_point.stealers_team().await;
if stealing_team.is_none() { continue; }
let stealing_team = stealing_team.unwrap();
//log::info!("Team {} is capturing point {} (# of players: {})", stealing_team, i, enemies);
let to_add = (delta as f32) * (Self::TICK_MS as f32) * cap_point.percent_per_second * max_progress / (100.0 * 1000.0);
let pre_add = cap_point.capture.fetch_add(to_add, std::sync::atomic::Ordering::SeqCst);
let post_add = pre_add + to_add;
if post_add >= max_progress {
let new_team = stealing_team as i8;
log::info!("Point {} was captured by team {} in game {}", i, new_team, generic.game_guid());
cap_point.capture.store(0.0, std::sync::atomic::Ordering::SeqCst);
cap_point.team.store(new_team, std::sync::atomic::Ordering::SeqCst);
2025-09-03 22:33:26 -04:00
if owned_points.get(&(new_team as u8)).copied().unwrap_or(0) == 0 {
captured_firsts.insert(new_team as u8);
}
if point_owner >= 0 && *owned_points.get(&(point_owner as u8)).unwrap() == 1 {
lost_lasts.insert(point_owner as u8);
}
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureCompleted,
id: i as u8,
defending_team: point_owner,
attacking_team: new_team,
},
true,
).await;
}
let progress_now = cap_point.capture.load(std::sync::atomic::Ordering::SeqCst);
let data = rlnl::events::ingame::TeamBaseState {
base_team_or_mining_point_index: i as u8,
current_progress: rlnl::types::ByteFloat::from(progress_now),
max_progress: rlnl::types::ByteFloat::from(max_progress),
};
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointProgress,
literustlib::packet::Property::ReliableOrdered,
&data,
true
).await;
}
Some(PointTickInfo {
owned: owned_points,
captured_firsts,
lost_lasts,
delta,
})
}
}
struct BaseTracker {
bases: std::collections::HashMap<u8, BaseInfo>,
}
impl BaseTracker {
fn new<'a>(bases_iter: impl std::iter::Iterator<Item=&'a u8>, crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> Self {
let mut bases = std::collections::HashMap::new();
for base_id in bases_iter {
bases.insert(*base_id, BaseInfo::new(crystals));
}
Self {
bases,
}
}
}
struct BaseInfo {
cube_index: atomic_float::AtomicF32,
crystals_healths: Vec<std::sync::atomic::AtomicU8>,
}
impl BaseInfo {
fn new(crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> Self {
Self {
cube_index: atomic_float::AtomicF32::new(0.0),
crystals_healths: (0..crystals.len())
.map(|_| std::sync::atomic::AtomicU8::new(0))
.collect()
}
}
#[inline]
fn calculate_crystal_health(&self, i: usize, max_health: u32) -> u32 {
(((self.crystals_healths[i].load(std::sync::atomic::Ordering::Relaxed) as f32) / (u8::MAX as f32))
* (max_health as f32)).ceil() as u32
}
fn first_damaged(&self, old_index: usize, max_health: u32) -> Option<usize> {
for i in 0..old_index {
let health = self.calculate_crystal_health(i, max_health);
if health != 0 && health != max_health {
return Some(i);
}
}
None
}
}
enum WinMode {
BaseFull,
OutOfTime,
OutOfPlayers,
}
pub struct BattleArenaLogic {
game_duration: std::time::Duration,
game_end: std::sync::atomic::AtomicI64,
respawn_full_heal_duration: f32,
respawn_heal_duration: f32,
timer_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
player_tracking: PlayerTracker,
capture_tracking: PointTracker,
base_tracking: BaseTracker,
//cube_parser: std::sync::Arc<oj_rc_core::cubes::CubeLocationsParser>,
crystals: Vec<oj_rc_core::cubes::CubeLocationInfo>,
config: oj_rc_core::data::battle_arena_config::BattleArenaData,
}
impl BattleArenaLogic {
const CRYSTAL_ID: u32 = 3950293873;
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 {
game_duration: dur,
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)),
base_tracking: BaseTracker::new(map.bases.keys(), &crystals),
//cube_parser,
//ba_base: teambase,
//ba_equalizer: equalizer,
crystals,
config: ba_config,
}
}
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 battle arena match timer task");
}
*lock = None;
}
fn sphere_to_capture_point(sphere: &oj_rc_core::persist::config::Sphere, max_progress: f32) -> rlnl::types::CapturePoint {
rlnl::types::CapturePoint {
pos: rlnl::types::PosQuatPair {
pos: (sphere.center.x, sphere.center.y, sphere.center.z).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
team: -1,
progress: 0.0.into(),
max_progress: max_progress.into(),
}
}
fn default_capture_point(max_progress: f32) -> rlnl::types::CapturePoint {
rlnl::types::CapturePoint {
pos: rlnl::types::PosQuatPair {
pos: (0.0, 0.0, 0.0).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
team: -1,
progress: 0.0.into(),
max_progress: max_progress.into(),
}
}
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() {
// find winning team
let mut winning_team = None;
for (base, tracking) in self.base_tracking.bases.iter() {
if let Some((_, winning_score)) = winning_team {
let score = tracking.cube_index.load(std::sync::atomic::Ordering::SeqCst);
if score > winning_score {
winning_team = Some((*base, score));
}
} else {
winning_team = Some((*base, tracking.cube_index.load(std::sync::atomic::Ordering::SeqCst)));
}
}
let winners = if let Some((winning_team, _)) = winning_team {
winning_team
} else {
u8::MAX
};
// game is done, hooray
self.do_win(winners, WinMode::OutOfTime, generic).await;
true
} else {
false
}
}
async fn do_win(&self, winning_team: u8, ty: WinMode, generic: &crate::matches::GenericGamemodeEngine<Self>) {
generic.game_done();
let end_reason = match ty {
WinMode::BaseFull
| WinMode::OutOfPlayers => rlnl::types::GameEndReason::BaseDestroyed,
WinMode::OutOfTime => rlnl::types::GameEndReason::TimeOut,
};
let payload = rlnl::events::ingame::GameLoseWin {
winning_team,
end_reason,
};
for player in generic.users.read().await.values() {
let is_winner = player.descriptor.team == winning_team as i32;
let net_event = match ty {
WinMode::BaseFull
| WinMode::OutOfPlayers => {
if is_winner { rlnl::event_code::NetworkEvent::GameWonBaseDestroyed } else { rlnl::event_code::NetworkEvent::GameLostBaseDestroyed }
},
WinMode::OutOfTime => {
if is_winner { rlnl::event_code::NetworkEvent::GameWon } else { rlnl::event_code::NetworkEvent::GameLost }
}
};
crate::events::log_lnl_send_failure(
player.connection.rlnl()
.send_data(
&payload,
net_event,
literustlib::packet::Property::ReliableOrdered,
&player.connection.connection,
).await
);
}
}
async fn do_destruct_tasks(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, player_id: u8) {
if let Some(player_team) = self.player_tracking.team(player_id).await {
let was_in_point = self.player_tracking.swap_is_in_point(player_id, None).await;
if let Some(was_in_point) = was_in_point {
self.capture_tracking.on_exit(generic, was_in_point, player_id, player_team as i8, self.config.num_segments as f32).await;
}
}
}
2025-09-01 18:50:03 -04:00
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.config.respawn_time_seconds as u64);
let now = chrono::Utc::now();
let respawn_timestamp = now + respawn_time;
if let Some(player_respawn) = self.player_tracking.respawning.read().await.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.config.respawn_time_seconds as i16,
};
generic.broadcast(
rlnl::event_code::NetworkEvent::SetRespawnWaitingTime,
literustlib::packet::Property::ReliableOrdered,
&respawn_payload,
true
).await;
if let Some(player_team) = self.player_tracking.team(player_id).await {
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(Self::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_trait::async_trait]
impl CustomGameLogic for BattleArenaLogic {
async fn on_player_join(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, player: &crate::matches::generic::UserConnection, _others: &[oj_rc_core::persist::user::PlayerDescriptor]) -> bool {
log::info!("Player {} joined", player.descriptor.player_id);
self.player_tracking.track_player(&player.descriptor).await;
true
}
async fn on_player_end(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, player: &crate::matches::generic::UserConnection) -> bool {
let player_id = player.descriptor.player_id;
self.do_destruct_tasks(generic, player_id).await;
self.player_tracking.disconnect_player(player_id).await;
if let Some(winning_team) = self.player_tracking.single_remaining_team().await {
self.do_win(winning_team, WinMode::OutOfPlayers, generic).await;
}
true
}
async fn on_vehicle_destroyed(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _killer: u8, victim: u8) -> bool {
self.do_destruct_tasks(generic, victim).await;
2025-09-01 18:50:03 -04:00
self.do_respawn_tasks(generic, victim).await;
true
}
async fn on_vehicle_self_destruct(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, user: u8, _is_classic: bool) -> bool {
self.do_destruct_tasks(generic, user).await;
2025-09-01 18:50:03 -04:00
self.do_respawn_tasks(generic, user).await;
true
}
async fn extra_sync_events(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> Vec<crate::matches::RlnlPacket> {
vec![
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::GameModeSettings,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::UpdateGameModeSettings {
respawn_heal_duration: self.respawn_heal_duration,
respawn_full_heal_duration: self.respawn_full_heal_duration,
}),
}),
// TeamBase
if generic.map_config.bases.is_empty() {
None
} else {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::TeamBase,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::GetTeamBase {
base_1: rlnl::types::PosQuatPair {
pos: generic.map_config.bases.get(&0).map(|(s, _)| (s.center.x, s.center.y, s.center.z)).unwrap_or((0.0, 0.0, 0.0)).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
base_2: rlnl::types::PosQuatPair {
pos: generic.map_config.bases.get(&1).map(|(s, _)| (s.center.x, s.center.y, s.center.z)).unwrap_or((0.0, 0.0, 0.0)).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
protonium_cube_health: self.config.protonium_health as i32,
}),
})
},
// RegisterCapturePoints
if generic.map_config.capture_points.is_empty() {
None
} else {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::RegisterCapturePoints,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::GetCapturePoints {
points: [
2025-09-03 22:33:26 -04:00
generic.map_config.capture_points.first().map(|(s, _)| Self::sphere_to_capture_point(s, self.config.num_segments as f32)).unwrap_or_else(|| Self::default_capture_point(self.config.num_segments as f32)),
generic.map_config.capture_points.get(1).map(|(s, _)| Self::sphere_to_capture_point(s, self.config.num_segments as f32)).unwrap_or_else(|| Self::default_capture_point(self.config.num_segments as f32)),
generic.map_config.capture_points.get(2).map(|(s, _)| Self::sphere_to_capture_point(s, self.config.num_segments as f32)).unwrap_or_else(|| Self::default_capture_point(self.config.num_segments as f32)),
]
}),
})
},
// RegisterEqualizer
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::RegisterEqualizer,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::GetEqualizer {
pos: rlnl::types::PosQuatPair {
pos: (0.0, 0.0, 0.0).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
total_health: 42,
}),
}),
// SetShieldState
2025-09-03 22:33:26 -04:00
if generic.map_config.bases.contains_key(&0) {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SetShieldState,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::FusionShieldState {
team_id: 0,
full_power: 0,
}),
})
} else {
None
},
2025-09-03 22:33:26 -04:00
if generic.map_config.bases.contains_key(&1) {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SetShieldState,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::FusionShieldState {
team_id: 1,
full_power: 0,
}),
})
} else {
None
},
// CurrentGameTime
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::CurrentGameTime,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::GameTime(self.game_duration.as_millis() as f32 / 1000.0)),
}),
// SyncTeamBaseCubes
// TODO ???
/*crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::HealedCubes {
healed_machine: 0,
type_performing_healing: rlnl::types::TargetType::TeamBase,
target_type: rlnl::types::TargetType::TeamBase,
num_healed_cubes: 1,
hit_cubes: vec![
rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: 0, y: 0, z: 0, },
damage: 1,
}
],
}),
},*/
/*Some(
crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::HealedCubes {
healed_machine: 0,
type_performing_healing: rlnl::types::TargetType::TeamBase,
target_type: rlnl::types::TargetType::TeamBase,
num_healed_cubes: oj_rc_core::cubes::prefabs::CRYSTAL_COUNT as _,
hit_cubes: oj_rc_core::cubes::prefabs::team_base_ba_crystals(oj_rc_core::cubes::prefabs::CRYSTAL_COUNT)
.into_iter()
//.chain(vec![oj_rc_core::cubes::prefabs::team_base_ba_location()].into_iter())
.map(|loc| {
//log::info!("Doing sync-time base heal for cube at ({}, {}, {})", loc.0, loc.1, loc.2);
rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: loc.0, y: loc.1, z: loc.2, },
damage: Self::CRYSTAL_HEALTH,
}
})
.collect(),
}),
}
),*/
// SyncEqualizerNotification
/*crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncEqualizerNotification,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::EqualizerNotification {
notification: rlnl::types::EqualizerState::Lost,
team_id: 0,
time: 0,
max_health: 42,
health: 7,
}),
},
crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncEqualizerNotification,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::EqualizerNotification {
notification: rlnl::types::EqualizerState::Lost,
team_id: 1,
time: 0,
max_health: 42,
health: 7,
}),
},*/
2025-09-03 22:33:26 -04:00
].into_iter().flatten().collect()
}
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 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 game_end = game_start + self.game_duration;
let extra_packets = Vec::default();
let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, extra_packets, 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 battle arena mode suggests an assumption was wrong");
timer_t.abort();
}
*timer_lock = Some(new_timer_task);
self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed);
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 {
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 {
// game is not in progress, ignore motion event
log::debug!("Ignoring early motion event from player {}", motion.player_id);
return true;
}
if generic.is_game_done() {
self.abort_timer_sync().await;
return true;
}
if self.check_if_match_time_is_done(generic).await {
return true;
}
if generic.map_config.capture_points.is_empty() {
return true; // don't bother trying to track whether players are in capture points since there are none
}
if let Some(player_team) = self.player_tracking.team(motion.player_id).await {
let mut now_in_point = None;
for (point_i, point) in generic.map_config.capture_points.iter().enumerate() {
if crate::matches::GenericGamemodeEngine::<Self>::is_in(&location, &point.0) {
now_in_point = Some(point_i as u8);
break;
}
}
let was_in_point = self.player_tracking.swap_is_in_point(motion.player_id, now_in_point).await;
if was_in_point != now_in_point {
//log::info!("Player {}'s occupied capture point changed from {:?} to {:?}", motion.player_id, was_in_point, now_in_point);
if let Some(now_in_point) = now_in_point {
self.capture_tracking.on_enter(generic, now_in_point, motion.player_id, player_team as i8).await;
}
if let Some(was_in_point) = was_in_point {
self.capture_tracking.on_exit(generic, was_in_point, motion.player_id, player_team as i8, self.config.num_segments as f32).await;
}
}
}
if let Some(tick_info) = self.capture_tracking.tick(generic, self.config.num_segments as f32).await {
// handle shield (de)activation
for team in tick_info.captured_firsts {
generic.broadcast(
rlnl::event_code::NetworkEvent::SetShieldState,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::sync::FusionShieldState {
team_id: team as i8,
full_power: 1,
},
true,
).await;
}
for team in tick_info.lost_lasts {
generic.broadcast(
rlnl::event_code::NetworkEvent::SetShieldState,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::sync::FusionShieldState {
team_id: team as i8,
full_power: 0,
},
true,
).await;
}
// do base charge tick
for base_id in generic.map_config.bases.keys() {
//log::info!("Healing base {}", base_id);
if let Some(owned_points) = tick_info.owned.get(base_id) {
if let Some(tracked_base) = self.base_tracking.bases.get(base_id) {
let one_tick = (self.crystals.len() as f32)
* ((PointTracker::TICK_MS as f32) / (self.game_duration.as_millis() as f32))
* ((self.base_tracking.bases.len() as f32) / (self.capture_tracking.points.len() as f32));
let increment = tick_info.delta as f32 * (*owned_points as f32) * one_tick;
let old_float_index = tracked_base.cube_index.fetch_add(increment, std::sync::atomic::Ordering::SeqCst);
let new_float_index = old_float_index + increment;
let old_index = (old_float_index.ceil() as usize).clamp(0, self.crystals.len());
let new_index = (new_float_index.ceil() as usize).clamp(0, self.crystals.len());
if new_index != old_index {
log::debug!("Base {} increment passed a crystal index barrier", base_id);
let first_damaged = tracked_base.first_damaged(old_index, self.config.protonium_health as u32);
let payload = if new_index - old_index == 1 && first_damaged.is_some() {
// undo cube_index update
tracked_base.cube_index.fetch_sub(increment, std::sync::atomic::Ordering::SeqCst);
log::debug!("Skipping increment in favour of healing damaged/destroyed cube");
let first_damaged = first_damaged.unwrap();
let healing = self.config.protonium_health as u32 - tracked_base.calculate_crystal_health(first_damaged, self.config.protonium_health as u32);
tracked_base.crystals_healths[first_damaged].store(u8::MAX, std::sync::atomic::Ordering::Relaxed);
let target_crystal = &self.crystals[first_damaged];
rlnl::events::HealedCubes {
healed_machine: *base_id as u16,
type_performing_healing: rlnl::types::TargetType::TeamBase,
target_type: rlnl::types::TargetType::TeamBase,
num_healed_cubes: 1,
hit_cubes: vec![
rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: target_crystal.x, y: target_crystal.y, z: target_crystal.z, },
damage: healing as i32,
}
],
}
} else {
let target_crystals = &self.crystals[old_index..new_index];
for crystal_i in old_index..new_index {
tracked_base.crystals_healths[crystal_i].store(u8::MAX, std::sync::atomic::Ordering::Relaxed);
}
rlnl::events::HealedCubes {
healed_machine: *base_id as u16,
type_performing_healing: rlnl::types::TargetType::TeamBase,
target_type: rlnl::types::TargetType::TeamBase,
num_healed_cubes: target_crystals.len() as _,
hit_cubes: target_crystals
.iter()
.map(|loc| rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: loc.x, y: loc.y, z: loc.z, },
damage: self.config.protonium_health as i32,
})
.collect(),
}
};
generic.broadcast(
rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
literustlib::packet::Property::ReliableOrdered,
&payload,
true
).await;
if new_index == self.crystals.len() {
// team base is charged to 100%
self.do_win(*base_id, WinMode::BaseFull, 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::SendDamagedByEnemyShield, literustlib::packet::Property::ReliableOrdered) => {
generic.broadcast(
rlnl::event_code::NetworkEvent::DamagedByEnemyShield,
literustlib::packet::Property::ReliableOrdered,
&*data,
true,
).await;
},
(rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => {
// TODO
log::warn!("Ignoring SurrenderRequest because it's not implemented (yet)");
}
(rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest, literustlib::packet::Property::ReliableOrdered) => {
// TODO
log::warn!("Ignoring AwardTeamBaseProtoniumDestroyedRequest because it's not implemented (yet)");
}
_ => {}
}
}
}