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
89 pack_modes: BTreeMap<String, OperationalMode>,
91
92
93 dependencies: Dependencies,
95
96 dependencies_error: Option<String>,
98
99 changed_packs: BTreeSet<String>,
101
102 autosave_thread: Option<JoinHandle<()>>,
104
105 search: Option<GlobalSearch>,
107
108 pending_startpos: Option<PendingStartpos>,
110
111 diagnostics: Option<DiagnosticsResults>,
113
114 lua_api_cache: Option<LuaApiCache>,
116}
117
118#[derive(Debug, Clone, Copy, Default)]
120pub struct ExtractOptions {
121
122 pub disable_uuid_regeneration: bool,
124
125 pub tsv_keys_first: bool,
127}
128
129#[derive(Debug, Clone, Copy, Default)]
131pub struct SaveOptions {
132
133 pub disable_uuid_regeneration: bool,
135
136 pub allow_editing_of_ca_packfiles: bool,
138
139 pub clean: bool,
141}
142
143#[derive(Debug)]
145struct PasteEntry {
146
147 file_path: String,
149
150 base_path: String,
152
153 source_pack_key: String,
155}
156
157type LuaApiCache = (PathBuf, u64, Option<LuaApi>);
159
160impl SessionState {
165
166 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 pub fn game(&self) -> &GameInfo {
198 &self.game
199 }
200
201 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
217fn 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
228fn 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
233fn 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
238fn loaded_schema(schema: &Option<Schema>) -> Result<&Schema> {
240 schema.as_ref().ok_or_else(|| ApiError::SchemaNotLoaded.into())
241}
242
243fn loaded_schema_mut(schema: &mut Option<Schema>) -> Result<&mut Schema> {
245 schema.as_mut().ok_or_else(|| ApiError::SchemaNotLoaded.into())
246}
247
248fn 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
255fn 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
260fn 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
277fn 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
314fn load_schema(schema: &mut Option<Schema>, packs: &mut BTreeMap<String, Pack>, game: &GameInfo, disable_uuid_regeneration: bool) {
323
324 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}