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 count_plugins(existing_plugins: &[Plugin], existing_plugin_indexes: &[usize]) -> PluginCounts {
167 let mut counts = PluginCounts::default();
168
169 for index in existing_plugin_indexes {
170 if let Some(plugin) = existing_plugins.get(*index) {
171 counts.count_plugin(plugin);
172 }
173 }
174
175 counts
176}
177
178pub(super) fn activate<T: MutableLoadOrder>(
179 load_order: &mut T,
180 plugin_name: &str,
181) -> Result<(), Error> {
182 let counts = count_active_plugins(load_order);
183 let max_active_full_plugins = load_order.max_active_full_plugins();
184
185 let Some(plugin) = load_order.find_plugin_mut(plugin_name) else {
186 return Err(Error::PluginNotFound(plugin_name.to_owned()));
187 };
188
189 if !plugin.is_active() {
190 let is_light = plugin.is_light_plugin();
191 let is_medium = plugin.is_medium_plugin();
192 let is_full = !is_light && !is_medium;
193
194 if (is_light && counts.light == MAX_ACTIVE_LIGHT_PLUGINS)
195 || (is_medium && counts.medium == MAX_ACTIVE_MEDIUM_PLUGINS)
196 || (is_full && counts.full == max_active_full_plugins)
197 {
198 return Err(Error::TooManyActivePlugins {
199 light_count: counts.light,
200 medium_count: counts.medium,
201 full_count: counts.full,
202 });
203 }
204
205 plugin.activate()?;
206 }
207
208 Ok(())
209}
210
211pub(super) fn deactivate<T: MutableLoadOrder>(
212 load_order: &mut T,
213 plugin_name: &str,
214) -> Result<(), Error> {
215 if load_order.game_settings().is_implicitly_active(plugin_name) {
216 return Err(Error::ImplicitlyActivePlugin(plugin_name.to_owned()));
217 }
218
219 load_order
220 .find_plugin_mut(plugin_name)
221 .ok_or_else(|| Error::PluginNotFound(plugin_name.to_owned()))
222 .map(Plugin::deactivate)
223}
224
225pub(super) fn set_active_plugins<T: MutableLoadOrder>(
226 load_order: &mut T,
227 active_plugin_names: &[&str],
228) -> Result<(), Error> {
229 let existing_plugin_indices = load_order.lookup_plugins(active_plugin_names)?;
230
231 let counts = count_plugins(load_order.plugins(), &existing_plugin_indices);
232
233 if counts.full > load_order.max_active_full_plugins()
234 || counts.medium > MAX_ACTIVE_MEDIUM_PLUGINS
235 || counts.light > MAX_ACTIVE_LIGHT_PLUGINS
236 {
237 return Err(Error::TooManyActivePlugins {
238 light_count: counts.light,
239 medium_count: counts.medium,
240 full_count: counts.full,
241 });
242 }
243
244 for plugin_name in load_order.game_settings().implicitly_active_plugins() {
245 if load_order.index_of(plugin_name).is_some()
248 && !active_plugin_names.iter().any(|p| eq(*p, plugin_name))
249 {
250 return Err(Error::ImplicitlyActivePlugin(plugin_name.clone()));
251 }
252 }
253
254 load_order.deactivate_all();
255
256 for index in existing_plugin_indices {
257 if let Some(plugin) = load_order.plugins_mut().get_mut(index) {
258 plugin.activate()?;
259 }
260 }
261
262 Ok(())
263}
264
265pub(super) fn create_parent_dirs(path: &Path) -> Result<(), Error> {
266 if let Some(x) = path.parent() {
267 if !x.exists() {
268 create_dir_all(x).map_err(|e| Error::IoError(x.to_path_buf(), e))?;
269 }
270 }
271 Ok(())
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 use std::fs::remove_file;
279
280 use tempfile::tempdir;
281
282 use crate::enums::GameId;
283 use crate::load_order::tests::{
284 game_settings_for_test, load_and_insert, mock_game_files, prepare_bulk_full_plugins,
285 prepare_bulk_plugins, prepend_early_loader, prepend_master, set_blueprint_flag,
286 set_master_flag,
287 };
288 use crate::tests::{copy_to_test_dir, NON_ASCII};
289
290 struct TestLoadOrder {
291 game_settings: GameSettings,
292 plugins: Vec<Plugin>,
293 }
294
295 impl ReadableLoadOrderBase for TestLoadOrder {
296 fn game_settings_base(&self) -> &GameSettings {
297 &self.game_settings
298 }
299
300 fn plugins(&self) -> &[Plugin] {
301 &self.plugins
302 }
303 }
304
305 impl MutableLoadOrder for TestLoadOrder {
306 fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
307 &mut self.plugins
308 }
309 }
310
311 fn prepare(game_id: GameId, game_dir: &Path) -> TestLoadOrder {
312 let mut game_settings = game_settings_for_test(game_id, game_dir);
313 mock_game_files(&mut game_settings);
314
315 let mut plugins = vec![Plugin::with_active("Blank.esp", &game_settings, true).unwrap()];
316
317 if game_id != GameId::Starfield {
318 plugins.push(Plugin::new("Blank - Different.esp", &game_settings).unwrap());
319 }
320
321 TestLoadOrder {
322 game_settings,
323 plugins,
324 }
325 }
326
327 fn prepare_bulk_medium_plugins(load_order: &mut TestLoadOrder) -> Vec<String> {
328 prepare_bulk_plugins(load_order, "Blank.medium.esm", 260, |i| {
329 format!("Blank{i}.medium.esm")
330 })
331 }
332
333 fn prepare_bulk_light_plugins(load_order: &mut TestLoadOrder) -> Vec<String> {
334 prepare_bulk_plugins(load_order, "Blank.small.esm", 5000, |i| {
335 format!("Blank{i}.small.esm")
336 })
337 }
338
339 #[test]
340 fn add_should_error_if_the_plugin_is_already_in_the_load_order() {
341 let tmp_dir = tempdir().unwrap();
342 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
343
344 assert!(add(&mut load_order, "Blank.esm").is_ok());
345 assert!(add(&mut load_order, "Blank.esm").is_err());
346 }
347
348 #[test]
349 fn add_should_error_if_given_a_master_that_would_hoist_a_non_master() {
350 let tmp_dir = tempdir().unwrap();
351 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
352
353 let plugins_dir = &load_order.game_settings().plugins_directory();
354 copy_to_test_dir(
355 "Blank - Different.esm",
356 "Blank - Different.esm",
357 load_order.game_settings(),
358 );
359 set_master_flag(
360 GameId::Oblivion,
361 &plugins_dir.join("Blank - Different.esm"),
362 false,
363 )
364 .unwrap();
365 assert!(add(&mut load_order, "Blank - Different.esm").is_ok());
366
367 copy_to_test_dir(
368 "Blank - Different Master Dependent.esm",
369 "Blank - Different Master Dependent.esm",
370 load_order.game_settings(),
371 );
372
373 assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_err());
374 }
375
376 #[test]
377 fn add_should_error_if_the_plugin_is_not_valid() {
378 let tmp_dir = tempdir().unwrap();
379 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
380
381 assert!(add(&mut load_order, "invalid.esm").is_err());
382 }
383
384 #[test]
385 fn add_should_insert_a_master_before_non_masters() {
386 let tmp_dir = tempdir().unwrap();
387 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
388
389 assert!(!load_order.plugins[1].is_master_file());
390
391 assert_eq!(0, add(&mut load_order, "Blank.esm").unwrap());
392 assert_eq!(0, load_order.index_of("Blank.esm").unwrap());
393 }
394
395 #[test]
396 fn add_should_append_a_non_master() {
397 let tmp_dir = tempdir().unwrap();
398 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
399
400 assert_eq!(
401 2,
402 add(&mut load_order, "Blank - Master Dependent.esp").unwrap()
403 );
404 assert_eq!(
405 2,
406 load_order.index_of("Blank - Master Dependent.esp").unwrap()
407 );
408 }
409
410 #[test]
411 fn add_should_hoist_a_non_master_that_a_master_depends_on() {
412 let tmp_dir = tempdir().unwrap();
413 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
414
415 let plugins_dir = &load_order.game_settings().plugins_directory();
416 copy_to_test_dir(
417 "Blank - Different Master Dependent.esm",
418 "Blank - Different Master Dependent.esm",
419 load_order.game_settings(),
420 );
421 assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_ok());
422
423 copy_to_test_dir(
424 "Blank - Different.esm",
425 "Blank - Different.esm",
426 load_order.game_settings(),
427 );
428 set_master_flag(
429 GameId::Oblivion,
430 &plugins_dir.join("Blank - Different.esm"),
431 false,
432 )
433 .unwrap();
434 assert_eq!(0, add(&mut load_order, "Blank - Different.esm").unwrap());
435 }
436
437 #[test]
438 fn add_should_hoist_a_master_that_a_master_depends_on() {
439 let tmp_dir = tempdir().unwrap();
440 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
441
442 let plugin_name = "Blank - Master Dependent.esm";
443 copy_to_test_dir(plugin_name, plugin_name, load_order.game_settings());
444 assert_eq!(0, add(&mut load_order, plugin_name).unwrap());
445
446 assert_eq!(0, add(&mut load_order, "Blank.esm").unwrap());
447 }
448
449 #[test]
450 fn remove_should_error_if_the_plugin_is_not_in_the_load_order() {
451 let tmp_dir = tempdir().unwrap();
452 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
453 assert!(remove(&mut load_order, "Blank.esm").is_err());
454 }
455
456 #[test]
457 fn remove_should_error_if_the_plugin_is_installed() {
458 let tmp_dir = tempdir().unwrap();
459 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
460 assert!(remove(&mut load_order, "Blank.esp").is_err());
461 }
462
463 #[test]
464 fn remove_should_error_if_removing_a_master_would_leave_a_non_master_it_hoisted_loading_too_early(
465 ) {
466 let tmp_dir = tempdir().unwrap();
467 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
468
469 prepend_master(&mut load_order);
470
471 let plugin_to_remove = "Blank - Different Master Dependent.esm";
472
473 let plugins_dir = &load_order.game_settings().plugins_directory();
474 copy_to_test_dir(
475 plugin_to_remove,
476 plugin_to_remove,
477 load_order.game_settings(),
478 );
479 assert!(add(&mut load_order, plugin_to_remove).is_ok());
480
481 copy_to_test_dir(
482 "Blank - Different.esm",
483 "Blank - Different.esm",
484 load_order.game_settings(),
485 );
486 set_master_flag(
487 GameId::Oblivion,
488 &plugins_dir.join("Blank - Different.esm"),
489 false,
490 )
491 .unwrap();
492 assert_eq!(1, add(&mut load_order, "Blank - Different.esm").unwrap());
493
494 copy_to_test_dir(
495 "Blank - Master Dependent.esm",
496 "Blank - Master Dependent.esm",
497 load_order.game_settings(),
498 );
499 assert!(add(&mut load_order, "Blank - Master Dependent.esm").is_ok());
500
501 let blank_master_dependent = load_order.plugins.remove(1);
502 load_order.plugins.insert(3, blank_master_dependent);
503
504 std::fs::remove_file(plugins_dir.join(plugin_to_remove)).unwrap();
505
506 match remove(&mut load_order, plugin_to_remove).unwrap_err() {
507 Error::NonMasterBeforeMaster { master, non_master } => {
508 assert_eq!("Blank - Different Master Dependent.esm", master);
509 assert_eq!("Blank - Different.esm", non_master);
510 }
511 e => panic!("Unexpected error type: {e:?}"),
512 }
513 }
514
515 #[test]
516 fn remove_should_allow_removal_of_a_master_that_depends_on_a_blueprint_plugin() {
517 let tmp_dir = tempdir().unwrap();
518 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
519
520 let plugins_dir = &load_order.game_settings().plugins_directory();
521
522 let plugin_to_remove = "Blank - Override.full.esm";
523 copy_to_test_dir(
524 plugin_to_remove,
525 plugin_to_remove,
526 load_order.game_settings(),
527 );
528 assert!(add(&mut load_order, plugin_to_remove).is_ok());
529
530 let blueprint_plugin = "Blank.full.esm";
531 copy_to_test_dir(
532 blueprint_plugin,
533 blueprint_plugin,
534 load_order.game_settings(),
535 );
536 set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
537 assert_eq!(2, add(&mut load_order, blueprint_plugin).unwrap());
538
539 let following_master_plugin = "Blank.medium.esm";
540 copy_to_test_dir(
541 following_master_plugin,
542 following_master_plugin,
543 load_order.game_settings(),
544 );
545 assert!(add(&mut load_order, following_master_plugin).is_ok());
546
547 std::fs::remove_file(plugins_dir.join(plugin_to_remove)).unwrap();
548
549 assert!(remove(&mut load_order, plugin_to_remove).is_ok());
550 }
551
552 #[test]
553 fn remove_should_remove_the_given_plugin_from_the_load_order() {
554 let tmp_dir = tempdir().unwrap();
555 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
556
557 remove_file(
558 load_order
559 .game_settings()
560 .plugins_directory()
561 .join("Blank.esp"),
562 )
563 .unwrap();
564
565 assert!(remove(&mut load_order, "Blank.esp").is_ok());
566 assert!(load_order.index_of("Blank.esp").is_none());
567 }
568
569 #[test]
570 fn activate_should_activate_the_plugin_with_the_given_filename() {
571 let tmp_dir = tempdir().unwrap();
572 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
573
574 assert!(activate(&mut load_order, "Blank - Different.esp").is_ok());
575 assert!(load_order.is_active("Blank - Different.esp"));
576 }
577
578 #[test]
579 fn activate_should_error_if_the_plugin_is_not_valid() {
580 let tmp_dir = tempdir().unwrap();
581 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
582
583 assert!(activate(&mut load_order, "missing.esp").is_err());
584 assert!(load_order.index_of("missing.esp").is_none());
585 }
586
587 #[test]
588 fn activate_should_error_if_the_plugin_is_not_already_in_the_load_order() {
589 let tmp_dir = tempdir().unwrap();
590 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
591
592 assert!(activate(&mut load_order, "Blank.esm").is_err());
593 assert!(!load_order.is_active("Blank.esm"));
594 }
595
596 #[test]
597 fn activate_should_be_case_insensitive() {
598 let tmp_dir = tempdir().unwrap();
599 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
600
601 assert!(activate(&mut load_order, "Blank - different.esp").is_ok());
602 assert!(load_order.is_active("Blank - Different.esp"));
603 }
604
605 #[test]
606 fn activate_should_throw_if_increasing_the_number_of_active_plugins_past_the_limit() {
607 let tmp_dir = tempdir().unwrap();
608 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
609
610 let plugins = prepare_bulk_full_plugins(&mut load_order);
611 for plugin in &plugins[..254] {
612 activate(&mut load_order, plugin).unwrap();
613 }
614
615 assert!(activate(&mut load_order, "Blank - Different.esp").is_err());
616 assert!(!load_order.is_active("Blank - Different.esp"));
617 }
618
619 #[test]
620 fn activate_should_succeed_if_at_the_active_plugins_limit_and_the_plugin_is_already_active() {
621 let tmp_dir = tempdir().unwrap();
622 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
623
624 let plugins = prepare_bulk_full_plugins(&mut load_order);
625 for plugin in &plugins[..254] {
626 activate(&mut load_order, plugin).unwrap();
627 }
628
629 assert!(load_order.is_active("Blank.esp"));
630 assert!(activate(&mut load_order, "Blank.esp").is_ok());
631 }
632
633 #[test]
634 fn activate_should_fail_if_at_the_active_plugins_limit_and_the_plugin_is_an_update_plugin() {
635 let tmp_dir = tempdir().unwrap();
636 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
637
638 let plugins = prepare_bulk_full_plugins(&mut load_order);
639 for plugin in &plugins[..254] {
640 activate(&mut load_order, plugin).unwrap();
641 }
642
643 let plugin = "Blank - Override.esp";
644 load_and_insert(&mut load_order, plugin);
645
646 assert!(!load_order.is_active(plugin));
647
648 assert!(activate(&mut load_order, plugin).is_err());
649 assert!(!load_order.is_active(plugin));
650 }
651
652 #[test]
653 fn activate_should_count_active_update_plugins_towards_limit() {
654 let tmp_dir = tempdir().unwrap();
655 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
656
657 let plugins = prepare_bulk_full_plugins(&mut load_order);
658 for plugin in &plugins[..254] {
659 activate(&mut load_order, plugin).unwrap();
660 }
661
662 let plugin = "Blank - Override.esp";
663 load_and_insert(&mut load_order, plugin);
664
665 assert!(!load_order.is_active(plugin));
666
667 assert!(activate(&mut load_order, plugin).is_err());
668 assert!(!load_order.is_active(plugin));
669 }
670
671 #[test]
672 fn activate_should_lower_the_full_plugin_limit_if_a_light_plugin_is_present() {
673 let tmp_dir = tempdir().unwrap();
674 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
675
676 let plugins = prepare_bulk_full_plugins(&mut load_order);
677 for plugin in &plugins[..252] {
678 activate(&mut load_order, plugin).unwrap();
679 }
680
681 let plugin = "Blank.small.esm";
682 load_and_insert(&mut load_order, plugin);
683 activate(&mut load_order, plugin).unwrap();
684
685 let plugin = &plugins[253];
686 assert!(!load_order.is_active(plugin));
687
688 assert!(activate(&mut load_order, plugin).is_ok());
689 assert!(load_order.is_active(plugin));
690
691 let plugin = &plugins[254];
692 assert!(!load_order.is_active(plugin));
693
694 assert!(activate(&mut load_order, plugin).is_err());
695 assert!(!load_order.is_active(plugin));
696 }
697
698 #[test]
699 fn activate_should_lower_the_full_plugin_limit_if_a_medium_plugin_is_present() {
700 let tmp_dir = tempdir().unwrap();
701 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
702
703 let plugins = prepare_bulk_full_plugins(&mut load_order);
704 for plugin in &plugins[..252] {
705 activate(&mut load_order, plugin).unwrap();
706 }
707
708 let plugin = "Blank.medium.esm";
709 load_and_insert(&mut load_order, plugin);
710 activate(&mut load_order, plugin).unwrap();
711
712 let plugin = &plugins[253];
713 assert!(!load_order.is_active(plugin));
714
715 assert!(activate(&mut load_order, plugin).is_ok());
716 assert!(load_order.is_active(plugin));
717
718 let plugin = &plugins[254];
719 assert!(!load_order.is_active(plugin));
720
721 assert!(activate(&mut load_order, plugin).is_err());
722 assert!(!load_order.is_active(plugin));
723 }
724
725 #[test]
726 fn activate_should_lower_the_full_plugin_limit_if_light_and_medium_plugins_are_present() {
727 let tmp_dir = tempdir().unwrap();
728 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
729
730 let plugins = prepare_bulk_full_plugins(&mut load_order);
731 for plugin in &plugins[..251] {
732 activate(&mut load_order, plugin).unwrap();
733 }
734
735 for plugin in ["Blank.medium.esm", "Blank.small.esm"] {
736 load_and_insert(&mut load_order, plugin);
737 activate(&mut load_order, plugin).unwrap();
738 }
739
740 let plugin = &plugins[252];
741 assert!(!load_order.is_active(plugin));
742
743 assert!(activate(&mut load_order, plugin).is_ok());
744 assert!(load_order.is_active(plugin));
745
746 let plugin = &plugins[253];
747 assert!(!load_order.is_active(plugin));
748
749 assert!(activate(&mut load_order, plugin).is_err());
750 assert!(!load_order.is_active(plugin));
751 }
752
753 #[test]
754 fn activate_should_check_full_medium_and_small_plugins_active_limits_separately() {
755 let tmp_dir = tempdir().unwrap();
756 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
757
758 let full = prepare_bulk_full_plugins(&mut load_order);
759 let medium = prepare_bulk_medium_plugins(&mut load_order);
760 let light = prepare_bulk_light_plugins(&mut load_order);
761
762 let mut plugin_refs = Vec::with_capacity(4603);
763 plugin_refs.extend(full[..252].iter().map(String::as_str));
764 plugin_refs.extend(medium[..255].iter().map(String::as_str));
765 plugin_refs.extend(light[..4095].iter().map(String::as_str));
766
767 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
768
769 let plugin = &full[252];
770 assert!(!load_order.is_active(plugin));
771 assert!(activate(&mut load_order, plugin).is_ok());
772 assert!(load_order.is_active(plugin));
773
774 let plugin = &medium[255];
775 assert!(!load_order.is_active(plugin));
776 assert!(activate(&mut load_order, plugin).is_ok());
777 assert!(load_order.is_active(plugin));
778
779 let plugin = &light[4095];
780 assert!(!load_order.is_active(plugin));
781 assert!(activate(&mut load_order, plugin).is_ok());
782 assert!(load_order.is_active(plugin));
783
784 let plugin = &full[253];
785 assert!(activate(&mut load_order, plugin).is_err());
786 assert!(!load_order.is_active(plugin));
787
788 let plugin = &medium[256];
789 assert!(activate(&mut load_order, plugin).is_err());
790 assert!(!load_order.is_active(plugin));
791
792 let plugin = &light[4096];
793 assert!(activate(&mut load_order, plugin).is_err());
794 assert!(!load_order.is_active(plugin));
795 }
796
797 #[test]
798 fn deactivate_should_deactivate_the_plugin_with_the_given_filename() {
799 let tmp_dir = tempdir().unwrap();
800 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
801
802 assert!(load_order.is_active("Blank.esp"));
803 assert!(deactivate(&mut load_order, "Blank.esp").is_ok());
804 assert!(!load_order.is_active("Blank.esp"));
805 }
806
807 #[test]
808 fn deactivate_should_error_if_the_plugin_is_not_in_the_load_order() {
809 let tmp_dir = tempdir().unwrap();
810 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
811
812 assert!(deactivate(&mut load_order, "missing.esp").is_err());
813 assert!(load_order.index_of("missing.esp").is_none());
814 }
815
816 #[test]
817 fn deactivate_should_error_if_given_an_implicitly_active_plugin() {
818 let tmp_dir = tempdir().unwrap();
819 let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
820
821 prepend_early_loader(&mut load_order);
822
823 assert!(activate(&mut load_order, "Skyrim.esm").is_ok());
824 assert!(deactivate(&mut load_order, "Skyrim.esm").is_err());
825 assert!(load_order.is_active("Skyrim.esm"));
826 }
827
828 #[test]
829 fn deactivate_should_error_if_given_a_missing_implicitly_active_plugin() {
830 let tmp_dir = tempdir().unwrap();
831 let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
832
833 assert!(deactivate(&mut load_order, "Update.esm").is_err());
834 assert!(load_order.index_of("Update.esm").is_none());
835 }
836
837 #[test]
838 fn deactivate_should_do_nothing_if_the_plugin_is_inactive() {
839 let tmp_dir = tempdir().unwrap();
840 let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
841
842 assert!(!load_order.is_active("Blank - Different.esp"));
843 assert!(deactivate(&mut load_order, "Blank - Different.esp").is_ok());
844 assert!(!load_order.is_active("Blank - Different.esp"));
845 }
846
847 #[test]
848 fn set_active_plugins_should_error_if_passed_an_invalid_plugin_name() {
849 let tmp_dir = tempdir().unwrap();
850 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
851
852 let active_plugins = ["missing.esp"];
853 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
854 assert_eq!(1, load_order.active_plugin_names().len());
855 }
856
857 #[test]
858 fn set_active_plugins_should_error_if_the_given_plugins_are_missing_implicitly_active_plugins()
859 {
860 let tmp_dir = tempdir().unwrap();
861 let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
862
863 prepend_early_loader(&mut load_order);
864
865 let active_plugins = ["Blank.esp"];
866 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
867 assert_eq!(1, load_order.active_plugin_names().len());
868 }
869
870 #[test]
871 fn set_active_plugins_should_error_if_a_missing_implicitly_active_plugin_is_given() {
872 let tmp_dir = tempdir().unwrap();
873 let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
874
875 let active_plugins = ["Update.esm", "Blank.esp"];
876 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
877 assert_eq!(1, load_order.active_plugin_names().len());
878 }
879
880 #[test]
881 fn set_active_plugins_should_error_if_given_plugins_not_in_the_load_order() {
882 let tmp_dir = tempdir().unwrap();
883 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
884
885 let active_plugins = ["Blank - Master Dependent.esp", NON_ASCII];
886 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
887 assert!(!load_order.is_active("Blank - Master Dependent.esp"));
888 assert!(load_order
889 .index_of("Blank - Master Dependent.esp")
890 .is_none());
891 assert!(!load_order.is_active(NON_ASCII));
892 assert!(load_order.index_of(NON_ASCII).is_none());
893 }
894
895 #[test]
896 fn set_active_plugins_should_deactivate_all_plugins_not_given() {
897 let tmp_dir = tempdir().unwrap();
898 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
899
900 let active_plugins = ["Blank - Different.esp"];
901 assert!(load_order.is_active("Blank.esp"));
902 assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
903 assert!(!load_order.is_active("Blank.esp"));
904 }
905
906 #[test]
907 fn set_active_plugins_should_activate_all_given_plugins() {
908 let tmp_dir = tempdir().unwrap();
909 let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
910
911 let active_plugins = ["Blank - Different.esp"];
912 assert!(!load_order.is_active("Blank - Different.esp"));
913 assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
914 assert!(load_order.is_active("Blank - Different.esp"));
915 }
916
917 #[test]
918 fn set_active_plugins_should_count_update_plugins_towards_limit() {
919 let tmp_dir = tempdir().unwrap();
920 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
921
922 let blank_override = "Blank - Override.esp";
923 load_and_insert(&mut load_order, blank_override);
924
925 let mut active_plugins = vec![blank_override.to_owned()];
926
927 let plugins = prepare_bulk_full_plugins(&mut load_order);
928 for plugin in plugins.into_iter().take(255) {
929 active_plugins.push(plugin);
930 }
931
932 let active_plugins: Vec<&str> = active_plugins
933 .iter()
934 .map(std::string::String::as_str)
935 .collect();
936
937 assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
938 assert_eq!(1, load_order.active_plugin_names().len());
939 }
940
941 #[test]
942 fn set_active_plugins_should_lower_the_full_plugin_limit_if_a_light_plugin_is_present() {
943 let tmp_dir = tempdir().unwrap();
944 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
945
946 let full = prepare_bulk_full_plugins(&mut load_order);
947
948 let plugin = "Blank.small.esm";
949 load_and_insert(&mut load_order, plugin);
950
951 let mut plugin_refs = vec![plugin];
952 plugin_refs.extend(full[..254].iter().map(String::as_str));
953
954 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
955 assert_eq!(255, load_order.active_plugin_names().len());
956
957 plugin_refs.push(full[254].as_str());
958
959 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
960 assert_eq!(255, load_order.active_plugin_names().len());
961 }
962
963 #[test]
964 fn set_active_plugins_should_lower_the_full_plugin_limit_if_a_medium_plugin_is_present() {
965 let tmp_dir = tempdir().unwrap();
966 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
967
968 let full = prepare_bulk_full_plugins(&mut load_order);
969
970 let plugin = "Blank.medium.esm";
971 load_and_insert(&mut load_order, plugin);
972
973 let mut plugin_refs = vec![plugin];
974 plugin_refs.extend(full[..254].iter().map(String::as_str));
975
976 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
977 assert_eq!(255, load_order.active_plugin_names().len());
978
979 plugin_refs.push(full[254].as_str());
980
981 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
982 assert_eq!(255, load_order.active_plugin_names().len());
983 }
984
985 #[test]
986 fn set_active_plugins_should_lower_the_full_plugin_limit_if_light_and_plugins_are_present() {
987 let tmp_dir = tempdir().unwrap();
988 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
989
990 let full = prepare_bulk_full_plugins(&mut load_order);
991
992 let medium_plugin = "Blank.medium.esm";
993 let light_plugin = "Blank.small.esm";
994 load_and_insert(&mut load_order, medium_plugin);
995 load_and_insert(&mut load_order, light_plugin);
996
997 let mut plugin_refs = vec![medium_plugin, light_plugin];
998 plugin_refs.extend(full[..253].iter().map(String::as_str));
999
1000 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1001 assert_eq!(255, load_order.active_plugin_names().len());
1002
1003 plugin_refs.push(full[253].as_str());
1004
1005 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1006 assert_eq!(255, load_order.active_plugin_names().len());
1007 }
1008
1009 #[test]
1010 fn set_active_plugins_should_count_full_medium_and_small_plugins_separately() {
1011 let tmp_dir = tempdir().unwrap();
1012 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1013
1014 let full = prepare_bulk_full_plugins(&mut load_order);
1015 let medium = prepare_bulk_medium_plugins(&mut load_order);
1016 let light = prepare_bulk_light_plugins(&mut load_order);
1017
1018 let mut plugin_refs = Vec::with_capacity(4064);
1019 plugin_refs.extend(full[..252].iter().map(String::as_str));
1020 plugin_refs.extend(medium[..256].iter().map(String::as_str));
1021 plugin_refs.extend(light[..4096].iter().map(String::as_str));
1022
1023 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1024 assert_eq!(4604, load_order.active_plugin_names().len());
1025 }
1026
1027 #[test]
1028 fn set_active_plugins_should_error_if_given_more_than_254_full_plugins() {
1029 let tmp_dir = tempdir().unwrap();
1030 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1031
1032 let full = prepare_bulk_full_plugins(&mut load_order);
1033
1034 let plugin_refs: Vec<_> = full[..256].iter().map(String::as_str).collect();
1035
1036 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1037 assert_eq!(1, load_order.active_plugin_names().len());
1038 }
1039
1040 #[test]
1041 fn set_active_plugins_should_error_if_given_more_than_256_medium_plugins() {
1042 let tmp_dir = tempdir().unwrap();
1043 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1044
1045 let medium = prepare_bulk_medium_plugins(&mut load_order);
1046
1047 let plugin_refs: Vec<_> = medium[..257].iter().map(String::as_str).collect();
1048
1049 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1050 assert_eq!(1, load_order.active_plugin_names().len());
1051 }
1052
1053 #[test]
1054 fn set_active_plugins_should_error_if_given_more_than_4096_light_plugins() {
1055 let tmp_dir = tempdir().unwrap();
1056 let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1057
1058 let light = prepare_bulk_light_plugins(&mut load_order);
1059
1060 let plugin_refs: Vec<_> = light[..4097].iter().map(String::as_str).collect();
1061
1062 assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1063 assert_eq!(1, load_order.active_plugin_names().len());
1064 }
1065}