rpfm_extensions/search/portrait_settings.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::portrait_settings::PortraitSettings;
15
16use super::{find_in_string, MatchingMode, Replaceable, SearchSource, Searchable, replace_match_string};
17
18//-------------------------------------------------------------------------------//
19// Enums & Structs
20//-------------------------------------------------------------------------------//
21
22/// This struct represents all the matches of the global search within an PortraitSettings File.
23#[derive(Debug, Clone, Getters, MutGetters, Setters, Serialize, Deserialize)]
24#[getset(get = "pub", get_mut = "pub", set = "pub")]
25pub struct PortraitSettingsMatches {
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<PortraitSettingsMatch>,
40}
41
42/// This struct represents a match within an PortraitSettings File.
43#[derive(Debug, Default, Clone, Eq, PartialEq, Getters, MutGetters, Serialize, Deserialize)]
44#[getset(get = "pub", get_mut = "pub")]
45pub struct PortraitSettingsMatch {
46
47 /// The index of the entry in question in the PortraitSettings file. Not sure if the ids are unique, so we use the index.
48 entry: usize,
49
50 /// If the match corresponds to the id.
51 id: bool,
52
53 /// If the match corresponds to a camera settings head (skeleton node) value.
54 camera_settings_head: bool,
55
56 /// If the match corresponds to a camera settings body (skeleton node) value.
57 camera_settings_body: bool,
58
59 /// If the match corresponds to a variant value. We have their index and a bool for each value.
60 variant: Option<(usize, bool, bool, bool, bool, bool)>,
61
62 /// Byte where the match starts.
63 start: usize,
64
65 /// Byte where the match ends.
66 end: usize,
67
68 /// Matched data.
69 text: String,
70}
71
72//-------------------------------------------------------------------------------//
73// Implementations
74//-------------------------------------------------------------------------------//
75
76impl Searchable for PortraitSettings {
77 type SearchMatches = PortraitSettingsMatches;
78
79 fn search(&self, file_path: &str, pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> PortraitSettingsMatches {
80 let mut matches = PortraitSettingsMatches::new(file_path);
81
82 match matching_mode {
83 MatchingMode::Regex(regex) => {
84 for (index, data) in self.entries().iter().enumerate() {
85 for entry_match in regex.find_iter(data.id()) {
86 matches.matches.push(
87 PortraitSettingsMatch::new(
88 index,
89 true,
90 false,
91 false,
92 None,
93 entry_match.start(),
94 entry_match.end(),
95 data.id().to_owned()
96 )
97 );
98 }
99
100 for entry_match in regex.find_iter(data.camera_settings_head().skeleton_node()) {
101 matches.matches.push(
102 PortraitSettingsMatch::new(
103 index,
104 false,
105 true,
106 false,
107 None,
108 entry_match.start(),
109 entry_match.end(),
110 data.camera_settings_head().skeleton_node().to_owned()
111 )
112 );
113 }
114
115 if let Some(camera_body) = data.camera_settings_body() {
116 for entry_match in regex.find_iter(camera_body.skeleton_node()) {
117 matches.matches.push(
118 PortraitSettingsMatch::new(
119 index,
120 false,
121 false,
122 true,
123 None,
124 entry_match.start(),
125 entry_match.end(),
126 camera_body.skeleton_node().to_owned()
127 )
128 );
129 }
130 }
131
132 for (vindex, variant) in data.variants().iter().enumerate() {
133 for entry_match in regex.find_iter(variant.filename()) {
134 matches.matches.push(
135 PortraitSettingsMatch::new(
136 index,
137 false,
138 false,
139 false,
140 Some((vindex, true, false, false, false, false)),
141 entry_match.start(),
142 entry_match.end(),
143 variant.filename().to_owned()
144 )
145 );
146 }
147
148 for entry_match in regex.find_iter(variant.file_diffuse()) {
149 matches.matches.push(
150 PortraitSettingsMatch::new(
151 index,
152 false,
153 false,
154 false,
155 Some((vindex, false, true, false, false, false)),
156 entry_match.start(),
157 entry_match.end(),
158 variant.file_diffuse().to_owned()
159 )
160 );
161 }
162
163 for entry_match in regex.find_iter(variant.file_mask_1()) {
164 matches.matches.push(
165 PortraitSettingsMatch::new(
166 index,
167 false,
168 false,
169 false,
170 Some((vindex, false, false, true, false, false)),
171 entry_match.start(),
172 entry_match.end(),
173 variant.file_mask_1().to_owned()
174 )
175 );
176 }
177
178 for entry_match in regex.find_iter(variant.file_mask_2()) {
179 matches.matches.push(
180 PortraitSettingsMatch::new(
181 index,
182 false,
183 false,
184 false,
185 Some((vindex, false, false, false, true, false)),
186 entry_match.start(),
187 entry_match.end(),
188 variant.file_mask_2().to_owned()
189 )
190 );
191 }
192
193 for entry_match in regex.find_iter(variant.file_mask_3()) {
194 matches.matches.push(
195 PortraitSettingsMatch::new(
196 index,
197 false,
198 false,
199 false,
200 Some((vindex, false, false, false, false, true)),
201 entry_match.start(),
202 entry_match.end(),
203 variant.file_mask_3().to_owned()
204 )
205 );
206 }
207 }
208 }
209 }
210
211 MatchingMode::Pattern(regex) => {
212 let pattern = if case_sensitive || regex.is_some() {
213 pattern.to_owned()
214 } else {
215 pattern.to_lowercase()
216 };
217
218 for (index, data) in self.entries().iter().enumerate() {
219
220 for (start, end, _) in &find_in_string(data.id(), &pattern, case_sensitive, regex) {
221 matches.matches.push(
222 PortraitSettingsMatch::new(
223 index,
224 true,
225 false,
226 false,
227 None,
228 *start,
229 *end,
230 data.id().to_owned()
231 )
232 );
233
234 }
235
236 for (start, end, _) in &find_in_string(data.camera_settings_head().skeleton_node(), &pattern, case_sensitive, regex) {
237 matches.matches.push(
238 PortraitSettingsMatch::new(
239 index,
240 false,
241 true,
242 false,
243 None,
244 *start,
245 *end,
246 data.camera_settings_head().skeleton_node().to_owned()
247 )
248 );
249 }
250
251 if let Some(camera_body) = data.camera_settings_body() {
252 for (start, end, _) in &find_in_string(camera_body.skeleton_node(), &pattern, case_sensitive, regex) {
253 matches.matches.push(
254 PortraitSettingsMatch::new(
255 index,
256 false,
257 false,
258 true,
259 None,
260 *start,
261 *end,
262 camera_body.skeleton_node().to_owned()
263 )
264 );
265 }
266 }
267
268 for (vindex, variant) in data.variants().iter().enumerate() {
269 for (start, end, _) in &find_in_string(variant.filename(), &pattern, case_sensitive, regex) {
270 matches.matches.push(
271 PortraitSettingsMatch::new(
272 index,
273 false,
274 false,
275 false,
276 Some((vindex, true, false, false, false, false)),
277 *start,
278 *end,
279 variant.filename().to_owned()
280 )
281 );
282 }
283
284 for (start, end, _) in &find_in_string(variant.file_diffuse(), &pattern, case_sensitive, regex) {
285 matches.matches.push(
286 PortraitSettingsMatch::new(
287 index,
288 false,
289 false,
290 false,
291 Some((vindex, false, true, false, false, false)),
292 *start,
293 *end,
294 variant.file_diffuse().to_owned()
295 )
296 );
297 }
298
299 for (start, end, _) in &find_in_string(variant.file_mask_1(), &pattern, case_sensitive, regex) {
300 matches.matches.push(
301 PortraitSettingsMatch::new(
302 index,
303 false,
304 false,
305 false,
306 Some((vindex, false, false, true, false, false)),
307 *start,
308 *end,
309 variant.file_mask_1().to_owned()
310 )
311 );
312 }
313
314 for (start, end, _) in &find_in_string(variant.file_mask_2(), &pattern, case_sensitive, regex) {
315 matches.matches.push(
316 PortraitSettingsMatch::new(
317 index,
318 false,
319 false,
320 false,
321 Some((vindex, false, false, false, true, false)),
322 *start,
323 *end,
324 variant.file_mask_2().to_owned()
325 )
326 );
327 }
328
329 for (start, end, _) in &find_in_string(variant.file_mask_3(), &pattern, case_sensitive, regex) {
330 matches.matches.push(
331 PortraitSettingsMatch::new(
332 index,
333 false,
334 false,
335 false,
336 Some((vindex, false, false, false, false, true)),
337 *start,
338 *end,
339 variant.file_mask_3().to_owned()
340 )
341 );
342 }
343 }
344 }
345 }
346 }
347
348 matches
349 }
350}
351
352impl Replaceable for PortraitSettings {
353
354 fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &PortraitSettingsMatches) -> bool {
355 let mut edited = false;
356
357 // NOTE: Due to changes in index positions, we need to do this in reverse.
358 // Otherwise we may cause one edit to generate invalid indexes for the next matches.
359 for search_match in search_matches.matches().iter().rev() {
360 edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, self);
361 }
362
363 edited
364 }
365}
366
367impl PortraitSettingsMatches {
368
369 /// This function creates a new `PortraitSettingsMatches` for the provided path.
370 pub fn new(path: &str) -> Self {
371 Self {
372 path: path.to_owned(),
373 matches: vec![],
374 source: SearchSource::default(),
375 container_name: String::new(),
376 }
377 }
378}
379
380impl PortraitSettingsMatch {
381
382 /// This function creates a new `PortraitSettingsMatch` with the provided data.
383 pub fn new(entry: usize, id: bool, camera_settings_head: bool, camera_settings_body: bool, variant: Option<(usize, bool, bool, bool, bool, bool)>, start: usize, end: usize, data: String) -> Self {
384 Self {
385 entry,
386 id,
387 camera_settings_head,
388 camera_settings_body,
389 variant,
390 start,
391 end,
392 text: data
393 }
394 }
395
396 /// This function replaces all the matches in the provided data.
397 fn replace(&self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, data: &mut PortraitSettings) -> bool {
398 let mut edited = false;
399
400 if let Some(entry) = data.entries_mut().get_mut(self.entry) {
401
402 // 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.
403 let (previous_data, current_data) = {
404 if self.id {
405 (entry.id().to_owned(), entry.id_mut())
406 } else if self.camera_settings_head {
407 (entry.camera_settings_head().skeleton_node().to_owned(), entry.camera_settings_head_mut().skeleton_node_mut())
408 } else if self.camera_settings_body {
409 match entry.camera_settings_body_mut() {
410 Some(body) => (body.skeleton_node().to_owned(), body.skeleton_node_mut()),
411 None => return false,
412 }
413 } else if let Some((vindex, filename, file_diffuse, file_mask_1, file_mask_2, file_mask_3)) = self.variant {
414 match entry.variants_mut().get_mut(vindex) {
415 Some(variant) => {
416 if filename {
417 (variant.filename().to_owned(), variant.filename_mut())
418 } else if file_diffuse {
419 (variant.file_diffuse().to_owned(), variant.file_diffuse_mut())
420 } else if file_mask_1 {
421 (variant.file_mask_1().to_owned(), variant.file_mask_1_mut())
422 } else if file_mask_2 {
423 (variant.file_mask_2().to_owned(), variant.file_mask_2_mut())
424 } else if file_mask_3 {
425 (variant.file_mask_3().to_owned(), variant.file_mask_3_mut())
426 } else {
427 return false;
428 }
429 }
430 None => return false,
431 }
432 }
433
434 // This is an error.
435 else {
436 return false
437 }
438 };
439
440 edited = replace_match_string(pattern, replace_pattern, case_sensitive, matching_mode, self.start, self.end, &previous_data, current_data);
441 }
442
443 edited
444 }
445}