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