diff --git a/Cargo.lock b/Cargo.lock index 4446b14..4ee61fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/rc_core/Cargo.toml b/rc_core/Cargo.toml index 148812b..a83bd19 100644 --- a/rc_core/Cargo.toml +++ b/rc_core/Cargo.toml @@ -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 diff --git a/rc_core/src/cubes/locations_of.rs b/rc_core/src/cubes/locations_of.rs new file mode 100644 index 0000000..797cbda --- /dev/null +++ b/rc_core/src/cubes/locations_of.rs @@ -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>(_iter: I) -> Self { + Self + } + + pub fn locations_of(&self, r: &mut dyn std::io::Read, cube_id: u32) -> Vec { + 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 { + 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() + } + } + } +} diff --git a/rc_core/src/cubes/mod.rs b/rc_core/src/cubes/mod.rs index fddf0bd..d11b66a 100644 --- a/rc_core/src/cubes/mod.rs +++ b/rc_core/src/cubes/mod.rs @@ -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, cpu_counter: std::sync::Arc, + locations: std::sync::Arc, } 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 { self.cpu_counter.clone() } + + pub fn locations_of(&self) -> std::sync::Arc { + self.locations.clone() + } } diff --git a/rc_core/src/cubes/parser.rs b/rc_core/src/cubes/parser.rs index 397f2ed..4fb1659 100644 --- a/rc_core/src/cubes/parser.rs +++ b/rc_core/src/cubes/parser.rs @@ -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 { + 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) -> std::io::Result> { + 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 { diff --git a/rc_services_room/src/data/battle_arena_config.rs b/rc_core/src/data/battle_arena_config.rs similarity index 84% rename from rc_services_room/src/data/battle_arena_config.rs rename to rc_core/src/data/battle_arena_config.rs index 67d9247..d6740fb 100644 --- a/rc_services_room/src/data/battle_arena_config.rs +++ b/rc_core/src/data/battle_arena_config.rs @@ -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::>().into()) +fn to_obj_arr_u(slice: &[u64]) -> Typed { + Typed::::ObjArr(slice.iter().map(|x| Typed::::Long(*x as i64)).collect::>>().into()) } -fn to_obj_arr_i(slice: &[i64]) -> Typed { - Typed::ObjArr(slice.iter().map(|x| Typed::Long(*x)).collect::>().into()) +fn to_obj_arr_i(slice: &[i64]) -> Typed { + Typed::::ObjArr(slice.iter().map(|x| Typed::::Long(*x)).collect::>>().into()) } impl BattleArenaData { - pub fn as_transmissible(&self) -> Typed { + pub fn as_transmissible(&self) -> Typed { Typed::HashMap(vec![ (Typed::Str("protoniumHealth".into()), Typed::Long(self.protonium_health)), (Typed::Str("respawnTimeSeconds".into()), Typed::Long(self.respawn_time_seconds)), diff --git a/rc_core/src/data/mod.rs b/rc_core/src/data/mod.rs index ce870c3..3356345 100644 --- a/rc_core/src/data/mod.rs +++ b/rc_core/src/data/mod.rs @@ -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; diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 30d38d2..ab424cb 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -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(), } } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index ba37c35..605b608 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -410,9 +410,18 @@ impl super::ConfigProvider 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 super::ConfigProvider for CubeConfig { total: self.battle.energy.total, } } + + fn ba_settings(&self) -> super::BattleArenaResolver { + super::BattleArenaResolver { + data: self.battle.multiplayer.battle_arena.clone(), + } + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index ce8f37d..8b7c5c0 100644 --- a/rc_core/src/persist/config/mod.rs +++ b/rc_core/src/persist/config/mod.rs @@ -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; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 6ba9fe3..b04eeec 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -35,6 +35,7 @@ pub trait ConfigProvider { fn url_links(&self) -> LinksConfig; fn fake_players(&self) -> Vec; 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>, // team -> points - pub bases: std::collections::HashMap, // team -> base + pub bases: std::collections::HashMap, // 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 { + 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(&self, user: &dyn crate::persist::user::CommonUser, factory: &crate::factory::Factory, weapon_list: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result, 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()) + ], + })) + } +} diff --git a/rc_core/src/persist/garage.rs b/rc_core/src/persist/garage.rs index c176311..8640358 100644 --- a/rc_core/src/persist/garage.rs +++ b/rc_core/src/persist/garage.rs @@ -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 { diff --git a/rc_core/src/persist/maps.rs b/rc_core/src/persist/maps.rs index 43ac8df..69fe43c 100644 --- a/rc_core/src/persist/maps.rs +++ b/rc_core/src/persist/maps.rs @@ -10,6 +10,7 @@ pub struct MapsConfig { pub struct MapConfig { pub spawn_points: Vec, pub bases: Vec, + pub capture_points: Vec, } #[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) -> 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) -> 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) -> 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 { let mut map = std::collections::HashMap::with_capacity(9); @@ -204,7 +233,7 @@ pub(super) fn default_map() -> std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap, + #[serde(default = "default_ba_conf")] + pub battle_arena: BattleArenaConfig, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -41,7 +43,8 @@ pub(super) fn default_net_conf() -> NetworkConf { min_update_timeout: 1, max_delay: 1, overflow_threshold: 10, - max_packet_size: 5888, + //max_packet_size: 5888, + max_packet_size: 1024, resend_delay_base: 0.1, resend_delay_rtt_mult: 0.5, network_peer_update_interval: 1, @@ -83,3 +86,85 @@ impl ClientEmulation { } } } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct BattleArenaConfig { + #[serde(default = "default_crystal_health")] + pub crystal_health: u32, + #[serde(default = "default_respawn_time")] + pub respawn_time_s: u64, + #[serde(default = "default_equalizer")] + pub equalizer: super::garage::PrefabVehicle, + #[serde(default = "default_equalizer_health")] + pub equalizer_health: u64, + //pub equalizer_trigger_time_s: Vec, + #[serde(default = "default_equalizer_warning")] + pub equalizer_warning_s: u64, + #[serde(default = "default_equalizer_duration")] + pub equalizer_duration_s: u64, + #[serde(default = "default_ba_base")] + pub base: super::garage::PrefabVehicle, + #[serde(default = "default_segments")] + pub num_segments: u16, +} + +pub(super) fn default_ba_conf() -> BattleArenaConfig { + BattleArenaConfig { + crystal_health: default_crystal_health(), + respawn_time_s: default_respawn_time(), + equalizer: default_equalizer(), + equalizer_health: default_equalizer_health(), + equalizer_warning_s: default_equalizer_warning(), + equalizer_duration_s: default_equalizer_duration(), + base: default_ba_base(), + num_segments: default_segments(), + } +} + +fn default_equalizer() -> super::garage::PrefabVehicle { + super::garage::PrefabVehicle { + name: Some("Prefab equalizer".to_owned()), + username: "server".to_owned(), + id: super::garage::PrefabId::Raw { + cube_data: vec![1, 0, 0, 0, 0x37, 0xF4, 0x31, 0xA5, 0, 0, 0, 0], + colour_data: vec![0, 0, 0, 0], // ignored + } + } +} + +fn default_ba_base() -> super::garage::PrefabVehicle { + super::garage::PrefabVehicle { + name: Some("Prefab team base".to_owned()), + username: "server".to_owned(), + id: super::garage::PrefabId::Raw { + cube_data: vec![ + 0x84, 0x01, 0x00, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x67, 0x38, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x67, 0x24, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x67, 0x2E, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x48, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x43, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x2A, 0x1A, 0x02, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x3A, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x58, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x61, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x3A, 0x33, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x3E, 0x1A, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x53, 0x38, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x2F, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x2F, 0x51, 0x02, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x30, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x58, 0x29, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x39, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x61, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x44, 0x29, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x44, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x2F, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x25, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x3E, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x43, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x43, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x43, 0x33, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x34, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x6B, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x57, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x57, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x25, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x61, 0x48, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x2F, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x30, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x5C, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x57, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x57, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x3F, 0x1A, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x61, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x5C, 0x38, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x39, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x3E, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x57, 0x48, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x6B, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x2A, 0x2E, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x3A, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x2A, 0x47, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x57, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x5C, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x2A, 0x4D, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x61, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x34, 0x38, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x39, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x57, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x3A, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x2F, 0x42, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x66, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x48, 0x4D, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x66, 0x4D, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x34, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x43, 0x48, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x2F, 0x33, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x52, 0x2E, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x2A, 0x1F, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x25, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x4D, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x2A, 0x38, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x62, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x25, 0x20, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x57, 0x3D, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x66, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x4D, 0x3D, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x2A, 0x42, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x2F, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x66, 0x1A, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x5C, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x48, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x25, 0x34, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x34, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x2A, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x5C, 0x39, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x5C, 0x4D, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x39, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x43, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x48, 0x39, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x4D, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x2F, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x48, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x30, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x2F, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x35, 0x42, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x34, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x4E, 0x29, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x3E, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x3F, 0x38, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x4D, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x62, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x58, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x4E, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x2F, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x4D, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x3E, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x66, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x62, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x2F, 0x3D, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x3E, 0x4D, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x49, 0x38, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x52, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x4E, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x4D, 0x33, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x44, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x6B, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x6B, 0x42, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x6B, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x25, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x49, 0x42, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x61, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x53, 0x1A, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x52, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x43, 0x2F, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x4D, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x34, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x6B, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x30, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x30, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x48, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x25, 0x2F, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x34, 0x2E, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x5C, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x52, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x5D, 0x38, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x34, 0x10, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x58, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x3E, 0x39, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x4D, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x3A, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x57, 0x33, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x4E, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x58, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x44, 0x33, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x4D, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x67, 0x1A, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x61, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x34, 0x39, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x44, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x25, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x43, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x52, 0x4D, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x39, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x4D, 0x2F, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x57, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x5C, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x66, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x2F, 0x34, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x39, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x44, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x41, 0x2A, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x44, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x3E, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x25, 0x34, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x6B, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x3A, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x4E, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x62, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x52, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x62, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x30, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x2F, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x52, 0x1A, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x4D, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x48, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x52, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x52, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x61, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x5C, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x4E, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x39, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x2A, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x6B, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x30, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x4E, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x6B, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x25, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x58, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x2B, 0x42, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x52, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x39, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x61, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x62, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x25, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x6B, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x25, 0x2A, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x34, 0x4D, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x6B, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x57, 0x2F, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x2F, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x2A, 0x11, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x44, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x49, 0x1A, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x39, 0x33, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x34, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x48, 0x10, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x66, 0x10, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x61, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x5C, 0x1A, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x39, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x2A, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x53, 0x42, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x4D, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x43, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x2F, 0x29, 0x02, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x34, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x61, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x2A, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x5C, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x5C, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x61, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x43, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x25, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x3E, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x57, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x4D, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x48, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x62, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x62, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x2A, 0x2F, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x48, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x25, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x57, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x39, 0x48, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x57, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x41, 0x25, 0x16, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x61, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x44, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x58, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x6B, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x66, 0x2E, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x25, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x4D, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x34, 0x2A, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x39, 0x42, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x61, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x58, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x67, 0x42, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x34, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x61, 0x42, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x43, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x43, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x2F, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x67, 0x38, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x41, 0x2A, 0x47, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x3E, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x43, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x2A, 0x1A, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x62, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x6B, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x39, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x4D, 0x42, 0x06, 0xB6, 0x0A, 0x2C, 0x24, 0x2C, 0x00, 0x2E, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x62, 0x33, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x4D, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x57, 0x42, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x61, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x6B, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x2A, 0x10, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x30, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x61, 0x33, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x34, 0x1A, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x39, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x25, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x66, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x44, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x66, 0x3D, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x34, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x57, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x35, 0x38, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x61, 0x3D, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x2A, 0x1B, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x48, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x2A, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x4D, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x3F, 0x42, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x62, 0x29, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x2A, 0x4C, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x3A, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x44, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x52, 0x39, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x48, 0x1A, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x35, 0x1A, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x3E, 0x2E, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x3E, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x34, 0x2F, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x2A, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x6B, 0x2F, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x4E, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x5D, 0x42, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x58, 0x3D, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x66, 0x39, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x27, 0x43, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x5C, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x2B, 0x1A, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x66, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x57, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x5C, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x57, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x3E, 0x38, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x3E, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x28, 0x57, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x6B, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x30, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x43, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x3A, 0x29, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x2A, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x39, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x43, 0x3D, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x2F, 0x48, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x57, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x2F, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x3A, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x25, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x43, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x48, 0x2E, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x52, 0x10, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x30, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x5C, 0x2E, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4B, 0x25, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x40, 0x5D, 0x1A, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x3A, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x52, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x66, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x61, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x4D, 0x48, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x6B, 0x48, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x61, 0x2F, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x4D, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x2F, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x2B, 0x2E, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x2A, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x39, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x48, 0x38, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x58, 0x33, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0A, 0x2A, 0x39, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x62, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x46, 0x43, 0x42, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x25, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x2A, 0x43, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x57, 0x4D, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x4E, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x09, 0x4E, 0x33, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x43, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x19, 0x48, 0x15, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x52, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x39, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x2F, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x66, 0x24, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x4E, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0E, 0x25, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x34, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3C, 0x2F, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x3E, 0x10, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x3A, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x3E, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x52, 0x38, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x14, 0x39, 0x1A, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x4D, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x13, 0x39, 0x2F, 0x0C, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x39, 0x47, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x3A, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x52, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x4E, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x25, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x58, 0x1F, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x45, 0x44, 0x15, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x1D, 0x5C, 0x10, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x48, 0x51, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x4F, 0x66, 0x38, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x39, 0x3D, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x0F, 0x43, 0x3E, 0x17, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x61, 0x1F, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x37, 0x2A, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x43, 0x4C, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x3B, 0x58, 0x47, 0x00, 0x71, 0xB3, 0x74, 0xEB, 0x32, 0x2F, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x2D, 0x2A, 0x0B, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x1E, 0x25, 0x10, 0x06, 0x71, 0xB3, 0x74, 0xEB, 0x18, 0x2F, 0x15, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x4A, 0x4D, 0x29, 0x16, 0x71, 0xB3, 0x74, 0xEB, 0x23, 0x66, 0x51, 0x06, + ], + colour_data: vec![0, 0, 0, 0], // ignored + } + } +} + +fn default_crystal_health() -> u32 { + 1_000 +} + +fn default_respawn_time() -> u64 { + 10 +} + +fn default_equalizer_health() -> u64 { + 1_000 +} + +fn default_equalizer_warning() -> u64 { + 10 +} + +fn default_equalizer_duration() -> u64 { + 30 +} + +fn default_segments() -> u16 { + 3 +} diff --git a/rc_core/src/persist/singleplayer.rs b/rc_core/src/persist/singleplayer.rs index cb027e2..c273caa 100644 --- a/rc_core/src/persist/singleplayer.rs +++ b/rc_core/src/persist/singleplayer.rs @@ -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() } } } diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 8bcf5e0..ebe21b6 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -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 { + 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::>(), + 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, 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::>(), - 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); diff --git a/rc_core/src/persist/user/common.rs b/rc_core/src/persist/user/common.rs new file mode 100644 index 0000000..da0d71f --- /dev/null +++ b/rc_core/src/persist/user/common.rs @@ -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 { + self.resolve_vehicle(vehicle, factory, weapon_order, cpu_counter).await + } +} diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 404620c..90eadde 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -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"; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index c49f223..4e5a4db 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -61,7 +61,7 @@ pub trait UserAuthenticator { } #[async_trait::async_trait] -pub trait User: ChatUser + LobbyUser + MultiplayerUser + IntercomUser { +pub trait User: 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, i16>; async fn subscribed_channels_strings(&self) -> Result, i16>; async fn add_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Result, 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) -> Result<(), polariton_server::operations::SimpleOpError>; } + +pub struct ResolvedVehicle { + pub mastery: i32, + pub tier: i32, + pub robot_name: String, + pub robot_map: Vec, + pub robot_uuid: String, + pub cpu: i32, + pub weapon_order: Vec, + pub colour_map: Vec, + pub spawn_effect: String, + pub death_effect: String, + pub weapon_rank: std::collections::HashMap, +} + +#[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; +} diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index da55765..e9ff639 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -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 {} } diff --git a/rc_multiplayer/src/events/player_leave.rs b/rc_multiplayer/src/events/player_leave.rs new file mode 100644 index 0000000..1de37cc --- /dev/null +++ b/rc_multiplayer/src/events/player_leave.rs @@ -0,0 +1,30 @@ +pub struct PlayerQuit { + msg_router: tokio::sync::mpsc::Sender, +} + +pub(super) fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::Dataless { + 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>, user: &crate::UserData, _sender: &std::sync::Arc>) { + 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"); + } + } +} diff --git a/rc_multiplayer/src/handlers/gamemode_specific.rs b/rc_multiplayer/src/handlers/gamemode_specific.rs new file mode 100644 index 0000000..57c29f9 --- /dev/null +++ b/rc_multiplayer/src/handlers/gamemode_specific.rs @@ -0,0 +1,42 @@ +#![allow(dead_code)] +pub struct GamemodeSpecific + crate::Broadcastable> { + msg_router: tokio::sync::mpsc::Sender, + event: rlnl::event_code::NetworkEvent, + property: literustlib::packet::Property, + _in: std::marker::PhantomData, +} + +impl + crate::Broadcastable> GamemodeSpecific { + pub fn handler(init_ctx: &crate::InitConfig) -> crate::handlers::simple_typed::SimpleRlnl { + 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 + crate::Broadcastable> crate::handlers::simple_typed::RlnlEventCodeHandler for GamemodeSpecific { + 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>, user: &crate::UserData, _sender: &std::sync::Arc>) { + 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); + } + } +} diff --git a/rc_multiplayer/src/handlers/ingame_broadcast.rs b/rc_multiplayer/src/handlers/ingame_broadcast.rs index 2b0595e..045e567 100644 --- a/rc_multiplayer/src/handlers/ingame_broadcast.rs +++ b/rc_multiplayer/src/handlers/ingame_broadcast.rs @@ -47,7 +47,7 @@ impl {:?} for user (no auth!)", Self::CODE, self.code_out); } } } diff --git a/rc_multiplayer/src/handlers/ingame_broadcast_dataless.rs b/rc_multiplayer/src/handlers/ingame_broadcast_dataless.rs index 9de917a..a534b8d 100644 --- a/rc_multiplayer/src/handlers/ingame_broadcast_dataless.rs +++ b/rc_multiplayer/src/handlers/ingame_broadcast_dataless.rs @@ -44,7 +44,7 @@ impl + 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(&self, data: &D, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, conn: &literustlib_server::Connection) -> std::io::Result { 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) -> std::io::Result { + //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 } diff --git a/rc_multiplayer/src/main.rs b/rc_multiplayer/src/main.rs index c5063da..4fe1053 100644 --- a/rc_multiplayer/src/main.rs +++ b/rc_multiplayer/src/main.rs @@ -12,7 +12,7 @@ mod vehicle_motion; pub struct InitConfig { pub config: oj_rc_core::persist::config::ConfigImpl, pub users: std::sync::Arc, - pub parsers: oj_rc_core::cubes::CubeParsers, + pub parsers: std::sync::Arc, pub matches_chann: tokio::sync::mpsc::Sender, } @@ -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(>::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 { diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index 6a0ccfe..82f81f2 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -4,10 +4,13 @@ pub struct GameMatches { mode_configs: oj_rc_core::data::game_mode::GameModeConfigs, map_configs: std::collections::HashMap, fake_players: Vec, + cube_parsers: std::sync::Arc, + ba_settings: std::sync::Arc, + factory: std::sync::Arc, } 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, factory: std::sync::Arc) -> 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: >::fake_players(conf), + cube_parsers, + ba_settings: std::sync::Arc::new(>::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 diff --git a/rc_multiplayer/src/matches/engine.rs b/rc_multiplayer/src/matches/engine.rs index 1ea7259..802c2f4 100644 --- a/rc_multiplayer/src/matches/engine.rs +++ b/rc_multiplayer/src/matches/engine.rs @@ -17,5 +17,6 @@ pub trait CustomGameLogic: Sized + Send + Sync + 'static { async fn on_game_completed(&self, generic: &super::GenericGamemodeEngine) -> bool; async fn on_broadcast(&self, generic: &super::GenericGamemodeEngine, user_id: i32, event_out: rlnl::event_code::NetworkEvent, event_in: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &Option>, skip_user: bool) -> bool; async fn on_motion(&self, generic: &super::GenericGamemodeEngine, motion: &rlnl::machine_motion::MachineMotion, location: (f32, f32, f32)) -> bool; + async fn on_custom(&self, generic: &super::GenericGamemodeEngine, user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box); } diff --git a/rc_multiplayer/src/matches/fake/experimental.rs b/rc_multiplayer/src/matches/fake/experimental.rs index ba6f9f1..c0baeb0 100644 --- a/rc_multiplayer/src/matches/fake/experimental.rs +++ b/rc_multiplayer/src/matches/fake/experimental.rs @@ -81,7 +81,7 @@ async fn erratic_behaviour(send_to: Vec, 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; diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index a1c9522..7f727aa 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -338,7 +338,13 @@ impl GenericGamemodeEngine { 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 GenericGamemodeEngine { &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 GenericGamemodeEngine { } }, + 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 GenericGamemodeEngine { 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 GenericGamemodeEngine { } } }, + 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 GenericGamemodeEngine { }, 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 GenericGamemodeEngine { } } - async fn send_loading_events(user: &UserSender, player_id: u8, players: std::sync::Arc>) -> std::io::Result<()> { + async fn send_loading_events(user: &UserSender, _player_id: u8, players: std::sync::Arc>) -> 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 _, diff --git a/rc_multiplayer/src/matches/messages.rs b/rc_multiplayer/src/matches/messages.rs index e45e858..ee99427 100644 --- a/rc_multiplayer/src/matches/messages.rs +++ b/rc_multiplayer/src/matches/messages.rs @@ -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>, }, + CustomLogicRlnl { + user_id: i32, + event: rlnl::event_code::NetworkEvent, + property: literustlib::packet::Property, + data: Box, + }, 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"), } diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs new file mode 100644 index 0000000..0d8d851 --- /dev/null +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -0,0 +1,937 @@ +use crate::matches::CustomGameLogic; + +struct PlayerTracker { + connected: tokio::sync::Mutex>>, // team -> set of player_id + in_point: tokio::sync::RwLock>, // player_id -> in point state (if val > u8::MAX then not in a point) + respawning: tokio::sync::RwLock>, // 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 { + 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) -> Option { + 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>, + 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 { + 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, + last_tick: std::sync::atomic::AtomicI64, +} + +struct PointTickInfo { + owned: std::collections::HashMap, // team -> capture point count + captured_firsts: std::collections::HashSet, // team + lost_lasts: std::collections::HashSet, // team + delta: i64, +} + +impl PointTracker { + const TICK_MS: i64 = 50; + + fn new(points: impl Iterator) -> 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, 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, 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, max_progress: f32) -> Option { + 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, +} + +impl BaseTracker { + fn new<'a>(bases_iter: impl std::iter::Iterator, 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, +} + +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 { + 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>>, + player_tracking: PlayerTracker, + capture_tracking: PointTracker, + base_tracking: BaseTracker, + //cube_parser: std::sync::Arc, + crystals: Vec, + 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) -> 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) { + 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, 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, _player: &crate::matches::generic::UserConnection) -> bool { + true + } + + async fn on_vehicle_destroyed(&self, generic: &crate::matches::GenericGamemodeEngine, _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, _user: u8, _is_classic: bool) -> bool { + true + } + + async fn extra_sync_events(&self, generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { + 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, game_start: chrono::DateTime) -> 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) -> bool { + self.abort_timer_sync().await; + true + } + + async fn on_broadcast(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _event_out: rlnl::event_code::NetworkEvent, _event_in: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: &Option>, _skip_user: bool) -> bool { + true + } + + async fn on_motion(&self, generic: &crate::matches::GenericGamemodeEngine, 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::::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, _user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box) { + 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)"); + } + _ => {} + } + } +} diff --git a/rc_multiplayer/src/matches/modes/elimination.rs b/rc_multiplayer/src/matches/modes/elimination.rs index 4dfcca6..7a92b97 100644 --- a/rc_multiplayer/src/matches/modes/elimination.rs +++ b/rc_multiplayer/src/matches/modes/elimination.rs @@ -497,10 +497,12 @@ impl CustomGameLogic for EliminationLogic { } async fn on_countdown_start(&self, generic: &crate::matches::GenericGamemodeEngine, game_start: chrono::DateTime) -> 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, 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, _user_id: i32, _event: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: Box) {} } // spawn points (best guess) diff --git a/rc_multiplayer/src/matches/modes/mod.rs b/rc_multiplayer/src/matches/modes/mod.rs index a92a7b7..ecbd8ea 100644 --- a/rc_multiplayer/src/matches/modes/mod.rs +++ b/rc_multiplayer/src/matches/modes/mod.rs @@ -4,3 +4,6 @@ pub use no_op::NoOpLogic; mod elimination; pub use elimination::EliminationLogic; + +mod battle_arena; +pub use battle_arena::BattleArenaLogic; diff --git a/rc_multiplayer/src/matches/modes/no_op.rs b/rc_multiplayer/src/matches/modes/no_op.rs index c9d92c8..cf18ef5 100644 --- a/rc_multiplayer/src/matches/modes/no_op.rs +++ b/rc_multiplayer/src/matches/modes/no_op.rs @@ -40,4 +40,6 @@ impl CustomGameLogic for NoOpLogic { async fn on_motion(&self, _generic: &crate::matches::GenericGamemodeEngine, _motion: &rlnl::machine_motion::MachineMotion, _location: (f32, f32, f32)) -> bool { true } + + async fn on_custom(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _event: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: Box) {} } diff --git a/rc_multiplayer/src/matches/timer.rs b/rc_multiplayer/src/matches/timer.rs index 7874206..55a54b6 100644 --- a/rc_multiplayer/src/matches/timer.rs +++ b/rc_multiplayer/src/matches/timer.rs @@ -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)>, game_start: chrono::DateTime, game_end: chrono::DateTime, extra_packets: Vec) -> 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)>, game_start: chrono::DateTime, game_end: chrono::DateTime, extra_packets: Vec, end_packets: Vec) -> 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) -> rlnl::events::GameTime { @@ -11,7 +11,7 @@ pub fn time_to_game_end_payload(game_end: chrono::DateTime) -> rlnl rlnl::events::GameTime(time_until_end_f32) } -async fn do_match_timer_async(players: Vec<(super::generic::UserSender, std::sync::Arc)>, game_start: chrono::DateTime, game_end: chrono::DateTime, extra_packets: Vec) { +async fn do_match_timer_async(players: Vec<(super::generic::UserSender, std::sync::Arc)>, game_start: chrono::DateTime, game_end: chrono::DateTime, extra_packets: Vec, end_packets: Vec) { 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"); diff --git a/rc_services_room/Cargo.toml b/rc_services_room/Cargo.toml index 0eeccb3..7116347 100644 --- a/rc_services_room/Cargo.toml +++ b/rc_services_room/Cargo.toml @@ -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 diff --git a/rc_services_room/src/data/mod.rs b/rc_services_room/src/data/mod.rs index fc299dc..96c4a3d 100644 --- a/rc_services_room/src/data/mod.rs +++ b/rc_services_room/src/data/mod.rs @@ -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; diff --git a/rc_services_room/src/operations/battle_arena_config.rs b/rc_services_room/src/operations/battle_arena_config.rs index c415fb6..d94b86b 100644 --- a/rc_services_room/src/operations/battle_arena_config.rs +++ b/rc_services_room/src/operations/battle_arena_config.rs @@ -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) + Sync + Sync> { - SimpleFunc::new(|params, _| { +pub(super) struct BattleArenaConfigurer { + factory: std::sync::Arc, + weapon_list: std::sync::Arc, + cpu_counter: std::sync::Arc, + ba_conf: oj_rc_core::persist::config::BattleArenaResolver, +} + +#[async_trait::async_trait] +impl SimpleOperation for BattleArenaConfigurer { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, params: ParameterTable, user: &Self::User) -> Result, 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(conf: &oj_rc_core::ConfigImpl, factory: &std::sync::Arc, weapon_list: std::sync::Arc, + cpu_counter: std::sync::Arc) -> SimpleOpImpl { + let ba_conf = >::ba_settings(conf); + SimpleOpImpl::new(BattleArenaConfigurer { + factory: factory.to_owned(), + weapon_list, + cpu_counter, + ba_conf }) } diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 76639ba..9433cfc 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -125,7 +125,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .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())