Skip to main content

rpfm_extensions/startpos/
layout.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//! Layouts of the records of a startpos, used to write new ones.
12//!
13//! A startpos is a tree of records with dozens of fields each, most of them constant. Instead of building each record
14//! by hand, the writer starts from a layout extracted from game-built startposes: for every record path, its version,
15//! its child slots in order (leaves with their most common value, child records always present or optional) and
16//! whether its children are an array of groups. Generators then only set the fields that change.
17//!
18//! Each campaign has its own layout, and the game has one merged from all its campaigns, so records any of them uses
19//! are known to all (and to new campaigns, see [`StartposLayout::with_fallback`]). Layout files live in the
20//! [`STARTPOS_LAYOUT_FOLDER`] folder of RPFM's config folder (see [`layout_file_name`]).
21//!
22//! # Field names
23//!
24//! Leaves are named after the previous child record that's always present and their position after it, skipping
25//! records: `MORGUE+3` is the third leaf after the `MORGUE` record. Leaves before any record use `^` (`^+1`). Optional
26//! records don't move the names and optional leaves count even when missing, so names are stable across instances. A record repeated in the same group gets its
27//! occurrence appended from the second one on (`FACTION_FLAG_AND_COLOURS@2`).
28//!
29//! # Paths
30//!
31//! A record's path is the names of the records leading to it from the root, separated by `/`
32//! (`CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION`). Repeated records share the layout of
33//! their path, whatever their occurrence (see [`child_path`]).
34//!
35//! # Lists
36//!
37//! Some records store variable-length lists in a single group (the family tree, the AI pathfinding and influence maps).
38//! Records with more than [`MAX_LAYOUT_SLOTS`] slots are free-form: their layout has no slots and generators push
39//! their children directly. Their child records still have layouts.
40//!
41//! # Variants
42//!
43//! The AI pools ([`VARIANT_ARRAYS`]) hold objects of many types in one array, each group starting with its type code.
44//! Each type also gets its own layout, at [`variant_path`], so its groups start from that type's defaults.
45//!
46//! # Defaults
47//!
48//! A leaf's default is its most common value. Arrays longer than a few dozen elements are generated data (vision bits,
49//! pathfinding and AI maps), not constants, so their default is an empty array of the same type.
50
51use serde_derive::{Deserialize, Serialize};
52
53use std::cmp::Reverse;
54use std::collections::{BTreeMap, HashMap};
55use std::fmt::Debug;
56use std::fs::File;
57use std::io::{BufReader, BufWriter, Write};
58use std::mem::{discriminant, Discriminant};
59use std::ops::RangeInclusive;
60use std::path::Path;
61
62use rpfm_lib::error::{RLibError, Result};
63use rpfm_lib::files::esf::{ESF, NodeType, RecordNode, RecordNodeFlags};
64
65#[cfg(test)] #[path = "layout_test.rs"] mod layout_test;
66
67/// Folder, inside RPFM's config folder, where the startpos layout files live.
68pub const STARTPOS_LAYOUT_FOLDER: &str = "startpos_layout";
69
70/// Name used for the leaves before the first record of a group.
71const START_ANCHOR: &str = "^";
72
73/// Most slots a record layout can have. Records with more are lists, kept free-form.
74pub const MAX_LAYOUT_SLOTS: usize = 256;
75
76/// Largest subsequence table (cells) used to align two leaf runs. Bigger runs are aligned in a single pass.
77const MAX_ALIGNMENT_CELLS: usize = 1 << 20;
78
79/// Most leaves a child record can have to keep a default per slot (see [`Slot::Record`]).
80const MAX_SLOT_DEFAULT_LEAVES: usize = 16;
81
82/// Longest array kept as a default value. Longer ones become empty arrays.
83const MAX_DEFAULT_ARRAY_LEN: usize = 64;
84
85/// Names of the arrays whose groups start with a type code, which get a layout per type (see [`variant_path`]).
86pub const VARIANT_ARRAYS: [&str; 3] = ["CAI_BDI_POOL_BELIEFS", "CAI_BDI_POOL_DESIRES", "CAI_BDI_POOL_INTENTIONS"];
87
88/// Largest unsigned value with a compact ESF encoding (24 bits).
89pub(crate) const MAX_COMPACT_U32: u32 = 0xFF_FFFF;
90
91/// Signed values with a compact ESF encoding (24 bits).
92pub(crate) const COMPACT_I32: RangeInclusive<i32> = -0x80_0000..=0x7F_FFFF;
93
94//---------------------------------------------------------------------------//
95//                              Types
96//---------------------------------------------------------------------------//
97
98/// Layouts of every record path of a startpos.
99#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
100pub struct StartposLayout {
101
102    /// Layouts, by record path.
103    records: BTreeMap<String, RecordLayout>,
104}
105
106/// Layout of the records at a path.
107#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
108pub struct RecordLayout {
109
110    /// Record name.
111    name: String,
112
113    /// Record version.
114    version: u8,
115
116    /// Encoding flags (`RecordNodeFlags` bits).
117    flags: u8,
118
119    /// The record's children are an array: any number of groups, each with these slots.
120    array: bool,
121
122    /// The record is a list: its groups take any children, pushed by the generators.
123    #[serde(default)]
124    free_form: bool,
125
126    /// Child slots of each group, in order.
127    slots: Vec<Slot>,
128}
129
130/// A child slot of a record group.
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub enum Slot {
133
134    /// A leaf value.
135    Leaf {
136
137        /// Field name (see the module docs).
138        name: String,
139
140        /// Most common value, with its encoding.
141        default: NodeType,
142
143        /// The leaf isn't in every instance.
144        #[serde(default)]
145        optional: bool,
146
147        /// The optional leaf is missing in most instances, so it starts out of a built record until it's set.
148        #[serde(default)]
149        absent_by_default: bool,
150    },
151
152    /// A child record.
153    Record {
154
155        /// Key of the child in the path (record name plus occurrence suffix).
156        key: String,
157
158        /// The record isn't in every instance.
159        optional: bool,
160
161        /// Most common content of the record in this slot, for small records whose content depends on the slot
162        /// (like repeated records numbered by position). `None` uses the default record of the child's path.
163        #[serde(default, skip_serializing_if = "Option::is_none")]
164        default: Option<RecordNode>,
165    },
166}
167
168/// Builds a record from its layout: every slot starts with its default, and generators replace what they need.
169#[derive(Debug, Clone)]
170pub struct RecordBuilder<'a> {
171    layout: &'a StartposLayout,
172    path: String,
173    record: &'a RecordLayout,
174    groups: Vec<GroupBuilder<'a>>,
175}
176
177/// A group of a record being built.
178#[derive(Debug, Clone)]
179pub struct GroupBuilder<'a> {
180    path: String,
181    record: &'a RecordLayout,
182    values: Vec<Option<NodeType>>,
183}
184
185/// A group of an existing record, read through the layout of its path: its children by slot id (leaf name or child
186/// record key).
187#[derive(Debug, Clone, Default)]
188pub struct GroupView<'a> {
189    fields: HashMap<&'a str, &'a NodeType>,
190}
191
192/// A child slot seen while extracting, with what's known so far about it.
193#[derive(Debug, Clone)]
194enum SlotStats {
195    Leaf { kind: Discriminant<NodeType>, values: HashMap<String, (usize, NodeType)> },
196    Record { key: String, seen: usize, contents: Option<HashMap<String, (usize, RecordNode)>> },
197}
198
199/// What's known so far about the records of a path.
200#[derive(Debug, Clone, Default)]
201struct PathStats {
202    name: String,
203    versions: HashMap<u8, usize>,
204    flags: HashMap<u8, usize>,
205    array: bool,
206    free_form: bool,
207    groups: usize,
208    slots: Vec<SlotStats>,
209}
210
211//---------------------------------------------------------------------------//
212//                              Implementations
213//---------------------------------------------------------------------------//
214
215impl StartposLayout {
216
217    /// Extracts the layouts of every record of a startpos.
218    ///
219    /// # Arguments
220    ///
221    /// * `startpos` - A game-built startpos.
222    ///
223    /// # Errors
224    ///
225    /// Returns an error if the root isn't a record.
226    pub fn from_esf(startpos: &ESF) -> Result<Self> {
227        let mut stats = HashMap::new();
228        collect_startpos(startpos, &mut stats)?;
229        Self::from_stats(stats)
230    }
231
232    /// Extracts the layouts of every record of several startposes, as if their records were in a single one: defaults
233    /// are the most common values across all of them. Startposes are taken one at a time, so only one needs to be in
234    /// memory.
235    ///
236    /// # Arguments
237    ///
238    /// * `startposes` - Game-built startposes of the same game, or the errors getting them.
239    ///
240    /// # Errors
241    ///
242    /// Returns the first error getting a startpos, or an error if a root isn't a record.
243    pub fn from_esfs(startposes: impl IntoIterator<Item = Result<ESF>>) -> Result<Self> {
244        let mut stats = HashMap::new();
245        for startpos in startposes {
246            collect_startpos(&startpos?, &mut stats)?;
247        }
248        Self::from_stats(stats)
249    }
250
251    /// Adds what this layout lacks from another one, like records a campaign never uses from a layout merged from all
252    /// the game's campaigns:
253    ///
254    /// - Paths it doesn't have, and paths it has only seen empty (arrays without items).
255    /// - Slots its records lack, after the slot they follow in the other layout. They're optional and start out of
256    ///   built records, so records only have them when a generator sets them.
257    ///
258    /// Everything this layout has keeps its own layout and defaults.
259    pub fn with_fallback(mut self, fallback: &StartposLayout) -> Self {
260        for (path, record) in &fallback.records {
261            match self.records.get_mut(path) {
262                Some(own) if own.slots.is_empty() => *own = record.clone(),
263                Some(own) => add_missing_slots(&mut own.slots, &record.slots),
264                None => {
265                    self.records.insert(path.to_owned(), record.clone());
266                },
267            }
268        }
269        self
270    }
271
272    /// Builds the layouts from the stats of the records of some startposes.
273    fn from_stats(stats: HashMap<String, PathStats>) -> Result<Self> {
274        let records = stats.into_iter().map(|(path, stats)| (path, stats.into_layout())).collect();
275        let mut layout = Self { records };
276        layout.drop_redundant_slot_defaults()?;
277        Ok(layout)
278    }
279
280    /// Removes the slot defaults of child records that match the default record of the child's path.
281    fn drop_redundant_slot_defaults(&mut self) -> Result<()> {
282        let mut redundant = vec![];
283        for (path, record) in &self.records {
284            for (index, slot) in record.slots.iter().enumerate() {
285                if let Slot::Record { key, default: Some(default), .. } = slot {
286                    if *default == self.default_record(&child_path(path, key))? {
287                        redundant.push((path.to_owned(), index));
288                    }
289                }
290            }
291        }
292
293        for (path, index) in redundant {
294            if let Some(Slot::Record { default, .. }) = self.records.get_mut(&path).and_then(|record| record.slots.get_mut(index)) {
295                *default = None;
296            }
297        }
298        Ok(())
299    }
300
301    /// Loads a layout file.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if the file can't be read or isn't a valid layout.
306    pub fn load(path: &Path) -> Result<Self> {
307        let file = BufReader::new(File::open(path)?);
308        ron::de::from_reader(file).map_err(|error| RLibError::StartposLayoutError(format!("invalid startpos layout: {error}")))
309    }
310
311    /// Saves the layout to a file.
312    ///
313    /// # Errors
314    ///
315    /// Returns an error if the file can't be written.
316    pub fn save(&self, path: &Path) -> Result<()> {
317        let mut file = BufWriter::new(File::create(path)?);
318        let config = ron::ser::PrettyConfig::default();
319        let text = ron::ser::to_string_pretty(self, config).map_err(|error| RLibError::StartposLayoutError(format!("can't serialize the startpos layout: {error}")))?;
320        file.write_all(text.as_bytes())?;
321        Ok(())
322    }
323
324    /// Returns the layout of a record path.
325    pub fn record(&self, path: &str) -> Option<&RecordLayout> {
326        self.records.get(path)
327    }
328
329    /// Starts building a record from its layout.
330    ///
331    /// # Arguments
332    ///
333    /// * `path` - Path of the record.
334    ///
335    /// # Returns
336    ///
337    /// A builder with every slot at its default: one group for normal records, none for arrays.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if the layout has no such path.
342    pub fn builder(&self, path: &str) -> Result<RecordBuilder<'_>> {
343        let record = self.records.get(path).ok_or_else(|| missing(path))?;
344        let mut builder = RecordBuilder { layout: self, path: path.to_owned(), record, groups: vec![] };
345        if !record.array {
346            builder.push_group()?;
347        }
348        Ok(builder)
349    }
350
351    /// Builds a record with every slot at its default.
352    ///
353    /// # Errors
354    ///
355    /// Returns an error if the layout lacks the path or one of its child records.
356    pub fn default_record(&self, path: &str) -> Result<RecordNode> {
357        self.builder(path)?.build()
358    }
359
360    /// Reads the groups of a record through the layout of its path.
361    ///
362    /// # Arguments
363    ///
364    /// * `path` - Path of the record.
365    /// * `record` - The record to read.
366    ///
367    /// # Errors
368    ///
369    /// Returns an error if the path has no layout, its layout is free-form, or a group doesn't fit it.
370    pub fn read<'a>(&'a self, path: &str, record: &'a RecordNode) -> Result<Vec<GroupView<'a>>> {
371        let layout = self.record(path).ok_or_else(|| missing(path))?;
372        record.children().iter().enumerate().map(|(index, group)| {
373            let slots = layout.match_children(group).ok_or_else(|| missing(&format!("{path}[{index}]")))?;
374            Ok(GroupView { fields: slots.into_iter().map(Slot::id).zip(group).collect() })
375        }).collect()
376    }
377
378    /// Reads the only group of a record through the layout of its path.
379    ///
380    /// # Arguments
381    ///
382    /// * `path` - Path of the record.
383    /// * `record` - The record to read.
384    ///
385    /// # Errors
386    ///
387    /// Returns an error if the record doesn't fit its layout or has no group.
388    pub fn read_single<'a>(&'a self, path: &str, record: &'a RecordNode) -> Result<GroupView<'a>> {
389        self.read(path, record)?.into_iter().next().ok_or_else(|| missing(&format!("group in {path}")))
390    }
391
392    /// Reads the items of an array record child of a group: each array group, with its item record's only group.
393    ///
394    /// # Arguments
395    ///
396    /// * `array_path` - Path of the array record.
397    /// * `parent` - The group holding the array.
398    /// * `array_key` - Key of the array record in the parent.
399    /// * `item_key` - Key of the item record in each array group.
400    ///
401    /// # Returns
402    ///
403    /// The array group and the item's group of each item. None if the parent lacks the array.
404    ///
405    /// # Errors
406    ///
407    /// Returns an error if a record doesn't fit its layout, or an array group lacks its item.
408    pub fn read_items<'a>(&'a self, array_path: &str, parent: &GroupView<'a>, array_key: &str, item_key: &str) -> Result<Vec<(GroupView<'a>, GroupView<'a>)>> {
409        let Some(array) = parent.record(array_key) else { return Ok(vec![]) };
410        let item_path = child_path(array_path, item_key);
411        self.read(array_path, array)?.into_iter()
412            .map(|entry| {
413                let item = entry.record(item_key).ok_or_else(|| missing(&item_path))?;
414                Ok((entry.clone(), self.read_single(&item_path, item)?))
415            })
416            .collect()
417    }
418
419    /// Builds a record with the given children as its only group, as they are. For records whose shape varies too
420    /// much for slots, like lists of buildings.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if the layout has no such path.
425    pub fn raw_record(&self, path: &str, children: Vec<NodeType>) -> Result<RecordNode> {
426        let record = self.records.get(path).ok_or_else(|| missing(path))?;
427        let mut builder = RecordBuilder { layout: self, path: path.to_owned(), record, groups: vec![] };
428        builder.push_children(children);
429        builder.build()
430    }
431}
432
433impl RecordLayout {
434
435    /// Returns the record name.
436    pub fn name(&self) -> &str {
437        &self.name
438    }
439
440    /// Returns the record version.
441    pub fn version(&self) -> u8 {
442        self.version
443    }
444
445    /// Returns if the record's children are an array of groups.
446    pub fn array(&self) -> bool {
447        self.array
448    }
449
450    /// Returns if the record is a free-form list.
451    pub fn free_form(&self) -> bool {
452        self.free_form
453    }
454
455    /// Returns the child slots of each group.
456    pub fn slots(&self) -> &[Slot] {
457        &self.slots
458    }
459
460    /// Matches the children of a group of a record at this path with the layout's slots.
461    ///
462    /// # Arguments
463    ///
464    /// * `group` - The children of one group of the record.
465    ///
466    /// # Returns
467    ///
468    /// The slot of each child, in order, or `None` if some child doesn't fit the layout or the record is free-form.
469    pub fn match_children(&self, group: &[NodeType]) -> Option<Vec<&Slot>> {
470        if self.free_form {
471            return None;
472        }
473        let tokens = group_tokens(group);
474        let left = self.slots.iter().map(Slot::kind).collect::<Vec<_>>();
475        let right = tokens.iter().map(token_kind).collect::<Vec<_>>();
476        let pairs = align_kinds(&left, &right);
477        (pairs.len() == tokens.len()).then(|| pairs.into_iter().map(|(slot, _)| &self.slots[slot]).collect())
478    }
479
480    /// Returns the position of a leaf slot.
481    fn leaf_position(&self, path: &str, name: &str) -> Result<usize> {
482        self.slots.iter()
483            .position(|slot| matches!(slot, Slot::Leaf { name: slot_name, .. } if slot_name == name))
484            .ok_or_else(|| missing(&format!("{path}:{name}")))
485    }
486
487    /// Returns the position of a child record slot.
488    fn record_position(&self, path: &str, key: &str) -> Result<usize> {
489        self.slots.iter()
490            .position(|slot| matches!(slot, Slot::Record { key: slot_key, .. } if slot_key == key))
491            .ok_or_else(|| missing(&format!("{path}/{key}")))
492    }
493}
494
495impl<'a> RecordBuilder<'a> {
496
497    /// Adds a group with every slot at its default.
498    ///
499    /// # Returns
500    ///
501    /// The new group, to set its values.
502    ///
503    /// # Errors
504    ///
505    /// Returns an error if the layout lacks one of the group's always present child records.
506    pub fn push_group(&mut self) -> Result<&mut GroupBuilder<'a>> {
507        let path = self.path.to_owned();
508        self.push_group_of(path, self.record)
509    }
510
511    /// Adds a group of a variant array with every slot at the default of its type.
512    ///
513    /// # Arguments
514    ///
515    /// * `code` - Type code of the group.
516    ///
517    /// # Returns
518    ///
519    /// The new group, to set its values.
520    ///
521    /// # Errors
522    ///
523    /// Returns an error if the layout has no such type, or lacks one of the group's always present child records.
524    pub fn push_variant(&mut self, code: u32) -> Result<&mut GroupBuilder<'a>> {
525        let path = variant_path(&self.path, code);
526        let layout = self.layout;
527        let record = layout.records.get(&path).ok_or_else(|| missing(&path))?;
528        self.push_group_of(path, record)
529    }
530
531    /// Adds a group with the defaults of the layout of a path.
532    fn push_group_of(&mut self, path: String, record: &'a RecordLayout) -> Result<&mut GroupBuilder<'a>> {
533        let mut values = vec![];
534        for slot in &record.slots {
535            values.push(match slot {
536                Slot::Leaf { absent_by_default: true, .. } => None,
537                Slot::Leaf { default, .. } => Some(default.clone()),
538                Slot::Record { optional: true, .. } => None,
539                Slot::Record { default: Some(default), .. } => Some(NodeType::Record(Box::new(default.clone()))),
540                Slot::Record { key, default: None, .. } => Some(NodeType::Record(Box::new(self.layout.default_record(&child_path(&path, key))?))),
541            });
542        }
543        self.groups.push(GroupBuilder { path, record, values });
544        Ok(self.groups.last_mut().expect("a group was just pushed"))
545    }
546
547    /// Adds a group with the given children, as they are. For array items whose shape varies too much for slots, like
548    /// trade splines with their segments.
549    pub fn push_children(&mut self, children: Vec<NodeType>) {
550        let values = children.into_iter().map(Some).collect();
551        self.groups.push(GroupBuilder { path: self.path.to_owned(), record: self.record, values });
552    }
553
554    /// Returns the first group, where normal (non array) records keep their children.
555    ///
556    /// # Errors
557    ///
558    /// Returns an error if the record has no groups (an array without items).
559    pub fn group(&mut self) -> Result<&mut GroupBuilder<'a>> {
560        let path = self.path.to_owned();
561        self.groups.first_mut().ok_or_else(|| missing(&format!("{path}[0]")))
562    }
563
564    /// Builds the record.
565    ///
566    /// # Errors
567    ///
568    /// This doesn't fail at the moment; it returns a `Result` so validation can be added without changing callers.
569    pub fn build(self) -> Result<RecordNode> {
570        let mut record = RecordNode::default();
571        record.set_name(self.record.name.to_owned());
572        record.set_version(self.record.version);
573        record.set_record_flags(RecordNodeFlags::from_bits_truncate(self.record.flags));
574        record.set_children(self.groups.into_iter().map(|group| group.values.into_iter().flatten().collect()).collect());
575        Ok(record)
576    }
577}
578
579impl GroupBuilder<'_> {
580
581    /// Replaces a leaf, keeping the encoding of its default.
582    ///
583    /// # Arguments
584    ///
585    /// * `name` - Field name.
586    /// * `value` - New value. It must be of the same type as the default.
587    ///
588    /// # Errors
589    ///
590    /// Returns an error if the field doesn't exist or the value has another type.
591    pub fn set(&mut self, name: &str, value: NodeType) -> Result<&mut Self> {
592        let position = self.record.leaf_position(&self.path, name)?;
593        let Some(Slot::Leaf { default, .. }) = self.record.slots.get(position) else { unreachable!("leaf_position returns leaf slots") };
594        if discriminant(default) != discriminant(&value) {
595            return Err(RLibError::StartposLayoutError(format!("{}:{name}: expected a value like {default:?}, got {value:?}", self.path)));
596        }
597        self.values[position] = Some(value);
598        Ok(self)
599    }
600
601    /// Replaces an unsigned leaf, keeping its type.
602    ///
603    /// Games write integers and floats with their compact encoding when the value has one, so the leaf gets the
604    /// `optimized` flag a decoded startpos would have.
605    ///
606    /// # Errors
607    ///
608    /// Returns an error if the field doesn't exist or isn't an unsigned integer.
609    pub fn set_u32(&mut self, name: &str, value: u32) -> Result<&mut Self> {
610        let mut node = self.default_of(name)?;
611        match &mut node {
612            NodeType::U32(node) => { node.set_value(value).set_optimized(value <= MAX_COMPACT_U32); },
613            NodeType::U16(node) => { *node = u16::try_from(value).map_err(|_| self.wrong_type(name, &value))?; },
614            NodeType::U8(node) => { *node = u8::try_from(value).map_err(|_| self.wrong_type(name, &value))?; },
615            _ => return Err(self.wrong_type(name, &value)),
616        }
617        self.set(name, node)
618    }
619
620    /// Replaces a signed leaf, keeping its type. See [`Self::set_u32`] for its `optimized` flag.
621    ///
622    /// # Errors
623    ///
624    /// Returns an error if the field doesn't exist or isn't a signed integer.
625    pub fn set_i32(&mut self, name: &str, value: i32) -> Result<&mut Self> {
626        let mut node = self.default_of(name)?;
627        match &mut node {
628            NodeType::I32(node) => { node.set_value(value).set_optimized(COMPACT_I32.contains(&value)); },
629            NodeType::I16(node) => { *node = i16::try_from(value).map_err(|_| self.wrong_type(name, &value))?; },
630            NodeType::I8(node) => { *node = i8::try_from(value).map_err(|_| self.wrong_type(name, &value))?; },
631            _ => return Err(self.wrong_type(name, &value)),
632        }
633        self.set(name, node)
634    }
635
636    /// Replaces a float leaf. Only zero has a compact encoding: see [`Self::set_u32`] for its `optimized` flag.
637    ///
638    /// # Errors
639    ///
640    /// Returns an error if the field doesn't exist or isn't a float.
641    pub fn set_f32(&mut self, name: &str, value: f32) -> Result<&mut Self> {
642        let mut node = self.default_of(name)?;
643        match &mut node {
644            NodeType::F32(node) => { node.set_value(value).set_optimized(value == 0.0); },
645            _ => return Err(self.wrong_type(name, &value)),
646        }
647        self.set(name, node)
648    }
649
650    /// Replaces a boolean leaf, keeping its encoding.
651    ///
652    /// # Errors
653    ///
654    /// Returns an error if the field doesn't exist or isn't a boolean.
655    pub fn set_bool(&mut self, name: &str, value: bool) -> Result<&mut Self> {
656        let mut node = self.default_of(name)?;
657        match &mut node {
658            NodeType::Bool(node) => { node.set_value(value); },
659            _ => return Err(self.wrong_type(name, &value)),
660        }
661        self.set(name, node)
662    }
663
664    /// Replaces a string leaf (ASCII or UTF-16).
665    ///
666    /// # Errors
667    ///
668    /// Returns an error if the field doesn't exist or isn't a string.
669    pub fn set_str(&mut self, name: &str, value: &str) -> Result<&mut Self> {
670        let mut node = self.default_of(name)?;
671        match &mut node {
672            NodeType::Ascii(node) | NodeType::Utf16(node) => { *node = value.to_owned(); },
673            _ => return Err(self.wrong_type(name, &value)),
674        }
675        self.set(name, node)
676    }
677
678    /// Adds a child to a free-form record.
679    ///
680    /// # Errors
681    ///
682    /// Returns an error if the record isn't free-form.
683    pub fn push_child(&mut self, node: NodeType) -> Result<&mut Self> {
684        if !self.record.free_form {
685            return Err(RLibError::StartposLayoutError(format!("{} has a fixed layout", self.path)));
686        }
687        self.values.push(Some(node));
688        Ok(self)
689    }
690
691    /// Replaces a string array leaf (ASCII or UTF-16).
692    ///
693    /// # Errors
694    ///
695    /// Returns an error if the field doesn't exist or isn't a string array.
696    pub fn set_strings(&mut self, name: &str, values: Vec<String>) -> Result<&mut Self> {
697        let mut node = self.default_of(name)?;
698        match &mut node {
699            NodeType::AsciiArray(node) | NodeType::Utf16Array(node) => { *node = values; },
700            _ => return Err(self.wrong_type(name, &values)),
701        }
702        self.set(name, node)
703    }
704
705    /// Replaces a byte array leaf (`U8Array` or `I8Array`).
706    ///
707    /// # Errors
708    ///
709    /// Returns an error if the field doesn't exist or isn't a byte array.
710    pub fn set_bytes(&mut self, name: &str, values: Vec<u8>) -> Result<&mut Self> {
711        let mut node = self.default_of(name)?;
712        match &mut node {
713            NodeType::U8Array(node) | NodeType::I8Array(node) => { *node = values; },
714            _ => return Err(self.wrong_type(name, &values)),
715        }
716        self.set(name, node)
717    }
718
719    /// Replaces an unsigned array leaf. Games write non-empty arrays with the compact encoding when every value has
720    /// one, so the leaf gets the `optimized` flag a decoded startpos would have.
721    ///
722    /// # Errors
723    ///
724    /// Returns an error if the field doesn't exist or isn't an unsigned array.
725    pub fn set_u32s(&mut self, name: &str, values: Vec<u32>) -> Result<&mut Self> {
726        let mut node = self.default_of(name)?;
727        match &mut node {
728            NodeType::U32Array(node) => {
729                let optimized = !values.is_empty() && values.iter().all(|value| *value <= MAX_COMPACT_U32);
730                node.set_value(values).set_optimized(optimized);
731            },
732            _ => return Err(self.wrong_type(name, &values)),
733        }
734        self.set(name, node)
735    }
736
737    /// Leaves an optional leaf out of the record.
738    ///
739    /// # Errors
740    ///
741    /// Returns an error if the field doesn't exist or isn't optional.
742    pub fn unset(&mut self, name: &str) -> Result<&mut Self> {
743        let position = self.record.leaf_position(&self.path, name)?;
744        if !matches!(self.record.slots[position], Slot::Leaf { optional: true, .. }) {
745            return Err(RLibError::StartposLayoutError(format!("{}:{name} is always present", self.path)));
746        }
747        self.values[position] = None;
748        Ok(self)
749    }
750
751    /// Replaces a child record.
752    ///
753    /// # Arguments
754    ///
755    /// * `key` - Child key (record name plus occurrence suffix).
756    /// * `record` - The new record, or `None` to leave an optional record out.
757    ///
758    /// # Errors
759    ///
760    /// Returns an error if the group has no such child record, or `None` is given for a record that's always present.
761    pub fn set_record(&mut self, key: &str, record: Option<RecordNode>) -> Result<&mut Self> {
762        let position = self.record.record_position(&self.path, key)?;
763        if record.is_none() && matches!(self.record.slots[position], Slot::Record { optional: false, .. }) {
764            return Err(RLibError::StartposLayoutError(format!("{}/{key} is always present", self.path)));
765        }
766        self.values[position] = record.map(|record| NodeType::Record(Box::new(record)));
767        Ok(self)
768    }
769
770    /// Returns the default value of a leaf.
771    fn default_of(&self, name: &str) -> Result<NodeType> {
772        match &self.record.slots[self.record.leaf_position(&self.path, name)?] {
773            Slot::Leaf { default, .. } => Ok(default.clone()),
774            Slot::Record { .. } => unreachable!("leaf_position returns leaf slots"),
775        }
776    }
777
778    /// Builds the error for a value that doesn't fit a leaf.
779    fn wrong_type(&self, name: &str, value: &dyn Debug) -> RLibError {
780        RLibError::StartposLayoutError(format!("{}:{name}: {value:?} doesn't fit the field", self.path))
781    }
782}
783
784impl<'a> GroupView<'a> {
785
786    /// Returns the slot ids of the children the group has.
787    pub fn ids(&self) -> impl Iterator<Item = &'a str> + '_ {
788        self.fields.keys().copied()
789    }
790
791    /// Returns a child by slot id, if the group has it.
792    pub fn get(&self, id: &str) -> Option<&'a NodeType> {
793        self.fields.get(id).copied()
794    }
795
796    /// Returns an unsigned leaf (`U32`, `U16` or `U8`).
797    pub fn u32(&self, name: &str) -> Option<u32> {
798        match self.get(name)? {
799            NodeType::U32(node) => Some(*node.value()),
800            NodeType::U16(value) => Some(u32::from(*value)),
801            NodeType::U8(value) => Some(u32::from(*value)),
802            _ => None,
803        }
804    }
805
806    /// Returns a signed leaf (`I32`, `I16` or `I8`).
807    pub fn i32(&self, name: &str) -> Option<i32> {
808        match self.get(name)? {
809            NodeType::I32(node) => Some(*node.value()),
810            NodeType::I16(value) => Some(i32::from(*value)),
811            NodeType::I8(value) => Some(i32::from(*value)),
812            _ => None,
813        }
814    }
815
816    /// Returns a boolean leaf.
817    pub fn bool(&self, name: &str) -> Option<bool> {
818        match self.get(name)? {
819            NodeType::Bool(node) => Some(*node.value()),
820            _ => None,
821        }
822    }
823
824    /// Returns a string leaf (ASCII or UTF-16).
825    pub fn str(&self, name: &str) -> Option<&'a str> {
826        match self.get(name)? {
827            NodeType::Ascii(value) | NodeType::Utf16(value) => Some(value),
828            _ => None,
829        }
830    }
831
832    /// Returns a float leaf.
833    pub fn f32(&self, name: &str) -> Option<f32> {
834        match self.get(name)? {
835            NodeType::F32(node) => Some(*node.value()),
836            _ => None,
837        }
838    }
839
840    /// Returns a string array leaf (ASCII or UTF-16).
841    pub fn strs(&self, name: &str) -> Option<&'a [String]> {
842        match self.get(name)? {
843            NodeType::AsciiArray(values) | NodeType::Utf16Array(values) => Some(values),
844            _ => None,
845        }
846    }
847
848    /// Returns an unsigned array leaf.
849    pub fn u32s(&self, name: &str) -> Option<&'a [u32]> {
850        match self.get(name)? {
851            NodeType::U32Array(node) => Some(node.value()),
852            _ => None,
853        }
854    }
855
856    /// Returns a child record by key.
857    pub fn record(&self, key: &str) -> Option<&'a RecordNode> {
858        match self.get(key)? {
859            NodeType::Record(record) => Some(record),
860            _ => None,
861        }
862    }
863}
864
865impl PathStats {
866
867    /// Adds a group of a record instance, aligning it with the slots seen so far.
868    fn add_group(&mut self, group: &[(Option<String>, &NodeType)]) {
869        self.groups += 1;
870        if self.free_form {
871            return;
872        }
873
874        // Fast path: same shape as the slots seen so far.
875        let same_shape = group.len() == self.slots.len() && group.iter().zip(&self.slots).all(|(token, slot)| token_kind(token) == slot.kind());
876        if same_shape {
877            for (slot, token) in self.slots.iter_mut().zip(group) {
878                slot.record(token.1);
879            }
880            return;
881        }
882
883        // Insert the tokens that didn't align, keeping the order of both sequences. Slots are moved, not cloned.
884        let alignment = align(&self.slots, group);
885        let slot_count = self.slots.len();
886        let mut old_slots = std::mem::take(&mut self.slots).into_iter();
887        let mut merged = Vec::with_capacity(slot_count.max(group.len()));
888        let mut slot_index = 0;
889        let mut group_index = 0;
890        let mut pairs = alignment.into_iter().peekable();
891        while slot_index < slot_count || group_index < group.len() {
892            match pairs.peek().copied() {
893                Some((slot, token)) if slot == slot_index && token == group_index => {
894                    let mut stats = old_slots.next().expect("slots are consumed in order");
895                    stats.record(group[token].1);
896                    merged.push(stats);
897                    slot_index += 1;
898                    group_index += 1;
899                    pairs.next();
900                },
901                Some((slot, _)) if slot_index < slot => {
902                    merged.push(old_slots.next().expect("slots are consumed in order"));
903                    slot_index += 1;
904                },
905                Some(_) => {
906                    merged.push(SlotStats::new(&group[group_index]));
907                    group_index += 1;
908                },
909                None if slot_index < slot_count => {
910                    merged.push(old_slots.next().expect("slots are consumed in order"));
911                    slot_index += 1;
912                },
913                None => {
914                    merged.push(SlotStats::new(&group[group_index]));
915                    group_index += 1;
916                },
917            }
918        }
919        self.slots = merged;
920        if self.slots.len() > MAX_LAYOUT_SLOTS {
921            self.free_form = true;
922            self.slots.clear();
923        }
924    }
925
926    /// Converts the stats into a layout.
927    fn into_layout(self) -> RecordLayout {
928        let mut slots = vec![];
929        let mut anchor = START_ANCHOR.to_owned();
930        let mut offset = 0;
931        for slot in self.slots {
932            match slot {
933                SlotStats::Record { key, seen, contents } => {
934                    let optional = seen < self.groups;
935                    if !optional {
936                        anchor = key.to_owned();
937                        offset = 0;
938                    }
939
940                    // Like leaves, ties go to the smallest text form.
941                    let default = contents.and_then(|contents| contents.into_iter()
942                        .max_by(|(text_a, (count_a, _)), (text_b, (count_b, _))| count_a.cmp(count_b).then_with(|| text_b.cmp(text_a)))
943                        .map(|(_, (_, mut record))| {
944                            trim_record(&mut record);
945                            record
946                        }));
947                    slots.push(Slot::Record { key, optional, default });
948                },
949                SlotStats::Leaf { values, .. } => {
950                    let seen = values.values().map(|(count, _)| count).sum::<usize>();
951                    let optional = seen < self.groups;
952                    let absent_by_default = seen * 2 < self.groups;
953                    offset += 1;
954
955                    // The most common value is the default; ties go to the smallest text form, to stay deterministic.
956                    let (_, (_, mut default)) = values.into_iter().max_by(|(text_a, (count_a, _)), (text_b, (count_b, _))| count_a.cmp(count_b).then_with(|| text_b.cmp(text_a))).expect("leaves have at least one value");
957                    trim_default(&mut default);
958                    slots.push(Slot::Leaf { name: format!("{anchor}+{offset}"), default, optional, absent_by_default });
959                },
960            }
961        }
962
963        let most_common = |counts: HashMap<u8, usize>| counts.into_iter().max_by_key(|&(value, count)| (count, Reverse(value))).map(|(value, _)| value).unwrap_or_default();
964        RecordLayout {
965            name: self.name,
966            version: most_common(self.versions),
967            flags: most_common(self.flags),
968            array: self.array,
969            free_form: self.free_form,
970            slots,
971        }
972    }
973}
974
975impl SlotStats {
976
977    /// Creates the stats of a slot from its first token.
978    fn new(token: &(Option<String>, &NodeType)) -> Self {
979        match token {
980            (Some(key), node) => {
981                let mut stats = Self::Record { key: key.to_owned(), seen: 0, contents: Some(HashMap::new()) };
982                stats.record(node);
983                stats
984            },
985            (None, node) => {
986                let mut values = HashMap::new();
987                values.insert(format!("{node:?}"), (1, (*node).clone()));
988                Self::Leaf { kind: discriminant(*node), values }
989            },
990        }
991    }
992
993    /// Records another instance of the slot.
994    fn record(&mut self, node: &NodeType) {
995        match self {
996            Self::Record { seen, contents, .. } => {
997                *seen += 1;
998
999                // Only small records keep their contents: big ones would take too much memory.
1000                match (node, contents.as_mut()) {
1001                    (NodeType::Record(record), Some(counts)) if record_leaf_count(record) <= MAX_SLOT_DEFAULT_LEAVES => {
1002                        counts.entry(format!("{record:?}")).or_insert_with(|| (0, (**record).clone())).0 += 1;
1003                    },
1004                    _ => *contents = None,
1005                }
1006            },
1007            Self::Leaf { values, .. } => values.entry(format!("{node:?}")).or_insert_with(|| (0, node.clone())).0 += 1,
1008        }
1009    }
1010}
1011
1012/// Returns the file name of a layout: the one merged from all the game's campaigns, or a campaign's.
1013///
1014/// # Arguments
1015///
1016/// * `game_key` - Key of the game (`attila`).
1017/// * `campaign` - Campaign key (`main_attila`), or `None` for the merged layout.
1018pub fn layout_file_name(game_key: &str, campaign: Option<&str>) -> String {
1019    match campaign {
1020        Some(campaign) => format!("{game_key}_{campaign}.ron"),
1021        None => format!("{game_key}.ron"),
1022    }
1023}
1024
1025/// Returns the path of a child record from its parent's path and its key: the occurrence suffix is dropped, so every
1026/// occurrence shares a layout.
1027pub fn child_path(parent: &str, key: &str) -> String {
1028    let name = key.split_once('@').map(|(name, _)| name).unwrap_or(key);
1029    format!("{parent}/{name}")
1030}
1031
1032/// Returns the path of the layout of a type of a variant array (see [`VARIANT_ARRAYS`]).
1033///
1034/// # Arguments
1035///
1036/// * `path` - Path of the array.
1037/// * `code` - Type code of the groups.
1038pub fn variant_path(path: &str, code: u32) -> String {
1039    format!("{path}[{code}]")
1040}
1041
1042/// Empties the arrays longer than [`MAX_DEFAULT_ARRAY_LEN`] of a record and everything under it.
1043fn trim_record(record: &mut RecordNode) {
1044    for node in record.children_mut().iter_mut().flatten() {
1045        match node {
1046            NodeType::Record(child) => trim_record(child),
1047            node => trim_default(node),
1048        }
1049    }
1050}
1051
1052/// Empties array defaults longer than [`MAX_DEFAULT_ARRAY_LEN`].
1053fn trim_default(node: &mut NodeType) {
1054    let len = match node {
1055        NodeType::Unknown26(values) | NodeType::I8Array(values) | NodeType::U8Array(values) => values.len(),
1056        NodeType::BoolArray(values) => values.len(),
1057        NodeType::I16Array(values) | NodeType::AngleArray(values) => values.len(),
1058        NodeType::I32Array(values) => values.value().len(),
1059        NodeType::I64Array(values) => values.len(),
1060        NodeType::U16Array(values) => values.len(),
1061        NodeType::U32Array(values) => values.value().len(),
1062        NodeType::U64Array(values) => values.len(),
1063        NodeType::F32Array(values) => values.len(),
1064        NodeType::F64Array(values) => values.len(),
1065        NodeType::Coord2dArray(values) => values.len(),
1066        NodeType::Coord3dArray(values) => values.len(),
1067        NodeType::Utf16Array(values) | NodeType::AsciiArray(values) => values.len(),
1068        _ => return,
1069    };
1070    if len <= MAX_DEFAULT_ARRAY_LEN {
1071        return;
1072    }
1073
1074    match node {
1075        NodeType::Unknown26(values) | NodeType::I8Array(values) | NodeType::U8Array(values) => values.clear(),
1076        NodeType::BoolArray(values) => values.clear(),
1077        NodeType::I16Array(values) | NodeType::AngleArray(values) => values.clear(),
1078        NodeType::I32Array(values) => values.value_mut().clear(),
1079        NodeType::I64Array(values) => values.clear(),
1080        NodeType::U16Array(values) => values.clear(),
1081        NodeType::U32Array(values) => values.value_mut().clear(),
1082        NodeType::U64Array(values) => values.clear(),
1083        NodeType::F32Array(values) => values.clear(),
1084        NodeType::F64Array(values) => values.clear(),
1085        NodeType::Coord2dArray(values) => values.clear(),
1086        NodeType::Coord3dArray(values) => values.clear(),
1087        NodeType::Utf16Array(values) | NodeType::AsciiArray(values) => values.clear(),
1088        _ => {},
1089    }
1090}
1091
1092/// Returns the number of leaves of a record and everything under it.
1093fn record_leaf_count(record: &RecordNode) -> usize {
1094    record.children().iter().flatten().map(|node| match node {
1095        NodeType::Record(child) => record_leaf_count(child),
1096        _ => 1,
1097    }).sum()
1098}
1099
1100/// Adds the slots of another layout's record that a record's slots lack, each after the slot it follows there, as
1101/// optional slots that start out of built records.
1102fn add_missing_slots(slots: &mut Vec<Slot>, fallback: &[Slot]) {
1103    let mut position = 0;
1104    for slot in fallback {
1105        if let Some(own) = slots.iter().position(|own| own.id() == slot.id()) {
1106            position = own + 1;
1107            continue;
1108        }
1109
1110        let mut slot = slot.clone();
1111        match &mut slot {
1112            Slot::Leaf { optional, absent_by_default, .. } => (*optional, *absent_by_default) = (true, true),
1113            Slot::Record { optional, .. } => *optional = true,
1114        }
1115        slots.insert(position, slot);
1116        position += 1;
1117    }
1118}
1119
1120/// Collects the stats of every record of a startpos.
1121fn collect_startpos(startpos: &ESF, stats: &mut HashMap<String, PathStats>) -> Result<()> {
1122    let NodeType::Record(root) = startpos.root_node() else {
1123        return Err(RLibError::StartposLayoutError("the startpos root is not a record".to_owned()));
1124    };
1125    collect(root, root.name(), stats);
1126    Ok(())
1127}
1128
1129/// Collects the stats of a record and everything under it. Groups of variant arrays also count for their type.
1130fn collect(record: &RecordNode, path: &str, stats: &mut HashMap<String, PathStats>) {
1131    let groups = record.children().iter().map(|group| group_tokens(group)).collect::<Vec<_>>();
1132    collect_groups(record, path, &groups, stats);
1133
1134    if VARIANT_ARRAYS.contains(&record.name().as_str()) {
1135        for tokens in &groups {
1136            if let Some((None, NodeType::U32(code))) = tokens.first() {
1137                collect_groups(record, &variant_path(path, *code.value()), std::slice::from_ref(tokens), stats);
1138            }
1139        }
1140    }
1141}
1142
1143/// Collects the stats of some groups of a record, at a path, and of everything under them.
1144fn collect_groups(record: &RecordNode, path: &str, groups: &[Vec<(Option<String>, &NodeType)>], stats: &mut HashMap<String, PathStats>) {
1145    let entry = stats.entry(path.to_owned()).or_insert_with(|| PathStats { name: record.name().to_owned(), ..Default::default() });
1146    *entry.versions.entry(*record.version()).or_default() += 1;
1147    *entry.flags.entry(record.record_flags().bits()).or_default() += 1;
1148    entry.array |= record.record_flags().contains(RecordNodeFlags::HAS_NESTED_BLOCKS);
1149    for tokens in groups {
1150        entry.add_group(tokens);
1151    }
1152
1153    for tokens in groups {
1154        for (key, node) in tokens {
1155            if let (Some(key), NodeType::Record(child)) = (key, node) {
1156                collect(child, &child_path(path, key), stats);
1157            }
1158        }
1159    }
1160}
1161
1162/// Returns the children of a group as tokens: records get their key (name plus occurrence suffix), leaves `None`.
1163fn group_tokens(group: &[NodeType]) -> Vec<(Option<String>, &NodeType)> {
1164    let mut seen = HashMap::<&str, usize>::new();
1165    group.iter().map(|node| match node {
1166        NodeType::Record(record) => {
1167            let count = seen.entry(record.name()).or_default();
1168            *count += 1;
1169            let key = if *count == 1 { record.name().to_owned() } else { format!("{}@{count}", record.name()) };
1170            (Some(key), node)
1171        },
1172        _ => (None, node),
1173    }).collect()
1174}
1175
1176/// What a child or slot is, for alignment: a record by key, or a leaf by type.
1177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1178enum Kind<'a> {
1179    Record(&'a str),
1180    Leaf(Discriminant<NodeType>),
1181}
1182
1183impl SlotStats {
1184
1185    /// Returns the kind of the slot.
1186    fn kind(&self) -> Kind<'_> {
1187        match self {
1188            Self::Record { key, .. } => Kind::Record(key),
1189            Self::Leaf { kind, .. } => Kind::Leaf(*kind),
1190        }
1191    }
1192}
1193
1194impl Slot {
1195
1196    /// Returns the slot's id: the name of a leaf, or the key of a child record.
1197    pub fn id(&self) -> &str {
1198        match self {
1199            Self::Leaf { name, .. } => name,
1200            Self::Record { key, .. } => key,
1201        }
1202    }
1203
1204    /// Returns the kind of the slot.
1205    fn kind(&self) -> Kind<'_> {
1206        match self {
1207            Self::Record { key, .. } => Kind::Record(key),
1208            Self::Leaf { default, .. } => Kind::Leaf(discriminant(default)),
1209        }
1210    }
1211}
1212
1213/// Returns the kind of a group token.
1214fn token_kind<'a>(token: &'a (Option<String>, &NodeType)) -> Kind<'a> {
1215    match token {
1216        (Some(key), _) => Kind::Record(key),
1217        (None, node) => Kind::Leaf(discriminant(*node)),
1218    }
1219}
1220
1221/// Aligns a group with the slots seen so far.
1222///
1223/// # Returns
1224///
1225/// The (slot index, token index) pairs that match, in order.
1226fn align(slots: &[SlotStats], group: &[(Option<String>, &NodeType)]) -> Vec<(usize, usize)> {
1227    let left = slots.iter().map(SlotStats::kind).collect::<Vec<_>>();
1228    let right = group.iter().map(token_kind).collect::<Vec<_>>();
1229    align_kinds(&left, &right)
1230}
1231
1232/// Aligns two sequences of kinds, as matched index pairs in order.
1233///
1234/// Records are aligned first (they're few, and keyed), then the leaves between each pair of aligned records: one to
1235/// one if both runs have the same types, by longest common subsequence otherwise. Groups can have thousands of
1236/// leaves, so a single subsequence table over whole groups would be too big, and runs too long for a table (variable
1237/// length data like the AI influence maps) are aligned in a single pass.
1238fn align_kinds(left: &[Kind], right: &[Kind]) -> Vec<(usize, usize)> {
1239    let records = |kinds: &[Kind]| kinds.iter().enumerate().filter(|(_, kind)| matches!(kind, Kind::Record(_))).map(|(index, _)| index).collect::<Vec<_>>();
1240    let (left_records, right_records) = (records(left), records(right));
1241    let record_pairs = longest_common_subsequence(&left_records, &right_records, |&l, &r| left[l] == right[r])
1242        .into_iter()
1243        .map(|(l, r)| (left_records[l], right_records[r]))
1244        .collect::<Vec<_>>();
1245
1246    // Leaf runs between consecutive aligned records (and before the first and after the last).
1247    let mut pairs = vec![];
1248    let mut starts = (0, 0);
1249    for &(l, r) in record_pairs.iter().chain(std::iter::once(&(left.len(), right.len()))) {
1250        let leaves = |kinds: &[Kind], start: usize, end: usize| (start..end).filter(|&index| matches!(kinds[index], Kind::Leaf(_))).collect::<Vec<_>>();
1251        let (left_leaves, right_leaves) = (leaves(left, starts.0, l), leaves(right, starts.1, r));
1252        let same_types = left_leaves.len() == right_leaves.len() && left_leaves.iter().zip(&right_leaves).all(|(&a, &b)| left[a] == right[b]);
1253        if same_types {
1254            pairs.extend(left_leaves.iter().copied().zip(right_leaves.iter().copied()));
1255        } else if left_leaves.len().saturating_mul(right_leaves.len()) > MAX_ALIGNMENT_CELLS {
1256            pairs.extend(align_in_one_pass(&left_leaves, &right_leaves, |&a, &b| left[a] == right[b]));
1257        } else {
1258            pairs.extend(longest_common_subsequence(&left_leaves, &right_leaves, |&a, &b| left[a] == right[b]).into_iter().map(|(a, b)| (left_leaves[a], right_leaves[b])));
1259        }
1260        if l < left.len() {
1261            pairs.push((l, r));
1262        }
1263        starts = (l + 1, r + 1);
1264    }
1265    pairs
1266}
1267
1268/// Aligns two sequences in a single pass: matching elements are paired in order, and on a mismatch the element of the
1269/// longer remaining sequence is skipped.
1270fn align_in_one_pass<A, B>(left: &[A], right: &[B], matches: impl Fn(&A, &B) -> bool) -> Vec<(usize, usize)> {
1271    let mut pairs = vec![];
1272    let (mut l, mut r) = (0, 0);
1273    while l < left.len() && r < right.len() {
1274        if matches(&left[l], &right[r]) {
1275            pairs.push((l, r));
1276            l += 1;
1277            r += 1;
1278        } else if left.len() - l >= right.len() - r {
1279            l += 1;
1280        } else {
1281            r += 1;
1282        }
1283    }
1284    pairs
1285}
1286
1287/// Returns the longest common subsequence of two sequences under a matching rule, as matched index pairs in order.
1288fn longest_common_subsequence<A, B>(left: &[A], right: &[B], matches: impl Fn(&A, &B) -> bool) -> Vec<(usize, usize)> {
1289    let mut lengths = vec![vec![0usize; right.len() + 1]; left.len() + 1];
1290    for l in (0..left.len()).rev() {
1291        for r in (0..right.len()).rev() {
1292            lengths[l][r] = if matches(&left[l], &right[r]) {
1293                lengths[l + 1][r + 1] + 1
1294            } else {
1295                lengths[l + 1][r].max(lengths[l][r + 1])
1296            };
1297        }
1298    }
1299
1300    let mut pairs = vec![];
1301    let (mut l, mut r) = (0, 0);
1302    while l < left.len() && r < right.len() {
1303        if matches(&left[l], &right[r]) {
1304            pairs.push((l, r));
1305            l += 1;
1306            r += 1;
1307        } else if lengths[l + 1][r] >= lengths[l][r + 1] {
1308            l += 1;
1309        } else {
1310            r += 1;
1311        }
1312    }
1313    pairs
1314}
1315
1316/// Builds the error for a path or field missing from the layout.
1317fn missing(path: &str) -> RLibError {
1318    RLibError::StartposLayoutError(format!("the startpos layout has no {path}"))
1319}