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 +}