From 0da7bb017c541190d8bb9ca7450298b89abebfdd Mon Sep 17 00:00:00 2001 From: northaxosky Date: Wed, 6 May 2026 12:10:54 -0700 Subject: [PATCH 01/10] feat: add Muzzle Flash Light, Hi-Res Bloom, Alt-Tab Fullscreen, Power Grid Scrap - Muzzle Flash Light: detour MuzzleFlash::UpdateLight to call SetAppCulled on the attached NiPointLight when the flash ends. Concept from WallSoGB/Fallout-Muzzle-Flash-Fix. - Hi-Res Bloom: pattern-scan BSShaderRenderTargets::Create and rewrite the hardcoded shr-by-2 imm8 per nBloomScale (1=full, 2=half, 4=vanilla, 8=eighth). Concept from WallSoGB/Fallout-High-Res-Bloom. Opt-in default. - Alt-Tab Fullscreen: IAT-detour D3D11CreateDeviceAndSwapChain, install MakeWindowAssociation(NO_WINDOW_CHANGES|NO_ALT_ENTER), chain a WndProc via SetWindowLongPtrA that drops exclusive fullscreen on focus loss. - Power Grid Scrap: hook TESObjectREFR::SetWantsDelete; if the deleted ref is workshop-linked and present in the workshop's powerGrid (V1170 IsItemPresentInWorkshop check), call Workshop::DeleteWorkshopItem before the original SetWantsDelete proceeds. Concept from SUP F4SE V1170 by Tomm (MIT). Bumps commonlibf4 to fix/inline-powerutils-helpers (PR #8 upstream); needed because two TUs now include RE/P/PowerUtils.h transitively. --- .Build/F4SE/Plugins/Addictol.toml | 15 ++ .../Modules/AdModuleAltTabFullscreen.h | 19 +++ .../Include/Modules/AdModuleHighResBloom.h | 19 +++ .../Modules/AdModuleMuzzleFlashLight.h | 19 +++ .../Include/Modules/AdModulePowerGridScrap.h | 19 +++ Addictol/Source/AdRegisterModules.cpp | 12 ++ .../Modules/AdModuleAltTabFullscreen.cpp | 126 +++++++++++++++ .../Source/Modules/AdModuleHighResBloom.cpp | 75 +++++++++ .../Modules/AdModuleMuzzleFlashLight.cpp | 53 +++++++ .../Source/Modules/AdModulePowerGridScrap.cpp | 146 ++++++++++++++++++ Depends/commonlibf4 | 2 +- VC/Addictol.vcxproj | 8 + VC/Addictol.vcxproj.filters | 24 +++ Version/build_version.txt | Bin 12 -> 12 bytes Version/resource_version2.h | Bin 2004 -> 2004 bytes 15 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 Addictol/Include/Modules/AdModuleAltTabFullscreen.h create mode 100644 Addictol/Include/Modules/AdModuleHighResBloom.h create mode 100644 Addictol/Include/Modules/AdModuleMuzzleFlashLight.h create mode 100644 Addictol/Include/Modules/AdModulePowerGridScrap.h create mode 100644 Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp create mode 100644 Addictol/Source/Modules/AdModuleHighResBloom.cpp create mode 100644 Addictol/Source/Modules/AdModuleMuzzleFlashLight.cpp create mode 100644 Addictol/Source/Modules/AdModulePowerGridScrap.cpp diff --git a/.Build/F4SE/Plugins/Addictol.toml b/.Build/F4SE/Plugins/Addictol.toml index 229bcec..5994216 100644 --- a/.Build/F4SE/Plugins/Addictol.toml +++ b/.Build/F4SE/Plugins/Addictol.toml @@ -53,6 +53,9 @@ bSaveAddedSoundCategories = true # Blocks the use of incorrect COM interface initialization settings for mods. bCOMInit = true +# Raises the bloom render-target resolution to reduce flicker on bright pixels (sun glints, neon, fire). Cost scales with nBloomScale. +bHighResBloom = false + [Fixes] @@ -150,6 +153,15 @@ bAnimSignedCrash = true # Fixes a startup CTD on non-English Windows installs caused by Bethesda.net response headers containing non-ASCII characters. bBethesdaNetCrash = true +# Fixes a bug where the muzzle-flash light keeps illuminating the scene after the flash ends. +bMuzzleFlashLight = true + +# Fixes the exclusive-fullscreen Alt-Tab hang by dropping fullscreen state on focus loss and blocking DXGI's auto Alt+Enter handler. +bAltTabFullscreen = true + +# Fixes a CTD when scrapping or wiring after a settlement mod has been removed, by cleaning up orphan power-grid entries left behind by deleted references. +bPowerGridScrap = true + [Warnings] @@ -192,6 +204,9 @@ bIgnorePreInstallBias = false # Delay (ms) before the deferred quit-to-desktop flag is set. Lets the UI/menu unwind so cleanup can't deadlock (needs bSafeExit fix). nQuitGameDelayMs = 1000 +# Bloom render-target downsample factor (needs bHighResBloom). 1 = full screen (highest quality, highest GPU cost), 2 = half (recommended), 4 = vanilla quarter, 8 = eighth. +nBloomScale = 2 + [Profiler] diff --git a/Addictol/Include/Modules/AdModuleAltTabFullscreen.h b/Addictol/Include/Modules/AdModuleAltTabFullscreen.h new file mode 100644 index 0000000..2181a5d --- /dev/null +++ b/Addictol/Include/Modules/AdModuleAltTabFullscreen.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleAltTabFullscreen : + public Module + { + public: + ModuleAltTabFullscreen(); + virtual ~ModuleAltTabFullscreen() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Include/Modules/AdModuleHighResBloom.h b/Addictol/Include/Modules/AdModuleHighResBloom.h new file mode 100644 index 0000000..cf07305 --- /dev/null +++ b/Addictol/Include/Modules/AdModuleHighResBloom.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleHighResBloom : + public Module + { + public: + ModuleHighResBloom(); + virtual ~ModuleHighResBloom() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Include/Modules/AdModuleMuzzleFlashLight.h b/Addictol/Include/Modules/AdModuleMuzzleFlashLight.h new file mode 100644 index 0000000..5927096 --- /dev/null +++ b/Addictol/Include/Modules/AdModuleMuzzleFlashLight.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleMuzzleFlashLight : + public Module + { + public: + ModuleMuzzleFlashLight(); + virtual ~ModuleMuzzleFlashLight() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Include/Modules/AdModulePowerGridScrap.h b/Addictol/Include/Modules/AdModulePowerGridScrap.h new file mode 100644 index 0000000..700791a --- /dev/null +++ b/Addictol/Include/Modules/AdModulePowerGridScrap.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModulePowerGridScrap : + public Module + { + public: + ModulePowerGridScrap(); + virtual ~ModulePowerGridScrap() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Source/AdRegisterModules.cpp b/Addictol/Source/AdRegisterModules.cpp index a827a11..3b73c28 100644 --- a/Addictol/Source/AdRegisterModules.cpp +++ b/Addictol/Source/AdRegisterModules.cpp @@ -52,6 +52,10 @@ #include #include #include +#include +#include +#include +#include // Create patches static auto sModuleThreads = std::make_shared(); @@ -106,6 +110,10 @@ static auto sModuleWorkbenchSound = std::make_shared(); static auto sModuleAnimSignedCrash = std::make_shared(); static auto sModuleBethesdaNetCrash = std::make_shared(); +static auto sModuleMuzzleFlashLight = std::make_shared(); +static auto sModuleHighResBloom = std::make_shared(); +static auto sModuleAltTabFullscreen = std::make_shared(); +static auto sModulePowerGridScrap = std::make_shared(); void AdRegisterPreloadModules() { @@ -174,6 +182,10 @@ void AdRegisterModules() modules.Register(sModuleActorCauseSaveBloat); modules.Register(sModuleAnimSignedCrash); modules.Register(sModuleBethesdaNetCrash); + modules.Register(sModuleMuzzleFlashLight); + modules.Register(sModuleHighResBloom); + modules.Register(sModuleAltTabFullscreen); + modules.Register(sModulePowerGridScrap); // Registers other patches modules.Register(sModuleThreads, kGameDataReady); diff --git a/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp b/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp new file mode 100644 index 0000000..1f40a81 --- /dev/null +++ b/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp @@ -0,0 +1,126 @@ +#include +#include + +#include +#include +#include + +namespace Addictol +{ + static REX::TOML::Bool<> bFixesAltTabFullscreen{ "Fixes"sv, "bAltTabFullscreen"sv, true }; + + using TD3D11CreateDeviceAndSwapChain = HRESULT(WINAPI*)( + IDXGIAdapter*, D3D_DRIVER_TYPE, HMODULE, UINT, + const D3D_FEATURE_LEVEL*, UINT, UINT, + const DXGI_SWAP_CHAIN_DESC*, IDXGISwapChain**, + ID3D11Device**, D3D_FEATURE_LEVEL*, ID3D11DeviceContext**); + + static TD3D11CreateDeviceAndSwapChain OriginalCreate = nullptr; + static IDXGISwapChain* g_swapChain = nullptr; + static HWND g_hwnd = nullptr; + static WNDPROC g_origWndProc = nullptr; + + static LRESULT CALLBACK Hook_WndProc(HWND a_hwnd, UINT a_msg, WPARAM a_wp, LPARAM a_lp) noexcept + { + if (g_swapChain) + { + const bool focusLost = + a_msg == WM_KILLFOCUS || + (a_msg == WM_ACTIVATEAPP && a_wp == FALSE) || + (a_msg == WM_ACTIVATE && LOWORD(a_wp) == WA_INACTIVE); + + if (focusLost) + { + BOOL isFullscreen = FALSE; + if (SUCCEEDED(g_swapChain->GetFullscreenState(&isFullscreen, nullptr)) && isFullscreen) + g_swapChain->SetFullscreenState(FALSE, nullptr); + } + } + + if (g_origWndProc) + return CallWindowProcA(g_origWndProc, a_hwnd, a_msg, a_wp, a_lp); + return DefWindowProcA(a_hwnd, a_msg, a_wp, a_lp); + } + + static HRESULT WINAPI Hook_D3D11Create( + IDXGIAdapter* a_adapter, + D3D_DRIVER_TYPE a_driverType, + HMODULE a_software, + UINT a_flags, + const D3D_FEATURE_LEVEL* a_featureLevels, + UINT a_numFeatureLevels, + UINT a_sdkVersion, + const DXGI_SWAP_CHAIN_DESC* a_desc, + IDXGISwapChain** a_outSwapChain, + ID3D11Device** a_outDevice, + D3D_FEATURE_LEVEL* a_outFeatureLevel, + ID3D11DeviceContext** a_outContext) noexcept + { + const auto hr = OriginalCreate + ? OriginalCreate(a_adapter, a_driverType, a_software, a_flags, a_featureLevels, + a_numFeatureLevels, a_sdkVersion, a_desc, a_outSwapChain, a_outDevice, + a_outFeatureLevel, a_outContext) + : E_FAIL; + + if (FAILED(hr) || !a_outSwapChain || !*a_outSwapChain || !a_desc) + return hr; + + // AddRef so WndProc dispatched during engine teardown won't dereference a freed swap chain. + g_swapChain = *a_outSwapChain; + g_swapChain->AddRef(); + g_hwnd = a_desc->OutputWindow; + + IDXGIFactory* factory = nullptr; + if (SUCCEEDED(g_swapChain->GetParent(__uuidof(IDXGIFactory), reinterpret_cast(&factory))) && factory) + { + factory->MakeWindowAssociation(g_hwnd, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER); + factory->Release(); + } + + if (g_hwnd && !g_origWndProc) + { + if (auto* prior = reinterpret_cast(GetWindowLongPtrA(g_hwnd, GWLP_WNDPROC))) + { + g_origWndProc = prior; + SetWindowLongPtrA(g_hwnd, GWLP_WNDPROC, reinterpret_cast(&Hook_WndProc)); + } + } + + return hr; + } + + ModuleAltTabFullscreen::ModuleAltTabFullscreen() : + Module("Alt-Tab Fullscreen", &bFixesAltTabFullscreen) + {} + + bool ModuleAltTabFullscreen::DoQuery() const noexcept + { + return true; + } + + bool ModuleAltTabFullscreen::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + OriginalCreate = reinterpret_cast(RELEX::DetourIAT( + "d3d11.dll", + "D3D11CreateDeviceAndSwapChain", + reinterpret_cast(&Hook_D3D11Create))); + + if (!OriginalCreate) + { + REX::WARN("Alt-Tab Fullscreen: D3D11CreateDeviceAndSwapChain not found in IAT."sv); + return false; + } + + return true; + } + + bool ModuleAltTabFullscreen::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleAltTabFullscreen::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/Addictol/Source/Modules/AdModuleHighResBloom.cpp b/Addictol/Source/Modules/AdModuleHighResBloom.cpp new file mode 100644 index 0000000..d00eb93 --- /dev/null +++ b/Addictol/Source/Modules/AdModuleHighResBloom.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include +#include + +namespace Addictol +{ + static REX::TOML::Bool<> bPatchesHighResBloom{ "Patches"sv, "bHighResBloom"sv, false }; + static REX::TOML::I32<> nAdditionalBloomScale{ "Additional"sv, "nBloomScale"sv, 2 }; + + // Vanilla bloom RT downsample: shr esi, 2; shr r12d, 2 (width >> 2; height >> 2). + static constexpr std::array kVanillaPattern{ 0xC1, 0xEE, 0x02, 0x41, 0xC1, 0xEC, 0x02 }; + static constexpr std::size_t kScanRange = 0x500; + + ModuleHighResBloom::ModuleHighResBloom() : + Module("High Res Bloom", &bPatchesHighResBloom) + {} + + bool ModuleHighResBloom::DoQuery() const noexcept + { + return true; + } + + bool ModuleHighResBloom::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + const auto scale = nAdditionalBloomScale.GetValue(); + if (scale != 1 && scale != 2 && scale != 4 && scale != 8) + { + REX::WARN("[HighResBloom] nBloomScale must be 1, 2, 4, or 8 (got {}); skipping patch."sv, scale); + return false; + } + + if (scale == 4) + { + REX::INFO("[HighResBloom] nBloomScale = 4 (vanilla); no patch applied."sv); + return true; + } + + const auto base = REL::ID{ 1118299, 2318909 }.address(); + const auto* bytes = reinterpret_cast(base); + + const auto* hit = std::search(bytes, bytes + kScanRange, kVanillaPattern.begin(), kVanillaPattern.end()); + if (hit == bytes + kScanRange) + { + REX::WARN("[HighResBloom] Could not locate vanilla bloom shr pair within 0x{:X} bytes of base 0x{:X}."sv, kScanRange, base); + return false; + } + + const auto target = reinterpret_cast(hit); + if (scale == 1) + { + RELEX::WriteSafeNop(target, kVanillaPattern.size()); + } + else + { + const auto imm = static_cast(scale == 2 ? 1 : 3); + RELEX::WriteSafe(target, { 0xC1, 0xEE, imm }); + RELEX::WriteSafe(target + 3, { 0x41, 0xC1, 0xEC, imm }); + } + + REX::INFO("[HighResBloom] Patched bloom RT downsample at 0x{:X} (scale {})."sv, target, scale); + return true; + } + + bool ModuleHighResBloom::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleHighResBloom::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/Addictol/Source/Modules/AdModuleMuzzleFlashLight.cpp b/Addictol/Source/Modules/AdModuleMuzzleFlashLight.cpp new file mode 100644 index 0000000..ac0f30b --- /dev/null +++ b/Addictol/Source/Modules/AdModuleMuzzleFlashLight.cpp @@ -0,0 +1,53 @@ +#include +#include + +#include + +namespace Addictol +{ + static REX::TOML::Bool<> bFixesMuzzleFlashLight{ "Fixes"sv, "bMuzzleFlashLight"sv, true }; + + using TUpdateLight = void(__fastcall*)(void*, bool); + static TUpdateLight OriginalUpdateLight = nullptr; + + // MuzzleFlash layout (RE'd from OG/AE disasm): bEnabled @ +0x00, pLight @ +0x20. + static void __fastcall HookUpdateLight(void* a_this, bool a_alive) noexcept + { + auto* p = reinterpret_cast(a_this); + if (auto* light = *reinterpret_cast(p + 0x20)) + { + const bool enabled = *reinterpret_cast(p); + light->SetAppCulled(!enabled); + } + + if (OriginalUpdateLight) + OriginalUpdateLight(a_this, a_alive); + } + + ModuleMuzzleFlashLight::ModuleMuzzleFlashLight() : + Module("Muzzle Flash Light", &bFixesMuzzleFlashLight) + {} + + bool ModuleMuzzleFlashLight::DoQuery() const noexcept + { + return true; + } + + bool ModuleMuzzleFlashLight::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + const auto target = REL::ID{ 976501, 2236921 }.address(); + *reinterpret_cast(&OriginalUpdateLight) = + RELEX::DetourJump(target, reinterpret_cast(&HookUpdateLight)); + return OriginalUpdateLight != nullptr; + } + + bool ModuleMuzzleFlashLight::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleMuzzleFlashLight::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/Addictol/Source/Modules/AdModulePowerGridScrap.cpp b/Addictol/Source/Modules/AdModulePowerGridScrap.cpp new file mode 100644 index 0000000..50e4840 --- /dev/null +++ b/Addictol/Source/Modules/AdModulePowerGridScrap.cpp @@ -0,0 +1,146 @@ +#include +#include + +#include + +// Concept ported from SUP F4SE V1170 by Tomm (MIT). Original at https://www.nexusmods.com/fallout4/mods/17295 + +#include +#include +#include +#include +#include +#include + +namespace RE::PowerUtils +{ + inline bool operator==(const GridConnection& a_lhs, const GridConnection& a_rhs) noexcept + { + return a_lhs.connection == a_rhs.connection && a_lhs.connector == a_rhs.connector; + } +} + +namespace Addictol +{ + static REX::TOML::Bool<> bFixesPowerGridScrap{ "Fixes"sv, "bPowerGridScrap"sv, true }; + + using TDeleteWorkshopItem = void(__fastcall*)(RE::TESObjectREFR*); + static REL::Relocation g_DeleteWorkshopItem{ REL::ID{ 853152, 2195121 } }; + + using TSetWantsDelete = void(__fastcall*)(RE::TESObjectREFR*, bool); + static TSetWantsDelete OriginalSetWantsDelete = nullptr; + + static std::atomic g_workshopItemKeyword{ nullptr }; + static thread_local bool s_inHook = false; + + [[nodiscard]] static RE::BGSKeyword* ResolveWorkshopItemKeyword() noexcept + { + auto* cached = g_workshopItemKeyword.load(std::memory_order_acquire); + if (cached) + return cached; + + auto* keyword = RE::TESForm::GetFormByEditorID("WorkshopItem"sv); + if (keyword) + g_workshopItemKeyword.store(keyword, std::memory_order_release); + return keyword; + } + + [[nodiscard]] static RE::Workshop::ExtraData* GetWorkshopExtra(RE::TESObjectREFR* a_workshopRef) noexcept + { + if (!a_workshopRef || !a_workshopRef->extraList) + return nullptr; + return static_cast( + a_workshopRef->extraList->GetByType(RE::Workshop::ExtraData::TYPE)); + } + + // V1170's IsItemPresentInWorkshop classifier: 0 = not tracked, otherwise the formID has a grid entry. + [[nodiscard]] static bool IsItemPresentInWorkshop(RE::Workshop::ExtraData* a_extra, RE::TESFormID a_formID) noexcept + { + if (!a_extra) + return false; + + for (auto* grid : a_extra->powerGrid) + { + if (!grid) + continue; + + for (const auto& [key, set] : grid->adjacencyMap) + { + if (key == a_formID) + return true; + if (!set) + continue; + for (const auto& conn : *set) + { + if (conn.connection == a_formID || conn.connector == a_formID) + return true; + } + } + } + return false; + } + + static void __fastcall Hook_SetWantsDelete(RE::TESObjectREFR* a_this, bool a_unk) + { + if (s_inHook || !a_this) + { + if (OriginalSetWantsDelete) + OriginalSetWantsDelete(a_this, a_unk); + return; + } + + auto* keyword = ResolveWorkshopItemKeyword(); + if (keyword) + { + if (auto* workshopRef = a_this->GetLinkedRef(keyword)) + { + if (auto* extra = GetWorkshopExtra(workshopRef)) + { + if (IsItemPresentInWorkshop(extra, a_this->formID)) + { + s_inHook = true; + g_DeleteWorkshopItem(a_this); + s_inHook = false; + } + } + } + } + + if (OriginalSetWantsDelete) + OriginalSetWantsDelete(a_this, a_unk); + } + + ModulePowerGridScrap::ModulePowerGridScrap() : + Module("Power Grid Scrap", &bFixesPowerGridScrap) + {} + + bool ModulePowerGridScrap::DoQuery() const noexcept + { + return true; + } + + bool ModulePowerGridScrap::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + const auto target = REL::Relocation(REL::VariantID{ 761346, 2201199 }).address(); + OriginalSetWantsDelete = reinterpret_cast( + RELEX::DetourJump(target, reinterpret_cast(&Hook_SetWantsDelete))); + + if (!OriginalSetWantsDelete) + { + REX::WARN("Power Grid Scrap: failed to detour TESObjectREFR::SetWantsDelete."sv); + return false; + } + + return true; + } + + bool ModulePowerGridScrap::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModulePowerGridScrap::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/Depends/commonlibf4 b/Depends/commonlibf4 index 49823f9..11c216c 160000 --- a/Depends/commonlibf4 +++ b/Depends/commonlibf4 @@ -1 +1 @@ -Subproject commit 49823f9659ebb65c85f0898c2c774bfe42ce15c2 +Subproject commit 11c216c5cf9764c7e1b9537adfb27a1648cc084e diff --git a/VC/Addictol.vcxproj b/VC/Addictol.vcxproj index e79adf0..aacb3cd 100644 --- a/VC/Addictol.vcxproj +++ b/VC/Addictol.vcxproj @@ -26,6 +26,7 @@ + @@ -47,6 +48,7 @@ + @@ -63,7 +65,9 @@ + + @@ -95,6 +99,7 @@ + @@ -116,6 +121,7 @@ + @@ -132,7 +138,9 @@ + + diff --git a/VC/Addictol.vcxproj.filters b/VC/Addictol.vcxproj.filters index 87001d1..a0b67da 100644 --- a/VC/Addictol.vcxproj.filters +++ b/VC/Addictol.vcxproj.filters @@ -211,6 +211,18 @@ Source\Modules + + Source\Modules + + + Source\Modules + + + Source\Modules + + + Source\Modules + @@ -415,6 +427,18 @@ Include\Modules + + Include\Modules + + + Include\Modules + + + Include\Modules + + + Include\Modules + diff --git a/Version/build_version.txt b/Version/build_version.txt index a73c05b95de242f96f47b678e0daa6d196d8e185..1f098a229749ba045eafb2f5d7cd14266b3ec304 100644 GIT binary patch literal 12 TcmezW&w|02!IXiQfr|kE95@2C literal 12 TcmezW&w{~-!I*)Ufr|kE93%p= diff --git a/Version/resource_version2.h b/Version/resource_version2.h index 53ad482daa47859e438a223e03b936f1dee201c2..8eaccad1bb9d7d9b245fdd488ade5e4855b59cbf 100644 GIT binary patch delta 28 kcmcb@e}#X;17>z(1``He2Cm7B%+ix1Sfw`0uz31|tSu2Cm7B%+ix1Sfw`0u Date: Wed, 6 May 2026 12:11:05 -0700 Subject: [PATCH 02/10] fix(BethesdaNetCrash): try MSVCR110.dll on OG (CRT shim absent) OG 1.10.163 imports wcsrtombs_s from MSVCR110.dll, not from api-ms-win-crt-convert-l1-1-0.dll which is the modern CRT api-set used by NG/AE. The IAT detour now walks both DLL names so the hook installs on all three runtimes. --- .../Source/Modules/AdModuleBethesdaNetCrash.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Addictol/Source/Modules/AdModuleBethesdaNetCrash.cpp b/Addictol/Source/Modules/AdModuleBethesdaNetCrash.cpp index 7f17030..edb2ab6 100644 --- a/Addictol/Source/Modules/AdModuleBethesdaNetCrash.cpp +++ b/Addictol/Source/Modules/AdModuleBethesdaNetCrash.cpp @@ -1,6 +1,7 @@ #include #include +#include #include namespace Addictol @@ -69,10 +70,19 @@ namespace Addictol bool ModuleBethesdaNetCrash::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept { - auto original = RELEX::DetourIAT( + // OG (1.10.163) imports wcsrtombs_s from MSVCR110.dll; NG/AE use the modern api-ms-* CRT shim. + static constexpr std::array kCandidateDlls{ "api-ms-win-crt-convert-l1-1-0.dll", - "wcsrtombs_s", - reinterpret_cast(&Hook_wcsrtombs_s)); + "MSVCR110.dll", + }; + + std::uintptr_t original = 0; + for (const auto* dll : kCandidateDlls) + { + original = RELEX::DetourIAT(dll, "wcsrtombs_s", reinterpret_cast(&Hook_wcsrtombs_s)); + if (original) + break; + } if (!original) { From 29081fa443b4a0d786c4650c9e65c41c37f8c1d1 Mon Sep 17 00:00:00 2001 From: northaxosky Date: Wed, 6 May 2026 12:11:05 -0700 Subject: [PATCH 03/10] chore(ConfigValidation): register new TOML keys Adds bAnimSignedCrash, bBethesdaNetCrash, bMuzzleFlashLight, bAltTabFullscreen, bPowerGridScrap, bHighResBloom and nBloomScale to the known-keys allowlist so SettingStore stops emitting 'unknown key' warnings for them. --- Addictol/Source/AdConfigValidation.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Addictol/Source/AdConfigValidation.cpp b/Addictol/Source/AdConfigValidation.cpp index 4b01acd..f3879b5 100644 --- a/Addictol/Source/AdConfigValidation.cpp +++ b/Addictol/Source/AdConfigValidation.cpp @@ -17,7 +17,7 @@ namespace Addictol "bFacegen", "bMemoryManager", "bSmallBlockAllocator", "bScaleformAllocator", "bBSMTAManager", "bBSPreCulledObjects", "bINISettingCollection", "bArchiveLimits", "bInputSwitch", "bFasterWorkshop", - "bSaveAddedSoundCategories", "bCOMInit" + "bSaveAddedSoundCategories", "bCOMInit", "bHighResBloom" }}, { "Fixes", { "bGreyMovie", "bPackageAllocateLocation", "bInitTints", "bLODDistance", @@ -28,7 +28,9 @@ namespace Addictol "bMagicEffectApplyEvent", "bEncounterZoneReset", "bLeveledListCrash", "bBakaMaxPapyrusOps", "bPapyrusGCBug", "bCreateD3DAndSwapchain", "bCheckInternetAccess", "bStolenPowerArmorOwnership", "bManyItems", - "bCombatMusic", "bWorkbenchSound", "bActorCauseSaveBloat" + "bCombatMusic", "bWorkbenchSound", "bActorCauseSaveBloat", + "bAnimSignedCrash", "bBethesdaNetCrash", + "bMuzzleFlashLight", "bAltTabFullscreen", "bPowerGridScrap" }}, { "Warnings", { "bImageSpaceAdapter", "bDuplicateAddonNodeIndex" @@ -38,7 +40,7 @@ namespace Addictol "uScaleformPageSize", "uScaleformHeapSize", "nSleepTimer", "nMaxLockCount", "bInteriorNavCutMultiThreading", "nMaxPapyrusOpsPerFrame", - "bIgnorePreInstallBias", "nQuitGameDelayMs" + "bIgnorePreInstallBias", "nQuitGameDelayMs", "nBloomScale" }}, { "Profiler", { "bProfiler", "bESPProfiler", "bESPSubHooks", "bDLLProfiler", From 1b5597b78fbb418c52a37237178f3d3cc2baaa00 Mon Sep 17 00:00:00 2001 From: northaxosky Date: Wed, 6 May 2026 12:21:52 -0700 Subject: [PATCH 04/10] chore: bump commonlibf4 to ec2bf96e (PR #8 merged) PR #8 (inline ItemIsPowerConnection / ItemIsPowerReceiver) was merged into commonlibf4 main; track the merged SHA so the feature branch can be deleted. --- Depends/commonlibf4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Depends/commonlibf4 b/Depends/commonlibf4 index 11c216c..ec2bf96 160000 --- a/Depends/commonlibf4 +++ b/Depends/commonlibf4 @@ -1 +1 @@ -Subproject commit 11c216c5cf9764c7e1b9537adfb27a1648cc084e +Subproject commit ec2bf96e01aeee034f4d2e028e1114b6ee252151 From 3883da0d4ed10df91037b2f8ead80e331265d527 Mon Sep 17 00:00:00 2001 From: northaxosky Date: Mon, 11 May 2026 23:08:00 -0700 Subject: [PATCH 05/10] feat: add DPI Scaling, Animated Static Reload, Viewmodel Shading, DoF Fix - DPI Scaling: preload-stage shim calling SetProcessDpiAwarenessContext (PMv2) with fallback cascade to SetProcessDpiAwareness and SetProcessDpiAware. No address-library binding required. Opt-in (bDpiScaling = false) since many users already apply the OS compat-tab shim manually. - Animated Static Reload: vtable[0x9D] detour on TESObjectREFR::ShouldSaveAnimationOnSaving (RE::VTABLE::TESObjectREFR[0]). Forces save when base is BGSMovableStatic and an active NiControllerSequence is in kAnimating + kLoop. Restores the engine's animation state across save/load (Diamond City fans, settlement generators, etc.). Concept ported from SKSE Animated Static Reload (max-su-2019 / doodlum). - Viewmodel Shading: detour DrawWorld::Move1stPersonToOrigin (OG addrlib 76526, NG/AE 2318293), post-add BSShaderManager::State::forwardLightOffset to BSShaderAccumulator::eyePosition. Corrects specular highlights on the first-person viewmodel. Concept ported from WallSoGB Viewmodel Shading Fix (NVSE). Player-attached-light mirror fix (NV 0x87513F) is a follow-up. - DoF Fix: detour ImageSpaceManager::RenderEffect worker (OG addrlib 325252, NG/AE 2316595) with a runtime-branched signature (OG passes the effect pointer in rdx, NG/AE pass an index into effectList._data). Filter on ImageSpaceEffectDepthOfField vtable; after vanilla DoF runs, re-issue the first-person viewmodel via BSShaderUtil::RenderScene (OG 1310228, NG/AE 2317576) so the viewmodel stays sharp through DoF. Concept ported from WallSoGB Fallout-DoF-Fix (NVSE). --- .Build/F4SE/Plugins/Addictol.toml | 12 ++ .../Modules/AdModuleAnimatedStaticReload.h | 19 +++ Addictol/Include/Modules/AdModuleDofFix.h | 19 +++ Addictol/Include/Modules/AdModuleDpiScaling.h | 19 +++ .../Modules/AdModuleViewmodelShading.h | 19 +++ Addictol/Source/AdConfigValidation.cpp | 5 +- Addictol/Source/AdRegisterModules.cpp | 12 ++ .../Modules/AdModuleAnimatedStaticReload.cpp | 113 ++++++++++++++++++ Addictol/Source/Modules/AdModuleDofFix.cpp | 106 ++++++++++++++++ .../Source/Modules/AdModuleDpiScaling.cpp | 98 +++++++++++++++ .../Modules/AdModuleViewmodelShading.cpp | 75 ++++++++++++ VC/Addictol.vcxproj | 8 ++ VC/Addictol.vcxproj.filters | 24 ++++ Version/build_version.txt | Bin 12 -> 12 bytes Version/resource_version2.h | Bin 2004 -> 2004 bytes 15 files changed, 527 insertions(+), 2 deletions(-) create mode 100644 Addictol/Include/Modules/AdModuleAnimatedStaticReload.h create mode 100644 Addictol/Include/Modules/AdModuleDofFix.h create mode 100644 Addictol/Include/Modules/AdModuleDpiScaling.h create mode 100644 Addictol/Include/Modules/AdModuleViewmodelShading.h create mode 100644 Addictol/Source/Modules/AdModuleAnimatedStaticReload.cpp create mode 100644 Addictol/Source/Modules/AdModuleDofFix.cpp create mode 100644 Addictol/Source/Modules/AdModuleDpiScaling.cpp create mode 100644 Addictol/Source/Modules/AdModuleViewmodelShading.cpp diff --git a/.Build/F4SE/Plugins/Addictol.toml b/.Build/F4SE/Plugins/Addictol.toml index 5994216..6cef352 100644 --- a/.Build/F4SE/Plugins/Addictol.toml +++ b/.Build/F4SE/Plugins/Addictol.toml @@ -56,6 +56,9 @@ bCOMInit = true # Raises the bloom render-target resolution to reduce flicker on bright pixels (sun glints, neon, fire). Cost scales with nBloomScale. bHighResBloom = false +# Marks the process as DPI-aware so menus and cursor track correctly on high-DPI desktops. Off by default since many users already apply the OS compat-tab "Override High DPI scaling -> Application" shim manually. If enabling this, remove that compat-tab setting first to avoid conflict. +bDpiScaling = false + [Fixes] @@ -162,6 +165,15 @@ bAltTabFullscreen = true # Fixes a CTD when scrapping or wiring after a settlement mod has been removed, by cleaning up orphan power-grid entries left behind by deleted references. bPowerGridScrap = true +# Fixes animated statics (fans, signs, generators) that stop animating after save/load until cell unload+reload. +bAnimatedStaticReload = true + +# Fixes wrong specular lighting on the first-person viewmodel caused by the eye-position vector missing the engine's per-frame light offset. +bViewmodelShading = true + +# Fixes the first-person viewmodel getting blurred by depth-of-field (iron-sight ADS, dialogue camera) by re-rasterizing it after the DoF pass. +bDofFix = true + [Warnings] diff --git a/Addictol/Include/Modules/AdModuleAnimatedStaticReload.h b/Addictol/Include/Modules/AdModuleAnimatedStaticReload.h new file mode 100644 index 0000000..cefaaab --- /dev/null +++ b/Addictol/Include/Modules/AdModuleAnimatedStaticReload.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleAnimatedStaticReload : + public Module + { + public: + ModuleAnimatedStaticReload(); + virtual ~ModuleAnimatedStaticReload() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Include/Modules/AdModuleDofFix.h b/Addictol/Include/Modules/AdModuleDofFix.h new file mode 100644 index 0000000..8d5d871 --- /dev/null +++ b/Addictol/Include/Modules/AdModuleDofFix.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleDofFix : + public Module + { + public: + ModuleDofFix(); + virtual ~ModuleDofFix() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Include/Modules/AdModuleDpiScaling.h b/Addictol/Include/Modules/AdModuleDpiScaling.h new file mode 100644 index 0000000..88dbfc1 --- /dev/null +++ b/Addictol/Include/Modules/AdModuleDpiScaling.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleDpiScaling : + public Module + { + public: + ModuleDpiScaling(); + virtual ~ModuleDpiScaling() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Include/Modules/AdModuleViewmodelShading.h b/Addictol/Include/Modules/AdModuleViewmodelShading.h new file mode 100644 index 0000000..eaf5fed --- /dev/null +++ b/Addictol/Include/Modules/AdModuleViewmodelShading.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleViewmodelShading : + public Module + { + public: + ModuleViewmodelShading(); + virtual ~ModuleViewmodelShading() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Source/AdConfigValidation.cpp b/Addictol/Source/AdConfigValidation.cpp index f3879b5..32bda95 100644 --- a/Addictol/Source/AdConfigValidation.cpp +++ b/Addictol/Source/AdConfigValidation.cpp @@ -17,7 +17,7 @@ namespace Addictol "bFacegen", "bMemoryManager", "bSmallBlockAllocator", "bScaleformAllocator", "bBSMTAManager", "bBSPreCulledObjects", "bINISettingCollection", "bArchiveLimits", "bInputSwitch", "bFasterWorkshop", - "bSaveAddedSoundCategories", "bCOMInit", "bHighResBloom" + "bSaveAddedSoundCategories", "bCOMInit", "bHighResBloom", "bDpiScaling" }}, { "Fixes", { "bGreyMovie", "bPackageAllocateLocation", "bInitTints", "bLODDistance", @@ -30,7 +30,8 @@ namespace Addictol "bCheckInternetAccess", "bStolenPowerArmorOwnership", "bManyItems", "bCombatMusic", "bWorkbenchSound", "bActorCauseSaveBloat", "bAnimSignedCrash", "bBethesdaNetCrash", - "bMuzzleFlashLight", "bAltTabFullscreen", "bPowerGridScrap" + "bMuzzleFlashLight", "bAltTabFullscreen", "bPowerGridScrap", + "bAnimatedStaticReload", "bViewmodelShading", "bDofFix" }}, { "Warnings", { "bImageSpaceAdapter", "bDuplicateAddonNodeIndex" diff --git a/Addictol/Source/AdRegisterModules.cpp b/Addictol/Source/AdRegisterModules.cpp index 3b73c28..b900d25 100644 --- a/Addictol/Source/AdRegisterModules.cpp +++ b/Addictol/Source/AdRegisterModules.cpp @@ -56,6 +56,10 @@ #include #include #include +#include +#include +#include +#include // Create patches static auto sModuleThreads = std::make_shared(); @@ -114,6 +118,10 @@ static auto sModuleMuzzleFlashLight = std::make_shared(); static auto sModuleAltTabFullscreen = std::make_shared(); static auto sModulePowerGridScrap = std::make_shared(); +static auto sModuleDpiScaling = std::make_shared(); +static auto sModuleAnimatedStaticReload = std::make_shared(); +static auto sModuleViewmodelShading = std::make_shared(); +static auto sModuleDofFix = std::make_shared(); void AdRegisterPreloadModules() { @@ -126,6 +134,7 @@ void AdRegisterPreloadModules() modules.Register(sModuleMaxStdIO); modules.Register(sModuleCheckInternetAccess); modules.Register(sModuleCOMInit); + modules.Register(sModuleDpiScaling); } void AdRegisterModules() @@ -186,6 +195,9 @@ void AdRegisterModules() modules.Register(sModuleHighResBloom); modules.Register(sModuleAltTabFullscreen); modules.Register(sModulePowerGridScrap); + modules.Register(sModuleAnimatedStaticReload); + modules.Register(sModuleViewmodelShading); + modules.Register(sModuleDofFix); // Registers other patches modules.Register(sModuleThreads, kGameDataReady); diff --git a/Addictol/Source/Modules/AdModuleAnimatedStaticReload.cpp b/Addictol/Source/Modules/AdModuleAnimatedStaticReload.cpp new file mode 100644 index 0000000..7050fd2 --- /dev/null +++ b/Addictol/Source/Modules/AdModuleAnimatedStaticReload.cpp @@ -0,0 +1,113 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Addictol +{ + static REX::TOML::Bool<> bFixesAnimatedStaticReload{ "Fixes"sv, "bAnimatedStaticReload"sv, true }; + + namespace animatedStaticReloadDetail + { + // NiTimeController::CycleType isn't exposed by commonlibf4; mirror the NetImmerse enum order. + enum CycleType : std::uint32_t + { + kLoop = 0, + kReverse = 1, + kClamp = 2, + }; + + static constexpr std::uint32_t kVfuncIndex = 0x9D; + + using TShouldSave = bool(__fastcall*)(const RE::TESObjectREFR*); + static TShouldSave OriginalShouldSave = nullptr; + + using ActiveSequences = RE::BSTArray>; + + [[nodiscard]] static CycleType GetCycleType(const RE::NiControllerSequence* a_seq) noexcept + { + return *reinterpret_cast(a_seq->cycleType); + } + + [[nodiscard]] static const ActiveSequences* GetActiveSequences(const RE::NiControllerManager* a_mgr) noexcept + { + if (!a_mgr) + return nullptr; + return reinterpret_cast(a_mgr->activeSequences); + } + + [[nodiscard]] static bool HasLoopingActiveSequence(const RE::NiAVObject* a_root) noexcept + { + if (!a_root) + return false; + auto* mgr = RE::NiControllerManager::GetNiControllerManager(a_root); + const auto* sequences = GetActiveSequences(mgr); + if (!sequences) + return false; + for (const auto& seq : *sequences) { + const auto* raw = seq.get(); + if (!raw) + continue; + if (raw->state == RE::NiControllerSequence::AnimState::kAnimating && + GetCycleType(raw) == kLoop) + return true; + } + return false; + } + + static bool __fastcall HookShouldSaveAnimationOnSaving(const RE::TESObjectREFR* a_ref) noexcept + { + const bool original = OriginalShouldSave ? OriginalShouldSave(a_ref) : false; + if (original || !a_ref) + return original; + + __try { + const auto* base = a_ref->GetObjectReference(); + if (!base || !base->As()) + return false; + return HasLoopingActiveSequence(a_ref->Get3D()); + } + __except (1) { + return original; + } + } + } + + ModuleAnimatedStaticReload::ModuleAnimatedStaticReload() : + Module("Animated Static Reload", &bFixesAnimatedStaticReload) + {} + + bool ModuleAnimatedStaticReload::DoQuery() const noexcept + { + return true; + } + + bool ModuleAnimatedStaticReload::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + using namespace animatedStaticReloadDetail; + + // Index 0 is the primary vtable; entries 1-7 are short sub-object vtables that have no method at slot 0x9D. + const auto vtable = RE::VTABLE::TESObjectREFR[0].address(); + *reinterpret_cast(&OriginalShouldSave) = + RELEX::DetourVTable(vtable, reinterpret_cast(&HookShouldSaveAnimationOnSaving), kVfuncIndex); + + return OriginalShouldSave != nullptr; + } + + bool ModuleAnimatedStaticReload::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleAnimatedStaticReload::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/Addictol/Source/Modules/AdModuleDofFix.cpp b/Addictol/Source/Modules/AdModuleDofFix.cpp new file mode 100644 index 0000000..b26a558 --- /dev/null +++ b/Addictol/Source/Modules/AdModuleDofFix.cpp @@ -0,0 +1,106 @@ +#include +#include + +#include +#include + +namespace Addictol +{ + static REX::TOML::Bool<> bFixesDofFix{ "Fixes"sv, "bDofFix"sv, true }; + + // Worker takes 5 args; the 5th lives at [rsp+0x28] in the caller frame. The hook must declare and + // forward all five or the original reads garbage when it reaches its 5th-arg load. + // OG arg2 is the effect pointer directly; NG/AE arg2 is an index into effectList._data (+0x18). + using TRenderEffect = void(__fastcall*)(void*, std::uintptr_t, std::int32_t, std::int32_t, void*); + static TRenderEffect OriginalRenderEffect = nullptr; + + using TRenderScene = void(__fastcall*)(void*, void*, bool); + static TRenderScene OriginalRenderScene = nullptr; + + static std::uintptr_t g_dofVTable = 0; + static void** g_viewmodelCameraGlobal = nullptr; + static void** g_viewmodelAccumGlobal = nullptr; + static bool g_workerArgIsEffectPtr = false; + + static constexpr std::size_t kEffectListData = 0x18; + static constexpr std::size_t kFirstPersonFlag = 0xB0; + + static void __fastcall HookRenderEffect(void* a_manager, std::uintptr_t a_idxOrEffect, std::int32_t a_a3, std::int32_t a_a4, void* a_a5) noexcept + { + void* effect = nullptr; + if (g_workerArgIsEffectPtr) + { + effect = reinterpret_cast(a_idxOrEffect); + } + else if (a_manager) + { + void** data = *reinterpret_cast(reinterpret_cast(a_manager) + kEffectListData); + if (data) + effect = data[static_cast(a_idxOrEffect)]; + } + + const bool isDof = effect && g_dofVTable && *reinterpret_cast(effect) == g_dofVTable; + + if (OriginalRenderEffect) + OriginalRenderEffect(a_manager, a_idxOrEffect, a_a3, a_a4, a_a5); + + if (!isDof || !OriginalRenderScene || !g_viewmodelCameraGlobal || !g_viewmodelAccumGlobal) + return; + + auto* camera = *g_viewmodelCameraGlobal; + auto* accumulator = *g_viewmodelAccumGlobal; + if (!camera || !accumulator) + return; + + const auto firstPerson = *reinterpret_cast(reinterpret_cast(accumulator) + kFirstPersonFlag); + if (!firstPerson) + return; + + OriginalRenderScene(camera, accumulator, false); + } + + ModuleDofFix::ModuleDofFix() : + Module("DoF Fix", &bFixesDofFix) + {} + + bool ModuleDofFix::DoQuery() const noexcept + { + return true; + } + + bool ModuleDofFix::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + g_dofVTable = RE::VTABLE::ImageSpaceEffectDepthOfField[0].address(); + if (!g_dofVTable) + { + REX::WARN("[DoFFix] Could not resolve ImageSpaceEffectDepthOfField vtable; skipping."sv); + return false; + } + + g_workerArgIsEffectPtr = RELEX::IsRuntimeOG(); + + // Camera + accumulator the engine passes to BSShaderUtil::RenderScene + // in DrawWorld::Imagespace; reusing them keeps the viewmodel re-render + // identical to what the vanilla pipeline already issues elsewhere. + g_viewmodelCameraGlobal = reinterpret_cast(REL::ID{ 300623, 2712879 }.address()); + g_viewmodelAccumGlobal = reinterpret_cast(REL::ID{ 726120, 2712936 }.address()); + + const auto renderSceneTarget = REL::ID{ 1310228, 2317576 }.address(); + OriginalRenderScene = reinterpret_cast(renderSceneTarget); + + const auto renderEffectTarget = REL::ID{ 325252, 2316595 }.address(); + *reinterpret_cast(&OriginalRenderEffect) = + RELEX::DetourJump(renderEffectTarget, reinterpret_cast(&HookRenderEffect)); + return OriginalRenderEffect != nullptr; + } + + bool ModuleDofFix::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleDofFix::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/Addictol/Source/Modules/AdModuleDpiScaling.cpp b/Addictol/Source/Modules/AdModuleDpiScaling.cpp new file mode 100644 index 0000000..99a63d2 --- /dev/null +++ b/Addictol/Source/Modules/AdModuleDpiScaling.cpp @@ -0,0 +1,98 @@ +#include +#include + +#include + +namespace Addictol +{ + static REX::TOML::Bool<> bPatchesDpiScaling{ "Patches"sv, "bDpiScaling"sv, false }; + + namespace detail + { + // Win10 1703+ context value; declared locally to avoid bumping the Windows SDK floor. + static const HANDLE kPerMonitorAwareV2 = reinterpret_cast(static_cast(-4)); + static constexpr int kProcessPerMonitorDpiAware = 2; + + using TSetProcessDpiAwarenessContext = BOOL(WINAPI*)(HANDLE); + using TSetProcessDpiAwareness = HRESULT(WINAPI*)(int); + using TSetProcessDpiAware = BOOL(WINAPI*)(); + + [[nodiscard]] static bool TrySetPerMonitorV2() noexcept + { + auto user32 = GetModuleHandleW(L"user32.dll"); + if (!user32) + return false; + auto fn = reinterpret_cast( + GetProcAddress(user32, "SetProcessDpiAwarenessContext")); + if (!fn) + return false; + // ACCESS_DENIED means the process is already DPI-aware via manifest / compat shim; treat as success. + return fn(kPerMonitorAwareV2) || GetLastError() == ERROR_ACCESS_DENIED; + } + + [[nodiscard]] static bool TrySetPerMonitor() noexcept + { + auto shcore = GetModuleHandleW(L"shcore.dll"); + if (!shcore) + shcore = LoadLibraryW(L"shcore.dll"); + if (!shcore) + return false; + auto fn = reinterpret_cast( + GetProcAddress(shcore, "SetProcessDpiAwareness")); + if (!fn) + return false; + const HRESULT hr = fn(kProcessPerMonitorDpiAware); + return SUCCEEDED(hr) || hr == E_ACCESSDENIED; + } + + [[nodiscard]] static bool TrySetDpiAware() noexcept + { + auto user32 = GetModuleHandleW(L"user32.dll"); + if (!user32) + return false; + auto fn = reinterpret_cast( + GetProcAddress(user32, "SetProcessDPIAware")); + if (!fn) + return false; + return fn() != FALSE; + } + } + + ModuleDpiScaling::ModuleDpiScaling() : + Module("DPI Scaling", &bPatchesDpiScaling) + {} + + bool ModuleDpiScaling::DoQuery() const noexcept + { + return true; + } + + bool ModuleDpiScaling::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + const char* level = nullptr; + if (detail::TrySetPerMonitorV2()) + level = "PER_MONITOR_AWARE_V2"; + else if (detail::TrySetPerMonitor()) + level = "PROCESS_PER_MONITOR_DPI_AWARE"; + else if (detail::TrySetDpiAware()) + level = "DPI_AWARE"; + + if (level) + REX::INFO("DPI Scaling: applied {}"sv, level); + else + REX::WARN("DPI Scaling: no SetProcessDpi* entry point accepted; process remains DPI-unaware"sv); + + // Always succeed; DPI-unaware is the vanilla behavior, not a fatal install error. + return true; + } + + bool ModuleDpiScaling::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleDpiScaling::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/Addictol/Source/Modules/AdModuleViewmodelShading.cpp b/Addictol/Source/Modules/AdModuleViewmodelShading.cpp new file mode 100644 index 0000000..99b270c --- /dev/null +++ b/Addictol/Source/Modules/AdModuleViewmodelShading.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include +#include + +namespace Addictol +{ + static REX::TOML::Bool<> bFixesViewmodelShading{ "Fixes"sv, "bViewmodelShading"sv, true }; + + using TMove1stPersonToOrigin = void(__fastcall*)(); + static TMove1stPersonToOrigin OriginalMove1stPersonToOrigin = nullptr; + + // State* @ +0x190 = NiPoint3 forwardLightOffset (see BSShaderManager.h). + // Accumulator* @ +0x570 = NiPoint3A eyePosition (see BSShaderAccumulator.h). + static constexpr std::size_t kForwardLightOffset = 0x190; + static constexpr std::size_t kEyePosition = 0x570; + + // Singletons resolved at install time; both are pointer-to-pointer globals. + static RE::BSShaderManager::State** g_stateGlobal = nullptr; + static RE::BSShaderAccumulator** g_accumulatorGlobal = nullptr; + + // Add forwardLightOffset.xyz onto eyePosition.xyz; the original write at + // +0x570/+0x574/+0x578 omitted this term, breaking 1st-person specular. + static void __fastcall HookMove1stPersonToOrigin() noexcept + { + if (OriginalMove1stPersonToOrigin) + OriginalMove1stPersonToOrigin(); + + auto* state = g_stateGlobal ? *g_stateGlobal : nullptr; + auto* accumulator = g_accumulatorGlobal ? *g_accumulatorGlobal : nullptr; + if (!state || !accumulator) + return; + + auto* statePtr = reinterpret_cast(state); + auto* accumulatorPtr = reinterpret_cast(accumulator); + + const auto& offset = *reinterpret_cast(statePtr + kForwardLightOffset); + auto* eye = reinterpret_cast(accumulatorPtr + kEyePosition); + + eye[0] += offset.x; + eye[1] += offset.y; + eye[2] += offset.z; + } + + ModuleViewmodelShading::ModuleViewmodelShading() : + Module("Viewmodel Shading", &bFixesViewmodelShading) + {} + + bool ModuleViewmodelShading::DoQuery() const noexcept + { + return true; + } + + bool ModuleViewmodelShading::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + g_stateGlobal = reinterpret_cast(REL::ID{ 1444212, 2712877 }.address()); + g_accumulatorGlobal = reinterpret_cast(REL::ID{ 1430301, 2712932 }.address()); + + const auto target = REL::ID{ 76526, 2318293 }.address(); + *reinterpret_cast(&OriginalMove1stPersonToOrigin) = + RELEX::DetourJump(target, reinterpret_cast(&HookMove1stPersonToOrigin)); + return OriginalMove1stPersonToOrigin != nullptr; + } + + bool ModuleViewmodelShading::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleViewmodelShading::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/VC/Addictol.vcxproj b/VC/Addictol.vcxproj index aacb3cd..3a3ea34 100644 --- a/VC/Addictol.vcxproj +++ b/VC/Addictol.vcxproj @@ -27,6 +27,10 @@ + + + + @@ -100,6 +104,10 @@ + + + + diff --git a/VC/Addictol.vcxproj.filters b/VC/Addictol.vcxproj.filters index a0b67da..b5356b4 100644 --- a/VC/Addictol.vcxproj.filters +++ b/VC/Addictol.vcxproj.filters @@ -223,6 +223,18 @@ Source\Modules + + Source\Modules + + + Source\Modules + + + Source\Modules + + + Source\Modules + @@ -439,6 +451,18 @@ Include\Modules + + Include\Modules + + + Include\Modules + + + Include\Modules + + + Include\Modules + diff --git a/Version/build_version.txt b/Version/build_version.txt index 1f098a229749ba045eafb2f5d7cd14266b3ec304..aa17df3bb0156088edf697e5bf9f8d6529085807 100644 GIT binary patch literal 12 TcmezW&w{~(!GM96fr|kE93ld- literal 12 TcmezW&w|02!IXiQfr|kE95@2C diff --git a/Version/resource_version2.h b/Version/resource_version2.h index 8eaccad1bb9d7d9b245fdd488ade5e4855b59cbf..0f19ff134a660c1b4ab6a73adc0007b5d6608242 100644 GIT binary patch delta 26 icmcb@e}#X;BW6}h23`iP$&Ad>lOtHAHp{T=UlOtHAHp{T=U Date: Tue, 12 May 2026 00:27:38 -0700 Subject: [PATCH 06/10] feat: add LOD Specular fix Detours BGSDistantObjectBlock::Prepare (OG addrlib 950871, NG/AE 2213394), walks children of the prepared block, and forces kSpecular on each BSGeometry's shade-slot property via BSShaderProperty::SetFlag (OG 1251793, NG/AE 2316281). Pre-state gate skips re-walk on already-prepared blocks. Re-enables specular sampling on distant-object LODs so they no longer look flat. Default opt-in (bLODSpecular = false) because FOLIP-generated LOD packs bake specular into the diffuse and would double-bright with the engine fix. Coexistence shape pending answer from FOLIP author. Concept ported from WallSoGB Fallout-LOD-Fixes sub-fix #1 (NVSE). --- .Build/F4SE/Plugins/Addictol.toml | 3 + .../Include/Modules/AdModuleLODSpecular.h | 19 ++++ Addictol/Source/AdConfigValidation.cpp | 3 +- Addictol/Source/AdRegisterModules.cpp | 3 + .../Source/Modules/AdModuleLODSpecular.cpp | 100 ++++++++++++++++++ VC/Addictol.vcxproj | 2 + VC/Addictol.vcxproj.filters | 6 ++ Version/build_version.txt | Bin 12 -> 12 bytes Version/resource_version2.h | Bin 2004 -> 2004 bytes 9 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 Addictol/Include/Modules/AdModuleLODSpecular.h create mode 100644 Addictol/Source/Modules/AdModuleLODSpecular.cpp diff --git a/.Build/F4SE/Plugins/Addictol.toml b/.Build/F4SE/Plugins/Addictol.toml index 6cef352..9489a0f 100644 --- a/.Build/F4SE/Plugins/Addictol.toml +++ b/.Build/F4SE/Plugins/Addictol.toml @@ -59,6 +59,9 @@ bHighResBloom = false # Marks the process as DPI-aware so menus and cursor track correctly on high-DPI desktops. Off by default since many users already apply the OS compat-tab "Override High DPI scaling -> Application" shim manually. If enabling this, remove that compat-tab setting first to avoid conflict. bDpiScaling = false +# Re-enables specular sampling on distant-object LOD shaders so distant geometry picks up correct highlights instead of looking flat. Off by default - conflicts with FOLIP-generated LOD packs that bake specular into the diffuse (would double-bright). Modest GPU cost increase on dense-LOD outdoor views. +bLODSpecular = false + [Fixes] diff --git a/Addictol/Include/Modules/AdModuleLODSpecular.h b/Addictol/Include/Modules/AdModuleLODSpecular.h new file mode 100644 index 0000000..aac4074 --- /dev/null +++ b/Addictol/Include/Modules/AdModuleLODSpecular.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace Addictol +{ + class ModuleLODSpecular : + public Module + { + public: + ModuleLODSpecular(); + virtual ~ModuleLODSpecular() = default; + + [[nodiscard]] virtual bool DoQuery() const noexcept override; + [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; + [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; + }; +} diff --git a/Addictol/Source/AdConfigValidation.cpp b/Addictol/Source/AdConfigValidation.cpp index 32bda95..a54c720 100644 --- a/Addictol/Source/AdConfigValidation.cpp +++ b/Addictol/Source/AdConfigValidation.cpp @@ -17,7 +17,8 @@ namespace Addictol "bFacegen", "bMemoryManager", "bSmallBlockAllocator", "bScaleformAllocator", "bBSMTAManager", "bBSPreCulledObjects", "bINISettingCollection", "bArchiveLimits", "bInputSwitch", "bFasterWorkshop", - "bSaveAddedSoundCategories", "bCOMInit", "bHighResBloom", "bDpiScaling" + "bSaveAddedSoundCategories", "bCOMInit", "bHighResBloom", "bDpiScaling", + "bLODSpecular" }}, { "Fixes", { "bGreyMovie", "bPackageAllocateLocation", "bInitTints", "bLODDistance", diff --git a/Addictol/Source/AdRegisterModules.cpp b/Addictol/Source/AdRegisterModules.cpp index b900d25..6995486 100644 --- a/Addictol/Source/AdRegisterModules.cpp +++ b/Addictol/Source/AdRegisterModules.cpp @@ -60,6 +60,7 @@ #include #include #include +#include // Create patches static auto sModuleThreads = std::make_shared(); @@ -122,6 +123,7 @@ static auto sModuleDpiScaling = std::make_shared(); static auto sModuleViewmodelShading = std::make_shared(); static auto sModuleDofFix = std::make_shared(); +static auto sModuleLODSpecular = std::make_shared(); void AdRegisterPreloadModules() { @@ -198,6 +200,7 @@ void AdRegisterModules() modules.Register(sModuleAnimatedStaticReload); modules.Register(sModuleViewmodelShading); modules.Register(sModuleDofFix); + modules.Register(sModuleLODSpecular); // Registers other patches modules.Register(sModuleThreads, kGameDataReady); diff --git a/Addictol/Source/Modules/AdModuleLODSpecular.cpp b/Addictol/Source/Modules/AdModuleLODSpecular.cpp new file mode 100644 index 0000000..b2b3562 --- /dev/null +++ b/Addictol/Source/Modules/AdModuleLODSpecular.cpp @@ -0,0 +1,100 @@ +#include +#include + +#include +#include +#include +#include + +namespace Addictol +{ + // Opt-in until FOLIP coexistence is resolved (Perchik's call). + static REX::TOML::Bool<> bPatchesLODSpecular{ "Patches"sv, "bLODSpecular"sv, false }; + + using TPrepare = void(__fastcall*)(void*); + using TSetFlag = void(__fastcall*)(RE::BSShaderProperty*, std::uint32_t, bool); + + static TPrepare OriginalPrepare = nullptr; + static TSetFlag SetShaderFlag = nullptr; + + // BGSDistantObjectBlock layout (verified OG/AE disasm): NiNode* block @ +0x8, bool bPrepared @ +0x2A. + static constexpr std::size_t kBlockOffset = 0x8; + static constexpr std::size_t kPreparedFlag = 0x2A; + static constexpr std::uint32_t kSpecularBit = 0; + + static void ForceSpecularOnChildren(void* a_this) noexcept + { + auto* base = reinterpret_cast(a_this); + auto* block = *reinterpret_cast(base + kBlockOffset); + if (!block) + return; + + // Range-for uses NiTArray's filled-slot iterator; raw size() can miss children past unfilled slots. + for (auto& slot : block->children) + { + auto* child = slot.get(); + if (!child) + continue; + + auto* geom = child->IsGeometry(); + if (!geom) + continue; + + auto* shade = static_cast(geom->properties[1].get()); + if (!shade) + continue; + + SetShaderFlag(shade, kSpecularBit, true); + } + } + + static void __fastcall HookPrepare(void* a_this) noexcept + { + // Gate on pre-state: original short-circuits on already-prepared blocks; we should too. + const bool wasPrepared = a_this && *(reinterpret_cast(a_this) + kPreparedFlag) != std::byte{ 0 }; + + if (OriginalPrepare) + OriginalPrepare(a_this); + + if (wasPrepared) + return; + + __try { + ForceSpecularOnChildren(a_this); + } + __except (1) { + // Swallow: child traversal must not crash the engine on malformed LOD data. + } + } + + ModuleLODSpecular::ModuleLODSpecular() : + Module("LOD Specular", &bPatchesLODSpecular) + {} + + bool ModuleLODSpecular::DoQuery() const noexcept + { + return true; + } + + bool ModuleLODSpecular::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + SetShaderFlag = reinterpret_cast(REL::ID{ 1251793, 2316281 }.address()); + if (!SetShaderFlag) + return false; + + const auto target = REL::ID{ 950871, 2213394 }.address(); + *reinterpret_cast(&OriginalPrepare) = + RELEX::DetourJump(target, reinterpret_cast(&HookPrepare)); + return OriginalPrepare != nullptr; + } + + bool ModuleLODSpecular::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept + { + return true; + } + + bool ModuleLODSpecular::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept + { + return true; + } +} diff --git a/VC/Addictol.vcxproj b/VC/Addictol.vcxproj index 3a3ea34..6fa1a73 100644 --- a/VC/Addictol.vcxproj +++ b/VC/Addictol.vcxproj @@ -60,6 +60,7 @@ + @@ -137,6 +138,7 @@ + diff --git a/VC/Addictol.vcxproj.filters b/VC/Addictol.vcxproj.filters index b5356b4..9e41aa3 100644 --- a/VC/Addictol.vcxproj.filters +++ b/VC/Addictol.vcxproj.filters @@ -235,6 +235,9 @@ Source\Modules + + Source\Modules + @@ -463,6 +466,9 @@ Include\Modules + + Include\Modules + diff --git a/Version/build_version.txt b/Version/build_version.txt index aa17df3bb0156088edf697e5bf9f8d6529085807..bb1fa37588a4058ba873c58dd038fee3a03e87bb 100644 GIT binary patch literal 12 TcmezW&w{~(!H9vEfr|kE94-R0 literal 12 TcmezW&w{~(!GM96fr|kE93ld- diff --git a/Version/resource_version2.h b/Version/resource_version2.h index 0f19ff134a660c1b4ab6a73adc0007b5d6608242..b3f73c79a9c8563b0f7dc687accd7593e0e47ef2 100644 GIT binary patch delta 28 kcmcb@e}#X;17>y;215p32Cm7B%+ix1Sfw`0uz(21^EB2Cm7B%+ix1Sfw`0uE&u=k From d35b2833065129cc3014ead4128eaee97cf13bca Mon Sep 17 00:00:00 2001 From: northaxosky Date: Tue, 12 May 2026 10:03:39 -0700 Subject: [PATCH 07/10] Revert "feat: add LOD Specular fix" This reverts commit 510afad4d4e763d77b98a497de38ca3b5369db84. --- .Build/F4SE/Plugins/Addictol.toml | 3 - .../Include/Modules/AdModuleLODSpecular.h | 19 ---- Addictol/Source/AdConfigValidation.cpp | 3 +- Addictol/Source/AdRegisterModules.cpp | 3 - .../Source/Modules/AdModuleLODSpecular.cpp | 100 ------------------ VC/Addictol.vcxproj | 2 - VC/Addictol.vcxproj.filters | 6 -- Version/build_version.txt | Bin 12 -> 12 bytes Version/resource_version2.h | Bin 2004 -> 2004 bytes 9 files changed, 1 insertion(+), 135 deletions(-) delete mode 100644 Addictol/Include/Modules/AdModuleLODSpecular.h delete mode 100644 Addictol/Source/Modules/AdModuleLODSpecular.cpp diff --git a/.Build/F4SE/Plugins/Addictol.toml b/.Build/F4SE/Plugins/Addictol.toml index 9489a0f..6cef352 100644 --- a/.Build/F4SE/Plugins/Addictol.toml +++ b/.Build/F4SE/Plugins/Addictol.toml @@ -59,9 +59,6 @@ bHighResBloom = false # Marks the process as DPI-aware so menus and cursor track correctly on high-DPI desktops. Off by default since many users already apply the OS compat-tab "Override High DPI scaling -> Application" shim manually. If enabling this, remove that compat-tab setting first to avoid conflict. bDpiScaling = false -# Re-enables specular sampling on distant-object LOD shaders so distant geometry picks up correct highlights instead of looking flat. Off by default - conflicts with FOLIP-generated LOD packs that bake specular into the diffuse (would double-bright). Modest GPU cost increase on dense-LOD outdoor views. -bLODSpecular = false - [Fixes] diff --git a/Addictol/Include/Modules/AdModuleLODSpecular.h b/Addictol/Include/Modules/AdModuleLODSpecular.h deleted file mode 100644 index aac4074..0000000 --- a/Addictol/Include/Modules/AdModuleLODSpecular.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -namespace Addictol -{ - class ModuleLODSpecular : - public Module - { - public: - ModuleLODSpecular(); - virtual ~ModuleLODSpecular() = default; - - [[nodiscard]] virtual bool DoQuery() const noexcept override; - [[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; - [[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override; - [[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override; - }; -} diff --git a/Addictol/Source/AdConfigValidation.cpp b/Addictol/Source/AdConfigValidation.cpp index a54c720..32bda95 100644 --- a/Addictol/Source/AdConfigValidation.cpp +++ b/Addictol/Source/AdConfigValidation.cpp @@ -17,8 +17,7 @@ namespace Addictol "bFacegen", "bMemoryManager", "bSmallBlockAllocator", "bScaleformAllocator", "bBSMTAManager", "bBSPreCulledObjects", "bINISettingCollection", "bArchiveLimits", "bInputSwitch", "bFasterWorkshop", - "bSaveAddedSoundCategories", "bCOMInit", "bHighResBloom", "bDpiScaling", - "bLODSpecular" + "bSaveAddedSoundCategories", "bCOMInit", "bHighResBloom", "bDpiScaling" }}, { "Fixes", { "bGreyMovie", "bPackageAllocateLocation", "bInitTints", "bLODDistance", diff --git a/Addictol/Source/AdRegisterModules.cpp b/Addictol/Source/AdRegisterModules.cpp index 6995486..b900d25 100644 --- a/Addictol/Source/AdRegisterModules.cpp +++ b/Addictol/Source/AdRegisterModules.cpp @@ -60,7 +60,6 @@ #include #include #include -#include // Create patches static auto sModuleThreads = std::make_shared(); @@ -123,7 +122,6 @@ static auto sModuleDpiScaling = std::make_shared(); static auto sModuleViewmodelShading = std::make_shared(); static auto sModuleDofFix = std::make_shared(); -static auto sModuleLODSpecular = std::make_shared(); void AdRegisterPreloadModules() { @@ -200,7 +198,6 @@ void AdRegisterModules() modules.Register(sModuleAnimatedStaticReload); modules.Register(sModuleViewmodelShading); modules.Register(sModuleDofFix); - modules.Register(sModuleLODSpecular); // Registers other patches modules.Register(sModuleThreads, kGameDataReady); diff --git a/Addictol/Source/Modules/AdModuleLODSpecular.cpp b/Addictol/Source/Modules/AdModuleLODSpecular.cpp deleted file mode 100644 index b2b3562..0000000 --- a/Addictol/Source/Modules/AdModuleLODSpecular.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include -#include - -#include -#include -#include -#include - -namespace Addictol -{ - // Opt-in until FOLIP coexistence is resolved (Perchik's call). - static REX::TOML::Bool<> bPatchesLODSpecular{ "Patches"sv, "bLODSpecular"sv, false }; - - using TPrepare = void(__fastcall*)(void*); - using TSetFlag = void(__fastcall*)(RE::BSShaderProperty*, std::uint32_t, bool); - - static TPrepare OriginalPrepare = nullptr; - static TSetFlag SetShaderFlag = nullptr; - - // BGSDistantObjectBlock layout (verified OG/AE disasm): NiNode* block @ +0x8, bool bPrepared @ +0x2A. - static constexpr std::size_t kBlockOffset = 0x8; - static constexpr std::size_t kPreparedFlag = 0x2A; - static constexpr std::uint32_t kSpecularBit = 0; - - static void ForceSpecularOnChildren(void* a_this) noexcept - { - auto* base = reinterpret_cast(a_this); - auto* block = *reinterpret_cast(base + kBlockOffset); - if (!block) - return; - - // Range-for uses NiTArray's filled-slot iterator; raw size() can miss children past unfilled slots. - for (auto& slot : block->children) - { - auto* child = slot.get(); - if (!child) - continue; - - auto* geom = child->IsGeometry(); - if (!geom) - continue; - - auto* shade = static_cast(geom->properties[1].get()); - if (!shade) - continue; - - SetShaderFlag(shade, kSpecularBit, true); - } - } - - static void __fastcall HookPrepare(void* a_this) noexcept - { - // Gate on pre-state: original short-circuits on already-prepared blocks; we should too. - const bool wasPrepared = a_this && *(reinterpret_cast(a_this) + kPreparedFlag) != std::byte{ 0 }; - - if (OriginalPrepare) - OriginalPrepare(a_this); - - if (wasPrepared) - return; - - __try { - ForceSpecularOnChildren(a_this); - } - __except (1) { - // Swallow: child traversal must not crash the engine on malformed LOD data. - } - } - - ModuleLODSpecular::ModuleLODSpecular() : - Module("LOD Specular", &bPatchesLODSpecular) - {} - - bool ModuleLODSpecular::DoQuery() const noexcept - { - return true; - } - - bool ModuleLODSpecular::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept - { - SetShaderFlag = reinterpret_cast(REL::ID{ 1251793, 2316281 }.address()); - if (!SetShaderFlag) - return false; - - const auto target = REL::ID{ 950871, 2213394 }.address(); - *reinterpret_cast(&OriginalPrepare) = - RELEX::DetourJump(target, reinterpret_cast(&HookPrepare)); - return OriginalPrepare != nullptr; - } - - bool ModuleLODSpecular::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept - { - return true; - } - - bool ModuleLODSpecular::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept - { - return true; - } -} diff --git a/VC/Addictol.vcxproj b/VC/Addictol.vcxproj index 6fa1a73..3a3ea34 100644 --- a/VC/Addictol.vcxproj +++ b/VC/Addictol.vcxproj @@ -60,7 +60,6 @@ - @@ -138,7 +137,6 @@ - diff --git a/VC/Addictol.vcxproj.filters b/VC/Addictol.vcxproj.filters index 9e41aa3..b5356b4 100644 --- a/VC/Addictol.vcxproj.filters +++ b/VC/Addictol.vcxproj.filters @@ -235,9 +235,6 @@ Source\Modules - - Source\Modules - @@ -466,9 +463,6 @@ Include\Modules - - Include\Modules - diff --git a/Version/build_version.txt b/Version/build_version.txt index bb1fa37588a4058ba873c58dd038fee3a03e87bb..aa17df3bb0156088edf697e5bf9f8d6529085807 100644 GIT binary patch literal 12 TcmezW&w{~(!GM96fr|kE93ld- literal 12 TcmezW&w{~(!H9vEfr|kE94-R0 diff --git a/Version/resource_version2.h b/Version/resource_version2.h index b3f73c79a9c8563b0f7dc687accd7593e0e47ef2..0f19ff134a660c1b4ab6a73adc0007b5d6608242 100644 GIT binary patch delta 28 kcmcb@e}#X;17>z(21^EB2Cm7B%+ix1Sfw`0uE&u=k delta 28 kcmcb@e}#X;17>y;215p32Cm7B%+ix1Sfw`0u Date: Mon, 18 May 2026 21:18:02 -0700 Subject: [PATCH 08/10] chore: enable AnimSignedCrash, BethesdaNetCrash, DpiScaling by default Reverses master hotfix 4d09f9d's opt-in defaults for AnimSignedCrash and BethesdaNetCrash; this branch ships the OG MSVCR110.dll fallback (807a497) that addressed the original failure. DpiScaling's install path no-ops via ACCESS_DENIED if an OS compat-tab DPI shim already locked awareness, so default-on is safe alongside the manual shim. --- .Build/F4SE/Plugins/Addictol.toml | 10 ++++------ Addictol/Source/Modules/AdModuleDpiScaling.cpp | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.Build/F4SE/Plugins/Addictol.toml b/.Build/F4SE/Plugins/Addictol.toml index e1a0bd4..df5bbcb 100644 --- a/.Build/F4SE/Plugins/Addictol.toml +++ b/.Build/F4SE/Plugins/Addictol.toml @@ -56,8 +56,8 @@ bCOMInit = true # Raises the bloom render-target resolution to reduce flicker on bright pixels (sun glints, neon, fire). Cost scales with nBloomScale. bHighResBloom = false -# Marks the process as DPI-aware so menus and cursor track correctly on high-DPI desktops. Off by default since many users already apply the OS compat-tab "Override High DPI scaling -> Application" shim manually. If enabling this, remove that compat-tab setting first to avoid conflict. -bDpiScaling = false +# Marks the process as DPI-aware so menus and cursor track correctly on high-DPI desktops. +bDpiScaling = true [Fixes] @@ -151,12 +151,10 @@ bWorkbenchSound = true bActorCauseSaveBloat = true # Fixes a CTD when loading animations with high-bit-set 16-bit event IDs. -# Opt-in until cross-runtime smoke test for PR #24 verifies on OG/NG/AE. -bAnimSignedCrash = false +bAnimSignedCrash = true # Fixes a startup CTD on non-English Windows installs caused by Bethesda.net response headers containing non-ASCII characters. -# Opt-in until cross-runtime smoke test for PR #24 verifies on OG/NG/AE (branch ships MSVCR110.dll fallback for OG). -bBethesdaNetCrash = false +bBethesdaNetCrash = true # Fixes a crash when a shader can't be found for a given technique id. bUtilityShader = true diff --git a/Addictol/Source/Modules/AdModuleDpiScaling.cpp b/Addictol/Source/Modules/AdModuleDpiScaling.cpp index 99a63d2..da6b747 100644 --- a/Addictol/Source/Modules/AdModuleDpiScaling.cpp +++ b/Addictol/Source/Modules/AdModuleDpiScaling.cpp @@ -5,7 +5,7 @@ namespace Addictol { - static REX::TOML::Bool<> bPatchesDpiScaling{ "Patches"sv, "bDpiScaling"sv, false }; + static REX::TOML::Bool<> bPatchesDpiScaling{ "Patches"sv, "bDpiScaling"sv, true }; namespace detail { From f3bf2629b0c295b02acd93045f6924295ea189fc Mon Sep 17 00:00:00 2001 From: northaxosky Date: Fri, 22 May 2026 19:57:39 -0700 Subject: [PATCH 09/10] fix(AltTabFullscreen): force borderless-windowed at swap-chain creation The previous approach subclassed the engine WndProc and called SetFullscreenState(FALSE) on focus loss, but never restored fullscreen on focus gain. After alt-tab back, the swap chain was windowed while the engine believed it was still in exclusive mode, causing a permanent black-screen lockup on the render thread. Switch to the same approach used by SSE Engine Fixes / Display Tweaks: at D3D11CreateDeviceAndSwapChain, force Windowed=TRUE and clear ALLOW_MODE_SWITCH whenever the engine requested exclusive. Keep MakeWindowAssociation(NO_WINDOW_CHANGES | NO_ALT_ENTER) so DXGI cannot promote us back into exclusive on Alt+Enter. Drop the WndProc subclass and all g_swapChain/g_hwnd/g_origWndProc state. --- .Build/F4SE/Plugins/Addictol.toml | 2 +- .../Modules/AdModuleAltTabFullscreen.cpp | 64 ++++++------------- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/.Build/F4SE/Plugins/Addictol.toml b/.Build/F4SE/Plugins/Addictol.toml index 9a577d4..d9eee5d 100644 --- a/.Build/F4SE/Plugins/Addictol.toml +++ b/.Build/F4SE/Plugins/Addictol.toml @@ -165,7 +165,7 @@ bPipBoyCursorConstraints = true # Fixes a bug where the muzzle-flash light keeps illuminating the scene after the flash ends. bMuzzleFlashLight = true -# Fixes the exclusive-fullscreen Alt-Tab hang by dropping fullscreen state on focus loss and blocking DXGI's auto Alt+Enter handler. +# Fixes the exclusive-fullscreen Alt-Tab hang by forcing the swap chain to borderless-windowed at creation and blocking DXGI's auto Alt+Enter handler. bAltTabFullscreen = true # Fixes a CTD when scrapping or wiring after a settlement mod has been removed, by cleaning up orphan power-grid entries left behind by deleted references. diff --git a/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp b/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp index 1f40a81..546aa62 100644 --- a/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp +++ b/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp @@ -16,31 +16,6 @@ namespace Addictol ID3D11Device**, D3D_FEATURE_LEVEL*, ID3D11DeviceContext**); static TD3D11CreateDeviceAndSwapChain OriginalCreate = nullptr; - static IDXGISwapChain* g_swapChain = nullptr; - static HWND g_hwnd = nullptr; - static WNDPROC g_origWndProc = nullptr; - - static LRESULT CALLBACK Hook_WndProc(HWND a_hwnd, UINT a_msg, WPARAM a_wp, LPARAM a_lp) noexcept - { - if (g_swapChain) - { - const bool focusLost = - a_msg == WM_KILLFOCUS || - (a_msg == WM_ACTIVATEAPP && a_wp == FALSE) || - (a_msg == WM_ACTIVATE && LOWORD(a_wp) == WA_INACTIVE); - - if (focusLost) - { - BOOL isFullscreen = FALSE; - if (SUCCEEDED(g_swapChain->GetFullscreenState(&isFullscreen, nullptr)) && isFullscreen) - g_swapChain->SetFullscreenState(FALSE, nullptr); - } - } - - if (g_origWndProc) - return CallWindowProcA(g_origWndProc, a_hwnd, a_msg, a_wp, a_lp); - return DefWindowProcA(a_hwnd, a_msg, a_wp, a_lp); - } static HRESULT WINAPI Hook_D3D11Create( IDXGIAdapter* a_adapter, @@ -56,36 +31,39 @@ namespace Addictol D3D_FEATURE_LEVEL* a_outFeatureLevel, ID3D11DeviceContext** a_outContext) noexcept { + // Force borderless-windowed when the engine requested exclusive fullscreen. + // The vanilla alt-tab hang is rooted in exclusive-mode ResizeBuffers re-entry; + // never entering exclusive eliminates the bug class entirely. + DXGI_SWAP_CHAIN_DESC patchedDesc{}; + const DXGI_SWAP_CHAIN_DESC* descToUse = a_desc; + if (a_desc && !a_desc->Windowed) + { + patchedDesc = *a_desc; + patchedDesc.Windowed = TRUE; + patchedDesc.Flags &= ~DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + descToUse = &patchedDesc; + REX::INFO("Alt-Tab Fullscreen: forcing exclusive swap chain to windowed ({}x{}, hwnd {})"sv, + a_desc->BufferDesc.Width, a_desc->BufferDesc.Height, + reinterpret_cast(a_desc->OutputWindow)); + } + const auto hr = OriginalCreate ? OriginalCreate(a_adapter, a_driverType, a_software, a_flags, a_featureLevels, - a_numFeatureLevels, a_sdkVersion, a_desc, a_outSwapChain, a_outDevice, + a_numFeatureLevels, a_sdkVersion, descToUse, a_outSwapChain, a_outDevice, a_outFeatureLevel, a_outContext) : E_FAIL; - if (FAILED(hr) || !a_outSwapChain || !*a_outSwapChain || !a_desc) + if (FAILED(hr) || !a_outSwapChain || !*a_outSwapChain || !descToUse) return hr; - // AddRef so WndProc dispatched during engine teardown won't dereference a freed swap chain. - g_swapChain = *a_outSwapChain; - g_swapChain->AddRef(); - g_hwnd = a_desc->OutputWindow; - + // Block DXGI's auto Alt+Enter; without this, DXGI can transition us back into exclusive. IDXGIFactory* factory = nullptr; - if (SUCCEEDED(g_swapChain->GetParent(__uuidof(IDXGIFactory), reinterpret_cast(&factory))) && factory) + if (SUCCEEDED((*a_outSwapChain)->GetParent(__uuidof(IDXGIFactory), reinterpret_cast(&factory))) && factory) { - factory->MakeWindowAssociation(g_hwnd, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER); + factory->MakeWindowAssociation(descToUse->OutputWindow, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER); factory->Release(); } - if (g_hwnd && !g_origWndProc) - { - if (auto* prior = reinterpret_cast(GetWindowLongPtrA(g_hwnd, GWLP_WNDPROC))) - { - g_origWndProc = prior; - SetWindowLongPtrA(g_hwnd, GWLP_WNDPROC, reinterpret_cast(&Hook_WndProc)); - } - } - return hr; } From dd24342d942e4bc91164d38a5e903341a62b496d Mon Sep 17 00:00:00 2001 From: northaxosky Date: Fri, 22 May 2026 20:10:47 -0700 Subject: [PATCH 10/10] chore: compress multi-line comment blocks in ported modules Collapse 4 explainer blocks (AltTabFullscreen, ViewmodelShading, DofFix x2) to one line each. Removes restated rationale and 'before this change' framing; keeps the non-obvious WHY. --- Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp | 4 +--- Addictol/Source/Modules/AdModuleDofFix.cpp | 9 +++------ Addictol/Source/Modules/AdModuleViewmodelShading.cpp | 3 +-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp b/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp index 546aa62..c78a428 100644 --- a/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp +++ b/Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp @@ -31,9 +31,7 @@ namespace Addictol D3D_FEATURE_LEVEL* a_outFeatureLevel, ID3D11DeviceContext** a_outContext) noexcept { - // Force borderless-windowed when the engine requested exclusive fullscreen. - // The vanilla alt-tab hang is rooted in exclusive-mode ResizeBuffers re-entry; - // never entering exclusive eliminates the bug class entirely. + // Force borderless-windowed; staying out of exclusive avoids the alt-tab ResizeBuffers re-entry hang. DXGI_SWAP_CHAIN_DESC patchedDesc{}; const DXGI_SWAP_CHAIN_DESC* descToUse = a_desc; if (a_desc && !a_desc->Windowed) diff --git a/Addictol/Source/Modules/AdModuleDofFix.cpp b/Addictol/Source/Modules/AdModuleDofFix.cpp index b26a558..341bc3d 100644 --- a/Addictol/Source/Modules/AdModuleDofFix.cpp +++ b/Addictol/Source/Modules/AdModuleDofFix.cpp @@ -8,9 +8,8 @@ namespace Addictol { static REX::TOML::Bool<> bFixesDofFix{ "Fixes"sv, "bDofFix"sv, true }; - // Worker takes 5 args; the 5th lives at [rsp+0x28] in the caller frame. The hook must declare and - // forward all five or the original reads garbage when it reaches its 5th-arg load. - // OG arg2 is the effect pointer directly; NG/AE arg2 is an index into effectList._data (+0x18). + // Worker takes 5 args (5th at [rsp+0x28]); forward all five or the original loads garbage. + // OG arg2 is the effect pointer; NG/AE arg2 is an index into effectList._data (+0x18). using TRenderEffect = void(__fastcall*)(void*, std::uintptr_t, std::int32_t, std::int32_t, void*); static TRenderEffect OriginalRenderEffect = nullptr; @@ -79,9 +78,7 @@ namespace Addictol g_workerArgIsEffectPtr = RELEX::IsRuntimeOG(); - // Camera + accumulator the engine passes to BSShaderUtil::RenderScene - // in DrawWorld::Imagespace; reusing them keeps the viewmodel re-render - // identical to what the vanilla pipeline already issues elsewhere. + // Reuse the engine's camera + accumulator from BSShaderUtil::RenderScene for pipeline parity. g_viewmodelCameraGlobal = reinterpret_cast(REL::ID{ 300623, 2712879 }.address()); g_viewmodelAccumGlobal = reinterpret_cast(REL::ID{ 726120, 2712936 }.address()); diff --git a/Addictol/Source/Modules/AdModuleViewmodelShading.cpp b/Addictol/Source/Modules/AdModuleViewmodelShading.cpp index 99b270c..b98bf23 100644 --- a/Addictol/Source/Modules/AdModuleViewmodelShading.cpp +++ b/Addictol/Source/Modules/AdModuleViewmodelShading.cpp @@ -20,8 +20,7 @@ namespace Addictol static RE::BSShaderManager::State** g_stateGlobal = nullptr; static RE::BSShaderAccumulator** g_accumulatorGlobal = nullptr; - // Add forwardLightOffset.xyz onto eyePosition.xyz; the original write at - // +0x570/+0x574/+0x578 omitted this term, breaking 1st-person specular. + // Add forwardLightOffset.xyz onto eyePosition.xyz (vanilla omits this term, breaking 1st-person specular). static void __fastcall HookMove1stPersonToOrigin() noexcept { if (OriginalMove1stPersonToOrigin)