You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
11 KiB
351 lines
11 KiB
#pragma once
|
|
|
|
#include <SimpleIni.h>
|
|
|
|
static std::unordered_map<RE::TESForm*, std::uint32_t> supplyCount = {};
|
|
static bool bSuppliesFetched = false;
|
|
static constexpr RE::ITEM_REMOVE_REASON iRemoveReason = RE::ITEM_REMOVE_REASON::kStoreInContainer;
|
|
|
|
inline void LoadINI(std::map<std::string, bool>* settings, const char* iniPath)
|
|
{
|
|
for (auto it = settings->begin(); it != settings->end(); it++) {
|
|
logger::info("[DEFAULT] {} = {}", it->first, it->second);
|
|
}
|
|
|
|
if (!std::filesystem::exists(iniPath)) {
|
|
logger::warn("{} does not exist, using default values.", iniPath);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
CSimpleIniA ini;
|
|
ini.SetUnicode(false);
|
|
ini.SetMultiKey(false);
|
|
ini.LoadFile(iniPath);
|
|
|
|
std::list<CSimpleIniA::Entry> keysList;
|
|
ini.GetAllKeys("", keysList);
|
|
|
|
bool bUpdateINI = false;
|
|
for (auto it = settings->begin(); it != settings->end(); it++) {
|
|
bool bExists = false;
|
|
for (const auto& k : keysList) {
|
|
if (it->first == k.pItem) {
|
|
settings->insert_or_assign(k.pItem, ini.GetBoolValue("", k.pItem, settings->at(k.pItem)));
|
|
logger::info("[INI] {} = {}", k.pItem, settings->at(k.pItem));
|
|
bExists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bExists) {
|
|
ini.SetBoolValue("", it->first.c_str(), it->second);
|
|
bUpdateINI = true;
|
|
}
|
|
}
|
|
|
|
if (bUpdateINI) {
|
|
logger::info("New settings detected, adding to EnderalSEEasyCrafting.ini");
|
|
ini.SaveFile(iniPath);
|
|
}
|
|
|
|
} catch (const std::exception& e) {
|
|
logger::error("ERROR: {}", e.what());
|
|
}
|
|
}
|
|
|
|
inline const std::map<std::string, bool> GetSettings(bool bInit = false)
|
|
{
|
|
static std::map<std::string, bool> settings;
|
|
if (bInit) {
|
|
settings.insert({ "LoadSuppliesEverywhere", false });
|
|
settings.insert({ "StoreInventorySupplies", true });
|
|
settings.insert({ "ShowFetchingNotification", true });
|
|
settings.insert({ "ShowReturningNotification", false });
|
|
LoadINI(&settings, "Data/SKSE/Plugins/EnderalSEEasyCrafting.ini");
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
inline RE::TESObjectREFR* GetChest()
|
|
{
|
|
return RE::TESForm::LookupByID<RE::TESObjectREFR>(0x5C132);
|
|
}
|
|
|
|
inline void NotifyFetching(const char* a_notification)
|
|
{
|
|
if (GetSettings().at("ShowFetchingNotification")) {
|
|
RE::DebugNotification(a_notification, nullptr, false);
|
|
}
|
|
}
|
|
|
|
inline void FetchSuppliesByType(RE::FormType formType)
|
|
{
|
|
const auto chest = GetChest();
|
|
const auto targetRef = RE::PlayerCharacter::GetSingleton();
|
|
|
|
const auto inv = chest->GetInventory([formType](RE::TESBoundObject& a_form) {
|
|
if (!a_form.Is(formType)) {
|
|
return false;
|
|
}
|
|
|
|
if (formType == RE::FormType::Misc && a_form.As<RE::BGSKeywordForm>()->GetNumKeywords() == 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
for (const auto& [item, data] : inv) {
|
|
const auto& [count, entry] = data;
|
|
if (count > 0) {
|
|
supplyCount.insert({ item, count });
|
|
chest->RemoveItem(item, count, iRemoveReason, nullptr, targetRef, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
inline void FetchByWorkbench(RE::TESFurniture* workbench)
|
|
{
|
|
if (!workbench) {
|
|
logger::warn("Invalid workbench argument in FetchByWorkbench");
|
|
return;
|
|
}
|
|
|
|
std::unordered_set<RE::FormID> checkedItems;
|
|
|
|
const auto chest = GetChest();
|
|
const auto playerRef = RE::PlayerCharacter::GetSingleton();
|
|
const auto inv = chest->GetInventory();
|
|
const auto keywords = workbench->As<RE::BGSKeywordForm>();
|
|
|
|
for (const auto& recipe : RE::TESDataHandler::GetSingleton()->GetFormArray<RE::BGSConstructibleObject>()) {
|
|
if (recipe->benchKeyword && keywords->HasKeyword(recipe->benchKeyword) && recipe->requiredItems.numContainerObjects > 0) {
|
|
for (int i = 0; i < recipe->requiredItems.numContainerObjects; i++) {
|
|
const auto component = recipe->requiredItems.GetContainerObjectAt(i).value();
|
|
const auto formID = (component && component->obj) ? component->obj->formID : NULL;
|
|
if (formID && !checkedItems.contains(formID)) {
|
|
checkedItems.insert(formID);
|
|
const auto it = inv.find(component->obj);
|
|
if (it != inv.end()) {
|
|
supplyCount.insert({ it->first, it->second.first });
|
|
chest->RemoveItem(it->first, it->second.first, iRemoveReason, nullptr, playerRef, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
inline void FetchSuppliesByForm(RE::TESForm* a_form)
|
|
{
|
|
if (!a_form) {
|
|
return;
|
|
}
|
|
|
|
const auto chest = GetChest();
|
|
const auto targetRef = RE::PlayerCharacter::GetSingleton();
|
|
|
|
const auto inv = chest->GetInventory([&](RE::TESBoundObject& a_xform) {
|
|
return a_form == &a_xform;
|
|
});
|
|
|
|
for (const auto& [item, data] : inv) {
|
|
const auto& [count, entry] = data;
|
|
if (count > 0) {
|
|
supplyCount.insert({ item, count });
|
|
chest->RemoveItem(item, count, iRemoveReason, nullptr, targetRef, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
inline bool IsQuestItem(RE::TESForm* a_form, RE::TESQuest* questElixir, RE::TESQuest* questCuthbert)
|
|
{
|
|
if (questElixir) {
|
|
if (a_form->formID == 0x34D22 || a_form->formID == 0x727E0 || a_form->formID == 0x3AD6F || a_form->formID == 0x59B86) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (questCuthbert) {
|
|
if (a_form->formID == 0x891AF || a_form->formID == 0x34C5D || a_form->formID == 0x34D32) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
inline void ReturnSupplies()
|
|
{
|
|
if (!bSuppliesFetched) {
|
|
return;
|
|
}
|
|
|
|
if (GetSettings().at("ShowReturningNotification")) {
|
|
RE::DebugNotification("Returning supplies...");
|
|
}
|
|
|
|
const auto chest = GetChest();
|
|
const auto playerRef = RE::PlayerCharacter::GetSingleton();
|
|
|
|
auto questElixir = RE::TESDataHandler::GetSingleton()->LookupForm<RE::TESQuest>(0x1C82F, "Enderal - Forgotten Stories.esm"); // FS_NQ05
|
|
auto questCuthbert = RE::TESDataHandler::GetSingleton()->LookupForm<RE::TESQuest>(0x1CA09, "Enderal - Forgotten Stories.esm"); // FS_NQ07
|
|
|
|
if (questElixir->currentStage != 35) {
|
|
questElixir = nullptr;
|
|
}
|
|
|
|
if (questCuthbert->currentStage != 115) {
|
|
questCuthbert = nullptr;
|
|
}
|
|
|
|
for (auto& item : supplyCount) {
|
|
if (!IsQuestItem(item.first, questElixir, questCuthbert)) {
|
|
playerRef->RemoveItem(item.first->As<RE::TESBoundObject>(), item.second, iRemoveReason, nullptr, chest, 0, 0);
|
|
}
|
|
}
|
|
supplyCount.clear();
|
|
|
|
if (GetSettings().at("StoreInventorySupplies")) {
|
|
|
|
const auto kOreIngot = RE::TESForm::LookupByID(0x914EC)->As<RE::BGSKeyword>(); // VendorItemOreIngot
|
|
const auto kAnimalPart = RE::TESForm::LookupByID(0x914EB)->As<RE::BGSKeyword>(); // VendorItemAnimalPart
|
|
const auto kAnimalHide = RE::TESForm::LookupByID(0x914EA)->As<RE::BGSKeyword>(); // VendorItemAnimalHide
|
|
const auto kFirewood = RE::TESForm::LookupByID(0xBECD7)->As<RE::BGSKeyword>(); // VendorItemFireword
|
|
const auto kTool = RE::TESForm::LookupByID(0x914EE)->As<RE::BGSKeyword>(); // VendorItemTool
|
|
const auto kClutter = RE::TESForm::LookupByID(0x914E9)->As<RE::BGSKeyword>(); // VendorItemClutter
|
|
const auto kFoodRaw = RE::TESForm::LookupByID(0xA0E56)->As<RE::BGSKeyword>(); // VendorItemFoodRaw
|
|
|
|
std::unordered_set<RE::FormID> checkedItems;
|
|
std::unordered_set<RE::TESForm*> suppliesToStore;
|
|
|
|
for (const auto& recipe : RE::TESDataHandler::GetSingleton()->GetFormArray<RE::BGSConstructibleObject>()) {
|
|
if (recipe->requiredItems.numContainerObjects > 0) {
|
|
for (int i = 0; i < recipe->requiredItems.numContainerObjects; i++) {
|
|
const auto component = recipe->requiredItems.GetContainerObjectAt(i).value();
|
|
const auto formID = (component && component->obj) ? component->obj->formID : NULL;
|
|
if (formID && !checkedItems.contains(formID)) {
|
|
checkedItems.insert(formID);
|
|
|
|
const auto keywords = component->obj->As<RE::BGSKeywordForm>();
|
|
|
|
if (component->obj->Is(RE::FormType::Misc) && !component->obj->IsGold() && !component->obj->IsLockpick()) {
|
|
if (keywords->HasKeyword(kOreIngot) || keywords->HasKeyword(kAnimalPart) || keywords->HasKeyword(kAnimalHide) || keywords->HasKeyword(kFirewood) || keywords->HasKeyword(kTool) || keywords->HasKeyword(kClutter) || keywords->HasKeyword(kFoodRaw)) {
|
|
suppliesToStore.insert(component->obj);
|
|
}
|
|
} else if (component->obj->Is(RE::FormType::AlchemyItem) && keywords->HasKeyword(kFoodRaw)) {
|
|
suppliesToStore.insert(component->obj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto inv = playerRef->GetInventory([&](RE::TESBoundObject& a_form) {
|
|
return a_form.Is(RE::FormType::Ingredient) || suppliesToStore.contains(&a_form);
|
|
});
|
|
|
|
for (const auto& [item, data] : inv) {
|
|
const auto& [count, entry] = data;
|
|
if (count > 0 && !entry->IsFavorited() && !entry->IsQuestObject() && !IsQuestItem(item, questElixir, questCuthbert)) {
|
|
playerRef->RemoveItem(item, count, iRemoveReason, nullptr, chest, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::thread([]() {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(3));
|
|
SKSE::GetTaskInterface()->AddTask([]() {
|
|
const auto spell = RE::TESForm::LookupByID(0x824AF);
|
|
if (spell) {
|
|
RE::PlayerCharacter::GetSingleton()->RemoveSpell(spell->As<RE::SpellItem>());
|
|
spell->As<RE::SpellItem>()->effects[0]->effectItem.magnitude = 1;
|
|
}
|
|
});
|
|
}).detach();
|
|
|
|
bSuppliesFetched = false;
|
|
}
|
|
|
|
inline bool FetchSupplies(RE::TESFurniture* a_furn)
|
|
{
|
|
if (!a_furn || bSuppliesFetched) {
|
|
return false;
|
|
}
|
|
|
|
supplyCount.clear();
|
|
|
|
const auto chestAct = RE::TESForm::LookupByID(0x2EDD7);
|
|
const auto playerRef = RE::PlayerCharacter::GetSingleton();
|
|
bool bAllowFetch = false;
|
|
|
|
if (GetSettings().at("LoadSuppliesEverywhere") || playerRef->GetParentCell()->GetFactionOwner() || playerRef->GetParentCell()->GetActorOwner() || a_furn->As<RE::BGSKeywordForm>()->HasKeywordString("_00E_Phasmalist_CraftingSummoningWorkbench")) {
|
|
bAllowFetch = true;
|
|
} else {
|
|
RE::TES::GetSingleton()->ForEachReferenceInRange(playerRef, 4000, [&chestAct, &bAllowFetch](RE::TESObjectREFR& b_ref) {
|
|
if (chestAct == b_ref.GetBaseObject()) {
|
|
bAllowFetch = true;
|
|
return RE::BSContainer::ForEachResult::kStop;
|
|
}
|
|
return RE::BSContainer::ForEachResult::kContinue;
|
|
});
|
|
}
|
|
|
|
if (!bAllowFetch) {
|
|
return false;
|
|
}
|
|
|
|
bSuppliesFetched = true;
|
|
|
|
const auto spellForm = RE::TESForm::LookupByID(0x824AF);
|
|
if (spellForm) {
|
|
const auto spell = spellForm->As<RE::SpellItem>();
|
|
playerRef->RemoveSpell(spell);
|
|
spell->effects[0]->effectItem.magnitude = 30000;
|
|
playerRef->AddSpell(spell);
|
|
}
|
|
|
|
std::thread([a_furn]() {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(3));
|
|
SKSE::GetTaskInterface()->AddTask([a_furn]() {
|
|
NotifyFetching("Fetching supplies...");
|
|
|
|
switch (a_furn->workBenchData.benchType.underlying()) {
|
|
|
|
case (int)RE::TESFurniture::WorkBenchData::BenchType::kAlchemy:
|
|
case (int)RE::TESFurniture::WorkBenchData::BenchType::kAlchemyExperiment:
|
|
FetchSuppliesByType(RE::FormType::Ingredient);
|
|
break;
|
|
|
|
case (int)RE::TESFurniture::WorkBenchData::BenchType::kEnchanting:
|
|
case (int)RE::TESFurniture::WorkBenchData::BenchType::kEnchantingExperiment:
|
|
FetchSuppliesByType(RE::FormType::SoulGem);
|
|
break;
|
|
|
|
default:
|
|
FetchByWorkbench(a_furn);
|
|
}
|
|
});
|
|
}).detach();
|
|
|
|
return true;
|
|
}
|
|
|
|
inline bool GetSuppliesFetched()
|
|
{
|
|
return bSuppliesFetched;
|
|
}
|
|
|
|
inline void FetchSpectralizingSupplies()
|
|
{
|
|
if (GetSettings().at("ShowFetchingNotification")) {
|
|
RE::DebugNotification("Fetching supplies...");
|
|
}
|
|
|
|
bSuppliesFetched = true;
|
|
|
|
supplyCount.clear();
|
|
FetchSuppliesByType(RE::FormType::SoulGem);
|
|
FetchSuppliesByForm(RE::TESForm::LookupByID(0x34CDD)); // Bone Meal
|
|
FetchSuppliesByForm(RE::TESForm::LookupByID(0x3AD63)); // Ectoplasm
|
|
}
|
|
|