use crate::game::eu4::content::{
DivergentBlockPolicy, MergePolicies, NamedContainerPolicy, ScalarMergePolicy,
};
use crate::game::eu4::script::parser::{AstStatement, AstValue, ScalarValue};
use super::super::patch::{ClausewitzPatch, patches_semantically_equal};
use super::address::patch_kind;
use super::block_merge::{
synthesize_boolean_or, synthesize_scroll_stacked_insert, try_recursive_block_merge,
try_recursive_insert_merge, try_recursive_remove_replace_merge,
try_replace_block_named_container_merge, try_union_block_merge,
};
use super::rename::resolve_renames;
use super::{AttributedPatch, PatchAddress, PatchMergeStats, PatchResolution};
fn try_parse_f64(val: &AstValue) -> Option<f64> {
match val {
AstValue::Scalar {
value: ScalarValue::Number(n),
..
} => n.parse::<f64>().ok(),
_ => None,
}
}
fn number_text(value: &AstValue) -> Option<&str> {
match value {
AstValue::Scalar {
value: ScalarValue::Number(number),
..
} => Some(number),
_ => None,
}
}
fn make_number_value(text: String, reference_span: &AstValue) -> AstValue {
AstValue::Scalar {
value: ScalarValue::Number(text),
span: reference_span.span().clone(),
}
}
fn is_coordinate_scalar_key(key: &str) -> bool {
matches!(key, "x" | "y")
}
fn is_gui_layout_scalar_key(key: &str) -> bool {
is_coordinate_scalar_key(key) || matches!(key, "maxWidth" | "maxHeight")
}
fn is_gui_reference_scalar_key(key: &str) -> bool {
matches!(key, "spriteType")
}
fn all_values_numeric(values: impl IntoIterator<Item = AstValue>) -> bool {
values
.into_iter()
.all(|value| try_parse_f64(&value).is_some())
}
fn is_edit_kind(kind: &str) -> bool {
matches!(kind, "SetValue" | "ReplaceBlock")
}
pub(super) fn resolve_address(
addr: PatchAddress,
attributed: Vec<AttributedPatch>,
policies: &MergePolicies,
stats: &mut PatchMergeStats,
) -> PatchResolution {
if attributed.len() == 1 {
stats.single_mod_patches += 1;
return PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch);
}
if attributed
.windows(2)
.all(|w| patches_semantically_equal(&w[0].patch, &w[1].patch))
{
stats.convergent_patches += 1;
return PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch);
}
let kinds: Vec<&str> = attributed.iter().map(|a| patch_kind(&a.patch)).collect();
let has_mixed_kinds = kinds.windows(2).any(|w| w[0] != w[1]);
if has_mixed_kinds {
let distinct: std::collections::BTreeSet<&str> = kinds.iter().copied().collect();
if distinct == std::collections::BTreeSet::from(["RemoveNode", "ReplaceBlock"])
&& policies.divergent_block_policy_for_key(&addr.key) == DivergentBlockPolicy::Recurse
&& let Some(resolution) =
try_recursive_remove_replace_merge(&addr, &attributed, policies, stats)
{
return resolution;
}
let edit_kinds: Vec<&str> = distinct
.iter()
.copied()
.filter(|kind| *kind != "RemoveNode")
.collect();
if distinct.contains("RemoveNode")
&& policies.edit_wins_over_remove
&& edit_kinds.len() == 1
&& is_edit_kind(edit_kinds[0])
{
let kept_kind = edit_kinds[0];
let edits: Vec<AttributedPatch> = attributed
.into_iter()
.filter(|a| patch_kind(&a.patch) != "RemoveNode")
.collect();
stats.edit_over_remove_resolved += 1;
if edits.len() == 1 {
return PatchResolution::Resolved(edits.into_iter().next().unwrap().patch);
}
return match kept_kind {
"SetValue" => resolve_set_values(addr, edits, policies, stats),
"ReplaceBlock" => resolve_replace_blocks(addr, edits, policies, stats),
_ => unreachable!("is_edit_kind restricts to the two handled kinds"),
};
}
stats.conflict_patches += 1;
return PatchResolution::Conflict {
address: addr,
reason: format!("mixed patch kinds at same address: {}", kinds.join(", ")),
patches: attributed,
};
}
let kind = kinds[0];
match kind {
"InsertNode" => resolve_insert_nodes(addr, attributed, policies, stats),
"AppendListItem" => resolve_append_list_items(addr, attributed, stats),
"SetValue" => resolve_set_values(addr, attributed, policies, stats),
"RemoveNode" => resolve_remove_convergent(addr, attributed, stats),
"RemoveListItem" => resolve_remove_list_items(addr, attributed, stats),
"ReplaceBlock" => resolve_replace_blocks(addr, attributed, policies, stats),
"AppendBlockItem" => resolve_append_block_items(addr, attributed, stats),
"RemoveBlockItem" => resolve_remove_block_items(addr, attributed, stats),
"Rename" => resolve_renames(addr, attributed, stats),
_ => {
stats.conflict_patches += 1;
PatchResolution::Conflict {
address: addr,
reason: format!("unhandled patch kind: {kind}"),
patches: attributed,
}
}
}
}
fn resolve_insert_nodes(
addr: PatchAddress,
attributed: Vec<AttributedPatch>,
policies: &MergePolicies,
stats: &mut PatchMergeStats,
) -> PatchResolution {
let stmts: Vec<_> = attributed
.iter()
.map(|a| match &a.patch {
ClausewitzPatch::InsertNode { statement, .. } => statement,
_ => unreachable!(),
})
.collect();
if stmts
.windows(2)
.all(|w| crate::merge::patch::ast_statements_semantically_equal(w[0], w[1]))
{
stats.convergent_patches += 1;
return PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch);
}
if policies.named_container == NamedContainerPolicy::ScrollStack
&& let Some(synth) = synthesize_scroll_stacked_insert(&attributed)
{
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
return PatchResolution::AutoMerged {
result: synth,
strategy: "scroll_stack".to_string(),
contributing_mods: mods,
};
}
if let Some(resolution) = resolve_assignment_insert_nodes(&addr, &attributed, policies, stats) {
return resolution;
}
if policies.divergent_block_policy_for_key(&addr.key) == DivergentBlockPolicy::BooleanOr
&& let Some(synth) = synthesize_boolean_or(&addr, &attributed)
{
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
return PatchResolution::AutoMerged {
result: synth,
strategy: "boolean_or".to_string(),
contributing_mods: mods,
};
}
if policies.divergent_block_policy_for_key(&addr.key) == DivergentBlockPolicy::Recurse
&& let Some(resolution) = try_recursive_insert_merge(&addr, &attributed, policies, stats)
{
return resolution;
}
let summaries: Vec<String> = attributed
.iter()
.map(|a| match &a.patch {
ClausewitzPatch::InsertNode { statement, .. } => format!(
"`{}` from `{}`",
statement_text_for_reason(statement),
a.mod_id
),
_ => unreachable!(),
})
.collect();
stats.conflict_patches += 1;
PatchResolution::Conflict {
address: addr,
reason: format!(
"sibling mods inserted divergent statements at the same key: {}",
summaries.join(", ")
),
patches: attributed,
}
}
fn resolve_assignment_insert_nodes(
_addr: &PatchAddress,
attributed: &[AttributedPatch],
policies: &MergePolicies,
stats: &mut PatchMergeStats,
) -> Option<PatchResolution> {
let inserted: Vec<(&AttributedPatch, &AstValue)> = attributed
.iter()
.filter_map(|patch| match &patch.patch {
ClausewitzPatch::InsertNode {
statement:
AstStatement::Assignment {
value: value @ AstValue::Scalar { .. },
..
},
..
} => Some((patch, value)),
_ => None,
})
.collect();
if inserted.len() != attributed.len() {
return None;
}
match policies.scalar {
ScalarMergePolicy::Conflict => None,
ScalarMergePolicy::LastWriter => {
let winner = inserted
.iter()
.max_by_key(|(patch, _)| (patch.precedence, patch.mod_id.clone()))?
.0;
stats.auto_merged_patches += 1;
Some(auto_merge_insert_winner(attributed, winner, "LastWriter"))
}
ScalarMergePolicy::CoordinateFirstWriter => {
if !is_coordinate_scalar_key(&_addr.key)
|| !inserted
.iter()
.all(|(_, value)| try_parse_f64(value).is_some())
{
return None;
}
let winner = inserted
.iter()
.min_by_key(|(patch, _)| (patch.precedence, patch.mod_id.clone()))?
.0;
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
Some(PatchResolution::AutoMerged {
result: winner.patch.clone(),
strategy: "CoordinateFirstWriter".to_string(),
contributing_mods: mods,
})
}
ScalarMergePolicy::GuiWidget => {
if is_gui_layout_scalar_key(&_addr.key)
&& inserted
.iter()
.all(|(_, value)| try_parse_f64(value).is_some())
{
let winner = inserted
.iter()
.min_by_key(|(patch, _)| (patch.precedence, patch.mod_id.clone()))?
.0;
stats.auto_merged_patches += 1;
return Some(auto_merge_insert_winner(
attributed,
winner,
"GuiLayoutFirstWriter",
));
}
if is_gui_reference_scalar_key(&_addr.key) {
let winner = inserted
.iter()
.max_by_key(|(patch, _)| (patch.precedence, patch.mod_id.clone()))?
.0;
stats.auto_merged_patches += 1;
return Some(auto_merge_insert_winner(
attributed,
winner,
"GuiReferenceLastWriter",
));
}
None
}
ScalarMergePolicy::Sum
| ScalarMergePolicy::Avg
| ScalarMergePolicy::Max
| ScalarMergePolicy::Min => {
let values: Vec<AstValue> =
inserted.iter().map(|(_, value)| (*value).clone()).collect();
let merged_value = merge_numeric_values(&values, policies.scalar)?;
let first = attributed.first()?;
let result = match &first.patch {
ClausewitzPatch::InsertNode {
path,
key,
statement: AstStatement::Assignment { key_span, span, .. },
} => ClausewitzPatch::InsertNode {
path: path.clone(),
key: key.clone(),
statement: AstStatement::Assignment {
key: key.clone(),
key_span: key_span.clone(),
value: merged_value,
span: span.clone(),
},
},
_ => return None,
};
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
Some(PatchResolution::AutoMerged {
result,
strategy: format!("{:?}", policies.scalar),
contributing_mods: mods,
})
}
}
}
fn auto_merge_insert_winner(
attributed: &[AttributedPatch],
winner: &AttributedPatch,
strategy: &str,
) -> PatchResolution {
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
PatchResolution::AutoMerged {
result: winner.patch.clone(),
strategy: strategy.to_string(),
contributing_mods: mods,
}
}
fn resolve_append_list_items(
_addr: PatchAddress,
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
stats.convergent_patches += 1;
PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch)
}
fn resolve_set_values(
addr: PatchAddress,
attributed: Vec<AttributedPatch>,
policies: &MergePolicies,
stats: &mut PatchMergeStats,
) -> PatchResolution {
match policies.scalar {
ScalarMergePolicy::Conflict => conflict_set_values(addr, attributed, stats),
ScalarMergePolicy::Sum
| ScalarMergePolicy::Avg
| ScalarMergePolicy::Max
| ScalarMergePolicy::Min => resolve_numeric_set_values(addr, attributed, policies.scalar, stats),
ScalarMergePolicy::LastWriter => resolve_last_writer_set_value(attributed, stats),
ScalarMergePolicy::CoordinateFirstWriter => {
let numeric = all_values_numeric(attributed.iter().map(|a| match &a.patch {
ClausewitzPatch::SetValue { new_value, .. } => new_value.clone(),
_ => unreachable!(),
}));
if is_coordinate_scalar_key(&addr.key) && numeric {
resolve_first_writer_set_value(attributed, stats)
} else {
conflict_set_values(addr, attributed, stats)
}
}
ScalarMergePolicy::GuiWidget => {
let numeric = all_values_numeric(attributed.iter().map(|a| match &a.patch {
ClausewitzPatch::SetValue { new_value, .. } => new_value.clone(),
_ => unreachable!(),
}));
if is_gui_layout_scalar_key(&addr.key) && numeric {
resolve_first_writer_set_value_with_strategy(
attributed,
stats,
"GuiLayoutFirstWriter",
)
} else if is_gui_reference_scalar_key(&addr.key) {
resolve_last_writer_set_value_with_strategy(
attributed,
stats,
"GuiReferenceLastWriter",
)
} else {
conflict_set_values(addr, attributed, stats)
}
}
}
}
fn conflict_set_values(
addr: PatchAddress,
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
let new_values: Vec<String> = attributed
.iter()
.map(|a| match &a.patch {
ClausewitzPatch::SetValue { new_value, .. } => {
format!("`{}` from `{}`", value_text_for_reason(new_value), a.mod_id)
}
_ => unreachable!(),
})
.collect();
stats.conflict_patches += 1;
PatchResolution::Conflict {
address: addr,
reason: format!(
"sibling mods set the same scalar to divergent values: {}",
new_values.join(", ")
),
patches: attributed,
}
}
fn value_text_for_reason(value: &AstValue) -> String {
match value {
AstValue::Scalar { value, .. } => value.as_text(),
AstValue::Block { .. } => "<block>".to_string(),
}
}
fn statement_text_for_reason(stmt: &AstStatement) -> String {
match stmt {
AstStatement::Assignment { key, value, .. } => {
format!("{key} = {}", value_text_for_reason(value))
}
AstStatement::Item { value, .. } => value_text_for_reason(value),
AstStatement::Comment { .. } => "<comment>".to_string(),
}
}
fn resolve_numeric_set_values(
addr: PatchAddress,
attributed: Vec<AttributedPatch>,
policy: ScalarMergePolicy,
stats: &mut PatchMergeStats,
) -> PatchResolution {
let new_values: Vec<AstValue> = attributed
.iter()
.map(|a| match &a.patch {
ClausewitzPatch::SetValue { new_value, .. } => new_value.clone(),
_ => unreachable!(),
})
.collect();
let Some(merged_value) = merge_numeric_values(&new_values, policy) else {
stats.conflict_patches += 1;
return PatchResolution::Conflict {
address: addr,
reason: format!(
"numeric merge policy {:?} but not all values are numeric",
policy
),
patches: attributed,
};
};
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
let first = attributed.into_iter().next().unwrap();
let strategy = format!("{policy:?}");
let result = match first.patch {
ClausewitzPatch::SetValue {
path,
key,
old_value,
..
} => ClausewitzPatch::SetValue {
path,
key,
old_value,
new_value: merged_value,
},
_ => unreachable!(),
};
stats.auto_merged_patches += 1;
PatchResolution::AutoMerged {
result,
strategy,
contributing_mods: mods,
}
}
fn merge_numeric_values(values: &[AstValue], policy: ScalarMergePolicy) -> Option<AstValue> {
let inputs = values.iter().map(number_text).collect::<Option<Vec<_>>>()?;
let merged = policy.reduce_numeric_values(inputs)?;
Some(make_number_value(merged, values.first()?))
}
fn resolve_last_writer_set_value(
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
resolve_last_writer_set_value_with_strategy(attributed, stats, "LastWriter")
}
fn resolve_last_writer_set_value_with_strategy(
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
strategy: &str,
) -> PatchResolution {
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
let winner = attributed
.into_iter()
.max_by_key(|patch| (patch.precedence, patch.mod_id.clone()))
.expect("at least one attributed patch");
stats.auto_merged_patches += 1;
PatchResolution::AutoMerged {
result: winner.patch,
strategy: strategy.to_string(),
contributing_mods: mods,
}
}
fn resolve_first_writer_set_value(
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
resolve_first_writer_set_value_with_strategy(attributed, stats, "CoordinateFirstWriter")
}
fn resolve_first_writer_set_value_with_strategy(
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
strategy: &str,
) -> PatchResolution {
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
let winner = attributed
.into_iter()
.min_by_key(|patch| (patch.precedence, patch.mod_id.clone()))
.expect("at least one attributed patch");
stats.auto_merged_patches += 1;
PatchResolution::AutoMerged {
result: winner.patch,
strategy: strategy.to_string(),
contributing_mods: mods,
}
}
fn resolve_remove_convergent(
_addr: PatchAddress,
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
stats.convergent_patches += 1;
PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch)
}
fn resolve_remove_list_items(
_addr: PatchAddress,
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
stats.convergent_patches += 1;
PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch)
}
fn resolve_append_block_items(
_addr: PatchAddress,
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
stats.convergent_patches += 1;
PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch)
}
fn resolve_remove_block_items(
_addr: PatchAddress,
attributed: Vec<AttributedPatch>,
stats: &mut PatchMergeStats,
) -> PatchResolution {
stats.convergent_patches += 1;
PatchResolution::Resolved(attributed.into_iter().next().unwrap().patch)
}
fn resolve_replace_blocks(
addr: PatchAddress,
attributed: Vec<AttributedPatch>,
policies: &MergePolicies,
stats: &mut PatchMergeStats,
) -> PatchResolution {
if attributed
.windows(2)
.all(|w| patches_semantically_equal(&w[0].patch, &w[1].patch))
{
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
return PatchResolution::AutoMerged {
result: attributed.into_iter().next().unwrap().patch,
strategy: "semantic_equivalence".to_string(),
contributing_mods: mods,
};
}
let block_policy = policies.divergent_block_policy_for_key(&addr.key);
if block_policy == DivergentBlockPolicy::BooleanOr
&& let Some(synth) = synthesize_boolean_or(&addr, &attributed)
{
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
return PatchResolution::AutoMerged {
result: synth,
strategy: "boolean_or".to_string(),
contributing_mods: mods,
};
}
if block_policy == DivergentBlockPolicy::Union
&& let Some(merged) = try_union_block_merge(&attributed)
{
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
return PatchResolution::AutoMerged {
result: merged,
strategy: "union_block".to_string(),
contributing_mods: mods,
};
}
if block_policy == DivergentBlockPolicy::Recurse
&& let Some(resolution) = try_recursive_block_merge(&addr, &attributed, policies, stats)
{
return resolution;
}
if let Some(merged) = try_replace_block_named_container_merge(&attributed, policies) {
let mods: Vec<String> = attributed.iter().map(|a| a.mod_id.clone()).collect();
stats.auto_merged_patches += 1;
return PatchResolution::AutoMerged {
result: merged,
strategy: "named_container_union".to_string(),
contributing_mods: mods,
};
}
stats.conflict_patches += 1;
PatchResolution::Conflict {
address: addr,
reason: "multiple mods replace the same block with different content".to_string(),
patches: attributed,
}
}