From 7942083cd0c8187529e3b78355145b8141c418af Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Thu, 14 May 2026 18:38:42 +0100 Subject: [PATCH] Allow loaded plugins to be retrieved using a .ghost extension libloot trims any .ghost extension from plugins it loads, so be consistent with that behaviour when getting or sorting plugins. --- .../tests/api/interface/game_interface_test.h | 28 +++++++++++++++++-- src/game.rs | 22 +++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/cpp/src/tests/api/interface/game_interface_test.h b/cpp/src/tests/api/interface/game_interface_test.h index 83dbcf6d..9f3efe7a 100644 --- a/cpp/src/tests/api/interface/game_interface_test.h +++ b/cpp/src/tests/api/interface/game_interface_test.h @@ -580,6 +580,23 @@ TEST_P(GameInterfaceTest, EXPECT_NE(pointer1, pointer2); } +TEST_P(GameInterfaceTest, getPluginShouldStripGhostExtension) { + if (GetParam() == GameType::openmw) { + return; + } + + const auto ghostedName = std::string(BLANK_ESP) + ".ghost"; + copyPlugin(BLANK_ESP, ghostedName); + + handle_->LoadPlugins({BLANK_ESP}, true); + + EXPECT_EQ(1, handle_->GetLoadedPlugins().size()); + + const auto plugin = handle_->GetPlugin(ghostedName); + ASSERT_NE(nullptr, plugin); + EXPECT_EQ(BLANK_ESP, plugin->GetName()); +} + TEST_P(GameInterfaceTest, getLoadedPluginsShouldReturnAnEmptySetIfNoneHaveBeenLoaded) { EXPECT_TRUE(handle_->GetLoadedPlugins().empty()); @@ -666,14 +683,19 @@ TEST_P(GameInterfaceTest, sortPluginsShouldSupportGhostedPlugins) { return; } + const auto pluginName = + GetParam() == GameType::starfield ? BLANK_FULL_ESM : BLANK_ESM; + + copyPlugin(pluginName); copyPlugin(BLANK_ESP, "Blank.esp.ghost"); - handle_->LoadPlugins({BLANK_ESP}, false); + handle_->LoadPlugins({pluginName, BLANK_ESP}, false); const auto sortedOrder = handle_->SortPlugins( - {std::string(BLANK_ESP)}); + {std::string(pluginName) + ".ghost", std::string(BLANK_ESP)}); - std::vector expectedOrder{std::string(BLANK_ESP)}; + std::vector expectedOrder{std::string(pluginName), + std::string(BLANK_ESP)}; EXPECT_EQ(expectedOrder, sortedOrder); } diff --git a/src/game.rs b/src/game.rs index 13cf9c4d..cc058837 100644 --- a/src/game.rs +++ b/src/game.rs @@ -19,7 +19,7 @@ use crate::{ logging::{self, format_details, is_log_enabled}, metadata::{ Filename, - plugin_metadata::{GHOST_FILE_EXTENSION, iends_with_ascii}, + plugin_metadata::{GHOST_FILE_EXTENSION, iends_with_ascii, trim_dot_ghost}, }, plugin::{ LoadScope, Plugin, @@ -821,7 +821,8 @@ impl GameCache { } fn plugin(&self, plugin_name: &str) -> Option<&Arc> { - self.plugins.get(&Filename::new(plugin_name.to_owned())) + self.plugins + .get(&Filename::new(trim_dot_ghost(plugin_name).to_owned())) } pub(crate) fn archives_iter(&self) -> impl Iterator { @@ -2274,6 +2275,23 @@ mod tests { assert_eq!("Blank.esm", cache.plugin("blank.esm").unwrap().name()); } + #[test] + fn should_trim_dot_ghost_extension() { + let mut cache = GameCache::default(); + + cache.insert_plugins(vec![ + Plugin::new( + GameType::Oblivion, + &cache, + &source_plugins_path(GameType::Oblivion).join(BLANK_ESM), + LoadScope::HeaderOnly, + ) + .unwrap(), + ]); + + assert_eq!("Blank.esm", cache.plugin("blank.esm.ghost").unwrap().name()); + } + #[test] fn should_return_none_if_the_plugin_is_not_cached() { let cache = GameCache::default();