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