1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Fix battle arena ending at 96% base charge, fix part of #61

This commit is contained in:
NG (Graham)
2026-01-18 12:39:58 -05:00
parent 5fd4acdf62
commit ff0588ada1
10 changed files with 805 additions and 93 deletions

View File

@@ -5,6 +5,7 @@ pub struct GameMatches {
map_configs: std::collections::HashMap<String, oj_rc_core::persist::config::MapConfig>,
//fake_players: Vec<oj_rc_core::persist::config::FakePlayer>,
cube_parsers: std::sync::Arc<oj_rc_core::cubes::CubeParsers>,
ba_sorted_crystals: tokio::sync::RwLock<std::sync::Arc<Vec<oj_rc_core::cubes::CubeLocationInfo>>>, // cached after first calculation
ba_settings: std::sync::Arc<oj_rc_core::persist::config::BattleArenaResolver>,
pit_settings: std::sync::Arc<oj_rc_core::persist::config::PitSettings>,
tdm_settings: std::sync::Arc<oj_rc_core::persist::config::TeamDeathMatchSettings>,
@@ -23,6 +24,7 @@ impl GameMatches {
.collect(),
//fake_players: <oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::fake_players(conf),
cube_parsers,
ba_sorted_crystals: tokio::sync::RwLock::new(std::sync::Arc::new(Vec::default())),
ba_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::ba_settings(conf)),
pit_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::pit_settings(conf)),
tdm_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::tdm_settings(conf)),
@@ -110,7 +112,17 @@ impl GameMatches {
code: oj_rc_core::persist::user::MultiplayerErrorCode::CustomString,
message: e.error_msg().map(|x| x.to_owned()).unwrap_or_else(|| "Failed to resolve special settings for Battle Arena".to_owned()),
})?;
let inner = super::modes::BattleArenaLogic::new(&self.mode_configs.battle_arena, &map_config, &self.cube_parsers, &players, resolved_ba_conf);
let crystals = if self.ba_sorted_crystals.read().await.is_empty() {
let crystals = std::sync::Arc::new(
self.cube_parsers.locations_of()
.locations_of_reactor_sort(&mut std::io::Cursor::new(&resolved_ba_conf.base_machine_map))
);
*self.ba_sorted_crystals.write().await = crystals.clone();
crystals
} else {
self.ba_sorted_crystals.read().await.clone()
};
let inner = super::modes::BattleArenaLogic::new(&self.mode_configs.battle_arena, &map_config, &players, resolved_ba_conf, crystals);
let engine = super::GenericGamemodeEngine::new(
game_info,
map_config,

View File

@@ -589,17 +589,17 @@ impl EqualizerTracker {
}
}
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]) {
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) {
if !self.activated.load(std::sync::atomic::Ordering::Relaxed) { return; }
let damage_u64 = damage as u64;
let old_health = self.health.fetch_sub(damage as u64, std::sync::atomic::Ordering::Relaxed);
if old_health <= damage_u64 {
// equalizer is now destroyed
self.destroy_equalizer(generic, ba_config, bases, crystals).await;
self.destroy_equalizer(generic, ba_config, bases, crystals, crystal_health).await;
}
}
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]) {
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) {
if !self.activated.swap(false, std::sync::atomic::Ordering::Relaxed) { return; }
self.cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
let eq_start = self.start.load(std::sync::atomic::Ordering::Relaxed);
@@ -615,7 +615,7 @@ impl EqualizerTracker {
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 {
losing_base.crystals_healths[i].store(u8::MAX, std::sync::atomic::Ordering::Relaxed);
losing_base.crystals_healths[i].store(crystal_health, std::sync::atomic::Ordering::Relaxed);
}
let base_heal = losing_base.generate_full_base_heal(losing_team, ba_config.protonium_health as u32, crystals);
generic.broadcast(
@@ -739,7 +739,7 @@ impl BaseTracker {
//let base = self.bases.get(&base_id).unwrap();
let target_crystals = &crystals[old_index..new_index];
for crystal_i in old_index..new_index {
base.crystals_healths[crystal_i].store(u8::MAX, std::sync::atomic::Ordering::Relaxed);
base.crystals_healths[crystal_i].store(crystal_health, std::sync::atomic::Ordering::Relaxed);
}
rlnl::events::HealedCubes {
healed_machine: base_id as u16,
@@ -748,9 +748,12 @@ impl BaseTracker {
num_healed_cubes: target_crystals.len() as _,
hit_cubes: target_crystals
.iter()
.map(|loc| rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: loc.x, y: loc.y, z: loc.z, },
damage: crystal_health as i32,
.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(),
}
@@ -787,21 +790,23 @@ impl BaseTracker {
* ((PointTracker::TICK_MS as f32) / (generic.game_duration.as_millis() as f32))
* ((self.bases.len() as f32) / (capture_points_count as f32));
let increment = tick_info.delta as f32 * (*owned_points as f32) * one_tick * multiplier;
#[cfg(debug_assertions)]
let increment = increment + 0.1;
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 {
log::debug!("Base {} increment passed a crystal index barrier", base_id);
log::trace!("Base {} increment passed a crystal index barrier", base_id);
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
tracked_base.cube_index.fetch_sub(increment, std::sync::atomic::Ordering::SeqCst);
log::debug!("Skipping increment in favour of healing damaged/destroyed cube");
log::trace!("Skipping increment in favour of healing damaged/destroyed cube");
#[allow(clippy::unnecessary_unwrap)] // have you seen the mess this would be with another if statement?
let first_damaged = first_damaged.unwrap();
let healing = crystal_health - tracked_base.calculate_crystal_health(first_damaged, crystal_health);
tracked_base.crystals_healths[first_damaged].store(u8::MAX, std::sync::atomic::Ordering::Relaxed);
//let healing = crystal_health - tracked_base.calculate_crystal_health(first_damaged, crystal_health);
tracked_base.crystals_healths[first_damaged].store(crystal_health, std::sync::atomic::Ordering::Relaxed);
let target_crystal = &crystals[first_damaged];
rlnl::events::HealedCubes {
healed_machine: *base_id as u16,
@@ -811,7 +816,8 @@ impl BaseTracker {
hit_cubes: vec![
rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: target_crystal.x, y: target_crystal.y, z: target_crystal.z, },
damage: healing as i32,
//damage: healing as i32,
damage: crystal_health.saturating_sub(2) as i32,
}
],
}
@@ -843,6 +849,14 @@ impl BaseTracker {
true
).await;
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());
}
if new_index == crystals.len() {
// team base is charged to 100%
//self.do_win(*base_id, WinMode::BaseFull, generic).await;
@@ -862,7 +876,7 @@ impl BaseTracker {
struct BaseInfo {
cube_index: atomic_float::AtomicF32,
crystals_healths: Vec<std::sync::atomic::AtomicU8>,
crystals_healths: Vec<std::sync::atomic::AtomicU32>,
}
impl BaseInfo {
@@ -870,15 +884,14 @@ impl BaseInfo {
Self {
cube_index: atomic_float::AtomicF32::new(0.0),
crystals_healths: (0..crystals.len())
.map(|_| std::sync::atomic::AtomicU8::new(0))
.map(|_| std::sync::atomic::AtomicU32::new(0))
.collect()
}
}
#[inline]
fn calculate_crystal_health(&self, i: usize, max_health: u32) -> u32 {
(((self.crystals_healths[i].load(std::sync::atomic::Ordering::Relaxed) as f32) / (u8::MAX as f32))
* (max_health as f32)).ceil() as u32
fn calculate_crystal_health(&self, i: usize) -> u32 {
self.crystals_healths[i].load(std::sync::atomic::Ordering::Relaxed)
}
#[inline]
@@ -898,7 +911,7 @@ impl BaseInfo {
fn first_damaged(&self, old_index: usize, max_health: u32) -> Option<usize> {
for i in 0..old_index {
let health = self.calculate_crystal_health(i, max_health);
let health = self.calculate_crystal_health(i);
if health != 0 && health != max_health {
return Some(i);
}
@@ -907,27 +920,27 @@ impl BaseInfo {
}
/// returns whether crystal exists
fn damage_crystal_at_pos(&self, pos: rlnl::types::Byte3, damage: i32, crystals: &[oj_rc_core::cubes::CubeLocationInfo], max_health: u32) -> bool {
fn damage_crystal_at_pos(&self, pos: rlnl::types::Byte3, damage: i32, crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> Option<usize> {
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.damage_crystal(index, damage, max_health);
true
if index > max_index { return None; }
self.damage_crystal(index, damage);
Some(index)
} else {
false
None
}
}
/// returns whether crystal was destroyed
fn damage_crystal(&self, i: usize, damage: i32, max_health: u32) -> bool {
let damage_byte = (((damage as f32) / (max_health as f32)) * (u8::MAX as f32)).ceil() as u8;
let old_health = self.crystals_healths[i].fetch_sub(damage_byte, std::sync::atomic::Ordering::Relaxed);
log::debug!("Crystal {} damaged (was {}, now {}, delta {})", i, old_health, old_health.saturating_sub(damage_byte), damage_byte);
if old_health < damage_byte {
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);
log::info!("Crystal {} damaged (was {}, now {}, delta {})", i, old_health, old_health.saturating_sub(damage), damage);
if damage > old_health {
// guarantee underflow behaviour
self.crystals_healths[i].store(0, std::sync::atomic::Ordering::Relaxed);
}
old_health != 0 && old_health >= damage_byte
old_health != 0 && damage >= old_health
}
/// returns whether crystal exists
@@ -950,7 +963,7 @@ impl BaseInfo {
let max_index = self.max_index();
let target_crystals: Vec<rlnl::types::HitCubeInfo> = (0..crystals.len())
.take_while(|&i| i <= max_index)
.filter(|&i| self.calculate_crystal_health(i, max_health) != 0)
.filter(|&i| self.calculate_crystal_health(i) != 0)
.map(|i| {
let target_loc = &crystals[i];
rlnl::types::HitCubeInfo {
@@ -985,18 +998,14 @@ pub struct BattleArenaLogic {
base_tracking: BaseTracker,
surrender_tracking: super::trackers::SurrenderGameTracker,
//cube_parser: std::sync::Arc<oj_rc_core::cubes::CubeLocationsParser>,
crystals: Vec<oj_rc_core::cubes::CubeLocationInfo>,
crystals: std::sync::Arc<Vec<oj_rc_core::cubes::CubeLocationInfo>>,
config: oj_rc_core::data::battle_arena_config::BattleArenaData,
}
impl BattleArenaLogic {
const CRYSTAL_ID: u32 = 3950293873;
const CLASP_ID: u32 = 606866102;
pub fn new(config: &oj_rc_core::data::game_mode::GameModeConfig, map: &oj_rc_core::persist::config::MapConfig, parsers: &oj_rc_core::cubes::CubeParsers, players: &[oj_rc_core::persist::user::PlayerDescriptor], ba_config: oj_rc_core::data::battle_arena_config::BattleArenaData) -> Self {
let cube_parser = parsers.locations_of();
let crystals = cube_parser.locations_of_by_distance_to_first(&mut std::io::Cursor::new(&ba_config.base_machine_map), Self::CRYSTAL_ID, Self::CLASP_ID);
//let crystals = cube_parser.locations_of_by_distance_from(&mut std::io::Cursor::new(&ba_config.base_machine_map), Self::CRYSTAL_ID, (44, 255, 46));
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);
Self {
respawn_full_heal_duration: config.respawn_full_heal_duration,
respawn_heal_duration: config.respawn_heal_duration,
@@ -1170,50 +1179,38 @@ impl BattleArenaLogic {
}
}
async fn do_team_base_stealing(&self, cube_damage: &rlnl::events::ingame::DestroyCubeNoEffect, generic: &crate::matches::GenericGamemodeEngine<Self>, _actual_damage_data: impl FnOnce(Vec<rlnl::types::CubeState>) -> crate::matches::RlnlPacket) {
async fn do_team_base_stealing(&self, cube_damage: &rlnl::events::ingame::DestroyCubeNoEffect, generic: &crate::matches::GenericGamemodeEngine<Self>, actual_damage_data: impl FnOnce(Vec<rlnl::types::CubeState>) -> crate::matches::RlnlPacket) {
let base_id = cube_damage.hit_machine_id as u8;
let mut total_destroyed = 0;
let mut valid_cubes = Vec::with_capacity(cube_damage.hit_cubes.len());
let mut total_damaged = 0;
let mut actual_cube_damage = Vec::with_capacity(cube_damage.hit_cubes.len());
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 {
if base.damage_crystal_at_pos(hit_cube.loc, damage, &self.crystals, self.config.protonium_health as u32) {
valid_cubes.push(hit_cube.to_owned());
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");
}
} else if matches!(hit_cube.status.ty, rlnl::types::CubeHistoryEventType::Destroy) {
if base.destroy_crystal_at_pos(hit_cube.loc, &self.crystals) {
valid_cubes.push(hit_cube.to_owned());
// TODO handle cube graph disconnects
actual_cube_damage.push(hit_cube.to_owned());
total_destroyed += 1;
} else {
log::warn!("Did not destroy cube with destroy status");
log::warn!("Could not destroy cube with destroy status");
}
}
}
/*generic.broadcast(
rlnl::event_code::NetworkEvent::DestroyCubeNoEffect,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::DestroyCubeNoEffect {
shooting_machine_id: cube_damage.shooting_machine_id,
hit_machine_id: cube_damage.hit_machine_id,
target_type: rlnl::types::TargetType::TeamBase,
num_hits: valid_cubes.len() as _,
hit_cubes: valid_cubes,
},
true,
).await;*/
//let actual_damage_data = actual_damage_data(valid_cubes);
/*generic.broadcast(
log::debug!("Destroyed {} ({} damaged) base cubes; {}/{} valid", total_destroyed, total_damaged, actual_cube_damage.len(), cube_damage.hit_cubes.len());
let actual_damage_data = actual_damage_data(actual_cube_damage);
generic.broadcast(
actual_damage_data.event,
actual_damage_data.property,
actual_damage_data.data.as_ref(),
true,
).await;*/
/*generic.broadcast(
rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
literustlib::packet::Property::ReliableOrdered,
&base.generate_full_base_heal(base_id, self.config.protonium_health as u32, &self.crystals),
true,
).await;*/
).await;
}
if total_destroyed != 0 {
if let Some(team_id) = self.player_tracking.team(cube_damage.shooting_machine_id as u8).await {
@@ -1232,12 +1229,6 @@ impl BattleArenaLogic {
old_index,
new_index,
);
/*generic.broadcast(
rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
literustlib::packet::Property::ReliableOrdered,
&base.generate_full_base_heal(team_id, self.config.protonium_health as u32, &self.crystals),
true,
).await;*/
generic.broadcast(
rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
@@ -1253,11 +1244,12 @@ impl BattleArenaLogic {
}
async fn do_equalizer_damage(&self, cube_damage: &rlnl::events::ingame::DestroyCubeNoEffect, generic: &crate::matches::GenericGamemodeEngine<Self>) {
let crystal_health = self.config.protonium_health as u32;
for hit_cube in cube_damage.hit_cubes.iter() {
if let Some(damage) = hit_cube.status.damage {
self.base_tracking.equalizer.damage_equalizer(damage, generic, &self.config, &self.base_tracking.bases, &self.crystals).await;
self.base_tracking.equalizer.damage_equalizer(damage, generic, &self.config, &self.base_tracking.bases, &self.crystals, crystal_health).await;
} else if matches!(hit_cube.status.ty, rlnl::types::CubeHistoryEventType::Destroy) {
self.base_tracking.equalizer.destroy_equalizer(generic, &self.config, &self.base_tracking.bases, &self.crystals).await;
self.base_tracking.equalizer.destroy_equalizer(generic, &self.config, &self.base_tracking.bases, &self.crystals, crystal_health).await;
}
}
}
@@ -1512,7 +1504,7 @@ impl CustomGameLogic for BattleArenaLogic {
}
};
self.do_team_base_stealing(&pseudo, generic, actual_damage_data).await;
true
false
},
rlnl::types::TargetType::EqualizerCrystal => {
self.do_equalizer_damage(&pseudo, generic).await;
@@ -1543,8 +1535,8 @@ impl CustomGameLogic for BattleArenaLogic {
data: Box::new(cube_dmg.to_owned()),
}
};
self.do_team_base_stealing(cube_damage, generic, &actual_damage_data).await;
true
self.do_team_base_stealing(cube_damage, generic, actual_damage_data).await;
false
},
rlnl::types::TargetType::EqualizerCrystal => {
self.do_equalizer_damage(cube_damage, generic).await;