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