1use anyhow::{anyhow, Result};
15
16
17use std::collections::{BTreeMap, HashMap, HashSet};
18use std::env::temp_dir;
19use std::fs::{DirBuilder, File};
20use std::io::{BufWriter, Write};
21use std::path::{Component, Path, PathBuf};
22
23use rpfm_extensions::dependencies::{GAMES_NEEDING_VICTORY_OBJECTIVES, VICTORY_OBJECTIVES_FILE_NAME};
24use rpfm_extensions::gltf::{gltf_from_rigid, save_gltf_to_disk};
25use rpfm_extensions::optimizer::{OptimizableContainer, OptimizerOptions};
26use rpfm_extensions::startpos::extract::{extract_tables, ExtractedTables};
27use rpfm_extensions::startpos::extract::compare::compare_tables;
28use rpfm_extensions::startpos::generator::{generate_startpos, load_startpos, load_tables, GameLayouts, StartposSources, SUPPORTED_GAMES as GENERATOR_GAMES};
29use rpfm_extensions::startpos::layout::StartposLayout;
30use rpfm_extensions::startpos::split::CampaignRows;
31use rpfm_extensions::startpos::tables::StartposTables;
32use rpfm_extensions::startpos::validation::{validate, Severity};
33use rpfm_extensions::translator::PackTranslation;
34
35use rpfm_ipc::settings_keys::ASSEMBLY_KIT_SUFFIX;
36
37use rpfm_lib::files::{Container, ContainerPath, DecodeableExtraData, FileType, RFile, RFileDecoded, rigidmodel::RigidModel};
38use rpfm_lib::games::supported_games::KEY_THREE_KINGDOMS;
39use rpfm_lib::integrations::git::GitIntegration;
40
41use rpfm_telemetry::{error, info, warn};
42
43use crate::ceo_builder::{build_ceo, build_ceo_entries, build_ceo_post, get_trait_ceos};
44use rpfm_ipc::settings::{lua_autogen_game_path, scripts_path, startpos_layouts_path, translations_local_path, translations_remote_path, Settings};
45
46use rpfm_ipc::api::ApiError;
47use rpfm_ipc::api::diagnostics::DiagnosticLevel;
48use rpfm_ipc::api::files::FileRef;
49use rpfm_ipc::api::tables::FilesEdited;
50use rpfm_ipc::api::translations::{DEFAULT_TRANSLATIONS_LIMIT, ListTranslations, TranslationEntry, TranslationFilter, Translations};
51use rpfm_ipc::api::tools::{optimizer_option_values, CeoEntryData, CompareStartposTables, ExtractStartposTables, FilePaths, KeepStartposCampaign, FilesChanged, GenerateStartpos, InitMyMod, StartposColumnComparison, StartposComparison,
52 StartposIssue, StartposTableComparison, StartposValidation, StartposValueDifference, ValidateStartpos, MyModCreated, PluginScriptRun, RunPluginScript, SiegeAiPatched, StartStartpos, StartposCampaigns};
53
54use super::{DecodedFile, ExtractOptions, SessionState, encode_extra_data, loaded_schema, pack, pack_mut};
55use super::files::{container_path, pack_key_and_data_source, raw_paths};
56
57const VANILLA_FIXES_NAME: &str = "vanilla_fixes_";
64
65const THREE_KINGDOMS_SUB_STARTPOS: [&str; 2] = ["historical", "romance"];
67
68const STARTPOS_TABLES_FOLDER_PREFIX: &str = "db/start_pos_";
70
71const EXTRACTED_TABLES_SUFFIX: &str = "_startpos";
73
74const STARTPOS_LAST_CAMPAIGN_SETTING: &str = "starpos_last_campaign";
76
77#[derive(Debug, Clone)]
79pub(super) struct PendingStartpos {
80 pack: String,
81 campaign: String,
82 process_hlp_spd_data: bool,
83}
84
85#[derive(Debug, Clone, Default)]
87pub struct MyModOptions {
88
89 pub sublime_support: bool,
91
92 pub vscode_support: bool,
94
95 pub gitignore: Option<String>,
97}
98
99impl SessionState {
100
101 pub fn optimize_pack(&mut self, pack_key: &str, options: &OptimizerOptions) -> Result<(HashSet<String>, HashSet<String>)> {
107 let pack = pack_mut(&mut self.packs, pack_key)?;
108 let schema = loaded_schema(&self.schema)?;
109 Ok(pack.optimize(None, &mut self.dependencies, schema, &self.game, options)?)
110 }
111
112 pub fn patch_siege_ai(&mut self, pack_key: &str) -> Result<(String, Vec<ContainerPath>)> {
118 Ok(pack_mut(&mut self.packs, pack_key)?.patch_siege_ai()?)
119 }
120
121 pub fn pack_map(&mut self, pack_key: &str, tile_maps: Vec<PathBuf>, tiles: Vec<(PathBuf, String)>, options: OptimizerOptions) -> Result<(Vec<ContainerPath>, Vec<ContainerPath>)> {
134 let schema = loaded_schema(&self.schema)?;
135 Ok(self.dependencies.add_tile_maps_and_tiles(&mut self.packs, Some(pack_key), &self.game, schema, options, tile_maps, tiles)?)
136 }
137
138 pub fn generate_missing_loc_data(&mut self) -> Result<Vec<ContainerPath>> {
144 Ok(self.dependencies.generate_missing_loc_data(&mut self.packs)?)
145 }
146
147 pub fn initialize_mymod_folder(&self, mymod_base_path: &Path, mod_game: &str, mod_name: &str, options: &MyModOptions) -> Result<PathBuf> {
160 if !mymod_base_path.is_dir() {
161 return Err(anyhow!("MyMod path is not configured. Configure it in the settings and try again."));
162 }
163
164 let mut mymod_path = mymod_base_path.join(mod_game);
165 DirBuilder::new().recursive(true).create(&mymod_path)
166 .map_err(|error| anyhow!("Error while creating the MyMod's Game folder: {}.", error))?;
167
168 mymod_path.push(mod_name);
170 DirBuilder::new().recursive(true).create(&mymod_path)
171 .map_err(|error| anyhow!("Error while creating the MyMod's Assets folder: {}.", error))?;
172
173 if let Some(ref gitignore) = options.gitignore {
174 let git_integration = GitIntegration::new(&mymod_path, "", "", "");
175 git_integration.init()?;
176 git_integration.add_gitignore(gitignore)?;
177 }
178
179 if options.sublime_support || options.vscode_support {
181 if let Ok(lua_autogen_folder) = lua_autogen_game_path(&self.game) {
182 let lua_autogen_folder = lua_autogen_folder.to_string_lossy().replace('\\', "/");
183 write_lua_editor_configs(&mymod_path, mod_name, &lua_autogen_folder, options)?;
184 }
185 }
186
187 mymod_path.set_extension("pack");
188 Ok(mymod_path)
189 }
190
191 pub fn pack_translation(&self, pack_key: &str, src_lang: &str, language: &str) -> Result<PackTranslation> {
199 let game_key = self.game.key();
200 let local_path = translations_local_path()?;
201 let remote_path = translations_remote_path()?;
202
203 let mut base_english = HashMap::new();
204 let mut base_local_fixes = HashMap::new();
205
206 let vanilla_loc_name = PackTranslation::vanilla_loc_file_name(src_lang);
208 let vanilla_loc = [&local_path, &remote_path].iter()
209 .find_map(|path| RFile::tsv_import_from_path(&path.join(game_key).join(&vanilla_loc_name), &None).ok());
210
211 if let Some(mut vanilla_loc) = vanilla_loc {
212 let _ = vanilla_loc.guess_file_type();
213 if let Ok(RFileDecoded::Loc(vanilla_loc)) = vanilla_loc.decoded() {
214
215 let fixes_loc_path = remote_path.join(format!("{}/{}{}.tsv", game_key, VANILLA_FIXES_NAME, language));
217 if let Ok(mut fixes_loc) = RFile::tsv_import_from_path(&fixes_loc_path, &None) {
218 let _ = fixes_loc.guess_file_type();
219 if let Ok(RFileDecoded::Loc(fixes_loc)) = fixes_loc.decoded() {
220 base_local_fixes.extend(fixes_loc.data().iter().map(|row| (row[0].data_to_string().to_string(), row[1].data_to_string().to_string())));
221 }
222 }
223
224 base_english.extend(vanilla_loc.data().iter().map(|row| (row[0].data_to_string().to_string(), row[1].data_to_string().to_string())));
225 }
226 }
227
228 let pack = pack(&self.packs, pack_key)?;
229 Ok(PackTranslation::new(&[local_path, remote_path], pack, game_key, src_lang, language, &self.dependencies, &base_english, &base_local_fixes)?)
230 }
231
232 pub fn generate_vanilla_translation_source(&self, src_lang: &str, settings: &Settings) -> Result<bool> {
243 let local_path = translations_local_path()?;
244 let remote_path = translations_remote_path()?;
245
246 let game_path = settings.path_buf(self.game.key());
247 if let Err(error) = PackTranslation::generate_vanilla_loc(&self.game, &game_path, src_lang, &local_path.join(self.game.key())) {
248 warn!("Failed to generate the vanilla {src_lang} texts from the game files: {error}");
249 }
250
251 let vanilla_loc_name = PackTranslation::vanilla_loc_file_name(src_lang);
252 Ok([local_path, remote_path].iter().any(|path| path.join(self.game.key()).join(&vanilla_loc_name).is_file()))
253 }
254
255 pub fn check_starpos_victory_conditions(&self, pack_key: &str) -> Result<()> {
257 let pack = pack(&self.packs, pack_key)?;
258 if GAMES_NEEDING_VICTORY_OBJECTIVES.contains(&self.game.key()) && pack.file(VICTORY_OBJECTIVES_FILE_NAME, false).is_none() {
259 return Err(anyhow!("Missing \"db/victory_objectives.txt\" file. Processing the startpos without this file will result in issues in campaign. Add the file to the pack and try again."));
260 }
261
262 Ok(())
263 }
264
265 pub fn build_starpos(&mut self, pack_key: &str, campaign_id: &str, process_hlp_spd_data: bool, settings: &Settings) -> Result<()> {
274 let game_path = settings.path_buf(self.game.key());
275 let asskit_path = Some(settings.path_buf(&format!("{}{}", self.game.key(), ASSEMBLY_KIT_SUFFIX)));
276
277 if self.game.key() == KEY_THREE_KINGDOMS {
279 for (index, sub_start_pos) in THREE_KINGDOMS_SUB_STARTPOS.iter().enumerate() {
280 self.dependencies.build_starpos_pre(&mut self.packs, Some(pack_key), &self.game, &game_path, asskit_path.clone(), campaign_id, process_hlp_spd_data && index == 0, sub_start_pos)?;
281 }
282 } else {
283 self.dependencies.build_starpos_pre(&mut self.packs, Some(pack_key), &self.game, &game_path, asskit_path, campaign_id, process_hlp_spd_data, "")?;
284 }
285
286 Ok(())
287 }
288
289 pub fn build_starpos_post(&mut self, pack_key: &str, campaign_id: &str, process_hlp_spd_data: bool, cleanup: bool, settings: &Settings) -> Result<Vec<ContainerPath>> {
303 let game_path = settings.path_buf(self.game.key());
304 let asskit_path = Some(settings.path_buf(&format!("{}{}", self.game.key(), ASSEMBLY_KIT_SUFFIX)));
305 let sub_start_pos = if self.game.key() == KEY_THREE_KINGDOMS {
306 THREE_KINGDOMS_SUB_STARTPOS.iter().map(|name| name.to_string()).collect()
307 } else {
308 vec![]
309 };
310
311 Ok(self.dependencies.build_starpos_post(&mut self.packs, Some(pack_key), &self.game, &game_path, asskit_path, campaign_id, process_hlp_spd_data, cleanup, &sub_start_pos)?)
312 }
313
314 pub fn build_ceo(&mut self, pack_key: &str, akit_path: &Path, bob_exe_path: &Path) -> Result<()> {
316 info!("[BuildCeo] handler entered: pack={pack_key}, akit={}, bob={}", akit_path.display(), bob_exe_path.display());
317 let pack = pack_mut(&mut self.packs, pack_key)?;
318 build_ceo(pack, &self.schema, akit_path, bob_exe_path)
319 }
320
321 pub fn build_ceo_post(&mut self, pack_key: &str, akit_path: &Path) -> Result<FilesEdited> {
327 let added = build_ceo_post(pack_mut(&mut self.packs, pack_key)?, &akit_path.to_string_lossy())?;
328 Ok(FilesEdited { edited: raw_paths(&added) })
329 }
330
331 pub fn build_ceo_entries(&mut self, pack_key: &str, entries: &[CeoEntryData]) -> Result<FilesEdited> {
337 let schema = loaded_schema(&self.schema)?;
338 let added = build_ceo_entries(pack_mut(&mut self.packs, pack_key)?, schema, entries)?;
339 Ok(FilesEdited { edited: raw_paths(&added) })
340 }
341
342 pub fn trait_ceos(&self) -> Vec<(String, String)> {
344 get_trait_ceos(&self.dependencies)
345 }
346
347 pub fn update_anim_ids(&mut self, pack_key: &str, starting_id: i32, offset: i32) -> Result<Vec<ContainerPath>> {
359 Ok(pack_mut(&mut self.packs, pack_key)?.update_anim_ids(&self.game, starting_id, offset)?)
360 }
361
362 pub fn export_rigid_to_gltf(&mut self, rigid_model: &RigidModel, path: &Path) -> Result<()> {
364 let gltf = gltf_from_rigid(rigid_model, &mut self.dependencies)?;
365 save_gltf_to_disk(&gltf, path)?;
366 Ok(())
367 }
368
369 pub fn run_plugin_script(&mut self, request: &RunPluginScript, options: ExtractOptions) -> Result<PluginScriptRun> {
383 let script_path = &request.script;
384 let interpreter = plugin_script_interpreter(script_path)
385 .ok_or_else(|| anyhow!("Unsupported plugin script type: {}", script_path.display()))?;
386
387 let pack = pack_mut(&mut self.packs, &request.pack)?;
388 let container_paths = request.paths.iter().map(|path| container_path(|path| pack.has_file(path), path)).collect::<Vec<_>>();
389 let base_folder = temp_dir().join("rpfm_plugins").join(pack.disk_file_name());
390 let _ = std::fs::remove_dir_all(&base_folder);
391
392 let extra_data = encode_extra_data(&self.game, pack.compression_format(), options.disable_uuid_regeneration);
393 let mut extracted_paths = vec![];
394 for container_path in container_paths {
395 let mut paths = pack.extract(container_path, &base_folder, true, &self.schema, false, options.tsv_keys_first, &extra_data)
396 .map_err(|error| anyhow!("Error extracting files for the plugin script: {}", error))?;
397 extracted_paths.append(&mut paths);
398 }
399
400 let output = std::process::Command::new(interpreter)
401 .arg(script_path)
402 .args(&extracted_paths)
403 .current_dir(&base_folder)
404 .output()
405 .map_err(|error| {
406 error!("Failed to run the plugin script {}: {}", script_path.display(), error);
407 anyhow!("Failed to run the plugin script: {}", error)
408 })?;
409
410 let stdout = String::from_utf8_lossy(&output.stdout);
411 let stderr = String::from_utf8_lossy(&output.stderr);
412
413 let report = format!("Script: {}\nStatus: {}\n\n--- stdout ---\n{}\n--- stderr ---\n{}\n", script_path.display(), output.status, stdout, stderr);
416 if let Ok(folder) = scripts_path() {
417 let _ = std::fs::write(folder.join("last_run.log"), report.as_bytes());
418 }
419
420 info!("Plugin script {} finished with {}.", script_path.display(), output.status);
421 if !stderr.trim().is_empty() {
422 warn!("Plugin script stderr:\n{}", stderr.trim());
423 }
424
425 let message = if output.status.success() {
426 None
427 } else {
428 Some(format!("The plugin script finished with errors. See last_run.log in the scripts folder.\n\n{}", stderr.trim()))
429 };
430
431 let mut reimported_paths = vec![];
432 for disk_path in extracted_paths.iter().filter(|path| path.is_file()) {
433
434 let direct_path = container_path_from_disk_path(disk_path, &base_folder);
437 let container_path = match direct_path.strip_suffix(".tsv") {
438 Some(stripped) if pack.file_mut(&direct_path, false).is_none() => stripped.to_owned(),
439 _ => direct_path,
440 };
441
442 if let Some(file) = pack.file_mut(&container_path, false) {
443 if file.encode_from_external_data(&self.schema, disk_path).is_ok() {
444 reimported_paths.push(container_path);
445 }
446 }
447 }
448
449 let _ = std::fs::remove_dir_all(&base_folder);
450 Ok(PluginScriptRun { edited: reimported_paths, error: message })
451 }
452}
453
454impl SessionState {
455
456 pub fn optimize_pack_files(&mut self, pack_key: &str, options: &OptimizerOptions) -> Result<FilesChanged> {
458 let (deleted, added) = self.optimize_pack(pack_key, options)?;
459 Ok(FilesChanged { added: sorted(added), deleted: sorted(deleted) })
460 }
461
462 pub fn patch_siege_ai_files(&mut self, pack_key: &str) -> Result<SiegeAiPatched> {
464 let (message, deleted) = self.patch_siege_ai(pack_key)?;
465 Ok(SiegeAiPatched { message, deleted: raw_paths(&deleted) })
466 }
467
468 pub fn pack_map_files(&mut self, pack_key: &str, tile_maps: Vec<PathBuf>, tiles: Vec<(PathBuf, String)>, options: OptimizerOptions) -> Result<FilesChanged> {
470 let (added, deleted) = self.pack_map(pack_key, tile_maps, tiles, options)?;
471 Ok(FilesChanged { added: raw_paths(&added), deleted: raw_paths(&deleted) })
472 }
473
474 pub fn generate_missing_locs(&mut self) -> Result<FilesEdited> {
476 Ok(FilesEdited { edited: raw_paths(&self.generate_missing_loc_data()?) })
477 }
478
479 pub fn update_anim_id_files(&mut self, pack_key: &str, starting_id: i32, offset: i32) -> Result<FilesEdited> {
481 Ok(FilesEdited { edited: raw_paths(&self.update_anim_ids(pack_key, starting_id, offset)?) })
482 }
483
484 pub fn anims_by_skeleton(&mut self, skeleton: &str) -> FilePaths {
486 FilePaths { paths: sorted(self.anim_paths_by_skeleton(skeleton)) }
487 }
488
489 pub fn export_gltf(&mut self, file: &FileRef, destination: &Path) -> Result<()> {
495 let (pack_key, data_source) = pack_key_and_data_source(&file.source);
496 let pack_key = pack_key.to_owned();
497 match self.decode_file(&pack_key, &file.path, data_source, false)? {
498 DecodedFile::Decoded(decoded, _) => match *decoded {
499 RFileDecoded::RigidModel(rigid_model) => self.export_rigid_to_gltf(&rigid_model, destination),
500 _ => Err(ApiError::InvalidParams(format!("The file {} is not a RigidModel.", file.path)).into()),
501 },
502 _ => Err(ApiError::InvalidParams(format!("The file {} is not a RigidModel.", file.path)).into()),
503 }
504 }
505
506 pub fn init_mymod(&self, mymod_base_path: &Path, request: &InitMyMod) -> Result<MyModCreated> {
513 let options = MyModOptions {
514 sublime_support: request.sublime_support,
515 vscode_support: request.vscode_support,
516 gitignore: request.gitignore.clone(),
517 };
518
519 Ok(MyModCreated { pack_path: self.initialize_mymod_folder(mymod_base_path, &request.game_folder, &request.name, &options)? })
520 }
521
522 pub fn list_translations(&self, request: &ListTranslations) -> Result<Translations> {
524 let translation = self.pack_translation(&request.pack, &request.source_language, &request.language)?;
525 let mut entries = translation.translations().iter()
526 .filter(|(_, entry)| match request.filter {
527 TranslationFilter::All => true,
528 TranslationFilter::Untranslated => entry.dst().is_empty() && !*entry.rem(),
529 TranslationFilter::NeedsReview => *entry.retr() || *entry.aut(),
530 })
531 .map(|(key, entry)| TranslationEntry {
532 key: key.clone(),
533 source: entry.src().clone(),
534 translation: entry.dst().clone(),
535 needs_retranslation: *entry.retr(),
536 automatic: *entry.aut(),
537 removed: *entry.rem(),
538 })
539 .collect::<Vec<_>>();
540 entries.sort_unstable_by(|a, b| a.key.cmp(&b.key));
541
542 let total = entries.len();
543 let entries = entries.into_iter()
544 .skip(request.offset)
545 .take(request.limit.unwrap_or(DEFAULT_TRANSLATIONS_LIMIT))
546 .collect();
547
548 Ok(Translations { entries, total })
549 }
550
551 pub fn startpos_campaigns(&self, pack_key: &str) -> Result<StartposCampaigns> {
553 let last_used = pack(&self.packs, pack_key)?.settings().settings_text().get(STARTPOS_LAST_CAMPAIGN_SETTING).cloned();
554 let campaigns = sorted(self.column_values("campaigns_tables", "campaign_name", true, true));
555 Ok(StartposCampaigns { campaigns, last_used })
556 }
557
558 pub fn start_startpos(&mut self, request: &StartStartpos, settings: &Settings) -> Result<()> {
562 self.check_starpos_victory_conditions(&request.pack)?;
563 self.build_starpos(&request.pack, &request.campaign, request.process_hlp_spd_data, settings)?;
564
565 pack_mut(&mut self.packs, &request.pack)?.settings_mut().settings_text_mut().insert(STARTPOS_LAST_CAMPAIGN_SETTING.to_owned(), request.campaign.clone());
566 self.pending_startpos = Some(PendingStartpos {
567 pack: request.pack.clone(),
568 campaign: request.campaign.clone(),
569 process_hlp_spd_data: request.process_hlp_spd_data,
570 });
571
572 Ok(())
573 }
574
575 pub fn validate_startpos(&self, request: &ValidateStartpos) -> Result<StartposValidation> {
577 let schema = loaded_schema(&self.schema)?;
578 let tables = load_tables(&self.dependencies, pack(&self.packs, &request.pack)?, schema)?;
579 let issues = validate(&tables, schema, &request.campaign).into_iter()
580 .map(|issue| StartposIssue {
581 level: match issue.severity {
582 Severity::Error => DiagnosticLevel::Error,
583 Severity::Warning => DiagnosticLevel::Warning,
584 },
585 table: issue.table,
586 row: issue.row,
587 column: issue.column,
588 message: issue.message,
589 })
590 .collect();
591 Ok(StartposValidation { issues })
592 }
593
594 pub fn generate_startpos(&mut self, request: &GenerateStartpos, settings: &Settings) -> Result<FilesEdited> {
603 self.check_startpos_generator_support()?;
604 let schema = loaded_schema(&self.schema)?;
605 let pack_to_build = pack(&self.packs, &request.pack)?;
606 let tables = load_tables(&self.dependencies, pack_to_build, schema)?;
607 let errors = validate(&tables, schema, &request.campaign).into_iter()
608 .filter(|issue| issue.severity == Severity::Error)
609 .map(|issue| issue.to_string())
610 .collect::<Vec<_>>();
611 if !errors.is_empty() {
612 return Err(anyhow!("The startpos tables have errors that would break the build:\n{}", errors.join("\n")));
613 }
614
615 let layout = self.startpos_layout(&tables, &request.campaign, settings)?;
616 let sources = StartposSources::from_tables(tables, &self.dependencies, pack_to_build, &request.campaign)?;
617 let startpos = generate_startpos(&sources, &layout)?;
618
619 let path = format!("campaigns/{}/startpos.esf", request.campaign);
620 let pack = pack_mut(&mut self.packs, &request.pack)?;
621 pack.insert(RFile::new_from_decoded(&RFileDecoded::ESF(startpos), 0, &path))?;
622 pack.settings_mut().settings_text_mut().insert(STARTPOS_LAST_CAMPAIGN_SETTING.to_owned(), request.campaign.clone());
623 Ok(FilesEdited { edited: vec![path] })
624 }
625
626 pub fn extract_startpos_tables(&mut self, request: &ExtractStartposTables, settings: &Settings) -> Result<FilesEdited> {
634 let (extracted, _) = self.startpos_extraction(&request.pack, &request.campaign, settings)?;
635 let dbs = extracted.to_dbs(loaded_schema(&self.schema)?)?;
636
637 let pack = pack_mut(&mut self.packs, &request.pack)?;
638 let mut edited = vec![];
639 for db in dbs.into_iter().filter(|db| !db.is_empty()) {
640 let path = format!("db/{}/{}{EXTRACTED_TABLES_SUFFIX}", db.table_name(), request.campaign);
641 pack.insert(RFile::new_from_decoded(&RFileDecoded::DB(db), 0, &path))?;
642 edited.push(path);
643 }
644
645 if !extracted.victory_objectives().is_empty() {
646 let mut file = RFile::new_from_vec(extracted.victory_objectives_script().as_bytes(), FileType::Text, 0, VICTORY_OBJECTIVES_FILE_NAME);
647 file.guess_file_type()?;
648 pack.insert(file)?;
649 edited.push(VICTORY_OBJECTIVES_FILE_NAME.to_owned());
650 }
651 Ok(FilesEdited { edited })
652 }
653
654 pub fn keep_startpos_campaign(&mut self, request: &KeepStartposCampaign) -> Result<FilesChanged> {
661 let schema = loaded_schema(&self.schema)?;
662 let tables = load_tables(&self.dependencies, pack(&self.packs, &request.pack)?, schema)?;
663 let has_factions = tables.table("start_pos_factions").is_ok_and(|factions| factions.rows().any(|row| row.str("campaign").is_ok_and(|campaign| campaign == request.campaign)));
664 if !has_factions {
665 return Err(ApiError::InvalidParams(format!("No faction of the startpos tables is in the campaign {}.", request.campaign)).into());
666 }
667 let rows = CampaignRows::new(&tables, schema, &request.campaign)?;
668
669 let pack = pack_mut(&mut self.packs, &request.pack)?;
670 let mut paths = pack.files().keys().filter(|path| path.starts_with(STARTPOS_TABLES_FOLDER_PREFIX)).cloned().collect::<Vec<_>>();
671 paths.sort();
672
673 let mut extra_data = DecodeableExtraData::default();
674 extra_data.set_schema(Some(schema));
675 let extra_data = Some(extra_data);
676 let mut changed = FilesChanged { added: vec![], deleted: vec![] };
677 for path in paths {
678 let Some(file) = pack.file_mut(&path, false) else { continue };
679 file.decode(&extra_data, true, false)?;
680 let Ok(RFileDecoded::DB(db)) = file.decoded_mut() else { continue };
681 if rows.retain(db)? == 0 {
682 continue;
683 }
684 if db.is_empty() {
685 pack.remove(&ContainerPath::File(path.to_owned()));
686 changed.deleted.push(path);
687 } else {
688 changed.added.push(path);
689 }
690 }
691 Ok(changed)
692 }
693
694 pub fn compare_startpos_tables(&self, request: &CompareStartposTables, settings: &Settings) -> Result<StartposComparison> {
700 let (extracted, tables) = self.startpos_extraction(&request.pack, &request.campaign, settings)?;
701 let tables = compare_tables(&tables, &extracted, loaded_schema(&self.schema)?)?.into_iter()
702 .map(|comparison| StartposTableComparison {
703 table: comparison.table().to_owned(),
704 match_ratio: comparison.match_ratio(),
705 matched_rows: *comparison.matched_rows(),
706 missing_rows: comparison.missing_rows().to_vec(),
707 extra_rows: comparison.extra_rows().to_vec(),
708 columns: comparison.columns().iter()
709 .filter(|column| *column.different() > 0)
710 .map(|column| StartposColumnComparison {
711 column: column.column().to_owned(),
712 equal: *column.equal(),
713 different: *column.different(),
714 examples: column.examples().iter()
715 .map(|(row, original, extracted)| StartposValueDifference { row: row.to_owned(), original: original.to_owned(), extracted: extracted.to_owned() })
716 .collect(),
717 })
718 .collect(),
719 })
720 .collect();
721 Ok(StartposComparison { tables })
722 }
723
724 fn startpos_extraction(&self, pack_key: &str, campaign: &str, settings: &Settings) -> Result<(ExtractedTables, StartposTables)> {
726 self.check_startpos_generator_support()?;
727 let pack = pack(&self.packs, pack_key)?;
728 let tables = load_tables(&self.dependencies, pack, loaded_schema(&self.schema)?)?;
729 let layout = self.startpos_layout(&tables, campaign, settings)?;
730 let startpos = load_startpos(&self.dependencies, pack, campaign)?;
731 Ok((extract_tables(&startpos, &layout, &tables)?, tables))
732 }
733
734 fn check_startpos_generator_support(&self) -> Result<()> {
736 if !GENERATOR_GAMES.contains(&self.game.key()) {
737 return Err(ApiError::InvalidParams(format!("Startpos generation isn't supported for {} yet.", self.game.display_name())).into());
738 }
739 if !self.dependencies.is_vanilla_data_loaded(false) {
740 return Err(anyhow!("The dependencies cache isn't loaded. Generate it, then try again."));
741 }
742 Ok(())
743 }
744
745 fn startpos_layout(&self, tables: &StartposTables, campaign: &str, settings: &Settings) -> Result<StartposLayout> {
747 let game_data_path = self.game.data_path(&settings.path_buf(self.game.key()))?;
748 Ok(GameLayouts::load_or_extract(&startpos_layouts_path()?, &self.game, &game_data_path)?.for_campaign(tables, campaign))
749 }
750
751 pub fn finish_startpos(&mut self, cancel: bool, settings: &Settings) -> Result<FilesEdited> {
757 let pending = self.pending_startpos.take()
758 .ok_or_else(|| ApiError::InvalidParams("There's no startpos build started. Call startpos.start first.".to_owned()))?;
759
760 let added = self.build_starpos_post(&pending.pack, &pending.campaign, pending.process_hlp_spd_data, cancel, settings)?;
761 Ok(FilesEdited { edited: raw_paths(&added) })
762 }
763}
764
765pub fn optimizer_options_with(base: &OptimizerOptions, changes: &BTreeMap<String, bool>) -> Result<OptimizerOptions, ApiError> {
771
772 let mut values = optimizer_option_values(base);
774 for (name, value) in changes {
775 match values.get_mut(name) {
776 Some(option) => *option = *value,
777 None => return Err(ApiError::InvalidParams(format!("Unknown optimizer option: {name}. Valid ones: {}.", values.keys().cloned().collect::<Vec<_>>().join(", ")))),
778 }
779 }
780
781 serde_json::from_value(serde_json::to_value(values).map_err(|error| ApiError::Internal(error.to_string()))?)
782 .map_err(|error| ApiError::Internal(error.to_string()))
783}
784
785fn sorted(items: impl IntoIterator<Item = String>) -> Vec<String> {
787 let mut items = items.into_iter().collect::<Vec<_>>();
788 items.sort_unstable();
789 items
790}
791
792pub fn plugin_scripts() -> Result<Vec<PathBuf>> {
794 let mut scripts = std::fs::read_dir(scripts_path()?)
795 .map(|entries| entries.flatten()
796 .map(|entry| entry.path())
797 .filter(|path| path.is_file() && plugin_script_interpreter(path).is_some())
798 .collect::<Vec<_>>())
799 .unwrap_or_default();
800
801 scripts.sort();
802 Ok(scripts)
803}
804
805fn plugin_script_interpreter(path: &Path) -> Option<&'static str> {
809 match path.extension().and_then(|extension| extension.to_str()) {
810 Some("py") => Some("python"),
811 Some("lua") => Some("lua"),
812 _ => None,
813 }
814}
815
816fn container_path_from_disk_path(disk_path: &Path, base_folder: &Path) -> String {
821 disk_path.strip_prefix(base_folder)
822 .unwrap_or(disk_path)
823 .components()
824 .filter_map(|component| match component {
825 Component::Normal(part) => Some(part.to_string_lossy().to_string()),
826 _ => None,
827 })
828 .collect::<Vec<_>>()
829 .join("/")
830}
831
832fn write_lua_editor_configs(mymod_path: &Path, mod_name: &str, lua_autogen_folder: &str, options: &MyModOptions) -> Result<()> {
841 if options.vscode_support {
842 let vscode_config_path = mymod_path.join(".vscode");
843 DirBuilder::new().recursive(true).create(&vscode_config_path)
844 .map_err(|error| anyhow!("Error while creating the VSCode Config folder: {}.", error))?;
845
846 if let Ok(file) = File::create(vscode_config_path.join("extensions.json")) {
847 let mut file = BufWriter::new(file);
848 let _ = file.write_all("
849{
850 \"recommendations\": [
851 \"sumneko.lua\",
852 \"formulahendry.code-runner\"
853 ],
854}".as_bytes());
855 }
856 }
857
858 if options.sublime_support {
859 if let Ok(file) = File::create(mymod_path.join(format!("{mod_name}.sublime-project"))) {
860 let mut file = BufWriter::new(file);
861 let _ = file.write_all("
862{
863 \"folders\":
864 [
865 {
866 \"path\": \".\"
867 }
868 ]
869}".as_bytes());
870 }
871 }
872
873 if let Ok(file) = File::create(mymod_path.join(".luarc.json")) {
875 let mut file = BufWriter::new(file);
876 let _ = file.write_all(format!("
877{{
878 \"workspace.library\": [
879 \"{lua_autogen_folder}/global/\",
880 \"{lua_autogen_folder}/campaign/\",
881 \"{lua_autogen_folder}/frontend/\",
882 \"{lua_autogen_folder}/battle/\"
883 ],
884 \"runtime.version\": \"Lua 5.1\",
885 \"completion.autoRequire\": false,
886 \"workspace.preloadFileSize\": 1500,
887 \"workspace.ignoreSubmodules\": false,
888 \"diagnostics.workspaceDelay\": 500,
889 \"diagnostics.workspaceRate\": 40,
890 \"diagnostics.disable\": [
891 \"lowercase-global\",
892 \"trailing-space\"
893 ],
894 \"hint.setType\": true,
895 \"workspace.ignoreDir\": [
896 \".vscode\",
897 \".git\"
898 ]
899}}").as_bytes());
900 }
901
902 Ok(())
903}
904
905#[cfg(test)]
910#[path = "tools_test.rs"]
911mod tools_test;