rpfm_extensions/search/anim_fragment_battle.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::anim_fragment_battle::AnimFragmentBattle;
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 Anim Fragment Battle File.
23#[derive(Debug, Clone, Getters, MutGetters, Setters, Serialize, Deserialize)]
24#[getset(get = "pub", get_mut = "pub", set = "pub")]
25pub struct AnimFragmentBattleMatches {
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<AnimFragmentBattleMatch>,
40}
41
42/// Inner-row hit info for a fragment match.
43///
44/// Tuple shape: `(subrow, is_file_path, is_meta_file_path, is_snd_file_path)`.
45pub type AnimFragmentBattleAnimRefHit = (usize, bool, bool, bool);
46
47/// Per-entry hit info: outer row, optional inner sub-entry hit, and the five booleans
48/// telling which top-level entry columns produced the match.
49///
50/// Tuple shape: `(row, sub_entry, is_animation_id, is_blend_in_time, is_selection_weight, is_weapon_bone, is_uk_4)`.
51pub type AnimFragmentBattleEntryHit = (usize, Option<AnimFragmentBattleAnimRefHit>, bool, bool, bool, bool, bool);
52
53/// This struct represents a match within an Anim Fragment Battle File.
54#[derive(Debug, Clone, Eq, PartialEq, Getters, MutGetters, Serialize, Deserialize)]
55#[getset(get = "pub", get_mut = "pub")]
56pub struct AnimFragmentBattleMatch {
57
58 /// If the match corresponds to a skeleton name value.
59 skeleton_name: bool,
60
61 /// If the match corresponds to a table name value.
62 table_name: bool,
63
64 /// If the match corresponds to a mount table name value.
65 mount_table_name: bool,
66
67 /// If the match corresponds to a unmount table name value.
68 unmount_table_name: bool,
69
70 /// If the match corresponds to a locomotion table name value.
71 locomotion_graph: bool,
72
73 /// If the match corresponds to an entry in the table view.
74 entry: Option<AnimFragmentBattleEntryHit>,
75
76 /// Byte where the match starts.
77 start: usize,
78
79 /// Byte where the match ends.
80 end: usize,
81
82 /// The contents of the matched cell.
83 text: String,
84}
85
86//-------------------------------------------------------------------------------//
87// Implementations
88//-------------------------------------------------------------------------------//
89
90impl Searchable for AnimFragmentBattle {
91 type SearchMatches = AnimFragmentBattleMatches;
92
93 fn search(&self, file_path: &str, pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> AnimFragmentBattleMatches {
94 let mut matches = AnimFragmentBattleMatches::new(file_path);
95
96 match matching_mode {
97 MatchingMode::Regex(regex) => {
98 for entry_match in regex.find_iter(self.skeleton_name()) {
99 matches.matches.push(
100 AnimFragmentBattleMatch::new(
101 true,
102 false,
103 false,
104 false,
105 false,
106 None,
107 entry_match.start(),
108 entry_match.end(),
109 self.skeleton_name().to_owned()
110 )
111 );
112 }
113
114 for entry_match in regex.find_iter(self.table_name()) {
115 matches.matches.push(
116 AnimFragmentBattleMatch::new(
117 false,
118 true,
119 false,
120 false,
121 false,
122 None,
123 entry_match.start(),
124 entry_match.end(),
125 self.table_name().to_owned()
126 )
127 );
128 }
129
130 for entry_match in regex.find_iter(self.mount_table_name()) {
131 matches.matches.push(
132 AnimFragmentBattleMatch::new(
133 false,
134 false,
135 true,
136 false,
137 false,
138 None,
139 entry_match.start(),
140 entry_match.end(),
141 self.mount_table_name().to_owned()
142 )
143 );
144 }
145
146 for entry_match in regex.find_iter(self.unmount_table_name()) {
147 matches.matches.push(
148 AnimFragmentBattleMatch::new(
149 false,
150 false,
151 false,
152 true,
153 false,
154 None,
155 entry_match.start(),
156 entry_match.end(),
157 self.unmount_table_name().to_owned()
158 )
159 );
160 }
161
162 for entry_match in regex.find_iter(self.locomotion_graph()) {
163 matches.matches.push(
164 AnimFragmentBattleMatch::new(
165 false,
166 false,
167 false,
168 false,
169 true,
170 None,
171 entry_match.start(),
172 entry_match.end(),
173 self.locomotion_graph().to_owned()
174 )
175 );
176 }
177
178 for (row, entry) in self.entries().iter().enumerate() {
179 for (subrow, anim_refs) in entry.anim_refs().iter().enumerate() {
180 for entry_match in regex.find_iter(anim_refs.file_path()) {
181 matches.matches.push(
182 AnimFragmentBattleMatch::new(
183 false,
184 false,
185 false,
186 false,
187 false,
188 Some((row, Some((subrow, true, false, false)), false, false, false, false, false)),
189 entry_match.start(),
190 entry_match.end(),
191 anim_refs.file_path().to_owned()
192 )
193 );
194 }
195
196 for entry_match in regex.find_iter(anim_refs.meta_file_path()) {
197 matches.matches.push(
198 AnimFragmentBattleMatch::new(
199 false,
200 false,
201 false,
202 false,
203 false,
204 Some((row, Some((subrow, false, true, false)), false, false, false, false, false)),
205 entry_match.start(),
206 entry_match.end(),
207 anim_refs.meta_file_path().to_owned()
208 )
209 );
210 }
211
212 for entry_match in regex.find_iter(anim_refs.snd_file_path()) {
213 matches.matches.push(
214 AnimFragmentBattleMatch::new(
215 false,
216 false,
217 false,
218 false,
219 false,
220 Some((row, Some((subrow, false, false, true)), false, false, false, false, false)),
221 entry_match.start(),
222 entry_match.end(),
223 anim_refs.snd_file_path().to_owned()
224 )
225 );
226 }
227 }
228
229 for entry_match in regex.find_iter(entry.filename()) {
230 matches.matches.push(
231 AnimFragmentBattleMatch::new(
232 false,
233 false,
234 false,
235 false,
236 false,
237 Some((row, None, true, false, false, false, false)),
238 entry_match.start(),
239 entry_match.end(),
240 entry.filename().to_owned()
241 )
242 );
243 }
244
245 for entry_match in regex.find_iter(entry.metadata()) {
246 matches.matches.push(
247 AnimFragmentBattleMatch::new(
248 false,
249 false,
250 false,
251 false,
252 false,
253 Some((row, None, false, true, false, false, false)),
254 entry_match.start(),
255 entry_match.end(),
256 entry.metadata().to_owned()
257 )
258 );
259 }
260
261 for entry_match in regex.find_iter(entry.metadata_sound()) {
262 matches.matches.push(
263 AnimFragmentBattleMatch::new(
264 false,
265 false,
266 false,
267 false,
268 false,
269 Some((row, None, false, false, true, false, false)),
270 entry_match.start(),
271 entry_match.end(),
272 entry.metadata_sound().to_owned()
273 )
274 );
275 }
276
277 for entry_match in regex.find_iter(entry.skeleton_type()) {
278 matches.matches.push(
279 AnimFragmentBattleMatch::new(
280 false,
281 false,
282 false,
283 false,
284 false,
285 Some((row, None, false, false, false, true, false)),
286 entry_match.start(),
287 entry_match.end(),
288 entry.skeleton_type().to_owned()
289 )
290 );
291 }
292
293 for entry_match in regex.find_iter(entry.uk_4()) {
294 matches.matches.push(
295 AnimFragmentBattleMatch::new(
296 false,
297 false,
298 false,
299 false,
300 false,
301 Some((row, None, false, false, false, false, true)),
302 entry_match.start(),
303 entry_match.end(),
304 entry.uk_4().to_owned()
305 )
306 );
307 }
308 }
309 }
310
311 MatchingMode::Pattern(regex) => {
312 let pattern = if case_sensitive || regex.is_some() {
313 pattern.to_owned()
314 } else {
315 pattern.to_lowercase()
316 };
317
318 for (start, end, _) in &find_in_string(self.skeleton_name(), &pattern, case_sensitive, regex) {
319 matches.matches.push(
320 AnimFragmentBattleMatch::new(
321 true,
322 false,
323 false,
324 false,
325 false,
326 None,
327 *start,
328 *end,
329 self.skeleton_name().to_owned()
330 )
331 );
332 }
333
334 for (start, end, _) in &find_in_string(self.table_name(), &pattern, case_sensitive, regex) {
335 matches.matches.push(
336 AnimFragmentBattleMatch::new(
337 false,
338 true,
339 false,
340 false,
341 false,
342 None,
343 *start,
344 *end,
345 self.table_name().to_owned()
346 )
347 );
348 }
349
350 for (start, end, _) in &find_in_string(self.mount_table_name(), &pattern, case_sensitive, regex) {
351 matches.matches.push(
352 AnimFragmentBattleMatch::new(
353 false,
354 false,
355 true,
356 false,
357 false,
358 None,
359 *start,
360 *end,
361 self.mount_table_name().to_owned()
362 )
363 );
364 }
365
366 for (start, end, _) in &find_in_string(self.unmount_table_name(), &pattern, case_sensitive, regex) {
367 matches.matches.push(
368 AnimFragmentBattleMatch::new(
369 false,
370 false,
371 false,
372 true,
373 false,
374 None,
375 *start,
376 *end,
377 self.unmount_table_name().to_owned()
378 )
379 );
380 }
381
382 for (start, end, _) in &find_in_string(self.locomotion_graph(), &pattern, case_sensitive, regex) {
383 matches.matches.push(
384 AnimFragmentBattleMatch::new(
385 false,
386 false,
387 false,
388 false,
389 true,
390 None,
391 *start,
392 *end,
393 self.locomotion_graph().to_owned()
394 )
395 );
396 }
397
398 for (row, entry) in self.entries().iter().enumerate() {
399 for (subrow, anim_refs) in entry.anim_refs().iter().enumerate() {
400 for (start, end, _) in &find_in_string(anim_refs.file_path(), &pattern, case_sensitive, regex) {
401 matches.matches.push(
402 AnimFragmentBattleMatch::new(
403 false,
404 false,
405 false,
406 false,
407 false,
408 Some((row, Some((subrow, true, false, false)), false, false, false, false, false)),
409 *start,
410 *end,
411 anim_refs.file_path().to_owned()
412 )
413 );
414 }
415
416 for (start, end, _) in &find_in_string(anim_refs.meta_file_path(), &pattern, case_sensitive, regex) {
417 matches.matches.push(
418 AnimFragmentBattleMatch::new(
419 false,
420 false,
421 false,
422 false,
423 false,
424 Some((row, Some((subrow, false, true, false)), false, false, false, false, false)),
425 *start,
426 *end,
427 anim_refs.meta_file_path().to_owned()
428 )
429 );
430 }
431
432 for (start, end, _) in &find_in_string(anim_refs.snd_file_path(), &pattern, case_sensitive, regex) {
433 matches.matches.push(
434 AnimFragmentBattleMatch::new(
435 false,
436 false,
437 false,
438 false,
439 false,
440 Some((row, Some((subrow, false, false, true)), false, false, false, false, false)),
441 *start,
442 *end,
443 anim_refs.snd_file_path().to_owned()
444 )
445 );
446 }
447 }
448
449 for (start, end, _) in &find_in_string(entry.filename(), &pattern, case_sensitive, regex) {
450 matches.matches.push(
451 AnimFragmentBattleMatch::new(
452 false,
453 false,
454 false,
455 false,
456 false,
457 Some((row, None, true, false, false, false, false)),
458 *start,
459 *end,
460 entry.filename().to_owned()
461 )
462 );
463 }
464
465 for (start, end, _) in &find_in_string(entry.metadata(), &pattern, case_sensitive, regex) {
466 matches.matches.push(
467 AnimFragmentBattleMatch::new(
468 false,
469 false,
470 false,
471 false,
472 false,
473 Some((row, None, false, true, false, false, false)),
474 *start,
475 *end,
476 entry.metadata().to_owned()
477 )
478 );
479 }
480
481 for (start, end, _) in &find_in_string(entry.metadata_sound(), &pattern, case_sensitive, regex) {
482 matches.matches.push(
483 AnimFragmentBattleMatch::new(
484 false,
485 false,
486 false,
487 false,
488 false,
489 Some((row, None, false, false, true, false, false)),
490 *start,
491 *end,
492 entry.metadata_sound().to_owned()
493 )
494 );
495 }
496
497 for (start, end, _) in &find_in_string(entry.skeleton_type(), &pattern, case_sensitive, regex) {
498 matches.matches.push(
499 AnimFragmentBattleMatch::new(
500 false,
501 false,
502 false,
503 false,
504 false,
505 Some((row, None, false, false, false, true, false)),
506 *start,
507 *end,
508 entry.skeleton_type().to_owned()
509 )
510 );
511 }
512
513 for (start, end, _) in &find_in_string(entry.uk_4(), &pattern, case_sensitive, regex) {
514 matches.matches.push(
515 AnimFragmentBattleMatch::new(
516 false,
517 false,
518 false,
519 false,
520 false,
521 Some((row, None, false, false, false, false, true)),
522 *start,
523 *end,
524 entry.uk_4().to_owned()
525 )
526 );
527 }
528 }
529 }
530 }
531
532 matches
533 }
534}
535
536impl Replaceable for AnimFragmentBattle {
537
538 fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &AnimFragmentBattleMatches) -> bool {
539 let mut edited = false;
540
541 // NOTE: Due to changes in index positions, we need to do this in reverse.
542 // Otherwise we may cause one edit to generate invalid indexes for the next matches.
543 for search_match in search_matches.matches().iter().rev() {
544 edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, self);
545 }
546
547 edited
548 }
549}
550
551impl AnimFragmentBattleMatches {
552
553 /// This function creates a new `AnimFragmentBattleMatches` for the provided path.
554 pub fn new(path: &str) -> Self {
555 Self {
556 path: path.to_owned(),
557 matches: vec![],
558 source: SearchSource::default(),
559 container_name: String::new(),
560 }
561 }
562}
563
564impl AnimFragmentBattleMatch {
565
566 /// This function creates a new `AnimFragmentBattleMatch` with the provided data.
567 #[allow(clippy::too_many_arguments)]
568 pub fn new(skeleton_name: bool, table_name: bool, mount_table_name: bool, unmount_table_name: bool, locomotion_graph: bool, entry: Option<AnimFragmentBattleEntryHit>, start: usize, end: usize, text: String) -> Self {
569 Self {
570 skeleton_name,
571 table_name,
572 mount_table_name,
573 unmount_table_name,
574 locomotion_graph,
575 entry,
576 start,
577 end,
578 text,
579 }
580 }
581
582 /// This function replaces all the matches in the provided data.
583 fn replace(&self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, data: &mut AnimFragmentBattle) -> bool {
584
585 // 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.
586 let (previous_data, current_data) = {
587 if self.skeleton_name {
588 (data.skeleton_name().to_owned(), data.skeleton_name_mut())
589 } else if self.table_name {
590 (data.table_name().to_owned(), data.table_name_mut())
591 } else if self.mount_table_name {
592 (data.mount_table_name().to_owned(), data.mount_table_name_mut())
593 } else if self.unmount_table_name {
594 (data.unmount_table_name().to_owned(), data.unmount_table_name_mut())
595 } else if self.locomotion_graph {
596 (data.locomotion_graph().to_owned(), data.locomotion_graph_mut())
597 } else if let Some((row, anim_ref, filename, metadata, metadata_sound, skeleton_type, uk_4)) = self.entry {
598 match data.entries_mut().get_mut(row) {
599 Some(entry) => {
600 if let Some((subrow, file_path, meta_file_path, snd_file_path)) = anim_ref {
601 match entry.anim_refs_mut().get_mut(subrow) {
602 Some(subentry) => {
603 if file_path {
604 (subentry.file_path().to_owned(), subentry.file_path_mut())
605 } else if meta_file_path {
606 (subentry.meta_file_path().to_owned(), subentry.meta_file_path_mut())
607 } else if snd_file_path {
608 (subentry.snd_file_path().to_owned(), subentry.snd_file_path_mut())
609 } else {
610 return false;
611 }
612 }
613 None => return false,
614 }
615 } else if filename {
616 (entry.filename().to_owned(), entry.filename_mut())
617 } else if metadata {
618 (entry.metadata().to_owned(), entry.metadata_mut())
619 } else if metadata_sound {
620 (entry.metadata_sound().to_owned(), entry.metadata_sound_mut())
621 } else if skeleton_type {
622 (entry.skeleton_type().to_owned(), entry.skeleton_type_mut())
623 } else if uk_4 {
624 (entry.uk_4().to_owned(), entry.uk_4_mut())
625 } else {
626 return false;
627 }
628 }
629 None => return false,
630 }
631 }
632
633 // This is an error.
634 else {
635 return false
636 }
637 };
638
639 replace_match_string(pattern, replace_pattern, case_sensitive, matching_mode, self.start, self.end, &previous_data, current_data)
640 }
641}