diff --git a/include/RE/B/BGSConstructibleObject.h b/include/RE/B/BGSConstructibleObject.h index 5ae31b6..edc9032 100644 --- a/include/RE/B/BGSConstructibleObject.h +++ b/include/RE/B/BGSConstructibleObject.h @@ -3,6 +3,7 @@ #include "RE/B/BGSCraftableForm.h" #include "RE/B/BGSCraftingUseSound.h" #include "RE/B/BGSPickupPutdownSounds.h" +#include "RE/B/BGSTypedKeywordValueArray.h" #include "RE/B/BSTList.h" #include "RE/T/TESValueForm.h" @@ -33,19 +34,19 @@ namespace RE ~BGSConstructibleObject() override; // 00 // members - std::byte category[0x18]; // 150 - std::vector - TESBoundObject* unk168; // 168 - BGSCurveForm* unk170; // 170 - TESGlobal* buildLimit; // 178 - BSTArray>* unk180; // 180 - REX::TEnum learnMethod; // 188 - TESGlobal* unk190; // 190 - BGSKeyword* unk198; // 198 - std::uint32_t unk1A0; // 1A0 + BGSTypedKeywordValueArray recipeFilters; // 150 - CK RecipeFilters (prev. "category") + TESBoundObject* learnedFrom; // 168 - CK LearnedFrom (any TESBoundObject) + BGSCurveForm* baseReturnScaleTable; // 170 - CK BaseReturnScaleTable + TESGlobal* maxBuildCount; // 178 - CK MaxBuildCount + BSTArray>* workbenchRepairRecipe; // 180 - CK WorkbenchRepairRecipe + REX::TEnum learnMethod; // 188 - CK LearnMethod enum + TESGlobal* unk190; // 190 - TESGlobal??? + BGSKeyword* unk198; // 198 - BGSKeyword??? + std::uint32_t flags; // 1A0 - CK flag bitfield incl. ExcludeFromWorkshopLOD (bit layout TBD) (???) }; static_assert(sizeof(BGSConstructibleObject) == 0x1A8); static_assert(offsetof(BGSConstructibleObject, pickupSound) == 0xA8); // BGSPickupPutdownSounds base @ +0x0A0 static_assert(offsetof(BGSConstructibleObject, value) == 0x110); // TESValueForm base @ +0x108 static_assert(offsetof(BGSConstructibleObject, craftingUseSound) == 0x120); // BGSCraftingUseSound base @ +0x118 - static_assert(offsetof(BGSConstructibleObject, category) == 0x150); // first derived member + static_assert(offsetof(BGSConstructibleObject, recipeFilters) == 0x150); // first derived member } diff --git a/include/RE/B/BGSCraftableForm.h b/include/RE/B/BGSCraftableForm.h index 1e7fa1d..cf14530 100644 --- a/include/RE/B/BGSCraftableForm.h +++ b/include/RE/B/BGSCraftableForm.h @@ -27,14 +27,14 @@ namespace RE virtual void Unk_62(); // 62 // members - BGSKeyword* benchKeyword; // 60 + BGSKeyword* benchKeyword; // 60 - CK WorkbenchKeyword TESCondition conditions; // 68 - BSTArray>* components; // 78 + BSTArray>* components; // 78 - CK RequiredItemList BSTArray>* requiredPerks; // 80 - TESForm* createdObject; // 88 - std::uint8_t unk90; // 90 - float menuSortOrder; // 94 - std::uint8_t unk98; // 98 + TESForm* createdObject; // 88 - CK CreatedObject (arbitrary TESForm, not just TESBoundObject) + std::uint8_t unk90; // 90 - heatmap: 2911/3091 populated, values ~1 (CreatedObjectCount candidate) + float menuSortOrder; // 94 - CK MenuPriorityOrder + std::uint8_t unk98; // 98 - heatmap: 12/3091 populated (rare flag byte) }; static_assert(sizeof(BGSCraftableForm) == 0xA0); static_assert(offsetof(BGSCraftableForm, unk18) == 0x18); // TESForm base @ +0x00 diff --git a/include/RE/B/BGSTypedKeywordValueArray.h b/include/RE/B/BGSTypedKeywordValueArray.h index 7af8234..f5dc05f 100644 --- a/include/RE/B/BGSTypedKeywordValueArray.h +++ b/include/RE/B/BGSTypedKeywordValueArray.h @@ -1,7 +1,7 @@ #pragma once #include "RE/B/BGSKeyword.h" -#include "RE/M/MemoryManager.h" +#include "RE/B/BSTHeapSTLVector.h" namespace RE { @@ -40,57 +40,50 @@ namespace RE }; static_assert(sizeof(BGSTypedKeywordValue) == 0x8); - // Best-fit model: BGSTypedKeywordValueArray may be - // BSComponentDB2-backed index records during preload/finalize, while loaded - // ARMO/WEAP attachParents runtime data is a contiguous BGSKeyword* buffer. + // By the time plugins touch these at runtime (post-data-loaded ARMO and WEAP + // attachParents, form keyword lists, etc.) the backing storage is a contiguous + // BGSKeyword* buffer managed by the game's BSTHeapSTLAllocator — the same + // begin / end / capacityEnd triplet modeled by BSTHeapSTLVector. During + // preload/finalize of some keyword types (notably kAttachPoint) the field may + // briefly hold BSComponentDB2 index records instead of BGSKeyword pointers; + // do not mutate through this view during that window. template - class BGSTypedKeywordValueArray + class BGSTypedKeywordValueArray : public BSTHeapSTLVector { public: + // Membership test uses formID equality (not pointer equality) so that + // callers holding a BGSKeyword* from any lookup path — including forms + // rebuilt across load cycles — see the same answer as AddKeyword and + // RemoveKeyword, which also compare by formID. [[nodiscard]] bool HasKeyword(BGSKeyword* a_keyword) { if (!a_keyword) { return false; } - - for (auto it = begin; it != end; ++it) { - if (*it == a_keyword) { + for (auto* kw : *this) { + if (kw && kw->formID == a_keyword->formID) { return true; } } return false; } + // Typed-keyword arrays appear to keep capacity tightly matched to live + // size after engine-driven adds. Reserve exactly size() + 1 here before + // appending so the generic vector stays vector-like, while this wrapper preserves the + // tighter capacityEnd post-condition that callers may observe on these fields. [[nodiscard]] bool AddKeyword(BGSKeyword* a_keyword) { if (!a_keyword) { return false; } - - for (auto it = begin; it != end; ++it) { - if (*it && (*it)->formID == a_keyword->formID) { + for (auto* kw : *this) { + if (kw && kw->formID == a_keyword->formID) { return false; } } - - const auto oldSize = static_cast(end - begin); - const auto newSize = oldSize + 1; - - auto* newData = static_cast( - RE::MemoryManager::GetSingleton()->Allocate(sizeof(BGSKeyword*) * newSize, 0, false)); - - if (!newData) { - return false; - } - - for (std::size_t i = 0; i < oldSize; ++i) { - newData[i] = begin[i]; - } - - newData[oldSize] = a_keyword; - begin = newData; - end = newData + newSize; - capacityEnd = newData + newSize; + this->reserve_exact(this->size() + 1); + this->push_back(a_keyword); return true; } @@ -99,22 +92,15 @@ namespace RE if (!a_keyword) { return false; } - for (auto it = begin; it != end; ++it) { - if (*it && (*it)->formID == a_keyword->formID) { - for (auto jt = it; jt + 1 != end; ++jt) { - *jt = *(jt + 1); - } - --end; - *end = nullptr; + for (size_type i = 0; i < size(); ++i) { + auto* kw = (*this)[i]; + if (kw && kw->formID == a_keyword->formID) { + erase(i); return true; } } return false; } - - BGSKeyword** begin; // 00 - BGSKeyword** end; // 08 - BGSKeyword** capacityEnd; // 10 }; static_assert(sizeof(BGSTypedKeywordValueArray) == 0x18); } diff --git a/include/RE/B/BSTHeapSTLVector.h b/include/RE/B/BSTHeapSTLVector.h new file mode 100644 index 0000000..acd6815 --- /dev/null +++ b/include/RE/B/BSTHeapSTLVector.h @@ -0,0 +1,193 @@ +#pragma once + +#include "RE/M/MemoryManager.h" + +#include +#include +#include +#include + +namespace RE +{ + // View + owner of Starfield engine members commented as + // `std::vector>`. Layout is three pointers + // (begin / end / capacity-end), so sizeof == 0x18 on x64. + // + // Why not use `std::vector` directly? MSVC's debug build + // (`_ITERATOR_DEBUG_LEVEL=2`) inflates `sizeof(std::vector)` beyond 0x18 + // because of checked-iterator bookkeeping. This wrapper is build-mode-stable. + // + // Mutation uses the engine's MemoryManager (RE::malloc / RE::free), which is the + // same global heap Bethesda's BSTHeapSTLAllocator ultimately delegates to. Buffers + // allocated by the game can be freed by us and vice-versa without cross-heap + // corruption. The raw BSTHeapSTLAllocatorBase REL::IDs (34039/34440) are NOT + // plain allocators - calling 34039 with (bytes, align) returns an image-space + // pointer, not a heap buffer. Probably just wrong offset ids and needs to be further RE'd + // + // Threading: no internal locking. Callers must ensure no other thread is + // iterating or mutating the same vector. Safe during OnDataLoaded / main-thread + // script ticks; not safe from background threads. + template + class BSTHeapSTLVector + { + public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using iterator = T*; + using const_iterator = const T*; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + [[nodiscard]] constexpr iterator begin() noexcept { return _begin; } + [[nodiscard]] constexpr const_iterator begin() const noexcept { return _begin; } + [[nodiscard]] constexpr const_iterator cbegin() const noexcept { return _begin; } + + [[nodiscard]] constexpr iterator end() noexcept { return _end; } + [[nodiscard]] constexpr const_iterator end() const noexcept { return _end; } + [[nodiscard]] constexpr const_iterator cend() const noexcept { return _end; } + + [[nodiscard]] constexpr pointer data() noexcept { return _begin; } + [[nodiscard]] constexpr const_pointer data() const noexcept { return _begin; } + + [[nodiscard]] constexpr size_type size() const noexcept + { + return static_cast(_end - _begin); + } + + [[nodiscard]] constexpr size_type capacity() const noexcept + { + return static_cast(_capacityEnd - _begin); + } + + [[nodiscard]] constexpr bool empty() const noexcept { return _begin == _end; } + + [[nodiscard]] constexpr reference operator[](size_type a_index) noexcept { return _begin[a_index]; } + [[nodiscard]] constexpr const_reference operator[](size_type a_index) const noexcept { return _begin[a_index]; } + + [[nodiscard]] constexpr reference front() noexcept { return *_begin; } + [[nodiscard]] constexpr const_reference front() const noexcept { return *_begin; } + [[nodiscard]] constexpr reference back() noexcept { return _end[-1]; } + [[nodiscard]] constexpr const_reference back() const noexcept { return _end[-1]; } + + // Mutation ----------------------------------------------------------------- + + // Appends a copy of a_value. Grows the buffer via the same automatic + // reserve policy used by BSTArray when capacity is exhausted. + void push_back(T a_value) + { + if (_end == _capacityEnd) { + reserve_auto(size() + 1); + } + *_end = a_value; + ++_end; + } + + // Removes the element at index a_index (0-based). Shifts subsequent + // elements down and decrements end. Does nothing if a_index >= size(). + void erase(size_type a_index) + { + if (a_index >= size()) { + return; + } + for (auto p = _begin + a_index; p + 1 != _end; ++p) { + p[0] = p[1]; + } + --_end; + *_end = value_type{}; + } + + // Removes the first element equal to a_value and returns whether any slot + // was removed. Does not touch the backing buffer's capacity. + [[nodiscard]] bool erase_value(const T& a_value) + { + for (size_type i = 0; i < size(); ++i) { + if (_begin[i] == a_value) { + erase(i); + return true; + } + } + return false; + } + + // Empties the vector without freeing the buffer (capacity unchanged). + void clear() noexcept { _end = _begin; } + + // Pre-grows capacity to at least a_min. No-op if capacity already suffices. + void reserve(size_type a_min) + { + if (a_min > capacity()) { + reserve_exact(a_min); + } + } + + void shrink_to_fit() { reserve_exact(size()); } + + void pop_back() + { + if (!empty()) { + --_end; + } + } + + // Frees the backing buffer via the game's allocator and zeroes all three + // pointers. Use this (not `std::free`) when discarding a vector we built. + void release_buffer() + { + if (_begin) { + RE::free(_begin, alignof(T) > alignof(std::max_align_t)); + } + _begin = nullptr; + _end = nullptr; + _capacityEnd = nullptr; + } + + protected: + void reserve_exact(size_type a_capacity) + { + assert(a_capacity >= size()); + if (a_capacity == capacity()) { + return; + } + if (a_capacity == 0) { + release_buffer(); + return; + } + + const auto oldSize = size(); + const auto bytes = a_capacity * sizeof(T); + const auto alignRequired = alignof(T) > alignof(std::max_align_t); + auto* newBuffer = static_cast(RE::malloc(bytes, alignRequired ? alignof(T) : 0)); + if (!newBuffer) { + REX::FAIL("out of memory"); + } + std::memset(newBuffer, 0, bytes); + if (oldSize > 0) { + std::memcpy(newBuffer, _begin, oldSize * sizeof(T)); + } + if (_begin) { + RE::free(_begin, alignRequired); + } + _begin = newBuffer; + _end = newBuffer + oldSize; + _capacityEnd = newBuffer + a_capacity; + } + + private: + void reserve_auto(size_type a_capacity) + { + if (a_capacity > capacity()) { + const auto grow = std::max(a_capacity, capacity() * 2); + reserve_exact(grow); + } + } + + // members + T* _begin{ nullptr }; // 00 + T* _end{ nullptr }; // 08 + T* _capacityEnd{ nullptr }; // 10 + }; + static_assert(sizeof(BSTHeapSTLVector) == 0x18); +}