Skip to main content

rpfm_extensions/search/
atlas.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
11use getset::{Getters, MutGetters, Setters};
12use serde_derive::{Deserialize, Serialize};
13
14use rpfm_lib::files::atlas::Atlas;
15
16use super::{find_in_string, MatchingMode, replace_match_string, Replaceable, SearchSource, Searchable};
17
18//-------------------------------------------------------------------------------//
19//                              Enums & Structs
20//-------------------------------------------------------------------------------//
21
22/// This struct represents all the matches of the global search within an Atlas File.
23#[derive(Debug, Clone, Getters, MutGetters, Setters, Serialize, Deserialize)]
24#[getset(get = "pub", get_mut = "pub", set = "pub")]
25pub struct AtlasMatches {
26
27    /// The path of the file.
28    path: String,
29
30    /// The search source that produced these matches.
31    #[serde(default)]
32    source: SearchSource,
33
34    /// The container name (pack file name) this file belongs to.
35    #[serde(default)]
36    container_name: String,
37
38    /// The list of matches within the file.
39    matches: Vec<AtlasMatch>,
40}
41
42/// This struct represents a match within an Atlas File.
43#[derive(Debug, Clone, Eq, PartialEq, Getters, MutGetters, Serialize, Deserialize)]
44#[getset(get = "pub", get_mut = "pub")]
45pub struct AtlasMatch {
46
47    /// The name of the column where the match is.
48    column_name: String,
49
50    /// The logical index of the column where the match is.
51    column_number: u32,
52
53    /// The row number of this match.
54    row_number: i64,
55
56    /// Byte where the match starts.
57    start: usize,
58
59    /// Byte where the match ends.
60    end: usize,
61
62    /// The contents of the matched cell.
63    text: String,
64}
65
66//-------------------------------------------------------------------------------//
67//                             Implementations
68//-------------------------------------------------------------------------------//
69
70impl Searchable for Atlas {
71    type SearchMatches = AtlasMatches;
72
73    fn search(&self, file_path: &str, pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> AtlasMatches {
74        let mut matches = AtlasMatches::new(file_path);
75
76        match matching_mode {
77            MatchingMode::Regex(regex) => {
78                for (row, entry) in self.entries().iter().enumerate() {
79                    for entry_match in regex.find_iter(entry.string1()) {
80                        matches.matches.push(
81                            AtlasMatch::new(
82                                "String1",
83                                0,
84                                row as i64,
85                                entry_match.start(),
86                                entry_match.end(),
87                                entry.string1(),
88                            )
89                        );
90                    }
91
92                    for entry_match in regex.find_iter(entry.string2()) {
93                        matches.matches.push(
94                            AtlasMatch::new(
95                                "String2",
96                                0,
97                                row as i64,
98                                entry_match.start(),
99                                entry_match.end(),
100                                entry.string2(),
101                            )
102                        );
103                    }
104                }
105            }
106
107            MatchingMode::Pattern(regex) => {
108                let pattern = if case_sensitive || regex.is_some() {
109                    pattern.to_owned()
110                } else {
111                    pattern.to_lowercase()
112                };
113
114                for (row, entry) in self.entries().iter().enumerate() {
115                    for (start, end, _) in &find_in_string(entry.string1(), &pattern, case_sensitive, regex) {
116                        matches.matches.push(
117                            AtlasMatch::new(
118                                "String1",
119                                0,
120                                row as i64,
121                                *start,
122                                *end,
123                                entry.string1(),
124                            )
125                        );
126                    }
127
128                    for (start, end, _) in &find_in_string(entry.string2(), &pattern, case_sensitive, regex) {
129                        matches.matches.push(
130                            AtlasMatch::new(
131                                "String2",
132                                1,
133                                row as i64,
134                                *start,
135                                *end,
136                                entry.string2(),
137                            )
138                        );
139                    }
140                }
141            }
142        }
143
144        matches
145    }
146}
147
148impl Replaceable for Atlas {
149
150    fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &AtlasMatches) -> bool {
151        let mut edited = false;
152
153        // NOTE: Due to changes in index positions, we need to do this in reverse.
154        // Otherwise we may cause one edit to generate invalid indexes for the next matches.
155        for search_match in search_matches.matches().iter().rev() {
156            edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, self);
157        }
158
159        edited
160    }
161}
162
163impl AtlasMatches {
164
165    /// This function creates a new `AtlasMatches` for the provided path.
166    pub fn new(path: &str) -> Self {
167        Self {
168            path: path.to_owned(),
169            matches: vec![],
170            source: SearchSource::default(),
171            container_name: String::new(),
172        }
173    }
174}
175
176impl AtlasMatch {
177
178    /// This function creates a new `AtlasMatch` with the provided data.
179    pub fn new(column_name: &str, column_number: u32, row_number: i64, start: usize, end: usize, contents: &str) -> Self {
180        Self {
181            column_name: column_name.to_owned(),
182            column_number,
183            row_number,
184            start,
185            end,
186            text: contents.to_owned(),
187        }
188    }
189
190    /// This function replaces all the matches in the provided data.
191    fn replace(&self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, data: &mut Atlas) -> bool {
192        let mut edited = false;
193
194        if let Some(entry) = data.entries_mut().get_mut(self.row_number as usize) {
195
196            // Get all the previous data and references of data to manipulate here, so we don't duplicate a lot of code per-field in the match mode part.
197            let (previous_data, current_data) = {
198                if self.column_number == 0 {
199                    (entry.string1().to_owned(), entry.string1_mut())
200                } else if self.column_number == 1 {
201                    (entry.string2().to_owned(), entry.string2_mut())
202                }
203
204                // This is an error.
205                else {
206                    return false
207                }
208            };
209
210            edited = replace_match_string(pattern, replace_pattern, case_sensitive, matching_mode, self.start, self.end, &previous_data, current_data);
211        }
212
213        edited
214    }
215}