use std::collections::HashMap;
use std::fs;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::Path;
use std::time::{Duration, Instant};
use super::super::localisation_merge::{LocalisationMergeOutcome, merge_localisation_file};
use super::{StructuralMergeContext, effective_vanilla_base_mode, validate_structured_merge_entry};
use crate::game::eu4::Eu4;
use crate::game::eu4::content::ContentFamilyDescriptor;
use crate::game::eu4::script::emit::EmitOptions;
use crate::input::{ResolvedInput, ResolvedInputContributor};
use crate::merge::backend::{BackendOutcome, BackendRequest, BackendUnit, MergeBackend};
use crate::merge::conflict_handler::ConflictHandler;
use crate::merge::dag::{IgnoreReplacePath, ModDag};
use crate::merge::model::VanillaBaseMode;
use crate::merge::planning::module_view::{
CrossFileModuleViewError, build_cross_file_module_views,
};
use crate::model::{
DeferredUnitReason, DepMisuseFinding, GamePath, MergeModuleOutput, MergePlanEntry,
MergePlanStrategy, MergePlanTarget,
};
use crate::project::{DepOverride, ResolutionMap};
pub(super) struct UnitAnalysisContext<'a> {
pub(super) transforms: &'a crate::merge::transform::TransformPlan,
pub(super) input: &'a ResolvedInput,
pub(super) profile: &'a Eu4,
pub(super) backend: &'a dyn MergeBackend,
pub(super) mod_dag: &'a ModDag,
pub(super) ignore_replace_path: &'a IgnoreReplacePath,
pub(super) dep_overrides: &'a [DepOverride],
pub(super) dep_misuse: &'a [DepMisuseFinding],
pub(super) resolution_map: &'a ResolutionMap,
pub(super) mod_versions: &'a HashMap<String, String>,
pub(super) mod_display_names: &'a HashMap<String, String>,
pub(super) cache_game_version: &'a str,
pub(super) emit_options: &'a EmitOptions,
pub(super) include_game_base: bool,
pub(super) gui_scroll_merge: bool,
pub(super) provenance: bool,
}
pub(super) struct InteractivePrompt<'a> {
pub(super) handler: Option<&'a mut (dyn ConflictHandler + 'static)>,
pub(super) config_path: Option<&'a Path>,
}
impl InteractivePrompt<'_> {
pub(super) fn none() -> Self {
Self {
handler: None,
config_path: None,
}
}
fn reborrow(&mut self) -> InteractivePrompt<'_> {
InteractivePrompt {
handler: self.handler.as_deref_mut(),
config_path: self.config_path,
}
}
}
pub(super) type CaughtOutcome = Box<std::thread::Result<BackendOutcome>>;
pub(super) enum UnitAnalysis {
Nothing,
Localisation(Option<Result<LocalisationMergeOutcome, String>>),
File(FileAnalysis),
Module(ModuleAnalysis),
}
impl UnitAnalysis {
pub(super) fn could_prompt(&self) -> bool {
let unmerged = |outcome: &CaughtOutcome| !matches!(**outcome, Ok(Ok(_)));
match self {
Self::File(FileAnalysis::Merged(outcome)) => unmerged(outcome),
Self::Module(module) => module.namespaces.iter().any(
|namespace| matches!(namespace, NamespaceAnalysis::Analyzed(outcome) if unmerged(outcome)),
),
Self::Nothing | Self::Localisation(_) | Self::File(_) => false,
}
}
}
pub(super) enum FileAnalysis {
Merged(CaughtOutcome),
NoMergeNeeded,
NoVerifiedBase,
}
pub(super) struct ModuleAnalysis {
pub(super) namespaces: Vec<NamespaceAnalysis>,
pub(super) warnings: Vec<String>,
pub(super) elapsed: Duration,
}
pub(super) enum NamespaceAnalysis {
Analyzed(CaughtOutcome),
Failed(DeferredUnitReason, String),
}
impl NamespaceAnalysis {
fn merged(&self) -> bool {
matches!(self, Self::Analyzed(outcome) if matches!(**outcome, Ok(Ok(_))))
}
}
const WORKING_SET_PER_INPUT_BYTE: u64 = 2_000;
pub(super) fn working_set_estimate(input: &ResolvedInput, entry: &MergePlanEntry) -> u64 {
let input_bytes: u64 = entry
.target
.input_paths()
.iter()
.filter_map(|path| input.file_inventory.get(path))
.flatten()
.filter_map(|contributor| fs::metadata(contributor.absolute_path()).ok())
.map(|metadata| metadata.len())
.sum();
input_bytes.saturating_mul(WORKING_SET_PER_INPUT_BYTE)
}
pub(super) fn unit_needs_analysis(entry: &MergePlanEntry) -> bool {
matches!(
entry.strategy,
MergePlanStrategy::LocalisationMerge | MergePlanStrategy::StructuralMerge
)
}
pub(super) fn analyze_unit(
context: &UnitAnalysisContext<'_>,
entry: &MergePlanEntry,
prompt: InteractivePrompt<'_>,
) -> UnitAnalysis {
match entry.strategy {
MergePlanStrategy::LocalisationMerge => UnitAnalysis::Localisation(
context
.input
.file_inventory
.get(entry.output_path())
.map(|contributors| merge_localisation_file(entry.output_path(), contributors)),
),
MergePlanStrategy::StructuralMerge => analyze_structural_unit(context, entry, prompt),
MergePlanStrategy::CopyThrough
| MergePlanStrategy::LastWriterOverlay
| MergePlanStrategy::ManualConflict
| MergePlanStrategy::Generated => UnitAnalysis::Nothing,
}
}
fn analyze_structural_unit(
context: &UnitAnalysisContext<'_>,
entry: &MergePlanEntry,
prompt: InteractivePrompt<'_>,
) -> UnitAnalysis {
let game_path: &GamePath = entry.output_path();
let contributors: Option<&[ResolvedInputContributor]> = context
.input
.file_inventory
.get(game_path)
.map(Vec::as_slice);
let descriptor: Option<&ContentFamilyDescriptor> =
context.profile.classify_content_family(game_path);
let vanilla_base_mode: VanillaBaseMode = effective_vanilla_base_mode(
descriptor,
contributors,
VanillaBaseMode::from_include_game_base(context.include_game_base),
context.input.verified_absent_base_paths.contains(game_path),
);
if context.backend.profile().validate_semantic_units
&& validate_structured_merge_entry(entry, contributors, vanilla_base_mode, context.profile)
.is_err()
{
return UnitAnalysis::Nothing;
}
if matches!(&entry.target, MergePlanTarget::Module { .. }) {
let started: Instant = Instant::now();
let (namespaces, warnings) = analyze_module(context, entry, prompt);
return UnitAnalysis::Module(ModuleAnalysis {
namespaces,
warnings,
elapsed: started.elapsed(),
});
}
UnitAnalysis::File(analyze_file(
context,
game_path,
contributors,
descriptor,
vanilla_base_mode,
prompt,
))
}
fn analyze_file(
context: &UnitAnalysisContext<'_>,
path: &GamePath,
contributors: Option<&[ResolvedInputContributor]>,
descriptor: Option<&ContentFamilyDescriptor>,
vanilla_base_mode: VanillaBaseMode,
prompt: InteractivePrompt<'_>,
) -> FileAnalysis {
let can_run_semantic_merge: bool = !vanilla_base_mode.requires_non_empty()
|| contributors.is_some_and(|contributors| contributors.iter().any(|c| c.is_base_game));
let Some(contributors) = contributors.filter(|_| can_run_semantic_merge) else {
return FileAnalysis::NoVerifiedBase;
};
let non_base_count: usize = contributors
.iter()
.filter(|c| !c.is_base_game && !c.is_synthetic_base)
.count();
let Some((descriptor, merge_key_source)) = descriptor
.filter(|_| non_base_count >= 2)
.and_then(|descriptor| Some((descriptor, descriptor.merge_key_source?)))
else {
return FileAnalysis::NoMergeNeeded;
};
let merge_context: StructuralMergeContext<'_> = StructuralMergeContext {
entity_transform: context.transforms.entity_transform(path),
descriptor,
merge_key_source,
gui_scroll_merge: context.gui_scroll_merge,
mod_dag: context.mod_dag,
ignore_replace_path: context.ignore_replace_path,
dep_overrides: context.dep_overrides,
dep_misuse_findings: context.dep_misuse,
resolution_map: context.resolution_map,
mod_versions: context.mod_versions,
mod_display_names: context.mod_display_names,
cache_game_version: context.cache_game_version,
emit_options: context.emit_options,
provenance: context.provenance,
script_cache: &context.input.script_cache,
vanilla_base_mode,
};
FileAnalysis::Merged(Box::new(catch_unwind(AssertUnwindSafe(|| {
context.backend.analyze(BackendRequest {
target_path: path,
unit: BackendUnit::File(contributors),
context: merge_context,
interactive_handler: prompt.handler,
interactive_config_path: prompt.config_path,
})
}))))
}
fn analyze_module(
context: &UnitAnalysisContext<'_>,
entry: &MergePlanEntry,
mut prompt: InteractivePrompt<'_>,
) -> (Vec<NamespaceAnalysis>, Vec<String>) {
let mut analyses: Vec<NamespaceAnalysis> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
for namespace in entry.target.module_outputs() {
let analysis: NamespaceAnalysis =
analyze_module_namespace(context, entry, namespace, prompt.reborrow(), &mut warnings);
let merged: bool = analysis.merged();
analyses.push(analysis);
if !merged {
break;
}
}
(analyses, warnings)
}
fn analyze_module_namespace(
context: &UnitAnalysisContext<'_>,
entry: &MergePlanEntry,
namespace: &MergeModuleOutput,
prompt: InteractivePrompt<'_>,
warnings: &mut Vec<String>,
) -> NamespaceAnalysis {
let output_path: &GamePath = namespace.output_path();
eprintln!("[merge] definition module: start {output_path}");
let Some(descriptor) = context
.profile
.classify_content_family(namespace.output_path())
else {
return NamespaceAnalysis::Failed(
DeferredUnitReason::EngineFailure,
format!("missing content-family descriptor for {output_path}"),
);
};
let Some(merge_key_source) = descriptor.merge_key_source else {
return NamespaceAnalysis::Failed(
DeferredUnitReason::EngineFailure,
format!("missing merge-key policy for {output_path}"),
);
};
let views = match build_cross_file_module_views(
entry,
namespace,
context.input,
descriptor,
context.mod_dag,
context.ignore_replace_path,
context.dep_overrides,
context.backend.profile().duplicate_definition_override,
) {
Ok(views) => views,
Err(CrossFileModuleViewError::UnsupportedInput(reason)) => {
return NamespaceAnalysis::Failed(DeferredUnitReason::UnsupportedInput, reason);
}
Err(CrossFileModuleViewError::EngineFailure(reason)) => {
return NamespaceAnalysis::Failed(DeferredUnitReason::EngineFailure, reason);
}
};
warnings.extend(
views
.warnings
.iter()
.map(|warning| format!("{output_path}: {warning}")),
);
let merge_context: StructuralMergeContext<'_> = StructuralMergeContext {
entity_transform: context.transforms.entity_transform(output_path),
descriptor,
merge_key_source,
gui_scroll_merge: context.gui_scroll_merge,
mod_dag: context.mod_dag,
ignore_replace_path: context.ignore_replace_path,
dep_overrides: context.dep_overrides,
dep_misuse_findings: context.dep_misuse,
resolution_map: context.resolution_map,
mod_versions: context.mod_versions,
mod_display_names: context.mod_display_names,
cache_game_version: context.cache_game_version,
emit_options: context.emit_options,
provenance: context.provenance,
script_cache: &context.input.script_cache,
vanilla_base_mode: VanillaBaseMode::from_include_game_base(context.include_game_base),
};
NamespaceAnalysis::Analyzed(Box::new(catch_unwind(AssertUnwindSafe(|| {
context.backend.analyze(BackendRequest {
target_path: output_path,
unit: BackendUnit::DefinitionModule(&views),
context: merge_context,
interactive_handler: prompt.handler,
interactive_config_path: prompt.config_path,
})
}))))
}