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

5
Cargo.lock generated
View File

@@ -2425,18 +2425,21 @@ dependencies = [
]
[[package]]
name = "oj_factory_api"
name = "oj_factory_web"
version = "1.1.0"
dependencies = [
"actix-files",
"actix-web",
"base64",
"clap",
"env_logger",
"git-version",
"handlebars",
"libfj",
"log",
"oj_rc_core",
"oj_rc_factory",
"serde",
"serde_json",
]

View File

@@ -0,0 +1,387 @@
pub const CUBE_CONNECTIONS: &[CubeConnections] = &[
CubeConnections {
id: 606866102, // Protonium Clasp
connections: &[
CubeConnection {
direction: (0, 0, 1),
position: (0, 38, 25),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 48, 25),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 58, 25),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 68, 25),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 78, 25),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 88, 25),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 98, 25),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 108, 25),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 38, -24),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 48, -24),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 58, -24),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 68, -24),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 78, -24),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 88, -24),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 98, -24),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 108, -24),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 38, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 48, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 58, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 68, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 78, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 88, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 98, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (25, 108, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 38, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 48, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 58, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 68, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 78, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 88, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 98, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-24, 108, 0),
},
/*CubeConnection { // assumed by game client (but not useful)
direction: (0, -1, 0),
position: (0, 0, 0),
},*/
],
},
CubeConnections {
id: 3950293873, // Protonium Crystal
connections: &[
// -y (bottom)
CubeConnection { // assumed by game client
direction: (0, -1, 0),
position: (0, 0, 0),
},
CubeConnection {
direction: (0, -1, 0),
position: (1, 0, 0),
},
CubeConnection {
direction: (0, -1, 0),
position: (0, 0, 1),
},
CubeConnection {
direction: (0, -1, 0),
position: (1, 0, 1),
},
// +y (top)
CubeConnection {
direction: (0, 1, 0),
position: (0, 9, 0),
},
CubeConnection {
direction: (0, 1, 0),
position: (1, 9, 0),
},
CubeConnection {
direction: (0, 1, 0),
position: (0, 9, 1),
},
CubeConnection {
direction: (0, 1, 0),
position: (1, 9, 1),
},
// -x (right?)
CubeConnection {
direction: (1, 0, 0),
position: (5, 4, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (5, 4, 1),
},
CubeConnection {
direction: (1, 0, 0),
position: (5, 5, 0),
},
CubeConnection {
direction: (1, 0, 0),
position: (5, 5, 1),
},
// -x (left?)
CubeConnection {
direction: (-1, 0, 0),
position: (-4, 4, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-4, 4, 1),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-4, 5, 0),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-4, 5, 1),
},
// +z (back)
CubeConnection {
direction: (0, 0, 1),
position: (0, 4, 5),
},
CubeConnection {
direction: (0, 0, 1),
position: (1, 4, 5),
},
CubeConnection {
direction: (0, 0, 1),
position: (0, 5, 5),
},
CubeConnection {
direction: (0, 0, 1),
position: (1, 5, 5),
},
// -z (front)
CubeConnection {
direction: (0, 0, -1),
position: (0, 4, -4),
},
CubeConnection {
direction: (0, 0, -1),
position: (1, 4, -4),
},
CubeConnection {
direction: (0, 0, -1),
position: (0, 5, -4),
},
CubeConnection {
direction: (0, 0, -1),
position: (1, 5, -4),
},
// diagonal faces
CubeConnection {
direction: (-1, 0, 0),
position: (-2, 2, -2),
},
CubeConnection {
direction: (0, -1, 0),
position: (-2, 2, -2),
},
CubeConnection {
direction: (1, 0, 0),
position: (3, 2, -2),
},
CubeConnection {
direction: (0, -1, 0),
position: (3, 2, -2),
},
CubeConnection {
direction: (0, 0, -1),
position: (3, 2, -2),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-2, 2, 3),
},
CubeConnection {
direction: (0, -1, 0),
position: (-2, 2, 3),
},
CubeConnection {
direction: (0, 0, 1),
position: (-2, 2, 3),
},
CubeConnection {
direction: (1, 0, 0),
position: (3, 2, 3),
},
CubeConnection {
direction: (0, -1, 0),
position: (3, 2, 3),
},
CubeConnection {
direction: (0, 0, 1),
position: (3, 2, 3),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-2, 6, -2),
},
CubeConnection {
direction: (0, 1, 0),
position: (-2, 6, -2),
},
CubeConnection {
direction: (0, 0, -1),
position: (-2, 6, -2),
},
CubeConnection {
direction: (1, 0, 0),
position: (3, 6, -2),
},
CubeConnection {
direction: (0, 1, 0),
position: (3, 6, -2),
},
CubeConnection {
direction: (0, 0, -1),
position: (3, 6, -2),
},
CubeConnection {
direction: (-1, 0, 0),
position: (-2, 6, 3),
},
CubeConnection {
direction: (0, 1, 0),
position: (-2, 6, 3),
},
CubeConnection {
direction: (0, 0, 1),
position: (-2, 6, 3),
},
CubeConnection {
direction: (1, 0, 0),
position: (3, 6, 3),
},
CubeConnection {
direction: (0, 1, 0),
position: (3, 6, 3),
},
CubeConnection {
direction: (0, 0, 1),
position: (3, 6, 3),
},
],
},
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),
},
],
},
];
#[derive(Copy, Clone)]
pub struct CubeConnections {
pub id: u32,
pub connections: &'static [CubeConnection],
}
#[derive(Copy, Clone)]
pub struct CubeConnection {
pub direction: (i8, i8, i8),
pub position: (i8, i8, i8),
}

View File

@@ -1,3 +1,6 @@
const CRYSTAL_ID: u32 = 3950293873;
const CLASP_ID: u32 = 606866102;
pub struct CubeLocationsParser;
pub struct CubeLocationInfo {
@@ -68,6 +71,34 @@ impl CubeLocationsParser {
.collect()
}
fn locations_sorted_by_distance_from_point_reward_inlines(cubes: &[super::parser::Cube], point: (u8, u8, u8), locations_of_id: u32, inline_div: f32) -> Vec<CubeLocationInfo> {
let target_x = point.0 as f32;
let target_y = point.1 as f32;
let target_z = point.2 as f32;
let mut relevant_cubes: Vec<(f32, CubeLocationInfo)> = cubes.iter()
.filter(|x| x.id == locations_of_id)
.map(|cube| {
let is_inline = cube.x.wrapping_sub(point.0) == 0 || cube.z.wrapping_sub(point.2) == 0;
let distance = (
(cube.x as f32 - target_x).powi(2)
+ (cube.y as f32 - target_y).powi(2)
+ (cube.z as f32 - target_z).powi(2)
).sqrt();
let distance = if is_inline { distance / inline_div } else { distance };
(distance, CubeLocationInfo {
x: cube.x,
y: cube.y,
z: cube.z,
extras: cube.orientation,
})
})
.collect();
relevant_cubes.sort_by_key(|(distance, _)| (distance * 1_000_000.0) as i64);
relevant_cubes.into_iter()
.map(|(_, cube)| cube)
.collect()
}
pub fn locations_of_by_distance_to_first(&self, r: &mut dyn std::io::Read, locations_of_id: u32, distance_to_id: u32) -> Vec<CubeLocationInfo> {
match super::parser::Cube::parse_list(r) {
Ok(cubes) => {
@@ -113,4 +144,195 @@ impl CubeLocationsParser {
}
}
}
pub fn locations_of_by_distance_from_center(&self, r: &mut dyn std::io::Read, locations_of_id: u32) -> Vec<CubeLocationInfo> {
match super::parser::Cube::parse_list(r) {
Ok(cubes) => {
let mut total = 0;
let mut total_x = 0;
let mut total_y = 0;
let mut total_z = 0;
for cube in cubes.iter() {
if cube.id == locations_of_id {
total += 1;
total_x += cube.x as usize;
total_y += cube.y as usize;
total_z += cube.z as usize;
}
}
let center = (total_x / total, total_y / total, total_z / total);
let center = (center.0 as u8, center.1 as u8, center.2 as u8);
Self::locations_sorted_by_distance_from_point_reward_inlines(
&cubes,
center,
locations_of_id,
4.0,
)
}
Err(e) => {
log::error!("Failed to parse cube data to find cube locations: {}", e);
Vec::default()
}
}
}
pub fn locations_of_reactor_sort(&self, r: &mut dyn std::io::Read) -> Vec<CubeLocationInfo> {
use super::{CUBE_CONNECTIONS, CUBE_ROTATIONS};
match super::parser::Cube::parse_list(r) {
Ok(cubes) => {
// get connection data
let location_cube_connections = if let Some(conn) = CUBE_CONNECTIONS.iter().find(|x| x.id == CRYSTAL_ID) {
conn
} else {
CUBE_CONNECTIONS.iter().find(|x| x.id == 0).unwrap()
};
let connected_to_connections = if let Some(conn) = CUBE_CONNECTIONS.iter().find(|x| x.id == CLASP_ID) {
conn
} else {
CUBE_CONNECTIONS.iter().find(|x| x.id == 0).unwrap()
};
// 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;
}
}
let mut available_faces = if let Some(abs_connected_to) = abs_connected_to {
abs_connected_to
} else {
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() {
if cube.id == CRYSTAL_ID {
let rot_index = cube.orientation & 0b01111111;
let rot = &CUBE_ROTATIONS[rot_index as usize];
log::trace!("Calculating location {} connection points", i);
let calc_conns = calculate_absolute_connections(location_cube_connections, rot, (cube.x, cube.y, cube.z));
to_be_sorted.insert(i, calc_conns);
}
}
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();
while !to_be_sorted.is_empty() && iteration < 32 {
for (cube_i, calc_conns) in to_be_sorted.iter() {
let connection = is_sharing_connection(calc_conns, &available_faces)
.and_then(|(cube_face_i, face_available_i)| {
use rand::Rng;
if face_available_i < connected_to_connections.connections.len() && sorted.len() < connected_to_connections.connections.len() {
// prioritize finding all target connections first
Some((cube_face_i, face_available_i))
} else if iteration < 3 || (face_available_i >= connected_to_connections.connections.len() && sorted.len() < connected_to_connections.connections.len()) {
let random_face = random.random_range(0..calc_conns.len());
if cube_face_i >= random_face {
Some((cube_face_i, face_available_i))
} else {
None
}
} else {
Some((cube_face_i, face_available_i))
}
});
if connection.is_some() {
to_be_released.push(*cube_i);
let cube = &cubes[*cube_i];
sorted.push(CubeLocationInfo {
x: cube.x,
y: cube.y,
z: cube.z,
extras: cube.orientation,
});
}
}
for to_release in to_be_released.iter() {
if let Some(mut new_faces) = to_be_sorted.remove(to_release) {
available_faces.append(&mut new_faces);
}
}
// guarantee this will not endlessly try to find just target connections
// (this moves on to considering already-connected locations_of_id cubes)
log::debug!("Cube connection iteration {} found {} new connections", iteration, to_be_released.len());
to_be_released.clear();
iteration += 1;
}
if to_be_sorted.is_empty() {
log::debug!("Solved cube connections order in {} iterations", iteration);
} else {
log::warn!("Failed to solve cube connections order after {} iterations (returning incomplete list)", iteration);
}
sorted
}
Err(e) => {
log::error!("Failed to parse cube data to find cube locations by connections: {}", e);
Vec::default()
}
}
}
}
#[derive(Hash, Eq, PartialEq, Clone, Copy)]
struct AbsoluteConnection {
direction: (i8, i8, i8),
position: (u8, u8, u8),
}
fn calculate_absolute_connections(conns: &super::connections::CubeConnections, rot: &num_quaternion::Quaternion<f32>, pos: (u8, u8, u8)) -> Vec<AbsoluteConnection> {
let unit_rot = rot.normalize().unwrap();
let mut abs_conns = Vec::with_capacity(conns.connections.len());
for conn in conns.connections.iter() {
let rotated_pos = unit_rot.rotate_vector([
conn.position.0 as f32,
conn.position.1 as f32,
conn.position.2 as f32
]);
let rotated_dir = unit_rot.rotate_vector([
conn.direction.0 as f32,
conn.direction.1 as f32,
conn.direction.2 as f32
]);
let new_conn = AbsoluteConnection {
direction: (rotated_dir[0] as i8, rotated_dir[1] as i8, rotated_dir[2] as i8),
position: (
pos.0.saturating_add_signed(rotated_pos[0] as i8).saturating_add_signed(rotated_dir[0].clamp(0.0, 1.0) as i8),
pos.1.saturating_add_signed(rotated_pos[1] as i8).saturating_add_signed(rotated_dir[1].clamp(0.0, 1.0) as i8),
pos.2.saturating_add_signed(rotated_pos[2] as i8).saturating_add_signed(rotated_dir[2].clamp(0.0, 1.0) as i8),
),
};
log::trace!("absolute connection point {:?}", new_conn.position);
abs_conns.push(new_conn);
}
abs_conns
}
fn is_sharing_connection(a: &[AbsoluteConnection], b: &[AbsoluteConnection]) -> Option<(usize, usize)> {
for (i_a, conn_a) in a.iter().enumerate() {
for (i_b, conn_b) in b.iter().enumerate() {
if conn_b.position == conn_a.position
&& conn_b.direction.0 == -conn_a.direction.0
&& conn_b.direction.1 == -conn_a.direction.1
&& conn_b.direction.2 == -conn_a.direction.2 {
return Some((i_a, i_b));
}
}
}
None
}

View File

@@ -1,4 +1,4 @@
mod parser;
mod parser;
mod weapon_list;
pub use weapon_list::WeaponListParser;
@@ -12,6 +12,12 @@ pub use locations_of::{CubeLocationsParser, CubeLocationInfo};
mod offsetter;
pub use offsetter::OffsetParser;
mod connections;
pub use connections::CUBE_CONNECTIONS;
mod rotations;
pub use rotations::CUBE_ROTATIONS;
//pub mod prefabs;
pub struct CubeParsers {

View File

@@ -12,15 +12,27 @@ impl OffsetParser {
let colours = &mut colours[4..];
for cube in cubes.chunks_mut(8) {
// bytes 4, 5, 6 are x, y, z (respectively)
cube[4] = (cube[4] as i16 + offset.0) as _;
cube[5] = (cube[5] as i16 + offset.1) as _;
cube[6] = (cube[6] as i16 + offset.2) as _;
cube[4] = (cube[4] as u16).saturating_add_signed(offset.0) as _;
cube[5] = (cube[5] as u16).saturating_add_signed(offset.1) as _;
cube[6] = (cube[6] as u16).saturating_add_signed(offset.2) as _;
}
for colour in colours.chunks_mut(4) {
// last 3 bytes are x, y, z (respectively)
colour[1] = (colour[1] as i16 + offset.0) as _;
colour[2] = (colour[2] as i16 + offset.1) as _;
colour[3] = (colour[3] as i16 + offset.2) as _;
colour[1] = (colour[1] as u16).saturating_add_signed(offset.0) as _;
colour[2] = (colour[2] as u16).saturating_add_signed(offset.1) as _;
colour[3] = (colour[3] as u16).saturating_add_signed(offset.2) as _;
}
}
pub fn offset_inplace_with_id(&self, cubes: &mut [u8], offset: (i8, i8, i8), target_id: u32) {
let cubes = &mut cubes[4..];
for cube in cubes.chunks_mut(8) {
let id = u32::from_le_bytes([cube[0], cube[1], cube[2], cube[3]]);
if id != target_id { continue; }
// bytes 4, 5, 6 are x, y, z (respectively)
cube[4] = cube[4].saturating_add_signed(offset.0);
cube[5] = cube[5].saturating_add_signed(offset.1);
cube[6] = cube[6].saturating_add_signed(offset.2);
}
}
}

View File

@@ -0,0 +1,27 @@
#[allow(clippy::approx_constant)]
pub const CUBE_ROTATIONS: [num_quaternion::Quaternion<f32>; 24] = [
num_quaternion::Quaternion::new(1.0, 0.0, 0.0, 0.0),
num_quaternion::Quaternion::new(0.7071068, 0.0, 0.0, 0.7071068),
num_quaternion::Quaternion::new(0.0, 0.0, 0.0, 1.0),
num_quaternion::Quaternion::new(-0.7071068, 0.0, 0.0, 0.7071068),
num_quaternion::Quaternion::new(0.7071068, 0.0, 0.7071068, 0.0),
num_quaternion::Quaternion::new(0.5, 0.5, 0.5, 0.5),
num_quaternion::Quaternion::new(0.5, -0.5, -0.5, -0.5),
num_quaternion::Quaternion::new(-0.5, 0.5, -0.5, 0.5),
num_quaternion::Quaternion::new(-0.5, 0.5, 0.5, -0.5),
num_quaternion::Quaternion::new(0.5, 0.5, -0.5, -0.5),
num_quaternion::Quaternion::new(0.5, 0.5, -0.5, 0.5),
num_quaternion::Quaternion::new(-0.5, -0.5, -0.5, 0.5),
num_quaternion::Quaternion::new(-0.5, 0.5, -0.5, -0.5),
num_quaternion::Quaternion::new(0.0, 0.7071068, 0.0, 0.7071068),
num_quaternion::Quaternion::new(0.0, 0.0, 1.0, 0.0),
num_quaternion::Quaternion::new(0.0, 0.7071068, 0.7071068, 0.0),
num_quaternion::Quaternion::new(0.0, 1.0, 0.0, 0.0),
num_quaternion::Quaternion::new(0.0, 0.7071068, -0.7071068, 0.0),
num_quaternion::Quaternion::new(-0.7071068, 0.0, 0.7071068, 0.0),
num_quaternion::Quaternion::new(0.0, 0.7071068, 0.0, -0.7071068),
num_quaternion::Quaternion::new(0.7071068, 0.7071068, 0.0, 0.0),
num_quaternion::Quaternion::new(0.0, 0.0, -0.7071068, 0.7071068),
num_quaternion::Quaternion::new(-0.7071068, 0.7071068, 0.0, 0.0),
num_quaternion::Quaternion::new(0.0, 0.0, -0.7071068, -0.7071068),
];

File diff suppressed because one or more lines are too long

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;

View File

@@ -19,6 +19,7 @@ impl <C: Send + 'static> SimpleOperation<C> for BattleArenaConfigurer {
async fn handle(&self, params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
let user_info = user.user()?;
log::warn!("Retrieved ba config");
let data = self.ba_conf.resolve_typed(user_info.as_ref().as_ref(), self.factory.as_ref(), &self.weapon_list, &self.cpu_counter).await?;
let mut params = params.to_dict();
params.insert(PARAM_KEY, data);