use anyhow::Result; use tracing::info; use super::rcon::RconClient; /// Player information from CS2 RCON. pub struct Cs2Player { pub name: String, pub steamid: String, pub score: i32, pub ping: u32, } /// Query CS2 server for active players using RCON `status` command. pub async fn get_players(rcon_address: &str, rcon_password: &str) -> Result<(Vec, u32)> { let mut client = RconClient::connect(rcon_address, rcon_password).await?; let response = client.command("status").await?; let (players, max) = parse_status_response(&response); info!( count = players.len(), max = max, "CS2 player list retrieved" ); Ok((players, max)) } fn parse_status_response(response: &str) -> (Vec, u32) { let mut players = Vec::new(); let mut max_players = 0u32; let mut in_player_section = false; for line in response.lines() { let trimmed = line.trim(); // Parse max players from "players : X humans, Y bots (Z/M max)" if trimmed.starts_with("players") && trimmed.contains("max") { if let Some(max_str) = trimmed.split('/').last() { if let Some(num) = max_str.split_whitespace().next() { max_players = num.parse().unwrap_or(0); } } } // Player table header: starts with # if trimmed.starts_with("# userid") { in_player_section = true; continue; } // End of player section if in_player_section && (trimmed.is_empty() || trimmed.starts_with('#')) { if trimmed.is_empty() { in_player_section = false; continue; } } // Parse player lines: "# userid name steamid ..." if in_player_section && trimmed.starts_with('#') { let parts: Vec<&str> = trimmed.splitn(6, char::is_whitespace).collect(); if parts.len() >= 4 { let name = parts.get(2).unwrap_or(&"").trim_matches('"').to_string(); let steamid = parts.get(3).unwrap_or(&"").to_string(); players.push(Cs2Player { name, steamid, score: 0, ping: 0, }); } } } (players, max_players) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_status_basic() { let response = r#"hostname: Test Server version : 2.0.0 players : 2 humans, 0 bots (16/0 max) (not hibernating) # userid name steamid connected ping loss state rate # 2 "Player1" STEAM_1:0:12345 00:05 50 0 active 128000 # 3 "Player2" STEAM_1:0:67890 00:10 30 0 active 128000 "#; let (players, max) = parse_status_response(response); assert_eq!(max, 0); // simplified parser assert_eq!(players.len(), 2); } }