From 1b2eccfd6cc5bbd5bde5c00f481f4d0fbc157990 Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Thu, 30 Jul 2026 21:45:06 +0200 Subject: [PATCH] Added Widescreen Scale Removed by SkyHorizon --- SKSE/Plugins/EnderalSE.ini | 1 + source/Enderal DLL/src/Main.cpp | 8 +- source/Enderal DLL/src/PCH.h | 37 ++++++++ .../src/Patches/MenuAspectRatioFix.h | 91 +++++++++++++++++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 source/Enderal DLL/src/Patches/MenuAspectRatioFix.h diff --git a/SKSE/Plugins/EnderalSE.ini b/SKSE/Plugins/EnderalSE.ini index c0b2766bc..a602008df 100644 --- a/SKSE/Plugins/EnderalSE.ini +++ b/SKSE/Plugins/EnderalSE.ini @@ -9,3 +9,4 @@ AttachLightHitEffectCrashFix = true AutoScaleHeroMenu = true WarnFormTypeCollisions = true GogVramLeakFix = true +MenuAspectRatioFix = true diff --git a/source/Enderal DLL/src/Main.cpp b/source/Enderal DLL/src/Main.cpp index a1022fc80..e2070551d 100644 --- a/source/Enderal DLL/src/Main.cpp +++ b/source/Enderal DLL/src/Main.cpp @@ -17,6 +17,7 @@ #include "Patches/FormTypeCollisionDetector.h" #include "Patches/GogVramLeakFix.h" #include "Patches/ForceLanguage.h" +#include "Patches/MenuAspectRatioFix.h" using namespace SKSE; @@ -31,7 +32,8 @@ static std::map g_settings{ { "ForceBorderless", true }, { "AttachLightHitEffectCrashFix", true }, { "AutoScaleHeroMenu", true }, - { "GogVramLeakFix", true } + { "GogVramLeakFix", true }, + { "MenuAspectRatioFix", true } }; namespace { @@ -252,6 +254,10 @@ SKSEPluginLoad(const LoadInterface* skse) { logger::info("Initializing Stay At The System Page..."); JournalMenuEx::InstallHooks(); } + // Menus are only stretched to fill the viewport since 1.6.1130 + if (g_settings.at("MenuAspectRatioFix") && REL::Module::get().version() >= REL::Version(1, 6, 1130, 0)) { + MenuAspectRatioFix::Install(); + } } logger::info("{} has finished loading.", plugin->GetName()); diff --git a/source/Enderal DLL/src/PCH.h b/source/Enderal DLL/src/PCH.h index cbc4a6c9b..e30e74333 100644 --- a/source/Enderal DLL/src/PCH.h +++ b/source/Enderal DLL/src/PCH.h @@ -37,4 +37,41 @@ namespace SKSE::stl auto& trampoline = SKSE::GetTrampoline(); T::func = trampoline.write_branch(a_src, T::thunk); } + + // Detours a whole function: the first BYTES bytes are copied into the trampoline and followed by + // a jump back into the original, so T::func is a callable stand-in for the untouched function. + // BYTES must cover complete, position-independent instructions and be at least 5. + // Costs 14 bytes of trampoline for the branch island plus BYTES + 14 for the stub. + // Thanks Nukem and po3, via Widescreen Scale Removed by SkyHorizon (GPL-3.0). + template + void hook_function_prologue(std::uintptr_t a_src) + { + static_assert(BYTES >= 5, "not enough room for a jump"); + + // Xbyak::CodeGenerator has a member function std() - the STD instruction - which hides the + // namespace inside the class body, hence the leading :: on every std:: name below + struct Stub : Xbyak::CodeGenerator + { + Stub(std::uintptr_t a_target, std::size_t a_size) + { + for (::std::size_t i = 0; i < a_size; ++i) { + db(*reinterpret_cast(a_target + i)); + } + + jmp(ptr[rip]); + dq(a_target + a_size); + } + }; + + Stub stub(a_src, BYTES); + stub.ready(); + + auto& trampoline = SKSE::GetTrampoline(); + auto* mem = trampoline.allocate(stub.getSize()); + std::memcpy(mem, stub.getCode(), stub.getSize()); + + // Publish the stand-in before redirecting the function, the thunk needs it on its first call + T::func = reinterpret_cast(mem); + trampoline.write_branch<5>(a_src, T::thunk); + } } diff --git a/source/Enderal DLL/src/Patches/MenuAspectRatioFix.h b/source/Enderal DLL/src/Patches/MenuAspectRatioFix.h new file mode 100644 index 000000000..f4ff1341a --- /dev/null +++ b/source/Enderal DLL/src/Patches/MenuAspectRatioFix.h @@ -0,0 +1,91 @@ +#pragma once + +// Restores the Scaleform scale modes menus were loaded with before Skyrim 1.6.1130. +// +// 1.6.1130 switched nearly every menu to kExactFit, which stretches the movie across the whole +// viewport instead of preserving its aspect ratio. Enderal's menus - including the SkyUI-derived +// widescreen versions shipped with them - are authored for the old modes, so on anything that is +// not 16:9 they come out distorted. +// +// The table below is what 1.6.640 passes to BSScaleformManager::LoadMovie. Menus the update left +// alone (Book, BookMenu, Console, CreditsMenu, CursorMenu, FaderMenu, GiftMenu, StartMenu) are +// deliberately absent and keep whatever the caller asked for. +// +// Based on Widescreen Scale Removed by SkyHorizon (GPL-3.0) +// https://www.nexusmods.com/skyrimspecialedition/mods/136793 +namespace MenuAspectRatioFix +{ + using ScaleModeType = RE::GFxMovieView::ScaleModeType; + + std::optional GetScaleMode(const char* a_fileName) + { + static const std::map modes{ + { "FavoritesMenu"sv, ScaleModeType::kShowAll }, + { "HUDMenu"sv, ScaleModeType::kShowAll }, + { "LevelUpMenu"sv, ScaleModeType::kShowAll }, + { "LoadWaitSpinner"sv, ScaleModeType::kShowAll }, + { "LoadingMenu"sv, ScaleModeType::kShowAll }, + { "Map"sv, ScaleModeType::kShowAll }, + { "Quest_Journal"sv, ScaleModeType::kShowAll }, + { "SafeZone"sv, ScaleModeType::kShowAll }, + { "SleepWaitMenu"sv, ScaleModeType::kShowAll }, + { "Titles"sv, ScaleModeType::kShowAll }, + { "TutorialMenu"sv, ScaleModeType::kShowAll }, + { "TweenMenu"sv, ScaleModeType::kShowAll }, + + { "BarterMenu"sv, ScaleModeType::kNoBorder }, + { "ContainerMenu"sv, ScaleModeType::kNoBorder }, + { "CraftingMenu"sv, ScaleModeType::kNoBorder }, + { "DialogueMenu"sv, ScaleModeType::kNoBorder }, + { "InventoryMenu"sv, ScaleModeType::kNoBorder }, + { "LockpickingMenu"sv, ScaleModeType::kNoBorder }, + { "MagicMenu"sv, ScaleModeType::kNoBorder }, + { "MessageBox"sv, ScaleModeType::kNoBorder }, + { "RaceSex_menu"sv, ScaleModeType::kNoBorder }, + { "StatsMenu"sv, ScaleModeType::kNoBorder }, + { "TrainingMenu"sv, ScaleModeType::kNoBorder }, + + // Replaces CraftingMenu, https://www.nexusmods.com/skyrimspecialedition/mods/81409 + { "ConstructibleObjectMenu"sv, ScaleModeType::kNoBorder } + }; + + const auto it = modes.find(a_fileName); + return it != modes.end() ? std::optional{ it->second } : std::nullopt; + } + + struct LoadMovie + { + static bool thunk(RE::BSScaleformManager* a_scaleformManager, RE::IMenu* a_menu, RE::GPtr& a_viewOut, const char* a_fileName, ScaleModeType a_mode, float a_backgroundAlpha) + { + if (a_fileName && a_fileName[0]) { + if (const auto mode = GetScaleMode(a_fileName); mode) { + a_mode = *mode; + } + } + + return func(a_scaleformManager, a_menu, a_viewOut, a_fileName, a_mode, a_backgroundAlpha); + } + + static inline REL::Relocation func; + }; + + void Install() + { + // Checked: 1.6.640, 1.6.659, 1.6.1130, 1.6.1170, 1.6.1179 + const auto target = REL::RelocationID(80302, 82325).address(); + + // mov rax,rsp / mov qword ptr [rax+10h],rdx - relocated into the trampoline by the hook, + // so bail out rather than corrupt the function if a future runtime changes the prologue + constexpr std::uint8_t prologue[]{ 0x48, 0x8B, 0xC4, 0x48, 0x89, 0x50, 0x10 }; + + if (!target || std::memcmp(reinterpret_cast(target), prologue, sizeof(prologue)) != 0) { + logger::error("Unexpected BSScaleformManager::LoadMovie prologue, menu aspect ratio fix is disabled"); + return; + } + + SKSE::AllocTrampoline(64); + SKSE::stl::hook_function_prologue(target); + + logger::info("Applied menu aspect ratio fix"); + } +}