Skip to main content

rpfm_server/state/
mod.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//! Per-session state, and the operations a session can run on it.
12//!
13//! [`SessionState`] owns everything a session works with: the open packs, the selected game,
14//! its schema and its dependencies. The operations live in the submodules, split by domain,
15//! and return plain results instead of protocol messages. [`crate::api`] translates the
16//! requests of the API into these operations.
17
18use anyhow::Result;
19use rayon::prelude::*;
20
21use std::collections::{BTreeMap, BTreeSet};
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24use std::thread::JoinHandle;
25
26use rpfm_extensions::dependencies::Dependencies;
27use rpfm_extensions::lua::{ASSEMBLY_KIT_SCRIPT_DOCS_PATH, LuaApi};
28use rpfm_extensions::search::GlobalSearch;
29
30use rpfm_ipc::api::ApiError;
31use rpfm_ipc::api::packs::PackSummary;
32use rpfm_ipc::api::session::{DependenciesStatus, SessionStatus};
33use rpfm_ipc::api::packs::OperationalMode;
34use rpfm_ipc::settings_keys::ASSEMBLY_KIT_SUFFIX;
35
36use rpfm_lib::compression::CompressionFormat;
37use rpfm_lib::files::{Container, DecodeableExtraData, EncodeableExtraData, FileType, pack::Pack, RFile};
38use rpfm_lib::games::{GameInfo, supported_games::{KEY_WARHAMMER_3, SupportedGames}};
39use rpfm_lib::schema::Schema;
40
41use rpfm_telemetry::info;
42
43use crate::session::Session;
44use rpfm_ipc::settings::{schemas_path, table_patches_path, Settings};
45
46mod dependencies;
47mod diagnostics;
48mod files;
49mod packs;
50mod schema;
51mod search;
52mod tables;
53mod tools;
54
55pub use self::files::{DecodedFile, PathsByPack};
56use self::diagnostics::DiagnosticsResults;
57pub use self::tables::MergeOutcome;
58pub use self::tools::{MyModOptions, optimizer_options_with, plugin_scripts};
59use self::tools::PendingStartpos;
60
61//-------------------------------------------------------------------------------//
62//                              Enums & Structs
63//-------------------------------------------------------------------------------//
64
65/// Everything a session owns. One instance lives in each session's background loop.
66pub struct SessionState {
67
68    /// The session this state belongs to. Used to keep its list of open pack names in sync.
69    session: Arc<Session>,
70
71    /// Registry of every game RPFM supports.
72    supported_games: SupportedGames,
73
74    /// The game currently selected.
75    game: GameInfo,
76
77    /// Schema of the selected game, if it could be loaded.
78    schema: Option<Schema>,
79
80    /// If the game has already been selected once, so the first selection always rebuilds everything.
81    first_game_change_done: bool,
82
83    /// All open packs, by key.
84    ///
85    /// Each pack's `disk_file_path` is either its absolute path on disk, or only its file name if it's not saved there.
86    packs: BTreeMap<String, Pack>,
87
88    /// Packs opened only to copy files from them, by key. They're kept apart from `packs` so nothing working on all open packs uses them.
89    temporary_packs: BTreeMap<String, Pack>,
90
91    /// Operational mode (Normal or MyMod) of each open pack, keyed by the same pack key as `packs`.
92    pack_modes: BTreeMap<String, OperationalMode>,
93
94
95    /// Vanilla and parent files of the selected game.
96    dependencies: Dependencies,
97
98    /// Why the dependencies couldn't be fully loaded the last time they were, if they couldn't.
99    dependencies_error: Option<String>,
100
101    /// Keys of the packs changed since their last autosave.
102    changed_packs: BTreeSet<String>,
103
104    /// Thread writing the last autosave, if it may still be running. Saves wait for it, so they don't replace packs it's reading.
105    autosave_thread: Option<JoinHandle<()>>,
106
107    /// The last search, with its matches, if any.
108    search: Option<GlobalSearch>,
109
110    /// Startpos build started and not yet finished, if any.
111    pending_startpos: Option<PendingStartpos>,
112
113    /// Results of the last diagnostics check, if any.
114    diagnostics: Option<DiagnosticsResults>,
115
116    /// Lua scripting API of the selected game, built on first use. See [`cached_lua_api`].
117    lua_api_cache: Option<LuaApiCache>,
118}
119
120/// Options for operations that write files of a pack to disk.
121#[derive(Debug, Clone, Copy, Default)]
122pub struct ExtractOptions {
123
124    /// If tables keep their GUID when encoded.
125    pub disable_uuid_regeneration: bool,
126
127    /// If tables are written as TSV with the old column order, with keys first.
128    pub tsv_keys_first: bool,
129}
130
131/// Options for saving a pack.
132#[derive(Debug, Clone, Copy, Default)]
133pub struct SaveOptions {
134
135    /// If tables keep their GUID when encoded.
136    pub disable_uuid_regeneration: bool,
137
138    /// If packs of CA types can be saved.
139    pub allow_editing_of_ca_packfiles: bool,
140
141    /// If files that failed to decode are removed before saving.
142    pub clean: bool,
143}
144
145/// A file to paste. Only its path is stored: the file itself is cloned from its pack when pasting.
146#[derive(Debug)]
147struct PasteEntry {
148
149    /// Path of the file in its pack.
150    file_path: String,
151
152    /// Part of `file_path` stripped when pasting, so the pasted path starts at the selected file or folder.
153    base_path: String,
154
155    /// Key of the pack the file comes from.
156    source_pack_key: String,
157}
158
159/// Cached Lua scripting API: the docs path and dependencies build date it was built from, and the API itself.
160type LuaApiCache = (PathBuf, u64, Option<LuaApi>);
161
162//-------------------------------------------------------------------------------//
163//                             Implementations
164//-------------------------------------------------------------------------------//
165
166impl SessionState {
167
168    /// Creates the state of a new session, with Warhammer 3 selected and nothing loaded.
169    ///
170    /// # Arguments
171    ///
172    /// * `session` - The session this state belongs to.
173    pub fn new(session: Arc<Session>) -> Self {
174        let supported_games = SupportedGames::default();
175        let game = supported_games.game(KEY_WARHAMMER_3)
176            .expect("Warhammer 3 is always a supported game")
177            .clone();
178
179        Self {
180            session,
181            supported_games,
182            game,
183            schema: None,
184            first_game_change_done: false,
185            packs: BTreeMap::new(),
186            temporary_packs: BTreeMap::new(),
187            pack_modes: BTreeMap::new(),
188            dependencies: Dependencies::default(),
189            dependencies_error: None,
190            changed_packs: BTreeSet::new(),
191            autosave_thread: None,
192            search: None,
193            pending_startpos: None,
194            diagnostics: None,
195            lua_api_cache: None,
196        }
197    }
198
199    /// Returns the game currently selected.
200    pub fn game(&self) -> &GameInfo {
201        &self.game
202    }
203
204    /// Returns the selected game, what's loaded for it, and the open packs.
205    pub fn session_status(&self) -> SessionStatus {
206        SessionStatus {
207            game: self.game.key().to_owned(),
208            schema_loaded: self.schema.is_some(),
209            dependencies: DependenciesStatus {
210                vanilla_loaded: self.dependencies.is_vanilla_data_loaded(false),
211                assembly_kit_loaded: !self.dependencies.asskit_only_db_tables().is_empty(),
212                parent_files: self.dependencies.parent_files().len(),
213                error: self.dependencies_error.clone(),
214            },
215            packs: self.packs.iter().map(|(key, pack)| pack_summary(key, pack)).collect(),
216        }
217    }
218}
219
220/// Returns the short description of an open pack.
221fn pack_summary(key: &str, pack: &Pack) -> PackSummary {
222    PackSummary {
223        key: key.to_owned(),
224        name: pack.disk_file_name(),
225        path: Some(pack.disk_file_path().to_owned()).filter(|path| Path::new(path).is_absolute()),
226        pack_type: pack.pfh_file_type(),
227        file_count: pack.files().len(),
228    }
229}
230
231/// Returns the open pack with the provided key.
232fn pack<'a>(packs: &'a BTreeMap<String, Pack>, pack_key: &str) -> Result<&'a Pack> {
233    packs.get(pack_key).ok_or_else(|| ApiError::PackNotFound(pack_key.to_owned()).into())
234}
235
236/// Returns the open or temporary pack with the provided key.
237fn readable_pack<'a>(packs: &'a BTreeMap<String, Pack>, temporary_packs: &'a BTreeMap<String, Pack>, pack_key: &str) -> Result<&'a Pack> {
238    packs.get(pack_key).or_else(|| temporary_packs.get(pack_key)).ok_or_else(|| ApiError::PackNotFound(pack_key.to_owned()).into())
239}
240
241/// Returns the open pack with the provided key, mutably.
242fn pack_mut<'a>(packs: &'a mut BTreeMap<String, Pack>, pack_key: &str) -> Result<&'a mut Pack> {
243    packs.get_mut(pack_key).ok_or_else(|| ApiError::PackNotFound(pack_key.to_owned()).into())
244}
245
246/// Returns the loaded schema, or an error if there is none.
247fn loaded_schema(schema: &Option<Schema>) -> Result<&Schema> {
248    schema.as_ref().ok_or_else(|| ApiError::SchemaNotLoaded.into())
249}
250
251/// Returns the loaded schema mutably, or an error if there is none.
252fn loaded_schema_mut(schema: &mut Option<Schema>) -> Result<&mut Schema> {
253    schema.as_mut().ok_or_else(|| ApiError::SchemaNotLoaded.into())
254}
255
256/// Returns the names of the parent packs of every open pack.
257fn parent_pack_names(packs: &BTreeMap<String, Pack>) -> Vec<String> {
258    packs.values()
259        .flat_map(|pack| pack.dependencies().iter().map(|(_, name)| name.clone()))
260        .collect()
261}
262
263/// Returns the data needed to encode files of a pack for the provided game.
264fn encode_extra_data(game: &GameInfo, compression_format: CompressionFormat, disable_uuid_regeneration: bool) -> Option<EncodeableExtraData<'_>> {
265    Some(EncodeableExtraData::new_from_game_info_and_settings(game, compression_format, disable_uuid_regeneration))
266}
267
268/// Decodes the DB and Loc files among `files`, so they're in memory for the diagnostics to work.
269///
270/// Files that fail to decode are left as they are. Does nothing if there is no schema.
271fn decode_tables(files: &mut [&mut RFile], schema: &Option<Schema>) {
272    if let Some(schema) = schema {
273        let mut extra_data = DecodeableExtraData::default();
274        extra_data.set_schema(Some(schema));
275        let extra_data = Some(extra_data);
276
277        files.par_iter_mut()
278            .filter(|file| file.file_type() == FileType::DB || file.file_type() == FileType::Loc)
279            .for_each(|file| {
280                let _ = file.decode(&extra_data, true, false);
281            });
282    }
283}
284
285/// Returns the Lua scripting API of a game, rebuilding the cached one if it's outdated.
286///
287/// The API is rebuilt when the game or its Assembly Kit path change, or when the dependencies cache is regenerated.
288///
289/// # Arguments
290///
291/// * `cache` - Cached API, updated if outdated.
292/// * `game` - Game whose API to return.
293/// * `settings` - Settings, to find the game's Assembly Kit.
294/// * `dependencies` - Dependencies cache with the vanilla scripts of the game.
295///
296/// # Returns
297///
298/// The API, or `None` if the game's Assembly Kit has no scripting docs.
299fn cached_lua_api<'a>(cache: &'a mut Option<LuaApiCache>, game: &GameInfo, settings: &Settings, dependencies: &Dependencies) -> Option<&'a LuaApi> {
300    let docs_path = settings.path_buf(&format!("{}{}", game.key(), ASSEMBLY_KIT_SUFFIX)).join(ASSEMBLY_KIT_SCRIPT_DOCS_PATH);
301    let build_date = *dependencies.build_date();
302    let outdated = cache.as_ref().is_none_or(|(path, date, _)| *path != docs_path || *date != build_date);
303
304    if outdated {
305        let api = match LuaApi::from_assembly_kit(&docs_path) {
306            Ok(mut api) => {
307                api.add_vanilla_scripts(dependencies);
308                Some(api)
309            }
310            Err(error) => {
311                info!("Lua scripting API not available, Lua scripts will only get their syntax checked: {error}");
312                None
313            }
314        };
315
316        *cache = Some((docs_path, build_date, api));
317    }
318
319    cache.as_ref().and_then(|(_, _, api)| api.as_ref())
320}
321
322/// Loads the schema of a game, re-decoding the tables of every open pack with it.
323///
324/// # Arguments
325///
326/// * `schema` - Schema to replace. Set to `None` if the game's schema can't be loaded.
327/// * `packs` - Open packs whose tables need re-decoding.
328/// * `game` - Game whose schema to load.
329/// * `disable_uuid_regeneration` - If tables keep their GUID when encoded before the switch.
330fn load_schema(schema: &mut Option<Schema>, packs: &mut BTreeMap<String, Pack>, game: &GameInfo, disable_uuid_regeneration: bool) {
331
332    // Before loading the schema, make sure we don't have tables with definitions from the current schema.
333    for pack in packs.values_mut() {
334        let extra_data = encode_extra_data(game, pack.compression_format(), disable_uuid_regeneration);
335        let mut files = pack.files_by_type_mut(&[FileType::DB]);
336
337        files.par_iter_mut().for_each(|file| {
338            let _ = file.encode(&extra_data, true, true, false);
339        });
340    }
341
342    let schema_path = schemas_path().unwrap().join(game.schema_file_name());
343    let local_patches_path = table_patches_path().unwrap().join(game.schema_file_name());
344    *schema = Schema::load(&schema_path, Some(&local_patches_path)).ok();
345
346    for pack in packs.values_mut() {
347        decode_tables(&mut pack.files_by_type_mut(&[FileType::DB]), schema);
348    }
349}