From 497e7463e26f84f33e68233089895a81dc04e6d3 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 12 Apr 2024 00:19:04 +0300 Subject: [PATCH] Migrated regular distribution configs parsing to composable parser. --- SPID/include/FormData.h | 15 +- SPID/include/LookupConfigs.h | 300 +++++++++++++++++++++++++++++++-- SPID/include/Parser.h | 260 ++++------------------------ SPID/src/DeathDistribution.cpp | 4 +- SPID/src/LookupConfigs.cpp | 33 ++-- SPID/src/LookupForms.cpp | 8 +- SPID/src/main.cpp | 2 +- 7 files changed, 353 insertions(+), 269 deletions(-) diff --git a/SPID/include/FormData.h b/SPID/include/FormData.h index 7a10d7d..7fbd493 100644 --- a/SPID/include/FormData.h +++ b/SPID/include/FormData.h @@ -6,6 +6,7 @@ namespace Forms { namespace Lookup + { struct UnknownPluginException : std::exception { @@ -434,7 +435,7 @@ namespace Forms DataVec
& GetForms(bool a_onlyLevelEntries); DataVec& GetForms(); - void LookupForms(RE::TESDataHandler*, std::string_view a_type, Configs::INI::DataVec&); + void LookupForms(RE::TESDataHandler*, std::string_view a_type, Distribution::INI::DataVec&); void EmplaceForm(bool isValid, Form*, const IndexOrCount&, const FilterData&, const Path&); // Init formsWithLevels and formsNoLevels @@ -449,7 +450,7 @@ namespace Forms /// This counter is used for logging purposes. std::size_t lookupCount{ 0 }; - void LookupForm(RE::TESDataHandler*, Configs::INI::Data&); + void LookupForm(RE::TESDataHandler*, Distribution::INI::Data&); }; inline Distributables spells{ RECORD::kSpell }; @@ -492,7 +493,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, Configs::INI::Data& rawForm, std::function callback); + void LookupGenericForm(RE::TESDataHandler* const dataHandler, Distribution::INI::Data& rawForm, std::function callback); } template @@ -550,7 +551,7 @@ Forms::DataVec& Forms::Distributables::GetForms(bool a_onlyLevelEntr } template -void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, Configs::INI::Data& rawForm) +void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, Distribution::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); @@ -558,7 +559,7 @@ void Forms::Distributables::LookupForm(RE::TESDataHandler* dataHandler, Co } template -void Forms::Distributables::LookupForms(RE::TESDataHandler* dataHandler, std::string_view a_type, Configs::INI::DataVec& a_INIDataVec) +void Forms::Distributables::LookupForms(RE::TESDataHandler* dataHandler, std::string_view a_type, Distribution::INI::DataVec& a_INIDataVec) { if (a_INIDataVec.empty()) { return; @@ -606,9 +607,9 @@ void Forms::Distributables::FinishLookupForms() } template -void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, Configs::INI::Data& rawForm, std::function callback) +void Forms::LookupGenericForm(RE::TESDataHandler* const dataHandler, Distribution::INI::Data& rawForm, std::function callback) { - auto& [formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] = rawForm; + auto& [type, formOrEditorID, strings, filterIDs, level, traits, idxOrCount, chance, path] = rawForm; try { if (auto form = detail::get_form(dataHandler, formOrEditorID, path, LookupOptions::kCreateIfMissing); form) { diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 4722e8b..eb05843 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -57,25 +57,13 @@ namespace RECORD } } -namespace Configs +namespace Distribution { namespace INI { - enum TYPE : std::uint32_t - { - kFormIDPair = 0, - kFormID = kFormIDPair, - kStrings, - kESP = kStrings, - kFilterIDs, - kLevel, - kTraits, - kIdxOrCount, - kChance - }; - struct Data { + RECORD::TYPE type{ RECORD::TYPE::kForm }; FormOrEditorID rawForm{}; StringFilters stringFilters{}; Filters rawFormFilters{}; @@ -91,5 +79,289 @@ namespace Configs inline Map configs{}; std::pair GetConfigs(); + + namespace Exception + { + struct UnsupportedFormTypeException : std::exception + { + const std::string key; + + UnsupportedFormTypeException(const std::string& key) : + key(key) + {} + + const char* what() const noexcept override + { + return fmt::format("Unsupported Form type {}"sv, key).c_str(); + } + }; + + struct InvalidIndexOrCountException : std::exception + { + const std::string entry; + + InvalidIndexOrCountException(const std::string& entry) : + entry(entry) + {} + + const char* what() const noexcept override + { + return fmt::format("Invalid index or count {}"sv, entry).c_str(); + } + }; + } + + struct DefaultKeyComponentParser + { + template + void operator()(const std::string& key, Data& data) const; + }; + + struct DistributableFormComponentParser + { + template + void operator()(const std::string& key, Data& data) const; + }; + + struct StringFiltersComponentParser + { + template + void operator()(const std::string& entry, Data& data) const; + }; + + struct FormFiltersComponentParser + { + template + void operator()(const std::string& entry, Data& data) const; + }; + + struct LevelFiltersComponentParser + { + template + void operator()(const std::string& entry, Data& data) const; + }; + + struct TraitsFilterComponentParser + { + template + void operator()(const std::string& entry, Data& data) const; + }; + + struct IndexOrCountComponentParser + { + template + void operator()(const std::string& entry, Data& data) const; + }; + + struct ChanceComponentParser + { + template + void operator()(const std::string& str, Data& data) const; + }; + } +} + +namespace Distribution::INI +{ + template + void DefaultKeyComponentParser::operator()(const std::string& key, Data& data) const + { + auto type = RECORD::GetType(key); + if (type == RECORD::kTotal) { + throw Exception::UnsupportedFormTypeException(key); + } + + data.type = type; + } + + template + void DistributableFormComponentParser::operator()(const std::string& key, Data& data) const + { + data.rawForm = distribution::get_record(key); + } + + template + void StringFiltersComponentParser::operator()(const std::string& entry, Data& data) const + { + auto split_str = distribution::split_entry(entry); + 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); + } + } + } + + template + void FormFiltersComponentParser::operator()(const std::string& entry, Data& data) const + { + auto split_IDs = distribution::split_entry(entry); + 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)); + } + } + } + + template + void LevelFiltersComponentParser::operator()(const std::string& entry, Data& data) const + { + Range actorLevel; + std::vector skillLevels; + std::vector skillWeights; + auto split_levels = distribution::split_entry(entry, ","); + 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 }; + } + + template + void TraitsFilterComponentParser::operator()(const std::string& entry, Data& data) const + { + auto split_traits = distribution::split_entry(entry, "/"); + 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; + } + } + } + + template + void IndexOrCountComponentParser::operator()(const std::string& entry, Data& data) const + { + auto typeHint = data.type; + + if (typeHint == RECORD::kPackage) { // reuse item count for package stack index + data.idxOrCount = 0; + } + + if (!distribution::is_valid_entry(entry)) { + return; + } + try { + if (typeHint == RECORD::kPackage) { // If it's a package, then we only expect a single number. + data.idxOrCount = string::to_num(entry); + } else { + if (auto countPair = string::split(entry, "-"); 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(entry); + + data.idxOrCount = RandomCount(count, count); // create the exact match range. + } + } + } catch (const std::exception& e) { + throw Exception::InvalidIndexOrCountException(entry); + } + } + + template + void ChanceComponentParser::operator()(const std::string& str, Data& data) const + { + if (distribution::is_valid_entry(str)) { + data.chance = string::to_num(str); + } } } diff --git a/SPID/include/Parser.h b/SPID/include/Parser.h index 2fbd0ff..7bfda9e 100644 --- a/SPID/include/Parser.h +++ b/SPID/include/Parser.h @@ -2,6 +2,9 @@ namespace detail { + /// + /// An utility function that will iterate over the list of ComponentParsers and call each one with the corresponding section of the entry. + /// template void parse_each(Data& data, const std::string splited[sizeof...(ComponentParsers)], std::index_sequence) { @@ -9,16 +12,47 @@ namespace detail } } +struct NotEnoughComponentsException: std::exception +{ + const size_t componentParsersCount; + const size_t entrySectionsCount; + + NotEnoughComponentsException(size_t componentParsersCount, size_t entrySectionsCount) : + componentParsersCount(componentParsersCount), + entrySectionsCount(entrySectionsCount) + {} + + const char* what() const noexcept override + { + return fmt::format("Not enough components. Expected {}, but got {}"sv, componentParsersCount, entrySectionsCount).c_str(); + } +}; + +/// +/// A composable Parsing function that accepts a variadic list of functors that define structure of the entry being parsed. +/// +/// Number of component parsers must be at least the same as the number of sections in the entry. +/// If there are fewer sections than component parsers, the remaining parsers will be called with an empty string. +/// +/// Parsing may throw and exception if there was not enough ComponentParsers to match all available entries. +/// It will also rethrow any exceptions thrown by the ComponentParsers. +/// +/// A type of the data object that will accumulate parsing results. +/// A special ComponentParser that is used for parsing entry's Key. +/// A list of ComponentParsers ordered in the same way thay you expect sections to appear in the line. +/// Key associated with the entry. +/// The entry line as it was read from the file. +/// template Data Parse(const std::string& key, const std::string& entry) { constexpr const size_t numberOfComponents = sizeof...(ComponentParsers); const auto rawSections = string::split(entry, "|"); - const auto numberOfSections = rawSections.size(); + const size_t numberOfSections = rawSections.size(); if (numberOfSections > numberOfComponents) { - throw std::runtime_error("Not enough components"); + throw NotEnoughComponentsException(numberOfComponents, numberOfSections); } std::string sections[numberOfComponents]; @@ -33,229 +67,9 @@ Data Parse(const std::string& key, const std::string& entry) Data data{}; - KeyComponentParser kcp; - kcp(key, data); + KeyComponentParser()(key, data); detail::parse_each(data, sections, std::index_sequence_for()); return data; }; - -struct DefaultKeyComponentParser -{ - template - void operator()(const std::string& key, Data& data) const - { - // TODO: Parse type. - } -}; - -struct DistributableFormComponentParser -{ - template - void operator()(const std::string& key, Data& data) const - { - data.rawForm = distribution::get_record(key); - } -}; - -struct StringFiltersComponentParser -{ - template - void operator()(const std::string& str, Data& data) const - { - auto split_str = distribution::split_entry(str); - 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); - } - } - } -}; - -struct FormFiltersComponentParser -{ - template - void operator()(const std::string& str, Data& data) const - { - auto split_IDs = distribution::split_entry(str); - 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)); - } - } - } -}; - -struct LevelFiltersComponentParser -{ - template - void operator()(const std::string& str, Data& data) const - { - Range actorLevel; - std::vector skillLevels; - std::vector skillWeights; - auto split_levels = distribution::split_entry(str, ","); - 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 }; - } -}; - -struct TraitsFilterComponentParser -{ - template - void operator()(const std::string& str, Data& data) const - { - auto split_traits = distribution::split_entry(str, "/"); - 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; - } - } - } -}; - -struct IndexOrCountComponentParser -{ - template - void operator()(const std::string& str, Data& data) const - { - auto typeHint = RECORD::kPackage; - - // TODO: Get type hint from data. - if (typeHint == RECORD::kPackage) { // reuse item count for package stack index - data.idxOrCount = 0; - } - - if (!distribution::is_valid_entry(str)) { - return; - } - - if (typeHint == RECORD::kPackage) { // If it's a package, then we only expect a single number. - data.idxOrCount = string::to_num(str); - } else { - 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. - } - } - } -}; - -struct ChanceComponentParser -{ - template - void operator()(const std::string& str, Data& data) const - { - if (distribution::is_valid_entry(str)) { - data.chance = string::to_num(str); - } - } -}; diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index 298cd06..322ed10 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -23,8 +23,8 @@ namespace DeathDistribution kRequired = kForm }; - using Data = Configs::INI::Data; - using DataVec = Configs::INI::DataVec; + using Data = Distribution::INI::Data; + using DataVec = Distribution::INI::DataVec; Map deathConfigs{}; diff --git a/SPID/src/LookupConfigs.cpp b/SPID/src/LookupConfigs.cpp index 27feebe..87fc314 100644 --- a/SPID/src/LookupConfigs.cpp +++ b/SPID/src/LookupConfigs.cpp @@ -4,7 +4,7 @@ #include "LinkedDistribution.h" #include "Parser.h" -namespace Configs +namespace Distribution { namespace INI { @@ -51,11 +51,13 @@ namespace Configs return newValue; } + } - std::pair> parse_ini(const std::string& key, const std::string& value, const Path& path) - { - auto sanitized_value = sanitize(value); + std::optional TryParse(const std::string& key, const std::string& value, const Path& path) + { + auto sanitized_value = detail::sanitize(value); + try { auto data = Parse GetConfigs() @@ -121,16 +127,7 @@ namespace Configs 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(key.pItem, entry, truncatedPath); - - configs[type].emplace_back(data); - - if (sanitized_str) { + if (const auto sanitized_str = TryParse(key.pItem, entry, truncatedPath)) { oldFormatMap.emplace(key, std::make_pair(entry, *sanitized_str)); } } catch (...) { diff --git a/SPID/src/LookupForms.cpp b/SPID/src/LookupForms.cpp index 06b8d9c..c34e648 100644 --- a/SPID/src/LookupForms.cpp +++ b/SPID/src/LookupForms.cpp @@ -14,12 +14,12 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) if constexpr (!std::is_same_v) { const auto& recordName = RECORD::GetTypeName(a_distributable.GetType()); - a_distributable.LookupForms(dataHandler, recordName, Configs::INI::configs[a_distributable.GetType()]); + a_distributable.LookupForms(dataHandler, recordName, Distribution::INI::configs[a_distributable.GetType()]); } }); // Sort out Spells and Leveled Spells into two separate lists. - auto& rawSpells = Configs::INI::configs[RECORD::kSpell]; + auto& rawSpells = Distribution::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,7 +31,7 @@ bool LookupDistributables(RE::TESDataHandler* const dataHandler) }); } - auto& genericForms = Configs::INI::configs[RECORD::kForm]; + auto& genericForms = Distribution::INI::configs[RECORD::kForm]; for (auto& rawForm : genericForms) { // Add to appropriate list. (Note that type inferring doesn't recognize SleepOutfit, Skin) @@ -107,7 +107,7 @@ void LogDistributablesLookup() }); // Clear INI map once lookup is done - Configs::INI::configs.clear(); + Distribution::INI::configs.clear(); // Clear logger's buffer to free some memory :) buffered_logger::clear(); diff --git a/SPID/src/main.cpp b/SPID/src/main.cpp index 2a86c66..2c634d6 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) = Configs::INI::GetConfigs(); shouldLookupForms) { + if (std::tie(shouldLookupForms, shouldLogErrors) = Distribution::INI::GetConfigs(); shouldLookupForms) { logger::info("{:*^50}", "HOOKS"); Distribute::Actor::Install(); }