Preferred monitor: chosen in Optionen > Allgemein, used at the next start

The Internet-Optionen frame becomes Bildschirm with a Monitor dropdown
(Hauptmonitor or a connected monitor, kept by device path in
[display] Monitor=). The borderless window and the render size use that
monitor, falling back to the main monitor when it is not connected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-25 16:06:20 +01:00
1 parent c7d54a6cfd
commit 9bc557cd95
10 files changed
+407 -14

No files matched your search

+4 -5
View File
@@ -3,7 +3,7 @@
// The game renders at a fixed resolution (1024x768). Unmodified, windowed mode
// shows that as a 1024x768 popup centred on the desktop; fullscreen mode
// switches the display mode. With this mod the game always runs windowed, the
// popup covers the whole monitor, and each frame is scaled up into the largest
// popup covers the whole monitor (mods/monitor.h: the preferred one), and each frame is scaled up into the largest
// centred rectangle with the game's aspect ratio (black bars elsewhere).
//
// Mouse positions are mapped back from that rectangle to game pixels, both in
@@ -35,6 +35,7 @@
#include "game/render/dx_driver7.h"
#include "mods/hud.h"
#include "mods/input.h"
#include "mods/monitor.h"
#include "mods/widescreen.h"
#include "re/config.h"
#include "game/addresses.h"
@@ -90,9 +91,7 @@ RECT FitRect(int areaWidth, int areaHeight, int width, int height, bool integerS
void ApplyBorderless(game::DxDriver7* driver) {
HWND window = driver->window;
MONITORINFO monitor = {sizeof(monitor)};
GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST), &monitor);
const RECT& area = monitor.rcMonitor;
const RECT area = mods::monitor::GameMonitorRect();
const int areaWidth = area.right - area.left;
const int areaHeight = area.bottom - area.top;
@@ -171,7 +170,7 @@ int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* device
}
DxDisplayMode windowed = *mode;
windowed.fullscreen = 0;
mods::widescreen::ApplyRenderSize(windowed, self->window);
mods::widescreen::ApplyRenderSize(windowed);
g_viewport.active = false;
const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown);
if (result) {
+160
View File
@@ -0,0 +1,160 @@
// Preferred monitor (request of 2026-10-25): a setting in the graphics options
// picks the monitor the game starts on; with none set, or the preferred one
// not connected, the system's main monitor.
//
// The game window and its render size are decided when the display mode is
// set at startup (borderless.cpp, widescreen.cpp ApplyRenderSize): both ask
// GameMonitorRect. A change applies at the next start.
//
// Monitors are told apart by their device path (DisplayConfigGetDeviceInfo,
// target name: "\\?\DISPLAY#GSM5B7F#..."), which survives restarts, monitor
// order changes and other ports' changes, unlike the GDI names \\.\DISPLAYn.
//
// sacredK.ini:
// [display]
// Monitor=<device path> ; empty: the main monitor
#include "mods/monitor.h"
#include <algorithm>
#include "re/config.h"
#include "re/log.h"
namespace mods::monitor {
namespace {
constexpr const char* kSection = "display";
constexpr const char* kKey = "Monitor";
std::string Utf8(const wchar_t* text) {
const int size = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
if (size <= 1) {
return {};
}
std::string result(static_cast<size_t>(size - 1), '\0');
WideCharToMultiByte(CP_UTF8, 0, text, -1, result.data(), size, nullptr, nullptr);
return result;
}
struct Target {
std::wstring gdiName; // \\.\DISPLAYn of the source showing it
std::string id;
std::wstring name;
};
// The active display paths: which monitor shows which GDI display.
std::vector<Target> Targets() {
UINT32 pathCount = 0;
UINT32 modeCount = 0;
std::vector<DISPLAYCONFIG_PATH_INFO> paths;
std::vector<DISPLAYCONFIG_MODE_INFO> modes;
LONG result;
do {
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount) != ERROR_SUCCESS) {
return {};
}
paths.resize(pathCount);
modes.resize(modeCount);
result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathCount, paths.data(), &modeCount, modes.data(),
nullptr);
} while (result == ERROR_INSUFFICIENT_BUFFER);
if (result != ERROR_SUCCESS) {
return {};
}
paths.resize(pathCount);
std::vector<Target> targets;
for (const DISPLAYCONFIG_PATH_INFO& path : paths) {
DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {};
source.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
source.header.size = sizeof(source);
source.header.adapterId = path.sourceInfo.adapterId;
source.header.id = path.sourceInfo.id;
DISPLAYCONFIG_TARGET_DEVICE_NAME target = {};
target.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
target.header.size = sizeof(target);
target.header.adapterId = path.targetInfo.adapterId;
target.header.id = path.targetInfo.id;
if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS ||
DisplayConfigGetDeviceInfo(&target.header) != ERROR_SUCCESS) {
continue;
}
targets.push_back({source.viewGdiDeviceName, Utf8(target.monitorDevicePath), target.monitorFriendlyDeviceName});
}
return targets;
}
BOOL CALLBACK AddMonitor(HMONITOR handle, HDC, LPRECT, LPARAM data) {
auto* gdi = reinterpret_cast<std::vector<MONITORINFOEXW>*>(data);
MONITORINFOEXW info = {};
info.cbSize = sizeof(info);
if (GetMonitorInfoW(handle, &info)) {
gdi->push_back(info);
}
return TRUE;
}
} // namespace
std::vector<Monitor> List() {
std::vector<MONITORINFOEXW> gdi;
EnumDisplayMonitors(nullptr, nullptr, &AddMonitor, reinterpret_cast<LPARAM>(&gdi));
const std::vector<Target> targets = Targets();
std::vector<Monitor> monitors;
for (const MONITORINFOEXW& info : gdi) {
Monitor monitor{"", L"", info.rcMonitor, (info.dwFlags & MONITORINFOF_PRIMARY) != 0};
for (const Target& target : targets) {
if (target.gdiName == info.szDevice) { // mirrored displays: the first monitor
monitor.id = target.id;
monitor.name = target.name;
break;
}
}
if (monitor.id.empty()) {
monitor.id = Utf8(info.szDevice); // no display config (remote desktop, ...)
}
if (monitor.name.empty()) {
monitor.name = L"Monitor " + std::to_wstring(monitors.size() + 1);
}
monitors.push_back(monitor);
}
std::stable_sort(monitors.begin(), monitors.end(),
[](const Monitor& a, const Monitor& b) { return a.primary && !b.primary; });
return monitors;
}
std::string PreferredId() {
return re::ConfigString(kSection, kKey, "");
}
void SetPreferredId(const std::string& id) {
re::SetConfigString(kSection, kKey, id.c_str());
}
RECT GameMonitorRect() {
const std::vector<Monitor> monitors = List();
const std::string preferred = PreferredId();
const Monitor* chosen = nullptr;
for (const Monitor& monitor : monitors) {
if ((!preferred.empty() && monitor.id == preferred) || (!chosen && monitor.primary)) {
chosen = &monitor;
}
}
if (!chosen) {
// No monitor reported: the main monitor's rectangle as Windows has it.
MONITORINFO info = {sizeof(info)};
GetMonitorInfoW(MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY), &info);
return info.rcMonitor;
}
const bool found = !preferred.empty() && chosen->id == preferred;
static bool logged = false;
if (!logged) {
logged = true;
re::Log("monitor: %ls %ldx%ld at (%ld,%ld)%s", chosen->name.c_str(), chosen->rect.right - chosen->rect.left,
chosen->rect.bottom - chosen->rect.top, chosen->rect.left, chosen->rect.top,
found ? "" : preferred.empty() ? ", main monitor" : ", preferred monitor not connected: main monitor");
}
return chosen->rect;
}
} // namespace mods::monitor
+31
View File
@@ -0,0 +1,31 @@
#pragma once
// The monitor the game runs on (request of 2026-10-25): a preferred monitor
// from the options, or the system's main monitor when none is set or the
// preferred one is not connected. See monitor.cpp.
#include <string>
#include <vector>
#include <windows.h>
namespace mods::monitor {
struct Monitor {
std::string id; // stable across restarts and reordering: the monitor's device path
std::wstring name; // the monitor's own name (EDID), e.g. "LG ULTRAGEAR"
RECT rect; // desktop coordinates
bool primary;
};
// The connected monitors, the main monitor first.
std::vector<Monitor> List();
// The preferred monitor's id from sacredK.ini, empty for none.
std::string PreferredId();
void SetPreferredId(const std::string& id);
// The rectangle of the monitor to run on: the preferred one if connected,
// else the main monitor.
RECT GameMonitorRect();
} // namespace mods::monitor
+166 -1
View File
@@ -41,6 +41,7 @@
#include "mods/hud.h"
#include "mods/input.h"
#include "mods/lootfilter.h"
#include "mods/monitor.h"
#include "re/hook.h"
#include "re/log.h"
@@ -684,6 +685,160 @@ bool HandleSavesEvent(Window& w, void* event) {
}
}
// --- monitor (Allgemein page) ----------------------------------------------------
// Preferred monitor (request of 2026-10-25, mods/monitor.h) in place of the
// Internet-Optionen frame (Datendurchsatz, multiplayer only; the user's
// choice): its controls are hidden (the game keeps the saved value), its title
// reads "Bildschirm" (text UI_CFG_WAN, set by 0x672a20 / 0x724d50 when the
// window is made). One row: "Monitor" and a dropdown box; the list opens
// upwards (the frame is the lowest one), a click picks, a click elsewhere
// closes it. OK saves the choice, which applies at the next start.
constexpr const char* kScreenFrameName = "UI_FRAME_INTERNET";
constexpr const char* kScreenTitleName = "UI_LABEL_WAN";
constexpr const char* kScreenTitleKey = "UI_CFG_WAN";
constexpr int kScreenLabelWidth = 0x70;
constexpr int kArrowWidth = 9; // the box's down arrow, 5 rows
struct ScreenChoice {
std::string id; // empty: the main monitor
std::wstring text;
};
game::UiRect g_screenFrame = {};
std::vector<ScreenChoice> g_screenChoices;
std::string g_screenPending;
bool g_screenOpen = false;
int g_screenHover = -1; // list entry under the mouse
void CreateScreenControls(Window& w) {
for (const Control& c : w.controls) {
if (IsFrame(c.control) && game::ControlNamed(c.control, kScreenFrameName)) {
g_screenFrame = *reinterpret_cast<const game::UiRect*>(c.control + game::kControlRect);
}
}
for (Control& c : w.controls) {
const game::UiRect r = *reinterpret_cast<const game::UiRect*>(c.control + game::kControlRect);
const bool inside = r.x >= g_screenFrame.x && r.y >= g_screenFrame.y &&
r.x < g_screenFrame.x + g_screenFrame.w && r.y < g_screenFrame.y + g_screenFrame.h;
if (c.group == Group::Common &&
(game::ControlNamed(c.control, kScreenTitleName) || (inside && !IsFrame(c.control)))) {
c.group = Group::Hidden;
}
}
}
void LoadScreenPending() {
g_screenChoices = {{"", L"Hauptmonitor"}};
for (const mods::monitor::Monitor& m : mods::monitor::List()) {
g_screenChoices.push_back({m.id, m.name + L" (" + std::to_wstring(m.rect.right - m.rect.left) + L"x" +
std::to_wstring(m.rect.bottom - m.rect.top) + L")"});
}
g_screenPending = mods::monitor::PreferredId();
g_screenOpen = false;
g_screenHover = -1;
}
const wchar_t* ScreenPendingText() {
for (const ScreenChoice& choice : g_screenChoices) {
if (choice.id == g_screenPending) {
return choice.text.c_str();
}
}
return L"Nicht verbunden";
}
game::UiRect ScreenRowRect(void* window, int dx, int width) {
const game::UiRect origin = game::ControlScreenRect(window);
return {origin.x + g_screenFrame.x + kTextInset + dx,
origin.y + g_screenFrame.y + (g_screenFrame.h - kBoxHeight) / 2, static_cast<int16_t>(width),
static_cast<int16_t>(kBoxHeight)};
}
game::UiRect ScreenBoxRect(void* window) {
return ScreenRowRect(window, kScreenLabelWidth, g_screenFrame.w - 2 * kTextInset - kScreenLabelWidth);
}
// List entry `index`, upwards from the box: the last entry right above it.
game::UiRect ScreenEntryRect(void* window, int index) {
game::UiRect r = ScreenBoxRect(window);
r.y -= static_cast<int>(g_screenChoices.size() - index) * kBoxHeight;
return r;
}
int ScreenEntryAt(void* window, int x, int y) {
for (int i = 0; i < static_cast<int>(g_screenChoices.size()); ++i) {
if (game::Inside(ScreenEntryRect(window, i), x, y)) {
return i;
}
}
return -1;
}
void DrawScreen(const Window& w, void* device) {
void* window = w.window;
game::DrawText(device, kFont, game::CenterLineVertically(kFont, ScreenRowRect(window, 0, kScreenLabelWidth)),
L"Monitor", kTextColour, 0);
const game::UiRect box = ScreenBoxRect(window);
FillBox(device, box, g_screenOpen ? kSavesBoxHoverColour : kSavesBoxColour);
game::UiRect text = box;
text.x += 6;
text.w = static_cast<int16_t>(text.w - 6 - kArrowWidth - 8);
game::DrawText(device, kFont, game::CenterLineVertically(kFont, text), ScreenPendingText(), kTextColour, 0);
// The arrow: rows narrowing to a point.
const int arrowX = box.x + box.w - kArrowWidth - 6;
const int arrowY = box.y + (box.h - 5) / 2;
for (int row = 0; row < 5; ++row) {
FillBox(device, {arrowX + row, arrowY + row, static_cast<int16_t>(kArrowWidth - 2 * row), 1}, kTextColour);
}
if (g_screenPending != mods::monitor::PreferredId()) {
game::UiRect hint = {box.x, box.y + box.h + 2, box.w, static_cast<int16_t>(game::FontLineHeight(kFont))};
game::DrawText(device, kFont, hint, L"Ab dem nächsten Spielstart", kUnboundColour, 0);
}
if (!g_screenOpen) {
return;
}
for (int i = 0; i < static_cast<int>(g_screenChoices.size()); ++i) {
const game::UiRect entry = ScreenEntryRect(window, i);
const bool selected = g_screenChoices[i].id == g_screenPending;
FillBox(device, entry, selected ? kBoxSelectedColour : i == g_screenHover ? kBoxHoverColour : kBoxColour);
game::UiRect label = entry;
label.x += 6;
game::DrawText(device, kFont, game::CenterLineVertically(kFont, label), g_screenChoices[i].text.c_str(),
kTextColour, 0);
}
}
bool HandleScreenEvent(Window& w, void* event) {
switch (game::MouseEventKind(event)) {
case game::MouseEvent::Move:
g_screenHover =
g_screenOpen ? ScreenEntryAt(w.window, game::EventField(event, 0xc), game::EventField(event, 0x10)) : -1;
return false;
case game::MouseEvent::ButtonDown: {
const int x = game::EventField(event, game::kMouseButtonX);
const int y = game::EventField(event, game::kMouseButtonY);
if (g_screenOpen) {
const int entry = ScreenEntryAt(w.window, x, y);
if (entry >= 0 && game::EventField(event, game::kMouseButton) == game::kLeftButton) {
g_screenPending = g_screenChoices[entry].id;
}
g_screenOpen = false;
g_screenHover = -1;
return true; // the click only closes the list
}
if (game::EventField(event, game::kMouseButton) == game::kLeftButton &&
game::Inside(ScreenBoxRect(w.window), x, y)) {
g_screenOpen = true;
return true;
}
return false;
}
default:
return false;
}
}
// --- hooks ---------------------------------------------------------------------
struct RenderCall {
@@ -706,6 +861,7 @@ int RenderBody(void* context) {
LoadPending();
LoadLootPending();
g_autosavePending = mods::characters::AutosaveMinutes();
LoadScreenPending();
}
w->wasVisible = visible;
const uint16_t page = game::ActiveTab(w->sheet);
@@ -720,6 +876,8 @@ int RenderBody(void* context) {
}
if (!visible || page != kPageCommon) {
g_savesHover = -1;
g_screenOpen = false;
g_screenHover = -1;
}
if (!visible) {
return result;
@@ -731,6 +889,7 @@ int RenderBody(void* context) {
DrawLootPage(*w, call->device);
} else if (page == kPageCommon && g_pendingLoaded) {
DrawSaves(*w, call->device);
DrawScreen(*w, call->device);
}
return result;
}
@@ -773,7 +932,7 @@ int EventBody(void* context) {
return 1;
}
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageCommon &&
g_pendingLoaded && HandleSavesEvent(*w, call->event)) {
g_pendingLoaded && (HandleScreenEvent(*w, call->event) || HandleSavesEvent(*w, call->event))) {
return 1;
}
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageKeys &&
@@ -824,11 +983,16 @@ void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, Ui
RE_HOOK(0x00716570, UiOptions_Construct);
void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, UiRect rect, void* parent,
uint32_t flags) {
static const bool titled = [] {
game::SetTextOverride(game::GameTextIdByKey(kScreenTitleKey), L"Bildschirm");
return true;
}();
void* window = UiOptions_Construct_original(self, edx, name, rect, parent, flags);
if (window) {
g_windows.push_back({static_cast<uint8_t*>(window)});
Window& w = g_windows.back();
Classify(w);
CreateScreenControls(w);
CreateKeyControls(w);
CreateLootControls(w);
CreateSavesControls(w);
@@ -873,6 +1037,7 @@ void __fastcall UiOptions_Apply(void* self, void* edx) {
mods::input::SetBindingKeys(g_pending);
mods::lootfilter::SetModes(g_lootPending);
mods::characters::SetAutosaveMinutes(g_autosavePending);
mods::monitor::SetPreferredId(g_screenPending);
}
}
+5 -5
View File
@@ -26,6 +26,7 @@
#include "mods/hud.h"
#include "mods/loadingscreen.h"
#include "mods/megamap.h"
#include "mods/monitor.h"
#include <algorithm>
#include <atomic>
@@ -270,17 +271,16 @@ void PatchLetterbox(int width) {
} // namespace
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
void ApplyRenderSize(game::DxDisplayMode& mode) {
if (!Enabled()) {
return;
}
MONITORINFO monitor = {sizeof(monitor)};
GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY), &monitor);
const RECT monitor = mods::monitor::GameMonitorRect();
// Default: keep the original 768 pixels of world height (so everything
// keeps its on-screen size once scaled to the monitor) and extend the width
// to the monitor's aspect ratio. Height=0 renders at the monitor height 1:1.
const int monitorWidth = monitor.rcMonitor.right - monitor.rcMonitor.left;
const int monitorHeight = monitor.rcMonitor.bottom - monitor.rcMonitor.top;
const int monitorWidth = monitor.right - monitor.left;
const int monitorHeight = monitor.bottom - monitor.top;
int height = re::ConfigInt("widescreen", "Height", static_cast<int>(kOriginalHeight));
if (height <= 0) {
height = monitorHeight;
+1 -1
View File
@@ -9,7 +9,7 @@ namespace mods::widescreen {
// Called by the borderless mod before the renderer creates its surfaces:
// sets the render size to the monitor (or the configured size) and resizes
// the world view to match. No-op when disabled.
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window);
void ApplyRenderSize(game::DxDisplayMode& mode);
// True while the current thread draws the load / save progress screen.
bool IsLoadingScreen();