|
1 | | -use std::net::{TcpStream, Shutdown}; |
2 | | -use std::io::{self, Write, BufRead}; |
3 | | -use std::sync::mpsc::{Receiver, TryRecvError, Sender}; |
4 | | -use std::time::Duration; |
5 | | -use std::{thread, time}; |
| 1 | +mod server_subcommands; |
| 2 | +mod local_subcommands; |
| 3 | +mod input_waiter; |
6 | 4 |
|
7 | | -use rw3d_core::{ constants, commands, packet::WitcherPacket, scriptslog }; |
| 5 | +use local_subcommands::{LocalSubcommands, handle_local_subcommand}; |
| 6 | +use server_subcommands::{ServerSubcommands, handle_server_subcommand}; |
8 | 7 | use clap::{Parser, Subcommand}; |
9 | 8 |
|
10 | 9 |
|
11 | 10 | #[derive(Parser)] |
12 | 11 | #[clap(name="Rusty Witcher 3 Debugger", version="0.4")] |
13 | 12 | #[clap(about="A standalone debugging tool for The Witcher 3 written in Rust", long_about=None)] |
14 | 13 | struct Cli { |
15 | | - /// IPv4 address of the machine on which the game is run |
| 14 | + #[clap(flatten)] |
| 15 | + options: CliOptions, |
| 16 | + |
| 17 | + #[clap(subcommand)] |
| 18 | + command: CliCommands, |
| 19 | +} |
| 20 | + |
| 21 | +#[derive(Parser)] |
| 22 | +pub(crate) struct CliOptions { |
| 23 | + /// IPv4 address of the machine on which the game is run. |
16 | 24 | #[clap(long, default_value="127.0.0.1")] |
17 | 25 | ip: String, |
18 | 26 |
|
19 | | - /// Exit the program almost immediately after executing the command without listening to responses coming from the game |
| 27 | + /// Exit the program almost immediately after executing the command without listening to responses coming from the game. |
| 28 | + /// Doesn't apply to scriptslog command. |
20 | 29 | #[clap(long)] |
21 | 30 | no_listen: bool, |
22 | 31 |
|
23 | | - /// Enable verbose printing of packet contents |
| 32 | + /// Enable verbose printing of packet contents. |
24 | 33 | #[clap(long)] |
25 | 34 | verbose: bool, |
26 | 35 |
|
27 | | - /// Execute command immediately without doing short breaks between info messages beforehand |
| 36 | + /// Execute command immediately without doing short breaks between info messages beforehand. |
28 | 37 | #[clap(long)] |
29 | | - no_info_wait: bool, |
| 38 | + no_wait: bool, |
30 | 39 |
|
31 | 40 | /// The maximum amount of milliseconds that program should wait for any game messages until it will automatically exit. |
32 | 41 | /// This setting is ignored if --no-listen is set. |
33 | 42 | /// If set to a negative number will wait indefinitely for user's input. |
| 43 | + /// Doesn't apply to scriptslog command. |
34 | 44 | #[clap(long, short, default_value_t=-1)] |
35 | 45 | response_timeout: i64, |
36 | | - |
37 | | - /// Command to use |
38 | | - #[clap(subcommand)] |
39 | | - command: CliCommands, |
40 | 46 | } |
41 | 47 |
|
42 | | -#[derive(Subcommand, PartialEq, Eq)] |
| 48 | +#[derive(Subcommand)] |
43 | 49 | enum CliCommands { |
44 | | - /// Get the root path to game scripts |
45 | | - Rootpath, |
46 | | - /// Reload game scripts |
47 | | - Reload, |
48 | | - /// Run an exec function in the game |
49 | | - Exec{ |
50 | | - /// Command to be run in the game |
51 | | - cmd: String |
52 | | - }, |
53 | | - /// Get the list of mods installed |
54 | | - Modlist, |
55 | | - /// Get opcode of a script function |
56 | | - Opcode { |
57 | | - /// Name of the function |
58 | | - #[clap(short)] |
59 | | - func_name: String, |
60 | | - /// Name of the class; can be empty |
61 | | - #[clap(short)] |
62 | | - class_name: Option<String> |
63 | | - }, |
64 | | - /// Search for config variables |
65 | | - Varlist { |
66 | | - /// Var section to search; if left empty searches all sections |
67 | | - #[clap(short)] |
68 | | - section: Option<String>, |
69 | | - /// Token that should be included in vars; if left empty searches all variables |
70 | | - #[clap(short)] |
71 | | - name: Option<String> |
72 | | - }, |
73 | | - /// Sets a config variable |
74 | | - Varset { |
75 | | - /// Variable's section |
76 | | - #[clap(short)] |
77 | | - section: String, |
78 | | - /// Variable's name |
79 | | - #[clap(short)] |
80 | | - name: String, |
81 | | - /// Variable's new value |
82 | | - #[clap(short)] |
83 | | - value: String |
84 | | - }, |
85 | | - /// Prints game's script logs onto console |
86 | | - Scriptslog |
| 50 | + /// Subcommands that require connection to game's socket and sending messages to it |
| 51 | + #[clap(flatten)] |
| 52 | + ServerSubcommands(ServerSubcommands), |
| 53 | + |
| 54 | + /// Subcommands that can be executed without connecting to game's socket |
| 55 | + #[clap(flatten)] |
| 56 | + LocalSubcommands(LocalSubcommands) |
87 | 57 | } |
88 | 58 |
|
89 | 59 |
|
90 | 60 | fn main() { |
91 | 61 | let cli = Cli::parse(); |
92 | 62 |
|
93 | | - if cli.command != CliCommands::Scriptslog { |
94 | | - let connection = try_connect(cli.ip.clone(), 5, 1000); |
95 | | - |
96 | | - match connection { |
97 | | - Some(mut stream) => { |
98 | | - if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) } |
99 | | - println!("Successfully connected to the game!"); |
100 | | - |
101 | | - if !cli.no_listen { |
102 | | - if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) } |
103 | | - println!("Setting up listeners..."); |
104 | | - |
105 | | - let listeners = commands::listen_all(); |
106 | | - for l in &listeners { |
107 | | - stream.write( l.to_bytes().as_slice() ).unwrap(); |
108 | | - } |
109 | | - } |
110 | | - |
111 | | - |
112 | | - if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) } |
113 | | - println!("Handling the command..."); |
114 | | - |
115 | | - let p = match cli.command { |
116 | | - CliCommands::Reload => { |
117 | | - commands::scripts_reload() |
118 | | - } |
119 | | - CliCommands::Exec { cmd } => { |
120 | | - commands::scripts_execute(cmd) |
121 | | - } |
122 | | - CliCommands::Rootpath => { |
123 | | - commands::scripts_root_path() |
124 | | - } |
125 | | - CliCommands::Modlist => { |
126 | | - commands::mod_list() |
127 | | - } |
128 | | - CliCommands::Opcode { func_name, class_name } => { |
129 | | - commands::opcode(func_name, class_name) |
130 | | - } |
131 | | - CliCommands::Varlist { section, name } => { |
132 | | - commands::var_list(section, name) |
133 | | - } |
134 | | - CliCommands::Varset { section, name, value } => { |
135 | | - commands::var_set(section, name, value) |
136 | | - } |
137 | | - //FIXME code will need a makeover because of how scriptslog command makes the program behave differently |
138 | | - // this is a temporary measure |
139 | | - CliCommands::Scriptslog => panic!(), |
140 | | - }; |
141 | | - |
142 | | - stream.write( p.to_bytes().as_slice() ).unwrap(); |
143 | | - |
144 | | - |
145 | | - if !cli.no_info_wait || !cli.no_listen { |
146 | | - println!("\nYou can press Enter at any moment to exit the program.\n"); |
147 | | - if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(3000) ) } |
148 | | - } |
149 | | - |
150 | | - if !cli.no_listen { |
151 | | - println!("Game response:\n"); |
152 | | - if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) } |
153 | | - |
154 | | - // Channel to communicate to and from the the reader |
155 | | - let (reader_snd, reader_rcv) = std::sync::mpsc::channel(); |
156 | | - |
157 | | - // This thread is not expected to finish, so we won't assign a handle to it |
158 | | - // Takes reader_snd so it can communicate to the reader thread to stop execution when user presses Enter |
159 | | - std::thread::spawn(move || input_waiter_thread(reader_snd) ); |
160 | | - |
161 | | - // This function can either finish by itself by the means of response timeout |
162 | | - // or be stopped by input waiter thread if that one sends him a signal |
163 | | - read_responses(&mut stream, cli.response_timeout, reader_rcv, cli.verbose); |
164 | | - |
165 | | - } else { |
166 | | - // Wait a little bit to not finish the connection abruptly |
167 | | - thread::sleep( time::Duration::from_millis(500) ); |
168 | | - } |
169 | | - |
170 | | - if let Err(e) = stream.shutdown(Shutdown::Both) { |
171 | | - println!("{}", e); |
172 | | - } |
173 | | - |
174 | | - } |
175 | | - None => { |
176 | | - println!("Failed to connect to the game on address {}", cli.ip); |
177 | | - } |
178 | | - } |
179 | | - |
180 | | - } else { |
181 | | - if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) } |
182 | | - println!("Handling the command..."); |
183 | | - |
184 | | - let (logger_snd, logger_rcv) = std::sync::mpsc::channel(); |
185 | | - |
186 | | - std::thread::spawn(move || input_waiter_thread(logger_snd) ); |
187 | | - |
188 | | - println!("\nYou can press Enter at any moment to exit the program.\n"); |
189 | | - if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(3000) ) } |
190 | | - |
191 | | - if let Some(err) = scriptslog::read_from_scriptslog(|s| print!("{}", s), 1000, logger_rcv) { |
192 | | - println!("{}", err); |
193 | | - } |
194 | | - } |
195 | | -} |
196 | | - |
197 | | - |
198 | | - |
199 | | -fn try_connect(ip: String, max_tries: u8, tries_delay_ms: u64) -> Option<TcpStream> { |
200 | | - let mut tries = max_tries; |
201 | | - |
202 | | - while tries > 0 { |
203 | | - println!("Connecting to the game..."); |
204 | | - |
205 | | - match TcpStream::connect(ip.clone() + ":" + constants::GAME_PORT) { |
206 | | - Ok(conn) => { |
207 | | - return Some(conn); |
208 | | - } |
209 | | - Err(e) => { |
210 | | - println!("{}", e); |
211 | | - } |
212 | | - } |
213 | | - |
214 | | - tries -= 1; |
215 | | - thread::sleep( time::Duration::from_millis(tries_delay_ms) ); |
216 | | - } |
217 | | - |
218 | | - None |
219 | | -} |
220 | | - |
221 | | -fn input_waiter_thread(sender: Sender<()>) { |
222 | | - let mut line = String::new(); |
223 | | - io::stdin().lock().read_line(&mut line).unwrap(); |
224 | | - sender.send(()).unwrap(); |
225 | | -} |
226 | | - |
227 | | -fn read_responses(stream: &mut TcpStream, response_timeout: i64, cancel_token: Receiver<()>, verbose_print: bool ) { |
228 | | - let mut peek_buffer = [0u8;6]; |
229 | | - let mut packet_available: bool; |
230 | | - let mut response_wait_elapsed: i64 = 0; |
231 | | - |
232 | | - const READ_TIMEOUT: i64 = 1000; |
233 | | - // Timeout is set so that the peek operation won't block the thread indefinitely after it runs out of data to read |
234 | | - stream.set_read_timeout( Some(Duration::from_millis(READ_TIMEOUT as u64)) ).unwrap(); |
235 | | - |
236 | | - loop { |
237 | | - // test if the thread has been ordered to stop |
238 | | - match cancel_token.try_recv() { |
239 | | - Ok(_) | Err(TryRecvError::Disconnected) => { |
240 | | - break; |
241 | | - } |
242 | | - Err(TryRecvError::Empty) => {} |
243 | | - } |
244 | | - |
245 | | - // Test if there are packets available to be read from stream |
246 | | - // This can block up to the amount specified with set_read_timeout |
247 | | - match stream.peek(&mut peek_buffer) { |
248 | | - Ok(size) => { |
249 | | - packet_available = size > 0; |
250 | | - } |
251 | | - Err(_) => { |
252 | | - packet_available = false; |
253 | | - } |
254 | | - } |
255 | | - |
256 | | - if packet_available { |
257 | | - match WitcherPacket::from_stream(stream) { |
258 | | - Ok(packet) => { |
259 | | - if verbose_print { |
260 | | - println!("{:?}", packet); |
261 | | - } else { |
262 | | - println!("{}", packet); |
263 | | - } |
264 | | - } |
265 | | - Err(e) => { |
266 | | - println!("{}", e); |
267 | | - break; |
268 | | - } |
269 | | - } |
270 | | - |
271 | | - response_wait_elapsed = 0; |
272 | | - |
273 | | - } else { |
274 | | - // if not available it means peek probably waited TIMEOUT millis before it returned |
275 | | - response_wait_elapsed += READ_TIMEOUT; |
276 | | - |
277 | | - if response_timeout >= 0 && response_wait_elapsed >= response_timeout { |
278 | | - println!("\nGame response timeout reached."); |
279 | | - break; |
280 | | - } |
281 | | - } |
| 63 | + match cli.command { |
| 64 | + CliCommands::ServerSubcommands(c) => handle_server_subcommand(c, cli.options), |
| 65 | + CliCommands::LocalSubcommands(c) => handle_local_subcommand(c, cli.options), |
282 | 66 | } |
283 | 67 | } |
0 commit comments