Skip to content

Commit

Permalink
Don't special-case the game master file
Browse files Browse the repository at this point in the history
In all the games where the game's main master file must load first, it's also the first hardcoded plugin. Checking early loading plugins instead also ensures that any other such plugins also get loaded in the correct locations.

This fixes being unable to set a load order for Starfield where Starfield.esm is not the first plugin.
  • Loading branch information
Ortham committed Jun 29, 2024
1 parent 6513d92 commit c2622a3
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 158 deletions.
202 changes: 112 additions & 90 deletions src/load_order/asterisk_based.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -80,27 +82,6 @@ impl MutableLoadOrder for AsteriskBasedLoadOrder {
fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
&mut self.plugins
}

fn insert_position(&self, plugin: &Plugin) -> Option<usize> {
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 {
Expand Down Expand Up @@ -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<usize, Error> {
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)
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit c2622a3

Please sign in to comment.