Skip to main content

rpfm_extensions/merge/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Delta merge: combine same-named DB/Loc tables by row identity instead of blindly
12//! concatenating every row.
13//!
14//! The plain [`DB::merge`](rpfm_lib::files::db::DB::merge)/[`Loc::merge`](rpfm_lib::files::loc::Loc::merge)
15//! append every source row verbatim, so two mods editing the same rows produce duplicates instead of a
16//! combined result. This module instead matches rows by their schema key column(s)
17//! ([`Definition::key_column_positions`]), and for a key shared by more than one source resolves each
18//! field individually:
19//!
20//! - All contributing sources agree on the value: keep it.
21//! - They disagree, but only one of them actually diverges from the vanilla/parent baseline: keep the
22//!   one that diverges (that source is the only one that touched this field).
23//! - Otherwise it's a genuine conflict (including a row that's brand new, with different data, in more
24//!   than one source, since there's no baseline to break the tie): reported as a [`MergeConflict`]
25//!   unless the caller already supplied a [`MergeResolution`] for it.
26//!
27//! Tables with no key column can't be matched by row identity at all, so delta merge degrades to plain
28//! concatenation for them.
29
30use getset::{Getters, Setters};
31use serde::{Deserialize, Serialize};
32
33use std::collections::{HashMap, HashSet};
34
35use rpfm_lib::error::{RLibError, Result};
36use rpfm_lib::files::{db::DB, loc::Loc, RFileDecoded, table::DecodedData};
37use rpfm_lib::schema::{Definition, DefinitionPatch};
38
39use crate::dependencies::Dependencies;
40
41#[cfg(test)] mod tests;
42
43//-------------------------------------------------------------------------------//
44//                             Options & result types
45//-------------------------------------------------------------------------------//
46
47/// Configuration for a table merge.
48#[derive(Clone, Debug, Default, Getters, Setters, Deserialize, Serialize)]
49#[getset(get = "pub", set = "pub")]
50pub struct MergeOptions {
51
52    /// Merge rows by key instead of concatenating them. See the module docs for the resolution rules.
53    delta_merge: bool,
54
55    /// Resolutions for conflicts reported by a previous attempt at the same merge.
56    resolutions: Vec<MergeResolution>,
57}
58
59/// A single field that two or more sources changed in incompatible ways, needing a user decision.
60#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
61pub struct MergeConflict {
62
63    /// Stringified value of each key column, identifying the row this conflict belongs to.
64    pub row_key: Vec<String>,
65
66    /// Name of the conflicting column.
67    pub field_name: String,
68
69    /// The distinct values proposed for this field, each tagged with the source(s) that proposed it.
70    pub candidates: Vec<MergeCandidate>,
71}
72
73/// One of the candidate values for a [`MergeConflict`].
74#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
75pub struct MergeCandidate {
76
77    /// Stringified candidate value.
78    pub value: String,
79
80    /// Source table paths that proposed this value.
81    pub source_paths: Vec<String>,
82}
83
84/// The user's chosen value for one [`MergeConflict`], fed back into a follow-up merge attempt.
85#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
86pub struct MergeResolution {
87
88    /// Must match [`MergeConflict::row_key`] exactly.
89    pub row_key: Vec<String>,
90
91    /// Must match [`MergeConflict::field_name`] exactly.
92    pub field_name: String,
93
94    /// The candidate value the user picked.
95    pub chosen_value: String,
96}
97
98//-------------------------------------------------------------------------------//
99//                             Public entry points
100//-------------------------------------------------------------------------------//
101
102/// Delta-merges multiple DB tables of the same name into one, using `baseline` (the vanilla/parent
103/// version of the table, if any) to tell an intentional edit apart from an untouched row.
104///
105/// Every source is normalized to the first source's definition first, exactly like the plain
106/// [`DB::merge`]. Returns the merged table plus any conflicts that need a [`MergeResolution`] before
107/// they can be applied; unresolved conflicting fields are left at their baseline (or first-candidate,
108/// if there's no baseline) value in the returned table.
109pub fn delta_merge_db(sources: &[(&str, &DB)], baseline: Option<&DB>, resolutions: &[MergeResolution]) -> Result<(DB, Vec<MergeConflict>)> {
110    if sources.len() < 2 {
111        return Err(RLibError::RFileMergeTablesNotEnoughTablesProvided);
112    }
113
114    let table_names = sources.iter().map(|(_, db)| db.table_name()).collect::<HashSet<_>>();
115    if table_names.len() > 1 {
116        return Err(RLibError::RFileMergeTablesDifferentNames);
117    }
118
119    let definition = sources[0].1.definition().clone();
120    let patches = sources[0].1.patches().clone();
121    let table_name = sources[0].1.table_name().to_owned();
122
123    let source_data = sources.iter()
124        .map(|(path, db)| {
125            let mut db = (*db).clone();
126            db.set_definition(&definition);
127            (*path, db.data().to_vec())
128        })
129        .collect::<Vec<_>>();
130    let source_rows = source_data.iter().map(|(path, rows)| (*path, rows.as_slice())).collect::<Vec<_>>();
131
132    let baseline_data = baseline.map(|db| {
133        let mut db = db.clone();
134        db.set_definition(&definition);
135        db.data().to_vec()
136    });
137
138    let (rows, conflicts) = delta_merge_rows(&source_rows, baseline_data.as_deref(), &definition, Some(&patches), resolutions);
139
140    let mut merged = DB::new(&definition, Some(&patches), &table_name);
141    merged.set_data(&rows)?;
142
143    Ok((merged, conflicts))
144}
145
146/// Delta-merges multiple Loc tables into one. See [`delta_merge_db`] for the general behavior; Loc
147/// tables always key on their single `key` column.
148pub fn delta_merge_loc(sources: &[(&str, &Loc)], baseline: Option<&Loc>, resolutions: &[MergeResolution]) -> Result<(Loc, Vec<MergeConflict>)> {
149    if sources.len() < 2 {
150        return Err(RLibError::RFileMergeTablesNotEnoughTablesProvided);
151    }
152
153    let definition = sources[0].1.definition().clone();
154
155    let source_data = sources.iter()
156        .map(|(path, loc)| {
157            let mut loc = (*loc).clone();
158            loc.set_definition(&definition);
159            (*path, loc.data().to_vec())
160        })
161        .collect::<Vec<_>>();
162    let source_rows = source_data.iter().map(|(path, rows)| (*path, rows.as_slice())).collect::<Vec<_>>();
163
164    let baseline_data = baseline.map(|loc| {
165        let mut loc = loc.clone();
166        loc.set_definition(&definition);
167        loc.data().to_vec()
168    });
169
170    let (rows, conflicts) = delta_merge_rows(&source_rows, baseline_data.as_deref(), &definition, None, resolutions);
171
172    let mut merged = Loc::new();
173    merged.set_definition(&definition);
174    merged.set_data(&rows)?;
175
176    Ok((merged, conflicts))
177}
178
179/// Flattens the vanilla/parent versions of a DB table (in load order) into a single baseline table,
180/// for use as the `baseline` argument of [`delta_merge_db`]. Returns `None` if the table isn't loaded.
181pub fn db_baseline(dependencies: &Dependencies, table_name: &str) -> Option<DB> {
182    let files = dependencies.db_data(table_name, true, true).ok()?;
183
184    let mut baseline: Option<DB> = None;
185    for file in files {
186        if let Ok(RFileDecoded::DB(table)) = file.decoded() {
187            match baseline {
188                None => baseline = Some(table.clone()),
189                Some(ref mut merged) => merged.data_mut().extend(table.data().iter().cloned()),
190            }
191        }
192    }
193
194    baseline
195}
196
197/// Flattens the vanilla/parent Loc tables (in load order) into a single baseline table, for use as the
198/// `baseline` argument of [`delta_merge_loc`]. Returns `None` if no Loc data is loaded.
199pub fn loc_baseline(dependencies: &Dependencies) -> Option<Loc> {
200    let files = dependencies.loc_data(true, true).ok()?;
201
202    let mut baseline: Option<Loc> = None;
203    for file in files {
204        if let Ok(RFileDecoded::Loc(table)) = file.decoded() {
205            match baseline {
206                None => baseline = Some(table.clone()),
207                Some(ref mut merged) => merged.data_mut().extend(table.data().iter().cloned()),
208            }
209        }
210    }
211
212    baseline
213}
214
215//-------------------------------------------------------------------------------//
216//                             Core algorithm
217//-------------------------------------------------------------------------------//
218
219/// Generic row-diff/merge core shared by [`delta_merge_db`] and [`delta_merge_loc`]. Operates purely on
220/// rows and a [`Definition`], so it doesn't care whether it's merging DB or Loc data.
221///
222/// All of `sources`, `baseline`, and the rows they contain are expected to already share `definition`'s
223/// column layout (the two public wrappers normalize sources to a common definition before calling this).
224fn delta_merge_rows(
225    sources: &[(&str, &[Vec<DecodedData>])],
226    baseline: Option<&[Vec<DecodedData>]>,
227    definition: &Definition,
228    patches: Option<&DefinitionPatch>,
229    resolutions: &[MergeResolution],
230) -> (Vec<Vec<DecodedData>>, Vec<MergeConflict>) {
231    let fields = definition.fields_processed();
232    let key_positions = fields.iter().enumerate().filter(|(_, field)| field.is_key(patches)).map(|(position, _)| position).collect::<Vec<_>>();
233
234    // No key columns to match rows by: fall back to the same behavior as the plain merge.
235    if key_positions.is_empty() {
236        let rows = sources.iter().flat_map(|(_, rows)| rows.iter().cloned()).collect();
237        return (rows, vec![]);
238    }
239
240    let row_key = |row: &[DecodedData]| key_positions.iter().map(|&position| row[position].clone()).collect::<Vec<DecodedData>>();
241    let row_key_strings = |row: &[DecodedData]| key_positions.iter().map(|&position| row[position].data_to_string().to_string()).collect::<Vec<String>>();
242
243    let baseline_index = baseline.map(|rows| rows.iter().map(|row| (row_key(row), row)).collect::<HashMap<_, _>>()).unwrap_or_default();
244
245    // Group rows by key, preserving the order keys are first seen in across all sources.
246    let mut key_order = Vec::new();
247    let mut rows_by_key: HashMap<Vec<DecodedData>, Vec<(&str, &Vec<DecodedData>)>> = HashMap::new();
248    for (source_path, rows) in sources {
249        for row in rows.iter() {
250            let key = row_key(row);
251            if !rows_by_key.contains_key(&key) {
252                key_order.push(key.clone());
253            }
254            rows_by_key.entry(key).or_default().push((source_path, row));
255        }
256    }
257
258    let mut merged_rows = Vec::with_capacity(key_order.len());
259    let mut conflicts = Vec::new();
260
261    for key in key_order {
262        let candidates = &rows_by_key[&key];
263
264        // Only one source touched this key: nothing to reconcile, take it as-is.
265        if let [(_, row)] = candidates[..] {
266            merged_rows.push(row.clone());
267            continue;
268        }
269
270        let baseline_row = baseline_index.get(&key).copied();
271        let key_strings = row_key_strings(candidates[0].1);
272        let mut merged_row = baseline_row.cloned().unwrap_or_else(|| candidates[0].1.clone());
273
274        for (column, field) in fields.iter().enumerate() {
275            let mut distinct_values: Vec<(DecodedData, Vec<String>)> = Vec::new();
276            for (source_path, row) in candidates {
277                let value = &row[column];
278                match distinct_values.iter_mut().find(|(existing, _)| existing == value) {
279                    Some((_, source_paths)) => source_paths.push((*source_path).to_owned()),
280                    None => distinct_values.push((value.clone(), vec![(*source_path).to_owned()])),
281                }
282            }
283
284            // Every source agrees: use that value, whether or not it matches the baseline.
285            if let [(value, _)] = &distinct_values[..] {
286                merged_row[column] = value.clone();
287                continue;
288            }
289
290            // They disagree, but only one source actually diverges from the baseline: that's the edit.
291            if let Some(baseline_row) = baseline_row {
292                let mut diverging = distinct_values.iter().filter(|(value, _)| value != &baseline_row[column]);
293                if let (Some((value, _)), None) = (diverging.next(), diverging.next()) {
294                    merged_row[column] = value.clone();
295                    continue;
296                }
297            }
298
299            let resolution = resolutions.iter().find(|resolution| resolution.row_key == key_strings && resolution.field_name == field.name());
300            match resolution.and_then(|resolution| DecodedData::new_from_type_and_string(field.field_type(), &resolution.chosen_value).ok()) {
301                Some(value) => merged_row[column] = value,
302                None => conflicts.push(MergeConflict {
303                    row_key: key_strings.clone(),
304                    field_name: field.name().to_owned(),
305                    candidates: distinct_values.into_iter().map(|(value, source_paths)| MergeCandidate { value: value.data_to_string().to_string(), source_paths }).collect(),
306                }),
307            }
308        }
309
310        merged_rows.push(merged_row);
311    }
312
313    (merged_rows, conflicts)
314}