DS2Revival/mods/multiplayer/multiplayer.cpp
DS2Revival a1e2f8f9b5
Some checks failed
build / build (push) Failing after 1m35s
CI: build on the project's own runner at git.dsii.uk
2026-10-27 22:02:02 -04:00

483 lines
27 KiB
C++

// DS2Multiplayer.asi - multiplayer for Dungeon Siege II (Steam v2.3) and Broken World without GameSpy:
// our server instead of *.gamespy.com, a CD-key identity for Steam copies, the bugs that stop a fresh session
// from hosting, and a relay for hosts behind routers. Delete the .asi to turn it off.
//
// Loaded by the Ultimate ASI Loader. All patches are version-gated and byte-checked
// (see mods/common/patch.h); evidence for each site is in docs/.
#define _CRT_RAND_S
#include <winsock2.h>
#include "../common/plugin.h"
#include "../common/vreg.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "multiplayer.h"
#include <objbase.h>
#include <shellapi.h>
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "ole32.lib")
namespace mp {
bool TestLoopback() {
char flag[32] = {};
return GetEnvironmentVariableA("DS2DEV_DOCS", nullptr, 0) &&
GetEnvironmentVariableA("DS2DEV_LOOPBACK", flag, sizeof(flag)) && flag[0] == '1';
}
// 1 = 127.0.0.1; "127.x.y.z" gives each private instance its own loopback address (two-instance tests).
unsigned long TestLoopbackNet() {
char flag[32] = {};
GetEnvironmentVariableA("DS2DEV_LOOPBACK", flag, sizeof(flag));
unsigned a, b, c, d;
if (!std::strncmp(flag, "127.", 4) && sscanf_s(flag, "%u.%u.%u.%u", &a, &b, &c, &d) == 4 && b < 256 && c < 256 && d < 256)
return htonl((a << 24) | (b << 16) | (c << 8) | d);
return htonl(INADDR_LOOPBACK);
}
bool PrimaryIPv4(in_addr* out) {
if (TestLoopback()) { out->s_addr = TestLoopbackNet(); return true; }
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s == INVALID_SOCKET) return false;
sockaddr_in probe{};
probe.sin_family = AF_INET;
probe.sin_port = htons(9);
probe.sin_addr.s_addr = htonl(0xC0000201); // 192.0.2.1 (TEST-NET-1): only a route lookup, nothing is sent
sockaddr_in local{};
int len = sizeof(local);
bool ok = connect(s, reinterpret_cast<sockaddr*>(&probe), sizeof(probe)) == 0 &&
getsockname(s, reinterpret_cast<sockaddr*>(&local), &len) == 0;
closesocket(s);
if (ok) *out = local.sin_addr;
return ok;
}
} // namespace mp
namespace {
using Bind_t = int(WSAAPI*)(SOCKET, const sockaddr*, int);
Bind_t g_bind;
int WSAAPI TestBind(SOCKET s, const sockaddr* addr, int len) {
if (addr && len >= static_cast<int>(sizeof(sockaddr_in)) && addr->sa_family == AF_INET) {
sockaddr_in local = *reinterpret_cast<const sockaddr_in*>(addr);
local.sin_addr.s_addr = mp::TestLoopbackNet();
return g_bind(s, reinterpret_cast<const sockaddr*>(&local), sizeof(local));
}
return g_bind(s, addr, len);
}
// --- Multiplayer menu button -----------------------------------------------------------
// UIFrontend::TransitionToMain (0x44c2d3) looks up "button_multiplayer" in "main_menu" and
// unconditionally calls UIButton::DisableButton on it (call at 0x44c37a). The v2.3 Steam
// build ships every multiplayer code path; only this call hides it. We retarget the call
// to UIButton::EnableButton. See docs/multiplayer/menu-button.md.
constexpr DWORD kSiteDisableMpButton = 0x44c37a;
constexpr DWORD kUIButton_DisableButton = 0x7663b0;
constexpr DWORD kUIButton_EnableButton = 0x766440;
// --- Multiplayer START guard ------------------------------------------------------------
// The staging-area START handler (sub_4e32c3, UI command "staging_start_game") returns early unless
// WorldMap+0x1c (count of local party members placed on start positions) >= the number of human
// players. Nothing raises that count before START in a fresh session; retail only passed the check
// on a count left over from an earlier world in the same process (docs/multiplayer/start-guard.md).
// Remove the early return: cmp esi,eax / jb +0x67 at 0x4e32df -> cmp esi,eax / nop nop.
constexpr DWORD kSiteStartGuard = 0x4e32df;
constexpr BYTE kStartGuardOrig[4] = {0x3B, 0xF0, 0x72, 0x67};
constexpr BYTE kStartGuardPatch[4] = {0x3B, 0xF0, 0x90, 0x90};
// --- Windows Firewall prompt -------------------------------------------------------------
// UIFrontend::TransitionToMP (0x44aa15) first calls sub_44a84a, which asks Windows Firewall whether the exe
// and dpnsvr.exe are in its exception list and otherwise shows "error_window_firewall_exception_list".
// Its Yes handler (sub_44a77b) tries to add them, which needs administrator rights the game does not have,
// so Yes and No both just continue. The package's "Firewall setup.cmd" adds proper rules instead.
// All of it goes through the firewall helper (sub_67db6a: CoCreateInstance(CLSID_NetFwMgr), 0 = no
// firewall API -> no question). Answering "class not registered" for that CLSID skips the question in every
// build (in Broken World the caller sits in the crack-rebuilt .rld section, so a code patch is not an option).
const GUID kClsidNetFwMgr = {0x304CE942, 0x6E39, 0x40D8, {0x94, 0x3A, 0xB9, 0x13, 0xC4, 0x0C, 0x9C, 0xD4}};
using FwCoCreateInstance_t = HRESULT(__stdcall*)(const GUID&, void*, DWORD, const GUID&, void**);
FwCoCreateInstance_t g_fwCoCreateInstance;
HRESULT __stdcall FirewallCoCreateInstance(const GUID& clsid, void* outer, DWORD ctx, const GUID& iid, void** out) {
if (std::memcmp(&clsid, &kClsidNetFwMgr, sizeof(GUID)) == 0) {
if (out) *out = nullptr;
return REGDB_E_CLASSNOTREG;
}
return g_fwCoCreateInstance(clsid, outer, ctx, iid, out);
}
// --- Direct-connection timeout ------------------------------------------------------------
// A joiner gives up on the direct DirectPlay connection after a hard-coded 10 s (staging update
// sub_4ed7b6: fcomp qword [0xa8fe68] at 0x4ed86c, 10.0 shared with other code) and falls back to
// GameSpy NAT negotiation. The host only unlocks its session once it has loaded into the world
// (sub_423071, ws_sp_ingame/ws_mp_ingame), which takes longer than 10 s on a real world load, so
// point that one comparison at our own constant instead. Found by signature (Steam 0x4ed86c, Broken World
// 0x4fbec7; the pattern starts 23 bytes before the fcomp); the operand must point at the double 10.0.
constexpr char kDirectConnectTimeoutPattern[] =
"00 00 ?? ?? ?? ?? ?? ?? 8B F0 E8 ?? ?? ?? ?? DC A6 50 01 00 00 D9 C0 DC 1D ?? ?? ?? ?? DF E0 DD D8 F6 C4 41 75 6D 68";
constexpr DWORD kDirectConnectTimeoutOffset = 23;
double g_directConnectTimeout = 60.0;
void PatchDirectConnectTimeout(const ds2::Ini& ini) {
g_directConnectTimeout = ini.Int("Multiplayer", "DirectConnectTimeout", 60);
DWORD found = ds2::FindUnique(kDirectConnectTimeoutPattern);
if (!found) {
ds2::log().Write("SKIP staging: direct-connection timeout (signature not found)");
return;
}
DWORD site = found + kDirectConnectTimeoutOffset;
BYTE orig[6];
std::memcpy(orig, reinterpret_cast<const void*>(site), sizeof(orig));
DWORD constant = *reinterpret_cast<const DWORD*>(site + 2);
if (orig[0] != 0xDC || orig[1] != 0x1D || *reinterpret_cast<const double*>(constant) != 10.0) {
ds2::log().Write("SKIP staging: direct-connection timeout (unexpected code at %08lX)", site);
return;
}
BYTE patch[6] = {0xDC, 0x1D};
DWORD addr = reinterpret_cast<DWORD>(&g_directConnectTimeout);
std::memcpy(patch + 2, &addr, 4);
if (ds2::PatchBytes(site, orig, patch, sizeof(patch), "staging: direct-connection timeout"))
ds2::log().Detail(" direct-connection timeout = %.0f s", g_directConnectTimeout);
}
// --- Eight players --------------------------------------------------------------------------
// The stock data (world/global/ui/multiplayer.gas [gamemodes]) defines five modes: 8x1 "Old School" (8 players,
// 1 hero each) and 6x2 next to the default 4x1, 3x2 and 1x4. The engine is built for 8 (Server::GetMaxPlayers
// 0x4f4b26 returns 8, nine Player slots, CGameOptions::SetPlayerLimit 0x404d90 clamps to 2..8, the limit flows
// into DirectPlay's dwMaxPlayers, the GameSpy room and the staging UI's 8 rows). The 8x1 and 6x2 entries carry
// "oldschool = true", and both mode lists - host game (sub_4d4f94, test at 0x4d518d) and the game-list filter
// (sub_4e07f9, test at 0x4e0871) - skip such modes unless the provider is type 3 and the config bool
// "oldschool" is set, so online they never appear. Treat every mode as listable: "je" -> "jmp" after the flag
// test. Only the host needs it. Found by signature (Broken World: 0x4e1e49 / 0x4edf14).
struct ModeGate {
const char* pattern;
DWORD jeOffset;
};
constexpr ModeGate kModeGates[] = {
{"38 5E 14 74 33 8B 0D ?? ?? ?? ?? 8B 01 FF 50 0C 83 F8 03 75", 3}, // host game: game_mode_list
{"80 7B 14 00 74 35 8B 0D ?? ?? ?? ?? 8B 01 FF 50 0C 83 F8 03 75", 4}, // game list: mode filter
};
void ShowAllGameModes(const ds2::Ini& ini) {
if (!ini.Bool("Multiplayer", "EightPlayers", true)) return;
for (const ModeGate& g : kModeGates) {
DWORD found = ds2::FindUnique(g.pattern);
if (!found) {
ds2::log().Write("SKIP game modes: a mode-list gate was not found (signature)");
continue;
}
const BYTE je = 0x74, jmp = 0xEB;
ds2::PatchBytes(found + g.jeOffset, &je, &jmp, 1, "game modes: list the 8-player modes (8x1, 6x2) online");
}
}
// --- Multiplayer server for the lobby --------------------------------------------------------
// IsMultiPlayer() (0x7c29a1) is the first byte of the Server singleton [0xbcb290], set only when the
// server is created (World::InitServer = sub_80dd17(World*, bool mp) -> ctor sub_8256aa). The world
// state handler (sub_423071, vtable slot 0xa905ec) creates an MP server only when a state change
// LEAVES a game into mp_internet_game/mp_lan_game/mp_match_maker (block at 0x423139..0x42331a), so in
// a fresh session (main menu -> provider select -> lobby) the server stays single-player: START then
// loads the world as single player (ws_sp_ingame), the DirectPlay session is never unlocked and
// joiners are refused ("Session is locked"). Same code in v2.2. We create the MP server on the
// provider-select -> lobby transition, exactly as the handler does when returning from an MP game.
// Per build: vtable slot, handler, World::InitServer, World* and Server* singletons. Broken World's were
// located by signature from the Steam code (build/xlate.py; the same bug made its joiners crash on READY,
// reading the uninitialised multiplayer map data through 0xd01608+0x14).
struct LobbyAddrs {
DWORD vtblSlot, handler, initServer, worldPtr, serverPtr;
};
constexpr LobbyAddrs kLobbySteam = {0xa905ec, 0x423071, 0x80dd17, 0xbcb270, 0xbcb290};
constexpr LobbyAddrs kLobbyBrokenWorld = {0xb908f0, 0x42545e, 0x839f2d, 0xd01628, 0xd01554};
LobbyAddrs g_lobby = kLobbySteam;
constexpr int kWsMainMenu = 5, kWsMpProviderSelect = 33, kWsMpInternetGame = 34, kWsMpLanGame = 35, kWsMpMatchMaker = 36;
// --- Leaving the lobby --------------------------------------------------------------------------
// Exit -> Yes in the Internet/LAN lobby goes back to "Select Multiplayer Connection" (state 34 -> 33), but the
// lobby screens (lobby, party selection, chat) are never hidden: the handler only calls the lobby-close routine
// sub_4e2900(1) -> sub_4d9183 (UIMultiplayer::HideLobbyInterface) on the way into staging (0x423f20, 0x424082,
// ...), not on the way out (breakpoints: no call on 34 -> 33 -> 5). They stayed on screen - and clickable -
// behind the connection menu and then the main menu. Do what that routine does on those transitions: hide the
// lobby (found by its export name), then close the party selection (sub_4d58eb: if the lobby object's +0x8c flag (+0xbc in Broken World)
// is set, close the panel and clear it; found by signature, Broken World 0x4e25a7) on the object the export's
// stub loads ("mov ecx,[0xbcb3d0]; jmp ...").
bool g_closeLobby = false, g_lobbyServer = false;
using HideLobby_t = void(__fastcall*)(void*, DWORD);
HideLobby_t g_hideLobby, g_closePartySelect;
void** g_lobbyObject;
constexpr char kClosePartySelectPattern[] =
"56 8B F1 57 8D BE ?? 00 00 00 80 3F 00 74 12 8B 0E E8 ?? ?? ?? ?? 8B 4E 08 E8 ?? ?? ?? ?? C6 07 00 5F 5E C3";
// --- DirectPlay missing ----------------------------------------------------------------------------
// The game's network layer is DirectPlay 8 (dpnet.dll), a Windows feature that is off by default on Windows 10/11
// ("Legacy Components > DirectPlay"). Without it the game exits without a message as soon as a game is hosted or
// joined (test: every DirectPlay 8 class "not registered" -> the process ended on Host Game > OK). On entering
// "Select Multiplayer Connection" check once whether DirectPlay 8 can be created and, if not, say so and offer
// Windows' own feature installer (fondue.exe /enable-feature:DirectPlay). Both calls go through the game's own
// imports, so test harnesses that fake them see the same thing.
bool g_checkDirectPlay = true;
#define kDirectPlayHelpUrl "https://dsii.uk/faq#directplay-install" // error codes and fixes when enabling fails
void CheckDirectPlay() {
static bool checked = false;
if (checked || !g_checkDirectPlay) return;
checked = true;
using Co_t = HRESULT(__stdcall*)(const GUID&, void*, DWORD, const GUID&, void**);
using Msg_t = int(WINAPI*)(HWND, LPCSTR, LPCSTR, UINT);
DWORD coSlot = ds2::ImportSlot("ole32.dll", "CoCreateInstance");
DWORD msgSlot = ds2::ImportSlot("USER32.dll", "MessageBoxA");
if (!coSlot) return;
static const GUID kClsidAddress = {0x934A9523, 0xA3CA, 0x4BC5, {0xAD, 0xA0, 0xD6, 0xD9, 0x5D, 0x97, 0x94, 0x21}};
static const GUID kIidUnknown = {0x00000000, 0x0000, 0x0000, {0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}};
IUnknown* obj = nullptr;
HRESULT hr = (*reinterpret_cast<Co_t*>(coSlot))(kClsidAddress, nullptr, CLSCTX_INPROC_SERVER, kIidUnknown,
reinterpret_cast<void**>(&obj));
if (SUCCEEDED(hr)) {
if (obj) obj->Release();
return;
}
ds2::log().Write("DirectPlay 8 is not available (0x%08lX) - offering the Windows feature installer (help: %s)",
static_cast<unsigned long>(hr), kDirectPlayHelpUrl);
int answer = msgSlot ? (*reinterpret_cast<Msg_t*>(msgSlot))(
GetActiveWindow(),
"Dungeon Siege II plays online and on LAN through DirectPlay, a Windows feature that is not installed on this "
"PC. Without it the game closes when you host or join a game.\n\n"
"Install DirectPlay now? Windows will ask for permission. You can also install it later: Control Panel > "
"Programs > Turn Windows features on or off > Legacy Components > DirectPlay.\n\n"
"If DirectPlay cannot be installed or enabled on this PC, see " kDirectPlayHelpUrl,
"Dungeon Siege II - DirectPlay is missing", MB_YESNO | MB_ICONWARNING | MB_TOPMOST) : IDNO;
if (answer == IDYES) {
HINSTANCE r = ShellExecuteA(nullptr, "open", "fondue.exe", "/enable-feature:DirectPlay", nullptr, SW_SHOWNORMAL);
ds2::log().Write("started the DirectPlay installer (%d)", static_cast<int>(reinterpret_cast<INT_PTR>(r)));
}
}
bool IsLobbyState(int s) { return s == kWsMpInternetGame || s == kWsMpLanGame || s == kWsMpMatchMaker; }
void __fastcall HookWorldStateChanged(void* self, DWORD, int oldState, int newState) {
if (newState == kWsMpProviderSelect && oldState != kWsMpProviderSelect) CheckDirectPlay();
if (g_lobbyServer && oldState == kWsMpProviderSelect &&
(newState == kWsMpInternetGame || newState == kWsMpLanGame || newState == kWsMpMatchMaker)) {
void* world = *reinterpret_cast<void**>(g_lobby.worldPtr);
BYTE* server = *reinterpret_cast<BYTE**>(g_lobby.serverPtr);
if (world && (!server || !*server)) {
reinterpret_cast<void(__fastcall*)(void*, DWORD, int)>(g_lobby.initServer)(world, 0, 1);
ds2::log().Detail("lobby: created multiplayer server (state %d -> %d)", oldState, newState);
}
}
reinterpret_cast<void(__fastcall*)(void*, DWORD, int, int)>(g_lobby.handler)(self, 0, oldState, newState);
if (g_closeLobby && g_hideLobby && IsLobbyState(oldState) && (newState == kWsMpProviderSelect || newState == kWsMainMenu)) {
g_hideLobby(nullptr, 0);
if (g_closePartySelect && g_lobbyObject && *g_lobbyObject) g_closePartySelect(*g_lobbyObject, 0);
ds2::log().Detail("lobby: closed the lobby screens (state %d -> %d)", oldState, newState);
}
}
void InstallLobbyServer(const ds2::Ini& ini, ds2::Exe exe) {
g_lobbyServer = ini.Bool("Multiplayer", "LobbyServer", true);
g_closeLobby = ini.Bool("Multiplayer", "CloseLobbyOnExit", true);
g_checkDirectPlay = ini.Bool("Multiplayer", "CheckDirectPlay", true);
if (g_closeLobby) {
g_hideLobby = reinterpret_cast<HideLobby_t>(
GetProcAddress(GetModuleHandleA(nullptr), "?HideLobbyInterface@UIMultiplayer@@AAEXXZ"));
if (!g_hideLobby) ds2::log().Write("SKIP lobby: UIMultiplayer::HideLobbyInterface not exported");
auto stub = reinterpret_cast<const BYTE*>(g_hideLobby);
if (stub && stub[0] == 0x8B && stub[1] == 0x0D) // mov ecx, [lobby object]
g_lobbyObject = *reinterpret_cast<void** const*>(stub + 2);
g_closePartySelect = reinterpret_cast<HideLobby_t>(ds2::FindUnique(kClosePartySelectPattern));
if (!g_closePartySelect || !g_lobbyObject)
ds2::log().Write("SKIP lobby: party-selection close routine not found (the panel may stay on screen)");
}
if (!g_lobbyServer && !g_closeLobby && !g_checkDirectPlay) return;
g_lobby = exe == ds2::Exe::BrokenWorld ? kLobbyBrokenWorld : kLobbySteam;
ds2::PatchPointer(g_lobby.vtblSlot, g_lobby.handler, reinterpret_cast<void*>(HookWorldStateChanged),
"world state handler: lobby server and lobby exit");
}
// --- GameSpy hostname redirect ---------------------------------------------------------
// Every GameSpy SDK component resolves "<x>.gamespy.com" through WSOCK32 gethostbyname (ordinal 52,
// IAT slot 0xa8e650): <game>.available, <game>.master / <game>.ms<N>, peerchat, gpcm/gpsp, ...
// GameSpy is gone; those lookups resolve to [Network] Server instead (empty = no redirect).
using gethostbyname_t = void*(WINAPI*)(const char*);
gethostbyname_t g_gethostbyname;
constexpr char kDefaultServer[] = "use1.dsii.uk"; // the public revival server
char g_server[256];
bool IsGameSpyHost(const char* name) {
static const char kSuffix[] = ".gamespy.com";
size_t n = name ? std::strlen(name) : 0, k = sizeof(kSuffix) - 1;
return n > k && _stricmp(name + n - k, kSuffix) == 0;
}
// --- Single LAN address -------------------------------------------------------------------
// LAN mode runs a peer-to-peer chat network: every instance hosts a small chat server and nicknames are
// verified across the LAN ("NICKVRFY" / "Nickname failed"). The game resolves this computer's own name
// and uses every address it gets; on PCs with extra adapters (Hyper-V, WSL, Docker, VPNs) an instance
// meets its own server again through another adapter and rejects its own nickname ("'X' is already
// being used by another player"; the 2006 GPG FAQ blames "multiple LAN network connections"). When
// the game resolves its own host name we answer with the primary IPv4 only (the default-route adapter).
bool g_singleLanAddress = true;
bool IsOwnHostName(const char* name) {
char own[256];
return name && gethostname(own, sizeof(own)) == 0 && _stricmp(name, own) == 0;
}
void* PrimaryAddressOnly(void* hostentPtr) {
auto* he = static_cast<hostent*>(hostentPtr);
if (mp::TestLoopback() && he && he->h_addrtype == AF_INET && he->h_addr_list && he->h_addr_list[0]) {
in_addr local; local.s_addr = mp::TestLoopbackNet();
std::memcpy(he->h_addr_list[0], &local, 4); he->h_addr_list[1] = nullptr;
return hostentPtr;
}
if (!he || he->h_addrtype != AF_INET || !he->h_addr_list || !he->h_addr_list[0] || !he->h_addr_list[1])
return hostentPtr;
in_addr primary;
if (!mp::PrimaryIPv4(&primary)) return hostentPtr;
for (char** a = he->h_addr_list; *a; ++a) {
if (std::memcmp(*a, &primary, 4) == 0) {
std::memcpy(he->h_addr_list[0], *a, 4); // primary first, and the only one
he->h_addr_list[1] = nullptr;
return hostentPtr;
}
}
return hostentPtr;
}
void* WINAPI HookGethostbyname(const char* name) {
if (g_server[0] && IsGameSpyHost(name)) {
ds2::log().Detail("redirect %s -> %s", name, g_server);
return g_gethostbyname(g_server);
}
void* r = g_gethostbyname(name);
if (g_singleLanAddress && IsOwnHostName(name)) r = PrimaryAddressOnly(r);
return r;
}
// --- Virtual product registry key (CD-key hash) -----------------------------------------
// Online play needs the CD-key hash the retail installer stored as REG_BINARY "DoubleHash" (8 bytes)
// under HKLM\Software\Microsoft\Microsoft Games\DungeonSiege2 (reader sub_4e5290, formatted "%.19I64d";
// check in sub_4d0f62 -> "Could not find a valid CDKey"). Steam installs have no such key. When the real
// key is missing we serve DoubleHash/PID/DistroId ourselves (mods/common/vreg.h); a real key always wins.
// Broken World reads its online CD key from its own install key (PID, DistroId), served the same way.
unsigned long long g_cdKeyHash;
char g_pid[64];
DWORD g_distroId;
LSTATUS ServeCdKey(HKEY key, const char* name, LPDWORD type, LPBYTE data, LPDWORD size) {
using ds2::vreg::Serve;
if (key == ds2::vreg::kBwKey) {
if (_stricmp(name, "PID") == 0) {
// Broken World's PID doubles as its online CD key (sent as CDKEY to chat), format XXXX-XXXX-XXXX-XXXX:
// derived from this user's identity so every player is distinct.
char bwPid[24];
sprintf_s(bwPid, "%04X-%04X-%04X-%04X", static_cast<unsigned>(g_cdKeyHash >> 48) & 0xFFFF,
static_cast<unsigned>(g_cdKeyHash >> 32) & 0xFFFF, static_cast<unsigned>(g_cdKeyHash >> 16) & 0xFFFF,
static_cast<unsigned>(g_cdKeyHash) & 0xFFFF);
return ds2::vreg::ServeString(bwPid, type, data, size);
}
if (_stricmp(name, "DistroId") == 0) {
static const DWORD kBwDistroId = 0x47C; // what the Broken World installer wrote
return Serve(REG_DWORD, &kBwDistroId, sizeof(kBwDistroId), type, data, size);
}
return ERROR_FILE_NOT_FOUND;
}
if (_stricmp(name, "DoubleHash") == 0) return Serve(REG_BINARY, &g_cdKeyHash, sizeof(g_cdKeyHash), type, data, size);
if (_stricmp(name, "PID") == 0) return ds2::vreg::ServeString(g_pid, type, data, size);
if (_stricmp(name, "DistroId") == 0) return Serve(REG_DWORD, &g_distroId, sizeof(g_distroId), type, data, size);
return ERROR_FILE_NOT_FOUND;
}
void SetupVirtualProductKey(const ds2::Ini& ini, ds2::Exe exe) {
// Identity: an explicit [Network] CdKeyHash wins; otherwise a per-user value kept in
// HKCU\Software\DS2Revival (writable even when the game folder is not), generated on first run.
char buf[64];
ini.String("Network", "CdKeyHash", "", buf, sizeof(buf));
g_cdKeyHash = _strtoui64(buf, nullptr, 10);
HKEY user = nullptr;
if (!g_cdKeyHash && RegCreateKeyExA(HKEY_CURRENT_USER, "Software\\DS2Revival", 0, nullptr, 0,
KEY_QUERY_VALUE | KEY_SET_VALUE, nullptr, &user, nullptr) == ERROR_SUCCESS) {
DWORD type = 0, size = sizeof(g_cdKeyHash);
if (RegQueryValueExA(user, "CdKeyHash", nullptr, &type, reinterpret_cast<BYTE*>(&g_cdKeyHash), &size) !=
ERROR_SUCCESS ||
type != REG_QWORD)
g_cdKeyHash = 0;
if (!g_cdKeyHash) { // first run: a random identity (63 bits so it prints as 19 digits max)
unsigned int a = 0, b = 0;
rand_s(&a);
rand_s(&b);
g_cdKeyHash = ((static_cast<unsigned long long>(a) << 32) | b) & 0x7FFFFFFFFFFFFFFFull;
RegSetValueExA(user, "CdKeyHash", 0, REG_QWORD, reinterpret_cast<const BYTE*>(&g_cdKeyHash),
sizeof(g_cdKeyHash));
}
RegCloseKey(user);
}
char salt[64]; // test harness: distinct identities for several instances on one PC
if (GetEnvironmentVariableA("DS2FIX_IDENTITY_SALT", salt, sizeof(salt)))
for (const char* c = salt; *c; ++c) g_cdKeyHash = ((g_cdKeyHash * 1099511628211ull) ^ static_cast<BYTE>(*c)) & 0x7FFFFFFFFFFFFFFFull;
ini.String("Network", "ProductId", "00000-000-0000000-00000", g_pid, sizeof(g_pid));
// DistroId only gates the in-game version check (the news banner, news.cpp): non-zero when news is on.
g_distroId = static_cast<DWORD>(ini.IntOrAuto("Network", "DistroId", mp::NewsEnabled(ini) ? 1 : 0));
ds2::vreg::Install(ServeCdKey, true, exe == ds2::Exe::BrokenWorld, "virtual CD key");
ds2::log().Detail("virtual product key ready (CdKeyHash %llu)", g_cdKeyHash);
}
// Patches with fixed Steam v2.3 addresses (not located in other builds yet).
void ApplySteamOnly(const ds2::Ini& ini) {
InstallLobbyServer(ini, ds2::Exe::Steam23);
if (ini.Bool("Multiplayer", "FixStartGuard", true))
ds2::PatchBytes(kSiteStartGuard, kStartGuardOrig, kStartGuardPatch, sizeof(kStartGuardOrig),
"staging START: remove placed-party guard");
if (ini.Bool("Multiplayer", "EnableMenuButton", true)) {
ds2::RedirectCall(kSiteDisableMpButton, kUIButton_DisableButton, kUIButton_EnableButton,
"main menu: enable Multiplayer button");
}
}
// Patches that work in every build: import hooks and signature-located code.
void ApplyPortable(const ds2::Ini& ini, ds2::Exe exe) {
if (mp::TestLoopback()) {
g_bind = reinterpret_cast<Bind_t>(ds2::HookImport("WSOCK32.dll", "#2",
reinterpret_cast<void*>(TestBind), "bind (private loopback harness)"));
ds2::log().Write("private loopback harness: game/shim sockets restricted to 127.0.0.1; relay disabled");
}
mp::InstallDirectPlayShim(ini); // first: the firewall and relay CoCreateInstance hooks then see its objects
if (ini.Bool("Network", "VirtualCdKey", true)) SetupVirtualProductKey(ini, exe);
PatchDirectConnectTimeout(ini);
ShowAllGameModes(ini);
ini.String("Network", "Server", kDefaultServer, g_server, sizeof(g_server));
ds2::log().Write("server: '%s'", g_server);
g_singleLanAddress = ini.Bool("Network", "SingleLanAddress", true);
if (g_server[0] || g_singleLanAddress) {
g_gethostbyname = static_cast<gethostbyname_t>(ds2::HookImport(
"WSOCK32.dll", "#52", reinterpret_cast<void*>(HookGethostbyname), "gethostbyname (GameSpy redirect, LAN address)"));
if (g_server[0]) ds2::log().Detail("GameSpy hosts redirect to '%s'", g_server);
}
if (ini.Bool("Multiplayer", "SkipFirewallPrompt", true))
g_fwCoCreateInstance = static_cast<FwCoCreateInstance_t>(ds2::HookImport(
"ole32.dll", "CoCreateInstance", reinterpret_cast<void*>(FirewallCoCreateInstance),
"CoCreateInstance (skip the Windows Firewall exception prompt)"));
mp::InstallRelay(ini);
mp::InstallNews(ini, exe);
mp::InstallJoinVersionCheck(ini, exe);
}
} // namespace
namespace mp {
const char* ServerName() { return g_server; }
} // namespace mp
BOOL WINAPI DllMain(HMODULE self, DWORD reason, LPVOID) {
if (reason != DLL_PROCESS_ATTACH) return TRUE;
DisableThreadLibraryCalls(self);
ds2::Plugin plugin;
if (!ds2::StartPlugin(self, "DS2Multiplayer", plugin)) return TRUE;
ds2::SummaryAtExit summary;
ds2::Ini ini(plugin.ini);
ApplyPortable(ini, plugin.exe);
if (plugin.exe == ds2::Exe::Steam23)
ApplySteamOnly(ini);
else
InstallLobbyServer(ini, plugin.exe);
return TRUE;
}