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
15use ck3_history_extractor_lib::{
16 display::{GetPath, Renderer},
17 game_data::{GameDataLoader, Localizable},
18 parser::{process_section, yield_section, GameState, SaveFile, SaveFileError},
19 structures::{GameObjectDerived, Player},
20};
21
22mod jinja_env;
24use jinja_env::create_env;
25
26mod args;
28use args::Args;
29
30mod steam;
32
33const INTERVAL: Duration = Duration::from_secs(1);
35
36#[derive(From, Debug)]
38enum UserError {
39 NoTerminal,
41 FileDoesNotExist,
43 FileError(SaveFileError),
45}
46
47impl Display for UserError {
48 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
49 match self {
50 UserError::NoTerminal => write!(f, "The program is not running in a terminal"),
51 UserError::FileDoesNotExist => write!(f, "The file does not exist"),
52 UserError::FileError(e) => write!(f, "An error occurred during file handling: {}", e),
53 }
54 }
55}
56
57impl error::Error for UserError {
58 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
59 match self {
60 UserError::FileError(e) => Some(e),
61 _ => None,
62 }
63 }
64}
65
66fn main() -> Result<(), UserError> {
85 if cfg!(debug_assertions) {
86 env::set_var("RUST_BACKTRACE", "1");
87 }
88 setup_panic!();
89 let args = if env::args().len() < 2 {
91 if !stdout().is_terminal() {
92 return Err(UserError::NoTerminal);
93 }
94 Args::get_from_user()
95 } else {
96 Args::parse()
97 };
98 if !args.filename.exists() || !args.filename.is_file() {
100 return Err(UserError::FileDoesNotExist);
101 }
102 let bar_style = ProgressStyle::default_bar()
103 .template("[{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}")
104 .unwrap()
105 .progress_chars("#>-");
106 let spinner_style = ProgressStyle::default_spinner()
107 .template("[{elapsed_precise}] {spinner} {msg}")
108 .unwrap()
109 .tick_chars("|/-\\ ");
110 let mut include_paths = args.include;
111 if let Some(game_path) = args.game_path {
113 include_paths.push(game_path);
114 }
115 let mut loader = GameDataLoader::new(args.no_vis, args.language);
116 if !include_paths.is_empty() {
117 println!("Using game files from: {:?}", include_paths);
118 let progress_bar = ProgressBar::new(include_paths.len() as u64);
119 progress_bar.set_style(bar_style.clone());
120 progress_bar.enable_steady_tick(INTERVAL);
122 for path in progress_bar.wrap_iter(include_paths.iter().rev()) {
123 progress_bar.set_message(path.to_str().unwrap().to_owned());
124 loader.process_path(path).unwrap();
125 }
126 progress_bar.finish_with_message("Game files loaded");
127 }
128 let mut data = loader.finalize();
129 let save = SaveFile::open(args.filename)?;
131 let mut game_state: GameState = GameState::default();
133 let mut players: Vec<Player> = Vec::new();
134 let progress_bar = ProgressBar::new_spinner();
135 progress_bar.set_style(spinner_style.clone());
136 progress_bar.enable_steady_tick(INTERVAL);
137 let mut tape = save.tape();
138 while let Some(res) = yield_section(&mut tape) {
139 let section = res.unwrap();
140 progress_bar.set_message(section.get_name().to_owned());
141 process_section(section, &mut game_state, &mut players).unwrap();
143 progress_bar.inc(1);
144 }
145 progress_bar.finish_with_message("Save parsing complete");
146 game_state.localize(&mut data).unwrap();
148 let grapher = args.no_vis.not().then(|| game_state.new_grapher());
149 let timeline = args.no_vis.not().then(|| game_state.new_timeline());
150 let mut env = create_env(
151 args.use_internal,
152 data.get_map().is_some(),
153 args.no_vis,
154 &data,
155 );
156 let rendering_progress_bar = MultiProgress::new();
158 let player_progress = rendering_progress_bar.add(ProgressBar::new(players.len() as u64));
159 player_progress.set_style(bar_style);
160 player_progress.enable_steady_tick(INTERVAL);
161 for player in player_progress.wrap_iter(players.iter_mut()) {
162 player.localize(&mut data).unwrap();
163 let folder_name = player.get_name().to_string() + "'s history";
165 player_progress.set_message(format!("Rendering {}", folder_name));
166 let path = args.output.join(folder_name);
167 let mut renderer = Renderer::new(
168 path.as_path(),
169 &game_state,
170 &data,
171 grapher.as_ref(),
172 args.depth,
173 );
174 let render_spinner = rendering_progress_bar.add(ProgressBar::new_spinner());
175 render_spinner.set_style(spinner_style.clone());
176 render_spinner.enable_steady_tick(INTERVAL);
177 if !args.no_vis {
178 render_spinner.inc(renderer.add_object(timeline.as_ref().unwrap()) as u64);
179 }
180 render_spinner.inc(renderer.add_object(player) as u64);
181 renderer.render_all(&mut env);
182 render_spinner.finish_with_message("Rendering complete");
183 if stdin().is_terminal() && stdout().is_terminal() && !args.no_interaction {
184 if let Err(e) = open::that(player.get_path(path.as_path())) {
186 eprintln!("Error opening browser: {}", e);
187 }
188 }
189 rendering_progress_bar.remove(&render_spinner);
190 }
191 player_progress.finish_with_message("Players rendered");
192 if let Some(dump_path) = args.dump {
193 let json = serde_json::to_string_pretty(&game_state).unwrap();
194 fs::write(dump_path, json).unwrap();
195 }
196 if let Some(dump_path) = args.dump_data {
197 let json = serde_json::to_string_pretty(&data).unwrap();
198 fs::write(dump_path, json).unwrap();
199 }
200 return Ok(());
201}