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:
1 parent
c7d54a6cfd
commit
9bc557cd95
10 files changed
+407
-14
No files matched your search
@@ -39,6 +39,7 @@ add_library(sacredK SHARED
|
|||||||
src/game/engine/stream.cpp
|
src/game/engine/stream.cpp
|
||||||
src/game/ui/texts.cpp
|
src/game/ui/texts.cpp
|
||||||
src/game/engine/art.cpp
|
src/game/engine/art.cpp
|
||||||
|
src/mods/monitor.cpp
|
||||||
src/mods/borderless.cpp
|
src/mods/borderless.cpp
|
||||||
src/mods/widescreen.cpp
|
src/mods/widescreen.cpp
|
||||||
src/mods/hud.cpp
|
src/mods/hud.cpp
|
||||||
|
|||||||
@@ -132,3 +132,32 @@ the borders: screenshots 2026-10-20). One row *Autosave (Min.)*: Aus / 5 /
|
|||||||
lighter one). A click selects, OK writes
|
lighter one). A click selects, OK writes
|
||||||
`[characters] AutosaveMinutes` (`characters::SetAutosaveMinutes`, the timer
|
`[characters] AutosaveMinutes` (`characters::SetAutosaveMinutes`, the timer
|
||||||
uses it at once), Cancel drops it.
|
uses it at once), Cancel drops it.
|
||||||
|
|
||||||
|
## Allgemein: preferred monitor (2026-10-25)
|
||||||
|
|
||||||
|
Request: a setting for the monitor the game starts on; with none set, or the
|
||||||
|
preferred monitor not connected, the system's main monitor.
|
||||||
|
|
||||||
|
- `src/mods/monitor.cpp`: the monitors (`EnumDisplayMonitors`), named and
|
||||||
|
told apart through the display configuration (`QueryDisplayConfig`,
|
||||||
|
`DisplayConfigGetDeviceInfo`: the GDI name `\.\DISPLAYn` of each path's
|
||||||
|
source, the monitor's EDID name and device path of its target). The device
|
||||||
|
path is the id: it stays the same across restarts and when monitors are
|
||||||
|
rearranged, unlike `\.\DISPLAYn`. Kept in `sacredK.ini`
|
||||||
|
`[display] Monitor=` (empty: the main monitor).
|
||||||
|
- `GameMonitorRect` is asked where the screen is decided at startup: the
|
||||||
|
borderless window covers that monitor (`borderless.cpp`), and the render
|
||||||
|
size follows its aspect ratio (`widescreen.cpp` `ApplyRenderSize`). The
|
||||||
|
log line `monitor: <name> WxH at (x,y)` says which one was used and why.
|
||||||
|
A change applies at the next start.
|
||||||
|
- In the options (user's choice of place): the *Internet-Optionen* frame
|
||||||
|
(Datendurchsatz, multiplayer only) becomes *Bildschirm*. Its controls
|
||||||
|
(`UI_CFG_LANSPEED` label, DSL / ISDN radio buttons, everything inside
|
||||||
|
`UI_FRAME_INTERNET`) and the title label `UI_LABEL_WAN` are hidden; the
|
||||||
|
game keeps the saved Datendurchsatz. The title text (key `UI_CFG_WAN`) is
|
||||||
|
overridden with "Bildschirm" before the window is made.
|
||||||
|
- One row: *Monitor* and a box with the choice (Hauptmonitor, then each
|
||||||
|
monitor as "name (WxH)", "Nicht verbunden" for a preferred monitor that is
|
||||||
|
missing). A click opens the list upwards (the frame is the lowest one); a
|
||||||
|
click on an entry picks it, any click closes the list. A changed choice
|
||||||
|
shows "Ab dem nächsten Spielstart" below the box. OK saves, Cancel drops.
|
||||||
@@ -156,11 +156,16 @@ void SetTextOverride(uint32_t id, const std::wstring& text) {
|
|||||||
entry->string = {begin, begin + entry->text.size(), begin + entry->text.size()};
|
entry->string = {begin, begin + entry->text.size(), begin + entry->text.size()};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring GameTextByKey(const char* key) {
|
uint32_t GameTextIdByKey(const char* key) {
|
||||||
// The key's hash (0x80e010, cdecl), looked up "by key" as GetTextByKey does.
|
// The key's hash (0x80e010, cdecl), looked up "by key" as GetTextByKey does.
|
||||||
using HashFn = uint32_t(__cdecl*)(const char*);
|
using HashFn = uint32_t(__cdecl*)(const char*);
|
||||||
const uint32_t hash = reinterpret_cast<HashFn>(0x0080e010)(key);
|
const uint32_t hash = reinterpret_cast<HashFn>(0x0080e010)(key);
|
||||||
return hash ? GameText(hash | 0x80000000) : std::wstring();
|
return hash ? hash | 0x80000000 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring GameTextByKey(const char* key) {
|
||||||
|
const uint32_t id = GameTextIdByKey(key);
|
||||||
|
return id ? GameText(id) : std::wstring();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring WideStringText(const void* string) {
|
std::wstring WideStringText(const void* string) {
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ void SetTextOverride(uint32_t id, const std::wstring& text);
|
|||||||
// The game's text under key `key` (TextManager::GetTextByKey 0x672fd0), e.g.
|
// The game's text under key `key` (TextManager::GetTextByKey 0x672fd0), e.g.
|
||||||
// "UI_ATTRIB_STRENGTH"; empty if there is none.
|
// "UI_ATTRIB_STRENGTH"; empty if there is none.
|
||||||
std::wstring GameTextByKey(const char* key);
|
std::wstring GameTextByKey(const char* key);
|
||||||
|
// The id of the game's text under key `key` (0 if the key has no hash), e.g.
|
||||||
|
// for SetTextOverride.
|
||||||
|
uint32_t GameTextIdByKey(const char* key);
|
||||||
|
|
||||||
// A game wide string ({begin, end, capacity end}, e.g. a tooltip's text) as
|
// A game wide string ({begin, end, capacity end}, e.g. a tooltip's text) as
|
||||||
// text, and replaced by `value` (grown with the game's allocator).
|
// text, and replaced by `value` (grown with the game's allocator).
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// The game renders at a fixed resolution (1024x768). Unmodified, windowed mode
|
// The game renders at a fixed resolution (1024x768). Unmodified, windowed mode
|
||||||
// shows that as a 1024x768 popup centred on the desktop; fullscreen 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
|
// 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).
|
// centred rectangle with the game's aspect ratio (black bars elsewhere).
|
||||||
//
|
//
|
||||||
// Mouse positions are mapped back from that rectangle to game pixels, both in
|
// Mouse positions are mapped back from that rectangle to game pixels, both in
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "game/render/dx_driver7.h"
|
#include "game/render/dx_driver7.h"
|
||||||
#include "mods/hud.h"
|
#include "mods/hud.h"
|
||||||
#include "mods/input.h"
|
#include "mods/input.h"
|
||||||
|
#include "mods/monitor.h"
|
||||||
#include "mods/widescreen.h"
|
#include "mods/widescreen.h"
|
||||||
#include "re/config.h"
|
#include "re/config.h"
|
||||||
#include "game/addresses.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) {
|
void ApplyBorderless(game::DxDriver7* driver) {
|
||||||
HWND window = driver->window;
|
HWND window = driver->window;
|
||||||
MONITORINFO monitor = {sizeof(monitor)};
|
const RECT area = mods::monitor::GameMonitorRect();
|
||||||
GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST), &monitor);
|
|
||||||
const RECT& area = monitor.rcMonitor;
|
|
||||||
const int areaWidth = area.right - area.left;
|
const int areaWidth = area.right - area.left;
|
||||||
const int areaHeight = area.bottom - area.top;
|
const int areaHeight = area.bottom - area.top;
|
||||||
|
|
||||||
@@ -171,7 +170,7 @@ int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* device
|
|||||||
}
|
}
|
||||||
DxDisplayMode windowed = *mode;
|
DxDisplayMode windowed = *mode;
|
||||||
windowed.fullscreen = 0;
|
windowed.fullscreen = 0;
|
||||||
mods::widescreen::ApplyRenderSize(windowed, self->window);
|
mods::widescreen::ApplyRenderSize(windowed);
|
||||||
g_viewport.active = false;
|
g_viewport.active = false;
|
||||||
const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown);
|
const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -41,6 +41,7 @@
|
|||||||
#include "mods/hud.h"
|
#include "mods/hud.h"
|
||||||
#include "mods/input.h"
|
#include "mods/input.h"
|
||||||
#include "mods/lootfilter.h"
|
#include "mods/lootfilter.h"
|
||||||
|
#include "mods/monitor.h"
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
#include "re/log.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 ---------------------------------------------------------------------
|
// --- hooks ---------------------------------------------------------------------
|
||||||
|
|
||||||
struct RenderCall {
|
struct RenderCall {
|
||||||
@@ -706,6 +861,7 @@ int RenderBody(void* context) {
|
|||||||
LoadPending();
|
LoadPending();
|
||||||
LoadLootPending();
|
LoadLootPending();
|
||||||
g_autosavePending = mods::characters::AutosaveMinutes();
|
g_autosavePending = mods::characters::AutosaveMinutes();
|
||||||
|
LoadScreenPending();
|
||||||
}
|
}
|
||||||
w->wasVisible = visible;
|
w->wasVisible = visible;
|
||||||
const uint16_t page = game::ActiveTab(w->sheet);
|
const uint16_t page = game::ActiveTab(w->sheet);
|
||||||
@@ -720,6 +876,8 @@ int RenderBody(void* context) {
|
|||||||
}
|
}
|
||||||
if (!visible || page != kPageCommon) {
|
if (!visible || page != kPageCommon) {
|
||||||
g_savesHover = -1;
|
g_savesHover = -1;
|
||||||
|
g_screenOpen = false;
|
||||||
|
g_screenHover = -1;
|
||||||
}
|
}
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return result;
|
return result;
|
||||||
@@ -731,6 +889,7 @@ int RenderBody(void* context) {
|
|||||||
DrawLootPage(*w, call->device);
|
DrawLootPage(*w, call->device);
|
||||||
} else if (page == kPageCommon && g_pendingLoaded) {
|
} else if (page == kPageCommon && g_pendingLoaded) {
|
||||||
DrawSaves(*w, call->device);
|
DrawSaves(*w, call->device);
|
||||||
|
DrawScreen(*w, call->device);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -773,7 +932,7 @@ int EventBody(void* context) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageCommon &&
|
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;
|
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 &&
|
||||||
@@ -824,11 +983,16 @@ void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, Ui
|
|||||||
RE_HOOK(0x00716570, UiOptions_Construct);
|
RE_HOOK(0x00716570, UiOptions_Construct);
|
||||||
void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, UiRect rect, void* parent,
|
void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, UiRect rect, void* parent,
|
||||||
uint32_t flags) {
|
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);
|
void* window = UiOptions_Construct_original(self, edx, name, rect, parent, flags);
|
||||||
if (window) {
|
if (window) {
|
||||||
g_windows.push_back({static_cast<uint8_t*>(window)});
|
g_windows.push_back({static_cast<uint8_t*>(window)});
|
||||||
Window& w = g_windows.back();
|
Window& w = g_windows.back();
|
||||||
Classify(w);
|
Classify(w);
|
||||||
|
CreateScreenControls(w);
|
||||||
CreateKeyControls(w);
|
CreateKeyControls(w);
|
||||||
CreateLootControls(w);
|
CreateLootControls(w);
|
||||||
CreateSavesControls(w);
|
CreateSavesControls(w);
|
||||||
@@ -873,6 +1037,7 @@ void __fastcall UiOptions_Apply(void* self, void* edx) {
|
|||||||
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);
|
mods::characters::SetAutosaveMinutes(g_autosavePending);
|
||||||
|
mods::monitor::SetPreferredId(g_screenPending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "mods/hud.h"
|
#include "mods/hud.h"
|
||||||
#include "mods/loadingscreen.h"
|
#include "mods/loadingscreen.h"
|
||||||
#include "mods/megamap.h"
|
#include "mods/megamap.h"
|
||||||
|
#include "mods/monitor.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -270,17 +271,16 @@ void PatchLetterbox(int width) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
|
void ApplyRenderSize(game::DxDisplayMode& mode) {
|
||||||
if (!Enabled()) {
|
if (!Enabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MONITORINFO monitor = {sizeof(monitor)};
|
const RECT monitor = mods::monitor::GameMonitorRect();
|
||||||
GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY), &monitor);
|
|
||||||
// Default: keep the original 768 pixels of world height (so everything
|
// 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
|
// 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.
|
// 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 monitorWidth = monitor.right - monitor.left;
|
||||||
const int monitorHeight = monitor.rcMonitor.bottom - monitor.rcMonitor.top;
|
const int monitorHeight = monitor.bottom - monitor.top;
|
||||||
int height = re::ConfigInt("widescreen", "Height", static_cast<int>(kOriginalHeight));
|
int height = re::ConfigInt("widescreen", "Height", static_cast<int>(kOriginalHeight));
|
||||||
if (height <= 0) {
|
if (height <= 0) {
|
||||||
height = monitorHeight;
|
height = monitorHeight;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace mods::widescreen {
|
|||||||
// Called by the borderless mod before the renderer creates its surfaces:
|
// Called by the borderless mod before the renderer creates its surfaces:
|
||||||
// sets the render size to the monitor (or the configured size) and resizes
|
// sets the render size to the monitor (or the configured size) and resizes
|
||||||
// the world view to match. No-op when disabled.
|
// 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.
|
// True while the current thread draws the load / save progress screen.
|
||||||
bool IsLoadingScreen();
|
bool IsLoadingScreen();
|
||||||
|
|||||||
Reference in new issue
Block a user