1use anyhow::Result;
14use serde::Serialize;
15use serde_json::{Map, Value};
16
17use std::collections::{BTreeMap, HashSet};
18
19use rpfm_extensions::search::{GlobalSearch, Matches, MatchHolder, SearchSource};
20use rpfm_extensions::search::anim_fragment_battle::AnimFragmentBattleMatches;
21use rpfm_extensions::search::atlas::AtlasMatches;
22use rpfm_extensions::search::portrait_settings::PortraitSettingsMatches;
23use rpfm_extensions::search::rigid_model::RigidModelMatches;
24use rpfm_extensions::search::schema::SchemaMatches;
25use rpfm_extensions::search::table::TableMatches;
26use rpfm_extensions::search::text::TextMatches;
27use rpfm_extensions::search::unit_variant::UnitVariantMatches;
28use rpfm_extensions::search::unknown::UnknownMatches;
29
30use rpfm_ipc::api::ApiError;
31use rpfm_ipc::api::files::FileSource;
32use rpfm_ipc::api::search::{
33 DEFAULT_MATCHES_LIMIT, DEFAULT_SEARCH_FILE_TYPES, search_on_from_file_types, ListSearchMatches, ReplaceSearchMatches, RunSearch, SearchMatch, SearchMatchList,
34 SearchReplaced, SearchReport, SearchSummary,
35};
36use rpfm_ipc::helpers::RFileInfo;
37
38use rpfm_lib::files::{Container, ContainerPath};
39
40use super::{SessionState, loaded_schema};
41
42trait FileMatches {
44
45 fn file_path(&self) -> &str;
47
48 fn file_source(&self) -> Option<FileSource>;
50
51 fn match_count(&self) -> usize;
53
54 fn match_details(&self, index: usize) -> Value;
56
57 fn retain_matches(&mut self, indexes: &HashSet<usize>);
59}
60
61macro_rules! impl_file_matches {
65 ($($matches_type:ty),*) => {
66 $(
67 impl FileMatches for $matches_type {
68 fn file_path(&self) -> &str {
69 self.path()
70 }
71
72 fn file_source(&self) -> Option<FileSource> {
73 Some(file_source(self.source()))
74 }
75
76 fn match_count(&self) -> usize {
77 self.matches().len()
78 }
79
80 fn match_details(&self, index: usize) -> Value {
81 to_value(&self.matches()[index])
82 }
83
84 fn retain_matches(&mut self, indexes: &HashSet<usize>) {
85 retain_indexes(self.matches_mut(), indexes);
86 }
87 }
88 )*
89 };
90}
91
92impl_file_matches!(AnimFragmentBattleMatches, AtlasMatches, PortraitSettingsMatches, RigidModelMatches, TableMatches, UnitVariantMatches, UnknownMatches);
93
94impl FileMatches for TextMatches {
96 fn file_path(&self) -> &str {
97 self.path()
98 }
99
100 fn file_source(&self) -> Option<FileSource> {
101 Some(file_source(self.source()))
102 }
103
104 fn match_count(&self) -> usize {
105 self.matches().len()
106 }
107
108 fn match_details(&self, index: usize) -> Value {
109 let text_match = &self.matches()[index];
110 let mut details = to_value(text_match);
111 if let (Value::Object(details), Some(text)) = (&mut details, self.matches_strings().get(*text_match.text_index())) {
112 details.insert("text".to_owned(), Value::String(text.clone()));
113 }
114
115 details
116 }
117
118 fn retain_matches(&mut self, indexes: &HashSet<usize>) {
119 retain_indexes(self.matches_mut(), indexes);
120 }
121}
122
123impl FileMatches for SchemaMatches {
125 fn file_path(&self) -> &str {
126 ""
127 }
128
129 fn file_source(&self) -> Option<FileSource> {
130 None
131 }
132
133 fn match_count(&self) -> usize {
134 self.matches().len()
135 }
136
137 fn match_details(&self, index: usize) -> Value {
138 to_value(&self.matches()[index])
139 }
140
141 fn retain_matches(&mut self, indexes: &HashSet<usize>) {
142 retain_indexes(self.matches_mut(), indexes);
143 }
144}
145
146#[derive(Debug, Clone, Copy)]
148struct MatchRef {
149 file_type: &'static str,
150 file_index: usize,
151 match_index: usize,
152}
153
154impl SessionState {
155
156 fn global_search_replace(&mut self, mut search: GlobalSearch, matches: &[MatchHolder]) -> Result<(GlobalSearch, Vec<RFileInfo>)> {
167 let schema = loaded_schema(&self.schema)?;
168 let edited_paths = search.replace(&self.game, schema, &mut self.packs, &mut self.dependencies, matches)?;
169 Ok((search, self.files_info_in_all_packs(&edited_paths)))
170 }
171
172 pub fn run_search(&mut self, request: &RunSearch) -> Result<SearchSummary> {
178 let mut search = GlobalSearch::default();
179 search.set_pattern(request.pattern.clone());
180 search.set_case_sensitive(request.case_sensitive);
181 search.set_use_regex(request.use_regex);
182 search.set_sources(request.sources.iter().map(search_source).collect());
183 search.set_game_key(self.game.key().to_owned());
184
185 let file_types = match request.file_types {
186 Some(ref file_types) => file_types.iter().map(String::as_str).collect::<Vec<_>>(),
187 None => DEFAULT_SEARCH_FILE_TYPES.to_vec(),
188 };
189 search.set_search_on(search_on_from_file_types(&file_types).map_err(ApiError::InvalidParams)?);
190
191 let schema = loaded_schema(&self.schema)?;
192 search.search(&self.game, schema, &mut self.packs, &mut self.dependencies, &[]);
193
194 let summary = summarize(search.matches());
195 self.search = Some(search);
196 Ok(summary)
197 }
198
199 pub fn list_search_matches(&self, request: &ListSearchMatches) -> Result<SearchMatchList> {
205 let matches = self.search.as_ref().ok_or(ApiError::SearchNotRun)?.matches();
206 let files = files_by_type(matches);
207
208 let mut page = vec![];
209 let mut total = 0;
210 let limit = request.limit.unwrap_or(DEFAULT_MATCHES_LIMIT);
211 for (id, match_ref) in match_refs(&files).into_iter().enumerate() {
212 let file = files[match_ref.file_type][match_ref.file_index];
213 let type_matches = request.file_types.as_ref().is_none_or(|file_types| file_types.iter().any(|file_type| file_type == match_ref.file_type));
214 if !type_matches || !file.file_path().starts_with(&request.path_prefix) {
215 continue;
216 }
217
218 if total >= request.offset && page.len() < limit {
219 page.push(SearchMatch {
220 id,
221 source: file.file_source(),
222 path: file.file_path().to_owned(),
223 file_type: match_ref.file_type.to_owned(),
224 details: file.match_details(match_ref.match_index),
225 });
226 }
227
228 total += 1;
229 }
230
231 Ok(SearchMatchList { matches: page, total })
232 }
233
234 pub fn replace_search_matches(&mut self, request: &ReplaceSearchMatches) -> Result<SearchReplaced> {
240 let mut search = self.search.take().ok_or(ApiError::SearchNotRun)?;
241 search.set_replace_text(request.replace_text.clone());
242
243 let all_ids;
245 let ids = match request.matches {
246 Some(ref ids) => ids,
247 None => {
248 all_ids = (0..match_refs(&files_by_type(search.matches())).len()).collect::<Vec<_>>();
249 &all_ids
250 }
251 };
252
253 let result = match selected_match_holders(search.matches(), ids) {
254 Ok(holders) => self.global_search_replace(search.clone(), &holders),
255 Err(error) => Err(error.into()),
256 };
257
258 let (mut replaced, files_info) = match result {
260 Ok(result) => result,
261 Err(error) => {
262 self.search = Some(search);
263 return Err(error);
264 }
265 };
266
267 let edited = files_info.iter().map(|info| info.path().to_owned()).collect::<Vec<_>>();
268 let edited_paths = edited.iter().map(|path| ContainerPath::File(path.clone())).collect::<Vec<_>>();
269 let schema = loaded_schema(&self.schema)?;
270 replaced.search(&self.game, schema, &mut self.packs, &mut self.dependencies, &edited_paths);
271
272 let summary = summarize(replaced.matches());
273 self.search = Some(replaced);
274 Ok(SearchReplaced { edited, summary })
275 }
276
277 pub fn search_report(&self) -> Result<SearchReport> {
283 let search = self.search.as_ref().ok_or(ApiError::SearchNotRun)?;
284 Ok(SearchReport { search: search.clone(), first_ids: first_ids(&files_by_type(search.matches())) })
285 }
286
287 fn files_info_in_all_packs(&self, paths: &[ContainerPath]) -> Vec<RFileInfo> {
289 paths.iter()
290 .flat_map(|path| self.packs.values().flat_map(move |pack| pack.files_by_path(path, false)))
291 .map(RFileInfo::from)
292 .collect()
293 }
294}
295
296fn files_by_type(matches: &Matches) -> BTreeMap<&'static str, Vec<&dyn FileMatches>> {
298 fn as_dyn<T: FileMatches>(files: &[T]) -> Vec<&dyn FileMatches> {
299 files.iter().map(|file| file as &dyn FileMatches).collect()
300 }
301
302 BTreeMap::from([
303 ("Anim", as_dyn(matches.anim())),
304 ("AnimFragmentBattle", as_dyn(matches.anim_fragment_battle())),
305 ("AnimPack", as_dyn(matches.anim_pack())),
306 ("AnimsTable", as_dyn(matches.anims_table())),
307 ("Atlas", as_dyn(matches.atlas())),
308 ("Audio", as_dyn(matches.audio())),
309 ("BMD", as_dyn(matches.bmd())),
310 ("DB", as_dyn(matches.db())),
311 ("ESF", as_dyn(matches.esf())),
312 ("GroupFormations", as_dyn(matches.group_formations())),
313 ("Image", as_dyn(matches.image())),
314 ("Loc", as_dyn(matches.loc())),
315 ("MatchedCombat", as_dyn(matches.matched_combat())),
316 ("Pack", as_dyn(matches.pack())),
317 ("PortraitSettings", as_dyn(matches.portrait_settings())),
318 ("RigidModel", as_dyn(matches.rigid_model())),
319 ("Schema", vec![matches.schema() as &dyn FileMatches]),
320 ("SoundBank", as_dyn(matches.sound_bank())),
321 ("Text", as_dyn(matches.text())),
322 ("UIC", as_dyn(matches.uic())),
323 ("UnitVariant", as_dyn(matches.unit_variant())),
324 ("Unknown", as_dyn(matches.unknown())),
325 ("Video", as_dyn(matches.video())),
326 ])
327}
328
329fn match_refs(files: &BTreeMap<&'static str, Vec<&dyn FileMatches>>) -> Vec<MatchRef> {
331 files.iter()
332 .flat_map(|(file_type, files)| files.iter().enumerate()
333 .flat_map(move |(file_index, file)| (0..file.match_count())
334 .map(move |match_index| MatchRef { file_type, file_index, match_index })))
335 .collect()
336}
337
338fn first_ids(files: &BTreeMap<&'static str, Vec<&dyn FileMatches>>) -> BTreeMap<String, Vec<usize>> {
340 let mut next_id = 0;
341 files.iter()
342 .map(|(file_type, files)| {
343 let ids = files.iter()
344 .map(|file| {
345 let id = next_id;
346 next_id += file.match_count();
347 id
348 })
349 .collect();
350
351 ((*file_type).to_owned(), ids)
352 })
353 .collect()
354}
355
356fn selected_match_holders(matches: &Matches, ids: &[usize]) -> Result<Vec<MatchHolder>, ApiError> {
362 let refs = match_refs(&files_by_type(matches));
363 let mut selected: BTreeMap<(&str, usize), HashSet<usize>> = BTreeMap::new();
364 for id in ids {
365 let match_ref = refs.get(*id).ok_or_else(|| ApiError::InvalidParams(format!("There is no match with ID {id}.")))?;
366 selected.entry((match_ref.file_type, match_ref.file_index)).or_default().insert(match_ref.match_index);
367 }
368
369 let mut holders = vec![];
370 holders.extend(selected_files(matches.anim_fragment_battle(), "AnimFragmentBattle", &selected).map(MatchHolder::AnimFragmentBattle));
371 holders.extend(selected_files(matches.atlas(), "Atlas", &selected).map(MatchHolder::Atlas));
372 holders.extend(selected_files(matches.db(), "DB", &selected).map(MatchHolder::Db));
373 holders.extend(selected_files(matches.loc(), "Loc", &selected).map(MatchHolder::Loc));
374 holders.extend(selected_files(matches.portrait_settings(), "PortraitSettings", &selected).map(MatchHolder::PortraitSettings));
375 holders.extend(selected_files(matches.rigid_model(), "RigidModel", &selected).map(MatchHolder::RigidModel));
376 holders.extend(selected_files(matches.text(), "Text", &selected).map(MatchHolder::Text));
377 holders.extend(selected_files(matches.unit_variant(), "UnitVariant", &selected).map(MatchHolder::UnitVariant));
378 Ok(holders)
379}
380
381fn selected_files<'a, T: FileMatches + Clone>(files: &'a [T], file_type: &'a str, selected: &'a BTreeMap<(&str, usize), HashSet<usize>>) -> impl Iterator<Item = T> + 'a {
383 files.iter().enumerate().filter_map(move |(file_index, file)| {
384 if !matches!(file.file_source(), Some(FileSource::Pack(_))) {
385 return None;
386 }
387
388 let indexes = selected.get(&(file_type, file_index))?;
389 let mut file = file.clone();
390 file.retain_matches(indexes);
391 Some(file)
392 })
393}
394
395fn summarize(matches: &Matches) -> SearchSummary {
397 let mut summary = SearchSummary::default();
398 for (file_type, files) in files_by_type(matches) {
399 let count = files.iter().map(|file| file.match_count()).sum::<usize>();
400 if count > 0 {
401 summary.total += count;
402 summary.files += files.iter().filter(|file| file.match_count() > 0 && !file.file_path().is_empty()).count();
403 summary.by_type.insert(file_type.to_owned(), count);
404 }
405 }
406
407 summary
408}
409
410fn search_source(source: &FileSource) -> SearchSource {
412 match source {
413 FileSource::Pack(pack_key) => SearchSource::Pack(pack_key.clone()),
414 FileSource::GameFiles => SearchSource::GameFiles,
415 FileSource::ParentFiles => SearchSource::ParentFiles,
416 FileSource::AssemblyKit => SearchSource::AssKitFiles,
417 }
418}
419
420fn file_source(source: &SearchSource) -> FileSource {
422 match source {
423 SearchSource::Pack(pack_key) => FileSource::Pack(pack_key.clone()),
424 SearchSource::GameFiles => FileSource::GameFiles,
425 SearchSource::ParentFiles => FileSource::ParentFiles,
426 SearchSource::AssKitFiles => FileSource::AssemblyKit,
427 }
428}
429
430fn retain_indexes<T>(items: &mut Vec<T>, indexes: &HashSet<usize>) {
432 let mut index = 0;
433 items.retain(|_| {
434 index += 1;
435 indexes.contains(&(index - 1))
436 });
437}
438
439fn to_value(value: &impl Serialize) -> Value {
441 serde_json::to_value(value).unwrap_or_else(|_| Value::Object(Map::new()))
442}
443
444#[cfg(test)]
449#[path = "search_test.rs"]
450mod search_test;