From d934e6ea3310b601ccb9739a41bbf11b8be30dcd Mon Sep 17 00:00:00 2001 From: JohnM <234386938+treuenten@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:12:10 +0800 Subject: [PATCH] Keep bots inside the 18 player slots instead of colliding with corpses A bot took the next unused client id with no upper bound, and the roster only capped bots at MAX_HOST_BOTS (20). The lobby's Enemy and Friendly Bots rules plus a `bot add`, or two `bot add` commands, could therefore make more than 17 bots beside the host. Client ids are also entity numbers, and the corpse ring starts at entity 18 (PLAYER_CORPSE_ENTITY_BASE), so bots 18 and up shared an entity number with a corpse: the net layer rejected the snapshot entity as a centity-number gap, and the end-of-match client data overran its 18 rows (setclientmatchdata IndexOutOfRange). Bots now claim ids 1..17 only, wrapping around to reuse freed ids, and adding stops when every slot is taken. Co-Authored-By: Claude Opus 5.5 --- crates/bots/src/roster.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/bots/src/roster.rs b/crates/bots/src/roster.rs index f5c301c..abe6900 100644 --- a/crates/bots/src/roster.rs +++ b/crates/bots/src/roster.rs @@ -5,6 +5,8 @@ use crate::controller::HostController; pub const MAX_HOST_BOTS: u32 = 20; +const MAX_CLIENT_SLOTS: u32 = playerstate_iw4::PLAYER_CORPSE_ENTITY_BASE as u32; + const BOT_NAMES: [&str; 52] = [ "contextrot", "t.me/contextrot", @@ -250,16 +252,12 @@ impl BotRoster { } fn claim_id(&mut self, taken: &[ClientId]) -> Option { - // Only `bots + taken` ids are spoken for, so one candidate more than - // that always turns up a free one. - let candidates = self - .bots - .len() - .saturating_add(taken.len()) - .saturating_add(1); - for _ in 0..candidates { + for _ in 1..MAX_CLIENT_SLOTS { + if !(1..MAX_CLIENT_SLOTS).contains(&self.next_client) { + self.next_client = 1; + } let id = ClientId(self.next_client); - self.next_client = self.next_client.wrapping_add(1).max(1); + self.next_client += 1; if !taken.contains(&id) && !self.is_bot(id) { return Some(id); }