From 93beb1b4e7bce949cc1a8818988c5d086c89cc59 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sat, 7 Feb 2026 17:47:14 -0500 Subject: [PATCH] Init BA base crystal order on startup if possible, change it occasionally --- rc_core/src/cubes/locations_of.rs | 21 ++++- rc_core/src/persist/config/mod.rs | 2 +- rc_core/src/persist/config/traits.rs | 22 +++++ rc_core/src/persist/multiplayer.rs | 66 ++++++++++++++- rc_multiplayer/src/matches/aggregate.rs | 102 +++++++++++++++++++++++- 5 files changed, 204 insertions(+), 9 deletions(-) diff --git a/rc_core/src/cubes/locations_of.rs b/rc_core/src/cubes/locations_of.rs index bed1d75..d5ba6dd 100644 --- a/rc_core/src/cubes/locations_of.rs +++ b/rc_core/src/cubes/locations_of.rs @@ -177,6 +177,21 @@ impl CubeLocationsParser { } pub fn locations_of_reactor_sort(&self, r: &mut dyn std::io::Read) -> Vec { + self.locations_of_reactor_sort_custom(r, 32, 2) + } + + /// Generate sorted crystal cube list using custom thresholds + /// + /// `bail_after_iters` is the maximum loops to attempt before giving up trying to sort the vehicle's cubes. + /// Decreasing this guarantees a faster function return but increases the risk of the result being incomplete. + /// In the game client, an incomplete result will cause the match to end at a base charge lower than 100%. + /// + /// `deterministic_after_iters` is the maxiumum initial loops to allow for random connection traversal. + /// Decreasing this makes the crystal order more consistent and makes the function return faster. + /// Increasing this makes the crystal order more random and interesting but usually requires more iterations (slower). + /// + /// Usually, this algorithm takes `deterministic_after_iters + 2` iterations to complete. + pub fn locations_of_reactor_sort_custom(&self, r: &mut dyn std::io::Read, bail_after_iters: usize, deterministic_after_iters: usize) -> Vec { use super::{CUBE_CONNECTIONS, CUBE_ROTATIONS}; match super::parser::Cube::parse_list(r) { Ok(cubes) => { @@ -223,15 +238,15 @@ impl CubeLocationsParser { let mut iteration = 0; let mut random = rand::rng(); let mut to_be_released = Vec::new(); - while !to_be_sorted.is_empty() && iteration < 32 { + while !to_be_sorted.is_empty() && iteration < bail_after_iters { for (cube_i, calc_conns) in to_be_sorted.iter() { let connection = is_sharing_connection(calc_conns, &available_faces) .and_then(|(cube_face_i, face_available_i)| { - use rand::Rng; if face_available_i < connected_to_connections.connections.len() && sorted.len() < connected_to_connections.connections.len() { // prioritize finding all target connections first Some((cube_face_i, face_available_i)) - } else if iteration < 3 || (face_available_i >= connected_to_connections.connections.len() && sorted.len() < connected_to_connections.connections.len()) { + } else if iteration < deterministic_after_iters || (face_available_i >= connected_to_connections.connections.len() && sorted.len() < connected_to_connections.connections.len()) { + use rand::Rng; let random_face = random.random_range(0..calc_conns.len()); if cube_face_i >= random_face { Some((cube_face_i, face_available_i)) diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index 634b892..3dfe18e 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, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings}; +pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings, BattleArenaCrystalParams}; mod validation; pub use validation::{SelfValidator, ValidationInfo, ValidationMessage}; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 6c0b52a..6e14ed2 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -412,6 +412,28 @@ impl BattleArenaResolver { ], })) } + + pub fn resolve_base_machine_immediate_early(&self) -> Option> { + match &self.data.base.id { + crate::persist::PrefabId::Raw { cube_data, .. } => { + Some(cube_data.to_owned()) + }, + _ => None, + } + } + + pub fn crystal_sort_params(&self) -> BattleArenaCrystalParams { + BattleArenaCrystalParams { + max_iterations: self.data.max_base_iterations as usize, + max_random_iterations: self.data.max_base_random_iterations as usize, + } + } +} + +#[derive(Debug)] +pub struct BattleArenaCrystalParams { + pub max_iterations: usize, + pub max_random_iterations: usize, } #[derive(Debug)] diff --git a/rc_core/src/persist/multiplayer.rs b/rc_core/src/persist/multiplayer.rs index 03b1b74..841f074 100644 --- a/rc_core/src/persist/multiplayer.rs +++ b/rc_core/src/persist/multiplayer.rs @@ -197,8 +197,8 @@ pub(super) fn default_fake_users() -> Vec { }, }, implementation: ClientEmulation::ClientAI, - },*/ - /*FakePlayerConf { + }, + FakePlayerConf { team: None, vehicle: super::garage::PrefabVehicle { name: Some("fake3".to_owned()), @@ -221,6 +221,54 @@ pub(super) fn default_fake_users() -> Vec { }, }, implementation: ClientEmulation::ClientAI, + }, + FakePlayerConf { + team: None, + vehicle: super::garage::PrefabVehicle { + name: Some("fake5".to_owned()), + username: "Server5".to_owned(), + id: super::garage::PrefabId::Raw { + cube_data: Vec::from(crate::persist::VALID_ROBOT), + colour_data: Vec::from(crate::persist::VALID_COLOUR), + }, + }, + implementation: ClientEmulation::ClientAI, + }, + FakePlayerConf { + team: None, + vehicle: super::garage::PrefabVehicle { + name: Some("fake6".to_owned()), + username: "Server6".to_owned(), + id: super::garage::PrefabId::Raw { + cube_data: Vec::from(crate::persist::VALID_ROBOT), + colour_data: Vec::from(crate::persist::VALID_COLOUR), + }, + }, + implementation: ClientEmulation::ClientAI, + }, + FakePlayerConf { + team: None, + vehicle: super::garage::PrefabVehicle { + name: Some("fake7".to_owned()), + username: "Server7".to_owned(), + id: super::garage::PrefabId::Raw { + cube_data: Vec::from(crate::persist::VALID_ROBOT), + colour_data: Vec::from(crate::persist::VALID_COLOUR), + }, + }, + implementation: ClientEmulation::ClientAI, + }, + FakePlayerConf { + team: None, + vehicle: super::garage::PrefabVehicle { + name: Some("fake8".to_owned()), + username: "Server8".to_owned(), + id: super::garage::PrefabId::Raw { + cube_data: Vec::from(crate::persist::VALID_ROBOT), + colour_data: Vec::from(crate::persist::VALID_COLOUR), + }, + }, + implementation: ClientEmulation::ClientAI, },*/ ] } @@ -313,6 +361,10 @@ pub struct BattleArenaConfig { pub base: super::garage::PrefabVehicle, #[serde(default = "default_segments")] pub num_segments: u16, + #[serde(default = "default_max_base_iterations")] + pub max_base_iterations: u32, + #[serde(default = "default_max_base_random_iterations")] + pub max_base_random_iterations: u32, } pub(super) fn default_ba_conf() -> BattleArenaConfig { @@ -326,6 +378,8 @@ pub(super) fn default_ba_conf() -> BattleArenaConfig { equalizer_duration_s: default_equalizer_duration(), base: default_ba_base(), num_segments: default_segments(), + max_base_iterations: default_max_base_iterations(), + max_base_random_iterations: default_max_base_random_iterations(), } } @@ -378,6 +432,14 @@ fn default_segments() -> u16 { 3 } +fn default_max_base_iterations() -> u32 { + 64 +} + +fn default_max_base_random_iterations() -> u32 { + 3 +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(untagged)] pub enum PitWinCondition { diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index 22fa4b4..32266c4 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -5,16 +5,19 @@ pub struct GameMatches { map_configs: std::collections::HashMap, //fake_players: Vec, cube_parsers: std::sync::Arc, - ba_sorted_crystals: tokio::sync::RwLock>>, // cached after first calculation + ba_sorted_crystals: std::sync::Arc>>>, // cached after first calculation ba_settings: std::sync::Arc, pit_settings: std::sync::Arc, tdm_settings: std::sync::Arc, factory: std::sync::Arc, mp_settings: std::sync::Arc, + is_crystal_regen_running: std::sync::Arc, } impl GameMatches { pub fn new(conf: &oj_rc_core::persist::config::ConfigImpl, cube_parsers: std::sync::Arc, factory: std::sync::Arc) -> Self { + let ba_settings = std::sync::Arc::new(>::ba_settings(conf)); + let ba_sorted_crystals = std::sync::Arc::new(Self::maybe_init_ba_sorted_crystals(&ba_settings, &cube_parsers)); Self { matches: std::collections::HashMap::new(), routing: std::collections::HashMap::new(), @@ -25,15 +28,44 @@ impl GameMatches { .collect(), //fake_players: >::fake_players(conf), cube_parsers, - ba_sorted_crystals: tokio::sync::RwLock::new(std::sync::Arc::new(Vec::default())), - ba_settings: std::sync::Arc::new(>::ba_settings(conf)), + ba_sorted_crystals, + ba_settings, pit_settings: std::sync::Arc::new(>::pit_settings(conf)), tdm_settings: std::sync::Arc::new(>::tdm_settings(conf)), factory, mp_settings: std::sync::Arc::new(>::multiplayer_settings(conf)), + is_crystal_regen_running: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), } } + fn maybe_init_ba_sorted_crystals(ba_conf: &oj_rc_core::persist::config::BattleArenaResolver, cube_parsers: &oj_rc_core::cubes::CubeParsers) -> tokio::sync::RwLock>> { + if let Some(base_machine_map) = ba_conf.resolve_base_machine_immediate_early() { + let gen_start = chrono::Utc::now(); + let gen_params = ba_conf.crystal_sort_params(); + let crystals = Self::generate_ordered_crystal_list(&gen_params, cube_parsers, &base_machine_map); + let gen_end = chrono::Utc::now(); + let delta = gen_end.signed_duration_since(gen_start); + log::info!("Early base crystal list initialization took {}ms for {} crystals", delta.num_milliseconds(), crystals.len()); + tokio::sync::RwLock::new(std::sync::Arc::new(crystals)) + } else { + log::warn!("First Batte Arena match will take longer to initialization due to non-raw base machine"); + tokio::sync::RwLock::new(std::sync::Arc::new(Vec::default())) + } + } + + fn generate_ordered_crystal_list( + gen_params: &oj_rc_core::persist::config::BattleArenaCrystalParams, + cube_parsers: &oj_rc_core::cubes::CubeParsers, + vehicle_data: &[u8], + ) -> Vec { + cube_parsers.locations_of() + .locations_of_reactor_sort_custom( + &mut std::io::Cursor::new(vehicle_data), + gen_params.max_iterations, + gen_params.max_random_iterations, + ) + } + pub fn spawn(self) -> tokio::sync::mpsc::Sender { let (tx, rx) = tokio::sync::mpsc::channel(super::CHANNEL_BOUND); tokio::spawn(self.run(rx)); @@ -117,6 +149,8 @@ impl GameMatches { })?; let crystals = if self.ba_sorted_crystals.read().await.is_empty() { let crystals = std::sync::Arc::new( + // NOTE: this uses default (lower) crystal sort params to prevent sorting from taking too long + // if this takes too long, players can be disconnected from the multiplayer due to client timeout self.cube_parsers.locations_of() .locations_of_reactor_sort(&mut std::io::Cursor::new(&resolved_ba_conf.base_machine_map)) ); @@ -207,6 +241,16 @@ impl GameMatches { }; self.matches.insert(game_guid.clone(), tx.clone()); self.routing.insert(user.user_id(), game_guid.clone()); + if !self.is_crystal_regen_running.swap(true, std::sync::atomic::Ordering::SeqCst) { + tokio::task::spawn(Self::regenerate_crystal_order_task( + self.ba_sorted_crystals.clone(), + self.cube_parsers.clone(), + self.factory.clone(), + self.ba_settings.clone(), + user.clone(), + self.is_crystal_regen_running.clone(), + )); + } if tx.send(super::GameMessage::NewConnection { user, game_guid, connection, response, sender }).await.is_err() { log::error!("Failed to send NewConnection game message to new match"); } @@ -275,4 +319,56 @@ impl GameMatches { } log::warn!("Match message router has completed"); } + + async fn regenerate_crystal_order_task( + crystal_order: std::sync::Arc>>>, + parsers: std::sync::Arc, + factory: std::sync::Arc, + ba_resolver: std::sync::Arc, + user: std::sync::Arc>, + tracker: std::sync::Arc, + ) { + //tracker.store(true, std::sync::atomic::Ordering::SeqCst); + let ba_resolved_result = ba_resolver.resolve( + user.as_ref().as_ref(), + factory.as_ref(), + parsers.weapon_order().as_ref(), + parsers.cpu_counter().as_ref(), + ).await; + match ba_resolved_result { + Ok(ba_resolved) => { + log::info!("Regenerate crystal order task started successfully, task sleeping for now"); + // wait a long while to prevent this from interfering with the match's startup + tokio::time::sleep(std::time::Duration::from_secs(5 * 60)).await; // 5 minutes + let gen_start = chrono::Utc::now(); + let new_order_result = tokio::task::spawn_blocking(move || { + Self::generate_ordered_crystal_list( + &ba_resolver.crystal_sort_params(), + parsers.as_ref(), + &ba_resolved.base_machine_map, + ) + }).await; + match new_order_result { + Ok(new_order) => { + let gen_end = chrono::Utc::now(); + let delta = gen_end.signed_duration_since(gen_start); + log::info!("Base crystal order re-gen took {}ms for {} crystals", delta.num_milliseconds(), new_order.len()); + *crystal_order.write().await = std::sync::Arc::new(new_order); + }, + Err(e) => { + log::error!("Failed to regenerate crystal order: {}", e); + } + } + + }, + Err(e) => { + if let Some(e_msg) = e.error_msg() { + log::error!("Failed to regenerate crystal order: {} ({})", e_msg, e.error_code()); + } else { + log::error!("Failed to regenerate crystal order ({})", e.error_code()); + } + } + } + tracker.store(false, std::sync::atomic::Ordering::SeqCst); + } }