1use crate::cli::arg::{
2 DataArgs, DataBuildArgs, DataInstallArgs, DataListArgs, FochCliDataCommands,
3};
4use crate::cli::handler::HandlerResult;
5use foch::game::eu4::Eu4;
6use foch::game::eu4::base::snapshot::{
7 BaseBuildObserver, BaseDataSource, INSTALLED_COVERAGE_FILE_NAME,
8 build_base_snapshot_with_observer, default_release_tag, install_built_snapshot,
9 install_snapshot_from_release, list_installed_base_data, resolve_game_root_and_version,
10 write_release_artifacts, write_snapshot_bundle,
11};
12use foch::input::{Config, FileFilter};
13
14pub fn handle_data(data_args: &DataArgs, config: Config) -> HandlerResult {
15 match &data_args.command {
16 FochCliDataCommands::Install(args) => handle_data_install(args, config),
17 FochCliDataCommands::Build(args) => handle_data_build(args, &config),
18 FochCliDataCommands::List(args) => handle_data_list(args),
19 }
20}
21
22fn handle_data_install(args: &DataInstallArgs, config: Config) -> HandlerResult {
23 let game = parse_game(&args.game_name)?;
24 let resolved_version = if args.game_version.eq_ignore_ascii_case("auto") {
25 let (_game_root, version) = resolve_game_root_and_version(&config, &game)
26 .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
27 version
28 } else {
29 args.game_version.clone()
30 };
31 let installed =
32 install_snapshot_from_release(&game, &resolved_version, args.release_tag.as_deref())
33 .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
34 println!(
35 "installed base data: game={} version={} source=download path={}",
36 installed.metadata.game,
37 installed.metadata.game_version,
38 installed.install_dir.display()
39 );
40 Ok(0)
41}
42
43fn handle_data_build(args: &DataBuildArgs, config: &Config) -> HandlerResult {
44 let observer = BaseBuildObserver::stderr(&args.game_name);
45 for line in run_data_build(args, config, observer)? {
46 println!("{line}");
47 }
48 Ok(0)
49}
50
51pub fn run_data_build(
55 args: &DataBuildArgs,
56 config: &Config,
57 mut observer: BaseBuildObserver,
58) -> Result<Vec<String>, Box<dyn std::error::Error>> {
59 let mut report = Vec::new();
60 if !args.install && args.output_dir.is_none() && !args.release_asset {
61 return Err(
62 "please specify at least one of --install, --output-dir, or --release-asset".into(),
63 );
64 }
65
66 let game = parse_game(&args.game_name)?;
67 let resolved_version = if args.game_version.eq_ignore_ascii_case("auto") {
68 None
69 } else {
70 Some(args.game_version.as_str())
71 };
72 let filter = FileFilter::new(game, &config.extra_ignore_patterns)
73 .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
74 let build = build_base_snapshot_with_observer(
75 &game,
76 &args.from_game_path,
77 resolved_version,
78 &filter,
79 &mut observer,
80 )
81 .map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
82
83 observer
84 .run_stage(
85 "write_outputs",
86 |counts: &mut std::collections::BTreeMap<String, u64>| {
87 if args.install {
88 let installed = install_built_snapshot(
89 &build.encoded_snapshot,
90 BaseDataSource::Build,
91 Some(build.snapshot_asset_name.clone()),
92 Some(build.snapshot_sha256.clone()),
93 )?;
94 counts.insert(
95 "install_snapshot_bytes".to_string(),
96 build.encoded_snapshot.len() as u64,
97 );
98 let coverage_bytes =
99 std::fs::metadata(installed.install_dir.join(INSTALLED_COVERAGE_FILE_NAME))
100 .map(|item| item.len())
101 .unwrap_or_default();
102 counts.insert("install_coverage_bytes".to_string(), coverage_bytes);
103 report.push(format!(
104 "built and installed base data: game={} version={} path={}",
105 installed.metadata.game,
106 installed.metadata.game_version,
107 installed.install_dir.display()
108 ));
109 }
110
111 if args.release_asset {
112 let output_dir = args.output_dir.as_ref().ok_or_else(|| {
113 "--output-dir is required when using --release-asset".to_string()
114 })?;
115 let release_output = write_release_artifacts(
116 &build.encoded_snapshot,
117 output_dir,
118 &default_release_tag(),
119 )?;
120 let manifest_bytes = std::fs::metadata(&release_output.manifest_path)
121 .map(|item| item.len())
122 .unwrap_or_default();
123 let coverage_bytes = std::fs::metadata(&release_output.coverage_path)
124 .map(|item| item.len())
125 .unwrap_or_default();
126 counts.insert(
127 "release_snapshot_bytes".to_string(),
128 build.encoded_snapshot.len() as u64,
129 );
130 counts.insert("release_manifest_bytes".to_string(), manifest_bytes);
131 counts.insert("release_coverage_bytes".to_string(), coverage_bytes);
132 report.push(format!(
133 "wrote release data asset: snapshot={} manifest={} coverage={}",
134 release_output.snapshot_path.display(),
135 release_output.manifest_path.display(),
136 release_output.coverage_path.display()
137 ));
138 } else if let Some(output_dir) = args.output_dir.as_ref() {
139 let bundle = write_snapshot_bundle(
140 &build.encoded_snapshot,
141 output_dir,
142 BaseDataSource::Build,
143 Some(build.snapshot_asset_name.clone()),
144 Some(build.snapshot_sha256.clone()),
145 )?;
146 let metadata_bytes = std::fs::metadata(&bundle.metadata_path)
147 .map(|item| item.len())
148 .unwrap_or_default();
149 let coverage_bytes = std::fs::metadata(&bundle.coverage_path)
150 .map(|item| item.len())
151 .unwrap_or_default();
152 counts.insert(
153 "bundle_snapshot_bytes".to_string(),
154 build.encoded_snapshot.len() as u64,
155 );
156 counts.insert("bundle_metadata_bytes".to_string(), metadata_bytes);
157 counts.insert("bundle_coverage_bytes".to_string(), coverage_bytes);
158 report.push(format!(
159 "wrote snapshot bundle: snapshot={} metadata={} coverage={}",
160 bundle.snapshot_path.display(),
161 bundle.metadata_path.display(),
162 bundle.coverage_path.display()
163 ));
164 }
165 Ok(())
166 },
167 )
168 .map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
169
170 let profile = observer.finish();
171 if let Some(path) = args.profile_out.as_ref() {
172 if let Some(parent) = path.parent()
173 && !parent.as_os_str().is_empty()
174 {
175 std::fs::create_dir_all(parent)?;
176 }
177 std::fs::write(path, serde_json::to_vec_pretty(&profile)?)?;
178 report.push(format!("wrote profiling result: {}", path.display()));
179 }
180
181 Ok(report)
182}
183
184fn handle_data_list(args: &DataListArgs) -> HandlerResult {
185 let entries = list_installed_base_data()
186 .map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
187 if args.json {
188 println!("{}", serde_json::to_string_pretty(&entries)?);
189 return Ok(0);
190 }
191
192 if entries.is_empty() {
193 println!("No installed base data");
194 return Ok(0);
195 }
196
197 for entry in entries {
198 println!(
199 "game={} version={} data={} source={:?} path={}",
200 entry.game,
201 entry.game_version,
202 entry.snapshot_format_version,
203 entry.source,
204 entry.install_path.display()
205 );
206 }
207
208 Ok(0)
209}
210
211fn parse_game(value: &str) -> Result<Eu4, Box<dyn std::error::Error>> {
212 Eu4::from_key(value).ok_or_else(|| format!("unsupported game key: {value}").into())
213}