Characters: 16:9 "Spielen" screen, autosave interval option

- Shared screen kit (mods/screen, docs/screens.md): a 1365x768 canvas
  centred in the render width on our own dark background, the game's
  frames and buttons; the main menu renders full width while it is shown.
- The load window (cUI_Savegame) is replaced by the Spielen screen:
  character list, hero panel, campaign x difficulty grid, save/autosave
  choice, rename/delete/reset/new character dialogs.
- Migrated characters without a shared hero load their hero's world
  first, then start the new world.
- Options, Allgemein: autosave interval (Aus/5/10/15/30 minutes).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-20 23:20:45 +02:00
1 parent 732d9c2abb
commit 34a119433a
16 files changed
+1391 -468

No files matched your search

+752 -391
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -7,4 +7,9 @@ namespace mods::characters {
// save window (on the next engine tick).
void RequestSave();
// Minutes between silent autosaves (0: off); sacredK.ini [characters]
// AutosaveMinutes, set on the options window's Allgemein page.
int AutosaveMinutes();
void SetAutosaveMinutes(int minutes);
} // namespace mods::characters
+28 -2
View File
@@ -30,6 +30,7 @@
#include "game/addresses.h"
#include "game/render/dx_driver7.h"
#include "game/ui/events.h"
#include "mods/hud.h"
#include "mods/widescreen.h"
#include "re/config.h"
#include "re/hook.h"
@@ -47,11 +48,15 @@ thread_local bool g_eventShifted = false;
// Screen: laid out across the whole render width by its mod (drawn unshifted).
enum class Anchor { Center, Right, Screen };
// Canvas: a 16:9 screen (kCanvasWidth), centred.
enum class Anchor { Center, Right, Screen, Canvas };
int OffsetFor(Anchor anchor) {
static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0;
const int width = mods::widescreen::RenderWidth();
if (anchor == Anchor::Canvas) {
return width > kCanvasWidth ? (width - kCanvasWidth) / 2 : 0;
}
if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() ||
mods::widescreen::MenuIsShowing() || anchor == Anchor::Screen) {
return 0;
@@ -341,6 +346,9 @@ size_t VertexSize(DWORD fvf) {
return size;
}
// A 16:9 screen draws (RenderCanvas): no 1024 layout to stretch.
thread_local bool g_canvasScope = false;
// Returns vertices shifted by g_offsetX, or the input when nothing to do.
// Primitives covering the whole 1024x768 layout (e.g. the dimming behind
// modal dialogs) are stretched across the render width instead.
@@ -359,7 +367,7 @@ void* ShiftVertices(DWORD fvf, void* vertices, DWORD count) {
minY = std::min(minY, position[1]);
maxY = std::max(maxY, position[1]);
}
const bool fullLayout = minX <= 0.5f && maxX >= kLayoutWidth - 0.5f && minY <= 0.5f && maxY >= 767.5f;
const bool fullLayout = !g_canvasScope && minX <= 0.5f && maxX >= kLayoutWidth - 0.5f && minY <= 0.5f && maxY >= 767.5f;
const float scale = static_cast<float>(mods::widescreen::RenderWidth()) / kLayoutWidth;
for (DWORD i = 0; i < count; ++i) {
auto* x = reinterpret_cast<float*>(buffer.data() + stride * i);
@@ -788,6 +796,24 @@ int HandleCentred(void* event, int (*body)(void* context), void* context) {
return body(context);
}
int CanvasOffsetX() {
return OffsetFor(Anchor::Canvas);
}
int RenderCanvas(void* window, int (*body)(void* context), void* context) {
DrawOffset offset(window, OffsetFor(Anchor::Canvas));
const bool outer = !g_canvasScope;
g_canvasScope = true;
const int result = body(context);
g_canvasScope = !outer;
return result;
}
int HandleCanvas(void* event, int (*body)(void* context), void* context) {
EventOffset offset(event, OffsetFor(Anchor::Canvas));
return body(context);
}
} // namespace mods::hud
namespace game {
+10
View File
@@ -30,4 +30,14 @@ int HandleCentred(void* event, int (*body)(void* context), void* context);
// with a viewport on it (as the save game and export screens).
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
// the render width. RenderCanvas / HandleCanvas run body(context) with drawing,
// cursor and mouse events in canvas coordinates; CanvasOffsetX is the canvas'
// left edge in the render target.
constexpr int kCanvasWidth = 1365;
constexpr int kCanvasHeight = 768;
int CanvasOffsetX();
int RenderCanvas(void* window, int (*body)(void* context), void* context);
int HandleCanvas(void* event, int (*body)(void* context), void* context);
} // namespace mods::hud
+143 -14
View File
@@ -1,5 +1,6 @@
// Options window in three tabs: Allgemein, Beute, Tastenbelegung (request of
// 2026-10-18). Beute holds the game's auto-pickup settings (first iteration:
// 2026-10-18). Allgemein also gets the autosave interval (characters.cpp);
// Beute holds the game's auto-pickup settings (first iteration:
// moved there from the common page); Tastenbelegung sets the key bindings of
// mods/input.cpp, which had only been settable in sacredK.ini.
//
@@ -32,9 +33,11 @@
#include "game/ui/control.h"
#include "game/ui/draw.h"
#include "game/ui/events.h"
#include "game/ui/frame.h"
#include "game/ui/tabs.h"
#include "game/ui/text.h"
#include "game/ui/texts.h"
#include "mods/characters.h"
#include "mods/hud.h"
#include "mods/input.h"
#include "mods/lootfilter.h"
@@ -55,21 +58,15 @@ constexpr size_t kOkButton = 0x15c;
constexpr size_t kPickupRadios[] = {0x16c, 0x170, 0x174};
constexpr const char* kPickupControlNames[] = {"UI_LABEL_COLLECT", "UI_FRAME_SEP_AUTOCOLLECT"};
constexpr size_t kControlName = 0x30; // char[32], set by 0x732090
// cUI_Frame (vtable 0x8955ac): its render (0x73a540) doesn't test the visible
// flag but +0xdc (set to 1 by its constructors, read nowhere else).
constexpr uint32_t kFrameVtable = 0x008955ac;
constexpr size_t kFrameDrawn = 0xdc;
// cUI_Frame (game/ui/frame.h) draws by +0xdc, not by the visible flag.
// The key bindings page gets a translucent backdrop and a frame per column,
// made as 0x7184e0 makes the others: cUI_Window2 0x724ed0 (rect*, colour;
// ret 8) adds a backdrop (cUI_Static) to +0x90; frames are cUI_Frame
// (0xe0 bytes, constructor 0x739f90 (name, type 1, rect*, 1), ret 0x10)
// ret 8) adds a backdrop (cUI_Static) to +0x90; frames (game/ui/frame.h)
// pushed onto +0x90 (vector push_back 0x6bcf50, (&pointer), ret 4).
// (Drawing the existing ones at other rects did not fit: a frame's border
// pieces keep the place they got when it was made.)
constexpr uint32_t kBackdropColour = 0x9f000000;
constexpr int kFrameBorder = 0x14; // backdrop beyond the frame on each side
constexpr size_t kFrameSize = 0xe0;
constexpr uint32_t kMainFrame = 1;
// Layout of 0x7184e0 (window coordinates): the frames' borders begin 0x14
// left of / above the frame positions, their labels at y 0x20 / 0x100 / 0x1e0.
@@ -120,7 +117,7 @@ int32_t& ControlY(uint8_t* control) {
}
bool IsFrame(const uint8_t* control) {
return *reinterpret_cast<const uint32_t*>(control) == kFrameVtable;
return *reinterpret_cast<const uint32_t*>(control) == game::kFrameVtable;
}
// cUI_Scrollbar (vtable 0x895664, the volume and map transparency sliders):
@@ -224,7 +221,7 @@ void ShowPage(Window& w, uint16_t page) {
c.hiddenByUs = false;
}
if (IsFrame(c.control)) {
c.control[kFrameDrawn] = show ? 1 : 0;
c.control[game::kFrameDrawn] = show ? 1 : 0;
}
}
}
@@ -445,9 +442,8 @@ void AddFrame(uint8_t* window, game::UiRect frame, const char* name) {
static_cast<int16_t>(frame.h + 2 * kFrameBorder)};
using AddBackdropFn = void(__fastcall*)(void*, void*, game::UiRect*, uint32_t);
reinterpret_cast<AddBackdropFn>(0x00724ed0)(window, nullptr, &backdrop, kBackdropColour);
if (void* memory = game::GameNew(kFrameSize)) {
using FrameFn = void*(__fastcall*)(void*, void*, const char*, uint32_t, game::UiRect*, uint32_t);
PushControl(window, reinterpret_cast<FrameFn>(0x00739f90)(memory, nullptr, name, kMainFrame, &frame, 1));
if (uint8_t* control = game::NewFrame(name, frame)) {
PushControl(window, control);
}
}
@@ -567,6 +563,127 @@ void DrawLootPage(const Window& w, void* device) {
}
}
// --- saves (Allgemein page) ------------------------------------------------------
// The autosave interval (request of 2026-10-20), in the left column below
// the general frame (moved up, it is taller than the frames beside it): a
// frame of its own, as the right column's lowest frame (Internet-Optionen):
// its height, and the gap the right column has between its frames, both
// measured when the window is made. (A frame lower than that breaks its
// side ornaments; screenshots 2026-10-20.)
constexpr int kSavesFallbackGap = 0x50;
constexpr int16_t kSavesFallbackHeight = 0x40;
constexpr int kSavesLabelWidth = 0x90;
constexpr int kSavesBoxGap = 8;
constexpr int kAutosaveChoices[] = {0, 5, 10, 15, 30}; // minutes, 0: off
constexpr int kAutosaveChoiceCount = static_cast<int>(std::size(kAutosaveChoices));
constexpr int kSavesBoxWidth =
(kFrameWidth - 2 * kTextInset - kSavesLabelWidth - (kAutosaveChoiceCount - 1) * kSavesBoxGap) /
kAutosaveChoiceCount;
// Lighter than kBoxColour: on the frame's dark backdrop that one is not seen.
constexpr uint32_t kSavesBoxColour = 0xc0303030;
constexpr uint32_t kSavesBoxHoverColour = 0xc0484848;
game::UiRect g_savesFrame = {kFrameX[0], 0, kFrameWidth, kSavesFallbackHeight};
int g_autosavePending = 0; // minutes until OK
int g_savesHover = -1; // choice under the mouse
void CreateSavesControls(Window& w) {
int bottom = 0;
std::vector<game::UiRect> right;
for (const Control& c : w.controls) {
if (c.group != Group::Common || !IsFrame(c.control)) {
continue;
}
const game::UiRect rect = *reinterpret_cast<const game::UiRect*>(c.control + game::kControlRect);
if (ControlX(c.control) < kColumnSplit) {
bottom = std::max(bottom, rect.y + rect.h);
} else if (rect.w == kFrameWidth) {
right.push_back(rect); // the main frames, not their label plates (200 wide)
}
}
// The lowest frame and the nearest one ending above it. (Measured: the
// right column's frames are 384 wide at y 88, 312, 536, 128 / 128 / 64
// high: gaps of 96. Their label plates are frames too, 200 wide.)
std::sort(right.begin(), right.end(), [](const game::UiRect& a, const game::UiRect& b) { return a.y < b.y; });
int gap = kSavesFallbackGap;
if (!right.empty()) {
const game::UiRect& lowest = right.back();
int aboveBottom = -1;
for (const game::UiRect& r : right) {
if (r.y + r.h <= lowest.y) {
aboveBottom = std::max(aboveBottom, r.y + r.h);
}
}
if (aboveBottom >= 0) {
gap = lowest.y - aboveBottom;
}
g_savesFrame.h = lowest.h;
}
g_savesFrame.y = bottom + gap;
const size_t existing = ControlCount(w.window);
AddFrame(w.window, g_savesFrame, "UI_FRAME_SAVES");
AssignFrom(w, existing, Group::Common);
}
game::UiRect SavesRowRect(void* window, int dx, int width) {
const game::UiRect origin = game::ControlScreenRect(window);
return {origin.x + g_savesFrame.x + kTextInset + dx, origin.y + g_savesFrame.y + (g_savesFrame.h - kBoxHeight) / 2,
static_cast<int16_t>(width), static_cast<int16_t>(kBoxHeight)};
}
game::UiRect SavesBoxRect(void* window, int choice) {
return SavesRowRect(window, kSavesLabelWidth + choice * (kSavesBoxWidth + kSavesBoxGap), kSavesBoxWidth);
}
int SavesBoxAt(void* window, int x, int y) {
for (int choice = 0; choice < kAutosaveChoiceCount; ++choice) {
if (game::Inside(SavesBoxRect(window, choice), x, y)) {
return choice;
}
}
return -1;
}
void DrawSaves(const Window& w, void* device) {
void* window = w.window;
game::DrawText(device, kFont, game::CenterLineVertically(kFont, SavesRowRect(window, 0, kSavesLabelWidth)),
L"Autosave (Min.)", kTextColour, 0);
for (int choice = 0; choice < kAutosaveChoiceCount; ++choice) {
const game::UiRect box = SavesBoxRect(window, choice);
const int minutes = kAutosaveChoices[choice];
FillBox(device, box,
g_autosavePending == minutes ? kBoxSelectedColour
: g_savesHover == choice ? kSavesBoxHoverColour
: kSavesBoxColour);
const std::wstring text = minutes ? std::to_wstring(minutes) : L"Aus";
game::DrawText(device, kFont, game::CenterLineVertically(kFont, box), text.c_str(), kTextColour,
game::kTextCenter);
}
}
bool HandleSavesEvent(Window& w, void* event) {
switch (game::MouseEventKind(event)) {
case game::MouseEvent::Move:
g_savesHover = SavesBoxAt(w.window, game::EventField(event, 0xc), game::EventField(event, 0x10));
return false;
case game::MouseEvent::ButtonDown: {
if (game::EventField(event, game::kMouseButton) != game::kLeftButton) {
return false;
}
const int box = SavesBoxAt(w.window, game::EventField(event, game::kMouseButtonX),
game::EventField(event, game::kMouseButtonY));
if (box < 0) {
return false;
}
g_autosavePending = kAutosaveChoices[box];
return true;
}
default:
return false;
}
}
// --- hooks ---------------------------------------------------------------------
struct RenderCall {
@@ -588,6 +705,7 @@ int RenderBody(void* context) {
if (visible && !w->wasVisible) {
LoadPending();
LoadLootPending();
g_autosavePending = mods::characters::AutosaveMinutes();
}
w->wasVisible = visible;
const uint16_t page = game::ActiveTab(w->sheet);
@@ -600,6 +718,9 @@ int RenderBody(void* context) {
if (!visible || page != kPageLoot) {
g_lootHover = -1;
}
if (!visible || page != kPageCommon) {
g_savesHover = -1;
}
if (!visible) {
return result;
}
@@ -608,6 +729,8 @@ int RenderBody(void* context) {
DrawKeysPage(*w, call->device);
} else if (page == kPageLoot && g_pendingLoaded) {
DrawLootPage(*w, call->device);
} else if (page == kPageCommon && g_pendingLoaded) {
DrawSaves(*w, call->device);
}
return result;
}
@@ -649,6 +772,10 @@ int EventBody(void* context) {
g_pendingLoaded && HandleLootEvent(*w, call->event)) {
return 1;
}
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageCommon &&
g_pendingLoaded && HandleSavesEvent(*w, call->event)) {
return 1;
}
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageKeys &&
g_pendingLoaded) {
switch (game::MouseEventKind(call->event)) {
@@ -704,6 +831,7 @@ void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, Ui
Classify(w);
CreateKeyControls(w);
CreateLootControls(w);
CreateSavesControls(w);
CreateTabs(w);
re::Log("options: tabs created (%u controls)", static_cast<unsigned>(w.controls.size()));
}
@@ -744,6 +872,7 @@ void __fastcall UiOptions_Apply(void* self, void* edx) {
if (g_pendingLoaded) {
mods::input::SetBindingKeys(g_pending);
mods::lootfilter::SetModes(g_lootPending);
mods::characters::SetAutosaveMinutes(g_autosavePending);
}
}
+154
View File
@@ -0,0 +1,154 @@
// 16:9 menu screens (mods/screen.h, docs/screens.md).
#include "mods/screen.h"
#include <algorithm>
#include <atomic>
#include <mutex>
#include <set>
#include <windows.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/texts.h"
#include "mods/widescreen.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;
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: the menu thread renders the full width
// while any is.
std::mutex g_shownLock;
std::set<const void*> g_shown;
void NoteShown(const void* window, bool shown) {
std::lock_guard guard(g_shownLock);
if (shown) {
g_shown.insert(window);
} else {
g_shown.erase(window);
}
widescreen::SetWideMenuScreen(!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);
game::FillVertical(device, 0, 0, width, height, kBackgroundTop, kBackgroundBottom);
game::FillHorizontal(device, 0, 0, kVignetteWidth, height, kVignette, 0);
game::FillHorizontal(device, width - kVignetteWidth, 0, width, height, 0, kVignette);
}
} // namespace
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
+105
View File
@@ -0,0 +1,105 @@
#pragma once
// 16:9 menu screens (request of 2026-10-20, docs/screens.md): a screen of our
// own replaces a full-screen window of the game's (its render and mouse
// handling), laid out on a 1365x768 canvas (1920x1080 in UI units), centred in
// the render width; wider screens only get more background at the sides. The
// look is the game's: its frames and buttons on a dark background of ours.
//
// A screen hooks its window's render and event methods and calls Render /
// Handle with a body that draws / handles in canvas coordinates with the
// helpers below.
#include <cstdint>
#include <string>
#include "game/ui/text.h"
#include "mods/hud.h"
namespace mods::screen {
using game::UiRect;
constexpr int kWidth = hud::kCanvasWidth;
constexpr int kHeight = hud::kCanvasHeight;
// Spacing: the game's frames reach about 0x20 beyond their rects, buttons
// about 0x21 (measured on the options window).
constexpr int kFrameOrnament = 0x20;
// The frames are made of pieces of 64: other sizes leave gaps (the game's
// frames are 384x128, 384x64; screenshot 2026-10-20).
constexpr int kFrameStep = 64;
constexpr int kButtonOrnament = 0x21;
constexpr int kMargin = kFrameOrnament + 16; // canvas edge to a panel's rect
constexpr int kPanelGap = 96; // between two panels' rects, as the options window's columns
constexpr int kPanelPadding = 16; // panel rect to its content
constexpr uint16_t kFont = 1;
constexpr uint32_t kTextColour = 0xffe6d6aa; // as the options' texts
constexpr uint32_t kHeadingColour = 0xfff0c060; // as the options' headings
constexpr uint32_t kDimColour = 0xff8c8474;
constexpr uint32_t kRowColour = 0x40302418;
constexpr uint32_t kRowHoverColour = 0x80403020;
constexpr uint32_t kRowSelectedColour = 0xc0604818;
// 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).
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);
void Fill(void* device, const UiRect& rect, uint32_t colour);
// One line of text in `rect`, centred vertically (game::kTextCenter /
// kTextRight for horizontal alignment).
void Text(void* device, const UiRect& rect, const std::wstring& text, uint32_t colour, uint32_t flags = 0);
UiRect Inset(const UiRect& rect, int by);
// A panel: a dark backdrop with the game's frame around `rect` (made anew when
// the rect changes) and an optional heading at its top.
class Panel {
public:
explicit Panel(const char* name) : name_(name) {}
// The content area below the heading.
UiRect Draw(void* device, const UiRect& rect, const std::wstring& heading = {});
private:
const char* name_;
UiRect rect_ = {};
uint8_t* frame_ = nullptr;
};
// A button of the game's (look and sounds of `model`, a button of the
// window), made on first use.
class Button {
public:
Button(const char* name, std::wstring caption) : name_(name), caption_(std::move(caption)) {}
void Draw(void* device, const uint8_t* model, const UiRect& rect);
// Passes the event to the button (pressed look, sound); true for a left
// click on it. Only for buttons drawn this frame.
bool Clicked(void* event);
void SetCaption(const std::wstring& caption);
private:
const char* name_;
std::wstring caption_;
uint32_t captionId_ = 0;
uint8_t* control_ = nullptr;
UiRect rect_ = {};
unsigned drawnFrame_ = 0;
};
// Height of the model's buttons (as the window made them).
int16_t ButtonHeight(const uint8_t* model);
// Two clicks on the same thing within the system's double-click time.
class DoubleClick {
public:
bool Click(int target);
private:
int last_ = -1;
unsigned long at_ = 0;
};
} // namespace mods::screen
+6 -1
View File
@@ -70,6 +70,7 @@ std::atomic<DWORD> g_menuThread = 0;
// menu is showing. UI input is processed outside the menu thread.
std::atomic<bool> g_lastSceneClassic = false;
thread_local int g_loadingScreenDepth = 0;
std::atomic<bool> g_wideMenuScreen = false;
// D3DVIEWPORT7 (d3d.h is not in current Windows SDKs).
struct Viewport7 {
@@ -346,7 +347,11 @@ bool IsClassicFrame() {
if (g_renderWidth == 0) {
return false;
}
return g_loadingScreenDepth > 0 || GetCurrentThreadId() == g_menuThread.load();
return g_loadingScreenDepth > 0 || (GetCurrentThreadId() == g_menuThread.load() && !g_wideMenuScreen);
}
void SetWideMenuScreen(bool shown) {
g_wideMenuScreen = shown;
}
} // namespace mods::widescreen
+5
View File
@@ -22,4 +22,9 @@ int RenderWidth();
// 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);
} // namespace mods::widescreen