#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; bool g_bWarnMissingMoreHUD = true; 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 g_artifactMap; std::unordered_set g_artifactFormTypes; std::unordered_set g_artifactAllFormTypes; std::unordered_map 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(); // ETR_CellStorageContainer g_listNew = dataHandler->LookupForm(0x800, "Artifact Tracker.esp"); // ETR_ItemsNew g_listStored = dataHandler->LookupForm(0x801, "Artifact Tracker.esp"); // ETR_ItemsStored g_listFound = dataHandler->LookupForm(0x802, "Artifact Tracker.esp"); // ETR_ItemsFound g_persistentStorage = dataHandler->LookupForm(0x803, "Artifact Tracker.esp"); // ETR_PersistentStorageList g_homeKeyword = dataHandler->LookupForm(0xFC1A3, "Skyrim.esm"); // LocTypePlayerHouse const auto extraArtifactKeyword = dataHandler->LookupForm(0xAFC110, "Update.esm"); // ETR_ExtraArtifact const auto notArtifactKeyword = dataHandler->LookupForm(0xAFC111, "Update.esm"); // ETR_NotArtifact const auto npcRaceKeyword = dataHandler->LookupForm(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 settings{ { "DumpItemList", false }, { "NewArtifactNotifications", false }, { "WarnMissingMoreHUD", true }, { "WarnMissingKID", true }, }; LoadINI(&settings, "Data/SKSE/Plugins/ArtifactTracker.ini"); g_bNotifyNewArtifact = settings.at("NewArtifactNotifications"); g_bWarnMissingMoreHUD = settings.at("WarnMissingMoreHUD"); // 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()) { 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()) { 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()) { 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()) { 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()) { if (form->HasKeyword(extraArtifactKeyword) && !form->HasKeyword(notArtifactKeyword)) { g_artifactMap[form->formID] = form; g_artifactFormTypes.insert(RE::FormType::AlchemyItem); } } for (const auto& form : dataHandler->GetFormArray()) { if (form->HasKeyword(extraArtifactKeyword) && !form->HasKeyword(notArtifactKeyword)) { g_artifactMap[form->formID] = form; g_artifactFormTypes.insert(RE::FormType::Ingredient); } } for (const auto& form : dataHandler->GetFormArray()) { 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; if (bKID) { log::info("Keyword Item Distributor is detected."); } else { log::info("Keyword Item Distributor has NOT been detected, using the baseline configuration."); } 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()); } } if (dataHandler->LookupLoadedModByName("DBM_RelicNotifications.esp")) { RE::DebugMessageBox("Artifact Tracker is incompatible with The Curator's Companion."); } if (!bKID && settings.at("WarnMissingKID")) { RE::DebugMessageBox("Artifact Tracker requires Keyword Item Distributor. If its absence is intentional, set WarnMissingKID=false in ArtifactTracker.ini."); } 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(); } 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 stackCallback; bool bMoreHUDInstalled = false; 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("dbmNew", std::move(g_listNew)), stackCallback); vm->DispatchStaticCall("AhzMoreHud", "RegisterIconFormList", RE::MakeFunctionArguments("dbmFound", std::move(g_listFound)), stackCallback); vm->DispatchStaticCall("AhzMoreHud", "RegisterIconFormList", RE::MakeFunctionArguments("dbmDisp", std::move(g_listStored)), stackCallback); bMoreHUDInstalled = true; } else { log::error("MoreHUD has not been installed correctly."); } } else if (!g_bLoaded) { log::error("MoreHUD has not been detected."); } bool bMoreHUDInvInstalled = false; 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("dbmNew", std::move(g_listNew)), stackCallback); vm->DispatchStaticCall("AhzMoreHudIE", "RegisterIconFormList", RE::MakeFunctionArguments("dbmFound", std::move(g_listFound)), stackCallback); vm->DispatchStaticCall("AhzMoreHudIE", "RegisterIconFormList", RE::MakeFunctionArguments("dbmDisp", std::move(g_listStored)), stackCallback); bool bMoreHUDInvInstalled = true; } else { log::error("MoreHUD Inventory Edition has not been installed correctly."); } } else if (!g_bLoaded) { log::error("MoreHUD Inventory Edition has not been detected."); } if (g_bWarnMissingMoreHUD && !bMoreHUDInstalled && !bMoreHUDInvInstalled) { RE::DebugMessageBox("Artifact Tracker requires up-to-date MoreHUD and/or MoreHUD Inventory Edition. If their absence is intentional, set WarnMissingMoreHUD=false in ArtifactTracker.ini."); } // 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(std::move(g_listNew)), stackCallback); vm->DispatchStaticCall("QuickLootEE", "RegisterFoundItemsList", RE::MakeFunctionArguments(std::move(g_listFound)), stackCallback); vm->DispatchStaticCall("QuickLootEE", "RegisterDisplayedItemsList", RE::MakeFunctionArguments(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(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(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()->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(a_formID); RE::BGSLocation* location = cell ? cell->GetLocation() : nullptr; if (!cell || !location || !cell->IsInteriorCell() || !location->HasKeyword(g_homeKeyword)) { if (IsHome()) { RE::ScriptEventSourceHolder::GetSingleton()->RemoveEventSink(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(); 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(EventListener::GetSingleton()); } } void OnCellEnter(const RE::BGSLocation* location, const RE::TESObjectCELL* cell) { RE::ScriptEventSourceHolder::GetSingleton()->RemoveEventSink(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 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 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(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 stackCallback; RE::BSScript::Internal::VirtualMachine::GetSingleton()->DispatchStaticCall("ETR_NewArtifactNotification", "Show", RE::MakeFunctionArguments(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()->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(); 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)) { SKSE::GetTaskInterface()->AddTask([form]() { if (!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(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(), 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)) { // Seems like OnContainerChanged runs concurrently with updating ContainerChanges. // In small modlists ContainerChanges may not be propagated yet in this event, so we need to schedule GetItemCount. SKSE::GetTaskInterface()->AddTask([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(); 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(); 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()->ForEachForm([&](RE::TESForm& a_exform) { const auto refrItem = a_exform.As(); if (refrItem) { AddRefArtifactsToList(refrItem, a_targetList, a_excludeList); } return true; }); return; } const auto containerRef = a_refOrList->As(); 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(); } } }