2022-06-21 21:51:57 +00:00
|
|
|
#include "BookCheck.h"
|
|
|
|
|
|
|
|
// Mostly borrowed from Fix Note icon for SkyUI by 0xC0000005
|
|
|
|
namespace BookCheck
|
|
|
|
{
|
2022-06-21 23:30:45 +00:00
|
|
|
std::unordered_map<RE::FormID, RE::TESObjectBOOK*> validBooks;
|
2022-06-21 21:51:57 +00:00
|
|
|
|
|
|
|
void PreloadBookList()
|
|
|
|
{
|
|
|
|
const auto dataHandler = RE::TESDataHandler::GetSingleton();
|
|
|
|
|
|
|
|
if (!dataHandler) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-06-22 00:52:12 +00:00
|
|
|
const RE::BGSKeyword* recipeKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0xF5CB0, "Skyrim.esm"); // VendorItemRecipe
|
2022-06-21 21:51:57 +00:00
|
|
|
|
|
|
|
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectBOOK>()) {
|
2022-06-22 00:52:12 +00:00
|
|
|
if (!form || !form->GetPlayable() || form->TeachesSpell()) {
|
2022-06-21 21:51:57 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (form->HasKeyword(recipeKeyword) || IsBook(form)) {
|
2022-06-21 23:30:45 +00:00
|
|
|
validBooks[form->formID] = form;
|
2022-06-21 21:51:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-21 23:30:45 +00:00
|
|
|
std::unordered_map<RE::FormID, RE::TESObjectBOOK*> GetBookList()
|
2022-06-21 21:51:57 +00:00
|
|
|
{
|
|
|
|
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<region>.
|
|
|
|
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<RE::BGSPickupPutdownSounds>()->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;
|
|
|
|
}
|
|
|
|
}
|