1use std::collections::HashSet;
20use std::fs::create_dir_all;
21use std::path::Path;
22
23use unicase::{eq, UniCase};
24
25use super::mutable::MutableLoadOrder;
26use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase};
27use crate::enums::Error;
28use crate::plugin::Plugin;
29use crate::GameSettings;
30
31const MAX_ACTIVE_LIGHT_PLUGINS: usize = 4096;
32const MAX_ACTIVE_MEDIUM_PLUGINS: usize = 256;
33
34pub trait WritableLoadOrder: ReadableLoadOrder + std::fmt::Debug {
35 fn game_settings_mut(&mut self) -> &mut GameSettings;
36
37 fn load(&mut self) -> Result<(), Error>;
38
39 fn save(&mut self) -> Result<(), Error>;
40
41 fn add(&mut self, plugin_name: &str) -> Result<usize, Error>;
42
43 fn remove(&mut self, plugin_name: &str) -> Result<(), Error>;
44
45 fn set_load_order(&mut self, plugin_names: &[&str]) -> Result<(), Error>;
46
47 fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error>;
48
49 fn is_self_consistent(&self) -> Result<bool, Error>;
50
51 fn is_ambiguous(&self) -> Result<bool, Error>;
52
53 fn activate(&mut self, plugin_name: &str) -> Result<(), Error>;
54
55 fn deactivate(&mut self, plugin_name: &str) -> Result<(), Error>;
56
57 fn set_active_plugins(&mut self, active_plugin_names: &[&str]) -> Result<(), Error>;
58}
59
60pub(super) fn add<T: MutableLoadOrder>(
61 load_order: &mut T,
62 plugin_name: &str,
63) -> Result<usize, Error> {
64 if load_order.index_of(plugin_name).is_some() {
65 Err(Error::DuplicatePlugin(plugin_name.to_owned()))
66 } else {
67 let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
68
69 if let Some(position) = load_order.insert_position(&plugin) {
70 load_order.validate_index(&plugin, position)?;
71 load_order.plugins_mut().insert(position, plugin);
72 Ok(position)
73 } else {
74 load_order.validate_index(&plugin, load_order.plugins().len())?;
75 load_order.plugins_mut().push(plugin);
76 Ok(load_order.plugins().len() - 1)
77 }
78 }
79}
80
81pub(super) fn remove<T: MutableLoadOrder>(
82 load_order: &mut T,
83 plugin_name: &str,
84) -> Result<(), Error> {
85 match load_order.find_plugin_and_index(plugin_name) {
86 Some((index, plugin)) => {
87 let plugin_path = load_order.game_settings().plugin_path(plugin_name);
88 if plugin_path.exists() {
89 return Err(Error::InstalledPlugin(plugin_name.to_owned()));
90 }
91
92 if plugin.is_master_file() {
97 let next_master = &load_order
98 .plugins()
99 .iter()
100 .skip(index + 1)
101 .find(|p| p.is_master_file());
102
103 if let Some(next_master) = next_master {
104 let next_master_masters = next_master.masters()?;
105 let next_master_master_names: HashSet<_> =
106 next_master_masters.iter().map(UniCase::new).collect();
107
108 let mut masters = plugin.masters()?;
109
110 masters.retain(|m| !next_master_master_names.contains(&UniCase::new(m)));
112
113 if let Some(n) = masters.iter().find(|n| {
115 load_order
116 .find_plugin(n)
117 .is_some_and(|p| !p.is_master_file())
120 }) {
121 return Err(Error::NonMasterBeforeMaster {
122 master: plugin_name.to_owned(),
123 non_master: n.to_owned(),
124 });
125 }
126 }
127 }
128
129 load_order.plugins_mut().remove(index);
130
131 Ok(())
132 }
133 None => Err(Error::PluginNotFound(plugin_name.to_owned())),
134 }
135}
136
137#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
138struct PluginCounts {
139 light: usize,
140 medium: usize,
141 full: usize,
142}
143
144impl PluginCounts {
145 fn count_plugin(&mut self, plugin: &Plugin) {
146 if plugin.is_light_plugin() {
147 self.light += 1;
148 } else if plugin.is_medium_plugin() {
149 self.medium += 1;
150 } else {
151 self.full += 1;
152 }
153 }
154}
155
156fn count_active_plugins<T: ReadableLoadOrderBase>(load_order: &T) -> PluginCounts {
157 let mut counts = PluginCounts::default();
158
159 for plugin in load_order.plugins().iter().filter(|p| p.is_active()) {
160 counts.count_plugin(plugin);
161 }
162
163 counts
164}
165
166fn validate_plugin_counts(
167 counts: &PluginCounts,
168 max_active_full_plugins: usize,
169) -> Result<(), Error> {
170 if (counts.light > MAX_ACTIVE_LIGHT_PLUGINS)
171 || (counts.medium > MAX_ACTIVE_MEDIUM_PLUGINS)
172 || (counts.full > max_active_full_plugins)
173 {
174 Err(Error::TooManyActivePlugins {
175 light_count: counts.light,
176 medium_count: counts.medium,
177 full_count: counts.full,
178 })
179 } else {
180 Ok(())
181 }
182}
183
184fn count_plugins(existing_plugins: &[Plugin], existing_plugin_indexes: &[usize]) -> PluginCounts {
185 let mut counts = PluginCounts::default();
186
187 for index in existing_plugin_indexes {
188 if let Some(plugin) = existing_plugins.get(*index) {
189 counts.count_plugin(plugin);
190 }
191 }
192
193 counts
194}
195
196pub(super) fn activate<T: MutableLoadOrder>(
197 load_order: &mut T,
198 plugin_name: &str,
199) -> Result<(), Error> {
200 if load_order
201 .game_settings()
202 .supports_blueprint_ships_plugins()
203 {
204 return activate_with_blueprint_ships_plugin(load_order, plugin_name);
205 }
206
207 let mut counts = count_active_plugins(load_order);
208 let max_active_full_plugins = load_order.max_active_full_plugins();
209
210 let Some(plugin) = load_order.find_plugin_mut(plugin_name) else {
211 return Err(Error::PluginNotFound(plugin_name.to_owned()));
212 };
213
214 if !plugin.is_active() {
215 counts.count_plugin(plugin);
216
217 validate_plugin_counts(&counts, max_active_full_plugins)?;
218
219 plugin.activate()?;
220 }
221
222 Ok(())
223}
224
225fn activate_with_blueprint_ships_plugin<T: MutableLoadOrder>(
226 load_order: &mut T,
227 plugin_name: &str,
228) -> Result<(), Error> {
229 let Some((plugin_index, plugin)) = load_order.find_plugin_and_index(plugin_name) else {
230 return Err(Error::PluginNotFound(plugin_name.to_owned()));
231 };
232
233 if !plugin.is_active() {
234 let max_active_full_plugins = load_order.max_active_full_plugins();
235 let mut counts = count_active_plugins(load_order);
236
237 counts.count_plugin(plugin);
238
239 let blueprint_ships_plugin_index =
244 find_blueprint_ships_plugin_for_plugin(load_order, plugin_name)
245 .filter(|(_, p)| !p.is_active())
246 .inspect(|(_, p)| counts.count_plugin(p))
247 .map(|(i, _)| i);
248
249 validate_plugin_counts(&counts, max_active_full_plugins)?;
250
251 if let Some(plugin) = load_order.plugins_mut().get_mut(plugin_index) {
252 plugin.activate()?;
253 }
254
255 if let Some(index) = blueprint_ships_plugin_index {
256 if let Some(plugin) = load_order.plugins_mut().get_mut(index) {
257 plugin.activate()?;
258 }
259 }
260 }
261
262 Ok(())
263}
264
265fn find_blueprint_ships_plugin_for_plugin<'a, T: ReadableLoadOrderBase>(
266 load_order: &'a T,
267 plugin_name: &str,
268) -> Option<(usize, &'a Plugin)> {
269 if load_order
270 .game_settings()
271 .supports_blueprint_ships_plugins()
272 {
273 blueprint_ships_plugin_name(plugin_name).and_then(|n| load_order.find_plugin_and_index(&n))
274 } else {
275 None
276 }
277}
278
279pub(super) fn deactivate<T: MutableLoadOrder>(
280 load_order: &mut T,
281 plugin_name: &str,
282) -> Result<(), Error> {
283 if load_order.game_settings().is_implicitly_active(plugin_name) {
284 return Err(Error::ImplicitlyActivePlugin(plugin_name.to_owned()));
285 }
286
287 load_order
288 .find_plugin_mut(plugin_name)
289 .ok_or_else(|| Error::PluginNotFound(plugin_name.to_owned()))
290 .map(Plugin::deactivate)
291}
292
293pub(super) fn set_active_plugins<T: MutableLoadOrder>(
294 load_order: &mut T,
295 active_plugin_names: &[&str],
296) -> Result<(), Error> {
297 let existing_plugin_indices = load_order.lookup_plugins(active_plugin_names)?;
298
299 let counts = count_plugins(load_order.plugins(), &existing_plugin_indices);
300
301 validate_plugin_counts(&counts, load_order.max_active_full_plugins())?;
302
303 for plugin_name in load_order.game_settings().implicitly_active_plugins() {
304 validate_plugin_is_active(load_order, active_plugin_names, plugin_name)?;
307 }
308
309 if load_order
310 .game_settings()
311 .supports_blueprint_ships_plugins()
312 {
313 for active_plugin in active_plugin_names {
317 if let Some(blueprint_ships_plugin_name) = blueprint_ships_plugin_name(active_plugin) {
318 validate_plugin_is_active(
319 load_order,
320 active_plugin_names,
321 &blueprint_ships_plugin_name,
322 )?;
323 }
324 }
325 }
326
327 load_order.deactivate_all();
328
329 for index in existing_plugin_indices {
330 if let Some(plugin) = load_order.plugins_mut().get_mut(index) {
331 plugin.activate()?;
332 }
333 }
334
335 Ok(())
336}
337
338fn blueprint_ships_plugin_name(plugin_name: &str) -> Option<String> {
339 const EXTENSION_LENGTH: usize = 4;
341
342 plugin_name
343 .get(..plugin_name.len() - EXTENSION_LENGTH)
344 .map(|n| format!("BlueprintShips-{n}.esm"))
345}
346
347pub(super) fn blueprint_ships_base_plugin_name(blueprint_ships_plugin_name: &str) -> Option<&str> {
348 const BLUEPRINT_SHIPS_PREFIX: &str = "BlueprintShips-";
349 const BLUEPRINT_SHIPS_SUFFIX: &str = ".esm";
350
351 blueprint_ships_plugin_name
352 .split_at_checked(BLUEPRINT_SHIPS_PREFIX.len())
353 .filter(|(prefix, _)| BLUEPRINT_SHIPS_PREFIX.eq_ignore_ascii_case(prefix))
354 .and_then(|(_, remainder)| {
355 remainder
356 .split_at_checked(remainder.len() - BLUEPRINT_SHIPS_SUFFIX.len())
357 .filter(|(_, suffix)| BLUEPRINT_SHIPS_SUFFIX.eq_ignore_ascii_case(suffix))
358 .map(|(base, _)| base)
359 })
360}
361
362fn validate_plugin_is_active<T: MutableLoadOrder>(
363 load_order: &T,
364 active_plugin_names: &[&str],
365 plugin_name: &str,
366) -> Result<(), Error> {
367 if load_order.index_of(plugin_name).is_some()
368 && !active_plugin_names.iter().any(|p| eq(*p, plugin_name))
369 {
370 return Err(Error::ImplicitlyActivePlugin(plugin_name.to_owned()));
371 }
372
373 Ok(())
374}
375
376pub(super) fn create_parent_dirs(path: &Path) -> Result<(), Error> {
377 if let Some(x) = path.parent() {
378 if !x.exists() {
379 create_dir_all(x).map_err(|e| Error::IoError(x.to_path_buf(), e))?;
380 }
381 }
382 Ok(())
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 use std::fs::remove_file;
390
391 use tempfile::tempdir;
392
393 use crate::enums::GameId;
394 use crate::load_order::tests::{
395 game_settings_for_test, load_and_insert, mock_game_files, prepare_bulk_full_plugins,
396 prepare_bulk_plugins, prepend_early_loader, prepend_master, set_blueprint_flag,
397 set_master_flag,
398 };
399 use crate::tests::{copy_to_test_dir, NON_ASCII};
400
401 struct TestLoadOrder {
402 game_settings: GameSettings,
403 plugins: Vec<Plugin>,
404 }
405
406 impl ReadableLoadOrderBase for TestLoadOrder {
407 fn game_settings_base(&self) -> &GameSettings {
408 &self.game_settings
409 }
410
411 fn plugins(&self) -> &[Plugin] {
412 &self.plugins
413 }
414 }
415
416 impl MutableLoadOrder for TestLoadOrder {
417 fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
418 &mut self.plugins
419 }
420 }
421
422 fn prepare(game_id: GameId, game_dir: &Path) -> TestLoadOrder {
423 let mut game_settings = game_settings_for_test(game_id, game_dir);
424 mock_game_files(&mut game_settings);
425
426 let mut plugins = vec![Plugin::with_active("Blank.esp", &game_settings, true).unwrap()];
427
428 if game_id != GameId::Starfield {
429 plugins.push(Plugin::new("Blank - Different.esp", &game_settings).unwrap());
430 }
431
432 TestLoadOrder {
433 game_settings,
434 plugins,
435 }
436 }
437
438 fn prepare_bulk_medium_plugins(load_order: &mut TestLoadOrder) -> Vec<String> {
439 prepare_bulk_plugins(load_order, "Blank.medium.esm", 260, |i| {
440 format!("Blank{i}.medium.esm")
441 })
442 }
443
444 fn prepare_bulk_light_plugins(load_order: &mut TestLoadOrder) -> Vec<String> {
445 prepare_bulk_plugins(load_order, "Blank.small.esm", 5000, |i| {
446 format!("Blank{i}.small.esm")
447 })
448 }
449
450 #[test]
451 fn add_should_error_if_the_plugin_is_already_in_the_load_order() {
452 let tmp_dir = tempdir().unwrap();
453 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
454
455 assert!(add(&mut load_order, "Blank.esm").is_ok());
456 assert!(add(&mut load_order, "Blank.esm").is_err());
457 }
458
459 #[test]
460 fn add_should_error_if_given_a_master_that_would_hoist_a_non_master() {
461 let tmp_dir = tempdir().unwrap();
462 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
463
464 let plugins_dir = &load_order.game_settings().plugins_directory();
465 copy_to_test_dir(
466 "Blank - Different.esm",
467 "Blank - Different.esm",
468 load_order.game_settings(),
469 );
470 set_master_flag(
471 GameId::Oblivion,
472 &plugins_dir.join("Blank - Different.esm"),
473 false,
474 )
475 .unwrap();
476 assert!(add(&mut load_order, "Blank - Different.esm").is_ok());
477
478 copy_to_test_dir(
479 "Blank - Different Master Dependent.esm",
480 "Blank - Different Master Dependent.esm",
481 load_order.game_settings(),
482 );
483
484 assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_err());
485 }
486
487 #[test]
488 fn add_should_error_if_the_plugin_is_not_valid() {
489 let tmp_dir = tempdir().unwrap();
490 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
491
492 assert!(add(&mut load_order, "invalid.esm").is_err());
493 }
494
495 #[test]
496 fn add_should_insert_a_master_before_non_masters() {
497 let tmp_dir = tempdir().unwrap();
498 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
499
500 assert!(!load_order.plugins[1].is_master_file());
501
502 assert_eq!(0, add(&mut load_order, "Blank.esm").unwrap());
503 assert_eq!(0, load_order.index_of("Blank.esm").unwrap());
504 }
505
506 #[test]
507 fn add_should_append_a_non_master() {
508 let tmp_dir = tempdir().unwrap();
509 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
510
511 assert_eq!(
512 2,
513 add(&mut load_order, "Blank - Master Dependent.esp").unwrap()
514 );
515 assert_eq!(
516 2,
517 load_order.index_of("Blank - Master Dependent.esp").unwrap()
518 );
519 }
520
521 #[test]
522 fn add_should_hoist_a_non_master_that_a_master_depends_on() {
523 let tmp_dir = tempdir().unwrap();
524 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
525
526 let plugins_dir = &load_order.game_settings().plugins_directory();
527 copy_to_test_dir(
528 "Blank - Different Master Dependent.esm",
529 "Blank - Different Master Dependent.esm",
530 load_order.game_settings(),
531 );
532 assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_ok());
533
534 copy_to_test_dir(
535 "Blank - Different.esm",
536 "Blank - Different.esm",
537 load_order.game_settings(),
538 );
539 set_master_flag(
540 GameId::Oblivion,
541 &plugins_dir.join("Blank - Different.esm"),
542 false,
543 )
544 .unwrap();
545 assert_eq!(0, add(&mut load_order, "Blank - Different.esm").unwrap());
546 }
547
548 #[test]
549 fn add_should_hoist_a_master_that_a_master_depends_on() {
550 let tmp_dir = tempdir().unwrap();
551 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
552
553 let plugin_name = "Blank - Master Dependent.esm";
554 copy_to_test_dir(plugin_name, plugin_name, load_order.game_settings());
555 assert_eq!(0, add(&mut load_order, plugin_name).unwrap());
556
557 assert_eq!(0, add(&mut load_order, "Blank.esm").unwrap());
558 }
559
560 #[test]
561 fn remove_should_error_if_the_plugin_is_not_in_the_load_order() {
562 let tmp_dir = tempdir().unwrap();
563 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
564 assert!(remove(&mut load_order, "Blank.esm").is_err());
565 }
566
567 #[test]
568 fn remove_should_error_if_the_plugin_is_installed() {
569 let tmp_dir = tempdir().unwrap();
570 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
571 assert!(remove(&mut load_order, "Blank.esp").is_err());
572 }
573
574 #[test]
575 fn remove_should_error_if_removing_a_master_would_leave_a_non_master_it_hoisted_loading_too_early(
576 ) {
577 let tmp_dir = tempdir().unwrap();
578 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
579
580 prepend_master(&mut load_order);
581
582 let plugin_to_remove = "Blank - Different Master Dependent.esm";
583
584 let plugins_dir = &load_order.game_settings().plugins_directory();
585 copy_to_test_dir(
586 plugin_to_remove,
587 plugin_to_remove,
588 load_order.game_settings(),
589 );
590 assert!(add(&mut load_order, plugin_to_remove).is_ok());
591
592 copy_to_test_dir(
593 "Blank - Different.esm",
594 "Blank - Different.esm",
595 load_order.game_settings(),
596 );
597 set_master_flag(
598 GameId::Oblivion,
599 &plugins_dir.join("Blank - Different.esm"),
600 false,
601 )
602 .unwrap();
603 assert_eq!(1, add(&mut load_order, "Blank - Different.esm").unwrap());
604
605 copy_to_test_dir(
606 "Blank - Master Dependent.esm",
607 "Blank - Master Dependent.esm",
608 load_order.game_settings(),
609 );
610 assert!(add(&mut load_order, "Blank - Master Dependent.esm").is_ok());
611
612 let blank_master_dependent = load_order.plugins.remove(1);
613 load_order.plugins.insert(3, blank_master_dependent);
614
615 std::fs::remove_file(plugins_dir.join(plugin_to_remove)).unwrap();
616
617 match remove(&mut load_order, plugin_to_remove).unwrap_err() {
618 Error::NonMasterBeforeMaster { master, non_master } => {
619 assert_eq!("Blank - Different Master Dependent.esm", master);
620 assert_eq!("Blank - Different.esm", non_master);
621 }
622 e => panic!("Unexpected error type: {e:?}"),
623 }
624 }
625
626 #[test]
627 fn remove_should_allow_removal_of_a_master_that_depends_on_a_blueprint_plugin() {
628 let tmp_dir = tempdir().unwrap();
629 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
630
631 let plugins_dir = &load_order.game_settings().plugins_directory();
632
633 let plugin_to_remove = "Blank - Override.full.esm";
634 copy_to_test_dir(
635 plugin_to_remove,
636 plugin_to_remove,
637 load_order.game_settings(),
638 );
639 assert!(add(&mut load_order, plugin_to_remove).is_ok());
640
641 let blueprint_plugin = "Blank.full.esm";
642 copy_to_test_dir(
643 blueprint_plugin,
644 blueprint_plugin,
645 load_order.game_settings(),
646 );
647 set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
648 assert_eq!(2, add(&mut load_order, blueprint_plugin).unwrap());
649
650 let following_master_plugin = "Blank.medium.esm";
651 copy_to_test_dir(
652 following_master_plugin,
653 following_master_plugin,
654 load_order.game_settings(),
655 );
656 assert!(add(&mut load_order, following_master_plugin).is_ok());
657
658 std::fs::remove_file(plugins_dir.join(plugin_to_remove)).unwrap();
659
660 assert!(remove(&mut load_order, plugin_to_remove).is_ok());
661 }
662
663 #[test]
664 fn remove_should_remove_the_given_plugin_from_the_load_order() {
665 let tmp_dir = tempdir().unwrap();
666 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
667
668 remove_file(
669 load_order
670 .game_settings()
671 .plugins_directory()
672 .join("Blank.esp"),
673 )
674 .unwrap();
675
676 assert!(remove(&mut load_order, "Blank.esp").is_ok());
677 assert!(load_order.index_of("Blank.esp").is_none());
678 }
679
680 #[test]
681 fn activate_should_activate_the_plugin_with_the_given_filename() {
682 let tmp_dir = tempdir().unwrap();
683 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
684
685 assert!(activate(&mut load_order, "Blank - Different.esp").is_ok());
686 assert!(load_order.is_active("Blank - Different.esp"));
687 }
688
689 #[test]
690 fn activate_should_error_if_the_plugin_is_not_valid() {
691 let tmp_dir = tempdir().unwrap();
692 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
693
694 assert!(activate(&mut load_order, "missing.esp").is_err());
695 assert!(load_order.index_of("missing.esp").is_none());
696 }
697
698 #[test]
699 fn activate_should_error_if_the_plugin_is_not_already_in_the_load_order() {
700 let tmp_dir = tempdir().unwrap();
701 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
702
703 assert!(activate(&mut load_order, "Blank.esm").is_err());
704 assert!(!load_order.is_active("Blank.esm"));
705 }
706
707 #[test]
708 fn activate_should_be_case_insensitive() {
709 let tmp_dir = tempdir().unwrap();
710 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
711
712 assert!(activate(&mut load_order, "Blank - different.esp").is_ok());
713 assert!(load_order.is_active("Blank - Different.esp"));
714 }
715
716 #[test]
717 fn activate_should_throw_if_increasing_the_number_of_active_plugins_past_the_limit() {
718 let tmp_dir = tempdir().unwrap();
719 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
720
721 let plugins = prepare_bulk_full_plugins(&mut load_order);
722 for plugin in &plugins[..254] {
723 activate(&mut load_order, plugin).unwrap();
724 }
725
726 assert!(activate(&mut load_order, "Blank - Different.esp").is_err());
727 assert!(!load_order.is_active("Blank - Different.esp"));
728 }
729
730 #[test]
731 fn activate_should_succeed_if_at_the_active_plugins_limit_and_the_plugin_is_already_active() {
732 let tmp_dir = tempdir().unwrap();
733 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
734
735 let plugins = prepare_bulk_full_plugins(&mut load_order);
736 for plugin in &plugins[..254] {
737 activate(&mut load_order, plugin).unwrap();
738 }
739
740 assert!(load_order.is_active("Blank.esp"));
741 assert!(activate(&mut load_order, "Blank.esp").is_ok());
742 }
743
744 #[test]
745 fn activate_should_fail_if_at_the_active_plugins_limit_and_the_plugin_is_an_update_plugin() {
746 let tmp_dir = tempdir().unwrap();
747 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
748
749 let plugins = prepare_bulk_full_plugins(&mut load_order);
750 for plugin in &plugins[..254] {
751 activate(&mut load_order, plugin).unwrap();
752 }
753
754 let plugin = "Blank - Override.esp";
755 load_and_insert(&mut load_order, plugin);
756
757 assert!(!load_order.is_active(plugin));
758
759 assert!(activate(&mut load_order, plugin).is_err());
760 assert!(!load_order.is_active(plugin));
761 }
762
763 #[test]
764 fn activate_should_count_active_update_plugins_towards_limit() {
765 let tmp_dir = tempdir().unwrap();
766 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
767
768 let plugins = prepare_bulk_full_plugins(&mut load_order);
769 for plugin in &plugins[..254] {
770 activate(&mut load_order, plugin).unwrap();
771 }
772
773 let plugin = "Blank - Override.esp";
774 load_and_insert(&mut load_order, plugin);
775
776 assert!(!load_order.is_active(plugin));
777
778 assert!(activate(&mut load_order, plugin).is_err());
779 assert!(!load_order.is_active(plugin));
780 }
781
782 #[test]
783 fn activate_should_lower_the_full_plugin_limit_if_a_light_plugin_is_present() {
784 let tmp_dir = tempdir().unwrap();
785 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
786
787 let plugins = prepare_bulk_full_plugins(&mut load_order);
788 for plugin in &plugins[..252] {
789 activate(&mut load_order, plugin).unwrap();
790 }
791
792 let plugin = "Blank.small.esm";
793 load_and_insert(&mut load_order, plugin);
794 activate(&mut load_order, plugin).unwrap();
795
796 let plugin = &plugins[253];
797 assert!(!load_order.is_active(plugin));
798
799 assert!(activate(&mut load_order, plugin).is_ok());
800 assert!(load_order.is_active(plugin));
801
802 let plugin = &plugins[254];
803 assert!(!load_order.is_active(plugin));
804
805 assert!(activate(&mut load_order, plugin).is_err());
806 assert!(!load_order.is_active(plugin));
807 }
808
809 #[test]
810 fn activate_should_lower_the_full_plugin_limit_if_a_medium_plugin_is_present() {
811 let tmp_dir = tempdir().unwrap();
812 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
813
814 let plugins = prepare_bulk_full_plugins(&mut load_order);
815 for plugin in &plugins[..252] {
816 activate(&mut load_order, plugin).unwrap();
817 }
818
819 let plugin = "Blank.medium.esm";
820 load_and_insert(&mut load_order, plugin);
821 activate(&mut load_order, plugin).unwrap();
822
823 let plugin = &plugins[253];
824 assert!(!load_order.is_active(plugin));
825
826 assert!(activate(&mut load_order, plugin).is_ok());
827 assert!(load_order.is_active(plugin));
828
829 let plugin = &plugins[254];
830 assert!(!load_order.is_active(plugin));
831
832 assert!(activate(&mut load_order, plugin).is_err());
833 assert!(!load_order.is_active(plugin));
834 }
835
836 #[test]
837 fn activate_should_lower_the_full_plugin_limit_if_light_and_medium_plugins_are_present() {
838 let tmp_dir = tempdir().unwrap();
839 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
840
841 let plugins = prepare_bulk_full_plugins(&mut load_order);
842 for plugin in &plugins[..251] {
843 activate(&mut load_order, plugin).unwrap();
844 }
845
846 for plugin in ["Blank.medium.esm", "Blank.small.esm"] {
847 load_and_insert(&mut load_order, plugin);
848 activate(&mut load_order, plugin).unwrap();
849 }
850
851 let plugin = &plugins[252];
852 assert!(!load_order.is_active(plugin));
853
854 assert!(activate(&mut load_order, plugin).is_ok());
855 assert!(load_order.is_active(plugin));
856
857 let plugin = &plugins[253];
858 assert!(!load_order.is_active(plugin));
859
860 assert!(activate(&mut load_order, plugin).is_err());
861 assert!(!load_order.is_active(plugin));
862 }
863
864 #[test]
865 fn activate_should_check_full_medium_and_small_plugins_active_limits_separately() {
866 let tmp_dir = tempdir().unwrap();
867 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
868
869 let full = prepare_bulk_full_plugins(&mut load_order);
870 let medium = prepare_bulk_medium_plugins(&mut load_order);
871 let light = prepare_bulk_light_plugins(&mut load_order);
872
873 let mut plugin_refs = Vec::with_capacity(4603);
874 plugin_refs.extend(full[..252].iter().map(String::as_str));
875 plugin_refs.extend(medium[..255].iter().map(String::as_str));
876 plugin_refs.extend(light[..4095].iter().map(String::as_str));
877
878 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
879
880 let plugin = &full[252];
881 assert!(!load_order.is_active(plugin));
882 assert!(activate(&mut load_order, plugin).is_ok());
883 assert!(load_order.is_active(plugin));
884
885 let plugin = &medium[255];
886 assert!(!load_order.is_active(plugin));
887 assert!(activate(&mut load_order, plugin).is_ok());
888 assert!(load_order.is_active(plugin));
889
890 let plugin = &light[4095];
891 assert!(!load_order.is_active(plugin));
892 assert!(activate(&mut load_order, plugin).is_ok());
893 assert!(load_order.is_active(plugin));
894
895 let plugin = &full[253];
896 assert!(activate(&mut load_order, plugin).is_err());
897 assert!(!load_order.is_active(plugin));
898
899 let plugin = &medium[256];
900 assert!(activate(&mut load_order, plugin).is_err());
901 assert!(!load_order.is_active(plugin));
902
903 let plugin = &light[4096];
904 assert!(activate(&mut load_order, plugin).is_err());
905 assert!(!load_order.is_active(plugin));
906 }
907
908 #[test]
909 fn activate_with_blueprint_ship_base_plugin_should_also_activate_blueprint_ships_plugin() {
910 let tmp_dir = tempdir().unwrap();
911 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
912
913 let plugin_name = "Blank.esp";
914 deactivate(&mut load_order, plugin_name).unwrap();
915
916 let blueprint_ships = "BlueprintShips-Blank.esm";
917 copy_to_test_dir(
918 "Blank.full.esm",
919 blueprint_ships,
920 load_order.game_settings(),
921 );
922 add(&mut load_order, blueprint_ships).unwrap();
923
924 assert!(!load_order.is_active(plugin_name));
925 assert!(!load_order.is_active(blueprint_ships));
926
927 activate(&mut load_order, plugin_name).unwrap();
928
929 assert!(load_order.is_active(plugin_name));
930 assert!(load_order.is_active(blueprint_ships));
931 }
932
933 #[test]
934 fn activate_should_succeed_if_blueprint_ships_plugins_are_supported_but_not_present() {
935 let tmp_dir = tempdir().unwrap();
936 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
937
938 let plugin_name = "Blank.esp";
939 deactivate(&mut load_order, plugin_name).unwrap();
940
941 assert!(!load_order.is_active(plugin_name));
942
943 activate(&mut load_order, plugin_name).unwrap();
944
945 assert!(load_order.is_active(plugin_name));
946 }
947
948 #[test]
949 fn activate_with_blueprint_ship_base_plugin_should_count_activating_blueprint_ships_plugin() {
950 let tmp_dir = tempdir().unwrap();
951 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
952
953 let plugin_name = "Blank.esp";
954 deactivate(&mut load_order, plugin_name).unwrap();
955
956 let plugins = prepare_bulk_full_plugins(&mut load_order);
957 for plugin in &plugins[..254] {
958 activate(&mut load_order, plugin).unwrap();
959 }
960
961 let blueprint_ships = "BlueprintShips-Blank.esm";
962 copy_to_test_dir(
963 "Blank.full.esm",
964 blueprint_ships,
965 load_order.game_settings(),
966 );
967 add(&mut load_order, blueprint_ships).unwrap();
968
969 assert!(!load_order.is_active(plugin_name));
970 assert!(!load_order.is_active(blueprint_ships));
971
972 let err = activate(&mut load_order, plugin_name).unwrap_err();
973
974 match err {
975 Error::TooManyActivePlugins {
976 light_count,
977 medium_count,
978 full_count,
979 } => {
980 assert_eq!(0, light_count);
981 assert_eq!(0, medium_count);
982 assert_eq!(256, full_count);
983 }
984 e => panic!("Unexpected error type: {e:?}"),
985 }
986 }
987
988 #[test]
989 fn deactivate_should_deactivate_the_plugin_with_the_given_filename() {
990 let tmp_dir = tempdir().unwrap();
991 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
992
993 assert!(load_order.is_active("Blank.esp"));
994 assert!(deactivate(&mut load_order, "Blank.esp").is_ok());
995 assert!(!load_order.is_active("Blank.esp"));
996 }
997
998 #[test]
999 fn deactivate_should_error_if_the_plugin_is_not_in_the_load_order() {
1000 let tmp_dir = tempdir().unwrap();
1001 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1002
1003 assert!(deactivate(&mut load_order, "missing.esp").is_err());
1004 assert!(load_order.index_of("missing.esp").is_none());
1005 }
1006
1007 #[test]
1008 fn deactivate_should_error_if_given_an_implicitly_active_plugin() {
1009 let tmp_dir = tempdir().unwrap();
1010 let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1011
1012 prepend_early_loader(&mut load_order);
1013
1014 assert!(activate(&mut load_order, "Skyrim.esm").is_ok());
1015 assert!(deactivate(&mut load_order, "Skyrim.esm").is_err());
1016 assert!(load_order.is_active("Skyrim.esm"));
1017 }
1018
1019 #[test]
1020 fn deactivate_should_error_if_given_a_missing_implicitly_active_plugin() {
1021 let tmp_dir = tempdir().unwrap();
1022 let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
1023
1024 assert!(deactivate(&mut load_order, "Update.esm").is_err());
1025 assert!(load_order.index_of("Update.esm").is_none());
1026 }
1027
1028 #[test]
1029 fn deactivate_should_do_nothing_if_the_plugin_is_inactive() {
1030 let tmp_dir = tempdir().unwrap();
1031 let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
1032
1033 assert!(!load_order.is_active("Blank - Different.esp"));
1034 assert!(deactivate(&mut load_order, "Blank - Different.esp").is_ok());
1035 assert!(!load_order.is_active("Blank - Different.esp"));
1036 }
1037
1038 #[test]
1039 fn set_active_plugins_should_error_if_passed_an_invalid_plugin_name() {
1040 let tmp_dir = tempdir().unwrap();
1041 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1042
1043 let active_plugins = ["missing.esp"];
1044 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1045 assert_eq!(1, load_order.active_plugin_names().len());
1046 }
1047
1048 #[test]
1049 fn set_active_plugins_should_error_if_the_given_plugins_are_missing_implicitly_active_plugins()
1050 {
1051 let tmp_dir = tempdir().unwrap();
1052 let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1053
1054 prepend_early_loader(&mut load_order);
1055
1056 let active_plugins = ["Blank.esp"];
1057 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1058 assert_eq!(1, load_order.active_plugin_names().len());
1059 }
1060
1061 #[test]
1062 fn set_active_plugins_should_error_if_a_missing_implicitly_active_plugin_is_given() {
1063 let tmp_dir = tempdir().unwrap();
1064 let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
1065
1066 let active_plugins = ["Update.esm", "Blank.esp"];
1067 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1068 assert_eq!(1, load_order.active_plugin_names().len());
1069 }
1070
1071 #[test]
1072 fn set_active_plugins_should_error_if_given_plugins_not_in_the_load_order() {
1073 let tmp_dir = tempdir().unwrap();
1074 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1075
1076 let active_plugins = ["Blank - Master Dependent.esp", NON_ASCII];
1077 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1078 assert!(!load_order.is_active("Blank - Master Dependent.esp"));
1079 assert!(load_order
1080 .index_of("Blank - Master Dependent.esp")
1081 .is_none());
1082 assert!(!load_order.is_active(NON_ASCII));
1083 assert!(load_order.index_of(NON_ASCII).is_none());
1084 }
1085
1086 #[test]
1087 fn set_active_plugins_should_deactivate_all_plugins_not_given() {
1088 let tmp_dir = tempdir().unwrap();
1089 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1090
1091 let active_plugins = ["Blank - Different.esp"];
1092 assert!(load_order.is_active("Blank.esp"));
1093 assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
1094 assert!(!load_order.is_active("Blank.esp"));
1095 }
1096
1097 #[test]
1098 fn set_active_plugins_should_activate_all_given_plugins() {
1099 let tmp_dir = tempdir().unwrap();
1100 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1101
1102 let active_plugins = ["Blank - Different.esp"];
1103 assert!(!load_order.is_active("Blank - Different.esp"));
1104 assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
1105 assert!(load_order.is_active("Blank - Different.esp"));
1106 }
1107
1108 #[test]
1109 fn set_active_plugins_should_count_update_plugins_towards_limit() {
1110 let tmp_dir = tempdir().unwrap();
1111 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1112
1113 let blank_override = "Blank - Override.esp";
1114 load_and_insert(&mut load_order, blank_override);
1115
1116 let mut active_plugins = vec![blank_override.to_owned()];
1117
1118 let plugins = prepare_bulk_full_plugins(&mut load_order);
1119 for plugin in plugins.into_iter().take(255) {
1120 active_plugins.push(plugin);
1121 }
1122
1123 let active_plugins: Vec<&str> = active_plugins
1124 .iter()
1125 .map(std::string::String::as_str)
1126 .collect();
1127
1128 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1129 assert_eq!(1, load_order.active_plugin_names().len());
1130 }
1131
1132 #[test]
1133 fn set_active_plugins_should_lower_the_full_plugin_limit_if_a_light_plugin_is_present() {
1134 let tmp_dir = tempdir().unwrap();
1135 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1136
1137 let full = prepare_bulk_full_plugins(&mut load_order);
1138
1139 let plugin = "Blank.small.esm";
1140 load_and_insert(&mut load_order, plugin);
1141
1142 let mut plugin_refs = vec![plugin];
1143 plugin_refs.extend(full[..254].iter().map(String::as_str));
1144
1145 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1146 assert_eq!(255, load_order.active_plugin_names().len());
1147
1148 plugin_refs.push(full[254].as_str());
1149
1150 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1151 assert_eq!(255, load_order.active_plugin_names().len());
1152 }
1153
1154 #[test]
1155 fn set_active_plugins_should_lower_the_full_plugin_limit_if_a_medium_plugin_is_present() {
1156 let tmp_dir = tempdir().unwrap();
1157 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1158
1159 let full = prepare_bulk_full_plugins(&mut load_order);
1160
1161 let plugin = "Blank.medium.esm";
1162 load_and_insert(&mut load_order, plugin);
1163
1164 let mut plugin_refs = vec![plugin];
1165 plugin_refs.extend(full[..254].iter().map(String::as_str));
1166
1167 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1168 assert_eq!(255, load_order.active_plugin_names().len());
1169
1170 plugin_refs.push(full[254].as_str());
1171
1172 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1173 assert_eq!(255, load_order.active_plugin_names().len());
1174 }
1175
1176 #[test]
1177 fn set_active_plugins_should_lower_the_full_plugin_limit_if_light_and_plugins_are_present() {
1178 let tmp_dir = tempdir().unwrap();
1179 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1180
1181 let full = prepare_bulk_full_plugins(&mut load_order);
1182
1183 let medium_plugin = "Blank.medium.esm";
1184 let light_plugin = "Blank.small.esm";
1185 load_and_insert(&mut load_order, medium_plugin);
1186 load_and_insert(&mut load_order, light_plugin);
1187
1188 let mut plugin_refs = vec![medium_plugin, light_plugin];
1189 plugin_refs.extend(full[..253].iter().map(String::as_str));
1190
1191 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1192 assert_eq!(255, load_order.active_plugin_names().len());
1193
1194 plugin_refs.push(full[253].as_str());
1195
1196 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1197 assert_eq!(255, load_order.active_plugin_names().len());
1198 }
1199
1200 #[test]
1201 fn set_active_plugins_should_count_full_medium_and_small_plugins_separately() {
1202 let tmp_dir = tempdir().unwrap();
1203 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1204
1205 let full = prepare_bulk_full_plugins(&mut load_order);
1206 let medium = prepare_bulk_medium_plugins(&mut load_order);
1207 let light = prepare_bulk_light_plugins(&mut load_order);
1208
1209 let mut plugin_refs = Vec::with_capacity(4064);
1210 plugin_refs.extend(full[..252].iter().map(String::as_str));
1211 plugin_refs.extend(medium[..256].iter().map(String::as_str));
1212 plugin_refs.extend(light[..4096].iter().map(String::as_str));
1213
1214 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1215 assert_eq!(4604, load_order.active_plugin_names().len());
1216 }
1217
1218 #[test]
1219 fn set_active_plugins_should_error_if_given_more_than_254_full_plugins() {
1220 let tmp_dir = tempdir().unwrap();
1221 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1222
1223 let full = prepare_bulk_full_plugins(&mut load_order);
1224
1225 let plugin_refs: Vec<_> = full[..256].iter().map(String::as_str).collect();
1226
1227 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1228 assert_eq!(1, load_order.active_plugin_names().len());
1229 }
1230
1231 #[test]
1232 fn set_active_plugins_should_error_if_given_more_than_256_medium_plugins() {
1233 let tmp_dir = tempdir().unwrap();
1234 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1235
1236 let medium = prepare_bulk_medium_plugins(&mut load_order);
1237
1238 let plugin_refs: Vec<_> = medium[..257].iter().map(String::as_str).collect();
1239
1240 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1241 assert_eq!(1, load_order.active_plugin_names().len());
1242 }
1243
1244 #[test]
1245 fn set_active_plugins_should_error_if_given_more_than_4096_light_plugins() {
1246 let tmp_dir = tempdir().unwrap();
1247 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1248
1249 let light = prepare_bulk_light_plugins(&mut load_order);
1250
1251 let plugin_refs: Vec<_> = light[..4097].iter().map(String::as_str).collect();
1252
1253 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1254 assert_eq!(1, load_order.active_plugin_names().len());
1255 }
1256
1257 #[test]
1258 fn set_active_plugins_should_error_if_an_implicitly_active_blueprint_ships_plugin_is_not_given()
1259 {
1260 let tmp_dir = tempdir().unwrap();
1261 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1262
1263 let blueprint_ships = "BlueprintShips-Blank.esm";
1264 copy_to_test_dir(
1265 "Blank.full.esm",
1266 blueprint_ships,
1267 load_order.game_settings(),
1268 );
1269 add(&mut load_order, blueprint_ships).unwrap();
1270
1271 let err = set_active_plugins(&mut load_order, &["Blank.esp"]).unwrap_err();
1272
1273 match err {
1274 Error::ImplicitlyActivePlugin(n) => assert_eq!(blueprint_ships, n),
1275 e => panic!("Unexpected error type: {e:?}"),
1276 }
1277 }
1278}