From e8624fcfc0a6cbd8da01333d484c8c1838b93706 Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Wed, 23 Sep 2026 10:26:53 +0300 Subject: [PATCH 1/5] Return false from the registration maps when there is no VM Register and Unregister in RegistrationMap and RegistrationMapUnique return bool, but the branch that finds no VM ended in a bare return, which clang-cl refuses to compile. It now returns false, the same as the invalid handle check right after it. --- src/F4SE/RegistrationMap.cpp | 4 ++-- src/F4SE/RegistrationMapUnique.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/F4SE/RegistrationMap.cpp b/src/F4SE/RegistrationMap.cpp index d10f2bb9..c0c32223 100644 --- a/src/F4SE/RegistrationMap.cpp +++ b/src/F4SE/RegistrationMap.cpp @@ -322,7 +322,7 @@ namespace F4SE auto vm = game->GetVM(); if (!vm) { REX::ERROR("Failed to get VM!"sv); - return; + return false; } const auto invalidHandle = vm->GetObjectHandlePolicy().EmptyHandle(); @@ -351,7 +351,7 @@ namespace F4SE auto vm = game->GetVM(); if (!vm) { REX::ERROR("Failed to get VM!"sv); - return; + return false; } const auto invalidHandle = vm->GetObjectHandlePolicy().EmptyHandle(); diff --git a/src/F4SE/RegistrationMapUnique.cpp b/src/F4SE/RegistrationMapUnique.cpp index 2add9fbb..1f3377ad 100644 --- a/src/F4SE/RegistrationMapUnique.cpp +++ b/src/F4SE/RegistrationMapUnique.cpp @@ -403,7 +403,7 @@ namespace F4SE auto vm = game->GetVM(); if (!vm) { REX::ERROR("Failed to get VM!"sv); - return; + return false; } const auto invalidHandle = vm->GetObjectHandlePolicy().EmptyHandle(); @@ -432,7 +432,7 @@ namespace F4SE auto vm = game->GetVM(); if (!vm) { REX::ERROR("Failed to get VM!"sv); - return; + return false; } const auto invalidHandle = vm->GetObjectHandlePolicy().EmptyHandle(); From 65e6922adb0d7d1d01cdde6882902d17f650b118 Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:00:40 +0300 Subject: [PATCH 2/5] Fix wrong layouts, types, a wrong ID and misleading offset comments BGSDefaultObjectManager::GetSingleton called ID 2192850, which is not a getter. That function looks up one default object by its DEFAULT_OBJECT index, and builds the manager first if needed. GetSingleton called it with no index, so the lookup used whatever value was left in the ecx register and returned a random form instead of the manager. The manager itself lives at ID 4796209. GetSingleton now returns that address, the same way BSTimer and BSGraphics::State return theirs. MessageBoxData is 0x70 bytes, not 0x68. MessageMenuManager::QueueMessage allocates 0x70 bytes for it, and the constructor fills two more members. cancelButtonIndex at 0x68 is the button the Cancel key presses, or -1 when the key cannot close the box. hasCancelButton at 0x6C is true when that index is not -1. When MessageBoxMenu shows a box, it copies those two members to 0xF8 and 0xE8. That puts currentMessage at 0xF0 and makes the menu 0x100 bytes, which matches what its creator allocates. MessageMenuManager::Create takes the Cancel button index as its last argument and passes it straight to QueueMessage. The binding stopped at a_ensureUnique, so that argument was whatever happened to be on the stack. It is now a_cancelButtonIndex and defaults to -1. TESLeveledList keeps its entry counts in single bytes that the engine reads as numbers from 0 to 255, so scriptListCount, baseListCount and maxUseAllCount are std::uint8_t. Read as signed, a list of more than 127 entries looked shorter than it is. GetUseAll only tests bit 2 of llFlags, so it is const. Offset comments match where the members land: BGSComponent::modScrapScalar is at A0, ActiveEffect::hitEffectController at 10, and the ActiveEffect members from elapsedSeconds to conditionStatus sit four bytes further on than their comments said. --- include/RE/A/ActiveEffect.h | 12 ++++++------ include/RE/B/BGSComponent.h | 2 +- include/RE/B/BGSDefaultObjectManager.h | 5 ++--- include/RE/IDs.h | 2 +- include/RE/M/MessageBoxData.h | 20 +++++++++++--------- include/RE/M/MessageBoxMenu.h | 6 ++++-- include/RE/M/MessageMenuManager.h | 5 +++-- include/RE/T/TESLeveledList.h | 8 ++++---- 8 files changed, 32 insertions(+), 28 deletions(-) diff --git a/include/RE/A/ActiveEffect.h b/include/RE/A/ActiveEffect.h index 79fbf1da..03301963 100644 --- a/include/RE/A/ActiveEffect.h +++ b/include/RE/A/ActiveEffect.h @@ -67,7 +67,7 @@ namespace RE [[nodiscard]] Actor* GetTargetActor(); // members - ActiveEffectReferenceEffectController hitEffectController; // 0C + ActiveEffectReferenceEffectController hitEffectController; // 10 BSSoundHandle persistentSound; // 30 ActorHandle caster; // 38 NiPointer sourceNode; // 40 @@ -77,11 +77,11 @@ namespace RE TESBoundObject* source; // 60 BSSimpleList* hitEffects; // 68 MagicItem* displacementSpell; // 70 - float elapsedSeconds; // 74 - float duration; // 78 - float magnitude; // 7C - REX::TEnumSet flags; // 80 - ConditionStatus conditionStatus; // 84 + float elapsedSeconds; // 78 + float duration; // 7C + float magnitude; // 80 + REX::TEnumSet flags; // 84 + ConditionStatus conditionStatus; // 88 std::uint16_t uniqueID; // 8C MagicSystem::CastingSource castingSource; // 90 }; diff --git a/include/RE/B/BGSComponent.h b/include/RE/B/BGSComponent.h index fddcad83..121d9249 100644 --- a/include/RE/B/BGSComponent.h +++ b/include/RE/B/BGSComponent.h @@ -21,7 +21,7 @@ namespace RE // members TESObjectMISC* scrapItem; // 98 - TESGlobal* modScrapScalar; // 90 + TESGlobal* modScrapScalar; // A0 }; static_assert(sizeof(BGSComponent) == 0xA8); } diff --git a/include/RE/B/BGSDefaultObjectManager.h b/include/RE/B/BGSDefaultObjectManager.h index 2bbbc782..47bfec27 100644 --- a/include/RE/B/BGSDefaultObjectManager.h +++ b/include/RE/B/BGSDefaultObjectManager.h @@ -17,9 +17,8 @@ namespace RE [[nodiscard]] static BGSDefaultObjectManager* GetSingleton() { - using func_t = decltype(&BGSDefaultObjectManager::GetSingleton); - static REL::Relocation func{ ID::BGSDefaultObjectManager::Singleton }; - return func(); + static REL::Relocation singleton{ ID::BGSDefaultObjectManager::Singleton }; + return singleton.get(); } [[nodiscard]] TESForm* GetDefaultObject(DEFAULT_OBJECT a_obj) const noexcept diff --git a/include/RE/IDs.h b/include/RE/IDs.h index 91bdb73b..7408ca45 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -196,7 +196,7 @@ namespace RE::ID namespace BGSDefaultObjectManager { - inline constexpr REL::ID Singleton{ 2192850 }; + inline constexpr REL::ID Singleton{ 4796209 }; } namespace BGSDynamicPersistenceManager diff --git a/include/RE/M/MessageBoxData.h b/include/RE/M/MessageBoxData.h index be2693fa..f1a06946 100644 --- a/include/RE/M/MessageBoxData.h +++ b/include/RE/M/MessageBoxData.h @@ -19,14 +19,16 @@ namespace RE static constexpr auto VTABLE{ VTABLE::MessageBoxData }; // members - BSString headerText; // 18 - BSString bodyText; // 28 - BSTArray buttonText; // 38 - REX::TEnumSet warningContext; // 50 - BSTSmartPointer callback; // 58 - REX::TEnumSet menuDepth; // 60 - bool modal; // 64 - bool ensureUnique; // 65 + BSString headerText; // 18 + BSString bodyText; // 28 + BSTArray buttonText; // 38 + REX::TEnumSet warningContext; // 50 + BSTSmartPointer callback; // 58 + REX::TEnumSet menuDepth; // 60 + bool modal; // 64 + bool ensureUnique; // 65 + std::int32_t cancelButtonIndex; // 68 + bool hasCancelButton; // 6C }; - static_assert(sizeof(MessageBoxData) == 0x68); + static_assert(sizeof(MessageBoxData) == 0x70); } diff --git a/include/RE/M/MessageBoxMenu.h b/include/RE/M/MessageBoxMenu.h index c28b93b4..bc707493 100644 --- a/include/RE/M/MessageBoxMenu.h +++ b/include/RE/M/MessageBoxMenu.h @@ -30,7 +30,9 @@ namespace RE } // members - MessageBoxData* currentMessage; // E8 + bool hasCancelButton; // E8 + MessageBoxData* currentMessage; // F0 + std::int32_t cancelButtonIndex; // F8 }; - static_assert(sizeof(MessageBoxMenu) == 0xF0); + static_assert(sizeof(MessageBoxMenu) == 0x100); } diff --git a/include/RE/M/MessageMenuManager.h b/include/RE/M/MessageMenuManager.h index 6b000d43..8084881f 100644 --- a/include/RE/M/MessageMenuManager.h +++ b/include/RE/M/MessageMenuManager.h @@ -26,11 +26,12 @@ namespace RE const char* a_button2Text = nullptr, const char* a_button3Text = nullptr, const char* a_button4Text = nullptr, - bool a_ensureUnique = false) + bool a_ensureUnique = false, + std::int32_t a_cancelButtonIndex = -1) { using func_t = decltype(&MessageMenuManager::Create); static REL::Relocation func{ ID::MessageMenuManager::Create }; - return func(this, a_headerText, a_bodyText, a_callback, a_warningContext, a_button1Text, a_button2Text, a_button3Text, a_button4Text, a_ensureUnique); + return func(this, a_headerText, a_bodyText, a_callback, a_warningContext, a_button1Text, a_button2Text, a_button3Text, a_button4Text, a_ensureUnique, a_cancelButtonIndex); } // members diff --git a/include/RE/T/TESLeveledList.h b/include/RE/T/TESLeveledList.h index 6f6d7a9e..3a682b75 100644 --- a/include/RE/T/TESLeveledList.h +++ b/include/RE/T/TESLeveledList.h @@ -63,7 +63,7 @@ namespace RE return func(this, a_ref, a_outCont, a_legendary); } - bool GetUseAll() + [[nodiscard]] bool GetUseAll() const { using func_t = decltype(&TESLeveledList::GetUseAll); static REL::Relocation func{ ID::TESLeveledList::GetUseAll }; @@ -75,11 +75,11 @@ namespace RE BSTArray>* keywordChances; // 10 LEVELED_OBJECT* leveledLists; // 18 LEVELED_OBJECT** scriptAddedLists; // 20 - std::int8_t scriptListCount; // 28 - std::int8_t baseListCount; // 29 + std::uint8_t scriptListCount; // 28 + std::uint8_t baseListCount; // 29 std::int8_t chanceNone; // 2A std::int8_t llFlags; // 2B - std::int8_t maxUseAllCount; // 2C + std::uint8_t maxUseAllCount; // 2C }; static_assert(sizeof(TESLeveledList) == 0x30); } From 448230064eab0e34ea90db4fff45db9975b2426a Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:17:36 +0300 Subject: [PATCH 3/5] Add MagicCaster and the values of the MagicSystem enums A magic caster is the object that casts spells for a reference, meaning an actor or an object placed in the world. Each actor has one ActorMagicCaster per casting source, such as the left hand, the right hand or instant casts. Any other reference, such as a mine or a turret, gets a NonActorMagicCaster in its extra data. TESObjectREFR::GetMagicCaster returns either kind. Weapons use them too, because a weapon's object effects are spells cast on whatever the weapon hits. MagicCaster::Cast, ID 2226296, applies the spell stored in currentSpell to its targets. a_effectiveness scales how strongly every effect lands, and for a weapon's object effect an actor's CheckCast leaves it at 1.0. Three paths end in Cast: CastSpellImmediate, the queued cast that applies a hit's object effects, and the cast that applies a worn item's enchantments to its wearer. The layouts come from the constructors and the allocation sizes. MagicCaster is 0x48 bytes. ActorMagicCaster is 0x100 bytes, with its actor at 0xB8 and its casting source at 0xF4. NonActorMagicCaster is 0x70 bytes and is also an ExtraMagicCaster. It keeps the reference that casts at 0x60, and the actor who gets the blame for it at 0x68. Virtual functions are named only where their code shows what they do, and placeholders keep the named ones in their right slots. CastingType, Delivery and SpellType take their values from the game's own lists of setting names. The game finds the name of a spell's type, casting type and delivery by using the enum value as a position in those lists, so the list order is the enum order. The values also match every value the magic code checks for. --- include/RE/A/ActorMagicCaster.h | 49 ++++++++++++++++ include/RE/E/ExtraMagicCaster.h | 20 +++++++ include/RE/Fallout.h | 5 ++ include/RE/IDs.h | 5 ++ include/RE/M/MagicCaster.h | 91 ++++++++++++++++++++++++++++++ include/RE/M/MagicSystem.h | 41 ++++++++++++++ include/RE/N/NonActorMagicCaster.h | 29 ++++++++++ 7 files changed, 240 insertions(+) create mode 100644 include/RE/A/ActorMagicCaster.h create mode 100644 include/RE/E/ExtraMagicCaster.h create mode 100644 include/RE/M/MagicCaster.h create mode 100644 include/RE/M/MagicSystem.h create mode 100644 include/RE/N/NonActorMagicCaster.h diff --git a/include/RE/A/ActorMagicCaster.h b/include/RE/A/ActorMagicCaster.h new file mode 100644 index 00000000..1e1a3bd4 --- /dev/null +++ b/include/RE/A/ActorMagicCaster.h @@ -0,0 +1,49 @@ +#pragma once + +#include "RE/B/BSTEvent.h" +#include "RE/M/MagicCaster.h" +#include "RE/R/RefAttachTechniqueInput.h" +#include "RE/S/SimpleAnimationGraphManagerHolder.h" + +namespace RE +{ + class BSAnimationGraphEvent; + + class __declspec(novtable) ActorMagicCaster : + public MagicCaster, // 00 + public SimpleAnimationGraphManagerHolder, // 48 + public BSTEventSink // 60 + { + public: + static constexpr auto RTTI{ RTTI::ActorMagicCaster }; + static constexpr auto VTABLE{ VTABLE::ActorMagicCaster }; + + ~ActorMagicCaster() override; // 00 + + // override (MagicCaster) + bool CheckCast(MagicItem* a_spell, bool a_dualCast, float* a_effectiveness, MagicSystem::CannotCastReason* a_reason, bool a_arg5) override; // 0B + TESObjectREFR* GetCasterStatsObject() const override; // 0C + Actor* GetCasterAsActor() const override; // 0D + NiNode* GetMagicNode() override; // 0F + void ClearMagicNode() override; // 10 + MagicSystem::CastingSource GetCastingSource() const override; // 16 + bool GetIsDualCasting() const override; // 17 + void SetDualCasting(bool a_dualCasting) override; // 18 + void AdjustActiveEffect(ActiveEffect* a_effect, float a_power, bool a_hostileEffectivenessOnly) override; // 1D + + // members + RefAttachTechniqueInput unk68; // 68 + std::uint64_t unkB0; // B0 + Actor* actor; // B8 + NiNode* magicNode; // C0 + std::uint64_t unkC8; // C8 + std::uint64_t unkD0; // D0 + std::uint64_t unkD8; // D8 + std::uint64_t unkE0; // E0 + std::uint64_t unkE8; // E8 + std::uint32_t unkF0; // F0 + MagicSystem::CastingSource castingSource; // F4 + std::uint32_t flags; // F8 + }; + static_assert(sizeof(ActorMagicCaster) == 0x100); +} diff --git a/include/RE/E/ExtraMagicCaster.h b/include/RE/E/ExtraMagicCaster.h new file mode 100644 index 00000000..c4b133f9 --- /dev/null +++ b/include/RE/E/ExtraMagicCaster.h @@ -0,0 +1,20 @@ +#pragma once + +#include "RE/B/BSExtraData.h" +#include "RE/M/MagicCaster.h" + +namespace RE +{ + class __declspec(novtable) ExtraMagicCaster : + public BSExtraData, // 00 + public MagicCaster // 18 + { + public: + static constexpr auto RTTI{ RTTI::ExtraMagicCaster }; + static constexpr auto VTABLE{ VTABLE::ExtraMagicCaster }; + static constexpr auto TYPE{ EXTRA_DATA_TYPE::kMagicCaster }; + + ~ExtraMagicCaster() override; // 00 + }; + static_assert(sizeof(ExtraMagicCaster) == 0x60); +} diff --git a/include/RE/Fallout.h b/include/RE/Fallout.h index aed37151..b5188191 100644 --- a/include/RE/Fallout.h +++ b/include/RE/Fallout.h @@ -35,6 +35,7 @@ #include "RE/A/ActorEquipManagerEvent.h" #include "RE/A/ActorItemEquipped.h" #include "RE/A/ActorKill.h" +#include "RE/A/ActorMagicCaster.h" #include "RE/A/ActorMotionFeedbackData.h" #include "RE/A/ActorMovementData.h" #include "RE/A/ActorPackage.h" @@ -701,6 +702,7 @@ #include "RE/E/ExtraLinkedRefChildren.h" #include "RE/E/ExtraLocation.h" #include "RE/E/ExtraLock.h" +#include "RE/E/ExtraMagicCaster.h" #include "RE/E/ExtraMapMarker.h" #include "RE/E/ExtraMaterialSwap.h" #include "RE/E/ExtraOutfitItem.h" @@ -985,7 +987,9 @@ #include "RE/M/MENU_RENDER_CONTEXT.h" #include "RE/M/MESSAGEBOX_BUTTON.h" #include "RE/M/MOVABLE_STATIC_DATA.h" +#include "RE/M/MagicCaster.h" #include "RE/M/MagicItem.h" +#include "RE/M/MagicSystem.h" #include "RE/M/MagicTarget.h" #include "RE/M/Main.h" #include "RE/M/MainMenu.h" @@ -1089,6 +1093,7 @@ #include "RE/N/NiTransform.h" #include "RE/N/NiUpdateData.h" #include "RE/N/NiVisibleArray.h" +#include "RE/N/NonActorMagicCaster.h" #include "RE/N/NonMorphFaceManagement.h" #include "RE/N/nsHUDNotifications.h" #include "RE/N/nsHUDTypes.h" diff --git a/include/RE/IDs.h b/include/RE/IDs.h index 7408ca45..0f3842de 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -1467,6 +1467,11 @@ namespace RE::ID inline constexpr REL::ID GetEventSource{ 2249292 }; } + namespace MagicCaster + { + inline constexpr REL::ID Cast{ 2226296 }; + } + namespace MagicTarget { inline constexpr REL::ID IsTakingHealthDamageFromActiveEffect{ 2226397 }; diff --git a/include/RE/M/MagicCaster.h b/include/RE/M/MagicCaster.h new file mode 100644 index 00000000..758f997d --- /dev/null +++ b/include/RE/M/MagicCaster.h @@ -0,0 +1,91 @@ +#pragma once + +#include "RE/B/BSPointerHandle.h" +#include "RE/B/BSSoundHandle.h" +#include "RE/B/BSTArray.h" + +namespace RE +{ + class ActiveEffect; + class Actor; + class MagicItem; + class NiNode; + class TESBoundObject; + class TESObjectREFR; + + namespace MagicSystem + { + enum class CannotCastReason; + enum class CastingSource : std::int32_t; + } + + class __declspec(novtable) MagicCaster + { + public: + static constexpr auto RTTI{ RTTI::MagicCaster }; + static constexpr auto VTABLE{ VTABLE::MagicCaster }; + + virtual ~MagicCaster(); // 00 + + // add + virtual void Unk01(); // 01 + virtual void CastSpellImmediate(MagicItem* a_spell, bool a_noHitEffectArt, TESObjectREFR* a_target, float a_effectiveness, bool a_hostileEffectivenessOnly, float a_magnitudeOverride, Actor* a_blameActor); // 02 + virtual void Unk03(); // 03 + virtual void Unk04(); // 04 + virtual void Unk05(); // 05 + virtual void Unk06(); // 06 + virtual void Unk07(); // 07 + virtual void Unk08(); // 08 + virtual void Unk09(); // 09 + virtual void Unk0A(); // 0A + virtual bool CheckCast(MagicItem* a_spell, bool a_dualCast, float* a_effectiveness, MagicSystem::CannotCastReason* a_reason, bool a_arg5); // 0B + virtual TESObjectREFR* GetCasterStatsObject() const; // 0C + virtual Actor* GetCasterAsActor() const; // 0D + virtual TESObjectREFR* GetCasterObjectReference(Actor** a_outCaster) const; // 0E + virtual NiNode* GetMagicNode(); // 0F + virtual void ClearMagicNode(); // 10 + virtual void Unk11(); // 11 + virtual void Unk12(); // 12 + virtual void Unk13(); // 13 + virtual void Unk14(); // 14 + virtual void Unk15(); // 15 + virtual MagicSystem::CastingSource GetCastingSource() const; // 16 + virtual bool GetIsDualCasting() const; // 17 + virtual void SetDualCasting(bool a_dualCasting); // 18 + virtual void Unk19(); // 19 + virtual void Unk1A(); // 1A + virtual void Unk1B(); // 1B + virtual void Unk1C(); // 1C + virtual void AdjustActiveEffect(ActiveEffect* a_effect, float a_power, bool a_hostileEffectivenessOnly); // 1D + + // Casts currentSpell on the targets it finds. a_effectiveness is the + // power the spell lands at, a multiplier on each effect's magnitude, or + // on its duration for an effect that has no magnitude, and + // a_hostileEffectivenessOnly narrows it to the hostile effects. Some + // kinds of spell ignore it, abilities, addictions and constant effect + // enchantments among them. a_targetCount receives how many targets were + // reached, and a_source names what the spell came from, the weapon for + // the object effects of a melee blow. Returns whether anything was cast. + // + // A weapon's object effects, a blast's and anything CastSpellImmediate + // casts all end here. + bool Cast(float a_effectiveness, std::uint32_t* a_targetCount, TESBoundObject* a_source, bool a_noHitEffectArt, bool a_hostileEffectivenessOnly) + { + using func_t = decltype(&MagicCaster::Cast); + static REL::Relocation func{ ID::MagicCaster::Cast }; + return func(this, a_effectiveness, a_targetCount, a_source, a_noHitEffectArt, a_hostileEffectivenessOnly); + } + + // members + BSTArray sounds; // 08 + ObjectRefHandle desiredTarget; // 20 + MagicItem* currentSpell; // 28 + std::uint32_t state; // 30 + float unk34; // 34 + float unk38; // 38 + float magnitudeOverride; // 3C + float unk40; // 40 + float unk44; // 44 + }; + static_assert(sizeof(MagicCaster) == 0x48); +} diff --git a/include/RE/M/MagicSystem.h b/include/RE/M/MagicSystem.h new file mode 100644 index 00000000..6b32db83 --- /dev/null +++ b/include/RE/M/MagicSystem.h @@ -0,0 +1,41 @@ +#pragma once + +namespace RE +{ + namespace MagicSystem + { + enum class CastingType : std::int32_t + { + kConstantEffect = 0, + kFireAndForget = 1, + kConcentration = 2 + }; + + enum class Delivery : std::int32_t + { + kSelf = 0, + kTouch = 1, + kAimed = 2, + kTargetActor = 3, + kTargetLocation = 4 + }; + + enum class SpellType : std::int32_t + { + kSpell = 0, + kDisease = 1, + kPower = 2, + kLesserPower = 3, + kAbility = 4, + kPoison = 5, + kEnchantment = 6, + kPotion = 7, + kIngredient = 8, + kLeveledSpell = 9, + kAddiction = 10, + kVoicePower = 11, + kStaffEnchantment = 12, + kScroll = 13 + }; + } +} diff --git a/include/RE/N/NonActorMagicCaster.h b/include/RE/N/NonActorMagicCaster.h new file mode 100644 index 00000000..415a0fb0 --- /dev/null +++ b/include/RE/N/NonActorMagicCaster.h @@ -0,0 +1,29 @@ +#pragma once + +#include "RE/B/BSPointerHandle.h" +#include "RE/E/ExtraMagicCaster.h" + +namespace RE +{ + class __declspec(novtable) NonActorMagicCaster : + public ExtraMagicCaster // 00 + { + public: + static constexpr auto RTTI{ RTTI::NonActorMagicCaster }; + static constexpr auto VTABLE{ VTABLE::NonActorMagicCaster }; + + ~NonActorMagicCaster() override; // 00 + + // override (MagicCaster) + void CastSpellImmediate(MagicItem* a_spell, bool a_noHitEffectArt, TESObjectREFR* a_target, float a_effectiveness, bool a_hostileEffectivenessOnly, float a_magnitudeOverride, Actor* a_blameActor) override; // 02 + TESObjectREFR* GetCasterStatsObject() const override; // 0C + Actor* GetCasterAsActor() const override; // 0D + TESObjectREFR* GetCasterObjectReference(Actor** a_outCaster) const override; // 0E + NiNode* GetMagicNode() override; // 0F + + // members + TESObjectREFR* ref; // 60 + ActorHandle blameActor; // 68 + }; + static_assert(sizeof(NonActorMagicCaster) == 0x70); +} From 86cecd58cc822d6bc030da4257b634d252221053 Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:48:01 +0300 Subject: [PATCH 4/5] Add weapon, blast, inventory and form bindings Actor::GetEquippedItemHealth, ID 2231099, is the weapon condition an attack reads and passes to CombatFormulas::CalcWeaponDamage. It takes an equip index, but always reads the first equipped weapon, because it looks up the equip slot without naming an item. HitData::ApplyDamageTypes, ID 2236861, adds a weapon's damage types to a hit. Physical damage is one number, while the rest of what a weapon deals, such as the energy on a laser, is a list of damage type and value pairs on its instance data. An ordinary hit passes the same range falloff here that it passes CalcWeaponDamage. Explosion::ProcessTargets, ID 2236664, deals a blast to what FindTargets caught. It strikes the actors, casts the explosion's object effect on them and damages everything else. Actor::OnMagazineEmpty, ID 2231137, is what UseAmmo calls when a magazine runs dry. It releases the trigger of a gun in the middle of firing, which is what stops an automatic weapon at the last round. BipedAnim::SetObjectGraphVariableInt, ID 2194367, sets a variable on the animation graph of the model in one biped slot. UseAmmo uses it to tell a gun how many rounds are left. BGSAudio::PlaySoundDescriptor, ID 2214755, plays a sound descriptor at a point, on a moving object or with no position in the world. GamePlayFormulas::CalculateItemValue gets a binding for its existing ID. It turns a base value into the price the player sees, and while a barter screen is open it puts the trader's markup on every price the game works out. BGSInventoryList::AddStack, ID 2194191, adds a stack to an inventory, joining a matching stack or starting a new one. AddItem1, AddItem2 and filling a list from a container all add through it. Console::RunQueuedCommands, ID 2248538, runs the commands ExecuteCommand queued. ExecuteCommand only parses the line, so a console command takes effect during this call. TESDataHandler::FIRST_DYNAMIC_FORM_ID is the first ID the game gives to forms it makes during play. LoadFiles resets nextID to it after every data load, AllocateFormID returns to it when the counter runs out, and ReleaseFormID never recycles an ID below it. So the game never gives FF000001 to FF0007FF to a form it makes. --- include/RE/A/Actor.h | 24 ++++++++++++++++++++++++ include/RE/B/BGSAudio.h | 16 ++++++++++++++++ include/RE/B/BGSInventoryList.h | 16 ++++++++++++++++ include/RE/B/BipedAnim.h | 12 ++++++++++++ include/RE/C/Console.h | 11 +++++++++++ include/RE/E/Explosion.h | 13 +++++++++++++ include/RE/G/GamePlayFormulas.h | 12 ++++++++++++ include/RE/H/HitData.h | 12 ++++++++++++ include/RE/IDs.h | 16 ++++++++++++++++ include/RE/T/TESDataHandler.h | 6 ++++++ 10 files changed, 138 insertions(+) diff --git a/include/RE/A/Actor.h b/include/RE/A/Actor.h index cb22f411..969ef6d9 100644 --- a/include/RE/A/Actor.h +++ b/include/RE/A/Actor.h @@ -335,6 +335,20 @@ namespace RE return func(this); } + // The item health an attack reads for the weapon in a_equipIndex, off the + // stack's health extra data, 1.0 for a stack that has none and 0.0 when + // no equipped weapon matches. + // + // It looks up the equip slot without naming an item, which always gives + // the first slot, so it reads the first equipped weapon stack flagged + // kSlotIndex1 whatever a_equipIndex says. + [[nodiscard]] float GetEquippedItemHealth(BGSEquipIndex a_equipIndex) const + { + using func_t = decltype(&Actor::GetEquippedItemHealth); + static REL::Relocation func{ ID::Actor::GetEquippedItemHealth }; + return func(this, a_equipIndex); + } + bool GetGhost() { using func_t = decltype(&Actor::GetGhost); @@ -476,6 +490,16 @@ namespace RE return func(this, a_deltaTime, a_deltaPos, a_defer); } + // What UseAmmo calls once the magazine in a_equipIndex runs dry. It + // releases the trigger of a gun in the middle of firing, which is what + // stops an automatic weapon at the last round. + void OnMagazineEmpty(BGSEquipIndex a_equipIndex) + { + using func_t = decltype(&Actor::OnMagazineEmpty); + static REL::Relocation func{ ID::Actor::OnMagazineEmpty }; + return func(this, a_equipIndex); + } + bool PerformAction(BGSAction* a_action, TESObjectREFR* a_target) { using func_t = decltype(&Actor::PerformAction); diff --git a/include/RE/B/BGSAudio.h b/include/RE/B/BGSAudio.h index ebd7fce1..2d14f951 100644 --- a/include/RE/B/BGSAudio.h +++ b/include/RE/B/BGSAudio.h @@ -4,6 +4,8 @@ namespace RE { class BGSSoundDescriptorForm; class BGSSoundOutput; + class NiAVObject; + class NiPoint3; class BGSAudio { @@ -21,5 +23,19 @@ namespace RE static REL::Relocation func{ ID::BGSAudio::GetUIOutputModel }; return func(); } + + // Plays a sound once. The last two arguments say where it plays from: + // a_objectToFollow the sound moves with this object + // a_position the sound stays at this point + // neither the sound plays from nowhere, like the click of + // an empty gun + // If both are given, the object wins. The game always adds 0x10 to + // a_usageFlags. + static void PlaySoundDescriptor(BGSSoundDescriptorForm* a_descriptor, std::uint32_t a_usageFlags, const NiPoint3* a_position, NiAVObject* a_objectToFollow) + { + using func_t = decltype(&BGSAudio::PlaySoundDescriptor); + static REL::Relocation func{ ID::BGSAudio::PlaySoundDescriptor }; + return func(a_descriptor, a_usageFlags, a_position, a_objectToFollow); + } }; } diff --git a/include/RE/B/BGSInventoryList.h b/include/RE/B/BGSInventoryList.h index a20de9d0..0efe31bf 100644 --- a/include/RE/B/BGSInventoryList.h +++ b/include/RE/B/BGSInventoryList.h @@ -45,6 +45,22 @@ namespace RE AddItem2(a_object, a_count, new ExtraDataList(), a_flags); } + // Adds a copy of a_stack, a stack of a_object, to the list. It joins a + // stack of the same object whose extra data matches, which is how + // identical items come to share one stack and a count, and goes in as a + // stack of its own otherwise. a_oldCount and a_newCount, when given, + // receive the count of the stack it went into before and after. + // + // AddItem1 and AddItem2 add through it, and so does filling a list from a + // container, for each entry whose base form has an object template. All + // three hold the list's write lock. + void AddStack(TESBoundObject* a_object, BGSInventoryItem::Stack* a_stack, std::uint32_t* a_oldCount = nullptr, std::uint32_t* a_newCount = nullptr) + { + using func_t = decltype(&BGSInventoryList::AddStack); + static REL::Relocation func{ ID::BGSInventoryList::AddStack }; + return func(this, a_object, a_stack, a_oldCount, a_newCount); + } + inline void BuildFromContainer(const TESContainer* a_container) { using func_t = decltype(&BGSInventoryList::BuildFromContainer); diff --git a/include/RE/B/BipedAnim.h b/include/RE/B/BipedAnim.h index 969b701e..297faefe 100644 --- a/include/RE/B/BipedAnim.h +++ b/include/RE/B/BipedAnim.h @@ -2,6 +2,7 @@ #include "RE/B/BIPED_OBJECT.h" #include "RE/B/BIPOBJECT.h" +#include "RE/B/BSFixedString.h" #include "RE/B/BSIntrusiveRefCounted.h" #include "RE/B/BSPointerHandle.h" @@ -33,6 +34,17 @@ namespace RE return root; } + // Sets a whole number variable on the animation graph of the model in one + // biped slot. False when the slot has no graph or the graph refuses it. + // UseAmmo tells a gun's own graph how many rounds are left this way, + // through iWeaponCharge on kWeaponGun. + bool SetObjectGraphVariableInt(BIPED_OBJECT a_bipedObject, const BSFixedString& a_variable, std::int32_t a_value) + { + using func_t = decltype(&BipedAnim::SetObjectGraphVariableInt); + static REL::Relocation func{ ID::BipedAnim::SetObjectGraphVariableInt }; + return func(this, a_bipedObject, a_variable, a_value); + } + // members NiNode* root; // 0008 BIPOBJECT object[std::to_underlying(BIPED_OBJECT::kTotal)]; // 0010 diff --git a/include/RE/C/Console.h b/include/RE/C/Console.h index 0c16b9d8..987aa31d 100644 --- a/include/RE/C/Console.h +++ b/include/RE/C/Console.h @@ -21,6 +21,17 @@ namespace RE return func(a_command); } + // Runs the commands ExecuteCommand has queued. ExecuteCommand only parses + // what was typed and puts it in a queue, and the game's update drains + // the queue through this, the ForEachRef form included, so everything a + // console command does happens inside this call and on its thread. + static void RunQueuedCommands() + { + using func_t = decltype(&Console::RunQueuedCommands); + static REL::Relocation func{ ID::Console::RunQueuedCommands }; + return func(); + } + [[nodiscard]] static decltype(auto) GetCurrentPickIndex() { static REL::Relocation currentPickIndex{ ID::Console::GetCurrentPickIndex }; diff --git a/include/RE/E/Explosion.h b/include/RE/E/Explosion.h index 3a9cb55b..0199d1b6 100644 --- a/include/RE/E/Explosion.h +++ b/include/RE/E/Explosion.h @@ -102,6 +102,19 @@ namespace RE return func(this); } + // Deals the blast to each target FindTargets gathered that is not + // kProcessed yet, marks it so, and marks the explosion + // kTargetsProcessed. An actor is struck with a blow filled in from the + // explosion, unless the explosion is kNonHostile, and has the + // explosion's object effect cast on it. Anything else takes the blast's + // damage. + void ProcessTargets() + { + using func_t = decltype(&Explosion::ProcessTargets); + static REL::Relocation func{ ID::Explosion::ProcessTargets }; + return func(this); + } + // members hknpClosestUniqueBodyIdHitCollector collector; // 110 void* explosionDBHandle; // 520 - TODO diff --git a/include/RE/G/GamePlayFormulas.h b/include/RE/G/GamePlayFormulas.h index ba8dfb71..a6d0a918 100644 --- a/include/RE/G/GamePlayFormulas.h +++ b/include/RE/G/GamePlayFormulas.h @@ -48,6 +48,18 @@ namespace RE return func(a_itemHealth); } + // Turns an item's base value into the price the player sees. While a + // barter screen is open every price the game works out comes through + // here with the trader's markup put on, from the player's Charisma. + // a_health only counts when the fHealthDataValue6 game setting is above + // 1.0, and Fallout4.esm does not define it. + inline float CalculateItemValue(float a_baseValue, float a_health) + { + using func_t = decltype(&GamePlayFormulas::CalculateItemValue); + static REL::Relocation func{ ID::GamePlayFormulas::CalculateItemValue }; + return func(a_baseValue, a_health); + } + inline float CalcSprintingActionPoints(float a_equippedWeight, float a_elapsedTime, float a_endurance) { using func_t = decltype(&GamePlayFormulas::CalcSprintingActionPoints); diff --git a/include/RE/H/HitData.h b/include/RE/H/HitData.h index eb352819..07594409 100644 --- a/include/RE/H/HitData.h +++ b/include/RE/H/HitData.h @@ -45,6 +45,18 @@ namespace RE kExplosion = 1 << 20 }; + // Adds the damage types on a_weapon's instance data to the hit. Each + // value is multiplied by a_mult, then resisted against the target's + // matching resistance and passed through the perk entry points that + // change it. An ordinary hit passes the same range falloff here that it + // passes CombatFormulas::CalcWeaponDamage for the physical half. + void ApplyDamageTypes(const BGSObjectInstanceT& a_weapon, float a_mult) + { + using func_t = decltype(&HitData::ApplyDamageTypes); + static REL::Relocation func{ ID::HitData::ApplyDamageTypes }; + return func(this, a_weapon, a_mult); + } + // members DamageImpactData impactData; // 00 ActorHandle aggressor; // 40 diff --git a/include/RE/IDs.h b/include/RE/IDs.h index 0f3842de..3fe4d627 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -23,6 +23,7 @@ namespace RE::ID inline constexpr REL::ID GetCurrentCollisionGroup{ 2229993 }; inline constexpr REL::ID GetCurrentFireLocation{ 2231167 }; inline constexpr REL::ID GetDesiredSpeed{ 2230410 }; + inline constexpr REL::ID GetEquippedItemHealth{ 2231099 }; inline constexpr REL::ID GetGhost{ 2229668 }; inline constexpr REL::ID GetHostileToActor{ 2229968 }; inline constexpr REL::ID GetMountHandle{ 2231230 }; @@ -42,6 +43,7 @@ namespace RE::ID inline constexpr REL::ID IsQuadruped{ 2229614 }; inline constexpr REL::ID IsSneaking{ 2207655 }; inline constexpr REL::ID Move{ 2229934 }; + inline constexpr REL::ID OnMagazineEmpty{ 2231137 }; inline constexpr REL::ID PerformAction{ 2231177 }; inline constexpr REL::ID PlaySoundByEditorName{ 2230172 }; inline constexpr REL::ID RemovePerk{ 2230122 }; @@ -168,6 +170,7 @@ namespace RE::ID { inline constexpr REL::ID GetSoundDescriptor{ 2214758 }; inline constexpr REL::ID GetUIOutputModel{ 2214800 }; + inline constexpr REL::ID PlaySoundDescriptor{ 2214755 }; } namespace BGSConstructibleObject @@ -247,6 +250,7 @@ namespace RE::ID { inline constexpr REL::ID AddItem1{ 2194159 }; inline constexpr REL::ID AddItem2{ 2194160 }; + inline constexpr REL::ID AddStack{ 2194191 }; inline constexpr REL::ID BuildFromContainer{ 2194158 }; inline constexpr REL::ID Clear{ 2194162 }; inline constexpr REL::ID ctor{ 2194153 }; @@ -506,6 +510,11 @@ namespace RE::ID inline constexpr REL::ID SetMotion{ 2277723 }; } + namespace BipedAnim + { + inline constexpr REL::ID SetObjectGraphVariableInt{ 2194367 }; + } + namespace BIPOBJECT { inline constexpr REL::ID dtor{ 2194330 }; @@ -952,6 +961,7 @@ namespace RE::ID namespace Console { inline constexpr REL::ID ExecuteCommand{ 2248537 }; + inline constexpr REL::ID RunQueuedCommands{ 2248538 }; inline constexpr REL::ID GetCurrentPickIndex{ 2701382 }; inline constexpr REL::ID GetPickRef{ 2701395 }; inline constexpr REL::ID GetPickRefs{ 2701391 }; @@ -1040,6 +1050,7 @@ namespace RE::ID { inline constexpr REL::ID GetActorOwner{ 2236659 }; inline constexpr REL::ID GetDamage{ 2236658 }; + inline constexpr REL::ID ProcessTargets{ 2236664 }; } namespace ExteriorCellSingleton @@ -1154,6 +1165,11 @@ namespace RE::ID inline constexpr REL::ID Get{ 2194566 }; } + namespace HitData + { + inline constexpr REL::ID ApplyDamageTypes{ 2236861 }; + } + namespace hkContainerHeapAllocator { inline constexpr REL::ID Singleton{ 2665426 }; diff --git a/include/RE/T/TESDataHandler.h b/include/RE/T/TESDataHandler.h index ee54e9c6..5dca50b4 100644 --- a/include/RE/T/TESDataHandler.h +++ b/include/RE/T/TESDataHandler.h @@ -25,6 +25,12 @@ namespace RE public BSTSingletonSDM // 0058 { public: + // The first ID the game gives to forms it makes during play. Every data + // load resets nextID to it, and the counter returns to it when it runs + // out. IDs below it are never recycled, so the game never gives + // FF000001 to FF0007FF to a form it makes. + static constexpr TESFormID FIRST_DYNAMIC_FORM_ID{ 0xFF000800 }; + [[nodiscard]] static TESDataHandler* GetSingleton() { static REL::Relocation singleton{ ID::TESDataHandler::Singleton }; From 31e62e263d9c60977954f46cd5d7e7355f2c51ec Mon Sep 17 00:00:00 2001 From: hxef <111711970+hxef@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:48:58 +0300 Subject: [PATCH 5/5] Add menu, HUD and string bindings MessageMenuManager::QueueMessage, ID 2249457, queues a message box with any number of buttons. Create passes it at most four button labels and always sets a_modal, which is where the four button limit comes from. With a_ensureUnique set, Create also skips the box when one with the same body text is already waiting in the queue. LoadingMenu::CollectLoadScreens, ID 2249240, gathers the loading screens that may show on this load. PopulateLoadScreens calls it and then picks one of them. LoadScreenCandidates is the struct it fills. Its layout comes from what the caller writes into it and what CollectLoadScreens reads back. HUDComponentBase is the base class of every element of the gameplay HUD, such as the ammo counter. hudModes at 0xC8 lists the HUD modes the element shows in, and whether the current mode lets it be seen. The layout comes from its constructor, and its size from HUDAmmoCounter's constructor, which starts its own members at 0xF8. HUDQuickContainerDataModel::AddItemRows, ID 2221647, builds the rows the quick container shows, one InventoryItemDisplayData for each visible entry. The model calls it when it updates, then sends the rows out through its QuickContainerStateEvent source at 0x40, which its constructor sets up. WorkbenchMenuBase::CodeObjectFunction lists the numbers the workbench gives to the functions its Flash movie can call, as MapCodeObjectFunctions registers them. When the movie calls one, Call receives its number in params.userData. InventoryUserUIUtils::PopulateItemCardInfo_Helper gets a binding for its existing ID. The container, barter, workbench and cooking menus build their item cards with it. BGSLocalizedStringIL::LookupByID, ID 2194243, reads one string from a plugin's .ILSTRINGS file, in the game's current language. --- include/RE/B/BGSLocalizedString.h | 13 +++++++++ include/RE/Fallout.h | 2 ++ include/RE/H/HUDComponentBase.h | 31 ++++++++++++++++++++ include/RE/H/HUDQuickContainerDataModel.h | 28 ++++++++++++++++++ include/RE/I/InventoryUserUIUtils.h | 18 ++++++++++++ include/RE/IDs.h | 12 ++++++++ include/RE/L/LoadingMenu.h | 35 +++++++++++++++++++++++ include/RE/M/MessageMenuManager.h | 25 ++++++++++++++++ include/RE/W/WorkbenchMenuBase.h | 16 +++++++++++ 9 files changed, 180 insertions(+) create mode 100644 include/RE/H/HUDComponentBase.h create mode 100644 include/RE/H/HUDQuickContainerDataModel.h diff --git a/include/RE/B/BGSLocalizedString.h b/include/RE/B/BGSLocalizedString.h index 513cf480..7c9714d0 100644 --- a/include/RE/B/BGSLocalizedString.h +++ b/include/RE/B/BGSLocalizedString.h @@ -40,6 +40,19 @@ namespace RE }; static_assert(sizeof(BGSLocalizedStringDL) == 0x4); + class BGSLocalizedStringIL + { + public: + // Sets a_text to string a_id from a_file's .ILSTRINGS file, in the + // game's current language. An a_id of 0 gives an empty string. + static void LookupByID(BSFixedStringCS& a_text, const TESFile* a_file, std::uint32_t a_id) + { + using func_t = decltype(&BGSLocalizedStringIL::LookupByID); + static REL::Relocation func{ ID::BGSLocalizedStringIL::LookupByID }; + return func(a_text, a_file, a_id); + } + }; + class BGSLocalizedString { public: diff --git a/include/RE/Fallout.h b/include/RE/Fallout.h index b5188191..6fc8c539 100644 --- a/include/RE/Fallout.h +++ b/include/RE/Fallout.h @@ -755,6 +755,7 @@ #include "RE/G/GunStateData.h" #include "RE/H/HUDColorTypes.h" #include "RE/H/HUDCompassMarkerBase.h" +#include "RE/H/HUDComponentBase.h" #include "RE/H/HUDMarkerData.h" #include "RE/H/HUDMenu.h" #include "RE/H/HUDMenuUtils.h" @@ -766,6 +767,7 @@ #include "RE/H/HUDNotificationEvent.h" #include "RE/H/HUDPerkVaultBoyData.h" #include "RE/H/HUDPerkVaultBoySwfDisplayEvent.h" +#include "RE/H/HUDQuickContainerDataModel.h" #include "RE/H/HUDSubtitleDisplayData.h" #include "RE/H/HUDSubtitleDisplayEvent.h" #include "RE/H/Hazard.h" diff --git a/include/RE/H/HUDComponentBase.h b/include/RE/H/HUDComponentBase.h new file mode 100644 index 00000000..4284850a --- /dev/null +++ b/include/RE/H/HUDComponentBase.h @@ -0,0 +1,31 @@ +#pragma once + +#include "RE/B/BSGFxShaderFXTarget.h" +#include "RE/H/HUDModes.h" + +namespace RE +{ + // The base class of every element of the gameplay HUD, such as the ammo + // counter. HUDMenu keeps them in hudObjects. + class __declspec(novtable) HUDComponentBase : + public BSGFxShaderFXTarget // 00 + { + public: + static constexpr auto RTTI{ RTTI::HUDComponentBase }; + static constexpr auto VTABLE{ VTABLE::HUDComponentBase }; + + ~HUDComponentBase() override; // 00 + + // members + std::uint64_t unkB0; // B0 + std::uint64_t unkB8; // B8 + bool unkC0; // C0 + HUDModes hudModes; // C8 + float unkE8; // E8 + float unkEC; // EC + bool unkF0; // F0 + bool unkF1; // F1 + bool unkF2; // F2 + }; + static_assert(sizeof(HUDComponentBase) == 0xF8); +} diff --git a/include/RE/H/HUDQuickContainerDataModel.h b/include/RE/H/HUDQuickContainerDataModel.h new file mode 100644 index 00000000..4c7d6488 --- /dev/null +++ b/include/RE/H/HUDQuickContainerDataModel.h @@ -0,0 +1,28 @@ +#pragma once + +namespace RE +{ + class QuickContainerStateData; + + // Builds what the quick container shows for the container under the + // crosshair, and sends the result to the HUD as a QuickContainerStateEvent. + class __declspec(novtable) HUDQuickContainerDataModel + { + public: + static constexpr auto RTTI{ RTTI::HUDQuickContainerDataModel }; + static constexpr auto VTABLE{ VTABLE::HUDQuickContainerDataModel }; + + virtual ~HUDQuickContainerDataModel(); // 00 + + // Fills a_state's itemData with one row per visible entry, each built by + // InventoryItemDisplayData's constructor. When more entries follow, a + // "..." row goes at the end. A locked container gets a single + // $LockedContainer row instead. + void AddItemRows(QuickContainerStateData& a_state) + { + using func_t = decltype(&HUDQuickContainerDataModel::AddItemRows); + static REL::Relocation func{ ID::HUDQuickContainerDataModel::AddItemRows }; + return func(this, a_state); + } + }; +} diff --git a/include/RE/I/InventoryUserUIUtils.h b/include/RE/I/InventoryUserUIUtils.h index 96db6b99..ed5f223f 100644 --- a/include/RE/I/InventoryUserUIUtils.h +++ b/include/RE/I/InventoryUserUIUtils.h @@ -1,10 +1,14 @@ #pragma once #include "RE/B/BSFixedString.h" +#include "RE/B/BSTArray.h" +#include "RE/B/BSTTuple.h" #include "Scaleform/G/GFx_Value.h" namespace RE { + class BGSInventoryItem; + namespace InventoryUserUIUtils { namespace detail @@ -39,5 +43,19 @@ namespace RE Scaleform::GFx::Value entry; detail::AddItemCardInfoEntry(a_array, entry, a_name, a_value); } + + // Builds the item card for one stack of a_item into a_target. a_target + // is either the card's own array of entries, or a list entry that gets + // the array as its ItemCardInfoList member. The container, barter, + // workbench and cooking menus build their cards here, and the Pip-Boy + // has its own code. Weapon and armor rows are compared against the + // equipped items in a_compareItems. For armor, + // a_compareArmorWeightAndValue also compares weight and value. + inline void PopulateItemCardInfo_Helper(Scaleform::GFx::Value& a_target, const BGSInventoryItem& a_item, std::uint32_t a_stackID, const BSScrapArray>& a_compareItems, bool a_compareArmorWeightAndValue) + { + using func_t = decltype(&InventoryUserUIUtils::PopulateItemCardInfo_Helper); + static REL::Relocation func{ ID::InventoryUserUIUtils::PopulateItemCardInfo_Helper }; + return func(a_target, a_item, a_stackID, a_compareItems, a_compareArmorWeightAndValue); + } } } diff --git a/include/RE/IDs.h b/include/RE/IDs.h index 3fe4d627..bb1b9565 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -287,6 +287,11 @@ namespace RE::ID inline constexpr REL::ID GetText{ 2194238 }; } + namespace BGSLocalizedStringIL + { + inline constexpr REL::ID LookupByID{ 2194243 }; + } + namespace BGSMessage { inline constexpr REL::ID AddButton{ 0 }; // inlined @@ -1332,6 +1337,11 @@ namespace RE::ID inline constexpr REL::ID GetEventSource{ 4801988 }; } + namespace HUDQuickContainerDataModel + { + inline constexpr REL::ID AddItemRows{ 2221647 }; + } + namespace IAnimationGraphManagerHolder { inline constexpr REL::ID SetGraphVariableBool{ 2214543 }; @@ -1469,6 +1479,7 @@ namespace RE::ID { inline constexpr REL::ID StartTestingLoadMenu{ 2249224 }; inline constexpr REL::ID PopulateLoadScreens{ 2249232 }; + inline constexpr REL::ID CollectLoadScreens{ 2249240 }; } namespace LockpickingMenu @@ -1567,6 +1578,7 @@ namespace RE::ID { inline constexpr REL::ID Singleton{ 4796373 }; inline constexpr REL::ID Create{ 2249456 }; + inline constexpr REL::ID QueueMessage{ 2249457 }; } namespace Moon diff --git a/include/RE/L/LoadingMenu.h b/include/RE/L/LoadingMenu.h index a6216763..f4548aac 100644 --- a/include/RE/L/LoadingMenu.h +++ b/include/RE/L/LoadingMenu.h @@ -9,8 +9,11 @@ namespace RE { + enum class ENUM_FORM_ID; class BGSModelMaterialSwap; class NiAVObject; + class TESDataHandler; + class TESLoadScreen; class __declspec(novtable) LoadingMenu : public GameMenuBase // 00 @@ -34,6 +37,38 @@ namespace RE virtual void OnThumbstickEvent(const ThumbstickEvent*) override; // 04 virtual void OnButtonEvent(const ButtonEvent*) override; // 08 + // The struct PopulateLoadScreens passes to CollectLoadScreens to fill. + // numNonDefaultScreens and validScreens point at members of the menu, + // and artCandidates at a list on the caller's stack. + class LoadScreenCandidates + { + public: + // members + LoadingMenu* menu; // 00 + std::uint32_t* numNonDefaultScreens; // 08 + BSScrapArray* artCandidates; // 10 + BSTArray* validScreens; // 18 + std::uint32_t numNonDefaultArtCandidates; // 20 + }; + static_assert(sizeof(LoadScreenCandidates) == 0x28); + + // Adds every form of a_formType that may show on this load to + // a_candidates. PopulateLoadScreens passes kLSCR. A screen qualifies + // when its conditions are true for the player. On the way out to the + // main menu, it qualifies when it is flagged to show there instead. + // + // Each screen that qualifies goes on validScreens, and also on + // artCandidates when it has a model. Screens with conditions are also + // counted in numNonDefaultScreens, and in numNonDefaultArtCandidates + // when they have a model. The game sorts those ahead of the rest and + // picks from them first. + static void CollectLoadScreens(TESDataHandler* a_dataHandler, ENUM_FORM_ID a_formType, LoadScreenCandidates* a_candidates) + { + using func_t = decltype(&LoadingMenu::CollectLoadScreens); + static REL::Relocation func{ ID::LoadingMenu::CollectLoadScreens }; + return func(a_dataHandler, a_formType, a_candidates); + } + static void StartTestingLoadMenu() { using func_t = decltype(&LoadingMenu::StartTestingLoadMenu); diff --git a/include/RE/M/MessageMenuManager.h b/include/RE/M/MessageMenuManager.h index 8084881f..5932273e 100644 --- a/include/RE/M/MessageMenuManager.h +++ b/include/RE/M/MessageMenuManager.h @@ -34,6 +34,31 @@ namespace RE return func(this, a_headerText, a_bodyText, a_callback, a_warningContext, a_button1Text, a_button2Text, a_button3Text, a_button4Text, a_ensureUnique, a_cancelButtonIndex); } + // Queues a message box with one button for each label in a_buttonText, + // with no limit on the count. The callback receives the index of the + // pressed button. a_modal sets the box's kModal menu flag. + // a_ensureUnique skips the box when the box shown just before it had + // the same body text. a_cancelButtonIndex is the button the Cancel key + // presses, or -1 for none. + // + // Create calls this with at most four labels and a_modal set. With its + // own a_ensureUnique set, Create also skips the box when one with the + // same body text is already waiting in the queue. + void QueueMessage( + const char* a_headerText, + const char* a_bodyText, + IMessageBoxCallback* a_callback, + WARNING_TYPES a_warningContext, + const BSScrapArray* a_buttonText, + bool a_modal = true, + bool a_ensureUnique = false, + std::int32_t a_cancelButtonIndex = -1) + { + using func_t = decltype(&MessageMenuManager::QueueMessage); + static REL::Relocation func{ ID::MessageMenuManager::QueueMessage }; + return func(this, a_headerText, a_bodyText, a_callback, a_warningContext, a_buttonText, a_modal, a_ensureUnique, a_cancelButtonIndex); + } + // members BSReadWriteLock messageRWLock; // 04 BSTArray messages; // 10 diff --git a/include/RE/W/WorkbenchMenuBase.h b/include/RE/W/WorkbenchMenuBase.h index 09b19d50..f352dc42 100644 --- a/include/RE/W/WorkbenchMenuBase.h +++ b/include/RE/W/WorkbenchMenuBase.h @@ -23,6 +23,22 @@ namespace RE static constexpr auto RTTI{ RTTI::WorkbenchMenuBase }; static constexpr auto VTABLE{ VTABLE::WorkbenchMenuBase }; + // The numbers the menu gives to the functions its Flash movie can call. + // When the movie calls one, Call receives its number in params.userData. + // ExamineMenu adds its own from 9 on. + enum class CodeObjectFunction : std::uint32_t + { + kRegisterComponents = 0, + kPlaySound = 1, + kStartAnimation = 2, + kConfirmBuild = 3, + kStartBuildConfirm = 4, + kCancelConfirm = 5, + kHideMenu = 6, + kSendTutorialEvent = 7, + kOnAlternateButton = 8 + }; + enum class HighlightMode { kModMenu,