Cosmetic changes in output
[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 const SEP_WIDTH: usize = 60;
76
77 impl Display for HLQueryResult {
78 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79 writeln!(f, "{}", "=".repeat(SEP_WIDTH))?;
80 write_datapoint!(f, PAD_WIDTH, "Address:", self.address)?;
81 writeln!(f, "{}", "=".repeat(SEP_WIDTH))?;
82
83 match &self.info {
84 Ok(info) => {
85 writeln!(f, "\n* Server info: *\n")?;
86 write_datapoint!(f, PAD_WIDTH, "Server name:", info.name)?;
87 write_datapoint!(f, PAD_WIDTH, "Game:", info.game)?;
88 write_datapoint!(f, PAD_WIDTH, "Folder:", info.folder)?;
89 write_datapoint!(f, PAD_WIDTH, "Current map:", info.map)?;
90 write_datapoint!(f, PAD_WIDTH, "Protocol version:", info.protocol)?;
91 write_datapoint!(f, PAD_WIDTH, "Steam App ID:", info.app_id)?;
92 write_datapoint!(f, PAD_WIDTH, "Number of players:", format!("{}/{}", info.players, info.max_players))?;
93 write_datapoint!(f, PAD_WIDTH, "Number of bots:", info.bots)?;
94 write_datapoint!(f, PAD_WIDTH, "Server type:", ServerType(&info.server_type))?;
95 write_datapoint!(f, PAD_WIDTH, "Server OS:", ServerOS(&info.server_os))?;
96 write_datapoint!(f, PAD_WIDTH, "Password protected:", if info.visibility { "Private" } else { "Public" })?;
97 write_datapoint!(f, PAD_WIDTH, "VAC enabled:", info.vac)?;
98 write_datapoint!(f, PAD_WIDTH, "The Ship mode:", TheShip(&info.the_ship))?;
99 write_datapoint!(f, PAD_WIDTH, "Server version:", info.version)?;
100 write_datapoint!(f, PAD_WIDTH, "Extra Data Flag:", info.edf)?;
101 write_datapoint!(f, PAD_WIDTH, "Server port:", info.extended_server_info.port.map_or("Unknown".to_string(), |i| i.to_string()))?;
102 write_datapoint!(f, PAD_WIDTH, "Steam ID:", info.extended_server_info.steam_id.map_or("Unknown".to_string(), |i| i.to_string()))?;
103 write_datapoint!(f, PAD_WIDTH, "Keywords:", info.extended_server_info.keywords.clone().unwrap_or("Unknown".to_string()))?;
104 write_datapoint!(f, PAD_WIDTH, "Game ID:", info.extended_server_info.game_id.map_or("Unknown".to_string(), |i| i.to_string()))?;
105 write_datapoint!(f, PAD_WIDTH, "Source TV port:", SourceTVInfo(&info.source_tv))?;
106 },
107 Err(e) =>
108 writeln!(f, "\nFailed to query server info:\t{}", e)?
109 }
110
111 match &self.rules {
112 Ok(rules) => {
113 writeln!(f, "\n\n* Game rules (CVARs): *\n")?;
114 for rule in rules {
115 write_datapoint!(f, PAD_WIDTH, rule.name, format!("\"{}\"", rule.value))?;
116 }
117 }
118 Err(e) =>
119 writeln!(f, "\nFailed to query game rules:\t{}", e)?
120 }
121
122 match &self.players {
123 Ok(players) => {
124 writeln!(f, "\n\n* Players: *")?;
125 writeln!(f, "\t{:>3} players online\n", players.len())?;
126 if players.len() > 0 {
127 writeln!(f, "{:^5} {:^30} {:^4} {:^10} {}", "Index", "Player name", "Score", "Duration", "The Ship")?;
128 for player in players {
129 writeln!(f, "{:>5} {:<30} {:>4} {:>10} {}",
130 player.index,
131 player.name,
132 player.score,
133 player.duration,
134 TheShipPlayer(&player.the_ship))?;
135 }
136 }
137 Ok(())
138 }
139 Err(e) =>
140 writeln!(f, "\nFailed to query players:\t{}", e)
141 }
142 }
143 }
144
145 #[derive(Debug, Serialize)]
146 struct HLQuery {
147 input: String,
148 result: Result<Vec<HLQueryResult>, HLQueryError>
149 }
150
151 impl HLQuery {
152 fn new<S: Into<String>>(input: S, result: Result<Vec<HLQueryResult>, HLQueryError>) -> Self {
153 let input = input.into();
154 Self { input, result }
155 }
156 }
157
158 impl Display for HLQuery {
159 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160 writeln!(f, "{}", "*".repeat(SEP_WIDTH))?;
161 write_datapoint!(f, PAD_WIDTH, "Input:", self.input)?;
162 match &self.result {
163 Ok(v) => {
164 write_datapoint!(f, PAD_WIDTH, "Number of addresses:", format!("{}", v.len()))?;
165 writeln!(f, "{}\n", "*".repeat(SEP_WIDTH))?;
166 for r in v {
167 writeln!(f, "{r}")?;
168 };
169 Ok(())
170 },
171 Err(e) =>
172 writeln!(f, "Failed to execute query:\t{e}\n")
173 }
174 }
175 }
176
177 #[derive(Debug)]
178 enum HLQueryError {
179 IOError(std::io::Error),
180 A2SError(a2s::errors::Error)
181 }
182
183 impl Display for HLQueryError {
184 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
185 match self {
186 IOError(e) => write!(f, "{:?}", e),
187 A2SError(e) => write!(f, "{:?}", e)
188 }
189 }
190 }
191
192 impl Error for HLQueryError {}
193
194 impl Serialize for HLQueryError {
195 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
196 where
197 S: Serializer,
198 {
199 serializer.serialize_str(&format!("{self}"))
200 }
201 }
202
203 impl From<std::io::Error> for HLQueryError {
204 fn from(e: std::io::Error) -> Self {
205 Self::IOError(e)
206 }
207 }
208
209 impl From<a2s::errors::Error> for HLQueryError {
210 fn from(e: a2s::errors::Error) -> Self {
211 Self::A2SError(e)
212 }
213 }
214
215
216 fn main() {
217 let cli = Cli::parse();
218
219 let client = A2SClient::new().unwrap();
220 let query_results: Vec<HLQuery> = cli.addresses.iter()
221 .map(|arg| {
222 let addresses = arg.to_socket_addrs();
223 (arg, addresses)
224 })
225 .map(|lookup_result| match lookup_result {
226 (input, Ok(iter_addr)) => {
227 (input, Ok(iter_addr.filter_map(|sa| match sa {
228 SocketAddr::V4(sa4) => Some(sa4),
229 _ => None
230 })))
231 },
232 (input, Err(e)) => (input, Err(HLQueryError::IOError(e)))
233 })
234 .map(|(input, address_group)| HLQuery::new(input, address_group.map(
235 |addresses| addresses.map(|addr| HLQueryResult::new(&client, addr)).collect())))
236 .collect();
237
238 if cli.json {
239 if cli.pretty {
240 println!("{}", serde_json::to_string_pretty(&query_results).unwrap());
241 }
242 else {
243 println!("{}", serde_json::to_string(&query_results).unwrap());
244 }
245 }
246 else if cli.rust {
247 if cli.pretty {
248 println!("{:#?}", query_results);
249 }
250 else {
251 println!("{:?}", query_results);
252 }
253 }
254 else {
255 for query in query_results {
256 println!("{}", query);
257 }
258 }
259 }