Skip to content

Commit

Permalink
Refresh implicitly active plugins when loading current state
Browse files Browse the repository at this point in the history
Now that implicitly active plugins are pulled from sources that can't be
reasonably assumed to be unchanging, refresh them as part of loading
current state in case they've changed since the last time load order
data was loaded.

Do it as the first thing so that a failure doesn't mess with existing
state.
  • Loading branch information
Ortham committed Sep 25, 2023
1 parent 4c6a16f commit 60f8202
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 29 deletions.
3 changes: 3 additions & 0 deletions ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ exclude = [
libloadorder = { version = "14.2.2", path = ".." }
libc = "0.2"

[dev-dependencies]
tempfile = "3.0.0"

[lib]
name = "loadorder_ffi"
crate-type = ["staticlib"]
Expand Down
65 changes: 65 additions & 0 deletions ffi/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ pub unsafe extern "C" fn lo_load_current_state(handle: lo_game_handle) -> c_uint
Ok(h) => h,
};

if let Err(x) = handle
.game_settings_mut()
.refresh_implicitly_active_plugins()
{
return handle_error(x);
}

if let Err(x) = handle.load() {
return handle_error(x);
}
Expand Down Expand Up @@ -571,4 +578,62 @@ mod tests {
assert_eq!(LIBLO_ERROR_INVALID_ARGS, result);
}
}

#[test]
#[cfg(not(windows))]
fn lo_load_current_state_should_refresh_implicitly_active_plugins() {
let tmp_dir = tempfile::tempdir().unwrap();
let game_path = tmp_dir.path();
let local_path = game_path.join("AppData/Local/Oblivion");
let ini_path = game_path.join("Documents/My Games/Oblivion/Oblivion.ini");

let mut handle: lo_game_handle = std::ptr::null_mut();

let game_path = CString::new(game_path.to_str().unwrap()).unwrap();
let local_path = CString::new(local_path.to_str().unwrap()).unwrap();

unsafe {
let result = lo_create_handle(
&mut handle,
LIBLO_GAME_TES4,
game_path.as_ptr(),
local_path.as_ptr(),
);
assert_eq!(LIBLO_OK, result);
}

unsafe {
let mut plugins: *mut *mut c_char = std::ptr::null_mut();
let mut num_plugins: size_t = 0;

let result = lo_get_implicitly_active_plugins(handle, &mut plugins, &mut num_plugins);
assert_eq!(LIBLO_OK, result);
assert_eq!(0, num_plugins);

crate::lo_free_string_array(plugins, num_plugins);
}

std::fs::create_dir_all(&ini_path.parent().unwrap()).unwrap();
std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();

unsafe {
let result = lo_load_current_state(handle);
assert_eq!(LIBLO_OK, result);
}

unsafe {
let mut plugins: *mut *mut c_char = std::ptr::null_mut();
let mut num_plugins: size_t = 0;

let result = lo_get_implicitly_active_plugins(handle, &mut plugins, &mut num_plugins);
assert_eq!(LIBLO_OK, result);
assert_eq!(1, num_plugins);

crate::lo_free_string_array(plugins, num_plugins);
}

unsafe {
lo_destroy_handle(handle);
}
}
}
129 changes: 105 additions & 24 deletions src/game_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub struct GameSettings {
id: GameId,
plugins_directory: PathBuf,
plugins_file_path: PathBuf,
my_games_path: PathBuf,
load_order_path: Option<PathBuf>,
implicitly_active_plugins: Vec<String>,
early_loading_plugins: Vec<String>,
Expand Down Expand Up @@ -123,41 +124,35 @@ impl GameSettings {
None => PathBuf::default(),
};

GameSettings::with_local_and_my_games_paths(game_id, game_path, local_path, &my_games_path)
GameSettings::with_local_and_my_games_paths(game_id, game_path, local_path, my_games_path)
}

pub(crate) fn with_local_and_my_games_paths(
game_id: GameId,
game_path: &Path,
local_path: &Path,
my_games_path: &Path,
my_games_path: PathBuf,
) -> Result<GameSettings, Error> {
let plugins_file_path = plugins_file_path(game_id, game_path, local_path)?;
let load_order_path = load_order_path(game_id, local_path);
let plugins_directory = game_path.join(plugins_folder_name(game_id));
let additional_plugins_directories = additional_plugins_directories(game_id, game_path);

let mut test_files = test_files(game_id, game_path, my_games_path)?;

if game_id == GameId::Fallout4 || game_id == GameId::Fallout4VR {
// Fallout 4 ignores plugins.txt and Fallout4.ccc if there are valid plugins listed as test files, so filter out invalid values.
test_files.retain(|f| {
let path = plugin_path(f, &plugins_directory, &additional_plugins_directories);
Plugin::with_path(&path, game_id, false).is_ok()
});
}

let early_loading_plugins =
early_loading_plugins(game_id, game_path, !test_files.is_empty())?;

let implicitly_active_plugins =
implicitly_active_plugins(game_id, game_path, &early_loading_plugins, &test_files)?;
let (early_loading_plugins, implicitly_active_plugins) =
GameSettings::load_implicitly_active_plugins(
game_id,
game_path,
&my_games_path,
&plugins_directory,
&additional_plugins_directories,
)?;

Ok(GameSettings {
id: game_id,
plugins_directory,
plugins_file_path,
load_order_path,
my_games_path,
implicitly_active_plugins,
early_loading_plugins,
additional_plugins_directories,
Expand Down Expand Up @@ -246,6 +241,51 @@ impl GameSettings {
&self.additional_plugins_directories,
)
}

pub fn refresh_implicitly_active_plugins(&mut self) -> Result<(), Error> {
let (early_loading_plugins, implicitly_active_plugins) =
GameSettings::load_implicitly_active_plugins(
self.id,
self.plugins_directory
.parent()
.expect("plugins directory path to have parent path component"),
&self.my_games_path,
&self.plugins_directory,
&self.additional_plugins_directories,
)?;

self.early_loading_plugins = early_loading_plugins;
self.implicitly_active_plugins = implicitly_active_plugins;

Ok(())
}

fn load_implicitly_active_plugins(
game_id: GameId,
game_path: &Path,
my_games_path: &Path,
plugins_directory: &Path,
additional_plugins_directories: &[PathBuf],
) -> Result<(Vec<String>, Vec<String>), Error> {
let mut test_files = test_files(game_id, game_path, my_games_path)?;

if game_id == GameId::Fallout4 || game_id == GameId::Fallout4VR {
// Fallout 4 ignores plugins.txt and Fallout4.ccc if there are valid plugins listed as
// test files, so filter out invalid values.
test_files.retain(|f| {
let path = plugin_path(f, plugins_directory, additional_plugins_directories);
Plugin::with_path(&path, game_id, false).is_ok()
});
}

let early_loading_plugins =
early_loading_plugins(game_id, game_path, !test_files.is_empty())?;

let implicitly_active_plugins =
implicitly_active_plugins(game_id, game_path, &early_loading_plugins, &test_files)?;

Ok((early_loading_plugins, implicitly_active_plugins))
}
}

// The local path can vary depending on where the game was bought from.
Expand Down Expand Up @@ -571,7 +611,7 @@ mod tests {
game_id,
&PathBuf::from("game"),
&PathBuf::from("local"),
&PathBuf::from("my games"),
PathBuf::from("my games"),
)
.unwrap()
}
Expand All @@ -581,7 +621,7 @@ mod tests {
game_id,
game_path,
&PathBuf::default(),
&PathBuf::default(),
PathBuf::default(),
)
.unwrap()
}
Expand Down Expand Up @@ -1053,7 +1093,7 @@ mod tests {
GameId::Fallout4,
game_path,
&PathBuf::default(),
game_path,
game_path.to_path_buf(),
)
.unwrap();

Expand Down Expand Up @@ -1085,7 +1125,7 @@ mod tests {
GameId::SkyrimSE,
game_path,
&PathBuf::default(),
game_path,
game_path.to_path_buf(),
)
.unwrap();

Expand Down Expand Up @@ -1118,7 +1158,7 @@ mod tests {
GameId::Fallout4,
game_path,
&PathBuf::default(),
game_path,
game_path.to_path_buf(),
)
.unwrap();

Expand Down Expand Up @@ -1151,7 +1191,7 @@ mod tests {
GameId::Fallout4VR,
game_path,
&PathBuf::default(),
game_path,
game_path.to_path_buf(),
)
.unwrap();

Expand Down Expand Up @@ -1213,7 +1253,7 @@ mod tests {
GameId::Fallout4,
game_path,
&PathBuf::default(),
game_path,
game_path.to_path_buf(),
)
.unwrap();

Expand Down Expand Up @@ -1366,4 +1406,45 @@ mod tests {
settings.plugin_path(plugin_name)
);
}

#[test]
fn refresh_implicitly_active_plugins_should_update_early_loading_and_implicitly_active_plugins()
{
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();

let mut settings = GameSettings::with_local_and_my_games_paths(
GameId::SkyrimSE,
game_path,
&PathBuf::default(),
game_path.to_path_buf(),
)
.unwrap();

let hardcoded_plugins = vec![
"Skyrim.esm",
"Update.esm",
"Dawnguard.esm",
"HearthFires.esm",
"Dragonborn.esm",
];
assert_eq!(hardcoded_plugins, settings.early_loading_plugins());
assert_eq!(hardcoded_plugins, settings.implicitly_active_plugins());

std::fs::write(game_path.join("Skyrim.ccc"), "ccBGSSSE002-ExoticArrows.esl").unwrap();
std::fs::write(
game_path.join("Skyrim.ini"),
"[General]\nsTestFile1=plugin.esp\n",
)
.unwrap();

settings.refresh_implicitly_active_plugins().unwrap();

let mut expected_plugins = hardcoded_plugins;
expected_plugins.push("ccBGSSSE002-ExoticArrows.esl");
assert_eq!(expected_plugins, settings.early_loading_plugins());

expected_plugins.push("plugin.esp");
assert_eq!(expected_plugins, settings.implicitly_active_plugins());
}
}
8 changes: 4 additions & 4 deletions src/load_order/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ pub fn game_settings_for_test(game_id: GameId, game_path: &Path) -> GameSettings
let my_games_path = game_path.join("my games");
create_dir_all(&my_games_path).unwrap();

GameSettings::with_local_and_my_games_paths(game_id, game_path, &local_path, &my_games_path)
GameSettings::with_local_and_my_games_paths(game_id, game_path, &local_path, my_games_path)
.unwrap()
}

pub fn mock_game_files(game_id: GameId, game_dir: &Path) -> (GameSettings, Vec<Plugin>) {
let settings = game_settings_for_test(game_id, game_dir);
let mut settings = game_settings_for_test(game_id, game_dir);

copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
Expand All @@ -101,8 +101,8 @@ pub fn mock_game_files(game_id: GameId, game_dir: &Path) -> (GameSettings, Vec<P
);
copy_to_test_dir("Blank.esp", "Blàñk.esp", &settings);

// Recreate settings to account for newly-created plugin files.
let settings = game_settings_for_test(game_id, game_dir);
// Refresh settings to account for newly-created plugin files.
settings.refresh_implicitly_active_plugins().unwrap();

let plugins = vec![
Plugin::new(settings.master_file(), &settings).unwrap(),
Expand Down
2 changes: 1 addition & 1 deletion src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ mod tests {
game_id,
game_path,
&PathBuf::default(),
&PathBuf::default(),
PathBuf::default(),
)
.unwrap()
}
Expand Down

0 comments on commit 60f8202

Please sign in to comment.