From e872e426203b6e656409c1e82e9daa660fbc0973 Mon Sep 17 00:00:00 2001 From: Igor Alan Albuquerque de Sousa <50077829+IgorAlanAlbuquerque@users.noreply.github.com> Date: Sat, 21 Feb 2026 02:20:43 -0300 Subject: [PATCH 01/11] Fix/mod doesn't work for spell (#1) The previous way of hooking the OnEquipItem to pass the play animation argument as false had the problem that the animation graph was not triggered. Therefore, the internal state of the animation graph's state machine became inconsistent. It did not accept attack inputs when switching from weapon to magic or magic to weapon. Here, the solution applied was to allow the animation graph to execute normally and hook the play animation within it to speed up the animation to a point of being imperceptible to the eye. In this way, all events such as "weapEquip_Out" are sent by the graph, removing the need to send these events to the actor's process event. --- cmake/headerlist.cmake | 1 - cmake/sourcelist.cmake | 1 - src/PCH.h | 1 - src/event.cpp | 35 -- src/event.h | 18 - src/hook.cpp | 141 ++++--- src/hook.h | 26 +- src/plugin.cpp | 13 +- src/util.h | 940 ++++++++++++++++++++++++----------------- 9 files changed, 656 insertions(+), 520 deletions(-) delete mode 100644 src/event.cpp delete mode 100644 src/event.h diff --git a/cmake/headerlist.cmake b/cmake/headerlist.cmake index 58fcb1e..0aba2be 100644 --- a/cmake/headerlist.cmake +++ b/cmake/headerlist.cmake @@ -2,6 +2,5 @@ set(headers ${headers} src/PCH.h src/log.h src/util.h - src/event.h src/hook.h ) \ No newline at end of file diff --git a/cmake/sourcelist.cmake b/cmake/sourcelist.cmake index dd11b4a..06fc6d5 100644 --- a/cmake/sourcelist.cmake +++ b/cmake/sourcelist.cmake @@ -1,5 +1,4 @@ set(sources ${sources} src/plugin.cpp - src/event.cpp src/hook.cpp ) \ No newline at end of file diff --git a/src/PCH.h b/src/PCH.h index de71a02..9d105b6 100644 --- a/src/PCH.h +++ b/src/PCH.h @@ -6,6 +6,5 @@ #define RELOCATION_OFFSET(SE, AE) REL::VariantOffset(SE, AE, 0).offset() #define DEBUG_MODE -//#define WORK_WITH_MAGIC_OBJECTS using namespace std::literals; diff --git a/src/event.cpp b/src/event.cpp deleted file mode 100644 index f06e377..0000000 --- a/src/event.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "event.h" -/**/ -bool AnimationEventTracker::Register() -{ - bool foundEventSource = eventSource != nullptr; - if (!foundEventSource) { - const auto pc = RE::PlayerCharacter::GetSingleton(); - - RE::BSAnimationGraphManagerPtr graphManager; - if (pc) pc->GetAnimationGraphManager(graphManager); - else spdlog::debug("Can't found Player Character"); - - if (graphManager) { - for (auto& animationGraph : graphManager->graphs) { - eventSource = animationGraph->GetEventSource(); - if (eventSource) { - spdlog::info("Registered {}", typeid(RE::BSAnimationGraphEvent).name()); - return true; - } - } - } - - if (!eventSource) { - spdlog::info("Failed to register {}", typeid(RE::BSAnimationGraphEvent).name()); - } - } return foundEventSource; -} - -void AnimationEventTracker::SendAnimationEvent(RE::Actor* a_this, const RE::BSFixedString a_tag, const RE::BSFixedString a_payload) -{ - if (a_this && eventSource) { - RE::BSAnimationGraphEvent event = {a_tag, a_this, a_payload}; - a_this->ProcessEvent(&event, eventSource); - } -} \ No newline at end of file diff --git a/src/event.h b/src/event.h deleted file mode 100644 index f898079..0000000 --- a/src/event.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -using EventChecker = RE::BSEventNotifyControl; -class AnimationEventTracker : public RE::BSTEventSink -{ -public: - static AnimationEventTracker* GetSingleton() {static AnimationEventTracker singleton; return &singleton;} - - bool Register(); - - virtual EventChecker ProcessEvent(const RE::BSAnimationGraphEvent* a_event, RE::BSTEventSource* a_eventSource) override - { - return EventChecker::kContinue; - }; - void SendAnimationEvent(RE::Actor* a_this, const RE::BSFixedString a_tag, const RE::BSFixedString a_payload = ""); - - RE::BSTEventSource* eventSource; -}; \ No newline at end of file diff --git a/src/hook.cpp b/src/hook.cpp index 3004824..ce8c1a5 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -1,70 +1,113 @@ #include "hook.h" -#include "event.h" #include "util.h" + +#include +#include +#include +#include + +using namespace std::literals; using namespace Util; -void EquipHook::OnEquipItemPC(RE::PlayerCharacter* a_this, bool a_playAnim) +namespace +{ + static inline std::atomic g_skipUntilMs{0}; + static inline std::atomic g_suppressForceEquipClips{0}; + + inline std::int64_t NowMs() + { + using clock = std::chrono::steady_clock; + return std::chrono::duration_cast(clock::now().time_since_epoch()).count(); + } + + inline bool InSkipWindow() + { + return NowMs() <= g_skipUntilMs.load(std::memory_order_relaxed); + } + + static bool IsEquipClip(std::string_view nm) + { + return Util::String::iContains(nm, "Equip"); + } +} + +void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) { - _OnEquipItemPC(a_this, !SkipAnim(a_this, a_playAnim)); + SkipAnim(a_this, a_playAnim); + _OnEquipItemPC(a_this, a_playAnim); } + /* void EquipHook::OnEquipItemNPC(RE::Actor* a_this, bool a_playAnim) { _OnEquipItemNPC(a_this, !SkipAnim(a_this, a_playAnim)); } */ -bool CheckIsValidBoundObject(const RE::TESForm* a_object) -{ -#ifdef WORK_WITH_MAGIC_OBJECTS - if ((a_object && a_object->IsMagicItem())) - return = true; -#endif - return {// && (uint32_t)a_object->As()->GetWeaponType() <= 6u) - a_object && (a_object->As() || a_object->As()) - }; -} -void SendEquipEvents(RE::Actor* a_this, RE::TESForm* a_lHandObject, RE::TESForm* a_rHandObject) + +bool CheckIsValidBoundObject(const RE::TESForm *a_object) { - if (auto animationEventTracker = AnimationEventTracker::GetSingleton()) { - animationEventTracker->SendAnimationEvent(a_this, "weaponDraw"); -#ifdef WORK_WITH_MAGIC_OBJECTS - if ((a_lHandObject && a_lHandObject->IsMagicItem()) || (a_rHandObject && a_rHandObject->IsMagicItem())) - animationEventTracker->SendAnimationEvent(a_this, "MagicEquip_Out"); -#endif - - if ((a_lHandObject && (a_lHandObject->IsWeapon() || a_lHandObject->IsArmor())) - || (a_rHandObject && (a_rHandObject->IsWeapon() || a_rHandObject->IsArmor()))) { - animationEventTracker->SendAnimationEvent(a_this, "WeapEquip_Out"); - animationEventTracker->SendAnimationEvent(a_this, "WeapEquip_OutMoving"); - } - } + if (!a_object) + return false; + if (a_object->IsMagicItem()) + return true; + return a_object->As() != nullptr || a_object->As() != nullptr; } -bool EquipHook::SkipAnim(RE::PlayerCharacter* a_this, bool a_playAnim) + +bool EquipHook::SkipAnim(RE::PlayerCharacter *a_this, bool a_playAnim) { - _skipAnim = !a_playAnim; - if (AnimationEventTracker::GetSingleton()->Register() && a_this && a_this->AsActorState() && a_this->AsActorState()->IsWeaponDrawn()) { + bool skipAnim = !a_playAnim; + if (a_this && a_this->AsActorState() && a_this->AsActorState()->IsWeaponDrawn()) + { auto rHandObj = a_this->GetEquippedObject(false); auto lHandObj = a_this->GetEquippedObject(true); - if (!(lHandObj && rHandObj) || CheckIsValidBoundObject(lHandObj) || CheckIsValidBoundObject(rHandObj)) { - int delay = 300; - bool skip3D = false; - a_this->GetGraphVariableBool("SkipEquipAnimation", _skipAnim); - a_this->GetGraphVariableInt("LoadBoundObjectDelay", delay); - a_this->GetGraphVariableBool("Skip3DLoading", skip3D); - if (delay < (int)(*g_deltaTimeRealTime * 1000.f)) delay = (int)(*g_deltaTimeRealTime * 1000.f); // the loading process will start next frame. - if (!skip3D && _skipAnim) { - std::jthread delayedEquipThread([=]() { - if (_skipAnim) { - spdlog::debug("equip anim skipped"); - std::this_thread::sleep_for(std::chrono::milliseconds(delay)); - SendEquipEvents(a_this, lHandObj, rHandObj); - spdlog::debug("weapon 3d model called"); - } - } - ); - delayedEquipThread.detach(); + if (!(lHandObj && rHandObj) || CheckIsValidBoundObject(lHandObj) || CheckIsValidBoundObject(rHandObj)) + { + a_this->GetGraphVariableBool("SkipEquipAnimation", skipAnim); + if (skipAnim) + { + g_skipUntilMs.store(NowMs() + 500, std::memory_order_relaxed); + g_suppressForceEquipClips.store(1, std::memory_order_relaxed); } } - } return _skipAnim; + } + _skipAnim = skipAnim; + return skipAnim; +} + +void EquipHook::Update_Hook(RE::hkbClipGenerator* a_this, const RE::hkbContext& a_context, float a_timestep) +{ + if (!a_this) { + _Update(a_this, a_context, a_timestep); + return; + } + + std::string_view nm{ a_this->animationName.c_str() }; + if (IsEquipClip(nm) && a_this->atEnd) return; + + _Update(a_this, a_context, a_timestep); +} + +void EquipHook::Activate_Hook(RE::hkbClipGenerator* a_this, const RE::hkbContext& a_context) +{ + _Activate(a_this, a_context); + + if (!a_this) return; + + const int sup = g_suppressForceEquipClips.load(std::memory_order_relaxed); + if (sup <= 0) return; + + std::string_view nm{ a_this->animationName.c_str() }; + if (!IsEquipClip(nm)) return; + + g_suppressForceEquipClips.store(0, std::memory_order_relaxed); + + float duration = 2.0f; + if (a_this->binding && a_this->binding->animation) { + duration = a_this->binding->animation->duration; + } + + a_this->mode = RE::hkbClipGenerator::PlaybackMode::kModeSinglePlay; + _Update(a_this, a_context, duration + 0.01f); + a_this->atEnd = true; } \ No newline at end of file diff --git a/src/hook.h b/src/hook.h index 4546fed..a1c2481 100644 --- a/src/hook.h +++ b/src/hook.h @@ -7,20 +7,28 @@ class EquipHook public: static void Hook() { - REL::Relocation PlayerCharacterVtbl{ RE::VTABLE_PlayerCharacter[0] }; - // REL::Relocation NonPlayerCharacterVtbl{ RE::VTABLE_Actor[0] }; + REL::Relocation PlayerCharacterVtbl{RE::VTABLE_PlayerCharacter[0]}; + //REL::Relocation NonPlayerCharacterVtbl{ RE::VTABLE_Actor[0] }; + REL::Relocation vtbl{RE::VTABLE_hkbClipGenerator[0]}; - _OnEquipItemPC = PlayerCharacterVtbl.write_vfunc(0xB2, OnEquipItemPC); - // _OnEquipItemNPC = NonPlayerCharacterVtbl.write_vfunc(0xB2, OnEquipItemNPC); // maybe later + _OnEquipItemPC = PlayerCharacterVtbl.write_vfunc(0xB2, OnEquipItemPC); + //_OnEquipItemNPC = NonPlayerCharacterVtbl.write_vfunc(0xB2, OnEquipItemNPC); + _Update = vtbl.write_vfunc(0x05, Update_Hook); + _Activate = vtbl.write_vfunc(0x04, Activate_Hook); } + private: - static void OnEquipItemPC(RE::PlayerCharacter* a_this, bool a_playAnim); -// static void OnEquipItemNPC(RE::Actor* a_this, bool a_playAnim); + static void OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim); + //static void OnEquipItemNPC(RE::Actor* a_this, bool a_playAnim); + static void Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context, float a_timestep); + static void Activate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); - static bool SkipAnim(RE::PlayerCharacter* a_this, bool a_playAnim); + static inline REL::Relocation _OnEquipItemPC; + //static inline REL::Relocation _OnEquipItemNPC; + static inline REL::Relocation _Update; + static inline REL::Relocation _Activate; - static inline REL::Relocation _OnEquipItemPC; -// static inline REL::Relocation _OnEquipItemNPC; + static bool SkipAnim(RE::PlayerCharacter *a_this, bool a_playAnim); static inline bool _skipAnim; }; \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index 9526a78..d548917 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -1,21 +1,10 @@ #include "log.h" #include "hook.h" -#include "event.h" void MessageHandler(SKSE::MessagingInterface::Message* a_msg) { - switch (a_msg->type) { - case SKSE::MessagingInterface::kDataLoaded: + if(a_msg->type == SKSE::MessagingInterface::kDataLoaded) { EquipHook::Hook(); - break; - case SKSE::MessagingInterface::kPostLoad: - break; - case SKSE::MessagingInterface::kPreLoadGame: - case SKSE::MessagingInterface::kPostLoadGame: - case SKSE::MessagingInterface::kNewGame: - if (auto animationEventTracker = AnimationEventTracker::GetSingleton(); animationEventTracker) - animationEventTracker->Register(); - break; } } SKSEPluginLoad(const SKSE::LoadInterface *skse) { diff --git a/src/util.h b/src/util.h index 4c149f9..7cd9010 100644 --- a/src/util.h +++ b/src/util.h @@ -9,51 +9,62 @@ #define PI8 0.3926990816987242f #define downVec {0.f, 0.f, -1.f} -#define upVec {0.f, 0.f, 1.f} +#define upVec {0.f, 0.f, 1.f} -static float* g_deltaTime = (float*)RELOCATION_ID(523660, 410199).address(); // sensitive to slow time spell -static float* g_deltaTimeRealTime = (float*)RELOCATION_ID(523661, 410200).address(); // const +static float *g_deltaTime = (float *)RELOCATION_ID(523660, 410199).address(); // sensitive to slow time spell +static float *g_deltaTimeRealTime = (float *)RELOCATION_ID(523661, 410200).address(); // const using namespace RE; -namespace PointerUtil //yoinked po3's code +namespace PointerUtil // yoinked po3's code { -template -inline auto adjust_pointer(U* a_ptr, std::ptrdiff_t a_adjust) noexcept -{ - auto addr = a_ptr ? reinterpret_cast(a_ptr) + a_adjust : 0; - if constexpr (std::is_const_v && std::is_volatile_v) { - return reinterpret_cast*>(addr); - } else if constexpr (std::is_const_v) { - return reinterpret_cast*>(addr); - } else if constexpr (std::is_volatile_v) { - return reinterpret_cast*>(addr); - } else { - return reinterpret_cast(addr); + template + inline auto adjust_pointer(U *a_ptr, std::ptrdiff_t a_adjust) noexcept + { + auto addr = a_ptr ? reinterpret_cast(a_ptr) + a_adjust : 0; + if constexpr (std::is_const_v && std::is_volatile_v) + { + return reinterpret_cast *>(addr); + } + else if constexpr (std::is_const_v) + { + return reinterpret_cast *>(addr); + } + else if constexpr (std::is_volatile_v) + { + return reinterpret_cast *>(addr); + } + else + { + return reinterpret_cast(addr); + } } } -} namespace SystemUtil { struct Engine { static float GetEngineTime() { - REL::Relocation g_engineTime{ RELOCATION_ID(517597, 404125) }; // credits to https://github.com/jarari + REL::Relocation g_engineTime{RELOCATION_ID(517597, 404125)}; // credits to https://github.com/jarari return *g_engineTime; } }; - struct File + struct File { static std::vector GetConfigs(std::string_view a_folder, std::string_view a_suffix, std::string_view a_extension = ".ini"sv) { std::vector configs{}; - for (const auto iterator = std::filesystem::directory_iterator(a_folder); const auto& entry : iterator) { - if (entry.exists()) { - if (const auto& path = entry.path(); !path.empty() && path.extension() == a_extension) { - if (const auto& fileName = entry.path().string(); fileName.rfind(a_suffix) != std::string::npos) { + for (const auto iterator = std::filesystem::directory_iterator(a_folder); const auto &entry : iterator) + { + if (entry.exists()) + { + if (const auto &path = entry.path(); !path.empty() && path.extension() == a_extension) + { + if (const auto &fileName = entry.path().string(); fileName.rfind(a_suffix) != std::string::npos) + { configs.push_back(fileName); } } @@ -67,31 +78,34 @@ namespace SystemUtil }; } -namespace KeyUtil +namespace KeyUtil { - enum class MACRO_LIMITS { + enum class MACRO_LIMITS + { kMaxMacros = 282 }; - enum class KBM_OFFSETS { + enum class KBM_OFFSETS + { // first 256 for keyboard, then 8 mouse buttons, then mouse wheel up, wheel down, then 16 gamepad buttons - kMacro_KeyboardOffset = 0, // not actually used, just for self-documentation + kMacro_KeyboardOffset = 0, // not actually used, just for self-documentation kMacro_NumKeyboardKeys = 256, - kMacro_MouseButtonOffset = kMacro_NumKeyboardKeys, // 256 + kMacro_MouseButtonOffset = kMacro_NumKeyboardKeys, // 256 kMacro_NumMouseButtons = 8, - kMacro_MouseWheelOffset = kMacro_MouseButtonOffset + kMacro_NumMouseButtons, // 264 + kMacro_MouseWheelOffset = kMacro_MouseButtonOffset + kMacro_NumMouseButtons, // 264 kMacro_MouseWheelDirections = 2, - kMacro_GamepadOffset = kMacro_MouseWheelOffset + kMacro_MouseWheelDirections, // 266 + kMacro_GamepadOffset = kMacro_MouseWheelOffset + kMacro_MouseWheelDirections, // 266 kMacro_NumGamepadButtons = 16, - // 282 + // 282 }; - enum class GAMEPAD_OFFSETS { + enum class GAMEPAD_OFFSETS + { kGamepadButtonOffset_DPAD_UP = static_cast(KBM_OFFSETS::kMacro_GamepadOffset), // 266 kGamepadButtonOffset_DPAD_DOWN, kGamepadButtonOffset_DPAD_LEFT, @@ -110,32 +124,49 @@ namespace KeyUtil kGamepadButtonOffset_RT // 281 }; - struct Interpreter { - public: - static uint32_t GamepadMaskToKeycode(uint32_t keyMask) { - switch (keyMask) { - case 0x001: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_UP); - case 0x002: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_DOWN); - case 0x004: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_LEFT); - case 0x008: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_RIGHT); - case 0x0010: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_START); - case 0x0020: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_BACK); - case 0x0040: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_LEFT_THUMB); - case 0x0080: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_RIGHT_THUMB); - case 0x0100: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_LEFT_SHOULDER); - case 0x0200: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_RIGHT_SHOULDER); - case 0x1000: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_A); - case 0x2000: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_B); - case 0x4000: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_X); - case 0x8000: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_Y); - case 0x9: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_LT); - case 0xA: return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_RT); - default: return 282; // Invalid - } -} - + public: + static uint32_t GamepadMaskToKeycode(uint32_t keyMask) + { + switch (keyMask) + { + case 0x001: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_UP); + case 0x002: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_DOWN); + case 0x004: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_LEFT); + case 0x008: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_DPAD_RIGHT); + case 0x0010: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_START); + case 0x0020: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_BACK); + case 0x0040: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_LEFT_THUMB); + case 0x0080: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_RIGHT_THUMB); + case 0x0100: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_LEFT_SHOULDER); + case 0x0200: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_RIGHT_SHOULDER); + case 0x1000: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_A); + case 0x2000: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_B); + case 0x4000: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_X); + case 0x8000: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_Y); + case 0x9: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_LT); + case 0xA: + return static_cast(GAMEPAD_OFFSETS::kGamepadButtonOffset_RT); + default: + return 282; // Invalid + } + } }; } @@ -144,47 +175,55 @@ namespace Util { struct String { - static std::vector Split(const std::string& a_str, std::string_view a_delimiter) + static std::vector Split(const std::string &a_str, std::string_view a_delimiter) { - auto range = a_str | std::ranges::views::split(a_delimiter) | std::ranges::views::transform([](auto&& r) { return std::string_view(r); }); - return { range.begin(), range.end() }; - } + std::vector result; + size_t start = 0; + size_t end = 0; + while ((end = a_str.find(a_delimiter, start)) != std::string::npos) + { + result.emplace_back(a_str.substr(start, end - start)); + start = end + a_delimiter.size(); + } + result.emplace_back(a_str.substr(start)); + return result; + } static bool iContains(std::string_view a_str1, std::string_view a_str2) { - if (a_str2.length() > a_str1.length()) { + if (a_str2.length() > a_str1.length()) + { return false; } - const auto subrange = std::ranges::search(a_str1, a_str2, [](unsigned char ch1, unsigned char ch2) { - return std::toupper(ch1) == std::toupper(ch2); - }); + const auto subrange = std::ranges::search(a_str1, a_str2, [](unsigned char ch1, unsigned char ch2) + { return std::toupper(ch1) == std::toupper(ch2); }); return !subrange.empty(); } static bool iEquals(std::string_view a_str1, std::string_view a_str2) { - return std::ranges::equal(a_str1, a_str2, [](unsigned char ch1, unsigned char ch2) { - return std::toupper(ch1) == std::toupper(ch2); - }); + return std::ranges::equal(a_str1, a_str2, [](unsigned char ch1, unsigned char ch2) + { return std::toupper(ch1) == std::toupper(ch2); }); } // https://stackoverflow.com/a/35452044 - static std::string Join(const std::vector& a_vec, std::string_view a_delimiter) + static std::string Join(const std::vector &a_vec, std::string_view a_delimiter) { return std::accumulate(a_vec.begin(), a_vec.end(), std::string{}, - [a_delimiter](const auto& str1, const auto& str2) { - return str1.empty() ? str2 : str1 + a_delimiter.data() + str2; - }); + [a_delimiter](const auto &str1, const auto &str2) + { + return str1.empty() ? str2 : str1 + a_delimiter.data() + str2; + }); } static std::vector ToFloatVector(const std::vector stringVector) { - std::vector floatNumbers; - for(auto str : stringVector) + std::vector floatNumbers; + for (auto str : stringVector) { float num = atof(str.c_str()); floatNumbers.push_back(num); @@ -194,30 +233,33 @@ namespace Util static std::string ToLower(std::string_view a_str) { std::string result(a_str); - std::ranges::transform(result, result.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + std::ranges::transform(result, result.begin(), [](unsigned char ch) + { return static_cast(std::tolower(ch)); }); return result; } static std::string ToUpper(std::string_view a_str) { std::string result(a_str); - std::ranges::transform(result, result.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); + std::ranges::transform(result, result.begin(), [](unsigned char ch) + { return static_cast(std::toupper(ch)); }); return result; } }; - - constexpr uint32_t hash(const char* data, size_t const size) noexcept + + constexpr uint32_t hash(const char *data, size_t const size) noexcept { uint32_t hash = 5381; - for (const char* c = data; c < data + size; ++c) { + for (const char *c = data; c < data + size; ++c) + { hash = ((hash << 5) + hash) + (unsigned char)*c; } return hash; } - constexpr uint32_t operator""_h(const char* str, size_t size) noexcept + constexpr uint32_t operator""_h(const char *str, size_t size) noexcept { return hash(str, size); } @@ -226,7 +268,7 @@ namespace Util namespace MathUtil { - struct Angle + struct Angle { [[nodiscard]] constexpr static float DegreeToRadian(float a_angle) { @@ -240,12 +282,12 @@ namespace MathUtil static NiPoint3 ToRadianVector(float x, float y, float z) { - RE::NiPoint3 rotationVector{ 0.f, 0.f, 0.f }; + RE::NiPoint3 rotationVector{0.f, 0.f, 0.f}; - rotationVector.x = DegreeToRadian(x); - rotationVector.y = DegreeToRadian(y); - rotationVector.z = DegreeToRadian(z); - return rotationVector; + rotationVector.x = DegreeToRadian(x); + rotationVector.y = DegreeToRadian(y); + rotationVector.z = DegreeToRadian(z); + return rotationVector; } static float NormalAbsoluteAngle(float a_angle) @@ -269,16 +311,18 @@ namespace MathUtil // return fmod(a_angle, TWO_PI) >= 0 ? (a_angle < PI) ? a_angle : a_angle - TWO_PI : (a_angle >= -PI) ? a_angle : a_angle + TWO_PI; } - }; + }; struct Algebra { - [[nodiscard]] static inline float generateRandomFloat(const float lower, const float upper) { + [[nodiscard]] static inline float generateRandomFloat(const float lower, const float upper) + { static std::default_random_engine generator; static std::uniform_real_distribution distribution(lower, upper); - return distribution(generator); + return distribution(generator); } - [[nodiscard]] static inline void SetRotationMatrix(RE::NiMatrix3& a_matrix, const float sacb, const float cacb, const float sb) { + [[nodiscard]] static inline void SetRotationMatrix(RE::NiMatrix3 &a_matrix, const float sacb, const float cacb, const float sb) + { const float cb = std::sqrtf(1 - sb * sb); const float ca = cacb / cb; const float sa = sacb / cb; @@ -292,39 +336,48 @@ namespace MathUtil a_matrix.entry[1][2] = -ca * sb; a_matrix.entry[2][2] = cb; } - [[nodiscard]] static void RotateMatrixAroundAxis(NiMatrix3& a_matrix, const float angleRad, const char* axis) { + [[nodiscard]] static void RotateMatrixAroundAxis(NiMatrix3 &a_matrix, const float angleRad, const char *axis) + { const float cosA = std::cos(angleRad); const float sinA = std::sin(angleRad); - if (std::strcmp(axis, "x") == 0) { + if (std::strcmp(axis, "x") == 0) + { a_matrix.entry[0][0] = 1.0; a_matrix.entry[1][1] = cosA; a_matrix.entry[1][2] = -sinA; a_matrix.entry[2][1] = sinA; a_matrix.entry[2][2] = cosA; - } else if (std::strcmp(axis, "y") == 0) { + } + else if (std::strcmp(axis, "y") == 0) + { a_matrix.entry[0][0] = cosA; a_matrix.entry[0][2] = sinA; a_matrix.entry[1][1] = 1.0; a_matrix.entry[2][0] = -sinA; a_matrix.entry[2][2] = cosA; - } else if (std::strcmp(axis, "z") == 0) { + } + else if (std::strcmp(axis, "z") == 0) + { a_matrix.entry[0][0] = cosA; a_matrix.entry[0][1] = -sinA; a_matrix.entry[1][0] = sinA; a_matrix.entry[1][1] = cosA; a_matrix.entry[2][2] = 1.0; - } else { + } + else + { return; } } - [[nodiscard]] static void RotateMatrixAroundAxisses(NiMatrix3& a_matrix, float angleRadX, float angleRadY, float angleRadZ) { + [[nodiscard]] static void RotateMatrixAroundAxisses(NiMatrix3 &a_matrix, float angleRadX, float angleRadY, float angleRadZ) + { float cosX = std::cos(angleRadX); float sinX = std::sin(angleRadX); float cosY = std::cos(angleRadY); float sinY = std::sin(angleRadY); float cosZ = std::cos(angleRadZ); float sinZ = std::sin(angleRadZ); - // X ekseninde döndürme matrisi + // X ekseninde döndürme matrisi NiMatrix3 rotationMatrixX; rotationMatrixX.entry[0][0] = 1.0; rotationMatrixX.entry[1][1] = cosX; @@ -351,87 +404,100 @@ namespace MathUtil // Orijinal matris ile çarpma a_matrix = a_matrix * rotationMatrixX * rotationMatrixY * rotationMatrixZ; } - // [[nodiscard]] static NiMatrix3 SetRotationMatrixByVector(float Ax, float Ay, float Az, float Bx, float By, float Bz) { - // const float cosAxBx = std::cos(Bx); - // const float cosAxBy = std::cos(angleRadX); - // const float cosAxBz = std::cos(angleRadY); - // const float cosAyBx = std::cos(angleRadY); - // const float cosAyBy = std::cos(By); - // const float cosAyBz = std::cos(angleRadZ); - // const float cosAzBx = std::cos(angleRadY); - // const float cosAzBy = std::cos(angleRadZ); - // const float cosAzBz = std::cos(Bz); -// - // NiMatrix3 matrix; - // // X axis - // matrix.entry[0][0] = 1.0; - // matrix.entry[1][0] = cosX; - // matrix.entry[2][0] = -sinX; -// - // // Y axis - // matrix.entry[0][1] = cosY; - // matrix.entry[1][1] = sinY; - // matrix.entry[2][1] = 1.0; -// - // // Z axis - // matrix.entry[0][2] = sinZ; - // matrix.entry[1][2] = cosZ; - // matrix.entry[2][2] = 1.0; -// - // return matrix; - // } + // [[nodiscard]] static NiMatrix3 SetRotationMatrixByVector(float Ax, float Ay, float Az, float Bx, float By, float Bz) { + // const float cosAxBx = std::cos(Bx); + // const float cosAxBy = std::cos(angleRadX); + // const float cosAxBz = std::cos(angleRadY); + // const float cosAyBx = std::cos(angleRadY); + // const float cosAyBy = std::cos(By); + // const float cosAyBz = std::cos(angleRadZ); + // const float cosAzBx = std::cos(angleRadY); + // const float cosAzBy = std::cos(angleRadZ); + // const float cosAzBz = std::cos(Bz); + // + // NiMatrix3 matrix; + // // X axis + // matrix.entry[0][0] = 1.0; + // matrix.entry[1][0] = cosX; + // matrix.entry[2][0] = -sinX; + // + // // Y axis + // matrix.entry[0][1] = cosY; + // matrix.entry[1][1] = sinY; + // matrix.entry[2][1] = 1.0; + // + // // Z axis + // matrix.entry[0][2] = sinZ; + // matrix.entry[1][2] = cosZ; + // matrix.entry[2][2] = 1.0; + // + // return matrix; + // } }; } namespace ObjectUtil { struct Projectile { - static bool DeleteAnExtraArrow(RE::Actor* a_victim, RE::NiAVObject* a_arrow3D) - { - if (a_victim && a_arrow3D) { - // auto attachedArrows = static_cast(a_victim->extraList.GetByType(RE::ExtraDataType::kAttachedArrows3D)); - // for (auto& extraArrow : attachedArrows->data) { - // if (extraArrow.arrow3D.get() == a_arrow3D) {extraArrow.timeStamp = (uint64_t)0; return true;} - // } - if (auto* xList = &a_victim->extraList; xList) + static bool DeleteAnExtraArrow(RE::Actor *a_victim, RE::NiAVObject *a_arrow3D) + { + if (a_victim && a_arrow3D) + { + // auto attachedArrows = static_cast(a_victim->extraList.GetByType(RE::ExtraDataType::kAttachedArrows3D)); + // for (auto& extraArrow : attachedArrows->data) { + // if (extraArrow.arrow3D.get() == a_arrow3D) {extraArrow.timeStamp = (uint64_t)0; return true;} + // } + if (auto *xList = &a_victim->extraList; xList) if (auto xArrows = xList->GetByType(); xArrows) if (!xArrows->data.empty()) - for (auto& extraArrow : xArrows->data) { - if (extraArrow.arrow3D.get() && extraArrow.arrow3D.get()->name == a_arrow3D->name) {extraArrow.timeStamp = 0; return true;} + for (auto &extraArrow : xArrows->data) + { + if (extraArrow.arrow3D.get() && extraArrow.arrow3D.get()->name == a_arrow3D->name) + { + extraArrow.timeStamp = 0; + return true; + } } - } return false; + } + return false; } }; struct Actor { /* - * Requires my modder utility for this feature: https://github.com/PhiloSocio/SkipEquipAnimation - */ - static void SkipEquipAnimationDuring(int a_durationMS, RE::Actor* a_this, int a_load3dDleayMS = 0) + * Requires my modder utility for this feature: https://github.com/PhiloSocio/SkipEquipAnimation + */ + static void SkipEquipAnimationDuring(int a_durationMS, RE::Actor *a_this, int a_load3dDleayMS = 0) { - if (a_this) { + if (a_this) + { a_this->SetGraphVariableBool("SkipEquipAnimation", true); a_this->SetGraphVariableBool("LoadBoundObjectDelay", a_load3dDleayMS); - if (a_durationMS < (int)(*g_deltaTimeRealTime * 2.f)) a_durationMS = (int)(*g_deltaTimeRealTime * 2.f); - std::jthread DisableEquipAnim([=]() { + if (a_durationMS < (int)(*g_deltaTimeRealTime * 2.f)) + a_durationMS = (int)(*g_deltaTimeRealTime * 2.f); + std::jthread DisableEquipAnim([=]() + { std::this_thread::sleep_for(std::chrono::milliseconds(a_durationMS)); - a_this->SetGraphVariableBool("SkipEquipAnimation", false); - }); + a_this->SetGraphVariableBool("SkipEquipAnimation", false); }); DisableEquipAnim.detach(); } } - static void SendAnimationEvent(RE::Actor* a_this, const RE::BSFixedString a_tag, const RE::BSFixedString a_payload = "") + static void SendAnimationEvent(RE::Actor *a_this, const RE::BSFixedString a_tag, const RE::BSFixedString a_payload = "") { - if (a_this) { + if (a_this) + { RE::BSAnimationGraphManagerPtr graphManager; a_this->GetAnimationGraphManager(graphManager); - if (graphManager) { + if (graphManager) + { bool bSinked = false; - for (auto& animationGraph : graphManager->graphs) { - if (auto eventSource = animationGraph->GetEventSource(); eventSource) { + for (auto &animationGraph : graphManager->graphs) + { + if (auto eventSource = animationGraph->GetEventSource(); eventSource) + { RE::BSAnimationGraphEvent event = {a_tag, a_this, a_payload}; a_this->ProcessEvent(&event, eventSource); break; @@ -440,10 +506,12 @@ namespace ObjectUtil } } } - static bool DoAction(RE::BGSAction* a_action, RE::Actor* a_actor = RE::PlayerCharacter::GetSingleton()) + static bool DoAction(RE::BGSAction *a_action, RE::Actor *a_actor = RE::PlayerCharacter::GetSingleton()) { - if (auto taskInterface = SKSE::GetTaskInterface(); taskInterface && a_action && a_actor) { - taskInterface->AddTask([a_action, a_actor]() { + if (auto taskInterface = SKSE::GetTaskInterface(); taskInterface && a_action && a_actor) + { + taskInterface->AddTask([a_action, a_actor]() + { std::unique_ptr data(TESActionData::Create()); if (data) { //data->source = a_actor->As()->GetHandle().get(); // alternate @@ -452,33 +520,41 @@ namespace ObjectUtil typedef bool func_t(TESActionData*); REL::Relocation func{ RELOCATION_ID(40551, 41557) }; // credits to https://github.com/jarari return func(data.get()); - } return false; - }); - } return false; - } - static void EquipItem(RE::Actor* a_actor, RE::FormID a_formID, const bool a_skipAnim = false, - uint32_t a_count = 1U, bool a_queueEquip = true, bool a_forceEquip = false, bool a_playSounds = true, bool a_applyNow = false, - const RE::BGSEquipSlot *a_slot = (const RE::BGSEquipSlot *)nullptr) { - if (a_actor) { + } return false; }); + } + return false; + } + static void EquipItem(RE::Actor *a_actor, RE::FormID a_formID, const bool a_skipAnim = false, + uint32_t a_count = 1U, bool a_queueEquip = true, bool a_forceEquip = false, bool a_playSounds = true, bool a_applyNow = false, + const RE::BGSEquipSlot *a_slot = (const RE::BGSEquipSlot *)nullptr) + { + if (a_actor) + { auto eqManager = RE::ActorEquipManager::GetSingleton(); auto invChanges = a_actor->GetInventoryChanges(); auto entries = invChanges ? invChanges->entryList : nullptr; - RE::ExtraDataList* xList = nullptr; + RE::ExtraDataList *xList = nullptr; if (entries) - for (auto entry : *entries) { - if (entry && eqManager && entry->extraLists && entry->object && entry->object->formID == a_formID) { - if (entry->extraLists->empty()) spdlog::debug("your extralist is empty!"); - else xList = entry->extraLists->front(); + for (auto entry : *entries) + { + if (entry && eqManager && entry->extraLists && entry->object && entry->object->formID == a_formID) + { + if (entry->extraLists->empty()) + spdlog::debug("your extralist is empty!"); + else + xList = entry->extraLists->front(); eqManager->EquipObject(a_actor, entry->object, xList, a_count, a_slot, a_queueEquip, a_forceEquip, a_playSounds, a_applyNow); - break; + break; } } - else spdlog::debug("there is no inventory changes!"); + else + spdlog::debug("there is no inventory changes!"); } } - static void UnEquipItem(RE::Actor* a_actor, const bool a_isLeft, const bool a_soundPlay) + static void UnEquipItem(RE::Actor *a_actor, const bool a_isLeft, const bool a_soundPlay) { - if (a_actor) { + if (a_actor) + { auto eData = a_actor->GetEquippedEntryData(a_isLeft); auto xLists = eData ? eData->extraLists : nullptr; auto xList = xLists ? xLists->front() : nullptr; @@ -492,12 +568,12 @@ namespace ObjectUtil struct Spell { - static void SetMagnitude(RE::SpellItem* a_spell, const float a_magnitude) + static void SetMagnitude(RE::SpellItem *a_spell, const float a_magnitude) { if (a_spell && a_spell->effects[0]) a_spell->effects[0]->effectItem.magnitude = a_magnitude; } - static void SetDuration(RE::SpellItem* a_spell, const uint8_t a_duration) + static void SetDuration(RE::SpellItem *a_spell, const uint8_t a_duration) { if (a_spell && a_spell->effects[0]) a_spell->effects[0]->effectItem.duration = a_duration; @@ -506,68 +582,91 @@ namespace ObjectUtil struct Poison { - static RE::AlchemyItem* GetEquippedObjPoison(RE::Actor* a_actor, const bool a_isLeft = false) + static RE::AlchemyItem *GetEquippedObjPoison(RE::Actor *a_actor, const bool a_isLeft = false) { - if (a_actor) { - if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); eData) { + if (a_actor) + { + if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); eData) + { auto xList = eData->IsPoisoned() ? eData->extraLists : nullptr; - if (xList) return GetPoison(xList); + if (xList) + return GetPoison(xList); } - } return nullptr; + } + return nullptr; } - static RE::AlchemyItem* GetPoison(RE::BSSimpleList* a_xList) + static RE::AlchemyItem *GetPoison(RE::BSSimpleList *a_xList) { - if (a_xList && !a_xList->empty()) { - for (auto xData : *a_xList) { + if (a_xList && !a_xList->empty()) + { + for (auto xData : *a_xList) + { if (xData) - if (auto xPoison = xData->GetByType(); auto poison = xPoison ? xPoison->poison : nullptr) { + if (auto xPoison = xData->GetByType(); auto poison = xPoison ? xPoison->poison : nullptr) + { return poison; } } - } return nullptr; + } + return nullptr; } }; struct Enchantment { // setters - static void EnchantEquippedWeapon(RE::Actor* a_actor, RE::EnchantmentItem* a_ench, const float a_charge = 500.f, const bool a_isLeft = false, const bool a_removeOnUnequip = false) + static void EnchantEquippedWeapon(RE::Actor *a_actor, RE::EnchantmentItem *a_ench, const float a_charge = 500.f, const bool a_isLeft = false, const bool a_removeOnUnequip = false) { - if (!a_actor || !a_ench) return; - if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); auto xList = eData ? eData->extraLists : nullptr) { - if (xList) return EnchantItem(xList, a_ench, a_charge, a_removeOnUnequip); + if (!a_actor || !a_ench) + return; + if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); auto xList = eData ? eData->extraLists : nullptr) + { + if (xList) + return EnchantItem(xList, a_ench, a_charge, a_removeOnUnequip); } } - static void DisEnchantEquippedWeapon(RE::Actor* a_actor, const bool a_isLeft = false, const bool a_defaultEnch = false) + static void DisEnchantEquippedWeapon(RE::Actor *a_actor, const bool a_isLeft = false, const bool a_defaultEnch = false) { - if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); auto obj = a_actor->GetEquippedObject(a_isLeft)) { - if (auto weap = obj->As(); weap) { - if (a_defaultEnch && weap->formEnchanting) { + if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); auto obj = a_actor->GetEquippedObject(a_isLeft)) + { + if (auto weap = obj->As(); weap) + { + if (a_defaultEnch && weap->formEnchanting) + { weap->formEnchanting = nullptr; weap->amountofEnchantment = 0; } - if (auto xList = eData->extraLists; xList) { + if (auto xList = eData->extraLists; xList) + { return DisEnchantItem(xList); } } } } - static void ChargeEquippedWeapon(RE::Actor* a_actor, const float a_charge, const bool a_isLeft = false) + static void ChargeEquippedWeapon(RE::Actor *a_actor, const float a_charge, const bool a_isLeft = false) { - if (a_actor && a_actor->AsActorValueOwner()) { + if (a_actor && a_actor->AsActorValueOwner()) + { float maxCharge = 0.f; - if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); auto xList = eData ? eData->extraLists : nullptr) { - if (eData->object && eData->object->As()) { + if (auto eData = a_actor->GetEquippedEntryData(a_isLeft); auto xList = eData ? eData->extraLists : nullptr) + { + if (eData->object && eData->object->As()) + { maxCharge = eData->object->As()->amountofEnchantment; - } for (auto xData : *xList) { - if (auto xEnch = xData->GetByType(); xEnch) { + } + for (auto xData : *xList) + { + if (auto xEnch = xData->GetByType(); xEnch) + { maxCharge = (float)xEnch->charge - 1.f; } - if (auto xCharge = xData->GetByType(); xCharge) { + if (auto xCharge = xData->GetByType(); xCharge) + { float sum = xCharge->charge + a_charge; - if (sum < 0.f) sum = 0.f; + if (sum < 0.f) + sum = 0.f; xCharge->charge = sum >= maxCharge ? maxCharge : sum; - break; + break; } } } @@ -577,118 +676,154 @@ namespace ObjectUtil a_actor->AsActorValueOwner()->ModActorValue(av, charge); } } - static void ChargeInventoryWeapon(RE::Actor* a_actor, RE::FormID a_weapID, const float a_charge) + static void ChargeInventoryWeapon(RE::Actor *a_actor, RE::FormID a_weapID, const float a_charge) { - if (a_actor) { + if (a_actor) + { auto invChanges = a_actor->GetInventoryChanges(); if (auto entries = invChanges ? invChanges->entryList : nullptr; entries && !entries->empty()) - for (auto entry : *entries) { - if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) { + for (auto entry : *entries) + { + if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) + { ChargeWeapon(entry, a_charge); } } } } - static void ChargeWeapon(RE::InventoryEntryData* a_eData, const float a_charge) + static void ChargeWeapon(RE::InventoryEntryData *a_eData, const float a_charge) { - if (a_eData && a_eData->extraLists && !a_eData->extraLists->empty()) { + if (a_eData && a_eData->extraLists && !a_eData->extraLists->empty()) + { float maxCharge = 0.f; auto xList = a_eData->extraLists; - if (a_eData->object && a_eData->object->As()) { + if (a_eData->object && a_eData->object->As()) + { maxCharge = a_eData->object->As()->amountofEnchantment; - } for (auto xData : *xList) { + } + for (auto xData : *xList) + { if (xData) - if (auto xEnch = xData->GetByType(); xEnch) { + if (auto xEnch = xData->GetByType(); xEnch) + { maxCharge = (float)xEnch->charge - 1.f; } - if (auto xCharge = xData->GetByType(); xCharge) { - float sum = xCharge->charge + a_charge; - if (sum < 0.f) sum = 0.f; - xCharge->charge = sum >= maxCharge ? maxCharge : sum; - break; - } + if (auto xCharge = xData->GetByType(); xCharge) + { + float sum = xCharge->charge + a_charge; + if (sum < 0.f) + sum = 0.f; + xCharge->charge = sum >= maxCharge ? maxCharge : sum; + break; + } } } } - - static void EnchantInventoryWeapon(RE::Actor* a_actor, RE::FormID a_weapID, RE::EnchantmentItem* a_ench, const float a_magnitude, const float a_charge, const bool a_removeOnUnequip = false) + + static void EnchantInventoryWeapon(RE::Actor *a_actor, RE::FormID a_weapID, RE::EnchantmentItem *a_ench, const float a_magnitude, const float a_charge, const bool a_removeOnUnequip = false) { - if (!a_actor || !a_ench) return; + if (!a_actor || !a_ench) + return; auto invChanges = a_actor->GetInventoryChanges(); auto entries = invChanges ? invChanges->entryList : nullptr; - RE::BSSimpleList* xList = nullptr; + RE::BSSimpleList *xList = nullptr; if (entries && !entries->empty()) - for (auto entry : *entries) { - if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) { + for (auto entry : *entries) + { + if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) + { xList = entry->extraLists; } - } if (xList) return EnchantItem(xList, a_ench, a_charge, a_removeOnUnequip); + } + if (xList) + return EnchantItem(xList, a_ench, a_charge, a_removeOnUnequip); } - static void EnchantItem(RE::BSSimpleList* a_xList, RE::EnchantmentItem* a_ench, const float a_charge = 500.f, const bool a_removeOnUnequip = false) + static void EnchantItem(RE::BSSimpleList *a_xList, RE::EnchantmentItem *a_ench, const float a_charge = 500.f, const bool a_removeOnUnequip = false) { - if (!a_xList || a_xList->empty() || !a_ench) return; + if (!a_xList || a_xList->empty() || !a_ench) + return; bool isEnchanted = false; bool isCharged = false; - for (auto xData : *a_xList) { - if (xData) { - if (auto xEnch = xData->GetByType(); xEnch) { + for (auto xData : *a_xList) + { + if (xData) + { + if (auto xEnch = xData->GetByType(); xEnch) + { xEnch->enchantment = a_ench; xEnch->charge = a_charge; xEnch->removeOnUnequip = a_removeOnUnequip; isEnchanted = true; - } if (auto xCharge = xData->GetByType(); xCharge) { + } + if (auto xCharge = xData->GetByType(); xCharge) + { xCharge->charge = a_charge; isCharged = true; } } } if (!isEnchanted) - for (auto xData : *a_xList) { + for (auto xData : *a_xList) + { auto newEnch = RE::BSExtraData::Create(); newEnch->enchantment = a_ench; newEnch->charge = a_charge; newEnch->removeOnUnequip = a_removeOnUnequip; - // RE::ExtraEnchantment* newEnch = new RE::ExtraEnchantment(a_ench, a_charge, a_removeOnUnequip); - // RE::ExtraEnchantment newEnch(a_ench, a_charge, a_removeOnUnequip); // causing crashes - if (xData) xData->Add(newEnch); - break; + // RE::ExtraEnchantment* newEnch = new RE::ExtraEnchantment(a_ench, a_charge, a_removeOnUnequip); + // RE::ExtraEnchantment newEnch(a_ench, a_charge, a_removeOnUnequip); // causing crashes + if (xData) + xData->Add(newEnch); + break; } if (!isCharged) - for (auto xData : *a_xList) { - // RE::ExtraCharge* newCharge = new RE::ExtraCharge(); - RE::ExtraCharge* newCharge = RE::BSExtraData::Create(); + for (auto xData : *a_xList) + { + // RE::ExtraCharge* newCharge = new RE::ExtraCharge(); + RE::ExtraCharge *newCharge = RE::BSExtraData::Create(); newCharge->charge = a_charge; - if (xData) xData->Add(newCharge); - break; + if (xData) + xData->Add(newCharge); + break; } } - static void DisEnchantInventoryWeapon(RE::Actor* a_actor, RE::FormID a_weapID) + static void DisEnchantInventoryWeapon(RE::Actor *a_actor, RE::FormID a_weapID) { - if (a_actor) { + if (a_actor) + { auto invChanges = a_actor->GetInventoryChanges(); auto entries = invChanges->entryList; - RE::BSSimpleList* xList = nullptr; - for (auto entry : *entries) { - if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) { + RE::BSSimpleList *xList = nullptr; + for (auto entry : *entries) + { + if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) + { xList = entry->extraLists; } - } if (xList) return DisEnchantItem(xList); + } + if (xList) + return DisEnchantItem(xList); } } - static void DisEnchantItem(RE::BSSimpleList* a_xList) + static void DisEnchantItem(RE::BSSimpleList *a_xList) { - if (a_xList) { - for (auto xData : *a_xList) { - if (xData) { - if (auto xEnch = xData->GetByType(); xEnch) { + if (a_xList) + { + for (auto xData : *a_xList) + { + if (xData) + { + if (auto xEnch = xData->GetByType(); xEnch) + { xEnch->enchantment = nullptr; xEnch->charge = 0; xData->Remove(xEnch); xEnch->~ExtraEnchantment(); xEnch = nullptr; - } if (auto xCharge = xData->GetByType(); xCharge) { + } + if (auto xCharge = xData->GetByType(); xCharge) + { xCharge->charge = 0.f; xData->Remove(xCharge); xCharge->~ExtraCharge(); @@ -699,54 +834,69 @@ namespace ObjectUtil } } // getters - static RE::EnchantmentItem* GetEquippedWeaponEnchantment(RE::Actor* a_actor, const bool a_isLeft = false, const bool a_baseEnchPrior = false) + static RE::EnchantmentItem *GetEquippedWeaponEnchantment(RE::Actor *a_actor, const bool a_isLeft = false, const bool a_baseEnchPrior = false) { - RE::EnchantmentItem* ench = nullptr; + RE::EnchantmentItem *ench = nullptr; if (a_actor) - if (auto obj = a_actor->GetEquippedObject(a_isLeft); obj) ench = GetInventoryItemEnchantment(a_actor, obj->formID, a_baseEnchPrior); + if (auto obj = a_actor->GetEquippedObject(a_isLeft); obj) + ench = GetInventoryItemEnchantment(a_actor, obj->formID, a_baseEnchPrior); return ench; } - static float GetEquippedWeaponCharge(RE::Actor* a_actor, const bool a_isLeft = false) + static float GetEquippedWeaponCharge(RE::Actor *a_actor, const bool a_isLeft = false) { float charge = 0.f; - if (a_actor && a_actor->AsActorValueOwner()) { + if (a_actor && a_actor->AsActorValueOwner()) + { const auto av = a_isLeft ? RE::ActorValue::kLeftItemCharge : RE::ActorValue::kRightItemCharge; charge = a_actor->AsActorValueOwner()->GetActorValue(av); - } return charge; + } + return charge; } - static RE::EnchantmentItem* GetInventoryItemEnchantment(RE::Actor* a_actor, RE::FormID a_weapID, const bool a_baseEnchPrior = false) + static RE::EnchantmentItem *GetInventoryItemEnchantment(RE::Actor *a_actor, RE::FormID a_weapID, const bool a_baseEnchPrior = false) { - RE::EnchantmentItem* formEnch = nullptr; - RE::EnchantmentItem* ench = nullptr; - if (a_actor) { + RE::EnchantmentItem *formEnch = nullptr; + RE::EnchantmentItem *ench = nullptr; + if (a_actor) + { auto invChanges = a_actor->GetInventoryChanges(); auto entries = invChanges->entryList; - for (auto entry : *entries) { - if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) { - if (auto eForm = entry->object->As(); eForm) formEnch = eForm->formEnchanting; - if (auto xList = entry->extraLists; xList && !xList->empty()) ench = GetExtraEnchantment(xList); + for (auto entry : *entries) + { + if (entry && entry->object && entry->object->IsWeapon() && entry->object->formID == a_weapID) + { + if (auto eForm = entry->object->As(); eForm) + formEnch = eForm->formEnchanting; + if (auto xList = entry->extraLists; xList && !xList->empty()) + ench = GetExtraEnchantment(xList); } } } - if (a_baseEnchPrior) return formEnch ? formEnch : ench; - else return ench ? ench : formEnch; + if (a_baseEnchPrior) + return formEnch ? formEnch : ench; + else + return ench ? ench : formEnch; } - static RE::EnchantmentItem* GetExtraEnchantment(RE::BSSimpleList* a_xList) + static RE::EnchantmentItem *GetExtraEnchantment(RE::BSSimpleList *a_xList) { - if (a_xList && !a_xList->empty()) { - for (auto xData : *a_xList) { + if (a_xList && !a_xList->empty()) + { + for (auto xData : *a_xList) + { if (xData) - if (auto xEnch = xData->GetByType(); xEnch) return xEnch->enchantment; + if (auto xEnch = xData->GetByType(); xEnch) + return xEnch->enchantment; } - } return nullptr; + } + return nullptr; } }; struct Sound { - static void PlaySound(RE::BGSSoundDescriptorForm* a_sound, RE::NiAVObject* a_source, const float a_volume = 1.f) + static void PlaySound(RE::BGSSoundDescriptorForm *a_sound, RE::NiAVObject *a_source, const float a_volume = 1.f) { - if (a_sound && a_source) { + if (a_sound && a_source) + { auto audioManager = RE::BSAudioManager::GetSingleton(); BSSoundHandle handle; audioManager->BuildSoundDataFromDescriptor(handle, a_sound->soundDescriptor); @@ -793,13 +943,12 @@ namespace ObjectUtil }; } - namespace AnimUtil { struct Idle { - static bool Play(RE::TESIdleForm* idle, RE::Actor* actor, RE::DEFAULT_OBJECT action, RE::Actor* target) - { + static bool Play(RE::TESIdleForm *idle, RE::Actor *actor, RE::DEFAULT_OBJECT action, RE::Actor *target) + { if (actor && actor->GetActorRuntimeData().currentProcess) { typedef bool (*func_t)(RE::AIProcess *, RE::Actor *, RE::DEFAULT_OBJECT, RE::TESIdleForm *, bool, bool, RE::Actor *); @@ -807,7 +956,7 @@ namespace AnimUtil return func(actor->GetActorRuntimeData().currentProcess, actor, action, idle, true, true, target); } return false; - } + } }; } @@ -815,140 +964,141 @@ namespace FormUtil { struct Parse { - static RE::TESForm *GetFormFromMod(uint32_t formid,std::string modname) - { + static RE::TESForm *GetFormFromMod(uint32_t formid, std::string modname) + { if (!modname.length() || !formid) return nullptr; RE::TESDataHandler *dh = RE::TESDataHandler::GetSingleton(); - return dh->LookupForm(formid, modname); - - } + return dh->LookupForm(formid, modname); + } - static RE::TESForm *GetFormFromMod(std::string modname, std::string formIDString) - { - if (formIDString.length() == 0) return nullptr; + static RE::TESForm *GetFormFromMod(std::string modname, std::string formIDString) + { + if (formIDString.length() == 0) + return nullptr; - uint32_t formID = std::stoi(formIDString, 0, 16); - return GetFormFromMod(formID,modname); - } + uint32_t formID = std::stoi(formIDString, 0, 16); + return GetFormFromMod(formID, modname); + } - static RE::TESForm *GetFormFromConfigString(std::string str, std::string_view delimiter) - { - std::vector splitData = Util::String::Split(str, delimiter); - if (splitData.size() < 2) return nullptr; - return GetFormFromMod(splitData[1], splitData[0]); - } - static RE::TESForm *GetFormFromConfigString(std::string str) - { - return GetFormFromConfigString(str, "~"sv); - } - static RE::FormID GetFormIDFromMod(uint32_t relativeFormID, std::string modName) - { - auto *dataHandler = TESDataHandler::GetSingleton(); + static RE::TESForm *GetFormFromConfigString(std::string str, std::string_view delimiter) + { + std::vector splitData = Util::String::Split(str, delimiter); + if (splitData.size() < 2) + return nullptr; + return GetFormFromMod(splitData[1], splitData[0]); + } + static RE::TESForm *GetFormFromConfigString(std::string str) + { + return GetFormFromConfigString(str, "~"sv); + } + static RE::FormID GetFormIDFromMod(uint32_t relativeFormID, std::string modName) + { + auto *dataHandler = TESDataHandler::GetSingleton(); - if (!dataHandler) + if (!dataHandler) return -1; - return dataHandler->LookupFormID(relativeFormID, modName); - } - - static RE::FormID GetFormIDFromMod(std::string relativeFormIDString, std::string modName) - { - if (relativeFormIDString.length() == 0) return -1; - + return dataHandler->LookupFormID(relativeFormID, modName); + } - uint32_t relativeFormID = std::stoi(relativeFormIDString, 0, 16); - return GetFormIDFromMod(relativeFormID, modName); - } + static RE::FormID GetFormIDFromMod(std::string relativeFormIDString, std::string modName) + { + if (relativeFormIDString.length() == 0) + return -1; - static RE::FormID GetFormIDFromConfigString(std::string str, std::string_view delimiter) - { - std::vector splitData = Util::String::Split(str, delimiter); - if (splitData.size() < 2) return -1; - return GetFormIDFromMod(splitData[0], splitData[1]); - } - static RE::FormID GetFormIDFromConfigString(std::string str) - { - return GetFormIDFromConfigString(str, "~"sv); - } + uint32_t relativeFormID = std::stoi(relativeFormIDString, 0, 16); + return GetFormIDFromMod(relativeFormID, modName); + } + static RE::FormID GetFormIDFromConfigString(std::string str, std::string_view delimiter) + { + std::vector splitData = Util::String::Split(str, delimiter); + if (splitData.size() < 2) + return -1; + return GetFormIDFromMod(splitData[0], splitData[1]); + } + static RE::FormID GetFormIDFromConfigString(std::string str) + { + return GetFormIDFromConfigString(str, "~"sv); + } }; - struct Quest + struct Quest { - public: - static BGSBaseAlias *FindAliasByName(std::string_view name, TESQuest *owningQuest) + public: + static BGSBaseAlias *FindAliasByName(std::string_view name, TESQuest *owningQuest) + { + RE::BSWriteLockGuard AliasLock{owningQuest->aliasAccessLock}; + for (auto *alias : owningQuest->aliases) { - RE::BSWriteLockGuard AliasLock{owningQuest->aliasAccessLock}; - for (auto *alias : owningQuest->aliases) - { std::string aliasName = alias->aliasName.c_str(); if (aliasName == name) return alias; - } - return nullptr; } + return nullptr; + } }; } namespace NifUtil { struct Node + { + static NiAVObject *Clone(NiAVObject *original) { - static NiAVObject* Clone(NiAVObject* original) - { - typedef NiAVObject* (*func_t)(NiAVObject* avObj); - REL::Relocation func{ RELOCATION_ID(68835, 70187) }; - return func(original); - } - static RE::NiAVObject* GetNiObject( - RE::NiNode* a_root, - const RE::BSFixedString& a_name) - { - return a_root->GetObjectByName(a_name); - } + typedef NiAVObject *(*func_t)(NiAVObject *avObj); + REL::Relocation func{RELOCATION_ID(68835, 70187)}; + return func(original); + } + static RE::NiAVObject *GetNiObject( + RE::NiNode *a_root, + const RE::BSFixedString &a_name) + { + return a_root->GetObjectByName(a_name); + } - static void AttachToNode( - RE::NiAVObject* a_object, - RE::NiNode* a_node) + static void AttachToNode( + RE::NiAVObject *a_object, + RE::NiNode *a_node) + { + if (a_object->parent != a_node) { - if (a_object->parent != a_node) - { - a_node->AttachChild(a_object, true); - } + a_node->AttachChild(a_object, true); } + } - static std::vector GetAllGeometries(RE::NiAVObject* root) - { - std::vector geometries; - RE::BSVisit::TraverseScenegraphGeometries(root, [&](BSGeometry* geom)-> RE::BSVisit::BSVisitControl - { + static std::vector GetAllGeometries(RE::NiAVObject *root) + { + std::vector geometries; + RE::BSVisit::TraverseScenegraphGeometries(root, [&](BSGeometry *geom) -> RE::BSVisit::BSVisitControl + { geometries.emplace_back(geom); - return RE::BSVisit::BSVisitControl::kContinue; - } - ); - return geometries; - } - - }; + return RE::BSVisit::BSVisitControl::kContinue; }); + return geometries; + } + }; struct Armature { - static RE::NiNode* GetActorNode(RE::Actor* actor, std::string nodeName) + static RE::NiNode *GetActorNode(RE::Actor *actor, std::string nodeName) { - auto root = actor->Get3D(); - if (!root) return nullptr; + auto root = actor->Get3D(); + if (!root) + return nullptr; - auto bone = root->GetObjectByName(nodeName); - if (!bone) return nullptr; + auto bone = root->GetObjectByName(nodeName); + if (!bone) + return nullptr; - auto node = bone->AsNode(); - if (!node) return nullptr; + auto node = bone->AsNode(); + if (!node) + return nullptr; - return node; + return node; } - static void AttachToNode(RE::NiAVObject* obj, RE::Actor* actor, std::string nodeName) + static void AttachToNode(RE::NiAVObject *obj, RE::Actor *actor, std::string nodeName) { - auto* node = GetActorNode(actor, nodeName); + auto *node = GetActorNode(actor, nodeName); if (node) { node->AttachChild(obj, true); @@ -958,14 +1108,16 @@ namespace NifUtil }; struct Collision { - static bool ToggleMeshCollision(RE::NiAVObject* root,RE::bhkWorld* world, bool collisionState) + static bool ToggleMeshCollision(RE::NiAVObject *root, RE::bhkWorld *world, bool collisionState) { constexpr auto no_collision_flag = static_cast(RE::CFilter::Flag::kNoCollision); - if (root && world) { - - RE::BSWriteLockGuard locker(world->worldLock); + if (root && world) + { + + RE::BSWriteLockGuard locker(world->worldLock); - RE::BSVisit::TraverseScenegraphCollision(root, [&](RE::bhkNiCollisionObject* a_col) -> RE::BSVisit::BSVisitControl { + RE::BSVisit::TraverseScenegraphCollision(root, [&](RE::bhkNiCollisionObject *a_col) -> RE::BSVisit::BSVisitControl + { if (auto hkpBody = a_col->body ? static_cast(a_col->body->referencedObject.get()) : nullptr; hkpBody) { auto& filter = hkpBody->collidable.broadPhaseHandle.collisionFilterInfo; if (!collisionState) { @@ -974,23 +1126,24 @@ namespace NifUtil filter &= ~no_collision_flag; } } - return RE::BSVisit::BSVisitControl::kContinue; - }); - } - else - { - return false; - } + return RE::BSVisit::BSVisitControl::kContinue; }); + } + else + { + return false; + } return true; } - static bool RemoveMeshCollision(RE::NiAVObject* root,RE::bhkWorld* world, bool collisionState) + static bool RemoveMeshCollision(RE::NiAVObject *root, RE::bhkWorld *world, bool collisionState) { constexpr auto no_collision_flag = static_cast(RE::CFilter::Flag::kNoCollision); - if (root && world) { - - RE::BSWriteLockGuard locker(world->worldLock); + if (root && world) + { + + RE::BSWriteLockGuard locker(world->worldLock); - RE::BSVisit::TraverseScenegraphCollision(root, [&](RE::bhkNiCollisionObject* a_col) -> RE::BSVisit::BSVisitControl { + RE::BSVisit::TraverseScenegraphCollision(root, [&](RE::bhkNiCollisionObject *a_col) -> RE::BSVisit::BSVisitControl + { if (auto hkpBody = a_col->body ? static_cast(a_col->body->referencedObject.get()) : nullptr; hkpBody) { auto& filter = hkpBody->collidable.broadPhaseHandle.collisionFilterInfo; if (!collisionState) { @@ -999,13 +1152,12 @@ namespace NifUtil filter &= ~no_collision_flag; } } - return RE::BSVisit::BSVisitControl::kContinue; - }); - } - else - { - return false; - } + return RE::BSVisit::BSVisitControl::kContinue; }); + } + else + { + return false; + } return true; } }; From a812ecd147df98118782681bbe9b894f8f165061 Mon Sep 17 00:00:00 2001 From: Igor Alan Albuquerque de Sousa <50077829+IgorAlanAlbuquerque@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:20:41 -0300 Subject: [PATCH 02/11] feat(equip-hook): :sparkles: introduce dual-path equip animation suppression (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a GetEquipMode() dispatcher that selects between three mutually exclusive behaviours on each OnEquipItemPC call: - SkipOld (SkipEquipAnimation=true): preserves the original mod flow — calls _OnEquipItemPC with playAnim=false and fires weaponDraw / WeapEquip_Out events after a frame delay so the weapon 3D model appears. Keeps full compatibility with WeaponThrowing axe-catch. - SkipInstant (InstantEquipAnim=true): new path. Calls _OnEquipItemPC with playAnim=true (graph state machine runs normally, attack inputs unlock immediately), then uses hkbClipGenerator vtable hooks to fast-forward the equip clip in Activate_Hook and suppress bone output in Generate_Hook so the character does not visually move. - Normal: unmodified pass-through. The SkipInstant path tracks the exact hkbClipGenerator instance pointer (g_suppressedClip) instead of a timing window, ensuring that other equip clips sharing the same frame — such as the axe-catch clip from WeaponThrowing — are never affected regardless of call order. Restore SendEquipEvents and the delayed-thread logic from the original hook on the SkipOld branch; remove the single-path hkbClipGenerator-only implementation that left SkipEquipAnimation=true cases without a delayed event dispatch. --- cmake/headerlist.cmake | 3 +- cmake/sourcelist.cmake | 1 + src/event.cpp | 44 +++++++++ src/event.h | 22 +++++ src/hook.cpp | 213 +++++++++++++++++++++++++++++------------ src/hook.h | 32 ++++--- 6 files changed, 242 insertions(+), 73 deletions(-) create mode 100644 src/event.cpp create mode 100644 src/event.h diff --git a/cmake/headerlist.cmake b/cmake/headerlist.cmake index 0aba2be..a41db02 100644 --- a/cmake/headerlist.cmake +++ b/cmake/headerlist.cmake @@ -2,5 +2,6 @@ set(headers ${headers} src/PCH.h src/log.h src/util.h - src/hook.h + src/hook.h + src/event.h ) \ No newline at end of file diff --git a/cmake/sourcelist.cmake b/cmake/sourcelist.cmake index 06fc6d5..5339f15 100644 --- a/cmake/sourcelist.cmake +++ b/cmake/sourcelist.cmake @@ -1,4 +1,5 @@ set(sources ${sources} src/plugin.cpp src/hook.cpp + src/event.cpp ) \ No newline at end of file diff --git a/src/event.cpp b/src/event.cpp new file mode 100644 index 0000000..b65dae6 --- /dev/null +++ b/src/event.cpp @@ -0,0 +1,44 @@ +#include "event.h" +/**/ +bool AnimationEventTracker::Register() +{ + bool foundEventSource = eventSource != nullptr; + if (!foundEventSource) + { + const auto pc = RE::PlayerCharacter::GetSingleton(); + + RE::BSAnimationGraphManagerPtr graphManager; + if (pc) + pc->GetAnimationGraphManager(graphManager); + else + spdlog::debug("Can't found Player Character"); + + if (graphManager) + { + for (auto &animationGraph : graphManager->graphs) + { + eventSource = animationGraph->GetEventSource(); + if (eventSource) + { + spdlog::info("Registered {}", typeid(RE::BSAnimationGraphEvent).name()); + return true; + } + } + } + + if (!eventSource) + { + spdlog::info("Failed to register {}", typeid(RE::BSAnimationGraphEvent).name()); + } + } + return foundEventSource; +} + +void AnimationEventTracker::SendAnimationEvent(RE::Actor *a_this, const RE::BSFixedString a_tag, const RE::BSFixedString a_payload) +{ + if (a_this && eventSource) + { + RE::BSAnimationGraphEvent event = {a_tag, a_this, a_payload}; + a_this->ProcessEvent(&event, eventSource); + } +} \ No newline at end of file diff --git a/src/event.h b/src/event.h new file mode 100644 index 0000000..65d5fd2 --- /dev/null +++ b/src/event.h @@ -0,0 +1,22 @@ +#pragma once + +using EventChecker = RE::BSEventNotifyControl; +class AnimationEventTracker : public RE::BSTEventSink +{ +public: + static AnimationEventTracker *GetSingleton() + { + static AnimationEventTracker singleton; + return &singleton; + } + + bool Register(); + + virtual EventChecker ProcessEvent(const RE::BSAnimationGraphEvent *a_event, RE::BSTEventSource *a_eventSource) override + { + return EventChecker::kContinue; + }; + void SendAnimationEvent(RE::Actor *a_this, const RE::BSFixedString a_tag, const RE::BSFixedString a_payload = ""); + + RE::BSTEventSource *eventSource; +}; \ No newline at end of file diff --git a/src/hook.cpp b/src/hook.cpp index ce8c1a5..5a4b9e5 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -1,29 +1,21 @@ #include "hook.h" +#include "event.h" #include "util.h" #include -#include #include #include +#include using namespace std::literals; using namespace Util; namespace { - static inline std::atomic g_skipUntilMs{0}; static inline std::atomic g_suppressForceEquipClips{0}; - inline std::int64_t NowMs() - { - using clock = std::chrono::steady_clock; - return std::chrono::duration_cast(clock::now().time_since_epoch()).count(); - } - - inline bool InSkipWindow() - { - return NowMs() <= g_skipUntilMs.load(std::memory_order_relaxed); - } + // The specific hkbClipGenerator instance being suppressed on the SkipInstant path + static inline std::atomic g_suppressedClip{nullptr}; static bool IsEquipClip(std::string_view nm) { @@ -31,20 +23,11 @@ namespace } } -void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) -{ - SkipAnim(a_this, a_playAnim); - _OnEquipItemPC(a_this, a_playAnim); -} +// --------------------------------------------------------------------------- +// Helpers shared by both paths +// --------------------------------------------------------------------------- -/* -void EquipHook::OnEquipItemNPC(RE::Actor* a_this, bool a_playAnim) -{ - _OnEquipItemNPC(a_this, !SkipAnim(a_this, a_playAnim)); -} -*/ - -bool CheckIsValidBoundObject(const RE::TESForm *a_object) +static bool CheckIsValidBoundObject(const RE::TESForm *a_object) { if (!a_object) return false; @@ -53,61 +36,173 @@ bool CheckIsValidBoundObject(const RE::TESForm *a_object) return a_object->As() != nullptr || a_object->As() != nullptr; } -bool EquipHook::SkipAnim(RE::PlayerCharacter *a_this, bool a_playAnim) +// Sends the animation events that normally fire at the end of an equip animation. +static void SendEquipEvents(RE::Actor *a_this, + RE::TESForm *a_lHandObject, + RE::TESForm *a_rHandObject) { - bool skipAnim = !a_playAnim; - if (a_this && a_this->AsActorState() && a_this->AsActorState()->IsWeaponDrawn()) - { - auto rHandObj = a_this->GetEquippedObject(false); - auto lHandObj = a_this->GetEquippedObject(true); + auto *tracker = AnimationEventTracker::GetSingleton(); + if (!tracker) + return; - if (!(lHandObj && rHandObj) || CheckIsValidBoundObject(lHandObj) || CheckIsValidBoundObject(rHandObj)) - { - a_this->GetGraphVariableBool("SkipEquipAnimation", skipAnim); - if (skipAnim) - { - g_skipUntilMs.store(NowMs() + 500, std::memory_order_relaxed); - g_suppressForceEquipClips.store(1, std::memory_order_relaxed); - } - } + tracker->SendAnimationEvent(a_this, "weaponDraw"); + + if ((a_lHandObject && (a_lHandObject->IsWeapon() || a_lHandObject->IsArmor())) || (a_rHandObject && (a_rHandObject->IsWeapon() || a_rHandObject->IsArmor()))) + { + tracker->SendAnimationEvent(a_this, "WeapEquip_Out"); + tracker->SendAnimationEvent(a_this, "WeapEquip_OutMoving"); } - _skipAnim = skipAnim; - return skipAnim; } -void EquipHook::Update_Hook(RE::hkbClipGenerator* a_this, const RE::hkbContext& a_context, float a_timestep) +// --------------------------------------------------------------------------- +// Mode detection +// --------------------------------------------------------------------------- + +EquipHook::EquipMode EquipHook::GetEquipMode(RE::PlayerCharacter *a_this, bool a_playAnim) { - if (!a_this) { - _Update(a_this, a_context, a_timestep); - return; - } + if (!a_playAnim) + return EquipMode::Skip; - std::string_view nm{ a_this->animationName.c_str() }; - if (IsEquipClip(nm) && a_this->atEnd) return; + if (!a_this || !a_this->AsActorState() || !a_this->AsActorState()->IsWeaponDrawn()) + return EquipMode::Normal; - _Update(a_this, a_context, a_timestep); + auto *rHandObj = a_this->GetEquippedObject(false); + auto *lHandObj = a_this->GetEquippedObject(true); + + if ((lHandObj && rHandObj) && !CheckIsValidBoundObject(lHandObj) && !CheckIsValidBoundObject(rHandObj)) + return EquipMode::Normal; + + // Set this variable from your own mod/Papyrus whenever you want the + // equip clip to be driven through the state machine instantly with + // zero visual movement (inputs unlocked immediately). + bool instantAnim = false; + a_this->GetGraphVariableBool("InstantEquipAnim", instantAnim); + if (instantAnim) + return EquipMode::InstantAnim; + + // Uses playAnim=false so the graph never plays the clip at all. + bool skipAnim = false; + a_this->GetGraphVariableBool("SkipEquipAnimation", skipAnim); + if (skipAnim) + return EquipMode::Skip; + + return EquipMode::Normal; } -void EquipHook::Activate_Hook(RE::hkbClipGenerator* a_this, const RE::hkbContext& a_context) +// --------------------------------------------------------------------------- +// Vtable hook: PlayerCharacter::OnEquipItem +// --------------------------------------------------------------------------- + +void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) { - _Activate(a_this, a_context); + switch (GetEquipMode(a_this, a_playAnim)) + { + case EquipMode::Skip: + { + // Original mod path: suppress the animation entirely by passing false. + // Then send the weapon-ready events after a short delay so the 3-D model + auto *tracker = AnimationEventTracker::GetSingleton(); + tracker->Register(); + + auto *rHandObj = a_this->GetEquippedObject(false); + auto *lHandObj = a_this->GetEquippedObject(true); + + int delay = 300; + bool skip3D = false; + a_this->GetGraphVariableInt("LoadBoundObjectDelay", delay); + a_this->GetGraphVariableBool("Skip3DLoading", skip3D); + if (delay < static_cast(*g_deltaTimeRealTime * 1000.f)) + delay = static_cast(*g_deltaTimeRealTime * 1000.f); + + _OnEquipItemPC(a_this, false); - if (!a_this) return; + if (!skip3D) + { + std::jthread([=]() + { + std::this_thread::sleep_for(std::chrono::milliseconds(delay)); + SendEquipEvents(a_this, lHandObj, rHandObj); }) + .detach(); + } + break; + } - const int sup = g_suppressForceEquipClips.load(std::memory_order_relaxed); - if (sup <= 0) return; + case EquipMode::InstantAnim: + // New path: let the graph run (playAnim=true) so state transitions happen + // normally and attack inputs are unlocked. Activate_Hook will fast-forward + // the clip and Generate_Hook will suppress bone movement for that instance. + g_suppressForceEquipClips.store(1, std::memory_order_relaxed); + _OnEquipItemPC(a_this, true); + break; + + default: + _OnEquipItemPC(a_this, a_playAnim); + break; + } +} - std::string_view nm{ a_this->animationName.c_str() }; - if (!IsEquipClip(nm)) return; +// --------------------------------------------------------------------------- +// Vtable hooks: hkbClipGenerator +// --------------------------------------------------------------------------- +void EquipHook::Activate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context) +{ + _Activate(a_this, a_context); + + if (!a_this) + return; + if (g_suppressForceEquipClips.load(std::memory_order_relaxed) <= 0) + return; + + std::string_view nm{a_this->animationName.c_str()}; + if (!IsEquipClip(nm)) + return; + + // Consume the flag immediately so only this specific clip instance is marked g_suppressForceEquipClips.store(0, std::memory_order_relaxed); + g_suppressedClip.store(a_this, std::memory_order_relaxed); + // Fast-forward the clip so the state machine transitions to "equip done" right + // now, unlocking attack inputs. float duration = 2.0f; - if (a_this->binding && a_this->binding->animation) { + if (a_this->binding && a_this->binding->animation) duration = a_this->binding->animation->duration; - } a_this->mode = RE::hkbClipGenerator::PlaybackMode::kModeSinglePlay; _Update(a_this, a_context, duration + 0.01f); a_this->atEnd = true; +} + +void EquipHook::Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context, float a_timestep) +{ + if (!a_this) + { + _Update(a_this, a_context, a_timestep); + return; + } + + // Suppress continued updates for the fast-forwarded clip. + // The graph will call Deactivate naturally when it transitions away. + if (a_this == g_suppressedClip.load(std::memory_order_relaxed) && a_this->atEnd) + return; + + _Update(a_this, a_context, a_timestep); +} + +void EquipHook::Deactivate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context) +{ + // Release the suppressed-instance pointer when the graph is done with the clip. + if (a_this == g_suppressedClip.load(std::memory_order_relaxed)) + g_suppressedClip.store(nullptr, std::memory_order_relaxed); + + _Deactivate(a_this, a_context); +} + +void EquipHook::Generate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context) +{ + // Produce no bone transforms for the suppressed clip + if (a_this == g_suppressedClip.load(std::memory_order_relaxed)) + return; + + _Generate(a_this, a_context); } \ No newline at end of file diff --git a/src/hook.h b/src/hook.h index a1c2481..469facc 100644 --- a/src/hook.h +++ b/src/hook.h @@ -1,6 +1,5 @@ #pragma once - -using EventChecker = RE::BSEventNotifyControl; +#include "event.h" class EquipHook { @@ -8,27 +7,34 @@ class EquipHook static void Hook() { REL::Relocation PlayerCharacterVtbl{RE::VTABLE_PlayerCharacter[0]}; - //REL::Relocation NonPlayerCharacterVtbl{ RE::VTABLE_Actor[0] }; REL::Relocation vtbl{RE::VTABLE_hkbClipGenerator[0]}; _OnEquipItemPC = PlayerCharacterVtbl.write_vfunc(0xB2, OnEquipItemPC); - //_OnEquipItemNPC = NonPlayerCharacterVtbl.write_vfunc(0xB2, OnEquipItemNPC); - _Update = vtbl.write_vfunc(0x05, Update_Hook); _Activate = vtbl.write_vfunc(0x04, Activate_Hook); + _Update = vtbl.write_vfunc(0x05, Update_Hook); + _Deactivate = vtbl.write_vfunc(0x07, Deactivate_Hook); + _Generate = vtbl.write_vfunc(0x17, Generate_Hook); } private: + enum class EquipMode + { + Normal, // play animation normally + Skip, // playAnim=false + delayed SendEquipEvents + InstantAnim, // playAnim=true, fast-forward clip, suppress Generate + }; + static void OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim); - //static void OnEquipItemNPC(RE::Actor* a_this, bool a_playAnim); - static void Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context, float a_timestep); static void Activate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); + static void Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context, float a_timestep); + static void Deactivate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); + static void Generate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); + + static EquipMode GetEquipMode(RE::PlayerCharacter *a_this, bool a_playAnim); static inline REL::Relocation _OnEquipItemPC; - //static inline REL::Relocation _OnEquipItemNPC; - static inline REL::Relocation _Update; static inline REL::Relocation _Activate; - - static bool SkipAnim(RE::PlayerCharacter *a_this, bool a_playAnim); - - static inline bool _skipAnim; + static inline REL::Relocation _Update; + static inline REL::Relocation _Deactivate; + static inline REL::Relocation _Generate; }; \ No newline at end of file From 2eb62e96442edd54b5d892d6036f0379b54e971c Mon Sep 17 00:00:00 2001 From: igora Date: Sun, 8 Mar 2026 23:24:23 -0300 Subject: [PATCH 03/11] fix(Sink): :bug: Added the event sink back on the plugin.cpp and make the instaEquipAnimation path send a_playAnim instead of always "true" --- src/hook.cpp | 2 +- src/plugin.cpp | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/hook.cpp b/src/hook.cpp index 5a4b9e5..9e25bb1 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -132,7 +132,7 @@ void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) // normally and attack inputs are unlocked. Activate_Hook will fast-forward // the clip and Generate_Hook will suppress bone movement for that instance. g_suppressForceEquipClips.store(1, std::memory_order_relaxed); - _OnEquipItemPC(a_this, true); + _OnEquipItemPC(a_this, a_playAnim); break; default: diff --git a/src/plugin.cpp b/src/plugin.cpp index d548917..65010c1 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -1,23 +1,36 @@ #include "log.h" #include "hook.h" -void MessageHandler(SKSE::MessagingInterface::Message* a_msg) +void MessageHandler(SKSE::MessagingInterface::Message *a_msg) { - if(a_msg->type == SKSE::MessagingInterface::kDataLoaded) { + switch (a_msg->type) + { + case SKSE::MessagingInterface::kDataLoaded: EquipHook::Hook(); + break; + case SKSE::MessagingInterface::kPostLoad: + break; + case SKSE::MessagingInterface::kPreLoadGame: + case SKSE::MessagingInterface::kPostLoadGame: + case SKSE::MessagingInterface::kNewGame: + if (auto animationEventTracker = AnimationEventTracker::GetSingleton(); animationEventTracker) + animationEventTracker->Register(); + break; } } -SKSEPluginLoad(const SKSE::LoadInterface *skse) { +SKSEPluginLoad(const SKSE::LoadInterface *skse) +{ SetupLog(); - auto* plugin = SKSE::PluginDeclaration::GetSingleton(); + auto *plugin = SKSE::PluginDeclaration::GetSingleton(); spdlog::info("{} v{} is loading...", plugin->GetName(), plugin->GetVersion()); SKSE::Init(skse); auto messaging = SKSE::GetMessagingInterface(); - if (!messaging->RegisterListener("SKSE", MessageHandler)) { + if (!messaging->RegisterListener("SKSE", MessageHandler)) + { return false; } From 1e4878aedd544211a376f8aa0aa80da6b3f30f2c Mon Sep 17 00:00:00 2001 From: igora Date: Sun, 8 Mar 2026 23:27:27 -0300 Subject: [PATCH 04/11] fix(include): :bug: I forgot to include event.h on the plugin.cpp --- src/plugin.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugin.cpp b/src/plugin.cpp index 65010c1..6c18b14 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -1,5 +1,6 @@ #include "log.h" #include "hook.h" +#include "event.h" void MessageHandler(SKSE::MessagingInterface::Message *a_msg) { From f857e9f6edbbee3ed8457ba90a18442a484dc87b Mon Sep 17 00:00:00 2001 From: Igor Alan Albuquerque de Sousa <50077829+IgorAlanAlbuquerque@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:30:00 -0300 Subject: [PATCH 05/11] Fix JSON syntax for InstantEquipAnim property --- .../BehaviorDataInjector/SkipEquipAnimation_BDI.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SKSE/Plugins/BehaviorDataInjector/SkipEquipAnimation_BDI.json b/SKSE/Plugins/BehaviorDataInjector/SkipEquipAnimation_BDI.json index 77d5fd3..272059a 100644 --- a/SKSE/Plugins/BehaviorDataInjector/SkipEquipAnimation_BDI.json +++ b/SKSE/Plugins/BehaviorDataInjector/SkipEquipAnimation_BDI.json @@ -6,6 +6,13 @@ "value": false }, + { + "projectPath": "actors\\Character", + "type": "kBool", + "name" "InstantEquipAnim", + "value": false + }, + { "projectPath": "actors\\Character", "type": "kInt", @@ -19,4 +26,4 @@ "name": "Skip3DLoading", "value": false } -] \ No newline at end of file +] From 18474d0581d451c091e7a75a6f6623bcc477ed4e Mon Sep 17 00:00:00 2001 From: igora Date: Wed, 11 Mar 2026 17:59:36 -0300 Subject: [PATCH 06/11] fix(version): :bookmark: Put the correct version on the cmakelists --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dd69398..e60a525 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.21) # Set your project name. This will be the name of your SKSE .dll file. -project(SkipEquipAnimation VERSION 1.0.4 LANGUAGES CXX) +project(SkipEquipAnimation VERSION 1.0.5 LANGUAGES CXX) set(ADD_COMMONLIBSSE_PLUGIN_AUTHOR "AnArchos" CACHE STRING "Plugin author") set(ADD_COMMONLIBSSE_PLUGIN_EMAIL "patreon.com/AnArchos" CACHE STRING "Plugin email") From 9aab443a1fe1fc24e86a06f2fa421eb71e3d8348 Mon Sep 17 00:00:00 2001 From: igora Date: Mon, 16 Mar 2026 17:38:32 -0300 Subject: [PATCH 07/11] fix(GetMode): :bug: Fixed a bug that causes to use skip equip animation mode when the "a_playerAnim" is false. --- src/hook.cpp | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/hook.cpp b/src/hook.cpp index 9e25bb1..312b8ca 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -60,32 +60,18 @@ static void SendEquipEvents(RE::Actor *a_this, EquipHook::EquipMode EquipHook::GetEquipMode(RE::PlayerCharacter *a_this, bool a_playAnim) { - if (!a_playAnim) - return EquipMode::Skip; - - if (!a_this || !a_this->AsActorState() || !a_this->AsActorState()->IsWeaponDrawn()) - return EquipMode::Normal; - - auto *rHandObj = a_this->GetEquippedObject(false); - auto *lHandObj = a_this->GetEquippedObject(true); - - if ((lHandObj && rHandObj) && !CheckIsValidBoundObject(lHandObj) && !CheckIsValidBoundObject(rHandObj)) - return EquipMode::Normal; - - // Set this variable from your own mod/Papyrus whenever you want the - // equip clip to be driven through the state machine instantly with - // zero visual movement (inputs unlocked immediately). - bool instantAnim = false; - a_this->GetGraphVariableBool("InstantEquipAnim", instantAnim); - if (instantAnim) - return EquipMode::InstantAnim; - - // Uses playAnim=false so the graph never plays the clip at all. - bool skipAnim = false; - a_this->GetGraphVariableBool("SkipEquipAnimation", skipAnim); - if (skipAnim) - return EquipMode::Skip; - + if (a_this) + { + bool instantAnim = false; + a_this->GetGraphVariableBool("InstantEquipAnim", instantAnim); + if (instantAnim && a_playAnim) + return EquipMode::InstantAnim; + + bool skipAnim = false; + a_this->GetGraphVariableBool("SkipEquipAnimation", skipAnim); + if (skipAnim && a_playAnim) + return EquipMode::Skip; + } return EquipMode::Normal; } From c2145e7a74ba0e449019eb029063e9ebd638a316 Mon Sep 17 00:00:00 2001 From: igora Date: Mon, 16 Mar 2026 17:41:46 -0300 Subject: [PATCH 08/11] docs(Version): :bookmark: Put the version as 1.0.6 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e60a525..e243a3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.21) # Set your project name. This will be the name of your SKSE .dll file. -project(SkipEquipAnimation VERSION 1.0.5 LANGUAGES CXX) +project(SkipEquipAnimation VERSION 1.0.6 LANGUAGES CXX) set(ADD_COMMONLIBSSE_PLUGIN_AUTHOR "AnArchos" CACHE STRING "Plugin author") set(ADD_COMMONLIBSSE_PLUGIN_EMAIL "patreon.com/AnArchos" CACHE STRING "Plugin email") From 8413e3f378dfae7e5a778fa5e18503b3e9f474ad Mon Sep 17 00:00:00 2001 From: igora Date: Tue, 17 Mar 2026 23:37:55 -0300 Subject: [PATCH 09/11] fix(hook): :bug: Remove the hook on generate of the hkbClipGenerator The hook in generate was used to prevent any bone transformations from being executed, but this sometimes caused the character to disappear for a moment due to no animation being performed. Now, the acceleration update allows bone transformations. --- CMakeLists.txt | 2 +- src/hook.cpp | 110 +++++++++++++++++++++++++++++-------------------- src/hook.h | 4 +- src/plugin.cpp | 1 + 4 files changed, 69 insertions(+), 48 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e243a3a..aafa058 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.21) # Set your project name. This will be the name of your SKSE .dll file. -project(SkipEquipAnimation VERSION 1.0.6 LANGUAGES CXX) +project(SkipEquipAnimation VERSION 1.0.7 LANGUAGES CXX) set(ADD_COMMONLIBSSE_PLUGIN_AUTHOR "AnArchos" CACHE STRING "Plugin author") set(ADD_COMMONLIBSSE_PLUGIN_EMAIL "patreon.com/AnArchos" CACHE STRING "Plugin email") diff --git a/src/hook.cpp b/src/hook.cpp index 312b8ca..281dc34 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -12,15 +12,20 @@ using namespace Util; namespace { + static inline std::vector g_pendingClips; + static inline std::vector g_suppressedClips; static inline std::atomic g_suppressForceEquipClips{0}; - - // The specific hkbClipGenerator instance being suppressed on the SkipInstant path - static inline std::atomic g_suppressedClip{nullptr}; + static inline RE::hkbCharacter *g_playerHkbCharacter{nullptr}; static bool IsEquipClip(std::string_view nm) { return Util::String::iContains(nm, "Equip"); } + + static bool IsPlayerClip(const RE::hkbContext &a_context) + { + return a_context.character == g_playerHkbCharacter; + } } // --------------------------------------------------------------------------- @@ -47,7 +52,8 @@ static void SendEquipEvents(RE::Actor *a_this, tracker->SendAnimationEvent(a_this, "weaponDraw"); - if ((a_lHandObject && (a_lHandObject->IsWeapon() || a_lHandObject->IsArmor())) || (a_rHandObject && (a_rHandObject->IsWeapon() || a_rHandObject->IsArmor()))) + if ((a_lHandObject && (a_lHandObject->IsWeapon() || a_lHandObject->IsArmor())) || + (a_rHandObject && (a_rHandObject->IsWeapon() || a_rHandObject->IsArmor()))) { tracker->SendAnimationEvent(a_this, "WeapEquip_Out"); tracker->SendAnimationEvent(a_this, "WeapEquip_OutMoving"); @@ -85,8 +91,6 @@ void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) { case EquipMode::Skip: { - // Original mod path: suppress the animation entirely by passing false. - // Then send the weapon-ready events after a short delay so the 3-D model auto *tracker = AnimationEventTracker::GetSingleton(); tracker->Register(); @@ -114,12 +118,16 @@ void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) } case EquipMode::InstantAnim: - // New path: let the graph run (playAnim=true) so state transitions happen - // normally and attack inputs are unlocked. Activate_Hook will fast-forward - // the clip and Generate_Hook will suppress bone movement for that instance. + { + RE::BSAnimationGraphManagerPtr graphManager; + a_this->GetAnimationGraphManager(graphManager); + if (graphManager && !graphManager->graphs.empty()) + g_playerHkbCharacter = &graphManager->graphs[0]->characterInstance; + g_suppressForceEquipClips.store(1, std::memory_order_relaxed); _OnEquipItemPC(a_this, a_playAnim); break; + } default: _OnEquipItemPC(a_this, a_playAnim); @@ -130,65 +138,79 @@ void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) // --------------------------------------------------------------------------- // Vtable hooks: hkbClipGenerator // --------------------------------------------------------------------------- - void EquipHook::Activate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context) { _Activate(a_this, a_context); - if (!a_this) - return; - if (g_suppressForceEquipClips.load(std::memory_order_relaxed) <= 0) + if (!a_this || !IsPlayerClip(a_context)) return; - std::string_view nm{a_this->animationName.c_str()}; - if (!IsEquipClip(nm)) + if (g_suppressForceEquipClips.load(std::memory_order_relaxed) <= 0 || + !IsEquipClip(std::string_view{a_this->animationName.c_str()})) return; - // Consume the flag immediately so only this specific clip instance is marked - g_suppressForceEquipClips.store(0, std::memory_order_relaxed); - g_suppressedClip.store(a_this, std::memory_order_relaxed); - - // Fast-forward the clip so the state machine transitions to "equip done" right - // now, unlocking attack inputs. - float duration = 2.0f; - if (a_this->binding && a_this->binding->animation) - duration = a_this->binding->animation->duration; - - a_this->mode = RE::hkbClipGenerator::PlaybackMode::kModeSinglePlay; - _Update(a_this, a_context, duration + 0.01f); - a_this->atEnd = true; + g_pendingClips.push_back(a_this); } void EquipHook::Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context, float a_timestep) { - if (!a_this) + if (a_this) { - _Update(a_this, a_context, a_timestep); - return; - } - // Suppress continued updates for the fast-forwarded clip. - // The graph will call Deactivate naturally when it transitions away. - if (a_this == g_suppressedClip.load(std::memory_order_relaxed) && a_this->atEnd) - return; + for (auto *clip : g_suppressedClips) + if (clip == a_this && a_this->atEnd) + return; + + auto it = std::find(g_pendingClips.begin(), g_pendingClips.end(), a_this); + if (it != g_pendingClips.end()) + { + g_pendingClips.erase(it); + + float duration = 2.0f; + if (a_this->binding && a_this->binding->animation) + duration = a_this->binding->animation->duration; + a_this->mode = RE::hkbClipGenerator::PlaybackMode::kModeSinglePlay; + + _Update(a_this, a_context, duration + 0.01f); + + a_this->atEnd = true; + g_suppressedClips.push_back(a_this); + return; + } + } _Update(a_this, a_context, a_timestep); } void EquipHook::Deactivate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context) { - // Release the suppressed-instance pointer when the graph is done with the clip. - if (a_this == g_suppressedClip.load(std::memory_order_relaxed)) - g_suppressedClip.store(nullptr, std::memory_order_relaxed); + bool wasInAnyList = false; + + auto it = std::find(g_pendingClips.begin(), g_pendingClips.end(), a_this); + if (it != g_pendingClips.end()) + { + g_pendingClips.erase(it); + wasInAnyList = true; + } + + auto it2 = std::find(g_suppressedClips.begin(), g_suppressedClips.end(), a_this); + if (it2 != g_suppressedClips.end()) + { + g_suppressedClips.erase(it2); + wasInAnyList = true; + } + + if (wasInAnyList && g_suppressedClips.empty() && g_pendingClips.empty()) + g_suppressForceEquipClips.store(0, std::memory_order_relaxed); _Deactivate(a_this, a_context); } -void EquipHook::Generate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context) +void EquipHook::ResetState() { - // Produce no bone transforms for the suppressed clip - if (a_this == g_suppressedClip.load(std::memory_order_relaxed)) - return; + g_pendingClips.clear(); + g_suppressedClips.clear(); + g_suppressForceEquipClips.store(0, std::memory_order_relaxed); - _Generate(a_this, a_context); + g_playerHkbCharacter = nullptr; } \ No newline at end of file diff --git a/src/hook.h b/src/hook.h index 469facc..da2118c 100644 --- a/src/hook.h +++ b/src/hook.h @@ -13,8 +13,8 @@ class EquipHook _Activate = vtbl.write_vfunc(0x04, Activate_Hook); _Update = vtbl.write_vfunc(0x05, Update_Hook); _Deactivate = vtbl.write_vfunc(0x07, Deactivate_Hook); - _Generate = vtbl.write_vfunc(0x17, Generate_Hook); } + static void ResetState(); private: enum class EquipMode @@ -28,7 +28,6 @@ class EquipHook static void Activate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); static void Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context, float a_timestep); static void Deactivate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); - static void Generate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); static EquipMode GetEquipMode(RE::PlayerCharacter *a_this, bool a_playAnim); @@ -36,5 +35,4 @@ class EquipHook static inline REL::Relocation _Activate; static inline REL::Relocation _Update; static inline REL::Relocation _Deactivate; - static inline REL::Relocation _Generate; }; \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index 6c18b14..885577c 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -14,6 +14,7 @@ void MessageHandler(SKSE::MessagingInterface::Message *a_msg) case SKSE::MessagingInterface::kPreLoadGame: case SKSE::MessagingInterface::kPostLoadGame: case SKSE::MessagingInterface::kNewGame: + EquipHook::ResetState(); if (auto animationEventTracker = AnimationEventTracker::GetSingleton(); animationEventTracker) animationEventTracker->Register(); break; From a240d35dbd9c4cf5dba510903b2329ef1fd88154 Mon Sep 17 00:00:00 2001 From: igora Date: Tue, 28 Apr 2026 14:18:25 -0300 Subject: [PATCH 10/11] feat(DrawWeapon): :sparkles: InstantEquipAnim now can skip draw weapon and sheath weapon animations --- CMakeLists.txt | 2 +- src/hook.cpp | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aafa058..2ec9d26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.21) # Set your project name. This will be the name of your SKSE .dll file. -project(SkipEquipAnimation VERSION 1.0.7 LANGUAGES CXX) +project(SkipEquipAnimation VERSION 1.0.8 LANGUAGES CXX) set(ADD_COMMONLIBSSE_PLUGIN_AUTHOR "AnArchos" CACHE STRING "Plugin author") set(ADD_COMMONLIBSSE_PLUGIN_EMAIL "patreon.com/AnArchos" CACHE STRING "Plugin email") diff --git a/src/hook.cpp b/src/hook.cpp index 281dc34..c176fcf 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -14,17 +14,21 @@ namespace { static inline std::vector g_pendingClips; static inline std::vector g_suppressedClips; - static inline std::atomic g_suppressForceEquipClips{0}; - static inline RE::hkbCharacter *g_playerHkbCharacter{nullptr}; static bool IsEquipClip(std::string_view nm) { return Util::String::iContains(nm, "Equip"); } - static bool IsPlayerClip(const RE::hkbContext &a_context) + RE::Actor *GetActorFromContext(const RE::hkbContext &a_context) { - return a_context.character == g_playerHkbCharacter; + if (!a_context.character) + return nullptr; + + const auto *graph = reinterpret_cast( + reinterpret_cast(a_context.character) - 0xC0); + + return graph->holder; } } @@ -119,12 +123,6 @@ void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) case EquipMode::InstantAnim: { - RE::BSAnimationGraphManagerPtr graphManager; - a_this->GetAnimationGraphManager(graphManager); - if (graphManager && !graphManager->graphs.empty()) - g_playerHkbCharacter = &graphManager->graphs[0]->characterInstance; - - g_suppressForceEquipClips.store(1, std::memory_order_relaxed); _OnEquipItemPC(a_this, a_playAnim); break; } @@ -142,11 +140,19 @@ void EquipHook::Activate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext { _Activate(a_this, a_context); - if (!a_this || !IsPlayerClip(a_context)) + if (!a_this) + return; + + auto *player = RE::PlayerCharacter::GetSingleton(); + if (GetActorFromContext(a_context) != player) return; - if (g_suppressForceEquipClips.load(std::memory_order_relaxed) <= 0 || - !IsEquipClip(std::string_view{a_this->animationName.c_str()})) + if (!IsEquipClip(std::string_view{a_this->animationName.c_str()})) + return; + + bool instantAnim = false; + player->GetGraphVariableBool("InstantEquipAnim", instantAnim); + if (!instantAnim) return; g_pendingClips.push_back(a_this); @@ -200,9 +206,6 @@ void EquipHook::Deactivate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbConte wasInAnyList = true; } - if (wasInAnyList && g_suppressedClips.empty() && g_pendingClips.empty()) - g_suppressForceEquipClips.store(0, std::memory_order_relaxed); - _Deactivate(a_this, a_context); } @@ -210,7 +213,4 @@ void EquipHook::ResetState() { g_pendingClips.clear(); g_suppressedClips.clear(); - g_suppressForceEquipClips.store(0, std::memory_order_relaxed); - - g_playerHkbCharacter = nullptr; } \ No newline at end of file From e4dcb5dc8e6fd377cd93fc193b72ae45798174e3 Mon Sep 17 00:00:00 2001 From: Igor Alan Albuquerque de Sousa Date: Thu, 14 May 2026 01:25:30 -0300 Subject: [PATCH 11/11] fix(InstatEquip): :bug: Fixed a bug where some cases the actor get stuck in the last frame of the equip animation Make the fastfoward happens again with clips in the surpress vector in case they get atEnd reseted --- CMakeLists.txt | 2 +- src/event.cpp | 2 ++ src/hook.cpp | 67 ++++++++++++++++++++++++++++---------------------- src/hook.h | 2 +- 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ec9d26..1d1c53e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.21) # Set your project name. This will be the name of your SKSE .dll file. -project(SkipEquipAnimation VERSION 1.0.8 LANGUAGES CXX) +project(SkipEquipAnimation VERSION 1.0.9 LANGUAGES CXX) set(ADD_COMMONLIBSSE_PLUGIN_AUTHOR "AnArchos" CACHE STRING "Plugin author") set(ADD_COMMONLIBSSE_PLUGIN_EMAIL "patreon.com/AnArchos" CACHE STRING "Plugin email") diff --git a/src/event.cpp b/src/event.cpp index b65dae6..8cad370 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -29,6 +29,8 @@ bool AnimationEventTracker::Register() if (!eventSource) { spdlog::info("Failed to register {}", typeid(RE::BSAnimationGraphEvent).name()); + }else{ + spdlog::info("Event Source registered {}", typeid(RE::BSAnimationGraphEvent).name()); } } return foundEventSource; diff --git a/src/hook.cpp b/src/hook.cpp index c176fcf..f9f13e7 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -15,7 +15,7 @@ namespace static inline std::vector g_pendingClips; static inline std::vector g_suppressedClips; - static bool IsEquipClip(std::string_view nm) + bool IsEquipClip(std::string_view nm) { return Util::String::iContains(nm, "Equip"); } @@ -47,8 +47,8 @@ static bool CheckIsValidBoundObject(const RE::TESForm *a_object) // Sends the animation events that normally fire at the end of an equip animation. static void SendEquipEvents(RE::Actor *a_this, - RE::TESForm *a_lHandObject, - RE::TESForm *a_rHandObject) + RE::TESForm const *a_lHandObject, + RE::TESForm const *a_rHandObject) { auto *tracker = AnimationEventTracker::GetSingleton(); if (!tracker) @@ -68,21 +68,22 @@ static void SendEquipEvents(RE::Actor *a_this, // Mode detection // --------------------------------------------------------------------------- -EquipHook::EquipMode EquipHook::GetEquipMode(RE::PlayerCharacter *a_this, bool a_playAnim) +EquipHook::EquipMode EquipHook::GetEquipMode(const RE::PlayerCharacter *a_this, bool a_playAnim) { + using enum EquipHook::EquipMode; if (a_this) { bool instantAnim = false; a_this->GetGraphVariableBool("InstantEquipAnim", instantAnim); if (instantAnim && a_playAnim) - return EquipMode::InstantAnim; + return InstantAnim; bool skipAnim = false; a_this->GetGraphVariableBool("SkipEquipAnimation", skipAnim); if (skipAnim && a_playAnim) - return EquipMode::Skip; + return Skip; } - return EquipMode::Normal; + return Normal; } // --------------------------------------------------------------------------- @@ -98,8 +99,8 @@ void EquipHook::OnEquipItemPC(RE::PlayerCharacter *a_this, bool a_playAnim) auto *tracker = AnimationEventTracker::GetSingleton(); tracker->Register(); - auto *rHandObj = a_this->GetEquippedObject(false); - auto *lHandObj = a_this->GetEquippedObject(true); + auto const *rHandObj = a_this->GetEquippedObject(false); + auto const *lHandObj = a_this->GetEquippedObject(true); int delay = 300; bool skip3D = false; @@ -143,11 +144,11 @@ void EquipHook::Activate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext if (!a_this) return; - auto *player = RE::PlayerCharacter::GetSingleton(); + auto const *player = RE::PlayerCharacter::GetSingleton(); if (GetActorFromContext(a_context) != player) return; - if (!IsEquipClip(std::string_view{a_this->animationName.c_str()})) + if (const std::string_view name{a_this->animationName.c_str()}; !IsEquipClip(name)) return; bool instantAnim = false; @@ -162,24 +163,36 @@ void EquipHook::Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext & { if (a_this) { - - for (auto *clip : g_suppressedClips) - if (clip == a_this && a_this->atEnd) + for (auto const *clip : g_suppressedClips) + { + if (clip == a_this) + { + if (a_this->atEnd) + return; + + float duration = 2.0f; + if (a_this->binding && a_this->binding->animation) + duration = a_this->binding->animation->duration; + + a_this->mode = RE::hkbClipGenerator::PlaybackMode::kModeSinglePlay; + _Update(a_this, a_context, duration + 0.01f); + a_this->atEnd = true; return; + } + } - auto it = std::find(g_pendingClips.begin(), g_pendingClips.end(), a_this); - if (it != g_pendingClips.end()) + if (auto it = std::ranges::find(g_pendingClips, a_this); it != g_pendingClips.end()) { g_pendingClips.erase(it); float duration = 2.0f; if (a_this->binding && a_this->binding->animation) duration = a_this->binding->animation->duration; - a_this->mode = RE::hkbClipGenerator::PlaybackMode::kModeSinglePlay; + a_this->mode = RE::hkbClipGenerator::PlaybackMode::kModeSinglePlay; _Update(a_this, a_context, duration + 0.01f); - a_this->atEnd = true; + g_suppressedClips.push_back(a_this); return; } @@ -190,20 +203,16 @@ void EquipHook::Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext & void EquipHook::Deactivate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context) { - bool wasInAnyList = false; - - auto it = std::find(g_pendingClips.begin(), g_pendingClips.end(), a_this); - if (it != g_pendingClips.end()) - { + if (auto it = std::ranges::find(g_pendingClips, a_this); it != g_pendingClips.end()) g_pendingClips.erase(it); - wasInAnyList = true; - } - auto it2 = std::find(g_suppressedClips.begin(), g_suppressedClips.end(), a_this); - if (it2 != g_suppressedClips.end()) - { + if (auto it2 = std::ranges::find(g_suppressedClips, a_this); it2 != g_suppressedClips.end()) g_suppressedClips.erase(it2); - wasInAnyList = true; + + if (!a_this) + { + _Deactivate(a_this, a_context); + return; } _Deactivate(a_this, a_context); diff --git a/src/hook.h b/src/hook.h index da2118c..0c56b98 100644 --- a/src/hook.h +++ b/src/hook.h @@ -29,7 +29,7 @@ class EquipHook static void Update_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context, float a_timestep); static void Deactivate_Hook(RE::hkbClipGenerator *a_this, const RE::hkbContext &a_context); - static EquipMode GetEquipMode(RE::PlayerCharacter *a_this, bool a_playAnim); + static EquipMode GetEquipMode(const RE::PlayerCharacter *a_this, bool a_playAnim); static inline REL::Relocation _OnEquipItemPC; static inline REL::Relocation _Activate;