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