- Megamap / TAB map zoomed view: heroes' markers and the other symbols (integer 200h centre immediates) follow the view's centre. - Map reveal: the hero's reveal stamp scaled 10x (mods/mapreveal.cpp, docs/map-reveal.md); creatures on the zoomed map shown within 10x the sight range. - Load / save progress screen drawn on the rock background across the full width, the picture blended over it (mods/loadingscreen.cpp); the classic 1024x768 frame is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
303 lines
11 KiB
C++
303 lines
11 KiB
C++
// 16:9 menu screens (mods/screen.h, docs/screens.md).
|
|
|
|
#include "mods/screen.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <map>
|
|
#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 (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;
|
|
|
|
// Frames drawn so far, to tell a button drawn this frame (events come between
|
|
// frames).
|
|
std::atomic<unsigned> g_frame{1};
|
|
|
|
// The windows whose screen is shown on the menu thread, with the menu frame
|
|
// they were last drawn in: the main menu's frame is not drawn centred while
|
|
// any is (the screen draws itself across the width). Entries not drawn for
|
|
// kShownFrames frames are dropped: the UI is rebuilt when a game starts or
|
|
// ends, and a window destroyed while shown kept the menu from being framed
|
|
// (user, 2026-10-21: "the old main menu is rendering again"). Menu thread only.
|
|
constexpr unsigned kShownFrames = 2;
|
|
unsigned g_menuFrame = 0;
|
|
std::map<const void*, unsigned> g_shown;
|
|
|
|
void NoteShown(const void* window, bool shown) {
|
|
if (!widescreen::IsMenuThread()) {
|
|
return;
|
|
}
|
|
if (shown) {
|
|
g_shown[window] = g_menuFrame;
|
|
} else {
|
|
g_shown.erase(window);
|
|
}
|
|
}
|
|
|
|
// 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()));
|
|
}
|
|
|
|
// 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 BeginMenuFrame() {
|
|
++g_menuFrame;
|
|
std::erase_if(g_shown, [](const auto& entry) { return g_menuFrame - entry.second > kShownFrames; });
|
|
return !g_shown.empty();
|
|
}
|
|
|
|
void DrawBackground(void* device) {
|
|
const float width = static_cast<float>(std::max(widescreen::RenderWidth(), kWidth));
|
|
const float height = static_cast<float>(kHeight);
|
|
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);
|
|
}
|
|
|
|
int Render(void* window, void* device, int (*body)(void* context), void* context) {
|
|
const bool shown = game::ControlShown(window);
|
|
NoteShown(window, shown);
|
|
if (!shown || !device) {
|
|
return 0;
|
|
}
|
|
++g_frame;
|
|
DrawBackground(device);
|
|
return hud::RenderCanvas(window, body, context);
|
|
}
|
|
|
|
int Handle(void* event, int (*body)(void* context), void* context) {
|
|
return hud::HandleCanvas(event, body, context);
|
|
}
|
|
|
|
void Fill(void* device, const UiRect& r, uint32_t colour) {
|
|
game::FillRect(device, static_cast<float>(r.x), static_cast<float>(r.y), static_cast<float>(r.x + r.w),
|
|
static_cast<float>(r.y + r.h), colour);
|
|
}
|
|
|
|
void Text(void* device, const UiRect& rect, const std::wstring& text, uint32_t colour, uint32_t flags) {
|
|
game::DrawText(device, kFont, game::CenterLineVertically(kFont, rect), text.c_str(), colour, flags);
|
|
}
|
|
|
|
UiRect Inset(const UiRect& r, int by) {
|
|
return {r.x + by, r.y + by, static_cast<int16_t>(r.w - 2 * by), static_cast<int16_t>(r.h - 2 * by)};
|
|
}
|
|
|
|
UiRect Panel::Draw(void* device, const UiRect& rect, const std::wstring& heading) {
|
|
if (!frame_ || rect.x != rect_.x || rect.y != rect_.y || rect.w != rect_.w || rect.h != rect_.h) {
|
|
if (frame_) {
|
|
game::DeleteControl(frame_);
|
|
}
|
|
frame_ = game::NewFrame(name_, rect);
|
|
rect_ = rect;
|
|
}
|
|
Fill(device, rect, kPanelBackdrop);
|
|
if (frame_) {
|
|
game::ControlRender(frame_, device);
|
|
}
|
|
UiRect content = Inset(rect, kPanelPadding);
|
|
if (!heading.empty()) {
|
|
Text(device, {content.x, content.y, content.w, kHeadingHeight}, heading, kHeadingColour);
|
|
content.y += kHeadingHeight;
|
|
content.h = static_cast<int16_t>(content.h - kHeadingHeight);
|
|
}
|
|
return content;
|
|
}
|
|
|
|
void Button::Draw(void* device, const uint8_t* model, const UiRect& rect) {
|
|
if (!control_) {
|
|
captionId_ = game::RegisterText(caption_.c_str());
|
|
control_ = game::NewButtonLike(model, rect, captionId_, name_);
|
|
if (!control_) {
|
|
return;
|
|
}
|
|
}
|
|
rect_ = rect;
|
|
*reinterpret_cast<UiRect*>(control_ + game::kControlRect) = rect;
|
|
game::ControlRender(control_, device);
|
|
drawnFrame_ = g_frame;
|
|
}
|
|
|
|
bool Button::Clicked(void* event) {
|
|
if (!control_ || drawnFrame_ != g_frame) {
|
|
return false;
|
|
}
|
|
game::ControlHandleEvent(control_, event);
|
|
return game::MouseEventKind(event) == game::MouseEvent::ButtonDown &&
|
|
game::EventField(event, game::kMouseButton) == game::kLeftButton &&
|
|
game::Inside(rect_, game::EventField(event, game::kMouseButtonX),
|
|
game::EventField(event, game::kMouseButtonY));
|
|
}
|
|
|
|
void Button::SetCaption(const std::wstring& caption) {
|
|
caption_ = caption;
|
|
if (captionId_) {
|
|
game::SetTextOverride(captionId_, caption_);
|
|
}
|
|
}
|
|
|
|
int16_t ButtonHeight(const uint8_t* model) {
|
|
return reinterpret_cast<const UiRect*>(model + game::kControlRect)->h;
|
|
}
|
|
|
|
bool DoubleClick::Click(int target) {
|
|
const unsigned long now = GetTickCount();
|
|
const bool twice = target == last_ && now - at_ <= GetDoubleClickTime();
|
|
last_ = twice ? -1 : target;
|
|
at_ = now;
|
|
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::IsLoadingScreen() || 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::BeginMenuFrame();
|
|
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
|