1use anyhow::{anyhow, Result};
14use rayon::prelude::*;
15use uuid::Uuid;
16
17use std::collections::BTreeMap;
18use std::fs::DirBuilder;
19use std::io::Cursor;
20use std::path::{Path, PathBuf};
21use std::time::SystemTime;
22
23use rpfm_ipc::api::ApiError;
24use rpfm_ipc::api::notes::{AddNote, NoteEntry};
25use rpfm_ipc::api::files::{FilesInfo, GetFilesInfo};
26use rpfm_ipc::api::packs::{PackDependency, PackDetails, PackSettingsValues, PackSummary, UpdatePack, UpdatePackSettings};
27use rpfm_ipc::api::schema::MissingDefinitions;
28use rpfm_ipc::helpers::ContainerInfo;
29use rpfm_ipc::api::packs::OperationalMode;
30
31use rpfm_lib::compression::CompressionFormat;
32use rpfm_lib::files::{Container, DecodeableExtraData, db::DB, FileType, pack::{Pack, PackSettings, PFHFlags}};
33use rpfm_lib::games::{GameInfo, pfh_file_type::PFHFileType};
34use rpfm_lib::notes::Note;
35use rpfm_lib::utils::{files_from_subdir, files_in_folder_from_newest_to_oldest};
36
37use rpfm_ipc::settings::{backup_autosave_path, Settings};
38use rpfm_ipc::settings_keys::{AUTOSAVE_AMOUNT, DISABLE_UUID_REGENERATION_ON_DB_TABLES};
39
40use rpfm_telemetry::warn;
41
42use super::{SaveOptions, SessionState, decode_tables, encode_extra_data, loaded_schema, pack, pack_mut, pack_summary};
43
44const DEFAULT_PACK_STEM: &str = "new_pack";
46
47const DEFAULT_PACK_EXT: &str = ".pack";
49
50const AUTOSAVE_FOLDER_SIZE_WARNING: u64 = 25 * 1024 * 1024 * 1024;
52
53const CA_PACKS_NAME: &str = "CA PackFiles";
55
56impl SessionState {
57
58 pub fn new_pack(&mut self, name: Option<&str>, settings: &Settings) -> Result<String> {
73 let name = match name {
74 Some(name) if !name.ends_with(DEFAULT_PACK_EXT) || name.contains(['/', '\\']) => {
75 return Err(ApiError::InvalidParams(format!("The name of the pack must be a file name ending in {DEFAULT_PACK_EXT}: {name}")).into());
76 }
77 Some(name) => name.to_owned(),
78 None => derive_new_pack_name(&self.packs),
79 };
80
81 let pack_version = self.game.pfh_version_by_file_type(PFHFileType::Mod);
82 let mut pack = Pack::new_with_name_and_version(&name, pack_version);
83
84 if let Some(version_number) = self.game.game_version_number(&settings.path_buf(self.game.key())) {
85 pack.set_game_version(version_number);
86 }
87
88 Ok(self.insert_pack(pack))
89 }
90
91 pub fn open_packs(&mut self, paths: &[PathBuf], lazy_loading: bool) -> Result<(String, ContainerInfo)> {
106 if let Some(path) = paths.iter().find(|path| {
107 let normalized = path.to_string_lossy().replace('\\', "/");
108 self.packs.values().any(|pack| pack.disk_file_path() == normalized.as_str())
109 }) {
110 return Err(anyhow!("Pack '{}' is already open. Close it first if you want to reopen it.", path.display()));
111 }
112
113 let mut pack = Pack::read_and_merge(paths, &self.game, lazy_loading, false, false)?;
114 decode_tables(&mut pack.files_by_type_mut(&[FileType::DB, FileType::Loc]), &self.schema);
115
116 if paths.len() > 1 {
117 let name = paths[0].file_name().map(|name| name.to_string_lossy().to_string()).unwrap_or_default();
118 pack.set_disk_file_path(name);
119 }
120
121 let info = ContainerInfo::from(&pack);
122 let key = self.insert_pack(pack);
123 Ok((key, info))
124 }
125
126 pub fn open_ca_packs(&mut self, settings: &Settings) -> Result<(String, ContainerInfo)> {
140 if self.packs.values().any(|pack| pack.disk_file_path() == CA_PACKS_NAME) {
141 return Err(anyhow!("Pack '{}' is already open. Close it first if you want to reopen it.", CA_PACKS_NAME));
142 }
143
144 let mut pack = Pack::read_and_merge_ca_packs(&self.game, &settings.path_buf(self.game.key()))?;
145 decode_tables(&mut pack.files_by_type_mut(&[FileType::DB, FileType::Loc]), &self.schema);
146 pack.set_disk_file_path(CA_PACKS_NAME.to_owned());
147
148 let info = ContainerInfo::from(&pack);
149 let key = self.insert_pack(pack);
150 Ok((key, info))
151 }
152
153 pub fn close_pack(&mut self, pack_key: &str) -> Result<()> {
155 self.packs.remove(pack_key).ok_or_else(|| ApiError::PackNotFound(pack_key.to_owned()))?;
156 self.pack_modes.remove(pack_key);
157 self.changed_packs.remove(pack_key);
158 self.session.remove_pack(pack_key);
159 Ok(())
160 }
161
162 pub fn close_all_packs(&mut self) {
164 for pack_key in self.packs.keys() {
165 self.session.remove_pack(pack_key);
166 }
167
168 self.packs.clear();
169 self.pack_modes.clear();
170 self.changed_packs.clear();
171 }
172
173 pub fn save_pack(&mut self, pack_key: &str, path: Option<&Path>, options: SaveOptions) -> Result<ContainerInfo> {
189 self.wait_for_autosave();
190 let pack = pack_mut(&mut self.packs, pack_key)?;
191
192 let pack_type = *pack.header().pfh_file_type();
193 if !options.allow_editing_of_ca_packfiles && pack_type != PFHFileType::Mod && pack_type != PFHFileType::Movie {
194 return Err(anyhow!("Pack cannot be saved due to being of CA-Only type. Either change the Pack Type or enable \"Allow Edition of CA Packs\" in the settings."));
195 }
196
197 if path.is_none() && !Path::new(pack.disk_file_path()).is_absolute() {
199 return Err(anyhow!("Pack '{}' has never been saved to disk. Use Save As to choose where to save it.", pack_key));
200 }
201
202 if let Some(path) = path {
203 if !path.is_absolute() {
204 return Err(ApiError::InvalidParams(format!("The path to save the pack to must be absolute: {}", path.display())).into());
205 }
206
207 if let Some(parent) = path.parent() {
208 DirBuilder::new().recursive(true).create(parent)?;
209 }
210 }
211
212 if options.clean {
213 pack.clean_undecoded();
214 }
215
216 let extra_data = encode_extra_data(&self.game, pack.compression_format(), options.disable_uuid_regeneration);
217 pack.save(path, &self.game, &extra_data)
218 .map_err(|error| anyhow!("Error while trying to save the currently open PackFile: {}", error))?;
219 self.session.set_pack_name(pack_key, &pack.disk_file_name());
220 self.changed_packs.remove(pack_key);
221
222 Ok(ContainerInfo::from(&*pack))
223 }
224
225 pub fn update_pack(&mut self, request: &UpdatePack) -> Result<PackDetails> {
231 let pack = pack_mut(&mut self.packs, &request.pack)?;
232
233 let enables_encryption = request.index_encrypted == Some(true) || request.data_encrypted == Some(true);
234 if enables_encryption && !pack.pfh_version().supports_encryption() {
235 return Err(anyhow!("Encryption is not supported in {} Packs.", pack.pfh_version().value()));
236 }
237
238 if let Some(pack_type) = request.pack_type {
239 pack.set_pfh_file_type(pack_type);
240 }
241
242 if let Some(compression) = request.compression {
243 pack.set_compression_format(compression, &self.game);
244 }
245
246 let mut bitmask = pack.bitmask();
247 for (flag, state) in [
248 (PFHFlags::HAS_ENCRYPTED_INDEX, request.index_encrypted),
249 (PFHFlags::HAS_ENCRYPTED_DATA, request.data_encrypted),
250 (PFHFlags::HAS_INDEX_WITH_TIMESTAMPS, request.index_includes_timestamp),
251 ] {
252 if let Some(state) = state {
253 bitmask.set(flag, state);
254 }
255 }
256 pack.set_bitmask(bitmask);
257
258 if let Some(ref dependencies) = request.dependencies {
259 pack.set_dependencies(dependencies.iter().map(|dependency| (dependency.enabled, dependency.name.clone())).collect());
260 }
261
262 if let Some(ref mode) = request.operational_mode {
263 self.pack_modes.insert(request.pack.clone(), mode.clone());
264 }
265
266 self.pack_details(&request.pack)
267 }
268
269 pub fn pack_summary(&self, pack_key: &str) -> Result<PackSummary> {
271 Ok(pack_summary(pack_key, pack(&self.packs, pack_key)?))
272 }
273
274 pub fn files_info(&self, request: &GetFilesInfo) -> Result<FilesInfo> {
276 let pack = pack(&self.packs, &request.pack)?;
277 let files = match request.paths {
278 Some(ref paths) => paths.iter().filter_map(|path| pack.files().get(path)).map(From::from).collect(),
279 None => pack.files().par_iter().map(|(_, file)| From::from(file)).collect(),
280 };
281
282 Ok(FilesInfo { files })
283 }
284
285 pub fn pack_details(&self, pack_key: &str) -> Result<PackDetails> {
287 let pack = pack(&self.packs, pack_key)?;
288 let bitmask = pack.bitmask();
289
290 Ok(PackDetails {
291 summary: pack_summary(pack_key, pack),
292 version: pack.pfh_version(),
293 compression: pack.compression_format(),
294 index_encrypted: bitmask.contains(PFHFlags::HAS_ENCRYPTED_INDEX),
295 data_encrypted: bitmask.contains(PFHFlags::HAS_ENCRYPTED_DATA),
296 index_includes_timestamp: bitmask.contains(PFHFlags::HAS_INDEX_WITH_TIMESTAMPS),
297 extended_header: bitmask.contains(PFHFlags::HAS_EXTENDED_HEADER),
298 timestamp: pack.internal_timestamp(),
299 dependencies: pack.dependencies().iter()
300 .map(|(enabled, name)| PackDependency { enabled: *enabled, name: name.clone() })
301 .collect(),
302 operational_mode: self.pack_operational_mode(pack_key),
303 })
304 }
305
306 pub fn pack_path(&self, pack_key: &str) -> Result<PathBuf> {
308 Ok(PathBuf::from(pack(&self.packs, pack_key)?.disk_file_path()))
309 }
310
311 pub fn pack_name(&self, pack_key: &str) -> Result<String> {
313 Ok(pack(&self.packs, pack_key)?.disk_file_name())
314 }
315
316 pub fn set_compression_format(&mut self, pack_key: &str, compression_format: CompressionFormat) -> Result<CompressionFormat> {
322 Ok(pack_mut(&mut self.packs, pack_key)?.set_compression_format(compression_format, &self.game))
323 }
324
325 pub fn pack_settings(&self, pack_key: &str) -> Result<PackSettings> {
327 Ok(pack(&self.packs, pack_key)?.settings().clone())
328 }
329
330 pub fn add_pack_ignored_diagnostics_line(&mut self, pack_key: &str, line: String) -> Result<()> {
332 let pack = pack_mut(&mut self.packs, pack_key)?;
333 let settings_text = pack.settings_mut().settings_text_mut();
334 match settings_text.get_mut("diagnostics_files_to_ignore") {
335 Some(diagnostics_ignored) => diagnostics_ignored.push_str(&line),
336 None => { settings_text.insert("diagnostics_files_to_ignore".to_owned(), line); },
337 }
338
339 Ok(())
340 }
341
342 pub fn pack_operational_mode(&self, pack_key: &str) -> OperationalMode {
344 self.pack_modes.get(pack_key).cloned().unwrap_or(OperationalMode::Normal)
345 }
346
347 pub fn pack_settings_values(&self, pack_key: &str) -> Result<PackSettingsValues> {
349 Ok(PackSettingsValues::from(pack(&self.packs, pack_key)?.settings()))
350 }
351
352 pub fn update_pack_settings(&mut self, request: &UpdatePackSettings) -> Result<PackSettingsValues> {
358 let settings = pack_mut(&mut self.packs, &request.pack)?.settings_mut();
359 settings.settings_text_mut().extend(request.settings.text.clone());
360 settings.settings_string_mut().extend(request.settings.string.clone());
361 settings.settings_bool_mut().extend(request.settings.bool.clone());
362 settings.settings_number_mut().extend(request.settings.number.clone());
363 self.pack_settings_values(&request.pack)
364 }
365
366 pub fn note_entries(&self, pack_key: &str, path: &str) -> Result<Vec<NoteEntry>> {
370 if path.is_empty() {
371 let notes = pack(&self.packs, pack_key)?.notes().file_notes().values().flatten().map(note_entry).collect();
372 return Ok(notes);
373 }
374
375 Ok(self.notes_for_path(pack_key, path)?.iter().map(note_entry).collect())
376 }
377
378 pub fn add_note_entry(&mut self, request: &AddNote) -> Result<NoteEntry> {
384 let mut note = Note::default();
385 note.set_path(request.path.clone());
386 note.set_message(request.message.clone());
387 note.set_url(request.url.clone());
388 note.set_id(request.id.unwrap_or_default());
389 Ok(note_entry(&self.add_note(&request.pack, note, request.id.is_some())?))
390 }
391
392 pub fn notes_for_path(&self, pack_key: &str, path: &str) -> Result<Vec<Note>> {
394 Ok(pack(&self.packs, pack_key)?.notes().notes_by_path(path))
395 }
396
397 pub fn add_note(&mut self, pack_key: &str, note: Note, replace: bool) -> Result<Note> {
403 Ok(pack_mut(&mut self.packs, pack_key)?.notes_mut().add_note(note, replace))
404 }
405
406 pub fn delete_note(&mut self, pack_key: &str, path: &str, id: u64) -> Result<()> {
408 pack_mut(&mut self.packs, pack_key)?.notes_mut().delete_note(path, id);
409 Ok(())
410 }
411
412 pub fn mark_packs_changed(&mut self, pack_key: Option<&str>) {
418 match pack_key {
419 Some(pack_key) if self.packs.contains_key(pack_key) => { self.changed_packs.insert(pack_key.to_owned()); },
420 Some(_) => {},
421 None => self.changed_packs.extend(self.packs.keys().cloned()),
422 }
423 }
424
425 pub fn autosave(&mut self, settings: &Settings) {
434 if let Ok(autosave_path) = backup_autosave_path() {
435 self.autosave_to(settings, autosave_path);
436 }
437 }
438
439 fn autosave_to(&mut self, settings: &Settings, autosave_path: PathBuf) {
443 if self.autosave_thread.as_ref().is_some_and(|thread| !thread.is_finished()) {
444 return;
445 }
446
447 let ca_paths = self.game.ca_packs_paths(&settings.path_buf(self.game.key()))
448 .unwrap_or_default()
449 .iter()
450 .map(|path| path.to_string_lossy().replace('\\', "/"))
451 .collect::<Vec<_>>();
452
453 let packs = std::mem::take(&mut self.changed_packs).into_iter()
454 .filter_map(|pack_key| self.packs.get(&pack_key))
455 .filter(|pack| {
456 let pack_type = pack.pfh_file_type();
457 !pack.settings().setting_bool("disable_autosaves").unwrap_or(&true) &&
458 (pack_type == PFHFileType::Mod || pack_type == PFHFileType::Movie) &&
459 !ca_paths.contains(&pack.disk_file_path().replace('\\', "/"))
460 })
461 .cloned()
462 .collect::<Vec<_>>();
463
464 if packs.is_empty() {
465 return;
466 }
467
468 let game = self.game.clone();
469 let disable_uuid_regeneration = settings.bool(DISABLE_UUID_REGENERATION_ON_DB_TABLES);
470 let autosave_amount = settings.i32(AUTOSAVE_AMOUNT).max(0) as usize;
471 self.autosave_thread = Some(std::thread::spawn(move || {
472 for mut pack in packs {
473 let folder = autosave_path.join(pack.disk_file_name());
474 if let Err(error) = save_autosave(&mut pack, &folder, &game, disable_uuid_regeneration, autosave_amount) {
475 warn!("Failed to autosave the pack {}: {error}", pack.disk_file_name());
476 }
477 }
478
479 let size = files_from_subdir(&autosave_path, true).unwrap_or_default().iter()
480 .filter_map(|path| path.metadata().ok())
481 .map(|metadata| metadata.len())
482 .sum::<u64>();
483
484 if size > AUTOSAVE_FOLDER_SIZE_WARNING {
485 warn!("The autosave folder is using {} GB. Consider lowering the amount of autosaves to keep, or deleting the ones you don't need.", size / 1024 / 1024 / 1024);
486 }
487 }));
488 }
489
490 pub fn wait_for_autosave(&mut self) {
494 if let Some(thread) = self.autosave_thread.take() {
495 let _ = thread.join();
496 }
497 }
498
499 pub fn missing_definitions(&mut self, pack_key: &str) -> Result<MissingDefinitions> {
503 let pack = pack_mut(&mut self.packs, pack_key)?;
504 let schema = loaded_schema(&self.schema)?;
505
506 let mut extra_data = DecodeableExtraData::default();
507 extra_data.set_schema(Some(schema));
508 let extra_data = Some(extra_data);
509
510 let mut files = pack.files_by_type_mut(&[FileType::DB]);
511 files.sort_by_key(|file| file.path_in_container_raw().to_lowercase());
512
513 let mut paths = vec![];
514 for file in files {
515 if file.decode(&extra_data, false, false).is_err() && file.load().is_ok() {
516 if let Ok(raw_data) = file.cached() {
517 let mut reader = Cursor::new(raw_data);
518 if let Ok((_, _, _, entry_count)) = DB::read_header(&mut reader) {
519 if entry_count > 0 {
520 paths.push(file.path_in_container_raw().to_owned());
521 }
522 }
523 }
524 }
525 }
526
527 Ok(MissingDefinitions { paths })
528 }
529
530 pub fn live_export(&mut self, pack_key: &str, settings: &Settings, disable_uuid_regeneration: bool, tsv_keys_first: bool) -> Result<()> {
539 let pack = pack_mut(&mut self.packs, pack_key)?;
540 let game_path = settings.path_buf(self.game.key());
541 pack.live_export(&self.game, &game_path, disable_uuid_regeneration, tsv_keys_first)?;
542 Ok(())
543 }
544
545 fn insert_pack(&mut self, pack: Pack) -> String {
551 let key = Uuid::new_v4().to_string();
552
553 self.session.set_pack_name(&key, &pack.disk_file_name());
554 self.pack_modes.insert(key.clone(), OperationalMode::Normal);
555 self.packs.insert(key.clone(), pack);
556 key
557 }
558}
559
560fn note_entry(note: &Note) -> NoteEntry {
562 NoteEntry {
563 id: *note.id(),
564 path: note.path().to_owned(),
565 message: note.message().to_owned(),
566 url: note.url().clone(),
567 }
568}
569
570fn save_autosave(pack: &mut Pack, folder: &Path, game: &GameInfo, disable_uuid_regeneration: bool, autosave_amount: usize) -> Result<()> {
584 DirBuilder::new().recursive(true).create(folder)?;
585
586 let date = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs();
587 let extra_data = encode_extra_data(game, pack.compression_format(), disable_uuid_regeneration);
588 pack.save(Some(&folder.join(format!("{date}.pack"))), game, &extra_data)?;
589
590 for file in files_in_folder_from_newest_to_oldest(folder)?.iter().skip(autosave_amount) {
591 let _ = std::fs::remove_file(file);
592 }
593
594 Ok(())
595}
596
597fn derive_new_pack_name(packs: &BTreeMap<String, Pack>) -> String {
599 let taken = |name: &str| packs.values().any(|pack| pack.disk_file_name() == name);
600 let base = format!("{}{}", DEFAULT_PACK_STEM, DEFAULT_PACK_EXT);
601 if !taken(&base) {
602 return base;
603 }
604
605 (2..).map(|suffix| format!("{}_{}{}", DEFAULT_PACK_STEM, suffix, DEFAULT_PACK_EXT))
606 .find(|candidate| !taken(candidate))
607 .expect("an unbounded range always finds a free name")
608}
609
610#[cfg(test)]
615#[path = "packs_test.rs"]
616mod packs_test;