DS2Revival/mods/multiplayer/dp8shim.cpp

2991 lines
128 KiB
C++

// DirectPlay 8 replacement: our own DirectPlay8Client / DirectPlay8Server / DirectPlay8Address speaking Microsoft's
// published DirectPlay 8 network protocol, so a player without the Windows "DirectPlay" feature (or on Wine/Proton,
// whose dpnet cannot host) plays with players on real DirectPlay in either role (docs/multiplayer/dp8-shim.md).
//
// [Network] DirectPlay = auto (default: the shim only when Windows cannot create the DirectPlay 8 classes - the
// feature is off, so dpnet.dll is Windows' on-demand stub - or Wine's built-in dpnet would load), native (never the
// shim), shim (always).
//
// Layers, bottom up (each written from the specification named, checked against captures of real dpnet):
// transport [MC-DPL8R] per peer: CONNECT/CONNECTED handshake, 8-bit sequence numbers, cumulative ACK in every
// data frame, SACK command frames with selective-ACK masks, retries with backoff, a congestion
// window of at most 64 frames, NEW_MSG/END_MSG fragmentation, coalesced frames (receive only),
// keep-alives, END_STREAM graceful close, HARD_DISCONNECT. No signing (DS2 never asks for it).
// enumeration [MC-DPLHP] EnumQuery / EnumResponse on the game port (lead byte 0).
// session [MC-DPL8CS] client/server subset: PLAYER_CONNECT_INFO(_EX) 0xC1, SEND_CONNECT_INFO 0xC2,
// ACK_CONNECT_INFO 0xC3, CONNECT_FAILED 0xC5, TERMINATE_SESSION 0xDF, application data as plain
// payload; plus 0xCF (application description update), which the specification does not list but
// dpnet sends (capture W1, dp8-shim.md section 4).
// API the COM interfaces and messages DS2 uses (dp8-shim.md section 2).
// Test hooks (environment): DS2DP8_LOSS=<percent> drops that share of outgoing datagrams, DS2DP8_DELAY=<ms> delays
// every outgoing datagram (and DS2DP8_JITTER=<ms> adds up to that much at random, which also reorders them).
#include "multiplayer.h"
#include "../common/dp8.h"
#include <ws2tcpip.h>
#include <atomic>
#include <cstdint>
#include <cstring>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
extern "C" BOOLEAN NTAPI SystemFunction036(PVOID buffer, ULONG length); // RtlGenRandom (advapi32)
namespace {
using namespace dp8;
using Bytes = std::vector<BYTE>;
using Events = std::vector<std::function<void()>>;
enum Mode { kNative, kAuto, kShim };
Mode g_mode = kNative;
int g_lossPct = 0;
DWORD g_delayMs = 0, g_jitterMs = 0;
uint32_t Rand32() {
uint32_t v = 0;
if (!SystemFunction036(&v, sizeof(v))) v = GetTickCount() * 2654435761u ^ GetCurrentProcessId();
return v;
}
std::atomic<DPNHANDLE> g_nextHandle{0x10000};
DPNHANDLE NewHandle() { return g_nextHandle++; }
// ---- little-endian helpers ---------------------------------------------------------------------------
void Put8(Bytes& b, BYTE v) { b.push_back(v); }
void Put16(Bytes& b, uint16_t v) { b.push_back(static_cast<BYTE>(v)); b.push_back(static_cast<BYTE>(v >> 8)); }
void Put32(Bytes& b, uint32_t v) { for (int i = 0; i < 4; ++i) b.push_back(static_cast<BYTE>(v >> (8 * i))); }
void PutRaw(Bytes& b, const void* p, size_t n) {
if (p && n) b.insert(b.end(), static_cast<const BYTE*>(p), static_cast<const BYTE*>(p) + n);
}
void PutGuid(Bytes& b, const GUID& g) { PutRaw(b, &g, sizeof(g)); }
void Set32(Bytes& b, size_t at, uint32_t v) { for (int i = 0; i < 4; ++i) b[at + i] = static_cast<BYTE>(v >> (8 * i)); }
uint32_t Get32(const BYTE* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (static_cast<uint32_t>(p[3]) << 24); }
uint16_t Get16(const BYTE* p) { return static_cast<uint16_t>(p[0] | (p[1] << 8)); }
GUID GetGuid(const BYTE* p) { GUID g; std::memcpy(&g, p, sizeof(g)); return g; }
Bytes WBytes(const std::wstring& s) { // zero-terminated UTF-16
Bytes b(reinterpret_cast<const BYTE*>(s.c_str()), reinterpret_cast<const BYTE*>(s.c_str() + s.size() + 1));
return b;
}
// A field referenced by (offset, size) inside a message whose offsets count from `origin`.
struct Span {
const BYTE* base; // origin
size_t len; // bytes from the origin to the end of the message
bool Get(uint32_t off, uint32_t size, const BYTE** out) const {
if (!off || !size) {
*out = nullptr;
return true;
}
if (off > len || size > len - off) return false;
*out = base + off;
return true;
}
bool Str(uint32_t off, uint32_t size, std::wstring* out) const {
const BYTE* p;
if (!Get(off, size, &p)) return false;
out->clear();
if (p) out->assign(reinterpret_cast<const WCHAR*>(p), wcsnlen(reinterpret_cast<const WCHAR*>(p), size / 2));
return true;
}
bool Blob(uint32_t off, uint32_t size, Bytes* out) const {
const BYTE* p;
if (!Get(off, size, &p)) return false;
out->assign(p, p ? p + size : p);
return true;
}
};
// Builds a message with a fixed part followed by variable data referenced by offsets from `origin`.
struct Builder {
Bytes b;
size_t origin = 0;
// Appends `data` to the end and writes its offset/size into the two DWORDs at `field` (nothing if empty).
void Var(size_t field, const Bytes& data) {
if (data.empty()) return;
Set32(b, field, static_cast<uint32_t>(b.size() - origin));
Set32(b, field + 4, static_cast<uint32_t>(data.size()));
b.insert(b.end(), data.begin(), data.end());
}
};
// ---- application description ----------------------------------------------------------------------------
// An application description we own (strings and data copied).
struct Desc {
DWORD flags = 0, maxPlayers = 0, curPlayers = 0;
GUID instance{}, app{};
std::wstring name, password;
bool hasPassword = false;
Bytes reserved, appReserved;
void From(const APPLICATION_DESC* d) {
flags = d->dwFlags;
instance = d->guidInstance;
app = d->guidApplication;
maxPlayers = d->dwMaxPlayers;
name = d->pwszSessionName ? d->pwszSessionName : L"";
hasPassword = d->pwszPassword != nullptr;
password = d->pwszPassword ? d->pwszPassword : L"";
reserved.assign(static_cast<const BYTE*>(d->pvReservedData),
static_cast<const BYTE*>(d->pvReservedData) + (d->pvReservedData ? d->dwReservedDataSize : 0));
appReserved.assign(static_cast<const BYTE*>(d->pvApplicationReservedData),
static_cast<const BYTE*>(d->pvApplicationReservedData) +
(d->pvApplicationReservedData ? d->dwApplicationReservedDataSize : 0));
}
// DPN_APPLICATION_DESC with its strings and data right behind it (GetApplicationDesc's buffer layout).
DWORD Size() const {
return static_cast<DWORD>(sizeof(APPLICATION_DESC) + (name.size() + 1) * 2 +
(hasPassword ? (password.size() + 1) * 2 : 0) + reserved.size() + appReserved.size());
}
void Fill(APPLICATION_DESC* d) const {
BYTE* p = reinterpret_cast<BYTE*>(d + 1);
std::memset(d, 0, sizeof(*d));
d->dwSize = sizeof(*d);
d->dwFlags = flags;
d->guidInstance = instance;
d->guidApplication = app;
d->dwMaxPlayers = maxPlayers;
d->dwCurrentPlayers = curPlayers;
d->pwszSessionName = reinterpret_cast<WCHAR*>(p);
std::memcpy(p, name.c_str(), (name.size() + 1) * 2);
p += (name.size() + 1) * 2;
if (hasPassword) {
d->pwszPassword = reinterpret_cast<WCHAR*>(p);
std::memcpy(p, password.c_str(), (password.size() + 1) * 2);
p += (password.size() + 1) * 2;
}
if (!reserved.empty()) {
d->pvReservedData = p;
d->dwReservedDataSize = static_cast<DWORD>(reserved.size());
std::memcpy(p, reserved.data(), reserved.size());
p += reserved.size();
}
if (!appReserved.empty()) {
d->pvApplicationReservedData = p;
d->dwApplicationReservedDataSize = static_cast<DWORD>(appReserved.size());
std::memcpy(p, appReserved.data(), appReserved.size());
}
}
// The 0x50-byte wire form shared by EnumResponse, SEND_CONNECT_INFO and 0xCF: size, flags, max, current,
// session name / password / reserved / application reserved (offset, size), instance and application GUIDs.
// Returns the position of the block in bb.b; the variable parts are appended by WireTail.
size_t WireBlock(Builder& bb) const {
size_t at = bb.b.size();
Put32(bb.b, 0x50);
Put32(bb.b, flags);
Put32(bb.b, maxPlayers);
Put32(bb.b, curPlayers);
for (int i = 0; i < 8; ++i) Put32(bb.b, 0);
PutGuid(bb.b, instance);
PutGuid(bb.b, app);
return at;
}
// dpnet's order (captures W1): application reserved data, then the session name. The password is sent only
// in SEND_CONNECT_INFO (MC-DPL8CS 3.1, never in an EnumResponse).
void WireTail(Builder& bb, size_t at, bool withPassword) const {
bb.Var(at + 40, appReserved);
bb.Var(at + 32, reserved);
if (withPassword && hasPassword && !password.empty() && (flags & SESSION_REQUIREPASSWORD))
bb.Var(at + 24, WBytes(password));
bb.Var(at + 16, WBytes(name));
}
bool ParseWire(const Span& s, const BYTE* block, size_t blockLen) {
if (blockLen < 0x50 || Get32(block) < 0x50) return false;
flags = Get32(block + 4);
maxPlayers = Get32(block + 8);
curPlayers = Get32(block + 12);
instance = GetGuid(block + 48);
app = GetGuid(block + 64);
if (!s.Str(Get32(block + 16), Get32(block + 20), &name) || !s.Str(Get32(block + 24), Get32(block + 28), &password) ||
!s.Blob(Get32(block + 32), Get32(block + 36), &reserved) ||
!s.Blob(Get32(block + 40), Get32(block + 44), &appReserved))
return false;
hasPassword = Get32(block + 24) != 0;
return true;
}
};
// ---- DirectPlay8Address ----------------------------------------------------------------------------
std::wstring GuidText(const GUID& g) {
WCHAR b[40];
swprintf_s(b, L"{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", g.Data1, g.Data2, g.Data3, g.Data4[0],
g.Data4[1], g.Data4[2], g.Data4[3], g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7]);
return b;
}
bool ParseGuid(const std::wstring& s, GUID* g) {
unsigned long d1;
unsigned d2, d3, b[8];
if (swscanf_s(s.c_str(), L"{%8lx-%4x-%4x-%2x%2x-%2x%2x%2x%2x%2x%2x}", &d1, &d2, &d3, &b[0], &b[1], &b[2], &b[3],
&b[4], &b[5], &b[6], &b[7]) != 11)
return false;
g->Data1 = d1;
g->Data2 = static_cast<USHORT>(d2);
g->Data3 = static_cast<USHORT>(d3);
for (int i = 0; i < 8; ++i) g->Data4[i] = static_cast<BYTE>(b[i]);
return true;
}
class Address final : public IDirectPlay8Address {
public:
struct Comp {
std::wstring name;
DWORD type;
Bytes data;
};
HRESULT __stdcall QueryInterface(REFIID iid, void** out) override {
if (!out) return E_POINTER;
if (iid == IID_IUnknown || iid == IID_Address) {
*out = this;
AddRef();
return S_OK;
}
*out = nullptr;
return E_NOINTERFACE;
}
ULONG __stdcall AddRef() override { return InterlockedIncrement(&ref_); }
ULONG __stdcall Release() override {
LONG r = InterlockedDecrement(&ref_);
if (!r) delete this;
return r;
}
HRESULT __stdcall BuildFromURLW(WCHAR* url) override { return url ? Parse(url) : E_POINTER; }
HRESULT __stdcall BuildFromURLA(CHAR* url) override {
if (!url) return E_POINTER;
int n = MultiByteToWideChar(CP_ACP, 0, url, -1, nullptr, 0);
std::wstring w(n > 0 ? n : 1, L'\0');
MultiByteToWideChar(CP_ACP, 0, url, -1, &w[0], n);
return Parse(w.c_str());
}
HRESULT __stdcall Duplicate(IDirectPlay8Address** out) override {
if (!out) return E_POINTER;
auto* a = new Address;
a->CopyFrom(*this);
*out = a;
return S_OK;
}
HRESULT __stdcall SetEqual(IDirectPlay8Address* other) override {
if (!other) return E_POINTER;
WCHAR url[1024];
DWORD n = 1024;
HRESULT hr = other->GetURLW(url, &n);
return SUCCEEDED(hr) ? Parse(url) : hr;
}
HRESULT __stdcall IsEqual(IDirectPlay8Address* other) override {
if (!other) return E_POINTER;
WCHAR url[1024];
DWORD n = 1024;
if (FAILED(other->GetURLW(url, &n))) return S_FALSE;
return _wcsicmp(url, Url().c_str()) == 0 ? static_cast<HRESULT>(0x00158005) /* DPNSUCCESS_EQUAL */
: static_cast<HRESULT>(0x0015800A) /* DPNSUCCESS_NOTEQUAL */;
}
HRESULT __stdcall Clear() override {
std::lock_guard<std::mutex> l(m_);
hasSp_ = hasDev_ = false;
comps_.clear();
user_.clear();
return S_OK;
}
HRESULT __stdcall GetURLW(WCHAR* url, DWORD* chars) override {
if (!chars) return E_POINTER;
std::wstring u = Url();
if (!url || *chars < u.size() + 1) {
*chars = static_cast<DWORD>(u.size() + 1);
return E_BUFFERTOOSMALL;
}
std::memcpy(url, u.c_str(), (u.size() + 1) * 2);
*chars = static_cast<DWORD>(u.size() + 1);
return S_OK;
}
HRESULT __stdcall GetURLA(CHAR* url, DWORD* chars) override {
if (!chars) return E_POINTER;
std::wstring u = Url();
std::string a; // URLs are escaped ASCII
for (WCHAR ch : u) a += static_cast<char>(ch);
if (!url || *chars < a.size() + 1) {
*chars = static_cast<DWORD>(a.size() + 1);
return E_BUFFERTOOSMALL;
}
std::memcpy(url, a.c_str(), a.size() + 1);
*chars = static_cast<DWORD>(a.size() + 1);
return S_OK;
}
HRESULT __stdcall GetSP(GUID* sp) override {
if (!sp) return E_POINTER;
if (!hasSp_) return E_DOESNOTEXIST;
*sp = sp_;
return S_OK;
}
HRESULT __stdcall GetUserData(void* data, DWORD* size) override {
if (!size) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
if (user_.empty()) return E_DOESNOTEXIST;
if (!data || *size < user_.size()) {
*size = static_cast<DWORD>(user_.size());
return E_BUFFERTOOSMALL;
}
std::memcpy(data, user_.data(), user_.size());
*size = static_cast<DWORD>(user_.size());
return S_OK;
}
HRESULT __stdcall SetSP(const GUID* sp) override {
if (!sp) return E_POINTER;
sp_ = *sp;
hasSp_ = true;
return S_OK;
}
HRESULT __stdcall SetUserData(const void* data, DWORD size) override {
std::lock_guard<std::mutex> l(m_);
user_.assign(static_cast<const BYTE*>(data), static_cast<const BYTE*>(data) + (data ? size : 0));
return S_OK;
}
HRESULT __stdcall GetNumComponents(DWORD* n) override {
if (!n) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
*n = static_cast<DWORD>(comps_.size() + (hasSp_ ? 1 : 0) + (hasDev_ ? 1 : 0));
return S_OK;
}
HRESULT __stdcall GetComponentByName(const WCHAR* name, void* buf, DWORD* size, DWORD* type) override {
if (!name || !size || !type) return E_POINTER;
Comp c;
if (!Find(name, &c)) return E_DOESNOTEXIST;
*type = c.type;
if (!buf || *size < c.data.size()) {
*size = static_cast<DWORD>(c.data.size());
return E_BUFFERTOOSMALL;
}
std::memcpy(buf, c.data.data(), c.data.size());
*size = static_cast<DWORD>(c.data.size());
return S_OK;
}
HRESULT __stdcall GetComponentByIndex(DWORD index, WCHAR* name, DWORD* nameLen, void* buf, DWORD* size,
DWORD* type) override {
if (!nameLen || !size || !type) return E_POINTER;
std::vector<Comp> all = All();
if (index >= all.size()) return static_cast<HRESULT>(0x80158500); // DPNERR_NOMOREADDRESSCOMPONENTS
const Comp& c = all[index];
bool tooSmall = !name || *nameLen < c.name.size() + 1 || !buf || *size < c.data.size();
*nameLen = static_cast<DWORD>(c.name.size() + 1);
*size = static_cast<DWORD>(c.data.size());
*type = c.type;
if (tooSmall) return E_BUFFERTOOSMALL;
std::memcpy(name, c.name.c_str(), (c.name.size() + 1) * 2);
std::memcpy(buf, c.data.data(), c.data.size());
return S_OK;
}
HRESULT __stdcall AddComponent(const WCHAR* name, const void* data, DWORD size, DWORD type) override {
if (!name || (!data && size)) return E_POINTER;
if (_wcsicmp(name, L"provider") == 0 || _wcsicmp(name, L"device") == 0) {
GUID g{};
if (type == DATATYPE_GUID && size == sizeof(GUID)) g = *static_cast<const GUID*>(data);
else if (type == DATATYPE_STRING && !ParseGuid(static_cast<const WCHAR*>(data), &g)) return E_INVALIDARG;
else if (type != DATATYPE_GUID && type != DATATYPE_STRING) return E_INVALIDARG;
return _wcsicmp(name, L"provider") == 0 ? SetSP(&g) : SetDevice(&g);
}
Comp c{name, type, Bytes(static_cast<const BYTE*>(data), static_cast<const BYTE*>(data) + size)};
if (type == DATATYPE_STRING_ANSI) { // stored wide, like dpnet
std::string a(static_cast<const char*>(data), strnlen(static_cast<const char*>(data), size));
std::wstring w(a.begin(), a.end());
c.type = DATATYPE_STRING;
c.data.assign(reinterpret_cast<const BYTE*>(w.c_str()), reinterpret_cast<const BYTE*>(w.c_str() + w.size() + 1));
}
Set(c);
return S_OK;
}
HRESULT __stdcall GetDevice(GUID* dev) override {
if (!dev) return E_POINTER;
if (!hasDev_) return E_DOESNOTEXIST;
*dev = dev_;
return S_OK;
}
HRESULT __stdcall SetDevice(const GUID* dev) override {
if (!dev) return E_POINTER;
dev_ = *dev;
hasDev_ = true;
return S_OK;
}
HRESULT __stdcall BuildFromDPADDRESS(void*, DWORD) override { return E_NOTIMPL; }
// Helpers for the shim.
void SetHost(const sockaddr_in& a) {
SetSP(&SP_TCPIP);
WCHAR ip[32];
InetNtopW(AF_INET, &a.sin_addr, ip, 32);
SetString(L"hostname", ip);
SetDword(L"port", ntohs(a.sin_port));
}
void SetString(const WCHAR* name, const WCHAR* v) {
Set({name, DATATYPE_STRING, Bytes(reinterpret_cast<const BYTE*>(v), reinterpret_cast<const BYTE*>(v + wcslen(v) + 1))});
}
void SetDword(const WCHAR* name, DWORD v) {
Set({name, DATATYPE_DWORD, Bytes(reinterpret_cast<const BYTE*>(&v), reinterpret_cast<const BYTE*>(&v + 1))});
}
private:
void CopyFrom(const Address& o) {
std::lock_guard<std::mutex> l(o.m_);
sp_ = o.sp_;
dev_ = o.dev_;
hasSp_ = o.hasSp_;
hasDev_ = o.hasDev_;
comps_ = o.comps_;
user_ = o.user_;
}
void Set(const Comp& c) {
std::lock_guard<std::mutex> l(m_);
for (Comp& e : comps_)
if (_wcsicmp(e.name.c_str(), c.name.c_str()) == 0) {
e = c;
return;
}
comps_.push_back(c);
}
std::vector<Comp> All() const {
std::vector<Comp> v;
if (hasSp_) v.push_back({L"provider", DATATYPE_GUID, Bytes(reinterpret_cast<const BYTE*>(&sp_), reinterpret_cast<const BYTE*>(&sp_ + 1))});
if (hasDev_) v.push_back({L"device", DATATYPE_GUID, Bytes(reinterpret_cast<const BYTE*>(&dev_), reinterpret_cast<const BYTE*>(&dev_ + 1))});
std::lock_guard<std::mutex> l(m_);
v.insert(v.end(), comps_.begin(), comps_.end());
return v;
}
bool Find(const WCHAR* name, Comp* out) const {
for (const Comp& c : All())
if (_wcsicmp(c.name.c_str(), name) == 0) {
*out = c;
return true;
}
return false;
}
static void Escape(std::wstring& out, const std::wstring& v) {
for (WCHAR ch : v) {
if ((ch >= L'a' && ch <= L'z') || (ch >= L'A' && ch <= L'Z') || (ch >= L'0' && ch <= L'9') || ch == L'-' ||
ch == L'.' || ch == L'_') {
out += ch;
} else {
WCHAR b[8];
swprintf_s(b, L"%%%02X", ch & 0xFF);
out += b;
}
}
}
std::wstring Url() const {
std::wstring u = L"x-directplay:/";
bool first = true;
for (const Comp& c : All()) {
if (!first) u += L';';
first = false;
u += c.name;
u += L'=';
if (c.type == DATATYPE_GUID && c.data.size() == sizeof(GUID)) Escape(u, GuidText(*reinterpret_cast<const GUID*>(c.data.data())));
else if (c.type == DATATYPE_DWORD && c.data.size() == 4) u += std::to_wstring(*reinterpret_cast<const DWORD*>(c.data.data()));
else if (c.type == DATATYPE_STRING) Escape(u, std::wstring(reinterpret_cast<const WCHAR*>(c.data.data()), wcsnlen(reinterpret_cast<const WCHAR*>(c.data.data()), c.data.size() / 2)));
else { // binary: hex, as dpnet does for "alt"
for (BYTE x : c.data) {
WCHAR b[4];
swprintf_s(b, L"%02x", x);
u += b;
}
}
}
return u;
}
static std::wstring Unescape(const std::wstring& v) {
std::wstring o;
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == L'%' && i + 2 < v.size() + 0 && iswxdigit(v[i + 1]) && iswxdigit(v[i + 2])) {
o += static_cast<WCHAR>(wcstoul(v.substr(i + 1, 2).c_str(), nullptr, 16));
i += 2;
} else {
o += v[i];
}
}
return o;
}
HRESULT Parse(const WCHAR* url) {
static const WCHAR kScheme[] = L"x-directplay:/";
if (_wcsnicmp(url, kScheme, wcslen(kScheme)) != 0) return E_INVALIDURL;
Clear();
std::wstring rest = url + wcslen(kScheme);
size_t pos = 0;
while (pos < rest.size()) {
size_t end = rest.find(L';', pos);
if (end == std::wstring::npos) end = rest.size();
std::wstring kv = rest.substr(pos, end - pos);
pos = end + 1;
size_t eq = kv.find(L'=');
if (eq == std::wstring::npos) continue;
std::wstring k = kv.substr(0, eq), v = Unescape(kv.substr(eq + 1));
if (_wcsicmp(k.c_str(), L"provider") == 0 || _wcsicmp(k.c_str(), L"device") == 0) {
GUID g;
if (ParseGuid(v, &g)) (_wcsicmp(k.c_str(), L"provider") == 0) ? SetSP(&g) : SetDevice(&g);
} else if (!v.empty() && v.size() <= 10 && v.find_first_not_of(L"0123456789") == std::wstring::npos) {
SetDword(k.c_str(), static_cast<DWORD>(wcstoul(v.c_str(), nullptr, 10)));
} else {
SetString(k.c_str(), v.c_str());
}
}
return S_OK;
}
LONG ref_ = 1;
mutable std::mutex m_;
GUID sp_{}, dev_{};
bool hasSp_ = false, hasDev_ = false;
std::vector<Comp> comps_;
Bytes user_;
};
// hostname (IPv4 literal or name) + port of an address; false if there is no usable host name.
bool ResolveAddress(IDirectPlay8Address* a, sockaddr_in* out, DWORD defaultPort) {
WCHAR host[256] = {};
DWORD size = sizeof(host) - 2, type = 0, port = 0;
if (!a || FAILED(a->GetComponentByName(L"hostname", host, &size, &type)) || type != DATATYPE_STRING) return false;
size = 4;
if (FAILED(a->GetComponentByName(L"port", &port, &size, &type)) || type != DATATYPE_DWORD || !port || port > 0xFFFF)
port = defaultPort;
sockaddr_in s{};
s.sin_family = AF_INET;
s.sin_port = htons(static_cast<u_short>(port));
if (InetPtonW(AF_INET, host, &s.sin_addr) != 1) {
ADDRINFOW hints{}, *res = nullptr;
hints.ai_family = AF_INET;
if (GetAddrInfoW(host, nullptr, &hints, &res) != 0 || !res) return false;
s.sin_addr = reinterpret_cast<sockaddr_in*>(res->ai_addr)->sin_addr;
FreeAddrInfoW(res);
}
*out = s;
return true;
}
DWORD PortOf(IDirectPlay8Address* a) {
DWORD port = 0, size = 4, type = 0;
if (a && SUCCEEDED(a->GetComponentByName(L"port", &port, &size, &type)) && type == DATATYPE_DWORD && port <= 0xFFFF) return port;
return 0;
}
// ---- sockets ---------------------------------------------------------------------------------------------------
SOCKET BindUdp(DWORD firstPort, DWORD lastPort, u_short* bound) {
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s == INVALID_SOCKET) return s;
BOOL excl = TRUE, bcast = TRUE;
setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast<const char*>(&excl), sizeof(excl));
setsockopt(s, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<const char*>(&bcast), sizeof(bcast));
DWORD off = 0; // a peer's ICMP "port unreachable" must not break recvfrom (WSAECONNRESET)
DWORD bytes = 0;
constexpr DWORD kUdpConnReset = _WSAIOW(IOC_VENDOR, 12); // SIO_UDP_CONNRESET (mstcpip.h)
WSAIoctl(s, kUdpConnReset, &off, sizeof(off), nullptr, 0, &bytes, nullptr, nullptr);
for (DWORD p = firstPort; p <= lastPort; ++p) {
sockaddr_in a{};
a.sin_family = AF_INET;
if (mp::TestLoopback()) a.sin_addr.s_addr = mp::TestLoopbackNet();
a.sin_port = htons(static_cast<u_short>(p));
if (bind(s, reinterpret_cast<sockaddr*>(&a), sizeof(a)) == 0) {
int len = sizeof(a);
getsockname(s, reinterpret_cast<sockaddr*>(&a), &len);
*bound = ntohs(a.sin_port);
return s;
}
if (!firstPort) break;
}
closesocket(s);
return INVALID_SOCKET;
}
bool SameAddr(const sockaddr_in& a, const sockaddr_in& b) { return a.sin_addr.s_addr == b.sin_addr.s_addr && a.sin_port == b.sin_port; }
uint64_t AddrKey(const sockaddr_in& a) { return (static_cast<uint64_t>(a.sin_addr.s_addr) << 16) | a.sin_port; }
std::string AddrText(const sockaddr_in& a) {
char b[32];
sprintf_s(b, "%u.%u.%u.%u:%u", a.sin_addr.S_un.S_un_b.s_b1, a.sin_addr.S_un.S_un_b.s_b2, a.sin_addr.S_un.S_un_b.s_b3,
a.sin_addr.S_un.S_un_b.s_b4, ntohs(a.sin_port));
return b;
}
// DS2DP8_WIRELOG=<file>: every datagram the shim sends, in the format of the ds2dev capture ("NET" lines), so
// tools/dp8wire.py checks it against the specifications like dpnet's. Datagrams received are "RCV" lines and the
// ones the test loss hook dropped are "LOST" lines; the decoder skips both.
FILE* g_wireLog;
DWORD g_wireT0;
std::mutex g_wireLock;
void WireLogLine(const char* tag, const char* arrow, u_short local, const sockaddr_in& peer, const BYTE* b, size_t n) {
if (!g_wireLog) return;
std::string hex;
hex.reserve(n * 2);
static const char kHex[] = "0123456789ABCDEF";
for (size_t i = 0; i < n; ++i) {
hex += kHex[b[i] >> 4];
hex += kHex[b[i] & 15];
}
std::lock_guard<std::mutex> l(g_wireLock);
fprintf(g_wireLog, "%9lu %5lu %s %u %s %u.%u.%u.%u:%u %u %s\n", GetTickCount() - g_wireT0, GetCurrentThreadId(), tag, local,
arrow, peer.sin_addr.S_un.S_un_b.s_b1, peer.sin_addr.S_un.S_un_b.s_b2, peer.sin_addr.S_un.S_un_b.s_b3,
peer.sin_addr.S_un.S_un_b.s_b4, ntohs(peer.sin_port), static_cast<unsigned>(n), hex.c_str());
fflush(g_wireLog);
}
void WireLog(u_short from, const sockaddr_in& to, const Bytes& b) { WireLogLine("NET", "->", from, to, b.data(), b.size()); }
// ---- transport [MC-DPL8R] -----------------------------------------------------------------------------------------
constexpr DWORD kProtocolVersion = 0x00010006; // what dpnet sends (capture W1); we never sign
enum : BYTE { CMD_DATA = 0x01, CMD_REL = 0x02, CMD_SEQ = 0x04, CMD_POLL = 0x08, CMD_NEW = 0x10, CMD_END = 0x20,
CMD_USER1 = 0x40, CMD_USER2 = 0x80 };
enum : BYTE { CTL_RETRY = 0x01, CTL_KEEPALIVE = 0x02, CTL_COALESCE = 0x04, CTL_END_STREAM = 0x08, CTL_SACK1 = 0x10,
CTL_SACK2 = 0x20, CTL_SEND1 = 0x40, CTL_SEND2 = 0x80 };
enum : BYTE { CF_FRAME = 0x80, CF_POLL = 0x08 };
enum : BYTE { OP_CONNECT = 1, OP_CONNECTED = 2, OP_CONNECTED_SIGNED = 3, OP_HARD_DISCONNECT = 4, OP_SACK = 6 };
enum : BYTE { SACKF_RESPONSE = 0x01, SACKF_SACK1 = 0x02, SACKF_SACK2 = 0x04, SACKF_SEND1 = 0x08, SACKF_SEND2 = 0x10 };
constexpr size_t kMaxFramePayload = 1356; // dpnet's largest data-frame payload (W1: 1,360-byte datagrams)
constexpr int kMaxWindow = 64, kStartWindow = 4;
constexpr DWORD kKeepAliveMs = 25000, kDelayedAckMs = 100, kShortAckMs = 20, kCloseTimeoutMs = 5000;
constexpr int kMaxRetries = 10, kMaxQuickRetries = 8, kMaxConnectTries = 14;
// DS2 captures peak at 1,702 bytes/message. Leave ample room for world bursts and eight players,
// while bounding all remote-controlled reassembly, reordered frames and held RECEIVE buffers.
constexpr size_t kMaxMessageBytes = 1024 * 1024, kPeerReceiveBytes = 2 * 1024 * 1024,
kEndpointReceiveBytes = 16 * 1024 * 1024;
constexpr DWORD kReassemblyMs = 30000;
constexpr size_t kMaxLinks = 16, kMaxPendingLinks = 8;
constexpr DWORD kPendingLinkMs = 30000;
constexpr size_t kPeerSendBytes = 2 * 1024 * 1024, kEndpointSendBytes = 16 * 1024 * 1024;
constexpr DWORD kProgressMs = 60000;
// Intervals here are below 2^31 ms. A timestamp slightly newer than a timer
// snapshot is in the future, not an elapsed interval of almost 49.7 days.
bool Elapsed(DWORD now, DWORD start, DWORD interval) {
return static_cast<LONG>(now - start) >= static_cast<LONG>(interval);
}
bool SeqBefore(BYTE a, BYTE b) { BYTE d = static_cast<BYTE>(b - a); return d != 0 && d < 128; }
struct OutMsg {
uint64_t serial;
DPNHANDLE handle;
void* ctx;
bool complete; // raise SEND_COMPLETE
DWORD bytes, start;
BYTE lastSeq;
bool lastSent; // the message's last frame has its sequence number
};
struct Link {
enum St { kConnecting, kAccepting, kUp, kClosing, kDown };
uint32_t id = 0;
sockaddr_in addr{};
uint32_t sess = 0;
St st = kConnecting;
BYTE msgId = 0, peerMsgId = 0;
DWORD hsNext = 0, hsRto = 200;
int hsTries = 0;
// sending
BYTE nextSend = 0;
DWORD progressAt = 0;
// sent: last (re)transmission, which also schedules the next retry; first: the first one, for round-trip samples
// tries: transmissions; quick: retries brought forward by a reported gap, which do not count towards the retry
// limit or the backoff; quickAt: when the next quick retry is due (0: none)
struct Frame { BYTE seq; Bytes pkt; DWORD sent, first, rto, quickAt; int tries, quick; bool sacked; };
std::deque<Frame> flight;
struct Pending { BYTE cmd, ctl; Bytes payload; uint64_t msg; bool last, end; };
std::deque<Pending> queue;
std::deque<OutMsg> msgs;
uint64_t nextSerial = 1;
int cwnd = kStartWindow;
DWORD rtt = 100, rttVar = 0, lastSend = 0, lastKeepAlive = 0, lastCut = 0;
// receiving
BYTE nextRecv = 0;
struct In { BYTE cmd, ctl; Bytes payload; bool dropped; };
std::map<BYTE, In> early; // frames ahead of nextRecv, by sequence number
Bytes partial;
struct Held { size_t bytes = 0, count = 0; };
std::shared_ptr<Held> held = std::make_shared<Held>();
DWORD partialAt = 0;
bool inMsg = false;
BYTE msgCmd = 0;
bool ackDue = false, lastWasRetry = false;
DWORD ackAt = 0, lastRecv = 0;
// closing
bool endQueued = false, endSent = false, endAcked = false, endRecv = false;
BYTE endSeq = 0;
DWORD closeAt = 0;
HRESULT why = S_OK;
// session layer (Server: player state; Client: unused)
int stage = 0;
DPNID player = 0;
std::wstring name;
void* pctx = nullptr;
bool kicked = false;
DWORD upAt = 0;
DWORD bornAt = GetTickCount();
};
// Callbacks from the transport to the session layer; always called with the owner's lock held.
struct Upper {
virtual void LinkUp(Link& l) = 0;
virtual void LinkMessage(Link& l, Bytes&& msg, bool core) = 0;
virtual void LinkSendDone(Link& l, const OutMsg& m, HRESULT hr) = 0;
virtual void LinkDown(Link& l) = 0; // l.why: S_OK = closed gracefully (by either side), else the error
virtual bool Listening() = 0;
};
class Endpoint {
public:
SOCKET s = INVALID_SOCKET;
u_short port = 0;
std::map<uint64_t, std::unique_ptr<Link>> links;
size_t heldBytes = 0, heldCount = 0;
// Fixed-size admission table: spoofed source IPs cannot grow the limiter itself.
struct Admission { DWORD ip = 0, at = 0, used = 0; unsigned tokens = 16; bool active = false; };
Admission sources[64]{};
DWORD admissionAt = GetTickCount();
unsigned admissionTokens = 32;
static void Refill(unsigned& tokens, DWORD& at, DWORD now, unsigned cap, DWORD interval) {
DWORD steps = (now - at) / interval;
if (steps) { tokens = steps >= cap ? cap : (tokens + steps > cap ? cap : tokens + steps); at = now; }
}
bool Admit(const sockaddr_in& from, DWORD now) {
if (links.size() >= kMaxLinks) return false;
size_t pending = 0, sourcePending = 0;
for (auto& kv : links) {
const Link& l = *kv.second;
if (l.st != Link::kDown && l.stage < 3) {
++pending;
if (l.addr.sin_addr.s_addr == from.sin_addr.s_addr) ++sourcePending;
}
}
if (pending >= kMaxPendingLinks || sourcePending >= 8) return false;
Admission* slot = nullptr;
for (auto& a : sources) {
if (a.active && a.ip == from.sin_addr.s_addr) { slot = &a; break; }
if (!slot && (!a.active || now - a.used >= 60000)) slot = &a;
}
if (!slot) return false;
if (!slot->active || slot->ip != from.sin_addr.s_addr)
*slot = Admission{from.sin_addr.s_addr, now, now, 16, true};
Refill(admissionTokens, admissionAt, now, 32, 125); // 8 new links/s, burst 32
Refill(slot->tokens, slot->at, now, 16, 250); // 4/source/s, burst 16 (NAT friendly)
slot->used = now;
if (!admissionTokens || !slot->tokens) return false;
--admissionTokens; --slot->tokens;
return true;
}
static size_t ReceiveBytes(const Link& l) {
size_t n = l.partial.size() + l.held->bytes;
for (auto& kv : l.early) n += kv.second.payload.size();
return n;
}
size_t ReceiveBytes() const {
size_t n = heldBytes;
for (auto& kv : links) n += ReceiveBytes(*kv.second) - kv.second->held->bytes;
return n;
}
Link* Find(const sockaddr_in& a) {
auto it = links.find(AddrKey(a));
return it == links.end() ? nullptr : it->second.get();
}
Link* ById(uint32_t id) {
for (auto& kv : links)
if (kv.second->id == id) return kv.second.get();
return nullptr;
}
Link* Add(const sockaddr_in& a) {
if (links.size() >= kMaxLinks) return nullptr;
auto l = std::make_unique<Link>();
l->id = nextLink_++;
l->addr = a;
Link* p = l.get();
links[AddrKey(a)] = std::move(l);
return p;
}
void Send(const sockaddr_in& to, const Bytes& b) {
if (g_lossPct && static_cast<int>(Rand32() % 100) < g_lossPct) { // test: lost on the way
WireLogLine("LOST", "->", port, to, b.data(), b.size());
return;
}
if (g_delayMs || g_jitterMs) {
DWORD due = GetTickCount() + g_delayMs + (g_jitterMs ? Rand32() % (g_jitterMs + 1) : 0);
delayed_.push_back({due, to, b});
return;
}
SendNow(to, b);
}
void FlushDelayed(DWORD now, bool all = false) {
for (auto it = delayed_.begin(); it != delayed_.end();) {
if (all || static_cast<LONG>(now - it->due) >= 0) {
SendNow(it->to, it->b);
it = delayed_.erase(it);
} else {
++it;
}
}
}
void Sweep() { // links that went down are removed after their LinkDown ran
for (auto it = links.begin(); it != links.end();)
it = it->second->st == Link::kDown ? links.erase(it) : std::next(it);
}
private:
void SendNow(const sockaddr_in& to, const Bytes& b) {
WireLog(port, to, b);
if (s != INVALID_SOCKET)
sendto(s, reinterpret_cast<const char*>(b.data()), static_cast<int>(b.size()), 0,
reinterpret_cast<const sockaddr*>(&to), sizeof(to));
}
struct Delayed { DWORD due; sockaddr_in to; Bytes b; };
std::vector<Delayed> delayed_;
uint32_t nextLink_ = 1;
};
namespace transport {
Bytes Cframe(const Link& l, BYTE op, bool poll, BYTE msgId, BYTE rspId) {
Bytes b;
Put8(b, CF_FRAME | (poll ? CF_POLL : 0));
Put8(b, op);
Put8(b, msgId);
Put8(b, rspId);
Put32(b, kProtocolVersion);
Put32(b, l.sess);
Put32(b, GetTickCount());
return b;
}
void SendSack(Endpoint& ep, Link& l) {
uint64_t mask = 0;
for (auto& kv : l.early) {
BYTE d = static_cast<BYTE>(kv.first - l.nextRecv - 1);
if (d < 64) mask |= 1ull << d;
}
Bytes b;
BYTE flags = SACKF_RESPONSE | (static_cast<uint32_t>(mask) ? SACKF_SACK1 : 0) | ((mask >> 32) ? SACKF_SACK2 : 0);
Put8(b, CF_FRAME);
Put8(b, OP_SACK);
Put8(b, flags);
Put8(b, l.lastWasRetry ? 1 : 0);
Put8(b, l.nextSend);
Put8(b, l.nextRecv);
Put16(b, 0);
Put32(b, GetTickCount());
if (flags & SACKF_SACK1) Put32(b, static_cast<uint32_t>(mask));
if (flags & SACKF_SACK2) Put32(b, static_cast<uint32_t>(mask >> 32));
ep.Send(l.addr, b);
l.ackDue = false;
l.lastSend = GetTickCount();
}
DWORD Rto(const Link& l) {
DWORD r = l.rtt * 5 / 2 + 100;
return r < 100 ? 100 : r > 5000 ? 5000 : r;
}
// Frames of the queue go out while the congestion window allows.
void Pump(Endpoint& ep, Link& l) {
if (l.st != Link::kUp && l.st != Link::kClosing) return;
DWORD now = GetTickCount();
while (!l.queue.empty() && static_cast<int>(l.flight.size()) < l.cwnd) {
Link::Pending p = std::move(l.queue.front());
l.queue.pop_front();
Link::Frame f;
f.seq = l.nextSend++;
f.pkt.reserve(4 + p.payload.size());
Put8(f.pkt, p.cmd);
Put8(f.pkt, p.ctl);
Put8(f.pkt, f.seq);
Put8(f.pkt, l.nextRecv);
f.pkt.insert(f.pkt.end(), p.payload.begin(), p.payload.end());
f.sent = f.first = now;
f.rto = Rto(l);
f.tries = 1;
f.quick = 0;
f.quickAt = 0;
f.sacked = false;
ep.Send(l.addr, f.pkt);
if (p.last)
for (auto& m : l.msgs)
if (m.serial == p.msg) {
m.lastSeq = f.seq;
m.lastSent = true;
}
if (p.end) {
l.endSent = true;
l.endSeq = f.seq;
}
if (l.flight.empty()) l.progressAt = now;
l.flight.push_back(std::move(f));
l.ackDue = false; // the frame carries our ACK (bNRcv)
l.lastSend = now;
}
}
// Splits a message into frames: DATA|REL|SEQ, NEW on the first, END and POLL on the last, USER_1 for session-layer
// messages (what dpnet sends for DS2's guaranteed sends, capture W1).
void Violation(Endpoint& ep, Upper& up, Link& l);
uint64_t QueueMsg(Endpoint& ep, Upper& up, Link& l, const BYTE* data, size_t size, bool core, DPNHANDLE h, void* ctx, bool complete) {
size_t peer = 0, total = 0, count = 0;
for (auto& kv : ep.links) {
for (auto& m : kv.second->msgs) { total += m.bytes; ++count; }
}
for (auto& m : l.msgs) peer += m.bytes;
if (l.st != Link::kUp || size > kMaxMessageBytes || size > kPeerSendBytes - peer ||
size > kEndpointSendBytes - total || l.msgs.size() >= 2048 || count >= 16384) {
Violation(ep, up, l);
return 0;
}
OutMsg m{l.nextSerial++, h, ctx, complete, static_cast<DWORD>(size), GetTickCount(), 0, false};
l.msgs.push_back(m);
size_t off = 0;
do {
size_t n = size - off < kMaxFramePayload ? size - off : kMaxFramePayload;
Link::Pending p;
bool first = off == 0, last = off + n == size;
p.cmd = CMD_DATA | CMD_REL | CMD_SEQ | (first ? CMD_NEW : 0) | (last ? (CMD_END | CMD_POLL) : 0) | (core ? CMD_USER1 : 0);
p.ctl = 0;
p.payload.assign(data + off, data + off + n);
p.msg = m.serial;
p.last = last;
p.end = false;
l.queue.push_back(std::move(p));
off += n;
} while (off < size);
Pump(ep, l);
return m.serial;
}
void QueueKeepAlive(Endpoint& ep, Link& l) {
Link::Pending p;
p.cmd = CMD_DATA | CMD_REL | CMD_SEQ | CMD_POLL | CMD_NEW | CMD_END;
p.ctl = CTL_KEEPALIVE;
Put32(p.payload, l.sess);
p.msg = 0;
p.last = p.end = false;
l.queue.push_back(std::move(p));
l.lastKeepAlive = GetTickCount();
Pump(ep, l);
}
void QueueEnd(Endpoint& ep, Link& l) {
if (l.endQueued) return;
Link::Pending p;
p.cmd = CMD_DATA | CMD_REL | CMD_SEQ | CMD_POLL | CMD_NEW | CMD_END; // as dpnet (W1): 3F 08
p.ctl = CTL_END_STREAM;
p.msg = 0;
p.last = false;
p.end = true;
l.queue.push_back(std::move(p));
l.endQueued = true;
Pump(ep, l);
}
void Down(Upper& up, Link& l, HRESULT why, HRESULT sendWhy = S_OK) {
if (l.st == Link::kDown) return;
l.st = Link::kDown;
l.why = why;
for (auto& m : l.msgs)
if (m.complete) up.LinkSendDone(l, m, sendWhy != S_OK ? sendWhy : why == S_OK ? E_CONNECTIONLOST : why);
l.msgs.clear();
l.queue.clear();
l.flight.clear();
l.early.clear();
Bytes().swap(l.partial);
l.inMsg = false;
up.LinkDown(l);
}
void Violation(Endpoint& ep, Upper& up, Link& l) {
for (int i = 0; i < 3; ++i) ep.Send(l.addr, Cframe(l, OP_HARD_DISCONNECT, false, ++l.msgId, 0));
Down(up, l, E_CONNECTIONLOST);
}
// Closes gracefully: what is queued goes first, then END_STREAM; the link ends when both sides' END_STREAM
// frames are through (MC-DPL8R 3.1.4.3) or after kCloseTimeoutMs.
void Close(Endpoint& ep, Upper& up, Link& l) {
if (l.st == Link::kConnecting || l.st == Link::kAccepting) {
Down(up, l, E_USERCANCEL);
return;
}
if (l.st != Link::kUp) return;
l.st = Link::kClosing;
l.closeAt = GetTickCount();
QueueEnd(ep, l);
}
void CheckClosed(Upper& up, Link& l) {
if (l.endSent && l.endAcked && l.endRecv) Down(up, l, S_OK);
}
void Established(Endpoint& ep, Upper& up, Link& l) {
l.st = Link::kUp;
l.lastRecv = l.upAt = GetTickCount();
QueueKeepAlive(ep, l); // both sides do this right after the handshake (W1; MC-DPL8R 4.1)
up.LinkUp(l);
}
// Acknowledgments: everything before nrcv, plus the frames the SACK mask names.
void OnAck(Endpoint& ep, Upper& up, Link& l, BYTE nrcv, uint64_t sackMask) {
DWORD now = GetTickCount();
// Only ACKs inside [oldest outstanding, next unsent] can describe this window.
// In particular, a stale ACK must not selectively suppress the oldest frame's retries.
if (l.flight.empty()) return;
BYTE distance = static_cast<BYTE>(nrcv - l.flight.front().seq);
if (distance > l.flight.size()) return;
bool advanced = false, retried = false;
DWORD newestFirst = 0;
while (!l.flight.empty() && SeqBefore(l.flight.front().seq, nrcv)) {
const Link::Frame& f = l.flight.front();
if (f.tries > 1) retried = true;
newestFirst = f.first;
if (l.endSent && f.seq == l.endSeq) l.endAcked = true;
l.flight.pop_front();
advanced = true;
if (l.cwnd < kMaxWindow) ++l.cwnd;
}
// A round-trip sample comes from the newest frame acknowledged, and only when nothing acknowledged with it was
// retried: an acknowledgment that closes a gap covers frames that waited for the retry, and timing those would
// inflate the estimate and with it every retry timeout (seen as five-second stalls under 10% loss).
if (advanced) l.progressAt = now;
if (advanced && !retried) {
DWORD sample = now - newestFirst;
DWORD err = sample > l.rtt ? sample - l.rtt : l.rtt - sample;
l.rttVar = (l.rttVar * 3 + err) / 4;
l.rtt = (l.rtt * 7 + sample) / 8;
}
if (sackMask) {
for (auto& f : l.flight) {
BYTE d = static_cast<BYTE>(f.seq - nrcv - 1);
if (d < 64 && (sackMask >> d) & 1) f.sacked = true;
}
// Frames before the newest one the peer holds are gaps it reported (MC-DPL8R 3.1.5.2.3): retry them once a
// round trip plus a reordering allowance has passed since they went out (a report can predate the last
// retry's arrival, and with jitter frames overtake each other without being lost).
size_t last = l.flight.size();
while (last > 0 && !l.flight[last - 1].sacked) --last;
DWORD reorder = l.rtt / 4 + 2 * l.rttVar;
if (reorder < kShortAckMs) reorder = kShortAckMs;
for (size_t i = 0; i + 1 < last; ++i) {
Link::Frame& f = l.flight[i];
if (f.sacked || f.quickAt || f.quick >= kMaxQuickRetries) continue;
DWORD at = f.sent + l.rtt + reorder;
f.quickAt = static_cast<LONG>(at - now) > 0 ? at : now;
if (!f.quickAt) f.quickAt = 1;
}
}
if (advanced) {
while (!l.msgs.empty() && l.msgs.front().lastSent && SeqBefore(l.msgs.front().lastSeq, nrcv)) {
OutMsg m = l.msgs.front();
l.msgs.pop_front();
if (m.complete) up.LinkSendDone(l, m, S_OK);
}
Pump(ep, l);
}
CheckClosed(up, l);
}
// One in-order frame (or a placeholder for one the peer will never resend).
void Deliver(Endpoint& ep, Upper& up, Link& l, Link::In& in) {
if (in.dropped) { // an unreliable frame that was lost: a message it belonged to is gone
l.inMsg = false;
l.partial.clear();
return;
}
if (in.ctl & CTL_KEEPALIVE) return;
if (in.ctl & CTL_END_STREAM) {
l.endRecv = true;
if (l.st == Link::kUp) {
l.st = Link::kClosing;
l.closeAt = GetTickCount();
}
QueueEnd(ep, l);
return;
}
if (in.ctl & CTL_COALESCE) { // MC-DPL8R 2.2.3: up to 32 two-byte headers, then the payloads, 4-byte aligned
const Bytes& b = in.payload;
size_t i = 0, n = 0;
BYTE hdr[32][2];
while (n < 32 && i + 2 <= b.size()) {
hdr[n][0] = b[i];
hdr[n][1] = b[i + 1];
i += 2;
if (hdr[n++][1] & 0x01) break;
}
if (n % 2) i += 2;
for (size_t k = 0; k < n && i <= b.size(); ++k) {
size_t size = hdr[k][0] | (static_cast<size_t>((hdr[k][1] >> 3) & 7) << 8);
if (i + size > b.size()) break;
up.LinkMessage(l, Bytes(b.begin() + i, b.begin() + i + size), (hdr[k][1] & CMD_USER1) != 0);
i += size;
if (k + 1 < n) i = (i + 3) & ~static_cast<size_t>(3);
}
return;
}
if (in.cmd & CMD_NEW) {
l.partial.clear();
l.inMsg = true;
l.msgCmd = in.cmd;
l.partialAt = GetTickCount();
}
if (!l.inMsg) return;
if (in.payload.size() > kMaxMessageBytes - l.partial.size()) {
Violation(ep, up, l);
return;
}
l.partial.insert(l.partial.end(), in.payload.begin(), in.payload.end());
if (in.cmd & CMD_END) {
l.inMsg = false;
Bytes msg;
msg.swap(l.partial);
up.LinkMessage(l, std::move(msg), (l.msgCmd & CMD_USER1) != 0);
}
}
// Send masks run backwards from the peer's next-send sequence (MC-DPL8R 3.1.1).
void ApplySendMask(Link& l, BYTE next, uint64_t mask) {
if (l.endRecv || static_cast<BYTE>(next - l.nextRecv) > kMaxWindow) return;
for (int i = 0; i < 64; ++i) if ((mask >> i) & 1) {
BYTE seq = static_cast<BYTE>(next - 1 - i);
if (static_cast<BYTE>(seq - l.nextRecv) < kMaxWindow && !l.early.count(seq))
l.early.emplace(seq, Link::In{0, 0, Bytes(), true});
}
}
void Drain(Endpoint& ep, Upper& up, Link& l) {
while (!l.early.empty() && l.st != Link::kDown) {
auto it = l.early.find(l.nextRecv);
if (it == l.early.end()) break;
Link::In in = std::move(it->second);
l.early.erase(it);
++l.nextRecv;
Deliver(ep, up, l, in);
}
}
void OnDframe(Endpoint& ep, Upper& up, Link& l, const BYTE* p, size_t n) {
if (n < 4) return;
BYTE cmd = p[0], ctl = p[1], seq = p[2], nrcv = p[3];
size_t off = 4;
uint64_t sack = 0, send = 0;
if (ctl & CTL_SACK1) { if (off + 4 > n) return; sack |= Get32(p + off); off += 4; }
if (ctl & CTL_SACK2) { if (off + 4 > n) return; sack |= static_cast<uint64_t>(Get32(p + off)) << 32; off += 4; }
if (ctl & CTL_SEND1) { if (off + 4 > n) return; send |= Get32(p + off); off += 4; }
if (ctl & CTL_SEND2) { if (off + 4 > n) return; send |= static_cast<uint64_t>(Get32(p + off)) << 32; off += 4; }
DWORD now = GetTickCount();
l.lastRecv = now;
l.lastWasRetry = (ctl & CTL_RETRY) != 0;
OnAck(ep, up, l, nrcv, sack);
if (l.st == Link::kDown) return;
ApplySendMask(l, seq, send);
Drain(ep, up, l);
if (l.st == Link::kDown) return;
BYTE d = static_cast<BYTE>(seq - l.nextRecv);
if (d >= 64 || l.endRecv) { // a duplicate, or anything after the peer's END_STREAM: just acknowledge
SendSack(ep, l);
return;
}
if (!l.early.count(seq) || l.early[seq].dropped) {
if (n - off > kPeerReceiveBytes - Endpoint::ReceiveBytes(l) ||
n - off > kEndpointReceiveBytes - ep.ReceiveBytes()) {
Violation(ep, up, l);
return;
}
Link::In in{cmd, ctl, Bytes(p + off, p + n), false};
if (ctl & CTL_KEEPALIVE) {
if (n - off < 4 || Get32(p + off) != l.sess) return; // MC-DPL8R 3.1.5.2: must carry our session ID
in.payload.clear();
}
l.early[seq] = std::move(in);
}
Drain(ep, up, l);
if (l.st == Link::kDown) return;
if (cmd & CMD_POLL) {
SendSack(ep, l);
if (l.endRecv) { // dpnet acknowledges an END_STREAM with four SACKs (W1, W2)
SendSack(ep, l);
SendSack(ep, l);
SendSack(ep, l);
}
} else if (!l.ackDue) {
l.ackDue = true;
l.ackAt = now + (l.early.empty() ? kDelayedAckMs : kShortAckMs);
}
CheckClosed(up, l);
}
// A datagram that is not an enumeration message (lead byte non-zero).
void OnDatagram(Endpoint& ep, Upper& up, const sockaddr_in& from, const BYTE* p, size_t n) {
if (n >= 4 && (p[0] & CMD_DATA)) {
Link* l = ep.Find(from);
if (!l) return;
if (l->st == Link::kAccepting) Established(ep, up, *l); // its CONNECTED got lost; data proves the handshake
if (l->st == Link::kUp || l->st == Link::kClosing) OnDframe(ep, up, *l, p, n);
return;
}
if (n < 12 || (p[0] != CF_FRAME && p[0] != (CF_FRAME | CF_POLL))) return;
bool poll = (p[0] & CF_POLL) != 0;
BYTE op = p[1];
Link* l = ep.Find(from);
if (op == OP_SACK) {
if (!l || (l->st != Link::kUp && l->st != Link::kClosing)) return;
BYTE flags = p[2], nseq = p[4], nrcv = p[5];
if (flags & ~0x1f) return;
size_t off = 12;
uint64_t sack = 0;
if (flags & SACKF_SACK1) { if (off + 4 > n) return; sack |= Get32(p + off); off += 4; }
if (flags & SACKF_SACK2) { if (off + 4 > n) return; sack |= static_cast<uint64_t>(Get32(p + off)) << 32; off += 4; }
uint64_t send = 0;
if (flags & SACKF_SEND1) { if (off + 4 > n) return; send |= Get32(p + off); off += 4; }
if (flags & SACKF_SEND2) { if (off + 4 > n) return; send |= static_cast<uint64_t>(Get32(p + off)) << 32; off += 4; }
if (off != n) return; // no signature: signed connections are not accepted
l->lastRecv = GetTickCount();
OnAck(ep, up, *l, nrcv, sack);
if (l->st == Link::kDown) return;
BYTE before = l->nextRecv;
ApplySendMask(*l, nseq, send);
Drain(ep, up, *l);
if (l->st != Link::kDown && l->nextRecv != before) SendSack(ep, *l);
CheckClosed(up, *l);
return;
}
if (n < 16) return;
uint32_t ver = Get32(p + 4), sess = Get32(p + 8);
if (op == OP_CONNECT) {
if ((ver >> 16) != 1) return;
if (!l) {
if (!up.Listening()) return;
if (!ep.Admit(from, GetTickCount())) return;
l = ep.Add(from);
if (!l) return;
l->st = Link::kAccepting;
l->sess = sess;
l->peerMsgId = p[2];
l->hsTries = 1;
l->hsRto = 200;
l->hsNext = GetTickCount() + l->hsRto;
ep.Send(from, Cframe(*l, OP_CONNECTED, true, l->msgId, l->peerMsgId));
} else if (l->st == Link::kAccepting && l->sess == sess) {
l->peerMsgId = p[2];
ep.Send(from, Cframe(*l, OP_CONNECTED, true, l->msgId, l->peerMsgId));
}
return;
}
if (!l || l->sess != sess) return;
if (op == OP_CONNECTED) {
if (l->st == Link::kConnecting && poll) {
ep.Send(from, Cframe(*l, OP_CONNECTED, false, ++l->msgId, p[2]));
Established(ep, up, *l);
} else if (l->st == Link::kAccepting && !poll) {
Established(ep, up, *l);
} else if ((l->st == Link::kUp || l->st == Link::kClosing) && poll) {
ep.Send(from, Cframe(*l, OP_CONNECTED, false, l->msgId, p[2])); // our confirmation was lost
}
return;
}
if (op == OP_HARD_DISCONNECT && (l->st == Link::kUp || l->st == Link::kClosing)) {
for (int i = 0; i < 3; ++i) ep.Send(from, Cframe(*l, OP_HARD_DISCONNECT, false, ++l->msgId, 0));
Down(up, *l, S_OK);
}
// CONNECTED_SIGNED: signing is never requested by DS2; such a peer is ignored (MC-DPL8R 1.7)
}
// Outbound connection: CONNECT with POLL, retried 200 ms doubling, at most 14 times (MC-DPL8R 3.1.2.1).
Link* Connect(Endpoint& ep, const sockaddr_in& to) {
Link* l = ep.Find(to);
if (l) return nullptr;
l = ep.Add(to);
if (!l) return nullptr;
l->st = Link::kConnecting;
do l->sess = Rand32(); while (!l->sess);
l->hsTries = 1;
l->hsRto = 200;
l->hsNext = GetTickCount() + l->hsRto;
ep.Send(to, Cframe(*l, OP_CONNECT, true, l->msgId, 0));
return l;
}
// Timers for one link: handshake retries, frame retries, delayed ACKs, keep-alives, close timeout.
void Tick(Endpoint& ep, Upper& up, Link& l, DWORD now) {
if (l.st == Link::kConnecting || l.st == Link::kAccepting) {
if (l.st == Link::kAccepting && static_cast<LONG>(now - l.bornAt) >= static_cast<LONG>(kPendingLinkMs)) {
Violation(ep, up, l);
return;
}
if (static_cast<LONG>(now - l.hsNext) < 0) return;
if (l.hsTries >= kMaxConnectTries) {
Down(up, l, l.st == Link::kConnecting ? static_cast<HRESULT>(0x80158510) /* NORESPONSE */ : E_TIMEDOUT);
return;
}
++l.hsTries;
l.hsRto = l.hsRto * 2 > 5000 ? 5000 : l.hsRto * 2;
l.hsNext = now + l.hsRto;
if (l.st == Link::kConnecting)
ep.Send(l.addr, Cframe(l, OP_CONNECT, true, ++l.msgId, 0));
else
ep.Send(l.addr, Cframe(l, OP_CONNECTED, true, ++l.msgId, l.peerMsgId));
return;
}
if (l.st != Link::kUp && l.st != Link::kClosing) return;
if (l.inMsg && static_cast<LONG>(now - l.partialAt) >= static_cast<LONG>(kReassemblyMs)) {
Violation(ep, up, l);
return;
}
if (!l.flight.empty() && static_cast<LONG>(now - l.progressAt) >= static_cast<LONG>(kProgressMs)) {
Violation(ep, up, l);
return;
}
bool cut = false;
for (auto& f : l.flight) {
if (f.sacked) continue;
bool quick = f.quickAt && static_cast<LONG>(now - f.quickAt) >= 0;
if (!quick && static_cast<LONG>(now - f.sent) < static_cast<LONG>(f.rto)) continue;
int timed = f.tries - f.quick; // transmissions so far, not counting quick retries
if (!quick && timed > kMaxRetries) {
Down(up, l, E_CONNECTIONLOST);
return;
}
f.pkt[1] |= CTL_RETRY;
f.pkt[3] = l.nextRecv;
ep.Send(l.addr, f.pkt);
++f.tries;
f.quickAt = 0; // (a timed retry also answers any pending gap report)
if (quick) { // a quick retry keeps the timeout it had, counted from now
++f.quick;
} else {
// linear backoff for the second and third retry, then exponential, capped at 5 s (MC-DPL8R 3.1.2.5)
++timed;
DWORD base = Rto(l);
f.rto = timed <= 3 ? base * timed : base << (timed - 2 < 8 ? timed - 2 : 8);
if (f.rto > 5000) f.rto = 5000;
}
cut = true;
f.sent = now;
l.ackDue = false;
l.lastSend = now;
}
if (cut && Elapsed(now, l.lastCut, l.rtt)) { // congestion: halve the window once per round trip
l.cwnd = l.cwnd / 2 < 2 ? 2 : l.cwnd / 2;
l.lastCut = now;
}
if (l.ackDue && static_cast<LONG>(now - l.ackAt) >= 0) SendSack(ep, l);
if (l.st == Link::kUp && Elapsed(now, l.lastRecv, kKeepAliveMs) && Elapsed(now, l.lastKeepAlive, kKeepAliveMs))
QueueKeepAlive(ep, l);
if (l.st == Link::kClosing && Elapsed(now, l.closeAt, kCloseTimeoutMs)) {
// the exchange did not finish: tell the peer the link is gone rather than leave it to its own timeout
for (int i = 0; i < 3; ++i) ep.Send(l.addr, Cframe(l, OP_HARD_DISCONNECT, false, ++l.msgId, 0));
Down(up, l, S_OK);
return;
}
Pump(ep, l);
}
} // namespace transport
// ---- session messages [MC-DPL8CS] -----------------------------------------------------------------------------
enum : uint32_t { CORE_PLAYER_CONNECT_INFO = 0xC1, CORE_SEND_CONNECT_INFO = 0xC2, CORE_ACK_CONNECT_INFO = 0xC3,
CORE_CONNECT_FAILED = 0xC5, CORE_UPDATE_APPLICATION_DESC = 0xCF, CORE_TERMINATE_SESSION = 0xDF };
constexpr DWORD kDnetVersion = 7; // what dpnet sends (W1: DirectX 9.0)
constexpr DWORD kObjectClient = 0x2;
constexpr DWORD kEntryHost = 0x002, kEntryClient = 0x200, kEntryServer = 0x400;
// PLAYER_CONNECT_INFO_EX: client flag, DNET version 7, connect data, password, name (dpnet's order, W1).
Bytes BuildConnectInfo(const std::wstring& name, const std::wstring* password, const Bytes& connectData, const GUID& inst,
const GUID& app) {
Builder bb;
bb.origin = 4;
Put32(bb.b, CORE_PLAYER_CONNECT_INFO);
Put32(bb.b, kObjectClient);
Put32(bb.b, kDnetVersion);
for (int i = 0; i < 10; ++i) Put32(bb.b, 0); // name, data, password, connect data, URL (offset, size)
PutGuid(bb.b, inst);
PutGuid(bb.b, app);
Put32(bb.b, 0); // alternate addresses (none, as dpnet sent)
Put32(bb.b, 0);
bb.Var(36, connectData);
if (password) bb.Var(28, WBytes(*password));
bb.Var(12, WBytes(name));
return bb.b;
}
struct ConnectInfo {
DWORD flags = 0, dnet = 0;
std::wstring name, password;
bool hasPassword = false;
Bytes connectData;
GUID inst{}, app{};
};
bool ParseConnectInfo(const Bytes& m, ConnectInfo* ci) {
if (m.size() < 4 + 80) return false;
Span s{m.data() + 4, m.size() - 4};
const BYTE* p = m.data();
ci->flags = Get32(p + 4);
ci->dnet = Get32(p + 8);
ci->inst = GetGuid(p + 52);
ci->app = GetGuid(p + 68);
ci->hasPassword = Get32(p + 28) != 0;
return s.Str(Get32(p + 12), Get32(p + 16), &ci->name) && s.Str(Get32(p + 28), Get32(p + 32), &ci->password) &&
s.Blob(Get32(p + 36), Get32(p + 40), &ci->connectData);
}
// SEND_CONNECT_INFO: the description, the new client's DPNID, a two-entry name table (server, client), reply data.
// Variable data in dpnet's order (W1): client name, server name, application reserved data, session name, reply.
Bytes BuildSendConnectInfo(const Desc& d, DPNID client, DPNID server, DWORD version, DWORD serverVersion,
DWORD clientVersion, const std::wstring& serverName, const std::wstring& clientName,
const Bytes& reply) {
Builder bb;
bb.origin = 4;
Put32(bb.b, CORE_SEND_CONNECT_INFO);
Put32(bb.b, 0); // reply (offset, size)
Put32(bb.b, 0);
size_t at = d.WireBlock(bb);
Put32(bb.b, client);
Put32(bb.b, version);
Put32(bb.b, 0);
Put32(bb.b, 2); // entries
Put32(bb.b, 0); // memberships
size_t entry[2];
const DPNID ids[2] = {server, client};
const DWORD flags[2] = {kEntryHost | kEntryServer, kEntryClient}, versions[2] = {serverVersion, clientVersion};
for (int i = 0; i < 2; ++i) {
entry[i] = bb.b.size();
Put32(bb.b, ids[i]);
Put32(bb.b, 0);
Put32(bb.b, flags[i]);
Put32(bb.b, versions[i]);
Put32(bb.b, 0);
Put32(bb.b, kDnetVersion);
for (int k = 0; k < 6; ++k) Put32(bb.b, 0); // name, data, URL (offset, size)
}
bb.Var(entry[1] + 24, WBytes(clientName));
bb.Var(entry[0] + 24, WBytes(serverName));
d.WireTail(bb, at, true);
bb.Var(4, reply);
return bb.b;
}
Bytes BuildUpdateDesc(const Desc& d) {
Builder bb;
bb.origin = 4;
Put32(bb.b, CORE_UPDATE_APPLICATION_DESC);
size_t at = d.WireBlock(bb);
d.WireTail(bb, at, false);
return bb.b;
}
Bytes BuildConnectFailed(HRESULT hr, const Bytes& reply) {
Builder bb;
bb.origin = 4;
Put32(bb.b, CORE_CONNECT_FAILED);
Put32(bb.b, static_cast<uint32_t>(hr));
Put32(bb.b, 0);
Put32(bb.b, 0);
bb.Var(8, reply);
return bb.b;
}
Bytes BuildTerminate(const Bytes& data) {
Builder bb;
bb.origin = 4;
Put32(bb.b, CORE_TERMINATE_SESSION);
Put32(bb.b, 0);
Put32(bb.b, 0);
bb.Var(4, data);
return bb.b;
}
// ---- enumeration [MC-DPLHP] -------------------------------------------------------------------------------------
Bytes BuildEnumQuery(uint16_t payload, const GUID& app, const Bytes& data) {
Bytes b;
Put8(b, 0);
Put8(b, 2);
Put16(b, payload);
bool withApp = app != GUID{};
Put8(b, withApp ? 1 : 2);
if (withApp) PutGuid(b, app);
PutRaw(b, data.data(), data.size());
return b;
}
Bytes BuildEnumResponse(uint16_t payload, const Desc& d, const Bytes& reply) {
Builder bb;
Put8(bb.b, 0);
Put8(bb.b, 3);
Put16(bb.b, payload);
bb.origin = 4; // offsets count from ReplyOffset
Put32(bb.b, 0);
Put32(bb.b, 0);
size_t at = d.WireBlock(bb);
d.WireTail(bb, at, false);
bb.Var(4, reply);
return bb.b;
}
// ---- common object state ------------------------------------------------------------------------------------
class Base {
#ifdef DS2_DP8_TEST
friend struct ShimTest;
#endif
public:
virtual ~Base() { if (thread_) CloseHandle(thread_); }
protected:
std::mutex m_;
PFNDPNMESSAGEHANDLER pfn_ = nullptr;
void* ctx_ = nullptr;
LONG ref_ = 1;
std::atomic<LONG> lives_{1}; // external ownership, worker and active dispatch/API scopes
struct Hold {
Base* p;
explicit Hold(Base* b) : p(b) { ++p->lives_; }
~Hold() { p->Drop(); }
};
void Drop() { if (--lives_ == 0) delete this; }
std::recursive_mutex dispatch_;
unsigned callbacks_ = 0;
bool freeBuffersPending_ = false;
bool flushing_ = false; // dispatch_ held; reentrant API calls append work for the outer dispatcher
Endpoint ep_;
Events ev_; // session events queued under the lock, raised outside it
HANDLE thread_ = nullptr;
DWORD threadId_ = 0;
std::atomic<bool> stop_{false};
std::wstring name_;
CAPS caps_{sizeof(CAPS), 0, 200, 14, 0};
struct Buffer { BYTE* data; size_t size; std::shared_ptr<Link::Held> peer; };
std::map<DPNHANDLE, Buffer> buffers_; // RECEIVE buffers the game kept (returned DPNSUCCESS_PENDING)
virtual void OnDatagramLocked(const sockaddr_in& from, const BYTE* p, size_t n) = 0;
virtual void TickLocked(DWORD now) = 0;
virtual Upper* TransportUpper() { return nullptr; }
HRESULT Fire(DWORD id, void* msg) {
std::lock_guard<std::recursive_mutex> dispatch(dispatch_);
++callbacks_;
HRESULT hr = pfn_ ? pfn_(ctx_, id, msg) : S_OK;
if (--callbacks_ == 0 && freeBuffersPending_) FreeBuffers();
return hr;
}
// Lock order (the cycle this avoids was a Broken World hang: the joiner's game thread held the game's NetPipe critical
// section and called CancelAsyncOperation while the worker was inside a game callback waiting for that same section):
// - m_ (our state) is never held across a game callback or a call into the game; callbacks are queued in ev_ and
// raised here, outside m_.
// - dispatch_ is held for the length of every callback, so it is held while the game may block on its own locks.
// An API thread therefore never WAITS for dispatch_ (as in DirectPlay, where an API call returns while the
// callback thread is busy): if another thread is dispatching, it drains ev_ before it lets go (the re-check after
// the unlock below), so the events this thread queued are still delivered, in order, after the running callback.
// - Only the worker waits for dispatch_ (an API thread can be inside a callback it started itself, e.g. a Host
// callback); it holds no lock then and gives up when asked to stop.
void Flush(bool worker = false) { // events can queue further events: run until none are left
Hold hold(this);
for (;;) {
{
std::unique_lock<std::recursive_mutex> dispatch(dispatch_, std::defer_lock);
if (!dispatch.try_lock()) {
// An API callback can Close and join us while it owns dispatch_. Stop waiting
// for that lock when asked to stop, leaving completions for its outer Flush.
if (!worker || stop_) return;
Sleep(1);
continue;
}
if (flushing_) return;
flushing_ = true;
struct Reset { bool& flag; ~Reset() { flag = false; } } reset{flushing_};
for (;;) {
Events v;
{
std::lock_guard<std::mutex> l(m_);
v.swap(ev_);
}
if (v.empty()) break;
for (auto& f : v) f();
}
} // dispatch_ released: an API thread that found it busy has queued its events before, so look again
if (worker && stop_) return;
std::lock_guard<std::mutex> l(m_);
if (ev_.empty()) return;
}
}
void Deliver(DPNID from, void* pctx, const Bytes& msg, uint32_t lid = 0, Upper* up = nullptr) {
DPNHANDLE h = NewHandle();
BYTE* buf;
{
std::lock_guard<std::mutex> l(m_);
Link* peer = ep_.ById(lid);
if (lid && !peer) return;
if (peer && (msg.size() > kPeerReceiveBytes - Endpoint::ReceiveBytes(*peer) ||
msg.size() > kEndpointReceiveBytes - ep_.ReceiveBytes() ||
peer->held->count >= 2048 || ep_.heldCount >= 8192)) {
transport::Violation(ep_, *up, *peer);
return;
}
auto data = std::make_unique<BYTE[]>(msg.empty() ? 1 : msg.size());
if (!msg.empty()) std::memcpy(data.get(), msg.data(), msg.size());
buffers_.emplace(h, Buffer{data.get(), msg.size(), peer ? peer->held : nullptr});
buf = data.release();
ep_.heldBytes += msg.size(); ++ep_.heldCount;
if (peer) { peer->held->bytes += msg.size(); ++peer->held->count; }
}
MSG_RECEIVE_T r{sizeof(r), from, pctx, buf, static_cast<DWORD>(msg.size()), h, RECEIVE_GUARANTEED};
if (Fire(MSG_RECEIVE, &r) != S_PENDING) ReleaseBuffer(h);
}
void FreeBuffer(const Buffer& b) { // m_ held
ep_.heldBytes -= b.size; --ep_.heldCount;
if (b.peer) { b.peer->bytes -= b.size; --b.peer->count; }
delete[] b.data;
}
HRESULT ReleaseBuffer(DPNHANDLE h) {
std::lock_guard<std::mutex> l(m_);
auto it = buffers_.find(h);
if (it == buffers_.end()) return E_INVALIDHANDLE;
FreeBuffer(it->second);
buffers_.erase(it);
return S_OK;
}
void QueueSendComplete(const OutMsg& m, HRESULT hr) {
ev_.push_back([this, m, hr] {
MSG_SEND_COMPLETE_T sc{sizeof(sc), m.handle, m.ctx, hr, GetTickCount() - m.start, 0, 0, SENDCOMPLETE_GUARANTEED,
nullptr, 0};
Fire(MSG_SEND_COMPLETE, &sc);
});
}
bool OpenSocket(DWORD first, DWORD last) {
if (ep_.s != INVALID_SOCKET) return true;
ep_.s = BindUdp(first, last, &ep_.port);
return ep_.s != INVALID_SOCKET;
}
static DWORD WINAPI ThreadProc(LPVOID p) {
Base* self = static_cast<Base*>(p);
self->Run();
self->Drop();
return 0;
}
bool StartWorker() {
if (thread_) return true;
stop_ = false;
++lives_;
thread_ = CreateThread(nullptr, 0, ThreadProc, static_cast<Base*>(this), 0, &threadId_);
if (!thread_) Drop();
return thread_ != nullptr;
}
void Run() {
Bytes buf(2048);
while (!stop_) {
fd_set rd;
FD_ZERO(&rd);
FD_SET(ep_.s, &rd);
timeval tv{0, 10000};
int n = 0;
sockaddr_in from{};
if (select(0, &rd, nullptr, nullptr, &tv) > 0) {
int fl = sizeof(from);
n = recvfrom(ep_.s, reinterpret_cast<char*>(buf.data()), static_cast<int>(buf.size()), 0,
reinterpret_cast<sockaddr*>(&from), &fl);
}
try {
{
std::lock_guard<std::mutex> l(m_);
if (n > 0) {
WireLogLine("RCV", "<-", ep_.port, from, buf.data(), static_cast<size_t>(n));
OnDatagramLocked(from, buf.data(), static_cast<size_t>(n));
}
DWORD now = GetTickCount(); // packet handling may establish/close using a later tick
TickLocked(now);
ep_.FlushDelayed(now);
ep_.Sweep();
}
Flush(true);
} catch (const std::bad_alloc&) {
// Contain peer-controlled allocation failures at the worker boundary. Reclaim the
// peer's storage before attempting the small disconnect/completion allocations.
std::lock_guard<std::mutex> l(m_);
if (Link* x = ep_.Find(from)) {
x->early.clear(); Bytes().swap(x->partial);
x->queue.clear(); x->flight.clear();
try { transport::Violation(ep_, *TransportUpper(), *x); }
catch (const std::bad_alloc&) { stop_ = true; }
}
}
}
std::lock_guard<std::mutex> l(m_);
if (ep_.s != INVALID_SOCKET) {
closesocket(ep_.s);
ep_.s = INVALID_SOCKET;
}
}
void StopWorker() {
stop_ = true;
if (thread_) {
// A callback may close/release its own object. The worker owns a lifetime reference and
// closes its socket on exit; never free its handle or shared buffers while it runs.
if (GetCurrentThreadId() == threadId_) return;
WaitForSingleObject(thread_, INFINITE);
CloseHandle(thread_);
thread_ = nullptr;
}
if (ep_.s != INVALID_SOCKET) {
closesocket(ep_.s);
ep_.s = INVALID_SOCKET;
}
}
// Lets the worker finish graceful closes (not from the worker itself: it would wait for itself).
void WaitLinksDown(DWORD ms) {
if (!thread_ || GetCurrentThreadId() == threadId_) return;
for (DWORD t0 = GetTickCount(); GetTickCount() - t0 < ms; Sleep(10)) {
std::lock_guard<std::mutex> l(m_);
if (ep_.links.empty()) return;
}
}
// Links still closing when we give up waiting get a HARD_DISCONNECT (three, as MC-DPL8R 3.1.4.5 recommends), so
// the peer knows at once instead of after its retry timeout. Lock held.
void HardDisconnectRemaining() {
for (auto& kv : ep_.links) {
Link& l = *kv.second;
if (l.st == Link::kDown) continue;
if (l.st == Link::kUp || l.st == Link::kClosing)
for (int i = 0; i < 3; ++i) ep_.Send(l.addr, transport::Cframe(l, OP_HARD_DISCONNECT, false, ++l.msgId, 0));
transport::Down(*TransportUpper(), l, S_OK, E_USERCANCEL);
}
ep_.FlushDelayed(GetTickCount(), true); // test delays: send what is queued before the socket closes
}
void FreeBuffers() {
std::lock_guard<std::recursive_mutex> dispatch(dispatch_);
if (callbacks_) { freeBuffersPending_ = true; return; }
freeBuffersPending_ = false;
std::lock_guard<std::mutex> l(m_);
for (auto& kv : buffers_) FreeBuffer(kv.second);
buffers_.clear();
}
static HRESULT SpCaps(const GUID* sp, SP_CAPS* c) {
if (!c) return E_POINTER;
if (sp && *sp != SP_TCPIP) return static_cast<HRESULT>(0x80158000 + 0x380);
DWORD size = c->dwSize;
std::memset(c, 0, sizeof(*c));
c->dwSize = size ? size : sizeof(*c);
c->dwFlags = 0xF; // what dpnet reports for TCP/IP (trace T4)
c->dwNumThreads = 1;
c->dwDefaultEnumCount = 5;
c->dwDefaultEnumRetryInterval = 1500;
c->dwDefaultEnumTimeout = 1500;
c->dwMaxEnumPayloadSize = 983;
c->dwBuffersPerThread = 1;
c->dwSystemBufferSize = 65536;
return S_OK;
}
static HRESULT EnumSps(const GUID* sp, SERVICE_PROVIDER_INFO* buf, DWORD* size, DWORD* count) {
if (!size || !count) return E_POINTER;
if (sp) return E_DOESNOTEXIST;
static const WCHAR kName[] = L"DirectPlay8 TCP/IP Service Provider";
DWORD need = sizeof(SERVICE_PROVIDER_INFO) + sizeof(kName);
if (!buf || *size < need) {
*size = need;
*count = 1;
return E_BUFFERTOOSMALL;
}
buf->dwFlags = 0;
buf->guid = SP_TCPIP;
buf->pwszName = reinterpret_cast<WCHAR*>(buf + 1);
std::memcpy(buf->pwszName, kName, sizeof(kName));
buf->pvReserved = nullptr;
buf->dwReserved = 0;
*count = 1;
return S_OK;
}
static HRESULT PlayerInfo(const std::wstring& name, DWORD flags, PLAYER_INFO* info, DWORD* size) {
DWORD need = static_cast<DWORD>(sizeof(PLAYER_INFO) + (name.size() + 1) * 2);
if (!info || *size < need) {
*size = need;
return E_BUFFERTOOSMALL;
}
info->dwSize = sizeof(PLAYER_INFO);
info->dwInfoFlags = INFO_NAME | INFO_DATA;
info->pwszName = reinterpret_cast<WCHAR*>(info + 1);
std::memcpy(info->pwszName, name.c_str(), (name.size() + 1) * 2);
info->pvData = nullptr;
info->dwDataSize = 0;
info->dwPlayerFlags = flags;
*size = need;
return S_OK;
}
static Bytes Gather(const BUFFER_DESC* bufs, DWORD n) {
Bytes b;
for (DWORD i = 0; bufs && i < n; ++i) PutRaw(b, bufs[i].pBufferData, bufs[i].dwBufferSize);
return b;
}
};
// ---- DirectPlay8Server ----------------------------------------------------------------------------------------
class Server final : public IDirectPlay8Server, Base, Upper {
#ifdef DS2_DP8_TEST
friend struct ShimTest;
#endif
public:
HRESULT __stdcall QueryInterface(REFIID iid, void** out) override {
if (!out) return E_POINTER;
if (iid == IID_IUnknown || iid == IID_Server) {
*out = static_cast<IDirectPlay8Server*>(this);
AddRef();
return S_OK;
}
*out = nullptr;
return E_NOINTERFACE;
}
ULONG __stdcall AddRef() override { return InterlockedIncrement(&ref_); }
ULONG __stdcall Release() override {
LONG r = InterlockedDecrement(&ref_);
if (!r) {
Close(0);
Drop();
}
return r;
}
HRESULT __stdcall Initialize(void* ctx, PFNDPNMESSAGEHANDLER pfn, DWORD) override {
if (!pfn) return E_INVALIDARG;
if (pfn_) return E_ALREADYINITIALIZED;
ctx_ = ctx;
pfn_ = pfn;
return S_OK;
}
HRESULT __stdcall EnumServiceProviders(const GUID* sp, const GUID*, SERVICE_PROVIDER_INFO* buf, DWORD* size,
DWORD* count, DWORD) override {
return EnumSps(sp, buf, size, count);
}
HRESULT __stdcall CancelAsyncOperation(DPNHANDLE, DWORD) override { return S_OK; }
HRESULT __stdcall GetSendQueueInfo(DPNID id, DWORD* msgs, DWORD* bytes, DWORD) override {
std::lock_guard<std::mutex> l(m_);
Link* p = Player(id, 2);
if (!p) return E_INVALIDPLAYER;
DWORD b = 0;
for (auto& m : p->msgs) b += m.bytes;
if (msgs) *msgs = static_cast<DWORD>(p->msgs.size());
if (bytes) *bytes = b;
return S_OK;
}
HRESULT __stdcall GetApplicationDesc(APPLICATION_DESC* buf, DWORD* size, DWORD) override {
if (!size) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
if (!hosting_) return E_NOCONNECTION;
desc_.curPlayers = Players();
DWORD need = desc_.Size();
if (!buf || *size < need) {
*size = need;
return E_BUFFERTOOSMALL;
}
desc_.Fill(buf);
*size = need;
return S_OK;
}
HRESULT __stdcall SetServerInfo(const PLAYER_INFO* info, void*, DPNHANDLE* h, DWORD) override {
if (!info) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
if (info->dwInfoFlags & INFO_NAME) name_ = info->pwszName ? info->pwszName : L"";
if (h) *h = 0;
return S_OK;
}
HRESULT __stdcall GetClientInfo(DPNID id, PLAYER_INFO* info, DWORD* size, DWORD) override {
if (!size) return E_POINTER;
std::wstring name;
{
std::lock_guard<std::mutex> l(m_);
Link* p = Player(id, 2);
if (!p) return E_INVALIDPLAYER; // also for the host's own player, like dpnet (trace T4)
name = p->name;
}
return PlayerInfo(name, 0, info, size);
}
HRESULT __stdcall GetClientAddress(DPNID id, IDirectPlay8Address** out, DWORD) override {
if (!out) return E_POINTER;
sockaddr_in a;
{
std::lock_guard<std::mutex> l(m_);
Link* p = Player(id, 2);
if (!p) return E_INVALIDPLAYER;
a = p->addr;
}
auto* addr = new Address;
addr->SetHost(a);
*out = addr;
return S_OK;
}
HRESULT __stdcall GetLocalHostAddresses(IDirectPlay8Address** out, DWORD* count, DWORD) override {
if (!count) return E_POINTER;
if (!hosting_) return E_NOCONNECTION;
if (!out || *count < 1) {
*count = 1;
return E_BUFFERTOOSMALL;
}
sockaddr_in a{};
a.sin_family = AF_INET;
a.sin_port = htons(ep_.port);
if (!mp::PrimaryIPv4(&a.sin_addr)) a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
auto* addr = new Address;
addr->SetHost(a);
out[0] = addr;
*count = 1;
return S_OK;
}
HRESULT __stdcall SetApplicationDesc(const APPLICATION_DESC* d, DWORD) override {
Hold hold(this);
if (!d) return E_POINTER;
{
std::lock_guard<std::mutex> l(m_);
if (!hosting_) return E_NOCONNECTION;
GUID inst = desc_.instance;
desc_.From(d);
desc_.instance = inst; // the instance GUID stays the session's
desc_.curPlayers = Players();
Bytes cf = BuildUpdateDesc(desc_); // dpnet tells every connected client (0xCF, capture W1)
for (auto& kv : ep_.links)
if (kv.second->stage >= 2 && kv.second->stage <= 3 && kv.second->st == Link::kUp)
transport::QueueMsg(ep_, *this, *kv.second, cf.data(), cf.size(), true, 0, nullptr, false);
ev_.push_back([this] { Fire(MSG_APPLICATION_DESC, nullptr); }); // via Flush: an API thread never waits for a running callback
}
Flush();
return S_OK;
}
HRESULT __stdcall Host(const APPLICATION_DESC* d, IDirectPlay8Address** devices, DWORD count, const void*, const void*,
void* pctx, DWORD) override {
Hold hold(this);
if (!d) return E_POINTER;
if (!pfn_) return E_UNINITIALIZED;
if (hosting_) return static_cast<HRESULT>(0x80158250); // DPNERR_HOSTING
DWORD want = count && devices ? PortOf(devices[0]) : 0;
if (!(want ? OpenSocket(want, want) : OpenSocket(kFirstPort, kLastPort)))
return static_cast<HRESULT>(0x80158200); // DPNERR_ENDPOINTNOTRECEIVING
{
std::lock_guard<std::mutex> l(m_);
desc_.From(d);
uint32_t r[4] = {Rand32(), Rand32(), Rand32(), Rand32()};
std::memcpy(&desc_.instance, r, sizeof(GUID));
ntVersion_ = 2; // name table: version 1 = the all-players group, 2 = the host (MC-DPL8CS 2.2.6/2.2.7)
nextIndex_ = 3;
hostId_ = Dpnid(2, 2);
hostCtx_ = pctx;
hosting_ = true;
}
ds2::log().Write("dp8 shim: hosting \"%ls\" on UDP %u (max %lu players)", desc_.name.c_str(), ep_.port, desc_.maxPlayers);
MSG_CREATE_PLAYER_T cp{sizeof(cp), hostId_, pctx};
Fire(MSG_CREATE_PLAYER, &cp);
hostCtx_ = cp.pvPlayerContext;
{
std::lock_guard<std::mutex> l(m_);
if (hosting_) StartWorker(); // Host's synchronous callback may have closed/released us
}
return S_OK;
}
HRESULT __stdcall SendTo(DPNID id, const BUFFER_DESC* bufs, DWORD n, DWORD, void* ctx, DPNHANDLE* h, DWORD flags) override {
if (!bufs && n) return E_POINTER;
Bytes data = Gather(bufs, n);
DPNHANDLE handle = NewHandle();
bool complete = !(flags & SEND_NOCOMPLETE);
{
std::lock_guard<std::mutex> l(m_);
if (!hosting_) return E_NOCONNECTION;
if (id == ALL_PLAYERS_GROUP) {
for (auto& kv : ep_.links)
if (kv.second->stage == 3 && kv.second->st == Link::kUp)
transport::QueueMsg(ep_, *this, *kv.second, data.data(), data.size(), false, handle, ctx, false);
complete = false;
} else {
Link* p = Player(id, 3);
if (!p || p->st != Link::kUp) return E_INVALIDPLAYER;
if (!transport::QueueMsg(ep_, *this, *p, data.data(), data.size(), false, handle, ctx, complete))
return E_CONNECTIONLOST;
}
}
if (h) *h = handle;
return complete ? S_PENDING : S_OK;
}
HRESULT __stdcall CreateGroup(const PLAYER_INFO*, void*, void*, DPNHANDLE*, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall DestroyGroup(DPNID, void*, DPNHANDLE*, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall AddPlayerToGroup(DPNID, DPNID, void*, DPNHANDLE*, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall RemovePlayerFromGroup(DPNID, DPNID, void*, DPNHANDLE*, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall SetGroupInfo(DPNID, PLAYER_INFO*, void*, DPNHANDLE*, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall GetGroupInfo(DPNID, PLAYER_INFO*, DWORD*, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall EnumPlayersAndGroups(DPNID* ids, DWORD* count, DWORD) override {
if (!count) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
std::vector<DPNID> v{hostId_};
for (auto& kv : ep_.links)
if (kv.second->stage == 3) v.push_back(kv.second->player);
if (!ids || *count < v.size()) {
*count = static_cast<DWORD>(v.size());
return E_BUFFERTOOSMALL;
}
std::memcpy(ids, v.data(), v.size() * sizeof(DPNID));
*count = static_cast<DWORD>(v.size());
return S_OK;
}
HRESULT __stdcall EnumGroupMembers(DPNID, DPNID*, DWORD*, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall Close(DWORD) override {
Hold hold(this);
{
std::lock_guard<std::mutex> l(m_);
if (!hosting_) return S_OK;
hosting_ = false;
for (auto& kv : ep_.links) transport::Close(ep_, *this, *kv.second); // END_STREAM to each (W2)
}
Flush();
WaitLinksDown(3000);
{
std::lock_guard<std::mutex> l(m_);
HardDisconnectRemaining();
}
StopWorker();
Flush();
std::vector<std::pair<DPNID, void*>> left;
{
std::lock_guard<std::mutex> l(m_);
for (auto& kv : ep_.links)
if (kv.second->stage == 3 && kv.second->st != Link::kDown) left.push_back({kv.second->player, kv.second->pctx});
ep_.links.clear();
}
for (auto& p : left) {
MSG_DESTROY_PLAYER_T dp{sizeof(dp), p.first, p.second, DESTROY_NORMAL};
Fire(MSG_DESTROY_PLAYER, &dp);
}
MSG_DESTROY_PLAYER_T dp{sizeof(dp), hostId_, hostCtx_, DESTROY_NORMAL};
Fire(MSG_DESTROY_PLAYER, &dp);
FreeBuffers();
ds2::log().Write("dp8 shim: host closed");
return S_OK;
}
HRESULT __stdcall DestroyClient(DPNID id, const void* data, DWORD size, DWORD) override {
Hold hold(this);
{
std::lock_guard<std::mutex> l(m_);
Link* p = Player(id, 3);
if (!p || p->st != Link::kUp) return E_INVALIDPLAYER;
Bytes df = BuildTerminate(Bytes(static_cast<const BYTE*>(data), static_cast<const BYTE*>(data) + (data ? size : 0)));
transport::QueueMsg(ep_, *this, *p, df.data(), df.size(), true, 0, nullptr, false);
p->kicked = true;
transport::Close(ep_, *this, *p);
}
Flush();
return S_OK;
}
HRESULT __stdcall ReturnBuffer(DPNHANDLE h, DWORD) override { return ReleaseBuffer(h); }
HRESULT __stdcall GetPlayerContext(DPNID id, void** ctx, DWORD) override {
if (!ctx) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
if (id == hostId_) {
*ctx = hostCtx_;
return S_OK;
}
Link* p = Player(id, 3);
if (!p) return E_INVALIDPLAYER;
*ctx = p->pctx;
return S_OK;
}
HRESULT __stdcall GetGroupContext(DPNID, void**, DWORD) override { return E_NOTIMPL; }
HRESULT __stdcall GetCaps(CAPS* c, DWORD) override {
if (!c) return E_POINTER;
*c = caps_;
return S_OK;
}
HRESULT __stdcall SetCaps(const CAPS* c, DWORD) override {
if (!c) return E_POINTER;
caps_ = *c;
return S_OK;
}
HRESULT __stdcall SetSPCaps(const GUID*, const SP_CAPS*, DWORD) override { return S_OK; }
HRESULT __stdcall GetSPCaps(const GUID* sp, SP_CAPS* c, DWORD) override { return SpCaps(sp, c); }
HRESULT __stdcall GetConnectionInfo(DPNID id, CONNECTION_INFO* info, DWORD) override {
if (!info) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
Link* p = Player(id, 3);
if (!p) return E_INVALIDPLAYER;
DWORD size = info->dwSize;
std::memset(info, 0, sizeof(*info));
info->dwSize = size ? size : sizeof(*info);
info->dwRoundTripLatencyMS = p->rtt;
return S_OK;
}
HRESULT __stdcall RegisterLobby(DPNHANDLE, void*, DWORD) override { return E_NOTIMPL; }
static constexpr DWORD kFirstPort = 2302, kLastPort = 2400; // dpnet's range when no port is given
private:
DPNID Dpnid(DWORD index, DWORD version) const { return ((version << 20) | index) ^ desc_.instance.Data1; }
Link* Player(DPNID id, int minStage) {
for (auto& kv : ep_.links)
if (kv.second->player == id && kv.second->stage >= minStage && kv.second->stage <= 3) return kv.second.get();
return nullptr;
}
DWORD Players() { // the host plus everyone admitted (SEND_CONNECT_INFO already counts the joiner, W1)
DWORD n = 1;
for (auto& kv : ep_.links)
if (kv.second->stage >= 2 && kv.second->stage <= 3 && kv.second->st != Link::kDown) ++n;
return n;
}
// -- transport callbacks (lock held) --
Upper* TransportUpper() override { return this; }
bool Listening() override { return hosting_; }
void LinkUp(Link& l) override { l.stage = 0; }
void LinkSendDone(Link&, const OutMsg& m, HRESULT hr) override { QueueSendComplete(m, hr); }
void LinkDown(Link& l) override {
if (l.stage == 3) {
DPNID id = l.player;
void* pctx = l.pctx;
DWORD reason = l.kicked ? DESTROY_HOSTDESTROYEDPLAYER : l.why == S_OK ? DESTROY_NORMAL : DESTROY_CONNECTIONLOST;
ds2::log().Write("dp8 shim: player %08lX left (reason %lu)", id, reason);
ev_.push_back([this, id, pctx, reason] {
MSG_DESTROY_PLAYER_T dp{sizeof(dp), id, pctx, reason};
Fire(MSG_DESTROY_PLAYER, &dp);
});
} else if (l.stage == 2) {
void* pctx = l.pctx;
ev_.push_back([this, pctx] {
MSG_INDICATED_CONNECT_ABORTED_T a{sizeof(a), pctx};
Fire(MSG_INDICATED_CONNECT_ABORTED, &a);
});
}
l.stage = 5;
}
void LinkMessage(Link& l, Bytes&& m, bool core) override {
if (!core) {
if (l.stage != 3) return;
uint32_t lid = l.id;
DPNID id = l.player;
ev_.push_back([this, lid, id, msg = std::move(m)] {
void* pctx = nullptr;
{
std::lock_guard<std::mutex> g(m_);
if (Link* x = ep_.ById(lid)) pctx = x->pctx;
}
Deliver(id, pctx, msg, lid, this);
});
return;
}
if (m.size() < 4) return;
uint32_t type = Get32(m.data());
if (type == CORE_PLAYER_CONNECT_INFO && l.stage == 0) {
ConnectInfo ci;
if (!ParseConnectInfo(m, &ci)) return;
HRESULT refuse = S_OK;
if (!(ci.flags & kObjectClient)) refuse = static_cast<HRESULT>(0x80158390); // INVALIDINTERFACE
else if (ci.app != desc_.app) refuse = static_cast<HRESULT>(0x80158300); // INVALIDAPPLICATION
else if (ci.inst != GUID{} && ci.inst != desc_.instance) refuse = static_cast<HRESULT>(0x80158380); // INVALIDINSTANCE
else if ((desc_.flags & SESSION_REQUIREPASSWORD) && ci.password != desc_.password)
refuse = static_cast<HRESULT>(0x80158410); // INVALIDPASSWORD
else if (desc_.maxPlayers && Players() >= desc_.maxPlayers) refuse = E_SESSIONFULL;
if (refuse != S_OK) {
Bytes c5 = BuildConnectFailed(refuse, Bytes());
transport::QueueMsg(ep_, *this, l, c5.data(), c5.size(), true, 0, nullptr, false);
l.stage = 4;
transport::Close(ep_, *this, l);
ds2::log().Write("dp8 shim: refused %s (0x%08lX)", AddrText(l.addr).c_str(), static_cast<unsigned long>(refuse));
return;
}
l.stage = 1;
l.name = ci.name;
uint32_t lid = l.id;
sockaddr_in from = l.addr;
ev_.push_back([this, lid, from, data = std::move(ci.connectData)]() mutable { Indicate(lid, from, data); });
} else if (type == CORE_ACK_CONNECT_INFO && l.stage == 2) {
l.stage = 3;
DPNID id = l.player;
uint32_t lid = l.id;
void* pctx = l.pctx;
ds2::log().Write("dp8 shim: player %08lX \"%ls\" joined from %s", id, l.name.c_str(), AddrText(l.addr).c_str());
ev_.push_back([this, lid, id, pctx] {
MSG_CREATE_PLAYER_T cp{sizeof(cp), id, pctx};
Fire(MSG_CREATE_PLAYER, &cp);
std::lock_guard<std::mutex> g(m_);
if (Link* x = ep_.ById(lid)) x->pctx = cp.pvPlayerContext;
});
}
}
// INDICATE_CONNECT (outside the lock), then SEND_CONNECT_INFO or CONNECT_FAILED.
void Indicate(uint32_t lid, const sockaddr_in& from, Bytes& data) {
auto* player = new Address;
player->SetHost(from);
auto* device = new Address;
device->SetSP(&SP_TCPIP);
device->SetDword(L"port", ep_.port);
MSG_INDICATE_CONNECT_T ic{sizeof(ic), data.empty() ? nullptr : data.data(), static_cast<DWORD>(data.size()), nullptr, 0,
nullptr, nullptr, player, device};
HRESULT hr = Fire(MSG_INDICATE_CONNECT, &ic);
player->Release();
device->Release();
Bytes reply(static_cast<BYTE*>(ic.pvReplyData), static_cast<BYTE*>(ic.pvReplyData) + (ic.pvReplyData ? ic.dwReplyDataSize : 0));
if (ic.pvReplyData) {
MSG_RETURN_BUFFER_T rb{sizeof(rb), S_OK, ic.pvReplyData, ic.pvReplyContext};
Fire(MSG_RETURN_BUFFER, &rb);
}
bool aborted = false;
{
std::lock_guard<std::mutex> g(m_);
Link* l = ep_.ById(lid);
if (!l || l->st != Link::kUp || !hosting_) {
aborted = SUCCEEDED(hr);
} else if (FAILED(hr)) {
Bytes c5 = BuildConnectFailed(E_HOSTREJECTEDCONNECTION, reply);
transport::QueueMsg(ep_, *this, *l, c5.data(), c5.size(), true, 0, nullptr, false);
l->stage = 4;
transport::Close(ep_, *this, *l);
} else {
l->pctx = ic.pvPlayerContext;
DWORD index = nextIndex_++, version = ++ntVersion_;
l->player = Dpnid(index, version);
l->stage = 2;
desc_.curPlayers = Players();
Bytes c2 = BuildSendConnectInfo(desc_, l->player, hostId_, ntVersion_, 2, version, name_, l->name, reply);
transport::QueueMsg(ep_, *this, *l, c2.data(), c2.size(), true, 0, nullptr, false);
}
}
if (aborted) {
MSG_INDICATED_CONNECT_ABORTED_T a{sizeof(a), ic.pvPlayerContext};
Fire(MSG_INDICATED_CONNECT_ABORTED, &a);
}
}
// An EnumQuery: the game decides whether to answer (it does not while the session is locked, status.md).
void OnDatagramLocked(const sockaddr_in& from, const BYTE* p, size_t n) override {
if (p[0] != 0) {
transport::OnDatagram(ep_, *this, from, p, n);
return;
}
if (n < 5 || p[1] != 2 || !hosting_) return;
uint16_t payload = Get16(p + 2);
BYTE type = p[4];
size_t off = 5;
if (type == 1) {
if (n < 21 || GetGuid(p + 5) != desc_.app) return;
off = 21;
} else if (type != 2) {
return;
}
Bytes data(p + off, p + n);
ev_.push_back([this, from, payload, data = std::move(data)]() mutable { EnumQuery(from, payload, data); });
}
void EnumQuery(const sockaddr_in& from, uint16_t payload, Bytes& data) {
auto* sender = new Address;
sender->SetHost(from);
auto* device = new Address;
device->SetSP(&SP_TCPIP);
device->SetDword(L"port", ep_.port);
MSG_ENUM_HOSTS_QUERY_T q{sizeof(q), sender, device, data.empty() ? nullptr : data.data(), static_cast<DWORD>(data.size()),
983, nullptr, 0, nullptr};
HRESULT hr = Fire(MSG_ENUM_HOSTS_QUERY, &q);
sender->Release();
device->Release();
if (SUCCEEDED(hr)) { // a failure means "do not answer", as with dpnet
std::lock_guard<std::mutex> g(m_);
if (hosting_) {
desc_.curPlayers = Players();
Bytes reply(static_cast<BYTE*>(q.pvResponseData),
static_cast<BYTE*>(q.pvResponseData) + (q.pvResponseData ? q.dwResponseDataSize : 0));
ep_.Send(from, BuildEnumResponse(payload, desc_, reply));
}
}
if (q.pvResponseData) {
MSG_RETURN_BUFFER_T rb{sizeof(rb), S_OK, q.pvResponseData, q.pvResponseContext};
Fire(MSG_RETURN_BUFFER, &rb);
}
}
void TickLocked(DWORD now) override {
for (auto& kv : ep_.links) {
Link& l = *kv.second;
transport::Tick(ep_, *this, l, now);
if (l.st == Link::kUp && l.stage < 3 && static_cast<LONG>(now - l.bornAt) >= static_cast<LONG>(kPendingLinkMs))
transport::Violation(ep_, *this, l); // whole unauthenticated handshake has one deadline
}
}
Desc desc_;
bool hosting_ = false;
DPNID hostId_ = 0;
void* hostCtx_ = nullptr;
DWORD ntVersion_ = 0, nextIndex_ = 0;
};
// ---- DirectPlay8Client ----------------------------------------------------------------------------------------
class Client final : public IDirectPlay8Client, Base, Upper {
#ifdef DS2_DP8_TEST
friend struct ShimTest;
#endif
public:
HRESULT __stdcall QueryInterface(REFIID iid, void** out) override {
if (!out) return E_POINTER;
if (iid == IID_IUnknown || iid == IID_Client) {
*out = static_cast<IDirectPlay8Client*>(this);
AddRef();
return S_OK;
}
*out = nullptr;
return E_NOINTERFACE;
}
ULONG __stdcall AddRef() override { return InterlockedIncrement(&ref_); }
ULONG __stdcall Release() override {
LONG r = InterlockedDecrement(&ref_);
if (!r) {
Close(0);
Drop();
}
return r;
}
HRESULT __stdcall Initialize(void* ctx, PFNDPNMESSAGEHANDLER pfn, DWORD) override {
if (!pfn) return E_INVALIDARG;
if (pfn_) return E_ALREADYINITIALIZED;
ctx_ = ctx;
pfn_ = pfn;
return S_OK;
}
HRESULT __stdcall EnumServiceProviders(const GUID* sp, const GUID*, SERVICE_PROVIDER_INFO* buf, DWORD* size,
DWORD* count, DWORD) override {
return EnumSps(sp, buf, size, count);
}
HRESULT __stdcall EnumHosts(APPLICATION_DESC* desc, IDirectPlay8Address* host, IDirectPlay8Address*, void* data,
DWORD size, DWORD count, DWORD retry, DWORD timeout, void* ctx, DPNHANDLE* h,
DWORD flags) override {
if (!pfn_) return E_UNINITIALIZED;
if (flags & OP_SYNC) return E_NOTIMPL; // DS2 enumerates asynchronously (trace)
Enum e;
if (!ResolveAddress(host, &e.to, Server::kFirstPort)) { // no host name: LAN broadcast
e.to.sin_family = AF_INET;
e.to.sin_addr.s_addr = htonl(INADDR_BROADCAST);
e.to.sin_port = htons(static_cast<u_short>(PortOf(host) ? PortOf(host) : Server::kFirstPort));
}
if (!EnsureSocket()) return static_cast<HRESULT>(0x80158200);
e.handle = NewHandle();
e.ctx = ctx;
e.app = desc ? desc->guidApplication : GUID{};
e.data.assign(static_cast<BYTE*>(data), static_cast<BYTE*>(data) + (data ? size : 0));
e.left = count ? count : 5;
e.interval = retry ? retry : 1500;
e.timeout = timeout ? timeout : 1500;
e.next = GetTickCount();
{
std::lock_guard<std::mutex> l(m_);
enums_.push_back(e);
}
ds2::log().Write("dp8 shim: enumerating %s from UDP %u", AddrText(e.to).c_str(), ep_.port);
if (h) *h = e.handle;
return S_PENDING;
}
HRESULT __stdcall CancelAsyncOperation(DPNHANDLE h, DWORD flags) override {
Hold hold(this);
{
std::lock_guard<std::mutex> l(m_);
for (auto it = enums_.begin(); it != enums_.end();) {
if ((h && it->handle == h) || (!h && (flags & (CANCEL_ENUM | CANCEL_ALL)))) {
*it->live = false;
DPNHANDLE eh = it->handle;
void* ectx = it->ctx;
ev_.push_back([this, eh, ectx] {
MSG_ASYNC_OP_COMPLETE_T ac{sizeof(ac), eh, ectx, E_USERCANCEL};
Fire(MSG_ASYNC_OP_COMPLETE, &ac);
});
it = enums_.erase(it);
} else {
++it;
}
}
if (state_ == kConnecting && ((h && h == connectHandle_) || (!h && (flags & (CANCEL_CONNECT | CANCEL_ALL))))) {
FailConnect(E_USERCANCEL, Bytes());
if (Link* x = ep_.ById(link_)) transport::Close(ep_, *this, *x);
}
}
Flush();
return S_OK;
}
HRESULT __stdcall Connect(const APPLICATION_DESC* desc, IDirectPlay8Address* host, IDirectPlay8Address*, const void*,
const void*, const void* data, DWORD size, void* ctx, DPNHANDLE* h, DWORD flags) override {
if (!desc || !host) return E_POINTER;
if (!pfn_) return E_UNINITIALIZED;
if (flags & OP_SYNC) return E_NOTIMPL; // DS2 connects asynchronously (trace)
sockaddr_in to;
if (!ResolveAddress(host, &to, Server::kFirstPort)) return E_ADDRESSING;
if (!EnsureSocket()) return static_cast<HRESULT>(0x80158200);
std::lock_guard<std::mutex> l(m_);
if (state_ != kIdle) return static_cast<HRESULT>(0x80158060); // DPNERR_ALREADYCONNECTED
Link* x = transport::Connect(ep_, to);
if (!x) return static_cast<HRESULT>(0x80158060);
link_ = x->id;
connectData_.assign(static_cast<const BYTE*>(data), static_cast<const BYTE*>(data) + (data ? size : 0));
hasPassword_ = desc->pwszPassword != nullptr;
password_ = desc->pwszPassword ? desc->pwszPassword : L"";
connectApp_ = desc->guidApplication;
connectInst_ = desc->guidInstance;
connectHandle_ = NewHandle();
connectCtx_ = ctx;
connectStart_ = GetTickCount();
terminated_ = false;
state_ = kConnecting;
if (h) *h = connectHandle_;
ds2::log().Write("dp8 shim: connecting to %s", AddrText(to).c_str());
return S_PENDING;
}
HRESULT __stdcall Send(const BUFFER_DESC* bufs, DWORD n, DWORD, void* ctx, DPNHANDLE* h, DWORD flags) override {
if (!bufs && n) return E_POINTER;
Bytes data = Gather(bufs, n);
DPNHANDLE handle = NewHandle();
bool complete = !(flags & SEND_NOCOMPLETE);
{
std::lock_guard<std::mutex> l(m_);
Link* x = state_ == kConnected ? ep_.ById(link_) : nullptr;
if (!x || x->st != Link::kUp) return E_NOCONNECTION;
if (!transport::QueueMsg(ep_, *this, *x, data.data(), data.size(), false, handle, ctx, complete))
return E_CONNECTIONLOST;
}
if (h) *h = handle;
return complete ? S_PENDING : S_OK;
}
HRESULT __stdcall GetSendQueueInfo(DWORD* msgs, DWORD* bytes, DWORD) override {
std::lock_guard<std::mutex> l(m_);
Link* x = state_ == kConnected ? ep_.ById(link_) : nullptr;
if (!x) return E_NOCONNECTION;
DWORD b = 0;
for (auto& m : x->msgs) b += m.bytes;
if (msgs) *msgs = static_cast<DWORD>(x->msgs.size());
if (bytes) *bytes = b;
return S_OK;
}
HRESULT __stdcall GetApplicationDesc(APPLICATION_DESC* buf, DWORD* size, DWORD) override {
if (!size) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
if (state_ != kConnected) return E_NOCONNECTION;
DWORD need = desc_.Size();
if (!buf || *size < need) {
*size = need;
return E_BUFFERTOOSMALL;
}
desc_.Fill(buf);
*size = need;
return S_OK;
}
HRESULT __stdcall SetClientInfo(const PLAYER_INFO* info, void*, DPNHANDLE* h, DWORD) override {
if (!info) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
if (info->dwInfoFlags & INFO_NAME) name_ = info->pwszName ? info->pwszName : L"";
if (h) *h = 0;
return S_OK;
}
HRESULT __stdcall GetServerInfo(PLAYER_INFO* info, DWORD* size, DWORD) override {
if (!size) return E_POINTER;
std::wstring name;
{
std::lock_guard<std::mutex> l(m_);
if (state_ != kConnected) return E_NOCONNECTION;
name = serverName_;
}
return PlayerInfo(name, PLAYER_HOST, info, size);
}
HRESULT __stdcall GetServerAddress(IDirectPlay8Address** out, DWORD) override {
if (!out) return E_POINTER;
sockaddr_in a;
{
std::lock_guard<std::mutex> l(m_);
Link* x = state_ == kConnected ? ep_.ById(link_) : nullptr;
if (!x) return E_NOCONNECTION;
a = x->addr;
}
auto* addr = new Address;
addr->SetHost(a);
*out = addr;
return S_OK;
}
HRESULT __stdcall Close(DWORD) override {
Hold hold(this);
{
std::lock_guard<std::mutex> l(m_);
if (state_ == kClosed) return S_OK;
for (auto& e : enums_) {
*e.live = false;
DPNHANDLE h = e.handle;
void* ctx = e.ctx;
ev_.push_back([this, h, ctx] {
MSG_ASYNC_OP_COMPLETE_T ac{sizeof(ac), h, ctx, E_USERCANCEL};
Fire(MSG_ASYNC_OP_COMPLETE, &ac);
});
}
enums_.clear();
if (state_ == kConnecting) FailConnect(E_USERCANCEL, Bytes());
state_ = kClosed;
if (Link* x = ep_.ById(link_)) transport::Close(ep_, *this, *x); // END_STREAM, as dpnet (W1)
}
Flush();
WaitLinksDown(3000);
{
std::lock_guard<std::mutex> l(m_);
HardDisconnectRemaining();
}
StopWorker();
Flush();
{
std::lock_guard<std::mutex> l(m_);
ep_.links.clear();
}
FreeBuffers();
return S_OK;
}
HRESULT __stdcall ReturnBuffer(DPNHANDLE h, DWORD) override { return ReleaseBuffer(h); }
HRESULT __stdcall GetCaps(CAPS* c, DWORD) override {
if (!c) return E_POINTER;
*c = caps_;
return S_OK;
}
HRESULT __stdcall SetCaps(const CAPS* c, DWORD) override {
if (!c) return E_POINTER;
caps_ = *c;
return S_OK;
}
HRESULT __stdcall SetSPCaps(const GUID*, const SP_CAPS*, DWORD) override { return S_OK; }
HRESULT __stdcall GetSPCaps(const GUID* sp, SP_CAPS* c, DWORD) override { return SpCaps(sp, c); }
HRESULT __stdcall GetConnectionInfo(CONNECTION_INFO* info, DWORD) override {
if (!info) return E_POINTER;
std::lock_guard<std::mutex> l(m_);
Link* x = state_ == kConnected ? ep_.ById(link_) : nullptr;
if (!x) return E_NOCONNECTION;
DWORD size = info->dwSize;
std::memset(info, 0, sizeof(*info));
info->dwSize = size ? size : sizeof(*info);
info->dwRoundTripLatencyMS = x->rtt;
return S_OK;
}
HRESULT __stdcall RegisterLobby(DPNHANDLE, void*, DWORD) override { return E_NOTIMPL; }
private:
enum State { kIdle, kConnecting, kConnected, kClosed };
struct Enum {
sockaddr_in to{};
DPNHANDLE handle = 0;
void* ctx = nullptr;
std::shared_ptr<std::atomic<bool>> live = std::make_shared<std::atomic<bool>>(true);
GUID app{};
Bytes data;
DWORD left = 0, interval = 0, timeout = 0, next = 0;
bool waiting = false; // all queries sent: completes after the timeout
std::map<uint16_t, DWORD> sent; // EnumPayload -> send time
};
bool EnsureSocket() {
std::lock_guard<std::mutex> l(m_);
// An ephemeral port. dpnet's clients take one from 2302.. as well (W1), but a joiner holding 2302 on every
// adapter pushes a later host on the same PC onto different ports per adapter, and the game then advertises
// a port that is wrong for the LAN address (run G2).
if (!OpenSocket(0, 0)) return false;
return StartWorker();
}
void FailConnect(HRESULT hr, const Bytes& reply) { // lock held
DPNHANDLE h = connectHandle_;
void* c = connectCtx_;
state_ = kIdle;
ds2::log().Write("dp8 shim: connect failed (0x%08lX)", static_cast<unsigned long>(hr));
ev_.push_back([this, h, c, hr, reply] {
MSG_CONNECT_COMPLETE_T cc{sizeof(cc), h, c, hr, reply.empty() ? nullptr : const_cast<BYTE*>(reply.data()),
static_cast<DWORD>(reply.size()), 0};
Fire(MSG_CONNECT_COMPLETE, &cc);
});
}
// -- transport callbacks (lock held) --
Upper* TransportUpper() override { return this; }
bool Listening() override { return false; }
void LinkUp(Link& l) override {
if (l.id != link_ || state_ != kConnecting) return;
Bytes c1 = BuildConnectInfo(name_, hasPassword_ ? &password_ : nullptr, connectData_, connectInst_, connectApp_);
transport::QueueMsg(ep_, *this, l, c1.data(), c1.size(), true, 0, nullptr, false);
}
void LinkSendDone(Link&, const OutMsg& m, HRESULT hr) override { QueueSendComplete(m, hr); }
void LinkDown(Link& l) override {
if (l.id != link_) return;
link_ = 0;
if (state_ == kConnecting) {
FailConnect(l.why == S_OK ? E_HOSTREJECTEDCONNECTION : l.why == E_CONNECTIONLOST ? static_cast<HRESULT>(0x80158510)
: l.why,
Bytes());
} else if (state_ == kConnected) {
state_ = kIdle;
if (!terminated_) { // the host closed or vanished: dpnet reports CONNECTIONLOST either way (T5, W2)
ds2::log().Write("dp8 shim: session ended (connection lost)");
ev_.push_back([this] {
MSG_TERMINATE_SESSION_T t{sizeof(t), E_CONNECTIONLOST, nullptr, 0};
Fire(MSG_TERMINATE_SESSION, &t);
});
}
}
}
void LinkMessage(Link& l, Bytes&& m, bool core) override {
if (l.id != link_) return;
if (!core) {
if (state_ != kConnected) return;
uint32_t lid = l.id;
ev_.push_back([this, lid, msg = std::move(m)] { Deliver(0, nullptr, msg, lid, this); }); // dpnidSender 0, as dpnet (T4)
return;
}
if (m.size() < 4) return;
uint32_t type = Get32(m.data());
Span s{m.data() + 4, m.size() - 4};
if (type == CORE_SEND_CONNECT_INFO && state_ == kConnecting && m.size() >= 112) {
const BYTE* p = m.data();
Desc d;
Bytes reply;
if (!d.ParseWire(s, p + 12, m.size() - 12) || !s.Blob(Get32(p + 4), Get32(p + 8), &reply)) return;
DPNID local = Get32(p + 92);
DWORD entries = Get32(p + 104);
std::wstring server;
for (DWORD i = 0; i < entries && 112 + (i + 1) * 48 <= m.size(); ++i) {
const BYTE* e = p + 112 + i * 48;
if (Get32(e + 8) & kEntryServer) s.Str(Get32(e + 24), Get32(e + 28), &server);
}
desc_ = d;
serverName_ = server;
local_ = local;
state_ = kConnected;
Bytes c3;
Put32(c3, CORE_ACK_CONNECT_INFO);
transport::QueueMsg(ep_, *this, l, c3.data(), c3.size(), true, 0, nullptr, false);
DPNHANDLE h = connectHandle_;
void* c = connectCtx_;
ds2::log().Write("dp8 shim: connected to \"%ls\" as %08lX", d.name.c_str(), local);
ev_.push_back([this, h, c, reply, local] {
MSG_CONNECT_COMPLETE_T cc{sizeof(cc), h, c, S_OK, reply.empty() ? nullptr : const_cast<BYTE*>(reply.data()),
static_cast<DWORD>(reply.size()), local};
Fire(MSG_CONNECT_COMPLETE, &cc);
});
} else if (type == CORE_CONNECT_FAILED && state_ == kConnecting && m.size() >= 16) {
Bytes reply;
s.Blob(Get32(m.data() + 8), Get32(m.data() + 12), &reply);
FailConnect(static_cast<HRESULT>(Get32(m.data() + 4)), reply);
transport::Close(ep_, *this, l);
} else if (type == CORE_UPDATE_APPLICATION_DESC && state_ == kConnected) {
Desc d;
if (!d.ParseWire(s, m.data() + 4, m.size() - 4)) return;
desc_ = d;
ev_.push_back([this] { Fire(MSG_APPLICATION_DESC, nullptr); });
} else if (type == CORE_TERMINATE_SESSION && state_ == kConnected && m.size() >= 12) {
Bytes data;
s.Blob(Get32(m.data() + 4), Get32(m.data() + 8), &data);
terminated_ = true;
ds2::log().Write("dp8 shim: the host ended our session (%u bytes of data)", static_cast<unsigned>(data.size()));
ev_.push_back([this, data] {
MSG_TERMINATE_SESSION_T t{sizeof(t), E_HOSTTERMINATEDSESSION, data.empty() ? nullptr : const_cast<BYTE*>(data.data()),
static_cast<DWORD>(data.size())};
Fire(MSG_TERMINATE_SESSION, &t);
});
}
}
void OnDatagramLocked(const sockaddr_in& from, const BYTE* p, size_t n) override {
if (p[0] != 0) {
transport::OnDatagram(ep_, *this, from, p, n);
return;
}
if (n < 4 + 88 || p[1] != 3) return;
uint16_t payload = Get16(p + 2);
for (auto& e : enums_) {
auto it = e.sent.find(payload);
if (it == e.sent.end()) continue;
Span s{p + 4, n - 4};
Desc d;
Bytes reply;
if (!d.ParseWire(s, p + 12, n - 12) || !s.Blob(Get32(p + 4), Get32(p + 8), &reply)) return;
DWORD rtt = GetTickCount() - it->second;
void* ctx = e.ctx;
auto live = e.live;
u_short port = ep_.port;
ev_.push_back([this, from, d, reply, ctx, live, rtt, port]() {
if (!*live) return; // includes a detached response which has not started dispatch yet
Bytes descBuf(d.Size());
auto* ad = reinterpret_cast<APPLICATION_DESC*>(descBuf.data());
d.Fill(ad);
auto* sender = new Address;
sender->SetHost(from);
auto* device = new Address;
device->SetSP(&SP_TCPIP);
device->SetDword(L"port", port);
MSG_ENUM_HOSTS_RESPONSE_T m{sizeof(m), sender, device, ad, reply.empty() ? nullptr : const_cast<BYTE*>(reply.data()),
static_cast<DWORD>(reply.size()), ctx, rtt};
Fire(MSG_ENUM_HOSTS_RESPONSE, &m);
sender->Release();
device->Release();
});
return;
}
}
void TickLocked(DWORD now) override {
for (auto it = enums_.begin(); it != enums_.end();) {
if (!it->waiting && static_cast<LONG>(now - it->next) >= 0) {
uint16_t payload = nextPayload_++;
it->sent[payload] = now;
ep_.Send(it->to, BuildEnumQuery(payload, it->app, it->data));
it->next = now + it->interval;
if (it->left != INFINITE && --it->left == 0) {
it->waiting = true;
it->next = now + (it->timeout == INFINITE ? 0x7FFFFFFF : it->timeout);
}
} else if (it->waiting && static_cast<LONG>(now - it->next) >= 0) {
DPNHANDLE eh = it->handle;
void* ectx = it->ctx;
ev_.push_back([this, eh, ectx] {
MSG_ASYNC_OP_COMPLETE_T ac{sizeof(ac), eh, ectx, S_OK};
Fire(MSG_ASYNC_OP_COMPLETE, &ac);
});
it = enums_.erase(it);
continue;
}
++it;
}
for (auto& kv : ep_.links) transport::Tick(ep_, *this, *kv.second, now);
if (state_ == kConnecting && Elapsed(now, connectStart_, 60000)) { // linked but no SEND_CONNECT_INFO
FailConnect(E_TIMEDOUT, Bytes());
if (Link* x = ep_.ById(link_)) transport::Close(ep_, *this, *x);
}
}
State state_ = kIdle;
uint32_t link_ = 0;
std::vector<Enum> enums_;
uint16_t nextPayload_ = static_cast<uint16_t>(Rand32());
Desc desc_;
DPNID local_ = 0;
std::wstring serverName_, password_;
bool hasPassword_ = false, terminated_ = false;
Bytes connectData_;
GUID connectApp_{}, connectInst_{};
DPNHANDLE connectHandle_ = 0;
void* connectCtx_ = nullptr;
DWORD connectStart_ = 0;
};
// ---- creation --------------------------------------------------------------------------------------------------
void ReadTestHooks() {
char v[16];
if (GetEnvironmentVariableA("DS2DP8_LOSS", v, sizeof(v))) g_lossPct = atoi(v);
if (GetEnvironmentVariableA("DS2DP8_DELAY", v, sizeof(v))) g_delayMs = static_cast<DWORD>(atoi(v));
if (GetEnvironmentVariableA("DS2DP8_JITTER", v, sizeof(v))) g_jitterMs = static_cast<DWORD>(atoi(v));
char path[MAX_PATH];
if (GetEnvironmentVariableA("DS2DP8_WIRELOG", path, sizeof(path)) && (g_wireLog = _fsopen(path, "w", _SH_DENYNO))) {
g_wireT0 = GetTickCount();
SYSTEMTIME t;
GetLocalTime(&t);
fprintf(g_wireLog, " 0 0 dp8 trace start %04d-%02d-%02d %02d:%02d:%02d.%03d pid %lu (shim)\n", t.wYear, t.wMonth,
t.wDay, t.wHour, t.wMinute, t.wSecond, t.wMilliseconds, GetCurrentProcessId());
}
if (g_lossPct || g_delayMs || g_jitterMs)
ds2::log().Write("dp8 shim: TEST loss %d%%, delay %lu ms, jitter %lu ms", g_lossPct, g_delayMs, g_jitterMs);
}
// DirectPlay8ThreadPool: the game creates one beside every Client/Server and treats a failed creation as fatal, but only
// asks it for the thread count and gives it time (dp8.h); our workers need neither, so it answers like a pool that was
// never initialized (which is what the game leaves it as: it never calls Initialize).
class ThreadPool final : public IDirectPlay8ThreadPool {
public:
HRESULT __stdcall QueryInterface(REFIID iid, void** out) override {
if (!out) return E_POINTER;
if (iid == IID_IUnknown || iid == IID_ThreadPool) {
*out = this;
AddRef();
return S_OK;
}
*out = nullptr;
return E_NOINTERFACE;
}
ULONG __stdcall AddRef() override { return InterlockedIncrement(&ref_); }
ULONG __stdcall Release() override {
LONG r = InterlockedDecrement(&ref_);
if (!r) delete this;
return r;
}
HRESULT __stdcall Initialize(void*, PFNDPNMESSAGEHANDLER, DWORD) override { return S_OK; }
HRESULT __stdcall Close(DWORD) override { return S_OK; }
HRESULT __stdcall GetThreadCount(DWORD, DWORD* count, DWORD) override {
if (!count) return E_POINTER;
return E_UNINITIALIZED;
}
HRESULT __stdcall SetThreadCount(DWORD, DWORD, DWORD) override { return E_UNINITIALIZED; }
HRESULT __stdcall DoWork(DWORD, DWORD) override { return E_UNINITIALIZED; }
private:
LONG ref_ = 1;
};
HRESULT CreateShimObject(const GUID& clsid, const GUID& iid, void** out) {
if (!out) return E_POINTER;
*out = nullptr;
static bool started = false;
if (!started) {
started = true;
WSADATA wd;
WSAStartup(MAKEWORD(2, 2), &wd);
ReadTestHooks();
}
IUnknown* obj = clsid == CLSID_Client ? static_cast<IUnknown*>(static_cast<IDirectPlay8Client*>(new Client))
: clsid == CLSID_Server ? static_cast<IUnknown*>(static_cast<IDirectPlay8Server*>(new Server))
: clsid == CLSID_Address ? static_cast<IUnknown*>(new Address)
: clsid == CLSID_ThreadPool ? static_cast<IUnknown*>(static_cast<IDirectPlay8ThreadPool*>(new ThreadPool))
: nullptr;
if (!obj) return REGDB_E_CLASSNOTREG;
HRESULT hr = obj->QueryInterface(iid, out);
obj->Release();
return hr;
}
// Wine/Proton: Wine's own dpnet registers the classes but cannot host (Server::Host is E_NOTIMPL), so under Wine
// `auto` uses the shim unless a native dpnet.dll (winetricks directplay) is what would load.
bool WineBuiltinDpnet() {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
if (!ntdll || !GetProcAddress(ntdll, "wine_get_version")) return false;
char path[MAX_PATH];
if (!GetSystemDirectoryA(path, MAX_PATH)) return true;
strcat_s(path, "\\dpnet.dll");
HANDLE f = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (f == INVALID_HANDLE_VALUE) return true;
char head[512] = {};
DWORD got = 0;
ReadFile(f, head, sizeof(head), &got, nullptr);
CloseHandle(f);
for (DWORD i = 0; i + 16 <= got; ++i)
if (std::memcmp(head + i, "Wine builtin DLL", 16) == 0) return true;
return false;
}
// Windows with the DirectPlay feature OFF still registers the DirectPlay 8 classes, but dpnet.dll is a 10 KB
// "Windows Components on Demand" stub (FileDescription "DirectPlay Stub", only DllMain exported). Creating a class
// loads it, and Windows answers with a modal "An app on your PC needs the following Windows feature: DirectPlay"
// dialog while CoCreateInstance waits (Windows 11 build 26100, on a test PC: dp8-shim.md section 6.1). `auto` must not
// make that call at all, so it reads the registered server file and looks for the export a real dpnet has.
enum class Exports { Unknown, Has, Lacks };
Exports PeExports(const BYTE* d, size_t n, const char* name) {
auto u32 = [&](size_t off, uint32_t& v) { if (off > n || n - off < 4) return false; std::memcpy(&v, d + off, 4); return true; };
IMAGE_DOS_HEADER dos;
if (n < sizeof(dos)) return Exports::Unknown;
std::memcpy(&dos, d, sizeof(dos));
if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0) return Exports::Unknown;
size_t nt = static_cast<size_t>(dos.e_lfanew), opt = nt + 4 + sizeof(IMAGE_FILE_HEADER);
IMAGE_FILE_HEADER fh;
if (opt > n || std::memcmp(d + nt, "PE\0\0", 4) != 0) return Exports::Unknown;
std::memcpy(&fh, d + nt + 4, sizeof(fh));
WORD magic = 0;
if (opt + 2 > n) return Exports::Unknown;
std::memcpy(&magic, d + opt, 2);
size_t dir = magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC ? opt + 96 : magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC ? opt + 112 : 0;
uint32_t expRva = 0, expSize = 0;
if (!dir || dir + 8 > opt + fh.SizeOfOptionalHeader || !u32(dir, expRva) || !u32(dir + 4, expSize)) return Exports::Unknown;
if (!expRva) return Exports::Lacks;
size_t sections = opt + fh.SizeOfOptionalHeader;
auto toFile = [&](uint32_t rva, size_t& off) {
for (unsigned i = 0; i < fh.NumberOfSections; ++i) {
IMAGE_SECTION_HEADER sh;
size_t at = sections + i * sizeof(sh);
if (at > n || n - at < sizeof(sh)) return false;
std::memcpy(&sh, d + at, sizeof(sh));
uint32_t span = sh.Misc.VirtualSize > sh.SizeOfRawData ? sh.Misc.VirtualSize : sh.SizeOfRawData;
if (rva >= sh.VirtualAddress && rva - sh.VirtualAddress < span) {
off = static_cast<size_t>(rva - sh.VirtualAddress) + sh.PointerToRawData;
return off < n;
}
}
return false;
};
size_t at = 0;
IMAGE_EXPORT_DIRECTORY ed;
if (!toFile(expRva, at) || n - at < sizeof(ed)) return Exports::Unknown;
std::memcpy(&ed, d + at, sizeof(ed));
size_t names = 0;
if (!ed.NumberOfNames) return Exports::Lacks;
if (!toFile(ed.AddressOfNames, names)) return Exports::Unknown;
size_t len = std::strlen(name);
for (uint32_t i = 0; i < ed.NumberOfNames && i < 4096; ++i) {
uint32_t rva = 0;
size_t s = 0;
if (!u32(names + 4 * static_cast<size_t>(i), rva) || !toFile(rva, s)) return Exports::Unknown;
if (n - s > len && std::memcmp(d + s, name, len + 1) == 0) return Exports::Has;
}
return Exports::Lacks;
}
// True when the file the DirectPlay 8 Address class is registered to load is the on-demand stub. Anything unreadable
// answers false: the real call then fails fast (class or file missing) or works, as before.
bool WindowsDpnetIsStub() {
char path[MAX_PATH] = {};
DWORD size = sizeof(path) - 1;
// {934A9523-A3CA-4BC5-ADA0-D6D95D979421} = CLSID_DirectPlay8Address; a 32-bit process reads the WOW64 view.
if (RegGetValueA(HKEY_CLASSES_ROOT, "CLSID\\{934A9523-A3CA-4BC5-ADA0-D6D95D979421}\\InprocServer32", nullptr, RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ,
nullptr, path, &size) != ERROR_SUCCESS || !path[0]) return false;
char full[MAX_PATH] = {};
if (std::strchr(path, '\\') || std::strchr(path, '/')) {
strcpy_s(full, path);
} else {
if (!GetSystemDirectoryA(full, MAX_PATH)) return false;
strcat_s(full, "\\");
strcat_s(full, path);
}
HANDLE f = CreateFileA(full, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
if (f == INVALID_HANDLE_VALUE) return false;
LARGE_INTEGER len{};
Bytes data;
if (GetFileSizeEx(f, &len) && len.QuadPart > 0 && len.QuadPart <= (8 << 20)) {
data.resize(static_cast<size_t>(len.QuadPart));
DWORD got = 0;
if (!ReadFile(f, data.data(), static_cast<DWORD>(data.size()), &got, nullptr) || got != data.size()) data.clear();
}
CloseHandle(f);
return !data.empty() && PeExports(data.data(), data.size(), "DllGetClassObject") == Exports::Lacks;
}
// Any class dpnet.dll serves (the game also creates DirectPlay classes besides the three we replace, e.g. while
// hosting) must not reach the stub either.
bool ClsidServedByDpnet(const GUID& clsid) {
wchar_t g[64] = {}, key[160] = {}, path[MAX_PATH] = {};
if (!StringFromGUID2(clsid, g, 64)) return false;
swprintf_s(key, L"CLSID\\%s\\InprocServer32", g);
DWORD size = sizeof(path) - sizeof(wchar_t);
if (RegGetValueW(HKEY_CLASSES_ROOT, key, nullptr, RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ, nullptr, path, &size) != ERROR_SUCCESS) return false;
const wchar_t* base = wcsrchr(path, L'\\');
return _wcsicmp(base ? base + 1 : path, L"dpnet.dll") == 0;
}
using CoCreateInstance_t = HRESULT(__stdcall*)(const GUID&, void*, DWORD, const GUID&, void**);
CoCreateInstance_t g_CoCreateInstance;
bool g_wineBuiltin = false;
bool g_dpnetStub = false;
HRESULT __stdcall ShimCoCreateInstance(const GUID& clsid, void* outer, DWORD ctx, const GUID& iid, void** out) {
bool dp8 = clsid == CLSID_Client || clsid == CLSID_Server || clsid == CLSID_Address || clsid == CLSID_ThreadPool;
if (!dp8) {
if (g_dpnetStub && ClsidServedByDpnet(clsid)) {
ds2::log().Write("dp8 shim: refused %08lX-%04X-%04X (a DirectPlay class we do not replace; dpnet.dll is Windows' stub)",
clsid.Data1, clsid.Data2, clsid.Data3);
if (out) *out = nullptr;
return REGDB_E_CLASSNOTREG;
}
return g_CoCreateInstance(clsid, outer, ctx, iid, out);
}
if (g_mode == kAuto && !g_wineBuiltin && !g_dpnetStub) {
HRESULT hr = g_CoCreateInstance(clsid, outer, ctx, iid, out);
if (SUCCEEDED(hr)) return hr;
static bool told = false;
if (!told) {
told = true;
ds2::log().Write("dp8 shim: Windows DirectPlay unavailable (0x%08lX) - using the shim", static_cast<unsigned long>(hr));
}
}
if (outer) return CLASS_E_NOAGGREGATION;
return CreateShimObject(clsid, iid, out);
}
} // namespace
namespace mp {
HRESULT CreateDirectPlayShim(const GUID& clsid, const GUID& iid, void** out) { return CreateShimObject(clsid, iid, out); }
void InstallDirectPlayShim(const ds2::Ini& ini) {
char mode[16];
ini.String("Network", "DirectPlay", "auto", mode, sizeof(mode));
g_mode = _stricmp(mode, "shim") == 0 ? kShim : _stricmp(mode, "auto") == 0 ? kAuto : kNative;
if (mp::TestLoopback()) g_mode = kShim; // Native DirectPlay opens wildcard sockets outside our control.
if (g_mode == kNative) return;
g_wineBuiltin = g_mode == kAuto && WineBuiltinDpnet();
g_dpnetStub = g_mode == kAuto && !g_wineBuiltin && WindowsDpnetIsStub();
g_CoCreateInstance = static_cast<CoCreateInstance_t>(ds2::HookImport(
"ole32.dll", "CoCreateInstance", reinterpret_cast<void*>(ShimCoCreateInstance), "CoCreateInstance (DirectPlay shim)"));
ds2::log().Write("dp8 shim: mode %s%s", mode,
g_wineBuiltin ? " (Wine's built-in dpnet found: using the shim)"
: g_dpnetStub ? " (the Windows DirectPlay feature is off, dpnet.dll is Windows' stub: using the shim without calling it)"
: "");
}
} // namespace mp