From f7124a1ab381ae21713805690c8479f2518e5af9 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 3 Mar 2024 00:18:26 +0200 Subject: [PATCH 01/53] Added parsing of ExclusionGroup. --- SPID/include/LookupConfigs.h | 13 ++++++++++ SPID/src/LookupConfigs.cpp | 50 +++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index b3111f8..fe7b4ff 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -50,9 +50,22 @@ namespace INI Chance chance{ 100 }; std::string path{}; }; + + struct Exclusion + { + std::string name{}; + Filters rawFormFilters{}; + std::string path{}; + }; + using DataVec = std::vector; inline StringMap configs{}; + /// + /// A raw list of ExclusionGroups that will be processed along with configs. + /// + inline std::vector exclusions{}; + std::pair GetConfigs(); } diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 8e42af4..11444af 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -46,6 +46,48 @@ namespace INI return newValue; } + std::optional parse_exclusion(const std::string& a_key, const std::string& a_value, const std::string& a_path) + { + if (a_key != "ExclusionGroup") { + return std::nullopt; + } + + const auto sections = string::split(a_value, "|"); + const auto size = sections.size(); + + if (size == 0) { + logger::warn("IGNORED: ExclusionGroup must have a name: {} = {}"sv, a_key, a_value); + return std::nullopt; + } + + if (size == 1) { + logger::warn("IGNORED: ExclusionGroup must have at least one filter name: {} = {}"sv, a_key, a_value); + return std::nullopt; + } + + auto split_IDs = distribution::split_entry(sections[1]); + + if (split_IDs.empty()) { + logger::warn("ExclusionGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); + return std::nullopt; + } + + Exclusion group{}; + group.name = sections[0]; + group.path = a_path; + + for (auto& IDs : split_IDs) { + if (IDs.at(0) == '-') { + IDs.erase(0, 1); + group.rawFormFilters.NOT.push_back(distribution::get_record(IDs)); + } else { + group.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + } + } + + return group; + } + std::pair> parse_ini(const std::string& a_key, const std::string& a_value, const std::string& a_path) { Data data{}; @@ -262,7 +304,13 @@ namespace INI auto truncatedPath = path.substr(5); //strip "Data\\" for (auto& [key, entry] : *values) { - try { + try { + if (const auto exclusionOpt = detail::parse_exclusion(key.pItem, entry, truncatedPath); exclusionOpt) { + const auto& exclusion = *exclusionOpt; + exclusions[exclusion.name] = exclusion; + continue; + } + auto [data, sanitized_str] = detail::parse_ini(key.pItem, entry, truncatedPath); configs[key.pItem].emplace_back(data); From 73149be618c99b73aa2e4fa497fc619c85479c0d Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Mon, 11 Mar 2024 01:43:21 +0200 Subject: [PATCH 02/53] Implemented ExclusionGroups logic. --- SPID/cmake/headerlist.cmake | 1 + SPID/cmake/sourcelist.cmake | 1 + SPID/include/Distribute.h | 24 +++++++------- SPID/include/ExclusionGroups.h | 44 ++++++++++++++++++++++++++ SPID/include/LookupConfigs.h | 5 ++- SPID/include/LookupNPC.h | 8 +++++ SPID/src/DistributeManager.cpp | 2 +- SPID/src/ExclusionGroups.cpp | 58 ++++++++++++++++++++++++++++++++++ SPID/src/LookupConfigs.cpp | 2 +- SPID/src/LookupForms.cpp | 6 ++++ SPID/src/LookupNPC.cpp | 13 ++++++++ 11 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 SPID/include/ExclusionGroups.h create mode 100644 SPID/src/ExclusionGroups.cpp diff --git a/SPID/cmake/headerlist.cmake b/SPID/cmake/headerlist.cmake index c2836ca..9346806 100644 --- a/SPID/cmake/headerlist.cmake +++ b/SPID/cmake/headerlist.cmake @@ -5,6 +5,7 @@ set(headers ${headers} include/Distribute.h include/DistributeManager.h include/DistributePCLevelMult.h + include/ExclusionGroups.h include/FormData.h include/KeywordDependencies.h include/LogBuffer.h diff --git a/SPID/cmake/sourcelist.cmake b/SPID/cmake/sourcelist.cmake index dde5501..46bd8f1 100644 --- a/SPID/cmake/sourcelist.cmake +++ b/SPID/cmake/sourcelist.cmake @@ -3,6 +3,7 @@ set(sources ${sources} src/Distribute.cpp src/DistributeManager.cpp src/DistributePCLevelMult.cpp + src/ExclusionGroups.cpp src/FormData.cpp src/KeywordDependencies.cpp src/LogBuffer.cpp diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 2c5bbc2..d433c36 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -77,7 +77,7 @@ namespace Distribute // for now, only packages/death items use this template void for_each_form( - const NPCData& a_npcData, + NPCData& a_npcData, Forms::Distributables
& a_distributables, const PCLevelMult::Input& a_input, std::function a_callback) @@ -85,7 +85,7 @@ namespace Distribute auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); for (auto& formData : vec) { - if (detail::passed_filters(a_npcData, a_input, formData)) { + if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { a_callback(formData.form, formData.idxOrCount); ++formData.npcCount; } @@ -96,7 +96,7 @@ namespace Distribute // skins template void for_each_form( - const NPCData& a_npcData, + NPCData& a_npcData, Forms::Distributables& a_distributables, const PCLevelMult::Input& a_input, std::function a_callback) @@ -104,7 +104,7 @@ namespace Distribute auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); for (auto& formData : vec) { // Vector is reversed in FinishLookupForms - if (detail::passed_filters(a_npcData, a_input, formData) && a_callback(formData.form)) { + if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData) && a_callback(formData.form)) { ++formData.npcCount; break; } @@ -114,14 +114,14 @@ namespace Distribute // outfits/sleep outfits template void for_each_form( - const NPCData& a_npcData, + NPCData& a_npcData, Forms::Distributables& a_distributables, std::function a_callback) { auto& vec = a_distributables.GetForms(false); for (auto& formData : vec) { // Vector is reversed in FinishLookupForms - if (detail::passed_filters(a_npcData, formData) && a_callback(formData.form)) { + if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, formData) && a_callback(formData.form)) { ++formData.npcCount; break; } @@ -131,7 +131,7 @@ namespace Distribute // items template void for_each_form( - const NPCData& a_npcData, + NPCData& a_npcData, Forms::Distributables& a_distributables, const PCLevelMult::Input& a_input, std::function&, bool)> a_callback) @@ -146,7 +146,7 @@ namespace Distribute bool hasLeveledItems = false; for (auto& formData : vec) { - if (detail::passed_filters(a_npcData, a_input, formData)) { + if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { if (formData.form->Is(RE::FormType::LeveledItem)) { hasLeveledItems = true; } @@ -192,7 +192,7 @@ namespace Distribute continue; } if constexpr (std::is_same_v) { - if (detail::passed_filters(a_npcData, a_input, formData) && a_npcData.InsertKeyword(form->GetFormEditorID())) { + if (!a_npcData.HasMutuallyExclusiveForm(form) && detail::passed_filters(a_npcData, a_input, formData) && a_npcData.InsertKeyword(form->GetFormEditorID())) { collectedForms.emplace_back(form); collectedFormIDs.emplace(formID); if (formData.filters.HasLevelFilters()) { @@ -201,7 +201,7 @@ namespace Distribute ++formData.npcCount; } } else { - if (detail::passed_filters(a_npcData, a_input, formData) && !detail::has_form(npc, form) && collectedFormIDs.emplace(formID).second) { + if (!a_npcData.HasMutuallyExclusiveForm(form) && detail::passed_filters(a_npcData, a_input, formData) && !detail::has_form(npc, form) && collectedFormIDs.emplace(formID).second) { collectedForms.emplace_back(form); if (formData.filters.HasLevelFilters()) { collectedLeveledFormIDs.emplace(formID); @@ -246,13 +246,13 @@ namespace Distribute continue; } if constexpr (std::is_same_v) { - if (detail::passed_filters(a_npcData, formData) && a_npcData.InsertKeyword(form->GetFormEditorID())) { + if (!a_npcData.HasMutuallyExclusiveForm(form) && detail::passed_filters(a_npcData, formData) && a_npcData.InsertKeyword(form->GetFormEditorID())) { collectedForms.emplace_back(form); collectedFormIDs.emplace(formID); ++formData.npcCount; } } else { - if (detail::passed_filters(a_npcData, formData) && !detail::has_form(npc, form) && collectedFormIDs.emplace(formID).second) { + if (!a_npcData.HasMutuallyExclusiveForm(form) && detail::passed_filters(a_npcData, formData) && !detail::has_form(npc, form) && collectedFormIDs.emplace(formID).second) { collectedForms.emplace_back(form); ++formData.npcCount; } diff --git a/SPID/include/ExclusionGroups.h b/SPID/include/ExclusionGroups.h new file mode 100644 index 0000000..9bee81e --- /dev/null +++ b/SPID/include/ExclusionGroups.h @@ -0,0 +1,44 @@ +#pragma once +#include "LookupConfigs.h" + +namespace Exclusion +{ + using GroupName = std::string; + using LinkedGroups = std::unordered_map>; + using ExclusionGroups = std::unordered_map>; + + class Manager : public ISingleton + { + public: + + /// + /// Does a forms lookup similar to what Filters do. + /// + /// As a result this method configures Manager with discovered valid exclusion groups. + /// + /// + /// A Raw exclusion entries that should be processed/ + void LookupExclusions(RE::TESDataHandler* dataHandler, INI::ExclusionsVec& exclusion); + + /// + /// Gets a set of all forms that are in the same exclusion group as the given form. + /// Note that a form can appear in multiple exclusion groups, all of those groups are returned. + /// + /// + /// A union of all groups that contain a given form. + std::unordered_set MutuallyExclusiveFormsForForm(RE::TESForm* form) const; + + private: + + /// + /// A map of exclusion group names related to each form in the exclusion groups. + /// Provides a quick and easy way to get all indices that needs to be checked. + /// + LinkedGroups linkedGroups{}; + + /// + /// A map of exclusion groups names and the forms that are part of each exclusion group. + /// + ExclusionGroups groups{}; + }; +} diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index fe7b4ff..bcb0e61 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -54,18 +54,21 @@ namespace INI struct Exclusion { std::string name{}; + + /// Raw filters in Exclusion only use NOT and MATCH, there is no meaning for ALL, so it's ignored. Filters rawFormFilters{}; std::string path{}; }; using DataVec = std::vector; + using ExclusionsVec = std::vector; inline StringMap configs{}; /// /// A raw list of ExclusionGroups that will be processed along with configs. /// - inline std::vector exclusions{}; + inline ExclusionsVec exclusions{}; std::pair GetConfigs(); } diff --git a/SPID/include/LookupNPC.h b/SPID/include/LookupNPC.h index 275ff11..1ae7f72 100644 --- a/SPID/include/LookupNPC.h +++ b/SPID/include/LookupNPC.h @@ -18,6 +18,14 @@ namespace NPC bool InsertKeyword(const char* a_keyword); [[nodiscard]] bool HasFormFilter(const FormVec& a_forms, bool all = false) const; + /// + /// Checks whether given NPC already has another form that is mutually exclusive with the given form, + /// according to the exclusion groups configuration. + /// + /// A Form that needs to be checked. + /// + [[nodiscard]] bool HasMutuallyExclusiveForm(RE::TESForm* otherForm) const; + [[nodiscard]] std::uint16_t GetLevel() const; [[nodiscard]] RE::SEX GetSex() const; [[nodiscard]] bool IsUnique() const; diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index be7e6a3..a4bb3e0 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -219,7 +219,7 @@ namespace Distribute::Event const auto actor = a_event->actorDying->As(); const auto npc = actor ? actor->GetActorBase() : nullptr; if (actor && npc) { - const auto npcData = NPCData(actor, npc); + auto npcData = NPCData(actor, npc); const auto input = PCLevelMult::Input{ actor, npc, false }; for_each_form(npcData, Forms::deathItems, input, [&](auto* a_deathItem, IdxOrCount a_count) { diff --git a/SPID/src/ExclusionGroups.cpp b/SPID/src/ExclusionGroups.cpp new file mode 100644 index 0000000..e911e48 --- /dev/null +++ b/SPID/src/ExclusionGroups.cpp @@ -0,0 +1,58 @@ +#include "ExclusionGroups.h" +#include "FormData.h" + +void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* dataHandler, INI::ExclusionsVec& exclusions) +{ + groups.clear(); + linkedGroups.clear(); + + for (auto& [name, filterIDs, path] : exclusions) { + auto& forms = groups[name]; + FormVec match{}; + FormVec formsNot{}; + + if (Forms::detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false) && + Forms::detail::formID_to_form(dataHandler, filterIDs.NOT, formsNot, path, false)) { + for (const auto& form : match) { + if (std::holds_alternative(form)) { + forms.insert(std::get(form)); + } + } + + for (auto& form : formsNot) { + if (std::holds_alternative(form)) { + forms.erase(std::get(form)); + } + } + } + } + + // Remove empty groups + for (auto& [name, forms] : groups) { + if (forms.empty()) { + groups.erase(name); + } + } + + for (auto& [name, forms] : groups) { + for (auto& form : forms) { + linkedGroups[form].insert(name); + } + } +} + +std::unordered_set Exclusion::Manager::MutuallyExclusiveFormsForForm(RE::TESForm* form) const +{ + std::unordered_set forms{}; + if (auto it = linkedGroups.find(form); it != linkedGroups.end()) { + std::ranges::for_each(it->second, [&](const GroupName& name) { + const auto& group = groups.at(name); + forms.insert(group.begin(), group.end()); + }); + } + + return forms; +} + + + diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 11444af..36e2cd4 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -307,7 +307,7 @@ namespace INI try { if (const auto exclusionOpt = detail::parse_exclusion(key.pItem, entry, truncatedPath); exclusionOpt) { const auto& exclusion = *exclusionOpt; - exclusions[exclusion.name] = exclusion; + exclusions.emplace_back(exclusion); continue; } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index d0a221b..845e644 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -1,5 +1,6 @@ #include "LookupForms.h" #include "FormData.h" +#include "ExclusionGroups.h" #include "KeywordDependencies.h" bool Lookup::LookupForms() @@ -22,6 +23,11 @@ bool Lookup::LookupForms() valid = true; } }); + + // Lookup exclusion forms too. + // P.S. Lookup process probably should build some sort of cache and reuse already discovered forms + // instead of quering data handler for the same raw FormOrEditorID. + Exclusion::Manager::GetSingleton()->LookupExclusions(dataHandler, INI::exclusions); } return valid; diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index 75d7024..b83a874 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -1,4 +1,5 @@ #include "LookupNPC.h" +#include namespace NPC { @@ -188,6 +189,18 @@ namespace NPC } } + bool Data::HasMutuallyExclusiveForm(RE::TESForm* a_form) const + { + auto excludedForms = Exclusion::Manager::GetSingleton()->MutuallyExclusiveFormsForForm(a_form); + if (excludedForms.empty()) { + return false; + } + return std::ranges::any_of(excludedForms, [&](auto form) { + return has_form(form); + }); + } + + std::uint16_t Data::GetLevel() const { return level; From d616f29575c83dcde013bdf07284c18dc0f28771 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Wed, 13 Mar 2024 00:41:19 +0200 Subject: [PATCH 03/53] Refactored form lookup to reuse the same logic for both form filters and distributables. --- SPID/include/FormData.h | 412 +++++++++++++++++++++++++++------------- 1 file changed, 283 insertions(+), 129 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 1c9f520..d6a8c2c 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -5,8 +5,98 @@ namespace Forms { + namespace Lookup + { + struct UnknownPluginException : std::exception + { + const std::string modName; + const std::string path; + + UnknownPluginException(const std::string& modName, const std::string& path) : + + modName(modName), + path(path) + {} + }; + + struct UnknownFormIDException : std::exception + { + const RE::FormID formID; + const std::optional modName; + const std::string path; + + UnknownFormIDException(RE::FormID a_formID, std::optional a_modName = std::nullopt, const std::string& a_path) : + + formID(a_formID), + modName(a_modName), + path(a_path) + {} + }; + + struct InvalidKeywordException : std::exception + { + const RE::FormID formID; + const std::optional modName; + const std::string path; + + InvalidKeywordException(RE::FormID formID, std::optional modName = std::nullopt, const std::string& path) : + formID(formID), + modName(modName), + path(path) + {} + }; + + struct KeywordNotFoundException : std::exception + { + const std::string editorID; + const bool isDynamic; + const std::string path; + + KeywordNotFoundException(const std::string& editorID, bool isDynamic, const std::string& path) : + editorID(editorID), + isDynamic(isDynamic), + path(path) + {} + }; + + struct UnknownEditorIDException : std::exception + { + const std::string editorID; + const std::string path; + + UnknownEditorIDException(const std::string& editorID, const std::string& path) : + editorID(editorID), + path(path) + {} + }; + + struct InvalidFormTypeException : std::exception + { + const RE::FormType formType; + const FormOrEditorID formOrEditorID; + const std::string path; + + InvalidFormTypeException(RE::FormType formType, const FormOrEditorID& formOrEditorID, const std::string& path) : + formType(formType), + formOrEditorID(formOrEditorID), + path(path) + {} + }; + + struct MalformedEditorIDException : std::exception + { + const std::string path; + + MalformedEditorIDException(const std::string& path) : + path(path) + {} + }; + } + namespace detail { + using namespace Lookup; + inline void get_merged_IDs(std::optional& a_formID, std::optional& a_modName) { const auto [mergedModName, mergedFormID] = g_mergeMapperInterface->GetNewFormID(a_modName.value_or("").c_str(), a_formID.value_or(0)); @@ -29,54 +119,176 @@ namespace Forms } } - inline bool formID_to_form(RE::TESDataHandler* a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const std::string& a_path, bool a_all = false) + template + std::variant get_form_or_mod(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) { - if (a_rawFormVec.empty()) { - return true; - } - for (auto& formOrEditorID : a_rawFormVec) { - std::visit(overload{ - [&](FormModPair& a_formMod) { - auto& [formID, modName] = a_formMod; - if (g_mergeMapperInterface) { - get_merged_IDs(formID, modName); + Form* form = nullptr; + RE::TESFile* mod = nullptr; + + constexpr auto as_form = [](RE::TESForm* anyForm) -> Form* { + if (!anyForm) { + return nullptr; + } + if constexpr (std::is_same_v) { + return anyForm; + } else if constexpr (std::is_same_v) { + return static_cast(anyForm); + } else { + return anyForm->As(); + } + }; + + std::visit(overload{ + [&](const FormModPair& formMod) { + auto [formID, modName] = formMod; + + // Only MyPlugin.esp + if (modName && !formID) { + if (const RE::TESFile* filterMod = dataHandler->LookupModByName(*modName); filterMod) { + mod = filterMod; + return; + } else { + throw UnknownPluginException(*modName, path); } - if (modName && !formID) { - if (const RE::TESFile* filterMod = a_dataHandler->LookupModByName(*modName); filterMod) { - a_formVec.emplace_back(filterMod); - } else { - buffered_logger::error("\t\t[{}] Filter ({}) SKIP - mod cannot be found", a_path, *modName); + } + + if (formID && g_mergeMapperInterface) { + get_merged_IDs(formID, modName); + } + + // Either 0x1235 or 0x1235~MyPlugin.esp + if (formID) { + if (modName) { + form = as_form(dataHandler->LookupForm(*formID, *modName)); + } else { + form = as_form(RE::TESForm::LookupByID(*formID)); + } + if (!form) { + throw UnknownFormIDException(formID, modName, path); + } + + if constexpr (std::is_same_v) { + if (string::is_empty(form->GetFormEditorID())) { + // Keywords with empty EditorIDs cause game to crash. + throw InvalidKeywordException(formID, modName, path); } - } else if (formID) { - if (auto filterForm = modName ? - a_dataHandler->LookupForm(*formID, *modName) : - RE::TESForm::LookupByID(*formID)) { - const auto formType = filterForm->GetFormType(); - if (Cache::FormType::GetWhitelisted(formType)) { - a_formVec.emplace_back(filterForm); - } else { - buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - invalid formtype ({})", a_path, *formID, modName.value_or(""), formType); - } + } + } + }, + [&](std::string& editorID) { + if (editorID.empty()) { + throw MalformedEditorIDException(path); + } + if constexpr (std::is_same_v) { + auto& keywordArray = dataHandler->GetFormArray(); + + auto result = std::find_if(keywordArray.begin(), keywordArray.end(), [&](const auto& keyword) { + return keyword && keyword->formEditorID == editorID.c_str(); + }); + + if (result != keywordArray.end()) { + if (const auto keyword = *result; keyword) { + form = keyword; } else { - buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - form doesn't exist", a_path, *formID, modName.value_or("")); + throw KeywordNotFoundException(editorID, false, path); } - } - }, - [&](std::string& a_editorID) { - if (!a_editorID.empty()) { - if (auto filterForm = RE::TESForm::LookupByEditorID(a_editorID); filterForm) { - const auto formType = filterForm->GetFormType(); - if (Cache::FormType::GetWhitelisted(formType)) { - a_formVec.emplace_back(filterForm); - } else { - buffered_logger::error("\t\t[{}] Filter ({}) SKIP - invalid formtype ({})", a_path, a_editorID, formType); - } + } else { + const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType(); + if (auto keyword = factory ? factory->Create() : nullptr; keyword) { + keyword->formEditorID = editorID; + keywordArray.push_back(keyword); + + form = keyword; } else { - buffered_logger::error("\t\t[{}] Filter ({}) SKIP - form doesn't exist", a_path, a_editorID); + throw KeywordNotFoundException(editorID, true, path); } } - } }, - formOrEditorID); + } else { + form = as_form(RE::TESForm::LookupByEditorID(editorID)); + if (!form) { + throw UnknownEditorIDException(editorID, path); + } + } + } }, + formOrEditorID); + + if (mod) { + return mod; + } + + if (whitelistedOnly && form) { + const auto formType = form->GetFormType(); + if (Cache::FormType::GetWhitelisted(formType)) { + return form; + } else { + throw InvalidFormTypeException(formType, *formID, *modName.value_or(""), path); + } + } + + return form; + } + + inline RE::TESFile* get_file(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path) + { + auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path); + + if (std::holds_alternative(formOrMod)) { + return std::get(formOrMod); + } + + return nullptr; + } + + template + Form* get_form(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) + { + auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path, whitelistedOnly); + + if (std::holds_alternative(formOrMod)) { + return *std::get(formOrMod); + } + + return nullptr; + } + + inline bool formID_to_form(RE::TESDataHandler* a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const std::string& a_path, bool a_all = false) + { + if (a_rawFormVec.empty()) { + return true; + } + + for (auto& formOrEditorID : a_rawFormVec) { + try { + auto form = get_form_or_mod(a_dataHandler, formOrEditorID, a_path, true); + a_formVec.emplace_back(form); + } catch (const UnknownFormIDException& e) { + buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); + } catch (const UnknownPluginException& e) { + buffered_logger::error("\t\t[{}] Filter ({}) SKIP - mod cannot be found", e.path, e.modName); + } catch (const InvalidKeywordException& e) { + buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); + } catch (const KeywordNotFoundException& e) { + if (e.isDynamic) { + buffered_logger::critical("\t\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); + } else { + buffered_logger::critical("\t\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); + } + return false; + } catch (const UnknownEditorIDException& e) { + buffered_logger::error("\t\t[{}] Filter ({}) SKIP - editorID doesn't exist", e.path, e.editorID); + } catch (const MalformedEditorIDException& e) { + buffered_logger::error("\t\t[{}] Filter (\"\") SKIP - malformed editorID", e.path); + } catch (const InvalidFormTypeException& e) { + std::visit(overload{ + [&](const FormModPair& formMod) { + auto& [formID, modName] = formMod; + buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - invalid formtype ({})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.formType)); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] Filter ({}) SKIP - invalid formtype ({})", e.path, editorID, RE::FormTypeToString(e.formType)); + } }, + e.formOrEditorID); + } } return !a_all && !a_formVec.empty() || a_formVec.size() == a_rawFormVec.size(); @@ -224,100 +436,42 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, std::uint32_t index = 0; for (auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] : a_INIDataVec) { - Form* form = nullptr; + try { + if (auto form = get_form(a_dataHandler, formOrEditorID, a_path); form) { + FormFilters filterForms{}; - constexpr auto as_form = [](RE::TESForm* a_form) -> Form* { - if (!a_form) { - return nullptr; + bool validEntry = detail::formID_to_form(a_dataHandler, filterIDs.ALL, filterForms.ALL, path, true); + if (validEntry) { + validEntry = detail::formID_to_form(a_dataHandler, filterIDs.NOT, filterForms.NOT, path); + } + if (validEntry) { + validEntry = detail::formID_to_form(a_dataHandler, filterIDs.MATCH, filterForms.MATCH, path); + } + + if (validEntry) { + forms.emplace_back(index, form, idxOrCount, FilterData(strings, filterForms, level, traits, chance), path); + index++; + } } - if constexpr (std::is_same_v) { - return a_form; - } else if constexpr (std::is_same_v) { - return static_cast(a_form); + } catch (const UnknownFormIDException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); + } catch (const InvalidKeywordException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); + } catch (const KeywordNotFoundException& e) { + if (e.isDynamic) { + buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); } else { - return a_form->As(); + buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); } - }; - - std::visit(overload{ - [&](const FormModPair& a_formMod) { - if (auto [formID, modName] = a_formMod; formID) { - if (g_mergeMapperInterface) { - detail::get_merged_IDs(formID, modName); - } - if (modName) { - form = as_form(a_dataHandler->LookupForm(*formID, *modName)); - } else { - form = as_form(RE::TESForm::LookupByID(*formID)); - } - if (!form) { - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", path, *formID, modName.value_or("")); - } else { - if constexpr (std::is_same_v) { - if (string::is_empty(form->GetFormEditorID())) { - form = nullptr; - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", path, *formID, modName.value_or("")); - } - } - } - } - }, - [&](const std::string& a_editorID) { - if (!a_editorID.empty()) { - if constexpr (std::is_same_v) { - auto& keywordArray = a_dataHandler->GetFormArray(); - - auto result = std::find_if(keywordArray.begin(), keywordArray.end(), [&](const auto& keyword) { - return keyword && keyword->formEditorID == a_editorID.c_str(); - }); - - if (result != keywordArray.end()) { - if (const auto keyword = *result; keyword) { - form = keyword; - } else { - buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", path, a_editorID); - } - } else { - const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType(); - if (auto keyword = factory ? factory->Create() : nullptr; keyword) { - keyword->formEditorID = a_editorID; - keywordArray.push_back(keyword); - - form = keyword; - } else { - buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", path, a_editorID); - } - } - } else { - form = as_form(RE::TESForm::LookupByEditorID(a_editorID)); - if (!form) { - buffered_logger::error("\t[{}] {} FAIL - editorID doesn't exist", path, a_editorID); - } - } - } - } }, - formOrEditorID); - - if (!form) { - continue; - } - - FormFilters filterForms{}; - - bool validEntry = detail::formID_to_form(a_dataHandler, filterIDs.ALL, filterForms.ALL, path, true); - if (validEntry) { - validEntry = detail::formID_to_form(a_dataHandler, filterIDs.NOT, filterForms.NOT, path); + } catch (const UnknownEditorIDException& e) { + buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); + } catch (const MalformedEditorIDException& e) { + buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); + } catch (const InvalidFormTypeException& e) { + // Whitelisting is disabled, so this should not occur + } catch (const UnknownPluginException& e) { + // Likewise, we don't expect plugin names in distributable forms. } - if (validEntry) { - validEntry = detail::formID_to_form(a_dataHandler, filterIDs.MATCH, filterForms.MATCH, path); - } - - if (!validEntry) { - continue; - } - - forms.emplace_back(index, form, idxOrCount, FilterData(strings, filterForms, level, traits, chance), path); - index++; } } From d0c60a19648d9546841dee16af713fd1eea3e408 Mon Sep 17 00:00:00 2001 From: adya Date: Sun, 10 Mar 2024 23:44:22 +0000 Subject: [PATCH 04/53] maintenance --- SPID/include/ExclusionGroups.h | 4 +--- SPID/include/FormData.h | 4 ++-- SPID/include/LookupConfigs.h | 2 +- SPID/src/DistributeManager.cpp | 2 +- SPID/src/ExclusionGroups.cpp | 7 ++----- SPID/src/LookupConfigs.cpp | 6 +++--- SPID/src/LookupForms.cpp | 2 +- SPID/src/LookupNPC.cpp | 1 - 8 files changed, 11 insertions(+), 17 deletions(-) diff --git a/SPID/include/ExclusionGroups.h b/SPID/include/ExclusionGroups.h index 9bee81e..caaf425 100644 --- a/SPID/include/ExclusionGroups.h +++ b/SPID/include/ExclusionGroups.h @@ -10,10 +10,9 @@ namespace Exclusion class Manager : public ISingleton { public: - /// /// Does a forms lookup similar to what Filters do. - /// + /// /// As a result this method configures Manager with discovered valid exclusion groups. /// /// @@ -29,7 +28,6 @@ namespace Exclusion std::unordered_set MutuallyExclusiveFormsForForm(RE::TESForm* form) const; private: - /// /// A map of exclusion group names related to each form in the exclusion groups. /// Provides a quick and easy way to get all indices that needs to be checked. diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index d6a8c2c..fab8f61 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -467,8 +467,8 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); } catch (const MalformedEditorIDException& e) { buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); - } catch (const InvalidFormTypeException& e) { - // Whitelisting is disabled, so this should not occur + } catch (const InvalidFormTypeException& e) { + // Whitelisting is disabled, so this should not occur } catch (const UnknownPluginException& e) { // Likewise, we don't expect plugin names in distributable forms. } diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index bcb0e61..35e5b31 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -53,7 +53,7 @@ namespace INI struct Exclusion { - std::string name{}; + std::string name{}; /// Raw filters in Exclusion only use NOT and MATCH, there is no meaning for ALL, so it's ignored. Filters rawFormFilters{}; diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index a4bb3e0..ab130f0 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -219,7 +219,7 @@ namespace Distribute::Event const auto actor = a_event->actorDying->As(); const auto npc = actor ? actor->GetActorBase() : nullptr; if (actor && npc) { - auto npcData = NPCData(actor, npc); + auto npcData = NPCData(actor, npc); const auto input = PCLevelMult::Input{ actor, npc, false }; for_each_form(npcData, Forms::deathItems, input, [&](auto* a_deathItem, IdxOrCount a_count) { diff --git a/SPID/src/ExclusionGroups.cpp b/SPID/src/ExclusionGroups.cpp index e911e48..b67cd39 100644 --- a/SPID/src/ExclusionGroups.cpp +++ b/SPID/src/ExclusionGroups.cpp @@ -7,7 +7,7 @@ void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* dataHandler, INI:: linkedGroups.clear(); for (auto& [name, filterIDs, path] : exclusions) { - auto& forms = groups[name]; + auto& forms = groups[name]; FormVec match{}; FormVec formsNot{}; @@ -33,7 +33,7 @@ void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* dataHandler, INI:: groups.erase(name); } } - + for (auto& [name, forms] : groups) { for (auto& form : forms) { linkedGroups[form].insert(name); @@ -53,6 +53,3 @@ std::unordered_set Exclusion::Manager::MutuallyExclusiveFormsForFo return forms; } - - - diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 36e2cd4..7a87bbe 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -46,7 +46,7 @@ namespace INI return newValue; } - std::optional parse_exclusion(const std::string& a_key, const std::string& a_value, const std::string& a_path) + std::optional parse_exclusion(const std::string& a_key, const std::string& a_value, const std::string& a_path) { if (a_key != "ExclusionGroup") { return std::nullopt; @@ -75,7 +75,7 @@ namespace INI Exclusion group{}; group.name = sections[0]; group.path = a_path; - + for (auto& IDs : split_IDs) { if (IDs.at(0) == '-') { IDs.erase(0, 1); @@ -304,7 +304,7 @@ namespace INI auto truncatedPath = path.substr(5); //strip "Data\\" for (auto& [key, entry] : *values) { - try { + try { if (const auto exclusionOpt = detail::parse_exclusion(key.pItem, entry, truncatedPath); exclusionOpt) { const auto& exclusion = *exclusionOpt; exclusions.emplace_back(exclusion); diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 845e644..4aaa1d1 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -1,6 +1,6 @@ #include "LookupForms.h" -#include "FormData.h" #include "ExclusionGroups.h" +#include "FormData.h" #include "KeywordDependencies.h" bool Lookup::LookupForms() diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index b83a874..bbd3625 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -200,7 +200,6 @@ namespace NPC }); } - std::uint16_t Data::GetLevel() const { return level; From 03cdbe535869daa5fcdee6ef4f58cbadd82c3c1e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 12 Mar 2024 02:47:16 +0200 Subject: [PATCH 05/53] Finalized Exclusion groups feature. - Fixed compilation errors - Fixed support for Keywords. - Extracted form lookup logic to avoid almost identical code blocks. - Refactored LookupForms to fit in a new type of lookup for Exclusion Groups. --- SPID/CMakeLists.txt | 4 +- SPID/include/ExclusionGroups.h | 12 +++- SPID/include/FormData.h | 111 ++++++++++++++++++--------------- SPID/include/LookupForms.h | 3 - SPID/src/ExclusionGroups.cpp | 19 +++++- SPID/src/LookupForms.cpp | 86 ++++++++++++++++--------- SPID/src/LookupNPC.cpp | 3 + SPID/src/main.cpp | 2 +- 8 files changed, 146 insertions(+), 94 deletions(-) diff --git a/SPID/CMakeLists.txt b/SPID/CMakeLists.txt index 985fa02..5fac466 100644 --- a/SPID/CMakeLists.txt +++ b/SPID/CMakeLists.txt @@ -240,8 +240,8 @@ if (COPY_BUILD) add_custom_command( TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy $ ${SkyrimPath}/SKSE/Plugins/ - COMMAND ${CMAKE_COMMAND} -E copy $ ${SkyrimPath}/SKSE/Plugins/ + COMMAND ${CMAKE_COMMAND} -E copy $ ${SkyrimPath}/Data/SKSE/Plugins/ + COMMAND ${CMAKE_COMMAND} -E copy $ ${SkyrimPath}/Data/SKSE/Plugins/ ) else () message( diff --git a/SPID/include/ExclusionGroups.h b/SPID/include/ExclusionGroups.h index caaf425..327b4ae 100644 --- a/SPID/include/ExclusionGroups.h +++ b/SPID/include/ExclusionGroups.h @@ -5,7 +5,7 @@ namespace Exclusion { using GroupName = std::string; using LinkedGroups = std::unordered_map>; - using ExclusionGroups = std::unordered_map>; + using Groups = std::unordered_map>; class Manager : public ISingleton { @@ -17,7 +17,7 @@ namespace Exclusion /// /// /// A Raw exclusion entries that should be processed/ - void LookupExclusions(RE::TESDataHandler* dataHandler, INI::ExclusionsVec& exclusion); + void LookupExclusions(RE::TESDataHandler* const dataHandler, INI::ExclusionsVec& exclusion); /// /// Gets a set of all forms that are in the same exclusion group as the given form. @@ -27,6 +27,12 @@ namespace Exclusion /// A union of all groups that contain a given form. std::unordered_set MutuallyExclusiveFormsForForm(RE::TESForm* form) const; + /// + /// Retrieves all exclusion groups. + /// + /// A reference to discovered exclusion groups + const Groups& GetGroups() const; + private: /// /// A map of exclusion group names related to each form in the exclusion groups. @@ -37,6 +43,6 @@ namespace Exclusion /// /// A map of exclusion groups names and the forms that are part of each exclusion group. /// - ExclusionGroups groups{}; + Groups groups{}; }; } diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index fab8f61..093acb9 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -13,7 +13,6 @@ namespace Forms const std::string path; UnknownPluginException(const std::string& modName, const std::string& path) : - modName(modName), path(path) {} @@ -25,11 +24,10 @@ namespace Forms const std::optional modName; const std::string path; - UnknownFormIDException(RE::FormID a_formID, std::optional a_modName = std::nullopt, const std::string& a_path) : - - formID(a_formID), - modName(a_modName), - path(a_path) + UnknownFormIDException(RE::FormID formID, const std::string& path, std::optional modName = std::nullopt) : + formID(formID), + path(path), + modName(modName) {} }; @@ -39,7 +37,7 @@ namespace Forms const std::optional modName; const std::string path; - InvalidKeywordException(RE::FormID formID, std::optional modName = std::nullopt, const std::string& path) : + InvalidKeywordException(RE::FormID formID, const std::string& path, std::optional modName = std::nullopt) : formID(formID), modName(modName), path(path) @@ -120,10 +118,10 @@ namespace Forms } template - std::variant get_form_or_mod(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) + std::variant get_form_or_mod(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) { - Form* form = nullptr; - RE::TESFile* mod = nullptr; + Form* form = nullptr; + const RE::TESFile* mod = nullptr; constexpr auto as_form = [](RE::TESForm* anyForm) -> Form* { if (!anyForm) { @@ -138,6 +136,32 @@ namespace Forms } }; + auto find_or_create_keyword = [&](const std::string& editorID) -> RE::BGSKeyword* { + auto& keywordArray = dataHandler->GetFormArray(); + + auto result = std::find_if(keywordArray.begin(), keywordArray.end(), [&](const auto& keyword) { + return keyword && keyword->formEditorID == editorID.c_str(); + }); + + if (result != keywordArray.end()) { + if (const auto keyword = *result; keyword) { + return keyword; + } else { + throw KeywordNotFoundException(editorID, false, path); + } + } else { + const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType(); + if (auto keyword = factory ? factory->Create() : nullptr; keyword) { + keyword->formEditorID = editorID; + keywordArray.push_back(keyword); + + return keyword; + } else { + throw KeywordNotFoundException(editorID, true, path); + } + } + }; + std::visit(overload{ [&](const FormModPair& formMod) { auto [formID, modName] = formMod; @@ -164,49 +188,32 @@ namespace Forms form = as_form(RE::TESForm::LookupByID(*formID)); } if (!form) { - throw UnknownFormIDException(formID, modName, path); + throw UnknownFormIDException(*formID, path, modName); } if constexpr (std::is_same_v) { if (string::is_empty(form->GetFormEditorID())) { // Keywords with empty EditorIDs cause game to crash. - throw InvalidKeywordException(formID, modName, path); + throw InvalidKeywordException(*formID, path, modName); } } } }, - [&](std::string& editorID) { + [&](const std::string& editorID) { if (editorID.empty()) { throw MalformedEditorIDException(path); } + // if constexpr (std::is_same_v) { - auto& keywordArray = dataHandler->GetFormArray(); - - auto result = std::find_if(keywordArray.begin(), keywordArray.end(), [&](const auto& keyword) { - return keyword && keyword->formEditorID == editorID.c_str(); - }); - - if (result != keywordArray.end()) { - if (const auto keyword = *result; keyword) { - form = keyword; - } else { - throw KeywordNotFoundException(editorID, false, path); - } - } else { - const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType(); - if (auto keyword = factory ? factory->Create() : nullptr; keyword) { - keyword->formEditorID = editorID; - keywordArray.push_back(keyword); - - form = keyword; - } else { - throw KeywordNotFoundException(editorID, true, path); - } - } + form = find_or_create_keyword(editorID); } else { form = as_form(RE::TESForm::LookupByEditorID(editorID)); if (!form) { - throw UnknownEditorIDException(editorID, path); + if constexpr (std::is_same_v) { + form = find_or_create_keyword(editorID); + } else { + throw UnknownEditorIDException(editorID, path); + } } } } }, @@ -221,19 +228,19 @@ namespace Forms if (Cache::FormType::GetWhitelisted(formType)) { return form; } else { - throw InvalidFormTypeException(formType, *formID, *modName.value_or(""), path); + throw InvalidFormTypeException(formType, formOrEditorID, path); } } return form; } - inline RE::TESFile* get_file(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path) + inline const RE::TESFile* get_file(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path) { auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path); - if (std::holds_alternative(formOrMod)) { - return std::get(formOrMod); + if (std::holds_alternative(formOrMod)) { + return std::get(formOrMod); } return nullptr; @@ -245,13 +252,13 @@ namespace Forms auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path, whitelistedOnly); if (std::holds_alternative(formOrMod)) { - return *std::get(formOrMod); + return std::get(formOrMod); } return nullptr; } - inline bool formID_to_form(RE::TESDataHandler* a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const std::string& a_path, bool a_all = false) + inline bool formID_to_form(RE::TESDataHandler* a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const std::string& a_path, bool a_all = false, bool whitelistedOnly = true) { if (a_rawFormVec.empty()) { return true; @@ -259,7 +266,7 @@ namespace Forms for (auto& formOrEditorID : a_rawFormVec) { try { - auto form = get_form_or_mod(a_dataHandler, formOrEditorID, a_path, true); + auto form = get_form_or_mod(a_dataHandler, formOrEditorID, a_path, whitelistedOnly); a_formVec.emplace_back(form); } catch (const UnknownFormIDException& e) { buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); @@ -437,7 +444,7 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, for (auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] : a_INIDataVec) { try { - if (auto form = get_form(a_dataHandler, formOrEditorID, a_path); form) { + if (auto form = detail::get_form(a_dataHandler, formOrEditorID, path); form) { FormFilters filterForms{}; bool validEntry = detail::formID_to_form(a_dataHandler, filterIDs.ALL, filterForms.ALL, path, true); @@ -453,23 +460,23 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, index++; } } - } catch (const UnknownFormIDException& e) { + } catch (const Lookup::UnknownFormIDException& e) { buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); - } catch (const InvalidKeywordException& e) { + } catch (const Lookup::InvalidKeywordException& e) { buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); - } catch (const KeywordNotFoundException& e) { + } catch (const Lookup::KeywordNotFoundException& e) { if (e.isDynamic) { buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); } else { buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); } - } catch (const UnknownEditorIDException& e) { + } catch (const Lookup::UnknownEditorIDException& e) { buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); - } catch (const MalformedEditorIDException& e) { + } catch (const Lookup::MalformedEditorIDException& e) { buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); - } catch (const InvalidFormTypeException& e) { + } catch (const Lookup::InvalidFormTypeException& e) { // Whitelisting is disabled, so this should not occur - } catch (const UnknownPluginException& e) { + } catch (const Lookup::UnknownPluginException& e) { // Likewise, we don't expect plugin names in distributable forms. } } diff --git a/SPID/include/LookupForms.h b/SPID/include/LookupForms.h index a3553df..754769c 100644 --- a/SPID/include/LookupForms.h +++ b/SPID/include/LookupForms.h @@ -3,7 +3,4 @@ namespace Lookup { bool LookupForms(); - void LogFormLookup(); - - bool DoFormLookup(); } diff --git a/SPID/src/ExclusionGroups.cpp b/SPID/src/ExclusionGroups.cpp index b67cd39..6f3c69c 100644 --- a/SPID/src/ExclusionGroups.cpp +++ b/SPID/src/ExclusionGroups.cpp @@ -1,7 +1,7 @@ #include "ExclusionGroups.h" #include "FormData.h" -void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* dataHandler, INI::ExclusionsVec& exclusions) +void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* const dataHandler, INI::ExclusionsVec& exclusions) { groups.clear(); linkedGroups.clear(); @@ -11,8 +11,8 @@ void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* dataHandler, INI:: FormVec match{}; FormVec formsNot{}; - if (Forms::detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false) && - Forms::detail::formID_to_form(dataHandler, filterIDs.NOT, formsNot, path, false)) { + if (Forms::detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false, false) && + Forms::detail::formID_to_form(dataHandler, filterIDs.NOT, formsNot, path, false, false)) { for (const auto& form : match) { if (std::holds_alternative(form)) { forms.insert(std::get(form)); @@ -39,6 +39,8 @@ void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* dataHandler, INI:: linkedGroups[form].insert(name); } } + + } std::unordered_set Exclusion::Manager::MutuallyExclusiveFormsForForm(RE::TESForm* form) const @@ -51,5 +53,16 @@ std::unordered_set Exclusion::Manager::MutuallyExclusiveFormsForFo }); } + // Remove self from the list. + forms.erase(form); + return forms; } + +const Exclusion::Groups& Exclusion::Manager::GetGroups() const +{ + return groups; +} + + + diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 4aaa1d1..31d3601 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -3,37 +3,30 @@ #include "FormData.h" #include "KeywordDependencies.h" -bool Lookup::LookupForms() +bool LookupDistributables(RE::TESDataHandler* const dataHandler) { using namespace Forms; bool valid = false; - if (const auto dataHandler = RE::TESDataHandler::GetSingleton()) { - ForEachDistributable([&](Distributables& a_distributable) { - const auto& recordName = RECORD::add[a_distributable.GetType()]; - - a_distributable.LookupForms(dataHandler, recordName, INI::configs[recordName]); - if constexpr (std::is_same_v) { - Dependencies::ResolveKeywords(); - } - a_distributable.FinishLookupForms(); + ForEachDistributable([&](Distributables& a_distributable) { + const auto& recordName = RECORD::add[a_distributable.GetType()]; - if (a_distributable) { - valid = true; - } - }); + a_distributable.LookupForms(dataHandler, recordName, INI::configs[recordName]); + if constexpr (std::is_same_v) { + Dependencies::ResolveKeywords(); + } + a_distributable.FinishLookupForms(); - // Lookup exclusion forms too. - // P.S. Lookup process probably should build some sort of cache and reuse already discovered forms - // instead of quering data handler for the same raw FormOrEditorID. - Exclusion::Manager::GetSingleton()->LookupExclusions(dataHandler, INI::exclusions); - } + if (a_distributable) { + valid = true; + } + }); return valid; } -void Lookup::LogFormLookup() +void LogDistributablesLookup() { using namespace Forms; @@ -58,20 +51,53 @@ void Lookup::LogFormLookup() buffered_logger::clear(); } -bool Lookup::DoFormLookup() +// Lookup exclusion forms too. +// P.S. Lookup process probably should build some sort of cache and reuse already discovered forms +// instead of quering data handler for the same raw FormOrEditorID. +void LookupExclusionGroups(RE::TESDataHandler* const dataHandler) +{ + Exclusion::Manager::GetSingleton()->LookupExclusions(dataHandler, INI::exclusions); +} + +void LogExclusionGroupsLookup() { - logger::info("{:*^50}", "LOOKUP"); + if (const auto manager = Exclusion::Manager::GetSingleton(); manager) { + const auto& groups = manager->GetGroups(); - Timer timer; + if (!groups.empty()) { + logger::info("{:*^50}", "EXCLUSIONS"); + + for (const auto& [group, forms] : groups) { + logger::info("Adding '{}' exclusion group", group); + for (const auto& form : forms) { + logger::info(" {}", describe(form)); + } + } + } + } +} + +bool Lookup::LookupForms() +{ + if (const auto dataHandler = RE::TESDataHandler::GetSingleton(); dataHandler) { + logger::info("{:*^50}", "LOOKUP"); + + Timer timer; + + timer.start(); + const bool success = LookupDistributables(dataHandler); + timer.end(); + + if (success) { + LogDistributablesLookup(); + logger::info("Lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); + } - timer.start(); - const bool success = LookupForms(); - timer.end(); + LookupExclusionGroups(dataHandler); + LogExclusionGroupsLookup(); - if (success) { - LogFormLookup(); - logger::info("Lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); + return success; } - return success; + return false; } diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index bbd3625..fea7912 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -196,6 +196,9 @@ namespace NPC return false; } return std::ranges::any_of(excludedForms, [&](auto form) { + if (const auto keyword = form->As(); keyword) { + return has_keyword_string(keyword->GetFormEditorID()); + } return has_form(form); }); } diff --git a/SPID/src/main.cpp b/SPID/src/main.cpp index 1b0a6ec..b3e47be 100644 --- a/SPID/src/main.cpp +++ b/SPID/src/main.cpp @@ -37,7 +37,7 @@ void MessageHandler(SKSE::MessagingInterface::Message* a_message) break; case SKSE::MessagingInterface::kDataLoaded: { - if (shouldDistribute = Lookup::DoFormLookup(); shouldDistribute) { + if (shouldDistribute = Lookup::LookupForms(); shouldDistribute) { Distribute::Setup(); } From 31edb4fa14d96fc14940434e7a02fc41c6a90591 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 12 Mar 2024 02:50:39 +0200 Subject: [PATCH 06/53] Fixed first Keyword wasn't preserving relative order. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Odd bug where first Keyword was always sorted alphabetically by EditorID 😅 --- SPID/src/KeywordDependencies.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPID/src/KeywordDependencies.cpp b/SPID/src/KeywordDependencies.cpp index 718f874..b643b37 100644 --- a/SPID/src/KeywordDependencies.cpp +++ b/SPID/src/KeywordDependencies.cpp @@ -16,7 +16,7 @@ struct keyword_less { const auto aIdx = getIndex(a); const auto bIdx = getIndex(b); - if (aIdx > 0 && bIdx > 0) { + if (aIdx >= 0 && bIdx >= 0) { if (aIdx < bIdx) { return true; } @@ -143,7 +143,7 @@ void Dependencies::ResolveKeywords() const auto endTime = std::chrono::steady_clock::now(); keywordForms.clear(); - logger::info("\tSorted keywords :"); + logger::info("\tSorted keywords: "); for (const auto& keyword : result) { const auto& [begin, end] = dataKeywords.equal_range(keyword); if (begin != end) { From db83300857b7bdab6f93a7d491e589aa03216092 Mon Sep 17 00:00:00 2001 From: adya Date: Tue, 12 Mar 2024 00:50:58 +0000 Subject: [PATCH 07/53] maintenance --- SPID/src/ExclusionGroups.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/SPID/src/ExclusionGroups.cpp b/SPID/src/ExclusionGroups.cpp index 6f3c69c..13ada82 100644 --- a/SPID/src/ExclusionGroups.cpp +++ b/SPID/src/ExclusionGroups.cpp @@ -39,8 +39,6 @@ void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* const dataHandler, linkedGroups[form].insert(name); } } - - } std::unordered_set Exclusion::Manager::MutuallyExclusiveFormsForForm(RE::TESForm* form) const @@ -63,6 +61,3 @@ const Exclusion::Groups& Exclusion::Manager::GetGroups() const { return groups; } - - - From a8b3dc9a0bc6a20226de5b623b9708f1141a65fe Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 12 Mar 2024 03:52:47 +0200 Subject: [PATCH 08/53] Reverted no longer needed change. --- SPID/include/Distribute.h | 8 ++++---- SPID/include/ExclusionGroups.h | 2 +- SPID/src/DistributeManager.cpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index d433c36..787931b 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -77,7 +77,7 @@ namespace Distribute // for now, only packages/death items use this template void for_each_form( - NPCData& a_npcData, + const NPCData& a_npcData, Forms::Distributables& a_distributables, const PCLevelMult::Input& a_input, std::function a_callback) @@ -96,7 +96,7 @@ namespace Distribute // skins template void for_each_form( - NPCData& a_npcData, + const NPCData& a_npcData, Forms::Distributables& a_distributables, const PCLevelMult::Input& a_input, std::function a_callback) @@ -114,7 +114,7 @@ namespace Distribute // outfits/sleep outfits template void for_each_form( - NPCData& a_npcData, + const NPCData& a_npcData, Forms::Distributables& a_distributables, std::function a_callback) { @@ -131,7 +131,7 @@ namespace Distribute // items template void for_each_form( - NPCData& a_npcData, + const NPCData& a_npcData, Forms::Distributables& a_distributables, const PCLevelMult::Input& a_input, std::function&, bool)> a_callback) diff --git a/SPID/include/ExclusionGroups.h b/SPID/include/ExclusionGroups.h index 327b4ae..1b4f23d 100644 --- a/SPID/include/ExclusionGroups.h +++ b/SPID/include/ExclusionGroups.h @@ -36,7 +36,7 @@ namespace Exclusion private: /// /// A map of exclusion group names related to each form in the exclusion groups. - /// Provides a quick and easy way to get all indices that needs to be checked. + /// Provides a quick and easy way to get names of all groups that need to be checked. /// LinkedGroups linkedGroups{}; diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index ab130f0..be7e6a3 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -219,7 +219,7 @@ namespace Distribute::Event const auto actor = a_event->actorDying->As(); const auto npc = actor ? actor->GetActorBase() : nullptr; if (actor && npc) { - auto npcData = NPCData(actor, npc); + const auto npcData = NPCData(actor, npc); const auto input = PCLevelMult::Input{ actor, npc, false }; for_each_form(npcData, Forms::deathItems, input, [&](auto* a_deathItem, IdxOrCount a_count) { From 5a95589280ea26a5ff288d7fac9a6e86782ee93d Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 12 Mar 2024 04:34:50 +0200 Subject: [PATCH 09/53] Replaced term "exclusion group" with "exclusive group". MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yeah, it bothered me too much 🤓 --- SPID/cmake/headerlist.cmake | 2 +- SPID/cmake/sourcelist.cmake | 2 +- .../{ExclusionGroups.h => ExclusiveGroups.h} | 20 +++++++++---------- SPID/include/LookupConfigs.h | 10 +++++----- SPID/include/LookupNPC.h | 2 +- ...xclusionGroups.cpp => ExclusiveGroups.cpp} | 10 +++++----- SPID/src/LookupConfigs.cpp | 17 ++++++++-------- SPID/src/LookupForms.cpp | 20 +++++++++---------- SPID/src/LookupNPC.cpp | 4 ++-- 9 files changed, 43 insertions(+), 44 deletions(-) rename SPID/include/{ExclusionGroups.h => ExclusiveGroups.h} (61%) rename SPID/src/{ExclusionGroups.cpp => ExclusiveGroups.cpp} (75%) diff --git a/SPID/cmake/headerlist.cmake b/SPID/cmake/headerlist.cmake index 9346806..9d8fa86 100644 --- a/SPID/cmake/headerlist.cmake +++ b/SPID/cmake/headerlist.cmake @@ -5,7 +5,7 @@ set(headers ${headers} include/Distribute.h include/DistributeManager.h include/DistributePCLevelMult.h - include/ExclusionGroups.h + include/ExclusiveGroups.h include/FormData.h include/KeywordDependencies.h include/LogBuffer.h diff --git a/SPID/cmake/sourcelist.cmake b/SPID/cmake/sourcelist.cmake index 46bd8f1..fc75060 100644 --- a/SPID/cmake/sourcelist.cmake +++ b/SPID/cmake/sourcelist.cmake @@ -3,7 +3,7 @@ set(sources ${sources} src/Distribute.cpp src/DistributeManager.cpp src/DistributePCLevelMult.cpp - src/ExclusionGroups.cpp + src/ExclusiveGroups.cpp src/FormData.cpp src/KeywordDependencies.cpp src/LogBuffer.cpp diff --git a/SPID/include/ExclusionGroups.h b/SPID/include/ExclusiveGroups.h similarity index 61% rename from SPID/include/ExclusionGroups.h rename to SPID/include/ExclusiveGroups.h index 1b4f23d..0fc97e8 100644 --- a/SPID/include/ExclusionGroups.h +++ b/SPID/include/ExclusiveGroups.h @@ -1,7 +1,7 @@ #pragma once #include "LookupConfigs.h" -namespace Exclusion +namespace ExclusiveGroups { using GroupName = std::string; using LinkedGroups = std::unordered_map>; @@ -13,35 +13,35 @@ namespace Exclusion /// /// Does a forms lookup similar to what Filters do. /// - /// As a result this method configures Manager with discovered valid exclusion groups. + /// As a result this method configures Manager with discovered valid exclusive groups. /// /// - /// A Raw exclusion entries that should be processed/ - void LookupExclusions(RE::TESDataHandler* const dataHandler, INI::ExclusionsVec& exclusion); + /// A raw exclusive group entries that should be processed/ + void LookupExclusiveGroups(RE::TESDataHandler* const dataHandler, INI::ExclusiveGroupsVec& rawExclusiveGroups); /// - /// Gets a set of all forms that are in the same exclusion group as the given form. - /// Note that a form can appear in multiple exclusion groups, all of those groups are returned. + /// Gets a set of all forms that are in the same exclusive group as the given form. + /// Note that a form can appear in multiple exclusive groups, all of those groups are returned. /// /// /// A union of all groups that contain a given form. std::unordered_set MutuallyExclusiveFormsForForm(RE::TESForm* form) const; /// - /// Retrieves all exclusion groups. + /// Retrieves all exclusive groups. /// - /// A reference to discovered exclusion groups + /// A reference to discovered exclusive groups const Groups& GetGroups() const; private: /// - /// A map of exclusion group names related to each form in the exclusion groups. + /// A map of exclusive group names related to each form in the exclusive groups. /// Provides a quick and easy way to get names of all groups that need to be checked. /// LinkedGroups linkedGroups{}; /// - /// A map of exclusion groups names and the forms that are part of each exclusion group. + /// A map of exclusive groups names and the forms that are part of each exclusive group. /// Groups groups{}; }; diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 35e5b31..063c2ed 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -51,24 +51,24 @@ namespace INI std::string path{}; }; - struct Exclusion + struct RawExclusiveGroup { std::string name{}; - /// Raw filters in Exclusion only use NOT and MATCH, there is no meaning for ALL, so it's ignored. + /// Raw filters in RawExclusiveGroup only use NOT and MATCH, there is no meaning for ALL, so it's ignored. Filters rawFormFilters{}; std::string path{}; }; using DataVec = std::vector; - using ExclusionsVec = std::vector; + using ExclusiveGroupsVec = std::vector; inline StringMap configs{}; /// - /// A raw list of ExclusionGroups that will be processed along with configs. + /// A list of RawExclusiveGroups that will be processed along with configs. /// - inline ExclusionsVec exclusions{}; + inline ExclusiveGroupsVec exclusiveGroups{}; std::pair GetConfigs(); } diff --git a/SPID/include/LookupNPC.h b/SPID/include/LookupNPC.h index 1ae7f72..619df91 100644 --- a/SPID/include/LookupNPC.h +++ b/SPID/include/LookupNPC.h @@ -20,7 +20,7 @@ namespace NPC /// /// Checks whether given NPC already has another form that is mutually exclusive with the given form, - /// according to the exclusion groups configuration. + /// according to the exclusive groups configuration. /// /// A Form that needs to be checked. /// diff --git a/SPID/src/ExclusionGroups.cpp b/SPID/src/ExclusiveGroups.cpp similarity index 75% rename from SPID/src/ExclusionGroups.cpp rename to SPID/src/ExclusiveGroups.cpp index 13ada82..6db5f4f 100644 --- a/SPID/src/ExclusionGroups.cpp +++ b/SPID/src/ExclusiveGroups.cpp @@ -1,12 +1,12 @@ -#include "ExclusionGroups.h" +#include "ExclusiveGroups.h" #include "FormData.h" -void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* const dataHandler, INI::ExclusionsVec& exclusions) +void ExclusiveGroups::Manager::LookupExclusiveGroups(RE::TESDataHandler* const dataHandler, INI::ExclusiveGroupsVec& exclusiveGroups) { groups.clear(); linkedGroups.clear(); - for (auto& [name, filterIDs, path] : exclusions) { + for (auto& [name, filterIDs, path] : exclusiveGroups) { auto& forms = groups[name]; FormVec match{}; FormVec formsNot{}; @@ -41,7 +41,7 @@ void Exclusion::Manager::LookupExclusions(RE::TESDataHandler* const dataHandler, } } -std::unordered_set Exclusion::Manager::MutuallyExclusiveFormsForForm(RE::TESForm* form) const +std::unordered_set ExclusiveGroups::Manager::MutuallyExclusiveFormsForForm(RE::TESForm* form) const { std::unordered_set forms{}; if (auto it = linkedGroups.find(form); it != linkedGroups.end()) { @@ -57,7 +57,7 @@ std::unordered_set Exclusion::Manager::MutuallyExclusiveFormsForFo return forms; } -const Exclusion::Groups& Exclusion::Manager::GetGroups() const +const ExclusiveGroups::Groups& ExclusiveGroups::Manager::GetGroups() const { return groups; } diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 7a87bbe..07df2e0 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -46,9 +46,9 @@ namespace INI return newValue; } - std::optional parse_exclusion(const std::string& a_key, const std::string& a_value, const std::string& a_path) + std::optional parse_exclusive_group(const std::string& a_key, const std::string& a_value, const std::string& a_path) { - if (a_key != "ExclusionGroup") { + if (a_key != "ExclusiveGroup") { return std::nullopt; } @@ -56,23 +56,23 @@ namespace INI const auto size = sections.size(); if (size == 0) { - logger::warn("IGNORED: ExclusionGroup must have a name: {} = {}"sv, a_key, a_value); + logger::warn("IGNORED: ExclusiveGroup must have a name: {} = {}"sv, a_key, a_value); return std::nullopt; } if (size == 1) { - logger::warn("IGNORED: ExclusionGroup must have at least one filter name: {} = {}"sv, a_key, a_value); + logger::warn("IGNORED: ExclusiveGroup must have at least one filter name: {} = {}"sv, a_key, a_value); return std::nullopt; } auto split_IDs = distribution::split_entry(sections[1]); if (split_IDs.empty()) { - logger::warn("ExclusionGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); + logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); return std::nullopt; } - Exclusion group{}; + RawExclusiveGroup group{}; group.name = sections[0]; group.path = a_path; @@ -305,9 +305,8 @@ namespace INI for (auto& [key, entry] : *values) { try { - if (const auto exclusionOpt = detail::parse_exclusion(key.pItem, entry, truncatedPath); exclusionOpt) { - const auto& exclusion = *exclusionOpt; - exclusions.emplace_back(exclusion); + if (const auto group = detail::parse_exclusive_group(key.pItem, entry, truncatedPath); group) { + exclusiveGroups.emplace_back(*group); continue; } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 31d3601..a56d673 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -1,5 +1,5 @@ #include "LookupForms.h" -#include "ExclusionGroups.h" +#include "ExclusiveGroups.h" #include "FormData.h" #include "KeywordDependencies.h" @@ -51,24 +51,24 @@ void LogDistributablesLookup() buffered_logger::clear(); } -// Lookup exclusion forms too. +// Lookup forms in exclusvie groups too. // P.S. Lookup process probably should build some sort of cache and reuse already discovered forms // instead of quering data handler for the same raw FormOrEditorID. -void LookupExclusionGroups(RE::TESDataHandler* const dataHandler) +void LookupExclusiveGroups(RE::TESDataHandler* const dataHandler) { - Exclusion::Manager::GetSingleton()->LookupExclusions(dataHandler, INI::exclusions); + ExclusiveGroups::Manager::GetSingleton()->LookupExclusiveGroups(dataHandler, INI::exclusiveGroups); } -void LogExclusionGroupsLookup() +void LogExclusiveGroupsLookup() { - if (const auto manager = Exclusion::Manager::GetSingleton(); manager) { + if (const auto manager = ExclusiveGroups::Manager::GetSingleton(); manager) { const auto& groups = manager->GetGroups(); if (!groups.empty()) { - logger::info("{:*^50}", "EXCLUSIONS"); + logger::info("{:*^50}", "EXCLUSIVE GROUPS"); for (const auto& [group, forms] : groups) { - logger::info("Adding '{}' exclusion group", group); + logger::info("Adding '{}' exclusive group", group); for (const auto& form : forms) { logger::info(" {}", describe(form)); } @@ -93,8 +93,8 @@ bool Lookup::LookupForms() logger::info("Lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); } - LookupExclusionGroups(dataHandler); - LogExclusionGroupsLookup(); + LookupExclusiveGroups(dataHandler); + LogExclusiveGroupsLookup(); return success; } diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index fea7912..1a15fad 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -1,5 +1,5 @@ #include "LookupNPC.h" -#include +#include namespace NPC { @@ -191,7 +191,7 @@ namespace NPC bool Data::HasMutuallyExclusiveForm(RE::TESForm* a_form) const { - auto excludedForms = Exclusion::Manager::GetSingleton()->MutuallyExclusiveFormsForForm(a_form); + auto excludedForms = ExclusiveGroups::Manager::GetSingleton()->MutuallyExclusiveFormsForForm(a_form); if (excludedForms.empty()) { return false; } From 1bf640da70ad91e482d557840b7e9fe868b27960 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 14 Mar 2024 01:57:14 +0200 Subject: [PATCH 10/53] Added support for random count value. Count can now be specified in form of range, e.g. 5/20 to produce anywhere between 5 and 20 items. --- SPID/include/Defs.h | 15 ++++++++++++++- SPID/include/Distribute.h | 21 ++++++++++++++------- SPID/include/FormData.h | 11 ++++++----- SPID/include/LookupConfigs.h | 3 ++- SPID/src/Distribute.cpp | 9 ++++++--- SPID/src/DistributeManager.cpp | 10 ++++++++-- SPID/src/LookupConfigs.cpp | 25 +++++++++++++++++++------ 7 files changed, 69 insertions(+), 25 deletions(-) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index 7691604..29db1ac 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -54,6 +54,16 @@ struct Range return value >= min && value <= max; } + [[nodiscard]] bool IsExact() const + { + return min == max; + } + + [[nodiscard]] T GetRandom() const + { + return IsExact() ? min : RNG().generate(min, max); + } + // members T min{ std::numeric_limits::min() }; T max{ std::numeric_limits::max() }; @@ -83,7 +93,10 @@ struct Traits std::optional teammate{}; }; -using IdxOrCount = std::int32_t; +using Index = std::int32_t; +using Count = std::int32_t; +using RandomCount = Range; +using IndexOrCount = std::variant; using Chance = std::uint32_t; /// A standardized way of converting any object to string. diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 787931b..9499025 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -80,14 +80,21 @@ namespace Distribute const NPCData& a_npcData, Forms::Distributables& a_distributables, const PCLevelMult::Input& a_input, - std::function a_callback) + std::function a_callback) { auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); for (auto& formData : vec) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { - a_callback(formData.form, formData.idxOrCount); - ++formData.npcCount; + if constexpr (std::is_same_v) { + if (a_callback(formData.form, formData.count.GetRandom())) { + ++formData.npcCount; + } + } else { + if (a_callback(formData.form, formData.packageIndex)) { + ++formData.npcCount; + } + } } } } @@ -134,7 +141,7 @@ namespace Distribute const NPCData& a_npcData, Forms::Distributables& a_distributables, const PCLevelMult::Input& a_input, - std::function&, bool)> a_callback) + std::function&, bool)> a_callback) { auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); @@ -142,15 +149,15 @@ namespace Distribute return; } - std::map collectedForms{}; - bool hasLeveledItems = false; + std::map collectedForms{}; + bool hasLeveledItems = false; for (auto& formData : vec) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { if (formData.form->Is(RE::FormType::LeveledItem)) { hasLeveledItems = true; } - collectedForms.emplace(formData.form, formData.idxOrCount); + collectedForms.emplace(formData.form, formData.count.GetRandom()); ++formData.npcCount; } } diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 093acb9..8c112aa 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -307,9 +307,10 @@ namespace Forms { std::uint32_t index{ 0 }; - Form* form{ nullptr }; - IdxOrCount idxOrCount{ 1 }; - FilterData filters{}; + Form* form{ nullptr }; + Index packageIndex{ 0 }; + RandomCount count{ 1, 1 }; + FilterData filters{}; std::string path{}; std::uint32_t npcCount{ 0 }; @@ -442,7 +443,7 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, forms.reserve(a_INIDataVec.size()); std::uint32_t index = 0; - for (auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] : a_INIDataVec) { + for (auto& [formOrEditorID, strings, filterIDs, level, traits, packageIndex, itemsCount, chance, path] : a_INIDataVec) { try { if (auto form = detail::get_form(a_dataHandler, formOrEditorID, path); form) { FormFilters filterForms{}; @@ -456,7 +457,7 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, } if (validEntry) { - forms.emplace_back(index, form, idxOrCount, FilterData(strings, filterForms, level, traits, chance), path); + forms.emplace_back(index, form, packageIndex, itemsCount, FilterData(strings, filterForms, level, traits, chance), path); index++; } } diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 063c2ed..a650760 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -46,7 +46,8 @@ namespace INI Filters rawFormFilters{}; LevelFilters levelFilters{}; Traits traits{}; - IdxOrCount idxOrCount{ 1 }; + Index index{ 0 }; + RandomCount count{ 1, 1 }; Chance chance{ 100 }; std::string path{}; }; diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 2e40cee..9e1d400 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -89,8 +89,11 @@ namespace Distribute npc->GetSpellList()->AddShouts(a_shouts); }); - for_each_form(a_npcData, Forms::packages, a_input, [&](auto* a_packageOrList, [[maybe_unused]] IdxOrCount a_idx) { - auto packageIdx = a_idx; + for_each_form(a_npcData, Forms::packages, a_input, [&](auto* a_packageOrList, [[maybe_unused]] IndexOrCount a_idx) { + if (!std::holds_alternative(a_idx)) { + return false; + } + auto packageIdx = std::get(a_idx); if (a_packageOrList->Is(RE::FormType::Package)) { auto package = a_packageOrList->As(); @@ -172,7 +175,7 @@ namespace Distribute const auto npc = a_npcData.GetNPC(); const auto actor = a_npcData.GetActor(); - for_each_form(a_npcData, Forms::items, a_input, [&](std::map& a_objects, const bool a_hasLvlItem) { + for_each_form(a_npcData, Forms::items, a_input, [&](std::map& a_objects, const bool a_hasLvlItem) { if (npc->AddObjectsToContainer(a_objects, npc)) { if (a_hasLvlItem) { detail::init_leveled_items(actor); diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index be7e6a3..1381c3b 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -222,8 +222,14 @@ namespace Distribute::Event const auto npcData = NPCData(actor, npc); const auto input = PCLevelMult::Input{ actor, npc, false }; - for_each_form(npcData, Forms::deathItems, input, [&](auto* a_deathItem, IdxOrCount a_count) { - detail::add_item(actor, a_deathItem, a_count); + for_each_form(npcData, Forms::deathItems, input, [&](auto* deathItem, IndexOrCount idxOrCount) { + if (!std::holds_alternative(idxOrCount)) { + return false; + } + + auto count = std::get(idxOrCount); + + detail::add_item(actor, deathItem, count.GetRandom()); return true; }); } diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 07df2e0..71c14a2 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -246,15 +246,28 @@ namespace INI } //ITEMCOUNT/INDEX - if (a_key == "Package") { // reuse item count for package stack index - data.idxOrCount = 0; - } + if (kIdxOrCount < size) { - if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { - data.idxOrCount = string::to_num(str); + if (a_key == "Package") { // reuse item count for package stack index + if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { + data.index = string::to_num(str); + } + } else { + if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { + if (auto countPair = string::split(str, "/"); countPair.size() > 1) { + auto minCount = string::to_num(countPair[0]); + auto maxCount = string::to_num(countPair[1]); + + data.count = RandomCount(minCount, maxCount); + } else { + auto count = string::to_num(str); + + data.count = RandomCount(count, count); // create the exact match range. + } + } } } - + //CHANCE if (kChance < size) { if (const auto& str = sections[kChance]; distribution::is_valid_entry(str)) { From 1b99281bc723c7a55c914fd7ce6baacb4fe31f57 Mon Sep 17 00:00:00 2001 From: adya Date: Thu, 14 Mar 2024 00:10:39 +0000 Subject: [PATCH 11/53] maintenance --- SPID/include/Defs.h | 2 +- SPID/include/Distribute.h | 14 +++++++------- SPID/src/LookupConfigs.cpp | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index 29db1ac..57c23f5 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -54,7 +54,7 @@ struct Range return value >= min && value <= max; } - [[nodiscard]] bool IsExact() const + [[nodiscard]] bool IsExact() const { return min == max; } diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 9499025..059dbab 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -77,9 +77,9 @@ namespace Distribute // for now, only packages/death items use this template void for_each_form( - const NPCData& a_npcData, - Forms::Distributables& a_distributables, - const PCLevelMult::Input& a_input, + const NPCData& a_npcData, + Forms::Distributables& a_distributables, + const PCLevelMult::Input& a_input, std::function a_callback) { auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); @@ -138,9 +138,9 @@ namespace Distribute // items template void for_each_form( - const NPCData& a_npcData, - Forms::Distributables& a_distributables, - const PCLevelMult::Input& a_input, + const NPCData& a_npcData, + Forms::Distributables& a_distributables, + const PCLevelMult::Input& a_input, std::function&, bool)> a_callback) { auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); @@ -150,7 +150,7 @@ namespace Distribute } std::map collectedForms{}; - bool hasLeveledItems = false; + bool hasLeveledItems = false; for (auto& formData : vec) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 71c14a2..ad8f5aa 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -246,7 +246,7 @@ namespace INI } //ITEMCOUNT/INDEX - + if (kIdxOrCount < size) { if (a_key == "Package") { // reuse item count for package stack index if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { @@ -267,7 +267,7 @@ namespace INI } } } - + //CHANCE if (kChance < size) { if (const auto& str = sections[kChance]; distribution::is_valid_entry(str)) { From f944a828a999d6aac2b618f121e36d6e21238a20 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 14 Mar 2024 02:26:00 +0200 Subject: [PATCH 12/53] Merged IndexOrCount into a single variant. --- SPID/include/Distribute.h | 13 +++---------- SPID/include/FormData.h | 23 +++++++++++++++++------ SPID/include/LookupConfigs.h | 3 +-- SPID/src/LookupConfigs.cpp | 13 ++++++++----- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 059dbab..25a2ba9 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -86,15 +86,8 @@ namespace Distribute for (auto& formData : vec) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { - if constexpr (std::is_same_v) { - if (a_callback(formData.form, formData.count.GetRandom())) { - ++formData.npcCount; - } - } else { - if (a_callback(formData.form, formData.packageIndex)) { - ++formData.npcCount; - } - } + a_callback(formData.form, formData.idxOrCount); + ++formData.npcCount; } } } @@ -157,7 +150,7 @@ namespace Distribute if (formData.form->Is(RE::FormType::LeveledItem)) { hasLeveledItems = true; } - collectedForms.emplace(formData.form, formData.count.GetRandom()); + collectedForms.emplace(formData.form, formData.GetCount().GetRandom()); ++formData.npcCount; } } diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 8c112aa..eb3ef08 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -307,15 +307,20 @@ namespace Forms { std::uint32_t index{ 0 }; - Form* form{ nullptr }; - Index packageIndex{ 0 }; - RandomCount count{ 1, 1 }; - FilterData filters{}; + Form* form{ nullptr }; + IndexOrCount idxOrCount{ RandomCount(1, 1) }; + FilterData filters{}; std::string path{}; std::uint32_t npcCount{ 0 }; bool operator==(const Data& a_rhs) const; + + /// + /// Unsafely gets a RandomCount from idxOrCount. + /// Onlly call this method when you're sure that idxOrCount is a RandomCount. + /// + const RandomCount& GetCount() const; }; template @@ -392,6 +397,12 @@ bool Forms::Data::operator==(const Data& a_rhs) const return form->GetFormID() == a_rhs.form->GetFormID(); } +template +const RandomCount& Forms::Data::GetCount() const +{ + return std::get(idxOrCount); +} + template Forms::Distributables::operator bool() { @@ -443,7 +454,7 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, forms.reserve(a_INIDataVec.size()); std::uint32_t index = 0; - for (auto& [formOrEditorID, strings, filterIDs, level, traits, packageIndex, itemsCount, chance, path] : a_INIDataVec) { + for (auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] : a_INIDataVec) { try { if (auto form = detail::get_form(a_dataHandler, formOrEditorID, path); form) { FormFilters filterForms{}; @@ -457,7 +468,7 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, } if (validEntry) { - forms.emplace_back(index, form, packageIndex, itemsCount, FilterData(strings, filterForms, level, traits, chance), path); + forms.emplace_back(index, form, idxOrCount, FilterData(strings, filterForms, level, traits, chance), path); index++; } } diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index a650760..07e9181 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -46,8 +46,7 @@ namespace INI Filters rawFormFilters{}; LevelFilters levelFilters{}; Traits traits{}; - Index index{ 0 }; - RandomCount count{ 1, 1 }; + IndexOrCount idxOrCount{ RandomCount(1, 1) }; Chance chance{ 100 }; std::string path{}; }; diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index ad8f5aa..260b89d 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -246,23 +246,26 @@ namespace INI } //ITEMCOUNT/INDEX + if (a_key == "Package") { // reuse item count for package stack index + data.idxOrCount = 0; + } if (kIdxOrCount < size) { - if (a_key == "Package") { // reuse item count for package stack index + if (a_key == "Package") { // If it's a package, then we only expect a single number. if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { - data.index = string::to_num(str); + data.idxOrCount = string::to_num(str); } } else { if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { - if (auto countPair = string::split(str, "/"); countPair.size() > 1) { + if (auto countPair = string::split(str, "-"); countPair.size() > 1) { auto minCount = string::to_num(countPair[0]); auto maxCount = string::to_num(countPair[1]); - data.count = RandomCount(minCount, maxCount); + data.idxOrCount = RandomCount(minCount, maxCount); } else { auto count = string::to_num(str); - data.count = RandomCount(count, count); // create the exact match range. + data.idxOrCount = RandomCount(count, count); // create the exact match range. } } } From e5eaac484af9ebbceefa64dcb3e7d84eae27f28a Mon Sep 17 00:00:00 2001 From: adya Date: Thu, 14 Mar 2024 00:26:19 +0000 Subject: [PATCH 13/53] maintenance --- SPID/src/LookupConfigs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 260b89d..f3c3a9f 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -251,7 +251,7 @@ namespace INI } if (kIdxOrCount < size) { - if (a_key == "Package") { // If it's a package, then we only expect a single number. + if (a_key == "Package") { // If it's a package, then we only expect a single number. if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { data.idxOrCount = string::to_num(str); } From f80555f1b5fd00f655679b815d111487fe6ebc6e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 14 Mar 2024 02:31:18 +0200 Subject: [PATCH 14/53] =?UTF-8?q?Removed=20unnecessary=20safety=20checks?= =?UTF-8?q?=20to=20keep=20maximum=20performance=20=F0=9F=98=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SPID/include/Distribute.h | 2 +- SPID/include/FormData.h | 12 ------------ SPID/src/Distribute.cpp | 3 --- SPID/src/DistributeManager.cpp | 4 ---- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 25a2ba9..f0fd2f2 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -150,7 +150,7 @@ namespace Distribute if (formData.form->Is(RE::FormType::LeveledItem)) { hasLeveledItems = true; } - collectedForms.emplace(formData.form, formData.GetCount().GetRandom()); + collectedForms.emplace(formData.form, std::get(formData.idxOrCount).GetRandom()); ++formData.npcCount; } } diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index eb3ef08..9c165b0 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -315,12 +315,6 @@ namespace Forms std::uint32_t npcCount{ 0 }; bool operator==(const Data& a_rhs) const; - - /// - /// Unsafely gets a RandomCount from idxOrCount. - /// Onlly call this method when you're sure that idxOrCount is a RandomCount. - /// - const RandomCount& GetCount() const; }; template @@ -397,12 +391,6 @@ bool Forms::Data::operator==(const Data& a_rhs) const return form->GetFormID() == a_rhs.form->GetFormID(); } -template -const RandomCount& Forms::Data::GetCount() const -{ - return std::get(idxOrCount); -} - template Forms::Distributables::operator bool() { diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 9e1d400..0f2a559 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -90,9 +90,6 @@ namespace Distribute }); for_each_form(a_npcData, Forms::packages, a_input, [&](auto* a_packageOrList, [[maybe_unused]] IndexOrCount a_idx) { - if (!std::holds_alternative(a_idx)) { - return false; - } auto packageIdx = std::get(a_idx); if (a_packageOrList->Is(RE::FormType::Package)) { diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 1381c3b..63b8414 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -223,10 +223,6 @@ namespace Distribute::Event const auto input = PCLevelMult::Input{ actor, npc, false }; for_each_form(npcData, Forms::deathItems, input, [&](auto* deathItem, IndexOrCount idxOrCount) { - if (!std::holds_alternative(idxOrCount)) { - return false; - } - auto count = std::get(idxOrCount); detail::add_item(actor, deathItem, count.GetRandom()); From 8bb55e5d072e9c0739d7f53e925c17b8e8c544fc Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Mon, 18 Mar 2024 02:58:50 +0200 Subject: [PATCH 15/53] WIP: Refactored distribution code to reuse in linked distribution. Key change is abstracted out distribution of a given set of entries and to not rely on global Distributables. --- SPID/include/Distribute.h | 131 +++++++----------- SPID/include/FormData.h | 28 ++++ SPID/src/Distribute.cpp | 245 ++++++++++++++++++++------------- SPID/src/DistributeManager.cpp | 2 +- 4 files changed, 231 insertions(+), 175 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index f0fd2f2..8160a9d 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -73,79 +73,86 @@ namespace Distribute bool can_equip_outfit(const RE::TESNPC* a_npc, RE::BGSOutfit* a_outfit); } +#pragma region Packages, Death Items // old method (distributing one by one) // for now, only packages/death items use this template void for_each_form( const NPCData& a_npcData, - Forms::Distributables& a_distributables, + Forms::DataVec& forms, const PCLevelMult::Input& a_input, - std::function a_callback) + std::function a_callback, + std::set* accumulatedForms = nullptr) { - auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); - - for (auto& formData : vec) { + for (auto& formData : forms) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { + if (accumulatedForms) { + accumulatedForms->insert(formData.form); + } a_callback(formData.form, formData.idxOrCount); ++formData.npcCount; } } } +#pragma endregion - // outfits/sleep outfits - // skins +#pragma region Outfits, Sleep Outfits, Skins template void for_each_form( - const NPCData& a_npcData, - Forms::Distributables& a_distributables, - const PCLevelMult::Input& a_input, - std::function a_callback) + const NPCData& a_npcData, + Forms::DataVec& forms, + const PCLevelMult::Input& a_input, + std::function a_callback, + std::set* accumulatedForms = nullptr) { - auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); - - for (auto& formData : vec) { // Vector is reversed in FinishLookupForms + for (auto& formData : forms) { // Vector is reversed in FinishLookupForms if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData) && a_callback(formData.form)) { + if (accumulatedForms) { + accumulatedForms->insert(formData.form); + } ++formData.npcCount; break; } } } +#pragma endregion + // TODO: Is this unused? // outfits/sleep outfits template void for_each_form( const NPCData& a_npcData, Forms::Distributables& a_distributables, - std::function a_callback) + std::function a_callback, + std::set* accumulatedForms = nullptr) { auto& vec = a_distributables.GetForms(false); for (auto& formData : vec) { // Vector is reversed in FinishLookupForms if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, formData) && a_callback(formData.form)) { + if (accumulatedForms) { + accumulatedForms->insert(formData.form); + } ++formData.npcCount; break; } } } - // items +#pragma region Items + // countable items template void for_each_form( const NPCData& a_npcData, - Forms::Distributables& a_distributables, + Forms::DataVec& forms, const PCLevelMult::Input& a_input, - std::function&, bool)> a_callback) + std::function&, bool)> a_callback, + std::set* accumulatedForms = nullptr) { - auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); - - if (vec.empty()) { - return; - } - std::map collectedForms{}; bool hasLeveledItems = false; - for (auto& formData : vec) { + for (auto& formData : forms) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { if (formData.form->Is(RE::FormType::LeveledItem)) { hasLeveledItems = true; @@ -156,36 +163,36 @@ namespace Distribute } if (!collectedForms.empty()) { + if (accumulatedForms) { + std::ranges::copy(collectedForms | std::views::keys, std::inserter(*accumulatedForms, accumulatedForms->end())); + } a_callback(collectedForms, hasLeveledItems); } } +#pragma endregion +#pragma region Spells, Perks, Shouts, Keywords // spells, perks, shouts, keywords // forms that can be added to template void for_each_form( NPCData& a_npcData, - Forms::Distributables& a_distributables, + Forms::DataVec& forms, const PCLevelMult::Input& a_input, - std::function&)> a_callback) + std::function&)> a_callback, + std::set* accumulatedForms = nullptr) { - auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); - - if (vec.empty()) { - return; - } - const auto npc = a_npcData.GetNPC(); std::vector collectedForms{}; Set collectedFormIDs{}; Set collectedLeveledFormIDs{}; - collectedForms.reserve(vec.size()); - collectedFormIDs.reserve(vec.size()); - collectedLeveledFormIDs.reserve(vec.size()); + collectedForms.reserve(forms.size()); + collectedFormIDs.reserve(forms.size()); + collectedLeveledFormIDs.reserve(forms.size()); - for (auto& formData : vec) { + for (auto& formData : forms) { auto form = formData.form; auto formID = form->GetFormID(); if (collectedFormIDs.contains(formID)) { @@ -212,60 +219,20 @@ namespace Distribute } if (!collectedForms.empty()) { + if (accumulatedForms) { + accumulatedForms->insert(collectedForms.begin(), collectedForms.end()); + } a_callback(collectedForms); if (!collectedLeveledFormIDs.empty()) { PCLevelMult::Manager::GetSingleton()->InsertDistributedEntry(a_input, Form::FORMTYPE, collectedLeveledFormIDs); } } } - - template - void for_each_form( - NPCData& a_npcData, - Forms::Distributables& a_distributables, - std::function&)> a_callback) - { - const auto& vec = a_distributables.GetForms(false); - - if (vec.empty()) { - return; - } - - const auto npc = a_npcData.GetNPC(); - - std::vector collectedForms{}; - Set collectedFormIDs{}; - - collectedForms.reserve(vec.size()); - collectedFormIDs.reserve(vec.size()); - - for (auto& formData : vec) { - auto form = formData.form; - auto formID = form->GetFormID(); - if (collectedFormIDs.contains(formID)) { - continue; - } - if constexpr (std::is_same_v) { - if (!a_npcData.HasMutuallyExclusiveForm(form) && detail::passed_filters(a_npcData, formData) && a_npcData.InsertKeyword(form->GetFormEditorID())) { - collectedForms.emplace_back(form); - collectedFormIDs.emplace(formID); - ++formData.npcCount; - } - } else { - if (!a_npcData.HasMutuallyExclusiveForm(form) && detail::passed_filters(a_npcData, formData) && !detail::has_form(npc, form) && collectedFormIDs.emplace(formID).second) { - collectedForms.emplace_back(form); - ++formData.npcCount; - } - } - } - - if (!collectedForms.empty()) { - a_callback(collectedForms); - } - } +#pragma endregion void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input); void DistributeItemOutfits(NPCData& a_npcData, const PCLevelMult::Input& a_input); void Distribute(NPCData& a_npcData, bool a_onlyLeveledEntries, bool a_noItemOutfits = false); + } diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 9c165b0..4e54629 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -320,6 +320,34 @@ namespace Forms template using DataVec = std::vector>; + + /// + /// A set of distributable forms that should be processed. + /// + /// DistributionSet is used to conveniently pack all distributable forms into one structure. + /// + struct DistributionSet + { + DataVec spells{}; + DataVec perks{}; + DataVec items{}; + DataVec shouts{}; + DataVec levSpells{}; + DataVec packages{}; + DataVec outfits{}; + DataVec keywords{}; + DataVec deathItems{}; + DataVec factions{}; + DataVec sleepOutfits{}; + DataVec skins{}; + }; + + /// + /// A container that holds distributable entries for a single form type. + /// + /// Note that this container tracks separately leveled (those using level in their filters) entries. + /// + /// Type of the forms to store. template struct Distributables { diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 0f2a559..446ecd6 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -52,115 +52,176 @@ namespace Distribute return true; } - } - - void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input) - { - if (a_input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(a_input)) { - return; - } - const auto npc = a_npcData.GetNPC(); + /// + /// Performs distribution of all configured forms to NPC described with a_npcData and a_input. + /// + /// General information about NPC that is being processed. + /// Leveling information about NPC that is being processed. + /// A set of forms that should be distributed to NPC. + /// An optional pointer to a set that will accumulate all distributed forms. + void distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input, Forms::DistributionSet& forms, std::set* accumulatedForms) + { + const auto npc = a_npcData.GetNPC(); - for_each_form(a_npcData, Forms::keywords, a_input, [&](const std::vector& a_keywords) { - npc->AddKeywords(a_keywords); - }); + for_each_form( + a_npcData, forms.keywords, a_input, [&](const std::vector& a_keywords) { + npc->AddKeywords(a_keywords); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::factions, a_input, [&](const std::vector& a_factions) { - npc->factions.reserve(static_cast(a_factions.size())); - for (auto& faction : a_factions) { - npc->factions.emplace_back(RE::FACTION_RANK{ faction, 1 }); - } - }); + for_each_form( + a_npcData, forms.factions, a_input, [&](const std::vector& a_factions) { + npc->factions.reserve(static_cast(a_factions.size())); + for (auto& faction : a_factions) { + npc->factions.emplace_back(RE::FACTION_RANK{ faction, 1 }); + } + }, + accumulatedForms); - for_each_form(a_npcData, Forms::spells, a_input, [&](const std::vector& a_spells) { - npc->GetSpellList()->AddSpells(a_spells); - }); + for_each_form( + a_npcData, forms.spells, a_input, [&](const std::vector& a_spells) { + npc->GetSpellList()->AddSpells(a_spells); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::levSpells, a_input, [&](const std::vector& a_levSpells) { - npc->GetSpellList()->AddLevSpells(a_levSpells); - }); + for_each_form( + a_npcData, forms.levSpells, a_input, [&](const std::vector& a_levSpells) { + npc->GetSpellList()->AddLevSpells(a_levSpells); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::perks, a_input, [&](const std::vector& a_perks) { - npc->AddPerks(a_perks, 1); - }); + for_each_form( + a_npcData, forms.perks, a_input, [&](const std::vector& a_perks) { + npc->AddPerks(a_perks, 1); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::shouts, a_input, [&](const std::vector& a_shouts) { - npc->GetSpellList()->AddShouts(a_shouts); - }); + for_each_form( + a_npcData, forms.shouts, a_input, [&](const std::vector& a_shouts) { + npc->GetSpellList()->AddShouts(a_shouts); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::packages, a_input, [&](auto* a_packageOrList, [[maybe_unused]] IndexOrCount a_idx) { - auto packageIdx = std::get(a_idx); + for_each_form( + a_npcData, forms.packages, a_input, [&](auto* a_packageOrList, [[maybe_unused]] IndexOrCount a_idx) { + auto packageIdx = std::get(a_idx); - if (a_packageOrList->Is(RE::FormType::Package)) { - auto package = a_packageOrList->As(); + if (a_packageOrList->Is(RE::FormType::Package)) { + auto package = a_packageOrList->As(); - if (packageIdx > 0) { - --packageIdx; //get actual position we want to insert at - } + if (packageIdx > 0) { + --packageIdx; //get actual position we want to insert at + } - auto& packageList = npc->aiPackages.packages; - if (std::ranges::find(packageList, package) == packageList.end()) { - if (packageList.empty() || packageIdx == 0) { - packageList.push_front(package); - } else { - auto idxIt = packageList.begin(); - for (idxIt; idxIt != packageList.end(); ++idxIt) { - auto idx = std::distance(packageList.begin(), idxIt); - if (packageIdx == idx) { - break; + auto& packageList = npc->aiPackages.packages; + if (std::ranges::find(packageList, package) == packageList.end()) { + if (packageList.empty() || packageIdx == 0) { + packageList.push_front(package); + } else { + auto idxIt = packageList.begin(); + for (idxIt; idxIt != packageList.end(); ++idxIt) { + auto idx = std::distance(packageList.begin(), idxIt); + if (packageIdx == idx) { + break; + } + } + if (idxIt != packageList.end()) { + packageList.insert_after(idxIt, package); + } } + return true; } - if (idxIt != packageList.end()) { - packageList.insert_after(idxIt, package); + } else if (a_packageOrList->Is(RE::FormType::FormList)) { + auto packageList = a_packageOrList->As(); + + switch (packageIdx) { + case 0: + npc->defaultPackList = packageList; + break; + case 1: + npc->spectatorOverRidePackList = packageList; + break; + case 2: + npc->observeCorpseOverRidePackList = packageList; + break; + case 3: + npc->guardWarnOverRidePackList = packageList; + break; + case 4: + npc->enterCombatOverRidePackList = packageList; + break; + default: + break; } + return true; } - return true; - } - } else if (a_packageOrList->Is(RE::FormType::FormList)) { - auto packageList = a_packageOrList->As(); - - switch (packageIdx) { - case 0: - npc->defaultPackList = packageList; - break; - case 1: - npc->spectatorOverRidePackList = packageList; - break; - case 2: - npc->observeCorpseOverRidePackList = packageList; - break; - case 3: - npc->guardWarnOverRidePackList = packageList; - break; - case 4: - npc->enterCombatOverRidePackList = packageList; - break; - default: - break; - } - return true; - } + return false; + }, + accumulatedForms); - return false; - }); + for_each_form( + a_npcData, forms.skins, a_input, [&](auto* a_skin) { + if (npc->skin != a_skin) { + npc->skin = a_skin; + return true; + } + return false; + }, + accumulatedForms); - for_each_form(a_npcData, Forms::skins, a_input, [&](auto* a_skin) { - if (npc->skin != a_skin) { - npc->skin = a_skin; - return true; - } - return false; - }); + for_each_form( + a_npcData, forms.sleepOutfits, a_input, [&](auto* a_outfit) { + if (npc->sleepOutfit != a_outfit) { + npc->sleepOutfit = a_outfit; + return true; + } + return false; + }, + accumulatedForms); + } - for_each_form(a_npcData, Forms::sleepOutfits, a_input, [&](auto* a_outfit) { - if (npc->sleepOutfit != a_outfit) { - npc->sleepOutfit = a_outfit; - return true; - } - return false; - }); + } + + // This only does one-level linking. So that linked entries won't trigger another level of distribution. + void DistributeLinkedEntries(NPCData& npcData, const PCLevelMult::Input& a_input, const std::set& forms) + { + // TODO: Get linked entries and repeat distribution for them. + + Forms::DistributionSet entries{}; + detail::distribute(npcData, a_input, entries, nullptr); + } + + void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input) + { + if (a_input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(a_input)) { + return; + } + + Forms::DistributionSet entries{ + Forms::spells.GetForms(a_input.onlyPlayerLevelEntries), + Forms::perks.GetForms(a_input.onlyPlayerLevelEntries), + {}, // items are processed separately + Forms::shouts.GetForms(a_input.onlyPlayerLevelEntries), + Forms::levSpells.GetForms(a_input.onlyPlayerLevelEntries), + Forms::packages.GetForms(a_input.onlyPlayerLevelEntries), + {}, // outfits are processed along with items. + Forms::keywords.GetForms(a_input.onlyPlayerLevelEntries), + {}, // deathItems are only processed on... well, death. + Forms::factions.GetForms(a_input.onlyPlayerLevelEntries), + Forms::sleepOutfits.GetForms(a_input.onlyPlayerLevelEntries), + Forms::skins.GetForms(a_input.onlyPlayerLevelEntries) + }; + + std::set distributedForms{}; + + detail::distribute(a_npcData, a_input, entries, &distributedForms); + // TODO: We can now log per-NPC distributed forms. + + if (!distributedForms.empty()) { + DistributeLinkedEntries(a_npcData, a_input, distributedForms); + } } void DistributeItemOutfits(NPCData& a_npcData, const PCLevelMult::Input& a_input) @@ -172,7 +233,7 @@ namespace Distribute const auto npc = a_npcData.GetNPC(); const auto actor = a_npcData.GetActor(); - for_each_form(a_npcData, Forms::items, a_input, [&](std::map& a_objects, const bool a_hasLvlItem) { + for_each_form(a_npcData, Forms::items.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](std::map& a_objects, const bool a_hasLvlItem) { if (npc->AddObjectsToContainer(a_objects, npc)) { if (a_hasLvlItem) { detail::init_leveled_items(actor); @@ -182,7 +243,7 @@ namespace Distribute return false; }); - for_each_form(a_npcData, Forms::outfits, a_input, [&](auto* a_outfit) { + for_each_form(a_npcData, Forms::outfits.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](auto* a_outfit) { if (detail::can_equip_outfit(npc, a_outfit)) { actor->RemoveOutfitItems(npc->defaultOutfit); npc->defaultOutfit = a_outfit; diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 63b8414..66b3a1f 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -222,7 +222,7 @@ namespace Distribute::Event const auto npcData = NPCData(actor, npc); const auto input = PCLevelMult::Input{ actor, npc, false }; - for_each_form(npcData, Forms::deathItems, input, [&](auto* deathItem, IndexOrCount idxOrCount) { + for_each_form(npcData, Forms::deathItems.GetForms(input.onlyPlayerLevelEntries), input, [&](auto* deathItem, IndexOrCount idxOrCount) { auto count = std::get(idxOrCount); detail::add_item(actor, deathItem, count.GetRandom()); From 0065c94d3e4193f2c1d9204d36200b3e88ff7b8a Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Mon, 18 Mar 2024 03:00:02 +0200 Subject: [PATCH 16/53] Introduced Linked Distribution initial outline. --- SPID/cmake/headerlist.cmake | 1 + SPID/cmake/sourcelist.cmake | 1 + SPID/include/ExclusiveGroups.h | 6 +-- SPID/include/LinkedDistribution.h | 79 +++++++++++++++++++++++++++++ SPID/src/ExclusiveGroups.cpp | 6 +-- SPID/src/LinkedDistribution.cpp | 82 +++++++++++++++++++++++++++++++ SPID/src/LookupConfigs.cpp | 14 +++--- 7 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 SPID/include/LinkedDistribution.h create mode 100644 SPID/src/LinkedDistribution.cpp diff --git a/SPID/cmake/headerlist.cmake b/SPID/cmake/headerlist.cmake index 9d8fa86..e233a89 100644 --- a/SPID/cmake/headerlist.cmake +++ b/SPID/cmake/headerlist.cmake @@ -8,6 +8,7 @@ set(headers ${headers} include/ExclusiveGroups.h include/FormData.h include/KeywordDependencies.h + include/LinkedDistribution.h include/LogBuffer.h include/LookupConfigs.h include/LookupFilters.h diff --git a/SPID/cmake/sourcelist.cmake b/SPID/cmake/sourcelist.cmake index fc75060..c78269e 100644 --- a/SPID/cmake/sourcelist.cmake +++ b/SPID/cmake/sourcelist.cmake @@ -6,6 +6,7 @@ set(sources ${sources} src/ExclusiveGroups.cpp src/FormData.cpp src/KeywordDependencies.cpp + src/LinkedDistribution.cpp src/LogBuffer.cpp src/LookupConfigs.cpp src/LookupFilters.cpp diff --git a/SPID/include/ExclusiveGroups.h b/SPID/include/ExclusiveGroups.h index 0fc97e8..4ea6043 100644 --- a/SPID/include/ExclusiveGroups.h +++ b/SPID/include/ExclusiveGroups.h @@ -15,15 +15,15 @@ namespace ExclusiveGroups /// /// As a result this method configures Manager with discovered valid exclusive groups. /// - /// - /// A raw exclusive group entries that should be processed/ + /// A DataHandler that will perform the actual lookup. + /// A raw exclusive group entries that should be processed. void LookupExclusiveGroups(RE::TESDataHandler* const dataHandler, INI::ExclusiveGroupsVec& rawExclusiveGroups); /// /// Gets a set of all forms that are in the same exclusive group as the given form. /// Note that a form can appear in multiple exclusive groups, all of those groups are returned. /// - /// + /// A form for which mutually exclusive forms will be returned. /// A union of all groups that contain a given form. std::unordered_set MutuallyExclusiveFormsForForm(RE::TESForm* form) const; diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h new file mode 100644 index 0000000..ba965c2 --- /dev/null +++ b/SPID/include/LinkedDistribution.h @@ -0,0 +1,79 @@ +#pragma once +#include "FormData.h" + +namespace LinkedDistribution +{ + namespace INI { + + struct RawLinkedItem + { + FormOrEditorID rawForm{}; + + /// Raw filters in RawLinkedItem only use MATCH, there is no meaning for ALL or NOT, so they are ignored. + Filters rawFormFilters{}; + + RandomCount count{ 1, 1 }; + Chance chance{ 100 }; + + std::string path{}; + }; + + using LinkedItemsVec = std::vector; + + inline LinkedItemsVec linkedItems{}; + + namespace Parser + { + bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); + } + } + + template + using DataSet = std::set>; + + template + using LinkedForms = std::unordered_map>; + + class Manager : public ISingleton + { + private: + template + const DataSet& LinkedFormsForForm(const RE::TESForm* form, const LinkedForms& linkedForms) const + { + if (const auto it = linkedForms.find(form); it != linkedForms.end()) { + return it->second; + } else { + static std::set empty{}; + return empty; + } + } + + public: + /// + /// Does a forms lookup similar to what Filters do. + /// + /// As a result this method configures Manager with discovered valid linked items. + /// + /// A DataHandler that will perform the actual lookup. + /// A raw linked item entries that should be processed. + void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems); + + + + private: + + LinkedForms spells{ RECORD::kSpell }; + LinkedForms perks{ RECORD::kPerk }; + LinkedForms items{ RECORD::kItem }; + LinkedForms shouts{ RECORD::kShout }; + LinkedForms levSpells{ RECORD::kLevSpell }; + LinkedForms packages{ RECORD::kPackage }; + LinkedForms outfits{ RECORD::kOutfit }; + LinkedForms keywords{ RECORD::kKeyword }; + LinkedForms deathItems{ RECORD::kDeathItem }; + LinkedForms factions{ RECORD::kFaction }; + LinkedForms sleepOutfits{ RECORD::kSleepOutfit }; + LinkedForms skins{ RECORD::kSkin }; + + }; +} diff --git a/SPID/src/ExclusiveGroups.cpp b/SPID/src/ExclusiveGroups.cpp index 6db5f4f..607c3c0 100644 --- a/SPID/src/ExclusiveGroups.cpp +++ b/SPID/src/ExclusiveGroups.cpp @@ -28,11 +28,7 @@ void ExclusiveGroups::Manager::LookupExclusiveGroups(RE::TESDataHandler* const d } // Remove empty groups - for (auto& [name, forms] : groups) { - if (forms.empty()) { - groups.erase(name); - } - } + std::erase_if(groups, [](const auto& pair) { return pair.second.empty(); }); for (auto& [name, forms] : groups) { for (auto& form : forms) { diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp new file mode 100644 index 0000000..155d936 --- /dev/null +++ b/SPID/src/LinkedDistribution.cpp @@ -0,0 +1,82 @@ +#include "LinkedDistribution.h" +#include "FormData.h" + +#pragma region Parsing +bool LinkedDistribution::INI::Parser::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) +{ + if (a_key != "LinkedItem") { + return false; + } + + const auto sections = string::split(a_value, "|"); + const auto size = sections.size(); + + if (size < 2) { + logger::warn("IGNORED: LinkedItem must have a form and at least one filter name: {} = {}"sv, a_key, a_value); + return false; + } + + auto split_IDs = distribution::split_entry(sections[1]); + + if (split_IDs.empty()) { + logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); + return false; + } + + LinkedDistribution::INI::RawLinkedItem item{}; + item.rawForm = sections[0]; + item.path = a_path; + + for (auto& IDs : split_IDs) { + item.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + } +} +#pragma endregion + +#pragma region Lookup +void LinkedDistribution::Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems) +{ + using namespace Forms; + + // TODO: Figure out templates here. + + + for (auto& [rawForm, filterIDs, count, chance, path] : rawLinkedItems) { + try { + if (const auto form = Forms::detail::get_form(dataHandler, rawForm, path); form) { + /*auto& forms = linkedForms[form]; + FormVec match{}; + + if (Forms::detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false, false)) { + for (const auto& form : match) { + if (std::holds_alternative(form)) { + forms.insert(std::get(form)); + } + } + }*/ + } + } catch (const Lookup::UnknownFormIDException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); + } catch (const Lookup::InvalidKeywordException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); + } catch (const Lookup::KeywordNotFoundException& e) { + if (e.isDynamic) { + buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); + } else { + buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); + } + } catch (const Lookup::UnknownEditorIDException& e) { + buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); + } catch (const Lookup::MalformedEditorIDException& e) { + buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); + } catch (const Lookup::InvalidFormTypeException& e) { + // Whitelisting is disabled, so this should not occur + } catch (const Lookup::UnknownPluginException& e) { + // Likewise, we don't expect plugin names in linked forms. + } + } + + // Remove empty linked forms + //std::erase_if(linkedForms, [](const auto& pair) { return pair.second.empty(); }); +} +#pragma endregion diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index f3c3a9f..f4b6ef1 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -1,4 +1,5 @@ #include "LookupConfigs.h" +#include "LinkedDistribution.h" namespace INI { @@ -55,13 +56,8 @@ namespace INI const auto sections = string::split(a_value, "|"); const auto size = sections.size(); - if (size == 0) { - logger::warn("IGNORED: ExclusiveGroup must have a name: {} = {}"sv, a_key, a_value); - return std::nullopt; - } - - if (size == 1) { - logger::warn("IGNORED: ExclusiveGroup must have at least one filter name: {} = {}"sv, a_key, a_value); + if (size < 2) { + logger::warn("IGNORED: ExclusiveGroup must have a name and at least one Form Filter: {} = {}"sv, a_key, a_value); return std::nullopt; } @@ -326,6 +322,10 @@ namespace INI continue; } + if (LinkedDistribution::INI::Parser::TryParse(key.pItem, entry, truncatedPath)) { + continue; + } + auto [data, sanitized_str] = detail::parse_ini(key.pItem, entry, truncatedPath); configs[key.pItem].emplace_back(data); From 933ac135490116ad728ba8e8513268f3f90cd643 Mon Sep 17 00:00:00 2001 From: adya Date: Mon, 18 Mar 2024 01:00:20 +0000 Subject: [PATCH 17/53] maintenance --- SPID/include/Distribute.h | 2 +- SPID/include/FormData.h | 3 +-- SPID/include/LinkedDistribution.h | 11 ++++------- SPID/src/Distribute.cpp | 6 +++--- SPID/src/LinkedDistribution.cpp | 5 ++--- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 8160a9d..6bd64ed 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -79,7 +79,7 @@ namespace Distribute template void for_each_form( const NPCData& a_npcData, - Forms::DataVec& forms, + Forms::DataVec& forms, const PCLevelMult::Input& a_input, std::function a_callback, std::set* accumulatedForms = nullptr) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 4e54629..cc570c8 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -320,7 +320,6 @@ namespace Forms template using DataVec = std::vector>; - /// /// A set of distributable forms that should be processed. /// @@ -344,7 +343,7 @@ namespace Forms /// /// A container that holds distributable entries for a single form type. - /// + /// /// Note that this container tracks separately leveled (those using level in their filters) entries. /// /// Type of the forms to store. diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index ba965c2..51de848 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -3,7 +3,8 @@ namespace LinkedDistribution { - namespace INI { + namespace INI + { struct RawLinkedItem { @@ -28,10 +29,10 @@ namespace LinkedDistribution } } - template + template using DataSet = std::set>; - template + template using LinkedForms = std::unordered_map>; class Manager : public ISingleton @@ -58,10 +59,7 @@ namespace LinkedDistribution /// A raw linked item entries that should be processed. void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems); - - private: - LinkedForms spells{ RECORD::kSpell }; LinkedForms perks{ RECORD::kPerk }; LinkedForms items{ RECORD::kItem }; @@ -74,6 +72,5 @@ namespace LinkedDistribution LinkedForms factions{ RECORD::kFaction }; LinkedForms sleepOutfits{ RECORD::kSleepOutfit }; LinkedForms skins{ RECORD::kSkin }; - }; } diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 446ecd6..5359ce6 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -202,13 +202,13 @@ namespace Distribute Forms::DistributionSet entries{ Forms::spells.GetForms(a_input.onlyPlayerLevelEntries), Forms::perks.GetForms(a_input.onlyPlayerLevelEntries), - {}, // items are processed separately + {}, // items are processed separately Forms::shouts.GetForms(a_input.onlyPlayerLevelEntries), Forms::levSpells.GetForms(a_input.onlyPlayerLevelEntries), Forms::packages.GetForms(a_input.onlyPlayerLevelEntries), - {}, // outfits are processed along with items. + {}, // outfits are processed along with items. Forms::keywords.GetForms(a_input.onlyPlayerLevelEntries), - {}, // deathItems are only processed on... well, death. + {}, // deathItems are only processed on... well, death. Forms::factions.GetForms(a_input.onlyPlayerLevelEntries), Forms::sleepOutfits.GetForms(a_input.onlyPlayerLevelEntries), Forms::skins.GetForms(a_input.onlyPlayerLevelEntries) diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 155d936..7566bd4 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -2,7 +2,7 @@ #include "FormData.h" #pragma region Parsing -bool LinkedDistribution::INI::Parser::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) +bool LinkedDistribution::INI::Parser::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) { if (a_key != "LinkedItem") { return false; @@ -34,12 +34,11 @@ bool LinkedDistribution::INI::Parser::TryParse(const std::string& a_key, const s #pragma endregion #pragma region Lookup -void LinkedDistribution::Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems) +void LinkedDistribution::Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems) { using namespace Forms; // TODO: Figure out templates here. - for (auto& [rawForm, filterIDs, count, chance, path] : rawLinkedItems) { try { From 827b34b907ec0b00cb6e00c252eb58f7baed5cf0 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 19 Mar 2024 02:20:38 +0200 Subject: [PATCH 18/53] Implemented Linked Distribution. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probably 😁 needs testing, and updating parsing and lookup to work with full entry format. --- SPID/include/FormData.h | 35 ++++--- SPID/include/LinkedDistribution.h | 41 +++++--- SPID/src/Distribute.cpp | 59 +++++++----- SPID/src/FormData.cpp | 5 + SPID/src/LinkedDistribution.cpp | 150 ++++++++++++++++++++---------- 5 files changed, 187 insertions(+), 103 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index cc570c8..38d0959 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -324,21 +324,32 @@ namespace Forms /// A set of distributable forms that should be processed. /// /// DistributionSet is used to conveniently pack all distributable forms into one structure. + /// Note that all entries store references so they are not owned by this structure. + /// If you want to omit certain type of entries, you can use static empty() method to get a reference to an empty container. /// struct DistributionSet { - DataVec spells{}; - DataVec perks{}; - DataVec items{}; - DataVec shouts{}; - DataVec levSpells{}; - DataVec packages{}; - DataVec outfits{}; - DataVec keywords{}; - DataVec deathItems{}; - DataVec factions{}; - DataVec sleepOutfits{}; - DataVec skins{}; + DataVec& spells; + DataVec& perks; + DataVec& items; + DataVec& shouts; + DataVec& levSpells; + DataVec& packages; + DataVec& outfits; + DataVec& keywords; + DataVec& deathItems; + DataVec& factions; + DataVec& sleepOutfits; + DataVec& skins; + + bool IsEmpty() const; + + template + static DataVec& empty() + { + static DataVec empty{}; + return empty; + } }; /// diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 51de848..6abb0ce 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -1,5 +1,7 @@ #pragma once #include "FormData.h" +#include "LookupNPC.h" +#include "PCLevelMultManager.h" namespace LinkedDistribution { @@ -29,25 +31,13 @@ namespace LinkedDistribution } } - template - using DataSet = std::set>; + using namespace Forms; - template - using LinkedForms = std::unordered_map>; + template + using LinkedForms = std::unordered_map>; class Manager : public ISingleton { - private: - template - const DataSet& LinkedFormsForForm(const RE::TESForm* form, const LinkedForms& linkedForms) const - { - if (const auto it = linkedForms.find(form); it != linkedForms.end()) { - return it->second; - } else { - static std::set empty{}; - return empty; - } - } public: /// @@ -59,7 +49,28 @@ namespace LinkedDistribution /// A raw linked item entries that should be processed. void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems); + + /// + /// Calculates DistributionSet for each linked form and calls a callback for each of them. + /// + /// A set of forms for which distribution sets should be calculated. + /// This is typically distributed forms accumulated during first distribution pass. + /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. + void ForEachLinkedDistributionSet(const std::set& linkedForms, std::function callback); + private: + + template + DataVec& LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const + { + if (auto it = linkedForms.find(form); it != linkedForms.end()) { + return it->second; + } else { + static DataVec empty{}; + return empty; + } + } + LinkedForms spells{ RECORD::kSpell }; LinkedForms perks{ RECORD::kPerk }; LinkedForms items{ RECORD::kItem }; diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 5359ce6..9db01e3 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -1,6 +1,7 @@ #include "Distribute.h" #include "DistributeManager.h" +#include "LinkedDistribution.h" namespace Distribute { @@ -54,24 +55,24 @@ namespace Distribute } /// - /// Performs distribution of all configured forms to NPC described with a_npcData and a_input. + /// Performs distribution of all configured forms to NPC described with npcData and input. /// - /// General information about NPC that is being processed. - /// Leveling information about NPC that is being processed. + /// General information about NPC that is being processed. + /// Leveling information about NPC that is being processed. /// A set of forms that should be distributed to NPC. /// An optional pointer to a set that will accumulate all distributed forms. - void distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input, Forms::DistributionSet& forms, std::set* accumulatedForms) + void distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, std::set* accumulatedForms) { - const auto npc = a_npcData.GetNPC(); + const auto npc = npcData.GetNPC(); for_each_form( - a_npcData, forms.keywords, a_input, [&](const std::vector& a_keywords) { + npcData, forms.keywords, input, [&](const std::vector& a_keywords) { npc->AddKeywords(a_keywords); }, accumulatedForms); for_each_form( - a_npcData, forms.factions, a_input, [&](const std::vector& a_factions) { + npcData, forms.factions, input, [&](const std::vector& a_factions) { npc->factions.reserve(static_cast(a_factions.size())); for (auto& faction : a_factions) { npc->factions.emplace_back(RE::FACTION_RANK{ faction, 1 }); @@ -80,31 +81,31 @@ namespace Distribute accumulatedForms); for_each_form( - a_npcData, forms.spells, a_input, [&](const std::vector& a_spells) { + npcData, forms.spells, input, [&](const std::vector& a_spells) { npc->GetSpellList()->AddSpells(a_spells); }, accumulatedForms); for_each_form( - a_npcData, forms.levSpells, a_input, [&](const std::vector& a_levSpells) { + npcData, forms.levSpells, input, [&](const std::vector& a_levSpells) { npc->GetSpellList()->AddLevSpells(a_levSpells); }, accumulatedForms); for_each_form( - a_npcData, forms.perks, a_input, [&](const std::vector& a_perks) { + npcData, forms.perks, input, [&](const std::vector& a_perks) { npc->AddPerks(a_perks, 1); }, accumulatedForms); for_each_form( - a_npcData, forms.shouts, a_input, [&](const std::vector& a_shouts) { + npcData, forms.shouts, input, [&](const std::vector& a_shouts) { npc->GetSpellList()->AddShouts(a_shouts); }, accumulatedForms); for_each_form( - a_npcData, forms.packages, a_input, [&](auto* a_packageOrList, [[maybe_unused]] IndexOrCount a_idx) { + npcData, forms.packages, input, [&](auto* a_packageOrList, [[maybe_unused]] IndexOrCount a_idx) { auto packageIdx = std::get(a_idx); if (a_packageOrList->Is(RE::FormType::Package)) { @@ -162,7 +163,7 @@ namespace Distribute accumulatedForms); for_each_form( - a_npcData, forms.skins, a_input, [&](auto* a_skin) { + npcData, forms.skins, input, [&](auto* a_skin) { if (npc->skin != a_skin) { npc->skin = a_skin; return true; @@ -172,7 +173,7 @@ namespace Distribute accumulatedForms); for_each_form( - a_npcData, forms.sleepOutfits, a_input, [&](auto* a_outfit) { + npcData, forms.sleepOutfits, input, [&](auto* a_outfit) { if (npc->sleepOutfit != a_outfit) { npc->sleepOutfit = a_outfit; return true; @@ -181,16 +182,14 @@ namespace Distribute }, accumulatedForms); } - } // This only does one-level linking. So that linked entries won't trigger another level of distribution. - void DistributeLinkedEntries(NPCData& npcData, const PCLevelMult::Input& a_input, const std::set& forms) + void DistributeLinkedEntries(NPCData& npcData, const PCLevelMult::Input& input, const std::set& forms) { - // TODO: Get linked entries and repeat distribution for them. - - Forms::DistributionSet entries{}; - detail::distribute(npcData, a_input, entries, nullptr); + LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(forms, [&](Forms::DistributionSet& set) { + detail::distribute(npcData, input, set, nullptr); + }); } void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input) @@ -202,13 +201,13 @@ namespace Distribute Forms::DistributionSet entries{ Forms::spells.GetForms(a_input.onlyPlayerLevelEntries), Forms::perks.GetForms(a_input.onlyPlayerLevelEntries), - {}, // items are processed separately + Forms::DistributionSet::empty(), // items are processed separately Forms::shouts.GetForms(a_input.onlyPlayerLevelEntries), Forms::levSpells.GetForms(a_input.onlyPlayerLevelEntries), Forms::packages.GetForms(a_input.onlyPlayerLevelEntries), - {}, // outfits are processed along with items. + Forms::DistributionSet::empty(), // outfits are processed along with items. Forms::keywords.GetForms(a_input.onlyPlayerLevelEntries), - {}, // deathItems are only processed on... well, death. + Forms::DistributionSet::empty(), // deathItems are only processed on... well, death. Forms::factions.GetForms(a_input.onlyPlayerLevelEntries), Forms::sleepOutfits.GetForms(a_input.onlyPlayerLevelEntries), Forms::skins.GetForms(a_input.onlyPlayerLevelEntries) @@ -233,6 +232,8 @@ namespace Distribute const auto npc = a_npcData.GetNPC(); const auto actor = a_npcData.GetActor(); + std::set distributedForms{}; + for_each_form(a_npcData, Forms::items.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](std::map& a_objects, const bool a_hasLvlItem) { if (npc->AddObjectsToContainer(a_objects, npc)) { if (a_hasLvlItem) { @@ -241,7 +242,8 @@ namespace Distribute return true; } return false; - }); + }, + &distributedForms); for_each_form(a_npcData, Forms::outfits.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](auto* a_outfit) { if (detail::can_equip_outfit(npc, a_outfit)) { @@ -251,7 +253,14 @@ namespace Distribute return true; } return false; - }); + }, + &distributedForms); + + // TODO: We can now log per-NPC distributed forms. + + if (!distributedForms.empty()) { + DistributeLinkedEntries(a_npcData, a_input, distributedForms); + } } void Distribute(NPCData& a_npcData, bool a_onlyLeveledEntries, bool a_noItemOutfits) diff --git a/SPID/src/FormData.cpp b/SPID/src/FormData.cpp index a78d689..676e3b2 100644 --- a/SPID/src/FormData.cpp +++ b/SPID/src/FormData.cpp @@ -21,3 +21,8 @@ std::size_t Forms::GetTotalLeveledEntries() return size; } + +bool Forms::DistributionSet::IsEmpty() const +{ + return spells.empty() && perks.empty() && items.empty() && shouts.empty() && levSpells.empty() && packages.empty() && outfits.empty() && keywords.empty() && deathItems.empty() && factions.empty() && sleepOutfits.empty() && skins.empty(); +} diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 7566bd4..6ac90eb 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -1,81 +1,129 @@ #include "LinkedDistribution.h" #include "FormData.h" -#pragma region Parsing -bool LinkedDistribution::INI::Parser::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) +namespace LinkedDistribution { - if (a_key != "LinkedItem") { - return false; - } - const auto sections = string::split(a_value, "|"); - const auto size = sections.size(); + using namespace Forms; - if (size < 2) { - logger::warn("IGNORED: LinkedItem must have a form and at least one filter name: {} = {}"sv, a_key, a_value); - return false; - } +#pragma region Parsing + bool INI::Parser::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) + { + if (a_key != "LinkedItem") { + return false; + } - auto split_IDs = distribution::split_entry(sections[1]); + const auto sections = string::split(a_value, "|"); + const auto size = sections.size(); - if (split_IDs.empty()) { - logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); - return false; - } + if (size < 2) { + logger::warn("IGNORED: LinkedItem must have a form and at least one filter name: {} = {}"sv, a_key, a_value); + return false; + } + + auto split_IDs = distribution::split_entry(sections[1]); + + if (split_IDs.empty()) { + logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); + return false; + } + + // TODO: Parse count and chance - LinkedDistribution::INI::RawLinkedItem item{}; - item.rawForm = sections[0]; - item.path = a_path; + INI::RawLinkedItem item{}; + item.rawForm = sections[0]; + item.path = a_path; - for (auto& IDs : split_IDs) { - item.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + for (auto& IDs : split_IDs) { + item.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + } } -} #pragma endregion #pragma region Lookup -void LinkedDistribution::Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems) -{ - using namespace Forms; + void Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems) + { + using namespace Forms; - // TODO: Figure out templates here. + // TODO: Figure out templates here. - for (auto& [rawForm, filterIDs, count, chance, path] : rawLinkedItems) { - try { - if (const auto form = Forms::detail::get_form(dataHandler, rawForm, path); form) { - /*auto& forms = linkedForms[form]; + for (auto& [rawForm, filterIDs, count, chance, path] : rawLinkedItems) { + try { + if (const auto form = detail::get_form(dataHandler, rawForm, path); form) { + /*auto& forms = linkedForms[form]; FormVec match{}; - if (Forms::detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false, false)) { + if (detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false, false)) { for (const auto& form : match) { if (std::holds_alternative(form)) { forms.insert(std::get(form)); } } }*/ + } + } catch (const Lookup::UnknownFormIDException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); + } catch (const Lookup::InvalidKeywordException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); + } catch (const Lookup::KeywordNotFoundException& e) { + if (e.isDynamic) { + buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); + } else { + buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); + } + } catch (const Lookup::UnknownEditorIDException& e) { + buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); + } catch (const Lookup::MalformedEditorIDException& e) { + buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); + } catch (const Lookup::InvalidFormTypeException& e) { + // Whitelisting is disabled, so this should not occur + } catch (const Lookup::UnknownPluginException& e) { + // Likewise, we don't expect plugin names in linked forms. } - } catch (const Lookup::UnknownFormIDException& e) { - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); - } catch (const Lookup::InvalidKeywordException& e) { - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); - } catch (const Lookup::KeywordNotFoundException& e) { - if (e.isDynamic) { - buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); - } else { - buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); + } + + // Remove empty linked forms + //std::erase_if(linkedForms, [](const auto& pair) { return pair.second.empty(); }); + } + + void Manager::ForEachLinkedDistributionSet(const std::set& targetForms, std::function performDistribution) + { + for (const auto form : targetForms) { + auto& linkedSpells = LinkedFormsForForm(form, spells); + auto& linkedPerks = LinkedFormsForForm(form, perks); + auto& linkedItems = LinkedFormsForForm(form, items); + auto& linkedShouts = LinkedFormsForForm(form, shouts); + auto& linkedLevSpells = LinkedFormsForForm(form, levSpells); + auto& linkedPackages = LinkedFormsForForm(form, packages); + auto& linkedOutfits = LinkedFormsForForm(form, outfits); + auto& linkedKeywords = LinkedFormsForForm(form, keywords); + auto& linkedDeathItems = LinkedFormsForForm(form, deathItems); + auto& linkedFactions = LinkedFormsForForm(form, factions); + auto& linkedSleepOutfits = LinkedFormsForForm(form, sleepOutfits); + auto& linkedSkins = LinkedFormsForForm(form, skins); + + DistributionSet linkedEntries{ + linkedSpells, + linkedPerks, + linkedItems, + linkedShouts, + linkedLevSpells, + linkedPackages, + linkedOutfits, + linkedKeywords, + linkedDeathItems, + linkedFactions, + linkedSleepOutfits, + linkedSkins + }; + + if (linkedEntries.IsEmpty()) { + continue; } - } catch (const Lookup::UnknownEditorIDException& e) { - buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); - } catch (const Lookup::MalformedEditorIDException& e) { - buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); - } catch (const Lookup::InvalidFormTypeException& e) { - // Whitelisting is disabled, so this should not occur - } catch (const Lookup::UnknownPluginException& e) { - // Likewise, we don't expect plugin names in linked forms. + + performDistribution(linkedEntries); } } - // Remove empty linked forms - //std::erase_if(linkedForms, [](const auto& pair) { return pair.second.empty(); }); -} #pragma endregion +} From 1926498ae14a00afff93d193d662c8691689805d Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 24 Mar 2024 23:50:59 +0200 Subject: [PATCH 19/53] Implemented Linked Distribution parsing. --- SPID/include/Distribute.h | 2 +- SPID/include/FormData.h | 8 +- SPID/include/LinkedDistribution.h | 94 +++++++++++++---- SPID/src/Distribute.cpp | 3 +- SPID/src/LinkedDistribution.cpp | 169 ++++++++++++++++++++---------- SPID/src/LookupConfigs.cpp | 2 +- SPID/src/LookupForms.cpp | 42 ++++++++ 7 files changed, 236 insertions(+), 84 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 6bd64ed..61d39e3 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -27,7 +27,7 @@ namespace Distribute auto result = a_formData.filters.PassedFilters(a_npcData); if (result != Filter::Result::kPass) { - if (result == Filter::Result::kFailRNG && hasLevelFilters) { + if (hasLevelFilters && result == Filter::Result::kFailRNG) { pcLevelMultManager->InsertRejectedEntry(a_input, distributedFormID, index); } return false; diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 38d0959..8b352d4 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -118,7 +118,7 @@ namespace Forms } template - std::variant get_form_or_mod(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) + std::variant get_form_or_mod(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) { Form* form = nullptr; const RE::TESFile* mod = nullptr; @@ -235,7 +235,7 @@ namespace Forms return form; } - inline const RE::TESFile* get_file(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path) + inline const RE::TESFile* get_file(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path) { auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path); @@ -247,7 +247,7 @@ namespace Forms } template - Form* get_form(RE::TESDataHandler* dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) + Form* get_form(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) { auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path, whitelistedOnly); @@ -258,7 +258,7 @@ namespace Forms return nullptr; } - inline bool formID_to_form(RE::TESDataHandler* a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const std::string& a_path, bool a_all = false, bool whitelistedOnly = true) + inline bool formID_to_form(RE::TESDataHandler* const a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const std::string& a_path, bool a_all = false, bool whitelistedOnly = true) { if (a_rawFormVec.empty()) { return true; diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 6abb0ce..930d2fb 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -7,7 +7,6 @@ namespace LinkedDistribution { namespace INI { - struct RawLinkedItem { FormOrEditorID rawForm{}; @@ -25,20 +24,36 @@ namespace LinkedDistribution inline LinkedItemsVec linkedItems{}; - namespace Parser - { - bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); - } + bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); } using namespace Forms; - template - using LinkedForms = std::unordered_map>; + class Manager; - class Manager : public ISingleton + template + struct LinkedForms { + friend Manager; // allow Manager to later modify forms directly. + + using Map = std::unordered_map>; + + LinkedForms(RECORD::TYPE type) : + type(type) + {} + + RECORD::TYPE GetType() const { return type; } + const Map& GetForms() const { return forms; } + + private: + RECORD::TYPE type; + Map forms{}; + void Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path); + }; + + class Manager : public ISingleton + { public: /// /// Does a forms lookup similar to what Filters do. @@ -47,8 +62,7 @@ namespace LinkedDistribution /// /// A DataHandler that will perform the actual lookup. /// A raw linked item entries that should be processed. - void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems); - + void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems = INI::linkedItems); /// /// Calculates DistributionSet for each linked form and calls a callback for each of them. @@ -58,18 +72,15 @@ namespace LinkedDistribution /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. void ForEachLinkedDistributionSet(const std::set& linkedForms, std::function callback); - private: + /// + /// Iterates over each type of LinkedForms and calls a callback with each of them. + /// + template + void ForEachLinkedForms(Func&& func, const Args&&... args); + private: template - DataVec& LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const - { - if (auto it = linkedForms.find(form); it != linkedForms.end()) { - return it->second; - } else { - static DataVec empty{}; - return empty; - } - } + DataVec& LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const; LinkedForms spells{ RECORD::kSpell }; LinkedForms perks{ RECORD::kPerk }; @@ -79,9 +90,48 @@ namespace LinkedDistribution LinkedForms packages{ RECORD::kPackage }; LinkedForms outfits{ RECORD::kOutfit }; LinkedForms keywords{ RECORD::kKeyword }; - LinkedForms deathItems{ RECORD::kDeathItem }; LinkedForms factions{ RECORD::kFaction }; - LinkedForms sleepOutfits{ RECORD::kSleepOutfit }; LinkedForms skins{ RECORD::kSkin }; }; + +#pragma region Implementation + template + DataVec& Manager::LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const + { + if (auto it = linkedForms.forms.find(form); it != linkedForms.forms.end()) { + return it->second; + } else { + static DataVec empty{}; + return empty; + } + } + + template + void Manager::ForEachLinkedForms(Func&& func, const Args&&... args) + { + func(keywords, std::forward(args)...); + func(spells, std::forward(args)...); + func(levSpells, std::forward(args)...); + func(perks, std::forward(args)...); + func(shouts, std::forward(args)...); + func(items, std::forward(args)...); + func(outfits, std::forward(args)...); + func(factions, std::forward(args)...); + func(packages, std::forward(args)...); + func(skins, std::forward(args)...); + } + + template + void LinkedForms::Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path) + { + for (const auto& linkedForm : linkedForms) { + if (std::holds_alternative(linkedForm)) { + auto& distributableForms = forms[std::get(linkedForm)]; + // Note that we don't use Data.index here, as these linked items doesn't have any leveled filters + // and as such do not to track their index. + distributableForms.emplace_back(0, form, count, FilterData({}, {}, {}, {}, chance), path); + } + } + } +#pragma endregion } diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 9db01e3..a288487 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -188,7 +188,7 @@ namespace Distribute void DistributeLinkedEntries(NPCData& npcData, const PCLevelMult::Input& input, const std::set& forms) { LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(forms, [&](Forms::DistributionSet& set) { - detail::distribute(npcData, input, set, nullptr); + detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? }); } @@ -198,6 +198,7 @@ namespace Distribute return; } + // TODO: Figure out how to distribute only death items perhaps? Forms::DistributionSet entries{ Forms::spells.GetForms(a_input.onlyPlayerLevelEntries), Forms::perks.GetForms(a_input.onlyPlayerLevelEntries), diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 6ac90eb..a1580a3 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -7,83 +7,144 @@ namespace LinkedDistribution using namespace Forms; #pragma region Parsing - bool INI::Parser::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) + namespace INI { - if (a_key != "LinkedItem") { - return false; - } + enum Sections : std::uint8_t + { + kForm = 0, + kLinkedForms, + kCount, + kChance, + + // Minimum required sections + kRequired = kLinkedForms + }; + + bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) + { + if (a_key != "LinkedItem") { + return false; + } - const auto sections = string::split(a_value, "|"); - const auto size = sections.size(); + const auto sections = string::split(a_value, "|"); + const auto size = sections.size(); - if (size < 2) { - logger::warn("IGNORED: LinkedItem must have a form and at least one filter name: {} = {}"sv, a_key, a_value); - return false; - } + if (size <= kRequired) { + logger::warn("IGNORED: LinkedItem must have a form and at least one Form Filter: {} = {}"sv, a_key, a_value); + return false; + } - auto split_IDs = distribution::split_entry(sections[1]); + auto split_IDs = distribution::split_entry(sections[kLinkedForms]); - if (split_IDs.empty()) { - logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); - return false; - } + if (split_IDs.empty()) { + logger::warn("IGNORED: LinkedItem must have at least one Form Filter : {} = {}"sv, a_key, a_value); + return false; + } + + INI::RawLinkedItem item{}; + item.rawForm = distribution::get_record(sections[kForm]); + item.path = a_path; - // TODO: Parse count and chance + for (auto& IDs : split_IDs) { + item.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + } + + if (kCount < size) { + if (const auto& str = sections[kCount]; distribution::is_valid_entry(str)) { + if (auto countPair = string::split(str, "-"); countPair.size() > 1) { + auto minCount = string::to_num(countPair[0]); + auto maxCount = string::to_num(countPair[1]); - INI::RawLinkedItem item{}; - item.rawForm = sections[0]; - item.path = a_path; + item.count = RandomCount(minCount, maxCount); + } else { + auto count = string::to_num(str); - for (auto& IDs : split_IDs) { - item.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + item.count = RandomCount(count, count); // create the exact match range. + } + } + } + + if (kChance < size) { + if (const auto& str = sections[kChance]; distribution::is_valid_entry(str)) { + item.chance = string::to_num(str); + } + } + + linkedItems.push_back(item); + + return true; } } #pragma endregion #pragma region Lookup + void Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems) { - using namespace Forms; - - // TODO: Figure out templates here. + using namespace Forms::Lookup; - for (auto& [rawForm, filterIDs, count, chance, path] : rawLinkedItems) { + for (auto& [formOrEditorID, linkedIDs, count, chance, path] : rawLinkedItems) { try { - if (const auto form = detail::get_form(dataHandler, rawForm, path); form) { - /*auto& forms = linkedForms[form]; + auto form = detail::get_form(dataHandler, formOrEditorID, path); FormVec match{}; - - if (detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false, false)) { - for (const auto& form : match) { - if (std::holds_alternative(form)) { - forms.insert(std::get(form)); - } - } - }*/ + if (!detail::formID_to_form(dataHandler, linkedIDs.MATCH, match, path, false, false)) { + continue; + } + // Add to appropriate list. + if (const auto keyword = form->As(); keyword) { + keywords.Link(keyword, match, count, chance, path); + } else if (const auto spell = form->As(); spell) { + spells.Link(spell, match, count, chance, path); + } else if (const auto perk = form->As(); perk) { + perks.Link(perk, match, count, chance, path); + } else if (const auto shout = form->As(); shout) { + shouts.Link(shout, match, count, chance, path); + } else if (const auto item = form->As(); item) { + items.Link(item, match, count, chance, path); + } else if (const auto outfit = form->As(); outfit) { + outfits.Link(outfit, match, count, chance, path); + } else if (const auto faction = form->As(); faction) { + factions.Link(faction, match, count, chance, path); + } else if (const auto skin = form->As(); skin) { + skins.Link(skin, match, count, chance, path); + } else if (const auto package = form->As(); package) { + auto type = package->GetFormType(); + if (type == RE::FormType::Package || type == RE::FormType::FormList) + packages.Link(package, match, count, chance, path); } - } catch (const Lookup::UnknownFormIDException& e) { - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); - } catch (const Lookup::InvalidKeywordException& e) { - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); - } catch (const Lookup::KeywordNotFoundException& e) { + } catch (const UnknownFormIDException& e) { + buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); + } catch (const UnknownPluginException& e) { + buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - mod cannot be found", e.path, e.modName); + } catch (const InvalidKeywordException& e) { + buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); + } catch (const KeywordNotFoundException& e) { if (e.isDynamic) { - buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); + buffered_logger::critical("\t\t[{}] LinkedItem {} FAIL - couldn't create keyword", e.path, e.editorID); } else { - buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); + buffered_logger::critical("\t\t[{}] LinkedItem {} FAIL - couldn't get existing keyword", e.path, e.editorID); } - } catch (const Lookup::UnknownEditorIDException& e) { - buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); - } catch (const Lookup::MalformedEditorIDException& e) { - buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); - } catch (const Lookup::InvalidFormTypeException& e) { - // Whitelisting is disabled, so this should not occur - } catch (const Lookup::UnknownPluginException& e) { - // Likewise, we don't expect plugin names in linked forms. + } catch (const UnknownEditorIDException& e) { + buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - editorID doesn't exist", e.path, e.editorID); + } catch (const MalformedEditorIDException& e) { + buffered_logger::error("\t\t[{}] LinkedItem (\"\") SKIP - malformed editorID", e.path); + } catch (const InvalidFormTypeException& e) { + std::visit(overload{ + [&](const FormModPair& formMod) { + auto& [formID, modName] = formMod; + buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - unsupported form type ({})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.formType)); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - unsupported form type ({})", e.path, editorID, RE::FormTypeToString(e.formType)); + } }, + e.formOrEditorID); } } // Remove empty linked forms - //std::erase_if(linkedForms, [](const auto& pair) { return pair.second.empty(); }); + ForEachLinkedForms([&](LinkedForms& forms) { + std::erase_if(forms.forms, [](const auto& pair) { return pair.second.empty(); }); + }); } void Manager::ForEachLinkedDistributionSet(const std::set& targetForms, std::function performDistribution) @@ -97,9 +158,7 @@ namespace LinkedDistribution auto& linkedPackages = LinkedFormsForForm(form, packages); auto& linkedOutfits = LinkedFormsForForm(form, outfits); auto& linkedKeywords = LinkedFormsForForm(form, keywords); - auto& linkedDeathItems = LinkedFormsForForm(form, deathItems); auto& linkedFactions = LinkedFormsForForm(form, factions); - auto& linkedSleepOutfits = LinkedFormsForForm(form, sleepOutfits); auto& linkedSkins = LinkedFormsForForm(form, skins); DistributionSet linkedEntries{ @@ -111,9 +170,9 @@ namespace LinkedDistribution linkedPackages, linkedOutfits, linkedKeywords, - linkedDeathItems, + DistributionSet::empty(), // deathItems can't be linked at the moment (only makes sense on death) linkedFactions, - linkedSleepOutfits, + DistributionSet::empty(), // sleeping outfits are not supported for now due to lack of support in config's syntax. linkedSkins }; diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index f4b6ef1..0c7f572 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -322,7 +322,7 @@ namespace INI continue; } - if (LinkedDistribution::INI::Parser::TryParse(key.pItem, entry, truncatedPath)) { + if (LinkedDistribution::INI::TryParse(key.pItem, entry, truncatedPath)) { continue; } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index a56d673..8cf578f 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -1,5 +1,6 @@ #include "LookupForms.h" #include "ExclusiveGroups.h" +#include "LinkedDistribution.h" #include "FormData.h" #include "KeywordDependencies.h" @@ -77,6 +78,44 @@ void LogExclusiveGroupsLookup() } } +void LookupLinkedItems(RE::TESDataHandler* const dataHandler) +{ + LinkedDistribution::Manager::GetSingleton()->LookupLinkedItems(dataHandler); +} + +void LogLinkedItemsLookup() +{ + using namespace LinkedDistribution; + + logger::info("{:*^50}", "LINKED ITEMS"); + + Manager::GetSingleton()->ForEachLinkedForms([](const LinkedForms& linkedForms) { + if (linkedForms.GetForms().empty()) { + return; + } + const auto& recordName = RECORD::add[linkedForms.GetType()]; + logger::info("Linked {}s: ", recordName); + + for (const auto& [form, linkedItems] : linkedForms.GetForms()) { + logger::info("\t{}", describe(form)); + + const auto lastItemIndex = linkedItems.size() - 1; + for (int i = 0; i < lastItemIndex; ++i) { + const auto& linkedItem = linkedItems[i]; + logger::info("\t├─── {}", describe(linkedItem.form)); + } + const auto& lastLinkedItem = linkedItems[lastItemIndex]; + logger::info("\t└─── {}", describe(lastLinkedItem.form)); + } + }); + + // Clear INI once lookup is done + LinkedDistribution::INI::linkedItems.clear(); + + // Clear logger's buffer to free some memory :) + buffered_logger::clear(); +} + bool Lookup::LookupForms() { if (const auto dataHandler = RE::TESDataHandler::GetSingleton(); dataHandler) { @@ -93,6 +132,9 @@ bool Lookup::LookupForms() logger::info("Lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); } + LookupLinkedItems(dataHandler); + LogLinkedItemsLookup(); + LookupExclusiveGroups(dataHandler); LogExclusiveGroupsLookup(); From a9840f0d8cab99584106856dd6bf19daf79a3c23 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Mon, 25 Mar 2024 00:51:33 +0200 Subject: [PATCH 20/53] Reversed linked items tree that is logged after lookup. --- SPID/include/LinkedDistribution.h | 15 ++++++----- SPID/src/LinkedDistribution.cpp | 44 +++++++++++++++++++++++++++++++ SPID/src/LookupForms.cpp | 30 +-------------------- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 930d2fb..c9827e5 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -64,6 +64,8 @@ namespace LinkedDistribution /// A raw linked item entries that should be processed. void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems = INI::linkedItems); + void LogLinkedItemsLookup(); + /// /// Calculates DistributionSet for each linked form and calls a callback for each of them. /// @@ -72,12 +74,6 @@ namespace LinkedDistribution /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. void ForEachLinkedDistributionSet(const std::set& linkedForms, std::function callback); - /// - /// Iterates over each type of LinkedForms and calls a callback with each of them. - /// - template - void ForEachLinkedForms(Func&& func, const Args&&... args); - private: template DataVec& LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const; @@ -92,6 +88,13 @@ namespace LinkedDistribution LinkedForms keywords{ RECORD::kKeyword }; LinkedForms factions{ RECORD::kFaction }; LinkedForms skins{ RECORD::kSkin }; + + /// + /// Iterates over each type of LinkedForms and calls a callback with each of them. + /// + template + void ForEachLinkedForms(Func&& func, const Args&&... args); + }; #pragma region Implementation diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index a1580a3..8c4799c 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -145,6 +145,50 @@ namespace LinkedDistribution ForEachLinkedForms([&](LinkedForms& forms) { std::erase_if(forms.forms, [](const auto& pair) { return pair.second.empty(); }); }); + + // Clear INI once lookup is done + rawLinkedItems.clear(); + + // Clear logger's buffer to free some memory :) + buffered_logger::clear(); + } + + void Manager::LogLinkedItemsLookup() + { + logger::info("{:*^50}", "LINKED ITEMS"); + + ForEachLinkedForms([](const LinkedForms& linkedForms) { + if (linkedForms.GetForms().empty()) { + return; + } + + std::unordered_map> map{}; + + // Iterate through the original map + for (const auto& pair : linkedForms.GetForms()) { + const auto key = pair.first; + const DataVec& values = pair.second; + + for (const auto& value : values) { + map[value.form].push_back(key); + } + } + + const auto& recordName = RECORD::add[linkedForms.GetType()]; + logger::info("Linked {}s: ", recordName); + + for (const auto& [form, linkedItems] : map) { + logger::info("\t{}", describe(form)); + + const auto lastItemIndex = linkedItems.size() - 1; + for (int i = 0; i < lastItemIndex; ++i) { + const auto& linkedItem = linkedItems[i]; + logger::info("\t├─── {}", describe(linkedItem)); + } + const auto& lastLinkedItem = linkedItems[lastItemIndex]; + logger::info("\t└─── {}", describe(lastLinkedItem)); + } + }); } void Manager::ForEachLinkedDistributionSet(const std::set& targetForms, std::function performDistribution) diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 8cf578f..aa6720e 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -85,35 +85,7 @@ void LookupLinkedItems(RE::TESDataHandler* const dataHandler) void LogLinkedItemsLookup() { - using namespace LinkedDistribution; - - logger::info("{:*^50}", "LINKED ITEMS"); - - Manager::GetSingleton()->ForEachLinkedForms([](const LinkedForms& linkedForms) { - if (linkedForms.GetForms().empty()) { - return; - } - const auto& recordName = RECORD::add[linkedForms.GetType()]; - logger::info("Linked {}s: ", recordName); - - for (const auto& [form, linkedItems] : linkedForms.GetForms()) { - logger::info("\t{}", describe(form)); - - const auto lastItemIndex = linkedItems.size() - 1; - for (int i = 0; i < lastItemIndex; ++i) { - const auto& linkedItem = linkedItems[i]; - logger::info("\t├─── {}", describe(linkedItem.form)); - } - const auto& lastLinkedItem = linkedItems[lastItemIndex]; - logger::info("\t└─── {}", describe(lastLinkedItem.form)); - } - }); - - // Clear INI once lookup is done - LinkedDistribution::INI::linkedItems.clear(); - - // Clear logger's buffer to free some memory :) - buffered_logger::clear(); + LinkedDistribution::Manager::GetSingleton()->LogLinkedItemsLookup(); } bool Lookup::LookupForms() From 52ac89c2e2dc17378bccf70e0ffa4bdc0452b10d Mon Sep 17 00:00:00 2001 From: adya Date: Sun, 24 Mar 2024 22:54:14 +0000 Subject: [PATCH 21/53] maintenance --- SPID/include/FormData.h | 4 ++-- SPID/include/LinkedDistribution.h | 5 ++--- SPID/src/Distribute.cpp | 36 ++++++++++++++++--------------- SPID/src/LinkedDistribution.cpp | 10 ++++----- SPID/src/LookupForms.cpp | 2 +- 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 8b352d4..cc649f2 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -324,7 +324,7 @@ namespace Forms /// A set of distributable forms that should be processed. /// /// DistributionSet is used to conveniently pack all distributable forms into one structure. - /// Note that all entries store references so they are not owned by this structure. + /// Note that all entries store references so they are not owned by this structure. /// If you want to omit certain type of entries, you can use static empty() method to get a reference to an empty container. /// struct DistributionSet @@ -344,7 +344,7 @@ namespace Forms bool IsEmpty() const; - template + template static DataVec& empty() { static DataVec empty{}; diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index c9827e5..ee56f73 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -34,7 +34,7 @@ namespace LinkedDistribution template struct LinkedForms { - friend Manager; // allow Manager to later modify forms directly. + friend Manager; // allow Manager to later modify forms directly. using Map = std::unordered_map>; @@ -47,7 +47,7 @@ namespace LinkedDistribution private: RECORD::TYPE type; - Map forms{}; + Map forms{}; void Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path); }; @@ -94,7 +94,6 @@ namespace LinkedDistribution /// template void ForEachLinkedForms(Func&& func, const Args&&... args); - }; #pragma region Implementation diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index a288487..9c40e55 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -188,7 +188,7 @@ namespace Distribute void DistributeLinkedEntries(NPCData& npcData, const PCLevelMult::Input& input, const std::set& forms) { LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(forms, [&](Forms::DistributionSet& set) { - detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? + detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? }); } @@ -202,7 +202,7 @@ namespace Distribute Forms::DistributionSet entries{ Forms::spells.GetForms(a_input.onlyPlayerLevelEntries), Forms::perks.GetForms(a_input.onlyPlayerLevelEntries), - Forms::DistributionSet::empty(), // items are processed separately + Forms::DistributionSet::empty(), // items are processed separately Forms::shouts.GetForms(a_input.onlyPlayerLevelEntries), Forms::levSpells.GetForms(a_input.onlyPlayerLevelEntries), Forms::packages.GetForms(a_input.onlyPlayerLevelEntries), @@ -235,25 +235,27 @@ namespace Distribute std::set distributedForms{}; - for_each_form(a_npcData, Forms::items.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](std::map& a_objects, const bool a_hasLvlItem) { - if (npc->AddObjectsToContainer(a_objects, npc)) { - if (a_hasLvlItem) { - detail::init_leveled_items(actor); + for_each_form( + a_npcData, Forms::items.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](std::map& a_objects, const bool a_hasLvlItem) { + if (npc->AddObjectsToContainer(a_objects, npc)) { + if (a_hasLvlItem) { + detail::init_leveled_items(actor); + } + return true; } - return true; - } - return false; + return false; }, &distributedForms); - for_each_form(a_npcData, Forms::outfits.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](auto* a_outfit) { - if (detail::can_equip_outfit(npc, a_outfit)) { - actor->RemoveOutfitItems(npc->defaultOutfit); - npc->defaultOutfit = a_outfit; - npc->AddKeyword(processedOutfit); - return true; - } - return false; + for_each_form( + a_npcData, Forms::outfits.GetForms(a_input.onlyPlayerLevelEntries), a_input, [&](auto* a_outfit) { + if (detail::can_equip_outfit(npc, a_outfit)) { + actor->RemoveOutfitItems(npc->defaultOutfit); + npc->defaultOutfit = a_outfit; + npc->AddKeyword(processedOutfit); + return true; + } + return false; }, &distributedForms); diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 8c4799c..a27cc9e 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -40,7 +40,7 @@ namespace LinkedDistribution logger::warn("IGNORED: LinkedItem must have at least one Form Filter : {} = {}"sv, a_key, a_value); return false; } - + INI::RawLinkedItem item{}; item.rawForm = distribution::get_record(sections[kForm]); item.path = a_path; @@ -162,10 +162,10 @@ namespace LinkedDistribution return; } - std::unordered_map> map{}; + std::unordered_map> map{}; // Iterate through the original map - for (const auto& pair : linkedForms.GetForms()) { + for (const auto& pair : linkedForms.GetForms()) { const auto key = pair.first; const DataVec& values = pair.second; @@ -214,9 +214,9 @@ namespace LinkedDistribution linkedPackages, linkedOutfits, linkedKeywords, - DistributionSet::empty(), // deathItems can't be linked at the moment (only makes sense on death) + DistributionSet::empty(), // deathItems can't be linked at the moment (only makes sense on death) linkedFactions, - DistributionSet::empty(), // sleeping outfits are not supported for now due to lack of support in config's syntax. + DistributionSet::empty(), // sleeping outfits are not supported for now due to lack of support in config's syntax. linkedSkins }; diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index aa6720e..c15ea1e 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -1,8 +1,8 @@ #include "LookupForms.h" #include "ExclusiveGroups.h" -#include "LinkedDistribution.h" #include "FormData.h" #include "KeywordDependencies.h" +#include "LinkedDistribution.h" bool LookupDistributables(RE::TESDataHandler* const dataHandler) { From fabda7ff5cd908720d6f85e501480e91c66effc5 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Mon, 25 Mar 2024 21:48:55 +0200 Subject: [PATCH 22/53] Updated CommonLibSSE to latest. --- extern/CommonLibSSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/CommonLibSSE b/extern/CommonLibSSE index 37c738c..26015a0 160000 --- a/extern/CommonLibSSE +++ b/extern/CommonLibSSE @@ -1 +1 @@ -Subproject commit 37c738cfd4485628e5fa503c9833e6113e4d6abc +Subproject commit 26015a042947ef3787eac754d559e9653c664a2c From 88e2ebabc2ee0685454abd899fa3cecc5a2a0497 Mon Sep 17 00:00:00 2001 From: adya Date: Mon, 25 Mar 2024 19:49:42 +0000 Subject: [PATCH 23/53] maintenance --- SPID/include/Distribute.h | 9 +++------ SPID/src/Distribute.cpp | 16 ++++++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index a0dd952..9c2c2ed 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -151,9 +151,8 @@ namespace Distribute for (auto& formData : forms) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { // TODO: Safe guard getting RandomCount and if for any reason there is a PackageIndex, default it to count = 1 - auto count = std::get(formData.idxOrCount).GetRandom(); - if (auto leveledItem = formData.form->As()) - { + auto count = std::get(formData.idxOrCount).GetRandom(); + if (auto leveledItem = formData.form->As()) { auto level = a_npcData.GetLevel(); RE::BSScrapArray calcedObjects{}; @@ -161,9 +160,7 @@ namespace Distribute for (auto& calcObj : calcedObjects) { collectedForms[static_cast(calcObj.form)] += calcObj.count; } - } - else - { + } else { collectedForms[formData.form] += count; } ++formData.npcCount; diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 8a9f337..944fe0d 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -121,15 +121,15 @@ namespace Distribute return false; }, accumulatedForms); - + for_each_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - if (npc->defaultOutfit != a_outfit && !npc->HasKeyword(processedOutfit)) { - npc->AddKeyword(processedOutfit); - npc->defaultOutfit = a_outfit; - return true; - } - return false; + if (npc->defaultOutfit != a_outfit && !npc->HasKeyword(processedOutfit)) { + npc->AddKeyword(processedOutfit); + npc->defaultOutfit = a_outfit; + return true; + } + return false; }, accumulatedForms); @@ -145,7 +145,7 @@ namespace Distribute for_each_form( npcData, forms.items, input, [&](std::map& a_objects) { - return npc->AddObjectsToContainer(a_objects, npc); + return npc->AddObjectsToContainer(a_objects, npc); }, accumulatedForms); From ca2a1d2ed8225f316d04f4138c277d7867fa329c Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Mon, 25 Mar 2024 23:32:24 +0200 Subject: [PATCH 24/53] Added a new generic record type "Form". --- SPID/include/LookupConfigs.h | 38 ++++++++++++++++++++++++++++++--- SPID/src/DistributeManager.cpp | 2 +- SPID/src/LinkedDistribution.cpp | 2 +- SPID/src/LookupForms.cpp | 4 ++-- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 07e9181..5f29a62 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -4,7 +4,12 @@ namespace RECORD { enum TYPE { - kSpell = 0, + /// + /// A generic form type that requries type inference. + /// + kForm = 0, + + kSpell, kPerk, kItem, kShout, @@ -20,8 +25,35 @@ namespace RECORD kTotal }; - inline constexpr std::array add{ "Spell"sv, "Perk"sv, "Item"sv, "Shout"sv, "LevSpell"sv, "Package"sv, "Outfit"sv, "Keyword"sv, "DeathItem"sv, "Faction"sv, "SleepOutfit"sv, "Skin"sv }; - inline constexpr std::array remove{ "-Spell"sv, "-Perk"sv, "-Item"sv, "-Shout"sv, "-LevSpell"sv, "-Package"sv, "-Outfit"sv, "-Keyword"sv, "-DeathItem"sv, "-Faction"sv, "-SleepOutfit"sv, "-Skin"sv }; + namespace detail + { + inline static constexpr std::array add{ + "Form"sv, + "Spell"sv, + "Perk"sv, + "Item"sv, + "Shout"sv, + "LevSpell"sv, + "Package"sv, + "Outfit"sv, + "Keyword"sv, + "DeathItem"sv, + "Faction"sv, + "SleepOutfit"sv, + "Skin"sv + }; + } + + inline constexpr std::string_view GetTypeName(const TYPE aType) + { + return detail::add.at(aType); + } + + inline constexpr TYPE GetType(const std::string& aType) + { + using namespace detail; + return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); + } } namespace INI diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index bc8dad2..9e72066 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -130,7 +130,7 @@ namespace Distribute ForEachDistributable([&](Distributables& a_distributable) { if (a_distributable && a_distributable.GetType() != RECORD::kDeathItem) { - logger::info("{}", RECORD::add[a_distributable.GetType()]); + logger::info("{}", RECORD::GetTypeName(a_distributable.GetType())); auto& forms = a_distributable.GetForms(); diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index a27cc9e..ecb0b18 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -174,7 +174,7 @@ namespace LinkedDistribution } } - const auto& recordName = RECORD::add[linkedForms.GetType()]; + const auto& recordName = RECORD::GetTypeName(linkedForms.GetType()); logger::info("Linked {}s: ", recordName); for (const auto& [form, linkedItems] : map) { diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index c15ea1e..1090303 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -11,7 +11,7 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) bool valid = false; ForEachDistributable([&](Distributables& a_distributable) { - const auto& recordName = RECORD::add[a_distributable.GetType()]; + const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); a_distributable.LookupForms(dataHandler, recordName, INI::configs[recordName]); if constexpr (std::is_same_v) { @@ -34,7 +34,7 @@ void LogDistributablesLookup() logger::info("{:*^50}", "PROCESSING"); ForEachDistributable([](const Distributables& a_distributable) { - const auto& recordName = RECORD::add[a_distributable.GetType()]; + const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); const auto all = INI::configs[recordName].size(); const auto added = a_distributable.GetSize(); From e6d0286797ef8d2eb8783de3171e731bdc2802ce Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 26 Mar 2024 00:29:05 +0200 Subject: [PATCH 25/53] Added a new exception to detect when hinted Form type doesn't match the actual one. --- SPID/include/FormData.h | 63 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 780725e..b284a23 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -31,6 +31,25 @@ namespace Forms {} }; + /// + /// An exception thrown when actual form's type does not match the form type excplicilty defined in the config. + /// E.g. Spell = 0x12345, but the 0x12345 form is actually a Perk. + /// + struct MismatchingFormTypeException : std::exception + { + const RE::FormType expectedFformType; + const RE::FormType actualFormType; + const FormOrEditorID formOrEditorID; + const std::string path; + + MismatchingFormTypeException(RE::FormType expectedFformType, RE::FormType actualFormType, const FormOrEditorID& formOrEditorID, const std::string& path) : + expectedFformType(expectedFformType), + actualFormType(actualFormType), + formOrEditorID(formOrEditorID), + path(path) + {} + }; + struct InvalidKeywordException : std::exception { const RE::FormID formID; @@ -68,6 +87,9 @@ namespace Forms {} }; + /// + /// An exception thrown when actual form's type is not in the whitelist. + /// struct InvalidFormTypeException : std::exception { const RE::FormType formType; @@ -182,15 +204,22 @@ namespace Forms // Either 0x1235 or 0x1235~MyPlugin.esp if (formID) { + RE::TESForm* anyForm; if (modName) { - form = as_form(dataHandler->LookupForm(*formID, *modName)); + anyForm = dataHandler->LookupForm(*formID, *modName); } else { - form = as_form(RE::TESForm::LookupByID(*formID)); + anyForm = RE::TESForm::LookupByID(*formID); } - if (!form) { + + if (!anyForm) { throw UnknownFormIDException(*formID, path, modName); } + form = as_form(anyForm); + if (!form) { + throw MismatchingFormTypeException(anyForm->GetFormType(), Form::FORMTYPE, FormModPair{*formID, modName}, path); + } + if constexpr (std::is_same_v) { if (string::is_empty(form->GetFormEditorID())) { // Keywords with empty EditorIDs cause game to crash. @@ -207,8 +236,12 @@ namespace Forms if constexpr (std::is_same_v) { form = find_or_create_keyword(editorID); } else { - form = as_form(RE::TESForm::LookupByEditorID(editorID)); - if (!form) { + if (const auto anyForm = RE::TESForm::LookupByEditorID(editorID); anyForm) { + form = as_form(anyForm); + if (!form) { + throw MismatchingFormTypeException(anyForm->GetFormType(), Form::FORMTYPE, editorID, path); + } + } else { if constexpr (std::is_same_v) { form = find_or_create_keyword(editorID); } else { @@ -285,6 +318,16 @@ namespace Forms buffered_logger::error("\t\t[{}] Filter ({}) SKIP - editorID doesn't exist", e.path, e.editorID); } catch (const MalformedEditorIDException& e) { buffered_logger::error("\t\t[{}] Filter (\"\") SKIP - malformed editorID", e.path); + } catch (const Lookup::MismatchingFormTypeException& e) { + std::visit(overload{ + [&](const FormModPair& formMod) { + auto& [formID, modName] = formMod; + buffered_logger::error("\t\t[{}] Filter[0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] Filter ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + } }, + e.formOrEditorID); } catch (const InvalidFormTypeException& e) { std::visit(overload{ [&](const FormModPair& formMod) { @@ -512,6 +555,16 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); } catch (const Lookup::MalformedEditorIDException& e) { buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); + } catch (const Lookup::MismatchingFormTypeException& e) { + std::visit(overload{ + [&](const FormModPair& formMod) { + auto& [formID, modName] = formMod; + buffered_logger::error("\t\t[{}] [0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + } }, + e.formOrEditorID); } catch (const Lookup::InvalidFormTypeException& e) { // Whitelisting is disabled, so this should not occur } catch (const Lookup::UnknownPluginException& e) { From a122403da634998c02ab15574aa66204ae7ec94a Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 26 Mar 2024 01:11:09 +0200 Subject: [PATCH 26/53] Added support for all LinkedForm types and type inferring. When LinkedForm is used SPID will pick up correct distributable type based on actual Form's type. --- SPID/include/LinkedDistribution.h | 113 +++++++++++++++++++++++++----- SPID/include/LookupConfigs.h | 6 ++ SPID/src/LinkedDistribution.cpp | 97 +++++++++++-------------- SPID/src/LookupForms.cpp | 12 ++-- 4 files changed, 149 insertions(+), 79 deletions(-) diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index ee56f73..ec474db 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -7,12 +7,12 @@ namespace LinkedDistribution { namespace INI { - struct RawLinkedItem + struct RawLinkedForm { - FormOrEditorID rawForm{}; + FormOrEditorID formOrEditorID{}; - /// Raw filters in RawLinkedItem only use MATCH, there is no meaning for ALL or NOT, so they are ignored. - Filters rawFormFilters{}; + /// Raw filters in RawLinkedForm only use MATCH, there is no meaning for ALL or NOT, so they are ignored. + Filters formIDs{}; RandomCount count{ 1, 1 }; Chance chance{ 100 }; @@ -20,34 +20,52 @@ namespace LinkedDistribution std::string path{}; }; - using LinkedItemsVec = std::vector; + using LinkedFormsVec = std::vector; + using LinkedFormsConfig = std::unordered_map; - inline LinkedItemsVec linkedItems{}; + inline LinkedFormsConfig linkedForms{}; + /// + /// Checks whether given entry is a linked form and attempts to parse it. + /// + /// true if given entry was a linked form. Note that returned value doesn't represent whether or parsing was successful. bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); } - using namespace Forms; class Manager; + + template + struct LinkedForms; + + namespace detail + { + template + Form* LookupLinkedForm(RE::TESDataHandler* const dataHandler, INI::RawLinkedForm& rawForm); + } + + using namespace Forms; template struct LinkedForms { friend Manager; // allow Manager to later modify forms directly. + friend Form* detail::LookupLinkedForm(RE::TESDataHandler* const, INI::RawLinkedForm&); - using Map = std::unordered_map>; + using FormsMap = std::unordered_map>; LinkedForms(RECORD::TYPE type) : type(type) {} RECORD::TYPE GetType() const { return type; } - const Map& GetForms() const { return forms; } + const FormsMap& GetForms() const { return forms; } + + void LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms); private: RECORD::TYPE type; - Map forms{}; + FormsMap forms{}; void Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path); }; @@ -58,13 +76,13 @@ namespace LinkedDistribution /// /// Does a forms lookup similar to what Filters do. /// - /// As a result this method configures Manager with discovered valid linked items. + /// As a result this method configures Manager with discovered valid linked forms. /// /// A DataHandler that will perform the actual lookup. - /// A raw linked item entries that should be processed. - void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems = INI::linkedItems); + /// A raw linked form entries that should be processed. + void LookupLinkedForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsConfig& rawLinkedForms = INI::linkedForms); - void LogLinkedItemsLookup(); + void LogLinkedFormsLookup(); /// /// Calculates DistributionSet for each linked form and calls a callback for each of them. @@ -93,10 +111,58 @@ namespace LinkedDistribution /// Iterates over each type of LinkedForms and calls a callback with each of them. /// template - void ForEachLinkedForms(Func&& func, const Args&&... args); + void ForEachLinkedForms(Func&& func, Args&&... args); }; #pragma region Implementation + + template + Form* detail::LookupLinkedForm(RE::TESDataHandler* const dataHandler, INI::RawLinkedForm& rawForm) + { + using namespace Forms::Lookup; + + try { + return Forms::detail::get_form(dataHandler, rawForm.formOrEditorID, rawForm.path); + } catch (const UnknownFormIDException& e) { + buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); + } catch (const UnknownPluginException& e) { + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - mod cannot be found", e.path, e.modName); + } catch (const InvalidKeywordException& e) { + buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); + } catch (const KeywordNotFoundException& e) { + if (e.isDynamic) { + buffered_logger::critical("\t\t[{}] LinkedForm {} FAIL - couldn't create keyword", e.path, e.editorID); + } else { + buffered_logger::critical("\t\t[{}] LinkedForm {} FAIL - couldn't get existing keyword", e.path, e.editorID); + } + } catch (const UnknownEditorIDException& e) { + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - editorID doesn't exist", e.path, e.editorID); + } catch (const MalformedEditorIDException& e) { + buffered_logger::error("\t\t[{}] LinkedForm (\"\") SKIP - malformed editorID", e.path); + } catch (const MismatchingFormTypeException& e) { + std::visit(overload{ + [&](const FormModPair& formMod) { + auto& [formID, modName] = formMod; + buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + } }, + e.formOrEditorID); + } catch (const InvalidFormTypeException& e) { + std::visit(overload{ + [&](const FormModPair& formMod) { + auto& [formID, modName] = formMod; + buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - unsupported form type ({})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.formType)); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - unsupported form type ({})", e.path, editorID, RE::FormTypeToString(e.formType)); + } }, + e.formOrEditorID); + } + return nullptr; + } + template DataVec& Manager::LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const { @@ -109,7 +175,7 @@ namespace LinkedDistribution } template - void Manager::ForEachLinkedForms(Func&& func, const Args&&... args) + void Manager::ForEachLinkedForms(Func&& func, Args&&... args) { func(keywords, std::forward(args)...); func(spells, std::forward(args)...); @@ -123,13 +189,26 @@ namespace LinkedDistribution func(skins, std::forward(args)...); } + template + void LinkedForms::LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms) + { + for (auto& rawForm : rawLinkedForms) { + auto form = detail::LookupLinkedForm(dataHandler, rawForm); + auto& [formID, parentFormIDs, count, chance, path] = rawForm; + FormVec parentForms{}; + if (Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { + Link(form, parentForms, count, chance, path); + } + } + } + template void LinkedForms::Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path) { for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { auto& distributableForms = forms[std::get(linkedForm)]; - // Note that we don't use Data.index here, as these linked items doesn't have any leveled filters + // Note that we don't use Data.index here, as these linked forms don't have any leveled filters // and as such do not to track their index. distributableForms.emplace_back(0, form, count, FilterData({}, {}, {}, {}, chance), path); } diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 5f29a62..5ce8604 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -54,6 +54,12 @@ namespace RECORD using namespace detail; return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); } + + inline constexpr TYPE GetType(const std::string_view& aType) + { + using namespace detail; + return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); + } } namespace INI diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index ecb0b18..1b83478 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -1,5 +1,6 @@ #include "LinkedDistribution.h" #include "FormData.h" +#include "LookupConfigs.h" namespace LinkedDistribution { @@ -22,31 +23,38 @@ namespace LinkedDistribution bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) { - if (a_key != "LinkedItem") { + if (!a_key.starts_with("Linked"sv)) { return false; } + std::string_view rawType = a_key.substr(6); + auto type = RECORD::GetType(rawType); + if (type == RECORD::kTotal) { + logger::warn("IGNORED: Invalid Linked Form type: {}"sv, a_key); + return true; + } + const auto sections = string::split(a_value, "|"); const auto size = sections.size(); if (size <= kRequired) { logger::warn("IGNORED: LinkedItem must have a form and at least one Form Filter: {} = {}"sv, a_key, a_value); - return false; + return true; } auto split_IDs = distribution::split_entry(sections[kLinkedForms]); if (split_IDs.empty()) { logger::warn("IGNORED: LinkedItem must have at least one Form Filter : {} = {}"sv, a_key, a_value); - return false; + return true; } - INI::RawLinkedItem item{}; - item.rawForm = distribution::get_record(sections[kForm]); + INI::RawLinkedForm item{}; + item.formOrEditorID = distribution::get_record(sections[kForm]); item.path = a_path; for (auto& IDs : split_IDs) { - item.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + item.formIDs.MATCH.push_back(distribution::get_record(IDs)); } if (kCount < size) { @@ -70,7 +78,7 @@ namespace LinkedDistribution } } - linkedItems.push_back(item); + linkedForms[type].push_back(item); return true; } @@ -79,65 +87,42 @@ namespace LinkedDistribution #pragma region Lookup - void Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems) + void Manager::LookupLinkedForms(RE::TESDataHandler* dataHandler, INI::LinkedFormsConfig& rawLinkedForms) { - using namespace Forms::Lookup; + ForEachLinkedForms([&](LinkedForms& forms) { + forms.LookupForms(dataHandler, rawLinkedForms[forms.GetType()]); + }); - for (auto& [formOrEditorID, linkedIDs, count, chance, path] : rawLinkedItems) { - try { - auto form = detail::get_form(dataHandler, formOrEditorID, path); - FormVec match{}; - if (!detail::formID_to_form(dataHandler, linkedIDs.MATCH, match, path, false, false)) { + auto& genericForms = rawLinkedForms[RECORD::kForm]; + for (auto& rawForm : genericForms) { + if (auto form = detail::LookupLinkedForm(dataHandler, rawForm); form) { + auto& [formID, parentFormIDs, count, chance, path] = rawForm; + FormVec parentForms{}; + if (!Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { continue; } // Add to appropriate list. if (const auto keyword = form->As(); keyword) { - keywords.Link(keyword, match, count, chance, path); + keywords.Link(keyword, parentForms, count, chance, path); } else if (const auto spell = form->As(); spell) { - spells.Link(spell, match, count, chance, path); + spells.Link(spell, parentForms, count, chance, path); } else if (const auto perk = form->As(); perk) { - perks.Link(perk, match, count, chance, path); + perks.Link(perk, parentForms, count, chance, path); } else if (const auto shout = form->As(); shout) { - shouts.Link(shout, match, count, chance, path); + shouts.Link(shout, parentForms, count, chance, path); } else if (const auto item = form->As(); item) { - items.Link(item, match, count, chance, path); + items.Link(item, parentForms, count, chance, path); } else if (const auto outfit = form->As(); outfit) { - outfits.Link(outfit, match, count, chance, path); + outfits.Link(outfit, parentForms, count, chance, path); } else if (const auto faction = form->As(); faction) { - factions.Link(faction, match, count, chance, path); + factions.Link(faction, parentForms, count, chance, path); } else if (const auto skin = form->As(); skin) { - skins.Link(skin, match, count, chance, path); + skins.Link(skin, parentForms, count, chance, path); } else if (const auto package = form->As(); package) { auto type = package->GetFormType(); if (type == RE::FormType::Package || type == RE::FormType::FormList) - packages.Link(package, match, count, chance, path); - } - } catch (const UnknownFormIDException& e) { - buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); - } catch (const UnknownPluginException& e) { - buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - mod cannot be found", e.path, e.modName); - } catch (const InvalidKeywordException& e) { - buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); - } catch (const KeywordNotFoundException& e) { - if (e.isDynamic) { - buffered_logger::critical("\t\t[{}] LinkedItem {} FAIL - couldn't create keyword", e.path, e.editorID); - } else { - buffered_logger::critical("\t\t[{}] LinkedItem {} FAIL - couldn't get existing keyword", e.path, e.editorID); + packages.Link(package, parentForms, count, chance, path); } - } catch (const UnknownEditorIDException& e) { - buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - editorID doesn't exist", e.path, e.editorID); - } catch (const MalformedEditorIDException& e) { - buffered_logger::error("\t\t[{}] LinkedItem (\"\") SKIP - malformed editorID", e.path); - } catch (const InvalidFormTypeException& e) { - std::visit(overload{ - [&](const FormModPair& formMod) { - auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - unsupported form type ({})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.formType)); - }, - [&](std::string editorID) { - buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - unsupported form type ({})", e.path, editorID, RE::FormTypeToString(e.formType)); - } }, - e.formOrEditorID); } } @@ -147,17 +132,17 @@ namespace LinkedDistribution }); // Clear INI once lookup is done - rawLinkedItems.clear(); + rawLinkedForms.clear(); // Clear logger's buffer to free some memory :) buffered_logger::clear(); } - void Manager::LogLinkedItemsLookup() + void Manager::LogLinkedFormsLookup() { logger::info("{:*^50}", "LINKED ITEMS"); - ForEachLinkedForms([](const LinkedForms& linkedForms) { + ForEachLinkedForms([](LinkedForms& linkedForms) { if (linkedForms.GetForms().empty()) { return; } @@ -177,15 +162,15 @@ namespace LinkedDistribution const auto& recordName = RECORD::GetTypeName(linkedForms.GetType()); logger::info("Linked {}s: ", recordName); - for (const auto& [form, linkedItems] : map) { + for (const auto& [form, linkedForms] : map) { logger::info("\t{}", describe(form)); - const auto lastItemIndex = linkedItems.size() - 1; + const auto lastItemIndex = linkedForms.size() - 1; for (int i = 0; i < lastItemIndex; ++i) { - const auto& linkedItem = linkedItems[i]; + const auto& linkedItem = linkedForms[i]; logger::info("\t├─── {}", describe(linkedItem)); } - const auto& lastLinkedItem = linkedItems[lastItemIndex]; + const auto& lastLinkedItem = linkedForms[lastItemIndex]; logger::info("\t└─── {}", describe(lastLinkedItem)); } }); diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 1090303..663a1d0 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -78,14 +78,14 @@ void LogExclusiveGroupsLookup() } } -void LookupLinkedItems(RE::TESDataHandler* const dataHandler) +void LookupLinkedForms(RE::TESDataHandler* const dataHandler) { - LinkedDistribution::Manager::GetSingleton()->LookupLinkedItems(dataHandler); + LinkedDistribution::Manager::GetSingleton()->LookupLinkedForms(dataHandler); } -void LogLinkedItemsLookup() +void LogLinkedFormsLookup() { - LinkedDistribution::Manager::GetSingleton()->LogLinkedItemsLookup(); + LinkedDistribution::Manager::GetSingleton()->LogLinkedFormsLookup(); } bool Lookup::LookupForms() @@ -104,8 +104,8 @@ bool Lookup::LookupForms() logger::info("Lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); } - LookupLinkedItems(dataHandler); - LogLinkedItemsLookup(); + LookupLinkedForms(dataHandler); + LogLinkedFormsLookup(); LookupExclusiveGroups(dataHandler); LogExclusiveGroupsLookup(); From 51178fae04e97e341a07226fa836290276205587 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 26 Mar 2024 01:49:01 +0200 Subject: [PATCH 27/53] Fixed indices for packages when inferring form type. --- SPID/include/LinkedDistribution.h | 12 +++-- SPID/src/LinkedDistribution.cpp | 74 ++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index ec474db..a3d0609 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -14,7 +14,7 @@ namespace LinkedDistribution /// Raw filters in RawLinkedForm only use MATCH, there is no meaning for ALL or NOT, so they are ignored. Filters formIDs{}; - RandomCount count{ 1, 1 }; + IndexOrCount idxOrCount { RandomCount(1, 1) }; Chance chance{ 100 }; std::string path{}; @@ -67,7 +67,7 @@ namespace LinkedDistribution RECORD::TYPE type; FormsMap forms{}; - void Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path); + void Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const Chance& chance, const std::string& path); }; class Manager : public ISingleton @@ -99,10 +99,12 @@ namespace LinkedDistribution LinkedForms spells{ RECORD::kSpell }; LinkedForms perks{ RECORD::kPerk }; LinkedForms items{ RECORD::kItem }; + LinkedForms deathItems{ RECORD::kDeathItem }; LinkedForms shouts{ RECORD::kShout }; LinkedForms levSpells{ RECORD::kLevSpell }; LinkedForms packages{ RECORD::kPackage }; LinkedForms outfits{ RECORD::kOutfit }; + LinkedForms sleepingOutfits{ RECORD::kSleepOutfit }; LinkedForms keywords{ RECORD::kKeyword }; LinkedForms factions{ RECORD::kFaction }; LinkedForms skins{ RECORD::kSkin }; @@ -183,7 +185,9 @@ namespace LinkedDistribution func(perks, std::forward(args)...); func(shouts, std::forward(args)...); func(items, std::forward(args)...); + func(deathItems, std::forward(args)...); func(outfits, std::forward(args)...); + func(sleepingOutfits, std::forward(args)...); func(factions, std::forward(args)...); func(packages, std::forward(args)...); func(skins, std::forward(args)...); @@ -203,14 +207,14 @@ namespace LinkedDistribution } template - void LinkedForms::Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path) + void LinkedForms::Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const Chance& chance, const std::string& path) { for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { auto& distributableForms = forms[std::get(linkedForm)]; // Note that we don't use Data.index here, as these linked forms don't have any leveled filters // and as such do not to track their index. - distributableForms.emplace_back(0, form, count, FilterData({}, {}, {}, {}, chance), path); + distributableForms.emplace_back(0, form, idxOrCount, FilterData({}, {}, {}, {}, chance), path); } } } diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 1b83478..24e7f97 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -14,7 +14,7 @@ namespace LinkedDistribution { kForm = 0, kLinkedForms, - kCount, + kIdxOrCount, kChance, // Minimum required sections @@ -26,11 +26,11 @@ namespace LinkedDistribution if (!a_key.starts_with("Linked"sv)) { return false; } - - std::string_view rawType = a_key.substr(6); + + std::string rawType = a_key.substr(6); auto type = RECORD::GetType(rawType); if (type == RECORD::kTotal) { - logger::warn("IGNORED: Invalid Linked Form type: {}"sv, a_key); + logger::warn("IGNORED: Invalid Linked Form type: {}"sv, rawType); return true; } @@ -57,17 +57,27 @@ namespace LinkedDistribution item.formIDs.MATCH.push_back(distribution::get_record(IDs)); } - if (kCount < size) { - if (const auto& str = sections[kCount]; distribution::is_valid_entry(str)) { - if (auto countPair = string::split(str, "-"); countPair.size() > 1) { - auto minCount = string::to_num(countPair[0]); - auto maxCount = string::to_num(countPair[1]); - - item.count = RandomCount(minCount, maxCount); - } else { - auto count = string::to_num(str); + if (type == RECORD::kPackage) { // reuse item count for package stack index + item.idxOrCount = 0; + } - item.count = RandomCount(count, count); // create the exact match range. + if (kIdxOrCount < size) { + if (type == RECORD::kPackage) { + if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { + item.idxOrCount = string::to_num(str); + } + } else { + if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { + if (auto countPair = string::split(str, "-"); countPair.size() > 1) { + auto minCount = string::to_num(countPair[0]); + auto maxCount = string::to_num(countPair[1]); + + item.idxOrCount = RandomCount(minCount, maxCount); + } else { + auto count = string::to_num(str); + + item.idxOrCount = RandomCount(count, count); // create the exact match range. + } } } } @@ -96,32 +106,44 @@ namespace LinkedDistribution auto& genericForms = rawLinkedForms[RECORD::kForm]; for (auto& rawForm : genericForms) { if (auto form = detail::LookupLinkedForm(dataHandler, rawForm); form) { - auto& [formID, parentFormIDs, count, chance, path] = rawForm; + auto& [formID, parentFormIDs, idxOrCount, chance, path] = rawForm; FormVec parentForms{}; if (!Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { continue; } - // Add to appropriate list. + // Add to appropriate list. (Note that type inferring doesn't recognize SleepingOutfit or DeathItems) if (const auto keyword = form->As(); keyword) { - keywords.Link(keyword, parentForms, count, chance, path); + keywords.Link(keyword, parentForms, idxOrCount, chance, path); } else if (const auto spell = form->As(); spell) { - spells.Link(spell, parentForms, count, chance, path); + spells.Link(spell, parentForms, idxOrCount, chance, path); } else if (const auto perk = form->As(); perk) { - perks.Link(perk, parentForms, count, chance, path); + perks.Link(perk, parentForms, idxOrCount, chance, path); } else if (const auto shout = form->As(); shout) { - shouts.Link(shout, parentForms, count, chance, path); + shouts.Link(shout, parentForms, idxOrCount, chance, path); } else if (const auto item = form->As(); item) { - items.Link(item, parentForms, count, chance, path); + items.Link(item, parentForms, idxOrCount, chance, path); } else if (const auto outfit = form->As(); outfit) { - outfits.Link(outfit, parentForms, count, chance, path); + outfits.Link(outfit, parentForms, idxOrCount, chance, path); } else if (const auto faction = form->As(); faction) { - factions.Link(faction, parentForms, count, chance, path); + factions.Link(faction, parentForms, idxOrCount, chance, path); } else if (const auto skin = form->As(); skin) { - skins.Link(skin, parentForms, count, chance, path); + skins.Link(skin, parentForms, idxOrCount, chance, path); } else if (const auto package = form->As(); package) { auto type = package->GetFormType(); - if (type == RE::FormType::Package || type == RE::FormType::FormList) - packages.Link(package, parentForms, count, chance, path); + if (type == RE::FormType::Package || type == RE::FormType::FormList) { + // During type inferring we'll default to RandomCount, so we need to properly convert it to Index if it's a package. + Index packageIndex = 1; + if (std::holds_alternative(idxOrCount)) { + auto& count = std::get(idxOrCount); + if (!count.IsExact()) { + logger::warn("Inferred Form is a package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", count.min); + } + packageIndex = count.min; + } else { + packageIndex = std::get(idxOrCount); + } + packages.Link(package, parentForms, packageIndex, chance, path); + } } } } From de12de15884af9d553f36cb03a3e6d414a7f6665 Mon Sep 17 00:00:00 2001 From: adya Date: Mon, 25 Mar 2024 23:49:19 +0000 Subject: [PATCH 28/53] maintenance --- SPID/include/FormData.h | 2 +- SPID/include/LinkedDistribution.h | 11 +++++------ SPID/include/LookupConfigs.h | 4 ++-- SPID/src/LinkedDistribution.cpp | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index b284a23..17e660d 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -217,7 +217,7 @@ namespace Forms form = as_form(anyForm); if (!form) { - throw MismatchingFormTypeException(anyForm->GetFormType(), Form::FORMTYPE, FormModPair{*formID, modName}, path); + throw MismatchingFormTypeException(anyForm->GetFormType(), Form::FORMTYPE, FormModPair{ *formID, modName }, path); } if constexpr (std::is_same_v) { diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index a3d0609..5f07052 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -14,8 +14,8 @@ namespace LinkedDistribution /// Raw filters in RawLinkedForm only use MATCH, there is no meaning for ALL or NOT, so they are ignored. Filters formIDs{}; - IndexOrCount idxOrCount { RandomCount(1, 1) }; - Chance chance{ 100 }; + IndexOrCount idxOrCount{ RandomCount(1, 1) }; + Chance chance{ 100 }; std::string path{}; }; @@ -32,10 +32,9 @@ namespace LinkedDistribution bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); } - class Manager; - - template + + template struct LinkedForms; namespace detail @@ -58,7 +57,7 @@ namespace LinkedDistribution type(type) {} - RECORD::TYPE GetType() const { return type; } + RECORD::TYPE GetType() const { return type; } const FormsMap& GetForms() const { return forms; } void LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms); diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 5ce8604..0769c1b 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -44,12 +44,12 @@ namespace RECORD }; } - inline constexpr std::string_view GetTypeName(const TYPE aType) + inline constexpr std::string_view GetTypeName(const TYPE aType) { return detail::add.at(aType); } - inline constexpr TYPE GetType(const std::string& aType) + inline constexpr TYPE GetType(const std::string& aType) { using namespace detail; return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 24e7f97..1f96d1e 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -26,9 +26,9 @@ namespace LinkedDistribution if (!a_key.starts_with("Linked"sv)) { return false; } - + std::string rawType = a_key.substr(6); - auto type = RECORD::GetType(rawType); + auto type = RECORD::GetType(rawType); if (type == RECORD::kTotal) { logger::warn("IGNORED: Invalid Linked Form type: {}"sv, rawType); return true; From 30d8fa7ab424ae50d758c7e2373548fab7d9f1c5 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Wed, 27 Mar 2024 00:00:19 +0200 Subject: [PATCH 29/53] Added support for linked death items. --- SPID/include/Distribute.h | 6 ++- SPID/include/LinkedDistribution.h | 13 ++++- SPID/src/Distribute.cpp | 87 +++++++++++++++++++++---------- SPID/src/DistributeManager.cpp | 9 +--- SPID/src/LinkedDistribution.cpp | 39 ++++++++++++-- 5 files changed, 111 insertions(+), 43 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 9c2c2ed..f840a3e 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -235,6 +235,8 @@ namespace Distribute } #pragma endregion - void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input); - void Distribute(NPCData& a_npcData, bool a_onlyLeveledEntries); + void Distribute(NPCData& npcData, const PCLevelMult::Input& input); + void Distribute(NPCData& npcData, bool onlyLeveledEntries); + + void DistributeDeathItems(NPCData& npcData, const PCLevelMult::Input& input); } diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 5f07052..955bb18 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -91,6 +91,15 @@ namespace LinkedDistribution /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. void ForEachLinkedDistributionSet(const std::set& linkedForms, std::function callback); + /// + /// Calculates DistributionSet with only DeathItems for each linked form and calls a callback for each of them. + /// This method is suitable for distributing items on death. + /// + /// A set of forms for which distribution sets should be calculated. + /// This is typically distributed forms accumulated during first distribution pass. + /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. + void ForEachLinkedDeathDistributionSet(const std::set& linkedForms, std::function callback); + private: template DataVec& LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const; @@ -103,7 +112,7 @@ namespace LinkedDistribution LinkedForms levSpells{ RECORD::kLevSpell }; LinkedForms packages{ RECORD::kPackage }; LinkedForms outfits{ RECORD::kOutfit }; - LinkedForms sleepingOutfits{ RECORD::kSleepOutfit }; + LinkedForms sleepOutfits{ RECORD::kSleepOutfit }; LinkedForms keywords{ RECORD::kKeyword }; LinkedForms factions{ RECORD::kFaction }; LinkedForms skins{ RECORD::kSkin }; @@ -186,7 +195,7 @@ namespace LinkedDistribution func(items, std::forward(args)...); func(deathItems, std::forward(args)...); func(outfits, std::forward(args)...); - func(sleepingOutfits, std::forward(args)...); + func(sleepOutfits, std::forward(args)...); func(factions, std::forward(args)...); func(packages, std::forward(args)...); func(skins, std::forward(args)...); diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 944fe0d..3917935 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -158,52 +158,85 @@ namespace Distribute return false; }, accumulatedForms); - } - } - // This only does one-level linking. So that linked entries won't trigger another level of distribution. - void DistributeLinkedEntries(NPCData& npcData, const PCLevelMult::Input& input, const std::set& forms) - { - LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(forms, [&](Forms::DistributionSet& set) { - detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? - }); + for_each_form( + npcData, forms.deathItems, input, [&](auto* deathItem, IndexOrCount idxOrCount) { + auto count = std::get(idxOrCount); + + detail::add_item(npcData.GetActor(), deathItem, count.GetRandom()); + return true; + }, + accumulatedForms); + } } - void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input) + void Distribute(NPCData& npcData, const PCLevelMult::Input& input) { - if (a_input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(a_input)) { + if (input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(input)) { return; } - // TODO: Figure out how to distribute only death items perhaps? Forms::DistributionSet entries{ - Forms::spells.GetForms(a_input.onlyPlayerLevelEntries), - Forms::perks.GetForms(a_input.onlyPlayerLevelEntries), - Forms::items.GetForms(a_input.onlyPlayerLevelEntries), - Forms::shouts.GetForms(a_input.onlyPlayerLevelEntries), - Forms::levSpells.GetForms(a_input.onlyPlayerLevelEntries), - Forms::packages.GetForms(a_input.onlyPlayerLevelEntries), - Forms::outfits.GetForms(a_input.onlyPlayerLevelEntries), - Forms::keywords.GetForms(a_input.onlyPlayerLevelEntries), + Forms::spells.GetForms(input.onlyPlayerLevelEntries), + Forms::perks.GetForms(input.onlyPlayerLevelEntries), + Forms::items.GetForms(input.onlyPlayerLevelEntries), + Forms::shouts.GetForms(input.onlyPlayerLevelEntries), + Forms::levSpells.GetForms(input.onlyPlayerLevelEntries), + Forms::packages.GetForms(input.onlyPlayerLevelEntries), + Forms::outfits.GetForms(input.onlyPlayerLevelEntries), + Forms::keywords.GetForms(input.onlyPlayerLevelEntries), Forms::DistributionSet::empty(), // deathItems are only processed on... well, death. - Forms::factions.GetForms(a_input.onlyPlayerLevelEntries), - Forms::sleepOutfits.GetForms(a_input.onlyPlayerLevelEntries), - Forms::skins.GetForms(a_input.onlyPlayerLevelEntries) + Forms::factions.GetForms(input.onlyPlayerLevelEntries), + Forms::sleepOutfits.GetForms(input.onlyPlayerLevelEntries), + Forms::skins.GetForms(input.onlyPlayerLevelEntries) }; std::set distributedForms{}; - detail::distribute(a_npcData, a_input, entries, &distributedForms); + detail::distribute(npcData, input, entries, &distributedForms); // TODO: We can now log per-NPC distributed forms. if (!distributedForms.empty()) { - DistributeLinkedEntries(a_npcData, a_input, distributedForms); + // This only does one-level linking. So that linked entries won't trigger another level of distribution. + LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(distributedForms, [&](Forms::DistributionSet& set) { + detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? to log what was distributed. + }); } } - void Distribute(NPCData& a_npcData, bool a_onlyLeveledEntries) + void Distribute(NPCData& npcData, bool onlyLeveledEntries) + { + const auto input = PCLevelMult::Input{ npcData.GetActor(), npcData.GetNPC(), onlyLeveledEntries }; + Distribute(npcData, input); + } + + void DistributeDeathItems(NPCData& npcData, const PCLevelMult::Input& input) { - const auto input = PCLevelMult::Input{ a_npcData.GetActor(), a_npcData.GetNPC(), a_onlyLeveledEntries }; - Distribute(a_npcData, input); + std::set distributedForms{}; + + Forms::DistributionSet entries{ + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::deathItems.GetForms(input.onlyPlayerLevelEntries), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty(), + Forms::DistributionSet::empty() + }; + + detail::distribute(npcData, input, entries, &distributedForms); + // TODO: We can now log per-NPC distributed forms. + + if (!distributedForms.empty()) { + + LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDeathDistributionSet(distributedForms, [&](Forms::DistributionSet& set) { + detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? to log what was distributed. + }); + } } } diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 9e72066..9bb2d8e 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -189,15 +189,10 @@ namespace Distribute::Event const auto actor = a_event->actorDying->As(); const auto npc = actor ? actor->GetActorBase() : nullptr; if (actor && npc) { - const auto npcData = NPCData(actor, npc); + auto npcData = NPCData(actor, npc); const auto input = PCLevelMult::Input{ actor, npc, false }; - for_each_form(npcData, Forms::deathItems.GetForms(input.onlyPlayerLevelEntries), input, [&](auto* deathItem, IndexOrCount idxOrCount) { - auto count = std::get(idxOrCount); - - detail::add_item(actor, deathItem, count.GetRandom()); - return true; - }); + DistributeDeathItems(npcData, input); } } diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 1f96d1e..67d0171 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -111,7 +111,7 @@ namespace LinkedDistribution if (!Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { continue; } - // Add to appropriate list. (Note that type inferring doesn't recognize SleepingOutfit or DeathItems) + // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit or DeathItems) if (const auto keyword = form->As(); keyword) { keywords.Link(keyword, parentForms, idxOrCount, chance, path); } else if (const auto spell = form->As(); spell) { @@ -131,12 +131,12 @@ namespace LinkedDistribution } else if (const auto package = form->As(); package) { auto type = package->GetFormType(); if (type == RE::FormType::Package || type == RE::FormType::FormList) { - // During type inferring we'll default to RandomCount, so we need to properly convert it to Index if it's a package. + // With generic Form entries we default to RandomCount, so we need to properly convert it to Index if it turned out to be a package. Index packageIndex = 1; if (std::holds_alternative(idxOrCount)) { auto& count = std::get(idxOrCount); if (!count.IsExact()) { - logger::warn("Inferred Form is a package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", count.min); + logger::warn("Inferred Form is a Package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", count.min); } packageIndex = count.min; } else { @@ -210,6 +210,7 @@ namespace LinkedDistribution auto& linkedOutfits = LinkedFormsForForm(form, outfits); auto& linkedKeywords = LinkedFormsForForm(form, keywords); auto& linkedFactions = LinkedFormsForForm(form, factions); + auto& linkedSleepOutfits = LinkedFormsForForm(form, sleepOutfits); auto& linkedSkins = LinkedFormsForForm(form, skins); DistributionSet linkedEntries{ @@ -221,9 +222,9 @@ namespace LinkedDistribution linkedPackages, linkedOutfits, linkedKeywords, - DistributionSet::empty(), // deathItems can't be linked at the moment (only makes sense on death) + DistributionSet::empty(), // deathItems are distributed only on death :) as such, linked items are also distributed only on death. linkedFactions, - DistributionSet::empty(), // sleeping outfits are not supported for now due to lack of support in config's syntax. + linkedSleepOutfits, linkedSkins }; @@ -235,5 +236,33 @@ namespace LinkedDistribution } } + void Manager::ForEachLinkedDeathDistributionSet(const std::set& targetForms, std::function performDistribution) + { + for (const auto form : targetForms) { + auto& linkedDeathItems = LinkedFormsForForm(form, deathItems); + + DistributionSet linkedEntries{ + DistributionSet::empty(), + DistributionSet::empty(), + DistributionSet::empty(), + DistributionSet::empty(), + DistributionSet::empty(), + DistributionSet::empty(), + DistributionSet::empty(), + DistributionSet::empty(), + linkedDeathItems, + DistributionSet::empty(), + DistributionSet::empty(), + DistributionSet::empty() + }; + + if (linkedEntries.IsEmpty()) { + continue; + } + + performDistribution(linkedEntries); + } + } + #pragma endregion } From 719ef954c087d8e3a89c573aeaecbff045e4618f Mon Sep 17 00:00:00 2001 From: adya Date: Tue, 26 Mar 2024 22:00:39 +0000 Subject: [PATCH 30/53] maintenance --- SPID/src/Distribute.cpp | 3 +-- SPID/src/DistributeManager.cpp | 2 +- SPID/src/LinkedDistribution.cpp | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 3917935..366cde2 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -228,12 +228,11 @@ namespace Distribute Forms::DistributionSet::empty(), Forms::DistributionSet::empty() }; - + detail::distribute(npcData, input, entries, &distributedForms); // TODO: We can now log per-NPC distributed forms. if (!distributedForms.empty()) { - LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDeathDistributionSet(distributedForms, [&](Forms::DistributionSet& set) { detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? to log what was distributed. }); diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 9bb2d8e..3b9e91f 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -189,7 +189,7 @@ namespace Distribute::Event const auto actor = a_event->actorDying->As(); const auto npc = actor ? actor->GetActorBase() : nullptr; if (actor && npc) { - auto npcData = NPCData(actor, npc); + auto npcData = NPCData(actor, npc); const auto input = PCLevelMult::Input{ actor, npc, false }; DistributeDeathItems(npcData, input); diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 67d0171..cdbdf67 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -240,7 +240,7 @@ namespace LinkedDistribution { for (const auto form : targetForms) { auto& linkedDeathItems = LinkedFormsForForm(form, deathItems); - + DistributionSet linkedEntries{ DistributionSet::empty(), DistributionSet::empty(), From 78dbffbfc9acd7ef22070d3a12e8929adc2e5d85 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 30 Mar 2024 01:04:31 +0200 Subject: [PATCH 31/53] Added type inference to regular entries. e.g. users can now simply write "Form = ..." instead of "Spell = ..." --- SPID/include/FormData.h | 124 ++++++++++++++++++------------ SPID/include/LinkedDistribution.h | 6 +- SPID/include/LookupConfigs.h | 8 +- SPID/src/LinkedDistribution.cpp | 6 +- SPID/src/LookupConfigs.cpp | 12 +-- SPID/src/LookupForms.cpp | 63 ++++++++++++--- 6 files changed, 149 insertions(+), 70 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 17e660d..db49c5d 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -427,6 +427,8 @@ namespace Forms RECORD::TYPE type; DataVec forms{}; DataVec formsWithLevels{}; + + void LookupForm(RE::TESDataHandler* a_dataHandler, INI::Data& rawForm); }; inline Distributables spells{ RECORD::kSpell }; @@ -461,6 +463,17 @@ namespace Forms a_func(packages, std::forward(args)...); a_func(skins, std::forward(args)...); } + + /// + /// Performs lookup for a single entry. + /// It's up to the callee to add the form to the appropriate distributable container. + /// + /// Type of the form to lookup. This type can be defaulted to accept any TESForm. + /// + /// A raw form entry that needs to be looked up. + /// A callback to be called with validated data after successful lookup. + template + void LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback); } template @@ -511,6 +524,14 @@ Forms::DataVec& Forms::Distributables::GetForms(bool a_onlyLevelEntr return forms; } +template +void Forms::Distributables::LookupForm(RE::TESDataHandler* a_dataHandler, INI::Data& rawForm) +{ + Forms::LookupGenericForm(a_dataHandler, rawForm, [&](Form* form, auto& idxOrCount, auto& filters, std::string& path) { + forms.emplace_back(forms.size(), form, idxOrCount, filters, path); + }); +} + template void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec) { @@ -521,55 +542,9 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, logger::info("{}", a_type); forms.reserve(a_INIDataVec.size()); - std::uint32_t index = 0; - - for (auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] : a_INIDataVec) { - try { - if (auto form = detail::get_form(a_dataHandler, formOrEditorID, path); form) { - FormFilters filterForms{}; - bool validEntry = detail::formID_to_form(a_dataHandler, filterIDs.ALL, filterForms.ALL, path, true); - if (validEntry) { - validEntry = detail::formID_to_form(a_dataHandler, filterIDs.NOT, filterForms.NOT, path); - } - if (validEntry) { - validEntry = detail::formID_to_form(a_dataHandler, filterIDs.MATCH, filterForms.MATCH, path); - } - - if (validEntry) { - forms.emplace_back(index, form, idxOrCount, FilterData(strings, filterForms, level, traits, chance), path); - index++; - } - } - } catch (const Lookup::UnknownFormIDException& e) { - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); - } catch (const Lookup::InvalidKeywordException& e) { - buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); - } catch (const Lookup::KeywordNotFoundException& e) { - if (e.isDynamic) { - buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); - } else { - buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); - } - } catch (const Lookup::UnknownEditorIDException& e) { - buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); - } catch (const Lookup::MalformedEditorIDException& e) { - buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); - } catch (const Lookup::MismatchingFormTypeException& e) { - std::visit(overload{ - [&](const FormModPair& formMod) { - auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] [0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); - }, - [&](std::string editorID) { - buffered_logger::error("\t\t[{}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); - } }, - e.formOrEditorID); - } catch (const Lookup::InvalidFormTypeException& e) { - // Whitelisting is disabled, so this should not occur - } catch (const Lookup::UnknownPluginException& e) { - // Likewise, we don't expect plugin names in distributable forms. - } + for (auto& rawForm : a_INIDataVec) { + LookupForm(a_dataHandler, rawForm); } } @@ -595,3 +570,56 @@ void Forms::Distributables::FinishLookupForms() std::back_inserter(formsWithLevels), [](const auto& formData) { return formData.filters.HasLevelFilters(); }); } + +template +void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback) +{ + auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] = rawForm; + + try { + if (auto form = detail::get_form(dataHandler, formOrEditorID, path); form) { + FormFilters filterForms{}; + + bool validEntry = detail::formID_to_form(dataHandler, filterIDs.ALL, filterForms.ALL, path, true); + if (validEntry) { + validEntry = detail::formID_to_form(dataHandler, filterIDs.NOT, filterForms.NOT, path); + } + if (validEntry) { + validEntry = detail::formID_to_form(dataHandler, filterIDs.MATCH, filterForms.MATCH, path); + } + + if (validEntry) { + FilterData filters{ strings, filterForms, level, traits, chance }; + callback(form, idxOrCount, filters, path); + } + } + } catch (const Lookup::UnknownFormIDException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); + } catch (const Lookup::InvalidKeywordException& e) { + buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or("")); + } catch (const Lookup::KeywordNotFoundException& e) { + if (e.isDynamic) { + buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", e.path, e.editorID); + } else { + buffered_logger::critical("\t[{}] {} FAIL - couldn't get existing keyword", e.path, e.editorID); + } + } catch (const Lookup::UnknownEditorIDException& e) { + buffered_logger::error("\t[{}] ({}) FAIL - editorID doesn't exist", e.path, e.editorID); + } catch (const Lookup::MalformedEditorIDException& e) { + buffered_logger::error("\t[{}] FAIL - editorID can't be empty", e.path); + } catch (const Lookup::MismatchingFormTypeException& e) { + std::visit(overload{ + [&](const FormModPair& formMod) { + auto& [formID, modName] = formMod; + buffered_logger::error("\t\t[{}] [0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + } }, + e.formOrEditorID); + } catch (const Lookup::InvalidFormTypeException& e) { + // Whitelisting is disabled, so this should not occur + } catch (const Lookup::UnknownPluginException& e) { + // Likewise, we don't expect plugin names in distributable forms. + } +} diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 955bb18..df4d71f 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -32,6 +32,8 @@ namespace LinkedDistribution bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); } + using namespace Forms; + class Manager; template @@ -42,9 +44,7 @@ namespace LinkedDistribution template Form* LookupLinkedForm(RE::TESDataHandler* const dataHandler, INI::RawLinkedForm& rawForm); } - - using namespace Forms; - + template struct LinkedForms { diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 0769c1b..d2d7013 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -60,6 +60,12 @@ namespace RECORD using namespace detail; return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); } + + inline constexpr TYPE GetType(const char* aType) + { + using namespace detail; + return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); + } } namespace INI @@ -101,7 +107,7 @@ namespace INI using DataVec = std::vector; using ExclusiveGroupsVec = std::vector; - inline StringMap configs{}; + inline Map configs{}; /// /// A list of RawExclusiveGroups that will be processed along with configs. diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index cdbdf67..bcdf193 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -128,8 +128,8 @@ namespace LinkedDistribution factions.Link(faction, parentForms, idxOrCount, chance, path); } else if (const auto skin = form->As(); skin) { skins.Link(skin, parentForms, idxOrCount, chance, path); - } else if (const auto package = form->As(); package) { - auto type = package->GetFormType(); + } else { + auto type = form->GetFormType(); if (type == RE::FormType::Package || type == RE::FormType::FormList) { // With generic Form entries we default to RandomCount, so we need to properly convert it to Index if it turned out to be a package. Index packageIndex = 1; @@ -142,7 +142,7 @@ namespace LinkedDistribution } else { packageIndex = std::get(idxOrCount); } - packages.Link(package, parentForms, packageIndex, chance, path); + packages.Link(form, parentForms, packageIndex, chance, path); } } } diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 0c7f572..a6539ae 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -84,7 +84,7 @@ namespace INI return group; } - std::pair> parse_ini(const std::string& a_key, const std::string& a_value, const std::string& a_path) + std::pair> parse_ini(const RECORD::TYPE& typeHint, const std::string& a_value, const std::string& a_path) { Data data{}; @@ -242,12 +242,12 @@ namespace INI } //ITEMCOUNT/INDEX - if (a_key == "Package") { // reuse item count for package stack index + if (typeHint == RECORD::kPackage) { // reuse item count for package stack index data.idxOrCount = 0; } if (kIdxOrCount < size) { - if (a_key == "Package") { // If it's a package, then we only expect a single number. + if (typeHint == RECORD::kPackage) { // If it's a package, then we only expect a single number. if (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { data.idxOrCount = string::to_num(str); } @@ -326,8 +326,10 @@ namespace INI continue; } - auto [data, sanitized_str] = detail::parse_ini(key.pItem, entry, truncatedPath); - configs[key.pItem].emplace_back(data); + auto type = RECORD::GetType(key.pItem); + auto [data, sanitized_str] = detail::parse_ini(type, entry, truncatedPath); + + configs[type].emplace_back(data); if (sanitized_str) { oldFormatMap.emplace(key, std::make_pair(entry, *sanitized_str)); diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 663a1d0..2ac680a 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -4,21 +4,64 @@ #include "KeywordDependencies.h" #include "LinkedDistribution.h" + bool LookupDistributables(RE::TESDataHandler* const dataHandler) { using namespace Forms; - bool valid = false; - ForEachDistributable([&](Distributables& a_distributable) { const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); - a_distributable.LookupForms(dataHandler, recordName, INI::configs[recordName]); - if constexpr (std::is_same_v) { - Dependencies::ResolveKeywords(); - } - a_distributable.FinishLookupForms(); + a_distributable.LookupForms(dataHandler, recordName, INI::configs[a_distributable.GetType()]); + }); + auto& genericForms = INI::configs[RECORD::kForm]; + + for (auto& rawForm : genericForms) { + // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit or DeathItems) + LookupGenericForm(dataHandler, rawForm, [&](auto form, auto& idxOrCount, auto& filters, std::string& path) { + if (const auto keyword = form->As(); keyword) { + keywords.GetForms().emplace_back(keywords.GetSize(), keyword, idxOrCount, filters, path); + } else if (const auto spell = form->As(); spell) { + spells.GetForms().emplace_back(spells.GetSize(), spell, idxOrCount, filters, path); + } else if (const auto perk = form->As(); perk) { + perks.GetForms().emplace_back(perks.GetSize(), perk, idxOrCount, filters, path); + } else if (const auto shout = form->As(); shout) { + shouts.GetForms().emplace_back(shouts.GetSize(), shout, idxOrCount, filters, path); + } else if (const auto item = form->As(); item) { + items.GetForms().emplace_back(items.GetSize(), item, idxOrCount, filters, path); + } else if (const auto outfit = form->As(); outfit) { + outfits.GetForms().emplace_back(outfits.GetSize(), outfit, idxOrCount, filters, path); + } else if (const auto faction = form->As(); faction) { + factions.GetForms().emplace_back(factions.GetSize(), faction, idxOrCount, filters, path); + } else if (const auto skin = form->As(); skin) { + skins.GetForms().emplace_back(skins.GetSize(), skin, idxOrCount, filters, path); + } else { + auto type = form->GetFormType(); + if (type == RE::FormType::Package || type == RE::FormType::FormList) { + // With generic Form entries we default to RandomCount, so we need to properly convert it to Index if it turned out to be a package. + Index packageIndex = 1; + if (std::holds_alternative(idxOrCount)) { + auto& count = std::get(idxOrCount); + if (!count.IsExact()) { + logger::warn("Inferred Form is a Package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", count.min); + } + packageIndex = count.min; + } else { + packageIndex = std::get(idxOrCount); + } + packages.GetForms().emplace_back(packages.GetSize(), form, packageIndex, filters, path); + } + } + }); + } + + Dependencies::ResolveKeywords(); + + bool valid = false; + + ForEachDistributable([&](Distributables& a_distributable) { + a_distributable.FinishLookupForms(); if (a_distributable) { valid = true; } @@ -33,15 +76,15 @@ void LogDistributablesLookup() logger::info("{:*^50}", "PROCESSING"); - ForEachDistributable([](const Distributables& a_distributable) { + ForEachDistributable([](Distributables& a_distributable) { const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); - const auto all = INI::configs[recordName].size(); + const auto all = INI::configs[a_distributable.GetType()].size(); const auto added = a_distributable.GetSize(); // Only log entries that are actually present in INIs. if (all > 0) { - logger::info("Adding {}/{} {}s", added, all, recordName); + logger::info("Registered {}/{} {}s:", added, all, recordName); } }); From 31216b3930272253d0194f4523f0eddfa68618b9 Mon Sep 17 00:00:00 2001 From: adya Date: Fri, 29 Mar 2024 23:04:58 +0000 Subject: [PATCH 32/53] maintenance --- SPID/include/FormData.h | 2 +- SPID/include/LinkedDistribution.h | 2 +- SPID/src/LookupForms.cpp | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index db49c5d..558eb58 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -524,7 +524,7 @@ Forms::DataVec& Forms::Distributables::GetForms(bool a_onlyLevelEntr return forms; } -template +template void Forms::Distributables::LookupForm(RE::TESDataHandler* a_dataHandler, INI::Data& rawForm) { Forms::LookupGenericForm(a_dataHandler, rawForm, [&](Form* form, auto& idxOrCount, auto& filters, std::string& path) { diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index df4d71f..e862e62 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -44,7 +44,7 @@ namespace LinkedDistribution template Form* LookupLinkedForm(RE::TESDataHandler* const dataHandler, INI::RawLinkedForm& rawForm); } - + template struct LinkedForms { diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 2ac680a..994e7d0 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -4,7 +4,6 @@ #include "KeywordDependencies.h" #include "LinkedDistribution.h" - bool LookupDistributables(RE::TESDataHandler* const dataHandler) { using namespace Forms; @@ -53,7 +52,7 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) packages.GetForms().emplace_back(packages.GetSize(), form, packageIndex, filters, path); } } - }); + }); } Dependencies::ResolveKeywords(); From 7747d4bd5de73af77cd733b5665267b4393088a1 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 30 Mar 2024 02:20:02 +0200 Subject: [PATCH 33/53] Added manual counter for processed forms to properly count inferred forms. --- SPID/include/FormData.h | 42 +++++++++++++++++++++++++++++----------- SPID/src/LookupForms.cpp | 24 +++++++++++------------ 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 558eb58..3d46c05 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -413,12 +413,15 @@ namespace Forms std::size_t GetSize() const; std::size_t GetLeveledSize() const; + std::size_t GetLookupCount() const; + RECORD::TYPE GetType() const; DataVec& GetForms(bool a_onlyLevelEntries); DataVec& GetForms(); void LookupForms(RE::TESDataHandler* a_dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec); + void EmplaceForm(bool isValid, Form*, const IndexOrCount&, const FilterData&, const std::string& path); // Init formsWithLevels and formsNoLevels void FinishLookupForms(); @@ -428,6 +431,10 @@ namespace Forms DataVec forms{}; DataVec formsWithLevels{}; + /// Total number of entries that were matched to this Distributable, including invalid. + /// This counter is used for logging purposes. + std::size_t lookupCount{ 0 }; + void LookupForm(RE::TESDataHandler* a_dataHandler, INI::Data& rawForm); }; @@ -473,7 +480,7 @@ namespace Forms /// A raw form entry that needs to be looked up. /// A callback to be called with validated data after successful lookup. template - void LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback); + void LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback); } template @@ -503,6 +510,12 @@ std::size_t Forms::Distributables::GetLeveledSize() const return formsWithLevels.size(); } +template +std::size_t Forms::Distributables::GetLookupCount() const +{ + return lookupCount; +} + template RECORD::TYPE Forms::Distributables::GetType() const { @@ -525,15 +538,15 @@ Forms::DataVec& Forms::Distributables::GetForms(bool a_onlyLevelEntr } template -void Forms::Distributables::LookupForm(RE::TESDataHandler* a_dataHandler, INI::Data& rawForm) +void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, INI::Data& rawForm) { - Forms::LookupGenericForm(a_dataHandler, rawForm, [&](Form* form, auto& idxOrCount, auto& filters, std::string& path) { - forms.emplace_back(forms.size(), form, idxOrCount, filters, path); + Forms::LookupGenericForm(dataHandler, rawForm, [&](bool isValid, Form* form, const auto& idxOrCount, const auto& filters, const auto& path) { + EmplaceForm(isValid, form, idxOrCount, filters, path); }); } template -void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec) +void Forms::Distributables::LookupForms(RE::TESDataHandler* dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec) { if (a_INIDataVec.empty()) { return; @@ -544,10 +557,19 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, forms.reserve(a_INIDataVec.size()); for (auto& rawForm : a_INIDataVec) { - LookupForm(a_dataHandler, rawForm); + LookupForm(dataHandler, rawForm); } } +template +void Forms::Distributables::EmplaceForm(bool isValid, Form* form, const IndexOrCount& idxOrCount, const FilterData& filters, const std::string& path) +{ + if (isValid) { + forms.emplace_back(forms.size(), form, idxOrCount, filters, path); + } + lookupCount++; +} + template void Forms::Distributables::FinishLookupForms() { @@ -572,7 +594,7 @@ void Forms::Distributables::FinishLookupForms() } template -void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback) +void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback) { auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] = rawForm; @@ -588,10 +610,8 @@ void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& validEntry = detail::formID_to_form(dataHandler, filterIDs.MATCH, filterForms.MATCH, path); } - if (validEntry) { - FilterData filters{ strings, filterForms, level, traits, chance }; - callback(form, idxOrCount, filters, path); - } + FilterData filters{ strings, filterForms, level, traits, chance }; + callback(validEntry, form, idxOrCount, filters, path); } } catch (const Lookup::UnknownFormIDException& e) { buffered_logger::error("\t[{}] [0x{:X}] ({}) FAIL - formID doesn't exist", e.path, e.formID, e.modName.value_or("")); diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 994e7d0..bf64d33 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -18,23 +18,23 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) for (auto& rawForm : genericForms) { // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit or DeathItems) - LookupGenericForm(dataHandler, rawForm, [&](auto form, auto& idxOrCount, auto& filters, std::string& path) { + LookupGenericForm(dataHandler, rawForm, [&](bool isValid, auto form, const auto& idxOrCount, const auto& filters, const auto& path) { if (const auto keyword = form->As(); keyword) { - keywords.GetForms().emplace_back(keywords.GetSize(), keyword, idxOrCount, filters, path); + keywords.EmplaceForm(isValid, keyword, idxOrCount, filters, path); } else if (const auto spell = form->As(); spell) { - spells.GetForms().emplace_back(spells.GetSize(), spell, idxOrCount, filters, path); + spells.EmplaceForm(isValid, spell, idxOrCount, filters, path); } else if (const auto perk = form->As(); perk) { - perks.GetForms().emplace_back(perks.GetSize(), perk, idxOrCount, filters, path); + perks.EmplaceForm(isValid, perk, idxOrCount, filters, path); } else if (const auto shout = form->As(); shout) { - shouts.GetForms().emplace_back(shouts.GetSize(), shout, idxOrCount, filters, path); + shouts.EmplaceForm(isValid, shout, idxOrCount, filters, path); } else if (const auto item = form->As(); item) { - items.GetForms().emplace_back(items.GetSize(), item, idxOrCount, filters, path); + items.EmplaceForm(isValid, item, idxOrCount, filters, path); } else if (const auto outfit = form->As(); outfit) { - outfits.GetForms().emplace_back(outfits.GetSize(), outfit, idxOrCount, filters, path); + outfits.EmplaceForm(isValid, outfit, idxOrCount, filters, path); } else if (const auto faction = form->As(); faction) { - factions.GetForms().emplace_back(factions.GetSize(), faction, idxOrCount, filters, path); + factions.EmplaceForm(isValid, faction, idxOrCount, filters, path); } else if (const auto skin = form->As(); skin) { - skins.GetForms().emplace_back(skins.GetSize(), skin, idxOrCount, filters, path); + skins.EmplaceForm(isValid, skin, idxOrCount, filters, path); } else { auto type = form->GetFormType(); if (type == RE::FormType::Package || type == RE::FormType::FormList) { @@ -49,7 +49,7 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) } else { packageIndex = std::get(idxOrCount); } - packages.GetForms().emplace_back(packages.GetSize(), form, packageIndex, filters, path); + packages.EmplaceForm(isValid, form, packageIndex, filters, path); } } }); @@ -78,12 +78,12 @@ void LogDistributablesLookup() ForEachDistributable([](Distributables& a_distributable) { const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); - const auto all = INI::configs[a_distributable.GetType()].size(); const auto added = a_distributable.GetSize(); + const auto all = a_distributable.GetLookupCount(); // Only log entries that are actually present in INIs. if (all > 0) { - logger::info("Registered {}/{} {}s:", added, all, recordName); + logger::info("Registered {}/{} {}s", added, all, recordName); } }); From f254f84cd763e32a5a2e28fd2ea48c153b1b6ac1 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 30 Mar 2024 23:57:59 +0200 Subject: [PATCH 34/53] Added support for fractional chances. --- SPID/include/Defs.h | 18 +++++++++++++++++- SPID/include/LinkedDistribution.h | 8 ++++---- SPID/include/LookupConfigs.h | 2 +- SPID/include/LookupFilters.h | 5 +++-- SPID/src/LinkedDistribution.cpp | 2 +- SPID/src/LookupConfigs.cpp | 2 +- SPID/src/LookupFilters.cpp | 9 +++++---- cmake/ports/clib-util/portfile.cmake | 4 ++-- 8 files changed, 34 insertions(+), 16 deletions(-) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index 57c23f5..e762db1 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -97,7 +97,23 @@ using Index = std::int32_t; using Count = std::int32_t; using RandomCount = Range; using IndexOrCount = std::variant; -using Chance = std::uint32_t; + +/// +/// A chance that is represented as a decimal value between 0 and 1. +/// For example, 0.5 would be 50%. +/// +/// This one is used in a processed Data for filtering. +/// +using DecimalChance = double; + +/// +/// A chance that is represented as a percent value between 0 and 100. +/// It also can be decimal, but would describe fraction of a percent. +/// So that 0.5 would be 0.5%. +/// +/// This is used during parsing of INI files. +/// +using PercentChance = double; /// A standardized way of converting any object to string. /// diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index e862e62..537b1fd 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -14,8 +14,8 @@ namespace LinkedDistribution /// Raw filters in RawLinkedForm only use MATCH, there is no meaning for ALL or NOT, so they are ignored. Filters formIDs{}; - IndexOrCount idxOrCount{ RandomCount(1, 1) }; - Chance chance{ 100 }; + IndexOrCount idxOrCount{ RandomCount(1, 1) }; + PercentChance chance{ 100 }; std::string path{}; }; @@ -66,7 +66,7 @@ namespace LinkedDistribution RECORD::TYPE type; FormsMap forms{}; - void Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const Chance& chance, const std::string& path); + void Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const std::string& path); }; class Manager : public ISingleton @@ -215,7 +215,7 @@ namespace LinkedDistribution } template - void LinkedForms::Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const Chance& chance, const std::string& path) + void LinkedForms::Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const std::string& path) { for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index d2d7013..d4c10fe 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -91,7 +91,7 @@ namespace INI LevelFilters levelFilters{}; Traits traits{}; IndexOrCount idxOrCount{ RandomCount(1, 1) }; - Chance chance{ 100 }; + PercentChance chance{ 100 }; std::string path{}; }; diff --git a/SPID/include/LookupFilters.h b/SPID/include/LookupFilters.h index cf2b448..5da1012 100644 --- a/SPID/include/LookupFilters.h +++ b/SPID/include/LookupFilters.h @@ -16,13 +16,14 @@ namespace Filter struct Data { - Data(StringFilters a_strings, FormFilters a_formFilters, LevelFilters a_level, Traits a_traits, Chance a_chance); + // Note that chance passed to this constructor is expected to be in percent. It will be converted to a decimal chance during by the constructor. + Data(StringFilters a_strings, FormFilters a_formFilters, LevelFilters a_level, Traits a_traits, PercentChance a_chance); StringFilters strings{}; FormFilters forms{}; LevelFilters levels{}; Traits traits{}; - Chance chance{ 100 }; + DecimalChance chance{ 1 }; bool hasLeveledFilters; diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index bcdf193..b0b7886 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -84,7 +84,7 @@ namespace LinkedDistribution if (kChance < size) { if (const auto& str = sections[kChance]; distribution::is_valid_entry(str)) { - item.chance = string::to_num(str); + item.chance = string::to_num(str); } } diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index a6539ae..8d4f869 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -270,7 +270,7 @@ namespace INI //CHANCE if (kChance < size) { if (const auto& str = sections[kChance]; distribution::is_valid_entry(str)) { - data.chance = string::to_num(str); + data.chance = string::to_num(str); } } diff --git a/SPID/src/LookupFilters.cpp b/SPID/src/LookupFilters.cpp index ba60729..bdf528c 100644 --- a/SPID/src/LookupFilters.cpp +++ b/SPID/src/LookupFilters.cpp @@ -3,12 +3,12 @@ namespace Filter { - Data::Data(StringFilters a_strings, FormFilters a_formFilters, LevelFilters a_level, Traits a_traits, Chance a_chance) : + Data::Data(StringFilters a_strings, FormFilters a_formFilters, LevelFilters a_level, Traits a_traits, PercentChance a_chance) : strings(std::move(a_strings)), forms(std::move(a_formFilters)), levels(std::move(a_level)), traits(a_traits), - chance(a_chance) + chance(a_chance / 100) { hasLeveledFilters = HasLevelFiltersImpl(); } @@ -192,11 +192,12 @@ namespace Filter Result Data::PassedFilters(const NPCData& a_npcData) const { // Fail chance first to avoid running unnecessary checks - if (chance < 100) { - const auto randNum = RNG().generate(0, 100); + if (chance < 1) { + double randNum = RNG().generate(); if (randNum > chance) { return Result::kFailRNG; } + std::string res = "passed"; } if (passed_string_filters(a_npcData) == Result::kFail) { diff --git a/cmake/ports/clib-util/portfile.cmake b/cmake/ports/clib-util/portfile.cmake index 23f1474..e099051 100644 --- a/cmake/ports/clib-util/portfile.cmake +++ b/cmake/ports/clib-util/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO powerof3/CLibUtil - REF a491e2f7919211eac6d32dd2814b7908134ec1b2 - SHA512 e6b830a061d7fc99115ee7d28de0165b8c6281006df6d053342e6b5b85606a46a846ebb30f3e4598aa976c25cfe7a9dffd1c4eb72942829d1a4615d81e62cf3c + REF 88d78d94464a04e582669beac56346edbbc4a662 + SHA512 960cf62e5317356f7c0d994e49f56effb89c415377e9c865e801c5ec28b57e9ec0fd2a9fd54136cd2382addedb6745cd5cc062c46cab5cccb1f634999491c9e1 HEAD_REF master ) From e1557782b1bcb5ae9bdf417fc25f79f733a82086 Mon Sep 17 00:00:00 2001 From: adya Date: Sat, 30 Mar 2024 21:58:17 +0000 Subject: [PATCH 35/53] maintenance --- SPID/include/Defs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index e762db1..f379e20 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -101,7 +101,7 @@ using IndexOrCount = std::variant; /// /// A chance that is represented as a decimal value between 0 and 1. /// For example, 0.5 would be 50%. -/// +/// /// This one is used in a processed Data for filtering. /// using DecimalChance = double; @@ -110,7 +110,7 @@ using DecimalChance = double; /// A chance that is represented as a percent value between 0 and 100. /// It also can be decimal, but would describe fraction of a percent. /// So that 0.5 would be 0.5%. -/// +/// /// This is used during parsing of INI files. /// using PercentChance = double; From a879a82cb9b0eba2658e513aa038fc3373ee8dc6 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 00:00:53 +0200 Subject: [PATCH 36/53] =?UTF-8?q?Fixed=20small=20typo=20in=20comment=20?= =?UTF-8?q?=F0=9F=98=8A=20clean=20up.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SPID/include/LookupFilters.h | 2 +- SPID/src/LookupFilters.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/SPID/include/LookupFilters.h b/SPID/include/LookupFilters.h index 5da1012..01c8161 100644 --- a/SPID/include/LookupFilters.h +++ b/SPID/include/LookupFilters.h @@ -16,7 +16,7 @@ namespace Filter struct Data { - // Note that chance passed to this constructor is expected to be in percent. It will be converted to a decimal chance during by the constructor. + // Note that chance passed to this constructor is expected to be in percent. It will be converted to a decimal chance by the constructor. Data(StringFilters a_strings, FormFilters a_formFilters, LevelFilters a_level, Traits a_traits, PercentChance a_chance); StringFilters strings{}; diff --git a/SPID/src/LookupFilters.cpp b/SPID/src/LookupFilters.cpp index bdf528c..edc395b 100644 --- a/SPID/src/LookupFilters.cpp +++ b/SPID/src/LookupFilters.cpp @@ -193,11 +193,10 @@ namespace Filter { // Fail chance first to avoid running unnecessary checks if (chance < 1) { - double randNum = RNG().generate(); + const auto randNum = RNG().generate(); if (randNum > chance) { return Result::kFailRNG; } - std::string res = "passed"; } if (passed_string_filters(a_npcData) == Result::kFail) { From 7a3130244c97d48667ba9ec0bff4818fa1101044 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 30 Mar 2024 19:15:07 +0200 Subject: [PATCH 37/53] Improved logging of type errors during parsing and lookup. --- SPID/include/FormData.h | 21 +++++++++++---------- SPID/include/LinkedDistribution.h | 4 ++-- SPID/include/LookupConfigs.h | 21 +++++---------------- SPID/src/LinkedDistribution.cpp | 6 ++++-- SPID/src/LookupConfigs.cpp | 6 +++++- SPID/src/LookupForms.cpp | 4 +++- 6 files changed, 30 insertions(+), 32 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 3d46c05..aef1856 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -37,13 +37,13 @@ namespace Forms /// struct MismatchingFormTypeException : std::exception { - const RE::FormType expectedFformType; + const RE::FormType expectedFormType; const RE::FormType actualFormType; const FormOrEditorID formOrEditorID; const std::string path; - MismatchingFormTypeException(RE::FormType expectedFformType, RE::FormType actualFormType, const FormOrEditorID& formOrEditorID, const std::string& path) : - expectedFformType(expectedFformType), + MismatchingFormTypeException(RE::FormType expectedFormType, RE::FormType actualFormType, const FormOrEditorID& formOrEditorID, const std::string& path) : + expectedFormType(expectedFormType), actualFormType(actualFormType), formOrEditorID(formOrEditorID), path(path) @@ -216,8 +216,10 @@ namespace Forms } form = as_form(anyForm); - if (!form) { - throw MismatchingFormTypeException(anyForm->GetFormType(), Form::FORMTYPE, FormModPair{ *formID, modName }, path); + if (!form || anyForm->GetFormType() != Form::FORMTYPE) { + // Ideally, we'd want to throw separate exception for unsupported form type, + // so that attempting to distribute, for example, CELL would properly report such error. + throw MismatchingFormTypeException(Form::FORMTYPE, anyForm->GetFormType(), FormModPair{ *formID, modName }, path); } if constexpr (std::is_same_v) { @@ -232,7 +234,6 @@ namespace Forms if (editorID.empty()) { throw MalformedEditorIDException(path); } - // if constexpr (std::is_same_v) { form = find_or_create_keyword(editorID); } else { @@ -322,10 +323,10 @@ namespace Forms std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] Filter[0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] Filter[0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] Filter ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] Filter ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); } }, e.formOrEditorID); } catch (const InvalidFormTypeException& e) { @@ -631,10 +632,10 @@ void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] [0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] [0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); } }, e.formOrEditorID); } catch (const Lookup::InvalidFormTypeException& e) { diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 537b1fd..f85bf1a 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -153,10 +153,10 @@ namespace LinkedDistribution std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); } }, e.formOrEditorID); } catch (const InvalidFormTypeException& e) { diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index d4c10fe..79f994c 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -27,7 +27,7 @@ namespace RECORD namespace detail { - inline static constexpr std::array add{ + inline static constexpr std::array names{ "Form"sv, "Spell"sv, "Perk"sv, @@ -46,25 +46,14 @@ namespace RECORD inline constexpr std::string_view GetTypeName(const TYPE aType) { - return detail::add.at(aType); + return detail::names.at(aType); } - inline constexpr TYPE GetType(const std::string& aType) + template + constexpr TYPE GetType(const T& aType) { using namespace detail; - return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); - } - - inline constexpr TYPE GetType(const std::string_view& aType) - { - using namespace detail; - return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); - } - - inline constexpr TYPE GetType(const char* aType) - { - using namespace detail; - return static_cast(std::distance(add.begin(), std::find(add.begin(), add.end(), aType))); + return static_cast(std::distance(names.begin(), std::find(names.begin(), names.end(), aType))); } } diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index b0b7886..b5c849a 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -30,7 +30,7 @@ namespace LinkedDistribution std::string rawType = a_key.substr(6); auto type = RECORD::GetType(rawType); if (type == RECORD::kTotal) { - logger::warn("IGNORED: Invalid Linked Form type: {}"sv, rawType); + logger::warn("IGNORED: Unsupported Linked Form type: {}"sv, rawType); return true; } @@ -136,13 +136,15 @@ namespace LinkedDistribution if (std::holds_alternative(idxOrCount)) { auto& count = std::get(idxOrCount); if (!count.IsExact()) { - logger::warn("Inferred Form is a Package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", count.min); + logger::warn("\t[{}] Inferred Form is a Package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", path, count.min); } packageIndex = count.min; } else { packageIndex = std::get(idxOrCount); } packages.Link(form, parentForms, packageIndex, chance, path); + } else { + logger::warn("\t[{}] Unsupported Form type: {}", path, RE::FormTypeToString(type)); } } } diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 8d4f869..88cbe95 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -327,6 +327,10 @@ namespace INI } auto type = RECORD::GetType(key.pItem); + if (type == RECORD::kTotal) { + logger::warn("\t\tUnsupported Form type: {}"sv, key.pItem); + continue; + } auto [data, sanitized_str] = detail::parse_ini(type, entry, truncatedPath); configs[type].emplace_back(data); @@ -335,7 +339,7 @@ namespace INI oldFormatMap.emplace(key, std::make_pair(entry, *sanitized_str)); } } catch (...) { - logger::warn("\t\tFailed to parse entry [{} = {}]", key.pItem, entry); + logger::warn("\t\tFailed to parse entry [{} = {}]"sv, key.pItem, entry); shouldLogErrors = true; } } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index bf64d33..996e5e3 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -43,13 +43,15 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) if (std::holds_alternative(idxOrCount)) { auto& count = std::get(idxOrCount); if (!count.IsExact()) { - logger::warn("Inferred Form is a Package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", count.min); + logger::warn("\t[{}] Inferred Form is a Package, but specifies a random count instead of index. Min value ({}) of the range will be used as an index.", path, count.min); } packageIndex = count.min; } else { packageIndex = std::get(idxOrCount); } packages.EmplaceForm(isValid, form, packageIndex, filters, path); + } else { + logger::warn("\t[{}] Unsupported Form type: {}", path, RE::FormTypeToString(type)); } } }); From f54569066af64d14fdba2f529a144fed8bfe003c Mon Sep 17 00:00:00 2001 From: adya Date: Sat, 30 Mar 2024 22:07:26 +0000 Subject: [PATCH 38/53] maintenance --- SPID/include/FormData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index aef1856..5335eb1 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -217,7 +217,7 @@ namespace Forms form = as_form(anyForm); if (!form || anyForm->GetFormType() != Form::FORMTYPE) { - // Ideally, we'd want to throw separate exception for unsupported form type, + // Ideally, we'd want to throw separate exception for unsupported form type, // so that attempting to distribute, for example, CELL would properly report such error. throw MismatchingFormTypeException(Form::FORMTYPE, anyForm->GetFormType(), FormModPair{ *formID, modName }, path); } From bde29a047b4f99d88809f9613faa5e9410e65885 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 00:23:36 +0200 Subject: [PATCH 39/53] Isolated Exclusive Groups feature's code in respective files. This bring Exclusive Groups in line with Linked Forms structure, where features are isolated as much as possible to their own header/source. --- SPID/include/ExclusiveGroups.h | 25 +++++- SPID/include/LookupConfigs.h | 17 +--- SPID/src/ExclusiveGroups.cpp | 138 +++++++++++++++++++++++---------- SPID/src/LookupConfigs.cpp | 41 +--------- SPID/src/LookupForms.cpp | 17 +--- 5 files changed, 127 insertions(+), 111 deletions(-) diff --git a/SPID/include/ExclusiveGroups.h b/SPID/include/ExclusiveGroups.h index 4ea6043..7fde062 100644 --- a/SPID/include/ExclusiveGroups.h +++ b/SPID/include/ExclusiveGroups.h @@ -3,6 +3,27 @@ namespace ExclusiveGroups { + namespace INI + { + struct RawExclusiveGroup + { + std::string name{}; + + /// Raw filters in RawExclusiveGroup only use NOT and MATCH, there is no meaning for ALL, so it's ignored. + Filters formIDs{}; + std::string path{}; + }; + + using ExclusiveGroupsVec = std::vector; + + /// + /// A list of RawExclusiveGroups that will be processed along with configs. + /// + inline ExclusiveGroupsVec exclusiveGroups{}; + + bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); + } + using GroupName = std::string; using LinkedGroups = std::unordered_map>; using Groups = std::unordered_map>; @@ -17,7 +38,9 @@ namespace ExclusiveGroups /// /// A DataHandler that will perform the actual lookup. /// A raw exclusive group entries that should be processed. - void LookupExclusiveGroups(RE::TESDataHandler* const dataHandler, INI::ExclusiveGroupsVec& rawExclusiveGroups); + void LookupExclusiveGroups(RE::TESDataHandler* const dataHandler, INI::ExclusiveGroupsVec& rawExclusiveGroups = INI::exclusiveGroups); + + void LogExclusiveGroupsLookup(); /// /// Gets a set of all forms that are in the same exclusive group as the given form. diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 79f994c..322331b 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -84,24 +84,9 @@ namespace INI std::string path{}; }; - struct RawExclusiveGroup - { - std::string name{}; - - /// Raw filters in RawExclusiveGroup only use NOT and MATCH, there is no meaning for ALL, so it's ignored. - Filters rawFormFilters{}; - std::string path{}; - }; - using DataVec = std::vector; - using ExclusiveGroupsVec = std::vector; - + inline Map configs{}; - /// - /// A list of RawExclusiveGroups that will be processed along with configs. - /// - inline ExclusiveGroupsVec exclusiveGroups{}; - std::pair GetConfigs(); } diff --git a/SPID/src/ExclusiveGroups.cpp b/SPID/src/ExclusiveGroups.cpp index 607c3c0..efe6b1b 100644 --- a/SPID/src/ExclusiveGroups.cpp +++ b/SPID/src/ExclusiveGroups.cpp @@ -1,59 +1,117 @@ #include "ExclusiveGroups.h" #include "FormData.h" -void ExclusiveGroups::Manager::LookupExclusiveGroups(RE::TESDataHandler* const dataHandler, INI::ExclusiveGroupsVec& exclusiveGroups) +namespace ExclusiveGroups { - groups.clear(); - linkedGroups.clear(); - - for (auto& [name, filterIDs, path] : exclusiveGroups) { - auto& forms = groups[name]; - FormVec match{}; - FormVec formsNot{}; - - if (Forms::detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false, false) && - Forms::detail::formID_to_form(dataHandler, filterIDs.NOT, formsNot, path, false, false)) { - for (const auto& form : match) { - if (std::holds_alternative(form)) { - forms.insert(std::get(form)); - } + bool INI::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) + { + if (a_key != "ExclusiveGroup") { + return false; + } + + const auto sections = string::split(a_value, "|"); + const auto size = sections.size(); + + if (size < 2) { + logger::warn("IGNORED: ExclusiveGroup must have a name and at least one Form Filter: {} = {}"sv, a_key, a_value); + return true; + } + + auto split_IDs = distribution::split_entry(sections[1]); + + if (split_IDs.empty()) { + logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); + return true; + } + + RawExclusiveGroup group{}; + group.name = sections[0]; + group.path = a_path; + + for (auto& IDs : split_IDs) { + if (IDs.at(0) == '-') { + IDs.erase(0, 1); + group.formIDs.NOT.push_back(distribution::get_record(IDs)); + } else { + group.formIDs.MATCH.push_back(distribution::get_record(IDs)); } + } + + exclusiveGroups.emplace_back(group); + + return true; + } + + void Manager::LookupExclusiveGroups(RE::TESDataHandler* const dataHandler, INI::ExclusiveGroupsVec& exclusiveGroups) + { + groups.clear(); + linkedGroups.clear(); - for (auto& form : formsNot) { - if (std::holds_alternative(form)) { - forms.erase(std::get(form)); + for (auto& [name, filterIDs, path] : exclusiveGroups) { + auto& forms = groups[name]; + FormVec match{}; + FormVec formsNot{}; + + if (Forms::detail::formID_to_form(dataHandler, filterIDs.MATCH, match, path, false, false) && + Forms::detail::formID_to_form(dataHandler, filterIDs.NOT, formsNot, path, false, false)) { + for (const auto& form : match) { + if (std::holds_alternative(form)) { + forms.insert(std::get(form)); + } + } + + for (auto& form : formsNot) { + if (std::holds_alternative(form)) { + forms.erase(std::get(form)); + } } } } - } - // Remove empty groups - std::erase_if(groups, [](const auto& pair) { return pair.second.empty(); }); + // Remove empty groups + std::erase_if(groups, [](const auto& pair) { return pair.second.empty(); }); - for (auto& [name, forms] : groups) { - for (auto& form : forms) { - linkedGroups[form].insert(name); + for (auto& [name, forms] : groups) { + for (auto& form : forms) { + linkedGroups[form].insert(name); + } } } -} -std::unordered_set ExclusiveGroups::Manager::MutuallyExclusiveFormsForForm(RE::TESForm* form) const -{ - std::unordered_set forms{}; - if (auto it = linkedGroups.find(form); it != linkedGroups.end()) { - std::ranges::for_each(it->second, [&](const GroupName& name) { - const auto& group = groups.at(name); - forms.insert(group.begin(), group.end()); - }); + void Manager::LogExclusiveGroupsLookup() + { + if (groups.empty()) { + return; + } + + logger::info("{:*^50}", "EXCLUSIVE GROUPS"); + + for (const auto& [group, forms] : groups) { + logger::info("Adding '{}' exclusive group", group); + for (const auto& form : forms) { + logger::info(" {}", describe(form)); + } + } } - // Remove self from the list. - forms.erase(form); + std::unordered_set Manager::MutuallyExclusiveFormsForForm(RE::TESForm* form) const + { + std::unordered_set forms{}; + if (auto it = linkedGroups.find(form); it != linkedGroups.end()) { + std::ranges::for_each(it->second, [&](const GroupName& name) { + const auto& group = groups.at(name); + forms.insert(group.begin(), group.end()); + }); + } - return forms; -} + // Remove self from the list. + forms.erase(form); -const ExclusiveGroups::Groups& ExclusiveGroups::Manager::GetGroups() const -{ - return groups; + return forms; + } + + const Groups& ExclusiveGroups::Manager::GetGroups() const + { + return groups; + } } diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 88cbe95..ffd28a9 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -1,5 +1,6 @@ #include "LookupConfigs.h" #include "LinkedDistribution.h" +#include "ExclusiveGroups.h" namespace INI { @@ -47,43 +48,6 @@ namespace INI return newValue; } - std::optional parse_exclusive_group(const std::string& a_key, const std::string& a_value, const std::string& a_path) - { - if (a_key != "ExclusiveGroup") { - return std::nullopt; - } - - const auto sections = string::split(a_value, "|"); - const auto size = sections.size(); - - if (size < 2) { - logger::warn("IGNORED: ExclusiveGroup must have a name and at least one Form Filter: {} = {}"sv, a_key, a_value); - return std::nullopt; - } - - auto split_IDs = distribution::split_entry(sections[1]); - - if (split_IDs.empty()) { - logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); - return std::nullopt; - } - - RawExclusiveGroup group{}; - group.name = sections[0]; - group.path = a_path; - - for (auto& IDs : split_IDs) { - if (IDs.at(0) == '-') { - IDs.erase(0, 1); - group.rawFormFilters.NOT.push_back(distribution::get_record(IDs)); - } else { - group.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); - } - } - - return group; - } - std::pair> parse_ini(const RECORD::TYPE& typeHint, const std::string& a_value, const std::string& a_path) { Data data{}; @@ -317,8 +281,7 @@ namespace INI for (auto& [key, entry] : *values) { try { - if (const auto group = detail::parse_exclusive_group(key.pItem, entry, truncatedPath); group) { - exclusiveGroups.emplace_back(*group); + if (ExclusiveGroups::INI::TryParse(key.pItem, entry, truncatedPath)) { continue; } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 996e5e3..86bdc49 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -101,25 +101,12 @@ void LogDistributablesLookup() // instead of quering data handler for the same raw FormOrEditorID. void LookupExclusiveGroups(RE::TESDataHandler* const dataHandler) { - ExclusiveGroups::Manager::GetSingleton()->LookupExclusiveGroups(dataHandler, INI::exclusiveGroups); + ExclusiveGroups::Manager::GetSingleton()->LookupExclusiveGroups(dataHandler); } void LogExclusiveGroupsLookup() { - if (const auto manager = ExclusiveGroups::Manager::GetSingleton(); manager) { - const auto& groups = manager->GetGroups(); - - if (!groups.empty()) { - logger::info("{:*^50}", "EXCLUSIVE GROUPS"); - - for (const auto& [group, forms] : groups) { - logger::info("Adding '{}' exclusive group", group); - for (const auto& form : forms) { - logger::info(" {}", describe(form)); - } - } - } - } + ExclusiveGroups::Manager::GetSingleton()->LogExclusiveGroupsLookup(); } void LookupLinkedForms(RE::TESDataHandler* const dataHandler) From d2f96881157b09a227ce5e03c5c7f3da7b38a0c2 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 00:58:52 +0200 Subject: [PATCH 40/53] Introduced an alias for Path parameter. --- SPID/include/Defs.h | 2 ++ SPID/include/ExclusiveGroups.h | 4 +-- SPID/include/FormData.h | 54 +++++++++++++++---------------- SPID/include/LinkedDistribution.h | 8 ++--- SPID/src/ExclusiveGroups.cpp | 2 +- SPID/src/LinkedDistribution.cpp | 14 ++++---- SPID/src/LookupConfigs.cpp | 2 +- 7 files changed, 44 insertions(+), 42 deletions(-) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index f379e20..fd2d450 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -93,6 +93,8 @@ struct Traits std::optional teammate{}; }; +using Path = std::string; + using Index = std::int32_t; using Count = std::int32_t; using RandomCount = Range; diff --git a/SPID/include/ExclusiveGroups.h b/SPID/include/ExclusiveGroups.h index 7fde062..0fb37ea 100644 --- a/SPID/include/ExclusiveGroups.h +++ b/SPID/include/ExclusiveGroups.h @@ -11,7 +11,7 @@ namespace ExclusiveGroups /// Raw filters in RawExclusiveGroup only use NOT and MATCH, there is no meaning for ALL, so it's ignored. Filters formIDs{}; - std::string path{}; + Path path{}; }; using ExclusiveGroupsVec = std::vector; @@ -21,7 +21,7 @@ namespace ExclusiveGroups /// inline ExclusiveGroupsVec exclusiveGroups{}; - bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); + bool TryParse(const std::string& a_key, const std::string& a_value, const Path& a_path); } using GroupName = std::string; diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 5335eb1..749dae1 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -10,9 +10,9 @@ namespace Forms struct UnknownPluginException : std::exception { const std::string modName; - const std::string path; + const Path path; - UnknownPluginException(const std::string& modName, const std::string& path) : + UnknownPluginException(const std::string& modName, const Path& path) : modName(modName), path(path) {} @@ -22,9 +22,9 @@ namespace Forms { const RE::FormID formID; const std::optional modName; - const std::string path; + const Path path; - UnknownFormIDException(RE::FormID formID, const std::string& path, std::optional modName = std::nullopt) : + UnknownFormIDException(RE::FormID formID, const Path& path, std::optional modName = std::nullopt) : formID(formID), path(path), modName(modName) @@ -40,9 +40,9 @@ namespace Forms const RE::FormType expectedFormType; const RE::FormType actualFormType; const FormOrEditorID formOrEditorID; - const std::string path; + const Path path; - MismatchingFormTypeException(RE::FormType expectedFormType, RE::FormType actualFormType, const FormOrEditorID& formOrEditorID, const std::string& path) : + MismatchingFormTypeException(RE::FormType expectedFormType, RE::FormType actualFormType, const FormOrEditorID& formOrEditorID, const Path& path) : expectedFormType(expectedFormType), actualFormType(actualFormType), formOrEditorID(formOrEditorID), @@ -54,9 +54,9 @@ namespace Forms { const RE::FormID formID; const std::optional modName; - const std::string path; + const Path path; - InvalidKeywordException(RE::FormID formID, const std::string& path, std::optional modName = std::nullopt) : + InvalidKeywordException(RE::FormID formID, const Path& path, std::optional modName = std::nullopt) : formID(formID), modName(modName), path(path) @@ -67,9 +67,9 @@ namespace Forms { const std::string editorID; const bool isDynamic; - const std::string path; + const Path path; - KeywordNotFoundException(const std::string& editorID, bool isDynamic, const std::string& path) : + KeywordNotFoundException(const std::string& editorID, bool isDynamic, const Path& path) : editorID(editorID), isDynamic(isDynamic), path(path) @@ -79,9 +79,9 @@ namespace Forms struct UnknownEditorIDException : std::exception { const std::string editorID; - const std::string path; + const Path path; - UnknownEditorIDException(const std::string& editorID, const std::string& path) : + UnknownEditorIDException(const std::string& editorID, const Path& path) : editorID(editorID), path(path) {} @@ -94,9 +94,9 @@ namespace Forms { const RE::FormType formType; const FormOrEditorID formOrEditorID; - const std::string path; + const Path path; - InvalidFormTypeException(RE::FormType formType, const FormOrEditorID& formOrEditorID, const std::string& path) : + InvalidFormTypeException(RE::FormType formType, const FormOrEditorID& formOrEditorID, const Path& path) : formType(formType), formOrEditorID(formOrEditorID), path(path) @@ -105,9 +105,9 @@ namespace Forms struct MalformedEditorIDException : std::exception { - const std::string path; + const Path path; - MalformedEditorIDException(const std::string& path) : + MalformedEditorIDException(const Path& path) : path(path) {} }; @@ -140,7 +140,7 @@ namespace Forms } template - std::variant get_form_or_mod(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) + std::variant get_form_or_mod(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const Path& path, bool whitelistedOnly = false) { Form* form = nullptr; const RE::TESFile* mod = nullptr; @@ -269,7 +269,7 @@ namespace Forms return form; } - inline const RE::TESFile* get_file(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path) + inline const RE::TESFile* get_file(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const Path& path) { auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path); @@ -281,7 +281,7 @@ namespace Forms } template - Form* get_form(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const std::string& path, bool whitelistedOnly = false) + Form* get_form(RE::TESDataHandler* const dataHandler, const FormOrEditorID& formOrEditorID, const Path& path, bool whitelistedOnly = false) { auto formOrMod = get_form_or_mod(dataHandler, formOrEditorID, path, whitelistedOnly); @@ -292,7 +292,7 @@ namespace Forms return nullptr; } - inline bool formID_to_form(RE::TESDataHandler* const a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const std::string& a_path, bool a_all = false, bool whitelistedOnly = true) + inline bool formID_to_form(RE::TESDataHandler* const a_dataHandler, RawFormVec& a_rawFormVec, FormVec& a_formVec, const Path& a_path, bool a_all = false, bool whitelistedOnly = true) { if (a_rawFormVec.empty()) { return true; @@ -355,7 +355,7 @@ namespace Forms IndexOrCount idxOrCount{ RandomCount(1, 1) }; FilterData filters{}; - std::string path{}; + Path path{}; std::uint32_t npcCount{ 0 }; bool operator==(const Data& a_rhs) const; @@ -421,8 +421,8 @@ namespace Forms DataVec& GetForms(bool a_onlyLevelEntries); DataVec& GetForms(); - void LookupForms(RE::TESDataHandler* a_dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec); - void EmplaceForm(bool isValid, Form*, const IndexOrCount&, const FilterData&, const std::string& path); + void LookupForms(RE::TESDataHandler*, std::string_view a_type, INI::DataVec&); + void EmplaceForm(bool isValid, Form*, const IndexOrCount&, const FilterData&, const Path&); // Init formsWithLevels and formsNoLevels void FinishLookupForms(); @@ -436,7 +436,7 @@ namespace Forms /// This counter is used for logging purposes. std::size_t lookupCount{ 0 }; - void LookupForm(RE::TESDataHandler* a_dataHandler, INI::Data& rawForm); + void LookupForm(RE::TESDataHandler*, INI::Data&); }; inline Distributables spells{ RECORD::kSpell }; @@ -481,7 +481,7 @@ namespace Forms /// A raw form entry that needs to be looked up. /// A callback to be called with validated data after successful lookup. template - void LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback); + void LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback); } template @@ -563,7 +563,7 @@ void Forms::Distributables::LookupForms(RE::TESDataHandler* dataHandler, s } template -void Forms::Distributables::EmplaceForm(bool isValid, Form* form, const IndexOrCount& idxOrCount, const FilterData& filters, const std::string& path) +void Forms::Distributables::EmplaceForm(bool isValid, Form* form, const IndexOrCount& idxOrCount, const FilterData& filters, const Path& path) { if (isValid) { forms.emplace_back(forms.size(), form, idxOrCount, filters, path); @@ -595,7 +595,7 @@ void Forms::Distributables::FinishLookupForms() } template -void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback) +void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback) { auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] = rawForm; diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index f85bf1a..a5eeb18 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -17,7 +17,7 @@ namespace LinkedDistribution IndexOrCount idxOrCount{ RandomCount(1, 1) }; PercentChance chance{ 100 }; - std::string path{}; + Path path{}; }; using LinkedFormsVec = std::vector; @@ -29,7 +29,7 @@ namespace LinkedDistribution /// Checks whether given entry is a linked form and attempts to parse it. /// /// true if given entry was a linked form. Note that returned value doesn't represent whether or parsing was successful. - bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); + bool TryParse(const std::string& key, const std::string& value, const Path&); } using namespace Forms; @@ -66,7 +66,7 @@ namespace LinkedDistribution RECORD::TYPE type; FormsMap forms{}; - void Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const std::string& path); + void Link(Form*, const FormVec& linkedForms, const IndexOrCount&, const PercentChance&, const Path&); }; class Manager : public ISingleton @@ -215,7 +215,7 @@ namespace LinkedDistribution } template - void LinkedForms::Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const std::string& path) + void LinkedForms::Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const Path& path) { for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { diff --git a/SPID/src/ExclusiveGroups.cpp b/SPID/src/ExclusiveGroups.cpp index efe6b1b..277b46e 100644 --- a/SPID/src/ExclusiveGroups.cpp +++ b/SPID/src/ExclusiveGroups.cpp @@ -3,7 +3,7 @@ namespace ExclusiveGroups { - bool INI::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) + bool INI::TryParse(const std::string& a_key, const std::string& a_value, const Path& a_path) { if (a_key != "ExclusiveGroup") { return false; diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index b5c849a..00a6e38 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -21,37 +21,37 @@ namespace LinkedDistribution kRequired = kLinkedForms }; - bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) + bool TryParse(const std::string& key, const std::string& value, const Path& path) { - if (!a_key.starts_with("Linked"sv)) { + if (!key.starts_with("Linked"sv)) { return false; } - std::string rawType = a_key.substr(6); + std::string rawType = key.substr(6); auto type = RECORD::GetType(rawType); if (type == RECORD::kTotal) { logger::warn("IGNORED: Unsupported Linked Form type: {}"sv, rawType); return true; } - const auto sections = string::split(a_value, "|"); + const auto sections = string::split(value, "|"); const auto size = sections.size(); if (size <= kRequired) { - logger::warn("IGNORED: LinkedItem must have a form and at least one Form Filter: {} = {}"sv, a_key, a_value); + logger::warn("IGNORED: LinkedItem must have a form and at least one Form Filter: {} = {}"sv, key, value); return true; } auto split_IDs = distribution::split_entry(sections[kLinkedForms]); if (split_IDs.empty()) { - logger::warn("IGNORED: LinkedItem must have at least one Form Filter : {} = {}"sv, a_key, a_value); + logger::warn("IGNORED: LinkedItem must have at least one Form Filter : {} = {}"sv, key, value); return true; } INI::RawLinkedForm item{}; item.formOrEditorID = distribution::get_record(sections[kForm]); - item.path = a_path; + item.path = path; for (auto& IDs : split_IDs) { item.formIDs.MATCH.push_back(distribution::get_record(IDs)); diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index ffd28a9..48d85af 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -48,7 +48,7 @@ namespace INI return newValue; } - std::pair> parse_ini(const RECORD::TYPE& typeHint, const std::string& a_value, const std::string& a_path) + std::pair> parse_ini(const RECORD::TYPE& typeHint, const std::string& a_value, const Path& a_path) { Data data{}; From bb018e231123d21939d5af5411710dcfaf5d67f6 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 02:01:34 +0200 Subject: [PATCH 41/53] Introduced Scope for linked forms. --- SPID/include/LinkedDistribution.h | 28 ++++++++++++++++++++++---- SPID/src/LinkedDistribution.cpp | 33 ++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index a5eeb18..4c3b544 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -5,12 +5,31 @@ namespace LinkedDistribution { + + /// + /// Scope of a linked form determines which distributions can trigger linked forms. + /// + enum Scope : std::uint8_t + { + /// + /// Local scope links forms only to distributions defined in the same configuration file. + /// + kLocal = 0, + + /// + /// Global scope links forms to all distributions in all loaded configuration files. + /// + kGlobal + }; + namespace INI { struct RawLinkedForm { FormOrEditorID formOrEditorID{}; + Scope scope{ kLocal }; + /// Raw filters in RawLinkedForm only use MATCH, there is no meaning for ALL or NOT, so they are ignored. Filters formIDs{}; @@ -66,7 +85,7 @@ namespace LinkedDistribution RECORD::TYPE type; FormsMap forms{}; - void Link(Form*, const FormVec& linkedForms, const IndexOrCount&, const PercentChance&, const Path&); + void Link(Form*, Scope, const FormVec& linkedForms, const IndexOrCount&, const PercentChance&, const Path&); }; class Manager : public ISingleton @@ -206,17 +225,18 @@ namespace LinkedDistribution { for (auto& rawForm : rawLinkedForms) { auto form = detail::LookupLinkedForm(dataHandler, rawForm); - auto& [formID, parentFormIDs, count, chance, path] = rawForm; + auto& [formID, scope, parentFormIDs, count, chance, path] = rawForm; FormVec parentForms{}; if (Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { - Link(form, parentForms, count, chance, path); + Link(form, scope, parentForms, count, chance, path); } } } template - void LinkedForms::Link(Form* form, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const Path& path) + void LinkedForms::Link(Form* form, Scope scope, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const Path& path) { + // TODO: Handle scope for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { auto& distributableForms = forms[std::get(linkedForm)]; diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 00a6e38..1374aff 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -21,14 +21,24 @@ namespace LinkedDistribution kRequired = kLinkedForms }; - bool TryParse(const std::string& key, const std::string& value, const Path& path) + bool TryParse(const std::string& originalKey, const std::string& value, const Path& path) { + std::string key = originalKey; + + Scope scope = kLocal; + + if (key.starts_with("Global"sv)) { + scope = kGlobal; + key.erase(0, 6); + } + if (!key.starts_with("Linked"sv)) { return false; } std::string rawType = key.substr(6); auto type = RECORD::GetType(rawType); + if (type == RECORD::kTotal) { logger::warn("IGNORED: Unsupported Linked Form type: {}"sv, rawType); return true; @@ -51,6 +61,7 @@ namespace LinkedDistribution INI::RawLinkedForm item{}; item.formOrEditorID = distribution::get_record(sections[kForm]); + item.scope = scope; item.path = path; for (auto& IDs : split_IDs) { @@ -106,28 +117,28 @@ namespace LinkedDistribution auto& genericForms = rawLinkedForms[RECORD::kForm]; for (auto& rawForm : genericForms) { if (auto form = detail::LookupLinkedForm(dataHandler, rawForm); form) { - auto& [formID, parentFormIDs, idxOrCount, chance, path] = rawForm; + auto& [formID, scope, parentFormIDs, idxOrCount, chance, path] = rawForm; FormVec parentForms{}; if (!Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { continue; } // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit or DeathItems) if (const auto keyword = form->As(); keyword) { - keywords.Link(keyword, parentForms, idxOrCount, chance, path); + keywords.Link(keyword, scope, parentForms, idxOrCount, chance, path); } else if (const auto spell = form->As(); spell) { - spells.Link(spell, parentForms, idxOrCount, chance, path); + spells.Link(spell, scope, parentForms, idxOrCount, chance, path); } else if (const auto perk = form->As(); perk) { - perks.Link(perk, parentForms, idxOrCount, chance, path); + perks.Link(perk, scope, parentForms, idxOrCount, chance, path); } else if (const auto shout = form->As(); shout) { - shouts.Link(shout, parentForms, idxOrCount, chance, path); + shouts.Link(shout, scope, parentForms, idxOrCount, chance, path); } else if (const auto item = form->As(); item) { - items.Link(item, parentForms, idxOrCount, chance, path); + items.Link(item, scope, parentForms, idxOrCount, chance, path); } else if (const auto outfit = form->As(); outfit) { - outfits.Link(outfit, parentForms, idxOrCount, chance, path); + outfits.Link(outfit, scope, parentForms, idxOrCount, chance, path); } else if (const auto faction = form->As(); faction) { - factions.Link(faction, parentForms, idxOrCount, chance, path); + factions.Link(faction, scope, parentForms, idxOrCount, chance, path); } else if (const auto skin = form->As(); skin) { - skins.Link(skin, parentForms, idxOrCount, chance, path); + skins.Link(skin, scope, parentForms, idxOrCount, chance, path); } else { auto type = form->GetFormType(); if (type == RE::FormType::Package || type == RE::FormType::FormList) { @@ -142,7 +153,7 @@ namespace LinkedDistribution } else { packageIndex = std::get(idxOrCount); } - packages.Link(form, parentForms, packageIndex, chance, path); + packages.Link(form, scope, parentForms, packageIndex, chance, path); } else { logger::warn("\t[{}] Unsupported Form type: {}", path, RE::FormTypeToString(type)); } From b2e692c84a4a5eb59c27ef61b1c1b32a180949f0 Mon Sep 17 00:00:00 2001 From: adya Date: Sun, 31 Mar 2024 00:01:49 +0000 Subject: [PATCH 42/53] maintenance --- SPID/include/LookupConfigs.h | 2 +- SPID/src/LinkedDistribution.cpp | 2 +- SPID/src/LookupConfigs.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 322331b..7431303 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -85,7 +85,7 @@ namespace INI }; using DataVec = std::vector; - + inline Map configs{}; std::pair GetConfigs(); diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 1374aff..4771d1f 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -25,7 +25,7 @@ namespace LinkedDistribution { std::string key = originalKey; - Scope scope = kLocal; + Scope scope = kLocal; if (key.starts_with("Global"sv)) { scope = kGlobal; diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 48d85af..11d30e6 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -1,6 +1,6 @@ #include "LookupConfigs.h" -#include "LinkedDistribution.h" #include "ExclusiveGroups.h" +#include "LinkedDistribution.h" namespace INI { From f54db3af7a7815e839a2d50e73586f7a6a94bfd7 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 21:14:09 +0300 Subject: [PATCH 43/53] Implemented Local scope for linked forms. --- SPID/include/Distribute.h | 36 +++++++++++++++++++------------ SPID/include/FormData.h | 7 +++--- SPID/include/LinkedDistribution.h | 35 ++++++++++++++++-------------- SPID/src/Distribute.cpp | 6 +++--- SPID/src/LinkedDistribution.cpp | 28 +++++++++++++----------- 5 files changed, 64 insertions(+), 48 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index f840a3e..f222af3 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -70,6 +70,8 @@ namespace Distribute void add_item(RE::Actor* a_actor, RE::TESBoundObject* a_item, std::uint32_t a_itemCount); } + using namespace Forms; + #pragma region Packages, Death Items // old method (distributing one by one) // for now, only packages/death items use this @@ -79,12 +81,12 @@ namespace Distribute Forms::DataVec& forms, const PCLevelMult::Input& a_input, std::function a_callback, - std::set* accumulatedForms = nullptr) + DistributedForms* accumulatedForms = nullptr) { for (auto& formData : forms) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { if (accumulatedForms) { - accumulatedForms->insert(formData.form); + accumulatedForms->insert({ formData.form, formData.path }); } a_callback(formData.form, formData.idxOrCount); ++formData.npcCount; @@ -100,12 +102,12 @@ namespace Distribute Forms::DataVec& forms, const PCLevelMult::Input& a_input, std::function a_callback, - std::set* accumulatedForms = nullptr) + DistributedForms* accumulatedForms = nullptr) { for (auto& formData : forms) { // Vector is reversed in FinishLookupForms if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData) && a_callback(formData.form)) { if (accumulatedForms) { - accumulatedForms->insert(formData.form); + accumulatedForms->insert({ formData.form, formData.path }); } ++formData.npcCount; break; @@ -121,14 +123,14 @@ namespace Distribute const NPCData& a_npcData, Forms::Distributables& a_distributables, std::function a_callback, - std::set* accumulatedForms = nullptr) + DistributedForms* accumulatedForms = nullptr) { auto& vec = a_distributables.GetForms(false); for (auto& formData : vec) { // Vector is reversed in FinishLookupForms if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, formData) && a_callback(formData.form)) { if (accumulatedForms) { - accumulatedForms->insert(formData.form); + accumulatedForms->insert({ formData.form, formData.path }); } ++formData.npcCount; break; @@ -144,7 +146,7 @@ namespace Distribute Forms::DataVec& forms, const PCLevelMult::Input& a_input, std::function&)> a_callback, - std::set* accumulatedForms = nullptr) + DistributedForms* accumulatedForms = nullptr) { std::map collectedForms{}; @@ -159,18 +161,21 @@ namespace Distribute leveledItem->CalculateCurrentFormList(level, count, calcedObjects, 0, true); for (auto& calcObj : calcedObjects) { collectedForms[static_cast(calcObj.form)] += calcObj.count; + if (accumulatedForms) { + accumulatedForms->insert({ calcObj.form, formData.path }); + } } } else { collectedForms[formData.form] += count; + if (accumulatedForms) { + accumulatedForms->insert({ formData.form, formData.path }); + } } ++formData.npcCount; } } if (!collectedForms.empty()) { - if (accumulatedForms) { - std::ranges::copy(collectedForms | std::views::keys, std::inserter(*accumulatedForms, accumulatedForms->end())); - } a_callback(collectedForms); } } @@ -185,7 +190,7 @@ namespace Distribute Forms::DataVec& forms, const PCLevelMult::Input& a_input, std::function&)> a_callback, - std::set* accumulatedForms = nullptr) + DistributedForms* accumulatedForms = nullptr) { const auto npc = a_npcData.GetNPC(); @@ -210,6 +215,9 @@ namespace Distribute if (formData.filters.HasLevelFilters()) { collectedLeveledFormIDs.emplace(formID); } + if (accumulatedForms) { + accumulatedForms->insert({ form, formData.path }); + } ++formData.npcCount; } } else { @@ -218,15 +226,15 @@ namespace Distribute if (formData.filters.HasLevelFilters()) { collectedLeveledFormIDs.emplace(formID); } + if (accumulatedForms) { + accumulatedForms->insert({ form, formData.path }); + } ++formData.npcCount; } } } if (!collectedForms.empty()) { - if (accumulatedForms) { - accumulatedForms->insert(collectedForms.begin(), collectedForms.end()); - } a_callback(collectedForms); if (!collectedLeveledFormIDs.empty()) { PCLevelMult::Manager::GetSingleton()->InsertDistributedEntry(a_input, Form::FORMTYPE, collectedLeveledFormIDs); diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 749dae1..ffe2813 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -216,9 +216,7 @@ namespace Forms } form = as_form(anyForm); - if (!form || anyForm->GetFormType() != Form::FORMTYPE) { - // Ideally, we'd want to throw separate exception for unsupported form type, - // so that attempting to distribute, for example, CELL would properly report such error. + if (!form) { throw MismatchingFormTypeException(Form::FORMTYPE, anyForm->GetFormType(), FormModPair{ *formID, modName }, path); } @@ -364,6 +362,9 @@ namespace Forms template using DataVec = std::vector>; + using DistributedForm = std::pair; + using DistributedForms = std::set; + /// /// A set of distributable forms that should be processed. /// diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 4c3b544..e4a85a2 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -70,7 +70,7 @@ namespace LinkedDistribution friend Manager; // allow Manager to later modify forms directly. friend Form* detail::LookupLinkedForm(RE::TESDataHandler* const, INI::RawLinkedForm&); - using FormsMap = std::unordered_map>; + using FormsMap = std::unordered_map>>; LinkedForms(RECORD::TYPE type) : type(type) @@ -108,7 +108,7 @@ namespace LinkedDistribution /// A set of forms for which distribution sets should be calculated. /// This is typically distributed forms accumulated during first distribution pass. /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. - void ForEachLinkedDistributionSet(const std::set& linkedForms, std::function callback); + void ForEachLinkedDistributionSet(const DistributedForms& linkedForms, std::function callback); /// /// Calculates DistributionSet with only DeathItems for each linked form and calls a callback for each of them. @@ -117,11 +117,11 @@ namespace LinkedDistribution /// A set of forms for which distribution sets should be calculated. /// This is typically distributed forms accumulated during first distribution pass. /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. - void ForEachLinkedDeathDistributionSet(const std::set& linkedForms, std::function callback); + void ForEachLinkedDeathDistributionSet(const DistributedForms& linkedForms, std::function callback); private: template - DataVec& LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const; + DataVec& LinkedFormsForForm(const DistributedForm&, LinkedForms&) const; LinkedForms spells{ RECORD::kSpell }; LinkedForms perks{ RECORD::kPerk }; @@ -193,14 +193,15 @@ namespace LinkedDistribution } template - DataVec& Manager::LinkedFormsForForm(RE::TESForm* form, LinkedForms& linkedForms) const + DataVec& Manager::LinkedFormsForForm(const DistributedForm& form, LinkedForms& linkedForms) const { - if (auto it = linkedForms.forms.find(form); it != linkedForms.forms.end()) { - return it->second; - } else { - static DataVec empty{}; - return empty; + if (const auto formsIt = linkedForms.forms.find(form.second); formsIt != linkedForms.forms.end()) { + if (const auto linkedFormsIt = formsIt->second.find(form.first); linkedFormsIt != formsIt->second.end()) { + return linkedFormsIt->second; + } } + static DataVec empty{}; + return empty; } template @@ -224,11 +225,12 @@ namespace LinkedDistribution void LinkedForms::LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms) { for (auto& rawForm : rawLinkedForms) { - auto form = detail::LookupLinkedForm(dataHandler, rawForm); - auto& [formID, scope, parentFormIDs, count, chance, path] = rawForm; - FormVec parentForms{}; - if (Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { - Link(form, scope, parentForms, count, chance, path); + if (auto form = detail::LookupLinkedForm(dataHandler, rawForm); form) { + auto& [formID, scope, parentFormIDs, count, chance, path] = rawForm; + FormVec parentForms{}; + if (Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { + Link(form, scope, parentForms, count, chance, path); + } } } } @@ -239,7 +241,8 @@ namespace LinkedDistribution // TODO: Handle scope for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { - auto& distributableForms = forms[std::get(linkedForm)]; + auto& distributableFormsAtPath = forms[path]; + auto& distributableForms = distributableFormsAtPath[std::get(linkedForm)]; // Note that we don't use Data.index here, as these linked forms don't have any leveled filters // and as such do not to track their index. distributableForms.emplace_back(0, form, idxOrCount, FilterData({}, {}, {}, {}, chance), path); diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 366cde2..6a6c6eb 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -21,7 +21,7 @@ namespace Distribute /// Leveling information about NPC that is being processed. /// A set of forms that should be distributed to NPC. /// An optional pointer to a set that will accumulate all distributed forms. - void distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, std::set* accumulatedForms) + void distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, DistributedForms* accumulatedForms) { const auto npc = npcData.GetNPC(); @@ -191,7 +191,7 @@ namespace Distribute Forms::skins.GetForms(input.onlyPlayerLevelEntries) }; - std::set distributedForms{}; + DistributedForms distributedForms{}; detail::distribute(npcData, input, entries, &distributedForms); // TODO: We can now log per-NPC distributed forms. @@ -212,7 +212,7 @@ namespace Distribute void DistributeDeathItems(NPCData& npcData, const PCLevelMult::Input& input) { - std::set distributedForms{}; + DistributedForms distributedForms{}; Forms::DistributionSet entries{ Forms::DistributionSet::empty(), diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 4771d1f..076f5d6 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -182,15 +182,19 @@ namespace LinkedDistribution return; } - std::unordered_map> map{}; + std::unordered_map> map{}; // Iterate through the original map for (const auto& pair : linkedForms.GetForms()) { - const auto key = pair.first; - const DataVec& values = pair.second; - - for (const auto& value : values) { - map[value.form].push_back(key); + const auto& path = pair.first; + const auto& formsMap = pair.second; + + for (const auto& pair : formsMap) { + const auto key = pair.first; + const auto& values = pair.second; + for (const auto& value : values) { + map[value.form].emplace_back(key, path); + } } } @@ -203,17 +207,17 @@ namespace LinkedDistribution const auto lastItemIndex = linkedForms.size() - 1; for (int i = 0; i < lastItemIndex; ++i) { const auto& linkedItem = linkedForms[i]; - logger::info("\t├─── {}", describe(linkedItem)); + logger::info("\t├─── {} @{}", describe(linkedItem.first), linkedItem.second); } const auto& lastLinkedItem = linkedForms[lastItemIndex]; - logger::info("\t└─── {}", describe(lastLinkedItem)); + logger::info("\t└─── {} @{}", describe(lastLinkedItem.first), lastLinkedItem.second); } }); } - void Manager::ForEachLinkedDistributionSet(const std::set& targetForms, std::function performDistribution) + void Manager::ForEachLinkedDistributionSet(const DistributedForms& targetForms, std::function performDistribution) { - for (const auto form : targetForms) { + for (const auto& form : targetForms) { auto& linkedSpells = LinkedFormsForForm(form, spells); auto& linkedPerks = LinkedFormsForForm(form, perks); auto& linkedItems = LinkedFormsForForm(form, items); @@ -249,9 +253,9 @@ namespace LinkedDistribution } } - void Manager::ForEachLinkedDeathDistributionSet(const std::set& targetForms, std::function performDistribution) + void Manager::ForEachLinkedDeathDistributionSet(const DistributedForms& targetForms, std::function performDistribution) { - for (const auto form : targetForms) { + for (const auto& form : targetForms) { auto& linkedDeathItems = LinkedFormsForForm(form, deathItems); DistributionSet linkedEntries{ From 6ff3edafdb03126a228862a09e2217a4b459047f Mon Sep 17 00:00:00 2001 From: adya Date: Sun, 31 Mar 2024 18:25:15 +0000 Subject: [PATCH 44/53] maintenance --- SPID/src/LinkedDistribution.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 076f5d6..ef1cded 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -186,11 +186,11 @@ namespace LinkedDistribution // Iterate through the original map for (const auto& pair : linkedForms.GetForms()) { - const auto& path = pair.first; + const auto& path = pair.first; const auto& formsMap = pair.second; for (const auto& pair : formsMap) { - const auto key = pair.first; + const auto key = pair.first; const auto& values = pair.second; for (const auto& value : values) { map[value.form].emplace_back(key, path); From b36708cbedd2a2b1b4c9b8179263411420de905e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 22:09:09 +0300 Subject: [PATCH 45/53] Implemented Global scope for linked forms. --- SPID/include/FormData.h | 11 ++++++++ SPID/include/LinkedDistribution.h | 21 ++++++++------- SPID/src/LinkedDistribution.cpp | 45 ++++++++++++++++++++----------- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index ffe2813..1591bb4 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -645,3 +645,14 @@ void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& // Likewise, we don't expect plugin names in distributable forms. } } + +inline std::ostream& operator<<(std::ostream& os, Forms::DistributedForm form) +{ + os << form.first; + + if (!form.second.empty()) { + os << " @" << form.second; + } + + return os; +} diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index e4a85a2..867b9a1 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -107,8 +107,8 @@ namespace LinkedDistribution /// /// A set of forms for which distribution sets should be calculated. /// This is typically distributed forms accumulated during first distribution pass. - /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. - void ForEachLinkedDistributionSet(const DistributedForms& linkedForms, std::function callback); + /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. + void ForEachLinkedDistributionSet(const DistributedForms& linkedForms, std::function distribute); /// /// Calculates DistributionSet with only DeathItems for each linked form and calls a callback for each of them. @@ -116,12 +116,15 @@ namespace LinkedDistribution /// /// A set of forms for which distribution sets should be calculated. /// This is typically distributed forms accumulated during first distribution pass. - /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. - void ForEachLinkedDeathDistributionSet(const DistributedForms& linkedForms, std::function callback); + /// A callback to be called with each DistributionSet. This is supposed to do the actual distribution. + void ForEachLinkedDeathDistributionSet(const DistributedForms& linkedForms, std::function distribute); private: template - DataVec& LinkedFormsForForm(const DistributedForm&, LinkedForms&) const; + DataVec& LinkedFormsForForm(const DistributedForm&, Scope, LinkedForms&) const; + + void ForEachLinkedDistributionSet(const DistributedForms& linkedForms, Scope, std::function distribute); + void ForEachLinkedDeathDistributionSet(const DistributedForms& linkedForms, Scope, std::function distribute); LinkedForms spells{ RECORD::kSpell }; LinkedForms perks{ RECORD::kPerk }; @@ -193,13 +196,14 @@ namespace LinkedDistribution } template - DataVec& Manager::LinkedFormsForForm(const DistributedForm& form, LinkedForms& linkedForms) const + DataVec& Manager::LinkedFormsForForm(const DistributedForm& form, Scope scope, LinkedForms& linkedForms) const { - if (const auto formsIt = linkedForms.forms.find(form.second); formsIt != linkedForms.forms.end()) { + if (const auto formsIt = linkedForms.forms.find(scope == kLocal ? form.second : ""); formsIt != linkedForms.forms.end()) { if (const auto linkedFormsIt = formsIt->second.find(form.first); linkedFormsIt != formsIt->second.end()) { return linkedFormsIt->second; } } + static DataVec empty{}; return empty; } @@ -238,10 +242,9 @@ namespace LinkedDistribution template void LinkedForms::Link(Form* form, Scope scope, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const Path& path) { - // TODO: Handle scope for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { - auto& distributableFormsAtPath = forms[path]; + auto& distributableFormsAtPath = forms[scope == kLocal ? path : ""]; // If item is global, we put it in a common map with no information about the path. auto& distributableForms = distributableFormsAtPath[std::get(linkedForm)]; // Note that we don't use Data.index here, as these linked forms don't have any leveled filters // and as such do not to track their index. diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index ef1cded..df6414f 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -207,28 +207,30 @@ namespace LinkedDistribution const auto lastItemIndex = linkedForms.size() - 1; for (int i = 0; i < lastItemIndex; ++i) { const auto& linkedItem = linkedForms[i]; - logger::info("\t├─── {} @{}", describe(linkedItem.first), linkedItem.second); + logger::info("\t├─── {}", describe(linkedItem)); } const auto& lastLinkedItem = linkedForms[lastItemIndex]; - logger::info("\t└─── {} @{}", describe(lastLinkedItem.first), lastLinkedItem.second); + logger::info("\t└─── {}", describe(lastLinkedItem)); } }); } +#pragma endregion - void Manager::ForEachLinkedDistributionSet(const DistributedForms& targetForms, std::function performDistribution) +#pragma region Distribution + void Manager::ForEachLinkedDistributionSet(const DistributedForms& targetForms, Scope scope, std::function performDistribution) { for (const auto& form : targetForms) { - auto& linkedSpells = LinkedFormsForForm(form, spells); - auto& linkedPerks = LinkedFormsForForm(form, perks); - auto& linkedItems = LinkedFormsForForm(form, items); - auto& linkedShouts = LinkedFormsForForm(form, shouts); - auto& linkedLevSpells = LinkedFormsForForm(form, levSpells); - auto& linkedPackages = LinkedFormsForForm(form, packages); - auto& linkedOutfits = LinkedFormsForForm(form, outfits); - auto& linkedKeywords = LinkedFormsForForm(form, keywords); - auto& linkedFactions = LinkedFormsForForm(form, factions); - auto& linkedSleepOutfits = LinkedFormsForForm(form, sleepOutfits); - auto& linkedSkins = LinkedFormsForForm(form, skins); + auto& linkedSpells = LinkedFormsForForm(form, scope, spells); + auto& linkedPerks = LinkedFormsForForm(form, scope, perks); + auto& linkedItems = LinkedFormsForForm(form, scope, items); + auto& linkedShouts = LinkedFormsForForm(form, scope, shouts); + auto& linkedLevSpells = LinkedFormsForForm(form, scope, levSpells); + auto& linkedPackages = LinkedFormsForForm(form, scope, packages); + auto& linkedOutfits = LinkedFormsForForm(form, scope, outfits); + auto& linkedKeywords = LinkedFormsForForm(form, scope, keywords); + auto& linkedFactions = LinkedFormsForForm(form, scope, factions); + auto& linkedSleepOutfits = LinkedFormsForForm(form, scope, sleepOutfits); + auto& linkedSkins = LinkedFormsForForm(form, scope, skins); DistributionSet linkedEntries{ linkedSpells, @@ -253,10 +255,16 @@ namespace LinkedDistribution } } - void Manager::ForEachLinkedDeathDistributionSet(const DistributedForms& targetForms, std::function performDistribution) + void Manager::ForEachLinkedDistributionSet(const DistributedForms& targetForms, std::function performDistribution) + { + ForEachLinkedDistributionSet(targetForms, Scope::kLocal, performDistribution); + ForEachLinkedDistributionSet(targetForms, Scope::kGlobal, performDistribution); + } + + void Manager::ForEachLinkedDeathDistributionSet(const DistributedForms& targetForms, Scope scope, std::function performDistribution) { for (const auto& form : targetForms) { - auto& linkedDeathItems = LinkedFormsForForm(form, deathItems); + auto& linkedDeathItems = LinkedFormsForForm(form, scope, deathItems); DistributionSet linkedEntries{ DistributionSet::empty(), @@ -281,5 +289,10 @@ namespace LinkedDistribution } } + void Manager::ForEachLinkedDeathDistributionSet(const DistributedForms& targetForms, std::function performDistribution) + { + ForEachLinkedDeathDistributionSet(targetForms, Scope::kLocal, performDistribution); + ForEachLinkedDeathDistributionSet(targetForms, Scope::kGlobal, performDistribution); + } #pragma endregion } From f16ce039327873328aaf127abdd9fc1737bbaa29 Mon Sep 17 00:00:00 2001 From: adya Date: Sun, 31 Mar 2024 19:09:25 +0000 Subject: [PATCH 46/53] maintenance --- SPID/include/LinkedDistribution.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 867b9a1..560e7e0 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -203,7 +203,7 @@ namespace LinkedDistribution return linkedFormsIt->second; } } - + static DataVec empty{}; return empty; } @@ -244,7 +244,7 @@ namespace LinkedDistribution { for (const auto& linkedForm : linkedForms) { if (std::holds_alternative(linkedForm)) { - auto& distributableFormsAtPath = forms[scope == kLocal ? path : ""]; // If item is global, we put it in a common map with no information about the path. + auto& distributableFormsAtPath = forms[scope == kLocal ? path : ""]; // If item is global, we put it in a common map with no information about the path. auto& distributableForms = distributableFormsAtPath[std::get(linkedForm)]; // Note that we don't use Data.index here, as these linked forms don't have any leveled filters // and as such do not to track their index. From 78f3468b384a5bab28eb6431e9b8fe21b26d46ff Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 22:14:28 +0300 Subject: [PATCH 47/53] Renamed ExclusiveGroup types to not be confused with Linked Forms. --- SPID/include/ExclusiveGroups.h | 12 ++++++------ SPID/src/ExclusiveGroups.cpp | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/SPID/include/ExclusiveGroups.h b/SPID/include/ExclusiveGroups.h index 0fb37ea..03ecf64 100644 --- a/SPID/include/ExclusiveGroups.h +++ b/SPID/include/ExclusiveGroups.h @@ -24,9 +24,9 @@ namespace ExclusiveGroups bool TryParse(const std::string& a_key, const std::string& a_value, const Path& a_path); } - using GroupName = std::string; - using LinkedGroups = std::unordered_map>; - using Groups = std::unordered_map>; + using Group = std::string; + using FormGroupMap = std::unordered_map>; + using GroupFormsMap = std::unordered_map>; class Manager : public ISingleton { @@ -54,18 +54,18 @@ namespace ExclusiveGroups /// Retrieves all exclusive groups. /// /// A reference to discovered exclusive groups - const Groups& GetGroups() const; + const GroupFormsMap& GetGroups() const; private: /// /// A map of exclusive group names related to each form in the exclusive groups. /// Provides a quick and easy way to get names of all groups that need to be checked. /// - LinkedGroups linkedGroups{}; + FormGroupMap linkedGroups{}; /// /// A map of exclusive groups names and the forms that are part of each exclusive group. /// - Groups groups{}; + GroupFormsMap groups{}; }; } diff --git a/SPID/src/ExclusiveGroups.cpp b/SPID/src/ExclusiveGroups.cpp index 277b46e..21e37f7 100644 --- a/SPID/src/ExclusiveGroups.cpp +++ b/SPID/src/ExclusiveGroups.cpp @@ -98,7 +98,7 @@ namespace ExclusiveGroups { std::unordered_set forms{}; if (auto it = linkedGroups.find(form); it != linkedGroups.end()) { - std::ranges::for_each(it->second, [&](const GroupName& name) { + std::ranges::for_each(it->second, [&](const Group& name) { const auto& group = groups.at(name); forms.insert(group.begin(), group.end()); }); @@ -110,7 +110,7 @@ namespace ExclusiveGroups return forms; } - const Groups& ExclusiveGroups::Manager::GetGroups() const + const GroupFormsMap& ExclusiveGroups::Manager::GetGroups() const { return groups; } From cb7e2950d44f3528c204d04fca632449b97b3c6e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 31 Mar 2024 23:34:25 +0300 Subject: [PATCH 48/53] Implemented a way to overwrite outfits with linked outfits. --- SPID/include/Distribute.h | 22 ---------------------- SPID/src/Distribute.cpp | 15 ++++++++------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index f222af3..065a1ac 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -116,28 +116,6 @@ namespace Distribute } #pragma endregion - // TODO: Is this unused? - // outfits/sleep outfits - template - void for_each_form( - const NPCData& a_npcData, - Forms::Distributables& a_distributables, - std::function a_callback, - DistributedForms* accumulatedForms = nullptr) - { - auto& vec = a_distributables.GetForms(false); - - for (auto& formData : vec) { // Vector is reversed in FinishLookupForms - if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, formData) && a_callback(formData.form)) { - if (accumulatedForms) { - accumulatedForms->insert({ formData.form, formData.path }); - } - ++formData.npcCount; - break; - } - } - } - #pragma region Items // countable items template diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 6a6c6eb..7ef6e96 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -20,8 +20,9 @@ namespace Distribute /// General information about NPC that is being processed. /// Leveling information about NPC that is being processed. /// A set of forms that should be distributed to NPC. + /// If true, overwritable forms (like Outfits) will be to overwrite last distributed form on NPC. /// An optional pointer to a set that will accumulate all distributed forms. - void distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, DistributedForms* accumulatedForms) + void distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms) { const auto npc = npcData.GetNPC(); @@ -124,7 +125,7 @@ namespace Distribute for_each_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - if (npc->defaultOutfit != a_outfit && !npc->HasKeyword(processedOutfit)) { + if (npc->defaultOutfit != a_outfit && (allowOverwrites || !npc->HasKeyword(processedOutfit))) { npc->AddKeyword(processedOutfit); npc->defaultOutfit = a_outfit; return true; @@ -193,13 +194,13 @@ namespace Distribute DistributedForms distributedForms{}; - detail::distribute(npcData, input, entries, &distributedForms); + detail::distribute(npcData, input, entries, false, &distributedForms); // TODO: We can now log per-NPC distributed forms. if (!distributedForms.empty()) { - // This only does one-level linking. So that linked entries won't trigger another level of distribution. + // TODO: This only does one-level linking. So that linked entries won't trigger another level of distribution. LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(distributedForms, [&](Forms::DistributionSet& set) { - detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? to log what was distributed. + detail::distribute(npcData, input, set, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. }); } } @@ -229,12 +230,12 @@ namespace Distribute Forms::DistributionSet::empty() }; - detail::distribute(npcData, input, entries, &distributedForms); + detail::distribute(npcData, input, entries, false, &distributedForms); // TODO: We can now log per-NPC distributed forms. if (!distributedForms.empty()) { LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDeathDistributionSet(distributedForms, [&](Forms::DistributionSet& set) { - detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? to log what was distributed. + detail::distribute(npcData, input, set, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. }); } } From 6c0adc099b2c44a5cfc2162e93d51b03fe94f5d6 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 4 Apr 2024 00:56:46 +0300 Subject: [PATCH 49/53] Remove redundant RE::FormTypeToString calls. FormType is automatically overloaded for std::format. --- SPID/include/FormData.h | 12 ++++++------ SPID/include/LinkedDistribution.h | 8 ++++---- SPID/src/LinkedDistribution.cpp | 2 +- SPID/src/LookupForms.cpp | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 1591bb4..f9485ce 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -321,20 +321,20 @@ namespace Forms std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] Filter[0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] Filter[0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), e.expectedFormType, e.actualFormType); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] Filter ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] Filter ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, e.expectedFormType, e.actualFormType); } }, e.formOrEditorID); } catch (const InvalidFormTypeException& e) { std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - invalid formtype ({})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.formType)); + buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - invalid formtype ({})", e.path, *formID, modName.value_or(""), e.formType); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] Filter ({}) SKIP - invalid formtype ({})", e.path, editorID, RE::FormTypeToString(e.formType)); + buffered_logger::error("\t\t[{}] Filter ({}) SKIP - invalid formtype ({})", e.path, editorID, e.formType); } }, e.formOrEditorID); } @@ -633,10 +633,10 @@ void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] [0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] [0x{:X}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), e.expectedFormType, e.actualFormType); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] ({}) FAIL - mismatching form type (expected: {}, actual: {})", e.path, editorID, e.expectedFormType, e.actualFormType); } }, e.formOrEditorID); } catch (const Lookup::InvalidFormTypeException& e) { diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index 560e7e0..b054f43 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -175,20 +175,20 @@ namespace LinkedDistribution std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, *formID, modName.value_or(""), e.expectedFormType, e.actualFormType); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFormType), RE::FormTypeToString(e.actualFormType)); + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, editorID, e.expectedFormType, e.actualFormType); } }, e.formOrEditorID); } catch (const InvalidFormTypeException& e) { std::visit(overload{ [&](const FormModPair& formMod) { auto& [formID, modName] = formMod; - buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - unsupported form type ({})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.formType)); + buffered_logger::error("\t\t[{}] LinkedForm [0x{:X}] ({}) SKIP - unsupported form type ({})", e.path, *formID, modName.value_or(""), e.formType); }, [&](std::string editorID) { - buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - unsupported form type ({})", e.path, editorID, RE::FormTypeToString(e.formType)); + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - unsupported form type ({})", e.path, editorID, e.formType); } }, e.formOrEditorID); } diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index df6414f..5bb1d34 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -155,7 +155,7 @@ namespace LinkedDistribution } packages.Link(form, scope, parentForms, packageIndex, chance, path); } else { - logger::warn("\t[{}] Unsupported Form type: {}", path, RE::FormTypeToString(type)); + logger::warn("\t[{}] Unsupported Form type: {}", path, type); } } } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 86bdc49..50369de 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -51,7 +51,7 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) } packages.EmplaceForm(isValid, form, packageIndex, filters, path); } else { - logger::warn("\t[{}] Unsupported Form type: {}", path, RE::FormTypeToString(type)); + logger::warn("\t[{}] Unsupported Form type: {}", path, type); } } }); From 830e42af3414edc9938fcbd9cf5ecc03dd77a367 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 6 Apr 2024 22:30:00 +0300 Subject: [PATCH 50/53] Removed skin from type inferring. Added missing leveled spell inferring. Skin wouldn't work because it would always fall into Item category. --- SPID/src/LinkedDistribution.cpp | 6 +++--- SPID/src/LookupForms.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 5bb1d34..c839ac5 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -122,11 +122,13 @@ namespace LinkedDistribution if (!Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { continue; } - // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit or DeathItems) + // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit, Skin or DeathItems) if (const auto keyword = form->As(); keyword) { keywords.Link(keyword, scope, parentForms, idxOrCount, chance, path); } else if (const auto spell = form->As(); spell) { spells.Link(spell, scope, parentForms, idxOrCount, chance, path); + } else if (const auto levSpell = form->As(); levSpell) { + levSpells.Link(levSpell, scope, parentForms, idxOrCount, chance, path); } else if (const auto perk = form->As(); perk) { perks.Link(perk, scope, parentForms, idxOrCount, chance, path); } else if (const auto shout = form->As(); shout) { @@ -137,8 +139,6 @@ namespace LinkedDistribution outfits.Link(outfit, scope, parentForms, idxOrCount, chance, path); } else if (const auto faction = form->As(); faction) { factions.Link(faction, scope, parentForms, idxOrCount, chance, path); - } else if (const auto skin = form->As(); skin) { - skins.Link(skin, scope, parentForms, idxOrCount, chance, path); } else { auto type = form->GetFormType(); if (type == RE::FormType::Package || type == RE::FormType::FormList) { diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 50369de..7cc5950 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -17,12 +17,14 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) auto& genericForms = INI::configs[RECORD::kForm]; for (auto& rawForm : genericForms) { - // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit or DeathItems) + // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit, Skin or DeathItems) LookupGenericForm(dataHandler, rawForm, [&](bool isValid, auto form, const auto& idxOrCount, const auto& filters, const auto& path) { if (const auto keyword = form->As(); keyword) { keywords.EmplaceForm(isValid, keyword, idxOrCount, filters, path); } else if (const auto spell = form->As(); spell) { spells.EmplaceForm(isValid, spell, idxOrCount, filters, path); + } else if (const auto levSpell = form->As(); levSpell) { + levSpells.EmplaceForm(isValid, levSpell, idxOrCount, filters, path); } else if (const auto perk = form->As(); perk) { perks.EmplaceForm(isValid, perk, idxOrCount, filters, path); } else if (const auto shout = form->As(); shout) { @@ -33,8 +35,6 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) outfits.EmplaceForm(isValid, outfit, idxOrCount, filters, path); } else if (const auto faction = form->As(); faction) { factions.EmplaceForm(isValid, faction, idxOrCount, filters, path); - } else if (const auto skin = form->As(); skin) { - skins.EmplaceForm(isValid, skin, idxOrCount, filters, path); } else { auto type = form->GetFormType(); if (type == RE::FormType::Package || type == RE::FormType::FormList) { From 3dbc218c09e5e4d238131a461a346cd1deb61e2e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 7 Apr 2024 00:07:10 +0300 Subject: [PATCH 51/53] Added also an NPC's baseTemplateForm to list of IDs. --- SPID/include/LookupNPC.h | 2 +- SPID/src/LookupNPC.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/SPID/include/LookupNPC.h b/SPID/include/LookupNPC.h index d3f5849..b01bfff 100644 --- a/SPID/include/LookupNPC.h +++ b/SPID/include/LookupNPC.h @@ -37,7 +37,7 @@ namespace NPC struct ID { ID() = default; - explicit ID(const RE::TESActorBase* a_base); + explicit ID(const RE::TESForm* a_base); ~ID() = default; [[nodiscard]] bool contains(const std::string& a_str) const; diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index b164e6b..4895ff6 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -3,7 +3,7 @@ namespace NPC { - Data::ID::ID(const RE::TESActorBase* a_base) : + Data::ID::ID(const RE::TESForm* a_base) : formID(a_base->GetFormID()), editorID(editorID::get_editorID(a_base)) {} @@ -42,6 +42,10 @@ namespace NPC return RE::BSContainer::ForEachResult::kContinue; }); + if (npc->baseTemplateForm) { + IDs.emplace_back(npc->baseTemplateForm); + } + if (const auto extraLvlCreature = a_actor->extraList.GetByType()) { if (const auto originalBase = extraLvlCreature->originalBase) { IDs.emplace_back(originalBase); From 6def751bb228357d7ea18a762339d11b8713ae85 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 7 Apr 2024 20:26:41 +0300 Subject: [PATCH 52/53] Allowed specifying LevSpell entries directly in Spell. LevSpell is still supported. --- SPID/src/LinkedDistribution.cpp | 24 +++++++++++++++++++++++- SPID/src/LookupForms.cpp | 19 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index c839ac5..b06ed58 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -110,10 +110,32 @@ namespace LinkedDistribution void Manager::LookupLinkedForms(RE::TESDataHandler* dataHandler, INI::LinkedFormsConfig& rawLinkedForms) { - ForEachLinkedForms([&](LinkedForms& forms) { + ForEachLinkedForms([&](LinkedForms& forms) { + // If it's spells distributable we want to manually lookup forms to pick LevSpells that are added into the list. + if constexpr (std::is_same_v) { + return; + } forms.LookupForms(dataHandler, rawLinkedForms[forms.GetType()]); }); + // Sort out Spells and Leveled Spells into two separate lists. + auto& rawSpells = rawLinkedForms[RECORD::kSpell]; + + for (auto& rawSpell : rawSpells) { + if (auto form = detail::LookupLinkedForm(dataHandler, rawSpell); form) { + auto& [formID, scope, parentFormIDs, idxOrCount, chance, path] = rawSpell; + FormVec parentForms{}; + if (!Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) { + continue; + } + if (const auto spell = form->As(); spell) { + spells.Link(spell, scope, parentForms, idxOrCount, chance, path); + } else if (const auto levSpell = form->As(); levSpell) { + levSpells.Link(levSpell, scope, parentForms, idxOrCount, chance, path); + } + } + } + auto& genericForms = rawLinkedForms[RECORD::kForm]; for (auto& rawForm : genericForms) { if (auto form = detail::LookupLinkedForm(dataHandler, rawForm); form) { diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 7cc5950..6b88551 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -8,12 +8,29 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) { using namespace Forms; - ForEachDistributable([&](Distributables& a_distributable) { + ForEachDistributable([&](Distributables& a_distributable) { + // If it's spells distributable we want to manually lookup forms to pick LevSpells that are added into the list. + if constexpr (std::is_same_v) { + return; + } const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); a_distributable.LookupForms(dataHandler, recordName, INI::configs[a_distributable.GetType()]); }); + // Sort out Spells and Leveled Spells into two separate lists. + auto& rawSpells = INI::configs[RECORD::kSpell]; + + for (auto& rawSpell : rawSpells) { + LookupGenericForm(dataHandler, rawSpell, [&](bool isValid, auto form, const auto& idxOrCount, const auto& filters, const auto& path) { + if (const auto spell = form->As(); spell) { + spells.EmplaceForm(isValid, spell, idxOrCount, filters, path); + } else if(const auto levSpell = form->As(); levSpell) { + levSpells.EmplaceForm(isValid, levSpell, idxOrCount, filters, path); + } + }); + } + auto& genericForms = INI::configs[RECORD::kForm]; for (auto& rawForm : genericForms) { From 85afb05194fa027f1e3cad2edcc7d7ef3306e90b Mon Sep 17 00:00:00 2001 From: adya Date: Sun, 7 Apr 2024 17:27:00 +0000 Subject: [PATCH 53/53] maintenance --- SPID/src/LookupForms.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 6b88551..f875be4 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -25,7 +25,7 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) LookupGenericForm(dataHandler, rawSpell, [&](bool isValid, auto form, const auto& idxOrCount, const auto& filters, const auto& path) { if (const auto spell = form->As(); spell) { spells.EmplaceForm(isValid, spell, idxOrCount, filters, path); - } else if(const auto levSpell = form->As(); levSpell) { + } else if (const auto levSpell = form->As(); levSpell) { levSpells.EmplaceForm(isValid, levSpell, idxOrCount, filters, path); } });