Skip to main content

rpfm_server/state/
packs.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
11//! Pack lifecycle and metadata operations: open, create, save, close, flags, settings and notes.
12
13use 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
44/// Stem used to seed names for newly created Packs (`new_pack.pack`, `new_pack_2.pack`, …).
45const DEFAULT_PACK_STEM: &str = "new_pack";
46
47/// Extension appended to [`DEFAULT_PACK_STEM`] when materialising a new Pack's filename.
48const DEFAULT_PACK_EXT: &str = ".pack";
49
50/// Size of the autosave folder, in bytes, over which a warning is logged after each autosave (25 GB).
51const AUTOSAVE_FOLDER_SIZE_WARNING: u64 = 25 * 1024 * 1024 * 1024;
52
53/// Name of the pack with all the CA packs of the selected game merged.
54const CA_PACKS_NAME: &str = "CA PackFiles";
55
56impl SessionState {
57
58    /// Creates a new empty pack.
59    ///
60    /// # Arguments
61    ///
62    /// * `name` - File name of the pack. If `None`, a free `new_pack.pack`-like name is used.
63    /// * `settings` - Settings, to find the game's install folder.
64    ///
65    /// # Returns
66    ///
67    /// The key of the new pack.
68    ///
69    /// # Errors
70    ///
71    /// Fails if the name is not a file name ending in `.pack`.
72    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    /// Opens one or more packs, merged into a single one.
92    ///
93    /// # Arguments
94    ///
95    /// * `paths` - Paths of the packs to open. If there are more than one, the merged pack has no path, and is named after the first one.
96    /// * `lazy_loading` - If file data should be read from disk only when needed.
97    /// * `temporary` - If the pack is only opened to copy files from it, so it's kept out of the open packs.
98    ///
99    /// # Returns
100    ///
101    /// The key and info of the opened pack.
102    ///
103    /// # Errors
104    ///
105    /// Fails if any of the packs is already open, or if any of them can't be read.
106    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    /// Opens all the CA packs of the selected game, merged into a single one.
135    ///
136    /// # Arguments
137    ///
138    /// * `settings` - Settings, to find the game's install folder.
139    ///
140    /// # Returns
141    ///
142    /// The key and info of the opened pack.
143    ///
144    /// # Errors
145    ///
146    /// Fails if they're already open, or if they can't be read.
147    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    /// Closes a pack without saving it.
162    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    /// Closes all open packs without saving them.
175    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    /// Saves a pack to disk.
187    ///
188    /// # Arguments
189    ///
190    /// * `pack_key` - Key of the pack to save.
191    /// * `path` - Absolute path to save the pack to, creating its missing folders. If `None`, the pack is saved to its current path.
192    /// * `options` - How to save the pack.
193    ///
194    /// # Returns
195    ///
196    /// The info of the saved pack.
197    ///
198    /// # Errors
199    ///
200    /// Fails if the pack is of a CA type and editing them is not allowed, if it has no path on disk yet, if the new path is not absolute, or if saving fails.
201    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        // New packs only have a bare name, which would silently save to the server's path.
211        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    /// Changes properties of a pack. Only the properties set in the request are changed.
239    ///
240    /// # Errors
241    ///
242    /// Fails, without changing anything, if the request enables encryption on a pack whose version doesn't support it.
243    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    /// Returns the short description of an open pack.
283    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    /// Returns the info of files of a pack: the ones at the requested paths, or all of them.
288    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    /// Returns the details of an open pack.
299    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    /// Returns the path of a pack on disk.
320    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    /// Returns the file name of a pack.
325    pub fn pack_name(&self, pack_key: &str) -> Result<String> {
326        Ok(pack(&self.packs, pack_key)?.disk_file_name())
327    }
328
329    /// Changes the compression format of a pack.
330    ///
331    /// # Returns
332    ///
333    /// The format actually set, which may differ from the requested one if the game doesn't support it.
334    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    /// Returns the settings of a pack.
339    pub fn pack_settings(&self, pack_key: &str) -> Result<PackSettings> {
340        Ok(pack(&self.packs, pack_key)?.settings().clone())
341    }
342
343    /// Appends a line to the list of files a pack ignores in the diagnostics.
344    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    /// Returns the operational mode of a pack. Unknown packs are in normal mode.
356    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    /// Returns the settings of a pack.
361    pub fn pack_settings_values(&self, pack_key: &str) -> Result<PackSettingsValues> {
362        Ok(PackSettingsValues::from(pack(&self.packs, pack_key)?.settings()))
363    }
364
365    /// Changes settings of a pack. Only the keys set in the request are changed.
366    ///
367    /// # Returns
368    ///
369    /// All the settings of the pack after the change.
370    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    /// Returns the notes of a pack for a path, with the notes of the folders containing it, as API entries.
380    ///
381    /// If the path is empty, all the notes of the pack are returned.
382    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    /// Attaches a note to a file or folder of a pack.
392    ///
393    /// # Returns
394    ///
395    /// The added note, with its ID.
396    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    /// Returns the notes of a pack under a path.
406    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    /// Adds a note to a pack.
411    ///
412    /// # Returns
413    ///
414    /// The added note, with its ID assigned.
415    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    /// Deletes a note from a pack.
420    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    /// Marks packs as changed since their last autosave.
426    ///
427    /// # Arguments
428    ///
429    /// * `pack_key` - Key of the changed pack, or `None` if any open pack may have changed.
430    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    /// Saves a backup copy of each pack changed since its last autosave in the autosave folder, keeping the newest ones.
439    ///
440    /// The packs are copied here, but saved from another thread, so the session can keep running requests meanwhile.
441    /// Vanilla packs, packs with autosaves disabled and packs that are neither mods nor movies are skipped.
442    ///
443    /// # Arguments
444    ///
445    /// * `settings` - Settings, for the game's install folder, the amount of autosaves to keep, and how to encode tables.
446    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    /// Same as [`Self::autosave`], but saving the backups in `autosave_path`.
453    ///
454    /// If the previous autosave is still running, this does nothing, and the changed packs are left for the next one.
455    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    /// Waits until the running autosave, if any, finishes.
504    ///
505    /// Autosaves read lazy-loaded files from the packs on disk, so this must be called before replacing any of them.
506    pub fn wait_for_autosave(&mut self) {
507        if let Some(thread) = self.autosave_thread.take() {
508            let _ = thread.join();
509        }
510    }
511
512    /// Returns the tables of a pack with rows that can't be decoded with the schema, sorted by path.
513    ///
514    /// This is slow, and only useful when a new patch lands and you want to know what tables need decoding.
515    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    /// Exports the files of a pack to the game's data folder, so they can be tested without saving the pack.
544    ///
545    /// # Arguments
546    ///
547    /// * `pack_key` - Key of the pack to export.
548    /// * `settings` - Settings, to find the game's install folder.
549    /// * `disable_uuid_regeneration` - If tables keep their GUID when encoded.
550    /// * `tsv_keys_first` - If exported TSV files use the old column order, with keys first.
551    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    /// Adds a pack to the open ones, in normal mode.
559    ///
560    /// # Returns
561    ///
562    /// The key of the pack.
563    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
573/// Returns a note as an API entry.
574fn 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
583/// Saves a pack as a new autosave in a folder, removing the oldest autosaves over the amount to keep.
584///
585/// # Arguments
586///
587/// * `pack` - Copy of the pack to save.
588/// * `folder` - Folder with the autosaves of the pack.
589/// * `game` - Game the pack is for.
590/// * `disable_uuid_regeneration` - If tables keep their GUID when encoded.
591/// * `autosave_amount` - Amount of autosaves to keep.
592///
593/// # Errors
594///
595/// If the folder can't be created, or the pack can't be saved.
596fn 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
610/// Derives a pack name for new (unsaved) packs no open pack has, like "new_pack.pack", "new_pack_2.pack", etc.
611fn 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//-------------------------------------------------------------------------------//
624//                                   Tests
625//-------------------------------------------------------------------------------//
626
627#[cfg(test)]
628#[path = "packs_test.rs"]
629mod packs_test;