1
Fork 0
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.
 
 
 

824 lines
28 KiB

#include "ArtifactTracker.h"
#include "BookCheck.h"
#include "EventListener.h"
#include "Util.h"
using namespace SKSE;
using namespace SKSE::log;
namespace ArtifactTracker
{
bool g_bLoaded = false;
bool g_bSaveLoaded = true;
bool g_bHomeContainer = false;
bool g_bBookShelf = false;
bool g_bTakeAll = false;
bool g_bNotifyNewArtifact = false;
std::uint32_t g_bTakeAllCount = 0;
std::int32_t g_iFollowerIndex = 0;
RE::TESBoundObject* g_cellContainer;
RE::BGSListForm* g_listNew;
RE::BGSListForm* g_listStored;
RE::BGSListForm* g_listFound;
RE::BGSListForm* g_persistentStorage;
RE::BGSKeyword* g_homeKeyword;
std::unordered_map<RE::FormID, RE::TESForm*> g_artifactMap;
std::unordered_set<RE::FormType> g_artifactFormTypes;
std::unordered_set<RE::FormType> g_artifactAllFormTypes;
std::unordered_map<RE::FormID, RE::TESObjectREFR*> g_persistentMap;
RE::TESObjectREFR* g_cellStorage;
const SKSE::LoadInterface* g_loadInterface;
bool Init(bool bKID)
{
if (g_bLoaded) {
return true;
}
const auto dataHandler = RE::TESDataHandler::GetSingleton();
if (!dataHandler) {
// Called before kDataLoaded?
log::error("DataHandler is not initialized.");
return false;
}
SKSE::GetModCallbackEventSource()->RemoveEventSink(EventListener::GetSingleton());
g_cellContainer = dataHandler->LookupForm(0x804, "Artifact Tracker.esp")->As<RE::TESBoundObject>(); // ETR_CellStorageContainer
g_listNew = dataHandler->LookupForm<RE::BGSListForm>(0x800, "Artifact Tracker.esp"); // ETR_ItemsNew
g_listStored = dataHandler->LookupForm<RE::BGSListForm>(0x801, "Artifact Tracker.esp"); // ETR_ItemsStored
g_listFound = dataHandler->LookupForm<RE::BGSListForm>(0x802, "Artifact Tracker.esp"); // ETR_ItemsFound
g_persistentStorage = dataHandler->LookupForm<RE::BGSListForm>(0x803, "Artifact Tracker.esp"); // ETR_PersistentStorageList
g_homeKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0xFC1A3, "Skyrim.esm"); // LocTypePlayerHouse
const auto extraArtifactKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0xAFC110, "Update.esm"); // ETR_ExtraArtifact
const auto notArtifactKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0xAFC111, "Update.esm"); // ETR_NotArtifact
const auto npcRaceKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0x13794, "Skyrim.esm"); // ActorTypeNPC
if (!g_cellContainer || !g_listNew || !g_listStored || !g_listFound || !g_persistentStorage || !g_homeKeyword || !extraArtifactKeyword || !notArtifactKeyword || !npcRaceKeyword) {
log::warn("Unable to load data from Artifact Tracker.esp");
RE::DebugMessageBox("Unable to load data from Artifact Tracker.esp, the mod is disabled.");
return false;
}
std::map<std::string, bool> settings{
{ "DumpItemList", false },
{ "NewArtifactNotifications", false },
};
LoadINI(&settings, "Data/SKSE/Plugins/ArtifactTracker.ini");
g_bNotifyNewArtifact = settings.at("NewArtifactNotifications");
// Preloading item lists
g_artifactAllFormTypes.insert(RE::FormType::Weapon);
g_artifactAllFormTypes.insert(RE::FormType::Armor);
g_artifactAllFormTypes.insert(RE::FormType::Book);
g_artifactAllFormTypes.insert(RE::FormType::Misc);
g_artifactAllFormTypes.insert(RE::FormType::AlchemyItem);
g_artifactAllFormTypes.insert(RE::FormType::Ingredient);
g_artifactAllFormTypes.insert(RE::FormType::SoulGem);
g_artifactFormTypes.insert(RE::FormType::Weapon);
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectWEAP>()) {
if (form->GetPlayable() && !form->IsBound() && !form->weaponData.flags.all(RE::TESObjectWEAP::Data::Flag::kCantDrop)) {
if ((!form->HasKeyword(notArtifactKeyword) || form->HasKeyword(extraArtifactKeyword)) && strlen(form->GetName()) > 0) {
g_artifactMap[form->formID] = form;
}
}
}
g_artifactMap.erase(0x1F4); // Unarmed
g_artifactFormTypes.insert(RE::FormType::Armor);
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectARMO>()) {
if (form->GetPlayable() && form->race && (form->race->formID == 0x19 || form->race->HasKeyword(npcRaceKeyword))) {
if ((!form->HasKeyword(notArtifactKeyword) || form->HasKeyword(extraArtifactKeyword)) && strlen(form->GetName()) > 0) {
g_artifactMap[form->formID] = form;
}
}
}
g_artifactMap.erase(0xD64); // SkinNaked
g_artifactMap.erase(0x69CE3); // SkinNakedBeast
g_artifactMap.erase(0xCDD86); // SkinNakedWerewolfBeast
g_artifactFormTypes.insert(RE::FormType::Book);
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectBOOK>()) {
if ((form->HasKeyword(extraArtifactKeyword) || (!form->TeachesSpell() && BookCheck::IsBook(form))) && !form->HasKeyword(notArtifactKeyword)) {
g_artifactMap[form->formID] = form;
}
}
g_artifactFormTypes.insert(RE::FormType::Misc);
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectMISC>()) {
if (form->GetPlayable() && (form->GetNumKeywords() == 0 || (!bKID && form->HasKeyword(extraArtifactKeyword)) || (bKID && (!form->HasKeyword(notArtifactKeyword) || form->HasKeyword(extraArtifactKeyword)))) && strlen(form->GetName()) > 0) {
g_artifactMap[form->formID] = form;
}
}
g_artifactMap.erase(0xA); // Lockpick
g_artifactMap.erase(0xF); // Gold
for (const auto& form : dataHandler->GetFormArray<RE::AlchemyItem>()) {
if (form->HasKeyword(extraArtifactKeyword) && !form->HasKeyword(notArtifactKeyword)) {
g_artifactMap[form->formID] = form;
g_artifactFormTypes.insert(RE::FormType::AlchemyItem);
}
}
for (const auto& form : dataHandler->GetFormArray<RE::IngredientItem>()) {
if (form->HasKeyword(extraArtifactKeyword) && !form->HasKeyword(notArtifactKeyword)) {
g_artifactMap[form->formID] = form;
g_artifactFormTypes.insert(RE::FormType::Ingredient);
}
}
for (const auto& form : dataHandler->GetFormArray<RE::TESSoulGem>()) {
if (form->HasKeyword(extraArtifactKeyword) && !form->HasKeyword(notArtifactKeyword)) {
g_artifactMap[form->formID] = form;
g_artifactFormTypes.insert(RE::FormType::SoulGem);
}
}
EventListener::Install();
OnGameLoad(); // covers new game and coc'ing from the main menu
g_bLoaded = true;
RE::ConsoleLog::GetSingleton()->Print(std::format("Artifact Tracker registered {} items.", g_artifactMap.size()).c_str());
log::info("Total artifacts: {}", g_artifactMap.size());
if (settings.at("DumpItemList")) {
for (const auto& item : g_artifactMap) {
log::info("[{:08X}] {}", item.second->formID, item.second->GetName());
}
}
return true;
}
bool IsArtifact(const RE::TESForm* a_form)
{
return a_form && g_artifactFormTypes.contains(a_form->GetFormType()) && g_artifactMap.contains(a_form->formID);
}
RE::TESForm* GetArtifactByID(const RE::FormID a_formID)
{
if (!a_formID) {
return nullptr;
}
const auto it = g_artifactMap.find(a_formID);
return it != g_artifactMap.end() ? it->second : nullptr;
}
void OnGameLoad()
{
#ifdef _DEBUG
log::info("OnGameLoad");
#endif
g_persistentMap.clear();
g_persistentStorage->ForEachForm([&](RE::TESForm& a_exform) {
if (&a_exform) {
g_persistentMap[a_exform.formID] = a_exform.As<RE::TESObjectREFR>();
}
return true;
});
std::uint32_t savedCount = g_listStored->forms.size() + g_listFound->forms.size() + g_listNew->forms.size();
if (savedCount != g_artifactMap.size()) {
ListRevert(g_listNew);
}
RescanStoredArtifacts();
RescanFoundArtifacts();
RescanNewArtifacts();
const auto vm = RE::BSScript::Internal::VirtualMachine::GetSingleton();
RE::BSTSmartPointer<RE::BSScript::IStackCallbackFunctor> stackCallback;
if (const auto pluginInfo = g_loadInterface->GetPluginInfo("Ahzaab's moreHUD Plugin"); pluginInfo) {
if (!g_bLoaded) log::info("Detected {} v{}", pluginInfo->name, pluginInfo->version);
if (pluginInfo->version == 0) {
log::error("MoreHUD has not been detected.");
} else if (pluginInfo->version < 30800) {
log::error("MoreHUD is outdated.");
} else if (vm->TypeIsValid("AhzMoreHud")) {
if (!g_bLoaded) log::info("Registering icons in MoreHUD...");
vm->DispatchStaticCall("AhzMoreHud", "RegisterIconFormList", RE::MakeFunctionArguments<RE::BSString, RE::BGSListForm*>("dbmNew", std::move(g_listNew)), stackCallback);
vm->DispatchStaticCall("AhzMoreHud", "RegisterIconFormList", RE::MakeFunctionArguments<RE::BSString, RE::BGSListForm*>("dbmFound", std::move(g_listFound)), stackCallback);
vm->DispatchStaticCall("AhzMoreHud", "RegisterIconFormList", RE::MakeFunctionArguments<RE::BSString, RE::BGSListForm*>("dbmDisp", std::move(g_listStored)), stackCallback);
} else {
log::error("MoreHUD has not been installed correctly.");
}
} else if (!g_bLoaded) {
log::error("MoreHUD has not been detected.");
}
if (const auto pluginInfo = g_loadInterface->GetPluginInfo("Ahzaab's moreHUD Inventory Plugin"); pluginInfo) {
if (!g_bLoaded) log::info("Detected {} v{}", pluginInfo->name, pluginInfo->version);
if (pluginInfo->version == 0) {
log::error("MoreHUD Inventory Edition has not been detected.");
} else if (pluginInfo->version < 10017) {
log::error("MoreHUD Inventory Edition is outdated.");
} else if (vm->TypeIsValid("AhzMoreHudIE")) {
if (!g_bLoaded) log::info("Registering icons in MoreHUD Inventory Edition...");
vm->DispatchStaticCall("AhzMoreHudIE", "RegisterIconFormList", RE::MakeFunctionArguments<RE::BSString, RE::BGSListForm*>("dbmNew", std::move(g_listNew)), stackCallback);
vm->DispatchStaticCall("AhzMoreHudIE", "RegisterIconFormList", RE::MakeFunctionArguments<RE::BSString, RE::BGSListForm*>("dbmFound", std::move(g_listFound)), stackCallback);
vm->DispatchStaticCall("AhzMoreHudIE", "RegisterIconFormList", RE::MakeFunctionArguments<RE::BSString, RE::BGSListForm*>("dbmDisp", std::move(g_listStored)), stackCallback);
} else {
log::error("MoreHUD Inventory Edition has not been installed correctly.");
}
} else if (!g_bLoaded) {
log::error("MoreHUD Inventory Edition has not been detected.");
}
// TODO: Uncomment when/if QuickLoot EE brings back registering formlists
/*
if (const auto pluginInfo = g_loadInterface->GetPluginInfo("QuickLootEE"); pluginInfo) {
if (!g_bLoaded) log::info("Detected {} v{}", pluginInfo->name, pluginInfo->version);
if (pluginInfo->version == 0) {
log::error("QuickLoot EE has not been detected.");
} else if (vm->TypeIsValid("QuickLootEE")) {
if (!g_bLoaded) log::info("Registering icons with QuickLootEE...");
vm->DispatchStaticCall("QuickLootEE", "RegisterNewItemsList", RE::MakeFunctionArguments<RE::BGSListForm*>(std::move(g_listNew)), stackCallback);
vm->DispatchStaticCall("QuickLootEE", "RegisterFoundItemsList", RE::MakeFunctionArguments<RE::BGSListForm*>(std::move(g_listFound)), stackCallback);
vm->DispatchStaticCall("QuickLootEE", "RegisterDisplayedItemsList", RE::MakeFunctionArguments<RE::BGSListForm*>(std::move(g_listStored)), stackCallback);
} else {
log::error("QuickLoot EE has not been installed correctly.");
}
} else if (!g_bLoaded) {
log::error("QuickLoot EE has not been detected.");
}
*/
}
void SetContainerMode(const bool bOpening)
{
if (bOpening) {
const auto refr = RE::TESObjectREFR::LookupByHandle(RE::ContainerMenu::GetTargetRefHandle());
g_bHomeContainer = IsHome()
&& refr
&& IsInSameCell(refr.get())
&& !g_persistentMap.contains(refr.get()->formID);
g_bBookShelf = g_bHomeContainer && refr->GetBaseObject()->formID == 0xDC9E7;
#ifdef _DEBUG
if (g_bHomeContainer) {
RE::DebugNotification("Delayed processing enabled");
}
#endif
} else if (g_bHomeContainer) {
g_bHomeContainer = false;
if (g_bBookShelf) {
g_bBookShelf = false;
std::thread([]() {
std::this_thread::sleep_for(std::chrono::milliseconds(1200));
ArtifactTracker::SyncCellStorage();
}).detach();
} else {
SyncCellStorage();
}
}
}
bool IsHome()
{
return (bool)g_cellStorage;
}
bool ToggleHomeMode(RE::TESObjectREFR* cellStorage)
{
if (cellStorage) {
g_bHomeContainer = false;
g_cellStorage = cellStorage;
RE::ScriptEventSourceHolder::GetSingleton()->AddEventSink<RE::TESActivateEvent>(EventListener::GetSingleton());
#ifdef _DEBUG
log::info("Home mode ON");
#endif
return true;
} else if (g_cellStorage) {
g_bHomeContainer = false;
g_cellStorage = nullptr;
RE::ScriptEventSourceHolder::GetSingleton()->RemoveEventSink<RE::TESActivateEvent>(EventListener::GetSingleton());
#ifdef _DEBUG
log::info("Home mode OFF");
#endif
}
return false;
}
bool IsValidContainer(RE::TESObjectREFR* a_ref)
{
if (!a_ref || a_ref->IsMarkedForDeletion()) {
return false;
}
const auto baseObj = a_ref->GetBaseObject();
return baseObj->formType == RE::FormType::Container || (baseObj->formType == RE::FormType::NPC && !a_ref->IsDisabled() && baseObj->As<RE::TESNPC>()->GetRace()->formID == 0x10760A);
}
void OnCellEnter(const RE::FormID a_formID)
{
if (!g_bSaveLoaded) {
// Cell load events fire before formlists are loaded from savegame
return;
}
RE::TESObjectCELL* cell = RE::TESForm::LookupByID<RE::TESObjectCELL>(a_formID);
RE::BGSLocation* location = cell ? cell->GetLocation() : nullptr;
if (!cell || !location || !cell->IsInteriorCell() || !location->HasKeyword(g_homeKeyword)) {
if (IsHome()) {
RE::ScriptEventSourceHolder::GetSingleton()->RemoveEventSink<RE::TESCellFullyLoadedEvent>(EventListener::GetSingleton());
ToggleHomeMode(nullptr);
}
return;
}
RE::TESObjectREFR* cellStorage = nullptr;
bool bHasDupes = false;
g_persistentStorage->ForEachForm([&](RE::TESForm& a_form) {
const auto refr = a_form.As<RE::TESObjectREFR>();
if (refr && refr->GetParentCell()->formID == a_formID && refr->GetBaseObject() == g_cellContainer) {
if (cellStorage) {
log::warn("Multiple cell storages detected in {}", cell->GetName());
bHasDupes = true;
} else {
cellStorage = refr;
}
}
return true;
});
#ifdef _DEBUG
if (cellStorage) {
log::info("Found cell storage in {} (first pass)", cell->GetName());
}
#endif
ToggleHomeMode(cellStorage);
if (!cellStorage || bHasDupes) {
RE::ScriptEventSourceHolder::GetSingleton()->AddEventSink<RE::TESCellFullyLoadedEvent>(EventListener::GetSingleton());
}
}
void OnCellEnter(const RE::BGSLocation* location, const RE::TESObjectCELL* cell)
{
RE::ScriptEventSourceHolder::GetSingleton()->RemoveEventSink<RE::TESCellFullyLoadedEvent>(EventListener::GetSingleton());
if (!g_bSaveLoaded) {
// Cell load events fire before formlists are loaded from savegame, duh!
return;
}
if (!location || !cell->IsInteriorCell() || cell != RE::PlayerCharacter::GetSingleton()->GetParentCell() || !location->HasKeyword(g_homeKeyword)) {
ToggleHomeMode(nullptr);
return;
}
RE::TESObjectREFR* cellStorage = nullptr;
std::vector<RE::TESObjectREFR*> dupes;
cell->ForEachReference([&cellStorage, &dupes](RE::TESObjectREFR& a_ref) {
if (a_ref.GetBaseObject() == g_cellContainer && !a_ref.IsMarkedForDeletion()) {
if (cellStorage) {
dupes.push_back(&a_ref);
} else {
cellStorage = &a_ref;
}
}
return true;
});
for (int i = 0; i < dupes.size(); i++) {
log::warn("Removing duplicate storage {:08X}", dupes[i]->formID);
g_persistentMap.erase(dupes[i]->formID);
ListRemoveItem(g_persistentStorage, dupes[i]);
dupes[i]->Disable();
dupes[i]->SetDelete(true);
}
dupes.clear();
if (cellStorage) {
#ifdef _DEBUG
log::info("Found cell storage in {} (second pass)", cell->GetName());
#endif
if (!g_persistentMap.contains(cellStorage->formID)) {
g_persistentStorage->AddForm(cellStorage);
g_persistentMap[cellStorage->formID] = cellStorage;
}
ToggleHomeMode(cellStorage);
SyncCellStorage();
return;
}
#ifdef _DEBUG
log::info("Adding new storage in {}", cell->GetName());
#endif
SKSE::GetTaskInterface()->AddTask([]() {
const auto cellStorage = RE::PlayerCharacter::GetSingleton()->PlaceObjectAtMe(g_cellContainer, true).get();
if (cellStorage) {
#ifdef _DEBUG
log::info("Created storage {:08X}", cellStorage->formID);
#endif
cellStorage->Disable();
g_persistentStorage->AddForm(cellStorage);
g_persistentMap[cellStorage->formID] = cellStorage;
ToggleHomeMode(cellStorage);
SyncCellStorage();
} else {
log::error("Failed to create cell storage in OnCellEnter");
ToggleHomeMode(nullptr);
}
});
}
void SyncCellStorage(const RE::TESObjectREFR* a_ignoreRef)
{
if (!IsHome()) {
#ifdef _DEBUG
log::info("SyncCellStorage called while not at home");
#endif
return;
}
#ifdef _DEBUG
log::info("Running SyncCellStorage");
#endif
const RE::FormID ignoreFormID = a_ignoreRef ? a_ignoreRef->formID : NULL;
SKSE::GetTaskInterface()->AddTask([ignoreFormID]() {
std::unordered_set<RE::FormID> cellItems;
const auto cell = g_cellStorage->GetParentCell();
const auto inv = g_cellStorage->GetInventory();
cell->ForEachReference([&](RE::TESObjectREFR& a_ref) {
if (ignoreFormID && ignoreFormID == a_ref.formID) {
return true;
}
const auto baseObj = a_ref.GetBaseObject();
if (IsValidContainer(&a_ref)) {
if (g_cellContainer == baseObj || baseObj->formID == 0xDC9E7 || g_persistentMap.contains(a_ref.formID)) { // skip persistent and PlayerBookShelfContainer
return true;
}
const auto contInv = a_ref.GetInventory([&](RE::TESBoundObject& a_object) -> bool {
return !cellItems.contains(a_object.formID) && g_artifactAllFormTypes.contains(a_object.GetFormType());
});
for (const auto& [item, data] : contInv) {
if (data.first > 0) {
cellItems.insert(item->formID);
if (inv.find(item) == inv.end()) {
g_cellStorage->AddObjectToContainer(item, nullptr, 1, nullptr);
}
if (IsArtifact(item) && !g_listStored->HasForm(item)) {
ListRemoveItem(g_listNew, item);
ListRemoveItem(g_listFound, item);
g_listStored->AddForm(item);
}
}
}
return true;
}
if (!g_artifactAllFormTypes.contains(baseObj->GetFormType()) || a_ref.IsDisabled() || a_ref.IsMarkedForDeletion() || cellItems.contains(baseObj->formID)) {
return true;
}
cellItems.insert(baseObj->formID);
if (inv.find(baseObj) == inv.end()) {
g_cellStorage->AddObjectToContainer(baseObj, nullptr, 1, nullptr);
}
if (IsArtifact(baseObj) && !g_listStored->HasForm(baseObj)) {
ListRemoveItem(g_listNew, baseObj);
ListRemoveItem(g_listFound, baseObj);
g_listStored->AddForm(baseObj);
}
return true;
});
for (const auto& [item, data] : inv) {
const auto& [count, entry] = data;
if (count > 0 && !cellItems.contains(item->formID)) {
g_cellStorage->RemoveItem(item, count, RE::ITEM_REMOVE_REASON::kRemove, nullptr, nullptr);
if (IsArtifact(item) && !RefListHasItem(g_persistentStorage, item->formID)) {
ListRemoveItem(g_listStored, item);
if (GetItemCount(RE::PlayerCharacter::GetSingleton(), item) || FollowersHaveItem(item)) {
ListRemoveItem(g_listNew, item);
g_listFound->AddForm(item);
} else {
ListRemoveItem(g_listFound, item);
g_listNew->AddForm(item);
}
}
}
}
cellItems.clear();
});
}
void OnContainerChanged(const RE::TESContainerChangedEvent* a_event, RE::TESForm* form)
{
if (a_event->newContainer) { // added to a container or actor
if (a_event->newContainer == 0x14) { // acquired by player
if (a_event->oldContainer) {
if (const auto it = g_persistentMap.find(a_event->oldContainer); it != g_persistentMap.end()) { // moved from a persistent container
const auto ref = it->second;
if (ref && !GetItemCount(ref, form)) { // no items left in the container
for (const auto& persref : g_persistentMap) {
if (persref.second != ref) {
if (GetItemCount(persref.second, form)) {
// if other containers have it, do nothing
return;
}
}
}
ListRemoveItem(g_listStored, form);
g_listFound->AddForm(form);
}
return;
} else if (g_cellStorage) { // taken from a container at home
if (!g_bHomeContainer) {
const auto container = RE::TESForm::LookupByID<RE::TESObjectREFR>(a_event->oldContainer);
if (container && !GetItemCount(container, form)) {
SyncCellStorage(container);
}
}
return;
}
}
if (!g_listStored->HasForm(form) && !g_listFound->HasForm(form)) { // it's a new item, move it to found
ListRemoveItem(g_listNew, form);
g_listFound->AddForm(form);
if (g_bNotifyNewArtifact) {
if (g_bTakeAll) {
g_bTakeAllCount++;
} else {
//RE::DebugNotification(fmt::format("New artifact acquired: {}", form->GetName()).c_str());
RE::BSTSmartPointer<RE::BSScript::IStackCallbackFunctor> stackCallback;
RE::BSScript::Internal::VirtualMachine::GetSingleton()->DispatchStaticCall("ETR_NewArtifactNotification", "Show", RE::MakeFunctionArguments<RE::TESForm*>(std::move(form)), stackCallback);
}
}
}
} else if (g_cellStorage && g_cellStorage->formID == a_event->newContainer) {
return; // ignore cell storage
} else if (g_persistentMap.contains(a_event->newContainer)) { // stored in a registered persistent container
if (!g_listStored->HasForm(form)) {
ListRemoveItem(g_listFound, form);
g_listStored->AddForm(form);
}
} else {
const auto ref = RE::TESForm::LookupByID(a_event->newContainer);
if (ref->Is(RE::FormType::ActorCharacter)) {
if (ref->As<RE::Actor>()->IsPlayerTeammate()) { // acquired by companion
if (!g_listFound->HasForm(form) && !g_listStored->HasForm(form)) {
ListRemoveItem(g_listNew, form);
g_listFound->AddForm(form);
}
}
} else {
const auto container = ref->As<RE::TESObjectREFR>();
if (container) {
if (g_cellStorage && IsInSameCell(container)) { // stored at home
if (!g_bHomeContainer && !g_listStored->HasForm(form)) {
ListRemoveItem(g_listFound, form);
ListRemoveItem(g_listNew, form);
g_listStored->AddForm(form);
}
} else if (a_event->oldContainer == 0x14 && !g_listStored->HasForm(form) && !GetItemCount(RE::PlayerCharacter::GetSingleton(), form) && !FollowersHaveItem(form)) {
// disposed by player
ListRemoveItem(g_listFound, form);
g_listNew->AddForm(form);
}
}
}
}
} else if (a_event->oldContainer) { // removed from container
if (g_cellStorage && a_event->reference) { // dropped or placed on rack at home by any actor
if (a_event->oldContainer != 0x14) {
const auto ref = RE::TESForm::LookupByID<RE::TESObjectREFR>(a_event->oldContainer);
if (!ref || !IsInSameCell(ref)) {
return;
}
}
if (!GetItemCount(g_cellStorage, form)) {
#ifdef _DEBUG
log::info("Added dropped {} to cell storage", form->GetName());
RE::DebugNotification("Adding to cell storage");
#endif
g_cellStorage->AddObjectToContainer(form->As<RE::TESBoundObject>(), nullptr, 1, nullptr);
}
if (!g_listStored->HasForm(form)) {
ListRemoveItem(g_listFound, form);
ListRemoveItem(g_listNew, form);
g_listStored->AddForm(form);
}
} else if (a_event->oldContainer == 0x14) { // dropped, consumed, dismantled, removed by script
if (!g_listStored->HasForm(form)) {
if (!GetItemCount(RE::PlayerCharacter::GetSingleton(), form) && !FollowersHaveItem(form)) {
ListRemoveItem(g_listFound, form);
g_listNew->AddForm(form);
} else if (!g_listFound->HasForm(form)) {
ListRemoveItem(g_listNew, form);
g_listFound->AddForm(form);
}
}
} else if (g_cellStorage && g_cellStorage->formID == a_event->oldContainer) {
return; // ignore cell storage
} else if (const auto it = g_persistentMap.find(a_event->oldContainer); it != g_persistentMap.end()) { // deleted from a persistent container
const auto ref = it->second;
if (ref && !GetItemCount(ref, form)) { // no items left in the container
for (const auto& persref : g_persistentMap) {
if (persref.second != ref) {
if (GetItemCount(persref.second, form)) {
// if other containers have it, do nothing
return;
}
}
}
ListRemoveItem(g_listStored, form);
if (GetItemCount(RE::PlayerCharacter::GetSingleton(), form) || FollowersHaveItem(form)) {
g_listFound->AddForm(form);
} else {
g_listNew->AddForm(form);
}
}
} else {
const auto ref = RE::TESForm::LookupByID(a_event->oldContainer);
if (ref->Is(RE::FormType::ActorCharacter)) {
const auto actor = ref->As<RE::Actor>();
if (actor && actor->IsPlayerTeammate() && !GetItemCount(actor, form)) { // removed from companion (probably, disarmed)
if (g_listFound->HasForm(form)) {
if (!GetItemCount(RE::PlayerCharacter::GetSingleton(), form)) {
// player does not have it, check companions
if (const auto processLists = RE::ProcessLists::GetSingleton(); processLists) {
for (auto& actorHandle : processLists->highActorHandles) {
if (auto actor = actorHandle.get(); actor && actor->IsPlayerTeammate() && actor->formID != ref->formID) {
if (GetItemCount(actor.get(), form)) {
// other companion has it, do nothing
return;
}
}
}
}
ListRemoveItem(g_listFound, form);
g_listNew->AddForm(form);
}
}
}
} else {
const auto container = ref->As<RE::TESObjectREFR>();
if (container) {
if (g_cellStorage && IsInSameCell(container)) { // removed from container at home
if (!GetItemCount(container, form)) {
SyncCellStorage(container);
}
}
}
}
}
}
}
void AddRefArtifactsToList(RE::TESForm* a_refOrList, RE::BGSListForm* a_targetList, RE::BGSListForm* a_excludeList)
{
if (!a_refOrList || !a_targetList) {
log::warn("Invalid arguments in AddRefArtifactsToList");
return;
}
if (a_refOrList->Is(RE::FormType::FormList)) {
a_refOrList->As<RE::BGSListForm>()->ForEachForm([&](RE::TESForm& a_exform) {
const auto refrItem = a_exform.As<RE::TESObjectREFR>();
if (refrItem) {
AddRefArtifactsToList(refrItem, a_targetList, a_excludeList);
}
return true;
});
return;
}
const auto containerRef = a_refOrList->As<RE::TESObjectREFR>();
if (!containerRef) {
log::warn("containerRef in AddRefArtifactsToList is not a reference");
return;
}
const auto inv = containerRef->GetInventory([&](RE::TESBoundObject& a_exform) {
return ArtifactTracker::IsArtifact(&a_exform) && (!a_excludeList || !a_excludeList->HasForm(&a_exform));
});
for (const auto& item : inv) {
if (item.second.first > 0) {
a_targetList->AddForm(item.first);
ListRemoveItem(g_listNew, item.first);
}
}
}
void RescanFoundArtifacts()
{
ListRevert(g_listFound);
AddRefArtifactsToList(RE::PlayerCharacter::GetSingleton(), g_listFound, g_listStored);
for (const auto& ref : GetPlayerFollowers()) {
AddRefArtifactsToList(ref, g_listFound, g_listStored);
}
}
void RescanStoredArtifacts()
{
ListRevert(g_listStored);
AddRefArtifactsToList(g_persistentStorage, g_listStored);
}
void RescanNewArtifacts()
{
for (auto const& item : g_artifactMap) {
if (!g_listNew->HasForm(item.second) && !g_listStored->HasForm(item.second) && !g_listFound->HasForm(item.second)) {
g_listNew->AddForm(item.second);
}
}
}
void OnLocationChange()
{
std::int32_t iCurrentFollowers = 0;
for (const auto& actor : GetPlayerFollowers()) {
iCurrentFollowers += actor->formID;
}
if (iCurrentFollowers != g_iFollowerIndex) {
g_iFollowerIndex = iCurrentFollowers;
std::thread([]() {
std::this_thread::sleep_for(std::chrono::milliseconds(3000)); // wait for followers to load into the new cell
SKSE::GetTaskInterface()->AddTask([]() {
RescanFoundArtifacts();
});
}).detach();
}
}
}