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
89    /// Operational mode (Normal or MyMod) of each open pack, keyed by the same pack key as `packs`.
90    pack_modes: BTreeMap<String, OperationalMode>,
91
92
93    /// Vanilla and parent files of the selected game.
94    dependencies: Dependencies,
95
96    /// Why the dependencies couldn't be fully loaded the last time they were, if they couldn't.
97    dependencies_error: Option<String>,
98
99    /// Keys of the packs changed since their last autosave.
100    changed_packs: BTreeSet<String>,
101
102    /// Thread writing the last autosave, if it may still be running. Saves wait for it, so they don't replace packs it's reading.
103    autosave_thread: Option<JoinHandle<()>>,
104
105    /// The last search, with its matches, if any.
106    search: Option<GlobalSearch>,
107
108    /// Startpos build started and not yet finished, if any.
109    pending_startpos: Option<PendingStartpos>,
110
111    /// Results of the last diagnostics check, if any.
112    diagnostics: Option<DiagnosticsResults>,
113
114    /// Lua scripting API of the selected game, built on first use. See [`cached_lua_api`].
115    lua_api_cache: Option<LuaApiCache>,
116}
117
118/// Options for operations that write files of a pack to disk.
119#[derive(Debug, Clone, Copy, Default)]
120pub struct ExtractOptions {
121
122    /// If tables keep their GUID when encoded.
123    pub disable_uuid_regeneration: bool,
124
125    /// If tables are written as TSV with the old column order, with keys first.
126    pub tsv_keys_first: bool,
127}
128
129/// Options for saving a pack.
130#[derive(Debug, Clone, Copy, Default)]
131pub struct SaveOptions {
132
133    /// If tables keep their GUID when encoded.
134    pub disable_uuid_regeneration: bool,
135
136    /// If packs of CA types can be saved.
137    pub allow_editing_of_ca_packfiles: bool,
138
139    /// If files that failed to decode are removed before saving.
140    pub clean: bool,
141}
142
143/// A file to paste. Only its path is stored: the file itself is cloned from its pack when pasting.
144#[derive(Debug)]
145struct PasteEntry {
146
147    /// Path of the file in its pack.
148    file_path: String,
149
150    /// Part of `file_path` stripped when pasting, so the pasted path starts at the selected file or folder.
151    base_path: String,
152
153    /// Key of the pack the file comes from.
154    source_pack_key: String,
155}
156
157/// Cached Lua scripting API: the docs path and dependencies build date it was built from, and the API itself.
158type LuaApiCache = (PathBuf, u64, Option<LuaApi>);
159
160//-------------------------------------------------------------------------------//
161//                             Implementations
162//-------------------------------------------------------------------------------//
163
164impl SessionState {
165
166    /// Creates the state of a new session, with Warhammer 3 selected and nothing loaded.
167    ///
168    /// # Arguments
169    ///
170    /// * `session` - The session this state belongs to.
171    pub fn new(session: Arc<Session>) -> Self {
172        let supported_games = SupportedGames::default();
173        let game = supported_games.game(KEY_WARHAMMER_3)
174            .expect("Warhammer 3 is always a supported game")
175            .clone();
176
177        Self {
178            session,
179            supported_games,
180            game,
181            schema: None,
182            first_game_change_done: false,
183            packs: BTreeMap::new(),
184            pack_modes: BTreeMap::new(),
185            dependencies: Dependencies::default(),
186            dependencies_error: None,
187            changed_packs: BTreeSet::new(),
188            autosave_thread: None,
189            search: None,
190            pending_startpos: None,
191            diagnostics: None,
192            lua_api_cache: None,
193        }
194    }
195
196    /// Returns the game currently selected.
197    pub fn game(&self) -> &GameInfo {
198        &self.game
199    }
200
201    /// Returns the selected game, what's loaded for it, and the open packs.
202    pub fn session_status(&self) -> SessionStatus {
203        SessionStatus {
204            game: self.game.key().to_owned(),
205            schema_loaded: self.schema.is_some(),
206            dependencies: DependenciesStatus {
207                vanilla_loaded: self.dependencies.is_vanilla_data_loaded(false),
208                assembly_kit_loaded: !self.dependencies.asskit_only_db_tables().is_empty(),
209                parent_files: self.dependencies.parent_files().len(),
210                error: self.dependencies_error.clone(),
211            },
212            packs: self.packs.iter().map(|(key, pack)| pack_summary(key, pack)).collect(),
213        }
214    }
215}
216
217/// Returns the short description of an open pack.
218fn pack_summary(key: &str, pack: &Pack) -> PackSummary {
219    PackSummary {
220        key: key.to_owned(),
221        name: pack.disk_file_name(),
222        path: Some(pack.disk_file_path().to_owned()).filter(|path| Path::new(path).is_absolute()),
223        pack_type: pack.pfh_file_type(),
224        file_count: pack.files().len(),
225    }
226}
227
228/// Returns the open pack with the provided key.
229fn pack<'a>(packs: &'a BTreeMap<String, Pack>, pack_key: &str) -> Result<&'a Pack> {
230    packs.get(pack_key).ok_or_else(|| ApiError::PackNotFound(pack_key.to_owned()).into())
231}
232
233/// Returns the open pack with the provided key, mutably.
234fn pack_mut<'a>(packs: &'a mut BTreeMap<String, Pack>, pack_key: &str) -> Result<&'a mut Pack> {
235    packs.get_mut(pack_key).ok_or_else(|| ApiError::PackNotFound(pack_key.to_owned()).into())
236}
237
238/// Returns the loaded schema, or an error if there is none.
239fn loaded_schema(schema: &Option<Schema>) -> Result<&Schema> {
240    schema.as_ref().ok_or_else(|| ApiError::SchemaNotLoaded.into())
241}
242
243/// Returns the loaded schema mutably, or an error if there is none.
244fn loaded_schema_mut(schema: &mut Option<Schema>) -> Result<&mut Schema> {
245    schema.as_mut().ok_or_else(|| ApiError::SchemaNotLoaded.into())
246}
247
248/// Returns the names of the parent packs of every open pack.
249fn parent_pack_names(packs: &BTreeMap<String, Pack>) -> Vec<String> {
250    packs.values()
251        .flat_map(|pack| pack.dependencies().iter().map(|(_, name)| name.clone()))
252        .collect()
253}
254
255/// Returns the data needed to encode files of a pack for the provided game.
256fn encode_extra_data(game: &GameInfo, compression_format: CompressionFormat, disable_uuid_regeneration: bool) -> Option<EncodeableExtraData<'_>> {
257    Some(EncodeableExtraData::new_from_game_info_and_settings(game, compression_format, disable_uuid_regeneration))
258}
259
260/// Decodes the DB and Loc files among `files`, so they're in memory for the diagnostics to work.
261///
262/// Files that fail to decode are left as they are. Does nothing if there is no schema.
263fn decode_tables(files: &mut [&mut RFile], schema: &Option<Schema>) {
264    if let Some(schema) = schema {
265        let mut extra_data = DecodeableExtraData::default();
266        extra_data.set_schema(Some(schema));
267        let extra_data = Some(extra_data);
268
269        files.par_iter_mut()
270            .filter(|file| file.file_type() == FileType::DB || file.file_type() == FileType::Loc)
271            .for_each(|file| {
272                let _ = file.decode(&extra_data, true, false);
273            });
274    }
275}
276
277/// Returns the Lua scripting API of a game, rebuilding the cached one if it's outdated.
278///
279/// The API is rebuilt when the game or its Assembly Kit path change, or when the dependencies cache is regenerated.
280///
281/// # Arguments
282///
283/// * `cache` - Cached API, updated if outdated.
284/// * `game` - Game whose API to return.
285/// * `settings` - Settings, to find the game's Assembly Kit.
286/// * `dependencies` - Dependencies cache with the vanilla scripts of the game.
287///
288/// # Returns
289///
290/// The API, or `None` if the game's Assembly Kit has no scripting docs.
291fn cached_lua_api<'a>(cache: &'a mut Option<LuaApiCache>, game: &GameInfo, settings: &Settings, dependencies: &Dependencies) -> Option<&'a LuaApi> {
292    let docs_path = settings.path_buf(&format!("{}{}", game.key(), ASSEMBLY_KIT_SUFFIX)).join(ASSEMBLY_KIT_SCRIPT_DOCS_PATH);
293    let build_date = *dependencies.build_date();
294    let outdated = cache.as_ref().is_none_or(|(path, date, _)| *path != docs_path || *date != build_date);
295
296    if outdated {
297        let api = match LuaApi::from_assembly_kit(&docs_path) {
298            Ok(mut api) => {
299                api.add_vanilla_scripts(dependencies);
300                Some(api)
301            }
302            Err(error) => {
303                info!("Lua scripting API not available, Lua scripts will only get their syntax checked: {error}");
304                None
305            }
306        };
307
308        *cache = Some((docs_path, build_date, api));
309    }
310
311    cache.as_ref().and_then(|(_, _, api)| api.as_ref())
312}
313
314/// Loads the schema of a game, re-decoding the tables of every open pack with it.
315///
316/// # Arguments
317///
318/// * `schema` - Schema to replace. Set to `None` if the game's schema can't be loaded.
319/// * `packs` - Open packs whose tables need re-decoding.
320/// * `game` - Game whose schema to load.
321/// * `disable_uuid_regeneration` - If tables keep their GUID when encoded before the switch.
322fn load_schema(schema: &mut Option<Schema>, packs: &mut BTreeMap<String, Pack>, game: &GameInfo, disable_uuid_regeneration: bool) {
323
324    // Before loading the schema, make sure we don't have tables with definitions from the current schema.
325    for pack in packs.values_mut() {
326        let extra_data = encode_extra_data(game, pack.compression_format(), disable_uuid_regeneration);
327        let mut files = pack.files_by_type_mut(&[FileType::DB]);
328
329        files.par_iter_mut().for_each(|file| {
330            let _ = file.encode(&extra_data, true, true, false);
331        });
332    }
333
334    let schema_path = schemas_path().unwrap().join(game.schema_file_name());
335    let local_patches_path = table_patches_path().unwrap().join(game.schema_file_name());
336    *schema = Schema::load(&schema_path, Some(&local_patches_path)).ok();
337
338    for pack in packs.values_mut() {
339        decode_tables(&mut pack.files_by_type_mut(&[FileType::DB]), schema);
340    }
341}