#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; } }