Skip to main content

rpfm_ipc/
helpers.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 I&smael 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//! # IPC Helpers Module
12//!
13//! This module provides helper data structures for marshalling complex data between the UI and server.
14//! These types serve as lightweight, serializable representations of core library types, designed
15//! specifically for IPC transfer.
16//!
17//! ## Key Types
18//!
19//! ### Container Information
20//!
21//! - [`ContainerInfo`]: A reduced representation of a PackFile, containing only the metadata needed
22//!   by the UI (file name, path, version, type, compression settings).
23//!
24//! ### File Information
25//!
26//! - [`RFileInfo`]: Metadata about a packed file within a container (path, container name, timestamp,
27//!   file type). Used extensively in tree views and file listings.
28//! - [`VideoInfo`]: Metadata specific to video files (format, dimensions, framerate, etc.).
29//!
30//! ### Dependencies
31//!
32//! - [`DependenciesInfo`]: Contains paths to all dependency files (AssKit tables, vanilla files,
33//!   parent mod files) for populating dependency tree views.
34//!
35//! ### Data Sources
36//!
37//! - [`DataSource`]: Discriminates where data comes from (current PackFile, game files, parent mods,
38//!   AssKit files, or external files). Used throughout the UI to track file origins.
39//!
40//! ### File Creation
41//!
42//! - [`NewFile`]: Enum containing the parameters needed to create new files of various types
43//!   (AnimPack, DB table, Loc file, etc.).
44//!
45//! ### API Responses
46//!
47//! - [`APIResponse`]: Represents update check results (new update available, no update, etc.).
48//!
49//! ## Design Notes
50//!
51//! These types are intentionally simple and focus on data transfer rather than business logic.
52//! They use `#[derive(Serialize, Deserialize)]` for JSON serialization over WebSocket, and
53//! `#[derive(Getters)]` for convenient read-only access to fields.
54
55use getset::Getters;
56use rayon::prelude::*;
57use schemars::JsonSchema;
58use serde_derive::{Serialize, Deserialize};
59
60use std::fmt::{self, Display};
61
62use rpfm_extensions::dependencies::Dependencies;
63
64use crate::api::files::{EntryCopy, FileSource, NewFileKind};
65use crate::api::packs::PackDetails;
66
67use rpfm_lib::compression::CompressionFormat;
68use rpfm_lib::games::{*, pfh_file_type::PFHFileType, pfh_version::PFHVersion};
69use rpfm_lib::files::{animpack::*, Container, ContainerPath, db::*, FileType, pack::*, RFile, text::TextFormat, video::*};
70
71//-------------------------------------------------------------------------------//
72//                              Enums & Structs
73//-------------------------------------------------------------------------------//
74
75/// This struct is a reduced version of the `PackFile` one, used to pass just the needed data to an UI.
76///
77/// Don't create this one manually. Get it `From` the `PackFile` one, and use it as you need it.
78#[derive(Clone, Debug, Default, Getters, Serialize, Deserialize)]
79#[getset(get = "pub")]
80pub struct ContainerInfo {
81
82    /// The name of the PackFile's file, if exists. If not, then this should be empty.
83    file_name: String,
84
85    /// The path of the PackFile on disk, if exists. If not, then this should be empty.
86    file_path: String,
87
88    /// The version of the PackFile.
89    pfh_version: PFHVersion,
90
91    /// The type of the PackFile.
92    pfh_file_type: PFHFileType,
93
94    /// The bitmasks applied to the PackFile.
95    bitmask: PFHFlags,
96
97    /// If the container needs to be compress on save. None for no compression.
98    compress: CompressionFormat,
99
100    /// The timestamp of the last time the PackFile was saved.
101    timestamp: u64,
102}
103
104/// This struct represents the detailed info about the `PackedFile` we can provide to whoever request it.
105#[derive(Clone, Debug, Default, Getters, Serialize, Deserialize)]
106#[getset(get = "pub")]
107pub struct RFileInfo {
108
109    /// This is the path of the `PackedFile`.
110    path: String,
111
112    /// This is the name of the `Container` this file belongs to.
113    container_name: Option<String>,
114
115    /// This is the ***Last Modified*** time.
116    timestamp: Option<u64>,
117
118    file_type: FileType,
119
120    // If the `PackedFile` is compressed or not.
121    //is_compressed: bool,
122
123    // If the `PackedFile` is encrypted or not.
124    //is_encrypted: bool,
125
126    // If the `PackedFile` has been cached or not.
127    //is_cached: bool,
128
129    // The type of the cached `PackedFile`.
130    //cached_type: String,
131}
132
133/// This struct represents the detailed info about the `PackedFile` we can provide to whoever request it.
134#[derive(Clone, Debug, Default, Getters, Serialize, Deserialize)]
135#[getset(get = "pub")]
136pub struct VideoInfo {
137
138    /// Format of the video file
139    format: SupportedFormats,
140
141    /// Version number.
142    version: u16,
143
144    /// Codec FourCC (usually 'VP80').
145    codec_four_cc: String,
146
147    /// Width of the video in pixels.
148    width: u16,
149
150    /// Height of the video in pixels.
151    height: u16,
152
153    /// Number of frames on the video.
154    num_frames: u32,
155
156    /// Framerate of the video.
157    framerate: f32,
158}
159
160/// This struct contains the minimal data needed (mainly paths), to know what we have loaded in out dependencies.
161///
162/// NOTE: As this is intended to be a "Just use it and discard it" struct, we allow public members to make operations
163/// where we can move out of here faster.
164#[derive(Debug, Clone, Default, Getters, Serialize, Deserialize)]
165#[getset(get = "pub")]
166pub struct DependenciesInfo {
167
168    /// Full PackedFile-like paths of each asskit-only table.
169    pub asskit_tables: Vec<RFileInfo>,
170
171    /// Full list of vanilla PackedFile paths.
172    pub vanilla_packed_files: Vec<RFileInfo>,
173
174    /// Full list of parent PackedFile paths.
175    pub parent_packed_files: Vec<RFileInfo>,
176}
177
178/// This enum represents the source of the data in the view.
179#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Ord, PartialOrd, JsonSchema, Serialize, Deserialize)]
180pub enum DataSource {
181
182    /// This means the data is from somewhere in our PackFile.
183    PackFile,
184
185    /// This means the data is from one of the game files.
186    GameFiles,
187
188    /// This means the data comes from a parent PackFile.
189    ParentFiles,
190
191    /// This means the data comes from the AssKit files.
192    AssKitFiles,
193
194    /// This means the data comes from an external file.
195    ExternalFile,
196}
197
198/// This enum contains the data needed to create a new PackedFile.
199#[derive(Clone, Debug, Serialize, Deserialize)]
200pub enum NewFile {
201
202    /// Name of the file.
203    AnimPack(String),
204
205    /// Name of the file, Name of the Table, Version of the Table.
206    DB(String, String, i32),
207
208    /// Name of the Table.
209    Loc(String),
210
211    /// Name of the file, version of the file, and a list of entries that must be cloned from existing values in vanilla files (from, to).
212    PortraitSettings(String, u32, Vec<(String, String)>),
213
214    /// Name of the file and its format.
215    Text(String, TextFormat),
216
217    /// Name of the file.
218    VMD(String),
219
220    /// Name of the file.
221    WSModel(String),
222}
223
224/// This enum controls the possible responses from the server when checking for an update.
225#[derive(Debug, PartialEq, Eq, serde_derive::Serialize, serde_derive::Deserialize)]
226pub enum APIResponse {
227
228    /// This means a beta update was found.
229    NewBetaUpdate(String),
230
231    /// This means a major stable update was found.
232    NewStableUpdate(String),
233
234    /// This means a minor stable update was found.
235    NewUpdateHotfix(String),
236
237    /// This means no update was found.
238    NoUpdate,
239
240    /// This means don't know if there was an update or not, because the version we got was invalid.
241    UnknownVersion,
242}
243
244/// Information about an active session on the server.
245///
246/// This struct provides a snapshot of a session's state, including connection count
247/// and timeout information. It's used by the session management dialog to display
248/// available sessions and allow users to connect to specific ones.
249#[derive(Clone, Debug, Default, Getters, Serialize, Deserialize)]
250#[getset(get = "pub")]
251pub struct SessionInfo {
252
253    /// Unique identifier for the session.
254    session_id: u64,
255
256    /// Number of active connections to this session.
257    connection_count: u32,
258
259    /// Seconds remaining until session timeout (None if session has active connections).
260    timeout_remaining_secs: Option<u64>,
261
262    /// Whether the session has been marked for shutdown.
263    is_shutting_down: bool,
264
265    /// Names of the pack files currently open in this session.
266    pack_names: Vec<String>,
267}
268
269//-------------------------------------------------------------------------------//
270//                             Implementations
271//-------------------------------------------------------------------------------//
272
273impl From<&Pack> for ContainerInfo {
274    fn from(pack: &Pack) -> Self {
275
276        // If we have no disk file for the pack, it's a new one.
277        let file_name = if pack.disk_file_path().is_empty() {
278            "new_file.pack"
279        } else {
280            pack.disk_file_path().split('/').next_back().unwrap_or("unknown.pack")
281        };
282
283        Self {
284            file_name: file_name.to_string(),
285            file_path: pack.disk_file_path().to_string(),
286            pfh_version: *pack.header().pfh_version(),
287            pfh_file_type: *pack.header().pfh_file_type(),
288            bitmask: *pack.header().bitmask(),
289            timestamp: *pack.header().internal_timestamp(),
290            compress: pack.compression_format(),
291        }
292    }
293}
294
295impl From<&PackDetails> for ContainerInfo {
296    fn from(details: &PackDetails) -> Self {
297        let mut bitmask = PFHFlags::empty();
298        bitmask.set(PFHFlags::HAS_ENCRYPTED_INDEX, details.index_encrypted);
299        bitmask.set(PFHFlags::HAS_ENCRYPTED_DATA, details.data_encrypted);
300        bitmask.set(PFHFlags::HAS_INDEX_WITH_TIMESTAMPS, details.index_includes_timestamp);
301        bitmask.set(PFHFlags::HAS_EXTENDED_HEADER, details.extended_header);
302
303        // Packs not saved on disk have no path.
304        Self {
305            file_name: details.summary.name.clone(),
306            file_path: details.summary.path.clone().unwrap_or_default(),
307            pfh_version: details.version,
308            pfh_file_type: details.summary.pack_type,
309            bitmask,
310            compress: details.compression,
311            timestamp: details.timestamp,
312        }
313    }
314}
315
316/// NOTE: DO NOT USE THIS FOR ANIMPACKS WITHIN PACKS.
317///
318/// It sets the path and name wrong in those cases.
319impl From<&AnimPack> for ContainerInfo {
320    fn from(animpack: &AnimPack) -> Self {
321        Self {
322            file_name: animpack.disk_file_path().split('/').next_back().unwrap_or("unknown.animpack").to_string(),
323            file_path: animpack.disk_file_path().to_string(),
324            ..Default::default()
325        }
326    }
327}
328
329/// Creates a [`ContainerInfo`] from an [`RFileInfo`].
330///
331/// This is used when treating an individual file as if it were a container (e.g., for AnimPacks
332/// stored within a Pack). Most fields default to their default values since a single file
333/// doesn't have pack-level metadata.
334impl From<&RFileInfo> for ContainerInfo {
335    fn from(file_info: &RFileInfo) -> Self {
336        Self {
337            file_name: ContainerPath::File(file_info.path().to_owned()).name().unwrap_or("unknown").to_string(),
338            file_path: file_info.path().to_owned(),
339            ..Default::default()
340        }
341    }
342}
343
344/// Creates an [`RFileInfo`] from an [`RFile`].
345///
346/// Extracts the path, container name, timestamp, and file type from the packed file.
347/// This is the primary way to create file info for display in the UI.
348impl From<&RFile> for RFileInfo {
349    fn from(rfile: &RFile) -> Self {
350        //let is_cached = !matches!(rfile.get_ref_decoded(), DecodedPackedFile::Unknown);
351        //let cached_type = if let DecodedPackedFile::Unknown = rfile.get_ref_decoded() { "Not Yet Cached".to_owned() }
352        //else { format!("{:?}", PackedFileType::from(rfile.get_ref_decoded())) };
353        Self {
354            path: rfile.path_in_container_raw().to_owned(),
355            container_name: rfile.container_name().clone(),
356            timestamp: rfile.timestamp(),
357            file_type: rfile.file_type(),
358            //is_compressed: rfile.get_ref_raw().get_compression_state(),
359            //is_encrypted: rfile.get_ref_raw().get_encryption_state(),
360            //is_cached,
361            //cached_type,
362        }
363    }
364}
365
366/// Creates a [`VideoInfo`] from a [`Video`].
367///
368/// Extracts all video metadata (format, codec, dimensions, frame count, framerate) for display
369/// in the UI's video viewer.
370impl From<&Video> for VideoInfo {
371    fn from(video: &Video) -> Self {
372        Self {
373            format: *video.format(),
374            version: *video.version(),
375            codec_four_cc: video.codec_four_cc().to_string(),
376            width: *video.width(),
377            height: *video.height(),
378            num_frames: *video.num_frames(),
379            framerate: *video.framerate(),
380        }
381    }
382}
383
384impl DependenciesInfo {
385    pub fn new(dependencies: &Dependencies, table_name_logic: &VanillaDBTableNameLogic) -> Self {
386        let asskit_tables = dependencies.asskit_only_db_tables().values().map(|table| {
387            let table_name = match table_name_logic {
388                VanillaDBTableNameLogic::DefaultName(ref name) => name,
389                VanillaDBTableNameLogic::FolderName => table.table_name(),
390            };
391
392            RFileInfo::from_db(table, table_name)
393        }).collect::<Vec<RFileInfo>>();
394
395        let vanilla_packed_files = dependencies.vanilla_loose_files()
396            .par_iter()
397            .chain(dependencies.vanilla_files().par_iter())
398            .map(|(_, value)| From::from(value))
399            .collect::<Vec<RFileInfo>>();
400
401        let parent_packed_files = dependencies.parent_files()
402            .par_iter()
403            .map(|(_, value)| From::from(value))
404            .collect::<Vec<RFileInfo>>();
405
406        Self {
407            asskit_tables,
408            vanilla_packed_files,
409            parent_packed_files,
410        }
411    }
412}
413
414impl RFileInfo {
415
416    /// Returns the table name for DB files.
417    ///
418    /// For DB files, the path format is `db/<table_name>/<file_name>`, so this extracts
419    /// the second path component. Returns `None` if the file is not a DB file or if
420    /// the path doesn't have the expected structure.
421    pub fn table_name(&self) -> Option<&str> {
422        if self.file_type == FileType::DB {
423            self.path().split('/').collect::<Vec<_>>().get(1).cloned()
424        } else {
425            None
426        }
427    }
428
429    /// Creates an [`RFileInfo`] from a [`DB`] table and its file name.
430    ///
431    /// This is used to create file info for AssKit-only tables that don't have a backing
432    /// [`RFile`]. The path is constructed as `db/<table_name>/<table_file_name>`.
433    pub fn from_db(db: &DB, table_file_name: &str) -> Self {
434        Self {
435            path: format!("db/{}/{}", db.table_name(), table_file_name),
436            container_name: None,
437            timestamp: None,
438            file_type: FileType::DB,
439        }
440    }
441}
442
443impl DataSource {
444
445    /// Returns the file source of the files of this data source.
446    ///
447    /// # Arguments
448    ///
449    /// * `pack_key` - Key of the pack, for files of open packs.
450    ///
451    /// # Returns
452    ///
453    /// The file source, or `None` for external files.
454    pub fn file_source(&self, pack_key: &str) -> Option<FileSource> {
455        match self {
456            Self::PackFile => Some(FileSource::Pack(pack_key.to_owned())),
457            Self::GameFiles => Some(FileSource::GameFiles),
458            Self::ParentFiles => Some(FileSource::ParentFiles),
459            Self::AssKitFiles => Some(FileSource::AssemblyKit),
460            Self::ExternalFile => None,
461        }
462    }
463}
464
465/// Kind of file of a new file. The name of the file is part of its path, so it's dropped.
466impl From<NewFile> for NewFileKind {
467    fn from(new_file: NewFile) -> Self {
468        match new_file {
469            NewFile::AnimPack(_) => Self::AnimPack,
470            NewFile::DB(_, table_name, version) => Self::Db { table_name, version: Some(version) },
471            NewFile::Loc(_) => Self::Loc,
472            NewFile::PortraitSettings(_, version, entries) => Self::PortraitSettings {
473                version,
474                copy_entries: entries.into_iter().map(|(from, to)| EntryCopy { from, to }).collect(),
475            },
476            NewFile::Text(_, format) => Self::Text { format: Some(format) },
477            NewFile::VMD(_) => Self::Vmd,
478            NewFile::WSModel(_) => Self::WsModel,
479        }
480    }
481}
482
483/// Data source of the files of a file source.
484impl From<&FileSource> for DataSource {
485    fn from(source: &FileSource) -> Self {
486        match source {
487            FileSource::Pack(_) => Self::PackFile,
488            FileSource::GameFiles => Self::GameFiles,
489            FileSource::ParentFiles => Self::ParentFiles,
490            FileSource::AssemblyKit => Self::AssKitFiles,
491        }
492    }
493}
494
495/// Displays the [`DataSource`] as a human-readable string.
496///
497/// Used for logging and UI display purposes.
498impl Display for DataSource {
499    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
500        Display::fmt(match self {
501            Self::PackFile => "PackFile",
502            Self::GameFiles => "GameFiles",
503            Self::ParentFiles => "ParentFiles",
504            Self::AssKitFiles => "AssKitFiles",
505            Self::ExternalFile => "ExternalFile",
506        }, f)
507    }
508}
509
510/// Parses a [`DataSource`] from its string representation.
511///
512/// This is the inverse of the [`Display`] implementation. Panics if the string doesn't match
513/// any known data source.
514impl From<&str> for DataSource {
515    fn from(value: &str) -> Self {
516        match value {
517            "PackFile" => Self::PackFile,
518            "GameFiles" => Self::GameFiles,
519            "ParentFiles" => Self::ParentFiles,
520            "AssKitFiles" => Self::AssKitFiles,
521            "ExternalFile" => Self::ExternalFile,
522            _ => unreachable!("from data source {}", value)
523        }
524    }
525}
526
527impl SessionInfo {
528
529    /// Create a new SessionInfo with the given parameters.
530    pub fn new(
531        session_id: u64,
532        connection_count: u32,
533        timeout_remaining_secs: Option<u64>,
534        is_shutting_down: bool,
535        pack_names: Vec<String>,
536    ) -> Self {
537        Self {
538            session_id,
539            connection_count,
540            timeout_remaining_secs,
541            is_shutting_down,
542            pack_names,
543        }
544    }
545}