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/Distribute.h b/SPID/include/Distribute.h index f0fd2f2..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; @@ -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/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/FormData.h b/SPID/include/FormData.h index 9c165b0..cc649f2 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; @@ -320,6 +320,44 @@ 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. + /// 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; + + bool IsEmpty() const; + + template + static DataVec& empty() + { + static DataVec empty{}; + return empty; + } + }; + + /// + /// 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/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h new file mode 100644 index 0000000..ee56f73 --- /dev/null +++ b/SPID/include/LinkedDistribution.h @@ -0,0 +1,139 @@ +#pragma once +#include "FormData.h" +#include "LookupNPC.h" +#include "PCLevelMultManager.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{}; + + 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 + { + 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. + /// + /// 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 = INI::linkedItems); + + void LogLinkedItemsLookup(); + + /// + /// 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; + + 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 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 + 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 0f2a559..9c40e55 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 { @@ -52,117 +53,177 @@ 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; - } + /// + /// 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. + /// 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) + { + const auto npc = npcData.GetNPC(); - const auto npc = a_npcData.GetNPC(); + for_each_form( + npcData, forms.keywords, input, [&](const std::vector& a_keywords) { + npc->AddKeywords(a_keywords); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::keywords, a_input, [&](const std::vector& a_keywords) { - npc->AddKeywords(a_keywords); - }); + for_each_form( + 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 }); + } + }, + 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( + npcData, forms.spells, input, [&](const std::vector& a_spells) { + npc->GetSpellList()->AddSpells(a_spells); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::spells, a_input, [&](const std::vector& a_spells) { - npc->GetSpellList()->AddSpells(a_spells); - }); + for_each_form( + npcData, forms.levSpells, input, [&](const std::vector& a_levSpells) { + npc->GetSpellList()->AddLevSpells(a_levSpells); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::levSpells, a_input, [&](const std::vector& a_levSpells) { - npc->GetSpellList()->AddLevSpells(a_levSpells); - }); + for_each_form( + npcData, forms.perks, input, [&](const std::vector& a_perks) { + npc->AddPerks(a_perks, 1); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::perks, a_input, [&](const std::vector& a_perks) { - npc->AddPerks(a_perks, 1); - }); + for_each_form( + npcData, forms.shouts, input, [&](const std::vector& a_shouts) { + npc->GetSpellList()->AddShouts(a_shouts); + }, + accumulatedForms); - for_each_form(a_npcData, Forms::shouts, a_input, [&](const std::vector& a_shouts) { - npc->GetSpellList()->AddShouts(a_shouts); - }); + for_each_form( + npcData, forms.packages, 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( + npcData, forms.skins, 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( + npcData, forms.sleepOutfits, 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& input, const std::set& forms) + { + LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(forms, [&](Forms::DistributionSet& set) { + detail::distribute(npcData, input, set, nullptr); // TODO: Accumulate forms here? }); } + void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input) + { + if (a_input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(a_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::DistributionSet::empty(), // items are processed separately + Forms::shouts.GetForms(a_input.onlyPlayerLevelEntries), + Forms::levSpells.GetForms(a_input.onlyPlayerLevelEntries), + Forms::packages.GetForms(a_input.onlyPlayerLevelEntries), + Forms::DistributionSet::empty(), // outfits are processed along with items. + Forms::keywords.GetForms(a_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) + }; + + 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) { if (a_input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(a_input)) { @@ -172,25 +233,37 @@ 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) { - if (npc->AddObjectsToContainer(a_objects, npc)) { - if (a_hasLvlItem) { - detail::init_leveled_items(actor); + 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); + } + return true; } - return true; - } - return false; - }); + return false; + }, + &distributedForms); - for_each_form(a_npcData, Forms::outfits, 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); + + // 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/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()); 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/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 new file mode 100644 index 0000000..a27cc9e --- /dev/null +++ b/SPID/src/LinkedDistribution.cpp @@ -0,0 +1,232 @@ +#include "LinkedDistribution.h" +#include "FormData.h" + +namespace LinkedDistribution +{ + + using namespace Forms; + +#pragma region Parsing + namespace INI + { + 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(); + + 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[kLinkedForms]); + + 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; + + 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]); + + item.count = RandomCount(minCount, maxCount); + } else { + auto count = string::to_num(str); + + 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::Lookup; + + 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)) { + 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 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); + } + } 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 + 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) + { + 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& linkedSkins = LinkedFormsForForm(form, skins); + + DistributionSet linkedEntries{ + linkedSpells, + linkedPerks, + linkedItems, + linkedShouts, + linkedLevSpells, + linkedPackages, + linkedOutfits, + linkedKeywords, + 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. + linkedSkins + }; + + if (linkedEntries.IsEmpty()) { + continue; + } + + performDistribution(linkedEntries); + } + } + +#pragma endregion +} diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index f3c3a9f..0c7f572 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::TryParse(key.pItem, entry, truncatedPath)) { + continue; + } + auto [data, sanitized_str] = detail::parse_ini(key.pItem, entry, truncatedPath); configs[key.pItem].emplace_back(data); diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index a56d673..c15ea1e 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -2,6 +2,7 @@ #include "ExclusiveGroups.h" #include "FormData.h" #include "KeywordDependencies.h" +#include "LinkedDistribution.h" bool LookupDistributables(RE::TESDataHandler* const dataHandler) { @@ -77,6 +78,16 @@ void LogExclusiveGroupsLookup() } } +void LookupLinkedItems(RE::TESDataHandler* const dataHandler) +{ + LinkedDistribution::Manager::GetSingleton()->LookupLinkedItems(dataHandler); +} + +void LogLinkedItemsLookup() +{ + LinkedDistribution::Manager::GetSingleton()->LogLinkedItemsLookup(); +} + bool Lookup::LookupForms() { if (const auto dataHandler = RE::TESDataHandler::GetSingleton(); dataHandler) { @@ -93,6 +104,9 @@ bool Lookup::LookupForms() logger::info("Lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); } + LookupLinkedItems(dataHandler); + LogLinkedItemsLookup(); + LookupExclusiveGroups(dataHandler); LogExclusiveGroupsLookup();