Compatibility with widescreen-authored UI mods
This commit is contained in:
parent
ca5d666b63
commit
cf84e2f026
@ -86,8 +86,11 @@ void HeroMenuPatch::FillMenuValues()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HeroMenuPatch::bAutoScaleHeroMenu && !REL::Module::IsVR()) {
|
if (HeroMenuPatch::bAutoScaleHeroMenu && !REL::Module::IsVR() && !MovieStageIsWiderThan16x9(uiMovie.get())) {
|
||||||
// Fit the movie into the screen (widescreen support)
|
// Fit the movie into the screen (widescreen support). A replacement 00e_heromenu.swf
|
||||||
|
// authored wider than 16:9 (Untarnished UI ports ship a 2560x720 one) is left alone:
|
||||||
|
// kShowAll letterboxes a wide stage into a band, and the wide-stage override in
|
||||||
|
// MenuAspectRatioFix already hands such movies the mode they are authored for.
|
||||||
uiMovie->SetViewScaleMode(RE::BSScaleformManager::ScaleModeType::kShowAll);
|
uiMovie->SetViewScaleMode(RE::BSScaleformManager::ScaleModeType::kShowAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,14 @@
|
|||||||
// clamped per art instance, and a clamp hit is logged as a warning: if that line ever appears, a
|
// clamped per art instance, and a clamp hit is logged as a warning: if that line ever appears, a
|
||||||
// measurement is wrong again - find out why before loosening the clamp.
|
// measurement is wrong again - find out why before loosening the clamp.
|
||||||
//
|
//
|
||||||
|
// The billboard is also *selected* by measurement, not by position in the slot array: the art is
|
||||||
|
// the only billboard whose projection matches the known art laws, and menuObjects can hold
|
||||||
|
// foreign geometry during a load. The fog slot's NIF (intmenufogparticles.nif, an empty stub in
|
||||||
|
// stock Enderal) is replaceable by UI packs, and one shipped a stray EditorMarker shape in it;
|
||||||
|
// taking "first slot with geometry" then wrecked the art through the clamps (see FindArt).
|
||||||
|
// Candidates that fail the law are skipped, and logged once per menu open when a tick selects
|
||||||
|
// nothing at all.
|
||||||
|
//
|
||||||
// Note this leaves loadingmenu.swf and the movie's GFx viewport completely untouched. Moving the
|
// 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
|
// 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
|
// starting at alpha 0, and setting any property on a timeline instance from script (in-movie
|
||||||
@ -62,6 +70,9 @@ namespace LoadScreenFrameFix
|
|||||||
{
|
{
|
||||||
RE::NiAVObject* root{ nullptr }; // the menuObjects attachment node - corrections are written here
|
RE::NiAVObject* root{ nullptr }; // the menuObjects attachment node - corrections are written here
|
||||||
RE::NiAVObject* shape{ nullptr }; // the billboard geometry - measurements are read here
|
RE::NiAVObject* shape{ nullptr }; // the billboard geometry - measurements are read here
|
||||||
|
float x{ 0.0f }; // projected screen x of the geometry's bound centre
|
||||||
|
float perUnit{ 0.0f }; // signed screen fraction travelled per world unit along X
|
||||||
|
float radius{ 0.0f }; // projected bound radius, as a fraction of the screen width
|
||||||
};
|
};
|
||||||
|
|
||||||
// Everything applied to one art instance so far. Long loads swap the art NIF mid-stream
|
// Everything applied to one art instance so far. Long loads swap the art NIF mid-stream
|
||||||
@ -85,54 +96,127 @@ namespace LoadScreenFrameFix
|
|||||||
|
|
||||||
inline State g_state;
|
inline State g_state;
|
||||||
|
|
||||||
// The largest non-particle geometry in the slot's subtree - the art billboard. Fog
|
// Every visible non-particle geometry in the subtree with a live bound. Fog emitters are
|
||||||
// emitters are NiParticles-derived and must not contaminate the measurement: their
|
// NiParticles-derived and must not contaminate the measurement: their bounds are
|
||||||
// bounds are computed in world space from the live particles, so they neither follow
|
// computed in world space from the live particles, so they neither follow the
|
||||||
// the attachment node's scale nor hold still.
|
// attachment node's scale nor hold still. Culled geometry is skipped too - invisible
|
||||||
inline RE::NiAVObject* FindShape(RE::NiAVObject* a_object)
|
// shapes are never the art, and the known foreign billboard (see FindArt) is a stray
|
||||||
|
// EditorMarker, which the engine hides by name.
|
||||||
|
inline void CollectShapes(RE::NiAVObject* a_object, std::vector<RE::NiAVObject*>& a_out)
|
||||||
{
|
{
|
||||||
if (!a_object) {
|
if (!a_object || a_object->GetAppCulled()) {
|
||||||
return nullptr;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a_object->AsGeometry()) {
|
if (a_object->AsGeometry()) {
|
||||||
return a_object->AsParticlesGeom() ? nullptr : a_object;
|
if (!a_object->AsParticlesGeom() && a_object->worldBound.radius > 0.0f) {
|
||||||
|
a_out.push_back(a_object);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RE::NiAVObject* best = nullptr;
|
|
||||||
|
|
||||||
if (const auto node = a_object->AsNode()) {
|
if (const auto node = a_object->AsNode()) {
|
||||||
for (const auto& child : node->GetChildren()) {
|
for (const auto& child : node->GetChildren()) {
|
||||||
const auto found = FindShape(child.get());
|
CollectShapes(child.get(), a_out);
|
||||||
if (found && (!best || found->worldBound.radius > best->worldBound.radius)) {
|
|
||||||
best = found;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return best;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The loading screen art is the only billboard attached to the UI3D scene while the
|
// One flag per Loading Menu open: when a tick finds billboards but selects none, they
|
||||||
// Loading Menu is up; slots holding nothing, or only emitters, are skipped.
|
// are logged once, so a foreign object in the scene is named instead of silently skipped.
|
||||||
inline Art FindArt()
|
inline bool g_loggedRejects = false;
|
||||||
|
|
||||||
|
// Picks the billboard whose projection matches the loading screen art. The art is not
|
||||||
|
// the only thing that can sit in UI3DSceneManager::menuObjects during a load: the fog
|
||||||
|
// slot is loaded from meshes/interface/intmenufogparticles.nif, which Enderal ships as
|
||||||
|
// an empty stub - why "first slot with geometry" survived every earlier test - but which
|
||||||
|
// UI packs replace. "Vel'dun UI Enderal" repurposes a world snowfall effect there
|
||||||
|
// (FXAmbGentlyFallingSnow00) and left its EditorMarker BSTriShape in: a non-particle
|
||||||
|
// geometry, present on every load path, bound radius 261.45 x 0.5 root scale = 130.7
|
||||||
|
// world units - exactly the ~131 units back-calculated from the wreck's log. At 32:9 it
|
||||||
|
// measured projected radius 0.7349 at x -0.0817, and the loop shrank the wrong node to
|
||||||
|
// the clamp floor and dragged it sideways: savegame loads and cell transitions both
|
||||||
|
// showed the art at 1/3 size, pinned to the right edge of the frame hole.
|
||||||
|
//
|
||||||
|
// The art's projection is known, so every candidate is checked against it:
|
||||||
|
//
|
||||||
|
// - projected radius: 0.35617 * (16/9)/aspect when the engine fitted the model
|
||||||
|
// (1.6.x), 0.35617 flat when it did not (1.5.97), with authoring variance around
|
||||||
|
// either law (see Resize). The window [0.70 * fitted, 1.35 * unfitted] covers both
|
||||||
|
// laws, every mid-correction state between them, and the +27% outlier art measured
|
||||||
|
// in the wild - and rejects the logo at 2x its top.
|
||||||
|
// - projected centre x: the engine's drift keeps the art centre inside mid-screen
|
||||||
|
// (0.376 at 32:9 stock, 0.5 once corrected), so [0.25, 0.75] is generous.
|
||||||
|
//
|
||||||
|
// Among the survivors the largest wins - the art is the dominant legitimate billboard.
|
||||||
|
inline Art FindArt(RE::NiCamera* a_camera, float a_aspect)
|
||||||
{
|
{
|
||||||
const auto manager = RE::UI3DSceneManager::GetSingleton();
|
const auto manager = RE::UI3DSceneManager::GetSingleton();
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const float fitted = 0.35617f * (16.0f / 9.0f) / a_aspect;
|
||||||
|
constexpr float unfitted = 0.35617f;
|
||||||
|
|
||||||
|
struct Reject
|
||||||
|
{
|
||||||
|
RE::NiAVObject* root;
|
||||||
|
RE::NiAVObject* shape;
|
||||||
|
float radius;
|
||||||
|
float x;
|
||||||
|
};
|
||||||
|
|
||||||
|
Art best;
|
||||||
|
std::vector<Reject> rejected;
|
||||||
|
std::vector<RE::NiAVObject*> shapes;
|
||||||
|
|
||||||
for (const auto& slot : manager->menuObjects) {
|
for (const auto& slot : manager->menuObjects) {
|
||||||
const auto object = slot.get();
|
const auto object = slot.get();
|
||||||
if (object) {
|
if (!object) {
|
||||||
const auto shape = FindShape(object);
|
continue;
|
||||||
if (shape && shape->worldBound.radius > 0.0f) {
|
}
|
||||||
return { object, shape };
|
|
||||||
|
shapes.clear();
|
||||||
|
CollectShapes(object, shapes);
|
||||||
|
|
||||||
|
for (const auto shape : shapes) {
|
||||||
|
const auto& centre = shape->worldBound.center;
|
||||||
|
const float radius = shape->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 (!a_camera->WorldPtToScreenPt3(centre, x, y, z, 1e-5f) ||
|
||||||
|
!a_camera->WorldPtToScreenPt3(probe, probeX, probeY, probeZ, 1e-5f)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float perUnit = (probeX - x) / radius;
|
||||||
|
const float projected = std::fabs(perUnit) * radius;
|
||||||
|
|
||||||
|
if (projected < 0.70f * fitted || projected > 1.35f * unfitted || x < 0.25f || x > 0.75f) {
|
||||||
|
rejected.push_back({ object, shape, projected, x });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::fabs(perUnit) > 1e-8f && projected > best.radius) {
|
||||||
|
best = { object, shape, x, perUnit, projected };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
// Logged only when nothing was selected: mid-attach art states can look odd for a
|
||||||
|
// frame, but they never end a tick empty-handed on a healthy load.
|
||||||
|
if (!best.shape && !rejected.empty() && !g_loggedRejects) {
|
||||||
|
g_loggedRejects = true;
|
||||||
|
for (const auto& reject : rejected) {
|
||||||
|
logger::info("Loading screen candidate rejected: {}/{} projects radius {:.4f} at x {:.4f}, not loading art (window {:.4f}..{:.4f}, x 0.25..0.75)", reject.root->name.c_str(), reject.shape->name.c_str(), reject.radius, reject.x, 0.70f * fitted, 1.35f * unfitted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.5.97 never fits the load screen model to the viewport at all - the billboard grows with
|
// 1.5.97 never fits the load screen model to the viewport at all - the billboard grows with
|
||||||
@ -157,6 +241,13 @@ namespace LoadScreenFrameFix
|
|||||||
if (a_view->GetViewScaleMode() != RE::GFxMovieView::ScaleModeType::kShowAll) {
|
if (a_view->GetViewScaleMode() != RE::GFxMovieView::ScaleModeType::kShowAll) {
|
||||||
// Under kExactFit the stage is stretched to the whole screen, so the frame is as
|
// 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.
|
// wide as the art already is; resizing to a kShowAll target would blow it up.
|
||||||
|
// kNoBorder means a third-party loadingmenu.swf authored wider than 16:9 (see
|
||||||
|
// the wide-stage override in MenuAspectRatioFix), running below its stage
|
||||||
|
// aspect. Skipping those is accepted: on 1.5.97 the oversized art then stays
|
||||||
|
// oversized, but such a frame covers the whole screen outside its hole, so the
|
||||||
|
// defect shows only as a zoomed-in picture inside the hole, never as spill -
|
||||||
|
// and the packs shipping these movies target 1.6.1130+ anyway. The centring
|
||||||
|
// half of the tick still runs for them, which it must: their holes are centred.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,61 +354,52 @@ namespace LoadScreenFrameFix
|
|||||||
|
|
||||||
const auto manager = RE::UI3DSceneManager::GetSingleton();
|
const auto manager = RE::UI3DSceneManager::GetSingleton();
|
||||||
const auto camera = manager ? manager->camera.get() : nullptr;
|
const auto camera = manager ? manager->camera.get() : nullptr;
|
||||||
const auto art = detail::FindArt();
|
if (!camera) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (art.root && camera) {
|
// FindArt already projected the selected billboard, so its numbers are reused here.
|
||||||
if (detail::g_state.shape != art.shape) {
|
// perUnit is the signed screen fraction per world unit along X - on this camera +X maps
|
||||||
detail::g_state = {};
|
// to screen left - so the solve below works whichever way the rig faces.
|
||||||
detail::g_state.shape = art.shape;
|
const auto art = detail::FindArt(camera, aspect);
|
||||||
}
|
if (!art.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& centre = art.shape->worldBound.center;
|
if (detail::g_state.shape != art.shape) {
|
||||||
const float radius = art.shape->worldBound.radius;
|
detail::g_state = {};
|
||||||
const RE::NiPoint3 probe{ centre.x + radius, centre.y, centre.z };
|
detail::g_state.shape = art.shape;
|
||||||
|
}
|
||||||
|
|
||||||
float x = 0.0f, y = 0.0f, z = 0.0f;
|
// Size first - it moves the art, so centring has to measure afterwards
|
||||||
float probeX = 0.0f, probeY = 0.0f, probeZ = 0.0f;
|
if (detail::Resize(art.root, art.radius, view.get(), aspect)) {
|
||||||
|
RE::NiUpdateData updateData{};
|
||||||
|
art.root->Update(updateData);
|
||||||
|
} else {
|
||||||
|
const float delta = (0.5f - art.x) / art.perUnit;
|
||||||
|
|
||||||
if (camera->WorldPtToScreenPt3(centre, x, y, z, 1e-5f) &&
|
// ~1 px at 3840 wide; below that the loop has converged
|
||||||
camera->WorldPtToScreenPt3(probe, probeX, probeY, probeZ, 1e-5f)) {
|
if (std::fabs(delta) > 0.05f) {
|
||||||
// Screen fraction travelled per world unit along X. Signed - on this camera +X
|
const float total = std::clamp(detail::g_state.shift + delta, -150.0f, 150.0f);
|
||||||
// maps to screen left - so the solve below works whichever way the rig faces.
|
const float applied = total - detail::g_state.shift;
|
||||||
const float perUnit = (probeX - x) / radius;
|
|
||||||
|
|
||||||
if (std::fabs(perUnit) > 1e-8f) {
|
if (std::fabs(applied) > 0.05f) {
|
||||||
// Size first - it moves the art, so centring has to measure afterwards
|
detail::g_state.shift = total;
|
||||||
if (detail::Resize(art.root, std::fabs(perUnit) * radius, view.get(), aspect)) {
|
art.root->local.translate.x += applied;
|
||||||
RE::NiUpdateData updateData{};
|
|
||||||
art.root->Update(updateData);
|
|
||||||
} else {
|
|
||||||
const float delta = (0.5f - x) / perUnit;
|
|
||||||
|
|
||||||
// ~1 px at 3840 wide; below that the loop has converged
|
RE::NiUpdateData updateData{};
|
||||||
if (std::fabs(delta) > 0.05f) {
|
art.root->Update(updateData);
|
||||||
const float total = std::clamp(detail::g_state.shift + delta, -150.0f, 150.0f);
|
|
||||||
const float applied = total - detail::g_state.shift;
|
|
||||||
|
|
||||||
if (std::fabs(applied) > 0.05f) {
|
if (!detail::g_state.loggedCentre) {
|
||||||
detail::g_state.shift = total;
|
detail::g_state.loggedCentre = true;
|
||||||
art.root->local.translate.x += applied;
|
logger::info("Loading screen art centred: projected x {:.4f}, moved {:.2f} units along X", art.x, applied);
|
||||||
|
|
||||||
RE::NiUpdateData updateData{};
|
|
||||||
art.root->Update(updateData);
|
|
||||||
|
|
||||||
if (!detail::g_state.loggedCentre) {
|
|
||||||
detail::g_state.loggedCentre = true;
|
|
||||||
logger::info("Loading screen art centred: projected x {:.4f}, moved {:.2f} units along X", x, applied);
|
|
||||||
}
|
|
||||||
} else if (!detail::g_state.warnedClamp) {
|
|
||||||
detail::g_state.warnedClamp = true;
|
|
||||||
logger::warn("Loading screen art correction clamped ({:.3f}x scale, {:.1f} units of shift applied) - a measurement is off, not converging", detail::g_state.scale, detail::g_state.shift);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (!detail::g_state.warnedClamp) {
|
||||||
|
detail::g_state.warnedClamp = true;
|
||||||
|
logger::warn("Loading screen art correction clamped ({:.3f}x scale, {:.1f} units of shift applied) - a measurement is off, not converging", detail::g_state.scale, detail::g_state.shift);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void AdvanceMovieEx(RE::IMenu* a_this, float a_interval, std::uint32_t a_currentTime);
|
inline void AdvanceMovieEx(RE::IMenu* a_this, float a_interval, std::uint32_t a_currentTime);
|
||||||
@ -350,6 +432,7 @@ namespace LoadScreenFrameFix
|
|||||||
inline void OnMenuOpen()
|
inline void OnMenuOpen()
|
||||||
{
|
{
|
||||||
detail::g_state = {};
|
detail::g_state = {};
|
||||||
|
detail::g_loggedRejects = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from EventListener when the Main Menu opens. Until that first happens, any billboard
|
// Called from EventListener when the Main Menu opens. Until that first happens, any billboard
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "Util.h"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
|
||||||
// Two independent corrections to the Scaleform scale mode menus are loaded with, both applied
|
// Three corrections to the Scaleform scale mode menus are loaded with, all applied from the same
|
||||||
// from the same BSScaleformManager::LoadMovie hook:
|
// BSScaleformManager::LoadMovie hook:
|
||||||
//
|
//
|
||||||
// 1. Restore the modes used before 1.6.1130. That build switched nearly every menu to
|
// 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
|
// kExactFit, which stretches the movie across the whole viewport instead of preserving its
|
||||||
@ -12,6 +14,10 @@
|
|||||||
// distorted. Only 1.6.1130 and later need this; see g_restoreScaleModes.
|
// 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
|
// 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.
|
// 1.6.1130 and is needed on every runtime, so it is not version gated; see ClampToViewport.
|
||||||
|
// 3. For a table menu whose movie file was replaced by a re-author on a stage wider than
|
||||||
|
// 16:9, override both of the above with a mode derived from the stage itself; see
|
||||||
|
// FitWideStage. Corrections 1 and 2 assume the 1280x720 authoring every vanilla, SkyUI and
|
||||||
|
// Enderal movie uses, and they break the wide re-authors at most aspect ratios.
|
||||||
//
|
//
|
||||||
// The table below is what 1.6.640 passes to LoadMovie, plus BookMenu and GiftMenu, which 1.6.1130
|
// 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,
|
// left on kNoBorder and which need the clamp. Menus that want no help (Book, Console, CreditsMenu,
|
||||||
@ -128,17 +134,68 @@ namespace MenuAspectRatioFix
|
|||||||
return viewportAspect > stageAspect ? ScaleModeType::kShowAll : a_mode;
|
return viewportAspect > stageAspect ? ScaleModeType::kShowAll : a_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Third-party UI packs replace table movies with re-authors on stages wider than 16:9 - the
|
||||||
|
// Untarnished UI ports (for example "Vel'dun UI Enderal") ship 2560x720 movies for the
|
||||||
|
// inventory family, the dialogue menu, the loading menu and the custom hero menu. Those
|
||||||
|
// movies implement widescreen themselves, as a kNoBorder crop: the stage always renders at
|
||||||
|
// the height-fit scale, so every element keeps one physical size, and the extra stage width
|
||||||
|
// stays outside the viewport until the display is as wide as the stage (their loading menu
|
||||||
|
// frame keeps a constant 676-stage-unit hole this way, aligned with the engine's 3D art at
|
||||||
|
// every aspect ratio). Both modes the table can pick destroy that design: kShowAll must show
|
||||||
|
// the whole stage, which letterboxes a 32:9 stage into a half-height band on a 16:9 display,
|
||||||
|
// and kExactFit distorts it. So for these movies the mode comes from the stage instead -
|
||||||
|
// kNoBorder up to the stage aspect, kShowAll beyond it, where kNoBorder would start cropping
|
||||||
|
// again (the two coincide when viewport and stage aspect are equal, hence the loose
|
||||||
|
// comparison). Like the clamp, this is not gated on g_restoreScaleModes: pre-1.6.1130 call
|
||||||
|
// sites hand these movies an equally wrong mode, kShowAll for the loading menu for instance.
|
||||||
|
//
|
||||||
|
// The stage size is only readable from the loaded movie, so this runs after the original
|
||||||
|
// LoadMovie - which applies its mode argument before returning - and writes the override to
|
||||||
|
// the view directly.
|
||||||
|
void FitWideStage(RE::GFxMovieView* a_view, const char* a_fileName)
|
||||||
|
{
|
||||||
|
const auto def = a_view->GetMovieDef();
|
||||||
|
const auto state = RE::BSGraphics::State::GetSingleton();
|
||||||
|
if (!def || !state || state->screenHeight == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float stageAspect = def->GetWidth() / def->GetHeight();
|
||||||
|
const float viewportAspect = static_cast<float>(state->screenWidth) / static_cast<float>(state->screenHeight);
|
||||||
|
const auto mode = viewportAspect >= stageAspect - 0.005f ? ScaleModeType::kShowAll : ScaleModeType::kNoBorder;
|
||||||
|
|
||||||
|
a_view->SetViewScaleMode(mode);
|
||||||
|
|
||||||
|
// Menus reload on every open, so log each movie once
|
||||||
|
static std::set<std::string, CaseInsensitiveLess> logged;
|
||||||
|
if (logged.emplace(a_fileName).second) {
|
||||||
|
logger::info("{} is authored wider than 16:9 ({:.0f}x{:.0f}), overriding its scale mode to {}", a_fileName, def->GetWidth(), def->GetHeight(), mode == ScaleModeType::kShowAll ? "kShowAll" : "kNoBorder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct LoadMovie
|
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)
|
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)
|
||||||
{
|
{
|
||||||
|
std::optional<ScaleModeType> tableMode;
|
||||||
|
|
||||||
if (a_fileName && a_fileName[0]) {
|
if (a_fileName && a_fileName[0]) {
|
||||||
if (const auto mode = GetScaleMode(a_fileName); mode) {
|
tableMode = GetScaleMode(a_fileName);
|
||||||
a_mode = ClampToViewport(g_restoreScaleModes ? *mode : a_mode);
|
if (tableMode) {
|
||||||
|
a_mode = ClampToViewport(g_restoreScaleModes ? *tableMode : a_mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(a_scaleformManager, a_menu, a_viewOut, a_fileName, a_mode, a_backgroundAlpha);
|
if (!func(a_scaleformManager, a_menu, a_viewOut, a_fileName, a_mode, a_backgroundAlpha)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The table still decides which menus may be touched at all
|
||||||
|
if (tableMode && a_viewOut && MovieStageIsWiderThan16x9(a_viewOut.get())) {
|
||||||
|
FitWideStage(a_viewOut.get(), a_fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline REL::Relocation<decltype(thunk)> func;
|
static inline REL::Relocation<decltype(thunk)> func;
|
||||||
|
|||||||
@ -28,6 +28,22 @@ inline std::uint32_t SemVerToInt(std::uint16_t major, std::uint16_t minor, std::
|
|||||||
return (static_cast<std::uint32_t>(major) << 24) | (static_cast<std::uint32_t>(minor) << 16) | (static_cast<std::uint32_t>(patch) << 8) | static_cast<std::uint32_t>(build);
|
return (static_cast<std::uint32_t>(major) << 24) | (static_cast<std::uint32_t>(minor) << 16) | (static_cast<std::uint32_t>(patch) << 8) | static_cast<std::uint32_t>(build);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True when the movie behind the view is authored on a stage wider than 16:9. Every vanilla,
|
||||||
|
// SkyUI and Enderal menu is 1280x720; a wider stage marks a third-party widescreen re-author
|
||||||
|
// (Untarnished UI ports ship 2560x720 movies), which carries its own widescreen strategy and
|
||||||
|
// must not be given the scale modes the 1280x720 movies are authored for. See the wide-stage
|
||||||
|
// override in MenuAspectRatioFix for the mode such movies want.
|
||||||
|
inline bool MovieStageIsWiderThan16x9(RE::GFxMovieView* a_view)
|
||||||
|
{
|
||||||
|
const auto def = a_view ? a_view->GetMovieDef() : nullptr;
|
||||||
|
if (!def) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float height = def->GetHeight();
|
||||||
|
return height > 0.0f && def->GetWidth() / height > 1280.0f / 720.0f + 0.01f;
|
||||||
|
}
|
||||||
|
|
||||||
inline void CheckIncompatibleMods()
|
inline void CheckIncompatibleMods()
|
||||||
{
|
{
|
||||||
const auto pluginVersion = SKSE::PluginDeclaration::GetSingleton()->GetVersion();
|
const auto pluginVersion = SKSE::PluginDeclaration::GetSingleton()->GetVersion();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user