DS2Revival/mods/common/exclusive_cursor.h

110 lines
5.9 KiB
C++

// The mouse pointer in the game's own exclusive fullscreen ([Display] ExclusiveCursor), shared by DS2Widescreen (which
// applies it) and DS2BrokenWorld (which must not undo it).
//
// In exclusive fullscreen the game draws its pointer with the Direct3D device cursor, which Windows shows only while the
// Win32 cursor display count is not negative. Two places in the exe push that count below zero and nothing ever raises it
// again, so on Windows 10 1703+ there is no pointer at all:
// 1. the window procedure's WM_ACTIVATE handler (Steam sub_5edfd3 at 0x5eec05): on activation it runs
// "while (ShowCursor(FALSE) >= 0)" (on deactivation "while (ShowCursor(TRUE) < 0)"), then ShowCursor(TRUE) in windowed mode only;
// 2. the start of the main loop (sub_5f2551 at 0x5f26b3): "if (window && fullscreen) ShowCursor(FALSE)".
// The community's patched executables skip both with a forced short jump; this does the same in memory, after checking the
// code around each site (Steam v2.3 0x5eec05 / 0x5f26b3, Broken World 0x613adb / 0x617509; found by signature, checked
// against the import slot of ShowCursor and the jump target). Without the two ShowCursor calls the count stays at its
// default (0, pointer shown) in both windowed and fullscreen, so applying them is harmless if the game turns out to run
// windowed; they are still only applied where the game can be in exclusive fullscreen:
// - the command line says fullscreen=true (any [Display] Mode), or
// - [Display] Mode=off and the command line does not say fullscreen=false (the game's own config decides).
// [Display] Mode=borderless / window pass fullscreen=false themselves, so they are never touched.
// Some Broken World downloads already carry the same two edits made by hand; they are recognised ("already skipped").
#pragma once
#include "patch.h"
namespace ds2::excursor {
// The value after "key" (e.g. "fullscreen=") in a command line, as a pointer into it; null if absent.
inline const char* ArgValue(const char* cmd, const char* key) {
size_t n = std::strlen(key);
for (const char* p = cmd; *p; ++p)
if ((p == cmd || p[-1] == ' ' || p[-1] == '"') && _strnicmp(p, key, n) == 0) return p + n;
return nullptr;
}
// Does the game run (or may it run) in its own exclusive fullscreen? mode = the [Display] Mode text.
inline bool ExclusiveMode(const char* mode, const char* cmd) {
const char* fs = ArgValue(cmd, "fullscreen=");
if (fs) { // the command line wins over the config; anything but false / 0 / no turns fullscreen on
bool off = !_strnicmp(fs, "false", 5) || !_strnicmp(fs, "no", 2) || *fs == '0';
return !off;
}
return _stricmp(mode, "off") == 0;
}
// Does the exclusive-fullscreen pointer fix apply in this run? (wanted = [Display] ExclusiveCursor)
inline bool Applies(bool wanted, const char* mode, const char* cmd) { return wanted && ExclusiveMode(mode, cmd); }
namespace detail {
struct Site {
const char* what;
const char* pattern;
int jcc; // offset of the conditional jump inside the pattern
int slot; // offset of the ShowCursor IAT slot operand inside the pattern
BYTE opcode; // the jump the game has (75 jne / 74 je); the patch is EB (jmp) with the same displacement
int rel; // required displacement of the jump if known (>= 0), else its target is checked to be a long jump
};
// WM_ACTIVATE handler: the jump is "activation while minimised: skip the handler"; the signature ends in the first
// ShowCursor call (the "show" half) of the handler.
constexpr Site kActivate = {"exclusive cursor: the ShowCursor loops of WM_ACTIVATE",
"8B 45 10 C1 E8 10 0F B7 C0 85 C0 ?? ?? 8B 45 10 25 FF FF 00 00 0F B7 C0 85 C0 75 ?? 6A 01 FF 15 ?? ?? ?? ??",
11, 32, 0x75, -1};
// Main loop start: the jump skips "if (window and fullscreen flag) ShowCursor(FALSE)"; the signature ends in that call.
constexpr Site kLoopStart = {"exclusive cursor: ShowCursor(FALSE) at the start of the main loop",
"8A 40 60 88 85 ?? ?? ?? ?? 0F B6 85 ?? ?? ?? ?? 85 C0 ?? 08 6A 00 FF 15 ?? ?? ?? ??", 18, 24, 0x74, 8};
enum class State { Missing, Applied, AlreadyDone, Refused };
inline State ApplySite(const Site& s, DWORD showCursorSlot) {
DWORD at = ds2::FindUnique(s.pattern);
if (!at) {
ds2::log().Write("SKIP %s - signature not found", s.what);
return State::Missing;
}
DWORD jcc = at + s.jcc;
auto b = [](DWORD a) { return *reinterpret_cast<const BYTE*>(a); };
if (*reinterpret_cast<const DWORD*>(at + s.slot) != showCursorSlot) {
ds2::log().Write("SKIP %s @%08lX - the call is not ShowCursor", s.what, jcc);
return State::Refused;
}
BYTE rel = b(jcc + 1);
DWORD dest = jcc + 2 + static_cast<signed char>(rel);
if (s.rel >= 0 ? rel != s.rel : b(dest) != 0xE9) { // the handler's jump target: a long jump to the end of the message switch
ds2::log().Write("SKIP %s @%08lX - unexpected jump displacement %02X", s.what, jcc, rel);
return State::Refused;
}
if (b(jcc) == 0xEB) {
ds2::log().Write("OK %s @%08lX is already skipped (edited exe)", s.what, jcc);
return State::AlreadyDone;
}
const BYTE expect[2] = {s.opcode, rel}, patch[2] = {0xEB, rel};
return ds2::PatchBytes(jcc, expect, patch, 2, s.what) ? State::Applied : State::Refused;
}
} // namespace detail
// Skips the two ShowCursor sites. Returns how many are now in the "skipped" state (0-2); logs every step.
inline int Apply() {
DWORD slot = ds2::ImportSlot("USER32.dll", "ShowCursor");
if (!slot) {
ds2::log().Write("SKIP exclusive cursor: the exe does not import ShowCursor");
return 0;
}
int done = 0;
for (const detail::Site* s : {&detail::kActivate, &detail::kLoopStart}) {
detail::State st = detail::ApplySite(*s, slot);
done += st == detail::State::Applied || st == detail::State::AlreadyDone;
}
return done;
}
} // namespace ds2::excursor