6e87bad1633f1fbe0a378ecaa9914858d1d1417d
[hlquery.git] / src / main.rs
1 use std::fmt;
2 use std::fmt::Display;
3 use std::error::Error;
4 use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs};
5 use clap::Parser;
6 use serde::{Serialize, Serializer};
7 use a2s::A2SClient;
8 use crate::HLQueryError::{IOError,A2SError};
9
10 macro_rules! write_datapoint {
11 ($formatter:expr,$pad:expr,$caption:expr,$var:expr) => {
12 writeln!($formatter, "{:<pad$} {}", $caption, $var, pad = $pad)
13 }
14 }
15
16 macro_rules! implement_displaydebug {
17 ($foreigntype:path,$localtype:ident) => {
18 struct $localtype<'a>(&'a $foreigntype);
19
20 impl Display for $localtype<'_> {
21 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22 write!(f, "{:?}", self.0)
23 }
24 }
25 }
26 }
27
28 implement_displaydebug!(a2s::info::ServerOS, ServerOS);
29 implement_displaydebug!(a2s::info::ServerType, ServerType);
30 implement_displaydebug!(Option<a2s::info::TheShip>, TheShip);
31 implement_displaydebug!(Option<a2s::info::SourceTVInfo>, SourceTVInfo);
32 implement_displaydebug!(Option<a2s::players::TheShipPlayer>, TheShipPlayer);
33
34 #[derive(Parser)]
35 #[command(name = "HLQuery")]
36 #[command(author = "MegaBrutal")]
37 #[command(version)]
38 #[command(about = "Query Half-Life servers", long_about = None)]
39 struct Cli {
40 /// Print output in JSON format
41 #[arg(short, long)]
42 json: bool,
43
44 /// Print output in Rust debug format
45 #[arg(short, long)]
46 rust: bool,
47
48 /// Pretty-print JSON or Rust objects
49 #[arg(short, long)]
50 pretty: bool,
51
52 addresses: Vec<String>
53 }
54
55 #[derive(Debug, Serialize)]
56 struct HLQueryResult {
57 address: SocketAddrV4,
58 info: Result<a2s::info::Info, HLQueryError>,
59 rules: Result<Vec<a2s::rules::Rule>, HLQueryError>,
60 players: Result<Vec<a2s::players::Player>, HLQueryError>
61 }
62
63 impl HLQueryResult {
64 fn new(a2s_client: &A2SClient, server: SocketAddrV4) -> Self {
65 Self {
66 address: server,
67 info: a2s_client.info(server).map_err(From::from),
68 rules: a2s_client.rules(server).map_err(From::from),
69 players: a2s_client.players(server).map_err(From::from)
70 }
71 }
72 }
73
74 const PAD_WIDTH: usize = 25;
75
76 impl Display for HLQueryResult {
77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78 write_datapoint!(f, PAD_WIDTH, "Address:", self.address)?;
79 match &self.info {
80 Ok(info) => {
81 writeln!(f, "\nServer info:")?;
82 write_datapoint!(f, PAD_WIDTH, "Server name:", info.name)?;
83 write_datapoint!(f, PAD_WIDTH, "Game:", info.game)?;
84 write_datapoint!(f, PAD_WIDTH, "Folder:", info.folder)?;
85 write_datapoint!(f, PAD_WIDTH, "Current map:", info.map)?;
86 write_datapoint!(f, PAD_WIDTH, "Protocol version:", info.protocol)?;
87 write_datapoint!(f, PAD_WIDTH, "Steam App ID:", info.app_id)?;
88 write_datapoint!(f, PAD_WIDTH, "Number of players:", format!("{}/{}", info.players, info.max_players))?;
89 write_datapoint!(f, PAD_WIDTH, "Number of bots:", info.bots)?;
90 write_datapoint!(f, PAD_WIDTH, "Server type:", ServerType(&info.server_type))?;
91 write_datapoint!(f, PAD_WIDTH, "Server OS:", ServerOS(&info.server_os))?;
92 write_datapoint!(f, PAD_WIDTH, "Password protected:", if info.visibility { "Private" } else { "Public" })?;
93 write_datapoint!(f, PAD_WIDTH, "VAC enabled:", info.vac)?;
94 write_datapoint!(f, PAD_WIDTH, "The Ship mode:", TheShip(&info.the_ship))?;
95 write_datapoint!(f, PAD_WIDTH, "Server version:", info.version)?;
96 write_datapoint!(f, PAD_WIDTH, "Extra Data Flag:", info.edf)?;
97 write_datapoint!(f, PAD_WIDTH, "Server port:", info.extended_server_info.port.map_or("Unknown".to_string(), |i| i.to_string()))?;
98 write_datapoint!(f, PAD_WIDTH, "Steam ID:", info.extended_server_info.steam_id.map_or("Unknown".to_string(), |i| i.to_string()))?;
99 write_datapoint!(f, PAD_WIDTH, "Keywords:", info.extended_server_info.keywords.clone().unwrap_or("Unknown".to_string()))?;
100 write_datapoint!(f, PAD_WIDTH, "Game ID:", info.extended_server_info.game_id.map_or("Unknown".to_string(), |i| i.to_string()))?;
101 write_datapoint!(f, PAD_WIDTH, "Source TV port:", SourceTVInfo(&info.source_tv))?;
102 },
103 Err(e) =>
104 writeln!(f, "Failed to query server info:\t{}", e)?
105 }
106
107 match &self.rules {
108 Ok(rules) => {
109 writeln!(f, "\nGame rules (CVARs):")?;
110 for rule in rules {
111 write_datapoint!(f, PAD_WIDTH, rule.name, format!("\"{}\"", rule.value))?;
112 }
113 }
114 Err(e) =>
115 writeln!(f, "Failed to query game rules:\t{}", e)?
116 }
117
118 match &self.players {
119 Ok(players) => {
120 writeln!(f, "\nPlayers:")?;
121 writeln!(f, "\t{:>3} players online.", players.len())?;
122 for player in players {
123 writeln!(f, "{}: {:<30} {:>4} {:>10} {}",
124 player.index,
125 player.name,
126 player.score,
127 player.duration,
128 TheShipPlayer(&player.the_ship))?;
129 }
130 Ok(())
131 }
132 Err(e) =>
133 writeln!(f, "Failed to query players:\t{}", e)
134 }
135 }
136 }
137
138 #[derive(Debug, Serialize)]
139 struct HLQuery {
140 input: String,
141 result: Result<Vec<HLQueryResult>, HLQueryError>
142 }
143
144 impl HLQuery {
145 fn new<S: Into<String>>(input: S, result: Result<Vec<HLQueryResult>, HLQueryError>) -> Self {
146 let input = input.into();
147 Self { input, result }
148 }
149 }
150
151 impl Display for HLQuery {
152 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153 write_datapoint!(f, PAD_WIDTH, "Input:", self.input)?;
154 match &self.result {
155 Ok(v) => {
156 write_datapoint!(f, PAD_WIDTH, "Number of addresses:", format!("{}\n", v.len()))?;
157 for r in v {
158 writeln!(f, "{r}\n")?;
159 };
160 Ok(())
161 },
162 Err(e) =>
163 writeln!(f, "Failed to execute query:\t{e}\n")
164 }
165 }
166 }
167
168 #[derive(Debug)]
169 enum HLQueryError {
170 IOError(std::io::Error),
171 A2SError(a2s::errors::Error)
172 }
173
174 impl Display for HLQueryError {
175 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176 match self {
177 IOError(e) => write!(f, "{:?}", e),
178 A2SError(e) => write!(f, "{:?}", e)
179 }
180 }
181 }
182
183 impl Error for HLQueryError {}
184
185 impl Serialize for HLQueryError {
186 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
187 where
188 S: Serializer,
189 {
190 serializer.serialize_str(&format!("{self}"))
191 }
192 }
193
194 impl From<std::io::Error> for HLQueryError {
195 fn from(e: std::io::Error) -> Self {
196 Self::IOError(e)
197 }
198 }
199
200 impl From<a2s::errors::Error> for HLQueryError {
201 fn from(e: a2s::errors::Error) -> Self {
202 Self::A2SError(e)
203 }
204 }
205
206
207 fn main() {
208 let cli = Cli::parse();
209
210 let client = A2SClient::new().unwrap();
211 let query_results: Vec<HLQuery> = cli.addresses.iter()
212 .map(|arg| {
213 let addresses = arg.to_socket_addrs();
214 (arg, addresses)
215 })
216 .map(|lookup_result| match lookup_result {
217 (input, Ok(iter_addr)) => {
218 (input, Ok(iter_addr.filter_map(|sa| match sa {
219 SocketAddr::V4(sa4) => Some(sa4),
220 _ => None
221 })))
222 },
223 (input, Err(e)) => (input, Err(HLQueryError::IOError(e)))
224 })
225 .map(|(input, address_group)| HLQuery::new(input, address_group.map(
226 |addresses| addresses.map(|addr| HLQueryResult::new(&client, addr)).collect())))
227 .collect();
228
229 if cli.json {
230 if cli.pretty {
231 println!("{}", serde_json::to_string_pretty(&query_results).unwrap());
232 }
233 else {
234 println!("{}", serde_json::to_string(&query_results).unwrap());
235 }
236 }
237 else if cli.rust {
238 if cli.pretty {
239 println!("{:#?}", query_results);
240 }
241 else {
242 println!("{:?}", query_results);
243 }
244 }
245 else {
246 for query in query_results {
247 println!("{}", query);
248 }
249 }
250 }