q3tool/
lib.rs

1//! A Rust library for interacting with ioq3 (Quake 3) based game servers.
2//!
3//! Provides an interface for getting C_VARs and a player list.
4//!
5//! ```no_run
6//! use q3tool::Q3Tool;
7//!
8//! # fn main() {
9//! let q = Q3Tool::new("someserverhost:27960", Some("supersecretpassword".to_owned()));
10//! let server_info = q.get_status().unwrap();
11//!    
12//! // Print all public server c_vars
13//! for (k, v) in server_info.vars() {
14//!     println!("{}: {}", k, v);
15//! }
16//!
17//! // Print a single server c_var
18//! println!("Hostname: {}", server_info.vars().get("sv_hostname").unwrap());
19//!
20//! // Print all players
21//! for player in server_info.players() {
22//!     println!("Name: {}, Score: {}, Ping: {}", player.name(), player.score(), player.ping());
23//! }
24//!
25//! // Send an rcon command
26//! let response = q.rcon("map ut4_casa").unwrap();
27//! # }
28
29pub mod error;
30pub mod player_info;
31pub mod server_info;
32
33use crate::error::Q3Error;
34use crate::server_info::ServerInfo;
35
36use format_bytes::format_bytes;
37use std::net;
38
39#[derive(Debug)]
40pub struct Q3Tool {
41    password: Option<String>,
42    host: String,
43}
44
45impl Q3Tool {
46    /// Creates a new instance of the Q3Tool struct but does not perform any requests
47    pub fn new(host: &str, password: Option<String>) -> Self {
48        Self {
49            host: host.to_owned(),
50            password,
51        }
52    }
53
54    /// Sends a UDP `getstatus` packet to the host and parses the response into a [ServerInfo]
55    pub fn get_status(&self) -> Result<ServerInfo, Q3Error> {
56        let info = self.send_request()?;
57        let info = Self::parse_response(info)?;
58        Ok(info)
59    }
60
61    /// Sends an RCON command to the host.
62    /// Returns the server response as a String.
63    pub fn rcon(&self, command: &str) -> Result<String, Q3Error> {
64        let socket = self.create_socket()?;
65        let mut buffer = [0; 2048];
66
67        let request = format_bytes!(
68            b"\xFF\xFF\xFF\xFFrcon {} {}",
69            self.password.as_ref().unwrap().as_bytes(),
70            command.as_bytes()
71        );
72        socket.send(&request)?;
73        socket.recv(&mut buffer)?;
74
75        let response = String::from_utf8_lossy(&buffer).into_owned();
76
77        Ok(response)
78    }
79
80    fn create_socket(&self) -> Result<net::UdpSocket, Q3Error> {
81        let socket = net::UdpSocket::bind("0.0.0.0:0")?;
82        socket.connect(&self.host)?;
83        Ok(socket)
84    }
85
86    fn send_request(&self) -> Result<String, Q3Error> {
87        let socket = self.create_socket()?;
88        let mut buffer = [0; 2048];
89
90        socket.send(b"\xFF\xFF\xFF\xFFgetstatus")?;
91        socket.recv(&mut buffer)?;
92
93        let info = String::from_utf8_lossy(&buffer).into_owned();
94
95        Ok(info)
96    }
97
98    fn parse_response(raw_info: String) -> Result<ServerInfo, Q3Error> {
99        if let Some((_header, info)) = raw_info.split_once('\n') {
100            Ok(ServerInfo::new(info.to_string())?)
101        } else {
102            Err(Q3Error::InvalidResponse)
103        }
104    }
105}