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

Implement basic battle arena functionality #32

This commit is contained in:
NG (Graham)
2025-08-30 21:36:01 -04:00
parent 1403a6e3d0
commit 7f9ed8bf35
41 changed files with 1836 additions and 327 deletions

2
Cargo.lock generated
View File

@@ -2686,6 +2686,7 @@ version = "0.4.0"
dependencies = [
"argon2",
"async-trait",
"base64 0.22.1",
"chrono",
"hex",
"jsonwebtoken",
@@ -2804,7 +2805,6 @@ name = "oj_rc_services_room"
version = "0.4.0"
dependencies = [
"async-trait",
"base64 0.22.1",
"chrono",
"clap",
"env_logger",

View File

@@ -10,6 +10,7 @@ authors.workspace = true
[dependencies]
log.workspace = true
polariton.workspace = true
base64 = "0.22"
hex = "0.4"
serde.workspace = true
serde_json.workspace = true

View File

@@ -0,0 +1,92 @@
pub struct CubeLocationsParser;
pub struct CubeLocationInfo {
pub x: u8,
pub y: u8,
pub z: u8,
extras: u8,
}
impl CubeLocationInfo {
pub fn orientation(&self) -> u8 {
self.extras & 0x7F
}
pub fn is_destroyed(&self) -> bool {
self.extras & 0x80 != 0
}
}
impl CubeLocationsParser {
pub fn with_cubes<'a, I: std::iter::Iterator<Item=&'a crate::persist::Cube>>(_iter: I) -> Self {
Self
}
pub fn locations_of(&self, r: &mut dyn std::io::Read, cube_id: u32) -> Vec<CubeLocationInfo> {
match super::parser::Cube::parse_list(r) {
Ok(cubes) => {
cubes.into_iter()
.filter(|x| x.id == cube_id)
.map(|cube| CubeLocationInfo {
x: cube.x,
y: cube.y,
z: cube.z,
extras: cube.orientation,
})
.collect()
}
Err(e) => {
log::error!("Failed to parse cube data to find cube locations: {}", e);
Vec::default()
}
}
}
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) => {
if let Some(target) = cubes.iter().filter(|x| x.id == distance_to_id).next() {
let target_x = target.x as f32;
let target_y = target.y as f32;
let target_z = target.z as f32;
let mut relevant_cubes: Vec<(f32, CubeLocationInfo)> = cubes.into_iter()
.filter(|x| x.id == locations_of_id)
.map(|cube| {
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();
(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()
} else {
log::warn!("No cube with id {} to calculate distance", distance_to_id);
cubes.into_iter()
.filter(|x| x.id == locations_of_id)
.map(|cube| CubeLocationInfo {
x: cube.x,
y: cube.y,
z: cube.z,
extras: cube.orientation,
})
.collect()
}
}
Err(e) => {
log::error!("Failed to parse cube data to find cube locations: {}", e);
Vec::default()
}
}
}
}

View File

@@ -6,9 +6,15 @@ pub use weapon_list::WeaponListParser;
mod cpu_count;
pub use cpu_count::CpuListParser;
mod locations_of;
pub use locations_of::{CubeLocationsParser, CubeLocationInfo};
//pub mod prefabs;
pub struct CubeParsers {
weapon_list: std::sync::Arc<WeaponListParser>,
cpu_counter: std::sync::Arc<CpuListParser>,
locations: std::sync::Arc<CubeLocationsParser>,
}
impl CubeParsers {
@@ -17,6 +23,7 @@ impl CubeParsers {
Self {
weapon_list: std::sync::Arc::new(WeaponListParser::with_cubes(cubes.values())),
cpu_counter: std::sync::Arc::new(CpuListParser::with_cubes(cubes.values())),
locations: std::sync::Arc::new(CubeLocationsParser::with_cubes(cubes.values())),
}
}
@@ -27,4 +34,8 @@ impl CubeParsers {
pub fn cpu_counter(&self) -> std::sync::Arc<CpuListParser> {
self.cpu_counter.clone()
}
pub fn locations_of(&self) -> std::sync::Arc<CubeLocationsParser> {
self.locations.clone()
}
}

View File

@@ -1,5 +1,7 @@
#![allow(dead_code)]
use std::io::Write;
pub struct Cube {
pub id: u32,
pub x: u8,
@@ -35,6 +37,22 @@ impl Cube {
}
Ok(cubes)
}
pub fn dump(&self, w: &mut dyn std::io::Write) -> std::io::Result<usize> {
w.write_all(&self.id.to_le_bytes())?;
w.write_all(&[self.x, self.y, self.z, self.orientation])?;
Ok(8)
}
pub fn dump_list(items: Vec<Self>) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::with_capacity(4 + (items.len() * 8));
let mut dumped = std::io::Cursor::new(&mut buf);
dumped.write_all(&(items.len() as u32).to_le_bytes())?;
for item in items {
item.dump(&mut dumped)?;
}
Ok(buf)
}
}
pub struct Colour {

View File

@@ -16,16 +16,16 @@ pub struct BattleArenaData {
pub heal_escalation_time_seconds: i64,
}
fn to_obj_arr_u(slice: &[u64]) -> Typed {
Typed::ObjArr(slice.iter().map(|x| Typed::Long(*x as i64)).collect::<Vec<Typed>>().into())
fn to_obj_arr_u<C>(slice: &[u64]) -> Typed<C> {
Typed::<C>::ObjArr(slice.iter().map(|x| Typed::<C>::Long(*x as i64)).collect::<Vec<Typed<C>>>().into())
}
fn to_obj_arr_i(slice: &[i64]) -> Typed {
Typed::ObjArr(slice.iter().map(|x| Typed::Long(*x)).collect::<Vec<Typed>>().into())
fn to_obj_arr_i<C>(slice: &[i64]) -> Typed<C> {
Typed::<C>::ObjArr(slice.iter().map(|x| Typed::<C>::Long(*x)).collect::<Vec<Typed<C>>>().into())
}
impl BattleArenaData {
pub fn as_transmissible(&self) -> Typed {
pub fn as_transmissible<C>(&self) -> Typed<C> {
Typed::HashMap(vec![
(Typed::Str("protoniumHealth".into()), Typed::Long(self.protonium_health)),
(Typed::Str("respawnTimeSeconds".into()), Typed::Long(self.respawn_time_seconds)),

View File

@@ -15,6 +15,7 @@ pub mod channel;
pub mod sanction;
pub mod robot_data;
pub mod lobby;
pub mod battle_arena_config;
pub mod error_codes;

View File

@@ -248,7 +248,7 @@ fn default_game_modes() -> GameModes {
respawn_heal_duration: 10.0,
respawn_full_heal_duration: 0.5,
kill_limit: 0,
game_time_m: 20,
game_time_m: 5,
},
elimination: GameMode {
respawn_heal_duration: 10.0,
@@ -369,6 +369,21 @@ fn default_rotation() -> GameEventSequence {
strategy: GameRotationStrategy::Sequence,
modes: vec![
GameEvents {
singleplayer: GameEvent {
map: GameMap::Earth1,
visibility: GameVisibility::Good,
mode: GameType::BattleArena,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Earth1,
visibility: GameVisibility::Good,
mode: GameType::BattleArena,
auto_heal: true,
},
duration_s: 5*60, // 5 minutes
}
/*GameEvents {
singleplayer: GameEvent {
map: GameMap::Neptune1,
visibility: GameVisibility::Good,
@@ -487,7 +502,7 @@ fn default_rotation() -> GameEventSequence {
auto_heal: true,
},
duration_s: 5*60,
},
},*/
]
}
}
@@ -498,6 +513,7 @@ fn default_multiplayer() -> super::MultiplayerConfig {
enabled: true,
network: super::multiplayer::default_net_conf(),
fakes: super::multiplayer::default_fake_users(),
battle_arena: super::multiplayer::default_ba_conf(),
}
}

View File

@@ -410,9 +410,18 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
z: base.z,
},
}, base.percent_per_second))).collect();
let capture_points = conf.capture_points.iter().map(|point| (super::Sphere {
radius: point.radius,
center: super::Point {
x: point.x,
y: point.y,
z: point.z,
}
}, point.percent_per_second)).collect();
let map_conf = super::MapConfig {
spawns,
bases,
capture_points,
};
(map.into_conf(), map_conf)
}).collect()
@@ -441,4 +450,10 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
total: self.battle.energy.total,
}
}
fn ba_settings(&self) -> super::BattleArenaResolver {
super::BattleArenaResolver {
data: self.battle.multiplayer.battle_arena.clone(),
}
}
}

View File

@@ -2,7 +2,7 @@ mod cubes_json;
pub use cubes_json::CubeConfig;
mod traits;
pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig};
pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver};
pub type ConfigImpl = CubeConfig;

View File

@@ -35,6 +35,7 @@ pub trait ConfigProvider<C: Clone> {
fn url_links(&self) -> LinksConfig;
fn fake_players(&self) -> Vec<FakePlayer>;
fn energy(&self) -> EnergyConfig;
fn ba_settings(&self) -> BattleArenaResolver;
}
pub struct CompleteCampaignProvider {
@@ -358,7 +359,8 @@ pub struct Sphere {
#[derive(Clone, Debug)]
pub struct MapConfig {
pub spawns: std::collections::HashMap<u8, Vec<Point>>, // team -> points
pub bases: std::collections::HashMap<u8, (Sphere, f32)>, // team -> base
pub bases: std::collections::HashMap<u8, (Sphere, f32)>, // team -> (base, capture speed)
pub capture_points: Vec<(Sphere, f32)>, // (capture point, capture speed)
}
#[derive(Clone, Debug)]
@@ -386,3 +388,38 @@ pub struct EnergyConfig {
pub refill_rate: f32,
pub total: u32,
}
pub struct BattleArenaResolver {
pub(super) data: crate::persist::multiplayer::BattleArenaConfig,
}
impl BattleArenaResolver {
pub async fn resolve(&self, user: &dyn crate::persist::user::CommonUser, factory: &crate::factory::Factory, weapon_list: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result<crate::data::battle_arena_config::BattleArenaData, polariton_server::operations::SimpleOpError> {
let equalizer_data = user.resolve_config_vehicle(&self.data.equalizer.clone().into_conf(), factory, weapon_list, cpu_counter).await?;
let base_data = user.resolve_config_vehicle(&self.data.base.clone().into_conf(), factory, weapon_list, cpu_counter).await?;
Ok(crate::data::battle_arena_config::BattleArenaData {
protonium_health: self.data.crystal_health as i64,
respawn_time_seconds: self.data.respawn_time_s as i64,
heal_over_time_per_tower: vec![10, 10, 10, 10], // Unused?
base_machine_map: base_data.robot_map,
equalizer_model: equalizer_data.robot_map,
equalizer_health: self.data.equalizer_health as i64,
equalizer_trigger_time_seconds: vec![10, 10, 10, 10, 10], // TODO
equalizer_warning_seconds: self.data.equalizer_warning_s as i64, // TODO
equalizer_duration_seconds: vec![20, 20, 20, 20, 20], // TODO
capture_time_seconds_per_player: vec![30, 20, 10, 5, 1], // TODO
num_segments: self.data.num_segments as i32,
heal_escalation_time_seconds: 5, // Unused?
})
}
pub async fn resolve_typed<C>(&self, user: &dyn crate::persist::user::CommonUser, factory: &crate::factory::Factory, weapon_list: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result<Typed<C>, polariton_server::operations::SimpleOpError> {
Ok(Typed::Dict(polariton::operation::Dict {
key_ty: polariton::serdes::TypePrefix::Str, // str
val_ty: polariton::serdes::TypePrefix::HashMap, // hashmap
items: vec![
(Typed::Str("BattleArenaSettings".into()), self.resolve(user, factory, weapon_list, cpu_counter).await?.as_transmissible())
],
}))
}
}

View File

@@ -185,6 +185,16 @@ pub struct PrefabVehicle {
pub id: PrefabId,
}
impl PrefabVehicle {
pub(super) fn into_conf(&self) -> super::config::VehicleInfo {
crate::persist::config::VehicleInfo {
name: self.name.clone(),
username: self.username.clone(),
id: self.id.clone().into(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum PrefabId {

View File

@@ -10,6 +10,7 @@ pub struct MapsConfig {
pub struct MapConfig {
pub spawn_points: Vec<SpawnPoint>,
pub bases: Vec<CaptureBase>,
pub capture_points: Vec<CapturePoint>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -35,13 +36,12 @@ impl SpawnPoint {
self
}
fn rotated_from(mut self, x: f32, y: f32, z: f32, rot: num_quaternion::Quaternion<f32>) -> Self {
if let Some(unit_rot) = rot.normalize() {
let rotated = unit_rot.rotate_vector([self.x, self.y, self.z]);
self.x = rotated[0] + x;
self.y = rotated[1] + y;
self.z = rotated[2] + z;
}
fn rotated(mut self, rot: num_quaternion::Quaternion<f32>) -> Self {
let unit_rot = rot.normalize().expect("Bad rotation quaternion for SpawnPoint");
let rotated = unit_rot.rotate_vector([self.x, self.y, self.z]);
self.x = rotated[0];
self.y = rotated[1];
self.z = rotated[2];
self
}
}
@@ -65,8 +65,37 @@ impl CaptureBase {
}
}
const DEFAULT_PERCENT_PER_SECOND: f32 = 2.5;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CapturePoint {
pub x: f32,
pub y: f32,
pub z: f32,
pub radius: f32,
pub percent_per_second: f32,
}
impl CapturePoint {
const fn offset(mut self, x: f32, y: f32, z: f32) -> Self {
self.x += x;
self.y += y;
self.z += z;
self
}
fn rotated(mut self, rot: num_quaternion::Quaternion<f32>) -> Self {
let unit_rot = rot.normalize().expect("Bad rotation quaternion for CapturePoint");
let rotated = unit_rot.rotate_vector([self.x, self.y, self.z]);
self.x = rotated[0];
self.y = rotated[1];
self.z = rotated[2];
self
}
}
const DEFAULT_BASE_PERCENT_PER_SECOND: f32 = 2.5;
const DEFAULT_BASE_RADIUS: f32 = 20.0;
const DEFAULT_CAPTURE_PERCENT_PER_SECOND: f32 = DEFAULT_BASE_PERCENT_PER_SECOND * 1.5;
const DEFAULT_CAPTURE_RADIUS: f32 = 14.0;
pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap, MapConfig> {
let mut map = std::collections::HashMap::with_capacity(9);
@@ -204,7 +233,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 2.700,
z: -240.888,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -212,9 +241,37 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 2.770,
z: 243.600,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
],
capture_points: vec![
CapturePoint {
x: 177.240,
y: -0.264,
z: 141.540,
radius: DEFAULT_CAPTURE_RADIUS,
percent_per_second: DEFAULT_CAPTURE_PERCENT_PER_SECOND,
},
CapturePoint {
x: -9.480,
y: 30.396,
z: -164.676,
radius: DEFAULT_CAPTURE_RADIUS,
percent_per_second: DEFAULT_CAPTURE_PERCENT_PER_SECOND,
},
CapturePoint {
x: -197.400,
y: -0.228,
z: 142.452,
radius: DEFAULT_CAPTURE_RADIUS,
percent_per_second: DEFAULT_CAPTURE_PERCENT_PER_SECOND,
}
].into_iter().map(|p| p.rotated(num_quaternion::Quaternion {
w: 0.707107,
x: 0.0,
y: 0.707107,
z: 0.0,
}).offset(13.320, 0.0, -9.84)).collect(),
});
map.insert(super::combat::GameMap::Earth2, MapConfig { // level4
spawn_points: vec![
@@ -400,12 +457,12 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: -71.445,
z: 266.000,
},
].into_iter().map(|x| x.rotated_from(0.0, 86.718, 0.0, num_quaternion::Quaternion {
].into_iter().map(|x| x.rotated(num_quaternion::Quaternion {
x: 0.0,
y: 0.707107,
z: 0.0,
w: 0.707107,
})).collect(),
}).offset(0.0, 86.718, 0.0)).collect(),
bases: vec![
CaptureBase {
team: 0,
@@ -413,7 +470,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 13.900,
z: -253.920,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -421,9 +478,10 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 13.900,
z: 259.200,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
],
capture_points: vec![], // TODO
});
map.insert(super::combat::GameMap::Mars1, MapConfig {
spawn_points: vec![
@@ -557,7 +615,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 4.09,
z: 20.3,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -565,9 +623,10 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 10.63,
z: 372.20,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
],
capture_points: vec![], // TODO
});
map.insert(super::combat::GameMap::Mars2, MapConfig {
spawn_points: vec![
@@ -701,7 +760,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 21.140,
z: 187.560,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -709,9 +768,10 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 21.230,
z: 620.280,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
].into_iter().map(|x| x.offset(-434.640, 0.0, -414.720)).collect(),
capture_points: vec![], // TODO
});
map.insert(super::combat::GameMap::Mars3, MapConfig {
spawn_points: vec![
@@ -845,7 +905,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 32.340,
z: -309.720,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -853,9 +913,10 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 32.320,
z: 207.360,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
].into_iter().map(|x| x.offset(49.608, 0.0, 52.493)).collect(),
capture_points: vec![], // TODO
});
map.insert(super::combat::GameMap::Neptune1, MapConfig {
spawn_points: vec![
@@ -989,7 +1050,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 1.580,
z: -82.150,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -997,9 +1058,10 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: -0.110,
z: 292.956,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
].into_iter().map(|x| x.offset(405.542, 0.0, 10.668)).collect(),
capture_points: vec![], // TODO
});
map.insert(super::combat::GameMap::Neptune2, MapConfig {
spawn_points: vec![
@@ -1133,7 +1195,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 18.370,
z: -181.488,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -1141,9 +1203,10 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 18.390,
z: 196.416,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
],
capture_points: vec![], // TODO
});
map.insert(super::combat::GameMap::Neptune3, MapConfig {
spawn_points: vec![
@@ -1277,7 +1340,7 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 42.270,
z: -151.440,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
CaptureBase {
team: 1,
@@ -1285,9 +1348,10 @@ pub(super) fn default_map() -> std::collections::HashMap<super::combat::GameMap,
y: 42.290,
z: 147.240,
radius: DEFAULT_BASE_RADIUS,
percent_per_second: DEFAULT_PERCENT_PER_SECOND,
percent_per_second: DEFAULT_BASE_PERCENT_PER_SECOND,
},
],
capture_points: vec![], // TODO
});
map
}

File diff suppressed because one or more lines are too long

View File

@@ -22,11 +22,7 @@ impl SingleplayerConfig {
crate::persist::config::SingleplayerConfig {
max_teammates: self.max_teammates,
max_enemies: self.max_enemies,
vehicles: self.vehicles.iter().map(|v| crate::persist::config::VehicleInfo {
name: v.name.clone(),
username: v.username.clone(),
id: v.id.clone().into(),
}).collect()
vehicles: self.vehicles.iter().map(|v| v.into_conf()).collect()
}
}
}

View File

@@ -417,6 +417,130 @@ impl UserData {
})
}
pub(super) async fn resolve_vehicle(&self, vehicle: &crate::persist::config::VehicleInfo, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result<super::ResolvedVehicle, polariton_server::operations::SimpleOpError> {
match &vehicle.id {
crate::persist::config::VehicleDescriptor::Factory { factory: factory_id } => {
match factory.vehicle(*factory_id).await {
Ok(Some(factory_vehicle)) => {
let uuid_i64 = crate::persist::user::uuid_sanitize(crate::persist::user::i64_join((1 << 30, *factory_id)));
let uuid_str = crate::persist::user::i64_as_uuid_str(uuid_i64);
let weapons_guess = weapon_order.guess_weapons(&mut std::io::Cursor::new(&factory_vehicle.0.cube_data));
let weapons_guess = vec![weapons_guess[0], 0, 0];
let weapon_ranks = weapons_guess.iter().map(|&x| (x, if x == 0 { 0 } else { 1 })).collect();
let cpu_count = if factory_vehicle.1.cpu == 0 {
cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&factory_vehicle.0.cube_data)).total as i32
} else {
factory_vehicle.1.cpu as i32
};
Ok(super::ResolvedVehicle {
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| factory_vehicle.1.name.clone()),
robot_map: factory_vehicle.0.cube_data,
robot_uuid: uuid_str,
cpu: cpu_count,
weapon_order: weapons_guess,
colour_map: factory_vehicle.0.colour_data,
spawn_effect: "Spawn".to_owned(),
death_effect: "Explosion".to_owned(),
weapon_rank: weapon_ranks,
})
},
Ok(None) => {
log::error!("Prefab vehicle {} does not exist in factory", factory_id);
return Err(polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::SingleplayerErrorCode::UnexpectedError as i16,
format!("Prefab vehicle {} does not exist in factory", factory_id),
));
},
Err(e) => {
log::error!("Failed to retrieve prefab vehicle {} from factory: {}", factory_id, e);
return Err(polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16,
format!("Failed to retrieve prefab vehicle {} from factory: {}", factory_id, e),
));
}
}
},
crate::persist::config::VehicleDescriptor::Database { garage } => {
match self.db.garage_by_id(*garage).await {
Ok(Some(db_vehicle)) => {
let cpu_count = if db_vehicle.total_robot_cpu <= 0 {
cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&db_vehicle.robot_data)).total as i32
} else {
db_vehicle.total_robot_cpu
};
let uuid_i64 = crate::persist::user::uuid_sanitize(crate::persist::user::i64_join((1 << 31, *garage as u32)));
let uuid_str = crate::persist::user::i64_as_uuid_str(uuid_i64);
Ok(super::ResolvedVehicle {
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| db_vehicle.name.clone()),
robot_map: db_vehicle.robot_data,
robot_uuid: uuid_str,
cpu: cpu_count,
weapon_order: oj_rc_database::schema::parse_int_csv(&db_vehicle.weapon_order).into_iter().map(|x| x as i32).collect::<Vec<_>>(),
colour_map: db_vehicle.colour_data,
spawn_effect: db_vehicle.spawn_animation_id,
death_effect: db_vehicle.death_animation_id,
weapon_rank: oj_rc_database::schema::parse_int_csv(&db_vehicle.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(),
})
},
Ok(None) => {
log::error!("Prefab vehicle {} does not exist in main garage database", garage);
return Err(polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::SingleplayerErrorCode::UnexpectedError as i16,
format!("Prefab vehicle {} does not exist in main garage database", garage),
));
}
Err(e) => {
log::error!("Failed to retrieve prefab vehicle {} from main garage database: {}", garage, e);
return Err(polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16,
format!("Failed to retrieve prefab vehicle {} from main garage database: {}", garage, e),
));
}
}
},
crate::persist::config::VehicleDescriptor::Raw {
cube_data,
colour_data,
} => {
use sha2::Digest;
let sha_bytes = sha2::Sha256::digest(&cube_data);
let u32_bytes = [
sha_bytes[0],
sha_bytes[1],
sha_bytes[2],
sha_bytes[3],
];
let uuid_i64 = crate::persist::user::uuid_sanitize(crate::persist::user::i64_join((1 << 29, u32::from_le_bytes(u32_bytes))));
let uuid_str = crate::persist::user::i64_as_uuid_str(uuid_i64);
let weapons_guess = weapon_order.guess_weapons(&mut std::io::Cursor::new(&cube_data));
let weapons_guess = vec![
weapons_guess.get(0).map(|x| *x).unwrap_or(0),
weapons_guess.get(1).map(|x| *x).unwrap_or(0),
weapons_guess.get(2).map(|x| *x).unwrap_or(0),
];
let weapon_ranks = weapons_guess.iter().map(|&x| (x, if x == 0 { 0 } else { 1 })).collect();
let cpu_counts = cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&cube_data));
Ok(super::ResolvedVehicle {
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| "Raw Robot".to_owned()),
robot_map: cube_data.to_owned(),
robot_uuid: uuid_str,
cpu: cpu_counts.total as i32,
weapon_order: weapons_guess,
colour_map: colour_data.to_owned(),
spawn_effect: "Spawn".to_owned(),
death_effect: "Explosion".to_owned(),
weapon_rank: weapon_ranks,
})
}
}
}
async fn resolve_some_singleplayer_vehicles(&self, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, singleplayer_config: &crate::persist::config::SingleplayerConfig, cpu_counter: &crate::cubes::CpuListParser) -> Result<Vec<crate::data::player_data::PlayerData>, i16> {
use rand::seq::IndexedRandom;
let mut players = Vec::with_capacity((singleplayer_config.max_enemies + singleplayer_config.max_teammates + 1) as usize);
@@ -439,121 +563,29 @@ impl UserData {
};
seen_usernames.insert(username.clone());
let team_num = if i < singleplayer_config.max_enemies { 1 } else { 0 };
let enemy = match &vehicle.id {
crate::persist::config::VehicleDescriptor::Factory { factory: factory_id } => {
//use oj_rc_factory::VehicleFactoryAdapter;
match factory.vehicle(*factory_id).await {
Ok(Some(factory_vehicle)) => {
let weapons_guess = weapon_order.guess_weapons(&mut std::io::Cursor::new(&factory_vehicle.0.cube_data));
let weapons_guess = vec![weapons_guess[0], 0, 0];
let weapon_ranks = weapons_guess.iter().map(|&x| (x, if x == 0 { 0 } else { 1 })).collect();
let cpu_count = if factory_vehicle.1.cpu == 0 {
cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&factory_vehicle.0.cube_data)).total as i32
} else {
factory_vehicle.1.cpu as i32
};
crate::data::player_data::PlayerData {
name: username.clone(),
display_name: username.clone(),
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| factory_vehicle.1.name.clone()),
robot_map: factory_vehicle.0.cube_data,
group: None,
team: team_num,
has_premium: false,
robot_uuid: uuid_str,
cpu: cpu_count,
avatar_id: None, // not serialised
weapon_order: weapons_guess,
colour_map: factory_vehicle.0.colour_data,
is_ai: true,
spawn_effect: "Spawn".to_owned(),
death_effect: "Explosion".to_owned(),
player_rank: 1,
weapon_rank: weapon_ranks,
}
},
Ok(None) => {
log::error!("Prefab vehicle {} does not exist in factory", factory_id);
return Err(crate::data::error_codes::SingleplayerErrorCode::UnexpectedError as i16);
},
Err(e) => {
log::error!("Failed to retrieve prefab vehicle {} from factory: {}", factory_id, e);
return Err(crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16);
}
}
},
crate::persist::config::VehicleDescriptor::Database { garage } => {
match self.db.garage_by_id(*garage).await {
Ok(Some(db_vehicle)) => {
let cpu_count = if db_vehicle.total_robot_cpu <= 0 {
cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&db_vehicle.robot_data)).total as i32
} else {
db_vehicle.total_robot_cpu
};
crate::data::player_data::PlayerData {
name: username.clone(),
display_name: username.clone(),
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| db_vehicle.name.clone()),
robot_map: db_vehicle.robot_data,
group: None,
team: team_num,
has_premium: false,
robot_uuid: uuid_str,
cpu: cpu_count,
avatar_id: None, // not serialised
weapon_order: oj_rc_database::schema::parse_int_csv(&db_vehicle.weapon_order).into_iter().map(|x| x as i32).collect::<Vec<_>>(),
colour_map: db_vehicle.colour_data,
is_ai: true,
spawn_effect: "Spawn".to_owned(),
death_effect: "Explosion".to_owned(),
player_rank: 1,
weapon_rank: oj_rc_database::schema::parse_int_csv(&db_vehicle.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(),
}
},
Ok(None) => {
log::error!("Prefab vehicle {} does not exist in main garage database", garage);
return Err(crate::data::error_codes::SingleplayerErrorCode::UnexpectedError as i16);
}
Err(e) => {
log::error!("Failed to retrieve prefab vehicle {} from main garage database: {}", garage, e);
return Err(crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16);
}
}
},
crate::persist::config::VehicleDescriptor::Raw {
cube_data,
colour_data,
} => {
let weapons_guess = weapon_order.guess_weapons(&mut std::io::Cursor::new(&cube_data));
let weapons_guess = vec![weapons_guess[0], 0, 0];
let weapon_ranks = weapons_guess.iter().map(|&x| (x, if x == 0 { 0 } else { 1 })).collect();
let cpu_counts = cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&cube_data));
crate::data::player_data::PlayerData {
name: username.clone(),
display_name: username.clone(),
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| "Raw Robot".to_owned()),
robot_map: cube_data.to_owned(),
group: None,
team: team_num,
has_premium: false,
robot_uuid: uuid_str,
cpu: cpu_counts.total as i32,
avatar_id: None, // not serialised
weapon_order: weapons_guess,
colour_map: colour_data.to_owned(),
is_ai: true,
spawn_effect: "Spawn".to_owned(),
death_effect: "Explosion".to_owned(),
player_rank: 1,
weapon_rank: weapon_ranks,
}
}
let enemy_vehicle = self.resolve_vehicle(vehicle, factory, weapon_order, cpu_counter).await?;
let weapons = vec![enemy_vehicle.weapon_order[0], 0, 0];
let weapon_ranks = weapons.iter().map(|&x| (x, if x == 0 { 0 } else { 1 })).collect();
let enemy = crate::data::player_data::PlayerData {
name: username.clone(),
display_name: username.clone(),
mastery: enemy_vehicle.mastery,
tier: enemy_vehicle.tier,
robot_name: enemy_vehicle.robot_name,
robot_map: enemy_vehicle.robot_map,
group: None,
team: team_num,
has_premium: false,
robot_uuid: uuid_str,
cpu: enemy_vehicle.cpu,
avatar_id: None, // not serialised
weapon_order: weapons,
colour_map: enemy_vehicle.colour_map,
is_ai: true,
spawn_effect: enemy_vehicle.spawn_effect,
death_effect: enemy_vehicle.death_effect,
player_rank: 1,
weapon_rank: weapon_ranks,
};
next_id += 1;
players.push(enemy);

View File

@@ -0,0 +1,8 @@
use super::account_json::UserData;
#[async_trait::async_trait]
impl super::CommonUser for UserData {
async fn resolve_config_vehicle(&self, vehicle: &crate::persist::config::VehicleInfo, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result<super::ResolvedVehicle, polariton_server::operations::SimpleOpError> {
self.resolve_vehicle(vehicle, factory, weapon_order, cpu_counter).await
}
}

View File

@@ -11,13 +11,14 @@ mod inventory;
pub use inventory::UnlockedParts;
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser};
mod intercom;
pub use intercom::generate_token as generate_intercom_token;
mod multiplayer;
mod lobby;
mod common;
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -61,7 +61,7 @@ pub trait UserAuthenticator {
}
#[async_trait::async_trait]
pub trait User<C>: ChatUser + LobbyUser + MultiplayerUser + IntercomUser {
pub trait User<C>: ChatUser + LobbyUser + MultiplayerUser + IntercomUser + CommonUser {
fn public_id(&self) -> &'_ str;
fn is_mod(&self) -> bool;
fn is_admin(&self) -> bool;
@@ -203,7 +203,7 @@ pub struct AvatarInfo {
}
#[async_trait::async_trait]
pub trait ChatUser {
pub trait ChatUser: CommonUser {
async fn subscribed_channels(&self) -> Result<polariton::operation::Typed<()>, i16>;
async fn subscribed_channels_strings(&self) -> Result<Vec<String>, i16>;
async fn add_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Result<polariton::operation::Typed<()>, i16>;
@@ -325,7 +325,7 @@ pub enum MultiplayerErrorCode {
}
#[async_trait::async_trait]
pub trait MultiplayerUser {
pub trait MultiplayerUser: CommonUser {
fn user_id(&self) -> i32;
fn user_name(&self) -> &'_ str;
fn display_name(&self) -> &'_ str;
@@ -339,3 +339,22 @@ pub trait MultiplayerUser {
pub trait IntercomUser {
async fn save_custom_avatar(&self, image: Vec<u8>) -> Result<(), polariton_server::operations::SimpleOpError>;
}
pub struct ResolvedVehicle {
pub mastery: i32,
pub tier: i32,
pub robot_name: String,
pub robot_map: Vec<u8>,
pub robot_uuid: String,
pub cpu: i32,
pub weapon_order: Vec<i32>,
pub colour_map: Vec<u8>,
pub spawn_effect: String,
pub death_effect: String,
pub weapon_rank: std::collections::HashMap<i32, i32>,
}
#[async_trait::async_trait]
pub trait CommonUser: Send + Sync {
async fn resolve_config_vehicle(&self, vehicle: &crate::persist::config::VehicleInfo, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result<ResolvedVehicle, polariton_server::operations::SimpleOpError>;
}

View File

@@ -15,6 +15,7 @@ mod kill_bonus;
mod assist_bonus;
mod damage_bonus;
mod heal_bonus;
mod player_leave;
pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHandler {
crate::handler::LnlEventHandler::new(init_ctx.users.clone(), crate::vehicle_motion::handler(init_ctx))
@@ -175,6 +176,23 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa
>::handler(init_ctx))
.add(self_destruct_elimination::handler(init_ctx))
.add(map_ping::handler(init_ctx))
// battle arena
.add(crate::handlers::GamemodeSpecific::<
{rlnl::event_code::NetworkEvent::SendDamagedByEnemyShield as i16},
{literustlib::packet::Property::ReliableOrdered as u8},
rlnl::events::ingame::DamagedByEnemyShield,
>::handler(init_ctx))
.add(crate::handlers::GamemodeSpecific::<
{rlnl::event_code::NetworkEvent::SurrenderRequest as i16},
{literustlib::packet::Property::ReliableOrdered as u8},
rlnl::events::ingame::InitiateSurrender,
>::handler(init_ctx))
.add(crate::handlers::GamemodeSpecific::<
{rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest as i16},
{literustlib::packet::Property::ReliableOrdered as u8},
rlnl::events::ingame::AwardProtoniumDestroyedCubes,
>::handler(init_ctx))
.add(player_leave::handler(init_ctx))
}
#[inline]
@@ -211,8 +229,17 @@ mod _broadcast_impls {
impl Broadcastable for rlnl::events::ingame::UpdateVotingAfterBattle {}
impl Broadcastable for rlnl::events::ingame::TeleportActivateEffect {}
impl Broadcastable for rlnl::events::ingame::MapPing {}
impl Broadcastable for rlnl::events::ingame::DamagedByEnemyShield {}
impl Broadcastable for rlnl::events::ingame::InitiateSurrender {}
impl Broadcastable for rlnl::events::ingame::AwardProtoniumDestroyedCubes {}
impl Broadcastable for rlnl::events::sync::UpdateGameModeSettings {}
impl Broadcastable for rlnl::events::sync::GetTeamBase {}
impl Broadcastable for rlnl::events::sync::GetCapturePoints {}
impl Broadcastable for rlnl::events::sync::GetEqualizer {}
impl Broadcastable for rlnl::events::sync::FusionShieldState {}
impl Broadcastable for rlnl::events::sync::EqualizerNotification {}
impl Broadcastable for rlnl::events::GameTime {}
impl Broadcastable for rlnl::events::ingame::TeamBaseState {}
impl Broadcastable for rlnl::events::ingame::GameEnd {}
}

View File

@@ -0,0 +1,30 @@
pub struct PlayerQuit {
msg_router: tokio::sync::mpsc::Sender<crate::matches::GameMessage>,
}
pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::Dataless<PlayerQuit> {
crate::handlers::Dataless::new(PlayerQuit::new(init_ctx))
}
impl PlayerQuit {
fn new(init_ctx: &crate::InitConfig) -> Self {
Self {
msg_router: init_ctx.matches_chann.clone(),
}
}
}
#[async_trait::async_trait]
impl crate::handlers::DatalessEventCodeHandler for PlayerQuit {
const CODE: rlnl::event_code::NetworkEvent = rlnl::event_code::NetworkEvent::PlayerQuitRequest;
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RequestLeave {
user_id: user_info.user_id(),
}).await);
} else {
log::error!("Failed to handle sync loading request for unknown user");
}
}
}

View File

@@ -0,0 +1,42 @@
#![allow(dead_code)]
pub struct GamemodeSpecific<const EVENT: i16, const PROPERTY: u8, InOut: byteserde::des_slice::ByteDeserializeSlice<InOut> + crate::Broadcastable> {
msg_router: tokio::sync::mpsc::Sender<crate::matches::GameMessage>,
event: rlnl::event_code::NetworkEvent,
property: literustlib::packet::Property,
_in: std::marker::PhantomData<InOut>,
}
impl <const EVENT: i16, const PROPERTY: u8, InOut: byteserde::des_slice::ByteDeserializeSlice<InOut> + crate::Broadcastable> GamemodeSpecific<EVENT, PROPERTY, InOut> {
pub fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::simple_typed::SimpleRlnl<InOut, Self> {
crate::handlers::simple_typed::SimpleRlnl::new(GamemodeSpecific::new(init_ctx))
}
fn new(init_ctx: &crate::InitConfig) -> Self {
Self {
msg_router: init_ctx.matches_chann.clone(),
event: crate::handler::i16_to_event_or_panic(EVENT),
property: literustlib::packet::Property::try_from(PROPERTY).expect("Invalid literustlib packet property"),
_in: std::marker::PhantomData::default(),
}
}
}
#[async_trait::async_trait]
impl <const EVENT: i16, const PROPERTY: u8, InOut: byteserde::des_slice::ByteDeserializeSlice<InOut> + crate::Broadcastable> crate::handlers::simple_typed::RlnlEventCodeHandler for GamemodeSpecific<EVENT, PROPERTY, InOut> {
type In = InOut;
const CODE: rlnl::event_code::NetworkEvent = crate::handler::i16_to_event_or_panic(EVENT);
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RebroadcastRlnl {
skip_user_id: user_info.user_id(),
event: self.event,
event_in: Self::CODE,
property: self.property,
data: Some(Box::new(data)),
}).await);
} else {
log::error!("Failed to send gamemode specifc event {:?} for user (no auth!)", self.event);
}
}
}

View File

@@ -47,7 +47,7 @@ impl <const EXCLUDE_SENDER: bool, const CODE_IN: i16, const CODE_OUT: i16, const
}
} else {
log::error!("Failed to rebroadcast OnPlayerInputChanged for user (no auth!)");
log::error!("Failed to rebroadcast {:?}->{:?} for user (no auth!)", Self::CODE, self.code_out);
}
}
}

View File

@@ -44,7 +44,7 @@ impl <const EXCLUDE_SENDER: bool, const CODE_IN: i16, const CODE_OUT: i16, const
}
} else {
log::error!("Failed to rebroadcast OnPlayerInputChanged for user (no auth!)");
log::error!("Failed to rebroadcast {:?}-?{:?} for user (no auth!)", Self::CODE, self.code_out);
}
}
}

View File

@@ -11,6 +11,10 @@ mod ingame_broadcast_dataless;
#[allow(unused_imports)]
pub use ingame_broadcast_dataless::DatalessBroadcaster;
mod gamemode_specific;
#[allow(unused_imports)]
pub use gamemode_specific::GamemodeSpecific;
mod stub;
#[allow(unused_imports)]
pub use stub::Stub;

View File

@@ -27,6 +27,7 @@ impl <In: byteserde::des_slice::ByteDeserializeSlice<In> + Send, H: RlnlEventCod
let mut des = byteserde::des_slice::ByteDeserializerSlice::new(&data);
match In::byte_deserialize(&mut des) {
Ok(rlnl_data) => {
//log::info!("Received {:?} message", H::CODE);
self.handler.handle(rlnl_data, peer, user, sender).await;
},
Err(e) => {
@@ -54,6 +55,7 @@ impl <'a> RlnlSender<'a> {
pub async fn send_data<D: byteserde::ser_heap::ByteSerializeHeap + ?Sized>(&self, data: &D, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, conn: &literustlib_server::Connection<crate::PacketData>) -> std::io::Result<usize> {
let mut ser = byteserde::ser_heap::ByteSerializerHeap::default();
//log::info!("Sending dataful event {:?} {:?}", event, property);
data.byte_serialize_heap(&mut ser).map_err(|e| std::io::Error::new(std::io::ErrorKind::Unsupported, e.message))?;
let event_data = crate::handler::EventData::with_data(
crate::data::MessageType::ServerMsg,
@@ -64,6 +66,7 @@ impl <'a> RlnlSender<'a> {
}
pub async fn send_empty(&self, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, conn: &literustlib_server::Connection<crate::PacketData>) -> std::io::Result<usize> {
//log::info!("Sending dataless event {:?} {:?}", event, property);
let event_data = crate::handler::EventData::without_data(crate::data::MessageType::ServerMsg, event);
self.sender.send_data(event_data, property, conn).await
}

View File

@@ -12,7 +12,7 @@ mod vehicle_motion;
pub struct InitConfig {
pub config: oj_rc_core::persist::config::ConfigImpl,
pub users: std::sync::Arc<oj_rc_core::persist::user::UserImpl>,
pub parsers: oj_rc_core::cubes::CubeParsers,
pub parsers: std::sync::Arc<oj_rc_core::cubes::CubeParsers>,
pub matches_chann: tokio::sync::mpsc::Sender<matches::GameMessage>,
}
@@ -25,8 +25,9 @@ async fn main() -> std::io::Result<()> {
let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data");
let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data"));
users.multiplayer_init().await.expect("Multiplayer init task failed");
let parsers = oj_rc_core::cubes::CubeParsers::new(&config);
let matches = matches::GameMatches::new(&config);
let factory = std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::factory::<'_, '_>(&config).await.expect("Bad vehicle factory (CRF) config"));
let parsers = std::sync::Arc::new(oj_rc_core::cubes::CubeParsers::new(&config));
let matches = matches::GameMatches::new(&config, parsers.clone(), factory.clone());
let matches_chann = matches.spawn();
let init_ctx = InitConfig {

View File

@@ -4,10 +4,13 @@ pub struct GameMatches {
mode_configs: oj_rc_core::data::game_mode::GameModeConfigs,
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_settings: std::sync::Arc<oj_rc_core::persist::config::BattleArenaResolver>,
factory: std::sync::Arc<oj_rc_core::factory::Factory>,
}
impl GameMatches {
pub fn new(conf: &oj_rc_core::persist::config::ConfigImpl) -> Self {
pub fn new(conf: &oj_rc_core::persist::config::ConfigImpl, cube_parsers: std::sync::Arc<oj_rc_core::cubes::CubeParsers>, factory: std::sync::Arc<oj_rc_core::factory::Factory>) -> Self {
Self {
matches: std::collections::HashMap::new(),
routing: std::collections::HashMap::new(),
@@ -17,6 +20,9 @@ impl GameMatches {
.map(|(map, conf)| (oj_rc_core::data::game_mode::GameMap::from_persist(map).as_str().to_owned(), conf))
.collect(),
fake_players: <oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::fake_players(conf),
cube_parsers,
ba_settings: std::sync::Arc::new(<oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::ba_settings(conf)),
factory,
}
}
@@ -59,6 +65,7 @@ impl GameMatches {
oj_rc_core::persist::config::MapConfig {
spawns: std::collections::HashMap::default(),
bases: std::collections::HashMap::default(),
capture_points: Vec::default(),
}
});
let players = user.game_players(guid).await?;
@@ -78,6 +85,27 @@ impl GameMatches {
fakes_handler,
);
Ok(engine.spawn())
},
oj_rc_core::data::game_mode::GameMode::BattleArena => {
log::warn!("Game {}: Battle Arena is experimental", guid);
let resolved_ba_conf = self.ba_settings.resolve(
user.as_ref(),
self.factory.as_ref(),
&self.cube_parsers.weapon_order(),
&self.cube_parsers.cpu_counter(),
).await.map_err(|e| oj_rc_core::persist::user::MultiplayerError {
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, resolved_ba_conf);
let engine = super::GenericGamemodeEngine::new(
game_info,
map_config,
players,
inner,
fakes_handler,
);
Ok(engine.spawn())
}
mode => {
// TODO support mode gamemodes

View File

@@ -17,5 +17,6 @@ pub trait CustomGameLogic: Sized + Send + Sync + 'static {
async fn on_game_completed(&self, generic: &super::GenericGamemodeEngine<Self>) -> bool;
async fn on_broadcast(&self, generic: &super::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;
async fn on_motion(&self, generic: &super::GenericGamemodeEngine<Self>, motion: &rlnl::machine_motion::MachineMotion, location: (f32, f32, f32)) -> bool;
async fn on_custom(&self, generic: &super::GenericGamemodeEngine<Self>, user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box<dyn crate::Broadcastable>);
}

View File

@@ -81,7 +81,7 @@ async fn erratic_behaviour(send_to: Vec<crate::matches::generic::UserSender>, is
for conn in send_to.iter() {
send_motion_data_to(conn, motion_data.clone()).await;
}
log::info!("Moved experimental bot to ({}, {}, {})", pos_x, pos_y, pos_z);
log::debug!("Moved experimental bot to ({}, {}, {})", pos_x, pos_y, pos_z);
}
fake_timestamp += 1.0;
tokio::time::sleep(SLEEP_PERIOD).await;

View File

@@ -338,7 +338,13 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
counters: UserData::new(),
};
if self.custom_logic_handler.on_player_join(&self, &new_user, &self.players_info).await {
self.spawn_send_loading_events(&new_user, id, self.players_info.clone());
//self.spawn_send_loading_events(&new_user, id, self.players_info.clone());
crate::events::log_lnl_send_failure(new_user.connection.rlnl().send_data(
&rlnl::events::ingame::PlayerId { player: id },
rlnl::event_code::NetworkEvent::GameGuidValidated,
literustlib::packet::Property::ReliableOrdered,
&new_user.connection.connection
).await);
log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid);
self.user_id_map.write().await.insert(new_user.user.user_id(), id);
users.insert(id, new_user);
@@ -360,18 +366,18 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
&rlnl::events::ingame::PlayerId { player: player_id },
true,
).await;
} else {
let mut has_active_connections = false;
for user in self.users.read().await.values() {
let mode = ConnectionMode::from_u8(user.state.mode.load(std::sync::atomic::Ordering::Relaxed));
has_active_connections |= !matches!(mode, ConnectionMode::Disconnected);
}
is_engaged = has_active_connections;
if !has_active_connections {
if self.custom_logic_handler.on_game_completed(&self).await {
if let Err(e) = conn.user.complete_game(self.game_guid()).await {
log::error!("Failed to mark game {} as complete: {}", self.game_guid(), e);
}
}
let mut has_active_connections = false;
for user in self.users.read().await.values() {
let mode = ConnectionMode::from_u8(user.state.mode.load(std::sync::atomic::Ordering::Relaxed));
has_active_connections |= !matches!(mode, ConnectionMode::Disconnected);
}
is_engaged = has_active_connections;
if !has_active_connections {
self.is_complete.store(true, std::sync::atomic::Ordering::Relaxed);
if self.custom_logic_handler.on_game_completed(&self).await {
if let Err(e) = conn.user.complete_game(self.game_guid()).await {
log::error!("Failed to mark game {} as complete: {}", self.game_guid(), e);
}
}
}
@@ -381,107 +387,86 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
}
},
super::GameMessage::RequestLeave { user_id } => {
log::info!("User {} wants to leave game {}", user_id, self.game_guid());
if let Some(player_id) = self.user_key_by_user_id(user_id).await {
if let Some(conn) = self.users.read().await.get(&player_id) {
crate::events::log_lnl_send_failure(conn.connection.rlnl().send_empty(
rlnl::event_code::NetworkEvent::PlayerQuitRequestComplete,
literustlib::packet::Property::ReliableOrdered,
&conn.connection.connection,
).await);
}
}
}
super::GameMessage::LoadingProgress { user_id, user_name, progress } => {
let progress_data = rlnl::events::loading::LoadingProgress {
user_name: rlnl::types::BinaryWriterString(user_name),
progress,
};
let mut all_users_loading_complete = true;
for conn in self.users.read().await.values() {
if user_id == conn.user.user_id() {
let progress_percent = ((progress * 100.0).ceil() as u8).clamp(0, 100);
log::debug!("User {} is loaded {}% into game {}", user_id, progress_percent, self.game_guid());
log::info!("User {} is loaded {}% into game {}", user_id, progress_percent, self.game_guid());
conn.state.progress.store(progress_percent, std::sync::atomic::Ordering::Relaxed);
if progress_percent != 100 {
all_users_loading_complete = false;
}
} else {
all_users_loading_complete &= conn.state.progress.load(std::sync::atomic::Ordering::Relaxed) == 100;
continue;
}
let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed));
match mode {
ConnectionMode::Loading | ConnectionMode::Disconnected => {},
ConnectionMode::WaitingForSync | ConnectionMode::Sync | ConnectionMode::WaitingToStart => {
self.broadcast(
crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data(
&progress_data,
rlnl::event_code::NetworkEvent::BroadcastLoadingProgress,
literustlib::packet::Property::ReliableOrdered,
&progress_data,
false,
).await;
/*if progress > 0.95 {
log::info!("User {} is ready, ending sync", user_id);
crate::events::log_lnl_send_failure(crate::handlers::simple_typed::RlnlSender::new(&conn.sender)
.send_empty(
rlnl::event_code::NetworkEvent::EndOfSync,
literustlib::packet::Property::ReliableOrdered,
&conn.connection,
)
.await);
conn.mode.store(ConnectionMode::InGame.to_u8(), std::sync::atomic::Ordering::Relaxed);
}*/
&conn.connection.connection,
).await);
},
ConnectionMode::InGame => {
log::warn!("Got loading progress for user {} who is supposed to be already in-game", user_id);
},
}
}
if all_users_loading_complete {
for (id, conn) in self.users.read().await.iter() {
if let Err(e) = conn.connection.rlnl().send_empty(
rlnl::event_code::NetworkEvent::EndOfSync,
literustlib::packet::Property::ReliableOrdered,
&conn.connection.connection
).await {
log::error!("Failed to send EndOfSync event to user {}: {}", id, e);
}
}
}
}
super::GameMessage::RequestLoadingProgress { user_id } => {
let mut user_info = None;
for conn in self.users.read().await.values() {
if user_id == conn.user.user_id() {
user_info = Some((
conn.connection.to_owned(),
rlnl::events::loading::LoadingProgress {
log::info!("Got request loading progress");
if let Some(user_key) = self.user_key_by_user_id(user_id).await {
if let Some(user_info) = self.users.read().await.get(&user_key) {
self.spawn_send_loading_events(user_info, user_key, self.players_info.clone());
let sender = user_info.connection.rlnl();
for conn in self.users.read().await.values() {
if user_id == conn.user.user_id() { continue; }
/*crate::events::log_lnl_send_failure(sender.send_data(
&user_info.1,
rlnl::event_code::NetworkEvent::BroadcastLoadingProgress,
literustlib::packet::Property::ReliableOrdered,
&user_info.0.connection,
).await);*/
let event = rlnl::events::loading::LoadingProgress {
user_name: rlnl::types::BinaryWriterString(conn.user.user_name().to_owned()),
progress: (conn.state.progress.load(std::sync::atomic::Ordering::Relaxed) as f32) / 100.0,
},
));
}
}
if let Some(user_info) = user_info {
let sender = crate::handlers::RlnlSender::new(&user_info.0.sender);
for conn in self.users.read().await.values() {
if user_id == conn.user.user_id() { continue; }
/*crate::events::log_lnl_send_failure(sender.send_data(
&user_info.1,
rlnl::event_code::NetworkEvent::BroadcastLoadingProgress,
literustlib::packet::Property::ReliableOrdered,
&user_info.0.connection,
).await);*/
let event = rlnl::events::loading::LoadingProgress {
user_name: rlnl::types::BinaryWriterString(conn.user.user_name().to_owned()),
progress: (conn.state.progress.load(std::sync::atomic::Ordering::Relaxed) as f32) / 100.0,
};
crate::events::log_lnl_send_failure(sender.send_data(
&event,
rlnl::event_code::NetworkEvent::BroadcastLoadingProgress,
literustlib::packet::Property::ReliableOrdered,
&user_info.0.connection,
).await)
}
for fake in self.fake_users.values() {
let event = rlnl::events::loading::LoadingProgress {
user_name: rlnl::types::BinaryWriterString(fake.descriptor.public_id.clone()),
progress: (fake.state.progress.load(std::sync::atomic::Ordering::Relaxed) as f32) / 100.0,
};
crate::events::log_lnl_send_failure(sender.send_data(
&event,
rlnl::event_code::NetworkEvent::BroadcastLoadingProgress,
literustlib::packet::Property::ReliableOrdered,
&user_info.0.connection,
).await)
};
crate::events::log_lnl_send_failure(sender.send_data(
&event,
rlnl::event_code::NetworkEvent::BroadcastLoadingProgress,
literustlib::packet::Property::ReliableOrdered,
&user_info.connection.connection,
).await)
}
for fake in self.fake_users.values() {
let event = rlnl::events::loading::LoadingProgress {
user_name: rlnl::types::BinaryWriterString(fake.descriptor.public_id.clone()),
progress: (fake.state.progress.load(std::sync::atomic::Ordering::Relaxed) as f32) / 100.0,
};
crate::events::log_lnl_send_failure(sender.send_data(
&event,
rlnl::event_code::NetworkEvent::BroadcastLoadingProgress,
literustlib::packet::Property::ReliableOrdered,
&user_info.connection.connection,
).await)
}
} else {
log::error!("Failed to find player {} in connected users for match {}", user_key, self.game_guid());
}
} else {
log::error!("Failed to find user {} in connected users for match {}", user_id, self.game_guid());
@@ -533,7 +518,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
super::GameMessage::LoadComplete { user_id } => {
if let Some(user_key) = self.user_key_by_user_id(user_id).await {
if let Some(conn) = self.users.read().await.get(&user_key) {
log::info!("Loading complete for game {}, user {} ({})", self.game_guid(), user_id, user_key);
log::info!("Loading complete for game {}, user {} (player {})", self.game_guid(), user_id, user_key);
conn.state.progress.store(100, std::sync::atomic::Ordering::Relaxed);
if matches!(ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed)), ConnectionMode::Sync) {
conn.state.mode.store(ConnectionMode::WaitingToStart.to_u8(), std::sync::atomic::Ordering::Relaxed);
@@ -780,7 +765,12 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
}
}
},
super::GameMessage::CustomLogicRlnl { user_id, event, property, data } => {
self.custom_logic_handler.on_custom(&self, user_id, event, property, data).await;
},
super::GameMessage::Motion { user_id, motion } => {
//let (looking_at_x, looking_at_y, looking_at_z) = motion.target_point.clone().into();
//log::info!("Player {} looking at ({}, {}, {})", motion.player_id, looking_at_x, looking_at_y, looking_at_z);
let (x, y, z) = motion.rb_state.rb_pos_rot.pos.into();
let (x2, y2, z2) = motion.rb_state.center_of_mass.into();
let (w3, x3, y3, z3) = motion.rb_state.rb_pos_rot.rot.into();
@@ -810,6 +800,26 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
}, literustlib::packet::Property::Unreliable, &conn.connection.connection).await);
}
}
if self.game_start.load(std::sync::atomic::Ordering::Relaxed) == -1 {
let mut all_users_loading_complete = true;
for conn in self.users.read().await.values() {
let mode = ConnectionMode::from_u8(conn.state.mode.load(std::sync::atomic::Ordering::Relaxed));
let is_in_sync = matches!(mode, ConnectionMode::Sync);
log::info!("Player {} is in mode {:?}", conn.descriptor.player_id, mode);
all_users_loading_complete &= is_in_sync && conn.state.progress.load(std::sync::atomic::Ordering::Relaxed) == 100;
}
if all_users_loading_complete {
for (id, conn) in self.users.read().await.iter() {
if let Err(e) = conn.connection.rlnl().send_empty(
rlnl::event_code::NetworkEvent::EndOfSync,
literustlib::packet::Property::ReliableOrdered,
&conn.connection.connection
).await {
log::error!("Failed to send EndOfSync event to user {}: {}", id, e);
}
}
}
}
} else {
log::warn!("Received machine motion with unknown player id {} from user {}", motion.player_id, user_id);
}
@@ -836,14 +846,15 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
}
}
async fn send_loading_events(user: &UserSender, player_id: u8, players: std::sync::Arc<Vec<oj_rc_core::persist::user::PlayerDescriptor>>) -> std::io::Result<()> {
async fn send_loading_events(user: &UserSender, _player_id: u8, players: std::sync::Arc<Vec<oj_rc_core::persist::user::PlayerDescriptor>>) -> std::io::Result<()> {
//tokio::time::sleep(std::time::Duration::from_millis(1)).await;
let sender = user.rlnl();
sender.send_data(
/*sender.send_data(
&rlnl::events::ingame::PlayerId { player: player_id },
rlnl::event_code::NetworkEvent::GameGuidValidated,
literustlib::packet::Property::ReliableOrdered,
&user.connection
).await?;
).await?;*/
sender.send_data(
&rlnl::events::loading::PlayerIDsAndNames {
num_players: players.len() as _,

View File

@@ -9,6 +9,9 @@ pub enum GameMessage {
EndConnection {
user_id: i32,
},
RequestLeave {
user_id: i32,
},
LoadingProgress {
user_id: i32,
user_name: String,
@@ -81,6 +84,12 @@ pub enum GameMessage {
property: literustlib::packet::Property,
data: Option<Box<dyn crate::Broadcastable>>,
},
CustomLogicRlnl {
user_id: i32,
event: rlnl::event_code::NetworkEvent,
property: literustlib::packet::Property,
data: Box<dyn crate::Broadcastable>,
},
Motion {
user_id: i32,
motion: rlnl::machine_motion::MachineMotion,
@@ -95,6 +104,7 @@ impl GameMessage {
user.user_id()
}
Self::EndConnection { user_id, .. } => *user_id,
Self::RequestLeave { user_id, .. } => *user_id,
Self::LoadingProgress { user_id, .. } => *user_id,
Self::RequestLoadingProgress { user_id, .. } => *user_id,
Self::WeaponSelect { user_id, .. } => *user_id,
@@ -111,6 +121,7 @@ impl GameMessage {
Self::HealCubesBonus { user_id, .. } => *user_id,
Self::BroadcastRlnl { user_id, .. } => *user_id,
Self::RebroadcastRlnl { skip_user_id, .. } => *skip_user_id,
Self::CustomLogicRlnl { user_id, .. } => *user_id,
Self::Motion { user_id, .. } => *user_id,
Self::NoOp => unreachable!("NoOp is irrelevant for user ID"),
}

View File

@@ -0,0 +1,937 @@
use crate::matches::CustomGameLogic;
struct PlayerTracker {
connected: tokio::sync::Mutex<std::collections::HashMap<u8, std::collections::HashSet<u8>>>, // team -> set of player_id
in_point: tokio::sync::RwLock<std::collections::HashMap<u8, std::sync::atomic::AtomicU16>>, // player_id -> in point state (if val > u8::MAX then not in a point)
respawning: tokio::sync::RwLock<std::collections::HashMap<u8, std::sync::atomic::AtomicI64>>, // player_id -> time when they'll spawn (time since unix epoch)
}
impl PlayerTracker {
fn new() -> Self {
Self {
connected: tokio::sync::Mutex::new(std::collections::HashMap::new()),
in_point: tokio::sync::RwLock::new(std::collections::HashMap::new()),
respawning: tokio::sync::RwLock::new(std::collections::HashMap::new()),
}
}
async fn team(&self, player_id: u8) -> Option<u8> {
for (team, players) in self.connected.lock().await.iter() {
if players.contains(&player_id) {
return Some(*team);
}
}
None
}
async fn swap_is_in_point(&self, player_id: u8, point: Option<u8>) -> Option<u8> {
self.in_point.read().await.get(&player_id).and_then(|x| {
let old_point = x.swap(point.map(|x| x as u16).unwrap_or(u16::MAX), std::sync::atomic::Ordering::Relaxed);
if old_point > u8::MAX as u16 {
None
} else {
Some(old_point as u8)
}
})
}
async fn track_player(&self, player: &oj_rc_core::persist::user::PlayerDescriptor) {
let mut conn_lock = self.connected.lock().await;
if let Some(team) = conn_lock.get_mut(&(player.team as u8)) {
team.insert(player.player_id);
} else {
let mut new_team = std::collections::HashSet::new();
new_team.insert(player.player_id);
conn_lock.insert(player.team as u8, new_team);
}
self.in_point.write().await.insert(player.player_id, std::sync::atomic::AtomicU16::new(u16::MAX));
self.respawning.write().await.insert(player.player_id, std::sync::atomic::AtomicI64::new(i64::MIN));
}
}
struct PointInfo {
team: std::sync::atomic::AtomicI8,
on_point: tokio::sync::RwLock<std::collections::HashMap<u8, std::sync::atomic::AtomicU8>>,
capture: atomic_float::AtomicF32,
percent_per_second: f32,
}
impl PointInfo {
fn new(percent_per_second: f32) -> Self {
Self {
team: std::sync::atomic::AtomicI8::new(-1),
on_point: tokio::sync::RwLock::new([
(0, std::sync::atomic::AtomicU8::new(0)),
(1, std::sync::atomic::AtomicU8::new(0)),
].into_iter().collect()),
capture: atomic_float::AtomicF32::new(0.0),
percent_per_second,
}
}
/*async fn friendlies_on_point(&self, team: u8) -> u8 {
if let Some(counter) = self.on_point.read().await.get(&team) {
counter.load(std::sync::atomic::Ordering::SeqCst)
} else {
0
}
}*/
async fn enemies_on_point(&self, team: u8) -> u8 {
let mut total = 0;
for (iter_team, counter) in self.on_point.read().await.iter() {
if team == *iter_team { continue; }
total += counter.load(std::sync::atomic::Ordering::SeqCst);
}
total
}
async fn owners_on_point(&self) -> u8 {
let team = self.team.load(std::sync::atomic::Ordering::SeqCst);
if team < 0 {
0
} else {
if let Some(counter) = self.on_point.read().await.get(&(team as u8)) {
counter.load(std::sync::atomic::Ordering::SeqCst)
} else {
0
}
}
}
async fn stealers_on_point(&self) -> u8 {
let owning_team = self.team.load(std::sync::atomic::Ordering::SeqCst);
let mut total = 0;
if owning_team < 0 {
for counter in self.on_point.read().await.values() {
total += counter.load(std::sync::atomic::Ordering::SeqCst);
}
} else {
for (team, counter) in self.on_point.read().await.iter() {
if (owning_team as u8) == *team { continue; }
total += counter.load(std::sync::atomic::Ordering::SeqCst);
}
}
total
}
async fn stealers_team(&self) -> Option<u8> {
let owning_team = self.team.load(std::sync::atomic::Ordering::SeqCst);
let mut stealing_team = None;
if owning_team < 0 {
for (team, counter) in self.on_point.read().await.iter() {
let count = counter.load(std::sync::atomic::Ordering::SeqCst);
if count != 0 {
if stealing_team.is_some() {
return None;
} else {
stealing_team = Some(*team);
}
}
}
} else {
let owning_team = owning_team as u8;
for (team, counter) in self.on_point.read().await.iter() {
if owning_team == *team { continue; }
let count = counter.load(std::sync::atomic::Ordering::SeqCst);
if count != 0 {
if stealing_team.is_some() {
return None;
} else {
stealing_team = Some(*team);
}
}
}
}
stealing_team
}
}
struct PointTracker {
points: Vec<PointInfo>,
last_tick: std::sync::atomic::AtomicI64,
}
struct PointTickInfo {
owned: std::collections::HashMap<u8, u8>, // team -> capture point count
captured_firsts: std::collections::HashSet<u8>, // team
lost_lasts: std::collections::HashSet<u8>, // team
delta: i64,
}
impl PointTracker {
const TICK_MS: i64 = 50;
fn new(points: impl Iterator<Item=f32>) -> Self {
Self {
points: points.map(PointInfo::new).collect(),
last_tick: std::sync::atomic::AtomicI64::new(i64::MIN),
}
}
async fn on_enter(&self, generic: &crate::matches::GenericGamemodeEngine<BattleArenaLogic>, point_i: u8, _player_id: u8, player_team: i8) {
if player_team < 0 {
return;
}
let player_team_u8 = player_team as u8;
if let Some(point) = self.points.get(point_i as usize) {
let point_team = point.team.load(std::sync::atomic::Ordering::SeqCst);
if !point.on_point.read().await.contains_key(&player_team_u8) {
point.on_point.write().await.insert(player_team_u8, std::sync::atomic::AtomicU8::new(0));
}
if point_team == player_team {
let old_friendlies = point.on_point.read().await[&player_team_u8].fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let current_enemies = point.enemies_on_point(player_team_u8).await;
if current_enemies != 0 && old_friendlies == 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureLocked,
id: point_i,
defending_team: point_team,
attacking_team: player_team as i8,
},
true,
).await;
}
} else {
let old_enemies = point.on_point.read().await[&player_team_u8].fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let current_contesters = point.enemies_on_point(player_team_u8).await;
if old_enemies == 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureStarted,
id: point_i,
defending_team: point_team,
attacking_team: player_team as i8,
},
true,
).await;
if current_contesters != 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureLocked,
id: point_i,
defending_team: point_team,
attacking_team: player_team as i8,
},
true,
).await;
}
}
}
}
}
async fn on_exit(&self, generic: &crate::matches::GenericGamemodeEngine<BattleArenaLogic>, point_i: u8, _player_id: u8, player_team: i8, max_progress: f32) {
if player_team < 0 {
return;
}
let player_team_u8 = player_team as u8;
if let Some(point) = self.points.get(point_i as usize) {
let point_team = point.team.load(std::sync::atomic::Ordering::SeqCst);
if !point.on_point.read().await.contains_key(&player_team_u8) {
point.on_point.write().await.insert(player_team_u8, std::sync::atomic::AtomicU8::new(0));
}
if point_team == player_team {
let old_friendlies = point.on_point.read().await[&player_team_u8].fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
let current_enemies = point.enemies_on_point(player_team_u8).await;
if old_friendlies == 0 {
// something is out of sync, let's just ignore it and try to undo any underflow
point.on_point.read().await[&player_team_u8].store(0, std::sync::atomic::Ordering::SeqCst);
} else {
if old_friendlies == 1 && current_enemies != 0 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureUnlocked,
id: point_i,
defending_team: point_team,
attacking_team: player_team as i8,
},
true,
).await;
}
}
} else {
let old_enemies = point.on_point.read().await[&player_team_u8].fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
//let current_friendlies = point.friendlies.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if old_enemies == 0 {
// something is out of sync, let's just ignore it and try to undo any underflow
point.on_point.read().await[&player_team_u8].store(0, std::sync::atomic::Ordering::SeqCst);
} else {
if old_enemies == 1 {
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureStoppedNoAttackers,
id: point_i,
defending_team: point_team,
attacking_team: player_team as i8,
},
true,
).await;
let progress_now = point.capture.load(std::sync::atomic::Ordering::SeqCst).floor();
point.capture.store(progress_now, std::sync::atomic::Ordering::SeqCst);
let data = rlnl::events::ingame::TeamBaseState {
base_team_or_mining_point_index: point_i,
current_progress: rlnl::types::ByteFloat::from(progress_now),
max_progress: rlnl::types::ByteFloat::from(max_progress),
};
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointProgress,
literustlib::packet::Property::ReliableOrdered,
&data,
true
).await;
}
}
}
}
}
async fn tick(&self, generic: &crate::matches::GenericGamemodeEngine<BattleArenaLogic>, max_progress: f32) -> Option<PointTickInfo> {
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 None; }
self.last_tick.store(last_tick + (delta * Self::TICK_MS), std::sync::atomic::Ordering::SeqCst);
delta
};
let mut owned_points = std::collections::HashMap::with_capacity(2);
let mut captured_firsts = std::collections::HashSet::new();
let mut lost_lasts = std::collections::HashSet::new();
for (i, cap_point) in self.points.iter().enumerate() {
let point_owner = cap_point.team.load(std::sync::atomic::Ordering::SeqCst);
if point_owner >= 0 {
let point_owner = point_owner as u8;
if let Some(count) = owned_points.get_mut(&point_owner) {
*count += 1;
} else {
owned_points.insert(point_owner, 1);
}
}
let friendlies = cap_point.owners_on_point().await;
let enemies = cap_point.stealers_on_point().await;
if friendlies != 0 { continue; }
if enemies == 0 { continue; }
let stealing_team = cap_point.stealers_team().await;
if stealing_team.is_none() { continue; }
let stealing_team = stealing_team.unwrap();
let to_add = (delta as f32) * (Self::TICK_MS as f32) * cap_point.percent_per_second * max_progress / (100.0 * 1000.0);
let pre_add = cap_point.capture.fetch_add(to_add, std::sync::atomic::Ordering::SeqCst);
let post_add = pre_add + to_add;
if post_add >= max_progress {
// ASSUMPTION: there are only 2 teams
let new_team = stealing_team as i8;
log::info!("Point {} was captured by team {} in game {}", i, new_team, generic.game_guid());
cap_point.capture.store(0.0, std::sync::atomic::Ordering::SeqCst);
cap_point.team.store(new_team, std::sync::atomic::Ordering::SeqCst);
if owned_points.get(&(new_team as u8)).map(|x| *x).unwrap_or(0) == 0 {
captured_firsts.insert(new_team as u8);
}
if point_owner >= 0 && *owned_points.get(&(point_owner as u8)).unwrap() == 1 {
lost_lasts.insert(point_owner as u8);
}
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointNotification,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::ingame::CapturePointNotification {
notification: rlnl::types::CapturePointNotificationType::CaptureCompleted,
id: i as u8,
defending_team: point_owner,
attacking_team: new_team,
},
true,
).await;
}
let progress_now = cap_point.capture.load(std::sync::atomic::Ordering::SeqCst);
let data = rlnl::events::ingame::TeamBaseState {
base_team_or_mining_point_index: i as u8,
current_progress: rlnl::types::ByteFloat::from(progress_now),
max_progress: rlnl::types::ByteFloat::from(max_progress),
};
generic.broadcast(
rlnl::event_code::NetworkEvent::CapturePointProgress,
literustlib::packet::Property::ReliableOrdered,
&data,
true
).await;
}
Some(PointTickInfo {
owned: owned_points,
captured_firsts,
lost_lasts,
delta,
})
}
}
struct BaseTracker {
bases: std::collections::HashMap<u8, BaseInfo>,
}
impl BaseTracker {
fn new<'a>(bases_iter: impl std::iter::Iterator<Item=&'a u8>, crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> Self {
let mut bases = std::collections::HashMap::new();
for base_id in bases_iter {
bases.insert(*base_id, BaseInfo::new(crystals));
}
Self {
bases,
}
}
}
struct BaseInfo {
cube_index: atomic_float::AtomicF32,
crystals_healths: Vec<std::sync::atomic::AtomicU8>,
}
impl BaseInfo {
fn new(crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> Self {
Self {
cube_index: atomic_float::AtomicF32::new(0.0),
crystals_healths: (0..crystals.len())
.map(|_| std::sync::atomic::AtomicU8::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 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);
if health != 0 && health != max_health {
return Some(i);
}
}
None
}
}
enum WinMode {
BaseFull,
OutOfTime,
}
pub struct BattleArenaLogic {
game_duration: std::time::Duration,
game_end: std::sync::atomic::AtomicI64,
respawn_full_heal_duration: f32,
respawn_heal_duration: f32,
timer_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
player_tracking: PlayerTracker,
capture_tracking: PointTracker,
base_tracking: BaseTracker,
//cube_parser: std::sync::Arc<oj_rc_core::cubes::CubeLocationsParser>,
crystals: 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, ba_config: oj_rc_core::data::battle_arena_config::BattleArenaData) -> Self {
let dur = std::time::Duration::from_secs((config.game_time_minutes as u64) * 60);
let fake_end = (chrono::Utc::now() + dur).timestamp();
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);
Self {
game_duration: dur,
respawn_full_heal_duration: config.respawn_full_heal_duration,
respawn_heal_duration: config.respawn_heal_duration,
game_end: std::sync::atomic::AtomicI64::new(fake_end),
timer_task: tokio::sync::Mutex::new(None),
player_tracking: PlayerTracker::new(),
capture_tracking: PointTracker::new(map.capture_points.iter().map(|(_, speed)| *speed)),
base_tracking: BaseTracker::new(map.bases.keys(), &crystals),
//cube_parser,
//ba_base: teambase,
//ba_equalizer: equalizer,
crystals,
config: ba_config,
}
}
async fn abort_timer_sync(&self) {
let mut lock = self.timer_task.lock().await;
if let Some(timer_t) = &*lock {
timer_t.abort();
log::debug!("Aborted battle arena match timer task");
}
*lock = None;
}
fn sphere_to_capture_point(sphere: &oj_rc_core::persist::config::Sphere, max_progress: f32) -> rlnl::types::CapturePoint {
rlnl::types::CapturePoint {
pos: rlnl::types::PosQuatPair {
pos: (sphere.center.x, sphere.center.y, sphere.center.z).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
team: -1,
progress: 0.0.into(),
max_progress: max_progress.into(),
}
}
fn default_capture_point(max_progress: f32) -> rlnl::types::CapturePoint {
rlnl::types::CapturePoint {
pos: rlnl::types::PosQuatPair {
pos: (0.0, 0.0, 0.0).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
team: -1,
progress: 0.0.into(),
max_progress: max_progress.into(),
}
}
async fn check_if_match_time_is_done(&self, generic: &crate::matches::GenericGamemodeEngine<Self>) -> bool {
if self.game_end.load(std::sync::atomic::Ordering::Relaxed) <= chrono::Utc::now().timestamp() {
// find winning team
let mut winning_team = None;
for (base, tracking) in self.base_tracking.bases.iter() {
if let Some((_, winning_score)) = winning_team {
let score = tracking.cube_index.load(std::sync::atomic::Ordering::SeqCst);
if score > winning_score {
winning_team = Some((*base, score));
}
} else {
winning_team = Some((*base, tracking.cube_index.load(std::sync::atomic::Ordering::SeqCst)));
}
}
let winners = if let Some((winning_team, _)) = winning_team {
winning_team
} else {
u8::MAX
};
// game is done, hooray
self.do_win(winners, WinMode::OutOfTime, generic).await;
true
} else {
false
}
}
async fn do_win(&self, winning_team: u8, ty: WinMode, generic: &crate::matches::GenericGamemodeEngine<Self>) {
generic.game_done();
let end_reason = match ty {
WinMode::BaseFull => rlnl::types::GameEndReason::BaseDestroyed,
WinMode::OutOfTime => rlnl::types::GameEndReason::TimeOut,
};
let payload = rlnl::events::ingame::GameLoseWin {
winning_team,
end_reason,
};
for player in generic.users.read().await.values() {
let is_winner = player.descriptor.team == winning_team as i32;
let net_event = match ty {
WinMode::BaseFull => {
if is_winner { rlnl::event_code::NetworkEvent::GameWonBaseDestroyed } else { rlnl::event_code::NetworkEvent::GameLostBaseDestroyed }
},
WinMode::OutOfTime => {
if is_winner { rlnl::event_code::NetworkEvent::GameWon } else { rlnl::event_code::NetworkEvent::GameLost }
}
};
crate::events::log_lnl_send_failure(
player.connection.rlnl()
.send_data(
&payload,
net_event,
literustlib::packet::Property::ReliableOrdered,
&player.connection.connection,
).await
);
}
}
}
#[async_trait::async_trait]
impl CustomGameLogic for BattleArenaLogic {
async fn on_player_join(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, player: &crate::matches::generic::UserConnection, _others: &[oj_rc_core::persist::user::PlayerDescriptor]) -> bool {
log::info!("Player {} joined", player.descriptor.player_id);
self.player_tracking.track_player(&player.descriptor).await;
true
}
async fn on_player_end(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> bool {
true
}
async fn on_vehicle_destroyed(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _killer: u8, victim: u8) -> bool {
if let Some(player_team) = self.player_tracking.team(victim).await {
let was_in_point = self.player_tracking.swap_is_in_point(victim, None).await;
if let Some(was_in_point) = was_in_point {
self.capture_tracking.on_exit(generic, was_in_point, victim, player_team as i8, self.config.num_segments as f32).await;
}
}
// TODO handle respawn
true
}
async fn on_vehicle_self_destruct(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _user: u8, _is_classic: bool) -> bool {
true
}
async fn extra_sync_events(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> Vec<crate::matches::RlnlPacket> {
vec![
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::GameModeSettings,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::UpdateGameModeSettings {
respawn_heal_duration: self.respawn_heal_duration,
respawn_full_heal_duration: self.respawn_full_heal_duration,
}),
}),
// TeamBase
if generic.map_config.bases.is_empty() {
None
} else {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::TeamBase,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::GetTeamBase {
base_1: rlnl::types::PosQuatPair {
pos: generic.map_config.bases.get(&0).map(|(s, _)| (s.center.x, s.center.y, s.center.z)).unwrap_or((0.0, 0.0, 0.0)).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
base_2: rlnl::types::PosQuatPair {
pos: generic.map_config.bases.get(&1).map(|(s, _)| (s.center.x, s.center.y, s.center.z)).unwrap_or((0.0, 0.0, 0.0)).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
protonium_cube_health: self.config.protonium_health as i32,
}),
})
},
// RegisterCapturePoints
if generic.map_config.capture_points.is_empty() {
None
} else {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::RegisterCapturePoints,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::GetCapturePoints {
points: [
generic.map_config.capture_points.get(0).map(|(s, _)| Self::sphere_to_capture_point(s, self.config.num_segments as f32)).unwrap_or_else(|| Self::default_capture_point(self.config.num_segments as f32)),
generic.map_config.capture_points.get(1).map(|(s, _)| Self::sphere_to_capture_point(s, self.config.num_segments as f32)).unwrap_or_else(|| Self::default_capture_point(self.config.num_segments as f32)),
generic.map_config.capture_points.get(2).map(|(s, _)| Self::sphere_to_capture_point(s, self.config.num_segments as f32)).unwrap_or_else(|| Self::default_capture_point(self.config.num_segments as f32)),
]
}),
})
},
// RegisterEqualizer
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::RegisterEqualizer,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::GetEqualizer {
pos: rlnl::types::PosQuatPair {
pos: (0.0, 0.0, 0.0).into(),
rot: (0.0, 0.0, 0.0, 0.0).into(),
},
total_health: 42,
}),
}),
// SetShieldState
if generic.map_config.bases.get(&0).is_some() {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SetShieldState,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::FusionShieldState {
team_id: 0,
full_power: 0,
}),
})
} else {
None
},
if generic.map_config.bases.get(&1).is_some() {
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SetShieldState,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::FusionShieldState {
team_id: 1,
full_power: 0,
}),
})
} else {
None
},
// CurrentGameTime
Some(crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::CurrentGameTime,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::GameTime(self.game_duration.as_millis() as f32 / 1000.0)),
}),
// SyncTeamBaseCubes
// TODO ???
/*crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::HealedCubes {
healed_machine: 0,
type_performing_healing: rlnl::types::TargetType::TeamBase,
target_type: rlnl::types::TargetType::TeamBase,
num_healed_cubes: 1,
hit_cubes: vec![
rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: 0, y: 0, z: 0, },
damage: 1,
}
],
}),
},*/
/*Some(
crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::HealedCubes {
healed_machine: 0,
type_performing_healing: rlnl::types::TargetType::TeamBase,
target_type: rlnl::types::TargetType::TeamBase,
num_healed_cubes: oj_rc_core::cubes::prefabs::CRYSTAL_COUNT as _,
hit_cubes: oj_rc_core::cubes::prefabs::team_base_ba_crystals(oj_rc_core::cubes::prefabs::CRYSTAL_COUNT)
.into_iter()
//.chain(vec![oj_rc_core::cubes::prefabs::team_base_ba_location()].into_iter())
.map(|loc| {
//log::info!("Doing sync-time base heal for cube at ({}, {}, {})", loc.0, loc.1, loc.2);
rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: loc.0, y: loc.1, z: loc.2, },
damage: Self::CRYSTAL_HEALTH,
}
})
.collect(),
}),
}
),*/
// SyncEqualizerNotification
/*crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncEqualizerNotification,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::EqualizerNotification {
notification: rlnl::types::EqualizerState::Lost,
team_id: 0,
time: 0,
max_health: 42,
health: 7,
}),
},
crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::SyncEqualizerNotification,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::sync::EqualizerNotification {
notification: rlnl::types::EqualizerState::Lost,
team_id: 1,
time: 0,
max_health: 42,
health: 7,
}),
},*/
].into_iter().filter_map(|x| x).collect()
}
async fn on_countdown_start(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, game_start: chrono::DateTime<chrono::Utc>) -> bool {
let read_lock = generic.users.read().await;
let mut senders = Vec::with_capacity(read_lock.len());
for conn in read_lock.values() {
senders.push((conn.connection.clone(), conn.state.clone()));
}
drop(read_lock);
let game_end = game_start + self.game_duration;
let extra_packets = Vec::default();
let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, extra_packets, Vec::default());
let mut timer_lock = self.timer_task.lock().await;
if let Some(timer_t) = &*timer_lock { // this is quite unlikely (i.e. impossible), but I've done it for completeness
log::warn!("Aborting an existing timer task for battle arena mode suggests an assumption was wrong");
timer_t.abort();
}
*timer_lock = Some(new_timer_task);
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
}
async fn on_motion(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, motion: &rlnl::machine_motion::MachineMotion, location: (f32, f32, f32)) -> bool {
let game_start = generic.game_start.load(std::sync::atomic::Ordering::Relaxed);
if generic.game_start.load(std::sync::atomic::Ordering::Relaxed) == -1 || chrono::Utc::now().timestamp() < game_start {
// game is not in progress, ignore motion event
log::debug!("Ignoring early motion event from player {}", motion.player_id);
return true;
}
if generic.is_game_done() {
self.abort_timer_sync().await;
return true;
}
if self.check_if_match_time_is_done(generic).await {
return true;
}
if generic.map_config.capture_points.is_empty() {
return true; // don't bother trying to track whether players are in capture points since there are none
}
if let Some(player_team) = self.player_tracking.team(motion.player_id).await {
let mut now_in_point = None;
for (point_i, point) in generic.map_config.capture_points.iter().enumerate() {
if crate::matches::GenericGamemodeEngine::<Self>::is_in(&location, &point.0) {
now_in_point = Some(point_i as u8);
break;
}
}
let was_in_point = self.player_tracking.swap_is_in_point(player_team, now_in_point).await;
if was_in_point != now_in_point {
//log::info!("Player {}'s occupied capture point changed from {:?} to {:?}", motion.player_id, was_in_point, now_in_point);
if let Some(now_in_point) = now_in_point {
self.capture_tracking.on_enter(generic, now_in_point, motion.player_id, player_team as i8).await;
}
if let Some(was_in_point) = was_in_point {
self.capture_tracking.on_exit(generic, was_in_point, motion.player_id, player_team as i8, self.config.num_segments as f32).await;
}
}
}
if let Some(tick_info) = self.capture_tracking.tick(generic, self.config.num_segments as f32).await {
// handle shield (de)activation
for team in tick_info.captured_firsts {
generic.broadcast(
rlnl::event_code::NetworkEvent::SetShieldState,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::sync::FusionShieldState {
team_id: team as i8,
full_power: 1,
},
true,
).await;
}
for team in tick_info.lost_lasts {
generic.broadcast(
rlnl::event_code::NetworkEvent::SetShieldState,
literustlib::packet::Property::ReliableOrdered,
&rlnl::events::sync::FusionShieldState {
team_id: team as i8,
full_power: 0,
},
true,
).await;
}
// do base charge tick
for base_id in generic.map_config.bases.keys() {
//log::info!("Healing base {}", base_id);
if let Some(owned_points) = tick_info.owned.get(base_id) {
if let Some(tracked_base) = self.base_tracking.bases.get(base_id) {
let one_tick = (self.crystals.len() as f32)
* ((PointTracker::TICK_MS as f32) / (self.game_duration.as_millis() as f32))
* ((self.base_tracking.bases.len() as f32) / (self.capture_tracking.points.len() as f32));
let increment = tick_info.delta as f32 * (*owned_points as f32) * one_tick;
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, self.crystals.len());
let new_index = (new_float_index.ceil() as usize).clamp(0, self.crystals.len());
if new_index != old_index {
log::debug!("Base {} increment passed a crystal index barrier", base_id);
let first_damaged = tracked_base.first_damaged(old_index, self.config.protonium_health as u32);
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");
let first_damaged = first_damaged.unwrap();
let healing = self.config.protonium_health as u32 - tracked_base.calculate_crystal_health(first_damaged, self.config.protonium_health as u32);
tracked_base.crystals_healths[first_damaged].store(u8::MAX, std::sync::atomic::Ordering::Relaxed);
let target_crystal = &self.crystals[first_damaged];
rlnl::events::HealedCubes {
healed_machine: *base_id as u16,
type_performing_healing: rlnl::types::TargetType::TeamBase,
target_type: rlnl::types::TargetType::TeamBase,
num_healed_cubes: 1,
hit_cubes: vec![
rlnl::types::HitCubeInfo {
pos: rlnl::types::Byte3 { x: target_crystal.x, y: target_crystal.y, z: target_crystal.z, },
damage: healing as i32,
}
],
}
} else {
let target_crystals = &self.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: self.config.protonium_health as i32,
})
.collect(),
}
};
generic.broadcast(
rlnl::event_code::NetworkEvent::SyncTeamBaseCubes,
literustlib::packet::Property::ReliableOrdered,
&payload,
true
).await;
if new_index == self.crystals.len() {
// team base is charged to 100%
self.do_win(*base_id, WinMode::BaseFull, generic).await;
}
}
}
}
}
}
true
}
async fn on_custom(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box<dyn crate::Broadcastable>) {
match (event, property) {
(rlnl::event_code::NetworkEvent::SendDamagedByEnemyShield, literustlib::packet::Property::ReliableOrdered) => {
generic.broadcast(
rlnl::event_code::NetworkEvent::DamagedByEnemyShield,
literustlib::packet::Property::ReliableOrdered,
&*data,
true,
).await;
},
(rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => {
// TODO
log::warn!("Ignoring SurrenderRequest because it's not implemented (yet)");
}
(rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest, literustlib::packet::Property::ReliableOrdered) => {
// TODO
log::warn!("Ignoring AwardTeamBaseProtoniumDestroyedRequest because it's not implemented (yet)");
}
_ => {}
}
}
}

View File

@@ -497,10 +497,12 @@ impl CustomGameLogic for EliminationLogic {
}
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() {
let read_lock = generic.users.read().await;
let mut senders = Vec::with_capacity(read_lock.len());
for conn in read_lock.values() {
senders.push((conn.connection.clone(), conn.state.clone()));
}
drop(read_lock);
let game_end = game_start + self.game_duration;
let teams = self.bases.teams();
let extra_packets = teams.iter().map(|team| crate::matches::RlnlPacket {
@@ -512,7 +514,16 @@ impl CustomGameLogic for EliminationLogic {
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 end_packets = vec![
crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::EndGame,
property: literustlib::packet::Property::ReliableOrdered,
data: Box::new(rlnl::events::ingame::GameEnd {
reason: rlnl::types::GameEndReason::TimeOut,
}),
}
];
let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, extra_packets, end_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");
@@ -534,6 +545,7 @@ impl CustomGameLogic for EliminationLogic {
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() {
self.abort_timer_sync().await;
return true;
}
if self.bases.is_baseless {
@@ -548,34 +560,21 @@ impl CustomGameLogic for EliminationLogic {
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;
let was_in_base = self.tracked.swap_is_in_base(motion.player_id, now_in_base).await;
if now_in_base != was_in_base {
if let Some(was_in_base) = was_in_base {
self.bases.on_exit(generic, was_in_base, player_team == was_in_base, 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;
if let Some(now_in_base) = now_in_base {
self.bases.on_enter(generic, now_in_base, player_team == now_in_base, motion.player_id).await;
}
// still outside of base
}
}
self.bases.tick(generic).await;
if generic.is_game_done() {
self.abort_timer_sync().await;
}
true
}
async fn on_custom(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _user_id: i32, _event: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: Box<dyn crate::Broadcastable>) {}
}
// spawn points (best guess)

View File

@@ -4,3 +4,6 @@ pub use no_op::NoOpLogic;
mod elimination;
pub use elimination::EliminationLogic;
mod battle_arena;
pub use battle_arena::BattleArenaLogic;

View File

@@ -40,4 +40,6 @@ impl CustomGameLogic for NoOpLogic {
async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _motion: &rlnl::machine_motion::MachineMotion, _location: (f32, f32, f32)) -> bool {
true
}
async fn on_custom(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _user_id: i32, _event: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: Box<dyn crate::Broadcastable>) {}
}

View File

@@ -1,7 +1,7 @@
const SLEEP_PERIOD: std::time::Duration = std::time::Duration::from_millis(250);
pub fn match_time_syncer(players: Vec<(super::generic::UserSender, std::sync::Arc<super::generic::UserState>)>, game_start: chrono::DateTime<chrono::Utc>, game_end: chrono::DateTime<chrono::Utc>, extra_packets: Vec<super::RlnlPacket>) -> tokio::task::JoinHandle<()> {
tokio::spawn(do_match_timer_async(players, game_start, game_end, extra_packets))
pub fn match_time_syncer(players: Vec<(super::generic::UserSender, std::sync::Arc<super::generic::UserState>)>, game_start: chrono::DateTime<chrono::Utc>, game_end: chrono::DateTime<chrono::Utc>, extra_packets: Vec<super::RlnlPacket>, end_packets: Vec<super::RlnlPacket>) -> tokio::task::JoinHandle<()> {
tokio::spawn(do_match_timer_async(players, game_start, game_end, extra_packets, end_packets))
}
pub fn time_to_game_end_payload(game_end: chrono::DateTime<chrono::Utc>) -> rlnl::events::GameTime {
@@ -11,7 +11,7 @@ pub fn time_to_game_end_payload(game_end: chrono::DateTime<chrono::Utc>) -> rlnl
rlnl::events::GameTime(time_until_end_f32)
}
async fn do_match_timer_async(players: Vec<(super::generic::UserSender, std::sync::Arc<super::generic::UserState>)>, game_start: chrono::DateTime<chrono::Utc>, game_end: chrono::DateTime<chrono::Utc>, extra_packets: Vec<super::RlnlPacket>) {
async fn do_match_timer_async(players: Vec<(super::generic::UserSender, std::sync::Arc<super::generic::UserState>)>, game_start: chrono::DateTime<chrono::Utc>, game_end: chrono::DateTime<chrono::Utc>, extra_packets: Vec<super::RlnlPacket>, end_packets: Vec<super::RlnlPacket>) {
let now = chrono::Utc::now();
let time_until_start_ms = game_start.signed_duration_since(now).num_milliseconds().clamp(0, i64::MAX) + SLEEP_PERIOD.as_millis() as i64;
tokio::time::sleep(std::time::Duration::from_millis(time_until_start_ms as u64)).await;
@@ -53,18 +53,18 @@ async fn do_match_timer_async(players: Vec<(super::generic::UserSender, std::syn
break 'timer_loop;
}
}
let payload = rlnl::events::ingame::GameEnd {
reason: rlnl::types::GameEndReason::TimeOut,
};
for player in players.iter() {
let sender = player.0.rlnl();
if let Err(e) = sender.send_data(
&payload,
rlnl::event_code::NetworkEvent::EndGame,
literustlib::packet::Property::ReliableOrdered,
&player.0.connection)
.await {
log::error!("Failed to send EndGame event to a user: {}", e);
for packet in end_packets {
for player in players.iter() {
let mode = super::generic::ConnectionMode::from_u8(player.1.mode.load(std::sync::atomic::Ordering::Relaxed));
if !matches!(mode, super::generic::ConnectionMode::Disconnected) {
let sender = player.0.rlnl();
crate::events::log_lnl_send_failure(sender.send_data(
&*packet.data,
packet.event,
packet.property,
&player.0.connection
).await);
}
}
}
log::debug!("Game timer (a)sync thread has completed");

View File

@@ -15,7 +15,6 @@ clap.workspace = true
polariton.workspace = true
oj_polariton_auth = { version = "*", path = "../polariton_auth" }
polariton_server.workspace = true
base64 = "0.22"
hex = "0.4"
serde.workspace = true
serde_json.workspace = true

View File

@@ -7,7 +7,7 @@ pub mod crf_config;
pub use oj_rc_core::data::weapon_list;
//pub use oj_rc_core::data::movement_list;
pub mod damage_boost;
pub mod battle_arena_config;
//pub mod battle_arena_config;
pub mod cpu_limits;
pub mod cosmetic_limits;
pub mod taunts_config;

View File

@@ -1,33 +1,38 @@
use polariton_server::operations::SimpleFunc;
use polariton::{operation::{Dict, ParameterTable, Typed}, serdes::TypePrefix};
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::ParameterTable;
use crate::data::battle_arena_config::*;
const CODE: u8 = 53;
const PARAM_KEY: u8 = 1;
pub(super) fn battle_arena_config_provider() -> SimpleFunc<53, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {
SimpleFunc::new(|params, _| {
pub(super) struct BattleArenaConfigurer {
factory: std::sync::Arc<oj_rc_core::factory::Factory>,
weapon_list: std::sync::Arc<oj_rc_core::cubes::WeaponListParser>,
cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>,
ba_conf: oj_rc_core::persist::config::BattleArenaResolver,
}
#[async_trait::async_trait]
impl <C: Send + 'static> SimpleOperation<C> for BattleArenaConfigurer {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
let user_info = user.user()?;
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, Typed::Dict(Dict {
key_ty: TypePrefix::Str, // str
val_ty: TypePrefix::HashMap, // hashmap
items: vec![
(Typed::Str("BattleArenaSettings".into()), BattleArenaData {
protonium_health: 1_000,
respawn_time_seconds: 10,
heal_over_time_per_tower: vec![10, 10, 10, 10],
base_machine_map: Vec::default(),
equalizer_model: Vec::default(),
equalizer_health: 1_000_000,
equalizer_trigger_time_seconds: vec![10, 10, 10, 10, 10],
equalizer_warning_seconds: 10,
equalizer_duration_seconds: vec![20, 20, 20, 20, 20],
capture_time_seconds_per_player: vec![30, 20, 10, 5, 1],
num_segments: 4,
heal_escalation_time_seconds: 5,
}.as_transmissible())
],
}));
params.insert(PARAM_KEY, data);
Ok(params.into())
}
}
pub(super) fn battle_arena_config_provider<C: Send + 'static>(conf: &oj_rc_core::ConfigImpl, factory: &std::sync::Arc<oj_rc_core::factory::Factory>, weapon_list: std::sync::Arc<oj_rc_core::cubes::WeaponListParser>,
cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>) -> SimpleOpImpl<C, crate::UserTy, BattleArenaConfigurer> {
let ba_conf = <oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::ba_settings(conf);
SimpleOpImpl::new(BattleArenaConfigurer {
factory: factory.to_owned(),
weapon_list,
cpu_counter,
ba_conf
})
}

View File

@@ -125,7 +125,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
.add(movement_stats::movement_config_provider(&init_ctx.cubes))
.add(power_bar_stats::power_bar_provider(&init_ctx.cubes))
.add(damage_boost_stats::damage_boost_provider())
.add(battle_arena_config::battle_arena_config_provider())
.add(battle_arena_config::battle_arena_config_provider(&init_ctx.cubes, &init_ctx.factory, init_ctx.parsers.weapon_order(), init_ctx.parsers.cpu_counter()))
.add(cpu_limits_config::cpu_config_provider())
.add(cosmetic_config::cosmetic_limits_config_provider())
.add(taunts_config::taunts_config_provider())