diff --git a/src/load_order/asterisk_based.rs b/src/load_order/asterisk_based.rs index 9309349..b999dd8 100644 --- a/src/load_order/asterisk_based.rs +++ b/src/load_order/asterisk_based.rs @@ -20,9 +20,11 @@ use std::collections::HashSet; use std::fs::File; use std::io::{BufWriter, Write}; -use unicase::{eq, UniCase}; +use unicase::UniCase; -use super::mutable::{generic_insert_position, hoist_masters, read_plugin_names, MutableLoadOrder}; +use super::mutable::{ + hoist_masters, read_plugin_names, validate_early_loader_positions, MutableLoadOrder, +}; use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase}; use super::strict_encode; use super::timestamp_based::save_load_order_using_timestamps; @@ -80,27 +82,6 @@ impl MutableLoadOrder for AsteriskBasedLoadOrder { fn plugins_mut(&mut self) -> &mut Vec { &mut self.plugins } - - fn insert_position(&self, plugin: &Plugin) -> Option { - if self.game_settings().loads_early(plugin.name()) { - if self.plugins().is_empty() { - return None; - } - - let mut loaded_plugin_count = 0; - for plugin_name in self.game_settings().early_loading_plugins() { - if eq(plugin.name(), plugin_name) { - return Some(loaded_plugin_count); - } - - if self.index_of(plugin_name).is_some() { - loaded_plugin_count += 1; - } - } - } - - generic_insert_position(self.plugins(), plugin) - } } impl WritableLoadOrder for AsteriskBasedLoadOrder { @@ -165,52 +146,20 @@ impl WritableLoadOrder for AsteriskBasedLoadOrder { } fn set_load_order(&mut self, plugin_names: &[&str]) -> Result<(), Error> { - let game_master_file = self.game_settings().master_file(); - - let is_game_master_first = plugin_names - .first() - .map(|name| eq(*name, game_master_file)) - .unwrap_or(false); - if !is_game_master_first { - return Err(Error::GameMasterMustLoadFirst(game_master_file.to_string())); - } - - // Check that all early loading plugins that are present load in - // their hardcoded order. - let mut missing_plugins_count = 0; - for (i, plugin_name) in self - .game_settings() - .early_loading_plugins() - .iter() - .enumerate() - { - match plugin_names.iter().position(|n| eq(*n, plugin_name)) { - Some(pos) => { - let expected_pos = i - missing_plugins_count; - if pos != expected_pos { - return Err(Error::InvalidEarlyLoadingPluginPosition { - name: plugin_name.clone(), - pos, - expected_pos, - }); - } - } - None => missing_plugins_count += 1, - } - } + validate_early_loader_positions( + plugin_names, + self.game_settings().early_loading_plugins(), + )?; self.replace_plugins(plugin_names) } fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result { - let game_master_file = self.game_settings().master_file(); - - if position != 0 && !self.plugins().is_empty() && eq(plugin_name, game_master_file) { - return Err(Error::GameMasterMustLoadFirst(game_master_file.to_string())); - } - if position == 0 && !eq(plugin_name, game_master_file) { - return Err(Error::GameMasterMustLoadFirst(game_master_file.to_string())); - } + self.validate_new_plugin_index( + plugin_name, + position, + self.game_settings().early_loading_plugins(), + )?; self.move_or_insert_plugin_with_index(plugin_name, position) } @@ -1188,28 +1137,6 @@ mod tests { assert_eq!(original_timestamp, new_timestamp); } - #[test] - fn set_load_order_should_error_if_given_an_empty_list() { - let tmp_dir = tempdir().unwrap(); - let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path()); - - let existing_filenames = to_owned(load_order.plugin_names()); - let filenames = vec![]; - assert!(load_order.set_load_order(&filenames).is_err()); - assert_eq!(existing_filenames, load_order.plugin_names()); - } - - #[test] - fn set_load_order_should_error_if_the_first_element_given_is_not_the_game_master() { - let tmp_dir = tempdir().unwrap(); - let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path()); - - let existing_filenames = to_owned(load_order.plugin_names()); - let filenames = vec!["Blank.esp"]; - assert!(load_order.set_load_order(&filenames).is_err()); - assert_eq!(existing_filenames, load_order.plugin_names()); - } - #[test] fn set_load_order_should_error_if_an_early_loading_plugin_loads_after_another_plugin() { let tmp_dir = tempdir().unwrap(); @@ -1343,25 +1270,120 @@ mod tests { } #[test] - fn set_plugin_index_should_error_if_setting_the_game_master_index_to_non_zero_in_bounds() { + fn set_plugin_index_should_error_if_moving_a_plugin_before_an_early_loader() { let tmp_dir = tempdir().unwrap(); let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path()); let existing_filenames = to_owned(load_order.plugin_names()); - assert!(load_order.set_plugin_index("Skyrim.esm", 1).is_err()); + + match load_order.set_plugin_index("Blank.esp", 0).unwrap_err() { + Error::InvalidEarlyLoadingPluginPosition { + name, + pos, + expected_pos, + } => { + assert_eq!("Skyrim.esm", name); + assert_eq!(1, pos); + assert_eq!(0, expected_pos); + } + e => panic!( + "Expected InvalidEarlyLoadingPluginPosition error, got {:?}", + e + ), + }; + assert_eq!(existing_filenames, load_order.plugin_names()); } #[test] - fn set_plugin_index_should_error_if_setting_a_zero_index_for_a_non_game_master_plugin() { + fn set_plugin_index_should_error_if_moving_an_early_loader_to_a_different_position() { let tmp_dir = tempdir().unwrap(); let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path()); let existing_filenames = to_owned(load_order.plugin_names()); - assert!(load_order.set_plugin_index("Blank.esm", 0).is_err()); + + match load_order.set_plugin_index("Skyrim.esm", 1).unwrap_err() { + Error::InvalidEarlyLoadingPluginPosition { + name, + pos, + expected_pos, + } => { + assert_eq!("Skyrim.esm", name); + assert_eq!(1, pos); + assert_eq!(0, expected_pos); + } + e => panic!( + "Expected InvalidEarlyLoadingPluginPosition error, got {:?}", + e + ), + }; + + assert_eq!(existing_filenames, load_order.plugin_names()); + } + + #[test] + fn set_plugin_index_should_error_if_inserting_an_early_loader_to_the_wrong_position() { + let tmp_dir = tempdir().unwrap(); + let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path()); + + load_order.set_plugin_index("Blank.esm", 1).unwrap(); + copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings()); + + let existing_filenames = to_owned(load_order.plugin_names()); + + match load_order + .set_plugin_index("Dragonborn.esm", 2) + .unwrap_err() + { + Error::InvalidEarlyLoadingPluginPosition { + name, + pos, + expected_pos, + } => { + assert_eq!("Dragonborn.esm", name); + assert_eq!(2, pos); + assert_eq!(1, expected_pos); + } + e => panic!( + "Expected InvalidEarlyLoadingPluginPosition error, got {:?}", + e + ), + }; + assert_eq!(existing_filenames, load_order.plugin_names()); } + #[test] + fn set_plugin_index_should_succeed_if_setting_an_early_loader_to_its_current_position() { + let tmp_dir = tempdir().unwrap(); + let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path()); + + assert!(load_order.set_plugin_index("Skyrim.esm", 0).is_ok()); + assert_eq!( + vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"], + load_order.plugin_names() + ); + } + + #[test] + fn set_plugin_index_should_succeed_if_inserting_a_new_early_loader() { + let tmp_dir = tempdir().unwrap(); + let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path()); + + copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings()); + + assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok()); + assert_eq!( + vec![ + "Skyrim.esm", + "Dragonborn.esm", + "Blank.esp", + "Blank - Different.esp" + ], + load_order.plugin_names() + ); + } + #[test] fn set_plugin_index_should_insert_a_new_plugin() { let tmp_dir = tempdir().unwrap(); diff --git a/src/load_order/mutable.rs b/src/load_order/mutable.rs index df66cf7..07dbc8f 100644 --- a/src/load_order/mutable.rs +++ b/src/load_order/mutable.rs @@ -25,7 +25,7 @@ use std::path::{Path, PathBuf}; use encoding_rs::WINDOWS_1252; use rayon::prelude::*; -use unicase::UniCase; +use unicase::{eq, UniCase}; use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase}; use crate::enums::Error; @@ -36,7 +36,38 @@ use crate::GameId; pub trait MutableLoadOrder: ReadableLoadOrder + ReadableLoadOrderBase + Sync { fn plugins_mut(&mut self) -> &mut Vec; - fn insert_position(&self, plugin: &Plugin) -> Option; + fn insert_position(&self, plugin: &Plugin) -> Option { + if self.game_settings().loads_early(plugin.name()) { + if self.plugins().is_empty() { + return None; + } + + let mut loaded_plugin_count = 0; + for plugin_name in self.game_settings().early_loading_plugins() { + if eq(plugin.name(), plugin_name) { + return Some(loaded_plugin_count); + } + + if self.index_of(plugin_name).is_some() { + loaded_plugin_count += 1; + } + } + } + + if plugin.is_master_file() { + find_first_non_master_position(self.plugins()) + } else { + // Check that there isn't a master that would hoist this plugin. + self.plugins() + .iter() + .filter(|p| p.is_master_file()) + .position(|p| { + p.masters() + .map(|masters| masters.iter().any(|m| plugin.name_matches(m))) + .unwrap_or(false) + }) + } + } fn find_plugins(&self) -> Vec { // A game might store some plugins outside of its main plugins directory @@ -146,6 +177,46 @@ pub trait MutableLoadOrder: ReadableLoadOrder + ReadableLoadOrderBase + Sync { Ok(()) } + + fn validate_new_plugin_index( + &self, + plugin_name: &str, + position: usize, + early_loading_plugins: &[String], + ) -> Result<(), Error> { + let mut next_index = 0; + for early_loader in early_loading_plugins { + let names_match = eq(plugin_name, &early_loader); + + let expected_index = match self.index_of(&early_loader) { + Some(i) => { + next_index = i + 1; + + if !names_match && position == i { + // We're trying to insert a plugin at this position but we don' + return Err(Error::InvalidEarlyLoadingPluginPosition { + name: early_loader.to_string(), + pos: i + 1, + expected_pos: i, + }); + } + + i + } + None => next_index, + }; + + if names_match && position != expected_index { + return Err(Error::InvalidEarlyLoadingPluginPosition { + name: plugin_name.to_string(), + pos: position, + expected_pos: expected_index, + }); + } + } + + Ok(()) + } } pub fn load_active_plugins(load_order: &mut T, line_mapper: F) -> Result<(), Error> @@ -235,17 +306,30 @@ pub fn hoist_masters(plugins: &mut Vec) -> Result<(), Error> { Ok(()) } -pub fn generic_insert_position(plugins: &[Plugin], plugin: &Plugin) -> Option { - if plugin.is_master_file() { - find_first_non_master_position(plugins) - } else { - // Check that there isn't a master that would hoist this plugin. - plugins.iter().filter(|p| p.is_master_file()).position(|p| { - p.masters() - .map(|masters| masters.iter().any(|m| plugin.name_matches(m))) - .unwrap_or(false) - }) +pub fn validate_early_loader_positions( + plugin_names: &[&str], + early_loading_plugins: &[String], +) -> Result<(), Error> { + // Check that all early loading plugins that are present load in + // their hardcoded order. + let mut missing_plugins_count = 0; + for (i, plugin_name) in early_loading_plugins.iter().enumerate() { + match plugin_names.iter().position(|n| eq(*n, plugin_name)) { + Some(pos) => { + let expected_pos = i - missing_plugins_count; + if pos != expected_pos { + return Err(Error::InvalidEarlyLoadingPluginPosition { + name: plugin_name.clone(), + pos, + expected_pos, + }); + } + } + None => missing_plugins_count += 1, + } } + + Ok(()) } fn find_plugins_in_dirs(directories: &[PathBuf], game: GameId) -> Vec { @@ -591,10 +675,6 @@ mod tests { fn plugins_mut(&mut self) -> &mut Vec { &mut self.plugins } - - fn insert_position(&self, plugin: &Plugin) -> Option { - generic_insert_position(self.plugins(), plugin) - } } fn prepare(game_path: &Path) -> GameSettings { diff --git a/src/load_order/textfile_based.rs b/src/load_order/textfile_based.rs index 35c1ac1..22ade65 100644 --- a/src/load_order/textfile_based.rs +++ b/src/load_order/textfile_based.rs @@ -24,8 +24,8 @@ use std::path::{Path, PathBuf}; use unicase::{eq, UniCase}; use super::mutable::{ - generic_insert_position, hoist_masters, load_active_plugins, plugin_line_mapper, - read_plugin_names, MutableLoadOrder, + hoist_masters, load_active_plugins, plugin_line_mapper, read_plugin_names, + validate_early_loader_positions, MutableLoadOrder, }; use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase}; use super::strict_encode; @@ -110,20 +110,6 @@ impl MutableLoadOrder for TextfileBasedLoadOrder { fn plugins_mut(&mut self) -> &mut Vec { &mut self.plugins } - - fn insert_position(&self, plugin: &Plugin) -> Option { - let is_game_master = eq(plugin.name(), self.game_settings().master_file()); - - if is_game_master { - if self.plugins().is_empty() { - None - } else { - Some(0) - } - } else { - generic_insert_position(self.plugins(), plugin) - } - } } impl WritableLoadOrder for TextfileBasedLoadOrder { @@ -174,23 +160,20 @@ impl WritableLoadOrder for TextfileBasedLoadOrder { } fn set_load_order(&mut self, plugin_names: &[&str]) -> Result<(), Error> { - let game_master_file = self.game_settings().master_file(); - if plugin_names.is_empty() || !eq(plugin_names[0], game_master_file) { - return Err(Error::GameMasterMustLoadFirst(game_master_file.to_string())); - } + validate_early_loader_positions( + plugin_names, + self.game_settings().early_loading_plugins(), + )?; self.replace_plugins(plugin_names) } fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result { - let game_master_file = self.game_settings().master_file(); - - if position != 0 && !self.plugins().is_empty() && eq(plugin_name, game_master_file) { - return Err(Error::GameMasterMustLoadFirst(game_master_file.to_string())); - } - if position == 0 && !eq(plugin_name, game_master_file) { - return Err(Error::GameMasterMustLoadFirst(game_master_file.to_string())); - } + self.validate_new_plugin_index( + plugin_name, + position, + self.game_settings().early_loading_plugins(), + )?; self.move_or_insert_plugin_with_index(plugin_name, position) } @@ -810,24 +793,13 @@ mod tests { }; } - #[test] - fn set_load_order_should_error_if_given_an_empty_list() { - let tmp_dir = tempdir().unwrap(); - let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path()); - - let existing_filenames = to_owned(load_order.plugin_names()); - let filenames = vec![]; - assert!(load_order.set_load_order(&filenames).is_err()); - assert_eq!(existing_filenames, load_order.plugin_names()); - } - #[test] fn set_load_order_should_error_if_the_first_element_given_is_not_the_game_master() { let tmp_dir = tempdir().unwrap(); let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path()); let existing_filenames = to_owned(load_order.plugin_names()); - let filenames = vec!["Blank.esp"]; + let filenames = vec!["Blank.esp", "Skyrim.esm"]; assert!(load_order.set_load_order(&filenames).is_err()); assert_eq!(existing_filenames, load_order.plugin_names()); } diff --git a/src/load_order/timestamp_based.rs b/src/load_order/timestamp_based.rs index 09f863e..da3f2ac 100644 --- a/src/load_order/timestamp_based.rs +++ b/src/load_order/timestamp_based.rs @@ -24,9 +24,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use rayon::prelude::*; use regex::Regex; -use super::mutable::{ - generic_insert_position, hoist_masters, load_active_plugins, MutableLoadOrder, -}; +use super::mutable::{hoist_masters, load_active_plugins, MutableLoadOrder}; use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase}; use super::strict_encode; use super::writable::{ @@ -102,10 +100,6 @@ impl MutableLoadOrder for TimestampBasedLoadOrder { fn plugins_mut(&mut self) -> &mut Vec { &mut self.plugins } - - fn insert_position(&self, plugin: &Plugin) -> Option { - generic_insert_position(self.plugins(), plugin) - } } impl WritableLoadOrder for TimestampBasedLoadOrder { diff --git a/src/load_order/writable.rs b/src/load_order/writable.rs index 542c9bb..b6358ce 100644 --- a/src/load_order/writable.rs +++ b/src/load_order/writable.rs @@ -279,7 +279,7 @@ mod tests { use crate::enums::GameId; use crate::game_settings::GameSettings; - use crate::load_order::mutable::{generic_insert_position, MutableLoadOrder}; + use crate::load_order::mutable::MutableLoadOrder; use crate::load_order::readable::{ReadableLoadOrder, ReadableLoadOrderBase}; use crate::load_order::tests::{load_and_insert, mock_game_files, set_master_flag}; use crate::tests::copy_to_test_dir; @@ -303,10 +303,6 @@ mod tests { fn plugins_mut(&mut self) -> &mut Vec { &mut self.plugins } - - fn insert_position(&self, plugin: &Plugin) -> Option { - generic_insert_position(self.plugins(), plugin) - } } fn prepare(game_id: GameId, game_dir: &Path) -> TestLoadOrder {