1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! A Rust library for interacting with ioq3 (Quake 3) based game servers.
//!
//! Provides an interface for getting C_VARs and a player list.
//!
//! ```no_run
//! use q3tool::Q3Tool;
//!
//! # fn main() {
//! let q = Q3Tool::new("someserverhost:27960", Some("supersecretpassword"));
//! let server_info = q.get_status().unwrap();
//!    
//! // Print all public server c_vars
//! for (k, v) in server_info.vars() {
//!     println!("{}: {}", k, v);
//! }
//!
//! // Print a single server c_var
//! println!("Hostname: {}", server_info.vars().get("sv_hostname").unwrap());
//!
//! // Print all players
//! for player in server_info.players() {
//!     println!("Name: {}, Score: {}, Ping: {}", player.name(), player.score(), player.ping());
//! }
//!
//! // Send an rcon command
//! let response = q.rcon("map ut4_casa").unwrap();
//! # }

pub mod error;
pub mod player_info;
pub mod server_info;

use crate::error::Q3Error;
use crate::server_info::ServerInfo;

use format_bytes::format_bytes;
use std::net;

#[derive(Debug)]
pub struct Q3Tool {
    password: Option<String>,
    host: String,
}

impl Q3Tool {
    /// Creates a new instance of the Q3Tool struct but does not perform any requests
    pub fn new(host: &str, password: Option<String>) -> Self {
        Self {
            host: host.to_owned(),
            password,
        }
    }

    /// Sends a UDP `getstatus` packet to the host and parses the response into a [ServerInfo]
    pub fn get_status(&self) -> Result<ServerInfo, Q3Error> {
        let info = self.send_request()?;
        let info = Self::parse_response(info)?;
        Ok(info)
    }

    /// Sends an RCON command to the host.
    /// Returns the server response as a String.
    pub fn rcon(&self, command: &str) -> Result<String, Q3Error> {
        let socket = self.create_socket()?;
        let mut buffer = [0; 2048];

        let request = format_bytes!(
            b"\xFF\xFF\xFF\xFFrcon {} {}",
            self.password.as_ref().unwrap().as_bytes(),
            command.as_bytes()
        );
        socket.send(&request)?;
        socket.recv(&mut buffer)?;

        let response = String::from_utf8_lossy(&buffer).into_owned();

        Ok(response)
    }

    fn create_socket(&self) -> Result<net::UdpSocket, Q3Error> {
        let socket = net::UdpSocket::bind("0.0.0.0:0")?;
        socket.connect(&self.host)?;
        Ok(socket)
    }

    fn send_request(&self) -> Result<String, Q3Error> {
        let socket = self.create_socket()?;
        let mut buffer = [0; 2048];

        socket.send(b"\xFF\xFF\xFF\xFFgetstatus")?;
        socket.recv(&mut buffer)?;

        let info = String::from_utf8_lossy(&buffer).into_owned();

        Ok(info)
    }

    fn parse_response(raw_info: String) -> Result<ServerInfo, Q3Error> {
        if let Some((_header, info)) = raw_info.split_once('\n') {
            Ok(ServerInfo::new(info.to_string())?)
        } else {
            Err(Q3Error::InvalidResponse)
        }
    }
}