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