DS2Revival/mods/brokenworld/brokenworld.cpp

536 lines
29 KiB
C++

// DS2BrokenWorld.asi - Dungeon Siege II: Broken World next to the Steam game, without a registry script, and
// Steam-made heroes imported into it. Delete the .asi to turn it off.
//
// The expansion is its own exe (v2.30.0.4277). Layout: <Dungeon Siege 2>/Broken World/DungeonSiege2.exe
// with its Resources/x*.ds2res and Maps/x*.ds2map; the base game's resources come from the parent folder,
// which we hand to it as the (virtual) DS2 install directory - no registry script needed.
// The Steam exe starts it automatically when that folder exists ([BrokenWorld] Launch=auto; hold Shift
// while starting, or set Launch=off, for the original game).
#include "../common/exclusive_cursor.h"
#include "../common/plugin.h"
#include "../common/vreg.h"
#include <intrin.h>
#include "cpuid_sites.h"
namespace {
// --- Install paths --------------------------------------------------------------------------------------
// Broken World finds the base game through AppPath / InstallationDirectory of the DS2 install key ("In order to
// play Dungeon Siege 2: Broken World, you must first install the full version of Dungeon Siege 2"; config var
// ds2_install_path -> %ds2_install_path%/Resources and /Maps) and itself through AppPath of its own key. Steam
// installs have neither; served when the real values are missing (mods/common/vreg.h).
char g_ds2Dir[MAX_PATH]; // the base game
char g_bwDir[MAX_PATH]; // Broken World's own folder
LSTATUS ServeInstallPaths(HKEY key, const char* name, LPDWORD type, LPBYTE data, LPDWORD size) {
if (_stricmp(name, "AppPath") != 0 && _stricmp(name, "InstallationDirectory") != 0) return ERROR_FILE_NOT_FOUND;
const char* dir = key == ds2::vreg::kBwKey ? g_bwDir : g_ds2Dir;
return dir[0] ? ds2::vreg::ServeString(dir, type, data, size) : ERROR_FILE_NOT_FOUND;
}
// Broken World reads a party's party_info.ds2 only if its format version is 0x3d (DS2 retail), 0x3f (its own)
// or the value of a crack-relocated constant at 0x17ffac8 that the crack left at 0 (only reference: the check
// "cmp eax,[0x17ffac8]" at 0x1811c62). The Steam v2.3 base game saves format 0x3e (party writer sub_41724b),
// so every Steam-made party failed to convert or import ("Failed to transfer vaulted character file ...
// corrupted"). Fill the constant with 0x3e.
constexpr DWORD kBwPartyVersionCheck = 0x1811c57;
constexpr BYTE kBwPartyVersionCheckBytes[] = {0x83, 0xF8, 0x3D, 0x89, 0x87, 0x98, 0x00, 0x00, 0x00, 0x74, 0x2D,
0x3B, 0x05, 0xC8, 0xFA, 0x7F, 0x01, 0x74, 0x25, 0x83, 0xF8, 0x3F};
constexpr DWORD kBwPartyVersionSlot = 0x17ffac8;
constexpr DWORD kSteamPartyVersion = 0x3e;
void AcceptSteamParties(const ds2::Ini& ini) {
if (!ini.Bool("BrokenWorld", "AcceptSteamParties", true)) return;
if (std::memcmp(reinterpret_cast<const void*>(kBwPartyVersionCheck), kBwPartyVersionCheckBytes,
sizeof(kBwPartyVersionCheckBytes)) != 0) {
ds2::log().Write("SKIP Broken World: party format check not found (different exe?)");
return;
}
const DWORD zero = 0;
ds2::PatchBytes(kBwPartyVersionSlot, &zero, &kSteamPartyVersion, sizeof(DWORD),
"Broken World: accept party format 0x3e (Steam DS2 characters)");
}
// After the format check, Broken World compares the party's content_crc (CRC of the game data it was saved
// with) against what it knows (check at 0x41495a = jmp [0x17feef8], __thiscall(crc) -> bool, in the protected
// section). Steam v2.3 parties carry the Steam data's CRC 0xD4EE6791, which a 2006 exe cannot know. Wrap the
// check: also accept the known editions (table below) and the CRCs in [BrokenWorld] AcceptContentCrcs (hex, comma
// separated).
// The slot is lazily bound: at start-up it holds a crack resolver stub (0x1871150 in the file) that, on the first
// call, writes the real function's address (0x181d35a) into the slot - over our wrapper - so only the first party
// read was ever wrapped. After every call the wrapper adopts whatever the slot now holds and puts itself back.
constexpr DWORD kBwContentCheckStub = 0x41495a;
constexpr BYTE kBwContentCheckStubBytes[] = {0xFF, 0x25, 0xF8, 0xEE, 0x7F, 0x01};
constexpr DWORD kBwContentCheckSlot = 0x17feef8;
using ContentCheck_t = bool(__fastcall*)(void* self, void* edx, DWORD crc);
ContentCheck_t g_bwContentCheck;
DWORD g_extraCrcs[8];
int g_extraCrcCount;
bool __fastcall BwContentCheck(void* self, void* edx, DWORD crc);
// --- Known editions ----------------------------------------------------------------------------------------
// A party's content_crc is the content id of the game it was saved with: the exe's PE header checksum plus three
// digests of the gas/skrit data (docs/multiplayer/mod-compatibility.md). The first four ids were measured by starting
// that setup in a private copy and reading the checksum and ContentSyncSpec (tools/contentid.py; docs/brokenworld.md,
// "Content ids of known editions"); the last four are constants in the games' own check:
// - D4EE6791 Steam Dungeon Siege 2 v2.3 (exe checksum 0x007DCE33)
// - D4EE4A9F the same data with the "fixed files v3" GOG exe (checksum 0x007DB141; only the checksum differs)
// - 790BFE9F Broken World (every Broken World exe we have has checksum 0xCAA45D2B; disc data = 2018 download data)
// - 75112728 Broken World with the 2018 repack's Resources\Widescreen Fix.ds2res (changes the world/contentdb digests)
// - CA03370A DE60C4EB 1A300219 AB123ABC: built into the exes (Steam: the first three; Broken World: all four). Not
// reproducible here (that needs retail data, which we do not have); AB123ABC is what the disc's premade heroes carry.
// All of them are accepted in single player, multiplayer and the vault (default [BrokenWorld] AcceptKnownEditions=1) and
// named in the log once. Any other id: single player accepts it (AcceptAnySaveSinglePlayer), multiplayer does not.
struct Edition {
DWORD id;
const char* name; // completes "party made with ..."
};
constexpr Edition kEditions[] = {
{0xD4EE6791, "Steam Dungeon Siege 2 v2.3"},
{0xD4EE4A9F, "the GOG exe"},
{0x790BFE9F, "Broken World"},
{0x75112728, "the 2018 repack's Widescreen Fix"},
{0xCA03370A, "an older Dungeon Siege 2 version (built-in id)"},
{0xDE60C4EB, "an older Dungeon Siege 2 version (built-in id)"},
{0x1A300219, "an older Dungeon Siege 2 version (built-in id)"},
{0xAB123ABC, "a Broken World premade hero (built-in id)"},
};
bool g_acceptKnownEditions = true;
DWORD g_loggedCrcs[32];
int g_loggedCrcCount;
const Edition* FindEdition(DWORD crc) {
for (const Edition& e : kEditions)
if (e.id == crc) return &e;
return nullptr;
}
// True the first time a CRC is logged (one line per CRC and run; the game checks every party file more than once).
bool FirstTime(DWORD crc) {
for (int i = 0; i < g_loggedCrcCount; ++i)
if (g_loggedCrcs[i] == crc) return false;
if (g_loggedCrcCount < 32) g_loggedCrcs[g_loggedCrcCount++] = crc;
return true;
}
// The game's own check accepted this CRC: name it in the log if it is a known edition.
void NoteKnown(DWORD crc) {
if (const Edition* e = FindEdition(crc))
if (FirstTime(crc)) ds2::log().Write("party content CRC %08lX: party made with %s", crc, e->name);
}
// The built-in table (when switched on) accepts this CRC.
bool AcceptEdition(DWORD crc) {
if (!g_acceptKnownEditions) return false;
const Edition* e = FindEdition(crc);
if (!e) return false;
if (FirstTime(crc)) ds2::log().Write("party content CRC %08lX accepted: party made with %s", crc, e->name);
return true;
}
// [BrokenWorld] AcceptContentCrcs: the player's own additions.
bool AcceptExtraCrc(DWORD crc) {
for (int i = 0; i < g_extraCrcCount; ++i)
if (crc == g_extraCrcs[i]) {
if (FirstTime(crc)) ds2::log().Write("party content CRC %08lX accepted (AcceptContentCrcs)", crc);
return true;
}
return false;
}
void LoadSettings(const ds2::Ini& ini) {
char list[256];
ini.String("BrokenWorld", "AcceptContentCrcs", "", list, sizeof(list));
char* ctx = nullptr;
for (char* tok = strtok_s(list, ", ", &ctx); tok && g_extraCrcCount < 8; tok = strtok_s(nullptr, ", ", &ctx))
g_extraCrcs[g_extraCrcCount++] = std::strtoul(tok, nullptr, 16);
g_acceptKnownEditions = ini.Bool("BrokenWorld", "AcceptKnownEditions", true);
}
// --- Saves made with other game data: accepted in single player --------------------------------------------
// The content check is a list-visibility gate: a party made on other game data (retail DS2 data instead of the
// Steam v2.3 data, a data mod, another patch level) has another content CRC and silently disappears from the
// hero list. [BrokenWorld] AcceptAnySaveSinglePlayer (default on) accepts any CRC while playing single player
// (each unknown CRC is logged once); multiplayer keeps the known list, so the content check there is unchanged.
// "Single player" is read from the engine at the moment of the check, in this order:
// - the Server object's first byte (IsMultiPlayer(): Server* at the pointer below) is set -> multiplayer;
// - the WorldState (pointer below; +0x1c current, +0x24 pending eWorldState) is one of the ws_mp_* states
// (33 mp_provider_select .. 44 mp_session_lost: provider/internet/LAN/match-maker pages, staging, MP hero
// and map select, MP in-game) -> multiplayer. Pending counts too: the hero's party.ds2 is read while the
// world loads the map, when current is a loading state and pending the target (tools/ds2state.py names).
// - anything else (main menu, ws_sp_*, loading toward them) -> single player.
// The hero list is read before any Server exists, so the Server byte alone cannot tell the multiplayer hero page
// from the single player one; the WorldState does (SP hero page = ws_sp_character_select 23, MP = 39).
struct EngineState {
DWORD worldStatePtr; // address of the WorldState* global
DWORD serverPtr; // address of the Server* global
};
constexpr EngineState kBwEngine = {0xd015d8, 0xd01554};
constexpr EngineState kSteamEngine = {0xbcb21c, 0xbcb290};
constexpr int kFirstMpState = 33, kLastMpState = 44;
bool g_acceptAnySave;
EngineState g_engine;
bool InSinglePlayer() {
bool single = true;
__try {
const BYTE* server = *reinterpret_cast<const BYTE* const*>(g_engine.serverPtr);
if (server && *server) single = false;
const BYTE* ws = *reinterpret_cast<const BYTE* const*>(g_engine.worldStatePtr);
if (ws) {
int cur = *reinterpret_cast<const int*>(ws + 0x1c), pend = *reinterpret_cast<const int*>(ws + 0x24);
if ((cur >= kFirstMpState && cur <= kLastMpState) || (pend >= kFirstMpState && pend <= kLastMpState))
single = false;
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
single = false; // cannot tell: keep the strict check
}
return single;
}
// True when an otherwise unknown content CRC is let through (single player and the setting on).
bool AcceptUnknownCrc(DWORD crc) {
if (!g_acceptAnySave || !InSinglePlayer()) return false;
if (FirstTime(crc)) ds2::log().Write("party content CRC %08lX: unknown game data, accepted in single player", crc);
return true;
}
void RehookContentCheck() {
auto slot = reinterpret_cast<DWORD*>(kBwContentCheckSlot);
DWORD mine = reinterpret_cast<DWORD>(BwContentCheck), now = *slot;
if (now == mine) return;
g_bwContentCheck = reinterpret_cast<ContentCheck_t>(now);
DWORD old;
if (!VirtualProtect(slot, 4, PAGE_EXECUTE_READWRITE, &old)) return;
InterlockedCompareExchange(reinterpret_cast<volatile LONG*>(slot), static_cast<LONG>(mine), static_cast<LONG>(now));
VirtualProtect(slot, 4, old, &old);
}
bool __fastcall BwContentCheck(void* self, void* edx, DWORD crc) {
bool known = g_bwContentCheck(self, edx, crc);
RehookContentCheck();
if (known) {
NoteKnown(crc);
return true;
}
if (AcceptEdition(crc) || AcceptExtraCrc(crc) || AcceptUnknownCrc(crc)) return true;
ds2::log().Write("Broken World: party content CRC %08lX not accepted", crc);
return false;
}
void AcceptSteamContentCrc(const ds2::Ini& ini) {
LoadSettings(ini);
g_acceptAnySave = ini.Bool("BrokenWorld", "AcceptAnySaveSinglePlayer", true);
g_engine = kBwEngine;
if (std::memcmp(reinterpret_cast<const void*>(kBwContentCheckStub), kBwContentCheckStubBytes,
sizeof(kBwContentCheckStubBytes)) != 0) {
ds2::log().Write("SKIP Broken World: content CRC check stub not found (different exe?)");
return;
}
DWORD orig = *reinterpret_cast<const DWORD*>(kBwContentCheckSlot);
g_bwContentCheck = reinterpret_cast<ContentCheck_t>(orig);
ds2::PatchPointer(kBwContentCheckSlot, orig, reinterpret_cast<void*>(BwContentCheck),
"Broken World: accept the known editions' content ids for imported parties");
}
// The Steam exe hides foreign-data parties the same way: its content check (0x4139d0, __stdcall(crc) -> bool,
// the content id or three constants at 0xbbeb58) gates the party_info.ds2 and party.ds2 readers (0x41c513,
// 0x41ee0e). Only matters when the Steam game itself runs ([BrokenWorld] Launch=off, Shift, no Broken World
// folder). Same setting, same single player test.
constexpr DWORD kSteamContentCheck = 0x4139d0;
constexpr BYTE kSteamContentCheckBytes[] = {0x55, 0x8B, 0xEC, 0x8B, 0x0D, 0x4C, 0xB2, 0xBC, 0x00};
using SteamContentCheck_t = bool(WINAPI*)(DWORD crc); // the game returns a bool in AL only
SteamContentCheck_t g_steamContentCheck;
bool WINAPI SteamContentCheck(DWORD crc) {
if (g_steamContentCheck(crc)) {
NoteKnown(crc);
return true;
}
return AcceptEdition(crc) || AcceptExtraCrc(crc) || AcceptUnknownCrc(crc);
}
void AcceptAnySaveSteam(const ds2::Ini& ini) {
LoadSettings(ini);
g_acceptAnySave = ini.Bool("BrokenWorld", "AcceptAnySaveSinglePlayer", true);
g_engine = kSteamEngine;
if (!g_acceptKnownEditions && !g_extraCrcCount && !g_acceptAnySave) return;
g_steamContentCheck = static_cast<SteamContentCheck_t>(
ds2::Detour(kSteamContentCheck, kSteamContentCheckBytes, sizeof(kSteamContentCheckBytes),
reinterpret_cast<void*>(SteamContentCheck), "Steam: accept parties made with the known editions' data (single player: any data)"));
}
using LaunchGetCommandLineA_t = LPSTR(WINAPI*)();
LaunchGetCommandLineA_t g_launchGetCommandLineA;
char g_bwExe[MAX_PATH];
// One plugin set for both games: when Broken World\scripts holds DS2Revival.asi (it loads the main scripts folder's plugins into
// Broken World), full copies of the plugins from an older install there would run instead of the main folder's (an older version
// next to newer ones). The Steam game renames them to .asi.old just before it starts Broken World, when nothing of Broken World is
// loaded yet (a direct start of the Broken World exe only logs them, see mods/revival_loader). The old .ini and .log files stay.
void RetireOldBrokenWorldCopies(const char* bwDir) {
char loader[MAX_PATH], main[MAX_PATH];
sprintf_s(loader, "%s\\scripts\\DS2Revival.asi", bwDir);
if (!ds2::FileExists(loader)) return;
ds2::ExeDir(main, sizeof(main));
static const char* const kNames[] = {"DS2Multiplayer", "DS2Widescreen", "DS2Graphics", "DS2Fixes", "DS2BrokenWorld", "DS2ModLoader", "DS2Controller"};
static const char* const kFolders[] = {"", "\\scripts", "\\plugins", "\\update"};
for (const char* name : kNames) {
char twin[MAX_PATH];
sprintf_s(twin, "%s\\scripts\\%s.asi", main, name);
if (!std::strcmp(name, "DS2Controller") && !ds2::FileExists(twin)) continue; // an opt-in plugin: only when the main game has it too
for (const char* sub : kFolders) {
char from[MAX_PATH], to[MAX_PATH];
sprintf_s(from, "%s%s\\%s.asi", bwDir, sub, name);
if (!ds2::FileExists(from)) continue;
sprintf_s(to, "%s.old", from);
if (MoveFileExA(from, to, MOVEFILE_REPLACE_EXISTING))
ds2::log().Write("Broken World: old plugin copy %s renamed to %s.asi.old (Broken World runs the main game's plugins)", from, name);
else
ds2::log().Write("Broken World: could not rename the old plugin copy %s (error %lu): delete it", from, GetLastError());
}
}
}
// The 2018 "Broken World Expansion" download ships Resources\Widescreen Fix.ds2res (a world/contentdb template override and a
// field-of-view skrit). DS2Widescreen does the same job, and the archive changes Broken World's content id (0x790BFE9F plain,
// 0x75112728 with it), so players with and without it cannot join each other. Switched off (renamed to .off) before Broken World
// starts, the same moment as the old plugin copies; only that exact file (its size), and [BrokenWorld] RetireWidescreenFix=0 keeps it.
bool g_retireWidescreenFix = true;
void RetireRepackWidescreenFix(const char* bwDir) {
if (!g_retireWidescreenFix) return;
char from[MAX_PATH], to[MAX_PATH];
sprintf_s(from, "%s\\Resources\\Widescreen Fix.ds2res", bwDir);
WIN32_FILE_ATTRIBUTE_DATA fa{};
if (!GetFileAttributesExA(from, GetFileExInfoStandard, &fa) || fa.nFileSizeHigh != 0 || fa.nFileSizeLow != 6252) return;
sprintf_s(to, "%s.off", from);
if (MoveFileExA(from, to, MOVEFILE_REPLACE_EXISTING))
ds2::log().Write("Broken World: the repack's Widescreen Fix.ds2res renamed to .off (DS2Widescreen sets the field of view; the archive changes the content id, so other players could not join)");
else
ds2::log().Write("Broken World: could not rename %s (error %lu)", from, GetLastError());
}
// First GetCommandLineA call = CRT start-up of the Steam exe (loader lock released, no game code run yet).
LPSTR WINAPI LaunchBrokenWorld() {
LPSTR cmd = g_launchGetCommandLineA();
static bool done = false;
if (done) return cmd;
done = true;
const char* args = cmd; // the arguments without the exe path
if (*args == '"') {
args = std::strchr(args + 1, '"');
args = args ? args + 1 : "";
} else {
while (*args && *args != ' ') ++args;
}
char line[4096], dir[MAX_PATH];
sprintf_s(line, "%c%s%c%s", 34, g_bwExe, 34, args);
strcpy_s(dir, g_bwExe);
if (char* s = std::strrchr(dir, 92)) *s = 0;
RetireOldBrokenWorldCopies(dir);
RetireRepackWidescreenFix(dir);
STARTUPINFOA si{sizeof(si)};
PROCESS_INFORMATION pi{};
if (CreateProcessA(g_bwExe, line, nullptr, nullptr, FALSE, 0, nullptr, dir, &si, &pi)) {
ds2::log().Write("started Broken World (pid %lu): %s", pi.dwProcessId, line);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
ExitProcess(0);
}
ds2::log().Write("could not start Broken World (%lu); running the original game", GetLastError());
return cmd;
}
void SetupBrokenWorldLauncher(const ds2::Ini& ini) {
char mode[16], dir[MAX_PATH];
ini.String("BrokenWorld", "Launch", "auto", mode, sizeof(mode));
g_retireWidescreenFix = ini.Bool("BrokenWorld", "RetireWidescreenFix", true);
if (_stricmp(mode, "off") == 0) return;
ds2::ExeDir(dir, sizeof(dir));
sprintf_s(g_bwExe, "%s%cBroken World%cDungeonSiege2.exe", dir, 92, 92);
if (!ds2::FileExists(g_bwExe)) return;
if (GetAsyncKeyState(VK_SHIFT) < 0) {
ds2::log().Write("Broken World found, Shift held: starting the original game");
return;
}
g_launchGetCommandLineA = static_cast<LaunchGetCommandLineA_t>(ds2::HookImport(
"KERNEL32.dll", "GetCommandLineA", reinterpret_cast<void*>(LaunchBrokenWorld), "GetCommandLineA (Broken World launcher)"));
}
// --- Frame rate: CPUID under a hypervisor -------------------------------------------------------------------
// Broken World's crack kept SecuROM's code virtual machine in its rebuilt .rld section, and the game calls
// protected functions every frame (world update). Each VM dispatcher executes CPUID leaf 1 as filler: the result
// is thrown away ("mov eax,1; push ebx; cpuid; mov eax,0xf41; pop ebx; mov cl,al", junk interleaved). On bare
// metal CPUID costs ~100 cycles, but whenever Windows runs its hypervisor (Memory Integrity / VBS, Hyper-V, WSL2,
// Docker, Virtual Machine Platform, Windows Sandbox...) every CPUID traps to it: measured ~400 ns each, and 76% of
// the main thread's samples sat right after one (in the jail, ~45 fps instead of 100+). That is why turning off
// Memory Integrity helps some players and not others (anything else may keep the hypervisor running).
// Each dispatcher CPUID (tools/bw_cpuid_sites.py -> cpuid_sites.h; CRC-checked) jumps to a stub that returns the
// leaf-1 values read once at start-up - exactly what the hardware returns - and runs a real CPUID for any other
// leaf. Flags are preserved; the instructions displaced by the 5-byte jump run in the stub.
DWORD Crc32(const BYTE* p, size_t n) {
DWORD c = 0xFFFFFFFF;
for (size_t i = 0; i < n; ++i) {
c ^= p[i];
for (int k = 0; k < 8; ++k) c = (c >> 1) ^ (0xEDB88320 & (0 - (c & 1)));
}
return ~c;
}
void FastCpuid(const ds2::Ini& ini) {
if (!ini.Bool("BrokenWorld", "FastCpuid", true)) return;
int leaf1[4];
__cpuid(leaf1, 1);
constexpr size_t kStub = 64, kSites = sizeof(kCpuidSites) / sizeof(kCpuidSites[0]);
auto pool = static_cast<BYTE*>(VirtualAlloc(nullptr, kSites * kStub, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
if (!pool) return;
int patched = 0, skipped = 0;
for (size_t i = 0; i < kSites; ++i) {
const CpuidSite& site = kCpuidSites[i];
auto at = reinterpret_cast<BYTE*>(site.va);
if (site.span > 16 || Crc32(at, site.span) != site.crc) {
++skipped;
continue;
}
BYTE* stub = pool + i * kStub;
BYTE* p = stub;
auto put32 = [&p](DWORD v) { std::memcpy(p, &v, 4); p += 4; };
*p++ = 0x9C; // pushfd
*p++ = 0x83, *p++ = 0xF8, *p++ = 0x01; // cmp eax, 1
*p++ = 0x75; // jne other_leaf
BYTE* toOther = p++;
*p++ = 0x9D; // popfd
*p++ = 0xB8, put32(static_cast<DWORD>(leaf1[0])); // mov eax, leaf1.eax
*p++ = 0xBB, put32(static_cast<DWORD>(leaf1[1])); // mov ebx, leaf1.ebx
*p++ = 0xB9, put32(static_cast<DWORD>(leaf1[2])); // mov ecx, leaf1.ecx
*p++ = 0xBA, put32(static_cast<DWORD>(leaf1[3])); // mov edx, leaf1.edx
*p++ = 0xEB; // jmp displaced
BYTE* toDisplaced = p++;
*toOther = static_cast<BYTE>(p - (toOther + 1));
*p++ = 0x9D; // other_leaf: popfd
*p++ = 0x0F, *p++ = 0xA2; // cpuid
*toDisplaced = static_cast<BYTE>(p - (toDisplaced + 1));
std::memcpy(p, at + 2, site.copy); // displaced: the instructions the jump overwrites
p += site.copy;
DWORD next = site.next ? site.next : site.va + 2 + site.copy;
*p++ = 0xE9, put32(next - (reinterpret_cast<DWORD>(p) + 4));
BYTE patch[16];
patch[0] = 0xE9;
DWORD rel = reinterpret_cast<DWORD>(stub) - (site.va + 5);
std::memcpy(patch + 1, &rel, 4);
std::memset(patch + 5, 0x90, site.span - 5);
DWORD old;
if (!VirtualProtect(at, site.span, PAGE_EXECUTE_READWRITE, &old)) {
++skipped;
continue;
}
std::memcpy(at, patch, site.span);
VirtualProtect(at, site.span, old, &old);
++patched;
}
FlushInstructionCache(GetCurrentProcess(), nullptr, 0);
ds2::log().Write("%s Broken World: %d SecuROM-VM CPUIDs answered from a cache (%d not matched)",
patched ? "OK " : "SKIP", patched, skipped);
}
// --- Camera rotation: undo repackers' mouse hacks ------------------------------------------------------------
// Holding the middle button rotates the camera with the mouse "fixed": the game hides the cursor, and every frame
// the helper sub_614a72 puts it back at the window centre (SetCursorPos at 0x614b5a) while the mouse handler
// (0x612618..) turns the distance from the centre into rotation. Some Broken World downloads carry hand edits on
// top of the crack that break this: the SetCursorPos call NOP'd and the "distance from centre" branch at 0x612622
// forced off (so the distance keeps growing and the camera spins far too fast), and the cursor-hiding calls
// (ShowCursor, 0x613adb and 0x617509) skipped. The unmodified crack and the Steam game do none of this, and
// DS2Widescreen handles the recentring in a borderless window. Each edit is undone only where the exe holds
// exactly that edit (checked with the instruction bytes around it). The two ShowCursor edits are the exception in the game's own
// exclusive fullscreen: there they are what brings the mouse pointer back (Windows 10+ shows none otherwise), so they stay
// ([Display] ExclusiveCursor, mods/common/exclusive_cursor.h, which DS2Widescreen also applies to an exe that lacks them).
// They are not the camera drag's pointer hiding (the main loop's start and the activation handler).
struct JumpRevert {
DWORD va;
BYTE jcc; // the original conditional jump opcode (74 je / 75 jne), now EB jmp
bool pointerFix; // also one of the two edits [Display] ExclusiveCursor wants (common/exclusive_cursor.h)
};
constexpr JumpRevert kMouseJumps[] = {
{0x612622, 0x74, false}, // mouse handler: use the distance from the centre when the mouse is fixed
{0x613adb, 0x75, true}, // activation: show/hide the cursor
{0x617509, 0x74, true}, // start of the main loop: hide the cursor in fullscreen
};
constexpr DWORD kRecenterCall = 0x614b5a; // "push eax; call [SetCursorPos]; mov al,[ebp-1]; leave"
void RestoreMouse(const ds2::Ini& ini) {
if (!ini.Bool("BrokenWorld", "RestoreMouse", true)) return;
int restored = 0;
// The exclusive-fullscreen pointer needs the two cursor-hiding edits (they are what brings the pointer back there): DS2Widescreen
// skips them in the clean exe, and an exe that already carries them keeps them, when the game can be in exclusive fullscreen.
char mode[32];
ini.String("Display", "Mode", "borderless", mode, sizeof(mode));
const bool keepPointer = ds2::excursor::Applies(ini.Bool("Display", "ExclusiveCursor", true), mode, GetCommandLineA());
for (const JumpRevert& j : kMouseJumps) {
auto at = reinterpret_cast<const BYTE*>(j.va);
if (j.pointerFix && keepPointer) {
ds2::log().Write("Broken World: mouse edit @%08lX kept for the exclusive-fullscreen pointer ([Display] ExclusiveCursor)", j.va);
continue;
}
if (at[-2] == 0x85 && at[-1] == 0xC0 && at[0] == 0xEB) { // test eax,eax; jmp (edited)
const BYTE jmp = 0xEB;
restored += ds2::PatchBytes(j.va, &jmp, &j.jcc, 1, "Broken World: undo a repacker's mouse edit") ? 1 : 0;
}
}
auto call = reinterpret_cast<const BYTE*>(kRecenterCall);
const BYTE nops[6] = {0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
DWORD slot = ds2::ImportSlot("USER32.dll", "SetCursorPos");
if (slot && call[-1] == 0x50 && std::memcmp(call, nops, 6) == 0 && call[6] == 0x8A && call[7] == 0x45 &&
call[8] == 0xFF && call[9] == 0xC9) {
BYTE patch[6] = {0xFF, 0x15}; // call dword ptr [SetCursorPos]
std::memcpy(patch + 2, &slot, 4);
restored += ds2::PatchBytes(kRecenterCall, nops, patch, 6, "Broken World: restore the camera's cursor recentring") ? 1 : 0;
}
ds2::log().Write("Broken World: %d repacker mouse edit(s) undone", restored);
}
void SetupBrokenWorld(const ds2::Ini& ini, const ds2::Plugin& plugin) {
// Base game = the parent folder, or [BrokenWorld] BaseGame.
char base[MAX_PATH];
ini.String("BrokenWorld", "BaseGame", "", base, sizeof(base));
if (!base[0]) strcpy_s(base, plugin.baseDir);
ds2::ExeDir(g_bwDir, sizeof(g_bwDir));
if (ds2::LooksLikeDs2Folder(base)) {
strcpy_s(g_ds2Dir, base);
ds2::log().Write("Broken World: base game in %s", g_ds2Dir);
// binkw32.dll / mss32.dll are delay-loaded by name from the exe's system folder; the expansion's
// copies are identical to the base game's, so let the search find those instead of duplicating.
char sys[MAX_PATH], own[MAX_PATH];
sprintf_s(own, "%s\\system\\binkw32.dll", g_bwDir);
if (ini.Bool("BrokenWorld", "SystemDllSearch", true) && !ds2::FileExists(own)) {
sprintf_s(sys, "%s\\system", g_ds2Dir);
SetDllDirectoryA(sys);
ds2::log().Write("Broken World: DLL search includes %s", sys);
}
} else {
ds2::log().Write("Broken World: no Dungeon Siege 2 (Resources/Logic.ds2res) in %s - set [BrokenWorld] BaseGame", base);
}
ds2::vreg::Install(ServeInstallPaths, true, true, "install paths");
AcceptSteamParties(ini);
AcceptSteamContentCrc(ini);
FastCpuid(ini);
RestoreMouse(ini);
}
} // namespace
BOOL WINAPI DllMain(HMODULE self, DWORD reason, LPVOID) {
if (reason != DLL_PROCESS_ATTACH) return TRUE;
DisableThreadLibraryCalls(self);
ds2::Plugin plugin;
if (!ds2::StartPlugin(self, "DS2BrokenWorld", plugin)) return TRUE;
ds2::Ini ini(plugin.ini);
if (plugin.exe == ds2::Exe::BrokenWorld)
SetupBrokenWorld(ini, plugin);
else {
if (plugin.exe == ds2::Exe::Steam23) AcceptAnySaveSteam(ini);
SetupBrokenWorldLauncher(ini);
}
return TRUE;
}