diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c79780e..1df2655c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,7 +89,7 @@ set(PSEUDOSEM_INCLUDE_DIRS "${SOURCE_DIR}/include") ExternalProject_Add(testing-metadata PREFIX "external" - URL "https://github.com/loot/testing-metadata/archive/1.0.0.tar.gz" + URL "https://github.com/loot/testing-metadata/archive/1.1.0.tar.gz" CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "") diff --git a/docs/LOOT Metadata Syntax.html b/docs/LOOT Metadata Syntax.html index f708e1ce..0963db15 100644 --- a/docs/LOOT Metadata Syntax.html +++ b/docs/LOOT Metadata Syntax.html @@ -240,13 +240,17 @@ var { +
Key NameValue TypeNotes
bash_tagsstring listA list of Bash Tags that are supported by the masterlist's game. These Bash Tags are used to provide autocomplete suggestions in LOOT's metadata editor.
globalsmessage listA list of message data structures for messages that are displayed independently of any plugin.
pluginsplugin list and plugin setThe plugin data structures that hold all the plugin metadata within the file. It is a mixture of a list and a set because no non-regex plugin value may be equal to any other non-regex plugin value, but there may be any number of equal regex plugin values, and non-regex plugin values may be equal to regex plugin values.

If multiple plugin values match a single plugin, their metadata is merged in the order the values are listed, and as defined later in this document.

Other keys may also be present, but are not processed by LOOT. The message and plugin data structures are detailed in the next section.

An example metadata file: -

globals:
+
bash_tags:
+  - 'C.Climate'
+  - 'Relev'
+globals:
   - type: say
     content: 'You are using the latest version of LOOT.'
     condition: 'version("LOOT", "0.5.0.0", ==)'
diff --git a/src/backend/game/game_settings.cpp b/src/backend/game/game_settings.cpp
index 5d33b284..12001ede 100644
--- a/src/backend/game/game_settings.cpp
+++ b/src/backend/game/game_settings.cpp
@@ -54,73 +54,6 @@ namespace loot {
             _masterFile = "Oblivion.esm";
             _repositoryURL = "https://github.com/loot/oblivion.git";
             _repositoryBranch = "v0.8";
-
-            bashTags = {
-                "Actors.ACBS",
-                "Actors.AIData",
-                "Actors.AIPackages",
-                "Actors.AIPackagesForceAdd",
-                "Actors.Animations",
-                "Actors.CombatStyle",
-                "Actors.DeathItem",
-                "Actors.Skeleton",
-                "Actors.Spells",
-                "Actors.SpellsForceAdd",
-                "Actors.Stats",
-                "Body-F",
-                "Body-M",
-                "Body-Size-F",
-                "Body-Size-M",
-                "C.Climate",
-                "C.Light",
-                "C.Music",
-                "C.Name",
-                "C.Owner",
-                "C.RecordFlags",
-                "C.Water",
-                "Creatures.Blood",
-                "Deactivate",
-                "Delev",
-                "Eyes",
-                "Factions",
-                "Filter",
-                "Graphics",
-                "Hair",
-                "IIM",
-                "Invent",
-                "InventOnly",
-                "Merge",
-                "MustBeActiveIfImported",
-                "Names",
-                "NoMerge",
-                "NpcFaces",
-                "NpcFacesForceFullImport",
-                "NPC.Class",
-                "Npc.EyesOnly",
-                "Npc.HairOnly",
-                "NPC.Race",
-                "Relations",
-                "Relev",
-                "Roads",
-                "R.AddSpells",
-                "R.Attributes-F",
-                "R.Attributes-M",
-                "R.ChangeSpells",
-                "R.Description",
-                "R.Ears",
-                "R.Head",
-                "R.Mouth",
-                "R.Relations",
-                "R.Skills",
-                "R.Teeth",
-                "ScriptContents",
-                "Scripts",
-                "Sound",
-                "SpellStats",
-                "Stats",
-                "Voice-F",
-                "Voice-M",
-            };
         }
         else if (Id() == GameSettings::tes5) {
             _name = "TES V: Skyrim";
@@ -129,78 +62,6 @@ namespace loot {
             _masterFile = "Skyrim.esm";
             _repositoryURL = "https://github.com/loot/skyrim.git";
             _repositoryBranch = "v0.8";
-
-            bashTags = {
-                "Actors.ACBS",
-                "Actors.AIData",
-                "Actors.AIPackages",
-                "Actors.AIPackagesForceAdd",
-                "Actors.Animations",
-                "Actors.CombatStyle",
-                "Actors.DeathItem",
-                "Actors.Skeleton",
-                "Actors.Spells",
-                "Actors.SpellsForceAdd",
-                "Actors.Stats",
-                "Body-F",
-                "Body-M",
-                "Body-Size-F",
-                "Body-Size-M",
-                "C.Acoustic",
-                "C.Climate",
-                "C.ImageSpace",
-                "C.Light",
-                "C.Location",
-                "C.Music",
-                "C.Name",
-                "C.Owner",
-                "C.RecordFlags",
-                "C.Regions",
-                "C.SkyLighting",
-                "C.Water",
-                "Creatures.Blood",
-                "Deactivate",
-                "Delev",
-                "Eyes",
-                "Factions",
-                "Filter",
-                "Graphics",
-                "Hair",
-                "IIM",
-                "Invent",
-                "InventOnly",
-                "Merge",
-                "MustBeActiveIfImported",
-                "Names",
-                "NoMerge",
-                "NpcFaces",
-                "NpcFacesForceFullImport",
-                "NPC.Class",
-                "Npc.EyesOnly",
-                "Npc.HairOnly",
-                "NPC.Race",
-                "Relations",
-                "Relev",
-                "Roads",
-                "R.AddSpells",
-                "R.Attributes-F",
-                "R.Attributes-M",
-                "R.ChangeSpells",
-                "R.Description",
-                "R.Ears",
-                "R.Head",
-                "R.Mouth",
-                "R.Relations",
-                "R.Skills",
-                "R.Teeth",
-                "ScriptContents",
-                "Scripts",
-                "Sound",
-                "SpellStats",
-                "Stats",
-                "Voice-F",
-                "Voice-M",
-            };
         }
         else if (Id() == GameSettings::fo3) {
             _name = "Fallout 3";
@@ -209,11 +70,6 @@ namespace loot {
             _masterFile = "Fallout3.esm";
             _repositoryURL = "https://github.com/loot/fallout3.git";
             _repositoryBranch = "v0.8";
-
-            bashTags = {
-                "Deflst",
-                "Destructible",
-            };
         }
         else if (Id() == GameSettings::fonv) {
             _name = "Fallout: New Vegas";
@@ -222,12 +78,6 @@ namespace loot {
             _masterFile = "FalloutNV.esm";
             _repositoryURL = "https://github.com/loot/falloutnv.git";
             _repositoryBranch = "v0.8";
-
-            bashTags = {
-                "Deflst",
-                "Destructible",
-                "WeaponMods",
-            };
         }
         else if (Id() == GameSettings::fo4) {
             _name = "Fallout 4";
@@ -341,10 +191,6 @@ namespace loot {
             return LootPaths::getLootDataPath() / _lootFolderName / "userlist.yaml";
     }
 
-    std::vector GameSettings::getSupportedBashTags() const {
-        return bashTags;
-    }
-
     GameSettings& GameSettings::SetName(const std::string& name) {
         BOOST_LOG_TRIVIAL(trace) << "Setting \"" << _name << "\" name to: " << name;
         _name = name;
diff --git a/src/backend/game/game_settings.h b/src/backend/game/game_settings.h
index 9fa767d5..2d14310e 100644
--- a/src/backend/game/game_settings.h
+++ b/src/backend/game/game_settings.h
@@ -59,8 +59,6 @@ namespace loot {
         boost::filesystem::path MasterlistPath() const;
         boost::filesystem::path UserlistPath() const;
 
-        std::vector getSupportedBashTags() const;
-
         GameSettings& SetName(const std::string& name);
         GameSettings& SetMaster(const std::string& masterFile);
         GameSettings& SetRegistryKey(const std::string& registry);
@@ -86,8 +84,6 @@ namespace loot {
         std::string _repositoryBranch;
 
         boost::filesystem::path _gamePath;  //Path to the game's folder.
-
-        std::vector bashTags;
     };
 }
 
diff --git a/src/backend/metadata_list.cpp b/src/backend/metadata_list.cpp
index 48c6c341..c063e62d 100644
--- a/src/backend/metadata_list.cpp
+++ b/src/backend/metadata_list.cpp
@@ -33,9 +33,7 @@ using namespace std;
 
 namespace loot {
     void MetadataList::Load(const boost::filesystem::path& filepath) {
-        plugins.clear();
-        regexPlugins.clear();
-        messages.clear();
+        clear();
 
         BOOST_LOG_TRIVIAL(debug) << "Loading file: " << filepath;
 
@@ -58,7 +56,10 @@ namespace loot {
             }
         }
         if (metadataList["globals"])
-            messages = metadataList["globals"].as< list >();
+            messages = metadataList["globals"].as>();
+
+        if (metadataList["bash_tags"])
+            bashTags_ = metadataList["bash_tags"].as>();
 
         BOOST_LOG_TRIVIAL(debug) << "File loaded successfully.";
     }
@@ -68,6 +69,7 @@ namespace loot {
         YAML::Emitter yout;
         yout.SetIndent(2);
         yout << YAML::BeginMap
+            << YAML::Key << "bash_tags" << YAML::Value << bashTags_
             << YAML::Key << "plugins" << YAML::Value << Plugins()
             << YAML::Key << "globals" << YAML::Value << messages
             << YAML::EndMap;
@@ -78,6 +80,7 @@ namespace loot {
     }
 
     void MetadataList::clear() {
+        bashTags_.clear();
         plugins.clear();
         regexPlugins.clear();
         messages.clear();
@@ -95,6 +98,10 @@ namespace loot {
         return messages;
     }
 
+    std::set MetadataList::BashTags() const {
+        return bashTags_;
+    }
+
     // Merges multiple matching regex entries if any are found.
     PluginMetadata MetadataList::FindPlugin(const PluginMetadata& plugin) const {
         PluginMetadata match(plugin.Name());
diff --git a/src/backend/metadata_list.h b/src/backend/metadata_list.h
index 8ecfd9ee..f6400a3f 100644
--- a/src/backend/metadata_list.h
+++ b/src/backend/metadata_list.h
@@ -54,6 +54,7 @@ namespace loot {
 
         std::list Plugins() const;
         std::list Messages() const;
+        std::set BashTags() const;
 
         // Merges multiple matching regex entries if any are found.
         PluginMetadata FindPlugin(const PluginMetadata& plugin) const;
@@ -69,6 +70,7 @@ namespace loot {
         void EvalAllConditions(Game& game, const unsigned int language);
 
     protected:
+        std::set bashTags_;
         std::unordered_set plugins;
         std::list regexPlugins;
         std::list messages;
diff --git a/src/gui/handler.cpp b/src/gui/handler.cpp
index 7f15450a..4c9226d0 100644
--- a/src/gui/handler.cpp
+++ b/src/gui/handler.cpp
@@ -701,7 +701,7 @@ namespace loot {
             // Now store global messages.
             gameNode["globalMessages"] = GetGeneralMessages();
 
-            gameNode["bashTags"] = _lootState.CurrentGame().getSupportedBashTags();
+            gameNode["bashTags"] = _lootState.CurrentGame().GetMasterlist().BashTags();
 
             // Now store plugin data.
             for (const auto& plugin : installed) {
@@ -822,6 +822,9 @@ namespace loot {
                     gameNode["masterlist"]["date"] = e.what();
                 }
 
+                // Store bash tags in case they have changed.
+                gameNode["bashTags"] = _lootState.CurrentGame().GetMasterlist().BashTags();
+
                 // Store global messages in case they have changed.
                 gameNode["globalMessages"] = GetGeneralMessages();
 
diff --git a/src/gui/html/js/events.js b/src/gui/html/js/events.js
index 16d8c2be..a83a7a3c 100644
--- a/src/gui/html/js/events.js
+++ b/src/gui/html/js/events.js
@@ -80,6 +80,9 @@ function updateMasterlist() {
       loot.game.masterlist = result.masterlist;
       loot.game.globalMessages = result.globalMessages;
 
+      /* Update Bash Tag autocomplete suggestions. */
+      loot.DOM.initialiseAutocompleteBashTags(result.bashTags);
+
       result.plugins.forEach((resultPlugin) => {
         const existingPlugin = loot.game.plugins.find(plugin => plugin.name === resultPlugin.name);
         if (existingPlugin) {
diff --git a/src/tests/backend/game/game_settings_test.h b/src/tests/backend/game/game_settings_test.h
index 1e2e8c67..69c7a780 100644
--- a/src/tests/backend/game/game_settings_test.h
+++ b/src/tests/backend/game/game_settings_test.h
@@ -59,8 +59,6 @@ namespace loot {
             EXPECT_EQ("", game.DataPath());
             EXPECT_EQ("", game.MasterlistPath());
             EXPECT_EQ("", game.UserlistPath());
-
-            EXPECT_TRUE(game.getSupportedBashTags().empty());
         }
 
         TEST_P(GameSettingsTest, idConstructorShouldInitialiseSettingsToDefaultsForThatGame) {
@@ -89,180 +87,6 @@ namespace loot {
             EXPECT_EQ(LootPaths::getLootDataPath() / "folder" / "userlist.yaml", game.UserlistPath());
         }
 
-        TEST_P(GameSettingsTest, idConstructorShouldSetCorrectBashTagsForTES4) {
-            game = GameSettings(GameSettings::tes4);
-
-            std::vector expectedBashTags = {
-                "Actors.ACBS",
-                "Actors.AIData",
-                "Actors.AIPackages",
-                "Actors.AIPackagesForceAdd",
-                "Actors.Animations",
-                "Actors.CombatStyle",
-                "Actors.DeathItem",
-                "Actors.Skeleton",
-                "Actors.Spells",
-                "Actors.SpellsForceAdd",
-                "Actors.Stats",
-                "Body-F",
-                "Body-M",
-                "Body-Size-F",
-                "Body-Size-M",
-                "C.Climate",
-                "C.Light",
-                "C.Music",
-                "C.Name",
-                "C.Owner",
-                "C.RecordFlags",
-                "C.Water",
-                "Creatures.Blood",
-                "Deactivate",
-                "Delev",
-                "Eyes",
-                "Factions",
-                "Filter",
-                "Graphics",
-                "Hair",
-                "IIM",
-                "Invent",
-                "InventOnly",
-                "Merge",
-                "MustBeActiveIfImported",
-                "Names",
-                "NoMerge",
-                "NpcFaces",
-                "NpcFacesForceFullImport",
-                "NPC.Class",
-                "Npc.EyesOnly",
-                "Npc.HairOnly",
-                "NPC.Race",
-                "Relations",
-                "Relev",
-                "Roads",
-                "R.AddSpells",
-                "R.Attributes-F",
-                "R.Attributes-M",
-                "R.ChangeSpells",
-                "R.Description",
-                "R.Ears",
-                "R.Head",
-                "R.Mouth",
-                "R.Relations",
-                "R.Skills",
-                "R.Teeth",
-                "ScriptContents",
-                "Scripts",
-                "Sound",
-                "SpellStats",
-                "Stats",
-                "Voice-F",
-                "Voice-M",
-            };
-            EXPECT_EQ(expectedBashTags, game.getSupportedBashTags());
-        }
-
-        TEST_P(GameSettingsTest, idConstructorShouldSetCorrectBashTagsForTES5) {
-            game = GameSettings(GameSettings::tes5);
-
-            std::vector expectedBashTags = {
-                "Actors.ACBS",
-                "Actors.AIData",
-                "Actors.AIPackages",
-                "Actors.AIPackagesForceAdd",
-                "Actors.Animations",
-                "Actors.CombatStyle",
-                "Actors.DeathItem",
-                "Actors.Skeleton",
-                "Actors.Spells",
-                "Actors.SpellsForceAdd",
-                "Actors.Stats",
-                "Body-F",
-                "Body-M",
-                "Body-Size-F",
-                "Body-Size-M",
-                "C.Acoustic",
-                "C.Climate",
-                "C.ImageSpace",
-                "C.Light",
-                "C.Location",
-                "C.Music",
-                "C.Name",
-                "C.Owner",
-                "C.RecordFlags",
-                "C.Regions",
-                "C.SkyLighting",
-                "C.Water",
-                "Creatures.Blood",
-                "Deactivate",
-                "Delev",
-                "Eyes",
-                "Factions",
-                "Filter",
-                "Graphics",
-                "Hair",
-                "IIM",
-                "Invent",
-                "InventOnly",
-                "Merge",
-                "MustBeActiveIfImported",
-                "Names",
-                "NoMerge",
-                "NpcFaces",
-                "NpcFacesForceFullImport",
-                "NPC.Class",
-                "Npc.EyesOnly",
-                "Npc.HairOnly",
-                "NPC.Race",
-                "Relations",
-                "Relev",
-                "Roads",
-                "R.AddSpells",
-                "R.Attributes-F",
-                "R.Attributes-M",
-                "R.ChangeSpells",
-                "R.Description",
-                "R.Ears",
-                "R.Head",
-                "R.Mouth",
-                "R.Relations",
-                "R.Skills",
-                "R.Teeth",
-                "ScriptContents",
-                "Scripts",
-                "Sound",
-                "SpellStats",
-                "Stats",
-                "Voice-F",
-                "Voice-M",
-            };
-            EXPECT_EQ(expectedBashTags, game.getSupportedBashTags());
-        }
-
-        TEST_P(GameSettingsTest, idConstructorShouldSetNoBashTagsForFO3) {
-            game = GameSettings(GameSettings::fo3);
-
-            std::vector expectedBashTags = {
-                "Deflst",
-                "Destructible",
-            };
-            EXPECT_EQ(expectedBashTags, game.getSupportedBashTags());
-        }
-
-        TEST_P(GameSettingsTest, idConstructorShouldSetNoBashTagsForFONV) {
-            game = GameSettings(GameSettings::fonv);
-
-            std::vector expectedBashTags = {
-                "Deflst",
-                "Destructible",
-                "WeaponMods",
-            };
-            EXPECT_EQ(expectedBashTags, game.getSupportedBashTags());
-        }
-
-        TEST_P(GameSettingsTest, idConstructorShouldSetNoBashTagsForFO4) {
-            EXPECT_TRUE(GameSettings(GameSettings::fo4).getSupportedBashTags().empty());
-        }
-
         TEST_P(GameSettingsTest, isInstalledShouldBeFalseIfGamePathIsNotSet) {
             GameSettings game;
             EXPECT_FALSE(game.IsInstalled());
diff --git a/src/tests/backend/metadata_list_test.h b/src/tests/backend/metadata_list_test.h
index 1f66edde..7ef81c93 100644
--- a/src/tests/backend/metadata_list_test.h
+++ b/src/tests/backend/metadata_list_test.h
@@ -26,6 +26,8 @@ along with LOOT.  If not, see
 #define LOOT_TEST_BACKEND_METADATA_LIST
 
 #include "backend/metadata_list.h"
+
+#include "backend/game/game.h"
 #include "tests/base_game_test.h"
 
 namespace loot {
@@ -110,6 +112,16 @@ namespace loot {
             }), names);
         }
 
+        TEST_P(MetadataListTest, loadShouldLoadBashTags) {
+            MetadataList metadataList;
+            ASSERT_NO_THROW(metadataList.Load(metadataPath));
+
+            EXPECT_EQ(std::set({
+                "C.Climate",
+                "Relev"
+            }), metadataList.BashTags());
+        }
+
         TEST_P(MetadataListTest, loadShouldThrowIfAnInvalidMetadataFileIsGiven) {
             MetadataList ml;
             for (const auto& path : invalidMetadataPaths) {
@@ -123,10 +135,12 @@ namespace loot {
             ASSERT_NO_THROW(metadataList.Load(metadataPath));
             ASSERT_FALSE(metadataList.Messages().empty());
             ASSERT_FALSE(metadataList.Plugins().empty());
+            ASSERT_FALSE(metadataList.BashTags().empty());
 
             EXPECT_ANY_THROW(metadataList.Load(blankEsm));
             EXPECT_TRUE(metadataList.Messages().empty());
             EXPECT_TRUE(metadataList.Plugins().empty());
+            EXPECT_TRUE(metadataList.BashTags().empty());
         }
 
         TEST_P(MetadataListTest, loadShouldClearExistingDataIfAMissingMetadataFileIsGiven) {
@@ -135,10 +149,12 @@ namespace loot {
             ASSERT_NO_THROW(metadataList.Load(metadataPath));
             ASSERT_FALSE(metadataList.Messages().empty());
             ASSERT_FALSE(metadataList.Plugins().empty());
+            ASSERT_FALSE(metadataList.BashTags().empty());
 
             EXPECT_ANY_THROW(metadataList.Load(missingMetadataPath));
             EXPECT_TRUE(metadataList.Messages().empty());
             EXPECT_TRUE(metadataList.Plugins().empty());
+            EXPECT_TRUE(metadataList.BashTags().empty());
         }
 
         TEST_P(MetadataListTest, saveShouldWriteTheLoadedMetadataToTheGivenFilePath) {
@@ -152,6 +168,11 @@ namespace loot {
             // Check the new file contains the same metadata.
             EXPECT_NO_THROW(metadataList.Load(savedMetadataPath));
 
+            EXPECT_EQ(std::set({
+                "C.Climate",
+                "Relev"
+            }), metadataList.BashTags());
+
             EXPECT_EQ(std::list({
                 Message(Message::say, "A global message."),
             }), metadataList.Messages());
@@ -173,15 +194,17 @@ namespace loot {
             }), names);
         }
 
-        TEST_P(MetadataListTest, clearShouldClearLoadedMessagesAndPlugins) {
+        TEST_P(MetadataListTest, clearShouldClearLoadedData) {
             MetadataList metadataList;
             ASSERT_NO_THROW(metadataList.Load(metadataPath));
             ASSERT_FALSE(metadataList.Messages().empty());
             ASSERT_FALSE(metadataList.Plugins().empty());
+            ASSERT_FALSE(metadataList.BashTags().empty());
 
             metadataList.clear();
             EXPECT_TRUE(metadataList.Messages().empty());
             EXPECT_TRUE(metadataList.Plugins().empty());
+            EXPECT_TRUE(metadataList.BashTags().empty());
         }
 
         TEST_P(MetadataListTest, findPluginShouldReturnAnEmptyPluginObjectIfTheGivenPluginIsNotInTheMetadataList) {