Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions include/RE/B/BGSConstructibleObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -33,19 +34,19 @@ namespace RE
~BGSConstructibleObject() override; // 00

// members
std::byte category[0x18]; // 150 - std::vector<BGSKeyword*, BSTHeapSTLAllocator<BGSKeyword, 2>
TESBoundObject* unk168; // 168
BGSCurveForm* unk170; // 170
TESGlobal* buildLimit; // 178
BSTArray<BSTTuple3<TESForm*, BGSCurveForm*, BGSTypedFormValuePair::SharedVal>>* unk180; // 180
REX::TEnum<LEARN_METHOD, std::uint8_t> learnMethod; // 188
TESGlobal* unk190; // 190
BGSKeyword* unk198; // 198
std::uint32_t unk1A0; // 1A0
BGSTypedKeywordValueArray<KeywordType::kRecipeFilter> recipeFilters; // 150 - CK RecipeFilters (prev. "category")
TESBoundObject* learnedFrom; // 168 - CK LearnedFrom (any TESBoundObject)
BGSCurveForm* baseReturnScaleTable; // 170 - CK BaseReturnScaleTable
TESGlobal* maxBuildCount; // 178 - CK MaxBuildCount
BSTArray<BSTTuple3<TESForm*, BGSCurveForm*, BGSTypedFormValuePair::SharedVal>>* workbenchRepairRecipe; // 180 - CK WorkbenchRepairRecipe
REX::TEnum<LEARN_METHOD, std::uint8_t> 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
}
12 changes: 6 additions & 6 deletions include/RE/B/BGSCraftableForm.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ namespace RE
virtual void Unk_62(); // 62

// members
BGSKeyword* benchKeyword; // 60
BGSKeyword* benchKeyword; // 60 - CK WorkbenchKeyword
TESCondition conditions; // 68
BSTArray<BSTTuple3<TESForm*, BGSCurveForm*, BGSTypedFormValuePair::SharedVal>>* components; // 78
BSTArray<BSTTuple3<TESForm*, BGSCurveForm*, BGSTypedFormValuePair::SharedVal>>* components; // 78 - CK RequiredItemList
BSTArray<BSTTuple3<TESForm*, BGSCurveForm*, BGSTypedFormValuePair::SharedVal>>* 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
Expand Down
68 changes: 27 additions & 41 deletions include/RE/B/BGSTypedKeywordValueArray.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once

#include "RE/B/BGSKeyword.h"
#include "RE/M/MemoryManager.h"
#include "RE/B/BSTHeapSTLVector.h"

namespace RE
{
Expand Down Expand Up @@ -40,57 +40,50 @@ namespace RE
};
static_assert(sizeof(BGSTypedKeywordValue<KeywordType::kNone>) == 0x8);

// Best-fit model: BGSTypedKeywordValueArray<KeywordType::kAttachPoint> 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 <KeywordType TYPE>
class BGSTypedKeywordValueArray
class BGSTypedKeywordValueArray : public BSTHeapSTLVector<BGSKeyword*>
{
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<std::size_t>(end - begin);
const auto newSize = oldSize + 1;

auto* newData = static_cast<BGSKeyword**>(
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;
}

Expand All @@ -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<KeywordType::kNone>) == 0x18);
}
193 changes: 193 additions & 0 deletions include/RE/B/BSTHeapSTLVector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#pragma once

#include "RE/M/MemoryManager.h"

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstring>

namespace RE
{
// View + owner of Starfield engine members commented as
// `std::vector<T, BSTHeapSTLAllocator<T, N>>`. Layout is three pointers
// (begin / end / capacity-end), so sizeof == 0x18 on x64.
//
// Why not use `std::vector<T>` directly? MSVC's debug build
// (`_ITERATOR_DEBUG_LEVEL=2`) inflates `sizeof(std::vector<void*>)` 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 T>
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<size_type>(_end - _begin);
}

[[nodiscard]] constexpr size_type capacity() const noexcept
{
return static_cast<size_type>(_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<T*>(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<void*>) == 0x18);
}
Loading