// 16:9 menu screens (mods/screen.h, docs/screens.md). #include "mods/screen.h" #include #include #include #include #include #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.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 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 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 PictureTextureIds() { std::vector 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 ids = PictureTextureIds(); std::vector surfaces; for (uint32_t id : ids) { if (void* surface = game::TextureSurface(id)) { surfaces.push_back(surface); } } hud::SetHiddenTextures(surfaces.data(), static_cast(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(kLogoHeight * (kLogoU1 - kLogoU0))); void DrawLogo(void* device) { if (void* logo = game::TextureSurface(game::TextureId(kLogoTexture))) { const float edge = static_cast(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(std::max(widescreen::RenderWidth(), kWidth)); const float height = static_cast(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(r.x), static_cast(r.y), static_cast(r.x + r.w), static_cast(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(r.w - 2 * by), static_cast(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(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(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(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(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(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(&SkipMenuScene)); } RE_STARTUP(MenuSceneStartup, RemoveMenuScene); } // namespace game