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, readable_pack};
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, temporary: bool) -> Result<(String, ContainerInfo)> {
107 if let Some(path) = paths.iter().find(|path| {
108 let normalized = path.to_string_lossy().replace('\\', "/");
109 self.packs.values().any(|pack| pack.disk_file_path() == normalized.as_str())
110 }) {
111 return Err(anyhow!("Pack '{}' is already open. Close it first if you want to reopen it.", path.display()));
112 }
113
114 let mut pack = Pack::read_and_merge(paths, &self.game, lazy_loading, false, false)?;
115 decode_tables(&mut pack.files_by_type_mut(&[FileType::DB, FileType::Loc]), &self.schema);
116
117 if paths.len() > 1 {
118 let name = paths[0].file_name().map(|name| name.to_string_lossy().to_string()).unwrap_or_default();
119 pack.set_disk_file_path(name);
120 }
121
122 let info = ContainerInfo::from(&pack);
123 let key = if temporary {
124 let key = Uuid::new_v4().to_string();
125 self.temporary_packs.insert(key.clone(), pack);
126 key
127 } else {
128 self.insert_pack(pack)
129 };
130
131 Ok((key, info))
132 }
133
134 pub fn open_ca_packs(&mut self, settings: &Settings) -> Result<(String, ContainerInfo)> {
148 if self.packs.values().any(|pack| pack.disk_file_path() == CA_PACKS_NAME) {
149 return Err(anyhow!("Pack '{}' is already open. Close it first if you want to reopen it.", CA_PACKS_NAME));
150 }
151
152 let mut pack = Pack::read_and_merge_ca_packs(&self.game, &settings.path_buf(self.game.key()))?;
153 decode_tables(&mut pack.files_by_type_mut(&[FileType::DB, FileType::Loc]), &self.schema);
154 pack.set_disk_file_path(CA_PACKS_NAME.to_owned());
155
156 let info = ContainerInfo::from(&pack);
157 let key = self.insert_pack(pack);
158 Ok((key, info))
159 }
160
161 pub fn close_pack(&mut self, pack_key: &str) -> Result<()> {
163 if self.temporary_packs.remove(pack_key).is_some() {
164 return Ok(());
165 }
166
167 self.packs.remove(pack_key).ok_or_else(|| ApiError::PackNotFound(pack_key.to_owned()))?;
168 self.pack_modes.remove(pack_key);
169 self.changed_packs.remove(pack_key);
170 self.session.remove_pack(pack_key);
171 Ok(())
172 }
173
174 pub fn close_all_packs(&mut self) {
176 for pack_key in self.packs.keys() {
177 self.session.remove_pack(pack_key);
178 }
179
180 self.packs.clear();
181 self.temporary_packs.clear();
182 self.pack_modes.clear();
183 self.changed_packs.clear();
184 }
185
186 pub fn save_pack(&mut self, pack_key: &str, path: Option<&Path>, options: SaveOptions) -> Result<ContainerInfo> {
202 self.wait_for_autosave();
203 let pack = pack_mut(&mut self.packs, pack_key)?;
204
205 let pack_type = *pack.header().pfh_file_type();
206 if !options.allow_editing_of_ca_packfiles && pack_type != PFHFileType::Mod && pack_type != PFHFileType::Movie {
207 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."));
208 }
209
210 if path.is_none() && !Path::new(pack.disk_file_path()).is_absolute() {
212 return Err(anyhow!("Pack '{}' has never been saved to disk. Use Save As to choose where to save it.", pack_key));
213 }
214
215 if let Some(path) = path {
216 if !path.is_absolute() {
217 return Err(ApiError::InvalidParams(format!("The path to save the pack to must be absolute: {}", path.display())).into());
218 }
219
220 if let Some(parent) = path.parent() {
221 DirBuilder::new().recursive(true).create(parent)?;
222 }
223 }
224
225 if options.clean {
226 pack.clean_undecoded();
227 }
228
229 let extra_data = encode_extra_data(&self.game, pack.compression_format(), options.disable_uuid_regeneration);
230 pack.save(path, &self.game, &extra_data)
231 .map_err(|error| anyhow!("Error while trying to save the currently open PackFile: {}", error))?;
232 self.session.set_pack_name(pack_key, &pack.disk_file_name());
233 self.changed_packs.remove(pack_key);
234
235 Ok(ContainerInfo::from(&*pack))
236 }
237
238 pub fn update_pack(&mut self, request: &UpdatePack) -> Result<PackDetails> {
244 let pack = pack_mut(&mut self.packs, &request.pack)?;
245
246 let enables_encryption = request.index_encrypted == Some(true) || request.data_encrypted == Some(true);
247 if enables_encryption && !pack.pfh_version().supports_encryption() {
248 return Err(anyhow!("Encryption is not supported in {} Packs.", pack.pfh_version().value()));
249 }
250
251 if let Some(pack_type) = request.pack_type {
252 pack.set_pfh_file_type(pack_type);
253 }
254
255 if let Some(compression) = request.compression {
256 pack.set_compression_format(compression, &self.game);
257 }
258
259 let mut bitmask = pack.bitmask();
260 for (flag, state) in [
261 (PFHFlags::HAS_ENCRYPTED_INDEX, request.index_encrypted),
262 (PFHFlags::HAS_ENCRYPTED_DATA, request.data_encrypted),
263 (PFHFlags::HAS_INDEX_WITH_TIMESTAMPS, request.index_includes_timestamp),
264 ] {
265 if let Some(state) = state {
266 bitmask.set(flag, state);
267 }
268 }
269 pack.set_bitmask(bitmask);
270
271 if let Some(ref dependencies) = request.dependencies {
272 pack.set_dependencies(dependencies.iter().map(|dependency| (dependency.enabled, dependency.name.clone())).collect());
273 }
274
275 if let Some(ref mode) = request.operational_mode {
276 self.pack_modes.insert(request.pack.clone(), mode.clone());
277 }
278
279 self.pack_details(&request.pack)
280 }
281
282 pub fn pack_summary(&self, pack_key: &str) -> Result<PackSummary> {
284 Ok(pack_summary(pack_key, readable_pack(&self.packs, &self.temporary_packs, pack_key)?))
285 }
286
287 pub fn files_info(&self, request: &GetFilesInfo) -> Result<FilesInfo> {
289 let pack = readable_pack(&self.packs, &self.temporary_packs, &request.pack)?;
290 let files = match request.paths {
291 Some(ref paths) => paths.iter().filter_map(|path| pack.files().get(path)).map(From::from).collect(),
292 None => pack.files().par_iter().map(|(_, file)| From::from(file)).collect(),
293 };
294
295 Ok(FilesInfo { files })
296 }
297
298 pub fn pack_details(&self, pack_key: &str) -> Result<PackDetails> {
300 let pack = readable_pack(&self.packs, &self.temporary_packs, pack_key)?;
301 let bitmask = pack.bitmask();
302
303 Ok(PackDetails {
304 summary: pack_summary(pack_key, pack),
305 version: pack.pfh_version(),
306 compression: pack.compression_format(),
307 index_encrypted: bitmask.contains(PFHFlags::HAS_ENCRYPTED_INDEX),
308 data_encrypted: bitmask.contains(PFHFlags::HAS_ENCRYPTED_DATA),
309 index_includes_timestamp: bitmask.contains(PFHFlags::HAS_INDEX_WITH_TIMESTAMPS),
310 extended_header: bitmask.contains(PFHFlags::HAS_EXTENDED_HEADER),
311 timestamp: pack.internal_timestamp(),
312 dependencies: pack.dependencies().iter()
313 .map(|(enabled, name)| PackDependency { enabled: *enabled, name: name.clone() })
314 .collect(),
315 operational_mode: self.pack_operational_mode(pack_key),
316 })
317 }
318
319 pub fn pack_path(&self, pack_key: &str) -> Result<PathBuf> {
321 Ok(PathBuf::from(pack(&self.packs, pack_key)?.disk_file_path()))
322 }
323
324 pub fn pack_name(&self, pack_key: &str) -> Result<String> {
326 Ok(pack(&self.packs, pack_key)?.disk_file_name())
327 }
328
329 pub fn set_compression_format(&mut self, pack_key: &str, compression_format: CompressionFormat) -> Result<CompressionFormat> {
335 Ok(pack_mut(&mut self.packs, pack_key)?.set_compression_format(compression_format, &self.game))
336 }
337
338 pub fn pack_settings(&self, pack_key: &str) -> Result<PackSettings> {
340 Ok(pack(&self.packs, pack_key)?.settings().clone())
341 }
342
343 pub fn add_pack_ignored_diagnostics_line(&mut self, pack_key: &str, line: String) -> Result<()> {
345 let pack = pack_mut(&mut self.packs, pack_key)?;
346 let settings_text = pack.settings_mut().settings_text_mut();
347 match settings_text.get_mut("diagnostics_files_to_ignore") {
348 Some(diagnostics_ignored) => diagnostics_ignored.push_str(&line),
349 None => { settings_text.insert("diagnostics_files_to_ignore".to_owned(), line); },
350 }
351
352 Ok(())
353 }
354
355 pub fn pack_operational_mode(&self, pack_key: &str) -> OperationalMode {
357 self.pack_modes.get(pack_key).cloned().unwrap_or(OperationalMode::Normal)
358 }
359
360 pub fn pack_settings_values(&self, pack_key: &str) -> Result<PackSettingsValues> {
362 Ok(PackSettingsValues::from(pack(&self.packs, pack_key)?.settings()))
363 }
364
365 pub fn update_pack_settings(&mut self, request: &UpdatePackSettings) -> Result<PackSettingsValues> {
371 let settings = pack_mut(&mut self.packs, &request.pack)?.settings_mut();
372 settings.settings_text_mut().extend(request.settings.text.clone());
373 settings.settings_string_mut().extend(request.settings.string.clone());
374 settings.settings_bool_mut().extend(request.settings.bool.clone());
375 settings.settings_number_mut().extend(request.settings.number.clone());
376 self.pack_settings_values(&request.pack)
377 }
378
379 pub fn note_entries(&self, pack_key: &str, path: &str) -> Result<Vec<NoteEntry>> {
383 if path.is_empty() {
384 let notes = pack(&self.packs, pack_key)?.notes().file_notes().values().flatten().map(note_entry).collect();
385 return Ok(notes);
386 }
387
388 Ok(self.notes_for_path(pack_key, path)?.iter().map(note_entry).collect())
389 }
390
391 pub fn add_note_entry(&mut self, request: &AddNote) -> Result<NoteEntry> {
397 let mut note = Note::default();
398 note.set_path(request.path.clone());
399 note.set_message(request.message.clone());
400 note.set_url(request.url.clone());
401 note.set_id(request.id.unwrap_or_default());
402 Ok(note_entry(&self.add_note(&request.pack, note, request.id.is_some())?))
403 }
404
405 pub fn notes_for_path(&self, pack_key: &str, path: &str) -> Result<Vec<Note>> {
407 Ok(pack(&self.packs, pack_key)?.notes().notes_by_path(path))
408 }
409
410 pub fn add_note(&mut self, pack_key: &str, note: Note, replace: bool) -> Result<Note> {
416 Ok(pack_mut(&mut self.packs, pack_key)?.notes_mut().add_note(note, replace))
417 }
418
419 pub fn delete_note(&mut self, pack_key: &str, path: &str, id: u64) -> Result<()> {
421 pack_mut(&mut self.packs, pack_key)?.notes_mut().delete_note(path, id);
422 Ok(())
423 }
424
425 pub fn mark_packs_changed(&mut self, pack_key: Option<&str>) {
431 match pack_key {
432 Some(pack_key) if self.packs.contains_key(pack_key) => { self.changed_packs.insert(pack_key.to_owned()); },
433 Some(_) => {},
434 None => self.changed_packs.extend(self.packs.keys().cloned()),
435 }
436 }
437
438 pub fn autosave(&mut self, settings: &Settings) {
447 if let Ok(autosave_path) = backup_autosave_path() {
448 self.autosave_to(settings, autosave_path);
449 }
450 }
451
452 fn autosave_to(&mut self, settings: &Settings, autosave_path: PathBuf) {
456 if self.autosave_thread.as_ref().is_some_and(|thread| !thread.is_finished()) {
457 return;
458 }
459
460 let ca_paths = self.game.ca_packs_paths(&settings.path_buf(self.game.key()))
461 .unwrap_or_default()
462 .iter()
463 .map(|path| path.to_string_lossy().replace('\\', "/"))
464 .collect::<Vec<_>>();
465
466 let packs = std::mem::take(&mut self.changed_packs).into_iter()
467 .filter_map(|pack_key| self.packs.get(&pack_key))
468 .filter(|pack| {
469 let pack_type = pack.pfh_file_type();
470 !pack.settings().setting_bool("disable_autosaves").unwrap_or(&true) &&
471 (pack_type == PFHFileType::Mod || pack_type == PFHFileType::Movie) &&
472 !ca_paths.contains(&pack.disk_file_path().replace('\\', "/"))
473 })
474 .cloned()
475 .collect::<Vec<_>>();
476
477 if packs.is_empty() {
478 return;
479 }
480
481 let game = self.game.clone();
482 let disable_uuid_regeneration = settings.bool(DISABLE_UUID_REGENERATION_ON_DB_TABLES);
483 let autosave_amount = settings.i32(AUTOSAVE_AMOUNT).max(0) as usize;
484 self.autosave_thread = Some(std::thread::spawn(move || {
485 for mut pack in packs {
486 let folder = autosave_path.join(pack.disk_file_name());
487 if let Err(error) = save_autosave(&mut pack, &folder, &game, disable_uuid_regeneration, autosave_amount) {
488 warn!("Failed to autosave the pack {}: {error}", pack.disk_file_name());
489 }
490 }
491
492 let size = files_from_subdir(&autosave_path, true).unwrap_or_default().iter()
493 .filter_map(|path| path.metadata().ok())
494 .map(|metadata| metadata.len())
495 .sum::<u64>();
496
497 if size > AUTOSAVE_FOLDER_SIZE_WARNING {
498 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);
499 }
500 }));
501 }
502
503 pub fn wait_for_autosave(&mut self) {
507 if let Some(thread) = self.autosave_thread.take() {
508 let _ = thread.join();
509 }
510 }
511
512 pub fn missing_definitions(&mut self, pack_key: &str) -> Result<MissingDefinitions> {
516 let pack = pack_mut(&mut self.packs, pack_key)?;
517 let schema = loaded_schema(&self.schema)?;
518
519 let mut extra_data = DecodeableExtraData::default();
520 extra_data.set_schema(Some(schema));
521 let extra_data = Some(extra_data);
522
523 let mut files = pack.files_by_type_mut(&[FileType::DB]);
524 files.sort_by_key(|file| file.path_in_container_raw().to_lowercase());
525
526 let mut paths = vec![];
527 for file in files {
528 if file.decode(&extra_data, false, false).is_err() && file.load().is_ok() {
529 if let Ok(raw_data) = file.cached() {
530 let mut reader = Cursor::new(raw_data);
531 if let Ok((_, _, _, entry_count)) = DB::read_header(&mut reader) {
532 if entry_count > 0 {
533 paths.push(file.path_in_container_raw().to_owned());
534 }
535 }
536 }
537 }
538 }
539
540 Ok(MissingDefinitions { paths })
541 }
542
543 pub fn live_export(&mut self, pack_key: &str, settings: &Settings, disable_uuid_regeneration: bool, tsv_keys_first: bool) -> Result<()> {
552 let pack = pack_mut(&mut self.packs, pack_key)?;
553 let game_path = settings.path_buf(self.game.key());
554 pack.live_export(&self.game, &game_path, disable_uuid_regeneration, tsv_keys_first)?;
555 Ok(())
556 }
557
558 fn insert_pack(&mut self, pack: Pack) -> String {
564 let key = Uuid::new_v4().to_string();
565
566 self.session.set_pack_name(&key, &pack.disk_file_name());
567 self.pack_modes.insert(key.clone(), OperationalMode::Normal);
568 self.packs.insert(key.clone(), pack);
569 key
570 }
571}
572
573fn note_entry(note: &Note) -> NoteEntry {
575 NoteEntry {
576 id: *note.id(),
577 path: note.path().to_owned(),
578 message: note.message().to_owned(),
579 url: note.url().clone(),
580 }
581}
582
583fn save_autosave(pack: &mut Pack, folder: &Path, game: &GameInfo, disable_uuid_regeneration: bool, autosave_amount: usize) -> Result<()> {
597 DirBuilder::new().recursive(true).create(folder)?;
598
599 let date = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs();
600 let extra_data = encode_extra_data(game, pack.compression_format(), disable_uuid_regeneration);
601 pack.save(Some(&folder.join(format!("{date}.pack"))), game, &extra_data)?;
602
603 for file in files_in_folder_from_newest_to_oldest(folder)?.iter().skip(autosave_amount) {
604 let _ = std::fs::remove_file(file);
605 }
606
607 Ok(())
608}
609
610fn derive_new_pack_name(packs: &BTreeMap<String, Pack>) -> String {
612 let taken = |name: &str| packs.values().any(|pack| pack.disk_file_name() == name);
613 let base = format!("{}{}", DEFAULT_PACK_STEM, DEFAULT_PACK_EXT);
614 if !taken(&base) {
615 return base;
616 }
617
618 (2..).map(|suffix| format!("{}_{}{}", DEFAULT_PACK_STEM, suffix, DEFAULT_PACK_EXT))
619 .find(|candidate| !taken(candidate))
620 .expect("an unbounded range always finds a free name")
621}
622
623#[cfg(test)]
628#[path = "packs_test.rs"]
629mod packs_test;