diff --git a/SKSE/Plugins/ArtifactTrackerFunctions.dll b/SKSE/Plugins/ArtifactTrackerFunctions.dll index 5fe18d6..b2cdf1a 100644 Binary files a/SKSE/Plugins/ArtifactTrackerFunctions.dll and b/SKSE/Plugins/ArtifactTrackerFunctions.dll differ diff --git a/Scripts/ETR_Functions.pex b/Scripts/ETR_Functions.pex index 1355e3b..2d8a26c 100644 Binary files a/Scripts/ETR_Functions.pex and b/Scripts/ETR_Functions.pex differ diff --git a/Scripts/ETR_TrackNewItems.pex b/Scripts/ETR_TrackNewItems.pex index c935fd9..157bb16 100644 Binary files a/Scripts/ETR_TrackNewItems.pex and b/Scripts/ETR_TrackNewItems.pex differ diff --git a/Source/ArtifactTrackerDLL/CMakeLists.txt b/Source/ArtifactTrackerDLL/CMakeLists.txt index 138a6ed..7558afe 100644 --- a/Source/ArtifactTrackerDLL/CMakeLists.txt +++ b/Source/ArtifactTrackerDLL/CMakeLists.txt @@ -23,6 +23,7 @@ configure_file( set(sources src/Main.cpp src/Papyrus.cpp + src/BookCheck.cpp ${CMAKE_CURRENT_BINARY_DIR}/version.rc) diff --git a/Source/ArtifactTrackerDLL/src/BookCheck.cpp b/Source/ArtifactTrackerDLL/src/BookCheck.cpp new file mode 100644 index 0000000..b4e4def --- /dev/null +++ b/Source/ArtifactTrackerDLL/src/BookCheck.cpp @@ -0,0 +1,144 @@ +#include "BookCheck.h" + +// Mostly borrowed from Fix Note icon for SkyUI by 0xC0000005 +namespace BookCheck +{ + std::vector validBooks; + + void PreloadBookList() + { + const auto dataHandler = RE::TESDataHandler::GetSingleton(); + + if (!dataHandler) { + return; + } + + RE::BGSKeyword* recipeKeyword = dataHandler->LookupForm(0xF5CB0, "Skyrim.esm"); // VendorItemRecipe + + for (const auto& form : dataHandler->GetFormArray()) { + if (!form || form->TeachesSpell()) { + continue; + } + if (form->HasKeyword(recipeKeyword) || IsBook(form)) { + validBooks.push_back(form); + } + } + } + + std::vector GetBookList() + { + return validBooks; + } + + const char* ExtractFileName(const char* const path, size_t& fileNameLength) + { + if (!path) { + fileNameLength = 0; + return NULL; + } + + const char* ptr = path; + const char* fileNameStart = NULL; + + while (*ptr != 0) { + char value = *ptr; + + if (value == '\\' || value == '/') { + fileNameStart = ptr + 1; + } + + ptr++; + } + + const char* result = fileNameStart ? fileNameStart : path; + + fileNameLength = ptr - result; + + return result; + } + + // Performs a case-sensitive search for 'needle' in the specified string, + // excluding any matches that are part of a larger word. + bool ContainsWholeWord(const char* const haystack, const size_t haystackLength, + const char* const needle, const size_t needleLength) + { + if (haystackLength >= needleLength) { + for (size_t i = 0; i < haystackLength; i++) { + if (haystack[i] != needle[0]) { + continue; + } + + size_t j = i + 1; + size_t k = 1; + + while (j < haystackLength && k < needleLength && haystack[j] == needle[k]) { + j++; + k++; + } + + if (k == needleLength) { + // Check that the match is not part of a larger word. + + char next = haystack[j]; + + return !(next >= 'a' && next <= 'z'); + } + } + } + + return false; + } + + bool StartsWith(const char* haystack, size_t haystackLength, + const char* needle, size_t needleLength) + { + return haystackLength >= needleLength && memcmp(haystack, needle, needleLength) == 0; + } + + bool IsNote(const char* const modelFileName, size_t modelFileNameLength) + { + // Most items will use one of the note models from the base game, e.g. Note01.nif. + if (ContainsWholeWord(modelFileName, modelFileNameLength, "Note", 4)) { + return true; + } + + // The Creation Club Fishing mod does not include 'Note' the inventory art model names for the + // regional fishing maps, instead those items use the following format: FishMap. + if (StartsWith(modelFileName, modelFileNameLength, "FishMap", 7)) { + return true; + } + + return false; + } + + bool IsJournal(const char* const modelFileName, size_t modelFileNameLength) + { + return ContainsWholeWord(modelFileName, modelFileNameLength, "Journal", 7); + } + + bool IsBook(RE::TESObjectBOOK* book) + { + if (!book || !book->inventoryModel) { + return false; + } + + RE::BGSSoundDescriptorForm* pickupSound = book->As()->pickupSound; + + if (pickupSound && pickupSound->formID == 0xC7A54) { // ITMNoteUp + return false; + } + + size_t modelFileNameLength; + const char* const modelFileName = BookCheck::ExtractFileName(book->inventoryModel->GetModel(), modelFileNameLength); + + if (modelFileNameLength == 0) { + return false; + } + + if (IsNote(modelFileName, modelFileNameLength) || IsJournal(modelFileName, modelFileNameLength)) { + return false; + } + + return true; + } +} diff --git a/Source/ArtifactTrackerDLL/src/BookCheck.h b/Source/ArtifactTrackerDLL/src/BookCheck.h new file mode 100644 index 0000000..00d355d --- /dev/null +++ b/Source/ArtifactTrackerDLL/src/BookCheck.h @@ -0,0 +1,23 @@ +#pragma once + +// Mostly borrowed from Fix Note icon for SkyUI by 0xC0000005 +namespace BookCheck +{ + void PreloadBookList(); + + std::vector GetBookList(); + + const char* ExtractFileName(const char* const path, size_t& fileNameLength); + + // Performs a case-sensitive search for 'needle' in the specified string, + // excluding any matches that are part of a larger word. + bool ContainsWholeWord(const char* const haystack, const size_t haystackLength, const char* const needle, const size_t needleLength); + + bool StartsWith(const char* haystack, size_t haystackLength, const char* needle, size_t needleLength); + + bool IsNote(const char* const modelFileName, size_t modelFileNameLength); + + bool IsJournal(const char* const modelFileName, size_t modelFileNameLength); + + bool IsBook(RE::TESObjectBOOK* book); +} diff --git a/Source/ArtifactTrackerDLL/src/Functions/ArtifactTracker.h b/Source/ArtifactTrackerDLL/src/Functions/ArtifactTracker.h index bffbe45..151c396 100644 --- a/Source/ArtifactTrackerDLL/src/Functions/ArtifactTracker.h +++ b/Source/ArtifactTrackerDLL/src/Functions/ArtifactTracker.h @@ -2,54 +2,54 @@ namespace Papyrus::ArtifactTracker { - inline bool is_excluded(RE::TESForm* a_form, RE::TESForm* a_excludeForm = NULL) - { - if (!a_excludeForm) { - return false; - } + inline bool is_excluded(RE::TESForm* a_form, RE::TESForm* a_excludeForm = NULL) + { + if (!a_excludeForm) { + return false; + } - const RE::BGSListForm* list = a_excludeForm->As(); + const RE::BGSListForm* list = a_excludeForm->As(); - if (list) { - if (list->HasForm(a_form)) { - return true; - } else { - bool isExcluded = false; - - list->ForEachForm([&](RE::TESForm& a_exform) { - const auto exlist = a_exform.As(); - if (exlist) { - if (exlist->forms.size() > 0) { - if (exlist->forms[0]->Is(RE::FormType::Keyword) ? a_form->HasKeywordInList(exlist, false) : exlist->HasForm(a_form)) { - isExcluded = true; - return false; - } - } - } else { - const auto exkeyword = a_exform.As(); - if (exkeyword && a_form->As()->HasKeyword(exkeyword)) { + if (list) { + if (list->HasForm(a_form)) { + return true; + } else { + bool isExcluded = false; + + list->ForEachForm([&](RE::TESForm& a_exform) { + const auto exlist = a_exform.As(); + if (exlist) { + if (exlist->forms.size() > 0) { + if (exlist->forms[0]->Is(RE::FormType::Keyword) ? a_form->HasKeywordInList(exlist, false) : exlist->HasForm(a_form)) { isExcluded = true; return false; } } - return true; - }); + } else { + const auto exkeyword = a_exform.As(); + if (exkeyword && a_form->As()->HasKeyword(exkeyword)) { + isExcluded = true; + return false; + } + } + return true; + }); - return isExcluded; - } + return isExcluded; } + } - const RE::BGSKeyword* keyword = a_excludeForm->As(); + const RE::BGSKeyword* keyword = a_excludeForm->As(); - if (keyword) { - if (a_form->As()->HasKeyword(keyword)) { - return true; - } + if (keyword) { + if (a_form->As()->HasKeyword(keyword)) { + return true; } - - return false; } + return false; + } + inline bool is_artifact(RE::TESForm* a_form, RE::TESForm* a_excludeForm = NULL) { const auto formType = a_form->GetFormType(); @@ -123,6 +123,8 @@ namespace Papyrus::ArtifactTracker a_vm->TraceStack("containerRef in AddItemsOfTypeAndKeywordToList is not a reference", a_stackID); return 0; } + + RE::BGSKeyword* recipeKeyword = RE::TESDataHandler::GetSingleton()->LookupForm(0xF5CB0, "Skyrim.esm"); // VendorItemRecipe auto inv = containerRef->GetInventory([&](RE::TESBoundObject& a_exform) { return a_exform.GetPlayable() @@ -130,7 +132,7 @@ namespace Papyrus::ArtifactTracker a_exform.formType == RE::FormType::Armor || (a_exform.formType == RE::FormType::Weapon && a_exform.formID != 0x000001F4) || a_exform.formType == RE::FormType::Misc - || a_exform.formType == RE::FormType::Book + || (a_exform.formType == RE::FormType::Book && !a_exform.As()->TeachesSpell() && (a_exform.As()->HasKeyword(recipeKeyword) || BookCheck::IsBook(a_exform.As()))) ) && (excludeOnlyMisc ? (a_exform.formType != RE::FormType::Misc || !is_excluded(&a_exform, a_excludeForm)) : !is_excluded(&a_exform, a_excludeForm)); }); @@ -295,6 +297,30 @@ namespace Papyrus::ArtifactTracker cellItems.clear(); } + inline std::int32_t AddAllBooksToList(VM* a_vm, StackID a_stackID, RE::StaticFunctionTag*, + RE::BGSListForm* a_targetList, + RE::TESForm* a_excludeForm = NULL) + { + if (!a_targetList) { + a_vm->TraceStack("a_targetList in AddItemsOfTypeAndKeywordToList is None", a_stackID); + return 0; + } + + if (a_excludeForm) { + for (const auto& book : BookCheck::GetBookList()) { + if (!is_excluded(book, a_excludeForm)) { + a_targetList->AddForm(book); + } + } + } else { + for (const auto& book : BookCheck::GetBookList()) { + a_targetList->AddForm(book); + } + } + + return a_targetList->forms.size(); + } + inline void Bind(VM& a_vm) { BIND(AddAllFormsToList); @@ -307,5 +333,7 @@ namespace Papyrus::ArtifactTracker logger::info("Registered HasRefInCell"sv); BIND(SyncCellStorage); logger::info("Registered SyncCellStorage"sv); + BIND(AddAllBooksToList); + logger::info("Registered AddAllBooksToList"sv); } } diff --git a/Source/ArtifactTrackerDLL/src/Main.cpp b/Source/ArtifactTrackerDLL/src/Main.cpp index 2abc0f6..48ed215 100644 --- a/Source/ArtifactTrackerDLL/src/Main.cpp +++ b/Source/ArtifactTrackerDLL/src/Main.cpp @@ -1,4 +1,5 @@ #include "Papyrus.h" +#include "BookCheck.h" using namespace RE::BSScript; using namespace SKSE; @@ -6,6 +7,7 @@ using namespace SKSE::log; using namespace SKSE::stl; namespace { + void InitializeLogging() { auto path = logger::log_directory(); if (!path) { @@ -23,6 +25,18 @@ namespace { spdlog::set_default_logger(std::move(log)); spdlog::set_pattern("[%l] %v"s); } + + void InitializeMessaging() { + if (!GetMessagingInterface()->RegisterListener([](MessagingInterface::Message* message) { + + if (message->type == MessagingInterface::kDataLoaded) { + BookCheck::PreloadBookList(); + } + + })) { + stl::report_and_fail("Unable to register message listener."); + } + } } SKSEPluginLoad(const LoadInterface* skse) { @@ -33,6 +47,7 @@ SKSEPluginLoad(const LoadInterface* skse) { log::info("{} {} is loading...", plugin->GetName(), version); Init(skse); + InitializeMessaging(); SKSE::GetPapyrusInterface()->Register(Papyrus::Bind); log::info("{} has finished loading.", plugin->GetName()); diff --git a/Source/ArtifactTrackerDLL/src/Papyrus.cpp b/Source/ArtifactTrackerDLL/src/Papyrus.cpp index b43d919..d069be7 100644 --- a/Source/ArtifactTrackerDLL/src/Papyrus.cpp +++ b/Source/ArtifactTrackerDLL/src/Papyrus.cpp @@ -1,4 +1,5 @@ #include "Papyrus.h" +#include "BookCheck.h" #include "Functions/ObjectReference.h" #include "Functions/ArtifactTracker.h" diff --git a/Source/Scripts/ETR_Functions.psc b/Source/Scripts/ETR_Functions.psc index 729b195..84b92f4 100644 --- a/Source/Scripts/ETR_Functions.psc +++ b/Source/Scripts/ETR_Functions.psc @@ -2,6 +2,8 @@ Scriptname ETR_Functions Hidden int function AddAllFormsToList(FormList targetList, int formType, Form excludeForm = None) native global +int function AddAllBooksToList(FormList targetList, Form excludeForm = None) native global + int function AddArtifactsToList(Form refOrList, FormList targetList, Form excludeForm = None, bool excludeOnlyMisc = false) native global int function GetItemCountInList(FormList refList, Form baseForm) native global diff --git a/Source/Scripts/ETR_TrackNewItems.psc b/Source/Scripts/ETR_TrackNewItems.psc index a9f7ae5..a0ed67a 100644 --- a/Source/Scripts/ETR_TrackNewItems.psc +++ b/Source/Scripts/ETR_TrackNewItems.psc @@ -38,11 +38,11 @@ event OnPlayerLoadGame() ahzmorehudie.RegisterIconFormList("dbmDisp", ETR_ItemsStored) endif - If SKSE.GetPluginVersion("QuickLootRE") >= 292 - QuickLootRE.RegisterNewItemsList(ETR_ItemsNew) - QuickLootRE.RegisterDisplayedItemsList(ETR_ItemsStored) - QuickLootRE.RegisterFoundItemsList(ETR_ItemsFound) - endif + ;If SKSE.GetPluginVersion("QuickLootRE") >= 292 + ; QuickLootRE.RegisterNewItemsList(ETR_ItemsNew) + ; QuickLootRE.RegisterDisplayedItemsList(ETR_ItemsStored) + ; QuickLootRE.RegisterFoundItemsList(ETR_ItemsFound) + ;endif ; Rebuild all lists to avoid discrepancies, stale data, and broken records @@ -61,9 +61,9 @@ event OnPlayerLoadGame() ETR_ItemsNew.Revert() ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 26, ETR_FoundAndStored) - ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 27, ETR_FoundAndStored) - ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 32, ETR_ExcludeFromNew) ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 41, ETR_FoundAndStored) + ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 32, ETR_ExcludeFromNew) + ETR_Functions.AddAllBooksToList(ETR_ItemsNew, ETR_FoundAndStored) endevent