diff --git a/SPID/CMakeLists.txt b/SPID/CMakeLists.txt index 41435a7..587a710 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/cmake/headerlist.cmake b/SPID/cmake/headerlist.cmake index c2836ca..e233a89 100644 --- a/SPID/cmake/headerlist.cmake +++ b/SPID/cmake/headerlist.cmake @@ -5,8 +5,10 @@ set(headers ${headers} include/Distribute.h include/DistributeManager.h include/DistributePCLevelMult.h + 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 dde5501..c78269e 100644 --- a/SPID/cmake/sourcelist.cmake +++ b/SPID/cmake/sourcelist.cmake @@ -3,8 +3,10 @@ set(sources ${sources} src/Distribute.cpp src/DistributeManager.cpp src/DistributePCLevelMult.cpp + 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/Defs.h b/SPID/include/Defs.h index 7691604..fd2d450 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,8 +93,29 @@ struct Traits std::optional teammate{}; }; -using IdxOrCount = std::int32_t; -using Chance = std::uint32_t; +using Path = std::string; + +using Index = std::int32_t; +using Count = std::int32_t; +using RandomCount = Range; +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; + +/// +/// 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/Distribute.h b/SPID/include/Distribute.h index f88545f..065a1ac 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; @@ -70,89 +70,84 @@ 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 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, + DistributedForms* accumulatedForms = nullptr) { - auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); - - for (auto& formData : vec) { - if (detail::passed_filters(a_npcData, a_input, formData)) { + for (auto& formData : forms) { + if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData)) { + if (accumulatedForms) { + accumulatedForms->insert({ formData.form, formData.path }); + } 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, + DistributedForms* accumulatedForms = nullptr) { - 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)) { - ++formData.npcCount; - break; - } - } - } - - // outfits/sleep outfits - template - void for_each_form( - const 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)) { + 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.path }); + } ++formData.npcCount; break; } } } +#pragma endregion - // items +#pragma region Items + // countable items 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, + DistributedForms* accumulatedForms = nullptr) { - auto& vec = a_distributables.GetForms(a_input.onlyPlayerLevelEntries); + std::map collectedForms{}; - if (vec.empty()) { - return; - } - - std::map collectedForms{}; - - for (auto& formData : vec) { - if (detail::passed_filters(a_npcData, a_input, formData)) { + 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 level = a_npcData.GetLevel(); RE::BSScrapArray calcedObjects{}; - leveledItem->CalculateCurrentFormList(level, formData.idxOrCount, calcedObjects, 0, true); + 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] += formData.idxOrCount; + collectedForms[formData.form] += count; + if (accumulatedForms) { + accumulatedForms->insert({ formData.form, formData.path }); + } } ++formData.npcCount; } @@ -162,53 +157,56 @@ namespace Distribute a_callback(collectedForms); } } +#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, + DistributedForms* 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)) { 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()) { collectedLeveledFormIDs.emplace(formID); } + if (accumulatedForms) { + accumulatedForms->insert({ form, formData.path }); + } ++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); } + if (accumulatedForms) { + accumulatedForms->insert({ form, formData.path }); + } ++formData.npcCount; } } @@ -221,52 +219,10 @@ namespace Distribute } } } +#pragma endregion - 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 (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) { - collectedForms.emplace_back(form); - ++formData.npcCount; - } - } - } - - if (!collectedForms.empty()) { - a_callback(collectedForms); - } - } + void Distribute(NPCData& npcData, const PCLevelMult::Input& input); + void Distribute(NPCData& npcData, bool onlyLeveledEntries); - void Distribute(NPCData& a_npcData, const PCLevelMult::Input& a_input); - void Distribute(NPCData& a_npcData, bool a_onlyLeveledEntries); + void DistributeDeathItems(NPCData& npcData, const PCLevelMult::Input& input); } diff --git a/SPID/include/ExclusiveGroups.h b/SPID/include/ExclusiveGroups.h new file mode 100644 index 0000000..03ecf64 --- /dev/null +++ b/SPID/include/ExclusiveGroups.h @@ -0,0 +1,71 @@ +#pragma once +#include "LookupConfigs.h" + +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{}; + Path 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 Path& a_path); + } + + using Group = std::string; + using FormGroupMap = std::unordered_map>; + using GroupFormsMap = 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 exclusive groups. + /// + /// 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 = INI::exclusiveGroups); + + void LogExclusiveGroupsLookup(); + + /// + /// 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; + + /// + /// Retrieves all exclusive groups. + /// + /// A reference to discovered exclusive groups + 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. + /// + FormGroupMap linkedGroups{}; + + /// + /// A map of exclusive groups names and the forms that are part of each exclusive group. + /// + GroupFormsMap groups{}; + }; +} diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 5111551..f9485ce 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -5,8 +5,118 @@ namespace Forms { + namespace Lookup + { + struct UnknownPluginException : std::exception + { + const std::string modName; + const Path path; + + UnknownPluginException(const std::string& modName, const Path& path) : + modName(modName), + path(path) + {} + }; + + struct UnknownFormIDException : std::exception + { + const RE::FormID formID; + const std::optional modName; + const Path path; + + UnknownFormIDException(RE::FormID formID, const Path& path, std::optional modName = std::nullopt) : + formID(formID), + path(path), + modName(modName) + {} + }; + + /// + /// 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 expectedFormType; + const RE::FormType actualFormType; + const FormOrEditorID formOrEditorID; + const Path path; + + MismatchingFormTypeException(RE::FormType expectedFormType, RE::FormType actualFormType, const FormOrEditorID& formOrEditorID, const Path& path) : + expectedFormType(expectedFormType), + actualFormType(actualFormType), + formOrEditorID(formOrEditorID), + path(path) + {} + }; + + struct InvalidKeywordException : std::exception + { + const RE::FormID formID; + const std::optional modName; + const Path path; + + InvalidKeywordException(RE::FormID formID, const Path& path, std::optional modName = std::nullopt) : + formID(formID), + modName(modName), + path(path) + {} + }; + + struct KeywordNotFoundException : std::exception + { + const std::string editorID; + const bool isDynamic; + const Path path; + + KeywordNotFoundException(const std::string& editorID, bool isDynamic, const Path& path) : + editorID(editorID), + isDynamic(isDynamic), + path(path) + {} + }; + + struct UnknownEditorIDException : std::exception + { + const std::string editorID; + const Path path; + + UnknownEditorIDException(const std::string& editorID, const Path& path) : + editorID(editorID), + path(path) + {} + }; + + /// + /// An exception thrown when actual form's type is not in the whitelist. + /// + struct InvalidFormTypeException : std::exception + { + const RE::FormType formType; + const FormOrEditorID formOrEditorID; + const Path path; + + InvalidFormTypeException(RE::FormType formType, const FormOrEditorID& formOrEditorID, const Path& path) : + formType(formType), + formOrEditorID(formOrEditorID), + path(path) + {} + }; + + struct MalformedEditorIDException : std::exception + { + const Path path; + + MalformedEditorIDException(const Path& 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 +139,205 @@ 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* const dataHandler, const FormOrEditorID& formOrEditorID, const Path& 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; + const 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(); + } + }; + + 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; + + // 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); - } - } else if (formID) { - if (auto filterForm = modName ? - a_dataHandler->LookupForm(*formID, *modName) : - RE::TESForm::LookupByID(*formID)) { - const auto formType = filterForm->GetFormType(); - if (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); - } - } else { - buffered_logger::error("\t\t[{}] Filter [0x{:X}] ({}) SKIP - form doesn't exist", a_path, *formID, modName.value_or("")); + } + + if (formID && g_mergeMapperInterface) { + get_merged_IDs(formID, modName); + } + + // Either 0x1235 or 0x1235~MyPlugin.esp + if (formID) { + RE::TESForm* anyForm; + if (modName) { + anyForm = dataHandler->LookupForm(*formID, *modName); + } else { + anyForm = RE::TESForm::LookupByID(*formID); + } + + if (!anyForm) { + throw UnknownFormIDException(*formID, path, modName); + } + + form = as_form(anyForm); + if (!form) { + throw MismatchingFormTypeException(Form::FORMTYPE, anyForm->GetFormType(), FormModPair{ *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, path, modName); } } - }, - [&](std::string& a_editorID) { - if (!a_editorID.empty()) { - if (auto filterForm = RE::TESForm::LookupByEditorID(a_editorID); filterForm) { - const auto formType = filterForm->GetFormType(); - if (FormType::GetWhitelisted(formType)) { - a_formVec.emplace_back(filterForm); - } else { - buffered_logger::error("\t\t[{}] Filter ({}) SKIP - invalid formtype ({})", a_path, a_editorID, formType); - } + } + }, + [&](const std::string& editorID) { + if (editorID.empty()) { + throw MalformedEditorIDException(path); + } + if constexpr (std::is_same_v) { + form = find_or_create_keyword(editorID); + } else { + 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 { - buffered_logger::error("\t\t[{}] Filter ({}) SKIP - form doesn't exist", a_path, a_editorID); + throw UnknownEditorIDException(editorID, path); } } - } }, - formOrEditorID); + } + } }, + formOrEditorID); + + if (mod) { + return mod; + } + + if (whitelistedOnly && form) { + const auto formType = form->GetFormType(); + if (FormType::GetWhitelisted(formType)) { + return form; + } else { + throw InvalidFormTypeException(formType, formOrEditorID, path); + } + } + + return form; + } + + 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); + + if (std::holds_alternative(formOrMod)) { + return std::get(formOrMod); + } + + return nullptr; + } + + template + 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); + + if (std::holds_alternative(formOrMod)) { + return std::get(formOrMod); + } + + return nullptr; + } + + 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; + } + + for (auto& formOrEditorID : a_rawFormVec) { + try { + 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("")); + } 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 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(""), e.expectedFormType, e.actualFormType); + }, + [&](std::string editorID) { + 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(""), e.formType); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] Filter ({}) SKIP - invalid formtype ({})", e.path, editorID, e.formType); + } }, + e.formOrEditorID); + } } return !a_all && !a_formVec.empty() || a_formVec.size() == a_rawFormVec.size(); @@ -88,11 +349,11 @@ namespace Forms { std::uint32_t index{ 0 }; - Form* form{ nullptr }; - IdxOrCount idxOrCount{ 1 }; - FilterData filters{}; + Form* form{ nullptr }; + IndexOrCount idxOrCount{ RandomCount(1, 1) }; + FilterData filters{}; - std::string path{}; + Path path{}; std::uint32_t npcCount{ 0 }; bool operator==(const Data& a_rhs) const; @@ -101,6 +362,47 @@ namespace Forms template using DataVec = std::vector>; + using DistributedForm = std::pair; + using DistributedForms = std::set; + + /// + /// 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 { @@ -113,12 +415,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 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(); @@ -127,6 +432,12 @@ namespace Forms RECORD::TYPE type; 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*, INI::Data&); }; inline Distributables spells{ RECORD::kSpell }; @@ -161,6 +472,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 @@ -190,6 +512,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 { @@ -212,7 +540,15 @@ Forms::DataVec& Forms::Distributables::GetForms(bool a_onlyLevelEntr } template -void Forms::Distributables::LookupForms(RE::TESDataHandler* a_dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec) +void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, INI::Data& rawForm) +{ + 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* dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec) { if (a_INIDataVec.empty()) { return; @@ -221,104 +557,19 @@ 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) { - Form* form = nullptr; - - constexpr auto as_form = [](RE::TESForm* a_form) -> Form* { - if (!a_form) { - return nullptr; - } - if constexpr (std::is_same_v) { - return a_form; - } else if constexpr (std::is_same_v) { - return static_cast(a_form); - } else { - return a_form->As(); - } - }; - - 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); - } - if (validEntry) { - validEntry = detail::formID_to_form(a_dataHandler, filterIDs.MATCH, filterForms.MATCH, path); - } - - if (!validEntry) { - continue; - } + for (auto& rawForm : a_INIDataVec) { + LookupForm(dataHandler, rawForm); + } +} - forms.emplace_back(index, form, idxOrCount, FilterData(strings, filterForms, level, traits, chance), path); - index++; +template +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); } + lookupCount++; } template @@ -343,3 +594,65 @@ 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); + } + + 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("")); + } 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(""), e.expectedFormType, e.actualFormType); + }, + [&](std::string editorID) { + 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) { + // Whitelisting is disabled, so this should not occur + } catch (const Lookup::UnknownPluginException& e) { + // 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 new file mode 100644 index 0000000..b054f43 --- /dev/null +++ b/SPID/include/LinkedDistribution.h @@ -0,0 +1,256 @@ +#pragma once +#include "FormData.h" +#include "LookupNPC.h" +#include "PCLevelMultManager.h" + +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{}; + + IndexOrCount idxOrCount{ RandomCount(1, 1) }; + PercentChance chance{ 100 }; + + Path path{}; + }; + + using LinkedFormsVec = std::vector; + using LinkedFormsConfig = std::unordered_map; + + 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& key, const std::string& value, const Path&); + } + + using namespace Forms; + + class Manager; + + template + struct LinkedForms; + + namespace detail + { + template + Form* LookupLinkedForm(RE::TESDataHandler* const dataHandler, INI::RawLinkedForm& rawForm); + } + + template + struct LinkedForms + { + friend Manager; // allow Manager to later modify forms directly. + friend Form* detail::LookupLinkedForm(RE::TESDataHandler* const, INI::RawLinkedForm&); + + using FormsMap = std::unordered_map>>; + + LinkedForms(RECORD::TYPE type) : + type(type) + {} + + RECORD::TYPE GetType() const { return type; } + const FormsMap& GetForms() const { return forms; } + + void LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms); + + private: + RECORD::TYPE type; + FormsMap forms{}; + + void Link(Form*, Scope, const FormVec& linkedForms, const IndexOrCount&, const PercentChance&, const 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 forms. + /// + /// A DataHandler that will perform the actual lookup. + /// A raw linked form entries that should be processed. + void LookupLinkedForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsConfig& rawLinkedForms = INI::linkedForms); + + void LogLinkedFormsLookup(); + + /// + /// 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 DistributedForms& linkedForms, std::function distribute); + + /// + /// 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 DistributedForms& linkedForms, std::function distribute); + + private: + template + 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 }; + 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 sleepOutfits{ RECORD::kSleepOutfit }; + 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, 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(""), e.expectedFormType, e.actualFormType); + }, + [&](std::string editorID) { + 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(""), e.formType); + }, + [&](std::string editorID) { + buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - unsupported form type ({})", e.path, editorID, e.formType); + } }, + e.formOrEditorID); + } + return nullptr; + } + + template + DataVec& Manager::LinkedFormsForForm(const DistributedForm& form, Scope scope, LinkedForms& linkedForms) const + { + 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; + } + + template + void Manager::ForEachLinkedForms(Func&& func, 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(deathItems, std::forward(args)...); + func(outfits, std::forward(args)...); + func(sleepOutfits, std::forward(args)...); + func(factions, std::forward(args)...); + func(packages, std::forward(args)...); + func(skins, std::forward(args)...); + } + + template + void LinkedForms::LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms) + { + for (auto& rawForm : rawLinkedForms) { + 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); + } + } + } + } + + template + void LinkedForms::Link(Form* form, Scope scope, const FormVec& linkedForms, const IndexOrCount& idxOrCount, const PercentChance& chance, const Path& path) + { + 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& 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); + } + } + } +#pragma endregion +} diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index b3111f8..7431303 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,36 @@ 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 names{ + "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::names.at(aType); + } + + template + constexpr TYPE GetType(const T& aType) + { + using namespace detail; + return static_cast(std::distance(names.begin(), std::find(names.begin(), names.end(), aType))); + } } namespace INI @@ -46,13 +79,14 @@ namespace INI Filters rawFormFilters{}; LevelFilters levelFilters{}; Traits traits{}; - IdxOrCount idxOrCount{ 1 }; - Chance chance{ 100 }; + IndexOrCount idxOrCount{ RandomCount(1, 1) }; + PercentChance chance{ 100 }; std::string path{}; }; + using DataVec = std::vector; - inline StringMap configs{}; + inline Map configs{}; std::pair GetConfigs(); } diff --git a/SPID/include/LookupFilters.h b/SPID/include/LookupFilters.h index cf2b448..01c8161 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 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/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/include/LookupNPC.h b/SPID/include/LookupNPC.h index 7f3be01..b01bfff 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 exclusive groups configuration. + /// + /// A Form that needs to be checked. + /// + [[nodiscard]] bool HasMutuallyExclusiveForm(RE::TESForm* otherForm) const; + [[nodiscard]] std::uint16_t GetLevel() const; [[nodiscard]] bool IsChild() const; [[nodiscard]] bool IsLeveled() const; @@ -29,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/Distribute.cpp b/SPID/src/Distribute.cpp index 2f3d4de..7ef6e96 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 { @@ -12,133 +13,230 @@ namespace Distribute REL::Relocation func{ RELOCATION_ID(55945, 56489) }; return func(a_actor, a_item, a_itemCount, true, 0, RE::BSScript::Internal::VirtualMachine::GetSingleton()); } - } - 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. + /// 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, bool allowOverwrites, DistributedForms* accumulatedForms) + { + const auto npc = npcData.GetNPC(); + + for_each_form( + npcData, forms.keywords, input, [&](const std::vector& a_keywords) { + npc->AddKeywords(a_keywords); + }, + accumulatedForms); + + 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( + npcData, forms.spells, input, [&](const std::vector& a_spells) { + npc->GetSpellList()->AddSpells(a_spells); + }, + accumulatedForms); + + for_each_form( + npcData, forms.levSpells, input, [&](const std::vector& a_levSpells) { + npc->GetSpellList()->AddLevSpells(a_levSpells); + }, + accumulatedForms); + + for_each_form( + npcData, forms.perks, input, [&](const std::vector& a_perks) { + npc->AddPerks(a_perks, 1); + }, + accumulatedForms); + + for_each_form( + npcData, forms.shouts, input, [&](const std::vector& a_shouts) { + npc->GetSpellList()->AddShouts(a_shouts); + }, + accumulatedForms); + + for_each_form( + 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)) { + auto package = a_packageOrList->As(); + + if (packageIdx > 0) { + --packageIdx; //get actual position we want to insert at + } - 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::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::perks, a_input, [&](const std::vector& a_perks) { - npc->AddPerks(a_perks, 1); - }); - - 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::levSpells, a_input, [&](const std::vector& a_levSpells) { - npc->GetSpellList()->AddLevSpells(a_levSpells); - }); - - 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::packages, a_input, [&](auto* a_packageOrList, [[maybe_unused]] IdxOrCount a_idx) { - auto packageIdx = a_idx; - - if (a_packageOrList->Is(RE::FormType::Package)) { - auto package = a_packageOrList->As(); - - 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 false; + }, + accumulatedForms); + + for_each_form( + npcData, forms.outfits, input, [&](auto* a_outfit) { + if (npc->defaultOutfit != a_outfit && (allowOverwrites || !npc->HasKeyword(processedOutfit))) { + npc->AddKeyword(processedOutfit); + npc->defaultOutfit = a_outfit; + return true; + } + return false; + }, + accumulatedForms); + + 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( + npcData, forms.items, input, [&](std::map& a_objects) { + return npc->AddObjectsToContainer(a_objects, npc); + }, + accumulatedForms); + + 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( + npcData, forms.deathItems, input, [&](auto* deathItem, IndexOrCount idxOrCount) { + auto count = std::get(idxOrCount); + + detail::add_item(npcData.GetActor(), deathItem, count.GetRandom()); 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; - }); - - for_each_form(a_npcData, Forms::outfits, a_input, [&](auto* a_outfit) { - if (npc->defaultOutfit != a_outfit && !npc->HasKeyword(processedOutfit)) { - npc->AddKeyword(processedOutfit); - npc->defaultOutfit = a_outfit; - 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; - }); - - for_each_form(a_npcData, Forms::items, a_input, [&](std::map& a_objects) { - return npc->AddObjectsToContainer(a_objects, npc); - }); - - 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); + } } - void Distribute(NPCData& a_npcData, bool a_onlyLeveledEntries) + void Distribute(NPCData& npcData, const PCLevelMult::Input& input) { - const auto input = PCLevelMult::Input{ a_npcData.GetActor(), a_npcData.GetNPC(), a_onlyLeveledEntries }; - Distribute(a_npcData, input); + if (input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(input)) { + return; + } + + Forms::DistributionSet entries{ + 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(input.onlyPlayerLevelEntries), + Forms::sleepOutfits.GetForms(input.onlyPlayerLevelEntries), + Forms::skins.GetForms(input.onlyPlayerLevelEntries) + }; + + DistributedForms distributedForms{}; + + detail::distribute(npcData, input, entries, false, &distributedForms); + // TODO: We can now log per-NPC distributed forms. + + if (!distributedForms.empty()) { + // 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, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. + }); + } + } + + 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) + { + DistributedForms 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, 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, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. + }); + } } } diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 33d2d13..3b9e91f 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(); @@ -189,13 +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, input, [&](auto* a_deathItem, IdxOrCount a_count) { - detail::add_item(actor, a_deathItem, a_count); - return true; - }); + DistributeDeathItems(npcData, input); } } diff --git a/SPID/src/ExclusiveGroups.cpp b/SPID/src/ExclusiveGroups.cpp new file mode 100644 index 0000000..21e37f7 --- /dev/null +++ b/SPID/src/ExclusiveGroups.cpp @@ -0,0 +1,117 @@ +#include "ExclusiveGroups.h" +#include "FormData.h" + +namespace ExclusiveGroups +{ + bool INI::TryParse(const std::string& a_key, const std::string& a_value, const Path& 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& [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(); }); + + for (auto& [name, forms] : groups) { + for (auto& form : forms) { + linkedGroups[form].insert(name); + } + } + } + + 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)); + } + } + } + + 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 Group& name) { + const auto& group = groups.at(name); + forms.insert(group.begin(), group.end()); + }); + } + + // Remove self from the list. + forms.erase(form); + + return forms; + } + + const GroupFormsMap& ExclusiveGroups::Manager::GetGroups() const + { + return groups; + } +} 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/KeywordDependencies.cpp b/SPID/src/KeywordDependencies.cpp index dedc37d..d0d89c9 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; } @@ -145,7 +145,7 @@ void Dependencies::ResolveKeywords() timer.end(); 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) { diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp new file mode 100644 index 0000000..b06ed58 --- /dev/null +++ b/SPID/src/LinkedDistribution.cpp @@ -0,0 +1,320 @@ +#include "LinkedDistribution.h" +#include "FormData.h" +#include "LookupConfigs.h" + +namespace LinkedDistribution +{ + + using namespace Forms; + +#pragma region Parsing + namespace INI + { + enum Sections : std::uint8_t + { + kForm = 0, + kLinkedForms, + kIdxOrCount, + kChance, + + // Minimum required sections + kRequired = kLinkedForms + }; + + 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; + } + + 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, 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, key, value); + return true; + } + + INI::RawLinkedForm item{}; + item.formOrEditorID = distribution::get_record(sections[kForm]); + item.scope = scope; + item.path = path; + + for (auto& IDs : split_IDs) { + item.formIDs.MATCH.push_back(distribution::get_record(IDs)); + } + + if (type == RECORD::kPackage) { // reuse item count for package stack index + item.idxOrCount = 0; + } + + 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. + } + } + } + } + + if (kChance < size) { + if (const auto& str = sections[kChance]; distribution::is_valid_entry(str)) { + item.chance = string::to_num(str); + } + } + + linkedForms[type].push_back(item); + + return true; + } + } +#pragma endregion + +#pragma region Lookup + + void Manager::LookupLinkedForms(RE::TESDataHandler* dataHandler, INI::LinkedFormsConfig& rawLinkedForms) + { + 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) { + 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, 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) { + shouts.Link(shout, scope, parentForms, idxOrCount, chance, path); + } else if (const auto item = form->As(); item) { + items.Link(item, scope, parentForms, idxOrCount, chance, path); + } else if (const auto outfit = form->As(); outfit) { + 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 { + 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("\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, scope, parentForms, packageIndex, chance, path); + } else { + logger::warn("\t[{}] Unsupported Form type: {}", path, type); + } + } + } + } + + // 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 + rawLinkedForms.clear(); + + // Clear logger's buffer to free some memory :) + buffered_logger::clear(); + } + + void Manager::LogLinkedFormsLookup() + { + logger::info("{:*^50}", "LINKED ITEMS"); + + ForEachLinkedForms([](LinkedForms& linkedForms) { + if (linkedForms.GetForms().empty()) { + return; + } + + std::unordered_map> map{}; + + // Iterate through the original map + for (const auto& pair : linkedForms.GetForms()) { + 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); + } + } + } + + const auto& recordName = RECORD::GetTypeName(linkedForms.GetType()); + logger::info("Linked {}s: ", recordName); + + for (const auto& [form, linkedForms] : map) { + logger::info("\t{}", describe(form)); + + const auto lastItemIndex = linkedForms.size() - 1; + for (int i = 0; i < lastItemIndex; ++i) { + const auto& linkedItem = linkedForms[i]; + logger::info("\t├─── {}", describe(linkedItem)); + } + const auto& lastLinkedItem = linkedForms[lastItemIndex]; + logger::info("\t└─── {}", describe(lastLinkedItem)); + } + }); + } +#pragma endregion + +#pragma region Distribution + void Manager::ForEachLinkedDistributionSet(const DistributedForms& targetForms, Scope scope, std::function performDistribution) + { + for (const auto& form : targetForms) { + 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, + linkedPerks, + linkedItems, + linkedShouts, + linkedLevSpells, + linkedPackages, + linkedOutfits, + linkedKeywords, + DistributionSet::empty(), // deathItems are distributed only on death :) as such, linked items are also distributed only on death. + linkedFactions, + linkedSleepOutfits, + linkedSkins + }; + + if (linkedEntries.IsEmpty()) { + continue; + } + + performDistribution(linkedEntries); + } + } + + 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, scope, 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); + } + } + + void Manager::ForEachLinkedDeathDistributionSet(const DistributedForms& targetForms, std::function performDistribution) + { + ForEachLinkedDeathDistributionSet(targetForms, Scope::kLocal, performDistribution); + ForEachLinkedDeathDistributionSet(targetForms, Scope::kGlobal, performDistribution); + } +#pragma endregion +} diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 8e42af4..11d30e6 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -1,4 +1,6 @@ #include "LookupConfigs.h" +#include "ExclusiveGroups.h" +#include "LinkedDistribution.h" namespace INI { @@ -46,7 +48,7 @@ namespace INI return newValue; } - 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 Path& a_path) { Data data{}; @@ -204,19 +206,35 @@ 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 (const auto& str = sections[kIdxOrCount]; distribution::is_valid_entry(str)) { - data.idxOrCount = string::to_num(str); + 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); + } + } 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.idxOrCount = RandomCount(minCount, maxCount); + } else { + auto count = string::to_num(str); + + data.idxOrCount = RandomCount(count, count); // create the exact match range. + } + } } } //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); } } @@ -263,14 +281,28 @@ namespace INI for (auto& [key, entry] : *values) { try { - auto [data, sanitized_str] = detail::parse_ini(key.pItem, entry, truncatedPath); - configs[key.pItem].emplace_back(data); + if (ExclusiveGroups::INI::TryParse(key.pItem, entry, truncatedPath)) { + continue; + } + + if (LinkedDistribution::INI::TryParse(key.pItem, entry, truncatedPath)) { + continue; + } + + 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); if (sanitized_str) { 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/LookupFilters.cpp b/SPID/src/LookupFilters.cpp index ba60729..edc395b 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,8 +192,8 @@ 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) { + const auto randNum = RNG().generate(); if (randNum > chance) { return Result::kFailRNG; } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index d0a221b..f875be4 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -1,47 +1,108 @@ #include "LookupForms.h" +#include "ExclusiveGroups.h" #include "FormData.h" #include "KeywordDependencies.h" +#include "LinkedDistribution.h" -bool Lookup::LookupForms() +bool LookupDistributables(RE::TESDataHandler* const dataHandler) { using namespace Forms; - bool valid = false; + 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()]); + }); - if (const auto dataHandler = RE::TESDataHandler::GetSingleton()) { - ForEachDistributable([&](Distributables& a_distributable) { - const auto& recordName = RECORD::add[a_distributable.GetType()]; + // Sort out Spells and Leveled Spells into two separate lists. + auto& rawSpells = INI::configs[RECORD::kSpell]; - a_distributable.LookupForms(dataHandler, recordName, INI::configs[recordName]); - if constexpr (std::is_same_v) { - Dependencies::ResolveKeywords(); + 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); } - a_distributable.FinishLookupForms(); + }); + } - if (a_distributable) { - valid = true; + auto& genericForms = INI::configs[RECORD::kForm]; + + for (auto& rawForm : genericForms) { + // 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) { + shouts.EmplaceForm(isValid, shout, idxOrCount, filters, path); + } else if (const auto item = form->As(); item) { + items.EmplaceForm(isValid, item, idxOrCount, filters, path); + } else if (const auto outfit = form->As(); outfit) { + outfits.EmplaceForm(isValid, outfit, idxOrCount, filters, path); + } else if (const auto faction = form->As(); faction) { + factions.EmplaceForm(isValid, faction, 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("\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, type); + } } }); } + Dependencies::ResolveKeywords(); + + bool valid = false; + + ForEachDistributable([&](Distributables& a_distributable) { + a_distributable.FinishLookupForms(); + if (a_distributable) { + valid = true; + } + }); + return valid; } -void Lookup::LogFormLookup() +void LogDistributablesLookup() { using namespace Forms; logger::info("{:*^50}", "PROCESSING"); - ForEachDistributable([](const Distributables& a_distributable) { - const auto& recordName = RECORD::add[a_distributable.GetType()]; + ForEachDistributable([](Distributables& a_distributable) { + const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); - const auto all = INI::configs[recordName].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("Adding {}/{} {}s", added, all, recordName); + logger::info("Registered {}/{} {}s", added, all, recordName); } }); @@ -52,20 +113,53 @@ void Lookup::LogFormLookup() buffered_logger::clear(); } -bool Lookup::DoFormLookup() +// 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 LookupExclusiveGroups(RE::TESDataHandler* const dataHandler) +{ + ExclusiveGroups::Manager::GetSingleton()->LookupExclusiveGroups(dataHandler); +} + +void LogExclusiveGroupsLookup() +{ + ExclusiveGroups::Manager::GetSingleton()->LogExclusiveGroupsLookup(); +} + +void LookupLinkedForms(RE::TESDataHandler* const dataHandler) { - logger::info("{:*^50}", "LOOKUP"); + LinkedDistribution::Manager::GetSingleton()->LookupLinkedForms(dataHandler); +} + +void LogLinkedFormsLookup() +{ + LinkedDistribution::Manager::GetSingleton()->LogLinkedFormsLookup(); +} + +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 timer; + LookupLinkedForms(dataHandler); + LogLinkedFormsLookup(); - timer.start(); - const bool success = LookupForms(); - timer.end(); + LookupExclusiveGroups(dataHandler); + LogExclusiveGroupsLookup(); - 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 8fc9d3a..4895ff6 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -1,8 +1,9 @@ #include "LookupNPC.h" +#include 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)) {} @@ -41,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); @@ -182,6 +187,20 @@ namespace NPC } } + bool Data::HasMutuallyExclusiveForm(RE::TESForm* a_form) const + { + auto excludedForms = ExclusiveGroups::Manager::GetSingleton()->MutuallyExclusiveFormsForForm(a_form); + if (excludedForms.empty()) { + 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); + }); + } + std::uint16_t Data::GetLevel() const { return level; 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(); } 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 ) 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