Skip to content

Commit

Permalink
Added support for all LinkedForm types and type inferring.
Browse files Browse the repository at this point in the history
When LinkedForm is used SPID will pick up correct distributable type based on actual Form's type.
  • Loading branch information
adya committed Mar 25, 2024
1 parent e6d0286 commit a122403
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 79 deletions.
113 changes: 96 additions & 17 deletions SPID/include/LinkedDistribution.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,65 @@ namespace LinkedDistribution
{
namespace INI
{
struct RawLinkedItem
struct RawLinkedForm
{
FormOrEditorID rawForm{};
FormOrEditorID formOrEditorID{};

/// Raw filters in RawLinkedItem only use MATCH, there is no meaning for ALL or NOT, so they are ignored.
Filters<FormOrEditorID> rawFormFilters{};
/// Raw filters in RawLinkedForm only use MATCH, there is no meaning for ALL or NOT, so they are ignored.
Filters<FormOrEditorID> formIDs{};

RandomCount count{ 1, 1 };
Chance chance{ 100 };

std::string path{};
};

using LinkedItemsVec = std::vector<RawLinkedItem>;
using LinkedFormsVec = std::vector<RawLinkedForm>;
using LinkedFormsConfig = std::unordered_map<RECORD::TYPE, LinkedFormsVec>;

inline LinkedItemsVec linkedItems{};
inline LinkedFormsConfig linkedForms{};

/// <summary>
/// Checks whether given entry is a linked form and attempts to parse it.
/// </summary>
/// <returns>true if given entry was a linked form. Note that returned value doesn't represent whether or parsing was successful.</returns>
bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path);
}

using namespace Forms;

class Manager;

template<class Form>
struct LinkedForms;

namespace detail
{
template <class Form = RE::TESForm>
Form* LookupLinkedForm(RE::TESDataHandler* const dataHandler, INI::RawLinkedForm& rawForm);
}

using namespace Forms;

template <class Form>
struct LinkedForms
{
friend Manager; // allow Manager to later modify forms directly.
friend Form* detail::LookupLinkedForm(RE::TESDataHandler* const, INI::RawLinkedForm&);

using Map = std::unordered_map<RE::TESForm*, DataVec<Form>>;
using FormsMap = std::unordered_map<RE::TESForm*, DataVec<Form>>;

LinkedForms(RECORD::TYPE type) :
type(type)
{}

RECORD::TYPE GetType() const { return type; }
const Map& GetForms() const { return forms; }
const FormsMap& GetForms() const { return forms; }

void LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms);

private:
RECORD::TYPE type;
Map forms{};
FormsMap forms{};

void Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path);
};
Expand All @@ -58,13 +76,13 @@ namespace LinkedDistribution
/// <summary>
/// Does a forms lookup similar to what Filters do.
///
/// As a result this method configures Manager with discovered valid linked items.
/// As a result this method configures Manager with discovered valid linked forms.
/// </summary>
/// <param name="dataHandler">A DataHandler that will perform the actual lookup.</param>
/// <param name="rawLinkedDistribution">A raw linked item entries that should be processed.</param>
void LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems = INI::linkedItems);
/// <param name="rawLinkedDistribution">A raw linked form entries that should be processed.</param>
void LookupLinkedForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsConfig& rawLinkedForms = INI::linkedForms);

void LogLinkedItemsLookup();
void LogLinkedFormsLookup();

/// <summary>
/// Calculates DistributionSet for each linked form and calls a callback for each of them.
Expand Down Expand Up @@ -93,10 +111,58 @@ namespace LinkedDistribution
/// Iterates over each type of LinkedForms and calls a callback with each of them.
/// </summary>
template <typename Func, typename... Args>
void ForEachLinkedForms(Func&& func, const Args&&... args);
void ForEachLinkedForms(Func&& func, Args&&... args);
};

#pragma region Implementation

template <class Form>
Form* detail::LookupLinkedForm(RE::TESDataHandler* const dataHandler, INI::RawLinkedForm& rawForm)
{
using namespace Forms::Lookup;

try {
return Forms::detail::get_form<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(""), RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(e.actualFormType));
},
[&](std::string editorID) {
buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - mismatching form type (expected: {}, actual: {})", e.path, editorID, RE::FormTypeToString(e.expectedFformType), RE::FormTypeToString(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(""), RE::FormTypeToString(e.formType));
},
[&](std::string editorID) {
buffered_logger::error("\t\t[{}] LinkedForm ({}) SKIP - unsupported form type ({})", e.path, editorID, RE::FormTypeToString(e.formType));
} },
e.formOrEditorID);
}
return nullptr;
}

template <class Form>
DataVec<Form>& Manager::LinkedFormsForForm(RE::TESForm* form, LinkedForms<Form>& linkedForms) const
{
Expand All @@ -109,7 +175,7 @@ namespace LinkedDistribution
}

template <typename Func, typename... Args>
void Manager::ForEachLinkedForms(Func&& func, const Args&&... args)
void Manager::ForEachLinkedForms(Func&& func, Args&&... args)
{
func(keywords, std::forward<Args>(args)...);
func(spells, std::forward<Args>(args)...);
Expand All @@ -123,13 +189,26 @@ namespace LinkedDistribution
func(skins, std::forward<Args>(args)...);
}

template <class Form>
void LinkedForms<Form>::LookupForms(RE::TESDataHandler* const dataHandler, INI::LinkedFormsVec& rawLinkedForms)
{
for (auto& rawForm : rawLinkedForms) {
auto form = detail::LookupLinkedForm<Form>(dataHandler, rawForm);
auto& [formID, parentFormIDs, count, chance, path] = rawForm;
FormVec parentForms{};
if (Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) {
Link(form, parentForms, count, chance, path);
}
}
}

template <class Form>
void LinkedForms<Form>::Link(Form* form, const FormVec& linkedForms, const RandomCount& count, const Chance& chance, const std::string& path)
{
for (const auto& linkedForm : linkedForms) {
if (std::holds_alternative<RE::TESForm*>(linkedForm)) {
auto& distributableForms = forms[std::get<RE::TESForm*>(linkedForm)];
// Note that we don't use Data.index here, as these linked items doesn't have any leveled filters
// 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, count, FilterData({}, {}, {}, {}, chance), path);
}
Expand Down
6 changes: 6 additions & 0 deletions SPID/include/LookupConfigs.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ namespace RECORD
using namespace detail;
return static_cast<TYPE>(std::distance(add.begin(), std::find(add.begin(), add.end(), aType)));
}

inline constexpr TYPE GetType(const std::string_view& aType)
{
using namespace detail;
return static_cast<TYPE>(std::distance(add.begin(), std::find(add.begin(), add.end(), aType)));
}
}

namespace INI
Expand Down
97 changes: 41 additions & 56 deletions SPID/src/LinkedDistribution.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "LinkedDistribution.h"
#include "FormData.h"
#include "LookupConfigs.h"

namespace LinkedDistribution
{
Expand All @@ -22,31 +23,38 @@ namespace LinkedDistribution

bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path)
{
if (a_key != "LinkedItem") {
if (!a_key.starts_with("Linked"sv)) {
return false;
}

std::string_view rawType = a_key.substr(6);
auto type = RECORD::GetType(rawType);
if (type == RECORD::kTotal) {
logger::warn("IGNORED: Invalid Linked Form type: {}"sv, a_key);
return true;
}

const auto sections = string::split(a_value, "|");
const auto size = sections.size();

if (size <= kRequired) {
logger::warn("IGNORED: LinkedItem must have a form and at least one Form Filter: {} = {}"sv, a_key, a_value);
return false;
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, a_key, a_value);
return false;
return true;
}

INI::RawLinkedItem item{};
item.rawForm = distribution::get_record(sections[kForm]);
INI::RawLinkedForm item{};
item.formOrEditorID = distribution::get_record(sections[kForm]);
item.path = a_path;

for (auto& IDs : split_IDs) {
item.rawFormFilters.MATCH.push_back(distribution::get_record(IDs));
item.formIDs.MATCH.push_back(distribution::get_record(IDs));
}

if (kCount < size) {
Expand All @@ -70,7 +78,7 @@ namespace LinkedDistribution
}
}

linkedItems.push_back(item);
linkedForms[type].push_back(item);

return true;
}
Expand All @@ -79,65 +87,42 @@ namespace LinkedDistribution

#pragma region Lookup

void Manager::LookupLinkedItems(RE::TESDataHandler* const dataHandler, INI::LinkedItemsVec& rawLinkedItems)
void Manager::LookupLinkedForms(RE::TESDataHandler* dataHandler, INI::LinkedFormsConfig& rawLinkedForms)
{
using namespace Forms::Lookup;
ForEachLinkedForms([&]<typename Form>(LinkedForms<Form>& forms) {
forms.LookupForms(dataHandler, rawLinkedForms[forms.GetType()]);
});

for (auto& [formOrEditorID, linkedIDs, count, chance, path] : rawLinkedItems) {
try {
auto form = detail::get_form(dataHandler, formOrEditorID, path);
FormVec match{};
if (!detail::formID_to_form(dataHandler, linkedIDs.MATCH, match, path, false, false)) {
auto& genericForms = rawLinkedForms[RECORD::kForm];
for (auto& rawForm : genericForms) {
if (auto form = detail::LookupLinkedForm(dataHandler, rawForm); form) {
auto& [formID, parentFormIDs, count, chance, path] = rawForm;
FormVec parentForms{};
if (!Forms::detail::formID_to_form(dataHandler, parentFormIDs.MATCH, parentForms, path, false, false)) {
continue;
}
// Add to appropriate list.
if (const auto keyword = form->As<RE::BGSKeyword>(); keyword) {
keywords.Link(keyword, match, count, chance, path);
keywords.Link(keyword, parentForms, count, chance, path);
} else if (const auto spell = form->As<RE::SpellItem>(); spell) {
spells.Link(spell, match, count, chance, path);
spells.Link(spell, parentForms, count, chance, path);
} else if (const auto perk = form->As<RE::BGSPerk>(); perk) {
perks.Link(perk, match, count, chance, path);
perks.Link(perk, parentForms, count, chance, path);
} else if (const auto shout = form->As<RE::TESShout>(); shout) {
shouts.Link(shout, match, count, chance, path);
shouts.Link(shout, parentForms, count, chance, path);
} else if (const auto item = form->As<RE::TESBoundObject>(); item) {
items.Link(item, match, count, chance, path);
items.Link(item, parentForms, count, chance, path);
} else if (const auto outfit = form->As<RE::BGSOutfit>(); outfit) {
outfits.Link(outfit, match, count, chance, path);
outfits.Link(outfit, parentForms, count, chance, path);
} else if (const auto faction = form->As<RE::TESFaction>(); faction) {
factions.Link(faction, match, count, chance, path);
factions.Link(faction, parentForms, count, chance, path);
} else if (const auto skin = form->As<RE::TESObjectARMO>(); skin) {
skins.Link(skin, match, count, chance, path);
skins.Link(skin, parentForms, count, chance, path);
} else if (const auto package = form->As<RE::TESForm>(); package) {
auto type = package->GetFormType();
if (type == RE::FormType::Package || type == RE::FormType::FormList)
packages.Link(package, match, count, chance, path);
}
} catch (const UnknownFormIDException& e) {
buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - formID doesn't exist", e.path, e.formID, e.modName.value_or(""));
} catch (const UnknownPluginException& e) {
buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - mod cannot be found", e.path, e.modName);
} catch (const InvalidKeywordException& e) {
buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - keyword does not have a valid editorID", e.path, e.formID, e.modName.value_or(""));
} catch (const KeywordNotFoundException& e) {
if (e.isDynamic) {
buffered_logger::critical("\t\t[{}] LinkedItem {} FAIL - couldn't create keyword", e.path, e.editorID);
} else {
buffered_logger::critical("\t\t[{}] LinkedItem {} FAIL - couldn't get existing keyword", e.path, e.editorID);
packages.Link(package, parentForms, count, chance, path);
}
} catch (const UnknownEditorIDException& e) {
buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - editorID doesn't exist", e.path, e.editorID);
} catch (const MalformedEditorIDException& e) {
buffered_logger::error("\t\t[{}] LinkedItem (\"\") SKIP - malformed editorID", e.path);
} catch (const InvalidFormTypeException& e) {
std::visit(overload{
[&](const FormModPair& formMod) {
auto& [formID, modName] = formMod;
buffered_logger::error("\t\t[{}] LinkedItem [0x{:X}] ({}) SKIP - unsupported form type ({})", e.path, *formID, modName.value_or(""), RE::FormTypeToString(e.formType));
},
[&](std::string editorID) {
buffered_logger::error("\t\t[{}] LinkedItem ({}) SKIP - unsupported form type ({})", e.path, editorID, RE::FormTypeToString(e.formType));
} },
e.formOrEditorID);
}
}

Expand All @@ -147,17 +132,17 @@ namespace LinkedDistribution
});

// Clear INI once lookup is done
rawLinkedItems.clear();
rawLinkedForms.clear();

// Clear logger's buffer to free some memory :)
buffered_logger::clear();
}

void Manager::LogLinkedItemsLookup()
void Manager::LogLinkedFormsLookup()
{
logger::info("{:*^50}", "LINKED ITEMS");

ForEachLinkedForms([]<typename Form>(const LinkedForms<Form>& linkedForms) {
ForEachLinkedForms([]<typename Form>(LinkedForms<Form>& linkedForms) {
if (linkedForms.GetForms().empty()) {
return;
}
Expand All @@ -177,15 +162,15 @@ namespace LinkedDistribution
const auto& recordName = RECORD::GetTypeName(linkedForms.GetType());
logger::info("Linked {}s: ", recordName);

for (const auto& [form, linkedItems] : map) {
for (const auto& [form, linkedForms] : map) {
logger::info("\t{}", describe(form));

const auto lastItemIndex = linkedItems.size() - 1;
const auto lastItemIndex = linkedForms.size() - 1;
for (int i = 0; i < lastItemIndex; ++i) {
const auto& linkedItem = linkedItems[i];
const auto& linkedItem = linkedForms[i];
logger::info("\t├─── {}", describe(linkedItem));
}
const auto& lastLinkedItem = linkedItems[lastItemIndex];
const auto& lastLinkedItem = linkedForms[lastItemIndex];
logger::info("\t└─── {}", describe(lastLinkedItem));
}
});
Expand Down
Loading

0 comments on commit a122403

Please sign in to comment.