From 06ed7c90616c0153e8d919a27fc1caf0221afbed Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 7 Apr 2024 18:45:02 +0300 Subject: [PATCH] Unified Death Distribution. On Death distribution will support everything that original distribution does. --- SPID/cmake/headerlist.cmake | 1 + SPID/cmake/sourcelist.cmake | 1 + SPID/include/DeathDistribution.h | 76 +++++ SPID/include/Defs.h | 6 +- SPID/include/Distribute.h | 20 +- SPID/include/DistributeManager.h | 2 - SPID/include/FormData.h | 43 ++- SPID/include/LinkedDistribution.h | 16 +- SPID/include/LookupConfigs.h | 55 ++-- SPID/src/DeathDistribution.cpp | 401 ++++++++++++++++++++++++ SPID/src/Distribute.cpp | 304 ++++++++---------- SPID/src/DistributeManager.cpp | 27 +- SPID/src/FormData.cpp | 2 +- SPID/src/LinkedDistribution.cpp | 15 +- SPID/src/LookupConfigs.cpp | 496 +++++++++++++++--------------- SPID/src/LookupForms.cpp | 32 +- SPID/src/LookupNPC.cpp | 2 +- SPID/src/main.cpp | 2 +- 18 files changed, 973 insertions(+), 528 deletions(-) create mode 100644 SPID/include/DeathDistribution.h create mode 100644 SPID/src/DeathDistribution.cpp diff --git a/SPID/cmake/headerlist.cmake b/SPID/cmake/headerlist.cmake index e233a89..a21d87b 100644 --- a/SPID/cmake/headerlist.cmake +++ b/SPID/cmake/headerlist.cmake @@ -1,5 +1,6 @@ set(headers ${headers} include/Cache.h + include/DeathDistribution.h include/Defs.h include/DependencyResolver.h include/Distribute.h diff --git a/SPID/cmake/sourcelist.cmake b/SPID/cmake/sourcelist.cmake index c78269e..fb8ad74 100644 --- a/SPID/cmake/sourcelist.cmake +++ b/SPID/cmake/sourcelist.cmake @@ -1,5 +1,6 @@ set(sources ${sources} src/Cache.cpp + src/DeathDistribution.cpp src/Distribute.cpp src/DistributeManager.cpp src/DistributePCLevelMult.cpp diff --git a/SPID/include/DeathDistribution.h b/SPID/include/DeathDistribution.h new file mode 100644 index 0000000..9bdec63 --- /dev/null +++ b/SPID/include/DeathDistribution.h @@ -0,0 +1,76 @@ +#pragma once +#include "FormData.h" + +namespace DeathDistribution +{ + namespace INI + { + /// + /// Checks whether given entry is an on death distribuatble form and attempts to parse it. + /// + /// true if given entry was an on death distribuatble form. Note that returned value doesn't represent whether parsing was successful. + bool TryParse(const std::string& key, const std::string& value, const Path&); + } + + using namespace Forms; + + class Manager : + public ISingleton, + public RE::BSTEventSink + { + public: + static void Register(); + + /// + /// Does a forms lookup similar to what Filters do. + /// + /// As a result this method configures Manager with discovered valid On Death Distributable Forms. + /// + /// A DataHandler that will perform the actual lookup. + void LookupForms(RE::TESDataHandler* const dataHandler); + + void LogFormsLookup(); + + bool IsEmpty(); + + private: + Distributables spells{ RECORD::kSpell }; + Distributables perks{ RECORD::kPerk }; + Distributables items{ RECORD::kItem }; + Distributables shouts{ RECORD::kShout }; + Distributables levSpells{ RECORD::kLevSpell }; + Distributables packages{ RECORD::kPackage }; + Distributables outfits{ RECORD::kOutfit }; + Distributables keywords{ RECORD::kKeyword }; + Distributables factions{ RECORD::kFaction }; + Distributables sleepOutfits{ RECORD::kSleepOutfit }; + Distributables skins{ RECORD::kSkin }; + + /// + /// Iterates over each type of LinkedForms and calls a callback with each of them. + /// + template + void ForEachDistributable(Func&& func, Args&&... args); + + protected: + RE::BSEventNotifyControl ProcessEvent(const RE::TESDeathEvent*, RE::BSTEventSource*) override; + }; + +#pragma region Implementation + template + void Manager::ForEachDistributable(Func&& func, Args&&... args) + { + func(keywords, std::forward(args)...); + func(factions, std::forward(args)...); + func(spells, std::forward(args)...); + func(levSpells, std::forward(args)...); + func(perks, std::forward(args)...); + func(shouts, std::forward(args)...); + func(packages, std::forward(args)...); + func(outfits, std::forward(args)...); + func(sleepOutfits, std::forward(args)...); + func(skins, std::forward(args)...); + func(items, std::forward(args)...); + } +#pragma endregion +} diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index fd2d450..d3a21a8 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -78,9 +78,9 @@ struct SkillLevel struct LevelFilters { - Range actorLevel; - std::vector skillLevels; // skill levels - std::vector skillWeights; // skill weights (from Class) + Range actorLevel{}; + std::vector skillLevels{}; // skill levels + std::vector skillWeights{}; // skill weights (from Class) }; struct Traits diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 065a1ac..c052b94 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -44,6 +44,9 @@ namespace Distribute return a_formData.filters.PassedFilters(a_npcData) == Filter::Result::kPass; } + /// + /// Check that NPC doesn't already have the form that is about to be distributed. + /// template bool has_form(RE::TESNPC* a_npc, Form* a_form) { @@ -66,15 +69,13 @@ namespace Distribute return false; } } - - void add_item(RE::Actor* a_actor, RE::TESBoundObject* a_item, std::uint32_t a_itemCount); } using namespace Forms; -#pragma region Packages, Death Items +#pragma region Packages // old method (distributing one by one) - // for now, only packages/death items use this + // for now, only packages use this template void for_each_form( const NPCData& a_npcData, @@ -221,8 +222,15 @@ namespace Distribute } #pragma endregion + /// + /// 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 = nullptr); void Distribute(NPCData& npcData, const PCLevelMult::Input& input); void Distribute(NPCData& npcData, bool onlyLeveledEntries); - - void DistributeDeathItems(NPCData& npcData, const PCLevelMult::Input& input); } diff --git a/SPID/include/DistributeManager.h b/SPID/include/DistributeManager.h index 29ae9ba..283b65f 100644 --- a/SPID/include/DistributeManager.h +++ b/SPID/include/DistributeManager.h @@ -20,14 +20,12 @@ namespace Distribute { class Manager : public ISingleton, - public RE::BSTEventSink, public RE::BSTEventSink { public: static void Register(); protected: - RE::BSEventNotifyControl ProcessEvent(const RE::TESDeathEvent* a_event, RE::BSTEventSource*) override; RE::BSEventNotifyControl ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) override; }; } diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index e2fe3ac..7a10d7d 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -395,7 +395,6 @@ namespace Forms DataVec& packages; DataVec& outfits; DataVec& keywords; - DataVec& deathItems; DataVec& factions; DataVec& sleepOutfits; DataVec& skins; @@ -435,7 +434,7 @@ namespace Forms DataVec
& GetForms(bool a_onlyLevelEntries); DataVec& GetForms(); - void LookupForms(RE::TESDataHandler*, std::string_view a_type, INI::DataVec&); + void LookupForms(RE::TESDataHandler*, std::string_view a_type, Configs::INI::DataVec&); void EmplaceForm(bool isValid, Form*, const IndexOrCount&, const FilterData&, const Path&); // Init formsWithLevels and formsNoLevels @@ -450,7 +449,7 @@ namespace Forms /// This counter is used for logging purposes. std::size_t lookupCount{ 0 }; - void LookupForm(RE::TESDataHandler*, INI::Data&); + void LookupForm(RE::TESDataHandler*, Configs::INI::Data&); }; inline Distributables spells{ RECORD::kSpell }; @@ -461,7 +460,6 @@ namespace Forms inline Distributables packages{ RECORD::kPackage }; inline Distributables outfits{ RECORD::kOutfit }; inline Distributables keywords{ RECORD::kKeyword }; - inline Distributables deathItems{ RECORD::kDeathItem }; inline Distributables factions{ RECORD::kFaction }; inline Distributables sleepOutfits{ RECORD::kSleepOutfit }; inline Distributables skins{ RECORD::kSkin }; @@ -470,20 +468,19 @@ namespace Forms std::size_t GetTotalLeveledEntries(); template - void ForEachDistributable(Func&& a_func, Args&&... args) + void ForEachDistributable(Func&& func, Args&&... args) { - a_func(keywords, std::forward(args)...); - a_func(factions, std::forward(args)...); - a_func(perks, std::forward(args)...); - a_func(spells, std::forward(args)...); - a_func(levSpells, std::forward(args)...); - a_func(shouts, std::forward(args)...); - a_func(items, std::forward(args)...); - a_func(deathItems, std::forward(args)...); - a_func(outfits, std::forward(args)...); - a_func(sleepOutfits, std::forward(args)...); - a_func(packages, std::forward(args)...); - a_func(skins, std::forward(args)...); + func(keywords, std::forward(args)...); + func(factions, std::forward(args)...); + func(spells, std::forward(args)...); + func(levSpells, std::forward(args)...); + func(perks, std::forward(args)...); + func(shouts, std::forward(args)...); + func(packages, std::forward(args)...); + func(outfits, std::forward(args)...); + func(sleepOutfits, std::forward(args)...); + func(skins, std::forward(args)...); + func(items, std::forward(args)...); } /// @@ -495,7 +492,7 @@ namespace Forms /// A raw form entry that needs to be looked up. /// A callback to be called with validated data after successful lookup. template - void LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback); + void LookupGenericForm(RE::TESDataHandler* const dataHandler, Configs::INI::Data& rawForm, std::function callback); } template @@ -553,7 +550,7 @@ Forms::DataVec& Forms::Distributables::GetForms(bool a_onlyLevelEntr } template -void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, INI::Data& rawForm) +void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, Configs::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); @@ -561,7 +558,7 @@ void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, IN } template -void Forms::Distributables::LookupForms(RE::TESDataHandler* dataHandler, std::string_view a_type, INI::DataVec& a_INIDataVec) +void Forms::Distributables::LookupForms(RE::TESDataHandler* dataHandler, std::string_view a_type, Configs::INI::DataVec& a_INIDataVec) { if (a_INIDataVec.empty()) { return; @@ -609,7 +606,7 @@ void Forms::Distributables::FinishLookupForms() } template -void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& rawForm, std::function callback) +void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, Configs::INI::Data& rawForm, std::function callback) { auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] = rawForm; @@ -652,9 +649,9 @@ void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, INI::Data& 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) { + } catch (const Lookup::InvalidFormTypeException&) { // Whitelisting is disabled, so this should not occur - } catch (const Lookup::UnknownPluginException& e) { + } catch (const Lookup::UnknownPluginException&) { // Likewise, we don't expect plugin names in distributable forms. } } diff --git a/SPID/include/LinkedDistribution.h b/SPID/include/LinkedDistribution.h index e5ab30a..d66c6a7 100644 --- a/SPID/include/LinkedDistribution.h +++ b/SPID/include/LinkedDistribution.h @@ -47,7 +47,7 @@ namespace LinkedDistribution /// /// Checks whether given entry is a linked form and attempts to parse it. /// - /// true if given entry was a linked form. Note that returned value doesn't represent whether or parsing was successful. + /// true if given entry was a linked form. Note that returned value doesn't represent whether parsing was successful. bool TryParse(const std::string& key, const std::string& value, const Path&); } @@ -129,7 +129,6 @@ namespace LinkedDistribution LinkedForms spells{ RECORD::kSpell }; LinkedForms perks{ RECORD::kPerk }; LinkedForms items{ RECORD::kItem }; - LinkedForms deathItems{ RECORD::kDeathItem }; LinkedForms shouts{ RECORD::kShout }; LinkedForms levSpells{ RECORD::kLevSpell }; LinkedForms packages{ RECORD::kPackage }; @@ -139,6 +138,18 @@ namespace LinkedDistribution LinkedForms factions{ RECORD::kFaction }; LinkedForms skins{ RECORD::kSkin }; + LinkedForms deathSpells{ RECORD::kSpell }; + LinkedForms deathPerks{ RECORD::kPerk }; + LinkedForms deathItems{ RECORD::kItem }; + LinkedForms deathShouts{ RECORD::kShout }; + LinkedForms deathLevSpells{ RECORD::kLevSpell }; + LinkedForms deathPackages{ RECORD::kPackage }; + LinkedForms deathOutfits{ RECORD::kOutfit }; + LinkedForms deathSleepOutfits{ RECORD::kSleepOutfit }; + LinkedForms deathKeywords{ RECORD::kKeyword }; + LinkedForms deathFactions{ RECORD::kFaction }; + LinkedForms deathSkins{ RECORD::kSkin }; + /// /// Iterates over each type of LinkedForms and calls a callback with each of them. /// @@ -217,7 +228,6 @@ namespace LinkedDistribution func(perks, std::forward(args)...); func(shouts, std::forward(args)...); func(items, std::forward(args)...); - func(deathItems, std::forward(args)...); func(outfits, std::forward(args)...); func(sleepOutfits, std::forward(args)...); func(factions, std::forward(args)...); diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 7431303..4722e8b 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -57,36 +57,39 @@ namespace RECORD } } -namespace INI +namespace Configs { - enum TYPE : std::uint32_t + namespace INI { - kFormIDPair = 0, - kFormID = kFormIDPair, - kStrings, - kESP = kStrings, - kFilterIDs, - kLevel, - kTraits, - kIdxOrCount, - kChance - }; + enum TYPE : std::uint32_t + { + kFormIDPair = 0, + kFormID = kFormIDPair, + kStrings, + kESP = kStrings, + kFilterIDs, + kLevel, + kTraits, + kIdxOrCount, + kChance + }; - struct Data - { - FormOrEditorID rawForm{}; - StringFilters stringFilters{}; - Filters rawFormFilters{}; - LevelFilters levelFilters{}; - Traits traits{}; - IndexOrCount idxOrCount{ RandomCount(1, 1) }; - PercentChance chance{ 100 }; - std::string path{}; - }; + struct Data + { + FormOrEditorID rawForm{}; + StringFilters stringFilters{}; + Filters rawFormFilters{}; + LevelFilters levelFilters{}; + Traits traits{}; + IndexOrCount idxOrCount{ RandomCount(1, 1) }; + PercentChance chance{ 100 }; + std::string path{}; + }; - using DataVec = std::vector; + using DataVec = std::vector; - inline Map configs{}; + inline Map configs{}; - std::pair GetConfigs(); + std::pair GetConfigs(); + } } diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp new file mode 100644 index 0000000..22d0762 --- /dev/null +++ b/SPID/src/DeathDistribution.cpp @@ -0,0 +1,401 @@ +#include "DeathDistribution.h" +#include "Distribute.h" +#include "LinkedDistribution.h" +#include "LookupNPC.h" +#include "PCLevelMultManager.h" + +namespace DeathDistribution +{ +#pragma region Parsing + namespace INI + { + enum Sections : std::uint32_t + { + kForm = 0, + kStrings, + kFilterIDs, + kLevels, + kTraits, + kIdxOrCount, + kChance, + + kRequired = kForm + }; + + using Data = Configs::INI::Data; + using DataVec = Configs::INI::DataVec; + + Map deathConfigs{}; + + bool TryParse(const std::string& key, const std::string& value, const Path& path) + { + if (!key.starts_with("Death"sv)) { + return false; + } + + std::string rawType = key.substr(5); + auto type = RECORD::GetType(rawType); + + if (type == RECORD::kTotal) { + logger::warn("IGNORED: Unsupported Form type ({}): {} = {}"sv, rawType, key, value); + return true; + } + + const auto sections = string::split(value, "|"); + const auto size = sections.size(); + + if (size <= kRequired) { + logger::warn("IGNORED: Entry must at least FormID or EditorID: {} = {}"sv, key, value); + return true; + } + + Data data{}; + + data.rawForm = distribution::get_record(sections[kForm]); + + //KEYWORDS + if (kStrings < size) { + StringFilters filters; + + auto split_str = distribution::split_entry(sections[kStrings]); + for (auto& str : split_str) { + if (str.contains("+"sv)) { + auto strings = distribution::split_entry(str, "+"); + data.stringFilters.ALL.insert(data.stringFilters.ALL.end(), strings.begin(), strings.end()); + + } else if (str.at(0) == '-') { + str.erase(0, 1); + data.stringFilters.NOT.emplace_back(str); + + } else if (str.at(0) == '*') { + str.erase(0, 1); + data.stringFilters.ANY.emplace_back(str); + + } else { + data.stringFilters.MATCH.emplace_back(str); + } + } + } + + //FILTER FORMS + if (kFilterIDs < size) { + auto split_IDs = distribution::split_entry(sections[kFilterIDs]); + for (auto& IDs : split_IDs) { + if (IDs.contains("+"sv)) { + auto splitIDs_ALL = distribution::split_entry(IDs, "+"); + for (auto& IDs_ALL : splitIDs_ALL) { + data.rawFormFilters.ALL.push_back(distribution::get_record(IDs_ALL)); + } + } else if (IDs.at(0) == '-') { + IDs.erase(0, 1); + data.rawFormFilters.NOT.push_back(distribution::get_record(IDs)); + + } else { + data.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + } + } + } + //LEVEL + if (kLevels < size) { + Range actorLevel; + std::vector skillLevels; + std::vector skillWeights; + auto split_levels = distribution::split_entry(sections[kLevels]); + for (auto& levels : split_levels) { + if (levels.contains('(')) { + //skill(min/max) + const auto isWeightFilter = levels.starts_with('w'); + auto sanitizedLevel = string::remove_non_alphanumeric(levels); + if (isWeightFilter) { + sanitizedLevel.erase(0, 1); + } + //skill min max + if (auto skills = string::split(sanitizedLevel, " "); !skills.empty()) { + if (auto type = string::to_num(skills[0]); type < 18) { + auto minLevel = string::to_num(skills[1]); + if (skills.size() > 2) { + auto maxLevel = string::to_num(skills[2]); + if (isWeightFilter) { + skillWeights.push_back({ type, Range(minLevel, maxLevel) }); + } else { + skillLevels.push_back({ type, Range(minLevel, maxLevel) }); + } + } else { + if (isWeightFilter) { + // Single value is treated as exact match. + skillWeights.push_back({ type, Range(minLevel) }); + } else { + skillLevels.push_back({ type, Range(minLevel) }); + } + } + } + } + } else { + if (auto actor_level = string::split(levels, "/"); actor_level.size() > 1) { + auto minLevel = string::to_num(actor_level[0]); + auto maxLevel = string::to_num(actor_level[1]); + + actorLevel = Range(minLevel, maxLevel); + } else { + auto level = string::to_num(levels); + + actorLevel = Range(level); + } + } + } + data.levelFilters = { actorLevel, skillLevels, skillWeights }; + } + + //TRAITS + if (kTraits < size) { + auto split_traits = distribution::split_entry(sections[kTraits], "/"); + for (auto& trait : split_traits) { + switch (string::const_hash(trait)) { + case "M"_h: + case "-F"_h: + data.traits.sex = RE::SEX::kMale; + break; + case "F"_h: + case "-M"_h: + data.traits.sex = RE::SEX::kFemale; + break; + case "U"_h: + data.traits.unique = true; + break; + case "-U"_h: + data.traits.unique = false; + break; + case "S"_h: + data.traits.summonable = true; + break; + case "-S"_h: + data.traits.summonable = false; + break; + case "C"_h: + data.traits.child = true; + break; + case "-C"_h: + data.traits.child = false; + break; + case "L"_h: + data.traits.leveled = true; + break; + case "-L"_h: + data.traits.leveled = false; + break; + case "T"_h: + data.traits.teammate = true; + break; + case "-T"_h: + data.traits.teammate = false; + break; + default: + break; + } + } + } + + //ITEMCOUNT/INDEX + if (type == RECORD::kPackage) { // reuse item count for package stack index + data.idxOrCount = 0; + } + + if (kIdxOrCount < size) { + if (type == 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.path = path; + + deathConfigs[type].emplace_back(data); + return true; + } + } +#pragma endregion + + void Manager::Register() + { + if (INI::deathConfigs.empty()) { + return; + } + + if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { + scripts->AddEventSink(GetSingleton()); + logger::info("Registered for {}", typeid(RE::TESDeathEvent).name()); + } + } + + void Manager::LookupForms(RE::TESDataHandler* const dataHandler) + { + using namespace Forms; + + 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) { + const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); + + a_distributable.LookupForms(dataHandler, recordName, INI::deathConfigs[a_distributable.GetType()]); + } + }); + + // Sort out Spells and Leveled Spells into two separate lists. + auto& rawSpells = INI::deathConfigs[RECORD::kSpell]; + + for (auto& rawSpell : rawSpells) { + LookupGenericForm(dataHandler, rawSpell, [&](bool isValid, auto form, const auto& idxOrCount, const auto& filters, const auto& path) { + if (const auto spell = form->As(); spell) { + spells.EmplaceForm(isValid, spell, idxOrCount, filters, path); + } else if (const auto levSpell = form->As(); levSpell) { + levSpells.EmplaceForm(isValid, levSpell, idxOrCount, filters, path); + } + }); + } + + auto& genericForms = INI::deathConfigs[RECORD::kForm]; + + for (auto& rawForm : genericForms) { + // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit, Skin) + 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); + } + } + }); + } + } + + bool Manager::IsEmpty() + { + return spells.GetForms().empty() && + perks.GetForms().empty() && + items.GetForms().empty() && + shouts.GetForms().empty() && + levSpells.GetForms().empty() && + packages.GetForms().empty() && + outfits.GetForms().empty() && + keywords.GetForms().empty() && + factions.GetForms().empty() && + sleepOutfits.GetForms().empty() && + skins.GetForms().empty(); + } + + void Manager::LogFormsLookup() + { + if (IsEmpty()) { + return; + } + + using namespace Forms; + + logger::info("{:*^50}", "ON DEATH"); + + ForEachDistributable([](Distributables& a_distributable) { + const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); + + const auto added = a_distributable.GetSize(); + const auto all = a_distributable.GetLookupCount(); + + // Only log entries that are actually present in INIs. + if (all > 0) { + logger::info("Registered {}/{} {}s", added, all, recordName); + } + }); + } + + RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESDeathEvent* a_event, RE::BSTEventSource*) + { + constexpr auto is_NPC = [](auto&& a_ref) { + return a_ref && !a_ref->IsPlayerRef(); + }; + + if (a_event && a_event->dead && is_NPC(a_event->actorDying)) { + const auto actor = a_event->actorDying->As(); + const auto npc = actor ? actor->GetActorBase() : nullptr; + if (actor && npc) { + auto npcData = NPCData(actor, npc); + const auto input = PCLevelMult::Input{ actor, npc, false }; + + DistributedForms distributedForms{}; + + Forms::DistributionSet entries{ + spells.GetForms(), + perks.GetForms(), + items.GetForms(), + shouts.GetForms(), + levSpells.GetForms(), + packages.GetForms(), + outfits.GetForms(), + keywords.GetForms(), + factions.GetForms(), + sleepOutfits.GetForms(), + skins.GetForms() + }; + + Distribute::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) { + Distribute::Distribute(npcData, input, set, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. + }); + } + } + } + + return RE::BSEventNotifyControl::kContinue; + } +} diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 23a68f6..d349eb5 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -5,160 +5,142 @@ namespace Distribute { - namespace detail + void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms) { - /// - /// 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 }); + 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 } - }, - 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 - } - 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); + 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; } } - 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; + if (idxIt != packageList.end()) { + packageList.insert_after(idxIt, package); + } } 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; + } 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 false; - }, - accumulatedForms); - - for_each_form( - npcData, forms.deathItems, input, [&](std::map& a_objects) { - return npc->AddObjectsToContainer(a_objects, npc); - }, - accumulatedForms); - } + } + }, + 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); } void Distribute(NPCData& npcData, const PCLevelMult::Input& input) @@ -176,7 +158,6 @@ namespace Distribute 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) @@ -184,13 +165,13 @@ namespace Distribute DistributedForms distributedForms{}; - detail::distribute(npcData, input, entries, false, &distributedForms); + 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. + Distribute(npcData, input, set, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. }); } } @@ -200,33 +181,4 @@ namespace Distribute 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 3b9e91f..be99113 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -1,4 +1,5 @@ #include "DistributeManager.h" +#include "DeathDistribution.h" #include "Distribute.h" #include "DistributePCLevelMult.h" @@ -90,6 +91,7 @@ namespace Distribute logger::info("{:*^50}", "EVENTS"); Event::Manager::Register(); PCLevelMult::Manager::Register(); + DeathDistribution::Manager::Register(); DoInitialDistribution(); @@ -171,32 +173,7 @@ namespace Distribute::Event if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { scripts->AddEventSink(GetSingleton()); logger::info("Registered for {}", typeid(RE::TESFormDeleteEvent).name()); - - if (Forms::deathItems) { - scripts->AddEventSink(GetSingleton()); - logger::info("Registered for {}", typeid(RE::TESDeathEvent).name()); - } - } - } - - RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESDeathEvent* a_event, RE::BSTEventSource*) - { - constexpr auto is_NPC = [](auto&& a_ref) { - return a_ref && !a_ref->IsPlayerRef(); - }; - - if (a_event && a_event->dead && is_NPC(a_event->actorDying)) { - const auto actor = a_event->actorDying->As(); - const auto npc = actor ? actor->GetActorBase() : nullptr; - if (actor && npc) { - auto npcData = NPCData(actor, npc); - const auto input = PCLevelMult::Input{ actor, npc, false }; - - DistributeDeathItems(npcData, input); - } } - - return RE::BSEventNotifyControl::kContinue; } RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) diff --git a/SPID/src/FormData.cpp b/SPID/src/FormData.cpp index 676e3b2..05c62f1 100644 --- a/SPID/src/FormData.cpp +++ b/SPID/src/FormData.cpp @@ -24,5 +24,5 @@ std::size_t Forms::GetTotalLeveledEntries() 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(); + return spells.empty() && perks.empty() && items.empty() && shouts.empty() && levSpells.empty() && packages.empty() && outfits.empty() && keywords.empty() && factions.empty() && sleepOutfits.empty() && skins.empty(); } diff --git a/SPID/src/LinkedDistribution.cpp b/SPID/src/LinkedDistribution.cpp index 48b44de..032b7cc 100644 --- a/SPID/src/LinkedDistribution.cpp +++ b/SPID/src/LinkedDistribution.cpp @@ -36,11 +36,13 @@ namespace LinkedDistribution return false; } + // TODO: Parse also Linked Death Forms + std::string rawType = key.substr(6); auto type = RECORD::GetType(rawType); if (type == RECORD::kTotal) { - logger::warn("IGNORED: Unsupported Linked Form type: {}"sv, rawType); + logger::warn("IGNORED: Unsupported Linked Form type ({}): {} = {}"sv, rawType, originalKey, value); return true; } @@ -48,14 +50,14 @@ namespace LinkedDistribution 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); + logger::warn("IGNORED: Linked Form must have a form and at least one Form Filter: {} = {}"sv, originalKey, 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); + logger::warn("IGNORED: Linked Form must have at least one parent Form to link to: {} = {}"sv, originalKey, value); return true; } @@ -112,10 +114,9 @@ namespace LinkedDistribution { 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; + if constexpr (!std::is_same_v) { + forms.LookupForms(dataHandler, rawLinkedForms[forms.GetType()]); } - forms.LookupForms(dataHandler, rawLinkedForms[forms.GetType()]); }); // Sort out Spells and Leveled Spells into two separate lists. @@ -263,7 +264,6 @@ namespace LinkedDistribution linkedPackages, linkedOutfits, linkedKeywords, - DistributionSet::empty(), // deathItems are distributed only on death :) as such, linked items are also distributed only on death. linkedFactions, linkedSleepOutfits, linkedSkins @@ -297,7 +297,6 @@ namespace LinkedDistribution DistributionSet::empty(), DistributionSet::empty(), DistributionSet::empty(), - linkedDeathItems, DistributionSet::empty(), DistributionSet::empty(), DistributionSet::empty() diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 11d30e6..24c81f6 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -1,326 +1,334 @@ #include "LookupConfigs.h" +#include "DeathDistribution.h" #include "ExclusiveGroups.h" #include "LinkedDistribution.h" -namespace INI +namespace Configs { - namespace detail + namespace INI { - std::string sanitize(const std::string& a_value) + namespace detail { - auto newValue = a_value; + std::string sanitize(const std::string& a_value) + { + auto newValue = a_value; - //formID hypen - if (!newValue.contains('~')) { - string::replace_first_instance(newValue, " - ", "~"); - } + //formID hypen + if (!newValue.contains('~')) { + string::replace_first_instance(newValue, " - ", "~"); + } #ifdef SKYRIMVR - // swap dawnguard and dragonborn forms - // we do this during sanitize instead of in get_formID to squelch log errors - // VR apparently does not load masters in order so the lookup fails - static const srell::regex re_dawnguard(R"((0x0*2)([0-9a-f]{6}))", srell::regex_constants::optimize | srell::regex::icase); - newValue = regex_replace(newValue, re_dawnguard, "0x$2~Dawnguard.esm"); - - static const srell::regex re_dragonborn(R"((0x0*4)([0-9a-f]{6}))", srell::regex_constants::optimize | srell::regex::icase); - newValue = regex_replace(newValue, re_dragonborn, "0x$2~Dragonborn.esm"); + // swap dawnguard and dragonborn forms + // we do this during sanitize instead of in get_formID to squelch log errors + // VR apparently does not load masters in order so the lookup fails + static const srell::regex re_dawnguard(R"((0x0*2)([0-9a-f]{6}))", srell::regex_constants::optimize | srell::regex::icase); + newValue = regex_replace(newValue, re_dawnguard, "0x$2~Dawnguard.esm"); + + static const srell::regex re_dragonborn(R"((0x0*4)([0-9a-f]{6}))", srell::regex_constants::optimize | srell::regex::icase); + newValue = regex_replace(newValue, re_dragonborn, "0x$2~Dragonborn.esm"); #endif - //strip spaces between " | " - static const srell::regex re_bar(R"(\s*\|\s*)", srell::regex_constants::optimize); - newValue = srell::regex_replace(newValue, re_bar, "|"); + //strip spaces between " | " + static const srell::regex re_bar(R"(\s*\|\s*)", srell::regex_constants::optimize); + newValue = srell::regex_replace(newValue, re_bar, "|"); - //strip spaces between " , " - static const srell::regex re_comma(R"(\s*,\s*)", srell::regex_constants::optimize); - newValue = srell::regex_replace(newValue, re_comma, ","); + //strip spaces between " , " + static const srell::regex re_comma(R"(\s*,\s*)", srell::regex_constants::optimize); + newValue = srell::regex_replace(newValue, re_comma, ","); - //convert 00012345 formIDs to 0x12345 - static const srell::regex re_formID(R"(\b00+([0-9a-fA-F]{1,6})\b)", srell::regex_constants::optimize); - newValue = srell::regex_replace(newValue, re_formID, "0x$1"); + //convert 00012345 formIDs to 0x12345 + static const srell::regex re_formID(R"(\b00+([0-9a-fA-F]{1,6})\b)", srell::regex_constants::optimize); + newValue = srell::regex_replace(newValue, re_formID, "0x$1"); - //strip leading zeros - static const srell::regex re_zeros(R"((0x00+)([0-9a-fA-F]+))", srell::regex_constants::optimize); - newValue = srell::regex_replace(newValue, re_zeros, "0x$2"); + //strip leading zeros + static const srell::regex re_zeros(R"((0x00+)([0-9a-fA-F]+))", srell::regex_constants::optimize); + newValue = srell::regex_replace(newValue, re_zeros, "0x$2"); - //NOT to hyphen - //string::replace_all(newValue, "NOT ", "-"); + //NOT to hyphen + //string::replace_all(newValue, "NOT ", "-"); - return newValue; - } + return newValue; + } - std::pair> parse_ini(const RECORD::TYPE& typeHint, const std::string& a_value, const Path& a_path) - { - Data data{}; + std::pair> parse_ini(const RECORD::TYPE& typeHint, const std::string& a_value, const Path& a_path) + { + Data data{}; - auto sanitized_value = sanitize(a_value); - const auto sections = string::split(sanitized_value, "|"); + auto sanitized_value = sanitize(a_value); + const auto sections = string::split(sanitized_value, "|"); - const auto size = sections.size(); + const auto size = sections.size(); - //[FORMID/ESP] / EDITORID - if (kFormID < size) { - data.rawForm = distribution::get_record(sections[kFormID]); - } + //[FORMID/ESP] / EDITORID + if (kFormID < size) { + data.rawForm = distribution::get_record(sections[kFormID]); + } - //KEYWORDS - if (kStrings < size) { - StringFilters filters; + //KEYWORDS + if (kStrings < size) { + StringFilters filters; - auto split_str = distribution::split_entry(sections[kStrings]); - for (auto& str : split_str) { - if (str.contains("+"sv)) { - auto strings = distribution::split_entry(str, "+"); - data.stringFilters.ALL.insert(data.stringFilters.ALL.end(), strings.begin(), strings.end()); + auto split_str = distribution::split_entry(sections[kStrings]); + for (auto& str : split_str) { + if (str.contains("+"sv)) { + auto strings = distribution::split_entry(str, "+"); + data.stringFilters.ALL.insert(data.stringFilters.ALL.end(), strings.begin(), strings.end()); - } else if (str.at(0) == '-') { - str.erase(0, 1); - data.stringFilters.NOT.emplace_back(str); + } else if (str.at(0) == '-') { + str.erase(0, 1); + data.stringFilters.NOT.emplace_back(str); - } else if (str.at(0) == '*') { - str.erase(0, 1); - data.stringFilters.ANY.emplace_back(str); + } else if (str.at(0) == '*') { + str.erase(0, 1); + data.stringFilters.ANY.emplace_back(str); - } else { - data.stringFilters.MATCH.emplace_back(str); + } else { + data.stringFilters.MATCH.emplace_back(str); + } } } - } - //FILTER FORMS - if (kFilterIDs < size) { - auto split_IDs = distribution::split_entry(sections[kFilterIDs]); - for (auto& IDs : split_IDs) { - if (IDs.contains("+"sv)) { - auto splitIDs_ALL = distribution::split_entry(IDs, "+"); - for (auto& IDs_ALL : splitIDs_ALL) { - data.rawFormFilters.ALL.push_back(distribution::get_record(IDs_ALL)); - } - } else if (IDs.at(0) == '-') { - IDs.erase(0, 1); - data.rawFormFilters.NOT.push_back(distribution::get_record(IDs)); + //FILTER FORMS + if (kFilterIDs < size) { + auto split_IDs = distribution::split_entry(sections[kFilterIDs]); + for (auto& IDs : split_IDs) { + if (IDs.contains("+"sv)) { + auto splitIDs_ALL = distribution::split_entry(IDs, "+"); + for (auto& IDs_ALL : splitIDs_ALL) { + data.rawFormFilters.ALL.push_back(distribution::get_record(IDs_ALL)); + } + } else if (IDs.at(0) == '-') { + IDs.erase(0, 1); + data.rawFormFilters.NOT.push_back(distribution::get_record(IDs)); - } else { - data.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + } else { + data.rawFormFilters.MATCH.push_back(distribution::get_record(IDs)); + } } } - } - //LEVEL - Range actorLevel; - std::vector skillLevels; - std::vector skillWeights; - if (kLevel < size) { - auto split_levels = distribution::split_entry(sections[kLevel]); - for (auto& levels : split_levels) { - if (levels.contains('(')) { - //skill(min/max) - const auto isWeightFilter = levels.starts_with('w'); - auto sanitizedLevel = string::remove_non_alphanumeric(levels); - if (isWeightFilter) { - sanitizedLevel.erase(0, 1); - } - //skill min max - if (auto skills = string::split(sanitizedLevel, " "); !skills.empty()) { - if (auto type = string::to_num(skills[0]); type < 18) { - auto minLevel = string::to_num(skills[1]); - if (skills.size() > 2) { - auto maxLevel = string::to_num(skills[2]); - if (isWeightFilter) { - skillWeights.push_back({ type, Range(minLevel, maxLevel) }); - } else { - skillLevels.push_back({ type, Range(minLevel, maxLevel) }); - } - } else { - if (isWeightFilter) { - // Single value is treated as exact match. - skillWeights.push_back({ type, Range(minLevel) }); + //LEVEL + Range actorLevel; + std::vector skillLevels; + std::vector skillWeights; + if (kLevel < size) { + auto split_levels = distribution::split_entry(sections[kLevel]); + for (auto& levels : split_levels) { + if (levels.contains('(')) { + //skill(min/max) + const auto isWeightFilter = levels.starts_with('w'); + auto sanitizedLevel = string::remove_non_alphanumeric(levels); + if (isWeightFilter) { + sanitizedLevel.erase(0, 1); + } + //skill min max + if (auto skills = string::split(sanitizedLevel, " "); !skills.empty()) { + if (auto type = string::to_num(skills[0]); type < 18) { + auto minLevel = string::to_num(skills[1]); + if (skills.size() > 2) { + auto maxLevel = string::to_num(skills[2]); + if (isWeightFilter) { + skillWeights.push_back({ type, Range(minLevel, maxLevel) }); + } else { + skillLevels.push_back({ type, Range(minLevel, maxLevel) }); + } } else { - skillLevels.push_back({ type, Range(minLevel) }); + if (isWeightFilter) { + // Single value is treated as exact match. + skillWeights.push_back({ type, Range(minLevel) }); + } else { + skillLevels.push_back({ type, Range(minLevel) }); + } } } } - } - } else { - if (auto actor_level = string::split(levels, "/"); actor_level.size() > 1) { - auto minLevel = string::to_num(actor_level[0]); - auto maxLevel = string::to_num(actor_level[1]); - - actorLevel = Range(minLevel, maxLevel); } else { - auto level = string::to_num(levels); + if (auto actor_level = string::split(levels, "/"); actor_level.size() > 1) { + auto minLevel = string::to_num(actor_level[0]); + auto maxLevel = string::to_num(actor_level[1]); - actorLevel = Range(level); + actorLevel = Range(minLevel, maxLevel); + } else { + auto level = string::to_num(levels); + + actorLevel = Range(level); + } } } } - } - data.levelFilters = { actorLevel, skillLevels, skillWeights }; - - //TRAITS - if (kTraits < size) { - auto split_traits = distribution::split_entry(sections[kTraits], "/"); - for (auto& trait : split_traits) { - switch (string::const_hash(trait)) { - case "M"_h: - case "-F"_h: - data.traits.sex = RE::SEX::kMale; - break; - case "F"_h: - case "-M"_h: - data.traits.sex = RE::SEX::kFemale; - break; - case "U"_h: - data.traits.unique = true; - break; - case "-U"_h: - data.traits.unique = false; - break; - case "S"_h: - data.traits.summonable = true; - break; - case "-S"_h: - data.traits.summonable = false; - break; - case "C"_h: - data.traits.child = true; - break; - case "-C"_h: - data.traits.child = false; - break; - case "L"_h: - data.traits.leveled = true; - break; - case "-L"_h: - data.traits.leveled = false; - break; - case "T"_h: - data.traits.teammate = true; - break; - case "-T"_h: - data.traits.teammate = false; - break; - default: - break; + data.levelFilters = { actorLevel, skillLevels, skillWeights }; + + //TRAITS + if (kTraits < size) { + auto split_traits = distribution::split_entry(sections[kTraits], "/"); + for (auto& trait : split_traits) { + switch (string::const_hash(trait)) { + case "M"_h: + case "-F"_h: + data.traits.sex = RE::SEX::kMale; + break; + case "F"_h: + case "-M"_h: + data.traits.sex = RE::SEX::kFemale; + break; + case "U"_h: + data.traits.unique = true; + break; + case "-U"_h: + data.traits.unique = false; + break; + case "S"_h: + data.traits.summonable = true; + break; + case "-S"_h: + data.traits.summonable = false; + break; + case "C"_h: + data.traits.child = true; + break; + case "-C"_h: + data.traits.child = false; + break; + case "L"_h: + data.traits.leveled = true; + break; + case "-L"_h: + data.traits.leveled = false; + break; + case "T"_h: + data.traits.teammate = true; + break; + case "-T"_h: + data.traits.teammate = false; + break; + default: + break; + } } } - } - //ITEMCOUNT/INDEX - if (typeHint == RECORD::kPackage) { // reuse item count for package stack index - data.idxOrCount = 0; - } + //ITEMCOUNT/INDEX + if (typeHint == RECORD::kPackage) { // reuse item count for package stack index + data.idxOrCount = 0; + } - if (kIdxOrCount < size) { - 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]); + if (kIdxOrCount < size) { + 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(minCount, maxCount); + } else { + auto count = string::to_num(str); - data.idxOrCount = RandomCount(count, count); // create the exact match range. + 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); + //CHANCE + if (kChance < size) { + if (const auto& str = sections[kChance]; distribution::is_valid_entry(str)) { + data.chance = string::to_num(str); + } } - } - data.path = a_path; + data.path = a_path; - if (sanitized_value != a_value) { - return { data, sanitized_value }; + if (sanitized_value != a_value) { + return { data, sanitized_value }; + } + return { data, std::nullopt }; } - return { data, std::nullopt }; } - } - std::pair GetConfigs() - { - logger::info("{:*^50}", "INI"); + std::pair GetConfigs() + { + logger::info("{:*^50}", "INI"); - std::vector files = distribution::get_configs(R"(Data\)", "_DISTR"sv); + std::vector files = distribution::get_configs(R"(Data\)", "_DISTR"sv); - if (files.empty()) { - logger::warn("No .ini files with _DISTR suffix were found within the Data folder, aborting..."); - return { false, false }; - } + if (files.empty()) { + logger::warn("No .ini files with _DISTR suffix were found within the Data folder, aborting..."); + return { false, false }; + } - logger::info("{} matching inis found", files.size()); + logger::info("{} matching inis found", files.size()); - bool shouldLogErrors{ false }; + bool shouldLogErrors{ false }; - for (const auto& path : files) { - logger::info("\tINI : {}", path); + for (const auto& path : files) { + logger::info("\tINI : {}", path); - CSimpleIniA ini; - ini.SetUnicode(); - ini.SetMultiKey(); + CSimpleIniA ini; + ini.SetUnicode(); + ini.SetMultiKey(); - if (const auto rc = ini.LoadFile(path.c_str()); rc < 0) { - logger::error("\t\tcouldn't read INI"); - continue; - } + if (const auto rc = ini.LoadFile(path.c_str()); rc < 0) { + logger::error("\t\tcouldn't read INI"); + continue; + } - if (auto values = ini.GetSection(""); values && !values->empty()) { - std::multimap, CSimpleIniA::Entry::LoadOrder> oldFormatMap; + if (auto values = ini.GetSection(""); values && !values->empty()) { + std::multimap, CSimpleIniA::Entry::LoadOrder> oldFormatMap; - auto truncatedPath = path.substr(5); //strip "Data\\" + auto truncatedPath = path.substr(5); //strip "Data\\" - for (auto& [key, entry] : *values) { - try { - if (ExclusiveGroups::INI::TryParse(key.pItem, entry, truncatedPath)) { - continue; - } + for (auto& [key, entry] : *values) { + try { + if (ExclusiveGroups::INI::TryParse(key.pItem, entry, truncatedPath)) { + continue; + } - if (LinkedDistribution::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); + if (DeathDistribution::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, key.pItem, entry); + continue; + } + auto [data, sanitized_str] = detail::parse_ini(type, entry, truncatedPath); - configs[type].emplace_back(data); + configs[type].emplace_back(data); - if (sanitized_str) { - oldFormatMap.emplace(key, std::make_pair(entry, *sanitized_str)); + if (sanitized_str) { + oldFormatMap.emplace(key, std::make_pair(entry, *sanitized_str)); + } + } catch (...) { + logger::warn("\t\tFailed to parse entry [{} = {}]"sv, key.pItem, entry); + shouldLogErrors = true; } - } catch (...) { - logger::warn("\t\tFailed to parse entry [{} = {}]"sv, key.pItem, entry); - shouldLogErrors = true; } - } - if (!oldFormatMap.empty()) { - logger::info("\t\tsanitizing {} entries", oldFormatMap.size()); + if (!oldFormatMap.empty()) { + logger::info("\t\tsanitizing {} entries", oldFormatMap.size()); - for (auto& [key, entry] : oldFormatMap) { - auto& [original, sanitized] = entry; - ini.DeleteValue("", key.pItem, original.c_str()); - ini.SetValue("", key.pItem, sanitized.c_str(), key.pComment, false); - } + for (auto& [key, entry] : oldFormatMap) { + auto& [original, sanitized] = entry; + ini.DeleteValue("", key.pItem, original.c_str()); + ini.SetValue("", key.pItem, sanitized.c_str(), key.pComment, false); + } - (void)ini.SaveFile(path.c_str()); + (void)ini.SaveFile(path.c_str()); + } } } - } - return { true, shouldLogErrors }; + return { true, shouldLogErrors }; + } } } diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index f875be4..46a2a5a 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -3,6 +3,7 @@ #include "FormData.h" #include "KeywordDependencies.h" #include "LinkedDistribution.h" +#include "DeathDistribution.h" bool LookupDistributables(RE::TESDataHandler* const dataHandler) { @@ -10,16 +11,15 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) 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()); + if constexpr (!std::is_same_v) { + const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); - a_distributable.LookupForms(dataHandler, recordName, INI::configs[a_distributable.GetType()]); + a_distributable.LookupForms(dataHandler, recordName, Configs::INI::configs[a_distributable.GetType()]); + } }); // Sort out Spells and Leveled Spells into two separate lists. - auto& rawSpells = INI::configs[RECORD::kSpell]; + auto& rawSpells = Configs::INI::configs[RECORD::kSpell]; for (auto& rawSpell : rawSpells) { LookupGenericForm(dataHandler, rawSpell, [&](bool isValid, auto form, const auto& idxOrCount, const auto& filters, const auto& path) { @@ -31,10 +31,10 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) }); } - auto& genericForms = INI::configs[RECORD::kForm]; + auto& genericForms = Configs::INI::configs[RECORD::kForm]; for (auto& rawForm : genericForms) { - // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit, Skin or DeathItems) + // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit, Skin) 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); @@ -107,7 +107,7 @@ void LogDistributablesLookup() }); // Clear INI map once lookup is done - INI::configs.clear(); + Configs::INI::configs.clear(); // Clear logger's buffer to free some memory :) buffered_logger::clear(); @@ -136,6 +136,16 @@ void LogLinkedFormsLookup() LinkedDistribution::Manager::GetSingleton()->LogLinkedFormsLookup(); } +void LookupDeathForms(RE::TESDataHandler* const dataHandler) +{ + DeathDistribution::Manager::GetSingleton()->LookupForms(dataHandler); +} + +void LogDeathFormsLookup() +{ + DeathDistribution::Manager::GetSingleton()->LogFormsLookup(); +} + bool Lookup::LookupForms() { if (const auto dataHandler = RE::TESDataHandler::GetSingleton(); dataHandler) { @@ -152,12 +162,16 @@ bool Lookup::LookupForms() logger::info("Lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); } + LookupDeathForms(dataHandler); + LogDeathFormsLookup(); + LookupLinkedForms(dataHandler); LogLinkedFormsLookup(); LookupExclusiveGroups(dataHandler); LogExclusiveGroupsLookup(); + return success; } diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index 4895ff6..6251b89 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -1,5 +1,5 @@ #include "LookupNPC.h" -#include +#include "ExclusiveGroups.h" namespace NPC { diff --git a/SPID/src/main.cpp b/SPID/src/main.cpp index b3e47be..2a86c66 100644 --- a/SPID/src/main.cpp +++ b/SPID/src/main.cpp @@ -17,7 +17,7 @@ void MessageHandler(SKSE::MessagingInterface::Message* a_message) const auto tweaks = GetModuleHandle(L"po3_Tweaks"); logger::info("powerofthree's Tweaks (po3_tweaks) detected : {}", tweaks != nullptr); - if (std::tie(shouldLookupForms, shouldLogErrors) = INI::GetConfigs(); shouldLookupForms) { + if (std::tie(shouldLookupForms, shouldLogErrors) = Configs::INI::GetConfigs(); shouldLookupForms) { logger::info("{:*^50}", "HOOKS"); Distribute::Actor::Install(); }