From 127de674b42a138f3db8175676d5153b6102e18b Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Wed, 29 Oct 2025 21:29:34 -0400 Subject: [PATCH] Allow client AIs to run on non-teammates when there are no teammates available, fix/answer #42 --- rc_core/src/persist/combat.rs | 29 +++++++-- rc_core/src/persist/multiplayer.rs | 4 +- rc_multiplayer/src/events/mod.rs | 4 ++ rc_multiplayer/src/matches/fake/client_ai.rs | 65 ++++++++++++++------ 4 files changed, 75 insertions(+), 27 deletions(-) diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 6dc0b2b..9ecc042 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -28,7 +28,7 @@ impl super::config::SelfValidator for BattleConfig { // TODO votes // TODO games is_ok &= self.singleplayer.validate_in(info, ctx, "singleplayer"); - is_ok &= self.rotation.validate_in(info, ctx, "rotation"); + is_ok &= self.rotation.validate_in(info, self, "rotation"); // TODO multiplayer // TODO maps // TODO energy @@ -134,7 +134,7 @@ pub struct GameEventSequence { } impl super::config::SelfValidator for GameEventSequence { - type Context = crate::ConfigImpl; + type Context = BattleConfig; fn validate(&self, info: &mut super::config::ValidationInfo, ctx: &Self::Context) -> bool { // TODO let mut is_ok = true; @@ -168,11 +168,11 @@ pub struct GameEvents { } impl super::config::SelfValidator for GameEvents { - type Context = crate::ConfigImpl; - fn validate(&self, info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + type Context = BattleConfig; + fn validate(&self, info: &mut super::config::ValidationInfo, ctx: &Self::Context) -> bool { // TODO let mut is_ok = true; - if !matches!(self.singleplayer.mode, GameType::SuddenDeath) { + if !matches!(self.singleplayer.mode, GameType::SuddenDeath | GameType::SinglePlayer) { info.warn(crate::persist::config::ValidationMessage { path: vec!["singleplayer".to_owned(), "mode".to_owned()], message: format!("Singleplayer game mode {:?} will be overidden by the client", self.singleplayer.mode), @@ -185,6 +185,23 @@ impl super::config::SelfValidator for GameEvents { }); is_ok = false; } + if matches!(self.multiplayer.mode, GameType::Pit) { + if ctx.multiplayer.fakes.iter().any(|f| (f.team as usize) < ctx.multiplayer.players_per_game) + || ctx.multiplayer.fakes.iter().enumerate() + .any(|(i, f)| ctx.multiplayer.fakes.iter().enumerate() + .any(|(i2, f2)| i != i2 && f.team == f2.team)) { + info.warn(crate::persist::config::ValidationMessage { + path: vec!["multiplayer".to_owned(), "mode".to_owned()], + message: format!("Multiplayer game mode {:?} does not work well with more than one player per team", self.multiplayer.mode), + }); + } + if ctx.multiplayer.fakes.iter().any(|f| matches!(f.implementation, super::multiplayer::ClientEmulation::ClientAI)) { + info.warn(crate::persist::config::ValidationMessage { + path: vec!["multiplayer".to_owned(), "mode".to_owned()], + message: format!("Multiplayer game mode {:?} does not work well with client-side AI", self.multiplayer.mode), + }); + } + } is_ok } } @@ -428,7 +445,7 @@ fn default_rotation() -> GameEventSequence { multiplayer: GameEvent { map: GameMap::Earth1, visibility: GameVisibility::Good, - mode: GameType::TeamDeathmatch, + mode: GameType::Pit, auto_heal: true, }, duration_s: 5*60, // 5 minutes diff --git a/rc_core/src/persist/multiplayer.rs b/rc_core/src/persist/multiplayer.rs index a150c44..9e0b1f2 100644 --- a/rc_core/src/persist/multiplayer.rs +++ b/rc_core/src/persist/multiplayer.rs @@ -120,7 +120,7 @@ pub(super) fn default_fake_users() -> Vec { implementation: ClientEmulation::Experimental, }, FakePlayerConf { - team: 0, + team: 1, vehicle: super::garage::PrefabVehicle { name: Some("fake1".to_owned()), username: "Server1".to_owned(), @@ -132,7 +132,7 @@ pub(super) fn default_fake_users() -> Vec { implementation: ClientEmulation::ClientAI, }, FakePlayerConf { - team: 1, + team: 2, vehicle: super::garage::PrefabVehicle { name: Some("fake2".to_owned()), username: "Server2".to_owned(), diff --git a/rc_multiplayer/src/events/mod.rs b/rc_multiplayer/src/events/mod.rs index bd0c2c5..2ae93a4 100644 --- a/rc_multiplayer/src/events/mod.rs +++ b/rc_multiplayer/src/events/mod.rs @@ -197,6 +197,10 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa {literustlib::packet::Property::ReliableOrdered as u8}, rlnl::events::ingame::AwardProtoniumDestroyedCubes, >::handler(init_ctx)) + .add(crate::handlers::Stub::< + {rlnl::event_code::NetworkEvent::ProtectTeamMateBonusRequest as i16}, + rlnl::events::ingame::DestroyedHealedCubesBonus, + >::handler(init_ctx)) .add(player_leave::handler(init_ctx)) } diff --git a/rc_multiplayer/src/matches/fake/client_ai.rs b/rc_multiplayer/src/matches/fake/client_ai.rs index a12de76..8895fd4 100644 --- a/rc_multiplayer/src/matches/fake/client_ai.rs +++ b/rc_multiplayer/src/matches/fake/client_ai.rs @@ -30,29 +30,56 @@ impl ClientAIPlayer { #[async_trait::async_trait] impl super::FakeUser for ClientAIPlayer { async fn on_init(&self, descriptors: &[oj_rc_core::persist::user::PlayerDescriptor], player_id: u8) { - let first_fake_i = descriptors.iter() - .filter(|x| x.team == self.me.team) - .enumerate() - .find(|x| x.1.mode.is_some()) - .map(|(i, _)| i) - .unwrap(); - let my_i = descriptors.iter() - .enumerate() - .find(|x| x.1.player_id == player_id) - .map(|(i, _)| i) - .unwrap(); let real_teammates_count = descriptors.iter() .filter(|x| x.team == self.me.team && x.mode.is_none()) .count(); - let my_offset = (my_i - first_fake_i) % real_teammates_count; - for (i, teammate) in descriptors.iter().filter(|x| x.team == self.me.team && x.mode.is_none()).enumerate() { - if i >= my_offset { - self.set_assigned_to(Some(teammate.player_id)); - break; + if real_teammates_count == 0 { + let real_players_count = descriptors.iter() + .filter(|x| x.mode.is_none()) + .count(); + let first_fake_i = descriptors.iter() + .enumerate() + .find(|x| x.1.mode.is_some()) + .map(|(i, _)| i) + .unwrap(); + let my_i = descriptors.iter() + .enumerate() + .find(|x| x.1.player_id == player_id) + .map(|(i, _)| i) + .unwrap(); + let my_offset = (my_i - first_fake_i) % real_players_count; + for (i, teammate) in descriptors.iter().filter(|x| x.mode.is_none()).enumerate() { + if i >= my_offset { + self.set_assigned_to(Some(teammate.player_id)); + break; + } + } + if self.assigned_to_player_id().is_none() { + log::warn!("Failed to assign client AI player {} to a real client; offset:{}, first_fake:{}, reals:{}, me:{}", player_id, my_offset, first_fake_i, real_teammates_count, my_i); + } + } else { + let first_fake_i = descriptors.iter() + .filter(|x| x.team == self.me.team) + .enumerate() + .find(|x| x.1.mode.is_some()) + .map(|(i, _)| i) + .unwrap(); + let my_i = descriptors.iter() + .filter(|x| x.team == self.me.team) + .enumerate() + .find(|x| x.1.player_id == player_id) + .map(|(i, _)| i) + .unwrap(); + let my_offset = (my_i - first_fake_i) % real_teammates_count; + for (i, teammate) in descriptors.iter().filter(|x| x.team == self.me.team && x.mode.is_none()).enumerate() { + if i >= my_offset { + self.set_assigned_to(Some(teammate.player_id)); + break; + } + } + if self.assigned_to_player_id().is_none() { + log::warn!("Failed to assign client AI player {} to a real client; offset:{}, first_fake:{}, reals:{}, me:{}", player_id, my_offset, first_fake_i, real_teammates_count, my_i); } - } - if self.assigned_to_player_id().is_none() { - log::warn!("Failed to assign client AI player {} to a real client; offset:{}, first_fake:{}, reals:{}, me:{}", player_id, my_offset, first_fake_i, real_teammates_count, my_i); } }