Skip to main content

loadorder/load_order/
writable.rs

1/*
2 * This file is part of libloadorder
3 *
4 * Copyright (C) 2017 Oliver Hamlet
5 *
6 * libloadorder is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * libloadorder is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with libloadorder. If not, see <http://www.gnu.org/licenses/>.
18 */
19use 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 this is a master file that depends on a non-master file, it shouldn't be removed
93            // without first moving the non-master file later in the load order, unless the next
94            // master file also depends on that same non-master file. The non-master file also
95            // doesn't need to be moved if this is the last master file in the load order.
96            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                    // Remove any masters that are also masters of the next master plugin.
111                    masters.retain(|m| !next_master_master_names.contains(&UniCase::new(m)));
112
113                    // Finally, check if any remaining masters are non-master plugins.
114                    if let Some(n) = masters.iter().find(|n| {
115                        load_order
116                            .find_plugin(n)
117                            // If the master isn't installed, assume it's a master file and so
118                            // doesn't prevent removal of the target plugin.
119                            .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
220    plugin.activate()
221}
222
223fn activate_with_blueprint_ships_plugin<T: MutableLoadOrder>(
224    load_order: &mut T,
225    plugin_name: &str,
226) -> Result<(), Error> {
227    let Some((plugin_index, plugin)) = load_order.find_plugin_and_index(plugin_name) else {
228        return Err(Error::PluginNotFound(plugin_name.to_owned()));
229    };
230
231    // If the game supports implicitly active blueprint ships plugins, check if
232    // a matching inactive plugin is present.
233    let blueprint_ships_pair = find_blueprint_ships_plugin_for_plugin(load_order, plugin_name)
234        .filter(|(_, p)| !p.is_active());
235
236    if !plugin.is_active() {
237        let max_active_full_plugins = load_order.max_active_full_plugins();
238        let mut counts = count_active_plugins(load_order);
239
240        counts.count_plugin(plugin);
241
242        if let Some((_, blueprint_ships_plugin)) = blueprint_ships_pair {
243            counts.count_plugin(blueprint_ships_plugin);
244        }
245
246        validate_plugin_counts(&counts, max_active_full_plugins)?;
247    }
248
249    // Drop the BlueprintShips plugin reference.
250    let blueprint_ships_plugin_index = blueprint_ships_pair.map(|(i, _)| i);
251
252    if let Some(plugin) = load_order.plugins_mut().get_mut(plugin_index) {
253        plugin.activate()?;
254    }
255
256    if let Some(index) = blueprint_ships_plugin_index {
257        if let Some(plugin) = load_order.plugins_mut().get_mut(index) {
258            plugin.implicitly_activate()?;
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 is_implicitly_active(load_order, 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    if load_order
293        .game_settings()
294        .supports_blueprint_ships_plugins()
295    {
296        // Find a BlueprintShips plugin that is implicitly active but not due to
297        // game config or another active plugin, and deactivate it.
298        if let Some(plugin) = blueprint_ships_plugin_name(plugin_name)
299            .filter(|n| !is_implicitly_active(load_order, n))
300            .and_then(|n| load_order.find_plugin_mut(&n))
301            .filter(|p| !p.is_explicitly_active())
302        {
303            plugin.deactivate();
304        }
305    }
306
307    Ok(())
308}
309
310fn is_implicitly_active<T: ReadableLoadOrder + ReadableLoadOrderBase>(
311    load_order: &T,
312    plugin_name: &str,
313) -> bool {
314    load_order.game_settings().is_implicitly_active(plugin_name)
315        || is_implicitly_activated_by_another_plugin(load_order, plugin_name)
316}
317
318fn is_implicitly_activated_by_another_plugin<T: ReadableLoadOrder + ReadableLoadOrderBase>(
319    load_order: &T,
320    plugin_name: &str,
321) -> bool {
322    if !load_order
323        .game_settings()
324        .supports_blueprint_ships_plugins()
325    {
326        return false;
327    }
328
329    let Some(name_without_extension) = blueprint_ships_base_plugin_name(plugin_name) else {
330        return false;
331    };
332
333    load_order
334        .plugins()
335        .iter()
336        .filter(|p| p.is_active())
337        .map(Plugin::name_without_extension)
338        .any(|n| unicase::eq(n, name_without_extension))
339}
340
341pub(super) fn set_active_plugins<T: MutableLoadOrder>(
342    load_order: &mut T,
343    active_plugin_names: &[&str],
344) -> Result<(), Error> {
345    let existing_plugin_indices = load_order.lookup_plugins(active_plugin_names)?;
346
347    let counts = count_plugins(load_order.plugins(), &existing_plugin_indices);
348
349    validate_plugin_counts(&counts, load_order.max_active_full_plugins())?;
350
351    for plugin_name in load_order.game_settings().implicitly_active_plugins() {
352        // If the plugin isn't installed, don't check that it's in the active
353        // plugins list. Installed plugins will have already been loaded.
354        validate_plugin_is_active(load_order, active_plugin_names, plugin_name)?;
355    }
356
357    if load_order
358        .game_settings()
359        .supports_blueprint_ships_plugins()
360    {
361        // Check that for any active plugins that would also cause a
362        // BlueprintShips to be implicitly active, that the BlueprintShips
363        // plugin is also listed.
364        for active_plugin in active_plugin_names {
365            if let Some(blueprint_ships_plugin_name) = blueprint_ships_plugin_name(active_plugin) {
366                validate_plugin_is_active(
367                    load_order,
368                    active_plugin_names,
369                    &blueprint_ships_plugin_name,
370                )?;
371            }
372        }
373    }
374
375    load_order.deactivate_all();
376
377    for index in existing_plugin_indices {
378        if let Some(plugin) = load_order.plugins_mut().get_mut(index) {
379            // set_active_plugins explicitly activates all given plugins,
380            // including those that were previously implicitly active.
381            plugin.activate()?;
382        }
383    }
384
385    Ok(())
386}
387
388fn blueprint_ships_plugin_name(plugin_name: &str) -> Option<String> {
389    // Supported extensions are .esm, .esp, .esl
390    const EXTENSION_LENGTH: usize = 4;
391
392    plugin_name
393        .get(..plugin_name.len() - EXTENSION_LENGTH)
394        .map(|n| format!("BlueprintShips-{n}.esm"))
395}
396
397pub(super) fn blueprint_ships_base_plugin_name(blueprint_ships_plugin_name: &str) -> Option<&str> {
398    const BLUEPRINT_SHIPS_PREFIX: &str = "BlueprintShips-";
399    const BLUEPRINT_SHIPS_SUFFIX: &str = ".esm";
400
401    blueprint_ships_plugin_name
402        .split_at_checked(BLUEPRINT_SHIPS_PREFIX.len())
403        .filter(|(prefix, _)| BLUEPRINT_SHIPS_PREFIX.eq_ignore_ascii_case(prefix))
404        .and_then(|(_, remainder)| {
405            remainder
406                .split_at_checked(remainder.len() - BLUEPRINT_SHIPS_SUFFIX.len())
407                .filter(|(_, suffix)| BLUEPRINT_SHIPS_SUFFIX.eq_ignore_ascii_case(suffix))
408                .map(|(base, _)| base)
409        })
410}
411
412fn validate_plugin_is_active<T: MutableLoadOrder>(
413    load_order: &T,
414    active_plugin_names: &[&str],
415    plugin_name: &str,
416) -> Result<(), Error> {
417    if load_order.index_of(plugin_name).is_some()
418        && !active_plugin_names.iter().any(|p| eq(*p, plugin_name))
419    {
420        return Err(Error::ImplicitlyActivePlugin(plugin_name.to_owned()));
421    }
422
423    Ok(())
424}
425
426pub(super) fn create_parent_dirs(path: &Path) -> Result<(), Error> {
427    if let Some(x) = path.parent() {
428        if !x.exists() {
429            create_dir_all(x).map_err(|e| Error::IoError(x.to_path_buf(), e))?;
430        }
431    }
432    Ok(())
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    use std::fs::remove_file;
440
441    use tempfile::tempdir;
442
443    use crate::enums::GameId;
444    use crate::load_order::tests::{
445        game_settings_for_test, load_and_insert, mock_game_files, prepare_bulk_full_plugins,
446        prepare_bulk_plugins, prepend_early_loader, prepend_master, set_blueprint_flag,
447        set_master_flag,
448    };
449    use crate::plugin::ActiveState;
450    use crate::tests::{copy_to_test_dir, NON_ASCII};
451
452    struct TestLoadOrder {
453        game_settings: GameSettings,
454        plugins: Vec<Plugin>,
455    }
456
457    impl ReadableLoadOrderBase for TestLoadOrder {
458        fn game_settings_base(&self) -> &GameSettings {
459            &self.game_settings
460        }
461
462        fn plugins(&self) -> &[Plugin] {
463            &self.plugins
464        }
465    }
466
467    impl MutableLoadOrder for TestLoadOrder {
468        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
469            &mut self.plugins
470        }
471    }
472
473    fn prepare(game_id: GameId, game_dir: &Path) -> TestLoadOrder {
474        let mut game_settings = game_settings_for_test(game_id, game_dir);
475        mock_game_files(&mut game_settings);
476
477        let mut plugins =
478            vec![
479                Plugin::with_active("Blank.esp", &game_settings, ActiveState::ExplicitlyActive)
480                    .unwrap(),
481            ];
482
483        if game_id != GameId::Starfield {
484            plugins.push(Plugin::new("Blank - Different.esp", &game_settings).unwrap());
485        }
486
487        TestLoadOrder {
488            game_settings,
489            plugins,
490        }
491    }
492
493    fn prepare_bulk_medium_plugins(load_order: &mut TestLoadOrder) -> Vec<String> {
494        prepare_bulk_plugins(load_order, "Blank.medium.esm", 260, |i| {
495            format!("Blank{i}.medium.esm")
496        })
497    }
498
499    fn prepare_bulk_light_plugins(load_order: &mut TestLoadOrder) -> Vec<String> {
500        prepare_bulk_plugins(load_order, "Blank.small.esm", 5000, |i| {
501            format!("Blank{i}.small.esm")
502        })
503    }
504
505    #[test]
506    fn add_should_error_if_the_plugin_is_already_in_the_load_order() {
507        let tmp_dir = tempdir().unwrap();
508        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
509
510        assert!(add(&mut load_order, "Blank.esm").is_ok());
511        assert!(add(&mut load_order, "Blank.esm").is_err());
512    }
513
514    #[test]
515    fn add_should_error_if_given_a_master_that_would_hoist_a_non_master() {
516        let tmp_dir = tempdir().unwrap();
517        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
518
519        let plugins_dir = &load_order.game_settings().plugins_directory();
520        copy_to_test_dir(
521            "Blank - Different.esm",
522            "Blank - Different.esm",
523            load_order.game_settings(),
524        );
525        set_master_flag(
526            GameId::Oblivion,
527            &plugins_dir.join("Blank - Different.esm"),
528            false,
529        )
530        .unwrap();
531        assert!(add(&mut load_order, "Blank - Different.esm").is_ok());
532
533        copy_to_test_dir(
534            "Blank - Different Master Dependent.esm",
535            "Blank - Different Master Dependent.esm",
536            load_order.game_settings(),
537        );
538
539        assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_err());
540    }
541
542    #[test]
543    fn add_should_error_if_the_plugin_is_not_valid() {
544        let tmp_dir = tempdir().unwrap();
545        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
546
547        assert!(add(&mut load_order, "invalid.esm").is_err());
548    }
549
550    #[test]
551    fn add_should_insert_a_master_before_non_masters() {
552        let tmp_dir = tempdir().unwrap();
553        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
554
555        assert!(!load_order.plugins[1].is_master_file());
556
557        assert_eq!(0, add(&mut load_order, "Blank.esm").unwrap());
558        assert_eq!(0, load_order.index_of("Blank.esm").unwrap());
559    }
560
561    #[test]
562    fn add_should_append_a_non_master() {
563        let tmp_dir = tempdir().unwrap();
564        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
565
566        assert_eq!(
567            2,
568            add(&mut load_order, "Blank - Master Dependent.esp").unwrap()
569        );
570        assert_eq!(
571            2,
572            load_order.index_of("Blank - Master Dependent.esp").unwrap()
573        );
574    }
575
576    #[test]
577    fn add_should_hoist_a_non_master_that_a_master_depends_on() {
578        let tmp_dir = tempdir().unwrap();
579        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
580
581        let plugins_dir = &load_order.game_settings().plugins_directory();
582        copy_to_test_dir(
583            "Blank - Different Master Dependent.esm",
584            "Blank - Different Master Dependent.esm",
585            load_order.game_settings(),
586        );
587        assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_ok());
588
589        copy_to_test_dir(
590            "Blank - Different.esm",
591            "Blank - Different.esm",
592            load_order.game_settings(),
593        );
594        set_master_flag(
595            GameId::Oblivion,
596            &plugins_dir.join("Blank - Different.esm"),
597            false,
598        )
599        .unwrap();
600        assert_eq!(0, add(&mut load_order, "Blank - Different.esm").unwrap());
601    }
602
603    #[test]
604    fn add_should_hoist_a_master_that_a_master_depends_on() {
605        let tmp_dir = tempdir().unwrap();
606        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
607
608        let plugin_name = "Blank - Master Dependent.esm";
609        copy_to_test_dir(plugin_name, plugin_name, load_order.game_settings());
610        assert_eq!(0, add(&mut load_order, plugin_name).unwrap());
611
612        assert_eq!(0, add(&mut load_order, "Blank.esm").unwrap());
613    }
614
615    #[test]
616    fn remove_should_error_if_the_plugin_is_not_in_the_load_order() {
617        let tmp_dir = tempdir().unwrap();
618        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
619        assert!(remove(&mut load_order, "Blank.esm").is_err());
620    }
621
622    #[test]
623    fn remove_should_error_if_the_plugin_is_installed() {
624        let tmp_dir = tempdir().unwrap();
625        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
626        assert!(remove(&mut load_order, "Blank.esp").is_err());
627    }
628
629    #[test]
630    fn remove_should_error_if_removing_a_master_would_leave_a_non_master_it_hoisted_loading_too_early(
631    ) {
632        let tmp_dir = tempdir().unwrap();
633        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
634
635        prepend_master(&mut load_order);
636
637        let plugin_to_remove = "Blank - Different Master Dependent.esm";
638
639        let plugins_dir = &load_order.game_settings().plugins_directory();
640        copy_to_test_dir(
641            plugin_to_remove,
642            plugin_to_remove,
643            load_order.game_settings(),
644        );
645        assert!(add(&mut load_order, plugin_to_remove).is_ok());
646
647        copy_to_test_dir(
648            "Blank - Different.esm",
649            "Blank - Different.esm",
650            load_order.game_settings(),
651        );
652        set_master_flag(
653            GameId::Oblivion,
654            &plugins_dir.join("Blank - Different.esm"),
655            false,
656        )
657        .unwrap();
658        assert_eq!(1, add(&mut load_order, "Blank - Different.esm").unwrap());
659
660        copy_to_test_dir(
661            "Blank - Master Dependent.esm",
662            "Blank - Master Dependent.esm",
663            load_order.game_settings(),
664        );
665        assert!(add(&mut load_order, "Blank - Master Dependent.esm").is_ok());
666
667        let blank_master_dependent = load_order.plugins.remove(1);
668        load_order.plugins.insert(3, blank_master_dependent);
669
670        std::fs::remove_file(plugins_dir.join(plugin_to_remove)).unwrap();
671
672        match remove(&mut load_order, plugin_to_remove).unwrap_err() {
673            Error::NonMasterBeforeMaster { master, non_master } => {
674                assert_eq!("Blank - Different Master Dependent.esm", master);
675                assert_eq!("Blank - Different.esm", non_master);
676            }
677            e => panic!("Unexpected error type: {e:?}"),
678        }
679    }
680
681    #[test]
682    fn remove_should_allow_removal_of_a_master_that_depends_on_a_blueprint_plugin() {
683        let tmp_dir = tempdir().unwrap();
684        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
685
686        let plugins_dir = &load_order.game_settings().plugins_directory();
687
688        let plugin_to_remove = "Blank - Override.full.esm";
689        copy_to_test_dir(
690            plugin_to_remove,
691            plugin_to_remove,
692            load_order.game_settings(),
693        );
694        assert!(add(&mut load_order, plugin_to_remove).is_ok());
695
696        let blueprint_plugin = "Blank.full.esm";
697        copy_to_test_dir(
698            blueprint_plugin,
699            blueprint_plugin,
700            load_order.game_settings(),
701        );
702        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
703        assert_eq!(2, add(&mut load_order, blueprint_plugin).unwrap());
704
705        let following_master_plugin = "Blank.medium.esm";
706        copy_to_test_dir(
707            following_master_plugin,
708            following_master_plugin,
709            load_order.game_settings(),
710        );
711        assert!(add(&mut load_order, following_master_plugin).is_ok());
712
713        std::fs::remove_file(plugins_dir.join(plugin_to_remove)).unwrap();
714
715        assert!(remove(&mut load_order, plugin_to_remove).is_ok());
716    }
717
718    #[test]
719    fn remove_should_remove_the_given_plugin_from_the_load_order() {
720        let tmp_dir = tempdir().unwrap();
721        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
722
723        remove_file(
724            load_order
725                .game_settings()
726                .plugins_directory()
727                .join("Blank.esp"),
728        )
729        .unwrap();
730
731        assert!(remove(&mut load_order, "Blank.esp").is_ok());
732        assert!(load_order.index_of("Blank.esp").is_none());
733    }
734
735    #[test]
736    fn activate_should_activate_the_plugin_with_the_given_filename() {
737        let tmp_dir = tempdir().unwrap();
738        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
739
740        assert!(activate(&mut load_order, "Blank - Different.esp").is_ok());
741        assert!(load_order.is_active("Blank - Different.esp"));
742    }
743
744    #[test]
745    fn activate_should_error_if_the_plugin_is_not_valid() {
746        let tmp_dir = tempdir().unwrap();
747        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
748
749        assert!(activate(&mut load_order, "missing.esp").is_err());
750        assert!(load_order.index_of("missing.esp").is_none());
751    }
752
753    #[test]
754    fn activate_should_error_if_the_plugin_is_not_already_in_the_load_order() {
755        let tmp_dir = tempdir().unwrap();
756        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
757
758        assert!(activate(&mut load_order, "Blank.esm").is_err());
759        assert!(!load_order.is_active("Blank.esm"));
760    }
761
762    #[test]
763    fn activate_should_be_case_insensitive() {
764        let tmp_dir = tempdir().unwrap();
765        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
766
767        assert!(activate(&mut load_order, "Blank - different.esp").is_ok());
768        assert!(load_order.is_active("Blank - Different.esp"));
769    }
770
771    #[test]
772    fn activate_should_throw_if_increasing_the_number_of_active_plugins_past_the_limit() {
773        let tmp_dir = tempdir().unwrap();
774        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
775
776        let plugins = prepare_bulk_full_plugins(&mut load_order);
777        for plugin in &plugins[..254] {
778            activate(&mut load_order, plugin).unwrap();
779        }
780
781        assert!(activate(&mut load_order, "Blank - Different.esp").is_err());
782        assert!(!load_order.is_active("Blank - Different.esp"));
783    }
784
785    #[test]
786    fn activate_should_succeed_if_at_the_active_plugins_limit_and_the_plugin_is_already_active() {
787        let tmp_dir = tempdir().unwrap();
788        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
789
790        let plugins = prepare_bulk_full_plugins(&mut load_order);
791        for plugin in &plugins[..254] {
792            activate(&mut load_order, plugin).unwrap();
793        }
794
795        assert!(load_order.is_active("Blank.esp"));
796        assert!(activate(&mut load_order, "Blank.esp").is_ok());
797    }
798
799    #[test]
800    fn activate_should_fail_if_at_the_active_plugins_limit_and_the_plugin_is_an_update_plugin() {
801        let tmp_dir = tempdir().unwrap();
802        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
803
804        let plugins = prepare_bulk_full_plugins(&mut load_order);
805        for plugin in &plugins[..254] {
806            activate(&mut load_order, plugin).unwrap();
807        }
808
809        let plugin = "Blank - Override.esp";
810        load_and_insert(&mut load_order, plugin);
811
812        assert!(!load_order.is_active(plugin));
813
814        assert!(activate(&mut load_order, plugin).is_err());
815        assert!(!load_order.is_active(plugin));
816    }
817
818    #[test]
819    fn activate_should_count_active_update_plugins_towards_limit() {
820        let tmp_dir = tempdir().unwrap();
821        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
822
823        let plugins = prepare_bulk_full_plugins(&mut load_order);
824        for plugin in &plugins[..254] {
825            activate(&mut load_order, plugin).unwrap();
826        }
827
828        let plugin = "Blank - Override.esp";
829        load_and_insert(&mut load_order, plugin);
830
831        assert!(!load_order.is_active(plugin));
832
833        assert!(activate(&mut load_order, plugin).is_err());
834        assert!(!load_order.is_active(plugin));
835    }
836
837    #[test]
838    fn activate_should_lower_the_full_plugin_limit_if_a_light_plugin_is_present() {
839        let tmp_dir = tempdir().unwrap();
840        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
841
842        let plugins = prepare_bulk_full_plugins(&mut load_order);
843        for plugin in &plugins[..252] {
844            activate(&mut load_order, plugin).unwrap();
845        }
846
847        let plugin = "Blank.small.esm";
848        load_and_insert(&mut load_order, plugin);
849        activate(&mut load_order, plugin).unwrap();
850
851        let plugin = &plugins[253];
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[254];
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_lower_the_full_plugin_limit_if_a_medium_plugin_is_present() {
866        let tmp_dir = tempdir().unwrap();
867        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
868
869        let plugins = prepare_bulk_full_plugins(&mut load_order);
870        for plugin in &plugins[..252] {
871            activate(&mut load_order, plugin).unwrap();
872        }
873
874        let plugin = "Blank.medium.esm";
875        load_and_insert(&mut load_order, plugin);
876        activate(&mut load_order, plugin).unwrap();
877
878        let plugin = &plugins[253];
879        assert!(!load_order.is_active(plugin));
880
881        assert!(activate(&mut load_order, plugin).is_ok());
882        assert!(load_order.is_active(plugin));
883
884        let plugin = &plugins[254];
885        assert!(!load_order.is_active(plugin));
886
887        assert!(activate(&mut load_order, plugin).is_err());
888        assert!(!load_order.is_active(plugin));
889    }
890
891    #[test]
892    fn activate_should_lower_the_full_plugin_limit_if_light_and_medium_plugins_are_present() {
893        let tmp_dir = tempdir().unwrap();
894        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
895
896        let plugins = prepare_bulk_full_plugins(&mut load_order);
897        for plugin in &plugins[..251] {
898            activate(&mut load_order, plugin).unwrap();
899        }
900
901        for plugin in ["Blank.medium.esm", "Blank.small.esm"] {
902            load_and_insert(&mut load_order, plugin);
903            activate(&mut load_order, plugin).unwrap();
904        }
905
906        let plugin = &plugins[252];
907        assert!(!load_order.is_active(plugin));
908
909        assert!(activate(&mut load_order, plugin).is_ok());
910        assert!(load_order.is_active(plugin));
911
912        let plugin = &plugins[253];
913        assert!(!load_order.is_active(plugin));
914
915        assert!(activate(&mut load_order, plugin).is_err());
916        assert!(!load_order.is_active(plugin));
917    }
918
919    #[test]
920    fn activate_should_check_full_medium_and_small_plugins_active_limits_separately() {
921        let tmp_dir = tempdir().unwrap();
922        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
923
924        let full = prepare_bulk_full_plugins(&mut load_order);
925        let medium = prepare_bulk_medium_plugins(&mut load_order);
926        let light = prepare_bulk_light_plugins(&mut load_order);
927
928        let mut plugin_refs = Vec::with_capacity(4603);
929        plugin_refs.extend(full[..252].iter().map(String::as_str));
930        plugin_refs.extend(medium[..255].iter().map(String::as_str));
931        plugin_refs.extend(light[..4095].iter().map(String::as_str));
932
933        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
934
935        let plugin = &full[252];
936        assert!(!load_order.is_active(plugin));
937        assert!(activate(&mut load_order, plugin).is_ok());
938        assert!(load_order.is_active(plugin));
939
940        let plugin = &medium[255];
941        assert!(!load_order.is_active(plugin));
942        assert!(activate(&mut load_order, plugin).is_ok());
943        assert!(load_order.is_active(plugin));
944
945        let plugin = &light[4095];
946        assert!(!load_order.is_active(plugin));
947        assert!(activate(&mut load_order, plugin).is_ok());
948        assert!(load_order.is_active(plugin));
949
950        let plugin = &full[253];
951        assert!(activate(&mut load_order, plugin).is_err());
952        assert!(!load_order.is_active(plugin));
953
954        let plugin = &medium[256];
955        assert!(activate(&mut load_order, plugin).is_err());
956        assert!(!load_order.is_active(plugin));
957
958        let plugin = &light[4096];
959        assert!(activate(&mut load_order, plugin).is_err());
960        assert!(!load_order.is_active(plugin));
961    }
962
963    #[test]
964    fn activate_should_explicitly_activate_an_implicitly_active_plugin() {
965        let tmp_dir = tempdir().unwrap();
966        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
967
968        let plugin_name = "Blank - Different.esp";
969        load_order
970            .find_plugin_mut(plugin_name)
971            .unwrap()
972            .implicitly_activate()
973            .unwrap();
974
975        assert!(load_order.is_active(plugin_name));
976        assert!(!load_order
977            .find_plugin(plugin_name)
978            .unwrap()
979            .is_explicitly_active());
980
981        activate(&mut load_order, plugin_name).unwrap();
982
983        assert!(load_order.is_active(plugin_name));
984        assert!(load_order
985            .find_plugin(plugin_name)
986            .unwrap()
987            .is_explicitly_active());
988    }
989
990    #[test]
991    fn activate_with_blueprint_ship_base_plugin_should_also_activate_blueprint_ships_plugin() {
992        let tmp_dir = tempdir().unwrap();
993        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
994
995        let plugin_name = "Blank.esp";
996        deactivate(&mut load_order, plugin_name).unwrap();
997
998        let blueprint_ships = "BlueprintShips-Blank.esm";
999        copy_to_test_dir(
1000            "Blank.full.esm",
1001            blueprint_ships,
1002            load_order.game_settings(),
1003        );
1004        add(&mut load_order, blueprint_ships).unwrap();
1005
1006        assert!(!load_order.is_active(plugin_name));
1007        assert!(!load_order.is_active(blueprint_ships));
1008
1009        activate(&mut load_order, plugin_name).unwrap();
1010
1011        assert!(load_order.is_active(plugin_name));
1012        assert!(load_order.is_active(blueprint_ships));
1013        assert!(load_order
1014            .find_plugin(plugin_name)
1015            .unwrap()
1016            .is_explicitly_active());
1017        assert!(!load_order
1018            .find_plugin(blueprint_ships)
1019            .unwrap()
1020            .is_explicitly_active());
1021    }
1022
1023    #[test]
1024    fn activate_should_explicitly_activate_an_implicitly_active_plugin_and_implicitly_active_its_blueprintships_plugin(
1025    ) {
1026        let tmp_dir = tempdir().unwrap();
1027        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1028
1029        let plugin_name = "Blank.esp";
1030        deactivate(&mut load_order, plugin_name).unwrap();
1031        load_order
1032            .find_plugin_mut(plugin_name)
1033            .unwrap()
1034            .implicitly_activate()
1035            .unwrap();
1036
1037        let blueprint_ships = "BlueprintShips-Blank.esm";
1038        copy_to_test_dir(
1039            "Blank.full.esm",
1040            blueprint_ships,
1041            load_order.game_settings(),
1042        );
1043        add(&mut load_order, blueprint_ships).unwrap();
1044
1045        assert!(load_order.is_active(plugin_name));
1046        assert!(!load_order
1047            .find_plugin(plugin_name)
1048            .unwrap()
1049            .is_explicitly_active());
1050        assert!(!load_order.is_active(blueprint_ships));
1051
1052        activate(&mut load_order, plugin_name).unwrap();
1053
1054        assert!(load_order.is_active(plugin_name));
1055        assert!(load_order.is_active(blueprint_ships));
1056        assert!(load_order
1057            .find_plugin(plugin_name)
1058            .unwrap()
1059            .is_explicitly_active());
1060        assert!(!load_order
1061            .find_plugin(blueprint_ships)
1062            .unwrap()
1063            .is_explicitly_active());
1064    }
1065
1066    #[test]
1067    fn activate_should_succeed_if_blueprint_ships_plugins_are_supported_but_not_present() {
1068        let tmp_dir = tempdir().unwrap();
1069        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1070
1071        let plugin_name = "Blank.esp";
1072        deactivate(&mut load_order, plugin_name).unwrap();
1073
1074        assert!(!load_order.is_active(plugin_name));
1075
1076        activate(&mut load_order, plugin_name).unwrap();
1077
1078        assert!(load_order.is_active(plugin_name));
1079        assert!(load_order
1080            .find_plugin(plugin_name)
1081            .unwrap()
1082            .is_explicitly_active());
1083    }
1084
1085    #[test]
1086    fn activate_with_blueprint_ship_base_plugin_should_count_activating_blueprint_ships_plugin() {
1087        let tmp_dir = tempdir().unwrap();
1088        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1089
1090        let plugin_name = "Blank.esp";
1091        deactivate(&mut load_order, plugin_name).unwrap();
1092
1093        let plugins = prepare_bulk_full_plugins(&mut load_order);
1094        for plugin in &plugins[..254] {
1095            activate(&mut load_order, plugin).unwrap();
1096        }
1097
1098        let blueprint_ships = "BlueprintShips-Blank.esm";
1099        copy_to_test_dir(
1100            "Blank.full.esm",
1101            blueprint_ships,
1102            load_order.game_settings(),
1103        );
1104        add(&mut load_order, blueprint_ships).unwrap();
1105
1106        assert!(!load_order.is_active(plugin_name));
1107        assert!(!load_order.is_active(blueprint_ships));
1108
1109        let err = activate(&mut load_order, plugin_name).unwrap_err();
1110
1111        match err {
1112            Error::TooManyActivePlugins {
1113                light_count,
1114                medium_count,
1115                full_count,
1116            } => {
1117                assert_eq!(0, light_count);
1118                assert_eq!(0, medium_count);
1119                assert_eq!(256, full_count);
1120            }
1121            e => panic!("Unexpected error type: {e:?}"),
1122        }
1123    }
1124
1125    #[test]
1126    fn deactivate_should_deactivate_the_plugin_with_the_given_filename() {
1127        let tmp_dir = tempdir().unwrap();
1128        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1129
1130        assert!(load_order.is_active("Blank.esp"));
1131        assert!(deactivate(&mut load_order, "Blank.esp").is_ok());
1132        assert!(!load_order.is_active("Blank.esp"));
1133    }
1134
1135    #[test]
1136    fn deactivate_should_error_if_the_plugin_is_not_in_the_load_order() {
1137        let tmp_dir = tempdir().unwrap();
1138        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1139
1140        assert!(deactivate(&mut load_order, "missing.esp").is_err());
1141        assert!(load_order.index_of("missing.esp").is_none());
1142    }
1143
1144    #[test]
1145    fn deactivate_should_error_if_given_an_implicitly_active_plugin() {
1146        let tmp_dir = tempdir().unwrap();
1147        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1148
1149        prepend_early_loader(&mut load_order);
1150
1151        assert!(activate(&mut load_order, "Skyrim.esm").is_ok());
1152        assert!(deactivate(&mut load_order, "Skyrim.esm").is_err());
1153        assert!(load_order.is_active("Skyrim.esm"));
1154    }
1155
1156    #[test]
1157    fn deactivate_should_error_if_given_a_missing_implicitly_active_plugin() {
1158        let tmp_dir = tempdir().unwrap();
1159        let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
1160
1161        assert!(deactivate(&mut load_order, "Update.esm").is_err());
1162        assert!(load_order.index_of("Update.esm").is_none());
1163    }
1164
1165    #[test]
1166    fn deactivate_should_error_if_given_a_blueprint_ships_plugin_that_is_implicitly_activated_by_another_plugin(
1167    ) {
1168        let tmp_dir = tempdir().unwrap();
1169        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1170
1171        let blueprint_ships = "BlueprintShips-Blank.esm";
1172        copy_to_test_dir(
1173            "Blank.full.esm",
1174            blueprint_ships,
1175            load_order.game_settings(),
1176        );
1177        add(&mut load_order, blueprint_ships).unwrap();
1178        activate(&mut load_order, blueprint_ships).unwrap();
1179
1180        let err = deactivate(&mut load_order, blueprint_ships).unwrap_err();
1181
1182        match err {
1183            Error::ImplicitlyActivePlugin(p) => assert_eq!(blueprint_ships, p),
1184            e => panic!("Unexpected error type: {e:?}"),
1185        }
1186
1187        assert!(load_order.is_active(blueprint_ships));
1188    }
1189
1190    #[test]
1191    fn deactivate_should_succeed_if_given_a_blueprint_ships_plugin_with_an_inactive_base_plugin() {
1192        let tmp_dir = tempdir().unwrap();
1193        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1194
1195        add(&mut load_order, "Blank.full.esm").unwrap();
1196
1197        let blueprint_ships = "BlueprintShips-Blank.full.esm";
1198        copy_to_test_dir(
1199            "Blank.full.esm",
1200            blueprint_ships,
1201            load_order.game_settings(),
1202        );
1203        add(&mut load_order, blueprint_ships).unwrap();
1204        activate(&mut load_order, blueprint_ships).unwrap();
1205
1206        deactivate(&mut load_order, blueprint_ships).unwrap();
1207
1208        assert!(!load_order.is_active(blueprint_ships));
1209    }
1210
1211    #[test]
1212    fn deactivate_should_succeed_if_given_a_blueprint_ships_plugin_that_is_not_implicitly_activated_by_another_plugin(
1213    ) {
1214        let tmp_dir = tempdir().unwrap();
1215        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1216
1217        let blueprint_ships = "BlueprintShips-A.esm";
1218        copy_to_test_dir(
1219            "Blank.full.esm",
1220            blueprint_ships,
1221            load_order.game_settings(),
1222        );
1223        add(&mut load_order, blueprint_ships).unwrap();
1224        activate(&mut load_order, blueprint_ships).unwrap();
1225
1226        deactivate(&mut load_order, blueprint_ships).unwrap();
1227
1228        assert!(!load_order.is_active(blueprint_ships));
1229    }
1230
1231    #[test]
1232    fn deactivate_should_deactivate_a_related_implicitly_active_blueprint_ships_plugin() {
1233        let tmp_dir = tempdir().unwrap();
1234        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1235
1236        let blueprint_ships = "BlueprintShips-Blank.esm";
1237        copy_to_test_dir(
1238            "Blank.full.esm",
1239            blueprint_ships,
1240            load_order.game_settings(),
1241        );
1242        let index = add(&mut load_order, blueprint_ships).unwrap();
1243        load_order.plugins[index].implicitly_activate().unwrap();
1244
1245        let plugin_name = "Blank.esp";
1246        assert!(load_order.is_active(plugin_name));
1247        assert!(load_order.is_active(blueprint_ships));
1248
1249        deactivate(&mut load_order, plugin_name).unwrap();
1250
1251        assert!(!load_order.is_active(plugin_name));
1252        assert!(!load_order.is_active(blueprint_ships));
1253    }
1254
1255    #[test]
1256    fn deactivate_should_not_deactivate_a_related_blueprint_ships_that_is_explicitly_active() {
1257        let tmp_dir = tempdir().unwrap();
1258        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1259
1260        let blueprint_ships = "BlueprintShips-Blank.esm";
1261        copy_to_test_dir(
1262            "Blank.full.esm",
1263            blueprint_ships,
1264            load_order.game_settings(),
1265        );
1266        add(&mut load_order, blueprint_ships).unwrap();
1267        activate(&mut load_order, blueprint_ships).unwrap();
1268
1269        let plugin_name = "Blank.esp";
1270        assert!(load_order.is_active(plugin_name));
1271        assert!(load_order.is_active(blueprint_ships));
1272
1273        deactivate(&mut load_order, plugin_name).unwrap();
1274
1275        assert!(!load_order.is_active(plugin_name));
1276        assert!(load_order.is_active(blueprint_ships));
1277    }
1278
1279    #[test]
1280    fn deactivate_should_not_deactivate_a_related_blueprint_ships_that_is_implicitly_active_due_to_another_plugin(
1281    ) {
1282        let tmp_dir = tempdir().unwrap();
1283        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1284
1285        let plugin_name = "Blank.esm";
1286        copy_to_test_dir("Blank.full.esm", plugin_name, load_order.game_settings());
1287        add(&mut load_order, plugin_name).unwrap();
1288        activate(&mut load_order, plugin_name).unwrap();
1289
1290        let blueprint_ships = "BlueprintShips-Blank.esm";
1291        copy_to_test_dir(
1292            "Blank.full.esm",
1293            blueprint_ships,
1294            load_order.game_settings(),
1295        );
1296        let index = add(&mut load_order, blueprint_ships).unwrap();
1297        load_order.plugins[index].implicitly_activate().unwrap();
1298
1299        let plugin_name = "Blank.esp";
1300        assert!(load_order.is_active(plugin_name));
1301        assert!(load_order.is_active(blueprint_ships));
1302
1303        deactivate(&mut load_order, plugin_name).unwrap();
1304
1305        assert!(!load_order.is_active(plugin_name));
1306        assert!(load_order.is_active(blueprint_ships));
1307    }
1308
1309    #[test]
1310    fn deactivate_should_not_deactivate_a_related_blueprint_ships_that_is_implicitly_active_due_to_game_settings(
1311    ) {
1312        let tmp_dir = tempdir().unwrap();
1313        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1314
1315        let blueprint_ships = "BlueprintShips-Blank.esm";
1316        copy_to_test_dir(
1317            "Blank.full.esm",
1318            blueprint_ships,
1319            load_order.game_settings(),
1320        );
1321        let index = add(&mut load_order, blueprint_ships).unwrap();
1322        load_order.plugins[index].implicitly_activate().unwrap();
1323
1324        std::fs::write(
1325            load_order
1326                .game_settings
1327                .plugins_directory()
1328                .parent()
1329                .unwrap()
1330                .join("Starfield.ccc"),
1331            blueprint_ships,
1332        )
1333        .unwrap();
1334        load_order
1335            .game_settings
1336            .refresh_implicitly_active_plugins()
1337            .unwrap();
1338
1339        let plugin_name = "Blank.esp";
1340        assert!(load_order.is_active(plugin_name));
1341        assert!(load_order.is_active(blueprint_ships));
1342
1343        deactivate(&mut load_order, plugin_name).unwrap();
1344
1345        assert!(!load_order.is_active(plugin_name));
1346        assert!(load_order.is_active(blueprint_ships));
1347    }
1348
1349    #[test]
1350    fn deactivate_should_do_nothing_if_the_plugin_is_inactive() {
1351        let tmp_dir = tempdir().unwrap();
1352        let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
1353
1354        assert!(!load_order.is_active("Blank - Different.esp"));
1355        assert!(deactivate(&mut load_order, "Blank - Different.esp").is_ok());
1356        assert!(!load_order.is_active("Blank - Different.esp"));
1357    }
1358
1359    #[test]
1360    fn set_active_plugins_should_error_if_passed_an_invalid_plugin_name() {
1361        let tmp_dir = tempdir().unwrap();
1362        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1363
1364        let active_plugins = ["missing.esp"];
1365        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1366        assert_eq!(1, load_order.active_plugin_names().len());
1367    }
1368
1369    #[test]
1370    fn set_active_plugins_should_error_if_the_given_plugins_are_missing_implicitly_active_plugins()
1371    {
1372        let tmp_dir = tempdir().unwrap();
1373        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1374
1375        prepend_early_loader(&mut load_order);
1376
1377        let active_plugins = ["Blank.esp"];
1378        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1379        assert_eq!(1, load_order.active_plugin_names().len());
1380    }
1381
1382    #[test]
1383    fn set_active_plugins_should_error_if_a_missing_implicitly_active_plugin_is_given() {
1384        let tmp_dir = tempdir().unwrap();
1385        let mut load_order = prepare(GameId::Skyrim, tmp_dir.path());
1386
1387        let active_plugins = ["Update.esm", "Blank.esp"];
1388        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1389        assert_eq!(1, load_order.active_plugin_names().len());
1390    }
1391
1392    #[test]
1393    fn set_active_plugins_should_error_if_given_plugins_not_in_the_load_order() {
1394        let tmp_dir = tempdir().unwrap();
1395        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1396
1397        let active_plugins = ["Blank - Master Dependent.esp", NON_ASCII];
1398        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1399        assert!(!load_order.is_active("Blank - Master Dependent.esp"));
1400        assert!(load_order
1401            .index_of("Blank - Master Dependent.esp")
1402            .is_none());
1403        assert!(!load_order.is_active(NON_ASCII));
1404        assert!(load_order.index_of(NON_ASCII).is_none());
1405    }
1406
1407    #[test]
1408    fn set_active_plugins_should_deactivate_all_plugins_not_given() {
1409        let tmp_dir = tempdir().unwrap();
1410        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1411
1412        let active_plugins = ["Blank - Different.esp"];
1413        assert!(load_order.is_active("Blank.esp"));
1414        assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
1415        assert!(!load_order.is_active("Blank.esp"));
1416    }
1417
1418    #[test]
1419    fn set_active_plugins_should_activate_all_given_plugins() {
1420        let tmp_dir = tempdir().unwrap();
1421        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1422
1423        let active_plugins = ["Blank - Different.esp"];
1424        assert!(!load_order.is_active("Blank - Different.esp"));
1425        assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
1426        assert!(load_order.is_active("Blank - Different.esp"));
1427    }
1428
1429    #[test]
1430    fn set_active_plugins_should_count_update_plugins_towards_limit() {
1431        let tmp_dir = tempdir().unwrap();
1432        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1433
1434        let blank_override = "Blank - Override.esp";
1435        load_and_insert(&mut load_order, blank_override);
1436
1437        let mut active_plugins = vec![blank_override.to_owned()];
1438
1439        let plugins = prepare_bulk_full_plugins(&mut load_order);
1440        for plugin in plugins.into_iter().take(255) {
1441            active_plugins.push(plugin);
1442        }
1443
1444        let active_plugins: Vec<&str> = active_plugins
1445            .iter()
1446            .map(std::string::String::as_str)
1447            .collect();
1448
1449        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1450        assert_eq!(1, load_order.active_plugin_names().len());
1451    }
1452
1453    #[test]
1454    fn set_active_plugins_should_lower_the_full_plugin_limit_if_a_light_plugin_is_present() {
1455        let tmp_dir = tempdir().unwrap();
1456        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1457
1458        let full = prepare_bulk_full_plugins(&mut load_order);
1459
1460        let plugin = "Blank.small.esm";
1461        load_and_insert(&mut load_order, plugin);
1462
1463        let mut plugin_refs = vec![plugin];
1464        plugin_refs.extend(full[..254].iter().map(String::as_str));
1465
1466        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1467        assert_eq!(255, load_order.active_plugin_names().len());
1468
1469        plugin_refs.push(full[254].as_str());
1470
1471        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1472        assert_eq!(255, load_order.active_plugin_names().len());
1473    }
1474
1475    #[test]
1476    fn set_active_plugins_should_lower_the_full_plugin_limit_if_a_medium_plugin_is_present() {
1477        let tmp_dir = tempdir().unwrap();
1478        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1479
1480        let full = prepare_bulk_full_plugins(&mut load_order);
1481
1482        let plugin = "Blank.medium.esm";
1483        load_and_insert(&mut load_order, plugin);
1484
1485        let mut plugin_refs = vec![plugin];
1486        plugin_refs.extend(full[..254].iter().map(String::as_str));
1487
1488        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1489        assert_eq!(255, load_order.active_plugin_names().len());
1490
1491        plugin_refs.push(full[254].as_str());
1492
1493        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1494        assert_eq!(255, load_order.active_plugin_names().len());
1495    }
1496
1497    #[test]
1498    fn set_active_plugins_should_lower_the_full_plugin_limit_if_light_and_plugins_are_present() {
1499        let tmp_dir = tempdir().unwrap();
1500        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1501
1502        let full = prepare_bulk_full_plugins(&mut load_order);
1503
1504        let medium_plugin = "Blank.medium.esm";
1505        let light_plugin = "Blank.small.esm";
1506        load_and_insert(&mut load_order, medium_plugin);
1507        load_and_insert(&mut load_order, light_plugin);
1508
1509        let mut plugin_refs = vec![medium_plugin, light_plugin];
1510        plugin_refs.extend(full[..253].iter().map(String::as_str));
1511
1512        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1513        assert_eq!(255, load_order.active_plugin_names().len());
1514
1515        plugin_refs.push(full[253].as_str());
1516
1517        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1518        assert_eq!(255, load_order.active_plugin_names().len());
1519    }
1520
1521    #[test]
1522    fn set_active_plugins_should_count_full_medium_and_small_plugins_separately() {
1523        let tmp_dir = tempdir().unwrap();
1524        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1525
1526        let full = prepare_bulk_full_plugins(&mut load_order);
1527        let medium = prepare_bulk_medium_plugins(&mut load_order);
1528        let light = prepare_bulk_light_plugins(&mut load_order);
1529
1530        let mut plugin_refs = Vec::with_capacity(4064);
1531        plugin_refs.extend(full[..252].iter().map(String::as_str));
1532        plugin_refs.extend(medium[..256].iter().map(String::as_str));
1533        plugin_refs.extend(light[..4096].iter().map(String::as_str));
1534
1535        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1536        assert_eq!(4604, load_order.active_plugin_names().len());
1537    }
1538
1539    #[test]
1540    fn set_active_plugins_should_error_if_given_more_than_254_full_plugins() {
1541        let tmp_dir = tempdir().unwrap();
1542        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1543
1544        let full = prepare_bulk_full_plugins(&mut load_order);
1545
1546        let plugin_refs: Vec<_> = full[..256].iter().map(String::as_str).collect();
1547
1548        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1549        assert_eq!(1, load_order.active_plugin_names().len());
1550    }
1551
1552    #[test]
1553    fn set_active_plugins_should_error_if_given_more_than_256_medium_plugins() {
1554        let tmp_dir = tempdir().unwrap();
1555        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1556
1557        let medium = prepare_bulk_medium_plugins(&mut load_order);
1558
1559        let plugin_refs: Vec<_> = medium[..257].iter().map(String::as_str).collect();
1560
1561        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1562        assert_eq!(1, load_order.active_plugin_names().len());
1563    }
1564
1565    #[test]
1566    fn set_active_plugins_should_error_if_given_more_than_4096_light_plugins() {
1567        let tmp_dir = tempdir().unwrap();
1568        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1569
1570        let light = prepare_bulk_light_plugins(&mut load_order);
1571
1572        let plugin_refs: Vec<_> = light[..4097].iter().map(String::as_str).collect();
1573
1574        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1575        assert_eq!(1, load_order.active_plugin_names().len());
1576    }
1577
1578    #[test]
1579    fn set_active_plugins_should_error_if_an_implicitly_active_blueprint_ships_plugin_is_not_given()
1580    {
1581        let tmp_dir = tempdir().unwrap();
1582        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1583
1584        let blueprint_ships = "BlueprintShips-Blank.esm";
1585        copy_to_test_dir(
1586            "Blank.full.esm",
1587            blueprint_ships,
1588            load_order.game_settings(),
1589        );
1590        add(&mut load_order, blueprint_ships).unwrap();
1591
1592        let err = set_active_plugins(&mut load_order, &["Blank.esp"]).unwrap_err();
1593
1594        match err {
1595            Error::ImplicitlyActivePlugin(n) => assert_eq!(blueprint_ships, n),
1596            e => panic!("Unexpected error type: {e:?}"),
1597        }
1598    }
1599}