Menu screens: rock background, centred frame, logo, 3D hero

- Spielen hero panel shows the selected hero in 3D (cUI_NetGranny,
  game/ui/granny.h).
- All menu screens draw on the options' rock wall; the main menu's 1024
  frame is drawn centred over it, mouse and cursor shifted to match.
- The main menu's and options' own pictures and the main menu's 3D scene
  are left out (textures hidden by surface, scene call redirected).
- Logo top left: FX_SACREDLOGO.TGA, drawn added.
- Game textures by name (game/ui/texture.h), textured and added quads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-21 00:05:55 +02:00
1 parent 34a119433a
commit db87e276bf
16 files changed
+620 -69

No files matched your search

+1
View File
@@ -138,6 +138,7 @@ POINT ToGame(POINT p) {
POINT result;
result.x = std::clamp(MulDiv(p.x - layout.client.left, layout.gameWidth, width), 0, layout.gameWidth - 1);
result.y = std::clamp(MulDiv(p.y - layout.client.top, layout.gameHeight, height), 0, layout.gameHeight - 1);
result.x -= mods::hud::MenuFrameOffsetX(); // the main menu's centred frame
return result;
}
+70 -3
View File
@@ -49,6 +49,7 @@
#include "game/ui/button.h"
#include "game/ui/control.h"
#include "game/ui/events.h"
#include "game/ui/granny.h"
#include "game/ui/text.h"
#include "game/ui/texts.h"
#include "game/ui/windows.h"
@@ -386,6 +387,8 @@ std::atomic<bool> g_autosavePending{false};
bool g_inGame = false; // a game runs (the load window opens from the Esc menu too)
std::atomic<uint64_t> g_lastSaveAt{0}; // GetTickCount64 of the last save of any kind
void ForgetHeroModel(); // the files may have changed: read the hero anew
std::string Description(const json& meta) {
return Convert(FromUtf8(meta.value("name", std::string())) + L" - " + ClassName(meta.value("class", 0u)),
CP_ACP);
@@ -417,6 +420,7 @@ void RebuildList() {
DeleteFileA((work + file).c_str());
}
g_rows.clear();
ForgetHeroModel();
for (const std::string& name : FilesIn(CharactersDir(), "*.sacredK")) {
if (EndsWith(name, kAutosaveExtension) || g_rows.size() >= kMaxRows) {
continue;
@@ -616,8 +620,10 @@ std::atomic<int> g_loadRow{-1};
// Layout on the canvas (1365x768): three panels side by side (sizes in the
// frames' 64 steps, screen::kFrameStep), centred, a row of buttons below.
constexpr int kPanelTop = 96; // the title above the frames' ornament
constexpr int kPanelHeight = 8 * screen::kFrameStep;
// Top: room for the heading and the frames' ornament; the buttons end
// above the screen's bottom.
constexpr int kPanelTop = 164 + screen::kFrameOrnament;
constexpr int kPanelHeight = 7 * screen::kFrameStep;
constexpr int kListWidth = 5 * screen::kFrameStep;
constexpr int kHeroWidth = 6 * screen::kFrameStep;
constexpr int kWorldsWidth = 6 * screen::kFrameStep;
@@ -1329,6 +1335,64 @@ void DrawList(void* device) {
}
}
// The selected character's hero in 3D (game/ui/granny.h): its hero file is
// unpacked to the game's save folder (_work) and read as a hero info. A
// migrated character without the shared hero has none until it is played.
constexpr float kHeroModelScale = 3.8f; // the game's 2.5 fills a third of the panel
constexpr int kHeroModelFeetAboveBottom = 24;
constexpr char kWorkPreviewHero[] = "preview.pax";
constexpr char kGamePreviewHero[] = "Save\\preview.pax";
struct HeroModel {
uint8_t* control = nullptr;
std::string file; // whose hero it shows
bool hasHero = false;
} g_heroModel;
void ForgetHeroModel() {
g_heroModel.file.clear();
}
void ShowHeroOf(const Row& row) {
if (row.file == g_heroModel.file) {
return;
}
g_heroModel.file = row.file;
g_heroModel.hasHero = false;
CharacterFile character(row.file);
std::optional<Bytes> hero = character.Open() ? character.Read("hero") : std::nullopt;
std::vector<uint8_t> info(game::kHeroInfoSize);
if (hero && WriteFileReplacing(WorkDir() + kWorkPreviewHero, *hero) &&
game::ReadHeroInfo(kGamePreviewHero, info.data())) {
game::SetNetGrannyHero(g_heroModel.control, info.data());
g_heroModel.hasHero = true;
} else {
game::ClearNetGrannyHero(g_heroModel.control);
}
DeleteFileA((WorkDir() + kWorkPreviewHero).c_str());
}
// The model stands centred in `rect` (canvas coordinates). The control centres
// it as in a 1024 wide layout (x - 512 around the screen centre); the canvas
// is centred too, so its x is moved by the difference.
void DrawHeroModel(void* device, const Row& row, game::UiRect rect) {
rect.x -= (screen::kWidth - 1024) / 2;
if (!g_heroModel.control) {
g_heroModel.control = game::NewNetGranny(rect);
if (!g_heroModel.control) {
return;
}
g_heroModel.control[game::kNetGrannyWhiteLight] = 1;
*reinterpret_cast<float*>(g_heroModel.control + game::kNetGrannyScale) = kHeroModelScale;
game::SetControlShown(g_heroModel.control, true);
}
*reinterpret_cast<game::UiRect*>(g_heroModel.control + game::kControlRect) = rect;
ShowHeroOf(row);
if (g_heroModel.hasHero) {
game::RenderNetGranny(g_heroModel.control, device);
}
}
void DrawHero(void* device) {
const game::UiRect content = g_heroPanel.Draw(device, kHeroRect);
const Row* row = SelectedCharacter();
@@ -1348,6 +1412,9 @@ void DrawHero(void* device) {
line(CampaignName(last->campaign) + L" · " + DifficultyName(last->difficulty), screen::kTextColour);
line(FormatTime(last->savedAt), screen::kDimColour);
}
// The control puts the feet at 5/6 of its rect's height (0x88f04c).
const int feet = content.y + content.h - kHeroModelFeetAboveBottom;
DrawHeroModel(device, *row, {content.x, y, content.w, static_cast<int16_t>((feet - y) * 6 / 5)});
}
void DrawCell(void* device, const Row& row, int campaign, int difficulty) {
@@ -1375,7 +1442,7 @@ void DrawCell(void* device, const Row& row, int campaign, int difficulty) {
if (cell.autosave && cell.world) {
screen::Text(device, bottom, L"+ Autosave", screen::kHeadingColour, game::kTextCenter);
} else if (row.storyCompleted[difficulty]) {
screen::Text(device, bottom, L"Story abgeschlossen", screen::kDimColour, game::kTextCenter);
screen::Text(device, bottom, L"abgeschlossen", screen::kDimColour, game::kTextCenter);
}
}
+111 -13
View File
@@ -21,6 +21,7 @@
// Enabled=1
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <cstring>
#include <vector>
@@ -31,6 +32,7 @@
#include "game/render/dx_driver7.h"
#include "game/ui/events.h"
#include "mods/hud.h"
#include "mods/screen.h"
#include "mods/widescreen.h"
#include "re/config.h"
#include "re/hook.h"
@@ -268,6 +270,29 @@ private:
CursorOffset cursor_;
};
// RAII: shift drawing only (the cursor is not seen shifted), for things
// already in the shifted layout's coordinates (the main menu's cursor).
class DrawShift {
public:
explicit DrawShift(int dx) : active_(!g_drawScoped && dx != 0) {
if (active_) {
g_drawScoped = true;
g_offsetX = dx;
}
}
~DrawShift() {
if (active_) {
g_drawScoped = false;
g_offsetX = 0;
}
}
DrawShift(const DrawShift&) = delete;
DrawShift& operator=(const DrawShift&) = delete;
private:
bool active_;
};
// Stores the offsets of a mouse event's x fields (game/ui/events.h); returns
// their count (0: no mouse event).
int MouseEventXOffsets(void* event, int (&offsets)[2]) {
@@ -383,13 +408,47 @@ using DrawIndexedPrimitiveFn = HRESULT(__stdcall*)(void* device, DWORD type, DWO
DrawPrimitiveFn g_drawPrimitive = nullptr;
DrawIndexedPrimitiveFn g_drawIndexedPrimitive = nullptr;
// Textures not drawn (SetHiddenTextures): the surface bound to stage 0 is
// tracked per thread (each thread draws with its own device calls).
constexpr int kHiddenTextureMax = 64;
std::atomic<void*> g_hiddenTextures[kHiddenTextureMax] = {};
thread_local void* g_boundTexture = nullptr;
using SetTextureFn = HRESULT(__stdcall*)(void* device, DWORD stage, void* surface);
SetTextureFn g_setTexture = nullptr;
HRESULT __stdcall SetTextureTracked(void* device, DWORD stage, void* surface) {
if (stage == 0) {
g_boundTexture = surface;
}
return g_setTexture(device, stage, surface);
}
bool BoundTextureHidden() {
if (!g_boundTexture) {
return false;
}
for (const auto& hidden : g_hiddenTextures) {
if (hidden.load(std::memory_order_relaxed) == g_boundTexture) {
return true;
}
}
return false;
}
HRESULT __stdcall DrawPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
DWORD flags) {
if (BoundTextureHidden()) {
return S_OK;
}
return g_drawPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, flags);
}
HRESULT __stdcall DrawIndexedPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
WORD* indices, DWORD indexCount, DWORD flags) {
if (BoundTextureHidden()) {
return S_OK;
}
return g_drawIndexedPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, indices,
indexCount, flags);
}
@@ -440,30 +499,25 @@ HRESULT __stdcall ClearShifted(void* device, DWORD count, Rect7* rects, DWORD fl
using GetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport);
constexpr int kVtableGetViewport = 15; // IDirect3DDevice7 (13 SetViewport, 14 MultiplyTransform)
constexpr DWORD kClearTarget = 1; // D3DCLEAR_TARGET
// RAII for full-screen windows (character export, save games) that fill the
// 1024 frame with a picture: the render target left and right of the frame is
// cleared black, and the window draws with a viewport on the frame. The
// RAII for full-screen windows (options in a game) that fill the 1024 frame
// with a picture: the menu screens' background (mods/screen.h) goes under
// it, and the window draws with a viewport on the frame. The
// export screen's 3D scene (heroes, background) uses a projection for a
// 1024x768 viewport; across the widened one it was stretched 1.8x wide, away
// from the slot labels and hit areas (measured on a screenshot, 2026-10-18).
// Clear clips to the viewport, so the sides are cleared before it is set.
class FrameScope {
public:
FrameScope(void* window, void* device, int dx) {
const auto* w = static_cast<const uint8_t*>(window);
if (dx == 0 || g_drawScoped || !device || !g_setViewport || !g_clear || (w[kControlFlags] & 1) == 0) {
if (dx == 0 || g_drawScoped || !device || !g_setViewport || (w[kControlFlags] & 1) == 0) {
return;
}
auto getViewport = reinterpret_cast<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]);
if (FAILED(getViewport(device, &previous_))) {
return;
}
const LONG height = static_cast<LONG>(previous_.height);
Rect7 sides[] = {{0, 0, dx, height},
{dx + kLayoutWidth, 0, mods::widescreen::RenderWidth(), height}};
g_clear(device, 2, sides, kClearTarget, 0, 1.0f, 0);
mods::screen::DrawBackground(device);
Viewport7 frame = {static_cast<DWORD>(dx), 0, kLayoutWidth, 768, previous_.minZ, previous_.maxZ};
device_ = device;
g_setViewport(device, &frame);
@@ -512,6 +566,10 @@ void OnDevice(void* device) {
g_drawIndexedPrimitive = reinterpret_cast<DrawIndexedPrimitiveFn>(vtable[26]);
re::AttachRuntimeHook("D3D7 DrawIndexedPrimitive", reinterpret_cast<void**>(&g_drawIndexedPrimitive),
reinterpret_cast<void*>(&DrawIndexedPrimitiveShifted));
// SetTexture 35.
g_setTexture = reinterpret_cast<SetTextureFn>(vtable[35]);
re::AttachRuntimeHook("D3D7 SetTexture", reinterpret_cast<void**>(&g_setTexture),
reinterpret_cast<void*>(&SetTextureTracked));
// Clear 10, SetViewport 13.
g_clear = reinterpret_cast<ClearFn>(vtable[10]);
re::AttachRuntimeHook("D3D7 Clear", reinterpret_cast<void**>(&g_clear), reinterpret_cast<void*>(&ClearShifted));
@@ -796,6 +854,41 @@ int HandleCentred(void* event, int (*body)(void* context), void* context) {
return body(context);
}
// Offset of the main menu's 1024 frame while it is drawn centred (set each
// menu frame by RenderMenuFrame; 0 while a 16:9 screen draws itself).
std::atomic<int> g_menuFrameDx = 0;
void SetHiddenTextures(void* const* surfaces, int count) {
for (int i = 0; i < kHiddenTextureMax; ++i) {
g_hiddenTextures[i].store(i < count ? surfaces[i] : nullptr, std::memory_order_relaxed);
}
}
int MenuFrameOffsetX() {
return mods::widescreen::MenuIsShowing() ? g_menuFrameDx.load() : 0;
}
int RenderMenuFrame(void* device, bool framed, int (*body)(void* context), void* context) {
const int width = mods::widescreen::RenderWidth();
const int dx = framed && width > kLayoutWidth ? (width - kLayoutWidth) / 2 : 0;
g_menuFrameDx = dx;
Viewport7 previous;
auto getViewport =
device ? reinterpret_cast<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]) : nullptr;
if (dx == 0 || g_drawScoped || !g_setViewport || !getViewport || FAILED(getViewport(device, &previous))) {
return body(context);
}
Viewport7 frame = {static_cast<DWORD>(dx), 0, kLayoutWidth, 768, previous.minZ, previous.maxZ};
g_setViewport(device, &frame);
g_drawScoped = true; // the windows inside draw in the frame's coordinates
g_offsetX = dx;
const int result = body(context);
g_offsetX = 0;
g_drawScoped = false;
g_setViewport(device, &previous);
return result;
}
int CanvasOffsetX() {
return OffsetFor(Anchor::Canvas);
}
@@ -830,12 +923,17 @@ int __fastcall UiTaskbar_HitTest(void* self, void* edx, int x, int y) {
return UiTaskbar_HitTest_original(self, edx, mods::hud::HitTestX(self, x), y);
}
// The item carried by the mouse cursor (0x6555e0, drawn by the render loop
// after the UI) is placed like UI items: relative to x = 512 of the layout.
// Items are only picked up from centred windows.
// The cursor and the item it carries (0x6555e0, drawn by the render loop
// after the UI) are placed like UI items: relative to x = 512 of the layout.
// Items are only picked up from centred windows. In the main menu the cursor
// is in the centred frame's coordinates (MenuFrameOffsetX).
int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b);
RE_HOOK(0x006555e0, MouseCursor_DrawItem);
int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b) {
if (mods::widescreen::MenuIsShowing()) {
mods::hud::DrawShift shift(mods::hud::MenuFrameOffsetX());
return MouseCursor_DrawItem_original(self, edx, a, b);
}
mods::hud::DrawOffset offset(nullptr, mods::hud::OffsetFor(Anchor::Center));
return MouseCursor_DrawItem_original(mods::hud::CursorFor(self), edx, a, b);
}
+14 -2
View File
@@ -26,8 +26,8 @@ int VisibleDialogTooltips(void** out, int max);
// shifted) or event handler (mouse position shifted by the window's offset).
int RenderCentred(void* window, int (*body)(void* context), void* context);
int HandleCentred(void* event, int (*body)(void* context), void* context);
// RenderCentred for full-screen windows: black beside the 1024 frame, drawn
// with a viewport on it (as the save game and export screens).
// RenderCentred for full-screen windows: the menu screens' background beside
// the 1024 frame, drawn with a viewport on it (as the options in a game).
int RenderFramed(void* window, void* device, int (*body)(void* context), void* context);
// 16:9 screens (mods/screen.h): laid out on a 1365x768 canvas, centred in
@@ -40,4 +40,16 @@ int CanvasOffsetX();
int RenderCanvas(void* window, int (*body)(void* context), void* context);
int HandleCanvas(void* event, int (*body)(void* context), void* context);
// The main menu (mods/screen.cpp): with `framed`, body(context) draws the
// menu's 1024x768 frame centred in the render width (shifted, with a
// viewport on it); mouse positions are then given in frame coordinates
// (MenuFrameOffsetX, applied by the borderless mod). Without, the menu draws
// unshifted (a 16:9 screen is shown).
int RenderMenuFrame(void* device, bool framed, int (*body)(void* context), void* context);
int MenuFrameOffsetX();
// Quads drawn with one of these textures (Direct3D surfaces) are left out,
// on every thread, until the list changes (at most 64).
void SetHiddenTextures(void* const* surfaces, int count);
} // namespace mods::hud
+156 -11
View File
@@ -6,25 +6,38 @@
#include <atomic>
#include <mutex>
#include <set>
#include <vector>
#include <windows.h>
#include "game/addresses.h"
#include "game/render/dx_driver7.h"
#include "game/ui/button.h"
#include "game/ui/control.h"
#include "game/ui/draw.h"
#include "game/ui/events.h"
#include "game/ui/frame.h"
#include "game/ui/texture.h"
#include "game/ui/texts.h"
#include "mods/widescreen.h"
#include "re/hook.h"
#include "re/patch.h"
namespace mods::screen {
namespace {
// Background (decided 2026-10-20: our own, no picture): dark, a little warm
// at the top, darker towards the bottom and the sides.
constexpr uint32_t kBackgroundTop = 0xff1c1712;
constexpr uint32_t kBackgroundBottom = 0xff070605;
constexpr uint32_t kVignette = 0xa0000000;
// Background (user, 2026-10-20: "textured dark", then "doesn't need to be that
// dark, but apply it to all screens"): the options window's rock wall,
// GUI_OPTION_BACKGROUND<row><column>.TGA (4 x 3 tiles of 256, a 1024x768
// picture), scaled to cover the render target, a little darkened, darker
// towards the bottom and the sides.
constexpr int kBackgroundColumns = 4;
constexpr int kBackgroundRows = 3;
constexpr float kBackgroundTile = 256.0f;
constexpr uint32_t kBackgroundTint = 0xffa0a0a0;
constexpr uint32_t kShadeTop = 0x10000000;
constexpr uint32_t kShadeBottom = 0x70000000;
constexpr uint32_t kVignette = 0x70000000;
constexpr int kVignetteWidth = 240;
constexpr uint32_t kPanelBackdrop = 0xb4000000;
constexpr int kHeadingHeight = 30;
@@ -33,8 +46,8 @@ constexpr int kHeadingHeight = 30;
// frames).
std::atomic<unsigned> g_frame{1};
// The windows whose screen is shown: the menu thread renders the full width
// while any is.
// The windows whose screen is shown: the main menu's frame is not drawn
// centred while any is (the screen draws itself across the width).
std::mutex g_shownLock;
std::set<const void*> g_shown;
@@ -45,19 +58,98 @@ void NoteShown(const void* window, bool shown) {
} else {
g_shown.erase(window);
}
widescreen::SetWideMenuScreen(!g_shown.empty());
}
// The windows' own pictures, replaced by the background (user, 2026-10-20:
// "the previous backgrounds from settings and mainMenu can be removed"), 12
// tiles each (logged: the textures the menu thread asks for, by name): the
// main menu GUI_UW_OPTION<1..12>.TGA with the sky above it UI_MAIN_SKY.TGA,
// the options window UW_OPTION_1_<01..12>.TGA. The main menu's 3D scene
// behind its picture is not drawn either (game::RemoveMenuScene).
std::vector<uint32_t> PictureTextureIds() {
std::vector<uint32_t> ids = {game::TextureId("UI_MAIN_SKY.TGA")};
char name[40];
for (int i = 1; i <= 12; ++i) {
wsprintfA(name, "GUI_UW_OPTION%d.TGA", i);
ids.push_back(game::TextureId(name));
wsprintfA(name, "UW_OPTION_1_%02d.TGA", i);
ids.push_back(game::TextureId(name));
}
return ids;
}
// Their surfaces (loaded now, as the window is about to draw them; the cache
// may have made them anew) go to the HUD layer's hidden list.
void HidePictures() {
static const std::vector<uint32_t> ids = PictureTextureIds();
std::vector<void*> surfaces;
for (uint32_t id : ids) {
if (void* surface = game::TextureSurface(id)) {
surfaces.push_back(surface);
}
}
hud::SetHiddenTextures(surfaces.data(), static_cast<int>(surfaces.size()));
}
bool ShownAny() {
std::lock_guard guard(g_shownLock);
return !g_shown.empty();
}
// The game's logo top left (user, 2026-10-21: "the bright blueish one"),
// kLogoMargin from the edges: FX_SACREDLOGO.TGA, the emblem (horns around a
// ring of runes) glowing blue on black, no alpha, added onto the background as
// the game's effects draw it; the glow within u kLogoU0..U1 of the square
// texture, the full height (measured on a screenshot of it drawn whole).
constexpr char kLogoTexture[] = "FX_SACREDLOGO.TGA";
constexpr float kLogoU0 = 0.13f, kLogoU1 = 0.87f;
static_assert(kLogoWidth == static_cast<int>(kLogoHeight * (kLogoU1 - kLogoU0)));
void DrawLogo(void* device) {
if (void* logo = game::TextureSurface(game::TextureId(kLogoTexture))) {
const float edge = static_cast<float>(kLogoMargin);
game::DrawTexturedAdded(device, logo, edge, edge, edge + kLogoWidth, edge + kLogoHeight, kLogoU0, 0, kLogoU1,
1);
}
}
void DrawRockWall(void* device, float width, float height) {
const float scale = std::max(width / (kBackgroundColumns * kBackgroundTile),
height / (kBackgroundRows * kBackgroundTile));
const float tile = kBackgroundTile * scale;
const float left = (width - kBackgroundColumns * tile) / 2;
const float top = (height - kBackgroundRows * tile) / 2;
for (int row = 0; row < kBackgroundRows; ++row) {
for (int column = 0; column < kBackgroundColumns; ++column) {
char name[40];
wsprintfA(name, "GUI_OPTION_BACKGROUND%d%d.TGA", row, column);
if (void* surface = game::TextureSurface(game::TextureId(name))) {
const float x = left + column * tile, y = top + row * tile;
game::DrawTextured(device, surface, x, y, x + tile, y + tile, 0, 0, 1, 1, kBackgroundTint);
}
}
}
}
} // namespace
bool AnyShown() {
return ShownAny();
}
void DrawBackground(void* device) {
const float width = static_cast<float>(std::max(widescreen::RenderWidth(), kWidth));
const float height = static_cast<float>(kHeight);
game::FillVertical(device, 0, 0, width, height, kBackgroundTop, kBackgroundBottom);
HidePictures();
game::FillRect(device, 0, 0, width, height, 0xff000000);
DrawRockWall(device, width, height);
game::FillVertical(device, 0, 0, width, height, kShadeTop, kShadeBottom);
game::FillHorizontal(device, 0, 0, kVignetteWidth, height, kVignette, 0);
game::FillHorizontal(device, width - kVignetteWidth, 0, width, height, 0, kVignette);
DrawLogo(device);
}
} // namespace
int Render(void* window, void* device, int (*body)(void* context), void* context) {
const bool shown = game::ControlShown(window);
NoteShown(window, shown);
@@ -151,4 +243,57 @@ bool DoubleClick::Click(int target) {
return twice;
}
} // namespace mods::screen
// The main menu (cUI_Manager's UI render 0x7587b0, thiscall (1 argument), ret
// 4, from the menu thread loop 0x756aa1 and the in-game render 0x60ea74): on
// the menu thread the background goes first, then the menu's 1024x768 frame
// is drawn centred over it (hud::RenderMenuFrame); a 16:9 screen draws its own.
namespace game {
uint32_t __fastcall UiManager_Render(void* self, void* edx, uint32_t argument);
RE_HOOK(0x007587b0, UiManager_Render);
uint32_t __fastcall UiManager_Render(void* self, void* edx, uint32_t argument) {
using namespace mods;
if (!widescreen::IsMenuThread() || widescreen::IsClassicFrame() || widescreen::RenderWidth() == 0) {
return UiManager_Render_original(self, edx, argument);
}
auto* driver = *reinterpret_cast<DxDriver7**>(addr::g_dxDriver);
void* device = driver ? driver->device : nullptr;
const bool framed = !screen::AnyShown();
if (framed && device) {
screen::DrawBackground(device);
}
struct Call {
void* self;
void* edx;
uint32_t argument;
uint32_t result;
} call{self, edx, argument, 0};
hud::RenderMenuFrame(
device, framed,
[](void* context) {
auto* c = static_cast<Call*>(context);
c->result = UiManager_Render_original(c->self, c->edx, c->argument);
return 0;
},
&call);
return call.result;
}
// cUI_MainMenu::Render (vf14 0x7122c0) draws a 3D scene (sky UI_UW_MAIN_CENTER,
// water UI_MAIN_REF; 0x75e620, thiscall (1 argument), ret 4, on the scene
// object 0x75df10) behind its picture: the call at 0x712bd9 goes nowhere.
constexpr uint32_t kMainMenuSceneCall = 0x00712bd9;
constexpr uint32_t kRenderMenuScene = 0x0075e620;
void __fastcall SkipMenuScene(void*, void*, uint32_t) {}
void RemoveMenuScene() {
re::RedirectCall(kMainMenuSceneCall, kRenderMenuScene, reinterpret_cast<const void*>(&SkipMenuScene));
}
RE_STARTUP(MenuSceneStartup, RemoveMenuScene);
} // namespace game
+13 -2
View File
@@ -33,6 +33,12 @@ constexpr int kMargin = kFrameOrnament + 16; // canvas edge to a panel
constexpr int kPanelGap = 96; // between two panels' rects, as the options window's columns
constexpr int kPanelPadding = 16; // panel rect to its content
// The logo in the background's top left corner (render target coordinates;
// screens may overlap it).
constexpr int kLogoMargin = 48;
constexpr int kLogoHeight = 124;
constexpr int kLogoWidth = kLogoHeight * 74 / 100; // the emblem's aspect (screen.cpp)
constexpr uint16_t kFont = 1;
constexpr uint32_t kTextColour = 0xffe6d6aa; // as the options' texts
constexpr uint32_t kHeadingColour = 0xfff0c060; // as the options' headings
@@ -41,9 +47,14 @@ constexpr uint32_t kRowColour = 0x40302418;
constexpr uint32_t kRowHoverColour = 0x80403020;
constexpr uint32_t kRowSelectedColour = 0xc0604818;
// The background of the menu screens across the whole render target (also
// beside the main menu's centred frame and full-screen windows in a game).
void DrawBackground(void* device);
// Whether a screen is shown (the main menu then does not draw centred).
bool AnyShown();
// Draws the screen of `window`: the background across the whole render
// target, then body(context) in canvas coordinates. While `window` is shown
// the main menu thread renders the full width (widescreen::SetWideMenuScreen).
// target, then body(context) in canvas coordinates.
int Render(void* window, void* device, int (*body)(void* context), void* context);
// Runs body(context) with the event's mouse position in canvas coordinates.
int Handle(void* event, int (*body)(void* context), void* context);
+14 -17
View File
@@ -63,14 +63,14 @@ bool g_pushesPatched = false;
int g_renderWidth = 0;
int g_renderHeight = 0;
// Frames from the main menu thread and the loading screen are laid out for
// 1024x768 ("classic"); everything else is the widened in-game view.
// The loading screen is laid out for 1024x768 ("classic"); the main menu
// thread renders the full width too (its windows drawn centred by
// mods/screen.cpp), the in-game view is widened.
std::atomic<DWORD> g_menuThread = 0;
// Whether the most recent scene (on any thread) was a classic one: the main
// menu is showing. UI input is processed outside the menu thread.
std::atomic<bool> g_lastSceneClassic = false;
// Whether the most recent scene (on any thread) was the main menu's or the
// loading screen's. UI input is processed outside the menu thread.
std::atomic<bool> g_lastSceneMenu = false;
thread_local int g_loadingScreenDepth = 0;
std::atomic<bool> g_wideMenuScreen = false;
// D3DVIEWPORT7 (d3d.h is not in current Windows SDKs).
struct Viewport7 {
@@ -340,18 +340,15 @@ int RenderWidth() {
}
bool MenuIsShowing() {
return g_lastSceneClassic;
return g_lastSceneMenu;
}
bool IsClassicFrame() {
if (g_renderWidth == 0) {
return false;
}
return g_loadingScreenDepth > 0 || (GetCurrentThreadId() == g_menuThread.load() && !g_wideMenuScreen);
return g_renderWidth != 0 && g_loadingScreenDepth > 0;
}
void SetWideMenuScreen(bool shown) {
g_wideMenuScreen = shown;
bool IsMenuThread() {
return GetCurrentThreadId() == g_menuThread.load();
}
} // namespace mods::widescreen
@@ -376,13 +373,13 @@ void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t pro
--g_loadingScreenDepth;
}
// Menus draw their 3D parts for a 1024x768 screen: give them a 1024x768
// viewport (the borderless mod then presents just that area, scaled), and the
// world the full render size.
// The loading screen draws for a 1024x768 screen: give it a 1024x768
// viewport (the borderless mod then presents just that area, scaled), and
// everything else the full render size.
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) {
mods::hud::OnDevice(self->device);
if (self->sceneDepth == 0) {
g_lastSceneClassic = IsClassicFrame();
g_lastSceneMenu = IsClassicFrame() || IsMenuThread();
}
if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) {
const bool classic = IsClassicFrame();
+6 -8
View File
@@ -11,20 +11,18 @@ namespace mods::widescreen {
// the world view to match. No-op when disabled.
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window);
// True while the current thread renders a frame laid out for 1024x768 (main
// menu, loading screen); the borderless mod then presents only that area.
// True while the current thread renders a frame laid out for 1024x768 (the
// loading screen); the borderless mod then presents only that area.
bool IsClassicFrame();
// Applied render width (0 while the original 1024x768 is in use).
int RenderWidth();
// True while the main menu / loading screen (classic layout) is on screen,
// regardless of the calling thread.
// True while the main menu or the loading screen is on screen, regardless of
// the calling thread.
bool MenuIsShowing();
// While a 16:9 menu screen (mods/screen.h) is shown from the main menu, the
// menu thread renders and presents the full render size instead of the
// classic 1024x768.
void SetWideMenuScreen(bool shown);
// True on the main menu's render thread (cUI_Manager's thread loop).
bool IsMenuThread();
} // namespace mods::widescreen