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