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
+1390 -467

No files matched your search

+1
View File
@@ -71,6 +71,7 @@ add_library(sacredK SHARED
src/mods/lootfilter.cpp src/mods/lootfilter.cpp
src/mods/userfolders.cpp src/mods/userfolders.cpp
src/mods/charfile.cpp src/mods/charfile.cpp
src/mods/screen.cpp
src/mods/characters.cpp src/mods/characters.cpp
src/mods/mainmenu.cpp src/mods/mainmenu.cpp
) )
+52 -45
View File
@@ -177,26 +177,55 @@ of both vectors, deleted) and UI_LABEL_LOADGAME ("Spielstände", shows
UI_WND_SAVEGAME) moves to the top with the text "Spielen"; the others move up, UI_WND_SAVEGAME) moves to the top with the text "Spielen"; the others move up,
keeping the gap before "Beenden". keeping the gap before "Beenden".
### Load window (cUI_Savegame) ### Spielen screen (cUI_Savegame)
Request 2026-10-20: redesign the load window, 16:9, using the space (layout
agreed: characters left, the character in the middle, worlds right, buttons
below; our own background). Built on the shared screen kit
(docs/screens.md); the window's own drawing and mouse handling are not used.
- The window still scans: before each scan (0x71e330) one preview
`GAMEnn.PAK` per character is written into `_work`; row nn = character nn.
Its entries are only needed for the load.
- Left: the characters, newest save first (name, class, level; "im Spiel" for
the one being played). Click selects, double click plays.
- Middle: name, class and level, the world played last with its date. (A
render of the hero is planned.)
- Right: a grid campaigns x difficulties. A cell shows the save's date,
"+ Autosave" when the autosave is newer, "Story abgeschlossen", "Neu" for a
difficulty that is open but has no world, "–" when closed. Open: Bronze and
Silber, every difficulty up to the highest with a world in either campaign,
and the one after each difficulty whose story the shared hero completed
(hero header +0xed..); new worlds need the shared hero, not in multiplayer
(byte 0x182cb6c). Click selects, double click plays. With a newer autosave
"Laden von:" picks the save or the autosave (default: the autosave).
"Welt zurücksetzen" for a saved world (not the character being played).
- Buttons: Neuer Charakter (main menu only), Umbenennen, Löschen (not for the
character being played), Zurück (action 0xc), Spielen.
- Spielen: a saved world loads through the window's load action (4, with its
"really load?" confirmation in a game): the window's selection is set to
its first entry and `g_loadRow` names the row, which `Engine_Load` takes
instead of the entry's. A new world starts a game (below; in a game after
stopping the running one). The window's own load / default actions (Enter)
run Spielen too; its delete actions (7, 8) are ignored.
- Dialogs over the screen: rename (keys go to the field through the input
key capture; the name is only in the hero header chunk 0xc3 (+8, UTF-16,
0x40 chars): rewritten in the hero, every world and the autosave, plus
`meta.name`), campaign of a new character, confirmations of reset and
delete. Buttons are cUI_Button2 copies of the load button
(`game::NewButtonLike`) with our own text ids and overrides.
- Delete moves the character file and its autosave to `charactersdeleted`.
- Neuer Charakter: sets `g_campaign`, hides the window and shows UI_CHARACTER
(the game's character creation, as the main menu's campaign entries did).
The first save makes the character file.
- Reset (decided 2026-10-20) removes the world section (a copy goes to
`charactersdeletedCharacterNN.sacredK.<section>.PAK`) and an autosave of
that world, and clears the shared hero's story completion of that
difficulty (hero header +0xed + d); the game counts it per difficulty, so
the next difficulty closes in both campaigns unless a world keeps it open.
### Character window (cUI_Character)
- Before each scan (0x71e330) one preview `GAMEnn.PAK` per character is
written into `_work` (header, thumbnail, world time, description
"Name - Klasse"); row nn = character nn. The preview's campaign field is
the running campaign, so the window lists every character. The row's
second line (call 0x71db2a to SetText 0x736f00) shows
"Stufe N - Campaign - difficulty".
- Laden (action 4 / 0xb) opens the world dialog when there is more than one
choice: per campaign worlds, newer autosaves "(Autosave)", and "(neu)" for
every difficulty without a world up to the highest open one (needs the
shared hero; not in multiplayer, byte 0x182cb6c). Open: Bronze and Silber,
every difficulty up to the highest with a world in either campaign, and the
one after each difficulty whose story the shared hero completed (hero
header +0xed..). `g_campaign` is set to the chosen world's before loading
or starting.
- "Neuer Charakter" (two places below Löschen; main menu only) asks for the
campaign, sets `g_campaign`, hides the window and shows UI_CHARACTER (the
game's character creation, as the main menu's campaign entries did). The
first save makes the character file.
- Vanilla made no new heroes for the Underworld: `cUI_Character::Show` - Vanilla made no new heroes for the Underworld: `cUI_Character::Show`
(vf24 0x70e2a0) switches to mode 7 in single player with `g_campaign` 1 (vf24 0x70e2a0) switches to mode 7 in single player with `g_campaign` 1
(0x70e3e8), the import of an exported hero of level 25+ (0x70eedc); with no (0x70e3e8), the import of an exported hero of level 25+ (0x70eedc); with no
@@ -209,30 +238,6 @@ keeping the gap before "Beenden".
- Class selection without "Importieren" (request 2026-10-20): button +0x3a0 - Class selection without "Importieren" (request 2026-10-20): button +0x3a0
(caption UI_IMPORT in modes 0/1); its modes-0/1 draw (0x70f67e) is removed, (caption UI_IMPORT in modes 0/1); its modes-0/1 draw (0x70f67e) is removed,
its click (0x70eab4 / 0x70eac7, SetMode(7)) jumps to the handler's end. its click (0x70eab4 / 0x70eac7, SetMode(7)) jumps to the handler's end.
- Load window without "Speichern" (request 2026-10-20; saving is direct):
button +0x820 (action 1, disabled in load mode, so its click falls through)
is not drawn (jump over 0x71ddbc); Löschen (+0x8dc) takes its rect, our
buttons follow below it.
- "Umbenennen" (below Löschen; not for the character being played) asks for
a name; keys go to the field through the input key capture. The name is
only in the hero header chunk 0xc3 (+8, UTF-16, 0x40 chars): rewritten in
the hero, every world and the autosave, plus `meta.name`. Confirmed: the
game loads renamed characters without complaint.
- Dialog buttons are cUI_Button2 copies of the load button
(`game::NewButtonLike`); their captions are our own text ids with
overrides (texts.cpp checks overrides for our ids too).
- "Zurücksetzen" (below Umbenennen; a selected character with the shared
hero and a world, not the one being played) lists the character's worlds,
then asks "<Welt> zurücksetzen?". A reset (decided 2026-10-20) removes the
world section (a copy goes to `charactersdeletedCharacterNN.sacredK.<section>.PAK`)
and an autosave of that world, and clears the shared hero's story
completion of that difficulty (hero header +0xed + d); the game counts it
per difficulty, so the next difficulty closes in both campaigns unless a
world keeps it open. The hero keeps everything else.
- The dialog is one generic list: each kind fills a title and entries
(FillDialog), "Abbrechen" last.
### Load ### Load
`cEngine::Load` (0x61b790) of a listed row: the chosen world and the hero are `cEngine::Load` (0x61b790) of a listed row: the chosen world and the hero are
@@ -265,7 +270,8 @@ Silent autosave: 10 minutes after the last save of any kind (or the game's
start), `SaveOnTick` does the direct save marked as an autosave, so it goes start), `SaveOnTick` does the direct save marked as an autosave, so it goes
into the autosave file. While a window is open or a save runs it waits for into the autosave file. While a window is open or a save runs it waits for
the next tick. A character never saved yet gets its character file from it. the next tick. A character never saved yet gets its character file from it.
Interval: `sacredK.ini` `[characters] AutosaveMinutes` (default 10, 0 off). Interval: options window, *Allgemein*, *Autosave (Min.)* (docs/options.md); kept in
`sacredK.ini` `[characters] AutosaveMinutes` (default 10, 0 off).
Direct save (K key, Esc menu "Speichern"): on the next engine tick, as the Direct save (K key, Esc menu "Speichern"): on the next engine tick, as the
quicksave does (0x618ebb): `cUI_Manager::CanOpenWindow` (0x75a690), quicksave does (0x618ebb): `cUI_Manager::CanOpenWindow` (0x75a690),
@@ -299,5 +305,6 @@ kept, the newest file overall is the shared hero.
## Open ## Open
- Quickload (row 0) does not know characters. - Quickload (row 0) does not know characters (not needed, user 2026-10-20).
- Spielen screen: a render of the hero; scrolling the list beyond 9 characters.
- Multiplayer (see `todo.md`). - Multiplayer (see `todo.md`).
+15
View File
@@ -117,3 +117,18 @@ choice (PICKUPAUTO radio buttons, their labels, the separator; matched by
pointer and control name at +0x30) is never shown, a framed table below the pointer and control name at +0x30) is never shown, a framed table below the
auto-pickup frame sets a mode per category. Its frame is made like the key auto-pickup frame sets a mode per category. Its frame is made like the key
page's (`AddFrame`). page's (`AddFrame`).
## Allgemein: autosave interval (2026-10-20)
Request: the silent autosave's interval (docs/characters.md, Save) settable
in the options, not only in `sacredK.ini`. The general frame moved up into
the auto-pickup frame's place; it is taller than the frames beside it, so the
new frame goes below the lowest frame of the left column. It copies the
right column's lowest frame (Internet-Optionen): its height and the gap the
right column leaves between its frames, measured from the window's frames
when it is made (a lower frame broke its side ornaments, a smaller gap joined
the borders: screenshots 2026-10-20). One row *Autosave (Min.)*: Aus / 5 /
10 / 15 / 30 (the key page's box colour is not seen on the dark backdrop: a
lighter one). A click selects, OK writes
`[characters] AutosaveMinutes` (`characters::SetAutosaveMinutes`, the timer
uses it at once), Cancel drops it.
+39
View File
@@ -0,0 +1,39 @@
# 16:9 menu screens
Request (2026-10-20): the game's menu screens are laid out for 1024x768; we
design our own UIs for them. There is no 16:9 artwork for the game, so the
background is our own (option 3 of the ones discussed: dark, no picture), and
the approach is shared by all menu screens. Target: 16:9, 1920x1080, which is
1365x768 in UI units; wider screens only get more background at the sides
(not designed for ultrawide).
## How a screen works
- `mods/screen.{h,cpp}`: a screen replaces a full-screen window's drawing and
mouse handling. Its render hook calls `screen::Render(window, device, body,
context)`: the background across the whole render target, then the body in
canvas coordinates (1365x768, centred: `hud::RenderCanvas`, the HUD
layer's offset machinery with `Anchor::Canvas`; drawing, the cursor and the
window's hit tests shifted alike). Mouse events go through `screen::Handle`
(`hud::HandleCanvas`). Keys stay the window's.
- In the main menu the game renders a classic 1024x768 frame (presented 4:3).
While a screen's window is shown, `widescreen::SetWideMenuScreen` makes the
menu thread render and present the full width.
- Parts: `screen::Panel` (dark backdrop and the game's frame, `cUI_Frame`,
game/ui/frame.h, made anew when its rect changes), `screen::Button` (a
button of the game's with the look of one of the window's,
`game::NewButtonLike`), `screen::Text`, `screen::Fill`,
`screen::DoubleClick`. Spacing constants from measurements: frames reach
0x20 beyond their rects and need a height of about 0x40, buttons reach 0x21.
Frames are built of 64-unit pieces: widths and heights in steps of 64
(`screen::kFrameStep`), other sizes leave gaps (screenshot 2026-10-20).
- Fonts (logged 2026-10-20): font 0 is another class (no line height field);
the TrueType fonts 1..7 have line heights 20, 19, 25, 31, 25, 28, 13.
Screens use font 1; 4 or 6 would suit headings.
- Inside the canvas scope the HUD layer does not stretch "full 1024 layout"
quads (`g_canvasScope`).
## Screens
- Spielen (the load window, cUI_Savegame): docs/characters.md, "Spielen
screen".
+17 -9
View File
@@ -184,12 +184,10 @@ From the characters rework (`docs/characters.md`, request of 2026-10-19):
are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and
travel in the save header (+0x88..+0x98) and the hero file. Done travel in the save header (+0x88..+0x98) and the hero file. Done
2026-10-20 (docs/characters.md). 2026-10-20 (docs/characters.md).
- **Autosave interval in the options window** (now only - **Autosave interval in the options window.** Done 2026-10-20: Allgemein
`sacredK.ini` `[characters] AutosaveMinutes`). page, "Spielstände" frame (docs/options.md).
- **Quickload (row 0) does not know characters.** Ctrl+Z saves into the - Quickload (row 0, `GAME00.PAK`) does not know characters; not needed
current character, but the quickload event (0x116, engine +0x68 = row, (user, 2026-10-20).
flag 0x200) loads `GAME00.PAK`, which is not listed; make it reload the
current character's world (or its newer autosave).
## Two saves in a row (2026-10-20, not reproduced) ## Two saves in a row (2026-10-20, not reproduced)
@@ -200,10 +198,20 @@ flag 0x40 is set in cEngine::ReceiveEvent at 0x618876 (quicksave),
log of the event that sets the flag (dwords 0..9) did not catch it again; log of the event that sets the flag (dwords 0..9) did not catch it again;
add it back if it shows. add it back if it shows.
Once (2026-10-20, not reproduced) an autosave seemed to fire right after a
new world started through the hero chain (Character08: hero world loaded,
then the new world). The log of that run had no save at all; the timed
autosave counts from the game start (OnGameStart).
## Savegames screen redesign ("Spielen") ## Savegames screen redesign ("Spielen")
Asked 2026-10-20: redesign the load window (cUI_Savegame, the "Spielen" Asked 2026-10-20: redesign the load window (cUI_Savegame, the "Spielen"
screen, characters.cpp): show a render of the selected character, use the screen, characters.cpp): show a render of the selected character, use the
space far better. The user looks for 16:9 artwork so the screens can be space far better. No 16:9 artwork exists for the game (user, 2026-10-20):
widened beyond the 1024 frame (now drawn in a centred 1024x768 viewport with we design our own UIs, using the width beyond the 1024 frame (now drawn in
black sides, hud.cpp FrameScope). a centred 1024x768 viewport with black sides, hud.cpp FrameScope). Design
for 16:9 (1920x1080, 1365x768 UI units), not ultrawide (user). Screen done
2026-10-20 (docs/characters.md, "Spielen screen"; docs/screens.md). Open:
the render of the selected hero (the hero panel shows text only; reuse
cUI_Character's 3D hero?), list scrolling beyond 9 characters, larger
heading font.
+23 -6
View File
@@ -19,18 +19,35 @@ struct ScreenVertex {
float u, v; float u, v;
}; };
// Fills (x0, y0)-(x1, y1) with the ARGB colour `color`. // Fills (x0, y0)-(x1, y1), ARGB colours per corner: top left, top right,
inline void FillRect(void* device, float x0, float y0, float x1, float y1, uint32_t color) { // bottom left, bottom right (blended between them).
inline void FillCorners(void* device, float x0, float y0, float x1, float y1, uint32_t topLeft, uint32_t topRight,
uint32_t bottomLeft, uint32_t bottomRight) {
void** vtable = *static_cast<void***>(device); void** vtable = *static_cast<void***>(device);
using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*); using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*);
using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD); using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD);
reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, nullptr); reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, nullptr);
ScreenVertex quad[4] = {{x0, y0, 0, 1, color, 0, 0, 0}, ScreenVertex quad[4] = {{x0, y0, 0, 1, topLeft, 0, 0, 0},
{x1, y0, 0, 1, color, 0, 1, 0}, {x1, y0, 0, 1, topRight, 0, 1, 0},
{x0, y1, 0, 1, color, 0, 0, 1}, {x0, y1, 0, 1, bottomLeft, 0, 0, 1},
{x1, y1, 0, 1, color, 0, 1, 1}}; {x1, y1, 0, 1, bottomRight, 0, 1, 1}};
reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(device, kTriangleStrip, kFvfScreenColourTexture, reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(device, kTriangleStrip, kFvfScreenColourTexture,
quad, 4, 0); quad, 4, 0);
} }
// Fills (x0, y0)-(x1, y1) with the ARGB colour `color`.
inline void FillRect(void* device, float x0, float y0, float x1, float y1, uint32_t color) {
FillCorners(device, x0, y0, x1, y1, color, color, color, color);
}
// Top to bottom from `top` to `bottom`.
inline void FillVertical(void* device, float x0, float y0, float x1, float y1, uint32_t top, uint32_t bottom) {
FillCorners(device, x0, y0, x1, y1, top, top, bottom, bottom);
}
// Left to right from `left` to `right`.
inline void FillHorizontal(void* device, float x0, float y0, float x1, float y1, uint32_t left, uint32_t right) {
FillCorners(device, x0, y0, x1, y1, left, right, left, right);
}
} // namespace game } // namespace game
+36
View File
@@ -0,0 +1,36 @@
#pragma once
// cUI_Frame (vtable 0x8955ac, 0xe0 bytes): the game's ornate frame. Its
// constructor (0x739f90, thiscall (name, type, rect*, 1), ret 0x10) places the
// border pieces once, around `rect`: a frame is not moved afterwards, a new one
// is made instead. Its render (0x73a540) draws while +0xdc is set (1 from the
// constructor), not by the visible flag. Type 1 is the windows' main frame
// (cUI_Options 0x7184e0). Its ornaments reach about 0x20 beyond the rect, and
// it needs a height of about 0x40 for its side pieces to join (measured on the
// options window, 2026-10-20).
#include <cstddef>
#include <cstdint>
#include "game/ui/tabs.h"
#include "game/ui/text.h"
namespace game {
constexpr uint32_t kFrameVtable = 0x008955ac;
constexpr size_t kFrameSize = 0xe0;
constexpr size_t kFrameDrawn = 0xdc;
constexpr uint32_t kFrameMain = 1;
constexpr int kFrameMinHeight = 0x40;
// A new frame around `rect` (relative to the parent it is added to, or the
// screen without one); null if out of memory.
inline uint8_t* NewFrame(const char* name, UiRect rect, uint32_t type = kFrameMain) {
void* memory = GameNew(kFrameSize);
if (!memory) {
return nullptr;
}
using FrameFn = uint8_t*(__fastcall*)(void*, void*, const char*, uint32_t, UiRect*, uint32_t);
return reinterpret_cast<FrameFn>(0x00739f90)(memory, nullptr, name, type, &rect, 1);
}
} // namespace game
+751 -390
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). // save window (on the next engine tick).
void RequestSave(); 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 } // namespace mods::characters
+28 -2
View File
@@ -30,6 +30,7 @@
#include "game/addresses.h" #include "game/addresses.h"
#include "game/render/dx_driver7.h" #include "game/render/dx_driver7.h"
#include "game/ui/events.h" #include "game/ui/events.h"
#include "mods/hud.h"
#include "mods/widescreen.h" #include "mods/widescreen.h"
#include "re/config.h" #include "re/config.h"
#include "re/hook.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). // 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) { int OffsetFor(Anchor anchor) {
static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0; static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0;
const int width = mods::widescreen::RenderWidth(); const int width = mods::widescreen::RenderWidth();
if (anchor == Anchor::Canvas) {
return width > kCanvasWidth ? (width - kCanvasWidth) / 2 : 0;
}
if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() || if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() ||
mods::widescreen::MenuIsShowing() || anchor == Anchor::Screen) { mods::widescreen::MenuIsShowing() || anchor == Anchor::Screen) {
return 0; return 0;
@@ -341,6 +346,9 @@ size_t VertexSize(DWORD fvf) {
return size; 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. // Returns vertices shifted by g_offsetX, or the input when nothing to do.
// Primitives covering the whole 1024x768 layout (e.g. the dimming behind // Primitives covering the whole 1024x768 layout (e.g. the dimming behind
// modal dialogs) are stretched across the render width instead. // 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]); minY = std::min(minY, position[1]);
maxY = std::max(maxY, 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; const float scale = static_cast<float>(mods::widescreen::RenderWidth()) / kLayoutWidth;
for (DWORD i = 0; i < count; ++i) { for (DWORD i = 0; i < count; ++i) {
auto* x = reinterpret_cast<float*>(buffer.data() + stride * 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); 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 mods::hud
namespace game { 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). // with a viewport on it (as the save game and export screens).
int RenderFramed(void* window, void* device, int (*body)(void* context), void* context); 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 } // namespace mods::hud
+143 -14
View File
@@ -1,5 +1,6 @@
// Options window in three tabs: Allgemein, Beute, Tastenbelegung (request of // 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 // moved there from the common page); Tastenbelegung sets the key bindings of
// mods/input.cpp, which had only been settable in sacredK.ini. // mods/input.cpp, which had only been settable in sacredK.ini.
// //
@@ -32,9 +33,11 @@
#include "game/ui/control.h" #include "game/ui/control.h"
#include "game/ui/draw.h" #include "game/ui/draw.h"
#include "game/ui/events.h" #include "game/ui/events.h"
#include "game/ui/frame.h"
#include "game/ui/tabs.h" #include "game/ui/tabs.h"
#include "game/ui/text.h" #include "game/ui/text.h"
#include "game/ui/texts.h" #include "game/ui/texts.h"
#include "mods/characters.h"
#include "mods/hud.h" #include "mods/hud.h"
#include "mods/input.h" #include "mods/input.h"
#include "mods/lootfilter.h" #include "mods/lootfilter.h"
@@ -55,21 +58,15 @@ constexpr size_t kOkButton = 0x15c;
constexpr size_t kPickupRadios[] = {0x16c, 0x170, 0x174}; constexpr size_t kPickupRadios[] = {0x16c, 0x170, 0x174};
constexpr const char* kPickupControlNames[] = {"UI_LABEL_COLLECT", "UI_FRAME_SEP_AUTOCOLLECT"}; constexpr const char* kPickupControlNames[] = {"UI_LABEL_COLLECT", "UI_FRAME_SEP_AUTOCOLLECT"};
constexpr size_t kControlName = 0x30; // char[32], set by 0x732090 constexpr size_t kControlName = 0x30; // char[32], set by 0x732090
// cUI_Frame (vtable 0x8955ac): its render (0x73a540) doesn't test the visible // cUI_Frame (game/ui/frame.h) draws by +0xdc, not by the visible flag.
// flag but +0xdc (set to 1 by its constructors, read nowhere else).
constexpr uint32_t kFrameVtable = 0x008955ac;
constexpr size_t kFrameDrawn = 0xdc;
// The key bindings page gets a translucent backdrop and a frame per column, // The key bindings page gets a translucent backdrop and a frame per column,
// made as 0x7184e0 makes the others: cUI_Window2 0x724ed0 (rect*, colour; // made as 0x7184e0 makes the others: cUI_Window2 0x724ed0 (rect*, colour;
// ret 8) adds a backdrop (cUI_Static) to +0x90; frames are cUI_Frame // ret 8) adds a backdrop (cUI_Static) to +0x90; frames (game/ui/frame.h)
// (0xe0 bytes, constructor 0x739f90 (name, type 1, rect*, 1), ret 0x10)
// pushed onto +0x90 (vector push_back 0x6bcf50, (&pointer), ret 4). // pushed onto +0x90 (vector push_back 0x6bcf50, (&pointer), ret 4).
// (Drawing the existing ones at other rects did not fit: a frame's border // (Drawing the existing ones at other rects did not fit: a frame's border
// pieces keep the place they got when it was made.) // pieces keep the place they got when it was made.)
constexpr uint32_t kBackdropColour = 0x9f000000; constexpr uint32_t kBackdropColour = 0x9f000000;
constexpr int kFrameBorder = 0x14; // backdrop beyond the frame on each side 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 // Layout of 0x7184e0 (window coordinates): the frames' borders begin 0x14
// left of / above the frame positions, their labels at y 0x20 / 0x100 / 0x1e0. // 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) { 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): // cUI_Scrollbar (vtable 0x895664, the volume and map transparency sliders):
@@ -224,7 +221,7 @@ void ShowPage(Window& w, uint16_t page) {
c.hiddenByUs = false; c.hiddenByUs = false;
} }
if (IsFrame(c.control)) { 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)}; static_cast<int16_t>(frame.h + 2 * kFrameBorder)};
using AddBackdropFn = void(__fastcall*)(void*, void*, game::UiRect*, uint32_t); using AddBackdropFn = void(__fastcall*)(void*, void*, game::UiRect*, uint32_t);
reinterpret_cast<AddBackdropFn>(0x00724ed0)(window, nullptr, &backdrop, kBackdropColour); reinterpret_cast<AddBackdropFn>(0x00724ed0)(window, nullptr, &backdrop, kBackdropColour);
if (void* memory = game::GameNew(kFrameSize)) { if (uint8_t* control = game::NewFrame(name, frame)) {
using FrameFn = void*(__fastcall*)(void*, void*, const char*, uint32_t, game::UiRect*, uint32_t); PushControl(window, control);
PushControl(window, reinterpret_cast<FrameFn>(0x00739f90)(memory, nullptr, name, kMainFrame, &frame, 1));
} }
} }
@@ -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 --------------------------------------------------------------------- // --- hooks ---------------------------------------------------------------------
struct RenderCall { struct RenderCall {
@@ -588,6 +705,7 @@ int RenderBody(void* context) {
if (visible && !w->wasVisible) { if (visible && !w->wasVisible) {
LoadPending(); LoadPending();
LoadLootPending(); LoadLootPending();
g_autosavePending = mods::characters::AutosaveMinutes();
} }
w->wasVisible = visible; w->wasVisible = visible;
const uint16_t page = game::ActiveTab(w->sheet); const uint16_t page = game::ActiveTab(w->sheet);
@@ -600,6 +718,9 @@ int RenderBody(void* context) {
if (!visible || page != kPageLoot) { if (!visible || page != kPageLoot) {
g_lootHover = -1; g_lootHover = -1;
} }
if (!visible || page != kPageCommon) {
g_savesHover = -1;
}
if (!visible) { if (!visible) {
return result; return result;
} }
@@ -608,6 +729,8 @@ int RenderBody(void* context) {
DrawKeysPage(*w, call->device); DrawKeysPage(*w, call->device);
} else if (page == kPageLoot && g_pendingLoaded) { } else if (page == kPageLoot && g_pendingLoaded) {
DrawLootPage(*w, call->device); DrawLootPage(*w, call->device);
} else if (page == kPageCommon && g_pendingLoaded) {
DrawSaves(*w, call->device);
} }
return result; return result;
} }
@@ -649,6 +772,10 @@ int EventBody(void* context) {
g_pendingLoaded && HandleLootEvent(*w, call->event)) { g_pendingLoaded && HandleLootEvent(*w, call->event)) {
return 1; 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 && if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageKeys &&
g_pendingLoaded) { g_pendingLoaded) {
switch (game::MouseEventKind(call->event)) { switch (game::MouseEventKind(call->event)) {
@@ -704,6 +831,7 @@ void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, Ui
Classify(w); Classify(w);
CreateKeyControls(w); CreateKeyControls(w);
CreateLootControls(w); CreateLootControls(w);
CreateSavesControls(w);
CreateTabs(w); CreateTabs(w);
re::Log("options: tabs created (%u controls)", static_cast<unsigned>(w.controls.size())); 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) { if (g_pendingLoaded) {
mods::input::SetBindingKeys(g_pending); mods::input::SetBindingKeys(g_pending);
mods::lootfilter::SetModes(g_lootPending); 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. // menu is showing. UI input is processed outside the menu thread.
std::atomic<bool> g_lastSceneClassic = false; std::atomic<bool> g_lastSceneClassic = false;
thread_local int g_loadingScreenDepth = 0; thread_local int g_loadingScreenDepth = 0;
std::atomic<bool> g_wideMenuScreen = false;
// D3DVIEWPORT7 (d3d.h is not in current Windows SDKs). // D3DVIEWPORT7 (d3d.h is not in current Windows SDKs).
struct Viewport7 { struct Viewport7 {
@@ -346,7 +347,11 @@ bool IsClassicFrame() {
if (g_renderWidth == 0) { if (g_renderWidth == 0) {
return false; 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 } // namespace mods::widescreen
+5
View File
@@ -22,4 +22,9 @@ int RenderWidth();
// regardless of the calling thread. // regardless of the calling thread.
bool MenuIsShowing(); 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 } // namespace mods::widescreen