ck3_history_extractor/
main.rs1use clap::Parser;
2use derive_more::From;
3use human_panic::setup_panic;
4use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
5use serde_json;
6use std::{
7 env, error,
8 fmt::{self, Debug, Display, Formatter},
9 fs,
10 io::{stdin, stdout, IsTerminal},
11 ops::Not,
12 time::Duration,
13};
14
15mod types;
17
18mod parser;
20use parser::{process_section, yield_section, GameState, SaveFile, SaveFileError};
21
22mod structures;
25use structures::{GameObjectDerived, Player};
26
27mod jinja_env;
29use jinja_env::create_env;
30
31mod display;
33use display::{GetPath, Renderer};
34
35mod game_data;
37use game_data::{GameDataLoader, Localizable};
38
39mod args;
41use args::Args;
42
43mod steam;
45
46const INTERVAL: Duration = Duration::from_secs(1);
48
49#[derive(From, Debug)]
51enum UserError {
52 NoTerminal,
54 FileDoesNotExist,
56 FileError(SaveFileError),
58}
59
60impl Display for UserError {
61 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
62 match self {
63 UserError::NoTerminal => write!(f, "The program is not running in a terminal"),
64 UserError::FileDoesNotExist => write!(f, "The file does not exist"),
65 UserError::FileError(e) => write!(f, "An error occurred during file handling: {}", e),
66 }
67 }
68}
69
70impl error::Error for UserError {
71 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
72 match self {
73 UserError::FileError(e) => Some(e),
74 _ => None,
75 }
76 }
77}
78
79fn main() -> Result<(), UserError> {
98 if cfg!(debug_assertions) {
99 env::set_var("RUST_BACKTRACE", "1");
100 }
101 setup_panic!();
102 let args = if env::args().len() < 2 {
104 if !stdout().is_terminal() {
105 return Err(UserError::NoTerminal);
106 }
107 Args::get_from_user()
108 } else {
109 Args::parse()
110 };
111 if !args.filename.exists() || !args.filename.is_file() {
113 return Err(UserError::FileDoesNotExist);
114 }
115 let bar_style = ProgressStyle::default_bar()
116 .template("[{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}")
117 .unwrap()
118 .progress_chars("#>-");
119 let spinner_style = ProgressStyle::default_spinner()
120 .template("[{elapsed_precise}] {spinner} {msg}")
121 .unwrap()
122 .tick_chars("|/-\\ ");
123 let mut include_paths = args.include;
124 if let Some(game_path) = args.game_path {
126 include_paths.push(game_path);
127 }
128 let mut loader = GameDataLoader::new(args.no_vis, args.language);
129 if !include_paths.is_empty() {
130 println!("Using game files from: {:?}", include_paths);
131 let progress_bar = ProgressBar::new(include_paths.len() as u64);
132 progress_bar.set_style(bar_style.clone());
133 progress_bar.enable_steady_tick(INTERVAL);
135 for path in progress_bar.wrap_iter(include_paths.iter().rev()) {
136 progress_bar.set_message(path.to_str().unwrap().to_owned());
137 loader.process_path(path).unwrap();
138 }
139 progress_bar.finish_with_message("Game files loaded");
140 }
141 let mut data = loader.finalize();
142 let save = SaveFile::open(args.filename)?;
144 let mut game_state: GameState = GameState::default();
146 let mut players: Vec<Player> = Vec::new();
147 let progress_bar = ProgressBar::new_spinner();
148 progress_bar.set_style(spinner_style.clone());
149 progress_bar.enable_steady_tick(INTERVAL);
150 let mut tape = save.tape();
151 while let Some(res) = yield_section(&mut tape) {
152 let mut section = res.unwrap();
153 progress_bar.set_message(section.get_name().to_owned());
154 process_section(&mut section, &mut game_state, &mut players).unwrap();
156 progress_bar.inc(1);
157 }
158 progress_bar.finish_with_message("Save parsing complete");
159 game_state.localize(&mut data).unwrap();
161 let grapher = args.no_vis.not().then(|| game_state.new_grapher());
162 let timeline = args.no_vis.not().then(|| game_state.new_timeline());
163 let mut env = create_env(
164 args.use_internal,
165 data.get_map().is_some(),
166 args.no_vis,
167 &data,
168 );
169 let rendering_progress_bar = MultiProgress::new();
171 let player_progress = rendering_progress_bar.add(ProgressBar::new(players.len() as u64));
172 player_progress.set_style(bar_style);
173 player_progress.enable_steady_tick(INTERVAL);
174 for player in player_progress.wrap_iter(players.iter_mut()) {
175 player.localize(&mut data).unwrap();
176 let folder_name = player.get_name().to_string() + "'s history";
178 player_progress.set_message(format!("Rendering {}", folder_name));
179 let path = args.output.join(folder_name);
180 let mut renderer = Renderer::new(
181 path.as_path(),
182 &game_state,
183 &data,
184 grapher.as_ref(),
185 args.depth,
186 );
187 let render_spinner = rendering_progress_bar.add(ProgressBar::new_spinner());
188 render_spinner.set_style(spinner_style.clone());
189 render_spinner.enable_steady_tick(INTERVAL);
190 if !args.no_vis {
191 render_spinner.inc(renderer.add_object(timeline.as_ref().unwrap()) as u64);
192 }
193 render_spinner.inc(renderer.add_object(player) as u64);
194 renderer.render_all(&mut env);
195 render_spinner.finish_with_message("Rendering complete");
196 if stdin().is_terminal() && stdout().is_terminal() && !args.no_interaction {
197 if let Err(e) = open::that(player.get_path(path.as_path())) {
199 eprintln!("Error opening browser: {}", e);
200 }
201 }
202 rendering_progress_bar.remove(&render_spinner);
203 }
204 player_progress.finish_with_message("Players rendered");
205 if let Some(dump_path) = args.dump {
206 let json = serde_json::to_string_pretty(&game_state).unwrap();
207 fs::write(dump_path, json).unwrap();
208 }
209 if let Some(dump_path) = args.dump_data {
210 let json = serde_json::to_string_pretty(&data).unwrap();
211 fs::write(dump_path, json).unwrap();
212 }
213 return Ok(());
214}