From edb5b2eba81be23fb49846796945da32e336ca05 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sat, 24 Jan 2026 13:36:32 -0500 Subject: [PATCH] Fix #61 base crystal stealing with block damage model --- assets/robocraft/config.json | 3 +- rc_core/src/cubes/connections.rs | 60 +-- rc_core/src/cubes/graph.rs | 446 ++++++++++++++++++ rc_core/src/cubes/locations_of.rs | 13 +- rc_core/src/cubes/mod.rs | 8 +- .../src/matches/modes/battle_arena.rs | 185 +++++--- 6 files changed, 608 insertions(+), 107 deletions(-) create mode 100644 rc_core/src/cubes/graph.rs diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index 30cb5ea..215f5f5 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -12303,7 +12303,8 @@ "type": "NotAFunctionalItem", "active": true, "visibility": "None", - "ignore_in_weapon_list": true + "ignore_in_weapon_list": true, + "indestructible": true }, "spriteName": "CubeThumb_LightChassis_Cube", "nameStrKey": "strProtoniumClaspName", diff --git a/rc_core/src/cubes/connections.rs b/rc_core/src/cubes/connections.rs index ee2c7f3..bca8a9b 100644 --- a/rc_core/src/cubes/connections.rs +++ b/rc_core/src/cubes/connections.rs @@ -1,3 +1,33 @@ +pub const DEFAULT_CONNECTION: CubeConnections = CubeConnections { + id: 0, // (default) + connections: &[ + CubeConnection { + direction: (0, 0, 1), + position: (0, 0, 0), + }, + CubeConnection { + direction: (0, 1, 0), + position: (0, 0, 0), + }, + CubeConnection { + direction: (1, 0, 0), + position: (0, 0, 0), + }, + CubeConnection { + direction: (0, 0, -1), + position: (0, 0, 0), + }, + CubeConnection { + direction: (0, -1, 0), + position: (0, 0, 0), + }, + CubeConnection { + direction: (-1, 0, 0), + position: (0, 0, 0), + }, + ], +}; + pub const CUBE_CONNECTIONS: &[CubeConnections] = &[ CubeConnections { id: 606866102, // Protonium Clasp @@ -343,35 +373,7 @@ pub const CUBE_CONNECTIONS: &[CubeConnections] = &[ }, ], }, - CubeConnections { - id: 0, // (default) - connections: &[ - CubeConnection { - direction: (0, 0, 1), - position: (0, 0, 0), - }, - CubeConnection { - direction: (0, 1, 0), - position: (0, 0, 0), - }, - CubeConnection { - direction: (1, 0, 0), - position: (0, 0, 0), - }, - CubeConnection { - direction: (0, 0, -1), - position: (0, 0, 0), - }, - CubeConnection { - direction: (0, -1, 0), - position: (0, 0, 0), - }, - CubeConnection { - direction: (-1, 0, 0), - position: (0, 0, 0), - }, - ], - }, + DEFAULT_CONNECTION, ]; #[derive(Copy, Clone)] diff --git a/rc_core/src/cubes/graph.rs b/rc_core/src/cubes/graph.rs new file mode 100644 index 0000000..4c2f104 --- /dev/null +++ b/rc_core/src/cubes/graph.rs @@ -0,0 +1,446 @@ +struct CubeDescriptor { + connected_to: std::sync::RwLock>, + connections: std::sync::Arc>, + //references: std::sync::atomic::AtomicU64, + health: std::sync::atomic::AtomicU32, +} + +impl std::clone::Clone for CubeDescriptor { + fn clone(&self) -> Self { + Self { + connected_to: std::sync::RwLock::new(self.connected_to.read().unwrap().to_owned()), + connections: self.connections.clone(), + health: std::sync::atomic::AtomicU32::new(self.health.load(std::sync::atomic::Ordering::Relaxed)), + } + } +} + +#[derive(Copy, Clone)] +struct Connection { + position: CellPoint, + direction: AxisDirection, +} + +#[repr(u8)] +#[derive(PartialEq, Eq, Copy, Clone)] +enum AxisDirection { + Up, // +y + Down, // -y + Right, // +x + Left, // -x + Back, // +z, + Front, // -z +} + +impl AxisDirection { + #[inline] + fn opposite(&self) -> Self { + match self { + Self::Up => Self::Down, + Self::Down => Self::Up, + Self::Right => Self::Left, + Self::Left => Self::Right, + Self::Back => Self::Front, + Self::Front => Self::Back, + } + } + + /// WARNING: will panic if you give it bad data + fn from_i8_direction(value: (i8, i8, i8)) -> Self { + match value { + (0, 1, 0) => Self::Up, + (0, -1, 0) => Self::Down, + (1, 0, 0) => Self::Right, + (-1, 0, 0) => Self::Left, + (0, 0, 1) => Self::Back, + (0, 0, -1) => Self::Front, + _ => panic!("Invalid axial direction {:?}", value), + } + } +} + +#[derive(Default)] +pub struct CubeGraph { + cubes: std::sync::RwLock>, + connects_to: std::sync::RwLock>>, + base: std::sync::RwLock>, +} + +impl CubeGraph { + pub fn with_data(r: &mut dyn std::io::Read, health_map: std::collections::HashMap, root_id: u32) -> std::io::Result { + let parsed_cubes = super::parser::Cube::parse_list(r)?; + let mut cube_graph = std::collections::HashMap::::with_capacity(parsed_cubes.len()); + let mut connection_graph = std::collections::HashMap::>::with_capacity(parsed_cubes.len()); + let mut root_location = None; + for cube in parsed_cubes { + let location = CellPoint { + x: cube.x, + y: cube.y, + z: cube.z, + }; + if cube.id == root_id { + root_location = Some(location); + } + let conns = super::CUBE_CONNECTIONS.iter().find(|x| x.id == cube.id).unwrap_or(&super::DEFAULT_CONNECTION); + let rot = super::CUBE_ROTATIONS[(cube.orientation & 0b01111111) as usize]; + let unit_rot = rot.normalize().unwrap(); + let is_destroyed = (cube.orientation & 0b10000000) != 0; + if is_destroyed && cube.id != root_id { continue; } + let cube_health = if is_destroyed { + 0 + } else if let Some(health) = health_map.get(&cube.id) { + *health + } else { + log::warn!("No health data for cube id {} in CubeGraph at ({}, {}, {})", cube.id, location.x, location.y, location.z); + 1 + }; + let mut abs_connections = Vec::with_capacity(conns.connections.len()); + for conn in conns.connections.iter() { + let conn = Self::calculate_connection( + (location.x, location.y, location.z), + conn, + &unit_rot, + ); + abs_connections.push(conn); + let connects_to = conn.position.connects_to(conn.direction); + let graph_pointer = Connection { + position: location, + direction: conn.direction, + }; + if let Some(connects_to_point_list) = connection_graph.get_mut(&connects_to) { + connects_to_point_list.push(graph_pointer); + } else { + connection_graph.insert(connects_to, vec![graph_pointer]); + } + } + cube_graph.insert(location, CubeDescriptor { + connected_to: std::sync::RwLock::new(std::collections::HashMap::with_capacity(abs_connections.len())), + connections: std::sync::Arc::new(abs_connections), + //references: std::sync::atomic::AtomicU64::new(0), + health: std::sync::atomic::AtomicU32::new(cube_health), + }); + } + + if let Some(root) = root_location { + let this = Self { + cubes: std::sync::RwLock::new(cube_graph), + connects_to: std::sync::RwLock::new(connection_graph), + base: std::sync::RwLock::new(None), + }; + this.rebase_on(&root); + Ok(this) + } else { + Err(std::io::Error::other("Graph root cube not found")) + } + } + + /*fn reset(&self) { + for cube in self.cubes.values() { + cube.references.store(0, std::sync::atomic::Ordering::Relaxed); + } + }*/ + + fn rebase_on(&self, point: &CellPoint) { + let cubes = self.cubes.read().unwrap(); + let connects_to = self.connects_to.read().unwrap(); + if cubes.is_empty() { return; } + if !cubes.contains_key(point) { + log::warn!("Cannot rebase CubeGraph at point ({}, {}, {}); no cube is there", point.x, point.y, point.z); + return; + } + if cubes.len() == 1 { + // trivial case, base is the only cube (no need to connect anything) + *self.base.write().unwrap() = Some(*point); + return; + } + let mut seen = std::collections::HashSet::with_capacity(cubes.len()); + seen.insert(*point); + let mut to_be_processed = std::collections::HashSet::new(); + to_be_processed.insert(*point); + while !to_be_processed.is_empty() { + let to_be_processed_now = to_be_processed.clone(); + to_be_processed.clear(); + for loc in to_be_processed_now.iter() { + let cube = cubes.get(loc).unwrap(); + //let src_ref_count = cube.references.load(std::sync::atomic::Ordering::Relaxed); + for conn in cube.connections.iter() { + if cube.connected_to.read().unwrap().contains_key(&conn.position) { continue; } // already connected + let opposite_direction = conn.direction.opposite(); + if let Some(other_conns) = connects_to.get(&conn.position) { + for other_conn in other_conns.iter() { + if other_conn.direction != opposite_direction { continue; } + let other_cube = cubes.get(&other_conn.position).unwrap(); + cube.connected_to.write().unwrap().insert(conn.position, other_conn.position); + let connected_to = conn.position.connects_to(conn.direction); + other_cube.connected_to.write().unwrap().insert(connected_to, *loc); + //cubes.get(&other_conn.position).unwrap().references.fetch_add(src_ref_count, std::sync::atomic::Ordering::Relaxed); + if seen.insert(other_conn.position) { + to_be_processed.insert(other_conn.position); + } + } + } + } + } + } + *self.base.write().unwrap() = Some(*point); + } + + pub fn add_cube(&self, point: &CellPoint, cube_id: u32, health: u32, extras: u8) { + let conns = super::CUBE_CONNECTIONS.iter().find(|x| x.id == cube_id).unwrap_or(&super::DEFAULT_CONNECTION); + let rot = super::CUBE_ROTATIONS[(extras & 0b01111111) as usize]; + let unit_rot = rot.normalize().unwrap(); + //let is_destroyed = health == 0 || (extras & 0b10000000) != 0; + let mut abs_connections = Vec::with_capacity(conns.connections.len()); + //let mut ref_count = 0; + let mut connects_to_lock = self.connects_to.write().unwrap(); + let mut connected_to = std::collections::HashMap::with_capacity(conns.connections.len()); + for conn in conns.connections.iter() { + let conn = Self::calculate_connection( + (point.x, point.y, point.z), + conn, + &unit_rot, + ); + abs_connections.push(conn); + let connects_to = conn.position.connects_to(conn.direction); + let opposite_direction = conn.direction.opposite(); + let graph_pointer = Connection { + position: *point, + direction: conn.direction, + }; + if let Some(connects_to_point_list) = connects_to_lock.get_mut(&connects_to) { + connects_to_point_list.push(graph_pointer); + } else { + connects_to_lock.insert(connects_to, vec![graph_pointer]); + } + if let Some(connects_to_point_list) = connects_to_lock.get_mut(&conn.position) { + let cubes_lock = self.cubes.read().unwrap(); + for other_cube_loc in connects_to_point_list.iter() { + if other_cube_loc.direction != opposite_direction { continue; } + let other_cube = cubes_lock.get(&other_cube_loc.position).unwrap(); + //ref_count += other_cube.references.load(std::sync::atomic::Ordering::Relaxed); + other_cube.connected_to.write().unwrap().insert(connects_to, *point); + connected_to.insert(conn.position, other_cube_loc.position); + } + } + } + drop(connects_to_lock); + self.cubes.write().unwrap().insert(*point, CubeDescriptor { + connected_to: std::sync::RwLock::new(connected_to), + connections: std::sync::Arc::new(abs_connections), + //references: std::sync::atomic::AtomicU64::new(ref_count), + health: std::sync::atomic::AtomicU32::new(health), + }); + } + + pub fn remove_cube(&self, point: &CellPoint) -> std::collections::HashSet { + self.remove_cubes_and_disconnects(&[*point]) + } + + fn remove_cube_only(&self, point: &CellPoint, cubes: &mut std::collections::HashMap) { + if let Some(cube) = cubes.remove(point) { + let mut connects_to = self.connects_to.write().unwrap(); + let connected_to = cube.connected_to.read().unwrap(); + for conn in cube.connections.iter() { + let my_connects_to = conn.position.connects_to(conn.direction); + connects_to.get_mut(&my_connects_to).unwrap().retain(|x| !(&x.position == point && x.direction == conn.direction)); + } + for other_conn in connected_to.values() { + let other_cube = cubes.get(other_conn).unwrap(); + other_cube.connected_to.write().unwrap().retain(|_, conn| conn != point); + } + } else { + log::warn!("Cannot remove cube at point ({}, {}, {}); no cube is there", point.x, point.y, point.z); + } + } + + fn remove_cubes_and_disconnects(&self, points: &[CellPoint]) -> std::collections::HashSet { + let mut cubes = self.cubes.write().unwrap(); + for to_remove in points.iter() { + self.remove_cube_only(to_remove, &mut cubes); + } + let mut chunks = ChunkTracker::with_capacity(6); + for (point, cube) in cubes.iter() { + chunks.add_cube(*point, cube.connected_to.read().unwrap().values()); + } + if let Some(root) = *self.base.read().unwrap() { + let disconnected_cubes = chunks.cubes_not_in_chunk(root); + for to_remove in disconnected_cubes.iter() { + if !cubes.contains_key(to_remove) { continue; } + self.remove_cube_only(to_remove, &mut cubes); + } + disconnected_cubes + } else { + log::warn!("Cannot calculate disconnections without base cube of CubeGraph"); + Default::default() + } + } + + pub fn damage_cube(&self, point: &CellPoint, damage: u32) { + let cubes = self.cubes.read().unwrap(); + if let Some(cube) = cubes.get(point) { + cube.health.fetch_sub(damage, std::sync::atomic::Ordering::Relaxed); + } else { + log::warn!("Cannot damage cube at point ({}, {}, {}); no cube is there", point.x, point.y, point.z); + } + } + + #[inline] + fn calculate_connection(location: (u8, u8, u8), relative_conn: &crate::cubes::connections::CubeConnection, unit_rot: &num_quaternion::UnitQuaternion) -> Connection { + let rotated_pos = unit_rot.rotate_vector([ + relative_conn.position.0 as f32, + relative_conn.position.1 as f32, + relative_conn.position.2 as f32 + ]); + let rotated_dir = unit_rot.rotate_vector([ + relative_conn.direction.0 as f32, + relative_conn.direction.1 as f32, + relative_conn.direction.2 as f32 + ]); + let abs_point = CellPoint { + x: location.0.saturating_add_signed(rotated_pos[0] as i8), + y: location.1.saturating_add_signed(rotated_pos[1] as i8), + z: location.2.saturating_add_signed(rotated_pos[2] as i8), + }; + let direction = AxisDirection::from_i8_direction((rotated_dir[0] as i8, rotated_dir[1] as i8, rotated_dir[2] as i8)); + Connection { + position: abs_point, + direction, + } + } +} + +impl std::clone::Clone for CubeGraph { + fn clone(&self) -> Self { + Self { + cubes: std::sync::RwLock::new(self.cubes.read().unwrap().to_owned()), + connects_to: std::sync::RwLock::new(self.connects_to.read().unwrap().to_owned()), + base: std::sync::RwLock::new(self.base.read().unwrap().to_owned()), + } + } +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +pub struct CellPoint { + pub x: u8, + pub y: u8, + pub z: u8, +} + +impl CellPoint { + #[inline] + fn connects_to(&self, direction: AxisDirection) -> Self { + match direction { + AxisDirection::Up => Self { + x: self.x, + y: self.y + 1, + z: self.z, + }, + AxisDirection::Down => Self { + x: self.x, + y: self.y - 1, + z: self.z, + }, + AxisDirection::Right => Self { + x: self.x + 1, + y: self.y, + z: self.z, + }, + AxisDirection::Left => Self { + x: self.x - 1, + y: self.y, + z: self.z, + }, + AxisDirection::Back => Self { + x: self.x, + y: self.y, + z: self.z + 1, + }, + AxisDirection::Front => Self { + x: self.x, + y: self.y, + z: self.z - 1, + }, + } + } +} + +impl std::convert::From<(u8, u8, u8)> for CellPoint { + fn from(value: (u8, u8, u8)) -> Self { + Self { + x: value.0, + y: value.1, + z: value.2, + } + } +} + +struct ChunkTracker { + chunks: Vec>, +} + +impl ChunkTracker { + fn with_capacity(capacity: usize) -> Self { + Self { + chunks: Vec::with_capacity(capacity), + } + } + + fn add_cube<'a>(&mut self, new_cube: CellPoint, connects_to: impl std::iter::Iterator) { + let connections: Vec<_> = connects_to.copied().collect(); + let mut found_in_chunks = Vec::with_capacity(self.chunks.len()); + for (i, chunk) in self.chunks.iter_mut().enumerate() { + for connected_to in connections.iter() { + if chunk.contains(connected_to) && !found_in_chunks.contains(&i) { + chunk.insert(new_cube); + found_in_chunks.push(i); + } + } + } + if found_in_chunks.is_empty() { + let mut new_chunk = std::collections::HashSet::::new(); + new_chunk.insert(new_cube); + for connected_to in connections.iter() { + new_chunk.insert(*connected_to); + } + self.chunks.push(new_chunk); + } else { + self.merge_chunks(found_in_chunks, &connections); + } + } + + fn merge_chunks(&mut self, mut chunks_to_merge: Vec, connections: &[CellPoint]) { + if chunks_to_merge.is_empty() || chunks_to_merge.len() == 1 { return; } + // by sorting and then reversing the order it is guaranteed that + // self.chunks.swap_remove(chunks_to_merge[i]) will not affect the index of other chunks + chunks_to_merge.sort(); + chunks_to_merge.reverse(); + let mut super_chunk = self.chunks.swap_remove(chunks_to_merge[0]); + for chunk_i in chunks_to_merge[1..].iter() { + let to_merge = self.chunks.swap_remove(*chunk_i); + for point in to_merge { + super_chunk.insert(point); + } + } + for connected_to in connections.iter() { + super_chunk.insert(*connected_to); + } + self.chunks.push(super_chunk); + } + + fn cubes_not_in_chunk(mut self, point: CellPoint) -> std::collections::HashSet { + let chunk_indices: Vec<_> = self.chunks.iter().enumerate() + .filter(|(_, chunk)| !chunk.contains(&point)) + .map(|(i, _)| i) + .collect(); + self.merge_chunks(chunk_indices, &[]); + if self.chunks[0].contains(&point) { + if self.chunks.len() == 1 { + Default::default() + } else { + self.chunks.swap_remove(1) + } + } else { + self.chunks.swap_remove(0) + } + } +} diff --git a/rc_core/src/cubes/locations_of.rs b/rc_core/src/cubes/locations_of.rs index 42d9967..bed1d75 100644 --- a/rc_core/src/cubes/locations_of.rs +++ b/rc_core/src/cubes/locations_of.rs @@ -1,8 +1,8 @@ -const CRYSTAL_ID: u32 = 3950293873; -const CLASP_ID: u32 = 606866102; +use super::{CLASP_ID, CRYSTAL_ID}; pub struct CubeLocationsParser; +#[derive(Clone)] pub struct CubeLocationInfo { pub x: u8, pub y: u8, @@ -193,19 +193,12 @@ impl CubeLocationsParser { }; // calculate connect target connection points let mut abs_connected_to = None; - let mut target_cube_loc = None; for cube in cubes.iter() { if cube.id == CLASP_ID { let rot_index = cube.orientation & 0b01111111; let rot = &CUBE_ROTATIONS[rot_index as usize]; log::trace!("Calculating target connection points"); abs_connected_to = Some(calculate_absolute_connections(connected_to_connections, rot, (cube.x, cube.y, cube.z))); - target_cube_loc = Some(CubeLocationInfo { - x: cube.x, - y: cube.y, - z: cube.z, - extras: cube.orientation, - }); break; } } @@ -215,7 +208,6 @@ impl CubeLocationsParser { log::error!("Failed to find cube with id {} to calculate connections to", CLASP_ID); return Vec::default(); }; - let target_cube_loc = target_cube_loc.unwrap(); // find cubes connected to target (or to another relevant cube connected to the target) let mut to_be_sorted = std::collections::HashMap::new(); for (i, cube) in cubes.iter().enumerate() { @@ -228,7 +220,6 @@ impl CubeLocationsParser { } } let mut sorted = Vec::with_capacity(to_be_sorted.len() + 1); - sorted.push(target_cube_loc); let mut iteration = 0; let mut random = rand::rng(); let mut to_be_released = Vec::new(); diff --git a/rc_core/src/cubes/mod.rs b/rc_core/src/cubes/mod.rs index 24f9638..4a7df69 100644 --- a/rc_core/src/cubes/mod.rs +++ b/rc_core/src/cubes/mod.rs @@ -13,11 +13,17 @@ mod offsetter; pub use offsetter::OffsetParser; mod connections; -pub use connections::CUBE_CONNECTIONS; +pub use connections::{CUBE_CONNECTIONS, DEFAULT_CONNECTION}; mod rotations; pub use rotations::CUBE_ROTATIONS; +mod graph; +pub use graph::{CubeGraph, CellPoint}; + +pub const CRYSTAL_ID: u32 = 3950293873; +pub const CLASP_ID: u32 = 606866102; + //pub mod prefabs; pub struct CubeParsers { diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index 6441a22..c9d40e5 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -723,10 +723,10 @@ struct BaseTickInfo { impl BaseTracker { const DOMINATING_MULT: f32 = 4.0; - fn new<'a>(bases_iter: impl std::iter::Iterator, crystals: &[oj_rc_core::cubes::CubeLocationInfo], ba_config: &oj_rc_core::data::battle_arena_config::BattleArenaData) -> Self { + fn new<'a>(bases_iter: impl std::iter::Iterator, crystals: &[oj_rc_core::cubes::CubeLocationInfo], ba_config: &oj_rc_core::data::battle_arena_config::BattleArenaData, base_graph: &oj_rc_core::cubes::CubeGraph) -> Self { let mut bases = std::collections::HashMap::new(); for base_id in bases_iter { - bases.insert(*base_id, BaseInfo::new(crystals)); + bases.insert(*base_id, BaseInfo::new(crystals, base_graph)); } Self { bases, @@ -735,30 +735,6 @@ impl BaseTracker { } } - fn apply_partial_base_heal(&self, crystals: &[oj_rc_core::cubes::CubeLocationInfo], crystal_health: u32, base_id: u8, base: &BaseInfo, 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]; - for crystal_i in old_index..new_index { - base.crystals_healths[crystal_i].store(crystal_health, std::sync::atomic::Ordering::Relaxed); - } - rlnl::events::HealedCubes { - healed_machine: base_id as u16, - type_performing_healing: rlnl::types::TargetType::TeamBase, - target_type: rlnl::types::TargetType::TeamBase, - num_healed_cubes: target_crystals.len() as _, - hit_cubes: target_crystals - .iter() - .map(|loc| { - 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(), - } - } - async fn tick(&self, tick_info: &PointTickInfo, crystals: &[oj_rc_core::cubes::CubeLocationInfo], generic: &crate::matches::GenericGamemodeEngine, 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) { @@ -790,24 +766,26 @@ 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::trace!("Base {} increment passed a crystal index barrier", base_id); + log::trace!("Base {} increment passed a crystal index barrier in game {}", base_id, generic.game_guid()); 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::trace!("Skipping increment in favour of healing damaged/destroyed cube"); + tracked_base.cube_index.fetch_sub(1.0, std::sync::atomic::Ordering::SeqCst); + //log::info!("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); + let existing_health = tracked_base.calculate_crystal_health(first_damaged); + let healing = crystal_health - existing_health; tracked_base.crystals_healths[first_damaged].store(crystal_health, std::sync::atomic::Ordering::Relaxed); let target_crystal = &crystals[first_damaged]; + if existing_health == 0 { + tracked_base.add_crystals_update_graph(vec![target_crystal.to_owned()], crystal_health); + } rlnl::events::HealedCubes { healed_machine: *base_id as u16, type_performing_healing: rlnl::types::TargetType::TeamBase, @@ -816,30 +794,13 @@ 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: crystal_health.saturating_sub(2) as i32, + damage: healing as i32, } ], } } else { - self.apply_partial_base_heal(crystals, crystal_health, *base_id, tracked_base, old_index, new_index) - /*let target_crystals = &crystals[old_index..new_index]; - for crystal_i in old_index..new_index { - tracked_base.crystals_healths[crystal_i].store(u8::MAX, std::sync::atomic::Ordering::Relaxed); - } - rlnl::events::HealedCubes { - healed_machine: *base_id as u16, - type_performing_healing: rlnl::types::TargetType::TeamBase, - target_type: rlnl::types::TargetType::TeamBase, - num_healed_cubes: target_crystals.len() as _, - hit_cubes: target_crystals - .iter() - .map(|loc| rlnl::types::HitCubeInfo { - pos: rlnl::types::Byte3 { x: loc.x, y: loc.y, z: loc.z, }, - damage: crystal_health as i32, - }) - .collect(), - }*/ + //log::info!("Doing base heal tick"); + tracked_base.apply_partial_base_heal(crystals, crystal_health, *base_id, old_index, new_index) }; generic.broadcast( @@ -859,7 +820,6 @@ impl BaseTracker { if new_index == crystals.len() { // team base is charged to 100% - //self.do_win(*base_id, WinMode::BaseFull, generic).await; return BaseTickInfo { win: Some((*base_id, WinMode::BaseFull)), }; @@ -876,16 +836,18 @@ impl BaseTracker { struct BaseInfo { cube_index: atomic_float::AtomicF32, - crystals_healths: Vec, + crystals_healths: std::sync::Arc>, + base_graph: std::sync::Arc, } impl BaseInfo { - fn new(crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> Self { + fn new(crystals: &[oj_rc_core::cubes::CubeLocationInfo], base_graph: &oj_rc_core::cubes::CubeGraph) -> Self { Self { cube_index: atomic_float::AtomicF32::new(0.0), - crystals_healths: (0..crystals.len()) + crystals_healths: std::sync::Arc::new((0..crystals.len()) .map(|_| std::sync::atomic::AtomicU32::new(0)) - .collect() + .collect()), + base_graph: std::sync::Arc::new(base_graph.to_owned()), } } @@ -912,7 +874,7 @@ impl BaseInfo { fn first_damaged(&self, old_index: usize, max_health: u32) -> Option { for i in 0..old_index { let health = self.calculate_crystal_health(i); - if health != 0 && health != max_health { + if health < max_health { return Some(i); } } @@ -935,7 +897,7 @@ impl BaseInfo { 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); + //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); @@ -943,8 +905,60 @@ impl BaseInfo { old_health != 0 && damage >= old_health } + fn destroy_crystals_update_graph( + &self, + positions: Vec, + crystal_positions: &std::sync::Arc>, + ) { + 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, + 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(), + ); + } + }); + } + /// returns whether crystal exists - fn destroy_crystal_at_pos(&self, pos: rlnl::types::Byte3, crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> bool { + fn destroy_crystal_at_pos(&self, pos: rlnl::types::Byte3, crystals: &std::sync::Arc>) -> bool { 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; } @@ -980,6 +994,34 @@ impl BaseInfo { hit_cubes: target_crystals, } } + + 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(), + } + } } enum WinMode { @@ -989,6 +1031,13 @@ enum WinMode { Surrender, } +fn crystal_health_map(crystal_health: u32) -> std::collections::HashMap { + 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 +} + pub struct BattleArenaLogic { respawn_full_heal_duration: f32, respawn_heal_duration: f32, @@ -1006,6 +1055,11 @@ impl BattleArenaLogic { 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>) -> 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); + 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"); Self { respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_heal_duration: config.respawn_heal_duration, @@ -1013,7 +1067,7 @@ impl BattleArenaLogic { player_tracking: PlayerTracker::new(players), capture_tracking: PointTracker::new(map.capture_points.iter().map(|(_, speed)| *speed)), surrender_tracking: super::trackers::SurrenderGameTracker::new(), - base_tracking: BaseTracker::new(map.bases.keys(), &crystals, &ba_config), + base_tracking: BaseTracker::new(map.bases.keys(), &crystals, &ba_config, &base_graph), //cube_parser, //ba_base: teambase, //ba_equalizer: equalizer, @@ -1184,6 +1238,7 @@ impl BattleArenaLogic { let mut total_destroyed = 0; let mut total_damaged = 0; let mut actual_cube_damage = Vec::with_capacity(cube_damage.hit_cubes.len()); + let mut destroyed_cubes_positions = 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 { @@ -1195,15 +1250,16 @@ impl BattleArenaLogic { } } else if matches!(hit_cube.status.ty, rlnl::types::CubeHistoryEventType::Destroy) { if base.destroy_crystal_at_pos(hit_cube.loc, &self.crystals) { - // TODO handle cube graph disconnects actual_cube_damage.push(hit_cube.to_owned()); + destroyed_cubes_positions.push(hit_cube.loc); total_destroyed += 1; } else { log::warn!("Could not destroy cube with destroy status"); } } } - log::debug!("Destroyed {} ({} damaged) base cubes; {}/{} valid", total_destroyed, total_damaged, actual_cube_damage.len(), cube_damage.hit_cubes.len()); + 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()); let actual_damage_data = actual_damage_data(actual_cube_damage); generic.broadcast( actual_damage_data.event, @@ -1221,11 +1277,10 @@ impl BattleArenaLogic { 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()); - let payload = self.base_tracking.apply_partial_base_heal( + let payload = base.apply_partial_base_heal( &self.crystals, self.config.protonium_health as u32, team_id, - base, old_index, new_index, );