2025-09-06 19:13:47 -04:00
use crate ::matches ::{ modes ::trackers ::SurrenderGameTracker , CustomGameLogic };
2025-08-30 21:36:01 -04:00
struct PlayerTracker {
connected : tokio ::sync ::Mutex < std ::collections ::HashMap < u8 , std ::collections ::HashSet < u8 >>> , // team -> set of player_id
2025-09-27 12:36:49 -04:00
in_point : std ::collections ::HashMap < u8 , std ::sync ::atomic ::AtomicU16 > , // player_id -> in point state (if val > u8::MAX then not in a point)
respawning : std ::collections ::HashMap < u8 , std ::sync ::atomic ::AtomicI64 > , // player_id -> time when they'll spawn (time since unix epoch)
2025-08-30 21:36:01 -04:00
}
impl PlayerTracker {
2025-09-27 12:36:49 -04:00
fn new ( players : & [ oj_rc_core ::persist ::user ::PlayerDescriptor ]) -> Self {
2025-08-30 21:36:01 -04:00
Self {
connected : tokio ::sync ::Mutex ::new ( std ::collections ::HashMap ::new ()),
2025-09-27 12:36:49 -04:00
in_point : players . iter (). map ( | player | ( player . player_id , std ::sync ::atomic ::AtomicU16 ::new ( u16 ::MAX ))). collect (),
respawning : players . iter (). map ( | player | ( player . player_id , std ::sync ::atomic ::AtomicI64 ::new ( i64 ::MIN ))). collect (),
2025-08-30 21:36:01 -04:00
}
}
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
}
2025-09-27 12:36:49 -04:00
fn swap_is_in_point ( & self , player_id : u8 , point : Option < u8 > ) -> Option < u8 > {
self . in_point . get ( & player_id ). and_then ( | x | {
2025-08-30 21:36:01 -04:00
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 );
}
}
2025-08-31 12:12:43 -04:00
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
}
2025-09-06 19:13:47 -04:00
async fn players_on_team ( & self , team : u8 ) -> Vec < u8 > {
if let Some ( members ) = self . connected . lock (). await . get ( & team ) {
2025-09-10 19:40:51 -04:00
members . iter (). copied (). collect ()
2025-09-06 19:13:47 -04:00
} else {
Vec ::default ()
}
}
2025-08-30 21:36:01 -04:00
}
struct PointInfo {
team : std ::sync ::atomic ::AtomicI8 ,
2025-12-16 23:04:32 -05:00
attackers : std ::sync ::atomic ::AtomicI8 ,
2025-08-30 21:36:01 -04:00
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 ),
2025-12-16 23:04:32 -05:00
attackers : std ::sync ::atomic ::AtomicI8 ::new ( - 1 ),
2025-08-30 21:36:01 -04:00
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 )
2025-08-30 21:36:01 -04:00
} else {
2025-09-03 22:33:26 -04:00
0
2025-08-30 21:36:01 -04:00
}
}
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 > ,
2025-09-06 22:38:55 -04:00
ticker : super ::trackers ::TickTracker < { Self ::TICK_MS } > ,
2025-09-07 15:50:56 -04:00
last_capture_team : std ::sync ::atomic ::AtomicU8 ,
last_capture_time : std ::sync ::atomic ::AtomicI64 ,
2025-08-30 21:36:01 -04:00
}
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
2025-09-07 15:50:56 -04:00
dominating : Option < u8 > ,
2025-09-06 22:38:55 -04:00
delta : u16 ,
2025-08-30 21:36:01 -04:00
}
impl PointTracker {
const TICK_MS : i64 = 50 ;
2025-09-07 15:50:56 -04:00
const TIME_BEFORE_DOMINANT_S : i64 = 30 ;
2025-08-30 21:36:01 -04:00
fn new ( points : impl Iterator < Item = f32 > ) -> Self {
Self {
points : points . map ( PointInfo ::new ). collect (),
2025-09-06 22:38:55 -04:00
ticker : super ::trackers ::TickTracker ::new (),
2025-09-07 15:50:56 -04:00
last_capture_team : std ::sync ::atomic ::AtomicU8 ::new ( u8 ::MAX ),
last_capture_time : std ::sync ::atomic ::AtomicI64 ::new ( i64 ::MIN ),
2025-08-30 21:36:01 -04:00
}
}
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 );
2025-12-16 23:04:32 -05:00
let attacking_team = point . attackers . load ( std ::sync ::atomic ::Ordering ::SeqCst );
2025-08-30 21:36:01 -04:00
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 ));
}
2025-12-16 23:04:32 -05:00
// update counter
let old_count = point . on_point . read (). await [ & player_team_u8 ]. fetch_add ( 1 , std ::sync ::atomic ::Ordering ::SeqCst );
2025-08-30 21:36:01 -04:00
if point_team == player_team {
2025-12-16 23:04:32 -05:00
let old_friendlies = old_count ;
2025-08-30 21:36:01 -04:00
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 {
2025-12-16 23:04:32 -05:00
notification : rlnl ::types ::CapturePointNotificationType ::CaptureStoppedByDefenders ,
2025-08-30 21:36:01 -04:00
id : point_i ,
defending_team : point_team ,
2025-09-03 22:33:26 -04:00
attacking_team : player_team ,
2025-08-30 21:36:01 -04:00
},
true ,
). await ;
}
2025-12-16 23:04:32 -05:00
} else if attacking_team == - 1 {
let old_enemies = old_count ;
2025-08-30 21:36:01 -04:00
if old_enemies == 0 {
2025-12-16 23:04:32 -05:00
let current_friendlies = point . owners_on_point (). await ;
point . attackers . store ( player_team , std ::sync ::atomic ::Ordering ::SeqCst );
2025-08-30 21:36:01 -04:00
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 ,
2025-08-30 21:36:01 -04:00
},
true ,
). await ;
2025-12-16 23:04:32 -05:00
if current_friendlies != 0 {
2025-08-30 21:36:01 -04:00
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::CapturePointNotification ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::CapturePointNotification {
2025-12-16 23:04:32 -05:00
notification : rlnl ::types ::CapturePointNotificationType ::CaptureStoppedByDefenders ,
2025-08-30 21:36:01 -04:00
id : point_i ,
defending_team : point_team ,
2025-09-03 22:33:26 -04:00
attacking_team : player_team ,
2025-08-30 21:36:01 -04:00
},
true ,
). await ;
}
}
2025-12-16 23:04:32 -05:00
} else {
let old_enemies = old_count ;
if old_enemies == 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 ,
attacking_team ,
},
true ,
). await ;
}
2025-08-30 21:36:01 -04:00
}
}
}
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 );
2025-12-16 23:04:32 -05:00
let attacking_team = point . attackers . load ( std ::sync ::atomic ::Ordering ::SeqCst );
2025-08-30 21:36:01 -04:00
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 ));
}
2025-12-16 23:04:32 -05:00
// update counter
let old_count = point . on_point . read (). await [ & player_team_u8 ]. fetch_sub ( 1 , std ::sync ::atomic ::Ordering ::SeqCst );
if old_count == 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 );
return ;
}
2025-08-30 21:36:01 -04:00
if point_team == player_team {
2025-12-16 23:04:32 -05:00
let old_friendlies = old_count ;
2025-08-30 21:36:01 -04:00
let current_enemies = point . enemies_on_point ( player_team_u8 ). await ;
2025-12-16 23:04:32 -05:00
if old_friendlies == 1 && current_enemies != 0 {
2025-09-03 22:33:26 -04:00
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::CapturePointNotification ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::CapturePointNotification {
2025-12-16 23:04:32 -05:00
notification : rlnl ::types ::CapturePointNotificationType ::CaptureStarted ,
2025-09-03 22:33:26 -04:00
id : point_i ,
defending_team : point_team ,
2025-12-16 23:04:32 -05:00
attacking_team ,
2025-09-03 22:33:26 -04:00
},
true ,
). await ;
2025-08-30 21:36:01 -04:00
}
} else {
2025-12-16 23:04:32 -05:00
let old_enemies = old_count ;
//let other_enemies = point.enemies_on_point(player_team_u8).await;
2025-08-30 21:36:01 -04:00
//let current_friendlies = point.friendlies.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2025-12-16 23:04:32 -05:00
if old_enemies == 1 {
2025-09-03 22:33:26 -04:00
//log::info!("Enemy has left the capture point");
2025-12-16 23:04:32 -05:00
// check if there's another team attacking the point
if let Some ( new_attackers ) = point . stealers_team (). await {
let new_attackers = new_attackers as i8 ;
// there is another attacking team; continue capture with them
//let new_enemies = point.on_point.read().await[&new_attackers].load(std::sync::atomic::Ordering::SeqCst);
if attacking_team == new_attackers {
log ::info! ( "Player {} stopped capturing point {}, resuming team {} capturing" , _player_id , point_i , attacking_team );
} else {
log ::info! ( "Player {} stopped capturing point {}, switching to team {} capturing" , _player_id , point_i , new_attackers );
point . attackers . store ( new_attackers , std ::sync ::atomic ::Ordering ::SeqCst );
}
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 ,
attacking_team : new_attackers ,
},
true ,
). await ;
} else if attacking_team == player_team {
// NOTE: this assumes there are only two teams, so if the attacking team leaves
// there will only be one team at most left on the point (which will become the new capturing team)
// stealers_team() returns None for 2 teams or 0 teams, so both cases would take this branch
log ::info! ( "Player {} stopped capturing point {}, no other attacking team" , _player_id , point_i );
// no more attackers; capture has stopped
point . attackers . store ( - 1 , std ::sync ::atomic ::Ordering ::SeqCst );
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 ;
} else {
log ::info! ( "Player {} left point {}, not attacking nor defending!??" , _player_id , point_i );
}
2025-08-30 21:36:01 -04:00
}
}
}
}
async fn tick ( & self , generic : & crate ::matches ::GenericGamemodeEngine < BattleArenaLogic > , max_progress : f32 ) -> Option < PointTickInfo > {
2025-09-06 22:38:55 -04:00
let delta = self . ticker . tick ();
if delta == 0 { return None ; }
2025-08-30 21:36:01 -04:00
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 ();
2025-12-11 20:59:44 -05:00
for cap_point in self . points . iter () {
2025-08-30 21:36:01 -04:00
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 );
}
}
2025-12-11 20:59:44 -05:00
}
for ( i , cap_point ) in self . points . iter (). enumerate () {
let point_owner = cap_point . team . load ( std ::sync ::atomic ::Ordering ::SeqCst );
2025-08-30 21:36:01 -04:00
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 ();
2025-09-03 18:38:56 -04:00
//log::info!("Team {} is capturing point {} (# of players: {})", stealing_team, i, enemies);
2025-08-30 21:36:01 -04:00
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-12-16 23:04:32 -05:00
cap_point . attackers . store ( - 1 , std ::sync ::atomic ::Ordering ::SeqCst );
2025-09-07 15:50:56 -04:00
self . last_capture_team . store ( stealing_team , std ::sync ::atomic ::Ordering ::Relaxed );
self . last_capture_time . store ( chrono ::Utc ::now (). timestamp (), std ::sync ::atomic ::Ordering ::Relaxed );
2025-09-03 22:33:26 -04:00
if owned_points . get ( & ( new_team as u8 )). copied (). unwrap_or ( 0 ) == 0 {
2025-08-30 21:36:01 -04:00
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 );
}
2025-12-11 21:19:38 -05:00
// in case 2+ points are captured in the same tick
2025-12-14 11:38:16 -05:00
if let Some ( new_team_owned_points ) = owned_points . get_mut ( & ( new_team as u8 )) {
* new_team_owned_points += 1 ;
} else {
owned_points . insert ( new_team as u8 , 1 );
}
2025-12-11 21:19:38 -05:00
if point_owner >= 0 {
2025-12-14 11:38:16 -05:00
if let Some ( old_owner_owned_points ) = owned_points . get_mut ( & ( point_owner as u8 )) {
* old_owner_owned_points -= 1 ;
}
2025-12-11 21:19:38 -05:00
}
2025-08-30 21:36:01 -04:00
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 ;
}
2025-09-07 15:50:56 -04:00
let last_capture_team = self . last_capture_team . load ( std ::sync ::atomic ::Ordering ::Relaxed );
let dominating = if last_capture_team != u8 ::MAX && self . points . iter (). all ( | p | p . team . load ( std ::sync ::atomic ::Ordering ::SeqCst ) == ( last_capture_team as i8 )) {
let last_capture_time = self . last_capture_time . load ( std ::sync ::atomic ::Ordering ::Relaxed );
let now = chrono ::Utc ::now (). timestamp ();
if now > last_capture_time && now - last_capture_time >= Self ::TIME_BEFORE_DOMINANT_S {
Some ( last_capture_team )
} else {
None
}
} else {
None
};
2025-08-30 21:36:01 -04:00
Some ( PointTickInfo {
owned : owned_points ,
captured_firsts ,
lost_lasts ,
2025-09-07 15:50:56 -04:00
dominating ,
2025-08-30 21:36:01 -04:00
delta ,
})
}
}
2025-09-07 15:50:56 -04:00
struct EqualizerTracker {
is_disabled : bool ,
start : std ::sync ::atomic ::AtomicI64 ,
activated : std ::sync ::atomic ::AtomicBool ,
cancelled : std ::sync ::atomic ::AtomicBool ,
losing_team : std ::sync ::atomic ::AtomicU8 ,
winning_team : std ::sync ::atomic ::AtomicU8 ,
trigger_index : std ::sync ::atomic ::AtomicU8 ,
health : std ::sync ::atomic ::AtomicU64 ,
}
impl EqualizerTracker {
fn new ( ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData ) -> Self {
let is_disabled = ba_config . equalizer_health <= 0
|| ba_config . equalizer_model . is_empty ()
//|| ba_config.equalizer_trigger_time_seconds.is_empty()
|| ba_config . equalizer_duration_seconds . is_empty ()
2025-09-10 19:40:51 -04:00
|| ba_config . equalizer_duration_seconds . contains ( & 0 );
2025-09-07 15:50:56 -04:00
if is_disabled {
log ::info! ( "Battle Arena equalizer is disabled by config (model ok? {}, health ok? {}, duration ok? {})" ,
! ba_config . equalizer_model . is_empty (),
ba_config . equalizer_health > 0 ,
2025-09-10 19:40:51 -04:00
! ba_config . equalizer_duration_seconds . is_empty () && ! ba_config . equalizer_duration_seconds . contains ( & 0 )
2025-09-07 15:50:56 -04:00
);
}
Self {
is_disabled ,
start : std ::sync ::atomic ::AtomicI64 ::new ( i64 ::MIN ),
activated : std ::sync ::atomic ::AtomicBool ::new ( false ),
cancelled : std ::sync ::atomic ::AtomicBool ::new ( false ),
losing_team : std ::sync ::atomic ::AtomicU8 ::new ( u8 ::MAX ),
winning_team : std ::sync ::atomic ::AtomicU8 ::new ( u8 ::MAX ),
trigger_index : std ::sync ::atomic ::AtomicU8 ::new ( 0 ),
health : std ::sync ::atomic ::AtomicU64 ::new ( 0 ),
}
}
async fn tick ( & self , generic : & crate ::matches ::GenericGamemodeEngine < BattleArenaLogic > , bases : & std ::collections ::HashMap < u8 , BaseInfo > , ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData ) {
if self . is_disabled { return ; }
let trigger_index = self . trigger_index . load ( std ::sync ::atomic ::Ordering ::Relaxed ) as usize ;
let game_start = generic . game_start . load ( std ::sync ::atomic ::Ordering ::Relaxed );
if game_start == i64 ::MIN { return ; }
let mut eq_start = self . start . load ( std ::sync ::atomic ::Ordering ::Relaxed );
if eq_start == i64 ::MIN {
eq_start = if let Some ( trigger_time_s ) = ba_config . equalizer_trigger_time_seconds . get ( trigger_index ) {
game_start + ( * trigger_time_s as i64 )
} else {
game_start + (( generic . game_duration . as_secs () / 2 ) as i64 )
};
self . start . store ( eq_start , std ::sync ::atomic ::Ordering ::Relaxed );
}
let eq_start = eq_start ; // no longer mutable
let now = chrono ::Utc ::now (). timestamp ();
if now >= eq_start {
let eq_end = if let Some ( duration_s ) = ba_config . equalizer_duration_seconds . get ( trigger_index ) {
eq_start + ( * duration_s as i64 )
} else {
i64 ::MAX
};
let is_activated = self . activated . load ( std ::sync ::atomic ::Ordering ::Relaxed );
let is_cancelled = self . cancelled . load ( std ::sync ::atomic ::Ordering ::Relaxed );
if is_activated {
if now > eq_end {
// do deactivation
log ::info! ( "Equalizer deactivated because the timer ran out" );
self . activated . store ( false , std ::sync ::atomic ::Ordering ::Relaxed );
self . send_notification ( rlnl ::types ::EqualizerState ::Defended , ba_config , eq_start , now , generic ). await ;
} else {
// check for change in leading team
let old_winning_team = self . winning_team . load ( std ::sync ::atomic ::Ordering ::Relaxed );
if let Some (( winning_base_id , _ )) = Self ::winning_base ( bases ) {
if winning_base_id != old_winning_team {
// cancel equalizer
log ::info! ( "Equalizer cancelled because winning base lost the lead" );
self . cancelled . store ( true , std ::sync ::atomic ::Ordering ::Relaxed );
self . activated . store ( false , std ::sync ::atomic ::Ordering ::Relaxed );
self . send_notification ( rlnl ::types ::EqualizerState ::Lost , ba_config , eq_start , now , generic ). await ;
}
}
}
} else if now < eq_end && ! is_cancelled {
// do activation
if let Some ( winning_base ) = Self ::winning_base ( bases ) {
if let Some ( losing_base ) = Self ::losing_base ( bases ) {
2025-12-20 21:53:47 -05:00
if losing_base . 0 != winning_base . 0 && winning_base . 1 > 0.0 {
self . activated . store ( true , std ::sync ::atomic ::Ordering ::Relaxed );
self . winning_team . store ( winning_base . 0 , std ::sync ::atomic ::Ordering ::Relaxed );
self . losing_team . store ( losing_base . 0 , std ::sync ::atomic ::Ordering ::Relaxed );
self . health . store ( ba_config . equalizer_health as u64 , std ::sync ::atomic ::Ordering ::Relaxed );
self . send_notification ( rlnl ::types ::EqualizerState ::Start , ba_config , eq_start , now , generic ). await ;
} else {
log ::info! ( "Skipping equalizer for game {} (no base charge or teams are tied)" , generic . game_guid ());
self . cancelled . store ( true , std ::sync ::atomic ::Ordering ::Relaxed );
}
2025-09-07 15:50:56 -04:00
} else {
log ::warn! ( "No losing team found for game {}" , generic . game_guid ());
}
} else {
log ::warn! ( "No winning team found for game {}" , generic . game_guid ());
}
}
}
}
2026-01-18 12:39:58 -05:00
async fn damage_equalizer ( & self , damage : i32 , generic : & crate ::matches ::GenericGamemodeEngine < BattleArenaLogic > , ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData , bases : & std ::collections ::HashMap < u8 , BaseInfo > , crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ], crystal_health : u32 ) {
2025-09-08 21:36:02 -04:00
if ! self . activated . load ( std ::sync ::atomic ::Ordering ::Relaxed ) { return ; }
2025-09-07 15:50:56 -04:00
let damage_u64 = damage as u64 ;
let old_health = self . health . fetch_sub ( damage as u64 , std ::sync ::atomic ::Ordering ::Relaxed );
2025-12-20 21:53:47 -05:00
if old_health <= damage_u64 {
2025-09-07 15:50:56 -04:00
// equalizer is now destroyed
2026-01-18 12:39:58 -05:00
self . destroy_equalizer ( generic , ba_config , bases , crystals , crystal_health ). await ;
2025-09-07 15:50:56 -04:00
}
}
2026-01-18 12:39:58 -05:00
async fn destroy_equalizer ( & self , generic : & crate ::matches ::GenericGamemodeEngine < BattleArenaLogic > , ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData , bases : & std ::collections ::HashMap < u8 , BaseInfo > , crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ], crystal_health : u32 ) {
2025-09-08 21:36:02 -04:00
if ! self . activated . swap ( false , std ::sync ::atomic ::Ordering ::Relaxed ) { return ; }
2025-09-07 15:50:56 -04:00
self . cancelled . store ( true , std ::sync ::atomic ::Ordering ::Relaxed );
let eq_start = self . start . load ( std ::sync ::atomic ::Ordering ::Relaxed );
let now = chrono ::Utc ::now (). timestamp ();
self . send_notification ( rlnl ::types ::EqualizerState ::Destroyed , ba_config , eq_start , now , generic ). await ;
self . health . store ( 0 , std ::sync ::atomic ::Ordering ::Relaxed );
// give health to losing team
let winning_team = self . winning_team . load ( std ::sync ::atomic ::Ordering ::Relaxed );
let losing_team = self . losing_team . load ( std ::sync ::atomic ::Ordering ::Relaxed );
if let Some ( losing_base ) = bases . get ( & losing_team ) {
if let Some ( winning_base ) = bases . get ( & winning_team ) {
let old_index = losing_base . max_index ();
losing_base . cube_index . store ( winning_base . cube_index . load ( std ::sync ::atomic ::Ordering ::Relaxed ), std ::sync ::atomic ::Ordering ::Relaxed );
let new_index = losing_base . max_index ();
for i in old_index .. new_index {
2026-01-18 12:39:58 -05:00
losing_base . crystals_healths [ i ]. store ( crystal_health , std ::sync ::atomic ::Ordering ::Relaxed );
2025-09-07 15:50:56 -04:00
}
let base_heal = losing_base . generate_full_base_heal ( losing_team , ba_config . protonium_health as u32 , crystals );
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::SyncTeamBaseCubes ,
literustlib ::packet ::Property ::ReliableOrdered ,
& base_heal ,
true
). await ;
} else {
log ::warn! ( "Winning base {} no longer exists for game {}" , winning_team , generic . game_guid ());
}
} else {
log ::warn! ( "Losing base {} no longer exists for game {}" , losing_team , generic . game_guid ());
}
}
async fn send_notification ( & self , variant : rlnl ::types ::EqualizerState , ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData , start : i64 , now : i64 , generic : & crate ::matches ::GenericGamemodeEngine < BattleArenaLogic > ) {
if let Some ( notif ) = self . generate_notification ( variant , ba_config , start , now ) {
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::EqualizerNotification ,
literustlib ::packet ::Property ::ReliableOrdered ,
& notif ,
true ,
). await ;
}
}
fn generate_notification ( & self , variant : rlnl ::types ::EqualizerState , ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData , start : i64 , now : i64 ) -> Option < rlnl ::events ::sync ::EqualizerNotification > {
let trigger_index = self . trigger_index . load ( std ::sync ::atomic ::Ordering ::Relaxed ) as usize ;
if let Some ( duration_s ) = ba_config . equalizer_duration_seconds . get ( trigger_index ) {
let end = start + ( * duration_s as i64 );
//let losing_team = self.losing_team.load(std::sync::atomic::Ordering::Relaxed);
let winning_team = self . winning_team . load ( std ::sync ::atomic ::Ordering ::Relaxed );
let current_health = self . health . load ( std ::sync ::atomic ::Ordering ::Relaxed );
let time_remaining = match variant {
rlnl ::types ::EqualizerState ::Start => * duration_s as i16 ,
rlnl ::types ::EqualizerState ::Lost
| rlnl ::types ::EqualizerState ::Destroyed => {
if now >= start {
if now < end {
(( * duration_s as i64 ) - ( now - start )) as i16
} else {
0
}
} else {
* duration_s as i16
}
},
rlnl ::types ::EqualizerState ::Defended => 0 ,
};
Some ( rlnl ::events ::sync ::EqualizerNotification {
notification : variant ,
//team_id: losing_team as i16,
team_id : winning_team as i16 ,
time : time_remaining ,
max_health : ba_config . equalizer_health as i32 ,
health : current_health as i32 ,
})
} else {
None
}
}
fn winning_base ( bases : & std ::collections ::HashMap < u8 , BaseInfo > ) -> Option < ( u8 , f32 ) > {
let mut max = None ;
for ( id , info ) in bases . iter () {
if let Some (( _ , charge )) = max {
let new_charge = info . base_charge ();
if new_charge > charge {
max = Some (( * id , new_charge ));
}
} else {
max = Some (( * id , info . base_charge ()));
}
}
max
}
fn losing_base ( bases : & std ::collections ::HashMap < u8 , BaseInfo > ) -> Option < ( u8 , f32 ) > {
let mut min = None ;
for ( id , info ) in bases . iter () {
if let Some (( _ , charge )) = min {
let new_charge = info . base_charge ();
if new_charge < charge {
min = Some (( * id , new_charge ));
}
} else {
min = Some (( * id , info . base_charge ()));
}
}
min
}
}
2025-08-30 21:36:01 -04:00
struct BaseTracker {
bases : std ::collections ::HashMap < u8 , BaseInfo > ,
2025-09-07 15:50:56 -04:00
equalizer : EqualizerTracker ,
dominating : std ::sync ::atomic ::AtomicBool ,
2025-08-30 21:36:01 -04:00
}
2025-09-06 22:38:55 -04:00
struct BaseTickInfo {
win : Option < ( u8 , WinMode ) > ,
}
2025-08-30 21:36:01 -04:00
impl BaseTracker {
2026-01-15 18:49:29 -05:00
const DOMINATING_MULT : f32 = 4.0 ;
2025-09-07 15:50:56 -04:00
2026-01-24 13:36:32 -05:00
fn new < 'a > ( bases_iter : impl std ::iter ::Iterator < Item =& 'a u8 > , crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ], ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData , base_graph : & oj_rc_core ::cubes ::CubeGraph ) -> Self {
2025-08-30 21:36:01 -04:00
let mut bases = std ::collections ::HashMap ::new ();
for base_id in bases_iter {
2026-01-24 13:36:32 -05:00
bases . insert ( * base_id , BaseInfo ::new ( crystals , base_graph ));
2025-08-30 21:36:01 -04:00
}
Self {
bases ,
2025-09-07 15:50:56 -04:00
equalizer : EqualizerTracker ::new ( ba_config ),
dominating : std ::sync ::atomic ::AtomicBool ::new ( false ),
2025-08-30 21:36:01 -04:00
}
}
2025-09-06 22:38:55 -04:00
2025-09-07 15:50:56 -04:00
async fn tick ( & self , tick_info : & PointTickInfo , crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ], generic : & crate ::matches ::GenericGamemodeEngine < BattleArenaLogic > , capture_points_count : usize , crystal_health : u32 , ba_config : & oj_rc_core ::data ::battle_arena_config ::BattleArenaData ) -> BaseTickInfo {
let multiplier = if let Some ( dominant_team ) = tick_info . dominating {
if ! self . dominating . swap ( true , std ::sync ::atomic ::Ordering ::Relaxed ) {
log ::info! ( "Team {} is now dominating game {}" , dominant_team , generic . game_guid ());
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::CapturePointNotification ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::CapturePointNotification {
notification : rlnl ::types ::CapturePointNotificationType ::Dominating ,
2026-01-15 18:49:29 -05:00
id : u8 ::MAX , // ignored?
attacking_team : dominant_team as i8 ,
defending_team : - 1 , // ignored?
//defending_team: (((dominant_team as usize) + 1) % generic.map_config.bases.len()) as i8
2025-09-07 15:50:56 -04:00
},
true
). await ;
}
Self ::DOMINATING_MULT
} else {
if self . dominating . swap ( false , std ::sync ::atomic ::Ordering ::Relaxed ) {
log ::info! ( "No longer dominating game {}" , generic . game_guid ());
}
1.0
};
2025-09-06 22:38:55 -04:00
for ( base_id , tracked_base ) in self . bases . iter () {
//log::info!("Healing base {}", base_id);
if let Some ( owned_points ) = tick_info . owned . get ( base_id ) {
let one_tick = ( crystals . len () as f32 )
* (( PointTracker ::TICK_MS as f32 ) / ( generic . game_duration . as_millis () as f32 ))
* (( self . bases . len () as f32 ) / ( capture_points_count as f32 ));
2025-09-07 15:50:56 -04:00
let increment = tick_info . delta as f32 * ( * owned_points as f32 ) * one_tick * multiplier ;
2025-09-06 22:38:55 -04:00
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 , crystals . len ());
let new_index = ( new_float_index . ceil () as usize ). clamp ( 0 , crystals . len ());
if new_index != old_index {
2026-01-24 13:36:32 -05:00
log ::trace! ( "Base {} increment passed a crystal index barrier in game {}" , base_id , generic . game_guid ());
2025-09-06 22:38:55 -04:00
let first_damaged = tracked_base . first_damaged ( old_index , crystal_health );
let payload = if new_index - old_index == 1 && first_damaged . is_some () {
// undo cube_index update
2026-01-24 13:36:32 -05:00
tracked_base . cube_index . fetch_sub ( 1.0 , std ::sync ::atomic ::Ordering ::SeqCst );
//log::info!("Skipping increment in favour of healing damaged/destroyed cube");
2025-09-10 19:40:51 -04:00
#[allow(clippy::unnecessary_unwrap)] // have you seen the mess this would be with another if statement?
2025-09-06 22:38:55 -04:00
let first_damaged = first_damaged . unwrap ();
2026-01-24 13:36:32 -05:00
let existing_health = tracked_base . calculate_crystal_health ( first_damaged );
let healing = crystal_health - existing_health ;
2026-01-18 12:39:58 -05:00
tracked_base . crystals_healths [ first_damaged ]. store ( crystal_health , std ::sync ::atomic ::Ordering ::Relaxed );
2025-09-06 22:38:55 -04:00
let target_crystal = & crystals [ first_damaged ];
2026-01-24 13:36:32 -05:00
if existing_health == 0 {
tracked_base . add_crystals_update_graph ( vec! [ target_crystal . to_owned ()], crystal_health );
}
2025-09-06 22:38:55 -04:00
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 , },
2026-01-24 13:36:32 -05:00
damage : healing as i32 ,
2025-09-06 22:38:55 -04:00
}
],
}
} else {
2026-01-24 13:36:32 -05:00
//log::info!("Doing base heal tick");
tracked_base . apply_partial_base_heal ( crystals , crystal_health , * base_id , old_index , new_index )
2025-09-06 22:38:55 -04:00
};
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::SyncTeamBaseCubes ,
literustlib ::packet ::Property ::ReliableOrdered ,
& payload ,
true
). await ;
2026-01-18 12:39:58 -05:00
let charge_ratio = ( new_index as f64 ) / ( crystals . len () as f64 );
let old_charge_ratio = ( old_index as f64 ) / ( crystals . len () as f64 );
let charge_percent = ( charge_ratio * 100.0 ) as usize ;
let old_charge_percent = ( old_charge_ratio * 100.0 ) as usize ;
if charge_percent != old_charge_percent && charge_percent . is_multiple_of ( 10 ) {
log ::debug! ( "Base {} is charged to {}% ({}/{}) in game {}" , base_id , charge_percent , new_index , crystals . len (), generic . game_guid ());
}
2025-09-06 22:38:55 -04:00
if new_index == crystals . len () {
// team base is charged to 100%
return BaseTickInfo {
win : Some (( * base_id , WinMode ::BaseFull )),
};
}
}
}
}
2025-09-07 15:50:56 -04:00
self . equalizer . tick ( generic , & self . bases , ba_config ). await ;
2025-09-06 22:38:55 -04:00
BaseTickInfo {
win : None ,
}
}
2025-08-30 21:36:01 -04:00
}
struct BaseInfo {
cube_index : atomic_float ::AtomicF32 ,
2026-01-24 13:36:32 -05:00
crystals_healths : std ::sync ::Arc < Vec < std ::sync ::atomic ::AtomicU32 >> ,
base_graph : std ::sync ::Arc < oj_rc_core ::cubes ::CubeGraph > ,
2025-08-30 21:36:01 -04:00
}
impl BaseInfo {
2026-01-24 13:36:32 -05:00
fn new ( crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ], base_graph : & oj_rc_core ::cubes ::CubeGraph ) -> Self {
2025-08-30 21:36:01 -04:00
Self {
cube_index : atomic_float ::AtomicF32 ::new ( 0.0 ),
2026-01-24 13:36:32 -05:00
crystals_healths : std ::sync ::Arc ::new (( 0 .. crystals . len ())
2026-01-18 12:39:58 -05:00
. map ( | _ | std ::sync ::atomic ::AtomicU32 ::new ( 0 ))
2026-01-24 13:36:32 -05:00
. collect ()),
base_graph : std ::sync ::Arc ::new ( base_graph . to_owned ()),
2025-08-30 21:36:01 -04:00
}
}
#[inline]
2026-01-18 12:39:58 -05:00
fn calculate_crystal_health ( & self , i : usize ) -> u32 {
self . crystals_healths [ i ]. load ( std ::sync ::atomic ::Ordering ::Relaxed )
2025-08-30 21:36:01 -04:00
}
2025-09-06 16:52:43 -04:00
#[inline]
fn max_index ( & self ) -> usize {
self . cube_index . load ( std ::sync ::atomic ::Ordering ::SeqCst ). ceil () as usize
}
2025-09-07 15:50:56 -04:00
// total base health, out of 1
fn base_charge ( & self ) -> f32 {
let mut total_health : usize = 0 ;
for crystal in self . crystals_healths . iter () {
let health = crystal . load ( std ::sync ::atomic ::Ordering ::Relaxed );
total_health += health as usize ;
}
( total_health as f32 ) / (( self . crystals_healths . len () * u8 ::MAX as usize ) as f32 )
}
2025-08-30 21:36:01 -04:00
fn first_damaged ( & self , old_index : usize , max_health : u32 ) -> Option < usize > {
for i in 0 .. old_index {
2026-01-18 12:39:58 -05:00
let health = self . calculate_crystal_health ( i );
2026-01-24 13:36:32 -05:00
if health < max_health {
2025-08-30 21:36:01 -04:00
return Some ( i );
}
}
None
}
2025-09-06 16:52:43 -04:00
/// returns whether crystal exists
2026-01-18 12:39:58 -05:00
fn damage_crystal_at_pos ( & self , pos : rlnl ::types ::Byte3 , damage : i32 , crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ]) -> Option < usize > {
2025-09-06 16:52:43 -04:00
if let Some (( index , _ )) = crystals . iter (). enumerate (). find ( | ( _i , crystal ) | crystal . x == pos . x && crystal . y == pos . y && crystal . z == pos . z ) {
let max_index = self . max_index ();
2026-01-18 12:39:58 -05:00
if index > max_index { return None ; }
self . damage_crystal ( index , damage );
Some ( index )
2025-09-06 16:52:43 -04:00
} else {
2026-01-18 12:39:58 -05:00
None
2025-09-06 16:52:43 -04:00
}
}
/// returns whether crystal was destroyed
2026-01-18 12:39:58 -05:00
fn damage_crystal ( & self , i : usize , damage : i32 ) -> bool {
let damage = damage as u32 ;
let old_health = self . crystals_healths [ i ]. fetch_sub ( damage , std ::sync ::atomic ::Ordering ::Relaxed );
2026-01-24 13:36:32 -05:00
//log::info!("Crystal {} damaged (was {}, now {}, delta {})", i, old_health, old_health.saturating_sub(damage), damage);
2026-01-18 12:39:58 -05:00
if damage > old_health {
2025-09-06 16:52:43 -04:00
// guarantee underflow behaviour
self . crystals_healths [ i ]. store ( 0 , std ::sync ::atomic ::Ordering ::Relaxed );
}
2026-01-18 12:39:58 -05:00
old_health != 0 && damage >= old_health
2025-09-06 16:52:43 -04:00
}
2026-01-24 13:36:32 -05:00
fn destroy_crystals_update_graph (
& self ,
positions : Vec < rlnl ::types ::Byte3 > ,
crystal_positions : & std ::sync ::Arc < Vec < oj_rc_core ::cubes ::CubeLocationInfo >> ,
) {
let base_graph = self . base_graph . clone ();
let crystal_healths = self . crystals_healths . clone ();
let crystal_positions = crystal_positions . to_owned ();
tokio ::task ::spawn_blocking ( move || {
for pos in positions {
let actual_destroyed = base_graph . remove_cube ( & oj_rc_core ::cubes ::CellPoint {
x : pos . x ,
y : pos . y ,
z : pos . z ,
});
for ( i , loc ) in crystal_positions . iter (). enumerate () {
let point = oj_rc_core ::cubes ::CellPoint {
x : loc . x ,
y : loc . y ,
z : loc . z ,
};
if actual_destroyed . contains ( & point ) {
crystal_healths [ i ]. store ( 0 , std ::sync ::atomic ::Ordering ::Relaxed );
}
}
}
});
}
fn add_crystals_update_graph (
& self ,
positions : Vec < oj_rc_core ::cubes ::CubeLocationInfo > ,
health : u32 ,
) {
let base_graph = self . base_graph . clone ();
tokio ::task ::spawn_blocking ( move || {
for pos in positions {
let point = oj_rc_core ::cubes ::CellPoint {
x : pos . x ,
y : pos . y ,
z : pos . z ,
};
base_graph . add_cube (
& point ,
oj_rc_core ::cubes ::CRYSTAL_ID ,
health ,
pos . orientation (),
);
}
});
}
2025-09-06 16:52:43 -04:00
/// returns whether crystal exists
2026-01-24 13:36:32 -05:00
fn destroy_crystal_at_pos ( & self , pos : rlnl ::types ::Byte3 , crystals : & std ::sync ::Arc < Vec < oj_rc_core ::cubes ::CubeLocationInfo >> ) -> bool {
2025-09-06 16:52:43 -04:00
if let Some (( index , _ )) = crystals . iter (). enumerate (). find ( | ( _i , crystal ) | crystal . x == pos . x && crystal . y == pos . y && crystal . z == pos . z ) {
let max_index = self . max_index ();
if index > max_index { return false ; }
self . destroy_crystal ( index );
true
} else {
false
}
}
fn destroy_crystal ( & self , i : usize ) {
self . crystals_healths [ i ]. store ( 0 , std ::sync ::atomic ::Ordering ::Relaxed );
}
fn generate_full_base_heal ( & self , base_id : u8 , max_health : u32 , crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ]) -> rlnl ::events ::HealedCubes {
let max_index = self . max_index ();
let target_crystals : Vec < rlnl ::types ::HitCubeInfo > = ( 0 .. crystals . len ())
. take_while ( |& i | i <= max_index )
2026-01-18 12:39:58 -05:00
. filter ( |& i | self . calculate_crystal_health ( i ) != 0 )
2025-09-06 16:52:43 -04:00
. map ( | i | {
let target_loc = & crystals [ i ];
rlnl ::types ::HitCubeInfo {
pos : rlnl ::types ::Byte3 { x : target_loc . x , y : target_loc . y , z : target_loc . z , },
damage : max_health as i32 ,
}
})
. collect ();
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 ,
}
}
2026-01-24 13:36:32 -05:00
fn apply_partial_base_heal ( & self , crystals : & [ oj_rc_core ::cubes ::CubeLocationInfo ], crystal_health : u32 , base_id : u8 , old_index : usize , new_index : usize ) -> rlnl ::events ::HealedCubes {
//let base = self.bases.get(&base_id).unwrap();
let target_crystals = & crystals [ old_index .. new_index ];
let mut healed = Vec ::with_capacity ( target_crystals . len ());
#[allow(clippy::needless_range_loop)] // this suggestion is deranged and way too long
for crystal_i in old_index .. new_index {
self . crystals_healths [ crystal_i ]. store ( crystal_health , std ::sync ::atomic ::Ordering ::Relaxed );
healed . push ( crystals [ crystal_i ]. clone ());
}
self . add_crystals_update_graph ( healed , crystal_health );
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 | {
log ::trace! ( "Healing base cube at grid ({}, {}, {})" , loc . x , loc . y , loc . z );
rlnl ::types ::HitCubeInfo {
pos : rlnl ::types ::Byte3 { x : loc . x , y : loc . y , z : loc . z , },
damage : crystal_health as i32 ,
}
})
. collect (),
}
}
2025-08-30 21:36:01 -04:00
}
enum WinMode {
BaseFull ,
OutOfTime ,
2025-08-31 12:12:43 -04:00
OutOfPlayers ,
2025-09-06 19:13:47 -04:00
Surrender ,
2025-08-30 21:36:01 -04:00
}
2026-01-24 13:36:32 -05:00
fn crystal_health_map ( crystal_health : u32 ) -> std ::collections ::HashMap < u32 , u32 > {
let mut map = std ::collections ::HashMap ::with_capacity ( 2 );
map . insert ( oj_rc_core ::cubes ::CLASP_ID , 1 );
map . insert ( oj_rc_core ::cubes ::CRYSTAL_ID , crystal_health );
map
}
2025-08-30 21:36:01 -04:00
pub struct BattleArenaLogic {
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 ,
2025-09-06 19:13:47 -04:00
surrender_tracking : super ::trackers ::SurrenderGameTracker ,
2025-08-30 21:36:01 -04:00
//cube_parser: std::sync::Arc<oj_rc_core::cubes::CubeLocationsParser>,
2026-01-18 12:39:58 -05:00
crystals : std ::sync ::Arc < Vec < oj_rc_core ::cubes ::CubeLocationInfo >> ,
2025-08-30 21:36:01 -04:00
config : oj_rc_core ::data ::battle_arena_config ::BattleArenaData ,
}
impl BattleArenaLogic {
2026-01-18 12:39:58 -05:00
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 ], ba_config : oj_rc_core ::data ::battle_arena_config ::BattleArenaData , crystals : std ::sync ::Arc < Vec < oj_rc_core ::cubes ::CubeLocationInfo >> ) -> Self {
//let min_y = crystals.iter().min_by_key(|x| x.y).unwrap();
//log::warn!("First crystal is as ({}, {}, {})", min_y.x, min_y.y, min_y.z);
2026-01-24 13:36:32 -05:00
let base_graph = oj_rc_core ::cubes ::CubeGraph ::with_data (
& mut std ::io ::Cursor ::new ( & ba_config . base_machine_map ),
crystal_health_map ( ba_config . protonium_health as u32 ),
oj_rc_core ::cubes ::CLASP_ID ,
). expect ( "Invalid Battle Arena base cube data" );
2025-08-30 21:36:01 -04:00
Self {
respawn_full_heal_duration : config . respawn_full_heal_duration ,
respawn_heal_duration : config . respawn_heal_duration ,
timer_task : tokio ::sync ::Mutex ::new ( None ),
2025-09-27 12:36:49 -04:00
player_tracking : PlayerTracker ::new ( players ),
2025-08-30 21:36:01 -04:00
capture_tracking : PointTracker ::new ( map . capture_points . iter (). map ( | ( _ , speed ) | * speed )),
2025-09-06 19:13:47 -04:00
surrender_tracking : super ::trackers ::SurrenderGameTracker ::new (),
2026-01-24 13:36:32 -05:00
base_tracking : BaseTracker ::new ( map . bases . keys (), & crystals , & ba_config , & base_graph ),
2025-08-30 21:36:01 -04:00
//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 {
2025-09-14 12:08:52 -04:00
if generic . is_game_past_end_time () {
2025-08-30 21:36:01 -04:00
// 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 {
2025-09-06 19:13:47 -04:00
WinMode ::BaseFull => rlnl ::types ::GameEndReason ::BaseDestroyed ,
WinMode ::OutOfPlayers => rlnl ::types ::GameEndReason ::OneTeamRemaining ,
WinMode ::Surrender => rlnl ::types ::GameEndReason ::Surrendered ,
2025-08-30 21:36:01 -04:00
WinMode ::OutOfTime => rlnl ::types ::GameEndReason ::TimeOut ,
};
let payload = rlnl ::events ::ingame ::GameLoseWin {
winning_team ,
end_reason ,
};
2025-09-27 12:36:49 -04:00
for ( player_id , player ) in generic . user_descriptors (). iter () {
if player . descriptor . user_id . is_none () { continue ; } // skip non-user players
2025-08-30 21:36:01 -04:00
let is_winner = player . descriptor . team == winning_team as i32 ;
let net_event = match ty {
2025-08-31 12:12:43 -04:00
WinMode ::BaseFull
2025-09-06 19:13:47 -04:00
| WinMode ::OutOfPlayers
| WinMode ::Surrender => {
2025-08-30 21:36:01 -04:00
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 }
}
};
2025-09-27 12:36:49 -04:00
generic . send_to_player (
* player_id ,
net_event ,
literustlib ::packet ::Property ::ReliableOrdered ,
& payload ,
). await ;
2025-08-30 21:36:01 -04:00
}
}
2025-08-31 12:12:43 -04:00
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 {
2025-09-27 12:36:49 -04:00
let was_in_point = self . player_tracking . swap_is_in_point ( player_id , None );
2025-08-31 12:12:43 -04:00
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 ;
2025-09-27 12:36:49 -04:00
if let Some ( player_respawn ) = self . player_tracking . respawning . get ( & player_id ) {
2025-09-01 18:50:03 -04:00
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 ,
}
};
2026-01-12 18:01:23 -05:00
if let Some ( player_desc ) = generic . user_descriptor ( 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 ,
player_desc . machine . is_alive . clone (),
));
} else {
log ::error! ( "Player {} cannot respawn because they are not in the game!?" , player_id );
}
2025-09-01 18:50:03 -04:00
} else {
log ::error! ( "Player {} cannot respawn because they are not in a team!?" , player_id );
}
}
2026-01-18 12:39:58 -05:00
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 ) {
2025-09-06 16:52:43 -04:00
let base_id = cube_damage . hit_machine_id as u8 ;
let mut total_destroyed = 0 ;
2026-01-18 12:39:58 -05:00
let mut total_damaged = 0 ;
let mut actual_cube_damage = Vec ::with_capacity ( cube_damage . hit_cubes . len ());
2026-01-24 13:36:32 -05:00
let mut destroyed_cubes_positions = Vec ::with_capacity ( cube_damage . hit_cubes . len ());
2025-09-06 16:52:43 -04:00
if let Some ( base ) = self . base_tracking . bases . get ( & base_id ) {
for hit_cube in cube_damage . hit_cubes . iter () {
if let Some ( damage ) = hit_cube . status . damage {
2026-01-18 12:39:58 -05:00
if base . damage_crystal_at_pos ( hit_cube . loc , damage , & self . crystals ). is_some () {
actual_cube_damage . push ( hit_cube . to_owned ());
total_damaged += 1 ;
} else {
log ::warn! ( "Could not damage cube with damage status" );
2025-09-06 16:52:43 -04:00
}
} else if matches! ( hit_cube . status . ty , rlnl ::types ::CubeHistoryEventType ::Destroy ) {
if base . destroy_crystal_at_pos ( hit_cube . loc , & self . crystals ) {
2026-01-18 12:39:58 -05:00
actual_cube_damage . push ( hit_cube . to_owned ());
2026-01-24 13:36:32 -05:00
destroyed_cubes_positions . push ( hit_cube . loc );
2025-09-06 16:52:43 -04:00
total_destroyed += 1 ;
} else {
2026-01-18 12:39:58 -05:00
log ::warn! ( "Could not destroy cube with destroy status" );
2025-09-06 16:52:43 -04:00
}
}
}
2026-01-24 13:36:32 -05:00
base . destroy_crystals_update_graph ( destroyed_cubes_positions , & self . crystals );
log ::debug! ( "Destroyed {} ({} damaged) base cubes; {}/{} valid, game {}" , total_destroyed , total_damaged , actual_cube_damage . len (), cube_damage . hit_cubes . len (), generic . game_guid ());
2026-01-18 12:39:58 -05:00
let actual_damage_data = actual_damage_data ( actual_cube_damage );
generic . broadcast (
2025-09-07 15:50:56 -04:00
actual_damage_data . event ,
actual_damage_data . property ,
actual_damage_data . data . as_ref (),
true ,
2026-01-18 12:39:58 -05:00
). await ;
2025-09-06 16:52:43 -04:00
}
if total_destroyed != 0 {
if let Some ( team_id ) = self . player_tracking . team ( cube_damage . shooting_machine_id as u8 ). await {
if let Some ( base ) = self . base_tracking . bases . get ( & team_id ) {
//log::info!("Player {} stole {} crystals", cube_damage.shooting_machine_id, total_destroyed);
2026-01-15 18:49:29 -05:00
let total_destroyed_f32 = total_destroyed as f32 ;
let old_float_index = base . cube_index . fetch_add ( total_destroyed_f32 , std ::sync ::atomic ::Ordering ::SeqCst );
let new_float_index = old_float_index + total_destroyed_f32 ;
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 ());
2026-01-24 13:36:32 -05:00
let payload = base . apply_partial_base_heal (
2026-01-15 18:49:29 -05:00
& self . crystals ,
self . config . protonium_health as u32 ,
team_id ,
old_index ,
new_index ,
);
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::SyncTeamBaseCubes ,
literustlib ::packet ::Property ::ReliableOrdered ,
& payload ,
true ,
2025-09-06 16:52:43 -04:00
). await ;
}
} else {
log ::warn! ( "Machine {} has no team" , cube_damage . shooting_machine_id );
}
}
2025-09-01 18:50:03 -04:00
}
2025-09-07 15:50:56 -04:00
async fn do_equalizer_damage ( & self , cube_damage : & rlnl ::events ::ingame ::DestroyCubeNoEffect , generic : & crate ::matches ::GenericGamemodeEngine < Self > ) {
2026-01-18 12:39:58 -05:00
let crystal_health = self . config . protonium_health as u32 ;
2025-09-07 15:50:56 -04:00
for hit_cube in cube_damage . hit_cubes . iter () {
if let Some ( damage ) = hit_cube . status . damage {
2026-01-18 12:39:58 -05:00
self . base_tracking . equalizer . damage_equalizer ( damage , generic , & self . config , & self . base_tracking . bases , & self . crystals , crystal_health ). await ;
2025-09-07 15:50:56 -04:00
} else if matches! ( hit_cube . status . ty , rlnl ::types ::CubeHistoryEventType ::Destroy ) {
2026-01-18 12:39:58 -05:00
self . base_tracking . equalizer . destroy_equalizer ( generic , & self . config , & self . base_tracking . bases , & self . crystals , crystal_health ). await ;
2025-09-07 15:50:56 -04:00
}
}
}
2025-08-30 21:36:01 -04:00
}
#[async_trait::async_trait]
impl CustomGameLogic for BattleArenaLogic {
2025-09-27 12:36:49 -04:00
async fn on_player_join ( & self , _generic : & crate ::matches ::GenericGamemodeEngine < Self > , _conn : & crate ::matches ::generic ::UserConnection , player : & crate ::matches ::generic ::UserDescriptor ) -> bool {
2025-08-30 21:36:01 -04:00
log ::info! ( "Player {} joined" , player . descriptor . player_id );
self . player_tracking . track_player ( & player . descriptor ). await ;
true
}
2025-09-27 12:36:49 -04:00
async fn on_player_end ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , _connection : & crate ::matches ::generic ::UserConnection , player : & crate ::matches ::generic ::UserDescriptor ) -> bool {
2025-09-14 12:08:52 -04:00
if generic . is_game_done () {
return true ;
}
2026-01-25 18:17:34 -05:00
let game_start = generic . game_start . load ( std ::sync ::atomic ::Ordering ::Relaxed );
if game_start == i64 ::MIN || game_start > chrono ::Utc ::now (). timestamp () {
// game has not started yet, player probably timed out while loading (which we can ignore)
return true ;
}
2025-08-31 12:12:43 -04:00
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 ;
}
2025-08-30 21:36:01 -04:00
true
}
async fn on_vehicle_destroyed ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , _killer : u8 , victim : u8 ) -> bool {
2025-08-31 12:12:43 -04:00
self . do_destruct_tasks ( generic , victim ). await ;
2025-09-01 18:50:03 -04:00
self . do_respawn_tasks ( generic , victim ). await ;
2025-08-30 21:36:01 -04:00
true
}
2025-08-31 12:12:43 -04:00
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 ;
2025-08-30 21:36:01 -04:00
true
}
2025-09-09 23:36:34 -04:00
async fn on_kill_bonus ( & self , _generic : & crate ::matches ::GenericGamemodeEngine < Self > , _killer : u8 , _victim : u8 ) -> bool {
true
}
2025-09-27 12:36:49 -04:00
async fn extra_sync_events ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , _connection : & crate ::matches ::generic ::UserConnection , _player : & crate ::matches ::generic ::UserDescriptor ) -> Vec < crate ::matches ::RlnlPacket > {
2025-08-30 21:36:01 -04:00
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 )),
2025-08-30 21:36:01 -04:00
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 {
2025-09-07 15:50:56 -04:00
pos : ( generic . map_config . equalizer . x , generic . map_config . equalizer . y , generic . map_config . equalizer . z ). into (),
2025-08-30 21:36:01 -04:00
rot : ( 0.0 , 0.0 , 0.0 , 0.0 ). into (),
},
2025-09-07 15:50:56 -04:00
total_health : self . config . equalizer_health as i32 ,
2025-08-30 21:36:01 -04:00
}),
}),
// SetShieldState
2025-09-06 11:16:29 -04:00
/*if generic.map_config.bases.contains_key(&0) {
2025-08-30 21:36:01 -04:00
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) {
2025-08-30 21:36:01 -04:00
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
2025-09-06 11:16:29 -04:00
},*/
2025-08-30 21:36:01 -04:00
// CurrentGameTime
Some ( crate ::matches ::RlnlPacket {
event : rlnl ::event_code ::NetworkEvent ::CurrentGameTime ,
property : literustlib ::packet ::Property ::ReliableOrdered ,
2025-09-06 19:13:47 -04:00
data : Box ::new ( rlnl ::events ::GameTime ( generic . game_duration . as_millis () as f32 / 1000.0 )),
2025-08-30 21:36:01 -04:00
}),
// 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 ()
2025-08-30 21:36:01 -04:00
}
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 ());
2025-09-27 12:36:49 -04:00
for ( player_id , conn ) in read_lock . iter () {
let state = generic . user_descriptor ( * player_id ). unwrap (). state . clone ();
senders . push (( conn . connection . clone (), state ));
2025-08-30 21:36:01 -04:00
}
drop ( read_lock );
2025-09-06 19:13:47 -04:00
let game_end = game_start + generic . game_duration ;
2025-08-30 21:36:01 -04:00
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 );
2025-09-06 19:13:47 -04:00
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::SetSurrenderTimes ,
literustlib ::packet ::Property ::ReliableOrdered ,
& SurrenderGameTracker ::surrender_times (),
false ,
). await ;
2025-08-30 21:36:01 -04:00
true
}
async fn on_game_completed ( & self , _generic : & crate ::matches ::GenericGamemodeEngine < Self > ) -> bool {
self . abort_timer_sync (). await ;
true
}
2025-09-06 16:52:43 -04:00
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 {
match ( event_in , data ) {
2025-09-07 15:50:56 -04:00
( rlnl ::event_code ::NetworkEvent ::DamageCube , Some ( data )) => {
let maybe_cube_dmg = < dyn core ::any ::Any > ::downcast_ref ::< rlnl ::events ::ingame ::DestroyCubesFull > ( data . as_ref ());
if let Some ( cube_damage ) = maybe_cube_dmg {
let pseudo = rlnl ::events ::ingame ::DestroyCubeNoEffect {
shooting_machine_id : cube_damage . shooting_machine_id ,
hit_machine_id : cube_damage . hit_machine_id ,
target_type : cube_damage . target_type ,
num_hits : cube_damage . num_hit_cubes ,
hit_cubes : cube_damage . hit_cubes . clone (),
};
match cube_damage . target_type {
rlnl ::types ::TargetType ::TeamBase => {
let actual_damage_data = | cubes : Vec < rlnl ::types ::CubeState >| {
let mut cube_dmg = cube_damage . to_owned ();
cube_dmg . num_hit_cubes = cubes . len () as _ ;
cube_dmg . hit_cubes = cubes ;
crate ::matches ::RlnlPacket {
event : rlnl ::event_code ::NetworkEvent ::DestroyCubesFull ,
property : literustlib ::packet ::Property ::ReliableOrdered ,
data : Box ::new ( cube_dmg ),
}
};
self . do_team_base_stealing ( & pseudo , generic , actual_damage_data ). await ;
2026-01-18 12:39:58 -05:00
false
2025-09-07 15:50:56 -04:00
},
rlnl ::types ::TargetType ::EqualizerCrystal => {
self . do_equalizer_damage ( & pseudo , generic ). await ;
true
}
_ => {
//log::info!("Got DamageCube with target_type {:?}", cube_damage.target_type);
true
},
}
} else {
log ::warn! ( "Got DamageCube event with bad serialization type" );
true
}
},
2025-09-06 16:52:43 -04:00
( rlnl ::event_code ::NetworkEvent ::DamageCubeNoEffect , Some ( data )) => {
let maybe_cube_dmg = < dyn core ::any ::Any > ::downcast_ref ::< rlnl ::events ::ingame ::DestroyCubeNoEffect > ( data . as_ref ());
if let Some ( cube_damage ) = maybe_cube_dmg {
2025-09-07 15:50:56 -04:00
match cube_damage . target_type {
rlnl ::types ::TargetType ::TeamBase => {
let actual_damage_data = | cubes : Vec < rlnl ::types ::CubeState >| {
let mut cube_dmg = cube_damage . to_owned ();
cube_dmg . num_hits = cubes . len () as _ ;
cube_dmg . hit_cubes = cubes ;
crate ::matches ::RlnlPacket {
event : rlnl ::event_code ::NetworkEvent ::DestroyCubeNoEffect ,
property : literustlib ::packet ::Property ::ReliableOrdered ,
data : Box ::new ( cube_dmg . to_owned ()),
}
};
2026-01-18 12:39:58 -05:00
self . do_team_base_stealing ( cube_damage , generic , actual_damage_data ). await ;
false
2025-09-07 15:50:56 -04:00
},
rlnl ::types ::TargetType ::EqualizerCrystal => {
self . do_equalizer_damage ( cube_damage , generic ). await ;
true
}
_ => {
//log::info!("Got DamageCubeNoEffect with target_type {:?}", cube_damage.target_type);
true
},
2025-09-06 16:52:43 -04:00
}
} else {
log ::warn! ( "Got DamageCubeNoEffect event with bad serialization type" );
true
}
},
_ => true
}
2025-08-30 21:36:01 -04:00
}
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 );
2025-09-14 12:08:52 -04:00
if generic . game_start . load ( std ::sync ::atomic ::Ordering ::Relaxed ) == i64 ::MIN || chrono ::Utc ::now (). timestamp () < game_start {
2025-08-30 21:36:01 -04:00
// 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 ;
2026-01-25 18:17:34 -05:00
#[cfg(debug_assertions)]
log ::debug! ( "Game {} is already complete" , generic . game_guid ());
2025-08-30 21:36:01 -04:00
return true ;
}
if self . check_if_match_time_is_done ( generic ). await {
2026-01-25 18:17:34 -05:00
#[cfg(debug_assertions)]
log ::debug! ( "Out of time for game {}" , generic . game_guid ());
2025-08-30 21:36:01 -04:00
return true ;
}
if generic . map_config . capture_points . is_empty () {
2026-01-25 18:17:34 -05:00
#[cfg(debug_assertions)]
log ::debug! ( "No capture points for game {}" , generic . game_guid ());
2025-08-30 21:36:01 -04:00
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 ;
}
}
2025-09-27 12:36:49 -04:00
let was_in_point = self . player_tracking . swap_is_in_point ( motion . player_id , now_in_point );
2025-08-30 21:36:01 -04:00
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 ;
}
}
2026-01-25 18:17:34 -05:00
} else {
log ::warn! ( "Unknown team for player {} in game {}" , motion . player_id , generic . game_guid ());
2025-08-30 21:36:01 -04:00
}
if let Some ( tick_info ) = self . capture_tracking . tick ( generic , self . config . num_segments as f32 ). await {
// handle shield (de)activation
2025-09-06 22:38:55 -04:00
for team in tick_info . captured_firsts . iter () {
2025-08-30 21:36:01 -04:00
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::SetShieldState ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::sync ::FusionShieldState {
2025-09-06 22:38:55 -04:00
team_id : * team as i8 ,
2025-08-30 21:36:01 -04:00
full_power : 1 ,
},
true ,
). await ;
}
2025-09-06 22:38:55 -04:00
for team in tick_info . lost_lasts . iter () {
2025-08-30 21:36:01 -04:00
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::SetShieldState ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::sync ::FusionShieldState {
2025-09-06 22:38:55 -04:00
team_id : * team as i8 ,
2025-08-30 21:36:01 -04:00
full_power : 0 ,
},
true ,
). await ;
}
// do base charge tick
2025-09-07 15:50:56 -04:00
let base_tick_info = self . base_tracking . tick ( & tick_info , & self . crystals , generic , self . capture_tracking . points . len (), self . config . protonium_health as u32 , & self . config ). await ;
2025-09-06 22:38:55 -04:00
if let Some (( winning_team , win_mode )) = base_tick_info . win {
self . do_win ( winning_team , win_mode , generic ). await ;
2025-08-30 21:36:01 -04:00
}
2025-09-06 19:13:47 -04:00
// handle surrender vote tick
self . surrender_tracking . tick ( generic ). await ;
2025-08-30 21:36:01 -04:00
}
true
}
2025-09-06 19:13:47 -04:00
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 > ) {
2025-08-30 21:36:01 -04:00
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 ) => {
2025-09-06 19:13:47 -04:00
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 {
let team_members = self . player_tracking . players_on_team ( team ). await ;
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 ( winning_team , WinMode ::Surrender , generic ). await ;
}
}
} else {
log ::warn! ( "Bad SurrenderRequest data" );
}
},
( rlnl ::event_code ::NetworkEvent ::SurrenderVoteCast , literustlib ::packet ::Property ::ReliableOrdered ) => {
2025-09-27 12:36:49 -04:00
if let Some ( player_id ) = generic . user_key_by_user_id ( user_id ) {
2025-09-06 19:13:47 -04:00
if let Some ( team ) = self . player_tracking . team ( player_id ). await {
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 ( winning_team , WinMode ::Surrender , generic ). await ;
}
}
}
}
},
2025-08-30 21:36:01 -04:00
( rlnl ::event_code ::NetworkEvent ::AwardTeamBaseProtoniumDestroyedRequest , literustlib ::packet ::Property ::ReliableOrdered ) => {
2025-09-06 16:52:43 -04:00
let maybe_crystal_bonus = < dyn core ::any ::Any > ::downcast_ref ::< rlnl ::events ::ingame ::AwardProtoniumDestroyedCubes > ( data . as_ref ());
if let Some ( crystal_destroyed ) = maybe_crystal_bonus {
2025-09-27 12:36:49 -04:00
if let Some ( generic_player ) = generic . user_descriptor ( crystal_destroyed . player_id ) {
2025-09-06 16:52:43 -04:00
generic_player . counters . crystals . fetch_add ( crystal_destroyed . destroyed_cubes as u32 , std ::sync ::atomic ::Ordering ::Relaxed );
let data = generic_player . counters . get_generic_packet ( crystal_destroyed . player_id , rlnl ::types ::IngameStatId ::DestroyedProtoniumCubes , Some ( crystal_destroyed . destroyed_cubes as _ ));
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::UpdateGameStats ,
literustlib ::packet ::Property ::ReliableOrdered ,
& data ,
true ,
). await ;
}
}
2025-08-30 21:36:01 -04:00
}
_ => {}
}
}
2025-09-09 23:36:34 -04:00
async fn on_spot_vehicle ( & self , _generic : & crate ::matches ::GenericGamemodeEngine < Self > , _user_id : i32 , _remote_player : u8 ) -> bool {
true
}
2025-08-30 21:36:01 -04:00
}