110 lines
5.9 KiB
C++
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().Detail("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
|