Compare commits
5 Commits
ce55b39a25
...
d710d60519
| Author | SHA1 | Date | |
|---|---|---|---|
| d710d60519 | |||
| 9cc519a8aa | |||
| 503d18ab34 | |||
| e610f6b796 | |||
| 1b2eccfd6c |
@ -9,3 +9,5 @@ AttachLightHitEffectCrashFix = true
|
|||||||
AutoScaleHeroMenu = true
|
AutoScaleHeroMenu = true
|
||||||
WarnFormTypeCollisions = true
|
WarnFormTypeCollisions = true
|
||||||
GogVramLeakFix = true
|
GogVramLeakFix = true
|
||||||
|
MenuAspectRatioFix = true
|
||||||
|
LoadScreenFrameFix = true
|
||||||
|
|||||||
BIN
interface/inventory components/bottombar.swf
(Stored with Git LFS)
Normal file
BIN
interface/inventory components/bottombar.swf
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -57,6 +57,8 @@ auto EventListener::ProcessEvent(
|
|||||||
}
|
}
|
||||||
HeroMenuPatch::FillMenuValues();
|
HeroMenuPatch::FillMenuValues();
|
||||||
}
|
}
|
||||||
|
} else if (a_event->menuName == RE::LoadingMenu::MENU_NAME) {
|
||||||
|
LoadScreenFrameFix::OnMenuOpen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (a_event->menuName == RE::DialogueMenu::MENU_NAME) {
|
if (a_event->menuName == RE::DialogueMenu::MENU_NAME) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "Patches/DialogueMenuPatch.h"
|
#include "Patches/DialogueMenuPatch.h"
|
||||||
#include "Patches/HeroMenuPatch.h"
|
#include "Patches/HeroMenuPatch.h"
|
||||||
|
#include "Patches/LoadScreenFrameFix.h"
|
||||||
#include "Util.h"
|
#include "Util.h"
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
#include "Patches/FormTypeCollisionDetector.h"
|
#include "Patches/FormTypeCollisionDetector.h"
|
||||||
#include "Patches/GogVramLeakFix.h"
|
#include "Patches/GogVramLeakFix.h"
|
||||||
#include "Patches/ForceLanguage.h"
|
#include "Patches/ForceLanguage.h"
|
||||||
|
#include "Patches/MenuAspectRatioFix.h"
|
||||||
|
|
||||||
using namespace SKSE;
|
using namespace SKSE;
|
||||||
|
|
||||||
@ -31,7 +32,9 @@ static std::map<std::string, bool> g_settings{
|
|||||||
{ "ForceBorderless", true },
|
{ "ForceBorderless", true },
|
||||||
{ "AttachLightHitEffectCrashFix", true },
|
{ "AttachLightHitEffectCrashFix", true },
|
||||||
{ "AutoScaleHeroMenu", true },
|
{ "AutoScaleHeroMenu", true },
|
||||||
{ "GogVramLeakFix", true }
|
{ "GogVramLeakFix", true },
|
||||||
|
{ "MenuAspectRatioFix", true },
|
||||||
|
{ "LoadScreenFrameFix", true }
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -252,6 +255,10 @@ SKSEPluginLoad(const LoadInterface* skse) {
|
|||||||
logger::info("Initializing Stay At The System Page...");
|
logger::info("Initializing Stay At The System Page...");
|
||||||
JournalMenuEx::InstallHooks();
|
JournalMenuEx::InstallHooks();
|
||||||
}
|
}
|
||||||
|
if (g_settings.at("MenuAspectRatioFix")) {
|
||||||
|
MenuAspectRatioFix::Install();
|
||||||
|
}
|
||||||
|
LoadScreenFrameFix::Install(g_settings.at("LoadScreenFrameFix"));
|
||||||
}
|
}
|
||||||
|
|
||||||
logger::info("{} has finished loading.", plugin->GetName());
|
logger::info("{} has finished loading.", plugin->GetName());
|
||||||
|
|||||||
@ -37,4 +37,41 @@ namespace SKSE::stl
|
|||||||
auto& trampoline = SKSE::GetTrampoline();
|
auto& trampoline = SKSE::GetTrampoline();
|
||||||
T::func = trampoline.write_branch<N>(a_src, T::thunk);
|
T::func = trampoline.write_branch<N>(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 <class T, std::size_t BYTES>
|
||||||
|
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<const ::std::uint8_t*>(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<std::uintptr_t>(mem);
|
||||||
|
trampoline.write_branch<5>(a_src, T::thunk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
201
source/Enderal DLL/src/Patches/LoadScreenFrameFix.h
Normal file
201
source/Enderal DLL/src/Patches/LoadScreenFrameFix.h
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Fits the 3D loading screen art to Enderal's Celtic frame (painted by loadingmenu.swf) on
|
||||||
|
// displays wider than 16:9. Two different engine defects, one per runtime family:
|
||||||
|
//
|
||||||
|
// 1.6.1170 - the art is the right size but drawn left of centre, so the frame sits beside the
|
||||||
|
// picture instead of around it. Corrected by translating the art (see below).
|
||||||
|
// 1.5.97 - the art is centred but never fitted to the viewport at all: the billboard grows
|
||||||
|
// with the framebuffer, so at 32:9 it is drawn about twice its intended size and
|
||||||
|
// spills past the black matte on both sides. Corrected by scaling the art, see
|
||||||
|
// detail::Resize. This is the same "cover instead of fit" that makes the 1.5.97 main
|
||||||
|
// menu background crop where 1.6.1170 letterboxes it.
|
||||||
|
//
|
||||||
|
// Both corrections run through the same measured-projection loop, so neither needs a runtime
|
||||||
|
// version gate: each is a no-op on the runtime that does not have that defect.
|
||||||
|
//
|
||||||
|
// The cause is an engine bug in how the load screen model is fitted to the viewport. The art is an
|
||||||
|
// ordinary billboard in the UI3D scene - UI3DSceneManager::menuObjects holds the attachment node,
|
||||||
|
// whose child is the load screen NIF root (e.g. LoadScreenDawnStarShield) - and above 16:9 the
|
||||||
|
// engine multiplies that node's **entire local transform** by (16/9)/aspect, translation included,
|
||||||
|
// not just its scale. The UI3D camera does not sit on the world origin (measured at
|
||||||
|
// (-50, 600, 80) in 1.6.1170), so scaling the translation about the origin also drags the art off
|
||||||
|
// the camera axis: its world X moves from -50, which is exactly the camera's X and therefore dead
|
||||||
|
// centre, towards 0. What is left is a drift of 50 * (1 - (16/9)/aspect) world units - 24.87 at
|
||||||
|
// 32:9, which projects to the (width - height * 16/9) / 4 px measured on screen. Vertically the
|
||||||
|
// same scaling applies but the art is authored above centre anyway, and the residual is small
|
||||||
|
// enough to stay behind the frame, so only X is corrected here.
|
||||||
|
//
|
||||||
|
// Rather than hardcode any of that, the correction is measured from the engine's own projection
|
||||||
|
// each frame: project the art's world bound centre and the same point offset by its bound radius
|
||||||
|
// along world X, which gives the screen fraction per world unit, then solve for the translation
|
||||||
|
// that puts the projected centre on 0.5. That is self-calibrating - it needs no addresses, no
|
||||||
|
// version gate, and no knowledge of the camera or the aspect - and it is a feedback loop, so once
|
||||||
|
// the art is centred the computed correction is ~0 and nothing more is written. It also converges
|
||||||
|
// again by itself if the engine re-applies its own transform.
|
||||||
|
//
|
||||||
|
// Note this leaves loadingmenu.swf and the movie's GFx viewport completely untouched. Moving the
|
||||||
|
// matte inside the movie is not an option in any case: it fades in through a timeline cxform ramp
|
||||||
|
// starting at alpha 0, and setting any property on a timeline instance from script (in-movie
|
||||||
|
// ActionScript and engine-side SetVariable alike - both were shipped, both failed identically)
|
||||||
|
// detaches the instance from timeline control and freezes it at alpha 0, i.e. invisible.
|
||||||
|
namespace LoadScreenFrameFix
|
||||||
|
{
|
||||||
|
inline bool g_enabled = false;
|
||||||
|
|
||||||
|
inline void Install(bool a_enabled)
|
||||||
|
{
|
||||||
|
g_enabled = a_enabled;
|
||||||
|
|
||||||
|
if (a_enabled) {
|
||||||
|
logger::info("Loading screen frame fix armed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace detail
|
||||||
|
{
|
||||||
|
// The loading screen billboard is the only thing attached to the UI3D scene while the
|
||||||
|
// Loading Menu is up, and the only node there with a non-empty bound - the sibling fog
|
||||||
|
// emitter has none.
|
||||||
|
inline RE::NiAVObject* FindArt()
|
||||||
|
{
|
||||||
|
const auto manager = RE::UI3DSceneManager::GetSingleton();
|
||||||
|
if (!manager) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& slot : manager->menuObjects) {
|
||||||
|
const auto object = slot.get();
|
||||||
|
if (object && object->worldBound.radius > 0.0f) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.5.97 never fits the load screen model to the viewport at all - the billboard grows with
|
||||||
|
// the framebuffer, so above 16:9 it is drawn far larger than the frame and spills past the
|
||||||
|
// matte on both sides (the same "cover instead of fit" the 1.5.97 main menu background
|
||||||
|
// shows). 1.6.1170 shrinks the node by (16/9)/aspect and needs no help here.
|
||||||
|
//
|
||||||
|
// Rather than gate on a runtime version - the build that introduced the fit is not known -
|
||||||
|
// the wanted size is derived from the stage the frame is painted on, and compared with what
|
||||||
|
// the engine actually produced. The art is authored 794.7 stage units wide against the
|
||||||
|
// movie's 1280, and the bound is a sphere around a 16:9 plane, so its projected radius
|
||||||
|
// should be (794.7 / 1280 / 2) * sqrt(1 + (9/16)^2) = 0.35617 of the stage's screen width.
|
||||||
|
// Verified against 1.6.1170 at 3840x1080: predicted 0.1781, measured 0.1777.
|
||||||
|
//
|
||||||
|
// Returns true when it changed the node, so the caller re-measures before centring.
|
||||||
|
inline bool Resize(RE::NiAVObject* a_art, float a_projectedRadius)
|
||||||
|
{
|
||||||
|
if (a_projectedRadius <= 0.0f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto view = RE::UI::GetSingleton()->GetMovieView(RE::LoadingMenu::MENU_NAME);
|
||||||
|
if (!view || view->GetViewScaleMode() != RE::GFxMovieView::ScaleModeType::kShowAll) {
|
||||||
|
// Under kExactFit the stage is stretched to the whole screen, so the frame is as
|
||||||
|
// wide as the art already is; resizing to a kShowAll target would blow it up.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RE::GViewport viewport;
|
||||||
|
view->GetViewport(&viewport);
|
||||||
|
if (viewport.height <= 0 || viewport.width <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kShowAll fits the 1280x720 stage to the shorter axis, so above 16:9 the stage covers
|
||||||
|
// (16/9)/aspect of the screen width, and at or below it the full width.
|
||||||
|
const float aspect = static_cast<float>(viewport.width) / static_cast<float>(viewport.height);
|
||||||
|
const float stageWidth = aspect > (16.0f / 9.0f) ? (16.0f / 9.0f) / aspect : 1.0f;
|
||||||
|
const float target = 0.35617f * stageWidth;
|
||||||
|
const float ratio = target / a_projectedRadius;
|
||||||
|
|
||||||
|
// Wide deadband: 1.6.1170 lands within 0.2%, so it never trips this
|
||||||
|
if (ratio > 0.95f && ratio < 1.05f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
a_art->local.scale *= ratio;
|
||||||
|
|
||||||
|
static bool logged = false;
|
||||||
|
if (!logged) {
|
||||||
|
logged = true;
|
||||||
|
logger::info("Loading screen art resized: projected radius {:.4f}, target {:.4f}, scaled by {:.3f}", a_projectedRadius, target, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs once per frame for as long as the Loading Menu is up, started from EventListener
|
||||||
|
inline void Centre(std::uint32_t a_pass)
|
||||||
|
{
|
||||||
|
if (!g_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ui = RE::UI::GetSingleton();
|
||||||
|
if (!ui || !ui->IsMenuOpen(RE::LoadingMenu::MENU_NAME)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto manager = RE::UI3DSceneManager::GetSingleton();
|
||||||
|
const auto camera = manager ? manager->camera.get() : nullptr;
|
||||||
|
const auto art = detail::FindArt();
|
||||||
|
|
||||||
|
if (art && camera) {
|
||||||
|
const auto& centre = art->worldBound.center;
|
||||||
|
const float radius = art->worldBound.radius;
|
||||||
|
const RE::NiPoint3 probe{ centre.x + radius, centre.y, centre.z };
|
||||||
|
|
||||||
|
float x = 0.0f, y = 0.0f, z = 0.0f;
|
||||||
|
float probeX = 0.0f, probeY = 0.0f, probeZ = 0.0f;
|
||||||
|
|
||||||
|
if (camera->WorldPtToScreenPt3(centre, x, y, z, 1e-5f) &&
|
||||||
|
camera->WorldPtToScreenPt3(probe, probeX, probeY, probeZ, 1e-5f)) {
|
||||||
|
// Screen fraction travelled per world unit along X. Signed - on this camera +X
|
||||||
|
// maps to screen left - so the solve below works whichever way the rig faces.
|
||||||
|
const float perUnit = (probeX - x) / radius;
|
||||||
|
|
||||||
|
if (std::fabs(perUnit) > 1e-8f) {
|
||||||
|
// Size first - it moves the art, so centring has to measure afterwards
|
||||||
|
if (detail::Resize(art, std::fabs(perUnit) * radius)) {
|
||||||
|
RE::NiUpdateData updateData{};
|
||||||
|
art->Update(updateData);
|
||||||
|
} else {
|
||||||
|
const float delta = (0.5f - x) / perUnit;
|
||||||
|
|
||||||
|
// ~1 px at 3840 wide; below that the loop has converged
|
||||||
|
if (std::fabs(delta) > 0.05f) {
|
||||||
|
art->local.translate.x += delta;
|
||||||
|
|
||||||
|
RE::NiUpdateData updateData{};
|
||||||
|
art->Update(updateData);
|
||||||
|
|
||||||
|
static bool logged = false;
|
||||||
|
if (!logged) {
|
||||||
|
logged = true;
|
||||||
|
logger::info("Loading screen art centred: projected x {:.4f}, moved {:.2f} units along X", x, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading screens outlast any fixed budget, so this simply follows the menu; the cap is
|
||||||
|
// only a backstop in case the menu is somehow never reported closed.
|
||||||
|
if (a_pass < 7200) {
|
||||||
|
SKSE::GetTaskInterface()->AddUITask([a_pass]() { Centre(a_pass + 1); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from EventListener when the Loading Menu opens
|
||||||
|
inline void OnMenuOpen()
|
||||||
|
{
|
||||||
|
Centre(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
source/Enderal DLL/src/Patches/MenuAspectRatioFix.h
Normal file
139
source/Enderal DLL/src/Patches/MenuAspectRatioFix.h
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Two independent corrections to the Scaleform scale mode menus are loaded with, both applied
|
||||||
|
// from the same BSScaleformManager::LoadMovie hook:
|
||||||
|
//
|
||||||
|
// 1. Restore the modes used before 1.6.1130. That build 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. Only 1.6.1130 and later need this; see g_restoreScaleModes.
|
||||||
|
// 2. Stop kNoBorder from cropping menus on displays wider than 16:9. That one predates
|
||||||
|
// 1.6.1130 and is needed on every runtime, so it is not version gated; see ClampToViewport.
|
||||||
|
//
|
||||||
|
// The table below is what 1.6.640 passes to LoadMovie, plus BookMenu and GiftMenu, which 1.6.1130
|
||||||
|
// left on kNoBorder and which need the clamp. Menus that want no help (Book, Console, CreditsMenu,
|
||||||
|
// CursorMenu, FaderMenu, StartMenu) are absent and keep whatever the caller asked for. It doubles
|
||||||
|
// as the list of menus this patch is allowed to touch at all, so on older runtimes - where the
|
||||||
|
// engine already passes these very modes - a third-party menu loaded with kNoBorder is left alone.
|
||||||
|
//
|
||||||
|
// Based on Widescreen Scale Removed by SkyHorizon (GPL-3.0)
|
||||||
|
// https://www.nexusmods.com/skyrimspecialedition/mods/136793
|
||||||
|
namespace MenuAspectRatioFix
|
||||||
|
{
|
||||||
|
using ScaleModeType = RE::GFxMovieView::ScaleModeType;
|
||||||
|
|
||||||
|
// True on the runtimes that stretch menus to fill the viewport, i.e. 1.6.1130 and later. Below
|
||||||
|
// that the engine already asks for the modes in the table, so only the clamp has work to do.
|
||||||
|
inline bool g_restoreScaleModes = false;
|
||||||
|
|
||||||
|
std::optional<ScaleModeType> GetScaleMode(const char* a_fileName)
|
||||||
|
{
|
||||||
|
static const std::map<std::string_view, ScaleModeType> 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 },
|
||||||
|
{ "BookMenu"sv, ScaleModeType::kNoBorder },
|
||||||
|
{ "ContainerMenu"sv, ScaleModeType::kNoBorder },
|
||||||
|
{ "CraftingMenu"sv, ScaleModeType::kNoBorder },
|
||||||
|
{ "DialogueMenu"sv, ScaleModeType::kNoBorder },
|
||||||
|
{ "GiftMenu"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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The movie gets the whole framebuffer as its viewport, so kNoBorder scales a 1280x720 menu
|
||||||
|
// until it covers the full width and crops whatever no longer fits: at 32:9 that is a 2x zoom
|
||||||
|
// with half the menu off screen. Past 16:9 it can only ever crop, so hand out kShowAll instead -
|
||||||
|
// that keeps the 16:9 scale and merely leaves more of the stage surroundings visible, which is
|
||||||
|
// exactly what SkyUI's widescreen menus stretch their bars over (they cover up to 32:9).
|
||||||
|
// Narrower than 16:9 kNoBorder is still the better answer: it hides the strips above and below
|
||||||
|
// the stage that kShowAll would expose.
|
||||||
|
ScaleModeType ClampToViewport(ScaleModeType a_mode)
|
||||||
|
{
|
||||||
|
if (a_mode != ScaleModeType::kNoBorder) {
|
||||||
|
return a_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto state = RE::BSGraphics::State::GetSingleton();
|
||||||
|
if (!state || state->screenHeight == 0) {
|
||||||
|
return a_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every vanilla menu is authored on a 1280x720 stage
|
||||||
|
constexpr float stageAspect = 1280.0f / 720.0f;
|
||||||
|
const float viewportAspect = static_cast<float>(state->screenWidth) / static_cast<float>(state->screenHeight);
|
||||||
|
|
||||||
|
return viewportAspect > stageAspect ? ScaleModeType::kShowAll : a_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoadMovie
|
||||||
|
{
|
||||||
|
static bool thunk(RE::BSScaleformManager* a_scaleformManager, RE::IMenu* a_menu, RE::GPtr<RE::GFxMovieView>& 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 = ClampToViewport(g_restoreScaleModes ? *mode : a_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(a_scaleformManager, a_menu, a_viewOut, a_fileName, a_mode, a_backgroundAlpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline REL::Relocation<decltype(thunk)> func;
|
||||||
|
};
|
||||||
|
|
||||||
|
void Install()
|
||||||
|
{
|
||||||
|
g_restoreScaleModes = REL::Module::get().version() >= REL::Version(1, 6, 1130, 0);
|
||||||
|
|
||||||
|
// Checked: 1.5.97, 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 followed by one argument spill - 7 bytes of position-independent code, which
|
||||||
|
// the hook relocates into the trampoline. The spilled argument differs between the runtime
|
||||||
|
// families, so accept either and bail out rather than corrupt the function if a future
|
||||||
|
// build changes the prologue again.
|
||||||
|
constexpr std::uint8_t prologueAE[]{ 0x48, 0x8B, 0xC4, 0x48, 0x89, 0x50, 0x10 }; // mov [rax+10h],rdx
|
||||||
|
constexpr std::uint8_t prologueSE[]{ 0x48, 0x8B, 0xC4, 0x4C, 0x89, 0x40, 0x18 }; // mov [rax+18h],r8
|
||||||
|
|
||||||
|
static_assert(sizeof(prologueAE) == sizeof(prologueSE));
|
||||||
|
|
||||||
|
const auto prologue = reinterpret_cast<const void*>(target);
|
||||||
|
|
||||||
|
if (!target ||
|
||||||
|
(std::memcmp(prologue, prologueAE, sizeof(prologueAE)) != 0 &&
|
||||||
|
std::memcmp(prologue, prologueSE, sizeof(prologueSE)) != 0)) {
|
||||||
|
logger::error("Unexpected BSScaleformManager::LoadMovie prologue, menu aspect ratio fix is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SKSE::AllocTrampoline(64);
|
||||||
|
SKSE::stl::hook_function_prologue<LoadMovie, sizeof(prologueAE)>(target);
|
||||||
|
|
||||||
|
logger::info("Applied menu aspect ratio fix{}", g_restoreScaleModes ? "" : " (viewport clamp only)");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user