From fe8f9ea9640d329a807fe194f1451c7c0e04e734 Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:31:38 +0300 Subject: [PATCH 1/4] Take ObjectRefHandle by reference where the engine expects a pointer FindAndWriteStackData and the InventoryItemDisplayData constructor expect a pointer to a handle, but they were passed the handle itself. The engine then used the handle's number as a memory address and read from the wrong place. Taking the handle as a const reference passes a pointer to it, as the engine expects. --- include/RE/B/BGSInventoryItem.h | 2 +- include/RE/I/InventoryItemDisplayData.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/RE/B/BGSInventoryItem.h b/include/RE/B/BGSInventoryItem.h index cfff4943e..5bf315adc 100644 --- a/include/RE/B/BGSInventoryItem.h +++ b/include/RE/B/BGSInventoryItem.h @@ -159,7 +159,7 @@ namespace RE }; static_assert(sizeof(SetHealthFunctor) == 0x18); - bool FindAndWriteStackData(StackDataCompareFunctor& a_compareFunc, StackDataWriteFunctor& a_writeFunc, bool a_manualMerge, ObjectRefHandle a_owner) + bool FindAndWriteStackData(StackDataCompareFunctor& a_compareFunc, StackDataWriteFunctor& a_writeFunc, bool a_manualMerge, const ObjectRefHandle& a_owner) { using func_t = decltype(&BGSInventoryItem::FindAndWriteStackData); static REL::Relocation func{ ID::BGSInventoryItem::FindAndWriteStackData }; diff --git a/include/RE/I/InventoryItemDisplayData.h b/include/RE/I/InventoryItemDisplayData.h index 93d6c366f..a0976acb7 100644 --- a/include/RE/I/InventoryItemDisplayData.h +++ b/include/RE/I/InventoryItemDisplayData.h @@ -11,7 +11,7 @@ namespace RE { public: InventoryItemDisplayData( - const ObjectRefHandle a_inventoryRef, + const ObjectRefHandle& a_inventoryRef, const InventoryUserUIInterfaceEntry& a_entry) { ctor(a_inventoryRef, a_entry); @@ -41,7 +41,7 @@ namespace RE private: InventoryItemDisplayData* ctor( - const ObjectRefHandle a_inventoryRef, + const ObjectRefHandle& a_inventoryRef, const InventoryUserUIInterfaceEntry& a_entry) { using func_t = decltype(&InventoryItemDisplayData::ctor); From 626ded2f4e04416507ebe8ebeab7769fe5036d98 Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:21:05 +0300 Subject: [PATCH 2/4] Fix and complete the BGSInventoryItem stack functors An item's copies are grouped into stacks, and copies that differ, such as in mods or a custom name, go in separate stacks. To change a stack, the engine uses two small helper objects called functors. One finds the stack and the other changes it. FindAndWriteStackData runs both. CheckExtraDataFunctor matched every stack, so it always found the first one. It now matches only the stack that has the extra data list it was given. The WriteDataImpl comments said vtable slot 01. It is the only virtual function, so it is slot 00. Adds the six missing functors: FindEquippedStackFunctor, HasExtraDataFunctor, IsUIEquivalentStackFunctor, ClearEquipFlagsFunctor, ModCountFunctor and SetFlagFunctor. Their members and sizes match the places the 1.11.240 engine creates them. IsUIEquivalentStackFunctor and the three write functors run the engine's own code through its vtable. --- include/RE/A/ApplyChangesFunctor.h | 2 +- include/RE/B/BGSInventoryItem.h | 121 ++++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/include/RE/A/ApplyChangesFunctor.h b/include/RE/A/ApplyChangesFunctor.h index d60da7484..d57bfa45d 100644 --- a/include/RE/A/ApplyChangesFunctor.h +++ b/include/RE/A/ApplyChangesFunctor.h @@ -12,7 +12,7 @@ namespace RE static constexpr auto VTABLE{ VTABLE::__ApplyChangesFunctor }; // override - virtual void WriteDataImpl(TESBoundObject& a_baseObj, BGSInventoryItem::Stack& a_stack) override // 01 + virtual void WriteDataImpl(TESBoundObject& a_baseObj, BGSInventoryItem::Stack& a_stack) override // 00 { using func_t = decltype(&ApplyChangesFunctor::WriteDataImpl); static REL::Relocation func{ ID::ApplyChangesFunctor::WriteDataImpl }; diff --git a/include/RE/B/BGSInventoryItem.h b/include/RE/B/BGSInventoryItem.h index 5bf315adc..2ab1f4a75 100644 --- a/include/RE/B/BGSInventoryItem.h +++ b/include/RE/B/BGSInventoryItem.h @@ -84,13 +84,65 @@ namespace RE {} // override (StackDataCompareFunctor) - bool CompareData(const BGSInventoryItem::Stack&) override { return true; } // this->extra == extra; ?? + bool CompareData(const BGSInventoryItem::Stack& a_stack) override { return a_stack.extra.get() == extra; } // 00 // members const ExtraDataList* extra; // 08 }; static_assert(sizeof(CheckExtraDataFunctor) == 0x10); + class alignas(0x08) FindEquippedStackFunctor : + public StackDataCompareFunctor // 00 + { + public: + static constexpr auto RTTI{ RTTI::BGSInventoryItem__FindEquippedStackFunctor }; + static constexpr auto VTABLE{ VTABLE::BGSInventoryItem__FindEquippedStackFunctor }; + + // override (StackDataCompareFunctor) + bool CompareData(const BGSInventoryItem::Stack& a_stack) override { return a_stack.IsEquipped(); } // 00 + }; + static_assert(sizeof(FindEquippedStackFunctor) == 0x8); + + class alignas(0x08) HasExtraDataFunctor : + public StackDataCompareFunctor // 00 + { + public: + static constexpr auto RTTI{ RTTI::BGSInventoryItem__HasExtraDataFunctor }; + static constexpr auto VTABLE{ VTABLE::BGSInventoryItem__HasExtraDataFunctor }; + + HasExtraDataFunctor(EXTRA_DATA_TYPE a_type) noexcept : + type(a_type) + {} + + // override (StackDataCompareFunctor) + bool CompareData(const BGSInventoryItem::Stack& a_stack) override { return a_stack.extra->HasType(type.get()); } // 00 + + // members + REX::TEnum type; // 08 + }; + static_assert(sizeof(HasExtraDataFunctor) == 0x10); + + class __declspec(novtable) alignas(0x08) IsUIEquivalentStackFunctor : + public StackDataCompareFunctor // 00 + { + public: + static constexpr auto RTTI{ RTTI::BGSInventoryItem__IsUIEquivalentStackFunctor }; + static constexpr auto VTABLE{ VTABLE::BGSInventoryItem__IsUIEquivalentStackFunctor }; + + IsUIEquivalentStackFunctor(BGSInventoryItem::Stack* a_stack) : + stack(a_stack) + { + REX::EMPLACE_VTABLE(this); + } + + // override (StackDataCompareFunctor) + bool CompareData(const BGSInventoryItem::Stack& a_stack) override; // 00 + + // members + BSTSmartPointer stack; // 08 + }; + static_assert(sizeof(IsUIEquivalentStackFunctor) == 0x10); + class __declspec(novtable) alignas(0x08) StackDataWriteFunctor { public: @@ -98,7 +150,7 @@ namespace RE static constexpr auto VTABLE{ VTABLE::BGSInventoryItem__StackDataWriteFunctor }; // add - virtual void WriteDataImpl(TESBoundObject& a_baseObj, BGSInventoryItem::Stack& a_stack) = 0; // 01 + virtual void WriteDataImpl(TESBoundObject& a_baseObj, BGSInventoryItem::Stack& a_stack) = 0; // 00 // members bool shouldSplitStacks{ true }; // 08 @@ -126,7 +178,7 @@ namespace RE } // override (StackDataWriteFunctor) - void WriteDataImpl(TESBoundObject&, BGSInventoryItem::Stack&) override; // 01 + void WriteDataImpl(TESBoundObject&, BGSInventoryItem::Stack&) override; // 00 // members BGSMod::Attachment::Mod* mod; // 10 @@ -152,13 +204,74 @@ namespace RE } // override (StackDataWriteFunctor) - void WriteDataImpl(TESBoundObject&, BGSInventoryItem::Stack&) override; // 01 + void WriteDataImpl(TESBoundObject&, BGSInventoryItem::Stack&) override; // 00 // members float health; // 10 }; static_assert(sizeof(SetHealthFunctor) == 0x18); + class __declspec(novtable) ClearEquipFlagsFunctor : + public StackDataWriteFunctor // 00 + { + public: + static constexpr auto RTTI{ RTTI::BGSInventoryItem__ClearEquipFlagsFunctor }; + static constexpr auto VTABLE{ VTABLE::BGSInventoryItem__ClearEquipFlagsFunctor }; + + ClearEquipFlagsFunctor() + { + REX::EMPLACE_VTABLE(this); + } + + // override (StackDataWriteFunctor) + void WriteDataImpl(TESBoundObject&, BGSInventoryItem::Stack&) override; // 00 + }; + static_assert(sizeof(ClearEquipFlagsFunctor) == 0x10); + + class __declspec(novtable) ModCountFunctor : + public StackDataWriteFunctor // 00 + { + public: + static constexpr auto RTTI{ RTTI::BGSInventoryItem__ModCountFunctor }; + static constexpr auto VTABLE{ VTABLE::BGSInventoryItem__ModCountFunctor }; + + ModCountFunctor(std::int32_t a_count) : + count(a_count) + { + REX::EMPLACE_VTABLE(this); + } + + // override (StackDataWriteFunctor) + void WriteDataImpl(TESBoundObject&, BGSInventoryItem::Stack&) override; // 00 + + // members + std::int32_t count; // 10 + }; + static_assert(sizeof(ModCountFunctor) == 0x18); + + class __declspec(novtable) SetFlagFunctor : + public StackDataWriteFunctor // 00 + { + public: + static constexpr auto RTTI{ RTTI::BGSInventoryItem__SetFlagFunctor }; + static constexpr auto VTABLE{ VTABLE::BGSInventoryItem__SetFlagFunctor }; + + SetFlagFunctor(std::uint32_t a_flagIndex, bool a_setFlag) : + flagIndex(a_flagIndex), + setFlag(a_setFlag) + { + REX::EMPLACE_VTABLE(this); + } + + // override (StackDataWriteFunctor) + void WriteDataImpl(TESBoundObject&, BGSInventoryItem::Stack&) override; // 00 + + // members + std::uint32_t flagIndex; // 10 + bool setFlag; // 14 + }; + static_assert(sizeof(SetFlagFunctor) == 0x18); + bool FindAndWriteStackData(StackDataCompareFunctor& a_compareFunc, StackDataWriteFunctor& a_writeFunc, bool a_manualMerge, const ObjectRefHandle& a_owner) { using func_t = decltype(&BGSInventoryItem::FindAndWriteStackData); From db5f09f3e73657e8ca6128bb81139a45daabc2c1 Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:07:08 +0300 Subject: [PATCH 3/4] Widen InventoryUserUIInterfaceEntry stack indices to 16 bits Each entry in an inventory menu lists the stacks it shows, by each stack's position in the item's stack list. One entry can cover several stacks. The engine stores each position in 2 bytes, but they were declared as 1 byte. So stackIndex[1] was read just 1 byte into the list, which is still inside the first position, and usually came out as 0. A position above 255 needs both of its bytes, so it came out wrong too. The game's own code reads these numbers 2 bytes at a time, both in InventoryItemDisplayData's constructor and in the function that adds up an entry's item count. In the disassembly this shows up as word ptr reads, and a word is 2 bytes. --- include/RE/I/InventoryUserUIInterfaceEntry.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/RE/I/InventoryUserUIInterfaceEntry.h b/include/RE/I/InventoryUserUIInterfaceEntry.h index 6efae0fc3..2ff23810b 100644 --- a/include/RE/I/InventoryUserUIInterfaceEntry.h +++ b/include/RE/I/InventoryUserUIInterfaceEntry.h @@ -9,8 +9,8 @@ namespace RE { public: // members - InventoryInterface::Handle invHandle; // 00 - BSTSmallArray stackIndex; // 08 + InventoryInterface::Handle invHandle; // 00 + BSTSmallArray stackIndex; // 08 }; static_assert(sizeof(InventoryUserUIInterfaceEntry) == 0x20); } From 55257e4717a0c593394de140fe7b2ac8a83b1d86 Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:59:20 +0300 Subject: [PATCH 4/4] Fix the kLocationSpecRefs flag value It was 1u < 28, which is 1, instead of 1u << 28. --- include/RE/C/CHANGE_TYPES.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/RE/C/CHANGE_TYPES.h b/include/RE/C/CHANGE_TYPES.h index 6c38f5404..1b9ec9210 100644 --- a/include/RE/C/CHANGE_TYPES.h +++ b/include/RE/C/CHANGE_TYPES.h @@ -54,7 +54,7 @@ namespace RE kTopicSaidPlayer = 1u << 30, kTopicSaidOnce = 1u << 31, kRelationshipData = 1u << 1, - kLocationSpecRefs = 1u < 28, + kLocationSpecRefs = 1u << 28, kLocationNewRefs = 1u << 29, kLocationKeywordData = 1u << 30, kLocationCleared = 1u << 31,