From 05bf4a097a59720f07609d7fcc9c867c83dc682a Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sat, 16 Aug 2025 21:32:14 -0400 Subject: [PATCH] Add basic server-only player harness --- Cargo.lock | 1 + rc_core/src/persist/combat.rs | 1 + rc_core/src/persist/config/cubes_json.rs | 9 ++ rc_core/src/persist/config/mod.rs | 2 +- rc_core/src/persist/config/traits.rs | 14 +++ rc_core/src/persist/multiplayer.rs | 37 +++++++ rc_core/src/persist/user/account_json.rs | 5 + rc_core/src/persist/user/lobby.rs | 4 +- rc_core/src/persist/user/multiplayer.rs | 22 ++--- rc_core/src/persist/user/traits.rs | 2 +- rc_lobby_room/src/lobby.rs | 6 +- rc_lobby_room/src/main.rs | 2 +- rc_multiplayer/Cargo.toml | 1 + rc_multiplayer/src/matches/aggregate.rs | 27 ++++- .../src/matches/fake/experimental.rs | 99 +++++++++++++++++++ rc_multiplayer/src/matches/fake/handler.rs | 63 ++++++++++++ rc_multiplayer/src/matches/fake/mod.rs | 8 ++ rc_multiplayer/src/matches/fake/traits.rs | 7 ++ rc_multiplayer/src/matches/generic.rs | 13 ++- rc_multiplayer/src/matches/mod.rs | 2 + 20 files changed, 304 insertions(+), 21 deletions(-) create mode 100644 rc_multiplayer/src/matches/fake/experimental.rs create mode 100644 rc_multiplayer/src/matches/fake/handler.rs create mode 100644 rc_multiplayer/src/matches/fake/mod.rs create mode 100644 rc_multiplayer/src/matches/fake/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 4428da1..241a78e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2785,6 +2785,7 @@ dependencies = [ "log", "num-quaternion", "oj_rc_core", + "rand 0.9.0", "rlnl", "tokio", ] diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 784baca..82c100f 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -489,6 +489,7 @@ fn default_multiplayer() -> super::MultiplayerConfig { players_per_game: 2, enabled: true, network: super::multiplayer::default_net_conf(), + fakes: super::multiplayer::default_fake_users(), } } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 0fb0baf..6cadc63 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -418,4 +418,13 @@ impl super::ConfigProvider for CubeConfig { wiki_url: self.settings.server.wiki_url.clone(), } } + + fn fake_players(&self) -> Vec { + self.battle.multiplayer.fakes.iter().map(|player| super::FakePlayer { + public_id: player.public_id.clone(), + display_name: player.display_name.clone(), + team: player.team, + implementation: player.implementation.clone().to_config(), + }).collect() + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index 17dcbd0..a596a4a 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}; +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}; pub type ConfigImpl = CubeConfig; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 44bc0ac..76b55f3 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -33,6 +33,7 @@ pub trait ConfigProvider { fn network_config(&self) -> crate::persist::NetworkConf; fn maps(&self) -> std::collections::HashMap; fn url_links(&self) -> LinksConfig; + fn fake_players(&self) -> Vec; } pub struct CompleteCampaignProvider { @@ -365,3 +366,16 @@ pub struct LinksConfig { pub support_url: String, pub wiki_url: String, } + +#[derive(Clone, Debug)] +pub struct FakePlayer { + pub public_id: String, + pub display_name: String, + pub team: u8, + pub implementation: ClientEmulator, +} + +#[derive(Clone, Copy, Debug)] +pub enum ClientEmulator { + Experiment, +} diff --git a/rc_core/src/persist/multiplayer.rs b/rc_core/src/persist/multiplayer.rs index e9bd6be..9e9bd22 100644 --- a/rc_core/src/persist/multiplayer.rs +++ b/rc_core/src/persist/multiplayer.rs @@ -6,6 +6,8 @@ pub struct MultiplayerConfig { pub enabled: bool, #[serde(default = "default_net_conf")] pub network: NetworkConf, + #[serde(default = "default_fake_users")] + pub fakes: Vec, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -46,3 +48,38 @@ pub(super) fn default_net_conf() -> NetworkConf { max_delay_for_disconnect_ms: 1000, } } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct FakePlayerConf { + pub public_id: String, + pub display_name: String, + pub team: u8, + #[serde(flatten)] + pub implementation: ClientEmulation, +} + +pub(super) fn default_fake_users() -> Vec { + //Vec::default() + vec![ + FakePlayerConf { + public_id: "ServerExperiment01".to_owned(), + display_name: "Server".to_owned(), + team: 1, + implementation: ClientEmulation::Experimental, + } + ] +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "impl")] +pub enum ClientEmulation { + Experimental, +} + +impl ClientEmulation { + pub(super) fn to_config(self) -> super::config::ClientEmulator { + match self { + Self::Experimental => super::config::ClientEmulator::Experiment, + } + } +} diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 70c356b..a91b07f 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -5,6 +5,7 @@ use crate::persist::config::ConfigProvider; pub struct AccountProvider { cubes: std::sync::Arc>, garage_upgrades: std::sync::Arc, + fake_players: std::sync::Arc>, auto_signups: bool, cdn: std::sync::Arc, secret: std::sync::Arc>, @@ -22,6 +23,7 @@ impl AccountProvider { Ok(Self { cubes: std::sync::Arc::new(>::ids(conf)), garage_upgrades: std::sync::Arc::new(>::garage_upgrades(conf)), + fake_players: std::sync::Arc::new(>::fake_players(conf)), auto_signups: server_settings.auto_signup, cdn: std::sync::Arc::new(server_settings.cdn_url), secret: std::sync::Arc::new(std::fs::read(&token_path)?), @@ -102,6 +104,7 @@ impl super::UserProvider for AccountProvider { perms: user_perms, cubes: self.cubes.clone(), garage_upgrades: self.garage_upgrades.clone(), + fake_players: self.fake_players.clone(), cdn: self.cdn.clone(), db: self.db.clone(), secret: self.secret.clone(), @@ -137,6 +140,7 @@ impl super::UserProvider for AccountProvider { perms: user_perms, cubes: self.cubes.clone(), garage_upgrades: self.garage_upgrades.clone(), + fake_players: self.fake_players.clone(), cdn: self.cdn.clone(), db: self.db.clone(), secret: self.secret.clone(), @@ -298,6 +302,7 @@ pub(super) struct UserData { pub(super) perms: oj_rc_database::schema::permissions::Model, pub(super) cubes: std::sync::Arc>, pub(super) garage_upgrades: std::sync::Arc, + pub(super) fake_players: std::sync::Arc>, pub(super) cdn: std::sync::Arc, pub(super) db: std::sync::Arc, pub(super) secret: std::sync::Arc>, diff --git a/rc_core/src/persist/user/lobby.rs b/rc_core/src/persist/user/lobby.rs index eb94338..53e8c10 100644 --- a/rc_core/src/persist/user/lobby.rs +++ b/rc_core/src/persist/user/lobby.rs @@ -17,7 +17,7 @@ impl super::LobbyUser for UserData { }) } - async fn start_game(&self, game: super::GameDescriptor, players: Vec) -> Result { + async fn start_game(&self, game: super::GameDescriptor, players: Vec, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser) -> Result { let now = chrono::Utc::now().timestamp(); let guid = crate::persist::user::str_to_i64(&game.guid) .ok_or_else(|| polariton_server::operations::SimpleOpError::with_message( @@ -32,7 +32,7 @@ impl super::LobbyUser for UserData { oj_rc_database::schema::multiplayer_game::GameType::Standard }; - let fake_players = self.generate_fake_players_data(guid).await; + let fake_players = self.generate_fake_players_data(guid, cpu_counter, weapon_lister).await; let game_dbo = oj_rc_database::schema::multiplayer_game::ActiveModel { id: oj_rc_database::sea_orm::ActiveValue::NotSet, diff --git a/rc_core/src/persist/user/multiplayer.rs b/rc_core/src/persist/user/multiplayer.rs index 9df5ef3..a76ce54 100644 --- a/rc_core/src/persist/user/multiplayer.rs +++ b/rc_core/src/persist/user/multiplayer.rs @@ -1,30 +1,30 @@ use super::account_json::UserData; impl UserData { - pub(super) async fn generate_fake_players_data(&self, _guid: i64) -> Vec { - vec![ - crate::data::player_data::PlayerData { - name: "FakeUser".to_owned(), - display_name: "Server".to_owned(), + pub(super) async fn generate_fake_players_data(&self, _guid: i64, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser) -> Vec { + self.fake_players.iter() + .map(|fake| crate::data::player_data::PlayerData { + name: fake.public_id.clone(), + display_name: fake.display_name.clone(), mastery: 1, tier: 1, - robot_name: "Very bad but very good".to_owned(), + robot_name: "fake".to_owned(), robot_map: crate::persist::VALID_ROBOT.into(), group: None, - team: 2, + team: fake.team as _, has_premium: true, robot_uuid: "1234_1234".to_owned(), - cpu: 42, + cpu: cpu_counter.calculate_cpu(&mut std::io::Cursor::new(crate::persist::VALID_ROBOT)).total as _, avatar_id: Some(0), - weapon_order: vec![0,0,0], + weapon_order: weapon_lister.guess_weapons(&mut std::io::Cursor::new(crate::persist::VALID_ROBOT)), colour_map: crate::persist::VALID_COLOUR.into(), is_ai: false, spawn_effect: "Spawn".into(), death_effect: "Explosion".into(), player_rank: 1, weapon_rank: Default::default(), - } - ] + }) + .collect() } } diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 5e07051..34e2ef9 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -249,7 +249,7 @@ impl SanctionType { pub trait LobbyUser { fn user_id(&self) -> i32; async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result; - async fn start_game(&self, game: GameDescriptor, players: Vec) -> Result; + async fn start_game(&self, game: GameDescriptor, players: Vec, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser) -> Result; } pub struct FakePlayers { diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index 5492f5f..9528330 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -39,11 +39,12 @@ pub struct QueueHandler { hostport: u16, network_conf: crate::data::network::NetworkConfigData, cpu_counter: std::sync::Arc, + weapon_guesser: std::sync::Arc, change_strategy: GamemodeChangeStrategy, } impl QueueHandler { - pub fn new(conf: &oj_rc_core::ConfigImpl, game_host: &str, cpu_counter: std::sync::Arc) -> Self { + pub fn new(conf: &oj_rc_core::ConfigImpl, game_host: &str, cpu_counter: std::sync::Arc, weapon_guesser: std::sync::Arc,) -> Self { let (domain, port_str) = game_host.split_once(':').expect("Invalid redirect address (must be domain:port)"); Self { users_in_queue: tokio::sync::Mutex::new(HashMap::new()), @@ -53,6 +54,7 @@ impl QueueHandler { hostport: port_str.parse().expect("Invalid redirect port"), network_conf: crate::data::network::NetworkConfigData::from_conf(oj_rc_core::ConfigProvider::<()>::network_config(conf)), cpu_counter, + weapon_guesser, change_strategy: GamemodeChangeStrategy::from_core(>::server_config(conf).queue_mode), } } @@ -81,7 +83,7 @@ impl QueueHandler { is_custom: false, is_complete: false, }; - match user.start_game(game_desc, player_descs).await { + match user.start_game(game_desc, player_descs, &self.cpu_counter, &self.weapon_guesser).await { Ok(fakes) => { let player_datas = players.iter().map(|x| x.player.clone()).chain(fakes.players.into_iter()).collect(); let enter_battle_ev = crate::events::battle_enter::BattleEnter { diff --git a/rc_lobby_room/src/main.rs b/rc_lobby_room/src/main.rs index 5ee5dfa..c5eb9ce 100644 --- a/rc_lobby_room/src/main.rs +++ b/rc_lobby_room/src/main.rs @@ -31,7 +31,7 @@ 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")); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); - let queue = std::sync::Arc::new(QueueHandler::new(&config, &args.redirect, parsers.cpu_counter())); + let queue = std::sync::Arc::new(QueueHandler::new(&config, &args.redirect, parsers.cpu_counter(), parsers.weapon_order())); let init_ctx = InitConfig { config, diff --git a/rc_multiplayer/Cargo.toml b/rc_multiplayer/Cargo.toml index 704479c..755d0d3 100644 --- a/rc_multiplayer/Cargo.toml +++ b/rc_multiplayer/Cargo.toml @@ -19,6 +19,7 @@ byteserde = "0.6.2" chrono.workspace = true atomic_float = "1.1" num-quaternion.workspace = true +rand.workspace = true #literustlib_server = { version = "0.1", path = "../../LiteRustLib/server" } #literustlib = { version = "0.1", path = "../../LiteRustLib" } diff --git a/rc_multiplayer/src/matches/aggregate.rs b/rc_multiplayer/src/matches/aggregate.rs index c93b62c..6a0ccfe 100644 --- a/rc_multiplayer/src/matches/aggregate.rs +++ b/rc_multiplayer/src/matches/aggregate.rs @@ -3,6 +3,7 @@ pub struct GameMatches { routing: std::collections::HashMap, // user id to game guid mode_configs: oj_rc_core::data::game_mode::GameModeConfigs, map_configs: std::collections::HashMap, + fake_players: Vec, } impl GameMatches { @@ -15,6 +16,7 @@ impl GameMatches { .into_iter() .map(|(map, conf)| (oj_rc_core::data::game_mode::GameMap::from_persist(map).as_str().to_owned(), conf)) .collect(), + fake_players: >::fake_players(conf), } } @@ -24,6 +26,26 @@ impl GameMatches { tx } + fn build_player_emulator(&self, emu: oj_rc_core::persist::config::ClientEmulator) -> Box { + match emu { + oj_rc_core::persist::config::ClientEmulator::Experiment => Box::new(super::fake::ExperimentalPlayer::new()), + } + } + + fn build_fake_players(&self, players: &[oj_rc_core::persist::user::PlayerDescriptor]) -> std::collections::HashMap> { + let mut fake_player_i = 0; + let mut fakes = std::collections::HashMap::with_capacity(self.fake_players.len()); + for player in players.iter() { + if fake_player_i >= self.fake_players.len() { break; } + if player.user_id.is_none() { + let fake = self.build_player_emulator(self.fake_players[fake_player_i].implementation); + fakes.insert(player.player_id, fake); + fake_player_i += 1; + } + } + fakes + } + async fn start_new_match_engine(&self, user: &Box, guid: &str) -> Result, oj_rc_core::persist::user::MultiplayerError> { let game_info = user.game_info(guid).await? .ok_or_else(|| oj_rc_core::persist::user::MultiplayerError { @@ -41,8 +63,10 @@ impl GameMatches { }); let players = user.game_players(guid).await?; if players.is_empty() { - log::warn!("No players found to game {}, loading may not work correctly", guid); + log::warn!("No players found for game {}, loading may not work correctly", guid); } + let fakes = self.build_fake_players(&players); + let fakes_handler = super::fake::Handler::start(fakes, players.clone()).await; match game_info.mode { oj_rc_core::data::game_mode::GameMode::SuddenDeath => { let inner = super::modes::EliminationLogic::new(&self.mode_configs.elimination, &map_config); @@ -51,6 +75,7 @@ impl GameMatches { map_config, players, inner, + fakes_handler, ); Ok(engine.spawn()) } diff --git a/rc_multiplayer/src/matches/fake/experimental.rs b/rc_multiplayer/src/matches/fake/experimental.rs new file mode 100644 index 0000000..ba6f9f1 --- /dev/null +++ b/rc_multiplayer/src/matches/fake/experimental.rs @@ -0,0 +1,99 @@ +use rand::Rng; +use byteserde::ser_heap::ByteSerializeHeap; + +pub struct ExperimentalPlayer { + me: tokio::sync::RwLock>, + is_complete: std::sync::Arc, +} + +impl ExperimentalPlayer { + pub fn new() -> Self { + Self { + me: tokio::sync::RwLock::new(None), + is_complete: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), + } + } +} + +#[async_trait::async_trait] +impl super::FakeUser for ExperimentalPlayer { + async fn on_init(&self, descriptors: &Vec, player_id: u8) { + if let Some(my_desc) = descriptors.iter().filter(|x| x.player_id == player_id).next() { + *self.me.write().await = Some(my_desc.to_owned()); + } + } + + async fn on_ready(&self, real_players: &std::collections::HashMap) { + let movement_rx = real_players.values().map(|x| x.to_owned()).collect(); + let is_complete = self.is_complete.clone(); + let player_id = self.me.read().await.as_ref().unwrap().player_id; + tokio::task::spawn(erratic_behaviour(movement_rx, is_complete, player_id)); + } + + async fn on_end(&self) { + self.is_complete.store(true, std::sync::atomic::Ordering::Relaxed); + } +} + +fn random_map_coord(rng: &mut rand::rngs::ThreadRng) -> f32 { + const CONVERSION_FACTOR: f32 = 300.0 / (i16::MAX as f32); + let int = rng.random::(); + (int as f32) * CONVERSION_FACTOR +} + +fn random_height_coord(rng: &mut rand::rngs::ThreadRng) -> f32 { + const CONVERSION_FACTOR: f32 = 50.0 / (i16::MAX as f32); + let int = rng.random::(); + ((int as f32) * CONVERSION_FACTOR) + 10.0 +} + +async fn erratic_behaviour(send_to: Vec, is_complete: std::sync::Arc, player_id: u8) { + const SLEEP_PERIOD: std::time::Duration = std::time::Duration::from_secs(1); + let mut fake_timestamp = 42.0; + while !is_complete.load(std::sync::atomic::Ordering::Relaxed) { + let (pos_x, pos_y, pos_z) = { + let mut rng = rand::rng(); + let pos_x: f32 = random_map_coord(&mut rng); + let pos_y: f32 = random_height_coord(&mut rng); + let pos_z: f32 = random_map_coord(&mut rng); + (pos_x, pos_y, pos_z) + }; + let motion = rlnl::machine_motion::MachineMotion { + last_sent_seconds_a: 1.0, + last_sent_seconds_b: 2.0, + timestamp: fake_timestamp, + player_id, + target_point: rlnl::types::CompressedVec3::from((50.0, 50.0, 50.0)), + rb_state: rlnl::machine_motion::RigidBodyState { + rb_pos_rot: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3::from((pos_x, pos_y, pos_z)), + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + angular_velocity: rlnl::types::CompressedVec3::from((0.0, 0.0, 0.0)), + center_of_mass: rlnl::types::CompressedVec3::from((1.0, 1.0, 1.0)), + }, + }; + let mut ser = byteserde::ser_heap::ByteSerializerHeap::default(); + if let Err(e) = motion.byte_serialize_heap(&mut ser) { + log::error!("Failed to serialize motion data from experimental fake player: {}", e); + } else { + let motion_data = bytes::Bytes::copy_from_slice(ser.as_slice()); + 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); + } + fake_timestamp += 1.0; + tokio::time::sleep(SLEEP_PERIOD).await; + } + is_complete.store(false, std::sync::atomic::Ordering::Relaxed); +} + +async fn send_motion_data_to(to: &crate::matches::generic::UserSender, motion: bytes::Bytes) { + crate::events::log_lnl_send_failure(to.sender.send_data(crate::handler::EventData { + message_ty: crate::data::MessageType::RobotMotion, + variant: 0, + data_size: motion.len() as _, + data: motion, + }, literustlib::packet::Property::Unreliable, &to.connection).await); +} diff --git a/rc_multiplayer/src/matches/fake/handler.rs b/rc_multiplayer/src/matches/fake/handler.rs new file mode 100644 index 0000000..0270fc8 --- /dev/null +++ b/rc_multiplayer/src/matches/fake/handler.rs @@ -0,0 +1,63 @@ +enum Message { + Ready { + real_players: std::collections::HashMap, + }, + Stop, +} + +pub struct Handler { + tx: tokio::sync::mpsc::UnboundedSender, +} + +impl Handler { + pub async fn start(fakes: std::collections::HashMap>, descriptors: Vec) -> Self { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + tokio::task::spawn(handler_loop(rx, fakes, descriptors)); + Self { + tx, + } + } + + pub fn on_ready(&self, real_players: std::collections::HashMap) { + Self::log_failure(self.tx.send(Message::Ready { real_players })); + } + + pub fn stop(&self) { + Self::log_failure(self.tx.send(Message::Stop)); + } + + #[inline] + fn log_failure(res: Result<(), tokio::sync::mpsc::error::SendError>) { + if let Err(_e) = res { + log::warn!("Failed to send message to fake user handler thread"); + } + } +} + +async fn handler_loop(mut rx: tokio::sync::mpsc::UnboundedReceiver, players: std::collections::HashMap>, descriptors: Vec) { + for player in descriptors.iter() { + if player.user_id.is_none() { + let player_id = player.player_id; + if let Some(fake_player) = players.get(&player_id) { + fake_player.on_init(&descriptors, player_id).await; + } else { + log::warn!("No fake user implementation for player {}", player_id); + } + } + } + while let Some(msg) = rx.recv().await { + match msg { + Message::Ready { real_players } => { + for fake in players.values() { + fake.on_ready(&real_players).await; + } + }, + Message::Stop => { + for fake in players.values() { + fake.on_end().await; + } + break; + }, + } + } +} diff --git a/rc_multiplayer/src/matches/fake/mod.rs b/rc_multiplayer/src/matches/fake/mod.rs new file mode 100644 index 0000000..1e02699 --- /dev/null +++ b/rc_multiplayer/src/matches/fake/mod.rs @@ -0,0 +1,8 @@ +mod handler; +pub use handler::Handler; + +mod traits; +pub use traits::FakeUser; + +mod experimental; +pub use experimental::ExperimentalPlayer; diff --git a/rc_multiplayer/src/matches/fake/traits.rs b/rc_multiplayer/src/matches/fake/traits.rs new file mode 100644 index 0000000..a387cde --- /dev/null +++ b/rc_multiplayer/src/matches/fake/traits.rs @@ -0,0 +1,7 @@ +#[async_trait::async_trait] +pub trait FakeUser: Send + Sync { + async fn on_init(&self, descriptors: &Vec, player_id: u8); + async fn on_ready(&self, real_players: &std::collections::HashMap); + //fn on_damage(&self, data: &rlnl::events::ingame::DestroyCubesFull); + async fn on_end(&self); +} diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 0ffb35e..a1c9522 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -194,6 +194,7 @@ pub(super) struct GenericGamemodeEngine { pub players_info: std::sync::Arc>, pub custom_logic_handler: L, pub fake_users: std::collections::HashMap, + pub fakes_handler: super::fake::Handler, } impl GenericGamemodeEngine { @@ -204,7 +205,8 @@ impl GenericGamemodeEngine { game: oj_rc_core::persist::user::GameDescriptor, map: oj_rc_core::persist::config::MapConfig, players: Vec, - custom: L + custom: L, + fakes_handler: super::fake::Handler, ) -> Self { let fake_users = players.iter() @@ -221,6 +223,7 @@ impl GenericGamemodeEngine { players_info: std::sync::Arc::new(players), custom_logic_handler: custom, fake_users, + fakes_handler, } } @@ -470,7 +473,7 @@ impl GenericGamemodeEngine { } for fake in self.fake_users.values() { let event = rlnl::events::loading::LoadingProgress { - user_name: rlnl::types::BinaryWriterString("FakeUser".to_owned()), + 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( @@ -555,6 +558,11 @@ impl GenericGamemodeEngine { let player_count = self.players_info.iter().filter(|x| x.user_id.is_some()).count(); log::info!("All players ({}) are ready for game {}", player_count, self.game_guid()); tokio::time::sleep(Self::END_OF_SYNC_DELAY).await; + self.fakes_handler.on_ready( + self.users.read().await.iter() + .map(|(id, real_player)| (*id, real_player.connection.clone())) + .collect() + ); let game_start = chrono::Utc::now() + Self::COUNTDOWN_DURATION; if self.custom_logic_handler.on_countdown_start(&self, game_start).await { let mut senders = Vec::new(); @@ -812,6 +820,7 @@ impl GenericGamemodeEngine { } } } + self.fakes_handler.stop(); log::info!("Game {} has exited", self.game_guid()); } diff --git a/rc_multiplayer/src/matches/mod.rs b/rc_multiplayer/src/matches/mod.rs index afaf41b..72c5820 100644 --- a/rc_multiplayer/src/matches/mod.rs +++ b/rc_multiplayer/src/matches/mod.rs @@ -16,4 +16,6 @@ pub mod modes; mod timer; +pub(self) mod fake; + pub const CHANNEL_BOUND: usize = 16;