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, VanillaDBTableNameLogic};
90use rpfm_lib::schema::Schema;
91
92use crate::dependencies::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 file_name = match game_info.vanilla_db_table_name_logic() {
509                                    VanillaDBTableNameLogic::FolderName => table_name.to_owned(),
510                                    VanillaDBTableNameLogic::DefaultName(ref default_name) => default_name.to_owned()
511                                };
512
513                                let path = format!("db/{table_name}/{file_name}");
514                                let mut result = table.search(&path, &self.pattern, self.case_sensitive, &matching_mode);
515                                result.set_source(source.clone());
516                                if !result.matches().is_empty() {
517                                    Some(result)
518                                } else {
519                                    None
520                                }
521                            }
522                        ).collect();
523                    }
524                },
525            }
526
527            self.matches.extend(temp_matches);
528        }
529
530        // Schema search runs once regardless of sources (it's not source-dependent).
531        if search_on.schema {
532            self.matches.schema = schema.search("", &pattern, case_sensitive, &matching_mode);
533        }
534
535        // Restore the pattern to what it was before searching.
536        self.pattern = pattern_original;
537    }
538
539    /// This function clears the Global Search result's data, and reset the UI for it.
540    pub fn clear(&mut self) {
541        *self = Self::default();
542    }
543
544    /// This function checks if it's possible to replace the provided matches.
545    pub fn replace_possible(&self, matches: &[MatchHolder]) -> Result<()> {
546        let patterns_same_lenght = self.pattern.len() == self.replace_text.len();
547
548        // Error out if at least one of the matches requires special conditions.
549        if matches.iter().any(|m| match m {
550            MatchHolder::Anim(_) => false,
551            MatchHolder::AnimFragmentBattle(_) => false,
552            MatchHolder::AnimPack(_) => false,
553            MatchHolder::AnimsTable(_) => false,
554            MatchHolder::Atlas(_) => false,
555            MatchHolder::Audio(_) => false,
556            MatchHolder::Bmd(_) => false,
557            MatchHolder::Db(_) => false,
558            MatchHolder::Esf(_) => false,
559            MatchHolder::GroupFormations(_) => false,
560            MatchHolder::Image(_) => false,
561            MatchHolder::Loc(_) => false,
562            MatchHolder::MatchedCombat(_) => false,
563            MatchHolder::Pack(_) => false,
564            MatchHolder::PortraitSettings(_) => false,
565            MatchHolder::RigidModel(_) => self.use_regex || !patterns_same_lenght,
566            MatchHolder::Schema(_) => false,
567            MatchHolder::SoundBank(_) => false,
568            MatchHolder::Text(_) => false,
569            MatchHolder::Uic(_) => false,
570            MatchHolder::UnitVariant(_) => false,
571            MatchHolder::Unknown(_) => self.use_regex || !patterns_same_lenght,
572            MatchHolder::Video(_) => false,
573        }) {
574            Err(RLibError::GlobalSearchReplaceRequiresSameLengthAndNotRegex)
575        } else {
576            Ok(())
577        }
578    }
579
580    /// This function performs a replace operation over the provided matches.
581    ///
582    /// NOTE: Schema matches are always ignored.
583    pub fn replace(&mut self, game_info: &GameInfo, schema: &Schema, packs: &mut BTreeMap<String, Pack>, dependencies: &mut Dependencies, matches: &[MatchHolder]) -> Result<Vec<ContainerPath>> {
584        let mut edited_paths = vec![];
585
586        // Don't do anything if we have no pattern to search.
587        if self.pattern.is_empty() {
588            return Ok(edited_paths)
589        }
590
591        // This is only useful for Packs, not for dependencies.
592        if !self.sources.iter().any(|s| matches!(s, SearchSource::Pack(_))) {
593            return Ok(edited_paths)
594        }
595
596        // Make sure we can actually do the replacements.
597        self.replace_possible(matches)?;
598
599        let mut extra_data = DecodeableExtraData::default();
600        extra_data.set_game_info(Some(game_info));
601        let extra_data = Some(extra_data);
602
603        // If we want to use regex and the pattern is invalid, use normal pattern instead of Regex.
604        let matching_mode = if self.use_regex {
605            match RegexBuilder::new(&self.pattern).case_insensitive(!self.case_sensitive).build() {
606                Ok(regex) => MatchingMode::Regex(regex),
607                Err(_) => MatchingMode::Pattern(RegexBuilder::new(&format!("(?i){}", regex::escape(&self.pattern)))
608                    .case_insensitive(!self.case_sensitive)
609                    .build()
610                    .ok()
611                ),
612            }
613        } else {
614            match RegexBuilder::new(&format!("(?i){}", regex::escape(&self.pattern))).case_insensitive(!self.case_sensitive).build() {
615                Ok(regex) => MatchingMode::Pattern(Some(regex)),
616                Err(_) => MatchingMode::Pattern(None),
617            }
618        };
619
620        // Just replace all the provided matches, one by one.
621        for match_file in matches {
622            match match_file {
623                MatchHolder::Anim(_) => continue,
624                MatchHolder::AnimFragmentBattle(search_matches) => {
625                    let container_path = ContainerPath::File(search_matches.path().to_string());
626                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
627                    if let Some(file) = file.get_mut(0) {
628
629                        // Make sure it has been decoded.
630                        let _ = file.decode(&extra_data, true, false);
631                        if let Ok(decoded) = file.decoded_mut() {
632                            let edited = match decoded {
633                                RFileDecoded::AnimFragmentBattle(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
634                                _ => unimplemented!(),
635                            };
636
637                            if edited {
638                                edited_paths.push(container_path);
639                            }
640                        }
641                    }
642                },
643                MatchHolder::AnimPack(_) => continue,
644                MatchHolder::AnimsTable(_) => continue,
645                MatchHolder::Atlas(search_matches) => {
646                    let container_path = ContainerPath::File(search_matches.path().to_string());
647                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
648                    if let Some(file) = file.get_mut(0) {
649
650                        // Make sure it has been decoded.
651                        let _ = file.decode(&None, true, false);
652                        if let Ok(decoded) = file.decoded_mut() {
653                            let edited = match decoded {
654                                RFileDecoded::Atlas(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
655                                _ => unimplemented!(),
656                            };
657
658                            if edited {
659                                edited_paths.push(container_path);
660                            }
661                        }
662                    }
663                },
664
665                MatchHolder::Audio(_) => continue,
666                MatchHolder::Bmd(_) => continue,
667
668                MatchHolder::Db(search_matches) => {
669                    let container_path = ContainerPath::File(search_matches.path().to_string());
670                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
671                    if let Some(file) = file.get_mut(0) {
672                        if let Ok(decoded) = file.decoded_mut() {
673                            let edited = match decoded {
674                                RFileDecoded::DB(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
675                                _ => unimplemented!(),
676                            };
677
678                            if edited {
679                                edited_paths.push(container_path);
680                            }
681                        }
682                    }
683                },
684
685                MatchHolder::Esf(_) => continue,
686                MatchHolder::GroupFormations(_) => continue,
687                MatchHolder::Image(_) => continue,
688                MatchHolder::Loc(search_matches) => {
689                    let container_path = ContainerPath::File(search_matches.path().to_string());
690                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
691                    if let Some(file) = file.get_mut(0) {
692                        if let Ok(decoded) = file.decoded_mut() {
693                            let edited = match decoded {
694                                RFileDecoded::Loc(table) => table.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
695                                _ => unimplemented!(),
696                            };
697
698                            if edited {
699                                edited_paths.push(container_path);
700                            }
701                        }
702                    }
703                },
704
705                MatchHolder::MatchedCombat(_) => continue,
706                MatchHolder::Pack(_) => continue,
707                MatchHolder::PortraitSettings(search_matches) => {
708                    let container_path = ContainerPath::File(search_matches.path().to_string());
709                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
710                    if let Some(file) = file.get_mut(0) {
711
712                        // Make sure it has been decoded.
713                        let _ = file.decode(&None, true, false);
714                        if let Ok(decoded) = file.decoded_mut() {
715                            let edited = match decoded {
716                                RFileDecoded::PortraitSettings(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
717                                _ => unimplemented!(),
718                            };
719
720                            if edited {
721                                edited_paths.push(container_path);
722                            }
723                        }
724                    }
725                },
726
727                MatchHolder::RigidModel(search_matches) => {
728                    let container_path = ContainerPath::File(search_matches.path().to_string());
729                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
730                    if let Some(file) = file.get_mut(0) {
731
732                        // Make sure it has been decoded.
733                        let _ = file.decode(&None, true, false);
734                        if let Ok(decoded) = file.decoded_mut() {
735                            let edited = match decoded {
736                                RFileDecoded::RigidModel(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
737                                _ => unimplemented!(),
738                            };
739
740                            if edited {
741                                edited_paths.push(container_path);
742                            }
743                        }
744                    }
745                },
746
747                MatchHolder::SoundBank(_) => continue,
748                MatchHolder::Text(search_matches) => {
749                    let container_path = ContainerPath::File(search_matches.path().to_string());
750                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
751                    if let Some(file) = file.get_mut(0) {
752
753                        // Make sure it has been decoded.
754                        let _ = file.decode(&None, true, false);
755                        if let Ok(decoded) = file.decoded_mut() {
756
757                            // NOTE: Make freaking sure this is sorted properly. Otherwise the replace logic will break when changing the lenght of the string.
758                            let mut search_matches = search_matches.clone();
759                            search_matches.matches_mut().par_sort_unstable_by(|a, b| {
760                                if a.row() == b.row() {
761                                    a.start().cmp(b.start())
762                                } else {
763                                    a.row().cmp(b.row())
764                                }
765                            });
766
767                            let edited = match decoded {
768                                RFileDecoded::Text(text) |
769                                RFileDecoded::VMD(text) |
770                                RFileDecoded::WSModel(text) => text.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, &search_matches),
771                                _ => unimplemented!(),
772                            };
773
774                            if edited {
775                                edited_paths.push(container_path);
776                            }
777                        }
778                    }
779                },
780
781                MatchHolder::Uic(_) => continue,
782                MatchHolder::UnitVariant(search_matches) => {
783                    let container_path = ContainerPath::File(search_matches.path().to_string());
784                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
785                    if let Some(file) = file.get_mut(0) {
786
787                        // Make sure it has been decoded.
788                        let _ = file.decode(&None, true, false);
789                        if let Ok(decoded) = file.decoded_mut() {
790                            let edited = match decoded {
791                                RFileDecoded::UnitVariant(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
792                                _ => unimplemented!(),
793                            };
794
795                            if edited {
796                                edited_paths.push(container_path);
797                            }
798                        }
799                    }
800                },
801
802                MatchHolder::Unknown(search_matches) => {
803                    let container_path = ContainerPath::File(search_matches.path().to_string());
804                    let mut file: Vec<&mut RFile> = packs.values_mut().flat_map(|pack| pack.files_by_path_mut(&container_path, false)).collect();
805                    if let Some(file) = file.get_mut(0) {
806
807                        // Make sure it has been decoded.
808                        let _ = file.decode(&None, true, false);
809                        if let Ok(decoded) = file.decoded_mut() {
810                            let edited = match decoded {
811                                RFileDecoded::Unknown(data) => data.replace(&self.pattern, &self.replace_text, self.case_sensitive, &matching_mode, search_matches),
812                                _ => unimplemented!(),
813                            };
814
815                            if edited {
816                                edited_paths.push(container_path);
817                            }
818                        }
819                    }
820                },
821                MatchHolder::Video(_) => continue,
822
823                // We cannot edit schemas here.
824                MatchHolder::Schema(_) => continue,
825            }
826        }
827
828        // Update the current search over the edited files.
829        self.search(game_info, schema, packs, dependencies, &edited_paths);
830
831        // Return the changed paths.
832        Ok(edited_paths)
833    }
834
835    pub fn replace_all(&mut self, game_info: &GameInfo, schema: &Schema, packs: &mut BTreeMap<String, Pack>, dependencies: &mut Dependencies) -> Result<Vec<ContainerPath>> {
836        let mut matches = vec![];
837
838        matches.extend(self.matches.anim.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
839        matches.extend(self.matches.anim_fragment_battle.iter().map(|x| MatchHolder::AnimFragmentBattle(x.clone())).collect::<Vec<_>>());
840        matches.extend(self.matches.anim_pack.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
841        matches.extend(self.matches.anims_table.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
842        matches.extend(self.matches.atlas.iter().map(|x| MatchHolder::Atlas(x.clone())).collect::<Vec<_>>());
843        matches.extend(self.matches.audio.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
844        matches.extend(self.matches.bmd.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
845        matches.extend(self.matches.db.iter().map(|x| MatchHolder::Db(x.clone())).collect::<Vec<_>>());
846        matches.extend(self.matches.esf.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
847        matches.extend(self.matches.group_formations.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
848        matches.extend(self.matches.image.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
849        matches.extend(self.matches.loc.iter().map(|x| MatchHolder::Loc(x.clone())).collect::<Vec<_>>());
850        matches.extend(self.matches.matched_combat.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
851        matches.extend(self.matches.pack.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
852        matches.extend(self.matches.portrait_settings.iter().map(|x| MatchHolder::PortraitSettings(x.clone())).collect::<Vec<_>>());
853        matches.extend(self.matches.rigid_model.iter().map(|x| MatchHolder::RigidModel(x.clone())).collect::<Vec<_>>());
854        matches.extend(self.matches.sound_bank.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
855        matches.extend(self.matches.text.iter().map(|x| MatchHolder::Text(x.clone())).collect::<Vec<_>>());
856        matches.extend(self.matches.uic.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
857        matches.extend(self.matches.unit_variant.iter().map(|x| MatchHolder::UnitVariant(x.clone())).collect::<Vec<_>>());
858        matches.extend(self.matches.unknown.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
859        matches.extend(self.matches.video.iter().map(|x| MatchHolder::Unknown(x.clone())).collect::<Vec<_>>());
860
861        self.replace(game_info, schema, packs, dependencies, &matches)
862    }
863}
864
865impl SearchOn {
866    pub fn types_to_search(&self) -> Vec<FileType> {
867        let mut types = vec![];
868
869        if *self.anim() { types.push(FileType::Anim); }
870        if *self.anim_fragment_battle() { types.push(FileType::AnimFragmentBattle); }
871        if *self.anim_pack() { types.push(FileType::AnimPack); }
872        if *self.anims_table() { types.push(FileType::AnimsTable); }
873        if *self.atlas() { types.push(FileType::Atlas); }
874        if *self.audio() { types.push(FileType::Audio); }
875        if *self.bmd() { types.push(FileType::BMD); }
876        if *self.db() { types.push(FileType::DB); }
877        if *self.esf() { types.push(FileType::ESF); }
878        if *self.group_formations() { types.push(FileType::GroupFormations); }
879        if *self.image() { types.push(FileType::Image); }
880        if *self.loc() { types.push(FileType::Loc); }
881        if *self.matched_combat() { types.push(FileType::MatchedCombat); }
882        if *self.pack() { types.push(FileType::Pack); }
883        if *self.portrait_settings() { types.push(FileType::PortraitSettings); }
884        if *self.rigid_model() { types.push(FileType::RigidModel); }
885        if *self.sound_bank() { types.push(FileType::SoundBank); }
886        if *self.text() {
887            types.push(FileType::Text);
888            types.push(FileType::VMD);
889            types.push(FileType::WSModel);
890        }
891        if *self.uic() { types.push(FileType::UIC); }
892        if *self.unit_variant() { types.push(FileType::UnitVariant); }
893        if *self.unknown() { types.push(FileType::Unknown); }
894        if *self.video() { types.push(FileType::Video); }
895
896        types
897    }
898}
899
900impl Matches {
901    pub fn retain_paths(&mut self, paths: &[String]) {
902        for path in paths {
903            self.anim.retain(|x| x.path() != path);
904            self.anim_fragment_battle.retain(|x| x.path() != path);
905            self.anim_pack.retain(|x| x.path() != path);
906            self.anims_table.retain(|x| x.path() != path);
907            self.atlas.retain(|x| x.path() != path);
908            self.audio.retain(|x| x.path() != path);
909            self.bmd.retain(|x| x.path() != path);
910            self.db.retain(|x| x.path() != path);
911            self.esf.retain(|x| x.path() != path);
912            self.group_formations.retain(|x| x.path() != path);
913            self.image.retain(|x| x.path() != path);
914            self.loc.retain(|x| x.path() != path);
915            self.matched_combat.retain(|x| x.path() != path);
916            self.pack.retain(|x| x.path() != path);
917            self.portrait_settings.retain(|x| x.path() != path);
918            self.rigid_model.retain(|x| x.path() != path);
919            self.sound_bank.retain(|x| x.path() != path);
920            self.text.retain(|x| x.path() != path);
921            self.uic.retain(|x| x.path() != path);
922            self.unit_variant.retain(|x| x.path() != path);
923            self.unknown.retain(|x| x.path() != path);
924            self.video.retain(|x| x.path() != path);
925        }
926    }
927
928    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) {
929        let matches = files.par_iter_mut()
930            .filter_map(|file| {
931                if search_on.anim && file.file_type() == FileType::Anim {
932                    /*
933                    if let Ok(RFileDecoded::Anim(data)) = file.decode(&None, false, true).transpose().unwrap() {
934                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
935                        if !result.matches().is_empty() {
936                            Some((Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
937                        } else {
938                            None
939                        }
940                    } else {
941                        None
942                    }*/
943                    None
944                } else if search_on.anim_fragment_battle && file.file_type() == FileType::AnimFragmentBattle {
945                    if let Ok(RFileDecoded::AnimFragmentBattle(data)) = file.decode(&extra_data, false, true).transpose().unwrap() {
946                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
947                        result.set_source(source.clone());
948                        result.set_container_name(file.container_name().clone().unwrap_or_default());
949                        if !result.matches().is_empty() {
950                            Some((None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
951                        } else {
952                            None
953                        }
954                    } else {
955                        None
956                    }
957                } else if search_on.anim_pack && file.file_type() == FileType::AnimPack {
958                    /*
959                    if let Ok(RFileDecoded::AnimPack(data)) = file.decode(&None, false, true).transpose().unwrap() {
960                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
961                        if !result.matches().is_empty() {
962                            Some((None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
963                        } else {
964                            None
965                        }
966                    } else {
967                        None
968                    }*/
969                    None
970                } else if search_on.anims_table && file.file_type() == FileType::AnimsTable {
971                    /*
972                    if let Ok(RFileDecoded::AnimsTable(data)) = file.decode(&None, false, true).transpose().unwrap() {
973                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
974                        if !result.matches().is_empty() {
975                            Some((None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
976                        } else {
977                            None
978                        }
979                    } else {
980                        None
981                    }*/
982                    None
983                } else if search_on.atlas && file.file_type() == FileType::Atlas {
984                    if let Ok(RFileDecoded::Atlas(data)) = file.decode(&None, false, true).transpose().unwrap() {
985                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
986                        result.set_source(source.clone());
987                        result.set_container_name(file.container_name().clone().unwrap_or_default());
988                        if !result.matches().is_empty() {
989                            Some((None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
990                        } else {
991                            None
992                        }
993                    } else {
994                        None
995                    }
996                } else if search_on.audio && file.file_type() == FileType::Audio {
997                    /*
998                    if let Ok(RFileDecoded::Audio(data)) = file.decode(&None, false, true).transpose().unwrap() {
999                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1000                        if !result.matches().is_empty() {
1001                            Some((None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
1002                        } else {
1003                            None
1004                        }
1005                    } else {
1006                        None
1007                    }*/
1008                    None
1009                } else if search_on.bmd && file.file_type() == FileType::BMD {
1010                    /*
1011                    if let Ok(RFileDecoded::BMD(data)) = file.decode(&None, false, true).transpose().unwrap() {
1012                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1013                        if !result.matches().is_empty() {
1014                            Some((None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None))
1015                        } else {
1016                            None
1017                        }
1018                    } else {
1019                        None
1020                    }*/
1021                    None
1022                } else if search_on.db && file.file_type() == FileType::DB {
1023                    if let Ok(RFileDecoded::DB(table)) = file.decoded() {
1024                        let mut result = table.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1025                        result.set_source(source.clone());
1026                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1027                        if !result.matches().is_empty() {
1028                            Some((None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None, None))
1029                        } else {
1030                            None
1031                        }
1032                    } else {
1033                        None
1034                    }
1035                } else if search_on.esf && file.file_type() == FileType::ESF {
1036                    /*
1037                    if let Ok(RFileDecoded::ESF(data)) = file.decode(&None, false, true).transpose().unwrap() {
1038                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1039                        if !result.matches().is_empty() {
1040                            Some((None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None, None))
1041                        } else {
1042                            None
1043                        }
1044                    } else {
1045                        None
1046                    }*/
1047                    None
1048                } else if search_on.group_formations && file.file_type() == FileType::GroupFormations {
1049                    /*
1050                    if let Ok(RFileDecoded::GroupFormations(data)) = file.decode(&None, false, true).transpose().unwrap() {
1051                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1052                        if !result.matches().is_empty() {
1053                            Some((None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None, None))
1054                        } else {
1055                            None
1056                        }
1057                    } else {
1058                        None
1059                    }*/
1060                    None
1061                } else if search_on.image && file.file_type() == FileType::Image {
1062                    /*
1063                    if let Ok(RFileDecoded::Image(data)) = file.decode(&None, false, true).transpose().unwrap() {
1064                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1065                        if !result.matches().is_empty() {
1066                            Some((None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None, None))
1067                        } else {
1068                            None
1069                        }
1070                    } else {
1071                        None
1072                    }*/
1073                    None
1074                } else if search_on.loc && file.file_type() == FileType::Loc {
1075                    if let Ok(RFileDecoded::Loc(table)) = file.decoded() {
1076                        let mut result = table.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1077                        result.set_source(source.clone());
1078                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1079                        if !result.matches().is_empty() {
1080                            Some((None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None, None))
1081                        } else {
1082                            None
1083                        }
1084                    } else {
1085                        None
1086                    }
1087                } else if search_on.matched_combat && file.file_type() == FileType::MatchedCombat {
1088                    /*
1089                    if let Ok(RFileDecoded::MatchedCombat(data)) = file.decode(&None, false, true).transpose().unwrap() {
1090                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1091                        if !result.matches().is_empty() {
1092                            Some((None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None, None))
1093                        } else {
1094                            None
1095                        }
1096                    } else {
1097                        None
1098                    }*/
1099                    None
1100                } else if search_on.pack && file.file_type() == FileType::Pack {
1101                    /*
1102                    if let Ok(RFileDecoded::Pack(data)) = file.decode(&None, false, true).transpose().unwrap() {
1103                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1104                        if !result.matches().is_empty() {
1105                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None, None))
1106                        } else {
1107                            None
1108                        }
1109                    } else {
1110                        None
1111                    }*/
1112                    None
1113                } else if search_on.portrait_settings && file.file_type() == FileType::PortraitSettings {
1114                    if let Ok(RFileDecoded::PortraitSettings(data)) = file.decode(&None, false, true).transpose().unwrap() {
1115                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1116                        result.set_source(source.clone());
1117                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1118                        if !result.matches().is_empty() {
1119                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None, None))
1120                        } else {
1121                            None
1122                        }
1123                    } else {
1124                        None
1125                    }
1126                } else if search_on.rigid_model && file.file_type() == FileType::RigidModel {
1127                    if let Ok(RFileDecoded::RigidModel(data)) = file.decode(&None, false, true).transpose().unwrap() {
1128                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1129                        result.set_source(source.clone());
1130                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1131                        if !result.matches().is_empty() {
1132                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None, None))
1133                        } else {
1134                            None
1135                        }
1136                    } else {
1137                        None
1138                    }
1139                } else if search_on.sound_bank && file.file_type() == FileType::SoundBank {
1140                    /*
1141                    if let Ok(RFileDecoded::SoundBank(data)) = file.decode(&None, false, true).transpose().unwrap() {
1142                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1143                        if !result.matches().is_empty() {
1144                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None, None))
1145                        } else {
1146                            None
1147                        }
1148                    } else {
1149                        None
1150                    }*/
1151                    None
1152                } else if search_on.text && file.file_type() == FileType::Text {
1153                    if let Ok(RFileDecoded::Text(text)) = file.decode(&None, false, true).transpose().unwrap() {
1154                        let mut result = text.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1155                        result.set_source(source.clone());
1156                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1157                        if !result.matches().is_empty() {
1158                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None))
1159                        } else {
1160                            None
1161                        }
1162                    } else {
1163                        None
1164                    }
1165                } else if search_on.text && file.file_type() == FileType::VMD {
1166                    if let Ok(RFileDecoded::VMD(text)) = file.decode(&None, false, true).transpose().unwrap() {
1167                        let mut result = text.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1168                        result.set_source(source.clone());
1169                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1170                        if !result.matches().is_empty() {
1171                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None))
1172                        } else {
1173                            None
1174                        }
1175                    } else {
1176                        None
1177                    }
1178                } else if search_on.text && file.file_type() == FileType::WSModel {
1179                    if let Ok(RFileDecoded::WSModel(text)) = file.decode(&None, false, true).transpose().unwrap() {
1180                        let mut result = text.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1181                        result.set_source(source.clone());
1182                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1183                        if !result.matches().is_empty() {
1184                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None, None))
1185                        } else {
1186                            None
1187                        }
1188                    } else {
1189                        None
1190                    }
1191                } else if search_on.uic && file.file_type() == FileType::UIC {
1192                    /*
1193                    if let Ok(RFileDecoded::UIC(data)) = file.decode(&None, false, true).transpose().unwrap() {
1194                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1195                        if !result.matches().is_empty() {
1196                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None, None))
1197                        } else {
1198                            None
1199                        }
1200                    } else {
1201                        None
1202                    }*/
1203                    None
1204                } else if search_on.unit_variant && file.file_type() == FileType::UnitVariant {
1205                    if let Ok(RFileDecoded::UnitVariant(data)) = file.decode(&None, false, true).transpose().unwrap() {
1206                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1207                        result.set_source(source.clone());
1208                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1209                        if !result.matches().is_empty() {
1210                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None, None))
1211                        } else {
1212                            None
1213                        }
1214                    } else {
1215                        None
1216                    }
1217                } else if search_on.unknown && file.file_type() == FileType::Unknown {
1218                    if let Ok(RFileDecoded::Unknown(data)) = file.decode(&None, false, true).transpose().unwrap() {
1219                        let mut result = data.search(file.path_in_container_raw(), pattern, case_sensitive, matching_mode);
1220                        result.set_source(source.clone());
1221                        result.set_container_name(file.container_name().clone().unwrap_or_default());
1222                        if !result.matches().is_empty() {
1223                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result), None))
1224                        } else {
1225                            None
1226                        }
1227                    } else {
1228                        None
1229                    }
1230                } else if search_on.video && file.file_type() == FileType::Video {
1231                    /*
1232                    if let Ok(RFileDecoded::Video(data)) = file.decode(&None, false, true).transpose().unwrap() {
1233                        let result = data.search(file.path_in_container_raw(), pattern, case_sensitive, &matching_mode);
1234                        if !result.matches().is_empty() {
1235                            Some((None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Some(result)))
1236                        } else {
1237                            None
1238                        }
1239                    } else {
1240                        None
1241                    }*/
1242                    None
1243                } else {
1244                    None
1245                }
1246            }
1247        ).collect::<Vec<(
1248            Option<UnknownMatches>, Option<AnimFragmentBattleMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<AtlasMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<TableMatches>,
1249            Option<UnknownMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<TableMatches>, Option<UnknownMatches>, Option<UnknownMatches>, Option<PortraitSettingsMatches>,
1250            Option<RigidModelMatches>, Option<UnknownMatches>, Option<TextMatches>, Option<UnknownMatches>, Option<UnitVariantMatches>, Option<UnknownMatches>, Option<UnknownMatches>
1251        )>>();
1252
1253        self.anim = matches.iter().filter_map(|x| x.0.clone()).collect::<Vec<_>>();
1254        self.anim_fragment_battle = matches.iter().filter_map(|x| x.1.clone()).collect::<Vec<_>>();
1255        self.anim_pack = matches.iter().filter_map(|x| x.2.clone()).collect::<Vec<_>>();
1256        self.anims_table = matches.iter().filter_map(|x| x.3.clone()).collect::<Vec<_>>();
1257        self.atlas = matches.iter().filter_map(|x| x.4.clone()).collect::<Vec<_>>();
1258        self.audio = matches.iter().filter_map(|x| x.5.clone()).collect::<Vec<_>>();
1259        self.bmd = matches.iter().filter_map(|x| x.6.clone()).collect::<Vec<_>>();
1260        self.db = matches.iter().filter_map(|x| x.7.clone()).collect::<Vec<_>>();
1261        self.esf = matches.iter().filter_map(|x| x.8.clone()).collect::<Vec<_>>();
1262        self.group_formations = matches.iter().filter_map(|x| x.9.clone()).collect::<Vec<_>>();
1263        self.image = matches.iter().filter_map(|x| x.10.clone()).collect::<Vec<_>>();
1264        self.loc = matches.iter().filter_map(|x| x.11.clone()).collect::<Vec<_>>();
1265        self.matched_combat = matches.iter().filter_map(|x| x.12.clone()).collect::<Vec<_>>();
1266        self.pack = matches.iter().filter_map(|x| x.13.clone()).collect::<Vec<_>>();
1267        self.portrait_settings = matches.iter().filter_map(|x| x.14.clone()).collect::<Vec<_>>();
1268        self.rigid_model = matches.iter().filter_map(|x| x.15.clone()).collect::<Vec<_>>();
1269        self.sound_bank = matches.iter().filter_map(|x| x.16.clone()).collect::<Vec<_>>();
1270        self.text = matches.iter().filter_map(|x| x.17.clone()).collect::<Vec<_>>();
1271        self.uic = matches.iter().filter_map(|x| x.18.clone()).collect::<Vec<_>>();
1272        self.unit_variant = matches.iter().filter_map(|x| x.19.clone()).collect::<Vec<_>>();
1273        self.unknown = matches.iter().filter_map(|x| x.20.clone()).collect::<Vec<_>>();
1274        self.video = matches.iter().filter_map(|x| x.21.clone()).collect::<Vec<_>>();
1275    }
1276
1277    /// Extends this `Matches` by appending all matches from another `Matches` instance.
1278    pub fn extend(&mut self, other: Matches) {
1279        self.anim.extend(other.anim);
1280        self.anim_fragment_battle.extend(other.anim_fragment_battle);
1281        self.anim_pack.extend(other.anim_pack);
1282        self.anims_table.extend(other.anims_table);
1283        self.atlas.extend(other.atlas);
1284        self.audio.extend(other.audio);
1285        self.bmd.extend(other.bmd);
1286        self.db.extend(other.db);
1287        self.esf.extend(other.esf);
1288        self.group_formations.extend(other.group_formations);
1289        self.image.extend(other.image);
1290        self.loc.extend(other.loc);
1291        self.matched_combat.extend(other.matched_combat);
1292        self.pack.extend(other.pack);
1293        self.portrait_settings.extend(other.portrait_settings);
1294        self.rigid_model.extend(other.rigid_model);
1295        self.sound_bank.extend(other.sound_bank);
1296        self.text.extend(other.text);
1297        self.uic.extend(other.uic);
1298        self.unit_variant.extend(other.unit_variant);
1299        self.unknown.extend(other.unknown);
1300        self.video.extend(other.video);
1301        // Note: schema is not extended here, it's handled separately.
1302    }
1303}
1304
1305impl Default for MatchingMode {
1306    fn default() -> Self {
1307        Self::Pattern(None)
1308    }
1309}
1310
1311//-------------------------------------------------------------------------------//
1312//                              Util functions
1313//-------------------------------------------------------------------------------//
1314
1315fn 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 {
1316
1317    // Only replace if the substring is actually a valid one.
1318    if current_data.get(start..end).is_some() {
1319        match matching_mode {
1320            MatchingMode::Regex(regex) => {
1321                if let Some(match_regex) = regex.find(&current_data[start..end]) {
1322                    if match_regex.start() == 0 && match_regex.end() == end - start {
1323                        current_data.replace_range(start..end, replace_pattern);
1324                    }
1325                }
1326            },
1327
1328            MatchingMode::Pattern(regex) => {
1329                let pattern = if case_sensitive || regex.is_some() {
1330                    pattern.to_owned()
1331                } else {
1332                    pattern.to_lowercase()
1333                };
1334
1335                if let Some((start_new, end_new, _)) = find_in_string(&current_data[start..end], &pattern, case_sensitive, regex).first() {
1336                    if *start_new == 0 && *end_new == end - start {
1337                        current_data.replace_range(start..end, replace_pattern);
1338                    }
1339                }
1340            }
1341        }
1342    }
1343
1344    previous_data != *current_data
1345}
1346
1347fn replace_match_bytes(replace_pattern: &str, start: usize, len: usize, data: &mut Vec<u8>) -> bool {
1348    let old_data = data[start..start + len].to_vec();
1349    data.splice(start..start + len, replace_pattern.as_bytes().to_vec());
1350    old_data != data[start..start + len]
1351}
1352
1353fn find_in_string(value: &str, pattern: &str, case_sensitive: bool, case_insensitive_regex: &Option<Regex>) -> Vec<(usize, usize, String)> {
1354    if case_sensitive {
1355        value.match_indices(&pattern).map(|(start, pat)| (start, start + pat.len(), pat.to_owned())).collect()
1356    } else if let Some(regex) = case_insensitive_regex {
1357        regex.find_iter(value).map(|m| (m.start(), m.end(), m.as_str().to_string())).collect()
1358    } else {
1359        value.to_lowercase().match_indices(&pattern).map(|(start, pat)| (start, start + pat.len(), value[start..start + pat.len()].to_string())).collect()
1360    }
1361}
1362
1363fn find_in_bytes(value: &[u8], pattern: &str, case_sensitive: bool, case_insensitive_regex: &Option<regex::bytes::Regex>) -> Vec<(usize, usize)> {
1364    if case_sensitive {
1365        let length = pattern.len();
1366        (0..value.len() - length)
1367            .filter_map(|index| if &value[index..index + length] == pattern.as_bytes() { Some((index, length)) } else { None })
1368            .collect()
1369
1370    } else if let Some(regex) = case_insensitive_regex {
1371        regex.find_iter(value).map(|m| (m.start(), m.len())).collect()
1372    } else {
1373        let pattern = pattern.as_bytes().to_ascii_lowercase();
1374        let value = value.to_ascii_lowercase();
1375        let length = pattern.len();
1376        (0..value.len() - length)
1377            .filter_map(|index| if value[index..index + length] == pattern { Some((index, length)) } else { None })
1378            .collect()    }
1379}