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:
1 parent
732d9c2abb
commit
34a119433a
16 files changed
+1390
-467
No files matched your search
@@ -71,6 +71,7 @@ add_library(sacredK SHARED
|
||||
src/mods/lootfilter.cpp
|
||||
src/mods/userfolders.cpp
|
||||
src/mods/charfile.cpp
|
||||
src/mods/screen.cpp
|
||||
src/mods/characters.cpp
|
||||
src/mods/mainmenu.cpp
|
||||
)
|
||||
|
||||
+52
-45
@@ -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,
|
||||
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`
|
||||
(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
|
||||
@@ -209,30 +238,6 @@ keeping the gap before "Beenden".
|
||||
- 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,
|
||||
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
|
||||
|
||||
`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
|
||||
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.
|
||||
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
|
||||
quicksave does (0x618ebb): `cUI_Manager::CanOpenWindow` (0x75a690),
|
||||
@@ -299,5 +305,6 @@ kept, the newest file overall is the shared hero.
|
||||
|
||||
## 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`).
|
||||
@@ -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
|
||||
auto-pickup frame sets a mode per category. Its frame is made like the key
|
||||
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.
|
||||
@@ -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
@@ -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
|
||||
travel in the save header (+0x88..+0x98) and the hero file. Done
|
||||
2026-10-20 (docs/characters.md).
|
||||
- **Autosave interval in the options window** (now only
|
||||
`sacredK.ini` `[characters] AutosaveMinutes`).
|
||||
- **Quickload (row 0) does not know characters.** Ctrl+Z saves into the
|
||||
current character, but the quickload event (0x116, engine +0x68 = row,
|
||||
flag 0x200) loads `GAME00.PAK`, which is not listed; make it reload the
|
||||
current character's world (or its newer autosave).
|
||||
- **Autosave interval in the options window.** Done 2026-10-20: Allgemein
|
||||
page, "Spielstände" frame (docs/options.md).
|
||||
- Quickload (row 0, `GAME00.PAK`) does not know characters; not needed
|
||||
(user, 2026-10-20).
|
||||
|
||||
## 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;
|
||||
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")
|
||||
|
||||
Asked 2026-10-20: redesign the load window (cUI_Savegame, the "Spielen"
|
||||
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
|
||||
widened beyond the 1024 frame (now drawn in a centred 1024x768 viewport with
|
||||
black sides, hud.cpp FrameScope).
|
||||
space far better. No 16:9 artwork exists for the game (user, 2026-10-20):
|
||||
we design our own UIs, using the width beyond the 1024 frame (now drawn in
|
||||
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
@@ -19,18 +19,35 @@ struct ScreenVertex {
|
||||
float u, v;
|
||||
};
|
||||
|
||||
// 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) {
|
||||
// Fills (x0, y0)-(x1, y1), ARGB colours per corner: top left, top right,
|
||||
// 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);
|
||||
using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*);
|
||||
using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD);
|
||||
reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, nullptr);
|
||||
ScreenVertex quad[4] = {{x0, y0, 0, 1, color, 0, 0, 0},
|
||||
{x1, y0, 0, 1, color, 0, 1, 0},
|
||||
{x0, y1, 0, 1, color, 0, 0, 1},
|
||||
{x1, y1, 0, 1, color, 0, 1, 1}};
|
||||
ScreenVertex quad[4] = {{x0, y0, 0, 1, topLeft, 0, 0, 0},
|
||||
{x1, y0, 0, 1, topRight, 0, 1, 0},
|
||||
{x0, y1, 0, 1, bottomLeft, 0, 0, 1},
|
||||
{x1, y1, 0, 1, bottomRight, 0, 1, 1}};
|
||||
reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(device, kTriangleStrip, kFvfScreenColourTexture,
|
||||
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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user