use std::collections::HashMap;
use crate::game::eu4::script::parser::AstStatement;
use crate::game::eu4::script::{ParsedScriptFile, parse_script_file};
use crate::project::DepOverride;
use super::dag::{FileDag, IgnoreReplacePath, ModDag, ModId, induced_file_dag_with_overrides};
use crate::input::{InputScriptCache, ResolvedInputContributor};
use crate::model::GamePath;
pub(crate) struct DagMergeInputRequest<'a> {
pub file_path: &'a GamePath,
pub contributors: &'a [ResolvedInputContributor],
pub mod_dag: &'a ModDag,
pub ignore_replace_path: &'a IgnoreReplacePath,
pub dep_overrides: &'a [DepOverride],
pub script_cache: Option<&'a InputScriptCache>,
}
pub(crate) struct PreparedDagMergeInput {
pub file_dag: FileDag,
pub vanilla: Option<ParsedScriptFile>,
pub contributors: HashMap<ModId, ParsedScriptFile>,
}
pub(crate) fn prepare_dag_merge_input(
request: DagMergeInputRequest<'_>,
) -> Result<PreparedDagMergeInput, String> {
let file_dag = induced_file_dag_with_overrides(
request.mod_dag,
request.file_path,
request.contributors,
request.ignore_replace_path,
request.dep_overrides,
);
let vanilla = parse_vanilla_contributor(
request.file_path,
request.contributors,
request.script_cache,
)?;
let contributors = parse_active_mod_contributors(
request.file_path,
request.contributors,
&file_dag,
request.script_cache,
)?;
Ok(PreparedDagMergeInput {
file_dag,
vanilla,
contributors,
})
}
pub(crate) fn contributor_mod_hashes(
contributors: &[ResolvedInputContributor],
file_dag: &FileDag,
) -> HashMap<ModId, String> {
let by_mod: HashMap<ModId, &ResolvedInputContributor> = contributors
.iter()
.filter(|contributor| !contributor.is_base_game && !contributor.is_synthetic_base)
.map(|contributor| (ModId(contributor.mod_id.clone()), contributor))
.collect();
file_dag
.contributors()
.iter()
.filter_map(|mod_id| {
let contributor = by_mod.get(mod_id)?;
let hash = contributor.mod_hash.as_ref()?;
Some((mod_id.clone(), hash.clone()))
})
.collect()
}
pub(crate) fn merge_ancestor_statements(vanilla: Option<&ParsedScriptFile>) -> Vec<AstStatement> {
vanilla
.map(|base| base.ast.statements.clone())
.unwrap_or_default()
}
pub(crate) fn template_for<'a>(
file_dag: &FileDag,
vanilla: Option<&'a ParsedScriptFile>,
contributors: &'a HashMap<ModId, ParsedScriptFile>,
) -> Option<&'a ParsedScriptFile> {
vanilla.or_else(|| {
file_dag
.contributors()
.iter()
.find_map(|mod_id| contributors.get(mod_id))
})
}
fn parse_vanilla_contributor(
file_path: &GamePath,
contributors: &[ResolvedInputContributor],
script_cache: Option<&InputScriptCache>,
) -> Result<Option<ParsedScriptFile>, String> {
let Some(base) = contributors
.iter()
.find(|contributor| contributor.is_base_game)
else {
return Ok(None);
};
parse_contributor(base, script_cache)
.map(Some)
.map_err(|error| format!("failed to parse vanilla file for {file_path}: {error}"))
}
fn parse_active_mod_contributors(
file_path: &GamePath,
contributors: &[ResolvedInputContributor],
file_dag: &FileDag,
script_cache: Option<&InputScriptCache>,
) -> Result<HashMap<ModId, ParsedScriptFile>, String> {
let by_mod: HashMap<ModId, &ResolvedInputContributor> = contributors
.iter()
.filter(|contributor| !contributor.is_base_game && !contributor.is_synthetic_base)
.map(|contributor| (ModId(contributor.mod_id.clone()), contributor))
.collect();
let mut parsed = HashMap::new();
for mod_id in file_dag.contributors() {
let contributor = by_mod
.get(mod_id)
.ok_or_else(|| format!("missing contributor {} for {file_path}", mod_id.as_str()))?;
let parsed_file = parse_contributor(contributor, script_cache).map_err(|error| {
format!(
"failed to parse mod file for {} in {file_path}: {error}",
contributor.mod_id
)
})?;
parsed.insert(mod_id.clone(), parsed_file);
}
Ok(parsed)
}
fn parse_contributor(
contributor: &ResolvedInputContributor,
script_cache: Option<&InputScriptCache>,
) -> Result<ParsedScriptFile, String> {
if let Some(script_cache) = script_cache {
return script_cache
.load(contributor)
.map(|parsed| (*parsed).clone());
}
Ok(parse_script_file(
&contributor.mod_id,
&contributor.root_path,
&contributor.relative_path,
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn verified_cache_error_never_falls_back_to_direct_parsing() {
let temp = TempDir::new().expect("temp dir");
let relative = "events/test.txt";
fs::create_dir_all(temp.path().join("events")).expect("create events dir");
fs::write(
temp.path().join(relative),
"country_event = { id = test.1 }\n",
)
.expect("write script");
let contributor = ResolvedInputContributor {
mod_id: "mod-a".to_string(),
root_path: temp.path().to_path_buf(),
relative_path: crate::model::GamePathBuf::parse(relative).expect("valid game path"),
precedence: 1,
is_base_game: false,
is_synthetic_base: false,
parse_ok_hint: Some(true),
mod_hash: Some("hash-a".to_string()),
};
let cache = InputScriptCache::default();
let error = parse_contributor(&contributor, Some(&cache))
.expect_err("missing verified entry must fail closed");
assert!(error.contains("no semantic-snapshot input"));
assert!(
parse_contributor(&contributor, None).is_ok(),
"direct parsing remains available only when no cache was supplied"
);
}
}