use crate::cli::arg::{
DataArgs, DataBuildArgs, DataInstallArgs, DataListArgs, FochCliDataCommands,
};
use crate::cli::handler::HandlerResult;
use foch::game::eu4::Eu4;
use foch::game::eu4::base::snapshot::{
BaseBuildObserver, BaseDataSource, INSTALLED_COVERAGE_FILE_NAME,
build_base_snapshot_with_observer, default_release_tag, install_built_snapshot,
install_snapshot_from_release, list_installed_base_data, resolve_game_root_and_version,
write_release_artifacts, write_snapshot_bundle,
};
use foch::input::{Config, FileFilter};
pub fn handle_data(data_args: &DataArgs, config: Config) -> HandlerResult {
match &data_args.command {
FochCliDataCommands::Install(args) => handle_data_install(args, config),
FochCliDataCommands::Build(args) => handle_data_build(args, &config),
FochCliDataCommands::List(args) => handle_data_list(args),
}
}
fn handle_data_install(args: &DataInstallArgs, config: Config) -> HandlerResult {
let game = parse_game(&args.game_name)?;
let resolved_version = if args.game_version.eq_ignore_ascii_case("auto") {
let (_game_root, version) = resolve_game_root_and_version(&config, &game)
.map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
version
} else {
args.game_version.clone()
};
let installed =
install_snapshot_from_release(&game, &resolved_version, args.release_tag.as_deref())
.map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
println!(
"installed base data: game={} version={} source=download path={}",
installed.metadata.game,
installed.metadata.game_version,
installed.install_dir.display()
);
Ok(0)
}
fn handle_data_build(args: &DataBuildArgs, config: &Config) -> HandlerResult {
let observer = BaseBuildObserver::stderr(&args.game_name);
for line in run_data_build(args, config, observer)? {
println!("{line}");
}
Ok(0)
}
pub fn run_data_build(
args: &DataBuildArgs,
config: &Config,
mut observer: BaseBuildObserver,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut report = Vec::new();
if !args.install && args.output_dir.is_none() && !args.release_asset {
return Err(
"please specify at least one of --install, --output-dir, or --release-asset".into(),
);
}
let game = parse_game(&args.game_name)?;
let resolved_version = if args.game_version.eq_ignore_ascii_case("auto") {
None
} else {
Some(args.game_version.as_str())
};
let filter = FileFilter::new(game, &config.extra_ignore_patterns)
.map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
let build = build_base_snapshot_with_observer(
&game,
&args.from_game_path,
resolved_version,
&filter,
&mut observer,
)
.map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
observer
.run_stage(
"write_outputs",
|counts: &mut std::collections::BTreeMap<String, u64>| {
if args.install {
let installed = install_built_snapshot(
&build.encoded_snapshot,
BaseDataSource::Build,
Some(build.snapshot_asset_name.clone()),
Some(build.snapshot_sha256.clone()),
)?;
counts.insert(
"install_snapshot_bytes".to_string(),
build.encoded_snapshot.len() as u64,
);
let coverage_bytes =
std::fs::metadata(installed.install_dir.join(INSTALLED_COVERAGE_FILE_NAME))
.map(|item| item.len())
.unwrap_or_default();
counts.insert("install_coverage_bytes".to_string(), coverage_bytes);
report.push(format!(
"built and installed base data: game={} version={} path={}",
installed.metadata.game,
installed.metadata.game_version,
installed.install_dir.display()
));
}
if args.release_asset {
let output_dir = args.output_dir.as_ref().ok_or_else(|| {
"--output-dir is required when using --release-asset".to_string()
})?;
let release_output = write_release_artifacts(
&build.encoded_snapshot,
output_dir,
&default_release_tag(),
)?;
let manifest_bytes = std::fs::metadata(&release_output.manifest_path)
.map(|item| item.len())
.unwrap_or_default();
let coverage_bytes = std::fs::metadata(&release_output.coverage_path)
.map(|item| item.len())
.unwrap_or_default();
counts.insert(
"release_snapshot_bytes".to_string(),
build.encoded_snapshot.len() as u64,
);
counts.insert("release_manifest_bytes".to_string(), manifest_bytes);
counts.insert("release_coverage_bytes".to_string(), coverage_bytes);
report.push(format!(
"wrote release data asset: snapshot={} manifest={} coverage={}",
release_output.snapshot_path.display(),
release_output.manifest_path.display(),
release_output.coverage_path.display()
));
} else if let Some(output_dir) = args.output_dir.as_ref() {
let bundle = write_snapshot_bundle(
&build.encoded_snapshot,
output_dir,
BaseDataSource::Build,
Some(build.snapshot_asset_name.clone()),
Some(build.snapshot_sha256.clone()),
)?;
let metadata_bytes = std::fs::metadata(&bundle.metadata_path)
.map(|item| item.len())
.unwrap_or_default();
let coverage_bytes = std::fs::metadata(&bundle.coverage_path)
.map(|item| item.len())
.unwrap_or_default();
counts.insert(
"bundle_snapshot_bytes".to_string(),
build.encoded_snapshot.len() as u64,
);
counts.insert("bundle_metadata_bytes".to_string(), metadata_bytes);
counts.insert("bundle_coverage_bytes".to_string(), coverage_bytes);
report.push(format!(
"wrote snapshot bundle: snapshot={} metadata={} coverage={}",
bundle.snapshot_path.display(),
bundle.metadata_path.display(),
bundle.coverage_path.display()
));
}
Ok(())
},
)
.map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
let profile = observer.finish();
if let Some(path) = args.profile_out.as_ref() {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, serde_json::to_vec_pretty(&profile)?)?;
report.push(format!("wrote profiling result: {}", path.display()));
}
Ok(report)
}
fn handle_data_list(args: &DataListArgs) -> HandlerResult {
let entries = list_installed_base_data()
.map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
if args.json {
println!("{}", serde_json::to_string_pretty(&entries)?);
return Ok(0);
}
if entries.is_empty() {
println!("No installed base data");
return Ok(0);
}
for entry in entries {
println!(
"game={} version={} data={} source={:?} path={}",
entry.game,
entry.game_version,
entry.snapshot_format_version,
entry.source,
entry.install_path.display()
);
}
Ok(0)
}
fn parse_game(value: &str) -> Result<Eu4, Box<dyn std::error::Error>> {
Eu4::from_key(value).ok_or_else(|| format!("unsupported game key: {value}").into())
}