1use std::cmp::Ordering;
21use std::fs::{read_dir, DirEntry, File};
22use std::io::{BufRead, BufReader};
23use std::iter::once;
24use std::path::Path;
25use std::path::PathBuf;
26
27use crate::enums::{Error, GameId, LoadOrderMethod};
28use crate::ini::{test_files, use_my_games_directory};
29use crate::is_enderal;
30use crate::load_order::{
31 AsteriskBasedLoadOrder, OpenMWLoadOrder, TextfileBasedLoadOrder, TimestampBasedLoadOrder,
32 WritableLoadOrder,
33};
34use crate::openmw_config;
35use crate::plugin::{has_plugin_extension, Plugin};
36
37#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
38pub struct GameSettings {
39 id: GameId,
40 game_path: PathBuf,
41 plugins_directory: PathBuf,
42 plugins_file_path: PathBuf,
43 my_games_path: PathBuf,
44 load_order_path: Option<PathBuf>,
45 implicitly_active_plugins: Vec<String>,
46 early_loading_plugins: Vec<String>,
47 additional_plugins_directories: Vec<PathBuf>,
48}
49
50const SKYRIM_HARDCODED_PLUGINS: &[&str] = &["Skyrim.esm"];
51
52const SKYRIM_SE_HARDCODED_PLUGINS: &[&str] = &[
53 "Skyrim.esm",
54 "Update.esm",
55 "Dawnguard.esm",
56 "HearthFires.esm",
57 "Dragonborn.esm",
58];
59
60const SKYRIM_VR_HARDCODED_PLUGINS: &[&str] = &[
61 "Skyrim.esm",
62 "Update.esm",
63 "Dawnguard.esm",
64 "HearthFires.esm",
65 "Dragonborn.esm",
66 "SkyrimVR.esm",
67];
68
69const FALLOUT4_HARDCODED_PLUGINS: &[&str] = &[
70 "Fallout4.esm",
71 "DLCRobot.esm",
72 "DLCworkshop01.esm",
73 "DLCCoast.esm",
74 "DLCworkshop02.esm",
75 "DLCworkshop03.esm",
76 "DLCNukaWorld.esm",
77 "DLCUltraHighResolution.esm",
78];
79
80const FALLOUT4VR_HARDCODED_PLUGINS: &[&str] = &["Fallout4.esm", "Fallout4_VR.esm"];
81
82pub(crate) const STARFIELD_HARDCODED_PLUGINS: &[&str] = &[
83 "Starfield.esm",
84 "Constellation.esm",
85 "OldMars.esm",
86 "ShatteredSpace.esm",
87 "SFBGS003.esm",
88 "SFBGS004.esm",
89 "SFBGS006.esm",
90 "SFBGS007.esm",
91 "SFBGS008.esm",
92];
93
94const OPENMW_HARDCODED_PLUGINS: &[&str] = &["builtin.omwscripts"];
95
96const MS_FO4_FAR_HARBOR_PATH: &str = "../../Fallout 4- Far Harbor (PC)/Content/Data";
101const MS_FO4_NUKA_WORLD_PATH: &str = "../../Fallout 4- Nuka-World (PC)/Content/Data";
102const MS_FO4_AUTOMATRON_PATH: &str = "../../Fallout 4- Automatron (PC)/Content/Data";
103const MS_FO4_TEXTURE_PACK_PATH: &str = "../../Fallout 4- High Resolution Texture Pack/Content/Data";
104const MS_FO4_WASTELAND_PATH: &str = "../../Fallout 4- Wasteland Workshop (PC)/Content/Data";
105const MS_FO4_CONTRAPTIONS_PATH: &str = "../../Fallout 4- Contraptions Workshop (PC)/Content/Data";
106const MS_FO4_VAULT_TEC_PATH: &str = "../../Fallout 4- Vault-Tec Workshop (PC)/Content/Data";
107
108const PLUGINS_TXT: &str = "Plugins.txt";
109
110const OBLIVION_REMASTERED_RELATIVE_DATA_PATH: &str = "OblivionRemastered/Content/Dev/ObvData/Data";
111
112impl GameSettings {
113 pub fn new(game_id: GameId, game_path: &Path) -> Result<GameSettings, Error> {
114 let local_path = local_path(game_id, game_path)?.unwrap_or_default();
115 GameSettings::with_local_path(game_id, game_path, &local_path)
116 }
117
118 pub fn with_local_path(
119 game_id: GameId,
120 game_path: &Path,
121 local_path: &Path,
122 ) -> Result<GameSettings, Error> {
123 let my_games_path = my_games_path(game_id, game_path, local_path)?.unwrap_or_default();
124
125 GameSettings::with_local_and_my_games_paths(game_id, game_path, local_path, my_games_path)
126 }
127
128 pub(crate) fn with_local_and_my_games_paths(
129 game_id: GameId,
130 game_path: &Path,
131 local_path: &Path,
132 my_games_path: PathBuf,
133 ) -> Result<GameSettings, Error> {
134 let plugins_file_path = plugins_file_path(game_id, game_path, local_path)?;
135 let load_order_path = load_order_path(game_id, local_path, &plugins_file_path);
136 let plugins_directory = plugins_directory(game_id, game_path, local_path)?;
137 let additional_plugins_directories =
138 additional_plugins_directories(game_id, game_path, &my_games_path)?;
139
140 let (early_loading_plugins, implicitly_active_plugins) =
141 GameSettings::load_implicitly_active_plugins(
142 game_id,
143 game_path,
144 &my_games_path,
145 &plugins_directory,
146 &additional_plugins_directories,
147 )?;
148
149 Ok(GameSettings {
150 id: game_id,
151 game_path: game_path.to_path_buf(),
152 plugins_directory,
153 plugins_file_path,
154 load_order_path,
155 my_games_path,
156 implicitly_active_plugins,
157 early_loading_plugins,
158 additional_plugins_directories,
159 })
160 }
161
162 pub fn id(&self) -> GameId {
163 self.id
164 }
165
166 pub fn load_order_method(&self) -> LoadOrderMethod {
167 match self.id {
168 GameId::OpenMW => LoadOrderMethod::OpenMW,
169 GameId::Morrowind | GameId::Oblivion | GameId::Fallout3 | GameId::FalloutNV => {
170 LoadOrderMethod::Timestamp
171 }
172 GameId::Skyrim | GameId::OblivionRemastered => LoadOrderMethod::Textfile,
173 GameId::SkyrimSE
174 | GameId::SkyrimVR
175 | GameId::Fallout4
176 | GameId::Fallout4VR
177 | GameId::Starfield => LoadOrderMethod::Asterisk,
178 }
179 }
180
181 pub fn into_load_order(self) -> Box<dyn WritableLoadOrder + Send + Sync + 'static> {
182 match self.load_order_method() {
183 LoadOrderMethod::Asterisk => Box::new(AsteriskBasedLoadOrder::new(self)),
184 LoadOrderMethod::Textfile => Box::new(TextfileBasedLoadOrder::new(self)),
185 LoadOrderMethod::Timestamp => Box::new(TimestampBasedLoadOrder::new(self)),
186 LoadOrderMethod::OpenMW => Box::new(OpenMWLoadOrder::new(self)),
187 }
188 }
189
190 #[deprecated = "The master file is not necessarily of any significance: you should probably use early_loading_plugins() instead."]
191 pub fn master_file(&self) -> &'static str {
192 match self.id {
193 GameId::Morrowind | GameId::OpenMW => "Morrowind.esm",
194 GameId::Oblivion | GameId::OblivionRemastered => "Oblivion.esm",
195 GameId::Skyrim | GameId::SkyrimSE | GameId::SkyrimVR => "Skyrim.esm",
196 GameId::Fallout3 => "Fallout3.esm",
197 GameId::FalloutNV => "FalloutNV.esm",
198 GameId::Fallout4 | GameId::Fallout4VR => "Fallout4.esm",
199 GameId::Starfield => "Starfield.esm",
200 }
201 }
202
203 pub fn implicitly_active_plugins(&self) -> &[String] {
204 &self.implicitly_active_plugins
205 }
206
207 pub fn is_implicitly_active(&self, plugin: &str) -> bool {
208 use unicase::eq;
209 self.implicitly_active_plugins()
210 .iter()
211 .any(|p| eq(p.as_str(), plugin))
212 }
213
214 pub fn early_loading_plugins(&self) -> &[String] {
215 &self.early_loading_plugins
216 }
217
218 pub fn loads_early(&self, plugin: &str) -> bool {
219 use unicase::eq;
220 self.early_loading_plugins()
221 .iter()
222 .any(|p| eq(p.as_str(), plugin))
223 }
224
225 pub fn plugins_directory(&self) -> PathBuf {
226 self.plugins_directory.clone()
227 }
228
229 pub fn active_plugins_file(&self) -> &PathBuf {
230 &self.plugins_file_path
231 }
232
233 pub fn load_order_file(&self) -> Option<&PathBuf> {
234 self.load_order_path.as_ref()
235 }
236
237 pub fn additional_plugins_directories(&self) -> &[PathBuf] {
238 &self.additional_plugins_directories
239 }
240
241 pub fn set_additional_plugins_directories(&mut self, paths: Vec<PathBuf>) {
242 self.additional_plugins_directories = paths;
243 }
244
245 pub(crate) fn find_plugins(&self) -> Vec<PathBuf> {
250 let main_dir_iter = once(&self.plugins_directory);
251 let other_directories_iter = self.additional_plugins_directories.iter();
252
253 if self.id == GameId::OpenMW {
258 find_plugins_in_directories(main_dir_iter.chain(other_directories_iter), self.id)
259 } else {
260 find_plugins_in_directories(other_directories_iter.chain(main_dir_iter), self.id)
261 }
262 }
263
264 pub(crate) fn game_path(&self) -> &Path {
265 &self.game_path
266 }
267
268 pub fn plugin_path(&self, plugin_name: &str) -> PathBuf {
269 plugin_path(
270 self.id,
271 plugin_name,
272 &self.plugins_directory,
273 &self.additional_plugins_directories,
274 )
275 }
276
277 pub fn refresh_implicitly_active_plugins(&mut self) -> Result<(), Error> {
278 let (early_loading_plugins, implicitly_active_plugins) =
279 GameSettings::load_implicitly_active_plugins(
280 self.id,
281 &self.game_path,
282 &self.my_games_path,
283 &self.plugins_directory,
284 &self.additional_plugins_directories,
285 )?;
286
287 self.early_loading_plugins = early_loading_plugins;
288 self.implicitly_active_plugins = implicitly_active_plugins;
289
290 Ok(())
291 }
292
293 fn load_implicitly_active_plugins(
294 game_id: GameId,
295 game_path: &Path,
296 my_games_path: &Path,
297 plugins_directory: &Path,
298 additional_plugins_directories: &[PathBuf],
299 ) -> Result<(Vec<String>, Vec<String>), Error> {
300 let mut test_files = test_files(game_id, game_path, my_games_path)?;
301
302 if matches!(
303 game_id,
304 GameId::Fallout4 | GameId::Fallout4VR | GameId::Starfield
305 ) {
306 test_files.retain(|f| {
309 let path = plugin_path(
310 game_id,
311 f,
312 plugins_directory,
313 additional_plugins_directories,
314 );
315 Plugin::with_path(&path, game_id, false).is_ok()
316 });
317 }
318
319 let early_loading_plugins =
320 early_loading_plugins(game_id, game_path, my_games_path, !test_files.is_empty())?;
321
322 let implicitly_active_plugins =
323 implicitly_active_plugins(game_id, game_path, &early_loading_plugins, &test_files)?;
324
325 Ok((early_loading_plugins, implicitly_active_plugins))
326 }
327}
328
329#[cfg(windows)]
330fn local_path(game_id: GameId, game_path: &Path) -> Result<Option<PathBuf>, Error> {
331 if game_id == GameId::OpenMW {
332 return openmw_config::user_config_dir(game_path).map(Some);
333 }
334
335 let Some(local_app_data_path) = dirs::data_local_dir() else {
336 return Err(Error::NoLocalAppData);
337 };
338
339 match appdata_folder_name(game_id, game_path) {
340 Some(x) => Ok(Some(local_app_data_path.join(x))),
341 None => Ok(None),
342 }
343}
344
345#[cfg(not(windows))]
346fn local_path(game_id: GameId, game_path: &Path) -> Result<Option<PathBuf>, Error> {
347 if game_id == GameId::OpenMW {
348 openmw_config::user_config_dir(game_path).map(Some)
349 } else if appdata_folder_name(game_id, game_path).is_none() {
350 Ok(None)
352 } else {
353 Err(Error::NoLocalAppData)
355 }
356}
357
358fn appdata_folder_name(game_id: GameId, game_path: &Path) -> Option<&'static str> {
360 match game_id {
361 GameId::Morrowind | GameId::OpenMW | GameId::OblivionRemastered => None,
362 GameId::Oblivion => Some("Oblivion"),
363 GameId::Skyrim => Some(skyrim_appdata_folder_name(game_path)),
364 GameId::SkyrimSE => Some(skyrim_se_appdata_folder_name(game_path)),
365 GameId::SkyrimVR => Some("Skyrim VR"),
366 GameId::Fallout3 => Some("Fallout3"),
367 GameId::FalloutNV => Some(falloutnv_appdata_folder_name(game_path)),
368 GameId::Fallout4 => Some(fallout4_appdata_folder_name(game_path)),
369 GameId::Fallout4VR => Some("Fallout4VR"),
370 GameId::Starfield => Some("Starfield"),
371 }
372}
373
374fn skyrim_appdata_folder_name(game_path: &Path) -> &'static str {
375 if is_enderal(game_path) {
376 "enderal"
378 } else {
379 "Skyrim"
380 }
381}
382
383fn skyrim_se_appdata_folder_name(game_path: &Path) -> &'static str {
384 let is_gog_install = game_path.join("Galaxy64.dll").exists();
385
386 if is_enderal(game_path) {
387 if is_gog_install {
388 "Enderal Special Edition GOG"
389 } else {
390 "Enderal Special Edition"
391 }
392 } else if is_gog_install {
393 "Skyrim Special Edition GOG"
395 } else if game_path.join("EOSSDK-Win64-Shipping.dll").exists() {
396 "Skyrim Special Edition EPIC"
398 } else if is_microsoft_store_install(GameId::SkyrimSE, game_path) {
399 "Skyrim Special Edition MS"
400 } else {
401 "Skyrim Special Edition"
403 }
404}
405
406fn falloutnv_appdata_folder_name(game_path: &Path) -> &'static str {
407 if game_path.join("EOSSDK-Win32-Shipping.dll").exists() {
408 "FalloutNV_Epic"
410 } else {
411 "FalloutNV"
412 }
413}
414
415fn fallout4_appdata_folder_name(game_path: &Path) -> &'static str {
416 if is_microsoft_store_install(GameId::Fallout4, game_path) {
417 "Fallout4 MS"
418 } else if game_path.join("EOSSDK-Win64-Shipping.dll").exists() {
419 "Fallout4 EPIC"
421 } else {
422 "Fallout4"
423 }
424}
425
426fn my_games_path(
427 game_id: GameId,
428 game_path: &Path,
429 local_path: &Path,
430) -> Result<Option<PathBuf>, Error> {
431 if game_id == GameId::OpenMW {
432 return Ok(Some(local_path.to_path_buf()));
435 }
436
437 my_games_folder_name(game_id, game_path)
438 .map(|folder| {
439 documents_path(local_path)
440 .map(|d| d.join("My Games").join(folder))
441 .ok_or_else(|| Error::NoDocumentsPath)
442 })
443 .transpose()
444}
445
446fn my_games_folder_name(game_id: GameId, game_path: &Path) -> Option<&'static str> {
447 match game_id {
448 GameId::OpenMW => Some("OpenMW"),
449 GameId::OblivionRemastered => Some("Oblivion Remastered"),
450 GameId::Skyrim => Some(skyrim_my_games_folder_name(game_path)),
451 _ => appdata_folder_name(game_id, game_path),
453 }
454}
455
456fn skyrim_my_games_folder_name(game_path: &Path) -> &'static str {
457 if is_enderal(game_path) {
458 "Enderal"
459 } else {
460 "Skyrim"
461 }
462}
463
464fn is_microsoft_store_install(game_id: GameId, game_path: &Path) -> bool {
465 const APPX_MANIFEST: &str = "appxmanifest.xml";
466
467 match game_id {
468 GameId::Morrowind | GameId::Oblivion | GameId::Fallout3 | GameId::FalloutNV => game_path
469 .parent()
470 .is_some_and(|parent| parent.join(APPX_MANIFEST).exists()),
471 GameId::SkyrimSE | GameId::Fallout4 | GameId::Starfield | GameId::OblivionRemastered => {
472 game_path.join(APPX_MANIFEST).exists()
473 }
474 _ => false,
475 }
476}
477
478#[cfg(windows)]
479fn documents_path(_local_path: &Path) -> Option<PathBuf> {
480 dirs::document_dir()
481}
482
483#[cfg(not(windows))]
484fn documents_path(local_path: &Path) -> Option<PathBuf> {
485 local_path
488 .parent()
489 .and_then(Path::parent)
490 .and_then(Path::parent)
491 .map(|p| p.join("Documents"))
492 .or_else(|| {
493 Some(local_path.join("../../../Documents"))
496 })
497}
498
499fn plugins_directory(
500 game_id: GameId,
501 game_path: &Path,
502 local_path: &Path,
503) -> Result<PathBuf, Error> {
504 match game_id {
505 GameId::OpenMW => openmw_config::resources_vfs_path(game_path, local_path),
506 GameId::Morrowind => Ok(game_path.join("Data Files")),
507 GameId::OblivionRemastered => Ok(game_path.join(OBLIVION_REMASTERED_RELATIVE_DATA_PATH)),
508 _ => Ok(game_path.join("Data")),
509 }
510}
511
512fn additional_plugins_directories(
513 game_id: GameId,
514 game_path: &Path,
515 my_games_path: &Path,
516) -> Result<Vec<PathBuf>, Error> {
517 if game_id == GameId::Fallout4 && is_microsoft_store_install(game_id, game_path) {
518 Ok(vec![
519 game_path.join(MS_FO4_AUTOMATRON_PATH),
520 game_path.join(MS_FO4_NUKA_WORLD_PATH),
521 game_path.join(MS_FO4_WASTELAND_PATH),
522 game_path.join(MS_FO4_TEXTURE_PACK_PATH),
523 game_path.join(MS_FO4_VAULT_TEC_PATH),
524 game_path.join(MS_FO4_FAR_HARBOR_PATH),
525 game_path.join(MS_FO4_CONTRAPTIONS_PATH),
526 ])
527 } else if game_id == GameId::Starfield {
528 Ok(vec![my_games_path.join("Data")])
529 } else if game_id == GameId::OpenMW {
530 openmw_config::additional_data_paths(game_path, my_games_path)
531 } else {
532 Ok(Vec::new())
533 }
534}
535
536fn load_order_path(
537 game_id: GameId,
538 local_path: &Path,
539 plugins_file_path: &Path,
540) -> Option<PathBuf> {
541 const LOADORDER_TXT: &str = "loadorder.txt";
542 match game_id {
543 GameId::Skyrim => Some(local_path.join(LOADORDER_TXT)),
544 GameId::OblivionRemastered => plugins_file_path.parent().map(|p| p.join(LOADORDER_TXT)),
545 _ => None,
546 }
547}
548
549fn plugins_file_path(
550 game_id: GameId,
551 game_path: &Path,
552 local_path: &Path,
553) -> Result<PathBuf, Error> {
554 match game_id {
555 GameId::OpenMW => Ok(local_path.join("openmw.cfg")),
556 GameId::Morrowind => Ok(game_path.join("Morrowind.ini")),
557 GameId::Oblivion => oblivion_plugins_file_path(game_path, local_path),
558 GameId::OblivionRemastered => Ok(game_path
559 .join(OBLIVION_REMASTERED_RELATIVE_DATA_PATH)
560 .join(PLUGINS_TXT)),
561 _ => Ok(local_path.join(PLUGINS_TXT)),
564 }
565}
566
567fn oblivion_plugins_file_path(game_path: &Path, local_path: &Path) -> Result<PathBuf, Error> {
568 let ini_path = game_path.join("Oblivion.ini");
569
570 let parent_path = if use_my_games_directory(&ini_path)? {
571 local_path
572 } else {
573 game_path
574 };
575
576 Ok(parent_path.join(PLUGINS_TXT))
578}
579
580fn ccc_file_paths(game_id: GameId, game_path: &Path, my_games_path: &Path) -> Vec<PathBuf> {
581 match game_id {
582 GameId::Fallout4 => vec![game_path.join("Fallout4.ccc")],
583 GameId::SkyrimSE => vec![game_path.join("Skyrim.ccc")],
584 GameId::Starfield => vec![
586 my_games_path.join("Starfield.ccc"),
587 game_path.join("Starfield.ccc"),
588 ],
589 _ => vec![],
590 }
591}
592
593fn hardcoded_plugins(game_id: GameId) -> &'static [&'static str] {
594 match game_id {
595 GameId::Skyrim => SKYRIM_HARDCODED_PLUGINS,
596 GameId::SkyrimSE => SKYRIM_SE_HARDCODED_PLUGINS,
597 GameId::SkyrimVR => SKYRIM_VR_HARDCODED_PLUGINS,
598 GameId::Fallout4 => FALLOUT4_HARDCODED_PLUGINS,
599 GameId::Fallout4VR => FALLOUT4VR_HARDCODED_PLUGINS,
600 GameId::Starfield => STARFIELD_HARDCODED_PLUGINS,
601 GameId::OpenMW => OPENMW_HARDCODED_PLUGINS,
602 _ => &[],
603 }
604}
605
606fn find_nam_plugins(plugins_path: &Path) -> Result<Vec<String>, Error> {
607 let mut plugin_names = Vec::new();
610
611 if !plugins_path.exists() {
612 return Ok(plugin_names);
613 }
614
615 let dir_iter = plugins_path
616 .read_dir()
617 .map_err(|e| Error::IoError(plugins_path.to_path_buf(), e))?
618 .filter_map(Result::ok)
619 .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false))
620 .filter(|e| {
621 e.path()
622 .extension()
623 .unwrap_or_default()
624 .eq_ignore_ascii_case("nam")
625 });
626
627 for entry in dir_iter {
628 let file_name = entry.file_name();
629
630 let plugin = Path::new(&file_name).with_extension("esp");
631 if let Some(esp) = plugin.to_str() {
632 plugin_names.push(esp.to_owned());
633 }
634
635 let master = Path::new(&file_name).with_extension("esm");
636 if let Some(esm) = master.to_str() {
637 plugin_names.push(esm.to_owned());
638 }
639 }
640
641 Ok(plugin_names)
642}
643
644fn early_loading_plugins(
645 game_id: GameId,
646 game_path: &Path,
647 my_games_path: &Path,
648 has_test_files: bool,
649) -> Result<Vec<String>, Error> {
650 let mut plugin_names: Vec<String> = hardcoded_plugins(game_id)
651 .iter()
652 .map(|s| (*s).to_owned())
653 .collect();
654
655 if matches!(game_id, GameId::Fallout4 | GameId::Starfield) && has_test_files {
656 return Ok(plugin_names);
659 }
660
661 for file_path in ccc_file_paths(game_id, game_path, my_games_path) {
662 if file_path.exists() {
663 let reader =
664 BufReader::new(File::open(&file_path).map_err(|e| Error::IoError(file_path, e))?);
665
666 let lines = reader
667 .lines()
668 .filter_map(|line| line.ok().filter(|l| !l.is_empty()));
669
670 plugin_names.extend(lines);
671 break;
672 }
673 }
674
675 if game_id == GameId::OpenMW {
676 plugin_names.extend(openmw_config::non_user_active_plugin_names(game_path)?);
677 }
678
679 deduplicate(&mut plugin_names);
680
681 Ok(plugin_names)
682}
683
684fn implicitly_active_plugins(
685 game_id: GameId,
686 game_path: &Path,
687 early_loading_plugins: &[String],
688 test_files: &[String],
689) -> Result<Vec<String>, Error> {
690 let mut plugin_names = Vec::new();
691
692 plugin_names.extend_from_slice(early_loading_plugins);
693 plugin_names.extend_from_slice(test_files);
694
695 if game_id == GameId::FalloutNV {
696 let nam_plugins = find_nam_plugins(&game_path.join("Data"))?;
700
701 plugin_names.extend(nam_plugins);
702 } else if game_id == GameId::Skyrim {
703 plugin_names.push("Update.esm".to_owned());
706 } else if game_id == GameId::Starfield {
707 plugin_names.push("BlueprintShips-Starfield.esm".to_owned());
710 }
711
712 deduplicate(&mut plugin_names);
713
714 Ok(plugin_names)
715}
716
717fn deduplicate(plugin_names: &mut Vec<String>) {
719 let mut set = std::collections::HashSet::new();
720 plugin_names.retain(|e| set.insert(unicase::UniCase::new(e.clone())));
721}
722
723fn find_map_path(directory: &Path, plugin_name: &str, game_id: GameId) -> Option<PathBuf> {
724 if game_id.allow_plugin_ghosting() {
725 use crate::ghostable_path::GhostablePath;
727
728 directory.join(plugin_name).resolve_path().ok()
729 } else {
730 let path = directory.join(plugin_name);
731 path.exists().then_some(path)
732 }
733}
734
735fn pick_plugin_path<'a>(
736 game_id: GameId,
737 plugin_name: &str,
738 plugins_directory: &Path,
739 mut dir_iter: impl Iterator<Item = &'a PathBuf>,
740) -> PathBuf {
741 dir_iter
742 .find_map(|d| find_map_path(d, plugin_name, game_id))
743 .unwrap_or_else(|| plugins_directory.join(plugin_name))
744}
745
746fn plugin_path(
747 game_id: GameId,
748 plugin_name: &str,
749 plugins_directory: &Path,
750 additional_plugins_directories: &[PathBuf],
751) -> PathBuf {
752 if game_id == GameId::Starfield {
759 use crate::ghostable_path::GhostablePath;
761
762 let path = plugins_directory.join(plugin_name);
763 if path.resolve_path().is_err() {
764 return path;
765 }
766 }
767
768 match game_id {
771 GameId::OpenMW => pick_plugin_path(
772 game_id,
773 plugin_name,
774 plugins_directory,
775 additional_plugins_directories.iter().rev(),
776 ),
777 _ => pick_plugin_path(
778 game_id,
779 plugin_name,
780 plugins_directory,
781 additional_plugins_directories.iter(),
782 ),
783 }
784}
785
786fn sort_plugins_dir_entries(a: &DirEntry, b: &DirEntry) -> Ordering {
787 let m_a = a.metadata().and_then(|m| m.modified()).ok();
790 let m_b = b.metadata().and_then(|m| m.modified()).ok();
791
792 match m_a.cmp(&m_b) {
793 Ordering::Equal => a.file_name().cmp(&b.file_name()).reverse(),
794 x => x,
795 }
796}
797
798fn sort_plugins_dir_entries_starfield(a: &DirEntry, b: &DirEntry) -> Ordering {
799 let m_a = a.metadata().and_then(|m| m.modified()).ok();
802 let m_b = b.metadata().and_then(|m| m.modified()).ok();
803
804 match m_a.cmp(&m_b) {
805 Ordering::Equal => a.file_name().cmp(&b.file_name()),
806 x => x,
807 }
808}
809
810fn sort_plugins_dir_entries_openmw(a: &DirEntry, b: &DirEntry) -> Ordering {
811 if a.path().parent() == b.path().parent() {
814 a.file_name().cmp(&b.file_name())
815 } else {
816 Ordering::Equal
817 }
818}
819
820fn find_plugins_in_directories<'a>(
821 directories_iter: impl Iterator<Item = &'a PathBuf>,
822 game_id: GameId,
823) -> Vec<PathBuf> {
824 let mut dir_entries: Vec<_> = directories_iter
825 .flat_map(read_dir)
826 .flatten()
827 .filter_map(Result::ok)
828 .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false))
829 .filter(|e| {
830 e.file_name()
831 .to_str()
832 .is_some_and(|f| has_plugin_extension(f, game_id))
833 })
834 .collect();
835
836 let compare = match game_id {
837 GameId::OpenMW => sort_plugins_dir_entries_openmw,
838 GameId::Starfield => sort_plugins_dir_entries_starfield,
839 _ => sort_plugins_dir_entries,
840 };
841
842 dir_entries.sort_by(compare);
843
844 dir_entries.into_iter().map(|e| e.path()).collect()
845}
846
847#[cfg(test)]
848mod tests {
849 #[cfg(windows)]
850 use std::env;
851 use std::{fs::create_dir_all, io::Write};
852 use tempfile::tempdir;
853
854 use crate::tests::{copy_to_dir, set_file_timestamps, NON_ASCII};
855
856 use super::*;
857
858 fn game_with_generic_paths(game_id: GameId) -> GameSettings {
859 GameSettings::with_local_and_my_games_paths(
860 game_id,
861 &PathBuf::from("game"),
862 &PathBuf::from("local"),
863 PathBuf::from("my games"),
864 )
865 .unwrap()
866 }
867
868 fn game_with_game_path(game_id: GameId, game_path: &Path) -> GameSettings {
869 GameSettings::with_local_and_my_games_paths(
870 game_id,
871 game_path,
872 &PathBuf::default(),
873 PathBuf::default(),
874 )
875 .unwrap()
876 }
877
878 fn game_with_ccc_plugins(
879 game_id: GameId,
880 game_path: &Path,
881 plugin_names: &[&str],
882 ) -> GameSettings {
883 let ccc_path = &ccc_file_paths(game_id, game_path, &PathBuf::new())[0];
884 create_ccc_file(ccc_path, plugin_names);
885
886 game_with_game_path(game_id, game_path)
887 }
888
889 fn create_ccc_file(path: &Path, plugin_names: &[&str]) {
890 create_dir_all(path.parent().unwrap()).unwrap();
891
892 let mut file = File::create(path).unwrap();
893
894 for plugin_name in plugin_names {
895 writeln!(file, "{plugin_name}").unwrap();
896 }
897 }
898
899 #[test]
900 #[cfg(windows)]
901 fn new_should_determine_correct_local_path_on_windows() {
902 let settings = GameSettings::new(GameId::Skyrim, Path::new("game")).unwrap();
903 let local_app_data = env::var("LOCALAPPDATA").unwrap();
904 let local_app_data_path = Path::new(&local_app_data);
905
906 assert_eq!(
907 local_app_data_path.join("Skyrim").join("Plugins.txt"),
908 *settings.active_plugins_file()
909 );
910 assert_eq!(
911 &local_app_data_path.join("Skyrim").join("loadorder.txt"),
912 *settings.load_order_file().as_ref().unwrap()
913 );
914 }
915
916 #[test]
917 #[cfg(windows)]
918 fn new_should_determine_correct_local_path_for_openmw() {
919 let tmp_dir = tempdir().unwrap();
920 let global_cfg_path = tmp_dir.path().join("openmw.cfg");
921
922 std::fs::write(&global_cfg_path, "config=local").unwrap();
923
924 let settings = GameSettings::new(GameId::OpenMW, tmp_dir.path()).unwrap();
925
926 assert_eq!(
927 &tmp_dir.path().join("local/openmw.cfg"),
928 settings.active_plugins_file()
929 );
930 assert_eq!(tmp_dir.path().join("local"), settings.my_games_path);
931 }
932
933 #[test]
934 fn new_should_use_an_empty_local_path_for_morrowind() {
935 let settings = GameSettings::new(GameId::Morrowind, Path::new("game")).unwrap();
936
937 assert_eq!(PathBuf::new(), settings.my_games_path);
938 }
939
940 #[test]
941 #[cfg(not(windows))]
942 fn new_should_determine_correct_local_path_for_openmw_on_linux() {
943 let config_path = Path::new("/etc/openmw");
944
945 let settings = GameSettings::new(GameId::OpenMW, Path::new("game")).unwrap();
946
947 assert_eq!(
948 &config_path.join("openmw.cfg"),
949 settings.active_plugins_file()
950 );
951 assert_eq!(config_path, settings.my_games_path);
952 }
953
954 #[test]
955 fn id_should_be_the_id_the_struct_was_created_with() {
956 let settings = game_with_generic_paths(GameId::Morrowind);
957 assert_eq!(GameId::Morrowind, settings.id());
958 }
959
960 #[test]
961 fn load_order_method_should_be_timestamp_for_tes3_tes4_fo3_and_fonv() {
962 let mut settings = game_with_generic_paths(GameId::Morrowind);
963 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
964
965 settings = game_with_generic_paths(GameId::Oblivion);
966 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
967
968 settings = game_with_generic_paths(GameId::Fallout3);
969 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
970
971 settings = game_with_generic_paths(GameId::FalloutNV);
972 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
973 }
974
975 #[test]
976 fn load_order_method_should_be_textfile_for_tes5() {
977 let settings = game_with_generic_paths(GameId::Skyrim);
978 assert_eq!(LoadOrderMethod::Textfile, settings.load_order_method());
979 }
980
981 #[test]
982 fn load_order_method_should_be_asterisk_for_tes5se_tes5vr_fo4_fo4vr_and_starfield() {
983 let mut settings = game_with_generic_paths(GameId::SkyrimSE);
984 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
985
986 settings = game_with_generic_paths(GameId::SkyrimVR);
987 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
988
989 settings = game_with_generic_paths(GameId::Fallout4);
990 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
991
992 settings = game_with_generic_paths(GameId::Fallout4VR);
993 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
994
995 settings = game_with_generic_paths(GameId::Starfield);
996 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
997 }
998
999 #[test]
1000 fn load_order_method_should_be_openmw_for_openmw() {
1001 let settings = game_with_generic_paths(GameId::OpenMW);
1002
1003 assert_eq!(LoadOrderMethod::OpenMW, settings.load_order_method());
1004 }
1005
1006 #[test]
1007 #[expect(deprecated)]
1008 fn master_file_should_be_mapped_from_game_id() {
1009 let mut settings = game_with_generic_paths(GameId::OpenMW);
1010 assert_eq!("Morrowind.esm", settings.master_file());
1011
1012 settings = game_with_generic_paths(GameId::Morrowind);
1013 assert_eq!("Morrowind.esm", settings.master_file());
1014
1015 settings = game_with_generic_paths(GameId::Oblivion);
1016 assert_eq!("Oblivion.esm", settings.master_file());
1017
1018 settings = game_with_generic_paths(GameId::Skyrim);
1019 assert_eq!("Skyrim.esm", settings.master_file());
1020
1021 settings = game_with_generic_paths(GameId::SkyrimSE);
1022 assert_eq!("Skyrim.esm", settings.master_file());
1023
1024 settings = game_with_generic_paths(GameId::SkyrimVR);
1025 assert_eq!("Skyrim.esm", settings.master_file());
1026
1027 settings = game_with_generic_paths(GameId::Fallout3);
1028 assert_eq!("Fallout3.esm", settings.master_file());
1029
1030 settings = game_with_generic_paths(GameId::FalloutNV);
1031 assert_eq!("FalloutNV.esm", settings.master_file());
1032
1033 settings = game_with_generic_paths(GameId::Fallout4);
1034 assert_eq!("Fallout4.esm", settings.master_file());
1035
1036 settings = game_with_generic_paths(GameId::Fallout4VR);
1037 assert_eq!("Fallout4.esm", settings.master_file());
1038
1039 settings = game_with_generic_paths(GameId::Starfield);
1040 assert_eq!("Starfield.esm", settings.master_file());
1041 }
1042
1043 #[test]
1044 fn appdata_folder_name_should_be_mapped_from_game_id() {
1045 let game_path = Path::new("");
1047
1048 assert!(appdata_folder_name(GameId::OpenMW, game_path).is_none());
1049
1050 assert!(appdata_folder_name(GameId::Morrowind, game_path).is_none());
1051
1052 let mut folder = appdata_folder_name(GameId::Oblivion, game_path).unwrap();
1053 assert_eq!("Oblivion", folder);
1054
1055 folder = appdata_folder_name(GameId::Skyrim, game_path).unwrap();
1056 assert_eq!("Skyrim", folder);
1057
1058 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1059 assert_eq!("Skyrim Special Edition", folder);
1060
1061 folder = appdata_folder_name(GameId::SkyrimVR, game_path).unwrap();
1062 assert_eq!("Skyrim VR", folder);
1063
1064 folder = appdata_folder_name(GameId::Fallout3, game_path).unwrap();
1065 assert_eq!("Fallout3", folder);
1066
1067 folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
1068 assert_eq!("FalloutNV", folder);
1069
1070 folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1071 assert_eq!("Fallout4", folder);
1072
1073 folder = appdata_folder_name(GameId::Fallout4VR, game_path).unwrap();
1074 assert_eq!("Fallout4VR", folder);
1075
1076 folder = appdata_folder_name(GameId::Starfield, game_path).unwrap();
1077 assert_eq!("Starfield", folder);
1078 }
1079
1080 #[test]
1081 fn appdata_folder_name_for_skyrim_se_should_have_gog_suffix_if_galaxy_dll_is_in_game_path() {
1082 let tmp_dir = tempdir().unwrap();
1083 let game_path = tmp_dir.path();
1084
1085 let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1086 assert_eq!("Skyrim Special Edition", folder);
1087
1088 let dll_path = game_path.join("Galaxy64.dll");
1089 File::create(&dll_path).unwrap();
1090
1091 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1092 assert_eq!("Skyrim Special Edition GOG", folder);
1093 }
1094
1095 #[test]
1096 fn appdata_folder_name_for_skyrim_se_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
1097 let tmp_dir = tempdir().unwrap();
1098 let game_path = tmp_dir.path();
1099
1100 let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1101 assert_eq!("Skyrim Special Edition", folder);
1102
1103 let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
1104 File::create(&dll_path).unwrap();
1105
1106 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1107 assert_eq!("Skyrim Special Edition EPIC", folder);
1108 }
1109
1110 #[test]
1111 fn appdata_folder_name_for_skyrim_se_should_have_ms_suffix_if_appxmanifest_xml_is_in_game_path()
1112 {
1113 let tmp_dir = tempdir().unwrap();
1114 let game_path = tmp_dir.path();
1115
1116 let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1117 assert_eq!("Skyrim Special Edition", folder);
1118
1119 let dll_path = game_path.join("appxmanifest.xml");
1120 File::create(&dll_path).unwrap();
1121
1122 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1123 assert_eq!("Skyrim Special Edition MS", folder);
1124 }
1125
1126 #[test]
1127 fn appdata_folder_name_for_skyrim_se_prefers_gog_suffix_over_epic_suffix() {
1128 let tmp_dir = tempdir().unwrap();
1129 let game_path = tmp_dir.path();
1130
1131 let dll_path = game_path.join("Galaxy64.dll");
1132 File::create(&dll_path).unwrap();
1133
1134 let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
1135 File::create(&dll_path).unwrap();
1136
1137 let folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1138 assert_eq!("Skyrim Special Edition GOG", folder);
1139 }
1140
1141 #[test]
1142 fn appdata_folder_name_for_fallout_nv_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
1143 let tmp_dir = tempdir().unwrap();
1144 let game_path = tmp_dir.path();
1145
1146 let mut folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
1147 assert_eq!("FalloutNV", folder);
1148
1149 let dll_path = game_path.join("EOSSDK-Win32-Shipping.dll");
1150 File::create(&dll_path).unwrap();
1151
1152 folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
1153 assert_eq!("FalloutNV_Epic", folder);
1154 }
1155
1156 #[test]
1157 fn appdata_folder_name_for_fallout4_should_have_ms_suffix_if_appxmanifest_xml_is_in_game_path()
1158 {
1159 let tmp_dir = tempdir().unwrap();
1160 let game_path = tmp_dir.path();
1161
1162 let mut folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1163 assert_eq!("Fallout4", folder);
1164
1165 let dll_path = game_path.join("appxmanifest.xml");
1166 File::create(&dll_path).unwrap();
1167
1168 folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1169 assert_eq!("Fallout4 MS", folder);
1170 }
1171
1172 #[test]
1173 fn appdata_folder_name_for_fallout4_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
1174 let tmp_dir = tempdir().unwrap();
1175 let game_path = tmp_dir.path();
1176
1177 let mut folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1178 assert_eq!("Fallout4", folder);
1179
1180 let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
1181 File::create(&dll_path).unwrap();
1182
1183 folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1184 assert_eq!("Fallout4 EPIC", folder);
1185 }
1186
1187 #[test]
1188 #[cfg(windows)]
1189 fn my_games_path_should_be_in_documents_path_on_windows() {
1190 let empty_path = Path::new("");
1191 let parent_path = dirs::document_dir().unwrap().join("My Games");
1192
1193 let path = my_games_path(GameId::Morrowind, empty_path, empty_path).unwrap();
1194 assert!(path.is_none());
1195
1196 let path = my_games_path(GameId::Oblivion, empty_path, empty_path)
1197 .unwrap()
1198 .unwrap();
1199 assert_eq!(parent_path.join("Oblivion"), path);
1200
1201 let path = my_games_path(GameId::Skyrim, empty_path, empty_path)
1202 .unwrap()
1203 .unwrap();
1204 assert_eq!(parent_path.join("Skyrim"), path);
1205
1206 let path = my_games_path(GameId::SkyrimSE, empty_path, empty_path)
1207 .unwrap()
1208 .unwrap();
1209 assert_eq!(parent_path.join("Skyrim Special Edition"), path);
1210
1211 let path = my_games_path(GameId::SkyrimVR, empty_path, empty_path)
1212 .unwrap()
1213 .unwrap();
1214 assert_eq!(parent_path.join("Skyrim VR"), path);
1215
1216 let path = my_games_path(GameId::Fallout3, empty_path, empty_path)
1217 .unwrap()
1218 .unwrap();
1219 assert_eq!(parent_path.join("Fallout3"), path);
1220
1221 let path = my_games_path(GameId::FalloutNV, empty_path, empty_path)
1222 .unwrap()
1223 .unwrap();
1224 assert_eq!(parent_path.join("FalloutNV"), path);
1225
1226 let path = my_games_path(GameId::Fallout4, empty_path, empty_path)
1227 .unwrap()
1228 .unwrap();
1229 assert_eq!(parent_path.join("Fallout4"), path);
1230
1231 let path = my_games_path(GameId::Fallout4VR, empty_path, empty_path)
1232 .unwrap()
1233 .unwrap();
1234 assert_eq!(parent_path.join("Fallout4VR"), path);
1235
1236 let path = my_games_path(GameId::Starfield, empty_path, empty_path)
1237 .unwrap()
1238 .unwrap();
1239 assert_eq!(parent_path.join("Starfield"), path);
1240 }
1241
1242 #[test]
1243 #[cfg(not(windows))]
1244 fn my_games_path_should_be_relative_to_local_path_on_linux() {
1245 let empty_path = Path::new("");
1246 let local_path = Path::new("wineprefix/drive_c/Users/user/AppData/Local/Game");
1247 let parent_path = Path::new("wineprefix/drive_c/Users/user/Documents/My Games");
1248
1249 let path = my_games_path(GameId::Morrowind, empty_path, local_path).unwrap();
1250 assert!(path.is_none());
1251
1252 let path = my_games_path(GameId::Oblivion, empty_path, local_path)
1253 .unwrap()
1254 .unwrap();
1255 assert_eq!(parent_path.join("Oblivion"), path);
1256
1257 let path = my_games_path(GameId::Skyrim, empty_path, local_path)
1258 .unwrap()
1259 .unwrap();
1260 assert_eq!(parent_path.join("Skyrim"), path);
1261
1262 let path = my_games_path(GameId::SkyrimSE, empty_path, local_path)
1263 .unwrap()
1264 .unwrap();
1265 assert_eq!(parent_path.join("Skyrim Special Edition"), path);
1266
1267 let path = my_games_path(GameId::SkyrimVR, empty_path, local_path)
1268 .unwrap()
1269 .unwrap();
1270 assert_eq!(parent_path.join("Skyrim VR"), path);
1271
1272 let path = my_games_path(GameId::Fallout3, empty_path, local_path)
1273 .unwrap()
1274 .unwrap();
1275 assert_eq!(parent_path.join("Fallout3"), path);
1276
1277 let path = my_games_path(GameId::FalloutNV, empty_path, local_path)
1278 .unwrap()
1279 .unwrap();
1280 assert_eq!(parent_path.join("FalloutNV"), path);
1281
1282 let path = my_games_path(GameId::Fallout4, empty_path, local_path)
1283 .unwrap()
1284 .unwrap();
1285 assert_eq!(parent_path.join("Fallout4"), path);
1286
1287 let path = my_games_path(GameId::Fallout4VR, empty_path, local_path)
1288 .unwrap()
1289 .unwrap();
1290 assert_eq!(parent_path.join("Fallout4VR"), path);
1291
1292 let path = my_games_path(GameId::Starfield, empty_path, local_path)
1293 .unwrap()
1294 .unwrap();
1295 assert_eq!(parent_path.join("Starfield"), path);
1296 }
1297
1298 #[test]
1299 #[cfg(windows)]
1300 fn my_games_path_should_be_local_path_for_openmw() {
1301 let local_path = Path::new("path/to/local");
1302
1303 let path = my_games_path(GameId::OpenMW, Path::new(""), local_path)
1304 .unwrap()
1305 .unwrap();
1306 assert_eq!(local_path, path);
1307 }
1308
1309 #[test]
1310 fn plugins_directory_should_be_mapped_from_game_id() {
1311 let data_path = Path::new("Data");
1312 let empty_path = Path::new("");
1313 let closure = |game_id| plugins_directory(game_id, empty_path, empty_path).unwrap();
1314
1315 assert_eq!(Path::new("resources/vfs"), closure(GameId::OpenMW));
1316 assert_eq!(Path::new("Data Files"), closure(GameId::Morrowind));
1317 assert_eq!(data_path, closure(GameId::Oblivion));
1318 assert_eq!(data_path, closure(GameId::Skyrim));
1319 assert_eq!(data_path, closure(GameId::SkyrimSE));
1320 assert_eq!(data_path, closure(GameId::SkyrimVR));
1321 assert_eq!(data_path, closure(GameId::Fallout3));
1322 assert_eq!(data_path, closure(GameId::FalloutNV));
1323 assert_eq!(data_path, closure(GameId::Fallout4));
1324 assert_eq!(data_path, closure(GameId::Fallout4VR));
1325 assert_eq!(data_path, closure(GameId::Starfield));
1326 }
1327
1328 #[test]
1329 fn active_plugins_file_should_be_mapped_from_game_id() {
1330 let mut settings = game_with_generic_paths(GameId::OpenMW);
1331 assert_eq!(
1332 Path::new("local/openmw.cfg"),
1333 settings.active_plugins_file()
1334 );
1335
1336 settings = game_with_generic_paths(GameId::Morrowind);
1337 assert_eq!(
1338 Path::new("game/Morrowind.ini"),
1339 settings.active_plugins_file()
1340 );
1341
1342 settings = game_with_generic_paths(GameId::Oblivion);
1343 assert_eq!(
1344 Path::new("local/Plugins.txt"),
1345 settings.active_plugins_file()
1346 );
1347
1348 settings = game_with_generic_paths(GameId::Skyrim);
1349 assert_eq!(
1350 Path::new("local/Plugins.txt"),
1351 settings.active_plugins_file()
1352 );
1353
1354 settings = game_with_generic_paths(GameId::SkyrimSE);
1355 assert_eq!(
1356 Path::new("local/Plugins.txt"),
1357 settings.active_plugins_file()
1358 );
1359
1360 settings = game_with_generic_paths(GameId::SkyrimVR);
1361 assert_eq!(
1362 Path::new("local/Plugins.txt"),
1363 settings.active_plugins_file()
1364 );
1365
1366 settings = game_with_generic_paths(GameId::Fallout3);
1367 assert_eq!(
1368 Path::new("local/Plugins.txt"),
1369 settings.active_plugins_file()
1370 );
1371
1372 settings = game_with_generic_paths(GameId::FalloutNV);
1373 assert_eq!(
1374 Path::new("local/Plugins.txt"),
1375 settings.active_plugins_file()
1376 );
1377
1378 settings = game_with_generic_paths(GameId::Fallout4);
1379 assert_eq!(
1380 Path::new("local/Plugins.txt"),
1381 settings.active_plugins_file()
1382 );
1383
1384 settings = game_with_generic_paths(GameId::Fallout4VR);
1385 assert_eq!(
1386 Path::new("local/Plugins.txt"),
1387 settings.active_plugins_file()
1388 );
1389
1390 settings = game_with_generic_paths(GameId::Starfield);
1391 assert_eq!(
1392 Path::new("local/Plugins.txt"),
1393 settings.active_plugins_file()
1394 );
1395 }
1396
1397 #[test]
1398 fn active_plugins_file_should_be_in_game_path_for_oblivion_if_ini_setting_is_not_1() {
1399 let tmp_dir = tempdir().unwrap();
1400 let game_path = tmp_dir.path();
1401 let ini_path = game_path.join("Oblivion.ini");
1402
1403 std::fs::write(ini_path, "[General]\nbUseMyGamesDirectory=0\n").unwrap();
1404
1405 let settings = game_with_game_path(GameId::Oblivion, game_path);
1406 assert_eq!(
1407 game_path.join("Plugins.txt"),
1408 *settings.active_plugins_file()
1409 );
1410 }
1411
1412 #[test]
1413 fn early_loading_plugins_should_be_mapped_from_game_id() {
1414 let mut settings = game_with_generic_paths(GameId::Skyrim);
1415 let mut plugins = vec!["Skyrim.esm"];
1416 assert_eq!(plugins, settings.early_loading_plugins());
1417
1418 settings = game_with_generic_paths(GameId::SkyrimSE);
1419 plugins = vec![
1420 "Skyrim.esm",
1421 "Update.esm",
1422 "Dawnguard.esm",
1423 "HearthFires.esm",
1424 "Dragonborn.esm",
1425 ];
1426 assert_eq!(plugins, settings.early_loading_plugins());
1427
1428 settings = game_with_generic_paths(GameId::SkyrimVR);
1429 plugins = vec![
1430 "Skyrim.esm",
1431 "Update.esm",
1432 "Dawnguard.esm",
1433 "HearthFires.esm",
1434 "Dragonborn.esm",
1435 "SkyrimVR.esm",
1436 ];
1437 assert_eq!(plugins, settings.early_loading_plugins());
1438
1439 settings = game_with_generic_paths(GameId::Fallout4);
1440 plugins = vec![
1441 "Fallout4.esm",
1442 "DLCRobot.esm",
1443 "DLCworkshop01.esm",
1444 "DLCCoast.esm",
1445 "DLCworkshop02.esm",
1446 "DLCworkshop03.esm",
1447 "DLCNukaWorld.esm",
1448 "DLCUltraHighResolution.esm",
1449 ];
1450 assert_eq!(plugins, settings.early_loading_plugins());
1451
1452 settings = game_with_generic_paths(GameId::OpenMW);
1453 plugins = vec!["builtin.omwscripts"];
1454 assert_eq!(plugins, settings.early_loading_plugins());
1455
1456 settings = game_with_generic_paths(GameId::Morrowind);
1457 assert!(settings.early_loading_plugins().is_empty());
1458
1459 settings = game_with_generic_paths(GameId::Oblivion);
1460 assert!(settings.early_loading_plugins().is_empty());
1461
1462 settings = game_with_generic_paths(GameId::Fallout3);
1463 assert!(settings.early_loading_plugins().is_empty());
1464
1465 settings = game_with_generic_paths(GameId::FalloutNV);
1466 assert!(settings.early_loading_plugins().is_empty());
1467
1468 settings = game_with_generic_paths(GameId::Fallout4VR);
1469 plugins = vec!["Fallout4.esm", "Fallout4_VR.esm"];
1470 assert_eq!(plugins, settings.early_loading_plugins());
1471
1472 settings = game_with_generic_paths(GameId::Starfield);
1473 plugins = vec![
1474 "Starfield.esm",
1475 "Constellation.esm",
1476 "OldMars.esm",
1477 "ShatteredSpace.esm",
1478 "SFBGS003.esm",
1479 "SFBGS004.esm",
1480 "SFBGS006.esm",
1481 "SFBGS007.esm",
1482 "SFBGS008.esm",
1483 ];
1484 assert_eq!(plugins, settings.early_loading_plugins());
1485 }
1486
1487 #[test]
1488 fn early_loading_plugins_should_include_plugins_loaded_from_ccc_file() {
1489 let tmp_dir = tempdir().unwrap();
1490 let game_path = tmp_dir.path();
1491
1492 let mut plugins = vec![
1493 "Skyrim.esm",
1494 "Update.esm",
1495 "Dawnguard.esm",
1496 "HearthFires.esm",
1497 "Dragonborn.esm",
1498 "ccBGSSSE002-ExoticArrows.esl",
1499 "ccBGSSSE003-Zombies.esl",
1500 "ccBGSSSE004-RuinsEdge.esl",
1501 "ccBGSSSE006-StendarsHammer.esl",
1502 "ccBGSSSE007-Chrysamere.esl",
1503 "ccBGSSSE010-PetDwarvenArmoredMudcrab.esl",
1504 "ccBGSSSE014-SpellPack01.esl",
1505 "ccBGSSSE019-StaffofSheogorath.esl",
1506 "ccMTYSSE001-KnightsoftheNine.esl",
1507 "ccQDRSSE001-SurvivalMode.esl",
1508 ];
1509 let mut settings = game_with_ccc_plugins(GameId::SkyrimSE, game_path, &plugins[5..]);
1510 assert_eq!(plugins, settings.early_loading_plugins());
1511
1512 plugins = vec![
1513 "Fallout4.esm",
1514 "DLCRobot.esm",
1515 "DLCworkshop01.esm",
1516 "DLCCoast.esm",
1517 "DLCworkshop02.esm",
1518 "DLCworkshop03.esm",
1519 "DLCNukaWorld.esm",
1520 "DLCUltraHighResolution.esm",
1521 "ccBGSFO4001-PipBoy(Black).esl",
1522 "ccBGSFO4002-PipBoy(Blue).esl",
1523 "ccBGSFO4003-PipBoy(Camo01).esl",
1524 "ccBGSFO4004-PipBoy(Camo02).esl",
1525 "ccBGSFO4006-PipBoy(Chrome).esl",
1526 "ccBGSFO4012-PipBoy(Red).esl",
1527 "ccBGSFO4014-PipBoy(White).esl",
1528 "ccBGSFO4016-Prey.esl",
1529 "ccBGSFO4017-Mauler.esl",
1530 "ccBGSFO4018-GaussRiflePrototype.esl",
1531 "ccBGSFO4019-ChineseStealthArmor.esl",
1532 "ccBGSFO4020-PowerArmorSkin(Black).esl",
1533 "ccBGSFO4022-PowerArmorSkin(Camo01).esl",
1534 "ccBGSFO4023-PowerArmorSkin(Camo02).esl",
1535 "ccBGSFO4025-PowerArmorSkin(Chrome).esl",
1536 "ccBGSFO4038-HorseArmor.esl",
1537 "ccBGSFO4039-TunnelSnakes.esl",
1538 "ccBGSFO4041-DoomMarineArmor.esl",
1539 "ccBGSFO4042-BFG.esl",
1540 "ccBGSFO4043-DoomChainsaw.esl",
1541 "ccBGSFO4044-HellfirePowerArmor.esl",
1542 "ccFSVFO4001-ModularMilitaryBackpack.esl",
1543 "ccFSVFO4002-MidCenturyModern.esl",
1544 "ccFRSFO4001-HandmadeShotgun.esl",
1545 "ccEEJFO4001-DecorationPack.esl",
1546 ];
1547 settings = game_with_ccc_plugins(GameId::Fallout4, game_path, &plugins[8..]);
1548 assert_eq!(plugins, settings.early_loading_plugins());
1549 }
1550
1551 #[test]
1552 fn early_loading_plugins_should_use_the_starfield_ccc_file_in_game_path() {
1553 let tmp_dir = tempdir().unwrap();
1554 let game_path = tmp_dir.path().join("game");
1555 let my_games_path = tmp_dir.path().join("my games");
1556
1557 create_ccc_file(&game_path.join("Starfield.ccc"), &["test.esm"]);
1558
1559 let settings = GameSettings::with_local_and_my_games_paths(
1560 GameId::Starfield,
1561 &game_path,
1562 &PathBuf::default(),
1563 my_games_path,
1564 )
1565 .unwrap();
1566
1567 let expected = &[
1568 "Starfield.esm",
1569 "Constellation.esm",
1570 "OldMars.esm",
1571 "ShatteredSpace.esm",
1572 "SFBGS003.esm",
1573 "SFBGS004.esm",
1574 "SFBGS006.esm",
1575 "SFBGS007.esm",
1576 "SFBGS008.esm",
1577 "test.esm",
1578 ];
1579 assert_eq!(expected, settings.early_loading_plugins());
1580 }
1581
1582 #[test]
1583 fn early_loading_plugins_should_use_the_starfield_ccc_file_in_my_games_path() {
1584 let tmp_dir = tempdir().unwrap();
1585 let game_path = tmp_dir.path().join("game");
1586 let my_games_path = tmp_dir.path().join("my games");
1587
1588 create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test.esm"]);
1589
1590 let settings = GameSettings::with_local_and_my_games_paths(
1591 GameId::Starfield,
1592 &game_path,
1593 &PathBuf::default(),
1594 my_games_path,
1595 )
1596 .unwrap();
1597
1598 let expected = &[
1599 "Starfield.esm",
1600 "Constellation.esm",
1601 "OldMars.esm",
1602 "ShatteredSpace.esm",
1603 "SFBGS003.esm",
1604 "SFBGS004.esm",
1605 "SFBGS006.esm",
1606 "SFBGS007.esm",
1607 "SFBGS008.esm",
1608 "test.esm",
1609 ];
1610 assert_eq!(expected, settings.early_loading_plugins());
1611 }
1612
1613 #[test]
1614 fn early_loading_plugins_should_use_the_first_ccc_file_that_exists() {
1615 let tmp_dir = tempdir().unwrap();
1616 let game_path = tmp_dir.path().join("game");
1617 let my_games_path = tmp_dir.path().join("my games");
1618
1619 create_ccc_file(&game_path.join("Starfield.ccc"), &["test1.esm"]);
1620 create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test2.esm"]);
1621
1622 let settings = GameSettings::with_local_and_my_games_paths(
1623 GameId::Starfield,
1624 &game_path,
1625 &PathBuf::default(),
1626 my_games_path,
1627 )
1628 .unwrap();
1629
1630 let expected = &[
1631 "Starfield.esm",
1632 "Constellation.esm",
1633 "OldMars.esm",
1634 "ShatteredSpace.esm",
1635 "SFBGS003.esm",
1636 "SFBGS004.esm",
1637 "SFBGS006.esm",
1638 "SFBGS007.esm",
1639 "SFBGS008.esm",
1640 "test2.esm",
1641 ];
1642 assert_eq!(expected, settings.early_loading_plugins());
1643 }
1644
1645 #[test]
1646 fn early_loading_plugins_should_not_include_cc_plugins_for_fallout4_if_test_files_are_configured(
1647 ) {
1648 let tmp_dir = tempdir().unwrap();
1649 let game_path = tmp_dir.path();
1650
1651 create_ccc_file(
1652 &game_path.join("Fallout4.ccc"),
1653 &["ccBGSFO4001-PipBoy(Black).esl"],
1654 );
1655
1656 let ini_path = game_path.join("Fallout4.ini");
1657 std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp\n").unwrap();
1658
1659 copy_to_dir(
1660 "Blank.esp",
1661 &game_path.join("Data"),
1662 "Blank.esp",
1663 GameId::Fallout4,
1664 );
1665
1666 let settings = GameSettings::with_local_and_my_games_paths(
1667 GameId::Fallout4,
1668 game_path,
1669 &PathBuf::default(),
1670 game_path.to_path_buf(),
1671 )
1672 .unwrap();
1673
1674 assert_eq!(FALLOUT4_HARDCODED_PLUGINS, settings.early_loading_plugins());
1675 }
1676
1677 #[test]
1678 fn early_loading_plugins_should_not_include_cc_plugins_for_starfield_if_test_files_are_configured(
1679 ) {
1680 let tmp_dir = tempdir().unwrap();
1681 let game_path = tmp_dir.path();
1682 let my_games_path = tmp_dir.path().join("my games");
1683
1684 create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test.esp"]);
1685
1686 let ini_path = game_path.join("Starfield.ini");
1687 std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp\n").unwrap();
1688
1689 copy_to_dir(
1690 "Blank.esp",
1691 &game_path.join("Data"),
1692 "Blank.esp",
1693 GameId::Starfield,
1694 );
1695
1696 let settings = GameSettings::with_local_and_my_games_paths(
1697 GameId::Starfield,
1698 game_path,
1699 &PathBuf::default(),
1700 my_games_path,
1701 )
1702 .unwrap();
1703
1704 assert!(!settings.loads_early("test.esp"));
1705 }
1706
1707 #[test]
1708 fn early_loading_plugins_should_include_plugins_from_global_config_for_openmw() {
1709 let tmp_dir = tempdir().unwrap();
1710 let global_cfg_path = tmp_dir.path().join("openmw.cfg");
1711
1712 std::fs::write(
1713 &global_cfg_path,
1714 "config=local\ncontent=test.esm\ncontent=test.esp",
1715 )
1716 .unwrap();
1717
1718 let settings = game_with_game_path(GameId::OpenMW, tmp_dir.path());
1719
1720 let expected = &["builtin.omwscripts", "test.esm", "test.esp"];
1721
1722 assert_eq!(expected, settings.early_loading_plugins());
1723 }
1724
1725 #[test]
1726 fn early_loading_plugins_should_ignore_later_duplicate_entries() {
1727 let tmp_dir = tempdir().unwrap();
1728 let game_path = tmp_dir.path();
1729 let my_games_path = tmp_dir.path().join("my games");
1730
1731 create_ccc_file(
1732 &my_games_path.join("Starfield.ccc"),
1733 &["Starfield.esm", "test.esm"],
1734 );
1735
1736 let settings = GameSettings::with_local_and_my_games_paths(
1737 GameId::Starfield,
1738 game_path,
1739 &PathBuf::default(),
1740 my_games_path,
1741 )
1742 .unwrap();
1743
1744 let expected = &[
1745 "Starfield.esm",
1746 "Constellation.esm",
1747 "OldMars.esm",
1748 "ShatteredSpace.esm",
1749 "SFBGS003.esm",
1750 "SFBGS004.esm",
1751 "SFBGS006.esm",
1752 "SFBGS007.esm",
1753 "SFBGS008.esm",
1754 "test.esm",
1755 ];
1756 assert_eq!(expected, settings.early_loading_plugins());
1757 }
1758
1759 #[test]
1760 fn implicitly_active_plugins_should_include_early_loading_plugins() {
1761 let tmp_dir = tempdir().unwrap();
1762 let game_path = tmp_dir.path();
1763
1764 let settings = game_with_game_path(GameId::SkyrimSE, game_path);
1765
1766 assert_eq!(
1767 settings.early_loading_plugins(),
1768 settings.implicitly_active_plugins()
1769 );
1770 }
1771
1772 #[test]
1773 fn implicitly_active_plugins_should_include_test_files() {
1774 let tmp_dir = tempdir().unwrap();
1775 let game_path = tmp_dir.path();
1776
1777 let ini_path = game_path.join("Skyrim.ini");
1778 std::fs::write(&ini_path, "[General]\nsTestFile1=plugin.esp\n").unwrap();
1779
1780 let settings = GameSettings::with_local_and_my_games_paths(
1781 GameId::SkyrimSE,
1782 game_path,
1783 &PathBuf::default(),
1784 game_path.to_path_buf(),
1785 )
1786 .unwrap();
1787
1788 let mut expected_plugins = settings.early_loading_plugins().to_vec();
1789 expected_plugins.push("plugin.esp".to_owned());
1790
1791 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
1792 }
1793
1794 #[test]
1795 fn implicitly_active_plugins_should_only_include_valid_test_files_for_fallout4() {
1796 let tmp_dir = tempdir().unwrap();
1797 let game_path = tmp_dir.path();
1798
1799 let ini_path = game_path.join("Fallout4.ini");
1800 std::fs::write(
1801 &ini_path,
1802 "[General]\nsTestFile1=plugin.esp\nsTestFile2=Blank.esp",
1803 )
1804 .unwrap();
1805
1806 copy_to_dir(
1807 "Blank.esp",
1808 &game_path.join("Data"),
1809 "Blank.esp",
1810 GameId::Fallout4,
1811 );
1812
1813 let settings = GameSettings::with_local_and_my_games_paths(
1814 GameId::Fallout4,
1815 game_path,
1816 &PathBuf::default(),
1817 game_path.to_path_buf(),
1818 )
1819 .unwrap();
1820
1821 let mut expected_plugins = settings.early_loading_plugins().to_vec();
1822 expected_plugins.push("Blank.esp".to_owned());
1823
1824 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
1825 }
1826
1827 #[test]
1828 fn implicitly_active_plugins_should_only_include_valid_test_files_for_fallout4vr() {
1829 let tmp_dir = tempdir().unwrap();
1830 let game_path = tmp_dir.path();
1831
1832 let ini_path = game_path.join("Fallout4VR.ini");
1833 std::fs::write(
1834 &ini_path,
1835 "[General]\nsTestFile1=plugin.esp\nsTestFile2=Blank.esp",
1836 )
1837 .unwrap();
1838
1839 copy_to_dir(
1840 "Blank.esp",
1841 &game_path.join("Data"),
1842 "Blank.esp",
1843 GameId::Fallout4VR,
1844 );
1845
1846 let settings = GameSettings::with_local_and_my_games_paths(
1847 GameId::Fallout4VR,
1848 game_path,
1849 &PathBuf::default(),
1850 game_path.to_path_buf(),
1851 )
1852 .unwrap();
1853
1854 let mut expected_plugins = settings.early_loading_plugins().to_vec();
1855 expected_plugins.push("Blank.esp".to_owned());
1856
1857 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
1858 }
1859
1860 #[test]
1861 fn implicitly_active_plugins_should_include_plugins_with_nam_files_for_fallout_nv() {
1862 let tmp_dir = tempdir().unwrap();
1863 let game_path = tmp_dir.path();
1864 let data_path = game_path.join("Data");
1865
1866 create_dir_all(&data_path).unwrap();
1867 File::create(data_path.join("plugin1.nam")).unwrap();
1868 File::create(data_path.join("plugin2.NAM")).unwrap();
1869
1870 let settings = game_with_game_path(GameId::FalloutNV, game_path);
1871 let expected_plugins = vec!["plugin1.esm", "plugin1.esp", "plugin2.esm", "plugin2.esp"];
1872 let mut plugins = settings.implicitly_active_plugins().to_vec();
1873 plugins.sort();
1874
1875 assert_eq!(expected_plugins, plugins);
1876 }
1877
1878 #[test]
1879 fn implicitly_active_plugins_should_include_update_esm_for_skyrim() {
1880 let settings = game_with_generic_paths(GameId::Skyrim);
1881 let plugins = settings.implicitly_active_plugins();
1882
1883 assert!(plugins.contains(&"Update.esm".to_owned()));
1884 }
1885
1886 #[test]
1887 fn implicitly_active_plugins_should_include_blueprintships_starfield_esm_for_starfield() {
1888 let settings = game_with_generic_paths(GameId::Starfield);
1889 let plugins = settings.implicitly_active_plugins();
1890
1891 assert!(plugins.contains(&"BlueprintShips-Starfield.esm".to_owned()));
1892 }
1893
1894 #[test]
1895 fn implicitly_active_plugins_should_include_plugins_with_nam_files_for_games_other_than_fallout_nv(
1896 ) {
1897 let tmp_dir = tempdir().unwrap();
1898 let game_path = tmp_dir.path();
1899 let data_path = game_path.join("Data");
1900
1901 create_dir_all(&data_path).unwrap();
1902 File::create(data_path.join("plugin.nam")).unwrap();
1903
1904 let settings = game_with_game_path(GameId::Fallout3, game_path);
1905 assert!(settings.implicitly_active_plugins().is_empty());
1906 }
1907
1908 #[test]
1909 fn implicitly_active_plugins_should_not_include_case_insensitive_duplicates() {
1910 let tmp_dir = tempdir().unwrap();
1911 let game_path = tmp_dir.path();
1912
1913 let ini_path = game_path.join("Fallout4.ini");
1914 std::fs::write(&ini_path, "[General]\nsTestFile1=fallout4.esm\n").unwrap();
1915
1916 let settings = GameSettings::with_local_and_my_games_paths(
1917 GameId::Fallout4,
1918 game_path,
1919 &PathBuf::default(),
1920 game_path.to_path_buf(),
1921 )
1922 .unwrap();
1923
1924 assert_eq!(
1925 settings.early_loading_plugins(),
1926 settings.implicitly_active_plugins()
1927 );
1928 }
1929
1930 #[test]
1931 fn is_implicitly_active_should_return_true_iff_the_plugin_is_implicitly_active() {
1932 let settings = game_with_generic_paths(GameId::Skyrim);
1933 assert!(settings.is_implicitly_active("Update.esm"));
1934 assert!(!settings.is_implicitly_active("Test.esm"));
1935 }
1936
1937 #[test]
1938 fn is_implicitly_active_should_match_case_insensitively() {
1939 let settings = game_with_generic_paths(GameId::Skyrim);
1940 assert!(settings.is_implicitly_active("update.esm"));
1941 }
1942
1943 #[test]
1944 fn loads_early_should_return_true_iff_the_plugin_loads_early() {
1945 let settings = game_with_generic_paths(GameId::SkyrimSE);
1946 assert!(settings.loads_early("Dawnguard.esm"));
1947 assert!(!settings.loads_early("Test.esm"));
1948 }
1949
1950 #[test]
1951 fn loads_early_should_match_case_insensitively() {
1952 let settings = game_with_generic_paths(GameId::SkyrimSE);
1953 assert!(settings.loads_early("dawnguard.esm"));
1954 }
1955
1956 #[test]
1957 fn plugins_folder_should_be_a_child_of_the_game_path() {
1958 let settings = game_with_generic_paths(GameId::Skyrim);
1959 assert_eq!(Path::new("game/Data"), settings.plugins_directory());
1960 }
1961
1962 #[test]
1963 fn load_order_file_should_be_in_local_path_for_skyrim_and_none_for_other_games() {
1964 let mut settings = game_with_generic_paths(GameId::Skyrim);
1965 assert_eq!(
1966 Path::new("local/loadorder.txt"),
1967 settings.load_order_file().unwrap()
1968 );
1969
1970 settings = game_with_generic_paths(GameId::SkyrimSE);
1971 assert!(settings.load_order_file().is_none());
1972
1973 settings = game_with_generic_paths(GameId::OpenMW);
1974 assert!(settings.load_order_file().is_none());
1975
1976 settings = game_with_generic_paths(GameId::Morrowind);
1977 assert!(settings.load_order_file().is_none());
1978
1979 settings = game_with_generic_paths(GameId::Oblivion);
1980 assert!(settings.load_order_file().is_none());
1981
1982 settings = game_with_generic_paths(GameId::Fallout3);
1983 assert!(settings.load_order_file().is_none());
1984
1985 settings = game_with_generic_paths(GameId::FalloutNV);
1986 assert!(settings.load_order_file().is_none());
1987
1988 settings = game_with_generic_paths(GameId::Fallout4);
1989 assert!(settings.load_order_file().is_none());
1990 }
1991
1992 #[test]
1993 fn additional_plugins_directories_should_be_empty_if_game_is_not_fallout4_or_starfield() {
1994 let tmp_dir = tempdir().unwrap();
1995 let game_path = tmp_dir.path();
1996
1997 File::create(game_path.join("appxmanifest.xml")).unwrap();
1998
1999 let game_ids = [
2000 GameId::Morrowind,
2001 GameId::Oblivion,
2002 GameId::Skyrim,
2003 GameId::SkyrimSE,
2004 GameId::SkyrimVR,
2005 GameId::Fallout3,
2006 GameId::FalloutNV,
2007 ];
2008
2009 for game_id in game_ids {
2010 let settings = game_with_game_path(game_id, game_path);
2011
2012 assert!(settings.additional_plugins_directories().is_empty());
2013 }
2014 }
2015
2016 #[test]
2017 fn additional_plugins_directories_should_be_empty_if_fallout4_is_not_from_the_microsoft_store()
2018 {
2019 let settings = game_with_generic_paths(GameId::Fallout4);
2020
2021 assert!(settings.additional_plugins_directories().is_empty());
2022 }
2023
2024 #[test]
2025 fn additional_plugins_directories_should_not_be_empty_if_game_is_fallout4_from_the_microsoft_store(
2026 ) {
2027 let tmp_dir = tempdir().unwrap();
2028 let game_path = tmp_dir.path();
2029
2030 File::create(game_path.join("appxmanifest.xml")).unwrap();
2031
2032 let settings = game_with_game_path(GameId::Fallout4, game_path);
2033
2034 assert_eq!(
2035 vec![
2036 game_path.join(MS_FO4_AUTOMATRON_PATH),
2037 game_path.join(MS_FO4_NUKA_WORLD_PATH),
2038 game_path.join(MS_FO4_WASTELAND_PATH),
2039 game_path.join(MS_FO4_TEXTURE_PACK_PATH),
2040 game_path.join(MS_FO4_VAULT_TEC_PATH),
2041 game_path.join(MS_FO4_FAR_HARBOR_PATH),
2042 game_path.join(MS_FO4_CONTRAPTIONS_PATH),
2043 ],
2044 settings.additional_plugins_directories()
2045 );
2046 }
2047
2048 #[test]
2049 fn additional_plugins_directories_should_not_be_empty_if_game_is_starfield() {
2050 let settings = game_with_generic_paths(GameId::Starfield);
2051
2052 assert_eq!(
2053 vec![Path::new("my games").join("Data")],
2054 settings.additional_plugins_directories()
2055 );
2056 }
2057
2058 #[test]
2059 fn additional_plugins_directories_should_read_from_openmw_cfgs() {
2060 let tmp_dir = tempdir().unwrap();
2061 let game_path = tmp_dir.path().join("game");
2062 let my_games_path = tmp_dir.path().join("my games");
2063 let global_cfg_path = game_path.join("openmw.cfg");
2064 let cfg_path = my_games_path.join("openmw.cfg");
2065
2066 create_dir_all(global_cfg_path.parent().unwrap()).unwrap();
2067 std::fs::write(&global_cfg_path, "config=\"../my games\"\ndata=\"foo/bar\"").unwrap();
2068
2069 create_dir_all(cfg_path.parent().unwrap()).unwrap();
2070 std::fs::write(
2071 &cfg_path,
2072 "data=\"Path\\&&&\"&a&&&&\\Data Files\"\ndata=games/path",
2073 )
2074 .unwrap();
2075
2076 let settings =
2077 GameSettings::with_local_path(GameId::OpenMW, &game_path, &my_games_path).unwrap();
2078
2079 let expected: Vec<PathBuf> = vec![
2080 game_path.join("foo/bar"),
2081 my_games_path.join("Path\\&\"a&&\\Data Files"),
2082 my_games_path.join("games/path"),
2083 ];
2084 assert_eq!(expected, settings.additional_plugins_directories());
2085 }
2086
2087 #[test]
2088 fn plugin_path_should_append_plugin_name_to_additional_plugin_directory_if_that_path_exists() {
2089 let tmp_dir = tempdir().unwrap();
2090 let other_dir = tmp_dir.path().join("other");
2091
2092 let plugin_name = "external.esp";
2093 let expected_plugin_path = other_dir.join(plugin_name);
2094
2095 let mut settings = game_with_generic_paths(GameId::Fallout4);
2096 settings.additional_plugins_directories = vec![other_dir.clone()];
2097
2098 copy_to_dir("Blank.esp", &other_dir, plugin_name, GameId::Fallout4);
2099
2100 let plugin_path = settings.plugin_path(plugin_name);
2101
2102 assert_eq!(expected_plugin_path, plugin_path);
2103 }
2104
2105 #[test]
2106 fn plugin_path_should_append_plugin_name_to_additional_plugin_directory_if_the_ghosted_path_exists(
2107 ) {
2108 let tmp_dir = tempdir().unwrap();
2109 let other_dir = tmp_dir.path().join("other");
2110
2111 let plugin_name = "external.esp";
2112 let ghosted_plugin_name = "external.esp.ghost";
2113 let expected_plugin_path = other_dir.join(ghosted_plugin_name);
2114
2115 let mut settings = game_with_generic_paths(GameId::Fallout4);
2116 settings.additional_plugins_directories = vec![other_dir.clone()];
2117
2118 copy_to_dir(
2119 "Blank.esp",
2120 &other_dir,
2121 ghosted_plugin_name,
2122 GameId::Fallout4,
2123 );
2124
2125 let plugin_path = settings.plugin_path(plugin_name);
2126
2127 assert_eq!(expected_plugin_path, plugin_path);
2128 }
2129
2130 #[test]
2131 fn plugin_path_should_not_resolve_ghosted_paths_for_openmw() {
2132 let tmp_dir = tempdir().unwrap();
2133 let game_path = tmp_dir.path().join("game");
2134 let other_dir = tmp_dir.path().join("other");
2135
2136 let plugin_name = "external.esp";
2137
2138 let mut settings = game_with_game_path(GameId::OpenMW, &game_path);
2139 settings.additional_plugins_directories = vec![other_dir.clone()];
2140
2141 copy_to_dir(
2142 "Blank.esp",
2143 &other_dir,
2144 "external.esp.ghost",
2145 GameId::OpenMW,
2146 );
2147
2148 let plugin_path = settings.plugin_path(plugin_name);
2149
2150 assert_eq!(
2151 game_path.join("resources/vfs").join(plugin_name),
2152 plugin_path
2153 );
2154 }
2155
2156 #[test]
2157 fn plugin_path_should_return_the_last_directory_that_contains_a_file_for_openmw() {
2158 let tmp_dir = tempdir().unwrap();
2159 let other_dir_1 = tmp_dir.path().join("other1");
2160 let other_dir_2 = tmp_dir.path().join("other2");
2161
2162 let plugin_name = "Blank.esp";
2163
2164 let mut settings = game_with_game_path(GameId::OpenMW, tmp_dir.path());
2165 settings.additional_plugins_directories = vec![other_dir_1.clone(), other_dir_2.clone()];
2166
2167 copy_to_dir("Blank.esp", &other_dir_1, plugin_name, GameId::OpenMW);
2168 copy_to_dir("Blank.esp", &other_dir_2, plugin_name, GameId::OpenMW);
2169
2170 let plugin_path = settings.plugin_path(plugin_name);
2171
2172 assert_eq!(other_dir_2.join(plugin_name), plugin_path);
2173 }
2174
2175 #[test]
2176 fn plugin_path_should_return_plugins_dir_subpath_if_name_does_not_match_any_external_plugin() {
2177 let settings = game_with_generic_paths(GameId::Fallout4);
2178
2179 let plugin_name = "DLCCoast.esm";
2180 assert_eq!(
2181 settings.plugins_directory().join(plugin_name),
2182 settings.plugin_path(plugin_name)
2183 );
2184 }
2185
2186 #[test]
2187 fn plugin_path_should_only_resolve_additional_starfield_plugin_paths_if_they_exist_or_are_ghosted_in_the_plugins_directory(
2188 ) {
2189 let tmp_dir = tempdir().unwrap();
2190 let game_path = tmp_dir.path().join("game");
2191 let data_path = game_path.join("Data");
2192 let other_dir = tmp_dir.path().join("other");
2193
2194 let plugin_name_1 = "external1.esp";
2195 let plugin_name_2 = "external2.esp";
2196 let plugin_name_3 = "external3.esp";
2197 let ghosted_plugin_name_3 = "external3.esp.ghost";
2198
2199 let mut settings = game_with_game_path(GameId::Starfield, &game_path);
2200 settings.additional_plugins_directories = vec![other_dir.clone()];
2201
2202 copy_to_dir("Blank.esp", &other_dir, plugin_name_1, GameId::Starfield);
2203 copy_to_dir("Blank.esp", &other_dir, plugin_name_2, GameId::Starfield);
2204 copy_to_dir("Blank.esp", &data_path, plugin_name_2, GameId::Starfield);
2205 copy_to_dir("Blank.esp", &other_dir, plugin_name_3, GameId::Starfield);
2206 copy_to_dir(
2207 "Blank.esp",
2208 &data_path,
2209 ghosted_plugin_name_3,
2210 GameId::Starfield,
2211 );
2212
2213 let plugin_1_path = settings.plugin_path(plugin_name_1);
2214 let plugin_2_path = settings.plugin_path(plugin_name_2);
2215 let plugin_3_path = settings.plugin_path(plugin_name_3);
2216
2217 assert_eq!(data_path.join(plugin_name_1), plugin_1_path);
2218 assert_eq!(other_dir.join(plugin_name_2), plugin_2_path);
2219 assert_eq!(other_dir.join(plugin_name_3), plugin_3_path);
2220 }
2221
2222 #[test]
2223 fn refresh_implicitly_active_plugins_should_update_early_loading_and_implicitly_active_plugins()
2224 {
2225 let tmp_dir = tempdir().unwrap();
2226 let game_path = tmp_dir.path();
2227
2228 let mut settings = GameSettings::with_local_and_my_games_paths(
2229 GameId::SkyrimSE,
2230 game_path,
2231 &PathBuf::default(),
2232 game_path.to_path_buf(),
2233 )
2234 .unwrap();
2235
2236 let hardcoded_plugins = vec![
2237 "Skyrim.esm",
2238 "Update.esm",
2239 "Dawnguard.esm",
2240 "HearthFires.esm",
2241 "Dragonborn.esm",
2242 ];
2243 assert_eq!(hardcoded_plugins, settings.early_loading_plugins());
2244 assert_eq!(hardcoded_plugins, settings.implicitly_active_plugins());
2245
2246 std::fs::write(game_path.join("Skyrim.ccc"), "ccBGSSSE002-ExoticArrows.esl").unwrap();
2247 std::fs::write(
2248 game_path.join("Skyrim.ini"),
2249 "[General]\nsTestFile1=plugin.esp\n",
2250 )
2251 .unwrap();
2252
2253 settings.refresh_implicitly_active_plugins().unwrap();
2254
2255 let mut expected_plugins = hardcoded_plugins;
2256 expected_plugins.push("ccBGSSSE002-ExoticArrows.esl");
2257 assert_eq!(expected_plugins, settings.early_loading_plugins());
2258
2259 expected_plugins.push("plugin.esp");
2260 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
2261 }
2262
2263 #[test]
2264 fn find_plugins_in_directories_should_sort_files_by_modification_timestamp() {
2265 let tmp_dir = tempdir().unwrap();
2266 let game_path = tmp_dir.path();
2267
2268 let plugin_names = [
2269 "Blank.esp",
2270 "Blank - Different.esp",
2271 "Blank - Master Dependent.esp",
2272 NON_ASCII,
2273 ];
2274
2275 copy_to_dir("Blank.esp", game_path, NON_ASCII, GameId::Oblivion);
2276
2277 for (i, plugin_name) in plugin_names.iter().enumerate() {
2278 let path = game_path.join(plugin_name);
2279 if !path.exists() {
2280 copy_to_dir(plugin_name, game_path, plugin_name, GameId::Oblivion);
2281 }
2282 set_file_timestamps(&path, i.try_into().unwrap());
2283 }
2284
2285 let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Oblivion);
2286
2287 let expected: Vec<_> = plugin_names.iter().map(|n| game_path.join(n)).collect();
2288
2289 assert_eq!(expected, result);
2290 }
2291
2292 #[test]
2293 fn find_plugins_in_directories_should_sort_files_by_descending_filename_if_timestamps_are_equal(
2294 ) {
2295 let tmp_dir = tempdir().unwrap();
2296 let game_path = tmp_dir.path();
2297
2298 let non_ascii = NON_ASCII;
2299 let plugin_names = [
2300 "Blank.esm",
2301 "Blank.esp",
2302 "Blank - Different.esp",
2303 "Blank - Master Dependent.esp",
2304 non_ascii,
2305 ];
2306
2307 copy_to_dir("Blank.esp", game_path, non_ascii, GameId::Oblivion);
2308
2309 for (i, plugin_name) in plugin_names.iter().enumerate() {
2310 let path = game_path.join(plugin_name);
2311 if !path.exists() {
2312 copy_to_dir(plugin_name, game_path, plugin_name, GameId::Oblivion);
2313 }
2314 set_file_timestamps(&path, i.try_into().unwrap());
2315 }
2316
2317 let timestamp = 3;
2318 set_file_timestamps(&game_path.join("Blank - Different.esp"), timestamp);
2319 set_file_timestamps(&game_path.join("Blank - Master Dependent.esp"), timestamp);
2320
2321 let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Oblivion);
2322
2323 let plugin_paths = vec![
2324 game_path.join("Blank.esm"),
2325 game_path.join("Blank.esp"),
2326 game_path.join("Blank - Master Dependent.esp"),
2327 game_path.join("Blank - Different.esp"),
2328 game_path.join(non_ascii),
2329 ];
2330
2331 assert_eq!(plugin_paths, result);
2332 }
2333
2334 #[test]
2335 fn find_plugins_in_directories_should_sort_files_by_ascending_filename_if_timestamps_are_equal_and_game_is_starfield(
2336 ) {
2337 let tmp_dir = tempdir().unwrap();
2338 let game_path = tmp_dir.path();
2339
2340 let plugin_names = [
2341 "Blank.full.esm",
2342 "Blank.small.esm",
2343 "Blank.medium.esm",
2344 "Blank.esp",
2345 "Blank - Override.esp",
2346 ];
2347
2348 let timestamp = 1_321_009_991;
2349
2350 for plugin_name in &plugin_names {
2351 let path = game_path.join(plugin_name);
2352 if !path.exists() {
2353 copy_to_dir(plugin_name, game_path, plugin_name, GameId::Starfield);
2354 }
2355 set_file_timestamps(&path, timestamp);
2356 }
2357
2358 let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Starfield);
2359
2360 let plugin_paths = vec![
2361 game_path.join("Blank - Override.esp"),
2362 game_path.join("Blank.esp"),
2363 game_path.join("Blank.full.esm"),
2364 game_path.join("Blank.medium.esm"),
2365 game_path.join("Blank.small.esm"),
2366 ];
2367
2368 assert_eq!(plugin_paths, result);
2369 }
2370}