2991 lines
128 KiB
C++
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
|