|
1 use argparse::{ArgumentParser, Store}; |
|
2 use ini::Ini; |
|
3 use netbuf::Buf; |
|
4 use log::{debug, warn, info}; |
|
5 use std::{ |
|
6 io::Write, |
|
7 net::TcpStream, |
|
8 process::Command, |
|
9 str::FromStr |
|
10 }; |
|
11 |
|
12 type CheckError = Box<std::error::Error>; |
|
13 |
|
14 fn extract_packet(buf: &mut Buf) -> Option<netbuf::Buf> { |
|
15 let packet_end = (&buf[..]).windows(2).position(|window| window == b"\n\n")?; |
|
16 |
|
17 let mut tail = buf.split_off(packet_end); |
|
18 |
|
19 std::mem::swap(&mut tail, buf); |
|
20 |
|
21 buf.consume(2); |
|
22 |
|
23 Some(tail) |
|
24 } |
|
25 |
|
26 fn check(executable: &str, data_prefix: &str, buffer: &[u8]) -> Result<Vec<Vec<u8>>, CheckError> { |
|
27 let mut replay = tempfile::NamedTempFile::new()?; |
|
28 |
|
29 for line in buffer.split(|b| *b == '\n' as u8) { |
|
30 replay.write(&base64::decode(line)?)?; |
|
31 } |
|
32 |
|
33 let temp_file_path = replay.path(); |
|
34 |
|
35 let mut home_dir = dirs::home_dir().unwrap(); |
|
36 home_dir.push(".hedgewars"); |
|
37 |
|
38 debug!("Checking replay in {}", temp_file_path.to_string_lossy()); |
|
39 |
|
40 let output = Command::new(executable) |
|
41 .arg("--user-prefix") |
|
42 .arg(&home_dir) |
|
43 .arg("--prefix") |
|
44 .arg(data_prefix) |
|
45 .arg("--nomusic") |
|
46 .arg("--nosound") |
|
47 .arg("--stats-only") |
|
48 .arg(temp_file_path) |
|
49 .output()?; |
|
50 |
|
51 let mut result = Vec::new(); |
|
52 |
|
53 let mut engine_lines = output |
|
54 .stderr |
|
55 .split(|b| *b == '\n' as u8) |
|
56 .skip_while(|l| *l != b"WINNERS" && *l != b"DRAW"); |
|
57 |
|
58 loop { |
|
59 match engine_lines.next() { |
|
60 Some(b"DRAW") => result.push(b"DRAW".to_vec()), |
|
61 Some(b"WINNERS") => { |
|
62 result.push(b"WINNERS".to_vec()); |
|
63 let winners = engine_lines.next().unwrap(); |
|
64 let winners_num = u32::from_str(&String::from_utf8(winners.to_vec())?)?; |
|
65 result.push(winners.to_vec()); |
|
66 |
|
67 for _i in 0..winners_num { |
|
68 result.push(engine_lines.next().unwrap().to_vec()); |
|
69 } |
|
70 } |
|
71 Some(b"GHOST_POINTS") => { |
|
72 result.push(b"GHOST_POINTS".to_vec()); |
|
73 let points = engine_lines.next().unwrap(); |
|
74 let points_num = u32::from_str(&String::from_utf8(points.to_vec())?)? * 2; |
|
75 result.push(points.to_vec()); |
|
76 |
|
77 for _i in 0..points_num { |
|
78 result.push(engine_lines.next().unwrap().to_vec()); |
|
79 } |
|
80 } |
|
81 Some(b"ACHIEVEMENT") => { |
|
82 result.push(b"ACHIEVEMENT".to_vec()); |
|
83 for _i in 0..4 { |
|
84 result.push(engine_lines.next().unwrap().to_vec()); |
|
85 } |
|
86 } |
|
87 _ => break, |
|
88 } |
|
89 } |
|
90 |
|
91 if result.len() > 0 { |
|
92 Ok(result) |
|
93 } else { |
|
94 Err("no data from engine".into()) |
|
95 } |
|
96 } |
|
97 |
|
98 fn connect_and_run( |
|
99 username: &str, |
|
100 password: &str, |
|
101 protocol_number: u32, |
|
102 executable: &str, |
|
103 data_prefix: &str, |
|
104 ) -> Result<(), CheckError> { |
|
105 info!("Connecting..."); |
|
106 |
|
107 let mut stream = TcpStream::connect("hedgewars.org:46631")?; |
|
108 stream.set_nonblocking(false)?; |
|
109 |
|
110 let mut buf = Buf::new(); |
|
111 |
|
112 loop { |
|
113 buf.read_from(&mut stream)?; |
|
114 |
|
115 while let Some(msg) = extract_packet(&mut buf) { |
|
116 if msg[..].starts_with(b"CONNECTED") { |
|
117 info!("Connected"); |
|
118 let p = format!( |
|
119 "CHECKER\n{}\n{}\n{}\n\n", |
|
120 protocol_number, username, password |
|
121 ); |
|
122 stream.write(p.as_bytes())?; |
|
123 } else if msg[..].starts_with(b"PING") { |
|
124 stream.write(b"PONG\n\n")?; |
|
125 } else if msg[..].starts_with(b"LOGONPASSED") { |
|
126 info!("Logged in"); |
|
127 stream.write(b"READY\n\n")?; |
|
128 } else if msg[..].starts_with(b"REPLAY") { |
|
129 info!("Got a replay"); |
|
130 match check(executable, data_prefix, &msg[7..]) { |
|
131 Ok(result) => { |
|
132 info!("Checked"); |
|
133 debug!( |
|
134 "Check result: [{}]", |
|
135 String::from_utf8_lossy(&result.join(&(',' as u8))) |
|
136 ); |
|
137 |
|
138 stream.write(b"CHECKED\nOK\n")?; |
|
139 stream.write(&result.join(&('\n' as u8)))?; |
|
140 stream.write(b"\n\nREADY\n\n")?; |
|
141 } |
|
142 Err(e) => { |
|
143 info!("Check failed: {:?}", e); |
|
144 stream.write(b"CHECKED\nFAIL\nerror\n\nREADY\n\n")?; |
|
145 } |
|
146 } |
|
147 } else if msg[..].starts_with(b"BYE") { |
|
148 warn!("Received BYE: {}", String::from_utf8_lossy(&msg[..])); |
|
149 return Ok(()); |
|
150 } else if msg[..].starts_with(b"CHAT") { |
|
151 let body = String::from_utf8_lossy(&msg[5..]); |
|
152 let mut l = body.lines(); |
|
153 info!("Chat [{}]: {}", l.next().unwrap(), l.next().unwrap()); |
|
154 } else if msg[..].starts_with(b"ROOM") { |
|
155 let body = String::from_utf8_lossy(&msg[5..]); |
|
156 let mut l = body.lines(); |
|
157 if let Some(action) = l.next() { |
|
158 if action == "ADD" { |
|
159 info!("Room added: {}", l.skip(1).next().unwrap()); |
|
160 } |
|
161 } |
|
162 } else if msg[..].starts_with(b"ERROR") { |
|
163 warn!("Received ERROR: {}", String::from_utf8_lossy(&msg[..])); |
|
164 return Ok(()); |
|
165 } else { |
|
166 warn!( |
|
167 "Unknown protocol command: {}", |
|
168 String::from_utf8_lossy(&msg[..]) |
|
169 ) |
|
170 } |
|
171 } |
|
172 } |
|
173 } |
|
174 |
|
175 fn get_protocol_number(executable: &str) -> std::io::Result<u32> { |
|
176 let output = Command::new(executable).arg("--protocol").output()?; |
|
177 |
|
178 Ok(u32::from_str(&String::from_utf8(output.stdout).unwrap().trim()).unwrap_or(55)) |
|
179 } |
|
180 |
|
181 fn main() { |
|
182 stderrlog::new() |
|
183 .verbosity(3) |
|
184 .timestamp(stderrlog::Timestamp::Second) |
|
185 .module(module_path!()) |
|
186 .init() |
|
187 .unwrap(); |
|
188 |
|
189 let mut frontend_settings = dirs::home_dir().unwrap(); |
|
190 frontend_settings.push(".hedgewars/settings.ini"); |
|
191 |
|
192 let i = Ini::load_from_file(frontend_settings.to_str().unwrap()).unwrap(); |
|
193 let username = i.get_from(Some("net"), "nick").unwrap(); |
|
194 let password = i.get_from(Some("net"), "passwordhash").unwrap(); |
|
195 |
|
196 let mut exe = "/usr/local/bin/hwengine".to_string(); |
|
197 let mut prefix = "/usr/local/share/hedgewars/Data".to_string(); |
|
198 { |
|
199 let mut ap = ArgumentParser::new(); |
|
200 ap.set_description("Game replay checker for hedgewars."); |
|
201 ap.refer(&mut exe) |
|
202 .add_option(&["--exe"], Store, "Path to hwengine executable"); |
|
203 ap.refer(&mut prefix) |
|
204 .add_option(&["--prefix"], Store, "Path main Data dir"); |
|
205 ap.parse_args_or_exit(); |
|
206 } |
|
207 |
|
208 info!("Executable: {}", exe); |
|
209 info!("Data dir: {}", prefix); |
|
210 |
|
211 let protocol_number = get_protocol_number(&exe.as_str()).unwrap_or_default(); |
|
212 |
|
213 info!("Using protocol number {}", protocol_number); |
|
214 |
|
215 connect_and_run(&username, &password, protocol_number, &exe, &prefix).unwrap(); |
|
216 } |
|
217 |
|
218 #[cfg(test)] |
|
219 #[test] |
|
220 fn test() { |
|
221 let mut buf = Buf::new(); |
|
222 buf.extend(b"Hell"); |
|
223 if let Some(_) = extract_packet(&mut buf) { |
|
224 assert!(false) |
|
225 } |
|
226 |
|
227 buf.extend(b"o\n\nWorld"); |
|
228 |
|
229 let packet2 = extract_packet(&mut buf).unwrap(); |
|
230 assert_eq!(&buf[..], b"World"); |
|
231 assert_eq!(&packet2[..], b"Hello"); |
|
232 |
|
233 if let Some(_) = extract_packet(&mut buf) { |
|
234 assert!(false) |
|
235 } |
|
236 } |