rpfm_extensions/search/rigid_model.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::rigidmodel::RigidModel;
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 RigidModel File.
23#[derive(Debug, Clone, Getters, MutGetters, Setters, Serialize, Deserialize)]
24#[getset(get = "pub", get_mut = "pub", set = "pub")]
25pub struct RigidModelMatches {
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<RigidModelMatch>,
40}
41
42/// This struct represents a match within an RigidModel File.
43#[derive(Debug, Clone, Eq, PartialEq, Getters, MutGetters, Serialize, Deserialize)]
44#[getset(get = "pub", get_mut = "pub")]
45pub struct RigidModelMatch {
46
47 /// If the match is in the skeleton id of the rigid.
48 skeleton_id: bool,
49
50 /// If the match is inside a mesh. The values are
51 /// - Lod index.
52 /// - Mesh index.
53 mesh_value: Option<(i32, i32)>,
54
55 /// If the match is in the name of a mesh.
56 mesh_name: bool,
57
58 /// If the match is in the name of the material.
59 mesh_mat_name: bool,
60
61 /// If the match is in the texture directory of a mesh.
62 mesh_textute_directory: bool,
63
64 /// If the match is in the filters of a mesh.
65 mesh_filters: bool,
66
67 /// If the match is in the name of one of the attachment points of a mesh.
68 mesh_att_point_name: Option<i32>,
69
70 /// If the match is in the name of one of the texture paths of a mesh.
71 mesh_texture_path: Option<i32>,
72
73 /// Byte where the match starts.
74 start: usize,
75
76 /// Byte where the match ends.
77 end: usize,
78
79 /// Matched data.
80 text: String,
81}
82
83//-------------------------------------------------------------------------------//
84// Implementations
85//-------------------------------------------------------------------------------//
86
87impl Searchable for RigidModel {
88 type SearchMatches = RigidModelMatches;
89
90 fn search(&self, file_path: &str, pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> RigidModelMatches {
91 let mut matches = RigidModelMatches::new(file_path);
92
93 match matching_mode {
94 MatchingMode::Regex(regex) => {
95
96 for entry_match in regex.find_iter(self.skeleton_id()) {
97 matches.matches.push(
98 RigidModelMatch::new(
99 true,
100 None,
101 false,
102 false,
103 false,
104 false,
105 None,
106 None,
107 entry_match.start(),
108 entry_match.end(),
109 self.skeleton_id().to_owned()
110 )
111 );
112 }
113
114 for (l_index, lod) in self.lods().iter().enumerate() {
115 for (m_index, mesh) in lod.mesh_blocks().iter().enumerate() {
116 for entry_match in regex.find_iter(mesh.mesh().name()) {
117 matches.matches.push(
118 RigidModelMatch::new(
119 false,
120 Some((l_index as i32, m_index as i32)),
121 true,
122 false,
123 false,
124 false,
125 None,
126 None,
127 entry_match.start(),
128 entry_match.end(),
129 mesh.mesh().name().to_owned()
130 )
131 );
132 }
133
134 for entry_match in regex.find_iter(mesh.material().name()) {
135 matches.matches.push(
136 RigidModelMatch::new(
137 false,
138 Some((l_index as i32, m_index as i32)),
139 false,
140 true,
141 false,
142 false,
143 None,
144 None,
145 entry_match.start(),
146 entry_match.end(),
147 mesh.material().name().to_owned()
148 )
149 );
150 }
151
152 for entry_match in regex.find_iter(mesh.material().texture_directory()) {
153 matches.matches.push(
154 RigidModelMatch::new(
155 false,
156 Some((l_index as i32, m_index as i32)),
157 false,
158 false,
159 true,
160 false,
161 None,
162 None,
163 entry_match.start(),
164 entry_match.end(),
165 mesh.material().texture_directory().to_owned()
166 )
167 );
168 }
169
170 for entry_match in regex.find_iter(mesh.material().filters()) {
171 matches.matches.push(
172 RigidModelMatch::new(
173 false,
174 Some((l_index as i32, m_index as i32)),
175 false,
176 false,
177 false,
178 true,
179 None,
180 None,
181 entry_match.start(),
182 entry_match.end(),
183 mesh.material().filters().to_owned()
184 )
185 );
186 }
187
188 for (a_index, attachment) in mesh.material().attachment_points().iter().enumerate() {
189 for entry_match in regex.find_iter(attachment.name()) {
190 matches.matches.push(
191 RigidModelMatch::new(
192 false,
193 Some((l_index as i32, m_index as i32)),
194 false,
195 false,
196 false,
197 false,
198 Some(a_index as i32),
199 None,
200 entry_match.start(),
201 entry_match.end(),
202 attachment.name().to_owned()
203 )
204 );
205 }
206 }
207
208 for (t_index, texture) in mesh.material().textures().iter().enumerate() {
209 for entry_match in regex.find_iter(texture.path()) {
210 matches.matches.push(
211 RigidModelMatch::new(
212 false,
213 Some((l_index as i32, m_index as i32)),
214 false,
215 false,
216 false,
217 false,
218 None,
219 Some(t_index as i32),
220 entry_match.start(),
221 entry_match.end(),
222 texture.path().to_owned()
223 )
224 );
225 }
226 }
227 }
228 }
229 }
230
231 MatchingMode::Pattern(regex) => {
232 let pattern = if case_sensitive || regex.is_some() {
233 pattern.to_owned()
234 } else {
235 pattern.to_lowercase()
236 };
237
238 for (start, end, _) in &find_in_string(self.skeleton_id(), &pattern, case_sensitive, regex) {
239 matches.matches.push(
240 RigidModelMatch::new(
241 true,
242 None,
243 false,
244 false,
245 false,
246 false,
247 None,
248 None,
249 *start,
250 *end,
251 self.skeleton_id().to_owned()
252 )
253 );
254 }
255
256 for (l_index, lod) in self.lods().iter().enumerate() {
257 for (m_index, mesh) in lod.mesh_blocks().iter().enumerate() {
258 for (start, end, _) in &find_in_string(mesh.mesh().name(), &pattern, case_sensitive, regex) {
259 matches.matches.push(
260 RigidModelMatch::new(
261 false,
262 Some((l_index as i32, m_index as i32)),
263 true,
264 false,
265 false,
266 false,
267 None,
268 None,
269 *start,
270 *end,
271 mesh.mesh().name().to_owned()
272 )
273 );
274 }
275
276 for (start, end, _) in &find_in_string(mesh.material().name(), &pattern, case_sensitive, regex) {
277 matches.matches.push(
278 RigidModelMatch::new(
279 false,
280 Some((l_index as i32, m_index as i32)),
281 false,
282 true,
283 false,
284 false,
285 None,
286 None,
287 *start,
288 *end,
289 mesh.material().name().to_owned()
290 )
291 );
292 }
293
294 for (start, end, _) in &find_in_string(mesh.material().texture_directory(), &pattern, case_sensitive, regex) {
295 matches.matches.push(
296 RigidModelMatch::new(
297 false,
298 Some((l_index as i32, m_index as i32)),
299 false,
300 false,
301 true,
302 false,
303 None,
304 None,
305 *start,
306 *end,
307 mesh.material().texture_directory().to_owned()
308 )
309 );
310 }
311
312 for (start, end, _) in &find_in_string(mesh.material().filters(), &pattern, case_sensitive, regex) {
313 matches.matches.push(
314 RigidModelMatch::new(
315 false,
316 Some((l_index as i32, m_index as i32)),
317 false,
318 false,
319 false,
320 true,
321 None,
322 None,
323 *start,
324 *end,
325 mesh.material().filters().to_owned()
326 )
327 );
328 }
329
330 for (a_index, attachment) in mesh.material().attachment_points().iter().enumerate() {
331 for (start, end, _) in &find_in_string(attachment.name(), &pattern, case_sensitive, regex) {
332 matches.matches.push(
333 RigidModelMatch::new(
334 false,
335 Some((l_index as i32, m_index as i32)),
336 false,
337 false,
338 false,
339 false,
340 Some(a_index as i32),
341 None,
342 *start,
343 *end,
344 attachment.name().to_owned()
345 )
346 );
347 }
348 }
349
350 for (t_index, texture) in mesh.material().textures().iter().enumerate() {
351 for (start, end, _) in &find_in_string(texture.path(), &pattern, case_sensitive, regex) {
352 matches.matches.push(
353 RigidModelMatch::new(
354 false,
355 Some((l_index as i32, m_index as i32)),
356 false,
357 false,
358 false,
359 false,
360 None,
361 Some(t_index as i32),
362 *start,
363 *end,
364 texture.path().to_owned()
365 )
366 );
367 }
368 }
369 }
370 }
371 }
372 }
373
374 matches
375 }
376}
377
378impl Replaceable for RigidModel {
379
380 fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &RigidModelMatches) -> bool {
381 let mut edited = false;
382
383 // NOTE: Due to changes in index positions, we need to do this in reverse.
384 // Otherwise we may cause one edit to generate invalid indexes for the next matches.
385 for search_match in search_matches.matches().iter().rev() {
386 edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, self);
387 }
388
389 edited
390 }
391}
392
393impl RigidModelMatches {
394
395 /// This function creates a new `RigidModelMatches` for the provided path.
396 pub fn new(path: &str) -> Self {
397 Self {
398 path: path.to_owned(),
399 matches: vec![],
400 source: SearchSource::default(),
401 container_name: String::new(),
402 }
403 }
404}
405
406impl RigidModelMatch {
407
408 /// This function creates a new `RigidModelMatch` with the provided data.
409 pub fn new(
410 skeleton_id: bool,
411 mesh_value: Option<(i32, i32)>,
412 mesh_name: bool,
413 mesh_mat_name: bool,
414 mesh_textute_directory: bool,
415 mesh_filters: bool,
416 mesh_att_point_name: Option<i32>,
417 mesh_texture_path: Option<i32>,
418 start: usize,
419 end: usize,
420 data: String
421 ) -> Self {
422 Self {
423 skeleton_id,
424 mesh_value,
425 mesh_name,
426 mesh_mat_name,
427 mesh_textute_directory,
428 mesh_filters,
429 mesh_att_point_name,
430 mesh_texture_path,
431 start,
432 end,
433 text: data,
434 }
435 }
436
437 /// This function replaces all the matches in the provided data.
438 fn replace(&self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, data: &mut RigidModel) -> bool {
439
440 // 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.
441 let (previous_data, current_data) = {
442 if self.skeleton_id {
443 (data.skeleton_id().to_owned(), data.skeleton_id_mut())
444 } else if let Some((l_index, m_index)) = self.mesh_value {
445 if let Some(lod) = data.lods_mut().get_mut(l_index as usize) {
446 if let Some(mesh) = lod.mesh_blocks_mut().get_mut(m_index as usize) {
447 if self.mesh_name {
448 (mesh.mesh().name().to_owned(), mesh.mesh_mut().name_mut())
449 } else if self.mesh_mat_name {
450 (mesh.material().name().to_owned(), mesh.material_mut().name_mut())
451 } else if self.mesh_textute_directory {
452 (mesh.material().texture_directory().to_owned(), mesh.material_mut().texture_directory_mut())
453 } else if self.mesh_filters {
454 (mesh.material().filters().to_owned(), mesh.material_mut().filters_mut())
455 } else if let Some(a_index) = self.mesh_att_point_name {
456 if let Some(att) = mesh.material_mut().attachment_points_mut().get_mut(a_index as usize) {
457 (att.name().to_owned(), att.name_mut())
458 } else {
459 return false
460 }
461 } else if let Some(t_index) = self.mesh_texture_path {
462 if let Some(tex) = mesh.material_mut().textures_mut().get_mut(t_index as usize) {
463 (tex.path().to_owned(), tex.path_mut())
464 } else {
465 return false
466 }
467 } else {
468 return false
469 }
470 } else {
471 return false
472 }
473 } else {
474 return false
475 }
476 } else {
477 return false
478 }
479 };
480
481 replace_match_string(pattern, replace_pattern, case_sensitive, matching_mode, self.start, self.end, &previous_data, current_data)
482 }
483}