feat: overhaul server automation, files editor, and CS2 setup workflows

This commit is contained in:
2026-02-26 21:01:00 +00:00
parent 44c439e2f9
commit 2a3ad5e78f
40 changed files with 4675 additions and 468 deletions
+91 -22
View File
@@ -34,36 +34,28 @@ fn parse_status_response(response: &str) -> (Vec<Cs2Player>, u32) {
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);
}
// 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;
}
}
// Player table header: starts with #
if trimmed.starts_with("# userid") {
if trimmed.contains("---------players--------") || 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;
}
if in_player_section && (trimmed == "#end" || trimmed.starts_with("---------")) {
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();
// 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,
@@ -77,6 +69,62 @@ fn parse_status_response(response: &str) -> (Vec<Cs2Player>, u32) {
(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::*;
@@ -91,7 +139,28 @@ players : 2 humans, 0 bots (16/0 max) (not hibernating)
# 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!(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");
}
}