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 .map(|f| keep_file_type(f, game_id))
688 .unwrap_or(false)
689}
690
691#[cfg(unix)]
692#[expect(
693 clippy::filetype_is_file,
694 reason = "Only files and symlinks are supported"
695)]
696fn keep_file_type(f: FileType, _game_id: GameId) -> bool {
697 f.is_file() || f.is_symlink()
698}
699
700#[cfg(windows)]
701#[expect(
702 clippy::filetype_is_file,
703 reason = "Only files and sometimes symlinks are supported"
704)]
705fn keep_file_type(f: FileType, game_id: GameId) -> bool {
706 if matches!(game_id, GameId::OblivionRemastered | GameId::OpenMW) {
707 f.is_file() || f.is_symlink()
708 } else {
709 f.is_file()
710 }
711}
712
713fn early_loading_plugins(
714 game_id: GameId,
715 game_path: &Path,
716 my_games_path: &Path,
717 has_test_files: bool,
718) -> Result<Vec<String>, Error> {
719 let mut plugin_names: Vec<String> = hardcoded_plugins(game_id)
720 .iter()
721 .map(|s| (*s).to_owned())
722 .collect();
723
724 if matches!(game_id, GameId::Fallout4 | GameId::Starfield) && has_test_files {
725 return Ok(plugin_names);
728 }
729
730 for file_path in ccc_file_paths(game_id, game_path, my_games_path) {
731 if file_path.exists() {
732 let reader =
733 BufReader::new(File::open(&file_path).map_err(|e| Error::IoError(file_path, e))?);
734
735 let lines = reader
736 .lines()
737 .filter_map(|line| line.ok().filter(|l| !l.is_empty()));
738
739 plugin_names.extend(lines);
740 break;
741 }
742 }
743
744 if game_id == GameId::OpenMW {
745 plugin_names.extend(openmw_config::non_user_active_plugin_names(game_path)?);
746 }
747
748 deduplicate(&mut plugin_names);
749
750 Ok(plugin_names)
751}
752
753fn implicitly_active_plugins(
754 game_id: GameId,
755 game_path: &Path,
756 early_loading_plugins: &[String],
757 test_files: &[String],
758) -> Result<Vec<String>, Error> {
759 let mut plugin_names = Vec::new();
760
761 plugin_names.extend_from_slice(early_loading_plugins);
762 plugin_names.extend_from_slice(test_files);
763
764 if game_id == GameId::FalloutNV {
765 let nam_plugins = find_nam_plugins(&game_path.join("Data"))?;
769
770 plugin_names.extend(nam_plugins);
771 } else if game_id == GameId::Skyrim {
772 plugin_names.push("Update.esm".to_owned());
775 }
776
777 deduplicate(&mut plugin_names);
778
779 Ok(plugin_names)
780}
781
782fn deduplicate(plugin_names: &mut Vec<String>) {
784 let mut set = std::collections::HashSet::new();
785 plugin_names.retain(|e| set.insert(unicase::UniCase::new(e.clone())));
786}
787
788fn find_map_path(directory: &Path, plugin_name: &str, game_id: GameId) -> Option<PathBuf> {
789 if game_id.allow_plugin_ghosting() {
790 use crate::ghostable_path::GhostablePath;
792
793 directory.join(plugin_name).resolve_path().ok()
794 } else {
795 let path = directory.join(plugin_name);
796 path.exists().then_some(path)
797 }
798}
799
800fn pick_plugin_path<'a>(
801 game_id: GameId,
802 plugin_name: &str,
803 plugins_directory: &Path,
804 mut dir_iter: impl Iterator<Item = &'a PathBuf>,
805) -> PathBuf {
806 dir_iter
807 .find_map(|d| find_map_path(d, plugin_name, game_id))
808 .unwrap_or_else(|| plugins_directory.join(plugin_name))
809}
810
811fn plugin_path(
812 game_id: GameId,
813 plugin_name: &str,
814 plugins_directory: &Path,
815 additional_plugins_directories: &[PathBuf],
816) -> PathBuf {
817 if game_id == GameId::Starfield {
824 use crate::ghostable_path::GhostablePath;
826
827 let path = plugins_directory.join(plugin_name);
828 if path.resolve_path().is_err() {
829 return path;
830 }
831 }
832
833 match game_id {
836 GameId::OpenMW => pick_plugin_path(
837 game_id,
838 plugin_name,
839 plugins_directory,
840 additional_plugins_directories.iter().rev(),
841 ),
842 _ => pick_plugin_path(
843 game_id,
844 plugin_name,
845 plugins_directory,
846 additional_plugins_directories.iter(),
847 ),
848 }
849}
850
851fn sort_plugins_dir_entries(a: &DirEntry, b: &DirEntry) -> Ordering {
852 let m_a = get_target_modified_timestamp(a);
855 let m_b = get_target_modified_timestamp(b);
856
857 match m_a.cmp(&m_b) {
858 Ordering::Equal => compare_uppercased_filenames(&a.file_name(), &b.file_name()).reverse(),
859 x => x,
860 }
861}
862
863fn compare_uppercased_filenames(a: &OsStr, b: &OsStr) -> Ordering {
864 match (a.to_str(), b.to_str()) {
865 (Some(a), Some(b)) => a.to_uppercase().cmp(&b.to_uppercase()),
866 _ => a.cmp(b),
870 }
871}
872
873fn get_target_modified_timestamp(entry: &DirEntry) -> Option<SystemTime> {
874 let metadata = if entry.file_type().is_ok_and(|f| f.is_symlink()) {
875 entry.path().metadata()
876 } else {
877 entry.metadata()
878 };
879
880 metadata.and_then(|m| m.modified()).ok()
881}
882
883fn sort_plugins_dir_entries_openmw(a: &DirEntry, b: &DirEntry) -> Ordering {
884 if a.path().parent() == b.path().parent() {
887 a.file_name().cmp(&b.file_name())
888 } else {
889 Ordering::Equal
890 }
891}
892
893fn find_plugins_in_directories<'a>(
894 directories_iter: impl Iterator<Item = &'a PathBuf>,
895 game_id: GameId,
896) -> Vec<PathBuf> {
897 let mut dir_entries: Vec<_> = directories_iter
898 .flat_map(read_dir)
899 .flatten()
900 .filter_map(Result::ok)
901 .filter(|e| is_file_type_supported(e, game_id))
902 .filter(|e| {
903 e.file_name()
904 .to_str()
905 .is_some_and(|f| has_plugin_extension(f, game_id))
906 })
907 .collect();
908
909 let compare = match game_id {
910 GameId::OpenMW => sort_plugins_dir_entries_openmw,
911 _ => sort_plugins_dir_entries,
912 };
913
914 dir_entries.sort_by(compare);
915
916 dir_entries.into_iter().map(|e| e.path()).collect()
917}
918
919#[cfg(test)]
920mod tests {
921 #[cfg(windows)]
922 use std::env;
923 use std::{fs::create_dir_all, io::Write};
924 use tempfile::tempdir;
925
926 use crate::tests::{copy_to_dir, set_file_timestamps, symlink_file, NON_ASCII};
927
928 use super::*;
929
930 fn game_with_generic_paths(game_id: GameId) -> GameSettings {
931 GameSettings::with_local_and_my_games_paths(
932 game_id,
933 &PathBuf::from("game"),
934 &PathBuf::from("local"),
935 PathBuf::from("my games"),
936 )
937 .unwrap()
938 }
939
940 fn game_with_game_path(game_id: GameId, game_path: &Path) -> GameSettings {
941 GameSettings::with_local_and_my_games_paths(
942 game_id,
943 game_path,
944 &PathBuf::default(),
945 PathBuf::default(),
946 )
947 .unwrap()
948 }
949
950 fn game_with_ccc_plugins(
951 game_id: GameId,
952 game_path: &Path,
953 plugin_names: &[&str],
954 ) -> GameSettings {
955 let ccc_path = &ccc_file_paths(game_id, game_path, &PathBuf::new())[0];
956 create_ccc_file(ccc_path, plugin_names);
957
958 game_with_game_path(game_id, game_path)
959 }
960
961 fn create_ccc_file(path: &Path, plugin_names: &[&str]) {
962 create_dir_all(path.parent().unwrap()).unwrap();
963
964 let mut file = File::create(path).unwrap();
965
966 for plugin_name in plugin_names {
967 writeln!(file, "{plugin_name}").unwrap();
968 }
969 }
970
971 fn generate_file_file_type() -> FileType {
972 let tmp_dir = tempdir().unwrap();
973 let file_path = tmp_dir.path().join("file");
974
975 File::create(&file_path).unwrap();
976
977 let file_file_type = file_path.metadata().unwrap().file_type();
978
979 assert!(file_file_type.is_file());
980
981 file_file_type
982 }
983
984 fn generate_symlink_file_type() -> FileType {
985 let tmp_dir = tempdir().unwrap();
986 let file_path = tmp_dir.path().join("file");
987 let symlink_path = tmp_dir.path().join("symlink");
988
989 File::create(&file_path).unwrap();
990 symlink_file(&file_path, &symlink_path);
991
992 let symlink_file_type = symlink_path.symlink_metadata().unwrap().file_type();
993
994 assert!(symlink_file_type.is_symlink());
995
996 symlink_file_type
997 }
998
999 #[test]
1000 #[cfg(windows)]
1001 fn new_should_determine_correct_local_path_on_windows() {
1002 let settings = GameSettings::new(GameId::Skyrim, Path::new("game")).unwrap();
1003 let local_app_data = env::var("LOCALAPPDATA").unwrap();
1004 let local_app_data_path = Path::new(&local_app_data);
1005
1006 assert_eq!(
1007 local_app_data_path.join("Skyrim").join("Plugins.txt"),
1008 *settings.active_plugins_file()
1009 );
1010 assert_eq!(
1011 &local_app_data_path.join("Skyrim").join("loadorder.txt"),
1012 *settings.load_order_file().as_ref().unwrap()
1013 );
1014 }
1015
1016 #[test]
1017 #[cfg(windows)]
1018 fn new_should_determine_correct_local_path_for_openmw() {
1019 let tmp_dir = tempdir().unwrap();
1020 let global_cfg_path = tmp_dir.path().join("openmw.cfg");
1021
1022 std::fs::write(&global_cfg_path, "config=local").unwrap();
1023
1024 let settings = GameSettings::new(GameId::OpenMW, tmp_dir.path()).unwrap();
1025
1026 assert_eq!(
1027 &tmp_dir.path().join("local/openmw.cfg"),
1028 settings.active_plugins_file()
1029 );
1030 assert_eq!(tmp_dir.path().join("local"), settings.my_games_path);
1031 }
1032
1033 #[test]
1034 fn new_should_use_an_empty_local_path_for_morrowind() {
1035 let settings = GameSettings::new(GameId::Morrowind, Path::new("game")).unwrap();
1036
1037 assert_eq!(PathBuf::new(), settings.my_games_path);
1038 }
1039
1040 #[test]
1041 #[cfg(not(windows))]
1042 fn new_should_determine_correct_local_path_for_openmw_on_linux() {
1043 let config_path = Path::new("/etc/openmw");
1044
1045 let settings = GameSettings::new(GameId::OpenMW, Path::new("game")).unwrap();
1046
1047 assert_eq!(
1048 &config_path.join("openmw.cfg"),
1049 settings.active_plugins_file()
1050 );
1051 assert_eq!(config_path, settings.my_games_path);
1052 }
1053
1054 #[test]
1055 fn id_should_be_the_id_the_struct_was_created_with() {
1056 let settings = game_with_generic_paths(GameId::Morrowind);
1057 assert_eq!(GameId::Morrowind, settings.id());
1058 }
1059
1060 #[test]
1061 fn supports_blueprint_ships_plugins_should_be_true_for_starfield_only() {
1062 let mut settings = game_with_generic_paths(GameId::Morrowind);
1063 assert!(!settings.supports_blueprint_ships_plugins());
1064
1065 settings = game_with_generic_paths(GameId::OpenMW);
1066 assert!(!settings.supports_blueprint_ships_plugins());
1067
1068 settings = game_with_generic_paths(GameId::Oblivion);
1069 assert!(!settings.supports_blueprint_ships_plugins());
1070
1071 settings = game_with_generic_paths(GameId::OblivionRemastered);
1072 assert!(!settings.supports_blueprint_ships_plugins());
1073
1074 settings = game_with_generic_paths(GameId::Skyrim);
1075 assert!(!settings.supports_blueprint_ships_plugins());
1076
1077 settings = game_with_generic_paths(GameId::SkyrimSE);
1078 assert!(!settings.supports_blueprint_ships_plugins());
1079
1080 settings = game_with_generic_paths(GameId::SkyrimVR);
1081 assert!(!settings.supports_blueprint_ships_plugins());
1082
1083 settings = game_with_generic_paths(GameId::Fallout3);
1084 assert!(!settings.supports_blueprint_ships_plugins());
1085
1086 settings = game_with_generic_paths(GameId::FalloutNV);
1087 assert!(!settings.supports_blueprint_ships_plugins());
1088
1089 settings = game_with_generic_paths(GameId::Fallout4);
1090 assert!(!settings.supports_blueprint_ships_plugins());
1091
1092 settings = game_with_generic_paths(GameId::Fallout4VR);
1093 assert!(!settings.supports_blueprint_ships_plugins());
1094
1095 settings = game_with_generic_paths(GameId::Starfield);
1096 assert!(settings.supports_blueprint_ships_plugins());
1097 }
1098
1099 #[test]
1100 fn load_order_method_should_be_timestamp_for_tes3_tes4_fo3_and_fonv() {
1101 let mut settings = game_with_generic_paths(GameId::Morrowind);
1102 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
1103
1104 settings = game_with_generic_paths(GameId::Oblivion);
1105 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
1106
1107 settings = game_with_generic_paths(GameId::Fallout3);
1108 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
1109
1110 settings = game_with_generic_paths(GameId::FalloutNV);
1111 assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
1112 }
1113
1114 #[test]
1115 fn load_order_method_should_be_textfile_for_tes5() {
1116 let settings = game_with_generic_paths(GameId::Skyrim);
1117 assert_eq!(LoadOrderMethod::Textfile, settings.load_order_method());
1118 }
1119
1120 #[test]
1121 fn load_order_method_should_be_asterisk_for_tes5se_tes5vr_fo4_fo4vr_and_starfield() {
1122 let mut settings = game_with_generic_paths(GameId::SkyrimSE);
1123 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
1124
1125 settings = game_with_generic_paths(GameId::SkyrimVR);
1126 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
1127
1128 settings = game_with_generic_paths(GameId::Fallout4);
1129 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
1130
1131 settings = game_with_generic_paths(GameId::Fallout4VR);
1132 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
1133
1134 settings = game_with_generic_paths(GameId::Starfield);
1135 assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
1136 }
1137
1138 #[test]
1139 fn load_order_method_should_be_openmw_for_openmw() {
1140 let settings = game_with_generic_paths(GameId::OpenMW);
1141
1142 assert_eq!(LoadOrderMethod::OpenMW, settings.load_order_method());
1143 }
1144
1145 #[test]
1146 #[expect(deprecated)]
1147 fn master_file_should_be_mapped_from_game_id() {
1148 let mut settings = game_with_generic_paths(GameId::OpenMW);
1149 assert_eq!("Morrowind.esm", settings.master_file());
1150
1151 settings = game_with_generic_paths(GameId::Morrowind);
1152 assert_eq!("Morrowind.esm", settings.master_file());
1153
1154 settings = game_with_generic_paths(GameId::Oblivion);
1155 assert_eq!("Oblivion.esm", settings.master_file());
1156
1157 settings = game_with_generic_paths(GameId::Skyrim);
1158 assert_eq!("Skyrim.esm", settings.master_file());
1159
1160 settings = game_with_generic_paths(GameId::SkyrimSE);
1161 assert_eq!("Skyrim.esm", settings.master_file());
1162
1163 settings = game_with_generic_paths(GameId::SkyrimVR);
1164 assert_eq!("Skyrim.esm", settings.master_file());
1165
1166 settings = game_with_generic_paths(GameId::Fallout3);
1167 assert_eq!("Fallout3.esm", settings.master_file());
1168
1169 settings = game_with_generic_paths(GameId::FalloutNV);
1170 assert_eq!("FalloutNV.esm", settings.master_file());
1171
1172 settings = game_with_generic_paths(GameId::Fallout4);
1173 assert_eq!("Fallout4.esm", settings.master_file());
1174
1175 settings = game_with_generic_paths(GameId::Fallout4VR);
1176 assert_eq!("Fallout4.esm", settings.master_file());
1177
1178 settings = game_with_generic_paths(GameId::Starfield);
1179 assert_eq!("Starfield.esm", settings.master_file());
1180 }
1181
1182 #[test]
1183 fn appdata_folder_name_should_be_mapped_from_game_id() {
1184 let game_path = Path::new("");
1186
1187 assert!(appdata_folder_name(GameId::OpenMW, game_path).is_none());
1188
1189 assert!(appdata_folder_name(GameId::Morrowind, game_path).is_none());
1190
1191 let mut folder = appdata_folder_name(GameId::Oblivion, game_path).unwrap();
1192 assert_eq!("Oblivion", folder);
1193
1194 folder = appdata_folder_name(GameId::Skyrim, game_path).unwrap();
1195 assert_eq!("Skyrim", folder);
1196
1197 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1198 assert_eq!("Skyrim Special Edition", folder);
1199
1200 folder = appdata_folder_name(GameId::SkyrimVR, game_path).unwrap();
1201 assert_eq!("Skyrim VR", folder);
1202
1203 folder = appdata_folder_name(GameId::Fallout3, game_path).unwrap();
1204 assert_eq!("Fallout3", folder);
1205
1206 folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
1207 assert_eq!("FalloutNV", folder);
1208
1209 folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1210 assert_eq!("Fallout4", folder);
1211
1212 folder = appdata_folder_name(GameId::Fallout4VR, game_path).unwrap();
1213 assert_eq!("Fallout4VR", folder);
1214
1215 folder = appdata_folder_name(GameId::Starfield, game_path).unwrap();
1216 assert_eq!("Starfield", folder);
1217 }
1218
1219 #[test]
1220 fn appdata_folder_name_for_skyrim_se_should_have_gog_suffix_if_galaxy_dll_is_in_game_path() {
1221 let tmp_dir = tempdir().unwrap();
1222 let game_path = tmp_dir.path();
1223
1224 let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1225 assert_eq!("Skyrim Special Edition", folder);
1226
1227 let dll_path = game_path.join("Galaxy64.dll");
1228 File::create(&dll_path).unwrap();
1229
1230 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1231 assert_eq!("Skyrim Special Edition GOG", folder);
1232 }
1233
1234 #[test]
1235 fn appdata_folder_name_for_skyrim_se_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
1236 let tmp_dir = tempdir().unwrap();
1237 let game_path = tmp_dir.path();
1238
1239 let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1240 assert_eq!("Skyrim Special Edition", folder);
1241
1242 let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
1243 File::create(&dll_path).unwrap();
1244
1245 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1246 assert_eq!("Skyrim Special Edition EPIC", folder);
1247 }
1248
1249 #[test]
1250 fn appdata_folder_name_for_skyrim_se_should_have_ms_suffix_if_appxmanifest_xml_is_in_game_path()
1251 {
1252 let tmp_dir = tempdir().unwrap();
1253 let game_path = tmp_dir.path();
1254
1255 let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1256 assert_eq!("Skyrim Special Edition", folder);
1257
1258 let dll_path = game_path.join("appxmanifest.xml");
1259 File::create(&dll_path).unwrap();
1260
1261 folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1262 assert_eq!("Skyrim Special Edition MS", folder);
1263 }
1264
1265 #[test]
1266 fn appdata_folder_name_for_skyrim_se_prefers_gog_suffix_over_epic_suffix() {
1267 let tmp_dir = tempdir().unwrap();
1268 let game_path = tmp_dir.path();
1269
1270 let dll_path = game_path.join("Galaxy64.dll");
1271 File::create(&dll_path).unwrap();
1272
1273 let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
1274 File::create(&dll_path).unwrap();
1275
1276 let folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
1277 assert_eq!("Skyrim Special Edition GOG", folder);
1278 }
1279
1280 #[test]
1281 fn appdata_folder_name_for_fallout_nv_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
1282 let tmp_dir = tempdir().unwrap();
1283 let game_path = tmp_dir.path();
1284
1285 let mut folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
1286 assert_eq!("FalloutNV", folder);
1287
1288 let dll_path = game_path.join("EOSSDK-Win32-Shipping.dll");
1289 File::create(&dll_path).unwrap();
1290
1291 folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
1292 assert_eq!("FalloutNV_Epic", folder);
1293 }
1294
1295 #[test]
1296 fn appdata_folder_name_for_fallout4_should_have_ms_suffix_if_appxmanifest_xml_is_in_game_path()
1297 {
1298 let tmp_dir = tempdir().unwrap();
1299 let game_path = tmp_dir.path();
1300
1301 let mut folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1302 assert_eq!("Fallout4", folder);
1303
1304 let dll_path = game_path.join("appxmanifest.xml");
1305 File::create(&dll_path).unwrap();
1306
1307 folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1308 assert_eq!("Fallout4 MS", folder);
1309 }
1310
1311 #[test]
1312 fn appdata_folder_name_for_fallout4_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
1313 let tmp_dir = tempdir().unwrap();
1314 let game_path = tmp_dir.path();
1315
1316 let mut folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1317 assert_eq!("Fallout4", folder);
1318
1319 let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
1320 File::create(&dll_path).unwrap();
1321
1322 folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
1323 assert_eq!("Fallout4 EPIC", folder);
1324 }
1325
1326 #[test]
1327 #[cfg(windows)]
1328 fn my_games_path_should_be_in_documents_path_on_windows() {
1329 let empty_path = Path::new("");
1330 let parent_path = dirs::document_dir().unwrap().join("My Games");
1331
1332 let path = my_games_path(GameId::Morrowind, empty_path, empty_path).unwrap();
1333 assert!(path.is_none());
1334
1335 let path = my_games_path(GameId::Oblivion, empty_path, empty_path)
1336 .unwrap()
1337 .unwrap();
1338 assert_eq!(parent_path.join("Oblivion"), path);
1339
1340 let path = my_games_path(GameId::Skyrim, empty_path, empty_path)
1341 .unwrap()
1342 .unwrap();
1343 assert_eq!(parent_path.join("Skyrim"), path);
1344
1345 let path = my_games_path(GameId::SkyrimSE, empty_path, empty_path)
1346 .unwrap()
1347 .unwrap();
1348 assert_eq!(parent_path.join("Skyrim Special Edition"), path);
1349
1350 let path = my_games_path(GameId::SkyrimVR, empty_path, empty_path)
1351 .unwrap()
1352 .unwrap();
1353 assert_eq!(parent_path.join("Skyrim VR"), path);
1354
1355 let path = my_games_path(GameId::Fallout3, empty_path, empty_path)
1356 .unwrap()
1357 .unwrap();
1358 assert_eq!(parent_path.join("Fallout3"), path);
1359
1360 let path = my_games_path(GameId::FalloutNV, empty_path, empty_path)
1361 .unwrap()
1362 .unwrap();
1363 assert_eq!(parent_path.join("FalloutNV"), path);
1364
1365 let path = my_games_path(GameId::Fallout4, empty_path, empty_path)
1366 .unwrap()
1367 .unwrap();
1368 assert_eq!(parent_path.join("Fallout4"), path);
1369
1370 let path = my_games_path(GameId::Fallout4VR, empty_path, empty_path)
1371 .unwrap()
1372 .unwrap();
1373 assert_eq!(parent_path.join("Fallout4VR"), path);
1374
1375 let path = my_games_path(GameId::Starfield, empty_path, empty_path)
1376 .unwrap()
1377 .unwrap();
1378 assert_eq!(parent_path.join("Starfield"), path);
1379 }
1380
1381 #[test]
1382 #[cfg(not(windows))]
1383 fn my_games_path_should_be_relative_to_local_path_on_linux() {
1384 let empty_path = Path::new("");
1385 let local_path = Path::new("wineprefix/drive_c/Users/user/AppData/Local/Game");
1386 let parent_path = Path::new("wineprefix/drive_c/Users/user/Documents/My Games");
1387
1388 let path = my_games_path(GameId::Morrowind, empty_path, local_path).unwrap();
1389 assert!(path.is_none());
1390
1391 let path = my_games_path(GameId::Oblivion, empty_path, local_path)
1392 .unwrap()
1393 .unwrap();
1394 assert_eq!(parent_path.join("Oblivion"), path);
1395
1396 let path = my_games_path(GameId::Skyrim, empty_path, local_path)
1397 .unwrap()
1398 .unwrap();
1399 assert_eq!(parent_path.join("Skyrim"), path);
1400
1401 let path = my_games_path(GameId::SkyrimSE, empty_path, local_path)
1402 .unwrap()
1403 .unwrap();
1404 assert_eq!(parent_path.join("Skyrim Special Edition"), path);
1405
1406 let path = my_games_path(GameId::SkyrimVR, empty_path, local_path)
1407 .unwrap()
1408 .unwrap();
1409 assert_eq!(parent_path.join("Skyrim VR"), path);
1410
1411 let path = my_games_path(GameId::Fallout3, empty_path, local_path)
1412 .unwrap()
1413 .unwrap();
1414 assert_eq!(parent_path.join("Fallout3"), path);
1415
1416 let path = my_games_path(GameId::FalloutNV, empty_path, local_path)
1417 .unwrap()
1418 .unwrap();
1419 assert_eq!(parent_path.join("FalloutNV"), path);
1420
1421 let path = my_games_path(GameId::Fallout4, empty_path, local_path)
1422 .unwrap()
1423 .unwrap();
1424 assert_eq!(parent_path.join("Fallout4"), path);
1425
1426 let path = my_games_path(GameId::Fallout4VR, empty_path, local_path)
1427 .unwrap()
1428 .unwrap();
1429 assert_eq!(parent_path.join("Fallout4VR"), path);
1430
1431 let path = my_games_path(GameId::Starfield, empty_path, local_path)
1432 .unwrap()
1433 .unwrap();
1434 assert_eq!(parent_path.join("Starfield"), path);
1435 }
1436
1437 #[test]
1438 #[cfg(windows)]
1439 fn my_games_path_should_be_local_path_for_openmw() {
1440 let local_path = Path::new("path/to/local");
1441
1442 let path = my_games_path(GameId::OpenMW, Path::new(""), local_path)
1443 .unwrap()
1444 .unwrap();
1445 assert_eq!(local_path, path);
1446 }
1447
1448 #[test]
1449 fn plugins_directory_should_be_mapped_from_game_id() {
1450 let data_path = Path::new("Data");
1451 let empty_path = Path::new("");
1452 let closure = |game_id| plugins_directory(game_id, empty_path, empty_path).unwrap();
1453
1454 assert_eq!(Path::new("resources/vfs"), closure(GameId::OpenMW));
1455 assert_eq!(Path::new("Data Files"), closure(GameId::Morrowind));
1456 assert_eq!(data_path, closure(GameId::Oblivion));
1457 assert_eq!(data_path, closure(GameId::Skyrim));
1458 assert_eq!(data_path, closure(GameId::SkyrimSE));
1459 assert_eq!(data_path, closure(GameId::SkyrimVR));
1460 assert_eq!(data_path, closure(GameId::Fallout3));
1461 assert_eq!(data_path, closure(GameId::FalloutNV));
1462 assert_eq!(data_path, closure(GameId::Fallout4));
1463 assert_eq!(data_path, closure(GameId::Fallout4VR));
1464 assert_eq!(data_path, closure(GameId::Starfield));
1465 }
1466
1467 #[test]
1468 fn active_plugins_file_should_be_mapped_from_game_id() {
1469 let mut settings = game_with_generic_paths(GameId::OpenMW);
1470 assert_eq!(
1471 Path::new("local/openmw.cfg"),
1472 settings.active_plugins_file()
1473 );
1474
1475 settings = game_with_generic_paths(GameId::Morrowind);
1476 assert_eq!(
1477 Path::new("game/Morrowind.ini"),
1478 settings.active_plugins_file()
1479 );
1480
1481 settings = game_with_generic_paths(GameId::Oblivion);
1482 assert_eq!(
1483 Path::new("local/Plugins.txt"),
1484 settings.active_plugins_file()
1485 );
1486
1487 settings = game_with_generic_paths(GameId::Skyrim);
1488 assert_eq!(
1489 Path::new("local/Plugins.txt"),
1490 settings.active_plugins_file()
1491 );
1492
1493 settings = game_with_generic_paths(GameId::SkyrimSE);
1494 assert_eq!(
1495 Path::new("local/Plugins.txt"),
1496 settings.active_plugins_file()
1497 );
1498
1499 settings = game_with_generic_paths(GameId::SkyrimVR);
1500 assert_eq!(
1501 Path::new("local/Plugins.txt"),
1502 settings.active_plugins_file()
1503 );
1504
1505 settings = game_with_generic_paths(GameId::Fallout3);
1506 assert_eq!(
1507 Path::new("local/Plugins.txt"),
1508 settings.active_plugins_file()
1509 );
1510
1511 settings = game_with_generic_paths(GameId::FalloutNV);
1512 assert_eq!(
1513 Path::new("local/Plugins.txt"),
1514 settings.active_plugins_file()
1515 );
1516
1517 settings = game_with_generic_paths(GameId::Fallout4);
1518 assert_eq!(
1519 Path::new("local/Plugins.txt"),
1520 settings.active_plugins_file()
1521 );
1522
1523 settings = game_with_generic_paths(GameId::Fallout4VR);
1524 assert_eq!(
1525 Path::new("local/Plugins.txt"),
1526 settings.active_plugins_file()
1527 );
1528
1529 settings = game_with_generic_paths(GameId::Starfield);
1530 assert_eq!(
1531 Path::new("local/Plugins.txt"),
1532 settings.active_plugins_file()
1533 );
1534 }
1535
1536 #[test]
1537 fn active_plugins_file_should_be_in_game_path_for_oblivion_if_ini_setting_is_not_1() {
1538 let tmp_dir = tempdir().unwrap();
1539 let game_path = tmp_dir.path();
1540 let ini_path = game_path.join("Oblivion.ini");
1541
1542 std::fs::write(ini_path, "[General]\nbUseMyGamesDirectory=0\n").unwrap();
1543
1544 let settings = game_with_game_path(GameId::Oblivion, game_path);
1545 assert_eq!(
1546 game_path.join("Plugins.txt"),
1547 *settings.active_plugins_file()
1548 );
1549 }
1550
1551 #[test]
1552 #[cfg(not(windows))]
1553 fn find_nam_plugins_should_also_find_symlinks_to_nam_plugins() {
1554 let tmp_dir = tempdir().unwrap();
1555 let data_path = tmp_dir.path().join("Data");
1556 let other_path = tmp_dir.path().join("other");
1557
1558 create_dir_all(&data_path).unwrap();
1559 create_dir_all(&other_path).unwrap();
1560 File::create(data_path.join("plugin1.nam")).unwrap();
1561
1562 let original = other_path.join("plugin2.NAM");
1563 File::create(&original).unwrap();
1564 symlink_file(&original, &data_path.join("plugin2.NAM"));
1565
1566 let mut plugins = find_nam_plugins(&data_path).unwrap();
1567 plugins.sort();
1568
1569 let expected_plugins = vec!["plugin1.esm", "plugin1.esp", "plugin2.esm", "plugin2.esp"];
1570
1571 assert_eq!(expected_plugins, plugins);
1572 }
1573
1574 #[test]
1575 fn keep_file_type_should_return_true_for_files_for_all_games() {
1576 let file = generate_file_file_type();
1577
1578 assert!(keep_file_type(file, GameId::Morrowind));
1579 assert!(keep_file_type(file, GameId::OpenMW));
1580 assert!(keep_file_type(file, GameId::Oblivion));
1581 assert!(keep_file_type(file, GameId::OblivionRemastered));
1582 assert!(keep_file_type(file, GameId::Skyrim));
1583 assert!(keep_file_type(file, GameId::SkyrimSE));
1584 assert!(keep_file_type(file, GameId::SkyrimVR));
1585 assert!(keep_file_type(file, GameId::Fallout3));
1586 assert!(keep_file_type(file, GameId::FalloutNV));
1587 assert!(keep_file_type(file, GameId::Fallout4));
1588 assert!(keep_file_type(file, GameId::Fallout4VR));
1589 assert!(keep_file_type(file, GameId::Starfield));
1590 }
1591
1592 #[test]
1593 #[cfg(not(windows))]
1594 fn keep_file_type_should_return_true_for_symlinks_for_all_games_on_linux() {
1595 let symlink = generate_symlink_file_type();
1596
1597 assert!(keep_file_type(symlink, GameId::Morrowind));
1598 assert!(keep_file_type(symlink, GameId::OpenMW));
1599 assert!(keep_file_type(symlink, GameId::Oblivion));
1600 assert!(keep_file_type(symlink, GameId::OblivionRemastered));
1601 assert!(keep_file_type(symlink, GameId::Skyrim));
1602 assert!(keep_file_type(symlink, GameId::SkyrimSE));
1603 assert!(keep_file_type(symlink, GameId::SkyrimVR));
1604 assert!(keep_file_type(symlink, GameId::Fallout3));
1605 assert!(keep_file_type(symlink, GameId::FalloutNV));
1606 assert!(keep_file_type(symlink, GameId::Fallout4));
1607 assert!(keep_file_type(symlink, GameId::Fallout4VR));
1608 assert!(keep_file_type(symlink, GameId::Starfield));
1609 }
1610
1611 #[test]
1612 #[cfg(windows)]
1613 fn keep_file_type_should_return_true_for_symlinks_for_openmw_and_oblivion_remastered_on_windows(
1614 ) {
1615 let symlink = generate_symlink_file_type();
1616
1617 assert!(!keep_file_type(symlink, GameId::Morrowind));
1618 assert!(keep_file_type(symlink, GameId::OpenMW));
1619 assert!(!keep_file_type(symlink, GameId::Oblivion));
1620 assert!(keep_file_type(symlink, GameId::OblivionRemastered));
1621 assert!(!keep_file_type(symlink, GameId::Skyrim));
1622 assert!(!keep_file_type(symlink, GameId::SkyrimSE));
1623 assert!(!keep_file_type(symlink, GameId::SkyrimVR));
1624 assert!(!keep_file_type(symlink, GameId::Fallout3));
1625 assert!(!keep_file_type(symlink, GameId::FalloutNV));
1626 assert!(!keep_file_type(symlink, GameId::Fallout4));
1627 assert!(!keep_file_type(symlink, GameId::Fallout4VR));
1628 assert!(!keep_file_type(symlink, GameId::Starfield));
1629 }
1630
1631 #[test]
1632 fn early_loading_plugins_should_be_mapped_from_game_id() {
1633 let mut settings = game_with_generic_paths(GameId::Skyrim);
1634 let mut plugins = vec!["Skyrim.esm"];
1635 assert_eq!(plugins, settings.early_loading_plugins());
1636
1637 settings = game_with_generic_paths(GameId::SkyrimSE);
1638 plugins = vec![
1639 "Skyrim.esm",
1640 "Update.esm",
1641 "Dawnguard.esm",
1642 "HearthFires.esm",
1643 "Dragonborn.esm",
1644 ];
1645 assert_eq!(plugins, settings.early_loading_plugins());
1646
1647 settings = game_with_generic_paths(GameId::SkyrimVR);
1648 plugins = vec![
1649 "Skyrim.esm",
1650 "Update.esm",
1651 "Dawnguard.esm",
1652 "HearthFires.esm",
1653 "Dragonborn.esm",
1654 "SkyrimVR.esm",
1655 ];
1656 assert_eq!(plugins, settings.early_loading_plugins());
1657
1658 settings = game_with_generic_paths(GameId::Fallout4);
1659 plugins = vec![
1660 "Fallout4.esm",
1661 "DLCRobot.esm",
1662 "DLCworkshop01.esm",
1663 "DLCCoast.esm",
1664 "DLCworkshop02.esm",
1665 "DLCworkshop03.esm",
1666 "DLCNukaWorld.esm",
1667 "DLCUltraHighResolution.esm",
1668 ];
1669 assert_eq!(plugins, settings.early_loading_plugins());
1670
1671 settings = game_with_generic_paths(GameId::OpenMW);
1672 plugins = vec!["builtin.omwscripts"];
1673 assert_eq!(plugins, settings.early_loading_plugins());
1674
1675 settings = game_with_generic_paths(GameId::Morrowind);
1676 assert!(settings.early_loading_plugins().is_empty());
1677
1678 settings = game_with_generic_paths(GameId::Oblivion);
1679 assert!(settings.early_loading_plugins().is_empty());
1680
1681 settings = game_with_generic_paths(GameId::Fallout3);
1682 assert!(settings.early_loading_plugins().is_empty());
1683
1684 settings = game_with_generic_paths(GameId::FalloutNV);
1685 assert!(settings.early_loading_plugins().is_empty());
1686
1687 settings = game_with_generic_paths(GameId::Fallout4VR);
1688 plugins = vec!["Fallout4.esm", "Fallout4_VR.esm"];
1689 assert_eq!(plugins, settings.early_loading_plugins());
1690
1691 settings = game_with_generic_paths(GameId::Starfield);
1692 plugins = vec![
1693 "Starfield.esm",
1694 "Constellation.esm",
1695 "OldMars.esm",
1696 "ShatteredSpace.esm",
1697 "SFBGS003.esm",
1698 "SFBGS004.esm",
1699 "SFBGS006.esm",
1700 "SFBGS007.esm",
1701 "SFBGS008.esm",
1702 "SFBGS00D.esm",
1703 "SFBGS047.esm",
1704 "SFBGS050.esm",
1705 ];
1706 assert_eq!(plugins, settings.early_loading_plugins());
1707 }
1708
1709 #[test]
1710 fn early_loading_plugins_should_include_plugins_loaded_from_ccc_file() {
1711 let tmp_dir = tempdir().unwrap();
1712 let game_path = tmp_dir.path();
1713
1714 let mut plugins = vec![
1715 "Skyrim.esm",
1716 "Update.esm",
1717 "Dawnguard.esm",
1718 "HearthFires.esm",
1719 "Dragonborn.esm",
1720 "ccBGSSSE002-ExoticArrows.esl",
1721 "ccBGSSSE003-Zombies.esl",
1722 "ccBGSSSE004-RuinsEdge.esl",
1723 "ccBGSSSE006-StendarsHammer.esl",
1724 "ccBGSSSE007-Chrysamere.esl",
1725 "ccBGSSSE010-PetDwarvenArmoredMudcrab.esl",
1726 "ccBGSSSE014-SpellPack01.esl",
1727 "ccBGSSSE019-StaffofSheogorath.esl",
1728 "ccMTYSSE001-KnightsoftheNine.esl",
1729 "ccQDRSSE001-SurvivalMode.esl",
1730 ];
1731 let mut settings = game_with_ccc_plugins(GameId::SkyrimSE, game_path, &plugins[5..]);
1732 assert_eq!(plugins, settings.early_loading_plugins());
1733
1734 plugins = vec![
1735 "Fallout4.esm",
1736 "DLCRobot.esm",
1737 "DLCworkshop01.esm",
1738 "DLCCoast.esm",
1739 "DLCworkshop02.esm",
1740 "DLCworkshop03.esm",
1741 "DLCNukaWorld.esm",
1742 "DLCUltraHighResolution.esm",
1743 "ccBGSFO4001-PipBoy(Black).esl",
1744 "ccBGSFO4002-PipBoy(Blue).esl",
1745 "ccBGSFO4003-PipBoy(Camo01).esl",
1746 "ccBGSFO4004-PipBoy(Camo02).esl",
1747 "ccBGSFO4006-PipBoy(Chrome).esl",
1748 "ccBGSFO4012-PipBoy(Red).esl",
1749 "ccBGSFO4014-PipBoy(White).esl",
1750 "ccBGSFO4016-Prey.esl",
1751 "ccBGSFO4017-Mauler.esl",
1752 "ccBGSFO4018-GaussRiflePrototype.esl",
1753 "ccBGSFO4019-ChineseStealthArmor.esl",
1754 "ccBGSFO4020-PowerArmorSkin(Black).esl",
1755 "ccBGSFO4022-PowerArmorSkin(Camo01).esl",
1756 "ccBGSFO4023-PowerArmorSkin(Camo02).esl",
1757 "ccBGSFO4025-PowerArmorSkin(Chrome).esl",
1758 "ccBGSFO4038-HorseArmor.esl",
1759 "ccBGSFO4039-TunnelSnakes.esl",
1760 "ccBGSFO4041-DoomMarineArmor.esl",
1761 "ccBGSFO4042-BFG.esl",
1762 "ccBGSFO4043-DoomChainsaw.esl",
1763 "ccBGSFO4044-HellfirePowerArmor.esl",
1764 "ccFSVFO4001-ModularMilitaryBackpack.esl",
1765 "ccFSVFO4002-MidCenturyModern.esl",
1766 "ccFRSFO4001-HandmadeShotgun.esl",
1767 "ccEEJFO4001-DecorationPack.esl",
1768 ];
1769 settings = game_with_ccc_plugins(GameId::Fallout4, game_path, &plugins[8..]);
1770 assert_eq!(plugins, settings.early_loading_plugins());
1771 }
1772
1773 #[test]
1774 fn early_loading_plugins_should_use_the_starfield_ccc_file_in_game_path() {
1775 let tmp_dir = tempdir().unwrap();
1776 let game_path = tmp_dir.path().join("game");
1777 let my_games_path = tmp_dir.path().join("my games");
1778
1779 create_ccc_file(&game_path.join("Starfield.ccc"), &["test.esm"]);
1780
1781 let settings = GameSettings::with_local_and_my_games_paths(
1782 GameId::Starfield,
1783 &game_path,
1784 &PathBuf::default(),
1785 my_games_path,
1786 )
1787 .unwrap();
1788
1789 let expected = &[
1790 "Starfield.esm",
1791 "Constellation.esm",
1792 "OldMars.esm",
1793 "ShatteredSpace.esm",
1794 "SFBGS003.esm",
1795 "SFBGS004.esm",
1796 "SFBGS006.esm",
1797 "SFBGS007.esm",
1798 "SFBGS008.esm",
1799 "SFBGS00D.esm",
1800 "SFBGS047.esm",
1801 "SFBGS050.esm",
1802 "test.esm",
1803 ];
1804 assert_eq!(expected, settings.early_loading_plugins());
1805 }
1806
1807 #[test]
1808 fn early_loading_plugins_should_use_the_starfield_ccc_file_in_my_games_path() {
1809 let tmp_dir = tempdir().unwrap();
1810 let game_path = tmp_dir.path().join("game");
1811 let my_games_path = tmp_dir.path().join("my games");
1812
1813 create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test.esm"]);
1814
1815 let settings = GameSettings::with_local_and_my_games_paths(
1816 GameId::Starfield,
1817 &game_path,
1818 &PathBuf::default(),
1819 my_games_path,
1820 )
1821 .unwrap();
1822
1823 let expected = &[
1824 "Starfield.esm",
1825 "Constellation.esm",
1826 "OldMars.esm",
1827 "ShatteredSpace.esm",
1828 "SFBGS003.esm",
1829 "SFBGS004.esm",
1830 "SFBGS006.esm",
1831 "SFBGS007.esm",
1832 "SFBGS008.esm",
1833 "SFBGS00D.esm",
1834 "SFBGS047.esm",
1835 "SFBGS050.esm",
1836 "test.esm",
1837 ];
1838 assert_eq!(expected, settings.early_loading_plugins());
1839 }
1840
1841 #[test]
1842 fn early_loading_plugins_should_use_the_first_ccc_file_that_exists() {
1843 let tmp_dir = tempdir().unwrap();
1844 let game_path = tmp_dir.path().join("game");
1845 let my_games_path = tmp_dir.path().join("my games");
1846
1847 create_ccc_file(&game_path.join("Starfield.ccc"), &["test1.esm"]);
1848 create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test2.esm"]);
1849
1850 let settings = GameSettings::with_local_and_my_games_paths(
1851 GameId::Starfield,
1852 &game_path,
1853 &PathBuf::default(),
1854 my_games_path,
1855 )
1856 .unwrap();
1857
1858 let expected = &[
1859 "Starfield.esm",
1860 "Constellation.esm",
1861 "OldMars.esm",
1862 "ShatteredSpace.esm",
1863 "SFBGS003.esm",
1864 "SFBGS004.esm",
1865 "SFBGS006.esm",
1866 "SFBGS007.esm",
1867 "SFBGS008.esm",
1868 "SFBGS00D.esm",
1869 "SFBGS047.esm",
1870 "SFBGS050.esm",
1871 "test2.esm",
1872 ];
1873 assert_eq!(expected, settings.early_loading_plugins());
1874 }
1875
1876 #[test]
1877 fn early_loading_plugins_should_not_include_cc_plugins_for_fallout4_if_test_files_are_configured(
1878 ) {
1879 let tmp_dir = tempdir().unwrap();
1880 let game_path = tmp_dir.path();
1881
1882 create_ccc_file(
1883 &game_path.join("Fallout4.ccc"),
1884 &["ccBGSFO4001-PipBoy(Black).esl"],
1885 );
1886
1887 let ini_path = game_path.join("Fallout4.ini");
1888 std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp\n").unwrap();
1889
1890 copy_to_dir(
1891 "Blank.esp",
1892 &game_path.join("Data"),
1893 "Blank.esp",
1894 GameId::Fallout4,
1895 );
1896
1897 let settings = GameSettings::with_local_and_my_games_paths(
1898 GameId::Fallout4,
1899 game_path,
1900 &PathBuf::default(),
1901 game_path.to_path_buf(),
1902 )
1903 .unwrap();
1904
1905 assert_eq!(FALLOUT4_HARDCODED_PLUGINS, settings.early_loading_plugins());
1906 }
1907
1908 #[test]
1909 fn early_loading_plugins_should_not_include_cc_plugins_for_starfield_if_test_files_are_configured(
1910 ) {
1911 let tmp_dir = tempdir().unwrap();
1912 let game_path = tmp_dir.path();
1913 let my_games_path = tmp_dir.path().join("my games");
1914
1915 create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test.esp"]);
1916
1917 let ini_path = game_path.join("Starfield.ini");
1918 std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp\n").unwrap();
1919
1920 copy_to_dir(
1921 "Blank.esp",
1922 &game_path.join("Data"),
1923 "Blank.esp",
1924 GameId::Starfield,
1925 );
1926
1927 let settings = GameSettings::with_local_and_my_games_paths(
1928 GameId::Starfield,
1929 game_path,
1930 &PathBuf::default(),
1931 my_games_path,
1932 )
1933 .unwrap();
1934
1935 assert!(!settings.loads_early("test.esp"));
1936 }
1937
1938 #[test]
1939 fn early_loading_plugins_should_include_plugins_from_global_config_for_openmw() {
1940 let tmp_dir = tempdir().unwrap();
1941 let global_cfg_path = tmp_dir.path().join("openmw.cfg");
1942
1943 std::fs::write(
1944 &global_cfg_path,
1945 "config=local\ncontent=test.esm\ncontent=test.esp",
1946 )
1947 .unwrap();
1948
1949 let settings = game_with_game_path(GameId::OpenMW, tmp_dir.path());
1950
1951 let expected = &["builtin.omwscripts", "test.esm", "test.esp"];
1952
1953 assert_eq!(expected, settings.early_loading_plugins());
1954 }
1955
1956 #[test]
1957 fn early_loading_plugins_should_ignore_later_duplicate_entries() {
1958 let tmp_dir = tempdir().unwrap();
1959 let game_path = tmp_dir.path();
1960 let my_games_path = tmp_dir.path().join("my games");
1961
1962 create_ccc_file(
1963 &my_games_path.join("Starfield.ccc"),
1964 &["Starfield.esm", "test.esm"],
1965 );
1966
1967 let settings = GameSettings::with_local_and_my_games_paths(
1968 GameId::Starfield,
1969 game_path,
1970 &PathBuf::default(),
1971 my_games_path,
1972 )
1973 .unwrap();
1974
1975 let expected = &[
1976 "Starfield.esm",
1977 "Constellation.esm",
1978 "OldMars.esm",
1979 "ShatteredSpace.esm",
1980 "SFBGS003.esm",
1981 "SFBGS004.esm",
1982 "SFBGS006.esm",
1983 "SFBGS007.esm",
1984 "SFBGS008.esm",
1985 "SFBGS00D.esm",
1986 "SFBGS047.esm",
1987 "SFBGS050.esm",
1988 "test.esm",
1989 ];
1990 assert_eq!(expected, settings.early_loading_plugins());
1991 }
1992
1993 #[test]
1994 fn implicitly_active_plugins_should_include_early_loading_plugins() {
1995 let tmp_dir = tempdir().unwrap();
1996 let game_path = tmp_dir.path();
1997
1998 let settings = game_with_game_path(GameId::SkyrimSE, game_path);
1999
2000 assert_eq!(
2001 settings.early_loading_plugins(),
2002 settings.implicitly_active_plugins()
2003 );
2004 }
2005
2006 #[test]
2007 fn implicitly_active_plugins_should_include_test_files() {
2008 let tmp_dir = tempdir().unwrap();
2009 let game_path = tmp_dir.path();
2010
2011 let ini_path = game_path.join("Skyrim.ini");
2012 std::fs::write(&ini_path, "[General]\nsTestFile1=plugin.esp\n").unwrap();
2013
2014 let settings = GameSettings::with_local_and_my_games_paths(
2015 GameId::SkyrimSE,
2016 game_path,
2017 &PathBuf::default(),
2018 game_path.to_path_buf(),
2019 )
2020 .unwrap();
2021
2022 let mut expected_plugins = settings.early_loading_plugins().to_vec();
2023 expected_plugins.push("plugin.esp".to_owned());
2024
2025 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
2026 }
2027
2028 #[test]
2029 fn implicitly_active_plugins_should_only_include_valid_test_files_for_fallout4() {
2030 let tmp_dir = tempdir().unwrap();
2031 let game_path = tmp_dir.path();
2032
2033 let ini_path = game_path.join("Fallout4.ini");
2034 std::fs::write(
2035 &ini_path,
2036 "[General]\nsTestFile1=plugin.esp\nsTestFile2=Blank.esp",
2037 )
2038 .unwrap();
2039
2040 copy_to_dir(
2041 "Blank.esp",
2042 &game_path.join("Data"),
2043 "Blank.esp",
2044 GameId::Fallout4,
2045 );
2046
2047 let settings = GameSettings::with_local_and_my_games_paths(
2048 GameId::Fallout4,
2049 game_path,
2050 &PathBuf::default(),
2051 game_path.to_path_buf(),
2052 )
2053 .unwrap();
2054
2055 let mut expected_plugins = settings.early_loading_plugins().to_vec();
2056 expected_plugins.push("Blank.esp".to_owned());
2057
2058 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
2059 }
2060
2061 #[test]
2062 fn implicitly_active_plugins_should_only_include_valid_test_files_for_fallout4vr() {
2063 let tmp_dir = tempdir().unwrap();
2064 let game_path = tmp_dir.path();
2065
2066 let ini_path = game_path.join("Fallout4VR.ini");
2067 std::fs::write(
2068 &ini_path,
2069 "[General]\nsTestFile1=plugin.esp\nsTestFile2=Blank.esp",
2070 )
2071 .unwrap();
2072
2073 copy_to_dir(
2074 "Blank.esp",
2075 &game_path.join("Data"),
2076 "Blank.esp",
2077 GameId::Fallout4VR,
2078 );
2079
2080 let settings = GameSettings::with_local_and_my_games_paths(
2081 GameId::Fallout4VR,
2082 game_path,
2083 &PathBuf::default(),
2084 game_path.to_path_buf(),
2085 )
2086 .unwrap();
2087
2088 let mut expected_plugins = settings.early_loading_plugins().to_vec();
2089 expected_plugins.push("Blank.esp".to_owned());
2090
2091 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
2092 }
2093
2094 #[test]
2095 fn implicitly_active_plugins_should_include_plugins_with_nam_files_for_fallout_nv() {
2096 let tmp_dir = tempdir().unwrap();
2097 let game_path = tmp_dir.path();
2098 let data_path = game_path.join("Data");
2099
2100 create_dir_all(&data_path).unwrap();
2101 File::create(data_path.join("plugin1.nam")).unwrap();
2102 File::create(data_path.join("plugin2.NAM")).unwrap();
2103
2104 let settings = game_with_game_path(GameId::FalloutNV, game_path);
2105 let expected_plugins = vec!["plugin1.esm", "plugin1.esp", "plugin2.esm", "plugin2.esp"];
2106 let mut plugins = settings.implicitly_active_plugins().to_vec();
2107 plugins.sort();
2108
2109 assert_eq!(expected_plugins, plugins);
2110 }
2111
2112 #[test]
2113 fn implicitly_active_plugins_should_include_update_esm_for_skyrim() {
2114 let settings = game_with_generic_paths(GameId::Skyrim);
2115 let plugins = settings.implicitly_active_plugins();
2116
2117 assert!(plugins.contains(&"Update.esm".to_owned()));
2118 }
2119
2120 #[test]
2121 fn implicitly_active_plugins_should_include_plugins_with_nam_files_for_games_other_than_fallout_nv(
2122 ) {
2123 let tmp_dir = tempdir().unwrap();
2124 let game_path = tmp_dir.path();
2125 let data_path = game_path.join("Data");
2126
2127 create_dir_all(&data_path).unwrap();
2128 File::create(data_path.join("plugin.nam")).unwrap();
2129
2130 let settings = game_with_game_path(GameId::Fallout3, game_path);
2131 assert!(settings.implicitly_active_plugins().is_empty());
2132 }
2133
2134 #[test]
2135 fn implicitly_active_plugins_should_not_include_case_insensitive_duplicates() {
2136 let tmp_dir = tempdir().unwrap();
2137 let game_path = tmp_dir.path();
2138
2139 let ini_path = game_path.join("Fallout4.ini");
2140 std::fs::write(&ini_path, "[General]\nsTestFile1=fallout4.esm\n").unwrap();
2141
2142 let settings = GameSettings::with_local_and_my_games_paths(
2143 GameId::Fallout4,
2144 game_path,
2145 &PathBuf::default(),
2146 game_path.to_path_buf(),
2147 )
2148 .unwrap();
2149
2150 assert_eq!(
2151 settings.early_loading_plugins(),
2152 settings.implicitly_active_plugins()
2153 );
2154 }
2155
2156 #[test]
2157 fn is_implicitly_active_should_return_true_iff_the_plugin_is_implicitly_active() {
2158 let settings = game_with_generic_paths(GameId::Skyrim);
2159 assert!(settings.is_implicitly_active("Update.esm"));
2160 assert!(!settings.is_implicitly_active("Test.esm"));
2161 }
2162
2163 #[test]
2164 fn is_implicitly_active_should_match_case_insensitively() {
2165 let settings = game_with_generic_paths(GameId::Skyrim);
2166 assert!(settings.is_implicitly_active("update.esm"));
2167 }
2168
2169 #[test]
2170 fn loads_early_should_return_true_iff_the_plugin_loads_early() {
2171 let settings = game_with_generic_paths(GameId::SkyrimSE);
2172 assert!(settings.loads_early("Dawnguard.esm"));
2173 assert!(!settings.loads_early("Test.esm"));
2174 }
2175
2176 #[test]
2177 fn loads_early_should_match_case_insensitively() {
2178 let settings = game_with_generic_paths(GameId::SkyrimSE);
2179 assert!(settings.loads_early("dawnguard.esm"));
2180 }
2181
2182 #[test]
2183 fn plugins_folder_should_be_a_child_of_the_game_path() {
2184 let settings = game_with_generic_paths(GameId::Skyrim);
2185 assert_eq!(Path::new("game/Data"), settings.plugins_directory());
2186 }
2187
2188 #[test]
2189 fn load_order_file_should_be_in_local_path_for_skyrim_and_none_for_other_games() {
2190 let mut settings = game_with_generic_paths(GameId::Skyrim);
2191 assert_eq!(
2192 Path::new("local/loadorder.txt"),
2193 settings.load_order_file().unwrap()
2194 );
2195
2196 settings = game_with_generic_paths(GameId::SkyrimSE);
2197 assert!(settings.load_order_file().is_none());
2198
2199 settings = game_with_generic_paths(GameId::OpenMW);
2200 assert!(settings.load_order_file().is_none());
2201
2202 settings = game_with_generic_paths(GameId::Morrowind);
2203 assert!(settings.load_order_file().is_none());
2204
2205 settings = game_with_generic_paths(GameId::Oblivion);
2206 assert!(settings.load_order_file().is_none());
2207
2208 settings = game_with_generic_paths(GameId::Fallout3);
2209 assert!(settings.load_order_file().is_none());
2210
2211 settings = game_with_generic_paths(GameId::FalloutNV);
2212 assert!(settings.load_order_file().is_none());
2213
2214 settings = game_with_generic_paths(GameId::Fallout4);
2215 assert!(settings.load_order_file().is_none());
2216 }
2217
2218 #[test]
2219 fn additional_plugins_directories_should_be_empty_if_game_is_not_fallout4_or_starfield() {
2220 let tmp_dir = tempdir().unwrap();
2221 let game_path = tmp_dir.path();
2222
2223 File::create(game_path.join("appxmanifest.xml")).unwrap();
2224
2225 let game_ids = [
2226 GameId::Morrowind,
2227 GameId::Oblivion,
2228 GameId::Skyrim,
2229 GameId::SkyrimSE,
2230 GameId::SkyrimVR,
2231 GameId::Fallout3,
2232 GameId::FalloutNV,
2233 ];
2234
2235 for game_id in game_ids {
2236 let settings = game_with_game_path(game_id, game_path);
2237
2238 assert!(settings.additional_plugins_directories().is_empty());
2239 }
2240 }
2241
2242 #[test]
2243 fn additional_plugins_directories_should_be_empty_if_fallout4_is_not_from_the_microsoft_store()
2244 {
2245 let settings = game_with_generic_paths(GameId::Fallout4);
2246
2247 assert!(settings.additional_plugins_directories().is_empty());
2248 }
2249
2250 #[test]
2251 fn additional_plugins_directories_should_not_be_empty_if_game_is_fallout4_from_the_microsoft_store(
2252 ) {
2253 let tmp_dir = tempdir().unwrap();
2254 let game_path = tmp_dir.path();
2255
2256 File::create(game_path.join("appxmanifest.xml")).unwrap();
2257
2258 let settings = game_with_game_path(GameId::Fallout4, game_path);
2259
2260 assert_eq!(
2261 vec![
2262 game_path.join(MS_FO4_AUTOMATRON_PATH),
2263 game_path.join(MS_FO4_NUKA_WORLD_PATH),
2264 game_path.join(MS_FO4_WASTELAND_PATH),
2265 game_path.join(MS_FO4_TEXTURE_PACK_PATH),
2266 game_path.join(MS_FO4_VAULT_TEC_PATH),
2267 game_path.join(MS_FO4_FAR_HARBOR_PATH),
2268 game_path.join(MS_FO4_CONTRAPTIONS_PATH),
2269 ],
2270 settings.additional_plugins_directories()
2271 );
2272 }
2273
2274 #[test]
2275 fn additional_plugins_directories_should_not_be_empty_if_game_is_starfield() {
2276 let settings = game_with_generic_paths(GameId::Starfield);
2277
2278 assert_eq!(
2279 vec![Path::new("my games").join("Data")],
2280 settings.additional_plugins_directories()
2281 );
2282 }
2283
2284 #[test]
2285 fn additional_plugins_directories_should_include_dlc_paths_if_game_is_ms_store_starfield() {
2286 let tmp_dir = tempdir().unwrap();
2287 let game_path = tmp_dir.path();
2288
2289 File::create(game_path.join("appxmanifest.xml")).unwrap();
2290
2291 let settings = game_with_game_path(GameId::Starfield, game_path);
2292
2293 assert_eq!(
2294 vec![
2295 Path::new("Data"),
2296 &game_path.join("../../Old Mars/Content/Data"),
2297 &game_path.join("../../Shattered Space/Content/Data"),
2298 ],
2299 settings.additional_plugins_directories()
2300 );
2301 }
2302
2303 #[test]
2304 fn additional_plugins_directories_should_read_from_openmw_cfgs() {
2305 let tmp_dir = tempdir().unwrap();
2306 let game_path = tmp_dir.path().join("game");
2307 let my_games_path = tmp_dir.path().join("my games");
2308 let global_cfg_path = game_path.join("openmw.cfg");
2309 let cfg_path = my_games_path.join("openmw.cfg");
2310
2311 create_dir_all(global_cfg_path.parent().unwrap()).unwrap();
2312 std::fs::write(&global_cfg_path, "config=\"../my games\"\ndata=\"foo/bar\"").unwrap();
2313
2314 create_dir_all(cfg_path.parent().unwrap()).unwrap();
2315 std::fs::write(
2316 &cfg_path,
2317 "data=\"Path\\&&&\"&a&&&&\\Data Files\"\ndata=games/path",
2318 )
2319 .unwrap();
2320
2321 let settings =
2322 GameSettings::with_local_path(GameId::OpenMW, &game_path, &my_games_path).unwrap();
2323
2324 let expected: Vec<PathBuf> = vec![
2325 game_path.join("foo/bar"),
2326 my_games_path.join("Path\\&\"a&&\\Data Files"),
2327 my_games_path.join("games/path"),
2328 ];
2329 assert_eq!(expected, settings.additional_plugins_directories());
2330 }
2331
2332 #[test]
2333 fn plugin_path_should_append_plugin_name_to_additional_plugin_directory_if_that_path_exists() {
2334 let tmp_dir = tempdir().unwrap();
2335 let other_dir = tmp_dir.path().join("other");
2336
2337 let plugin_name = "external.esp";
2338 let expected_plugin_path = other_dir.join(plugin_name);
2339
2340 let mut settings = game_with_generic_paths(GameId::Fallout4);
2341 settings.additional_plugins_directories = vec![other_dir.clone()];
2342
2343 copy_to_dir("Blank.esp", &other_dir, plugin_name, GameId::Fallout4);
2344
2345 let plugin_path = settings.plugin_path(plugin_name);
2346
2347 assert_eq!(expected_plugin_path, plugin_path);
2348 }
2349
2350 #[test]
2351 fn plugin_path_should_append_plugin_name_to_additional_plugin_directory_if_the_ghosted_path_exists(
2352 ) {
2353 let tmp_dir = tempdir().unwrap();
2354 let other_dir = tmp_dir.path().join("other");
2355
2356 let plugin_name = "external.esp";
2357 let ghosted_plugin_name = "external.esp.ghost";
2358 let expected_plugin_path = other_dir.join(ghosted_plugin_name);
2359
2360 let mut settings = game_with_generic_paths(GameId::Fallout4);
2361 settings.additional_plugins_directories = vec![other_dir.clone()];
2362
2363 copy_to_dir(
2364 "Blank.esp",
2365 &other_dir,
2366 ghosted_plugin_name,
2367 GameId::Fallout4,
2368 );
2369
2370 let plugin_path = settings.plugin_path(plugin_name);
2371
2372 assert_eq!(expected_plugin_path, plugin_path);
2373 }
2374
2375 #[test]
2376 fn plugin_path_should_not_resolve_ghosted_paths_for_openmw() {
2377 let tmp_dir = tempdir().unwrap();
2378 let game_path = tmp_dir.path().join("game");
2379 let other_dir = tmp_dir.path().join("other");
2380
2381 let plugin_name = "external.esp";
2382
2383 let mut settings = game_with_game_path(GameId::OpenMW, &game_path);
2384 settings.additional_plugins_directories = vec![other_dir.clone()];
2385
2386 copy_to_dir(
2387 "Blank.esp",
2388 &other_dir,
2389 "external.esp.ghost",
2390 GameId::OpenMW,
2391 );
2392
2393 let plugin_path = settings.plugin_path(plugin_name);
2394
2395 assert_eq!(
2396 game_path.join("resources/vfs").join(plugin_name),
2397 plugin_path
2398 );
2399 }
2400
2401 #[test]
2402 fn plugin_path_should_return_the_last_directory_that_contains_a_file_for_openmw() {
2403 let tmp_dir = tempdir().unwrap();
2404 let other_dir_1 = tmp_dir.path().join("other1");
2405 let other_dir_2 = tmp_dir.path().join("other2");
2406
2407 let plugin_name = "Blank.esp";
2408
2409 let mut settings = game_with_game_path(GameId::OpenMW, tmp_dir.path());
2410 settings.additional_plugins_directories = vec![other_dir_1.clone(), other_dir_2.clone()];
2411
2412 copy_to_dir("Blank.esp", &other_dir_1, plugin_name, GameId::OpenMW);
2413 copy_to_dir("Blank.esp", &other_dir_2, plugin_name, GameId::OpenMW);
2414
2415 let plugin_path = settings.plugin_path(plugin_name);
2416
2417 assert_eq!(other_dir_2.join(plugin_name), plugin_path);
2418 }
2419
2420 #[test]
2421 fn plugin_path_should_return_plugins_dir_subpath_if_name_does_not_match_any_external_plugin() {
2422 let settings = game_with_generic_paths(GameId::Fallout4);
2423
2424 let plugin_name = "DLCCoast.esm";
2425 assert_eq!(
2426 settings.plugins_directory().join(plugin_name),
2427 settings.plugin_path(plugin_name)
2428 );
2429 }
2430
2431 #[test]
2432 fn plugin_path_should_only_resolve_additional_starfield_plugin_paths_if_they_exist_or_are_ghosted_in_the_plugins_directory(
2433 ) {
2434 let tmp_dir = tempdir().unwrap();
2435 let game_path = tmp_dir.path().join("game");
2436 let data_path = game_path.join("Data");
2437 let other_dir = tmp_dir.path().join("other");
2438
2439 let plugin_name_1 = "external1.esp";
2440 let plugin_name_2 = "external2.esp";
2441 let plugin_name_3 = "external3.esp";
2442 let ghosted_plugin_name_3 = "external3.esp.ghost";
2443
2444 let mut settings = game_with_game_path(GameId::Starfield, &game_path);
2445 settings.additional_plugins_directories = vec![other_dir.clone()];
2446
2447 copy_to_dir("Blank.esp", &other_dir, plugin_name_1, GameId::Starfield);
2448 copy_to_dir("Blank.esp", &other_dir, plugin_name_2, GameId::Starfield);
2449 copy_to_dir("Blank.esp", &data_path, plugin_name_2, GameId::Starfield);
2450 copy_to_dir("Blank.esp", &other_dir, plugin_name_3, GameId::Starfield);
2451 copy_to_dir(
2452 "Blank.esp",
2453 &data_path,
2454 ghosted_plugin_name_3,
2455 GameId::Starfield,
2456 );
2457
2458 let plugin_1_path = settings.plugin_path(plugin_name_1);
2459 let plugin_2_path = settings.plugin_path(plugin_name_2);
2460 let plugin_3_path = settings.plugin_path(plugin_name_3);
2461
2462 assert_eq!(data_path.join(plugin_name_1), plugin_1_path);
2463 assert_eq!(other_dir.join(plugin_name_2), plugin_2_path);
2464 assert_eq!(other_dir.join(plugin_name_3), plugin_3_path);
2465 }
2466
2467 #[test]
2468 fn refresh_implicitly_active_plugins_should_update_early_loading_and_implicitly_active_plugins()
2469 {
2470 let tmp_dir = tempdir().unwrap();
2471 let game_path = tmp_dir.path();
2472
2473 let mut settings = GameSettings::with_local_and_my_games_paths(
2474 GameId::SkyrimSE,
2475 game_path,
2476 &PathBuf::default(),
2477 game_path.to_path_buf(),
2478 )
2479 .unwrap();
2480
2481 let hardcoded_plugins = vec![
2482 "Skyrim.esm",
2483 "Update.esm",
2484 "Dawnguard.esm",
2485 "HearthFires.esm",
2486 "Dragonborn.esm",
2487 ];
2488 assert_eq!(hardcoded_plugins, settings.early_loading_plugins());
2489 assert_eq!(hardcoded_plugins, settings.implicitly_active_plugins());
2490
2491 std::fs::write(game_path.join("Skyrim.ccc"), "ccBGSSSE002-ExoticArrows.esl").unwrap();
2492 std::fs::write(
2493 game_path.join("Skyrim.ini"),
2494 "[General]\nsTestFile1=plugin.esp\n",
2495 )
2496 .unwrap();
2497
2498 settings.refresh_implicitly_active_plugins().unwrap();
2499
2500 let mut expected_plugins = hardcoded_plugins;
2501 expected_plugins.push("ccBGSSSE002-ExoticArrows.esl");
2502 assert_eq!(expected_plugins, settings.early_loading_plugins());
2503
2504 expected_plugins.push("plugin.esp");
2505 assert_eq!(expected_plugins, settings.implicitly_active_plugins());
2506 }
2507
2508 #[test]
2509 fn get_target_modified_timestamp_should_return_the_modified_timestamp_of_a_symlinks_target_file(
2510 ) {
2511 let tmp_dir = tempdir().unwrap();
2512 let file_path = tmp_dir.path().join("file");
2513 let symlink_path = tmp_dir.path().join("symlink");
2514
2515 std::fs::File::create(&file_path).unwrap();
2516
2517 symlink_file(&file_path, &symlink_path);
2518
2519 let symlink_timestamp = symlink_path.symlink_metadata().unwrap().modified().unwrap();
2520 let file_timestamp = symlink_timestamp - std::time::Duration::from_secs(1);
2521 assert_ne!(symlink_timestamp, file_timestamp);
2522 let file = File::options().append(true).open(file_path).unwrap();
2523 file.set_modified(file_timestamp).unwrap();
2524
2525 let mut dir_entries = tmp_dir
2526 .path()
2527 .read_dir()
2528 .unwrap()
2529 .collect::<Result<Vec<_>, _>>()
2530 .unwrap();
2531
2532 dir_entries.sort_by_key(DirEntry::file_name);
2533
2534 assert!(dir_entries[0].file_type().unwrap().is_file());
2535 assert_eq!(
2536 file_timestamp,
2537 get_target_modified_timestamp(&dir_entries[0]).unwrap()
2538 );
2539
2540 assert!(dir_entries[1].file_type().unwrap().is_symlink());
2541 assert_eq!(
2542 file_timestamp,
2543 get_target_modified_timestamp(&dir_entries[1]).unwrap()
2544 );
2545 }
2546
2547 #[test]
2548 fn find_plugins_in_directories_should_sort_files_by_modification_timestamp() {
2549 let tmp_dir = tempdir().unwrap();
2550 let game_path = tmp_dir.path();
2551
2552 let plugin_names = [
2553 "Blank.esp",
2554 "Blank - Different.esp",
2555 "Blank - Master Dependent.esp",
2556 NON_ASCII,
2557 ];
2558
2559 copy_to_dir("Blank.esp", game_path, NON_ASCII, GameId::Oblivion);
2560
2561 for (i, plugin_name) in plugin_names.iter().enumerate() {
2562 let path = game_path.join(plugin_name);
2563 if !path.exists() {
2564 copy_to_dir(plugin_name, game_path, plugin_name, GameId::Oblivion);
2565 }
2566 set_file_timestamps(&path, i.try_into().unwrap());
2567 }
2568
2569 let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Oblivion);
2570
2571 let expected: Vec<_> = plugin_names.iter().map(|n| game_path.join(n)).collect();
2572
2573 assert_eq!(expected, result);
2574 }
2575
2576 #[test]
2577 fn find_plugins_in_directories_should_sort_files_by_descending_uppercased_filename_if_timestamps_are_equal(
2578 ) {
2579 let tmp_dir = tempdir().unwrap();
2580 let game_path = tmp_dir.path();
2581
2582 let non_ascii = NON_ASCII;
2583 let plugin_names = [
2584 "Blank.esm",
2585 "Blank.esp",
2586 "Blank - Different.esp",
2587 "Blank - Master Dependent.esp",
2588 non_ascii,
2589 ];
2590
2591 copy_to_dir("Blank.esp", game_path, non_ascii, GameId::Oblivion);
2592
2593 for (i, plugin_name) in plugin_names.iter().enumerate() {
2594 let path = game_path.join(plugin_name);
2595 if !path.exists() {
2596 copy_to_dir(plugin_name, game_path, plugin_name, GameId::Oblivion);
2597 }
2598 set_file_timestamps(&path, i.try_into().unwrap());
2599 }
2600
2601 let timestamp = 3;
2602 set_file_timestamps(&game_path.join("Blank - Different.esp"), timestamp);
2603 set_file_timestamps(&game_path.join("Blank - Master Dependent.esp"), timestamp);
2604
2605 copy_to_dir("Blank.esp", game_path, "a.esp", GameId::Oblivion);
2606 set_file_timestamps(&game_path.join("a.esp"), timestamp);
2607
2608 let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Oblivion);
2609
2610 let plugin_paths = vec![
2611 game_path.join("Blank.esm"),
2612 game_path.join("Blank.esp"),
2613 game_path.join("Blank - Master Dependent.esp"),
2614 game_path.join("Blank - Different.esp"),
2615 game_path.join("a.esp"),
2616 game_path.join(non_ascii),
2617 ];
2618
2619 assert_eq!(plugin_paths, result);
2620 }
2621
2622 #[test]
2623 fn find_plugins_in_directories_should_sort_files_by_descending_uppercased_filename_if_timestamps_are_equal_and_game_is_starfield(
2624 ) {
2625 let tmp_dir = tempdir().unwrap();
2626 let game_path = tmp_dir.path();
2627
2628 let plugin_names = [
2629 "Blank.full.esm",
2630 "Blank.small.esm",
2631 "Blank.medium.esm",
2632 "Blank.esp",
2633 "Blank - Override.esp",
2634 "a.esp",
2635 ];
2636
2637 let timestamp = 1_321_009_991;
2638
2639 for plugin_name in &plugin_names[..plugin_names.len() - 1] {
2640 let path = game_path.join(plugin_name);
2641 if !path.exists() {
2642 copy_to_dir(plugin_name, game_path, plugin_name, GameId::Starfield);
2643 }
2644 set_file_timestamps(&path, timestamp);
2645 }
2646 copy_to_dir(
2647 "Blank.esp",
2648 game_path,
2649 plugin_names.last().unwrap(),
2650 GameId::Starfield,
2651 );
2652
2653 let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Starfield);
2654
2655 let plugin_paths = vec![
2656 game_path.join("Blank.small.esm"),
2657 game_path.join("Blank.medium.esm"),
2658 game_path.join("Blank.full.esm"),
2659 game_path.join("Blank.esp"),
2660 game_path.join("Blank - Override.esp"),
2661 game_path.join("a.esp"),
2662 ];
2663
2664 assert_eq!(plugin_paths, result);
2665 }
2666
2667 #[test]
2668 fn find_plugins_in_directories_should_find_symlinks_to_plugins() {
2669 const BLANK_ESM: &str = "Blank.esm";
2670 const BLANK_ESP: &str = "Blank.esp";
2671
2672 let tmp_dir = tempdir().unwrap();
2673 let data_path = tmp_dir.path().join("game");
2674 let other_path = tmp_dir.path().join("other");
2675
2676 copy_to_dir(BLANK_ESM, &data_path, BLANK_ESM, GameId::OpenMW);
2677 copy_to_dir(BLANK_ESP, &other_path, BLANK_ESP, GameId::OpenMW);
2678
2679 symlink_file(&other_path.join(BLANK_ESP), &data_path.join(BLANK_ESP));
2680
2681 let result = find_plugins_in_directories(once(&data_path), GameId::OpenMW);
2682
2683 let plugin_paths = vec![data_path.join(BLANK_ESM), data_path.join(BLANK_ESP)];
2684
2685 assert_eq!(plugin_paths, result);
2686 }
2687}