1use 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
61pub struct SessionState {
67
68 session: Arc<Session>,
70
71 supported_games: SupportedGames,
73
74 game: GameInfo,
76
77 schema: Option<Schema>,
79
80 first_game_change_done: bool,
82
83 packs: BTreeMap<String, Pack>,
87
88 temporary_packs: BTreeMap<String, Pack>,
90
91 pack_modes: BTreeMap<String, OperationalMode>,
93
94
95 dependencies: Dependencies,
97
98 dependencies_error: Option<String>,
100
101 changed_packs: BTreeSet<String>,
103
104 autosave_thread: Option<JoinHandle<()>>,
106
107 search: Option<GlobalSearch>,
109
110 pending_startpos: Option<PendingStartpos>,
112
113 diagnostics: Option<DiagnosticsResults>,
115
116 lua_api_cache: Option<LuaApiCache>,
118}
119
120#[derive(Debug, Clone, Copy, Default)]
122pub struct ExtractOptions {
123
124 pub disable_uuid_regeneration: bool,
126
127 pub tsv_keys_first: bool,
129}
130
131#[derive(Debug, Clone, Copy, Default)]
133pub struct SaveOptions {
134
135 pub disable_uuid_regeneration: bool,
137
138 pub allow_editing_of_ca_packfiles: bool,
140
141 pub clean: bool,
143}
144
145#[derive(Debug)]
147struct PasteEntry {
148
149 file_path: String,
151
152 base_path: String,
154
155 source_pack_key: String,
157}
158
159type LuaApiCache = (PathBuf, u64, Option<LuaApi>);
161
162impl SessionState {
167
168 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 pub fn game(&self) -> &GameInfo {
201 &self.game
202 }
203
204 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
220fn 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
231fn 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
236fn 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
241fn 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
246fn loaded_schema(schema: &Option<Schema>) -> Result<&Schema> {
248 schema.as_ref().ok_or_else(|| ApiError::SchemaNotLoaded.into())
249}
250
251fn loaded_schema_mut(schema: &mut Option<Schema>) -> Result<&mut Schema> {
253 schema.as_mut().ok_or_else(|| ApiError::SchemaNotLoaded.into())
254}
255
256fn 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
263fn 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
268fn 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
285fn 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
322fn load_schema(schema: &mut Option<Schema>, packs: &mut BTreeMap<String, Pack>, game: &GameInfo, disable_uuid_regeneration: bool) {
331
332 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}