482 lines
27 KiB
C++
482 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().Write(" 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().Write("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().Write("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().Write("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().Write("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().Write("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::Ini ini(plugin.ini);
|
|
ApplyPortable(ini, plugin.exe);
|
|
if (plugin.exe == ds2::Exe::Steam23)
|
|
ApplySteamOnly(ini);
|
|
else
|
|
InstallLobbyServer(ini, plugin.exe);
|
|
return TRUE;
|
|
}
|