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