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