Skip to main content

rpfm_extensions/search/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Global search and replace functionality for Pack files.
12//!
13//! This module provides comprehensive search capabilities across entire packs,
14//! supporting multiple file types and search modes. It's designed for finding
15//! and optionally replacing text across DB tables, Loc files, scripts, and more.
16//!
17//! # Features
18//!
19//! - **Pattern Matching**: Simple string pattern search with optional case sensitivity
20//! - **Regex Support**: Full regular expression matching via the `regex` crate
21//! - **Multi-file Search**: Search across all files in a pack simultaneously
22//! - **Dependency Search**: Optionally include vanilla and parent mod files
23//! - **Replace Support**: Batch replacement for supported file types
24//!
25//! # Supported File Types
26//!
27//! Search is implemented for the following file types via the [`Searchable`] trait:
28//!
29//! - **DB/Loc Tables** ([`table`]): Search cell contents by column or across all columns
30//! - **Text Files** ([`text`]): Lua scripts, XML, and other text formats
31//! - **Atlas Files** ([`atlas`]): Texture atlas definitions
32//! - **Portrait Settings** ([`portrait_settings`]): Unit portrait configurations
33//! - **Animation Fragments** ([`anim_fragment_battle`]): Battle animation data
34//! - **Rigid Models** ([`rigid_model`]): 3D model metadata
35//! - **Unit Variants** ([`unit_variant`]): Unit variant definitions
36//! - **Schema** ([`schema`]): Search within schema definitions
37//! - **Unknown Files** ([`unknown`]): Raw binary search
38//!
39//! # Search Sources
40//!
41//! Searches can target different data sources:
42//!
43//! - **Pack Only**: Search only the currently loaded pack
44//! - **Parent Mods**: Include files from parent mod dependencies
45//! - **Vanilla Files**: Include game's vanilla data
46//! - **All Sources**: Search everywhere
47//!
48//! # Usage Example
49//!
50//! ```ignore
51//! use rpfm_extensions::search::{GlobalSearch, SearchSource, SearchOn};
52//!
53//! let mut search = GlobalSearch::default();
54//! search.set_pattern("swordsmen".to_string());
55//! search.set_case_sensitive(false);
56//! search.set_use_regex(false);
57//! search.set_sources(vec![SearchSource::Pack("my_pack".to_string())]);
58//! search.set_search_on(SearchOn::all());
59//!
60//! // Perform the search
61//! search.search(&mut pack, &schema, &dependencies);
62//!
63//! // Access results
64//! for match_holder in search.matches().db() {
65//!     println!("Found in {}: {} matches", match_holder.path(), match_holder.matches().len());
66//! }
67//!
68//! // Perform replacement
69//! search.set_replace_text("spearmen".to_string());
70//! search.replace(&mut pack, &schema);
71//! ```
72//!
73//! # Matching Modes
74//!
75//! The [`MatchingMode`] enum determines how the search pattern is interpreted:
76//!
77//! - **Pattern**: Standard string matching with optional regex fallback
78//! - **Regex**: Full regex pattern matching with capture groups
79
80use getset::*;
81use regex::{RegexBuilder, Regex};
82use rayon::prelude::*;
83use serde_derive::{Deserialize, Serialize};
84
85use std::collections::BTreeMap;
86
87use rpfm_lib::error::{Result, RLibError};
88use rpfm_lib::files::{Container, ContainerPath, DecodeableExtraData, FileType, pack::Pack, RFile, RFileDecoded};
89use rpfm_lib::games::GameInfo;
90use rpfm_lib::schema::Schema;
91
92use crate::dependencies::{ASSEMBLY_KIT_TABLE_FILE_NAME, Dependencies};
93
94//use self::anim::AnimMatches;
95use self::anim_fragment_battle::AnimFragmentBattleMatches;
96//use self::anim_pack::AnimPackMatches;
97//use self::anims_table::AnimsTableMatches;
98use self::atlas::AtlasMatches;
99//use self::audio::AudioMatches;
100//use self::bmd::BmdMatches;
101//use self::esf::EsfMatches;
102//use self::group_formations::GroupFormationsMatches;
103//use self::image::ImageMatches;
104//use self::matched_combat::MatchedCombatMatches;
105//use self::pack::PackMatches;
106use self::portrait_settings::PortraitSettingsMatches;
107use self::rigid_model::RigidModelMatches;
108//use self::sound_bank::SoundBankMatches;
109use self::table::TableMatches;
110use self::text::TextMatches;
111//use self::uic::UicMatches;
112use self::unit_variant::UnitVariantMatches;
113use self::unknown::UnknownMatches;
114//use self::video::VideoMatches;
115use self::schema::SchemaMatches;
116
117//pub mod anim;
118pub mod anim_fragment_battle;
119//pub mod anim_pack;
120//pub mod anims_table;
121pub mod atlas;
122//pub mod audio;
123//pub mod bmd;
124//pub mod esf;
125//pub mod group_formations;
126//pub mod image;
127//pub mod matched_combat;
128//pub mod pack;
129pub mod portrait_settings;
130pub mod rigid_model;
131//pub mod sound_bank;
132pub mod table;
133pub mod text;
134//pub mod uic;
135pub mod unit_variant;
136pub mod unknown;
137//pub mod video;
138pub mod schema;
139
140//-------------------------------------------------------------------------------//
141//                             Trait definitions
142//-------------------------------------------------------------------------------//
143
144/// Trait for file types that support text searching.
145///
146/// Implementors of this trait can be scanned for text matches using various
147/// matching modes (pattern, regex, case-sensitive, etc.).
148///
149/// # Associated Types
150///
151/// - `SearchMatches`: The type returned containing match results, specific to each
152///   file type (e.g., `TableMatches` for DB/Loc files).
153pub trait Searchable {
154    /// The type containing search results for this searchable type.
155    type SearchMatches;
156
157    /// Performs a search and returns all matches.
158    ///
159    /// # Arguments
160    ///
161    /// * `file_path` - Path of the file being searched (for result reporting)
162    /// * `pattern_to_search` - The search pattern or regex
163    /// * `case_sensitive` - Whether matching should be case-sensitive
164    /// * `matching_mode` - How to interpret the pattern (literal vs regex)
165    ///
166    /// # Returns
167    ///
168    /// A match result struct containing all found matches with their locations.
169    fn search(&self, file_path: &str, pattern_to_search: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> Self::SearchMatches;
170}
171
172/// Trait for searchable types that also support replacement.
173///
174/// Extends [`Searchable`] to allow replacing matched text with new content.
175/// Not all searchable types support replacement (e.g., read-only or binary files).
176pub trait Replaceable: Searchable {
177
178    /// Replaces matched text with the replacement pattern.
179    ///
180    /// # Arguments
181    ///
182    /// * `pattern` - The original search pattern
183    /// * `replace_pattern` - The text to replace matches with (literal, no regex)
184    /// * `case_sensitive` - Whether matching should be case-sensitive
185    /// * `matching_mode` - How to interpret the search pattern
186    /// * `search_matches` - Previously found matches to replace
187    ///
188    /// # Returns
189    ///
190    /// `true` if any replacements were made, `false` if no changes occurred.
191    ///
192    /// # Note
193    ///
194    /// Replacements may fail if:
195    /// - The search matches are outdated (file was modified since search)
196    /// - The replacement text is identical to the matched text
197    fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &Self::SearchMatches) -> bool;
198}
199
200//-------------------------------------------------------------------------------//
201//                              Enums & Structs
202//-------------------------------------------------------------------------------//
203
204/// Configuration and results for a global search operation.
205///
206/// This struct holds all parameters needed to perform a search across a pack,
207/// as well as the results from the most recent search operation.
208///
209/// # Example
210///
211/// ```ignore
212/// let mut search = GlobalSearch::default();
213/// search.set_pattern("cavalry".to_string());
214/// search.set_case_sensitive(false);
215/// search.search(&mut pack, &schema, &dependencies);
216/// ```
217#[derive(Default, Debug, Clone, Getters, MutGetters, Setters, Serialize, Deserialize)]
218#[getset(get = "pub", get_mut = "pub", set = "pub")]
219pub struct GlobalSearch {
220
221    /// The text pattern or regex to search for.
222    pattern: String,
223
224    /// Text to use for replacements.
225    ///
226    /// This is always a literal string - regex capture groups are not supported
227    /// in the replacement text.
228    replace_text: String,
229
230    /// Whether the search should be case-sensitive.
231    ///
232    /// When `false`, "Cavalry" will match "cavalry", "CAVALRY", etc.
233    case_sensitive: bool,
234
235    /// Whether to interpret the pattern as a regular expression.
236    ///
237    /// When `true`, the pattern is compiled as a regex. If compilation fails,
238    /// the search falls back to literal pattern matching.
239    use_regex: bool,
240
241    /// Which data sources to include in the search.
242    sources: Vec<SearchSource>,
243
244    /// Which file types to search within.
245    search_on: SearchOn,
246
247    /// Results from the most recent search operation.
248    matches: Matches,
249
250    /// Game key for the files being searched.
251    ///
252    /// Required for decoding certain game-specific file formats during search.
253    game_key: String,
254}
255
256/// How the search pattern should be interpreted.
257///
258/// Determines whether matching is done via literal string comparison
259/// or regular expression evaluation.
260#[derive(Debug, Clone)]
261pub enum MatchingMode {
262    /// Full regular expression matching.
263    ///
264    /// The contained `Regex` is pre-compiled for efficient repeated matching.
265    Regex(Regex),
266    /// Literal pattern matching with optional regex fallback.
267    ///
268    /// If `Some(Regex)`, the regex is used for case-insensitive matching.
269    /// If `None`, simple string comparison is used.
270    Pattern(Option<Regex>),
271}
272
273/// Container for search matches from any file type.
274///
275/// Each variant wraps the specific match type for that file format,
276/// allowing uniform handling of results from different file types.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub enum MatchHolder {
279    /// Matches in animation files.
280    Anim(UnknownMatches),
281    /// Matches in animation fragment battle files.
282    AnimFragmentBattle(AnimFragmentBattleMatches),
283    /// Matches in animation pack files.
284    AnimPack(UnknownMatches),
285    /// Matches in animation table files.
286    AnimsTable(UnknownMatches),
287    /// Matches in texture atlas files.
288    Atlas(AtlasMatches),
289    /// Matches in audio files.
290    Audio(UnknownMatches),
291    /// Matches in BMD files.
292    Bmd(UnknownMatches),
293    /// Matches in DB tables.
294    Db(TableMatches),
295    /// Matches in ESF files.
296    Esf(UnknownMatches),
297    /// Matches in group formation files.
298    GroupFormations(UnknownMatches),
299    /// Matches in image files.
300    Image(UnknownMatches),
301    /// Matches in Loc (localisation) tables.
302    Loc(TableMatches),
303    /// Matches in matched combat files.
304    MatchedCombat(UnknownMatches),
305    /// Matches in pack files.
306    Pack(UnknownMatches),
307    /// Matches in portrait settings files.
308    PortraitSettings(PortraitSettingsMatches),
309    /// Matches in rigid model files.
310    RigidModel(RigidModelMatches),
311    /// Matches in sound bank files.
312    SoundBank(UnknownMatches),
313    /// Matches in text/script files.
314    Text(TextMatches),
315    /// Matches in UIC files.
316    Uic(UnknownMatches),
317    /// Matches in unit variant files.
318    UnitVariant(UnitVariantMatches),
319    /// Matches in unknown/unsupported files.
320    Unknown(UnknownMatches),
321    /// Matches in video files.
322    Video(UnknownMatches),
323    /// Matches in schema definitions.
324    Schema(SchemaMatches),
325}
326
327/// Data source to search within.
328///
329/// Controls which files are included in the search scope.
330#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
331pub enum SearchSource {
332    /// Search a specific pack identified by its key.
333    Pack(String),
334    /// Search in parent mod dependencies.
335    ParentFiles,
336    /// Search in vanilla game files.
337    GameFiles,
338    /// Search in Assembly Kit files.
339    AssKitFiles,
340}
341
342impl Default for SearchSource {
343    fn default() -> Self {
344        Self::Pack(String::new())
345    }
346}
347
348/// Configuration for which file types to include in a search.
349///
350/// Each boolean field controls whether that file type will be searched.
351/// Use `SearchOn::all()` to enable all file types or configure individually.
352#[derive(Default, Debug, Clone, Getters, Setters, Serialize, Deserialize)]
353#[getset(get = "pub", set = "pub")]
354pub struct SearchOn {
355    anim: bool,
356    anim_fragment_battle: bool,
357    anim_pack: bool,
358    anims_table: bool,
359    atlas: bool,
360    audio: bool,
361    bmd: bool,
362    db: bool,
363    esf: bool,
364    group_formations: bool,
365    image: bool,
366    loc: bool,
367    matched_combat: bool,
368    pack: bool,
369    portrait_settings: bool,
370    rigid_model: bool,
371    sound_bank: bool,
372    text: bool,
373    uic: bool,
374    unit_variant: bool,
375    unknown: bool,
376    video: bool,
377    schema: bool,
378}
379
380/// This struct stores the search matches, separated by file type.
381#[derive(Default, Debug, Clone, Getters, Serialize, Deserialize)]
382#[getset(get = "pub")]
383pub struct Matches {
384    anim: Vec<UnknownMatches>,
385    anim_fragment_battle: Vec<AnimFragmentBattleMatches>,
386    anim_pack: Vec<UnknownMatches>,
387    anims_table: Vec<UnknownMatches>,
388    atlas: Vec<AtlasMatches>,
389    audio: Vec<UnknownMatches>,
390    bmd: Vec<UnknownMatches>,
391    db: Vec<TableMatches>,
392    esf: Vec<UnknownMatches>,
393    group_formations: Vec<UnknownMatches>,
394    image: Vec<UnknownMatches>,
395    loc: Vec<TableMatches>,
396    matched_combat: Vec<UnknownMatches>,
397    pack: Vec<UnknownMatches>,
398    portrait_settings: Vec<PortraitSettingsMatches>,
399    rigid_model: Vec<RigidModelMatches>,
400    sound_bank: Vec<UnknownMatches>,
401    text: Vec<TextMatches>,
402    uic: Vec<UnknownMatches>,
403    unit_variant: Vec<UnitVariantMatches>,
404    unknown: Vec<UnknownMatches>,
405    video: Vec<UnknownMatches>,
406    schema: SchemaMatches,
407}
408
409//---------------------------------------------------------------p----------------//
410//                             Implementations
411//-------------------------------------------------------------------------------//
412
413impl GlobalSearch {
414
415    /// This function performs a search over the parts of the provided Packs, storing his results.
416    pub fn search(&mut self, game_info: &GameInfo, schema: &Schema, packs: &mut BTreeMap<String, Pack>, dependencies: &mut Dependencies, update_paths: &[ContainerPath]) {
417
418        // Don't do anything if we have no pattern to search.
419        if self.pattern.is_empty() { return }
420
421        // If we want to use regex and the pattern is invalid, don't search.
422        let matching_mode = if self.use_regex {
423            match RegexBuilder::new(&self.pattern).case_insensitive(!self.case_sensitive).build() {
424                Ok(regex) => MatchingMode::Regex(regex),
425                Err(_) => MatchingMode::Pattern(RegexBuilder::new(&format!("(?i){}", regex::escape(&self.pattern)))
426                    .case_insensitive(!self.case_sensitive)
427                    .build()
428                    .ok()
429                ),
430            }
431        } else {
432            match RegexBuilder::new(&format!("(?i){}", regex::escape(&self.pattern))).case_insensitive(!self.case_sensitive).build() {
433                Ok(regex) => MatchingMode::Pattern(Some(regex)),
434                Err(_) => MatchingMode::Pattern(None),
435            }
436        };
437
438        // For incremental updates, only support when there's exactly one Pack source.
439        let has_single_pack_source = self.sources.len() == 1 && matches!(self.sources.first(), Some(SearchSource::Pack(_)));
440        let update_paths = if !update_paths.is_empty() && has_single_pack_source {
441            let container_paths = ContainerPath::dedup(update_paths);
442            let raw_paths = container_paths.par_iter()
443                .flat_map(|container_path| packs.values().flat_map(|pack| pack.paths_raw_from_container_path(container_path)).collect::<Vec<_>>())
444                .collect::<Vec<_>>();
445
446            self.matches_mut().retain_paths(&raw_paths);
447
448            container_paths
449        }
450
451        // Otherwise, ensure we don't store results from previous searches.
452        else {
453            self.matches = Matches::default();
454
455            vec![]
456        };
457
458        // Schema matches do not support "update search".
459        self.matches.schema = SchemaMatches::default();
460
461        let pattern_original = self.pattern.to_owned();
462        if !self.case_sensitive {
463            self.pattern = self.pattern.to_lowercase();
464        }
465
466        let pattern = self.pattern.to_owned();
467        let case_sensitive = self.case_sensitive;
468        let search_on = self.search_on().clone();
469        let files_to_search = self.search_on().types_to_search();
470
471        let mut extra_data = DecodeableExtraData::default();
472        extra_data.set_game_info(Some(game_info));
473        let extra_data = Some(extra_data);
474
475        // Clone sources to avoid borrow conflict with self.matches.
476        let sources = self.sources.clone();
477
478        for source in &sources {
479            let mut temp_matches = Matches::default();
480
481            match source {
482                SearchSource::Pack(key) => {
483                    if let Some(pack) = packs.get_mut(key) {
484                        let mut files: Vec<&mut RFile> = if !update_paths.is_empty() {
485                            pack.files_by_type_and_paths_mut(&files_to_search, &update_paths, false)
486                        } else {
487                            pack.files_by_type_mut(&files_to_search)
488                        };
489
490                        temp_matches.find_matches(&pattern, case_sensitive, &matching_mode, &search_on, &mut files, schema, extra_data.clone(), source);
491                    }
492                }
493                SearchSource::ParentFiles => {
494                    let files = dependencies.files_by_types_mut(&files_to_search, false, true);
495                    temp_matches.find_matches(&pattern, case_sensitive, &matching_mode, &search_on, &mut files.into_values().collect::<Vec<_>>(), schema, extra_data.clone(), source);
496                },
497                SearchSource::GameFiles => {
498                    let files = dependencies.files_by_types_mut(&files_to_search, true, false);
499                    temp_matches.find_matches(&pattern, case_sensitive, &matching_mode, &search_on, &mut files.into_values().collect::<Vec<_>>(), schema, extra_data.clone(), source);
500                },
501
502                // Asskit files are only tables.
503                SearchSource::AssKitFiles => {
504                    if self.search_on.db {
505                        temp_matches.db = dependencies.asskit_only_db_tables()
506                            .par_iter()
507                            .filter_map(|(table_name, table)| {
508                                let path = format!("db/{table_name}/{ASSEMBLY_KIT_TABLE_FILE_NAME}");
509                                let mut result = table.search(&path, &self.pattern, self.case_sensitive, &matching_mode);
510                                result.set_source(source.clone());
511                                if !result.matches().is_empty() {
512                                    Some(result)
513                                } else {
514                                    None
515                                }
516                            }
517                        ).collect();
518                    }
519                },
520            }
521
522            self.matches.extend(temp_matches);
523        }
524
525        // Schema search runs once regardless of sources (it's not source-dependent).
526        if search_on.schema {
527            self.matches.schema = schema.search("", &pattern, case_sensitive, &matching_mode);
528        }
529
530        // Restore the pattern to what it was before searching.
531        self.pattern = pattern_original;
532    }
533
534    /// This function clears the Global Search result's data, and reset the UI for it.
535    pub fn clear(&mut self) {
536        *self = Self::default();
537    }
538
539    /// This function checks if it's possible to replace the provided matches.
540    pub fn replace_possible(&self, matches: &[MatchHolder]) -> Result<()> {
541        let patterns_same_lenght = self.pattern.len() == self.replace_text.len();
542
543        // Error out if at least one of the matches requires special conditions.
544        if matches.iter().any(|m| match m {
545            MatchHolder::Anim(_) => false,
546            MatchHolder::AnimFragmentBattle(_) => false,
547            MatchHolder::AnimPack(_) => false,
548            MatchHolder::AnimsTable(_) => false,
549            MatchHolder::Atlas(_) => false,
550            MatchHolder::Audio(_) => false,
551            MatchHolder::Bmd(_) => false,
552            MatchHolder::Db(_) => false,
553            MatchHolder::Esf(_) => false,
554            MatchHolder::GroupFormations(_) => false,
555            MatchHolder::Image(_) => false,
556            MatchHolder::Loc(_) => false,
557            MatchHolder::MatchedCombat(_) => false,
558            MatchHolder::Pack(_) => false,
559            MatchHolder::PortraitSettings(_) => false,
560            MatchHolder::RigidModel(_) => self.use_regex || !patterns_same_lenght,
561            MatchHolder::Schema(_) => false,
562            MatchHolder::SoundBank(_) => false,
563            MatchHolder::Text(_) => false,
564            MatchHolder::Uic(_) => false,
565            MatchHolder::UnitVariant(_) => false,
566            MatchHolder::Unknown(_) => self.use_regex || !patterns_same_lenght,
567            MatchHolder::Video(_) => false,
568        }) {
569            Err(RLibError::GlobalSearchReplaceRequiresSameLengthAndNotRegex)
570        } else {
571            Ok(())
572        }
573    }
574
575    /// This function performs a replace operation over the provided matches.
576    ///
577    /// NOTE: Schema matches are always ignored.
578    pub fn replace(&mut self, game_info: &GameInfo, schema: &Schema, packs: &mut BTreeMap<String, Pack>, dependencies: &mut Dependencies, matches: &[MatchHolder]) -> Result<Vec<ContainerPath>> {
579        let mut edited_paths = vec![];
580
581        // Don't do anything if we have no pattern to search.
582        if self.pattern.is_empty() {
583            return Ok(edited_paths)
584        }
585
586        // This is only useful for Packs, not for dependencies.
587        if !self.sources.iter().any(|s| matches!(s, SearchSource::Pack(_))) {
588            return Ok(edited_paths)
589        }
590
591        // Make sure we can actually do the replacements.
592        self.replace_possible(matches)?;
593
594        let mut extra_data = DecodeableExtraData::default();
595        extra_data.set_game_info(Some(game_info));
596        let extra_data = Some(extra_data);
597
598        // If we want to use regex and the pattern is invalid, use normal pattern instead of Regex.
599        let matching_mode = if self.use_regex {
600            match RegexBuilder::new(&self.pattern).case_insensitive(!self.case_sensitive).build() {
601                Ok(regex) => MatchingMode::Regex(regex),
602                Err(_) => MatchingMode::Pattern(RegexBuilder::new(&format!("(?i){}", regex::escape(&self.pattern)))
603                    .case_insensitive(!self.case_sensitive)
604                    .build()
605                    .ok()
606                ),
607            }
608        } else {
609            match RegexBuilder::new(&format!("(?i){}", regex::escape(&self.pattern))).case_insensitive(!self.case_sensitive).build() {
610                Ok(regex) => MatchingMode::Pattern(Some(regex)),
611                Err(_) => MatchingMode::Pattern(None),
612            }
613        };
614
615        // Just replace all the provided matches, one by one.
616        for match_file in matches {
617            match match_file {
618                MatchHolder::Anim(_) => continue,
619                MatchHolder::AnimFragmentBattle(search_matches) => {
620                    let container_path = ContainerPath::File(search_matches.path().to_string());
621                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
622                    if let Some(file) = file.get_mut(0) {
623
624                        // Make sure it has been decoded.
625                        let _ = file.decode(&extra_data, true, false);
626                        if let Ok(decoded) = file.decoded_mut() {
627                            let edited = match decoded {
628                                RFileDecoded::AnimFragmentBattle(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
629                                _ => unimplemented!(),
630                            };
631
632                            if edited {
633                                edited_paths.push(container_path);
634                            }
635                        }
636                    }
637                },
638                MatchHolder::AnimPack(_) => continue,
639                MatchHolder::AnimsTable(_) => continue,
640                MatchHolder::Atlas(search_matches) => {
641                    let container_path = ContainerPath::File(search_matches.path().to_string());
642                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
643                    if let Some(file) = file.get_mut(0) {
644
645                        // Make sure it has been decoded.
646                        let _ = file.decode(&None, true, false);
647                        if let Ok(decoded) = file.decoded_mut() {
648                            let edited = match decoded {
649                                RFileDecoded::Atlas(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
650                                _ => unimplemented!(),
651                            };
652
653                            if edited {
654                                edited_paths.push(container_path);
655                            }
656                        }
657                    }
658                },
659
660                MatchHolder::Audio(_) => continue,
661                MatchHolder::Bmd(_) => continue,
662
663                MatchHolder::Db(search_matches) => {
664                    let container_path = ContainerPath::File(search_matches.path().to_string());
665                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
666                    if let Some(file) = file.get_mut(0) {
667                        if let Ok(decoded) = file.decoded_mut() {
668                            let edited = match decoded {
669                                RFileDecoded::DB(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
670                                _ => unimplemented!(),
671                            };
672
673                            if edited {
674                                edited_paths.push(container_path);
675                            }
676                        }
677                    }
678                },
679
680                MatchHolder::Esf(_) => continue,
681                MatchHolder::GroupFormations(_) => continue,
682                MatchHolder::Image(_) => continue,
683                MatchHolder::Loc(search_matches) => {
684                    let container_path = ContainerPath::File(search_matches.path().to_string());
685                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
686                    if let Some(file) = file.get_mut(0) {
687                        if let Ok(decoded) = file.decoded_mut() {
688                            let edited = match decoded {
689                                RFileDecoded::Loc(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
690                                _ => unimplemented!(),
691                            };
692
693                            if edited {
694                                edited_paths.push(container_path);
695                            }
696                        }
697                    }
698                },
699
700                MatchHolder::MatchedCombat(_) => continue,
701                MatchHolder::Pack(_) => continue,
702                MatchHolder::PortraitSettings(search_matches) => {
703                    let container_path = ContainerPath::File(search_matches.path().to_string());
704                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
705                    if let Some(file) = file.get_mut(0) {
706
707                        // Make sure it has been decoded.
708                        let _ = file.decode(&None, true, false);
709                        if let Ok(decoded) = file.decoded_mut() {
710                            let edited = match decoded {
711                                RFileDecoded::PortraitSettings(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
712                                _ => unimplemented!(),
713                            };
714
715                            if edited {
716                                edited_paths.push(container_path);
717                            }
718                        }
719                    }
720                },
721
722                MatchHolder::RigidModel(search_matches) => {
723                    let container_path = ContainerPath::File(search_matches.path().to_string());
724                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
725                    if let Some(file) = file.get_mut(0) {
726
727                        // Make sure it has been decoded.
728                        let _ = file.decode(&None, true, false);
729                        if let Ok(decoded) = file.decoded_mut() {
730                            let edited = match decoded {
731                                RFileDecoded::RigidModel(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
732                                _ => unimplemented!(),
733                            };
734
735                            if edited {
736                                edited_paths.push(container_path);
737                            }
738                        }
739                    }
740                },
741
742                MatchHolder::SoundBank(_) => continue,
743                MatchHolder::Text(search_matches) => {
744                    let container_path = ContainerPath::File(search_matches.path().to_string());
745                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
746                    if let Some(file) = file.get_mut(0) {
747
748                        // Make sure it has been decoded.
749                        let _ = file.decode(&None, true, false);
750                        if let Ok(decoded) = file.decoded_mut() {
751
752                            // NOTE: Make freaking sure this is sorted properly. Otherwise the replace logic will break when changing the lenght of the string.
753                            let mut search_matches = search_matches.clone();
754                            search_matches.matches_mut().par_sort_unstable_by(|a, b| {
755                                if a.row() == b.row() {
756                                    a.start().cmp(b.start())
757                                } else {
758                                    a.row().cmp(b.row())
759                                }
760                            });
761
762                            let edited = match decoded {
763                                RFileDecoded::Text(text) |
764                                RFileDecoded::VMD(text) |
765                                RFileDecoded::WSModel(text) => text.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, &search_matches),
766                                _ => unimplemented!(),
767                            };
768
769                            if edited {
770                                edited_paths.push(container_path);
771                            }
772                        }
773                    }
774                },
775
776                MatchHolder::Uic(_) => continue,
777                MatchHolder::UnitVariant(search_matches) => {
778                    let container_path = ContainerPath::File(search_matches.path().to_string());
779                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
780                    if let Some(file) = file.get_mut(0) {
781
782                        // Make sure it has been decoded.
783                        let _ = file.decode(&None, true, false);
784                        if let Ok(decoded) = file.decoded_mut() {
785                            let edited = match decoded {
786                                RFileDecoded::UnitVariant(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
787                                _ => unimplemented!(),
788                            };
789
790                            if edited {
791                                edited_paths.push(container_path);
792                            }
793                        }
794                    }
795                },
796
797                MatchHolder::Unknown(search_matches) => {
798                    let container_path = ContainerPath::File(search_matches.path().to_string());
799                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
800                    if let Some(file) = file.get_mut(0) {
801
802                        // Make sure it has been decoded.
803                        let _ = file.decode(&None, true, false);
804                        if let Ok(decoded) = file.decoded_mut() {
805                            let edited = match decoded {
806                                RFileDecoded::Unknown(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
807                                _ => unimplemented!(),
808                            };
809
810                            if edited {
811                                edited_paths.push(container_path);
812                            }
813                        }
814                    }
815                },
816                MatchHolder::Video(_) => continue,
817
818                // We cannot edit schemas here.
819                MatchHolder::Schema(_) => continue,
820            }
821        }
822
823        // Update the current search over the edited files.
824        self.search(game_info, schema, packs, dependencies, &edited_paths);
825
826        // Return the changed paths.
827        Ok(edited_paths)
828    }
829
830    pub fn replace_all(&mut self, game_info: &GameInfo, schema: &Schema, packs: &mut BTreeMap<String, Pack>, dependencies: &mut Dependencies) -> Result<Vec<ContainerPath>> {
831        let mut matches = vec![];
832
833        matches.extend(self.matches.anim.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
834        matches.extend(self.matches.anim_fragment_battle.iter().map(|x| MatchHolder::AnimFragmentBattle(x.clone())).collect::<Vec<_>>());
835        matches.extend(self.matches.anim_pack.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
836        matches.extend(self.matches.anims_table.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
837        matches.extend(self.matches.atlas.iter().map(|x| MatchHolder::Atlas(x.clone())).collect::<Vec<_>>());
838        matches.extend(self.matches.audio.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
839        matches.extend(self.matches.bmd.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
840        matches.extend(self.matches.db.iter().map(|x| MatchHolder::Db(x.clone())).collect::<Vec<_>>());
841        matches.extend(self.matches.esf.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
842        matches.extend(self.matches.group_formations.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
843        matches.extend(self.matches.image.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
844        matches.extend(self.matches.loc.iter().map(|x| MatchHolder::Loc(x.clone())).collect::<Vec<_>>());
845        matches.extend(self.matches.matched_combat.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
846        matches.extend(self.matches.pack.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
847        matches.extend(self.matches.portrait_settings.iter().map(|x| MatchHolder::PortraitSettings(x.clone())).collect::<Vec<_>>());
848        matches.extend(self.matches.rigid_model.iter().map(|x| MatchHolder::RigidModel(x.clone())).collect::<Vec<_>>());
849        matches.extend(self.matches.sound_bank.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
850        matches.extend(self.matches.text.iter().map(|x| MatchHolder::Text(x.clone())).collect::<Vec<_>>());
851        matches.extend(self.matches.uic.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
852        matches.extend(self.matches.unit_variant.iter().map(|x| MatchHolder::UnitVariant(x.clone())).collect::<Vec<_>>());
853        matches.extend(self.matches.unknown.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
854        matches.extend(self.matches.video.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
855
856        self.replace(game_info, schema, packs, dependencies, &matches)
857    }
858}
859
860impl SearchOn {
861    pub fn types_to_search(&self) -> Vec<FileType> {
862        let mut types = vec![];
863
864        if *self.anim() { types.push(FileType::Anim); }
865        if *self.anim_fragment_battle() { types.push(FileType::AnimFragmentBattle); }
866        if *self.anim_pack() { types.push(FileType::AnimPack); }
867        if *self.anims_table() { types.push(FileType::AnimsTable); }
868        if *self.atlas() { types.push(FileType::Atlas); }
869        if *self.audio() { types.push(FileType::Audio); }
870        if *self.bmd() { types.push(FileType::BMD); }
871        if *self.db() { types.push(FileType::DB); }
872        if *self.esf() { types.push(FileType::ESF); }
873        if *self.group_formations() { types.push(FileType::GroupFormations); }
874        if *self.image() { types.push(FileType::Image); }
875        if *self.loc() { types.push(FileType::Loc); }
876        if *self.matched_combat() { types.push(FileType::MatchedCombat); }
877        if *self.pack() { types.push(FileType::Pack); }
878        if *self.portrait_settings() { types.push(FileType::PortraitSettings); }
879        if *self.rigid_model() { types.push(FileType::RigidModel); }
880        if *self.sound_bank() { types.push(FileType::SoundBank); }
881        if *self.text() {
882            types.push(FileType::Text);
883            types.push(FileType::VMD);
884            types.push(FileType::WSModel);
885        }
886        if *self.uic() { types.push(FileType::UIC); }
887        if *self.unit_variant() { types.push(FileType::UnitVariant); }
888        if *self.unknown() { types.push(FileType::Unknown); }
889        if *self.video() { types.push(FileType::Video); }
890
891        types
892    }
893}
894
895impl Matches {
896    pub fn retain_paths(&mut self, paths: &[String]) {
897        for path in paths {
898            self.anim.retain(|x| x.path() != path);
899            self.anim_fragment_battle.retain(|x| x.path() != path);
900            self.anim_pack.retain(|x| x.path() != path);
901            self.anims_table.retain(|x| x.path() != path);
902            self.atlas.retain(|x| x.path() != path);
903            self.audio.retain(|x| x.path() != path);
904            self.bmd.retain(|x| x.path() != path);
905            self.db.retain(|x| x.path() != path);
906            self.esf.retain(|x| x.path() != path);
907            self.group_formations.retain(|x| x.path() != path);
908            self.image.retain(|x| x.path() != path);
909            self.loc.retain(|x| x.path() != path);
910            self.matched_combat.retain(|x| x.path() != path);
911            self.pack.retain(|x| x.path() != path);
912            self.portrait_settings.retain(|x| x.path() != path);
913            self.rigid_model.retain(|x| x.path() != path);
914            self.sound_bank.retain(|x| x.path() != path);
915            self.text.retain(|x| x.path() != path);
916            self.uic.retain(|x| x.path() != path);
917            self.unit_variant.retain(|x| x.path() != path);
918            self.unknown.retain(|x| x.path() != path);
919            self.video.retain(|x| x.path() != path);
920        }
921    }
922
923    #[allow(clippy::too_many_arguments)]
924    pub fn find_matches(&mut self, pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_on: &SearchOn, files: &mut Vec<&mut RFile>, _schema: &Schema, extra_data: Option<DecodeableExtraData>, source: &SearchSource) {
925        let matches = files.par_iter_mut()
926            .filter_map(|file| {
927                if search_on.anim && file.file_type() == FileType::Anim {
928                    /*
929                    if let Ok(RFileDecoded::Anim(data)) = file.decode(&None, false, true).transpose().unwrap() {
930                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
931                        if !result.matches().is_empty() {
932                            Some((Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
933                        } else {
934                            None
935                        }
936                    } else {
937                        None
938                    }*/
939                    None
940                } else if search_on.anim_fragment_battle && file.file_type() == FileType::AnimFragmentBattle {
941                    if let Ok(RFileDecoded::AnimFragmentBattle(data)) = file.decode(&extra_data, false, true).transpose().unwrap() {
942                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
943                        result.set_source(source.clone());
944                        result.set_container_name(file.container_name().clone().unwrap_or_default());
945                        if !result.matches().is_empty() {
946                            Some((None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
947                        } else {
948                            None
949                        }
950                    } else {
951                        None
952                    }
953                } else if search_on.anim_pack && file.file_type() == FileType::AnimPack {
954                    /*
955                    if let Ok(RFileDecoded::AnimPack(data)) = file.decode(&None, false, true).transpose().unwrap() {
956                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
957                        if !result.matches().is_empty() {
958                            Some((None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
959                        } else {
960                            None
961                        }
962                    } else {
963                        None
964                    }*/
965                    None
966                } else if search_on.anims_table && file.file_type() == FileType::AnimsTable {
967                    /*
968                    if let Ok(RFileDecoded::AnimsTable(data)) = file.decode(&None, false, true).transpose().unwrap() {
969                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
970                        if !result.matches().is_empty() {
971                            Some((None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
972                        } else {
973                            None
974                        }
975                    } else {
976                        None
977                    }*/
978                    None
979                } else if search_on.atlas && file.file_type() == FileType::Atlas {
980                    if let Ok(RFileDecoded::Atlas(data)) = file.decode(&None, false, true).transpose().unwrap() {
981                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
982                        result.set_source(source.clone());
983                        result.set_container_name(file.container_name().clone().unwrap_or_default());
984                        if !result.matches().is_empty() {
985                            Some((None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
986                        } else {
987                            None
988                        }
989                    } else {
990                        None
991                    }
992                } else if search_on.audio && file.file_type() == FileType::Audio {
993                    /*
994                    if let Ok(RFileDecoded::Audio(data)) = file.decode(&None, false, true).transpose().unwrap() {
995                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
996                        if !result.matches().is_empty() {
997                            Some((None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
998                        } else {
999                            None
1000                        }
1001                    } else {
1002                        None
1003                    }*/
1004                    None
1005                } else if search_on.bmd && file.file_type() == FileType::BMD {
1006                    /*
1007                    if let Ok(RFileDecoded::BMD(data)) = file.decode(&None, false, true).transpose().unwrap() {
1008                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1009                        if !result.matches().is_empty() {
1010                            Some((None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
1011                        } else {
1012                            None
1013                        }
1014                    } else {
1015                        None
1016                    }*/
1017                    None
1018                } else if search_on.db && file.file_type() == FileType::DB {
1019                    if let Ok(RFileDecoded::DB(table)) = file.decoded() {
1020                        let mut result = table.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1021                        result.set_source(source.clone());
1022                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1023                        if !result.matches().is_empty() {
1024                            Some((None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None))
1025                        } else {
1026                            None
1027                        }
1028                    } else {
1029                        None
1030                    }
1031                } else if search_on.esf && file.file_type() == FileType::ESF {
1032                    /*
1033                    if let Ok(RFileDecoded::ESF(data)) = file.decode(&None, false, true).transpose().unwrap() {
1034                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1035                        if !result.matches().is_empty() {
1036                            Some((None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None))
1037                        } else {
1038                            None
1039                        }
1040                    } else {
1041                        None
1042                    }*/
1043                    None
1044                } else if search_on.group_formations && file.file_type() == FileType::GroupFormations {
1045                    /*
1046                    if let Ok(RFileDecoded::GroupFormations(data)) = file.decode(&None, false, true).transpose().unwrap() {
1047                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1048                        if !result.matches().is_empty() {
1049                            Some((None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None))
1050                        } else {
1051                            None
1052                        }
1053                    } else {
1054                        None
1055                    }*/
1056                    None
1057                } else if search_on.image && file.file_type() == FileType::Image {
1058                    /*
1059                    if let Ok(RFileDecoded::Image(data)) = file.decode(&None, false, true).transpose().unwrap() {
1060                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1061                        if !result.matches().is_empty() {
1062                            Some((None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None))
1063                        } else {
1064                            None
1065                        }
1066                    } else {
1067                        None
1068                    }*/
1069                    None
1070                } else if search_on.loc && file.file_type() == FileType::Loc {
1071                    if let Ok(RFileDecoded::Loc(table)) = file.decoded() {
1072                        let mut result = table.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1073                        result.set_source(source.clone());
1074                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1075                        if !result.matches().is_empty() {
1076                            Some((None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None))
1077                        } else {
1078                            None
1079                        }
1080                    } else {
1081                        None
1082                    }
1083                } else if search_on.matched_combat && file.file_type() == FileType::MatchedCombat {
1084                    /*
1085                    if let Ok(RFileDecoded::MatchedCombat(data)) = file.decode(&None, false, true).transpose().unwrap() {
1086                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1087                        if !result.matches().is_empty() {
1088                            Some((None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None))
1089                        } else {
1090                            None
1091                        }
1092                    } else {
1093                        None
1094                    }*/
1095                    None
1096                } else if search_on.pack && file.file_type() == FileType::Pack {
1097                    /*
1098                    if let Ok(RFileDecoded::Pack(data)) = file.decode(&None, false, true).transpose().unwrap() {
1099                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1100                        if !result.matches().is_empty() {
1101                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None))
1102                        } else {
1103                            None
1104                        }
1105                    } else {
1106                        None
1107                    }*/
1108                    None
1109                } else if search_on.portrait_settings && file.file_type() == FileType::PortraitSettings {
1110                    if let Ok(RFileDecoded::PortraitSettings(data)) = file.decode(&None, false, true).transpose().unwrap() {
1111                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1112                        result.set_source(source.clone());
1113                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1114                        if !result.matches().is_empty() {
1115                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None))
1116                        } else {
1117                            None
1118                        }
1119                    } else {
1120                        None
1121                    }
1122                } else if search_on.rigid_model && file.file_type() == FileType::RigidModel {
1123                    if let Ok(RFileDecoded::RigidModel(data)) = file.decode(&None, false, true).transpose().unwrap() {
1124                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1125                        result.set_source(source.clone());
1126                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1127                        if !result.matches().is_empty() {
1128                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None))
1129                        } else {
1130                            None
1131                        }
1132                    } else {
1133                        None
1134                    }
1135                } else if search_on.sound_bank && file.file_type() == FileType::SoundBank {
1136                    /*
1137                    if let Ok(RFileDecoded::SoundBank(data)) = file.decode(&None, false, true).transpose().unwrap() {
1138                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1139                        if !result.matches().is_empty() {
1140                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None))
1141                        } else {
1142                            None
1143                        }
1144                    } else {
1145                        None
1146                    }*/
1147                    None
1148                } else if search_on.text && file.file_type() == FileType::Text {
1149                    if let Ok(RFileDecoded::Text(text)) = file.decode(&None, false, true).transpose().unwrap() {
1150                        let mut result = text.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1151                        result.set_source(source.clone());
1152                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1153                        if !result.matches().is_empty() {
1154                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None))
1155                        } else {
1156                            None
1157                        }
1158                    } else {
1159                        None
1160                    }
1161                } else if search_on.text && file.file_type() == FileType::VMD {
1162                    if let Ok(RFileDecoded::VMD(text)) = file.decode(&None, false, true).transpose().unwrap() {
1163                        let mut result = text.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1164                        result.set_source(source.clone());
1165                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1166                        if !result.matches().is_empty() {
1167                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None))
1168                        } else {
1169                            None
1170                        }
1171                    } else {
1172                        None
1173                    }
1174                } else if search_on.text && file.file_type() == FileType::WSModel {
1175                    if let Ok(RFileDecoded::WSModel(text)) = file.decode(&None, false, true).transpose().unwrap() {
1176                        let mut result = text.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1177                        result.set_source(source.clone());
1178                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1179                        if !result.matches().is_empty() {
1180                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None))
1181                        } else {
1182                            None
1183                        }
1184                    } else {
1185                        None
1186                    }
1187                } else if search_on.uic && file.file_type() == FileType::UIC {
1188                    /*
1189                    if let Ok(RFileDecoded::UIC(data)) = file.decode(&None, false, true).transpose().unwrap() {
1190                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1191                        if !result.matches().is_empty() {
1192                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None))
1193                        } else {
1194                            None
1195                        }
1196                    } else {
1197                        None
1198                    }*/
1199                    None
1200                } else if search_on.unit_variant && file.file_type() == FileType::UnitVariant {
1201                    if let Ok(RFileDecoded::UnitVariant(data)) = file.decode(&None, false, true).transpose().unwrap() {
1202                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1203                        result.set_source(source.clone());
1204                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1205                        if !result.matches().is_empty() {
1206                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None))
1207                        } else {
1208                            None
1209                        }
1210                    } else {
1211                        None
1212                    }
1213                } else if search_on.unknown && file.file_type() == FileType::Unknown {
1214                    if let Ok(RFileDecoded::Unknown(data)) = file.decode(&None, false, true).transpose().unwrap() {
1215                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1216                        result.set_source(source.clone());
1217                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1218                        if !result.matches().is_empty() {
1219                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None))
1220                        } else {
1221                            None
1222                        }
1223                    } else {
1224                        None
1225                    }
1226                } else if search_on.video && file.file_type() == FileType::Video {
1227                    /*
1228                    if let Ok(RFileDecoded::Video(data)) = file.decode(&None, false, true).transpose().unwrap() {
1229                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1230                        if !result.matches().is_empty() {
1231                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result)))
1232                        } else {
1233                            None
1234                        }
1235                    } else {
1236                        None
1237                    }*/
1238                    None
1239                } else {
1240                    None
1241                }
1242            }
1243        ).collect::<Vec<(
1244            Option<UnknownMatches>, Option<AnimFragmentBattleMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<AtlasMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<TableMatches>,
1245            Option<UnknownMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<TableMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<PortraitSettingsMatches>,
1246            Option<RigidModelMatches>, Option<UnknownMatches>, Option<TextMatches>, Option<UnknownMatches>, Option<UnitVariantMatches>, Option<UnknownMatches>, Option<UnknownMatches>
1247        )>>();
1248
1249        self.anim = matches.iter().filter_map(|x| x.0.clone()).collect::<Vec<_>>();
1250        self.anim_fragment_battle = matches.iter().filter_map(|x| x.1.clone()).collect::<Vec<_>>();
1251        self.anim_pack = matches.iter().filter_map(|x| x.2.clone()).collect::<Vec<_>>();
1252        self.anims_table = matches.iter().filter_map(|x| x.3.clone()).collect::<Vec<_>>();
1253        self.atlas = matches.iter().filter_map(|x| x.4.clone()).collect::<Vec<_>>();
1254        self.audio = matches.iter().filter_map(|x| x.5.clone()).collect::<Vec<_>>();
1255        self.bmd = matches.iter().filter_map(|x| x.6.clone()).collect::<Vec<_>>();
1256        self.db = matches.iter().filter_map(|x| x.7.clone()).collect::<Vec<_>>();
1257        self.esf = matches.iter().filter_map(|x| x.8.clone()).collect::<Vec<_>>();
1258        self.group_formations = matches.iter().filter_map(|x| x.9.clone()).collect::<Vec<_>>();
1259        self.image = matches.iter().filter_map(|x| x.10.clone()).collect::<Vec<_>>();
1260        self.loc = matches.iter().filter_map(|x| x.11.clone()).collect::<Vec<_>>();
1261        self.matched_combat = matches.iter().filter_map(|x| x.12.clone()).collect::<Vec<_>>();
1262        self.pack = matches.iter().filter_map(|x| x.13.clone()).collect::<Vec<_>>();
1263        self.portrait_settings = matches.iter().filter_map(|x| x.14.clone()).collect::<Vec<_>>();
1264        self.rigid_model = matches.iter().filter_map(|x| x.15.clone()).collect::<Vec<_>>();
1265        self.sound_bank = matches.iter().filter_map(|x| x.16.clone()).collect::<Vec<_>>();
1266        self.text = matches.iter().filter_map(|x| x.17.clone()).collect::<Vec<_>>();
1267        self.uic = matches.iter().filter_map(|x| x.18.clone()).collect::<Vec<_>>();
1268        self.unit_variant = matches.iter().filter_map(|x| x.19.clone()).collect::<Vec<_>>();
1269        self.unknown = matches.iter().filter_map(|x| x.20.clone()).collect::<Vec<_>>();
1270        self.video = matches.iter().filter_map(|x| x.21.clone()).collect::<Vec<_>>();
1271    }
1272
1273    /// Extends this `Matches` by appending all matches from another `Matches` instance.
1274    pub fn extend(&mut self, other: Matches) {
1275        self.anim.extend(other.anim);
1276        self.anim_fragment_battle.extend(other.anim_fragment_battle);
1277        self.anim_pack.extend(other.anim_pack);
1278        self.anims_table.extend(other.anims_table);
1279        self.atlas.extend(other.atlas);
1280        self.audio.extend(other.audio);
1281        self.bmd.extend(other.bmd);
1282        self.db.extend(other.db);
1283        self.esf.extend(other.esf);
1284        self.group_formations.extend(other.group_formations);
1285        self.image.extend(other.image);
1286        self.loc.extend(other.loc);
1287        self.matched_combat.extend(other.matched_combat);
1288        self.pack.extend(other.pack);
1289        self.portrait_settings.extend(other.portrait_settings);
1290        self.rigid_model.extend(other.rigid_model);
1291        self.sound_bank.extend(other.sound_bank);
1292        self.text.extend(other.text);
1293        self.uic.extend(other.uic);
1294        self.unit_variant.extend(other.unit_variant);
1295        self.unknown.extend(other.unknown);
1296        self.video.extend(other.video);
1297        // Note: schema is not extended here, it's handled separately.
1298    }
1299}
1300
1301impl Default for MatchingMode {
1302    fn default() -> Self {
1303        Self::Pattern(None)
1304    }
1305}
1306
1307//-------------------------------------------------------------------------------//
1308//                              Util functions
1309//-------------------------------------------------------------------------------//
1310
1311#[allow(clippy::too_many_arguments)]
1312fn replace_match_string(pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, start: usize, end: usize, previous_data: &str, current_data: &mut String) -> bool {
1313
1314    // Only replace if the substring is actually a valid one.
1315    if current_data.get(start..end).is_some() {
1316        match matching_mode {
1317            MatchingMode::Regex(regex) => {
1318                if let Some(match_regex) = regex.find(&current_data[start..end]) {
1319                    if match_regex.start() == 0 && match_regex.end() == end - start {
1320                        current_data.replace_range(start..end, replace_pattern);
1321                    }
1322                }
1323            },
1324
1325            MatchingMode::Pattern(regex) => {
1326                let pattern = if case_sensitive || regex.is_some() {
1327                    pattern.to_owned()
1328                } else {
1329                    pattern.to_lowercase()
1330                };
1331
1332                if let Some((start_new, end_new, _)) = find_in_string(&current_data[start..end], &pattern, case_sensitive, regex).first() {
1333                    if *start_new == 0 && *end_new == end - start {
1334                        current_data.replace_range(start..end, replace_pattern);
1335                    }
1336                }
1337            }
1338        }
1339    }
1340
1341    previous_data != *current_data
1342}
1343
1344fn replace_match_bytes(replace_pattern: &str, start: usize, len: usize, data: &mut Vec<u8>) -> bool {
1345    let old_data = data[start..start + len].to_vec();
1346    data.splice(start..start + len, replace_pattern.as_bytes().to_vec());
1347    old_data != data[start..start + len]
1348}
1349
1350fn find_in_string(value: &str, pattern: &str, case_sensitive: bool, case_insensitive_regex: &Option<Regex>) -> Vec<(usize, usize, String)> {
1351    if case_sensitive {
1352        value.match_indices(&pattern).map(|(start, pat)| (start, start + pat.len(), pat.to_owned())).collect()
1353    } else if let Some(regex) = case_insensitive_regex {
1354        regex.find_iter(value).map(|m| (m.start(), m.end(), m.as_str().to_string())).collect()
1355    } else {
1356        value.to_lowercase().match_indices(&pattern).map(|(start, pat)| (start, start + pat.len(), value[start..start + pat.len()].to_string())).collect()
1357    }
1358}
1359
1360fn find_in_bytes(value: &[u8], pattern: &str, case_sensitive: bool, case_insensitive_regex: &Option<regex::bytes::Regex>) -> Vec<(usize, usize)> {
1361    if case_sensitive {
1362        let length = pattern.len();
1363        (0..value.len() - length)
1364            .filter_map(|index| if &value[index..index + length] == pattern.as_bytes() { Some((index, length)) } else { None })
1365            .collect()
1366
1367    } else if let Some(regex) = case_insensitive_regex {
1368        regex.find_iter(value).map(|m| (m.start(), m.len())).collect()
1369    } else {
1370        let pattern = pattern.as_bytes().to_ascii_lowercase();
1371        let value = value.to_ascii_lowercase();
1372        let length = pattern.len();
1373        (0..value.len() - length)
1374            .filter_map(|index| if value[index..index + length] == pattern { Some((index, length)) } else { None })
1375            .collect()    }
1376}