2025-07-21 22:33:12 -04:00
use crate ::matches ::CustomGameLogic ;
struct PlayerTracker {
alive : tokio ::sync ::Mutex < std ::collections ::HashMap < u8 , std ::collections ::HashSet < u8 >>> , // team -> set of player_id
2025-07-26 21:57:46 -04:00
in_base : tokio ::sync ::RwLock < std ::collections ::HashMap < u8 , std ::sync ::atomic ::AtomicU16 >> , // player_id -> in base state (if base > u8::MAX then not in a base)
2025-07-21 22:33:12 -04:00
}
impl PlayerTracker {
async fn track_vehicle ( & self , player : & oj_rc_core ::persist ::user ::PlayerDescriptor ) {
let mut alive_lock = self . alive . lock (). await ;
if let Some ( team ) = alive_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 );
alive_lock . insert ( player . team as u8 , new_team );
}
2025-07-26 21:57:46 -04:00
self . in_base . write (). await . insert ( player . player_id , std ::sync ::atomic ::AtomicU16 ::new ( u16 ::MAX ));
2025-07-21 22:33:12 -04:00
}
async fn destroy_vehicle ( & self , player : & oj_rc_core ::persist ::user ::PlayerDescriptor ) {
let mut alive_lock = self . alive . lock (). await ;
if let Some ( team ) = alive_lock . get_mut ( & ( player . team as u8 )) {
team . remove ( & player . player_id );
} else {
log ::warn! ( "Destroyed player's vehicle for previously-unseen team" );
}
}
async fn winner_team ( & self ) -> Option < u8 > {
let alive_lock = self . alive . lock (). await ;
let mut only_alive_team = None ;
for ( team , players ) in alive_lock . iter () {
if ! players . is_empty () {
if only_alive_team . is_some () {
// more than one team is alive, game is not over
return None ;
} else {
only_alive_team = Some ( * team );
}
}
}
only_alive_team
}
2025-07-22 18:27:41 -04:00
2025-07-24 22:40:19 -04:00
async fn alive_count ( & self ) -> std ::collections ::HashMap < u8 , u8 > {
let alive_lock = self . alive . lock (). await ;
let mut alive_players = std ::collections ::HashMap ::new ();
for ( team , players ) in alive_lock . iter () {
if ! players . is_empty () {
alive_players . insert ( * team , players . len () as u8 );
}
}
alive_players
}
2025-07-26 21:57:46 -04:00
/*async fn teams(&self) -> std::collections::HashSet<u8> {
2025-07-22 18:27:41 -04:00
self.alive.lock().await.keys().map(|x| *x).collect()
2025-07-26 21:57:46 -04:00
}*/
async fn player_team ( & self , player_id : u8 ) -> Option < u8 > {
for ( team , players ) in self . alive . lock (). await . iter () {
if players . contains ( & player_id ) {
return Some ( * team );
}
}
None
}
async fn swap_is_in_base ( & self , player_id : u8 , base : Option < u8 > ) -> Option < u8 > {
self . in_base . read (). await . get ( & player_id ). and_then ( | x | {
let base = x . swap ( base . map ( | x | x as u16 ). unwrap_or ( u16 ::MAX ), std ::sync ::atomic ::Ordering ::Relaxed );
if base > u8 ::MAX as u16 {
None
} else {
Some ( base as u8 )
}
})
}
}
struct BaseCounters {
enemies : std ::sync ::atomic ::AtomicI16 ,
friendlies : std ::sync ::atomic ::AtomicI16 ,
capture : atomic_float ::AtomicF32 ,
percent_per_second : f32 ,
}
impl BaseCounters {
fn new ( percent_per_second : f32 ) -> Self {
Self {
enemies : std ::sync ::atomic ::AtomicI16 ::new ( 0 ),
friendlies : std ::sync ::atomic ::AtomicI16 ::new ( 0 ),
capture : atomic_float ::AtomicF32 ::new ( 0.0 ),
percent_per_second ,
}
}
}
struct BaseTracker {
bases : std ::collections ::HashMap < u8 , BaseCounters > ,
last_tick : std ::sync ::atomic ::AtomicI64 ,
is_baseless : bool ,
}
impl BaseTracker {
const TICK_MS : i64 = 50 ;
//const CAPTURE_PER_TICK: f32 = 0.007;
async fn on_enter ( & self , generic : & crate ::matches ::GenericGamemodeEngine < EliminationLogic > , base_id : u8 , is_friendly : bool , player_id : u8 ) {
if let Some ( base ) = self . bases . get ( & base_id ) {
if let Some ( conn ) = generic . users . read (). await . get ( & player_id ) {
crate ::events ::log_lnl_send_failure ( conn . connection . rlnl (). send_data (
& rlnl ::events ::ingame ::TeamBaseBoolean {
team : base_id ,
value : 1 ,
},
rlnl ::event_code ::NetworkEvent ::PlayerInsideBase ,
literustlib ::packet ::Property ::ReliableOrdered ,
& conn . connection . connection ,
). await );
} else { return ; }
if is_friendly {
let friendlies = base . friendlies . fetch_add ( 1 , std ::sync ::atomic ::Ordering ::SeqCst );
if friendlies == 0 {
// newly contesting
if base . enemies . load ( std ::sync ::atomic ::Ordering ::SeqCst ) > 0 {
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseContested ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseBoolean {
team : base_id ,
value : 1 ,
},
true
). await ;
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseCaptureStop ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : base_id ,
current_progress : rlnl ::types ::ByteFloat ::from ( base . capture . load ( std ::sync ::atomic ::Ordering ::SeqCst )),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
},
true
). await ;
}
}
} else {
let enemies = base . enemies . fetch_add ( 1 , std ::sync ::atomic ::Ordering ::SeqCst );
if enemies == 0 {
// newly capturing
if base . friendlies . load ( std ::sync ::atomic ::Ordering ::SeqCst ) > 0 {
// contested
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseContested ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseBoolean {
team : base_id ,
value : 1 ,
},
true
). await ;
} else {
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseCaptureStart ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : base_id ,
current_progress : rlnl ::types ::ByteFloat ::from ( base . capture . load ( std ::sync ::atomic ::Ordering ::SeqCst )),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
},
true
). await ;
}
}
}
}
}
async fn on_exit ( & self , generic : & crate ::matches ::GenericGamemodeEngine < EliminationLogic > , base_id : u8 , is_friendly : bool , player_id : u8 ) {
if let Some ( base ) = self . bases . get ( & base_id ) {
if let Some ( conn ) = generic . users . read (). await . get ( & player_id ) {
crate ::events ::log_lnl_send_failure ( conn . connection . rlnl (). send_data (
& rlnl ::events ::ingame ::TeamBaseBoolean {
team : base_id ,
value : 0 ,
},
rlnl ::event_code ::NetworkEvent ::PlayerInsideBase ,
literustlib ::packet ::Property ::ReliableOrdered ,
& conn . connection . connection ,
). await );
} else { return ; }
if is_friendly {
let friendlies = base . friendlies . fetch_sub ( 1 , std ::sync ::atomic ::Ordering ::SeqCst );
if friendlies <= 1 {
// no longer contesting
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseContested ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseBoolean {
team : base_id ,
value : 0 ,
},
true
). await ;
if base . enemies . load ( std ::sync ::atomic ::Ordering ::SeqCst ) > 0 {
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseCaptureStart ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : base_id ,
current_progress : rlnl ::types ::ByteFloat ::from ( base . capture . load ( std ::sync ::atomic ::Ordering ::SeqCst )),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
},
true
). await ;
}
}
} else {
let enemies = base . enemies . fetch_sub ( 1 , std ::sync ::atomic ::Ordering ::SeqCst );
if enemies <= 1 {
// no longer capturing
let last_state = base . capture . load ( std ::sync ::atomic ::Ordering ::SeqCst );
let rounded_state = last_state . floor ();
base . capture . store ( rounded_state , std ::sync ::atomic ::Ordering ::SeqCst );
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseCaptureStop ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : base_id ,
current_progress : rlnl ::types ::ByteFloat ::from ( last_state ),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
},
true
). await ;
/*generic.broadcast(
rlnl::event_code::NetworkEvent::TeamBaseCaptureReset,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::TeamBaseState {
base_team_or_mining_point_index: base_id,
current_progress: rlnl::types::ByteFloat::from(rounded_state),
max_progress: rlnl::types::ByteFloat::from(4.0),
},
true
).await;*/
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseState ,
literustlib ::packet ::Property ::ReliableOrdered ,
& rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : base_id ,
current_progress : rlnl ::types ::ByteFloat ::from ( rounded_state + f32 ::EPSILON ),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
},
true
). await ;
}
}
}
}
async fn tick ( & self , generic : & crate ::matches ::GenericGamemodeEngine < EliminationLogic > ) {
let now = chrono ::Utc ::now (). timestamp_millis ();
let last_tick = self . last_tick . load ( std ::sync ::atomic ::Ordering ::SeqCst );
let delta = if last_tick == i64 ::MIN {
// first tick
self . last_tick . store ( now , std ::sync ::atomic ::Ordering ::SeqCst );
1
} else {
let delta = ( now - last_tick ) / Self ::TICK_MS ;
if delta == 0 { return ; }
self . last_tick . store ( last_tick + ( delta * Self ::TICK_MS ), std ::sync ::atomic ::Ordering ::SeqCst );
delta
};
for ( team , base ) in self . bases . iter () {
if base . friendlies . load ( std ::sync ::atomic ::Ordering ::SeqCst ) > 0 { continue ; }
if base . enemies . load ( std ::sync ::atomic ::Ordering ::SeqCst ) <= 0 { continue ; }
let to_add = ( delta as f32 ) * ( Self ::TICK_MS as f32 ) * base . percent_per_second * 4.0 / ( 100.0 * 1000.0 );
let pre_add = base . capture . fetch_add ( to_add , std ::sync ::atomic ::Ordering ::SeqCst );
let post_add = pre_add + to_add ;
if post_add >= 4.0 {
log ::info! ( "Team {}'s base was captured in game {}" , team , generic . game_guid ());
base . capture . store ( 4.0 , std ::sync ::atomic ::Ordering ::SeqCst );
let data = rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : * team ,
current_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
};
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::TeamBaseFinalSectionComplete ,
literustlib ::packet ::Property ::ReliableOrdered ,
& data ,
true
). await ;
let winning_team = if * team == 0 { 1 u8 } else { 0 u8 };
let winning_team_i32 = winning_team as i32 ;
let win_data = rlnl ::events ::ingame ::GameLoseWin {
winning_team ,
end_reason : rlnl ::types ::GameEndReason ::BaseCaptured ,
};
for conn in generic . users . read (). await . values () {
let event = if conn . descriptor . team == winning_team_i32 {
rlnl ::event_code ::NetworkEvent ::GameWon
} else {
rlnl ::event_code ::NetworkEvent ::GameLost
};
crate ::events ::log_lnl_send_failure ( conn . connection . rlnl (). send_data (
& win_data ,
event ,
literustlib ::packet ::Property ::ReliableOrdered ,
& conn . connection . connection
). await );
}
generic . game_done ();
break ;
} else {
let data = rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : * team ,
current_progress : rlnl ::types ::ByteFloat ::from ( post_add ),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
};
let is_section_complete = pre_add . floor () < post_add . floor ();
generic . broadcast (
if is_section_complete { rlnl ::event_code ::NetworkEvent ::TeamBaseSectionComplete } else { rlnl ::event_code ::NetworkEvent ::TeamBaseState },
literustlib ::packet ::Property ::ReliableOrdered ,
& data ,
true
). await ;
}
}
}
fn teams ( & self ) -> std ::collections ::HashSet < u8 > {
self . bases . keys (). map ( | x | * x ). collect ()
2025-07-22 18:27:41 -04:00
}
2025-07-21 22:33:12 -04:00
}
pub struct EliminationLogic {
2025-07-22 18:27:41 -04:00
tracked : PlayerTracker ,
2025-07-26 21:57:46 -04:00
bases : BaseTracker ,
2025-07-22 18:27:41 -04:00
game_duration : std ::time ::Duration ,
2025-07-22 22:39:38 -04:00
respawn_full_heal_duration : f32 ,
respawn_heal_duration : f32 ,
2025-07-22 18:27:41 -04:00
game_end : std ::sync ::atomic ::AtomicI64 ,
timer_task : tokio ::sync ::Mutex < Option < tokio ::task ::JoinHandle < () >>> ,
2025-07-21 22:33:12 -04:00
}
impl EliminationLogic {
2025-07-26 21:57:46 -04:00
pub fn new ( config : & oj_rc_core ::data ::game_mode ::GameModeConfig , map : & oj_rc_core ::persist ::config ::MapConfig ) -> Self {
2025-07-22 22:39:38 -04:00
let dur = std ::time ::Duration ::from_secs (( config . game_time_minutes as u64 ) * 60 );
2025-07-22 18:27:41 -04:00
let fake_end = ( chrono ::Utc ::now () + dur ). timestamp ();
2025-07-21 22:33:12 -04:00
Self {
tracked : PlayerTracker {
alive : tokio ::sync ::Mutex ::new ( std ::collections ::HashMap ::new ()),
2025-07-26 21:57:46 -04:00
in_base : tokio ::sync ::RwLock ::new ( std ::collections ::HashMap ::new ()),
},
bases : BaseTracker {
bases : map . bases . iter (). map ( | ( team , base ) | ( * team , BaseCounters ::new ( base . 1 ))). collect (),
last_tick : std ::sync ::atomic ::AtomicI64 ::new ( i64 ::MIN ),
is_baseless : map . bases . is_empty (),
2025-07-21 22:33:12 -04:00
},
2025-07-22 18:27:41 -04:00
game_duration : dur ,
2025-07-22 22:39:38 -04:00
respawn_full_heal_duration : config . respawn_full_heal_duration ,
respawn_heal_duration : config . respawn_heal_duration ,
2025-07-22 18:27:41 -04:00
game_end : std ::sync ::atomic ::AtomicI64 ::new ( fake_end ),
timer_task : tokio ::sync ::Mutex ::new ( None ),
2025-07-21 22:33:12 -04:00
}
}
2025-07-22 18:27:41 -04:00
async fn abort_timer_sync ( & self ) {
let mut lock = self . timer_task . lock (). await ;
if let Some ( timer_t ) = &* lock {
timer_t . abort ();
log ::debug! ( "Aborted elimination match timer task" );
}
* lock = None ;
}
2025-07-26 21:57:46 -04:00
async fn on_last_player_gone ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , conn : & crate ::matches ::generic ::UserConnection ) {
log ::debug! ( "Everyone is dead, so long and thanks for all the fish" );
generic . game_done ();
self . abort_timer_sync (). await ;
let data = rlnl ::events ::ingame ::GameLoseWin {
winning_team : if conn . descriptor . team == 0 { 1 } else { 0 }, // always the other team
end_reason : rlnl ::types ::GameEndReason ::NoPlayersRemaining ,
};
crate ::events ::log_lnl_send_failure ( conn . connection . rlnl (). send_data (
& data ,
rlnl ::event_code ::NetworkEvent ::GameLost ,
literustlib ::packet ::Property ::ReliableOrdered ,
& conn . connection . connection
). await );
}
async fn send_win_info ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , winning_team : u8 ) {
generic . game_done ();
self . abort_timer_sync (). await ;
let data = rlnl ::events ::ingame ::GameLoseWin {
winning_team ,
end_reason : rlnl ::types ::GameEndReason ::OneTeamRemaining ,
};
let winning_team_i32 = winning_team as i32 ;
for conn in generic . users . read (). await . values () {
let event = if conn . descriptor . team == winning_team_i32 {
rlnl ::event_code ::NetworkEvent ::GameWon
} else {
rlnl ::event_code ::NetworkEvent ::GameLost
};
crate ::events ::log_lnl_send_failure ( conn . connection . rlnl (). send_data (
& data ,
event ,
literustlib ::packet ::Property ::ReliableOrdered ,
& conn . connection . connection
). await );
}
}
2025-07-21 22:33:12 -04:00
}
#[async_trait::async_trait]
impl CustomGameLogic for EliminationLogic {
async fn on_player_join ( & self , _generic : & crate ::matches ::GenericGamemodeEngine < Self > , player : & crate ::matches ::generic ::UserConnection , _others : & [ oj_rc_core ::persist ::user ::PlayerDescriptor ]) -> bool {
self . tracked . track_vehicle ( & player . descriptor ). await ;
true
}
async fn on_player_end ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , player : & crate ::matches ::generic ::UserConnection ) -> bool {
2025-07-22 18:27:41 -04:00
if generic . is_game_done () {
return true ;
}
if chrono ::Utc ::now (). timestamp () >= self . game_end . load ( std ::sync ::atomic ::Ordering ::Relaxed ) {
generic . game_done ();
self . abort_timer_sync (). await ;
return true ;
}
2025-07-21 22:33:12 -04:00
self . tracked . destroy_vehicle ( & player . descriptor ). await ;
if let Some ( winning_team ) = self . tracked . winner_team (). await {
2025-07-26 11:47:05 -04:00
log ::info! ( "Team {} has won sudden death game {} because player {} left" , winning_team , generic . game_guid (), player . descriptor . player_id );
2025-07-26 21:57:46 -04:00
self . send_win_info ( generic , winning_team ). await ;
2025-07-24 22:40:19 -04:00
} else if self . tracked . alive_count (). await . is_empty () {
2025-07-26 21:57:46 -04:00
self . on_last_player_gone ( generic , player ). await ;
2025-07-21 22:33:12 -04:00
}
true
}
async fn on_vehicle_destroyed ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , _killer : u8 , victim : u8 ) -> bool {
if let Some ( conn ) = generic . users . read (). await . get ( & victim ) {
self . tracked . destroy_vehicle ( & conn . descriptor ). await ;
let final_score = rlnl ::events ::ingame ::SetFinalGameScore {
player_id : victim ,
score : 42 ,
};
generic . broadcast (
rlnl ::event_code ::NetworkEvent ::SetFinalGameScore ,
literustlib ::packet ::Property ::ReliableOrdered ,
& final_score ,
true ,
). await ;
if let Some ( winning_team ) = self . tracked . winner_team (). await {
2025-07-26 11:47:05 -04:00
log ::info! ( "Team {} has won sudden death game {}" , winning_team , generic . game_guid ());
2025-07-26 21:57:46 -04:00
self . send_win_info ( generic , winning_team ). await ;
2025-07-21 22:33:12 -04:00
} else {
2025-07-26 11:47:05 -04:00
log ::info! ( "Player {} has been destroyed in sudden death game {}" , victim , generic . game_guid ());
2025-07-26 21:57:46 -04:00
if self . tracked . alive_count (). await . is_empty () {
self . on_last_player_gone ( generic , conn ). await ;
}
2025-07-21 22:33:12 -04:00
}
}
true
}
2025-07-22 18:27:41 -04:00
async fn on_vehicle_self_destruct ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , user : u8 , is_classic : bool ) -> bool {
if is_classic {
self . on_vehicle_destroyed ( generic , u8 ::MAX , user ). await
} else {
log ::warn! ( "Received non-elimination self destruct in elimination game mode" );
false
}
}
2025-07-21 22:33:12 -04:00
async fn extra_sync_events ( & self , _generic : & crate ::matches ::GenericGamemodeEngine < Self > , _player : & crate ::matches ::generic ::UserConnection ) -> Vec < crate ::matches ::RlnlPacket > {
vec! [
crate ::matches ::RlnlPacket {
event : rlnl ::event_code ::NetworkEvent ::GameModeSettings ,
property : literustlib ::packet ::Property ::ReliableOrdered ,
2025-07-22 22:39:38 -04:00
data : Box ::new ( rlnl ::events ::sync ::UpdateGameModeSettings {
respawn_heal_duration : self . respawn_heal_duration ,
respawn_full_heal_duration : self . respawn_full_heal_duration ,
2025-07-21 22:33:12 -04:00
}),
},
crate ::matches ::RlnlPacket {
event : rlnl ::event_code ::NetworkEvent ::CurrentGameTime ,
property : literustlib ::packet ::Property ::ReliableOrdered ,
2025-07-22 22:39:38 -04:00
data : Box ::new ( rlnl ::events ::GameTime ( self . game_duration . as_millis () as f32 / 1000.0 )),
2025-07-21 22:33:12 -04:00
},
]
}
2025-07-22 18:27:41 -04:00
async fn on_countdown_start ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , game_start : chrono ::DateTime < chrono ::Utc > ) -> bool {
let mut senders = Vec ::new ();
for conn in generic . users . read (). await . values () {
senders . push (( conn . connection . clone (), conn . state . clone ()));
}
let game_end = game_start + self . game_duration ;
2025-07-26 21:57:46 -04:00
let teams = self . bases . teams ();
2025-07-22 18:27:41 -04:00
let extra_packets = teams . iter (). map ( | team | crate ::matches ::RlnlPacket {
event : rlnl ::event_code ::NetworkEvent ::TeamBaseInitialise ,
property : literustlib ::packet ::Property ::ReliableOrdered ,
data : Box ::new ( rlnl ::events ::ingame ::TeamBaseState {
base_team_or_mining_point_index : * team ,
current_progress : rlnl ::types ::ByteFloat ::from ( 0.0 ),
max_progress : rlnl ::types ::ByteFloat ::from ( 4.0 ),
}),
}). collect ();
let new_timer_task = crate ::matches ::timer ::match_time_syncer ( senders , game_start , game_end , extra_packets );
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 elimination 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
}
2025-07-26 21:57:46 -04:00
async fn on_motion ( & self , generic : & crate ::matches ::GenericGamemodeEngine < Self > , motion : & rlnl ::machine_motion ::MachineMotion , location : ( f32 , f32 , f32 )) -> bool {
if generic . is_game_done () {
return true ;
}
if self . bases . is_baseless {
return true ; // don't bother trying to track whether players are in bases since there are no bases
}
// ignore dead or invalid players
if let Some ( player_team ) = self . tracked . player_team ( motion . player_id ). await {
let mut now_in_base = None ;
for ( & team , base ) in generic . map_config . bases . iter () {
if crate ::matches ::GenericGamemodeEngine ::< Self > ::is_in ( & location , & base . 0 ) {
now_in_base = Some ( team );
break ;
}
}
let in_base = self . tracked . swap_is_in_base ( motion . player_id , now_in_base ). await ;
if let Some ( team ) = in_base {
// was in a base
if let Some ( now_team ) = now_in_base {
if now_team != team {
// changed bases !?
self . bases . on_exit ( generic , team , player_team == team , motion . player_id ). await ;
self . bases . on_enter ( generic , now_team , player_team == now_team , motion . player_id ). await ;
}
// still in same base
} else {
// player has left the base
self . bases . on_exit ( generic , team , player_team == team , motion . player_id ). await ;
}
} else {
if let Some ( now_team ) = now_in_base {
// player entered a base
self . bases . on_enter ( generic , now_team , player_team == now_team , motion . player_id ). await ;
}
// still outside of base
}
}
self . bases . tick ( generic ). await ;
if generic . is_game_done () {
self . abort_timer_sync (). await ;
}
2025-07-22 18:27:41 -04:00
true
}
2025-07-21 22:33:12 -04:00
}
2025-07-26 11:47:05 -04:00
// spawn points (best guess)
// Mars 1: (16, 0, 19) and (355, 7, 372)
// Earth vanguard 2: (-248, 10, -251) and (267, 10, 258)