Skip to content

Commit c48bbb3

Browse files
feat: scriptslog reader command
1 parent 866b8cb commit c48bbb3

5 files changed

Lines changed: 190 additions & 78 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rusty_witcher3_debugger"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
edition = "2021"
55

66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -16,4 +16,5 @@ name = "rw3d_cli"
1616
path = "src/cli/cli.rs"
1717

1818
[dependencies]
19-
clap = { version = "^3.0", features = ["derive"] }
19+
clap = { version = "^3.0", features = ["derive"] }
20+
directories = "4.0"

src/cli/cli.rs

Lines changed: 96 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ use std::sync::mpsc::{Receiver, TryRecvError, Sender};
44
use std::time::Duration;
55
use std::{thread, time};
66

7-
use rw3d_core::{ constants, commands, packet::WitcherPacket };
7+
use rw3d_core::{ constants, commands, packet::WitcherPacket, scriptslog };
88
use clap::{Parser, Subcommand};
99

1010

1111
#[derive(Parser)]
12-
#[clap(name="Rusty Witcher 3 Debugger", version="0.3")]
12+
#[clap(name="Rusty Witcher 3 Debugger", version="0.4")]
1313
#[clap(about="A standalone debugging tool for The Witcher 3 written in Rust", long_about=None)]
1414
struct Cli {
1515
/// IPv4 address of the machine on which the game is run
@@ -39,7 +39,7 @@ struct Cli {
3939
command: CliCommands,
4040
}
4141

42-
#[derive(Subcommand)]
42+
#[derive(Subcommand, PartialEq, Eq)]
4343
enum CliCommands {
4444
/// Get the root path to game scripts
4545
Rootpath,
@@ -81,93 +81,115 @@ enum CliCommands {
8181
/// Variable's new value
8282
#[clap(short)]
8383
value: String
84-
}
84+
},
85+
/// Prints game's script logs onto console
86+
Scriptslog
8587
}
8688

8789

8890
fn main() {
8991
let cli = Cli::parse();
9092

91-
let connection = try_connect(cli.ip.clone(), 5, 1000);
92-
93-
match connection {
94-
Some(mut stream) => {
95-
if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) }
96-
println!("Successfully connected to the game!");
97-
98-
if !cli.no_listen {
93+
if cli.command != CliCommands::Scriptslog {
94+
let connection = try_connect(cli.ip.clone(), 5, 1000);
95+
96+
match connection {
97+
Some(mut stream) => {
9998
if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) }
100-
println!("Setting up listeners...");
101-
102-
let listeners = commands::listen_all();
103-
for l in &listeners {
104-
stream.write( l.to_bytes().as_slice() ).unwrap();
105-
}
106-
}
107-
108-
109-
if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) }
110-
println!("Handling the command...");
111-
112-
let p = match cli.command {
113-
CliCommands::Reload => {
114-
commands::scripts_reload()
115-
}
116-
CliCommands::Exec { cmd } => {
117-
commands::scripts_execute(cmd)
118-
}
119-
CliCommands::Rootpath => {
120-
commands::scripts_root_path()
121-
}
122-
CliCommands::Modlist => {
123-
commands::mod_list()
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+
}
124109
}
125-
CliCommands::Opcode { func_name, class_name } => {
126-
commands::opcode(func_name, class_name)
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) ) }
127148
}
128-
CliCommands::Varlist { section, name } => {
129-
commands::var_list(section, name)
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) );
130168
}
131-
CliCommands::Varset { section, name, value } => {
132-
commands::var_set(section, name, value)
169+
170+
if let Err(e) = stream.shutdown(Shutdown::Both) {
171+
println!("{}", e);
133172
}
134-
};
135-
136-
stream.write( p.to_bytes().as_slice() ).unwrap();
137-
138-
139-
if !cli.no_info_wait || !cli.no_listen {
140-
println!("\nYou can press Enter at any moment to exit the program.\n");
141-
if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(3000) ) }
173+
174+
}
175+
None => {
176+
println!("Failed to connect to the game on address {}", cli.ip);
142177
}
178+
}
143179

144-
if !cli.no_listen {
145-
println!("Game response:\n");
146-
if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) }
147-
148-
// Channel to communicate to and from the the reader
149-
let (reader_snd, reader_rcv) = std::sync::mpsc::channel();
150-
151-
// This thread is not expected to finish, so we won't assign a handle to it
152-
// Takes reader_snd so it can communicate to the reader thread to stop execution when user presses Enter
153-
std::thread::spawn(move || input_waiter_thread(reader_snd) );
154-
155-
// This function can either finish by itself by the means of response timeout
156-
// or be stopped by input waiter thread if that one sends him a signal
157-
read_responses(&mut stream, cli.response_timeout, reader_rcv, cli.verbose);
180+
} else {
181+
if !cli.no_info_wait { thread::sleep( time::Duration::from_millis(1000) ) }
182+
println!("Handling the command...");
158183

159-
} else {
160-
// Wait a little bit to not finish the connection abruptly
161-
thread::sleep( time::Duration::from_millis(500) );
162-
}
184+
let (logger_snd, logger_rcv) = std::sync::mpsc::channel();
163185

164-
if let Err(e) = stream.shutdown(Shutdown::Both) {
165-
println!("{}", e);
166-
}
186+
std::thread::spawn(move || input_waiter_thread(logger_snd) );
167187

168-
}
169-
None => {
170-
println!("Failed to connect to the game on address {}", cli.ip);
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);
171193
}
172194
}
173195
}

src/core/constants.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ pub const TYPE_INT32: [u8; 2] = [0x81, 0x32];
3535
pub const TYPE_UINT32: [u8; 2] = [0x71, 0x32];
3636
pub const TYPE_INT64: [u8; 2] = [0x81, 0x64];
3737
pub const TYPE_STRING_UTF8: [u8; 2] = [0xAC, 0x08];
38-
pub const TYPE_STRING_UTF16: [u8; 2] = [0x9C, 0x16];
38+
pub const TYPE_STRING_UTF16: [u8; 2] = [0x9C, 0x16];
39+
40+
41+
pub const SCRIPTSLOG_PATH_IN_DOCS: &str = "The Witcher 3\\scriptslog.txt";

src/core/core.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ mod tests;
33
pub mod constants;
44
mod packet_data;
55
pub mod packet;
6-
pub mod commands;
6+
pub mod commands;
7+
pub mod scriptslog;

src/core/scriptslog.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use std::{fs::{File, OpenOptions}, sync::mpsc::{Receiver, TryRecvError}, io::{BufReader, Seek, SeekFrom, Read}, time::Duration};
2+
use directories::UserDirs;
3+
4+
use crate::constants;
5+
6+
7+
/// A function that will keep reading from Witcher 3's script log file until it gets stopped through cancel token or stumbles upon some error.
8+
/// In case of error will return Some with that error message, otherwise will return None.
9+
/// For `printer` parameter you can pass any function or closure that you want to print log text with,
10+
/// the string passed to said printer will consist of one or more lines of text.
11+
pub fn read_from_scriptslog<P>( printer: P, refresh_time_millis: u64, cancel_token: Receiver<()> ) -> Option<String>
12+
where P: Fn(&String) -> () {
13+
match scriptslog_file() {
14+
Ok(file) => {
15+
let mut reader = BufReader::new(&file);
16+
// start from the end of the file
17+
let mut last_pos = reader.seek( SeekFrom::End(0) ).unwrap();
18+
let mut text = String::new();
19+
20+
loop {
21+
match cancel_token.try_recv() {
22+
Ok(_) | Err(TryRecvError::Disconnected) => {
23+
break;
24+
}
25+
Err(_) => {}
26+
}
27+
28+
let filesize = file.metadata().unwrap().len();
29+
30+
// if the file has been cleared since we've opened it we should go back to its beginning
31+
if last_pos > filesize {
32+
last_pos = reader.seek( SeekFrom::Start(0) ).unwrap();
33+
}
34+
35+
text.clear();
36+
match reader.read_to_string(&mut text) {
37+
Ok(size) => {
38+
if size > 0 {
39+
last_pos += size as u64;
40+
printer(&text);
41+
}
42+
}
43+
Err(e) => {
44+
return Some("File read error: ".to_owned() + &e.to_string())
45+
}
46+
}
47+
48+
std::thread::sleep( Duration::from_millis( refresh_time_millis ) );
49+
}
50+
51+
None
52+
}
53+
Err(e) => {
54+
Some(e)
55+
}
56+
}
57+
}
58+
59+
fn scriptslog_file() -> Result<File, String> {
60+
let mut docs = None;
61+
if let Some(ud) = UserDirs::new() {
62+
if let Some(path) = ud.document_dir() {
63+
if let Some(s) = path.to_str() {
64+
docs = Some(s.to_owned());
65+
}
66+
}
67+
}
68+
69+
if let Some(docs) = docs {
70+
let file = OpenOptions::new()
71+
.read(true)
72+
.write(true) // so that it can be created if doesn't exist
73+
.create(true)
74+
.open( docs + "\\" + constants::SCRIPTSLOG_PATH_IN_DOCS );
75+
76+
if let Err(e) = file {
77+
println!("{:?}", e.kind());
78+
return Err("File open error: ".to_owned() + &e.to_string());
79+
} else {
80+
return Ok(file.unwrap());
81+
}
82+
} else {
83+
Err( "Documents directory could not be found.".to_owned() )
84+
}
85+
}

0 commit comments

Comments
 (0)