From 8f3aec429d7a1571e5fbe1c364722150bc08d42f Mon Sep 17 00:00:00 2001 From: Quantumyilmaz <47591838+Quantumyilmaz@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:44:39 +0200 Subject: [PATCH 1/5] fix(input): correct QUserEvent reference ABI --- include/RE/B/BSInputEventUser.h | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/include/RE/B/BSInputEventUser.h b/include/RE/B/BSInputEventUser.h index a5a0df83..8a682490 100644 --- a/include/RE/B/BSInputEventUser.h +++ b/include/RE/B/BSInputEventUser.h @@ -3,6 +3,9 @@ #include "RE/B/BSFixedString.h" #include "RE/M/MemoryManager.h" +#include +#include + namespace RE { class ButtonEvent; @@ -51,8 +54,13 @@ namespace RE virtual ~InputEvent() = default; // 00 // add - virtual bool HasIDCode() const { return false; } - virtual const BSFixedString QUserEvent() const { return ""; } + virtual bool HasIDCode() const { return false; } + + virtual const BSFixedString& QUserEvent() const + { + static const BSFixedString empty; + return empty; + } // members DeviceType deviceType{ DeviceType::kNone }; // 08 @@ -64,6 +72,7 @@ namespace RE Status status{ Status::kUnhandled }; // 24 }; static_assert(sizeof(InputEvent) == 0x28); + static_assert(std::is_same_v().QUserEvent()), const BSFixedString&>); class IDEvent : public InputEvent @@ -76,9 +85,10 @@ namespace RE // override (InputEvent) virtual bool HasIDCode() const override { return true; } - virtual const BSFixedString QUserEvent() const override + virtual const BSFixedString& QUserEvent() const override { - return disabled ? "DISABLED" : strUserEvent; + static const BSFixedString disabledEvent{ "DISABLED" }; + return disabled ? disabledEvent : strUserEvent; } // members @@ -87,6 +97,7 @@ namespace RE bool disabled{ false }; // 34 }; static_assert(sizeof(IDEvent) == 0x38); + static_assert(std::is_same_v().QUserEvent()), const BSFixedString&>); class ICanBeChorded { From 7ef96eb40791edc4f8cc24887f7a96aa6b680503 Mon Sep 17 00:00:00 2001 From: Quantumyilmaz <47591838+Quantumyilmaz@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:45:20 +0200 Subject: [PATCH 2/5] feat(menu): map IMenu OnButtonEvent --- include/RE/IDs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/RE/IDs.h b/include/RE/IDs.h index ec67782b..efd40075 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -1088,7 +1088,7 @@ namespace RE::ID inline constexpr REL::ID dtor{ 0 }; // 187216 inline constexpr REL::ID ShouldHandleEvent{ 0 }; // 187262 inline constexpr REL::ID OnThumbstickEvent{ 0 }; // 187235 - inline constexpr REL::ID OnButtonEvent{ 0 }; // 187234 + inline constexpr REL::ID OnButtonEvent{ 130632 }; inline constexpr REL::ID LoadMovie{ 0 }; // 187240 inline constexpr REL::ID ProcessMessage{ 0 }; // 187247 inline constexpr REL::ID Unk09{ 0 }; // 80440 From f2b08ab29d3a4748fa267fbd62249937d7858a5a Mon Sep 17 00:00:00 2001 From: Quantumyilmaz <47591838+Quantumyilmaz@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:08:46 +0200 Subject: [PATCH 3/5] feat(quest): expose instance tracking state --- include/RE/IDs.h | 3 ++- include/RE/T/TESQuest.h | 55 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/include/RE/IDs.h b/include/RE/IDs.h index efd40075..a108c752 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -2506,7 +2506,8 @@ namespace RE::ID namespace TESQuest { - inline constexpr REL::ID IsStageDone{ 0 }; // 112585 + inline constexpr REL::ID IsStageDone{ 0 }; // 112585 + inline constexpr REL::ID ToggleTracking{ 91440 }; } namespace TESQuestRewardEvent diff --git a/include/RE/T/TESQuest.h b/include/RE/T/TESQuest.h index 833ea0ad..9293d55b 100644 --- a/include/RE/T/TESQuest.h +++ b/include/RE/T/TESQuest.h @@ -2,6 +2,9 @@ #include "RE/B/BGSStoryManagerTreeForm.h" +#include +#include + namespace RE { class BGSStoryEvent; @@ -16,6 +19,16 @@ namespace RE }; static_assert(sizeof(QUEST_DATA) == 0x8); + struct QuestInstanceKey + { + TESFormID formID; // 0 + std::uint32_t instanceID; // 4 + + [[nodiscard]] constexpr bool operator==(const QuestInstanceKey&) const noexcept = default; + }; + static_assert(sizeof(QuestInstanceKey) == 0x8); + static_assert(std::is_trivially_copyable_v); + class __declspec(novtable) TESQuest : public BGSStoryManagerTreeForm { @@ -23,6 +36,13 @@ namespace RE SF_RTTI_VTABLE(TESQuest); SF_FORMTYPE(QUST); + enum class RuntimeFlag : std::uint32_t + { + kRunning = 1 << 0, + kStopped = 1 << 1, + kTracked = 1 << 11 + }; + ~TESQuest() override; // 00 [[nodiscard]] bool IsStageDone(std::uint16_t a_stage) const @@ -32,11 +52,38 @@ namespace RE return func(this, a_stage); } + [[nodiscard]] constexpr std::uint32_t GetInstanceID() const noexcept { return instanceID; } + + [[nodiscard]] QuestInstanceKey GetInstanceKey() const noexcept + { + return { GetFormID(), GetInstanceID() }; + } + + [[nodiscard]] constexpr bool IsRunning() const noexcept { return runtimeFlags.all(RuntimeFlag::kRunning); } + [[nodiscard]] constexpr bool IsStopped() const noexcept { return runtimeFlags.all(RuntimeFlag::kStopped); } + [[nodiscard]] constexpr bool IsTracked() const noexcept { return runtimeFlags.all(RuntimeFlag::kTracked); } + + // This is the engine's toggle operation. Callers that need set semantics + // must compare IsTracked() with the desired state before invoking it. + void ToggleTracking() + { + const auto key = GetInstanceKey(); + using func_t = void (*)(const QuestInstanceKey*); + static REL::Relocation func{ ID::TESQuest::ToggleTracking }; + func(std::addressof(key)); + } + // members - std::byte pad038[0xD0]; // 038 - QUEST_DATA data; // 108 - std::byte pad110[0x220]; // 110 - BGSStoryEvent* startEventData; // 330 + std::byte pad038[0x38]; // 038 + std::uint32_t instanceID; // 070 + std::byte pad074[0x94]; // 074 + QUEST_DATA data; // 108 + std::byte pad110[0x4]; // 110 + REX::TEnumSet runtimeFlags; // 114 + std::byte pad118[0x218]; // 118 + BGSStoryEvent* startEventData; // 330 }; + static_assert(offsetof(TESQuest, instanceID) == 0x70); + static_assert(offsetof(TESQuest, runtimeFlags) == 0x114); static_assert(offsetof(TESQuest, startEventData) == 0x330); } From 869aa785d25dd03f2da4abda48b3b04b8b32027b Mon Sep 17 00:00:00 2001 From: Quantumyilmaz <47591838+Quantumyilmaz@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:24:20 +0200 Subject: [PATCH 4/5] feat(containers): expose BSTHeapSTLVector storage bounds --- include/RE/B/BSTHeapSTLVector.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/RE/B/BSTHeapSTLVector.h b/include/RE/B/BSTHeapSTLVector.h index acd68156..2ad7eb04 100644 --- a/include/RE/B/BSTHeapSTLVector.h +++ b/include/RE/B/BSTHeapSTLVector.h @@ -52,6 +52,12 @@ namespace RE [[nodiscard]] constexpr pointer data() noexcept { return _begin; } [[nodiscard]] constexpr const_pointer data() const noexcept { return _begin; } + // Raw capacity sentinel for validating engine-owned vectors before using + // size() or capacity(). Those helpers subtract pointers and therefore require + // an already-valid vector representation. + [[nodiscard]] constexpr pointer capacity_end() noexcept { return _capacityEnd; } + [[nodiscard]] constexpr const_pointer capacity_end() const noexcept { return _capacityEnd; } + [[nodiscard]] constexpr size_type size() const noexcept { return static_cast(_end - _begin); From 04a3d88e2925806355000190c9c3a9df586ebf3f Mon Sep 17 00:00:00 2001 From: Quantumyilmaz <47591838+Quantumyilmaz@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:14:58 +0200 Subject: [PATCH 5/5] feat(starmap): expose surface map runtime APIs --- include/RE/E/Events.h | 14 +---- include/RE/IDs.h | 14 +++++ include/RE/S/StarMap.h | 120 +++++++++++++++++++++++++++++++++++++++++ include/RE/Starfield.h | 1 + 4 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 include/RE/S/StarMap.h diff --git a/include/RE/E/Events.h b/include/RE/E/Events.h index 75666a0f..85dbc43d 100644 --- a/include/RE/E/Events.h +++ b/include/RE/E/Events.h @@ -5,6 +5,7 @@ #include "RE/B/BSPointerHandle.h" #include "RE/B/BSTEvent.h" #include "RE/N/NiSmartPointer.h" +#include "RE/S/StarMap.h" namespace RE { @@ -3374,19 +3375,6 @@ namespace RE static_assert(sizeof(SpellsLearned::Event) == 0x8); }; - struct StarMap - { - struct PlanetTraitKnownEvent - { - [[nodiscard]] static BSTEventSource* GetEventSource() - { - using func_t = decltype(&StarMap::PlanetTraitKnownEvent::GetEventSource); - static REL::Relocation func{ ID::StarMap::PlanetTraitKnownEvent::GetEventSource }; - return func(); - } - }; - }; - struct StarMapMenu_LandingInputInProgress { [[nodiscard]] static BSTEventSource* GetEventSource() diff --git a/include/RE/IDs.h b/include/RE/IDs.h index a108c752..ab29e2ed 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -2156,6 +2156,20 @@ namespace RE::ID { inline constexpr REL::ID RefreshPanelData{ 93988 }; inline constexpr REL::ID ScanHandler{ 94011 }; + inline constexpr REL::ID ComposeSurfaceQuestTarget{ 95013 }; + } + + namespace StarMap::StarMapMenu + { + inline constexpr REL::ID OnButtonEvent{ 94684 }; + inline constexpr REL::ID GetSurfaceMapState{ 94755 }; + } + + namespace StarMap::SurfaceMapState + { + inline constexpr REL::ID RebuildSurfaceMarkers{ 95000 }; + inline constexpr REL::ID Refresh{ 95003 }; + inline constexpr REL::ID GatherSurfaceQuestTargets{ 95012 }; } namespace StarMap::PlanetTraitKnownEvent diff --git a/include/RE/S/StarMap.h b/include/RE/S/StarMap.h new file mode 100644 index 00000000..cd9e9cd1 --- /dev/null +++ b/include/RE/S/StarMap.h @@ -0,0 +1,120 @@ +#pragma once + +#include "RE/B/BSCoreTypes.h" +#include "RE/B/BSFixedString.h" +#include "RE/B/BSTEvent.h" +#include "RE/B/BSTHeapSTLVector.h" +#include "RE/G/GameMenuBase.h" + +namespace RE +{ + struct StarMap + { + enum class SurfaceMarkerType : std::uint16_t + { + kQuest = 0x48 + }; + + struct PlanetTraitKnownEvent + { + [[nodiscard]] static BSTEventSource* GetEventSource() + { + using func_t = decltype(&PlanetTraitKnownEvent::GetEventSource); + static REL::Relocation func{ ID::StarMap::PlanetTraitKnownEvent::GetEventSource }; + return func(); + } + }; + + struct SurfaceMarkerStaticData + { + [[nodiscard]] constexpr bool IsLocation() const noexcept { return isLocation == 1; } + [[nodiscard]] constexpr bool HasQuestTarget() const noexcept { return hasQuestTarget == 1; } + [[nodiscard]] constexpr bool IsQuestActive() const noexcept { return questActive == 1; } + + // members + std::byte pad000[0x28]; // 00 + BSFixedString nameText; // 28 + BSFixedString extraText; // 30 + BSFixedString questTargetText; // 38 + std::uint32_t markerHandleBits; // 40 + std::uint32_t pad044; // 44 + BSTHeapSTLVector questOwners; // 48 + SurfaceMarkerType markerType; // 60 + std::byte pad062[0x6]; // 62 + std::uint8_t isLocation; // 68 + std::uint8_t hasQuestTarget; // 69 + std::uint8_t questActive; // 6A + std::byte pad06B[0x15]; // 6B + }; + static_assert(offsetof(SurfaceMarkerStaticData, nameText) == 0x28); + static_assert(offsetof(SurfaceMarkerStaticData, extraText) == 0x30); + static_assert(offsetof(SurfaceMarkerStaticData, questTargetText) == 0x38); + static_assert(offsetof(SurfaceMarkerStaticData, markerHandleBits) == 0x40); + static_assert(offsetof(SurfaceMarkerStaticData, questOwners) == 0x48); + static_assert(offsetof(SurfaceMarkerStaticData, markerType) == 0x60); + static_assert(offsetof(SurfaceMarkerStaticData, isLocation) == 0x68); + static_assert(offsetof(SurfaceMarkerStaticData, hasQuestTarget) == 0x69); + static_assert(offsetof(SurfaceMarkerStaticData, questActive) == 0x6A); + static_assert(sizeof(SurfaceMarkerStaticData) == 0x80); + + class SurfaceMapState + { + public: + SF_RTTI_VTABLE(StarMap__SurfaceMapState); + + inline static constexpr REL::ID PRIMARY_VTABLE = RE::VTABLE::StarMap__SurfaceMapState[0]; + + // Surface Map state is owned by the Star Map UI thread. These operations + // are not thread-safe or reentrant. + + void RebuildSurfaceMarkers() + { + using func_t = void (*)(SurfaceMapState*); + static REL::Relocation func{ ID::StarMap::SurfaceMapState::RebuildSurfaceMarkers }; + func(this); + } + + void Refresh() + { + using func_t = void (*)(SurfaceMapState*); + static REL::Relocation func{ ID::StarMap::SurfaceMapState::Refresh }; + func(this); + } + + // members + // Engine-owned storage. External callers must treat it as read-only. + // Elements and their nested storage are invalidated by rebuild, refresh, + // state transition, and destruction. Do not retain pointers or views. + std::byte pad000[0x8B8]; // 000 + BSTHeapSTLVector surfaceMarkers; // 8B8 + }; + static_assert(offsetof(SurfaceMapState, surfaceMarkers) == 0x8B8); + + class StarMapMenu : + public GameMenuBase + { + public: + SF_RTTI_VTABLE(StarMap__StarMapMenu); + SF_MENU_NAME("GalaxyStarMapMenu"); + + inline static constexpr REL::ID PRIMARY_VTABLE = RE::VTABLE::StarMap__StarMapMenu[12]; + inline static constexpr REL::ID BSINPUTEVENTUSER_VTABLE = RE::VTABLE::StarMap__StarMapMenu[13]; + + void OnButtonEvent(const ButtonEvent* a_event) override + { + using func_t = void (*)(BSInputEventUser*, const ButtonEvent*); + static REL::Relocation func{ ID::StarMap::StarMapMenu::OnButtonEvent }; + func(this, a_event); + } + + // Returns a non-owning owner-thread pointer. It is invalidated by Star Map + // state transition and menu destruction; do not retain it. + [[nodiscard]] SurfaceMapState* GetSurfaceMapState() + { + using func_t = SurfaceMapState* (*)(StarMapMenu*); + static REL::Relocation func{ ID::StarMap::StarMapMenu::GetSurfaceMapState }; + return func(this); + } + }; + }; +} diff --git a/include/RE/Starfield.h b/include/RE/Starfield.h index a297f5b4..91db2540 100644 --- a/include/RE/Starfield.h +++ b/include/RE/Starfield.h @@ -395,6 +395,7 @@ #include "RE/S/SpellItem.h" #include "RE/S/Stack.h" #include "RE/S/StackFrame.h" +#include "RE/S/StarMap.h" #include "RE/S/Store.h" #include "RE/S/Stream.h" #include "RE/S/StreamBase.h"