167 lines
4.8 KiB
Rust
167 lines
4.8 KiB
Rust
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<Cs2Player>, 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<Cs2Player>, 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 status line variants:
|
|
// "players : X humans, Y bots (Z/M max)"
|
|
// "players : X humans, Y bots (Z max)"
|
|
if trimmed.starts_with("players") {
|
|
if let Some(parsed_max) = parse_max_players_from_line(trimmed) {
|
|
max_players = parsed_max;
|
|
}
|
|
}
|
|
|
|
if trimmed.contains("---------players--------") || trimmed.starts_with("# userid") {
|
|
in_player_section = true;
|
|
continue;
|
|
}
|
|
|
|
if in_player_section && (trimmed == "#end" || trimmed.starts_with("---------")) {
|
|
in_player_section = false;
|
|
continue;
|
|
}
|
|
|
|
// Parse player lines for both old and current CS2 status formats.
|
|
if in_player_section {
|
|
if let Some((name, steamid)) = parse_player_line(trimmed) {
|
|
players.push(Cs2Player {
|
|
name,
|
|
steamid,
|
|
score: 0,
|
|
ping: 0,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
(players, max_players)
|
|
}
|
|
|
|
fn parse_max_players_from_line(line: &str) -> Option<u32> {
|
|
let start = line.find('(')?;
|
|
let end = line[start + 1..].find(')')? + start + 1;
|
|
let inside = &line[start + 1..end];
|
|
|
|
inside
|
|
.split(|c: char| !c.is_ascii_digit())
|
|
.filter(|s| !s.is_empty())
|
|
.filter_map(|s| s.parse::<u32>().ok())
|
|
.max()
|
|
}
|
|
|
|
fn parse_player_line(line: &str) -> Option<(String, String)> {
|
|
// Skip table/header rows.
|
|
if line.is_empty()
|
|
|| line.starts_with("id ")
|
|
|| line.contains("userid")
|
|
|| line.contains("steamid")
|
|
|| line.contains("adr name")
|
|
{
|
|
return None;
|
|
}
|
|
|
|
// Legacy format: # 2 "Player" STEAM_...
|
|
if let Some(quote_start) = line.find('"') {
|
|
let quote_end = line[quote_start + 1..].find('"')? + quote_start + 1;
|
|
let name = line[quote_start + 1..quote_end].trim().to_string();
|
|
if name.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let rest = line[quote_end + 1..].trim();
|
|
let steamid = rest.split_whitespace().next()?.to_string();
|
|
if steamid.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
return Some((name, steamid));
|
|
}
|
|
|
|
// Current CS2 format: ... 'PlayerName'
|
|
let quote_end = line.rfind('\'')?;
|
|
let before_end = &line[..quote_end];
|
|
let quote_start = before_end.rfind('\'')?;
|
|
if quote_start >= quote_end {
|
|
return None;
|
|
}
|
|
let name = line[quote_start + 1..quote_end].trim().to_string();
|
|
if name.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// New status output does not include steamid in player rows.
|
|
Some((name, String::new()))
|
|
}
|
|
|
|
#[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, 16);
|
|
assert_eq!(players.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_status_current_cs2_format() {
|
|
let response = r#"Server: Running [0.0.0.0:27015]
|
|
players : 1 humans, 2 bots (0 max) (not hibernating) (unreserved)
|
|
---------players--------
|
|
id time ping loss state rate adr name
|
|
65535 [NoChan] 0 0 challenging 0unknown ''
|
|
1 BOT 0 0 active 0 'Rezan'
|
|
2 00:21 11 0 active 786432 212.154.6.153:57008 'hibna'
|
|
3 BOT 0 0 active 0 'Squad'
|
|
#end
|
|
"#;
|
|
|
|
let (players, max) = parse_status_response(response);
|
|
assert_eq!(max, 0);
|
|
assert_eq!(players.len(), 3);
|
|
assert_eq!(players[0].name, "Rezan");
|
|
assert_eq!(players[1].name, "hibna");
|
|
assert_eq!(players[2].name, "Squad");
|
|
}
|
|
}
|