From 1926498ae14a00afff93d193d662c8691689805d Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 24 Mar 2024 23:50:59 +0200 Subject: [PATCH] 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();